函数


上次更新时间:8/28/2020, 5:08:00 PM 0
  • 函数是执行特定任务的代码块
  • 函数也是一个对象, 函数名就是个指向函数对象的指针
  • 当一个函数是一个对象的属性时,可称之为方法

# 函数定义

# 函数声明

函数声明由一系列function关键字组成。

function square(number) {
  return number * number;
}

// 匿名函数
function (number) {
  return number * number;
}
1
2
3
4
5
6
7
8

# 函数表达式

var square = function (number) {
  return number * number;
}
var x = square(4); // 16
1
2
3
4

# 使用Function构造函数 (ES6, 不推荐)

var square = new Function ('number', 'return number * number;')
var x = square(4); // 16
1
2

# 函数提升

在日常开发中,我们经常会混用函数声明和函数表达式来定义函数。实际上,解析器在向执行环境中加载数据时,对函数声明和函数表达式并非一视同仁。解析器会率先读取函数声明,并使其在执行 任何代码之前可用(可以访问);至于函数表达式,则必须等到解析器执行到它所在的代码行,才会真正被解释执行。

sum(10,10) // 20
function sum(num1, num2){ 
  return num1 + num2; 
}

square(10) // square is not a function
var square = function (number) {
  return number * number;
}

1
2
3
4
5
6
7
8
9
10

从上面的例子可以看出,函数声明可以在声明前被调用,函数表达式不可以,这是因为函数声明被提升了。

函数提升有关请看:函数提升

# 函数属性和方法

# 特殊对象

在函数内部,有两个特殊的对象:argumentsthis

  • arguments(实参对象): 类数组对象,包含着传入函数中的所有参数,可以通过数字下标获得传入实参值。
  • this: 函数据以执行的环境对象,可以被改变。
sum(1, 2, 'c', 'd')
function sum(num1, num2){
  console.log(num1, num2) // 1 2
  console.log(arguments[0]) // 1
  console.log(arguments[1]) // 2
  console.log(arguments[2]) // c
  console.log(arguments[3]) // d
}
1
2
3
4
5
6
7
8

# 属性

每个函数都包含两个属性:length 和 prototype。

  • length:函数希望接收的命名参数的个数
  • prototype:函数也是对象,所以它也会有prototype,也会有toString()、valueOf()等方法
  • name:返回函数名(ES6)
function sayName(name){ 
 console.log(name); 
} 
function sum(num1, num2){ 
 return num1 + num2; 
} 
function sayHi(){ 
 console.log("hi"); 
} 
console.log(sayName.length); // 1 
console.log(sum.length); // 2 
console.log(sayHi.length); // 0

console.log(sayName.toString())
console.log(sayName.valueOf())

console.log(sayName.name) // sayName
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17

# 方法

每个函数都有以下几个方法:

  • call(this, param1, param2, ...)
  • apply(this, arguments)
  • bind(this, param1, param2, ...)

区别:

  • call()、apply()功能一样,只是参数接收方式不同。《call() 和 apply() 的 区别》
  • call() 改变 this 指向的时候已经执行,bind() 会生成新一个函数,在调用这个新函数的时候才改变新函数的this
function foo(){
	return this;
}
var f1 = foo.call({a:1});		
var f2 = foo.apply({a:2});    
var f3 = foo.bind({a:1});

console.log(f1);	// {a:1}
console.log(f2);	// {a:2}
console.log(f3);	// function foo(){
						      //	return this;
						      // }

console.log(foo());		//window对象
console.log(f1());    // f1 is not a function
console.log(f3());		//{a: 1}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16

apply()和 call() 是非继承的方法,即直接从构造函数中获得。bind()是来自原型链上的方法,即是Function构造函数的原型对象上的一个方法。(这个概念不是很清楚,因为三个方法都能从Function.prototype上获得)

Function.prototype.call()
Function.prototype.apply()
Function.prototype.bind()
1
2
3

# bind()

bind() 方法创建一个新的函数,在 bind() 被调用时,这个新函数的 this 被指定为 bind() 的第一个参数。 MDN - bind()

应用场景:

  • 创建绑定函数,不论怎么调用,这个函数都有同样的 this 值
  • 使函数拥有预设的初始参数
  • 在 setTimeout 中改变this
  • 创建快捷方式

实现一个 bind()思路:

  1. 参数处理this 和 arguments
  2. 内部 this 对象用变量保存
  3. 内部对象使用 apply 改变 this 指向
  4. 返回一个函数

方法一:不支持使用 new 调用新创建的构造函数

Function.prototype.bind = function(context){
  var args = Array.prototype.slice.call(arguments, 1);
  var self = this;
  return function(){
      var innerArgs = Array.prototype.slice.call(arguments);
      var finalArgs = args.concat(innerArgs);
      return self.apply(context,finalArgs);
  };
};

