3.函数
3.1函数形参的默认值
在ES5中模拟默认参数
1 | function makeRequest(url, timeout, callback){ |
上者缺陷是传入0也会被默认为2000,所以更安全的选择如下1
2
3
4function makeRequest(url, timeout, callback){
timeout = (typeof timeout !== ‘undefined’) || 2000;
callback = (typeof callback!== ‘undefined’) || function() {};
}
ES6中的默认参数值
1 | 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
3let 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 | function doSomething() { } |
函数声明时name为声明时的函数名称,匿名函数表达式声明时name为被赋值为该匿名函数的变量的名称。
name属性的特殊情况
1 | var doSomething = function doSomethingElse(){ }; |
在这个例子中,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
7function Person(name) {
if (this instanceof Person) {
this.name = name;
} else {
throw new Error(‘必须通过new关键字调用’);
}
}
但是这种方法也不可靠,因为有一种不依赖new关键字的方法也可以将this绑定到Person实例上。1
2var 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
9function 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
12function 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
8let 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
3function doSomething() {
return doSomethingElse(); //尾调用
}
在ES5引擎中,尾调用的实现与其他函数调用的实现类似:创建一个新的栈帧,将其推入调用栈表示函数调用。也就是说,在循环调用是,每一个未用完的栈帧都会保存在内存中,当调用栈变得过大时会造成程序问题。
ES6中的尾调用优化
ES6缩减了严格模式下尾调用栈的大小(非严格模式下不受影响)。如果满足以下条件,尾调用不再创建新的栈帧,而是清除并重用当前栈帧: