深入理解ES6-第3章-函数

3.函数

3.1函数形参的默认值

在ES5中模拟默认参数
1
2
3
4
function makeRequest(url, timeout, callback){
timeout = timeout || 2000;
callback = callback || function() {};
}

  上者缺陷是传入0也会被默认为2000,所以更安全的选择如下

1
2
3
4
function makeRequest(url, timeout, callback){
timeout = (typeof timeout !== ‘undefined’) || 2000;
callback = (typeof callback!== ‘undefined’) || function() {};
}

ES6中的默认参数值
1
2
3
function makeRequest(url, timeout = 2000, callback = function(){}) {

}

  其中url为必需参数,另两个有默认值。在不传入默认参数或默认参数传入undefined时会使用默认值。有默认值的参数和必需参数顺序无要求。

默认参数对arguments对象的影响

  ES5中在非严格情况下,函数命名参数的变化会体现在arguments中,命名参数被赋予新值时,arguments也相应更新。在严格情况下,取消了这个行为,无论参数怎么变化,arguments对象不再随之变化。
  ES6中如果一个函数是用来默认参数值,则arguments行为与ES5严格模式相同。arguments只与传入的参数有关,默认参数不会给他赋值。

默认参数表达式

  可以通过函数执行来得到默认参数表达式。还可以使用先定义的参数作为后定义参数的默认值,先定义的参数不能访问后定义的参数。

默认参数的临时死区

  与let声明类似,定义参数时会为每一个参数创建一个新的标识符绑定,该绑定在初始化之前不可被引用,如果试图访问会导致程序抛出错误。当调用函数时,会通过传入的值或参数的默认值初始化该参数。
  注意:函数参数有自己的作用域和临时死区,其与函数体的作用域时各自独立的,也就是说参数的默认值不可访问函数体内声明的变量。即括号内可看做是单独的作用域。

3.2处理无名参数

ES5中的无名参数

  通过arguments来检查函数的所有参数。

不定参数

  在函数的命名参数前添加三个点(…)就表明这是一个不定参数,该参数为一个数组,包含着自它之后传入的所有参数,通过该数组名可以逐一访问里面的参数。
  注意:函数的length属性统计的是命名参数的数量,不定参数的加入不会影响length的值。

不定参数的使用限制

  每个函数最多只能声明一个不定参数,而且一定要放在所有参数的末尾。
  不定参数不能用于对象字面量setter中,是因为setter的参数有且只能有一个。

不定参数对arguments的影响

  无论是否使用不定参数,arguments对象总是包含所有传入函数的参数。

3.3 增强的Function构造函数

  支持在Function构造函数中传入默认函数以及不定参数。

3.4展开运算符

  展开运算符可以让你指定一个数组,将它们打散后作为各自独立的参数传入函数。ES5及以前需手动遍历或者使用apply()方法。

1
2
3
let values = [25, 50, 75, 100];
console.log(Math.max.apply(Math, values)); // ES5早期版本 100
console.log(Math.max(…values));

  可以将展开运算符与其他正常传入的参数混合使用。

3.5 name属性

  由于JavaScript中有多种定义函数的方式,因此辨别函数就是一项具有挑战性的任务。为此ES6为所有函数新增name属性

如何选择合适的名称
1
2
3
4
function doSomething() {  }
var doAnotherThing = function() { };
console.log(doSomething.name); //”doSomething”
console.log(doAnotherThing.name); //”doAnotherThing”

  函数声明时name为声明时的函数名称,匿名函数表达式声明时name为被赋值为该匿名函数的变量的名称。