function foo(){
	return this;
}

var f1 = foo.bind({a:1});
f1(); // {a: 1}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16

方法二:支持使用 new 调用新创建的构造函数

//  Yes, it does work with `new (funcA.bind(thisArg, args))`
if (!Function.prototype.bind) (function(){
  var ArrayPrototypeSlice = Array.prototype.slice;
  Function.prototype.bind = function(otherThis) {
    if (typeof this !== 'function') {
      throw new TypeError('Function.prototype.bind - what is trying to be bound is not callable');
    }

    var baseArgs= ArrayPrototypeSlice.call(arguments, 1),
        baseArgsLength = baseArgs.length,
        fToBind = this,
        fNOP    = function() {},
        fBound  = function() {
          baseArgs.length = baseArgsLength; // reset to default base arguments
          baseArgs.push.apply(baseArgs, arguments);
          return fToBind.apply(
                 fNOP.prototype.isPrototypeOf(this) ? this : otherThis, baseArgs
          );
        };

    if (this.prototype) {
      // Function.prototype doesn't have a prototype property
      // Function.prototype 是没有 prototype 属性的,所以要加上
      fNOP.prototype = this.prototype; 
    }
    fBound.prototype = new fNOP();

    return fBound;
  };
})();
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30

# this

在 JavaScript 中 this 不是固定不变的,它会随着执行环境的改变而改变。在绝大多数情况下,函数的调用方式决定了 this 的值(运行时绑定)。

# this 的指向

  • 在全局执行环境中(在任何函数体外部),this 都指向全局对象
  • 在对象方法中,this 表示该方法所属的对象
  • 在函数中,this的值取决于函数被调用的方式。如果没有设置,非严格模式下则指向全局对象,严格模式下为undefined
  • 在箭头函数中,this 指向定义时所在的对象,而不是使用时所在的对象
  • 在类中,this跟在函数中表现差不多,但在类的构造函数中,this 是一个常规对象,类中所有非静态的方法都会被添加到 this 的原型中
// 在全局执行环境中(在任何函数体外部),this 都指向全局对象
console.log(this === window); // true

// 在对象方法中,this 表示该方法所属的对象
var a = {
  name: 'jack',
  fun: function() {
    console.log(this.name);
  }
}
a.fun(); // jack

// 在箭头函数中,this 指向定义时所在的对象,而不是使用时所在的对象
var b = {
  name: 'jack',
  fun: () => {
    console.log(this.name);
  }
}
b.fun(); // undefined

// 在函数中,this的值取决于函数被调用的方式
var b = function() {
  return this;
}
b() === window; // true

var c = function() {
  "use strict";
  return this;
}
c() === undefined; // true


// 在类中的表现
class Example {
  constructor() {
    const proto = Object.getPrototypeOf(this);
    console.log(Object.getOwnPropertyNames(proto));
  }
  first(){}
  second(){}
  static third(){}
}
new Example(); // ['constructor', 'first', 'second']
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45

《this 指向有关面试题》

# 改变 this 的指向

使用以下方法可以改变 this 指向:

  • call()
  • apply()
  • bind()

具体介绍可看:call() 和 apply() 的 区别bind()

# 函数参数

  • 形参/显式参数(Parameters):函数声明时指定的变量
  • 实参/隐式参数(Arguments):函数调用时传入的参数

# arguments对象

函数内部属性,是一个类数组对象,包含着传入函数中的所有参数,可以通过数字下标获得传入实参值。

sum(1, 2, 'c', 'd')
function sum(num1, num2){
  console.log(num1, num2) // 1 2
  console.log(arguments[0]) // 1
  console.log(arguments[1]) // 2
  console.log(arguments[2]) // c
  console.log(arguments[3]) // d
  console.log(Object.prototype.toString.call(arguments)) // [object Arguments]
}
1
2
3
4
5
6
7
8
9

# ...拓展符号(ES6)

ES6 引入 rest 参数(形式为...变量名),用于获取函数的多余参数,这样就不需要使用arguments对象了。rest 参数搭配的变量是一个数组,该变量将多余的参数放入数组中。

使用注意:

  • rest 参数之后不能再有其他参数,否则报错
  • 函数的length属性,不包括 rest 参数
function add(...values) {
  console.log(Object.prototype.toString.call(values)) // [object Array]

  let sum = 0;

  for (var val of values) {
    sum += val;
  }

  return sum;
}

add(2, 5, 3) // 10
1
2
3
4
5
6
7
8
9
10
11
12
13

# 参数默认值(ES6)

使用注意:

  • 不能有同名参数,否则报错
  • 指定了默认值以后,函数的length属性,将返回没有指定默认值的参数个数,即length将失真
