在 ECMAScript 6 中,箭头函数是其中最有趣的新增特性。顾名思义,箭头函数是一种使用箭头(=>)定义函数的新语法,但是它与传统的 JavaScript 函数有些许不同,主要集中在以下方面:
- 没有this、super、atguments 和 new.target 绑定 箭头函数中的this、super、arguments及new.target这些值由外围最近一层非箭头函数决定。
- 不能通过new关键字调用 箭头函数没有[[Construct]]方法,所以不能被用作构造函数,如果通过new关键字调用箭头函数,程序会抛出错误。
- 没有原型 由于不可以通过new关键字调用箭头函数,因而没有构建原型的需求,所以箭头函数不存在prototype这个属性。
- 不可以改变this的绑定 函数内部的this值不可被改变,在函数的生命周期内始终保持一致。
- 不支持arguments对象 箭头函数没有arguments绑定,所以你必须通过命名参数和不定参数这两种形式访问函数的参数。
- 不支持重复的命名参数 无论在严格还是非严格模式下,箭头函数都不支持重复的命名参数;而在传统函数的规定中,只有在严格模式下才不能有重复的命名参数。
这些差异的产生有如下几个原因:首先,也是最重要的,this绑定是JavaScript程序中一个常见的错误来源,在函数内很容易就对this的值失去控制,其经常导致程序出现意想不到的行为,箭头函数消除了这方面的烦恼;其次,如果限制箭头函数的this值,简化代码执行的过程,则JavaScript引擎可以更轻松地优化这些操作,而常规函数往往同时会作为构造函数使用或者以其他方式对其进行修改。
在箭头函数内,其余的差异主要是减少错误以及理清模糊不清的地方。这样一来,JavaScript引擎就可以更好地优化箭头函数的执行过程。
箭头函数同样也有一个name属性,这与其他函数的规则相同。
箭头函数语法
箭头函数的语法多变,根据实际的使用场景有多种形式。所有变种都由函数参数、箭头、函数体组成,根据使用的需求,参数和函数体可以分别采取多种不同的形式。举个例子,在下面这段代码中,箭头函数采用了单一参数,并且只是简单地返回了参数的值:
let reflect = value => value;
// 实际上相当于
let reflect = function(value) {
return value;
};
当箭头函数只有一个参数时,可以直接写参数名,箭头紧随其后,箭头右侧的表达式被求值后便立即返回。即使没有显式的返回语句,这个箭头函数也可以返回传入的第一个参数,不需要更多的语法铺垫。
如果要传入两个或两个以上的参数,要在参数的两侧添加一对小括号,就像这样:
let sum = (num1, num2) => num1 + num2;
// 实际上相当于
let sum = function(num1, num2) {
return num1 + num2;
};
这里的sum()函数接受两个参数,将它们简单相加后返回最终结果,它与reflect()函数唯一的不同是,它的参数被包裹在小括号中,并且用逗号进行分隔(类似传统函数)。
如果函数没有参数,也要在声明的时候写一组没有内容的小括号,就像这样:
let getName = () => "Nicholas";
// 实际上相当于
let getName = function() {
return "Nicholas";
};
如果你希望为函数编写由多个表达式组成的更传统的函数体,那么需要用花括号包裹函数体,并显式地定义一个返回值,就像这个版本的sum()函数一样:
let sum = (num1, num2) => {
return num1 + num2;
};
// 实际上相当于
let sum = function(num1, num2) {
return num1 + num2;
};
除了arguments对象不可用以外,某种程度上你都可以将花括号里的代码视作传统的函数体定义。
如果想创建一个空函数,需要写一对没有内容的花括号,就像这样:
let doNothing = () => {};
// 实际上相当于
let doNothing = function() {};
花括号代表函数体的部分,到目前为止一切都运行良好。但是如果想在箭头函数外返回一个对象字面量,则需要将该字面量包裹在小括号里。举个例子:
let getTempItem = id => ({ id: id, name: "Temp" });
// 实际上相当于
let getTempItem = function(id) {
return {
id: id,
name: "Temp"
};
};
将对象字面量包裹在小括号中是为了将其与函数体区分开来。
创建立即执行函数表达式
JavaScript函数的一个流行的使用方式是创建立即执行函数表达式(IIFE),你可以定义一个匿名函数并立即调用,自始至终不保存对该函数的引用。当你想创建一个与其他程序隔离的作用域时,这种模式非常方便。举个例子:
let person = function(name) {
return {
getName: function() {
return name;
}
};
}("Nicholas");
console.log(person.getName()); // "Nicholas"
在这段代码中,立即执行函数表达式通过getName()方法创建了一个新对象,将参数name作为该对象的一个私有成员返回给函数的调用者。
只要将箭头函数包裹在小括号里,就可以用它实现相同的功能:
let person = ((name) => {
return {
getName: function() {
return name;
}
}
})("Nicholas");
console.log(person.getName()); // "Nicholas"
注意,小括号只包裹箭头函数定义,没有包含("Nicholas"),这一点与正常函数有所不同,由正常函数定义的立即执行函数表达式既可以用小括号包裹函数体,也可以额外包裹函数调用的部分。
箭头函数没有 this 绑定
函数内的this绑定是JavaScript中最常出现错误的因素,函数内的this值可以根据函数调用的上下文而改变,这有可能错误地影响其他对象。思考一下这个示例:
let PageHandler = {
id: "123456",
init: function() {
document.addEventListener("click", function(event){
this.doSomething(event.type); // 抛出错误
}, false)
},
doSomething: function(type) {
console.log("Handling" + type + " for " + this.id);
}
};
在这段代码中,对象PageHandler的设计初衷是用来处理页面上的交互,通过调用init()方法设置交互,依次分配事件处理程序来调用this.doSomething()。然而,这段代码并没有如预期的正常运行。
实际上,因为this绑定的是事件目标对象的引用(在这段代码中引用的是document),而没有绑定PageHandler,且由于this.doSonething()在目标document中不存在,所以无法正常执行,尝试运行这段代码只会使程序在触发事件处理程序时抛出错误。
可以使用bind()方法显式地将this绑定到PageHandler函数上来修正这个问题,就像这样:
let PageHandler = {
id: "123456",
init: function() {
document.addEventListener("click", (function(event){
this.doSomething(event.type); // 没有错误产生
}).bind(this), false)
},
doSomething: function(type) {
console.log("Handling" + type + " for " + this.id);
}
};
现在代码如预期的运行,但可能看起来仍然有点儿奇怪,调用bind(this)后事实上创建了一个新函数,它的this被绑定到当前的this,也就是PageHandler。为了避免创建一个额外的函数,我们可以通过一个更好的方式来修正这段代码:使用箭头函数。
箭头函数中没有this绑定,必须通过查找作用域链来决定其值。如果箭头函数被非箭头函数包含,则this绑定的是最近一层非箭头函数的this;否则,this的值会被设置为undefined。可以通过以下这种方式使用箭头函数:
let PageHandler = {
id: "123456",
init: function() {
document.addEventListener("click", event => this.doSomething(event.type), false)
},
doSomething: function(type) {
console.log("Handling" + type + " for " + this.id);
}
};
这个示例中的事件处理程序是一个调用了this.doSomething()的箭头函数,此处的this与init()函数里的this一致,所以此版本代码的运行结果与使用bind(this)一致。虽然doSomething()方法不返回值,但是它仍是函数体内唯一的一条执行语句,所以不必用花括号将它包裹起来。
箭头函数缺少正常函数所拥有的prototype属性,它的设计初衷是“即用即弃”,所以不能用它来定义新的类型。如果尝试通过new关键字调用一个箭头函数,会导致程序抛出错误,就像这个示例一样:
let MyType = () => {},
object = new MyType(); // 错误, 不可以通过 new 关键字调用箭头函数
在这段代码中,MyType是一个没有[[Construct]]方法的箭头函数,所以不能正常执行new MyType()。也正因为箭头函数不能与new关键字混用,所以JavaScript引擎可以进一步优化它们的行为。
同样,箭头函数中的this值取决于该函数外部非箭头函数的this值,且不能通过call()、apply()或bind()方法来改变this的值。
箭头函数和数组
箭头函数的语法简洁,非常适用于数组处理。举例来说,如果你想给数组排序,通常需要写一个自定义的比较器:
let result = values.sort(function(a, b) { return a - b; });
我们只想实现一个简单的功能,但这些代码实在太多了。这是用箭头函数简化后的版本:
let result = values.sort((a, b) => a - b);
诸如sort()、map()及reduce()这些可以接受回调函数的数组方法,都可以通过箭头函数语法简化编码过程并减少编码量。
箭头函数没有arguments绑定
箭头函数没有自己的arguments对象,且未来无论函数在哪个上下文中执行,箭头函数始终可以访问外围函数的arguments对象。举个例子:
function createArrowFunctionReturningFirstArg() {
return () => argument[0];
}
let arrowFunction = createArrowFunctionReturningFirstArg(5);
console.log(arrowFunction()); // 5
在createArrowFunctionReturningFirstArg()函数中,箭头函数引用了外围函数传入的第一个参数arguments[0],也就是后续执行过程中传入的数字5。即使函数箭头此时已不再处于创建它的函数的作用域中,却依然可以访问当时的arguments对象,这是arguments标识符的作用域链解决方案所规定的。
箭头函数的辨识方法
尽管箭头函数与传统函数的语法不同,但它同样可以被识别出来,请看以下这段代码:
let comparator = (a, b) => a - b;
console.log(typeof comparator); // "function"
console.log(comparator instanceof Function); // true
由console.log()的输出结果可知,使用typeof和instanceof操作符调用箭头函数与调用其他函数并无二致。
同样,仍然可以在箭头函数上调用call()、apply()及bind()方法,但与其他函数不同的是,箭头函数的this值不会受这些方法的影响。这里有一些示例:
let sum = (num1, num2) => num1 + num2;
console.log(sum.call(null, 1, 2)); // 3
console.log(sum.apple(null, [1, 2])); // 3
let boundSum = sum.bind(null, 1, 2);
console.log(boundSum()); // 3
通过call()方法和apply()方法调用sum()函数并传递参数;通过bind()方法创建boundSum()函数,并传入参数1和2。这些参数都不需要直接传入。
包括回调函数在内所有使用匿名函数表达式的地方都适合用箭头函数来改写