name属性的特殊情况
1
2
3
4
5
6
7
8
9
10
11
12
var doSomething = function doSomethingElse(){  };
var person = {
get firstName() {
return ‘aaa’;
},
sayName: function(){
console.log(this.name);
}
}
console.log(doSomething.name); //”doSomethingElse”
console.log(person.sayName.name); //”sayName”
console.log(person.firstName.name); //”get firstName”

  在这个例子中,doSomething.name的值为”doSomethingElse”,是由于函数表达式有一个名字,这个名字比函数本身被赋值的变量的权重高;对象中的函数为对象字面量;与之类似,person.firstName实际上是一个getter函数,所以名称前面有get前缀,同理setter函数有set前缀。
  通过bind()函数创建的函数,其名称将带有bound前缀;通过Function构造的函数,名称带有anonymous前缀。
  切记,函数的name属性的值不一定引用同名变量,它只是协助调试用的额外信息,所以不能使用name来获取对函数的引用。

3.6明确函数的多重用途

  ES5及以前函数具有多重功能,可以结合new使用,函数内的this值将指向一个新对象,函数最终将返回这个新对象。
  JavaScript函数中有两个不同的内部方法:[[Call]]和[[Construct]]。当通过new关键字调用函数时,执行的是[[Construct]]函数,它负责创建一个通常被称作实例的新对象,然后再执行函数体,把this绑定在实例上;如果不通过new关键字调用函数,则执行[[Call]]函数,从而执行代码中的函数体。具有[[Construct]]方法的函数被统称为构造函数。
  不是所有函数都有[[Construct]]方法,因此不是所有函数都可以用new来调用。

在ES5中判断函数被调用的方法

  使用instanceof检查this的值,看它是否为构造函数的实例。由于[[Construct]]方法会创建一个Person的新实例,并将this绑定到新实例上。

1
2
3
4
5
6
7
function Person(name) {
if (this instanceof Person) {
this.name = name;
} else {
throw new Error(‘必须通过new关键字调用’);
}
}

  但是这种方法也不可靠,因为有一种不依赖new关键字的方法也可以将this绑定到Person实例上。

1
2
var person = new Person(‘a’);
var notAPerson = Person.call(person, ‘b’); //有效

  此时将无法区分是通过Person.call()或者apply()还是new关键字调用得到的Person实例。

元属性(Metaproperty)new.target

  元属性指非对象的属性,其可以提供非对象目标的补充信息(如new)。当调用函数的[[Construct]]方法时,new.target被赋值为new操作符的目标,通常是新创建对象实例,也就是函数体内this的构造函数;如果调用[[Call]]方法,则new.target的值为undefined。

1
2
3
4
5
6
7
8
9
function Person(name) {
if (typeof new.target !== ‘undefined’) {
this.name = name;
} else {
throw new Error(‘必须通过new关键字调用’);
}
}
var person = new Person(‘a’);
var notAPerson = Person.call(person, ‘b’); //抛出错误

  也可以检查new.target是否被某个特定构造函数调用:

1
2
3
4
5
6
7
8
9
10
11
12
function Person(name) {
if (typeof new.target === Person) {
this.name = name;
} else {
throw new Error(‘必须通过new关键字调用’);
}
}
function AnotherPerson(name){
Person.call(this, name);
}
var person = new Person(‘a’);
var anotherPerson = new AnotherPerson (‘b’); //抛出错误

  注意:在函数外使用new.target是一个语法错误

3.7块级函数

  ES5的严格模式中引入了一个错误提示,当在代码块内部声明函数时程序会抛出错误。而ES6中会将该函数视作一个块级声明,从而可以在定义该函数的代码块中访问和调用它。该会计函数会被提升到代码块顶部,且代码块外部无法调用它。

块级函数的使用场景

  块级函数与let函数表达式类似,但是let不会提升,而块级函数会被提升。

非严格模式下的块级函数

  这时,这些函数不再提升到代码块的顶部,而是提升至外围函数或全局作用域的顶部。

3.8箭头函数

  箭头函数是一种使用箭头(=>)定义函数的新语法,与传统函数的不同:

  • 没有this、super、arguments和new.target绑定:箭头函数的this、super、arguments和new.target这些值由外围最近一层非箭头函数决定。
  • 不能通过new关键字调用:箭头函数没有[[Construct]]方法,所以不能被用作构造函数,通过new调用会报错。
  • 没有原型:由于不可以通过new调用,因而没有构建原型的需求,所以箭头函数没有prototype这个属性
  • 不可以改变this的绑定:函数内this不可改变,在函数的生命周期内始终保持一致。
  • 不支持arguments对象:没有arguments绑定,只能通过命名参数和不定参数访问参数。
  • 不支持重复的命名参数:无论在严格或非严格情况下,都不支持;而传统函数只有在严格模式下不支持。
      箭头函数也有一个name属性。
    箭头函数语法
      箭头函数语法多变,根据实际使用场景有多种形式。所有变种都由函数参数、箭头、函数体组成。
    a) 当箭头函数只有一个参数时,可以直接写参数名,箭头紧随其后,剪头右侧的表达式被求值后便立即返回即使没有返回语句,箭头函数也可以返回传入的第一个参数。
    b) 如需要传入2个及以上参数,需要在参数两侧加入小括号。
    c) 如果函数没有参数,也要在声明时写一组没有内容的小括号。
    d) 如果希望为函数编写由多个表达式组成的函数体,则用花括号包裹住函数体,并显式定义一个返回值。
    e) 如果想在箭头函数外返回一个对象字面量,则需要将她它包裹在小括号中。为了将其与函数体区分开。
    创建立即执行函数表达式(IIFE)
      IIFE:定义一个匿名函数并立即调用,自始至终不保存对该函数的引用。当你想创建一个与其他程序隔离的作用域时,这种模式非常方便。
      只要将箭头函数包裹在小括号里,并在最后加入函数调用括号,就可以用它实现相同的功能。
    1
    2
    3
    4
    5
    6
    7
    8
    let person = ((name) => {
    return {
    getName: function() {
    return name;
    }
    }
    })(‘aaa’);
    console.log(person.getName()); //”aaa”

  注意:小括号只包裹箭头函数定义,没有包含(‘aaa’),这一点与正常函数体有所不同,由正常函数定义的立即执行函数既可以用小括号包裹函数体,也可以额外包裹函数调用的部分。

箭头函数没有this绑定

  箭头函数中没有this绑定,必须通过查找作用域链来决定其值。如果箭头函数被非箭头函数包含,则this绑定的最近一层非箭头函数的this;否则,this的值会被设置为undefined。
  箭头函数缺少正常函数所拥有的的prototype属性,它的设计初衷是“即用即弃”,所以不能用来定义新的类型。如果尝试用过new调用一个箭头函数,会报错。
  不能通过call()、apply()或bind()方法来改变this的值。

箭头函数与数组

  sort()、map()及reduce(),简化回调函数。

1
var result = values.sort((a, b) => a-b);

箭头函数没有arguments绑定

  但是可以访问外围函数的arguments对象。

箭头函数的辨识方法

  使用typeof和instanceof调用与普通函数无区别。同样,仍然可以在箭头函数上调用call()、apply()和bind()方法,但箭头函数的this不会改变。

3.9 调用优化

  尾调用指的是函数作为另一个函数的最后一条语句被调用,就像这样:

1
2
3
function doSomething() {
return doSomethingElse(); //尾调用
}

  在ES5引擎中,尾调用的实现与其他函数调用的实现类似:创建一个新的栈帧,将其推入调用栈表示函数调用。也就是说,在循环调用是,每一个未用完的栈帧都会保存在内存中,当调用栈变得过大时会造成程序问题。

ES6中的尾调用优化

  ES6缩减了严格模式下尾调用栈的大小(非严格模式下不受影响)。如果满足以下条件,尾调用不再创建新的栈帧,而是清除并重用当前栈帧:

  • 尾调用不访问当前栈帧的变量(也就是说函数不是一个闭包)。
  • 在函数内部,尾调用是最后一条语句。
  • 尾调用的结果作为函数值立即返回。
    如何利用尾调用优化
      实际上,尾调用的优化发生在引擎背后,除非你尝试优化一个函数,否则无须思考此类问题。递归函数是其最主要的应用场景,此时尾调用优化的效果最显著。