sum() // 100
function sum(num1 = 10, num2 = 10){
  return num1 * num2
}
1
2
3
4

默认值不是单纯传值,而是每次都重新计算默认值表达式的值。也就是说,参数默认值是惰性求值的。

let x = 99;
function foo(p = x + 1) {
  console.log(p);
}

foo() // 100

x = 100;
foo()
1
2
3
4
5
6
7
8
9

# 函数类型

在函数的介绍中,经常能看到匿名函数、自执行函数、回调函数、高阶函数、闭包等等。有时候对这些函数没有比较清晰的概念,下面是一篇函数类型总结。

《javascript函数类型介绍》

# 函数作用域

函数作用域有关请看《作用域》章节

# 闭包

了解闭包之前,需要先了解《作用域》有关知识

# 作用域与闭包

函数的执行依赖于变量作用域,这个作用域是函数定义时决定的,而不是函数调用时决定。函数对象可以通过作用域链相互关联起来,函数体内变量都保存在函数作用域内,这种特性就叫闭包。当函数可以记住并访问所在的词法作用域时,就产生了闭包,即使函数是在当前词法作用域之外执行。

例子1:

function foo() {
  var i = 0;
  function bar() {
    console.log( i++ );
  }
  bar(); 
}
foo(); // 0
foo(); // 0
// 执行n次依然输出 0
1
2
3
4
5
6
7
8
9
10

上面的例子看起来长的很像闭包,但其实它只是基于词法作用域的查找规则,使得函数bar()能够访问外部作用域中的变量i,而每一次foo()执行后都会被销毁,再次调用时 i 被初始化为 0,所以无论执行多少次,输出都是 0。

例子2:

function foo() {
  var i = 0;
  function bar() { 
    console.log( i++ ); 
  }
  return bar; 
}
var baz = foo(); 
baz(); // 0
baz(); // 1
baz(); // 2
// 执行n次,每次加1
1
2
3
4
5
6
7
8
9
10
11
12

从例子2可以看出,即使bar()在自己定义的词法作用域外执行,也可以正常执行,因为作用域是定义时就已经决定了的。而 i 能不断加 1 是因为foo()一直没被销毁回收,所以每次执行bar()时,都是在内部作用域中的 i 进行自增。

一般来说,在 foo() 执行后,通常会期待 foo() 的整个内部作用域都被销毁,因为垃圾回收器会释放不再使用的内存空间。由于看上去 foo() 的内容不会再被使用,所以很自然地会考虑对其进行回收。而闭包的“神奇”之处正是可以阻止这件事情的发生。事实上内部作用域依然存在,因此没有被回收。谁在使用这个内部作用域?就是 bar() 本身在使用。 拜 bar() 所声明的位置所赐,它拥有涵盖 foo() 内部作用域的闭包,使得该作用域能够一直存活,以供 bar() 在之后任何时间进行引用。bar() 依然持有对该作用域的引用,而这个引用就叫作闭包。这也就是为什么 i 能够一直不断加1的原因。

# 循环与闭包

在 javascript 中是没有块级作用域的概念的,这就意味着在块语句中定义的变量,实际上是包含在函数中而非语句中创建。

闭包经典面试题

for (var i=1; i<=3; i++) {
  console.log(i)
  setTimeout( function timer() { 
    console.log( i ); 
  }, 1000 ); 
}
// 依次输出 1 2 3 -> 此时循环已经结束,i = 4
// setTimeout 开始执行,间隔 1s,输出 3 个 4
1
2
3
4
5
6
7
8

显然这个结果不是我们想要的,因为setTimeout的执行在for循环之后,而此时,i已经等于4。

方法一:使用立即执行函数

for (var i=1; i<=3; i++) {
  console.log('outer' + i);
  (function(j){
    console.log('inner' + j) // 内部保存了 i 的值
    setTimeout( function timer() { 
      console.log( j ); // 打印的是内部的变量 j ,而不是外部循环的 i 
    }, 1000 ); 
  })(i);
}
// 依次输出 outer1 inner1 outer2 inner2 outer31 inner3 
// setTimeout 开始执行,间隔 1s, 依次输出 1 2 3
1
2
3
4
5
6
7
8
9
10
11

方法二:使用 let 来声明变量,let能形成块级作用域

for (let i=1; i<=3; i++) {
  console.log(i) 
  setTimeout( function timer() { 
    console.log( i ); // 当前循环体内形成一个块级作用域,i被保存,下次循环不影响当前变量
  }, 1000 ); 
}
// 依次输出 1 2 3
// 依次输出 1 2 3
1
2
3
4
5
6
7
8

方法三:使用bind

for (var i=1; i<=3; i++) {
  function timer(i) { 
    console.log( i );
  }
  setTimeout( timer.bind(this, i), 1000 ); 
}
1
2
3
4
5
6

本质上,如果将函数(访问它们各自的词法作用域)当作第一级的值类型并到处传递,就会看到闭包在这些函数中的应用。如定时器、事件监听器、 Ajax 请求、跨窗口通信、Web Workers 或者任何其他的异步(或者同步)任务中,只要使用了回调函数,实际上就是在使用闭包!

# 私有变量

在同一作用域内定义多个闭包,它们享有同样的私有变量或变量。

function counter() {
  var n = 0;
  return {
    count: function() {
      return n++
    },
    reset: function() {
      n = 0;
    }
  }
}

var c = counter()
var d = counter()

c.count() // 0
d.count() // 0
c.reset() 
c.count() // 0 -> c 已经被reset
d.count() // 1 -> d 没有被影响
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20

从上面例子可以看出,c 和 d 虽然都访问counter的私有变量 n,但互相不影响,这是因为每次调用counter() 都会创建一个新的作用域链和一个新的私有变量。

# 模块模式

闭包也是一个非常强大的工具,可以实现多种代码模式,其中最强大的一个就是模块模式(module pattern)。模块模式是为单例创建私有变量和特权方法。所谓单例(singleton),指的就是只有一个实例的对象。

模块模式需要具备两个必要条件:

  1. 必须有外部的封闭函数,该函数必须至少被调用一次(每次调用都会创建一个新的模块实例)。
  2. 封闭函数必须返回至少一个内部函数,这样内部函数才能在私有作用域中形成闭包,并且可以访问或者修改私有的状态。

常见模块模式实现方法

  • 模块暴露,暴露内部变量或方法(例子1)
  • 在模块实例的内部保留对公共 API 对象的内部引用,可以从内部对模块实例进行修改,包括添加或删除方法和属性,以及修改它们的值。(例子2)

例子1

function CoolModule() {
  var something = "cool";
  var another = [1, 2, 3];

  function doSomething() { 
    console.log( something ); 
  }
  function doAnother() { 
    console.log( another.join( " ! " ) );
  }
  return { 
    doSomething: doSomething, 
    doAnother: doAnother 
  }; 
}
var foo = CoolModule(); 
foo.doSomething(); // cool 
foo.doAnother(); // 1 ! 2 ! 3
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18

例子2

function CoolModule(id) {
  // 修改公共 API 
  function change() {
    publicAPI.identify = identify2; 
  }
  function identify1() { 
    console.log( id ); 
  }
  function identify2() { 
    console.log( id.toUpperCase() ); 
  }
  var publicAPI = { change: change, identify: identify1 };

  return publicAPI; 
}

var foo = CoolModule('foo module')

foo.identify(); // foo module 
foo.change(); 
foo.identify(); // FOO MODULE
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21

# 现代模块机制与未来模块机制

《模块》

# 总结

  • 闭包的作用域链包含着它自己的作用域、包含函数的作用域和全局作用域
  • 当函数返回了一个闭包时,这个函数的作用域将会一直在内存中保存到闭包不存在为止
  • 闭包可以在对象中创建私有变量
  • 闭包可以模仿块级作用域
  • 闭包可以实现模块模式
  • 过度使用闭包会占用大量内存,影响性能。

# 箭头函数(ES6)

# 优点

  • 使表达更加简洁
  • 可简化回调函数
  • 可跟rest参数...结合使用
var f = () => 5;
// 等同于
var f = function () { return 5 };
1
2
3

更多用法请看 箭头函数 - MDN

# 使用注意

  • 函数体内的this对象,就是定义时所在的对象,而不是使用时所在的对象。
  • 不可以当作构造函数,也就是说,不可以使用new命令,否则会抛出一个错误。
  • 不可以使用arguments对象,该对象在函数体内不存在。如果要用,可以用 rest 参数代替。
  • 不可以使用yield命令,因此箭头函数不能用作 Generator 函数。

# 不适用场合

  1. 定义对象的方法,且该方法内部包括this。
const cat = {
  lives: 9,
  jumps: () => {
    this.lives--;
  }
}
1
2
3
4
5
6
  1. 需要动态this的时候,也不应使用箭头函数。
var button = document.getElementById('press');
button.addEventListener('click', () => {
  this.classList.toggle('on');
});
1
2
3
4

上面代码运行时,点击按钮会报错,因为button的监听函数是一个箭头函数,导致里面的this就是全局对象。如果改成普通函数,this就会动态指向被点击的按钮对象。

  1. 函数体很复杂时

如果函数体很复杂,有许多行,或者函数内部有大量的读写操作,不单纯是为了计算值,这时也不应该使用箭头函数,而是要使用普通函数,这样可以提高代码可读性。

上次更新时间: 8/28/2020, 5:08:00 PM