【译】《Understanding ECMAScript6》- 第二章-函数

时间:2022-04-25
本文章向大家介绍【译】《Understanding ECMAScript6》- 第二章-函数,主要内容包括函数在任何一门编程语言中都是很重要的一个环节。JavaScript至今已有多年的历史,但是它的函数仍然停留在很初级的阶段。函数问题的大量堆积,以及某些函数非常微妙的功能差异,很容易产生错误,并且有时候一个很简单的功能往往需要通过大量的代码来实现。、默认参数、剩余参数、解构参数、展开运算符、name属性、new.target, [[Call]], 和[[Construct]]、块级域函数、箭头函数、自执行函数-IIFEs、语义绑定(Lexical this binding)、语义参数绑定(Lexical arguments binding)、如何识别箭头函数、总结、基本概念、基础应用、原理机制和需要注意的事项等,并结合实例形式分析了其使用技巧,希望通过本文能帮助到大家理解应用这部分内容。

函数在任何一门编程语言中都是很重要的一个环节。JavaScript至今已有多年的历史,但是它的函数仍然停留在很初级的阶段。函数问题的大量堆积,以及某些函数非常微妙的功能差异,很容易产生错误,并且有时候一个很简单的功能往往需要通过大量的代码来实现。

ES6吸取了多年来JavaScript开发者的反馈,在ES5函数的基础上进行了大量的改进,令JavaScript程序更加健壮并且减少了错误发生率。

默认参数

JavaScript函数的特性之一,便是接受传入的参数可以与函数定义的参数数量不同。利用这种特性,函数可以根据参数的数量进行不同的处理,通常的做法是,如果某个参数没有被传入,则将其赋值一个默认值。如下:

function makeRequest(url, timeout, callback) {
    timeout = timeout || 2000;
    callback = callback || function() {};
    // the rest of the function
}

这个例子中,timeoutcallback都是可选参数,如果不被传入,则被赋值一个默认值。逻辑或操作符||在第一个操作数为非正值时返回第二个操作数。JavaScript函数定义的参数如果不被传入就是会设置为undefined,逻辑或操作符在处理参数个数补丁的场景中应用很普遍。但是这种方法有一个缺陷,如果timeout参数值为0,它仍然会被2000取代,因为0是非正值。

当然还有其他方法来处理参数数目不定的函数,比如通过检查arguments.length来获取传参的数量,以及逐个判断每个参数是否为undefined来弥补||的不足。

ES6新增了默认参数的支持,当一个参数没有被传入时将使用初始值进行运算,如下例:

function makeRequest(url, timeout = 2000, callback = function() {}) {
    // the rest of the function
}

上例中,只有参数url是规定为必选的,其余两个参数可选,并且有初始值。有了默认参数,我们就不需要再函数内部进行特殊处理,领函数主体更加简洁。当函数makeRequest()被传入三个参数时,默认参数将以传入值进行运算,如下:

// uses default timeout and callback
makeRequest("/foo");
// uses default callback
makeRequest("/foo", 500);
// doesn't use defaults
makeRequest("/foo", 500, function(body) {
    doSomething(body);
});

译者注:被赋值初始值的参数都被认为是可选参数,没有初始值的参数被认为是必选参数。

你可以指定任何一个参数作为默认参数,即使这个参数不是在参数队列的末尾,如下:

function makeRequest(url, timeout = 2000, callback) {
    // the rest of the function
}

上例中,默认参数timeout的默认值只有在第二个参数不被传入或被传入undefined时生效,如下:

// uses default timeout
makeRequest("/foo", undefined, function(body) {
    doSomething(body);
});
// uses default timeout
makeRequest("/foo");
// doesn't use default timeout
makeRequest("/foo", null, function(body) {
    doSomething(body);
});

译者注:如果默认参数被传值null,则会被认为是符合规范的,此时默认参数将被赋值null进行运算。

默认参数有一个很有趣的特性,它的默认值可以不是一个具体值,你甚至可以执行一个函数来获取它,如下:

function getCallback() {
    return function() {
        // some code
    };
}

function makeRequest(url, timeout = 2000, callback = getCallback()) {

    // the rest of the function

}

上例中,如果第三个参数未被传值,则会调用getCallback()方法获取默认值。这种特性可以令函数的参数具有更好的动态性。

剩余参数

由于JavaScript函数可以接受任意数目的参数,所以通常情况下开发者不必精确定义每个参数。对于没有精确定义的参数,JavaScript提供arguments变量来获取所有被传入的参数。这种方式虽然可以解决大多数需求,但处理的过程并不轻松。举例如下:

function pick(object) {
    let result = Object.create(null);
    for (let i = 1, len = arguments.length; i < len; i++) {
        result[arguments[i]] = object[arguments[i]];
    }
    return result;
}
let book = {
    title: "Understanding ECMAScript 6",
    author: "Nicholas C. Zakas",
    year: 2015
};
let bookData = pick(book, "author", "year");

console.log(bookData.author);   // "Nicholas C. Zakas"
console.log(bookData.year);     // 2015

上例中的pick()函数的作用是复制参数object的指定属性,并返回一个带有这些复制属性的对象。第一个参数object是被复制的对象,其余参数是指定复制的属性名称。这个函数由几点需要注意:首先,如果不看pick()函数的内部逻辑,从声明方式来看并不能知道它可以处理多个参数。当然,你也可以在声明的时候添加多个命名参数,但是仍然无法表明它可以处理任意数目的参数;其次,由于第一个参数是命名参数并且直接参与运算,所以再遍历arguments对象时必须从索引1开始,而不是索引0。虽然通过索引值遍历arguments对象并不困难,但这仍然是一种很精细的工作。ES6新增的剩余参数机制可以为上述问题提供相对便利的解决方案。

剩余参数的声明语法是命名参数配合...前缀。此命名参数是一个包含参数队列中除去必选参数以外参数的数组。利用剩余参数,pick()参数可以用以下方式声明:

function pick(object, ...keys) {
    let result = Object.create(null);
    for (let i = 0, len = keys.length; i < len; i++) {
        result[keys[i]] = object[keys[i]];
    }
    return result;
}

keys是一个剩余参数,它包含除第一个参数以外的所有参数(arguments包含所有参数,包括第一个参数)。开发者可以从头至尾的遍历keys,不用担心索引值问题。另外,这种声明方式可以明确的表明此函数可以处理任意数目的参数。

剩余参数的唯一约束就是在剩余参数之后不能声明任何命名参数。如下的声明方法将产生语法错误:

// Syntax error: Can't have a named parameter after rest parameters
function pick(object, ...keys, last) {
    let result = Object.create(null);
    for (let i = 0, len = keys.length; i < len; i++) {
        result[keys[i]] = object[keys[i]];
    }
    return result;
}

参数last在剩余参数keys后面声明,将引起语法错误。

剩余参数的初衷是取代arguments。ES4曾经想去掉arguments并用剩余参数代替。虽然ES4没有成功推出,但这种理念被ES6继承了,不同的是,ES6仍然保留了arguments

解构参数

第一章中介绍了解构赋值,其实解构并不局限于赋值表达式中的应用,ES6中引入的解构参数机制能够丰富应用程序的表现力。

译者注:对于"解构"一词,可以简单的理解为“结构分解”。起源于哲学领域。

我们经常看到以下这种代码,使用一个options对象包含所有的可选参数:

function setCookie(name, value, options) {
    options = options || {};
    var secure = options.secure,
        path = options.path,
        domain = options.domain,
        expires = options.expires;

    // ...
}
setCookie("type", "js", {
    secure: true,
    expires: 60000
});

在JavaScript库文件中有很多类似setCookie()的写法。除namevalue以外的所有参数均是可选的。由于这些可选参数没有先后顺序,不能将它们声明为函数的命名参数,所以它们只能作为对象的命名属性传入函数,才能保证被正确的解析并保证非必选性。这种方法虽然能够满足需求,但是降低了API的可读性。

译者注:第一章提到的剩余参数并不能满足这种需求,因为剩余参数内的元素是非命名参数。

有了解构参数,上述的代码可以修改为以下形式:

function setCookie(name, value, { secure, path, domain, expires }) {

    // ...
}
setCookie("type", "js", {
    secure: true,
    expires: 60000
});

上述代码的功能与前例中的函数相同。请注意第三个参数是以解构的形式声明的,称之为解构参数。解构参数清晰地表达出函数所需可选参数的名称。如果解构参数内部的元素不被传入,则默认为undefined

需要注意的是,如果解构参数整体不被传入,则会抛出运行错误。比如上例中的setCookie()函数只传入namevalue参数,就会抛出运行错误:

// Error!
setCookie("type", "js");

上述函数因为没有传入第三个参数,最终抛出运行错误。造成这种问题的原理在于解构参数本质上是解构赋值的缩略形式。JavaScript引擎处理解构参数的原理如下:

function setCookie(name, value, options) {
    var { secure, path, domain, expires } = options;
    // ...
}

如果解构赋值表达式的右操作符为nullundefined则会抛出错误,所以当解构参数整体不被传入时,便会引起运行错误。

我们可以结合默认参数解决这种问题

function setCookie(name, value, { secure, path, domain, expires } = {}) {
    // ...
}

这时如果 函数setCookie()第三个参数不被传入,则解构参数内部的securepathdomainexpories全部默认为undefined

笔者建议开发者在使用解构参数时将它赋予默认值,以避免上文提到的这种问题。

展开运算符

ES6新增的展开运算符与剩余参数密切相关。剩余参数原理是将多个独立的参数整合为一个数组,而展开操作符是将一个数组分解并将数组的元素作为独立的参数传入一个函数。要理解展开运算符,我们可以联想到Math.max()函数,Math.max()函数接受任意数量的参数并且返回所有参数中的最大值,如下:

let value1 = 25,
    value2 = 50;
console.log(Math.max(value1, value2));      // 50

Math.max()函数可以处理任意数量的独立数字,但是如果我们需要获取一个数组中的最大值,由于Math.max()并不接受数组作为参数,所以在ES6之前,开发者要么通过循环,要么通过apply()函数用以下的方式处理:

let values = [25, 50, 75, 100]
console.log(Math.max.apply(Math, values));  // 100

译者注:此处需要读者熟悉apply()函数的语法,读者可以思考为何call()函数无法满足需求

虽然上述代码可以满足需求,但是这种繁琐的语法令代码的可读性非常差。

展开运算符可以用更加简洁易读的方式实现需求。只需要按照剩余参数的语法,将携带...前缀的数组传入函数即可,不需要apply()函数那么繁琐的操作。JavaScript引擎会将数组分解并将数组内的元素作为独立参数传入:

let values = [25, 50, 75, 100]
// equivalent to
// console.log(Math.max(25, 50, 75, 100));
console.log(Math.max(...values));           // 100

上述代码中Math.max()使用常规的语法被调用,代码简洁易读。

你甚至可以将展开运算符与其他参数混合使用。比如,为了过滤数组中的负值,我们想要在使用Math.max()函数获取数组内最大值的时候,规定返回的结果不能小于0。可以通过以下代码实现:

let values = [-25, -50, -75, -100]
console.log(Math.max(...values, 0));        // 0

上述代码中的0作为独立参数传入,数组value使用展开运算符传入。

译者注:使用展开运算符的参数并不是剩余参数,读者需要将二者区别开。剩余参数后不能有任何独立参数,而使用展开运算符的参数后面可以传入其他参数。

name属性

JavaScript函数的声明方式多种多样,造成函数的辨识操作非常困难。被广泛使用的匿名函数令这项操作更加困难,开发者往往需要通过堆栈跟踪才能从一堆乱糟糟的代码中辨识一个函数。为解决这种问题,ES6为所有函数新增了name属性。

ES6每个函数都有一个name属性:

function doSomething() {
    // ...
}
var doAnotherThing = function() {
    // ...
};
console.log(doSomething.name);          // "doSomething"
console.log(doAnotherThing.name);       // "doAnotherThing"

上述代码中,以函数声明方式声明的doSomething()函数的name属性为doSomething。以赋值声明方式声明的匿名函数doAnotherThing()name属性值为被赋值的变量名doAnotherThing

用以上两种方式声明的函数,其name属性值一名了然。对于使用其他方式声明的函数,ES6同样制定了name属性的取值规范:

var doSomething = function doSomethingElse() {
    // ...
};
var person = {
    get firstName() {
        return "Nicholas"
    },
    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,是因为函数表达式自身具备name属性并且比被赋值的变量名有更高的优先级;person.sayName()name属性值为sayName,取值来自于对象字面量;person.firstName是一个getter函数,它的name属性取值get firstName以表明类型(与getter函数类似,setter函数set前缀修饰)。

ES6同样为其他方式声明的name属性取值指定了规范。比如使用bind()创建的函数name属性用bound前缀修饰;使用Function构造函数声明的函数name属性用anonymous前缀修饰:

var doSomething = function() {
    // ...
};
console.log(doSomething.bind().name);   // "bound doSomething"
console.log((new Function()).name);     // "anonymous"

bind()创建的函数name属性取值被绑定函数的name属性值配合bound修饰前缀,所以绑定函数doSomething()name属性取值为bound doSomething

译者注:bind()函数的作用和调用方法与call()类似,只不过bind()函数的第一个参数可以不传,默认为被绑定的函数作为执行作用域,参考Function.prototype.bind()

new.target, [[Call]], 和[[Construct]]

在ES5及其之前的版本中,使用new调用的函数和不使用new调用的函数具有完全不同的运作机制。使用new操作符时,被调用的函数内部的this指向一个新对象并且最后这个新对象会作为运行结果被返回。如下:

function Person(name) {
    this.name = name;
}
var person = new Person("Nicholas");
var notAPerson = Person("Nicholas");
console.log(person);        // "[Object object]"
console.log(notAPerson);    // "undefined"

上述代码中,没有使用new操作符调用的函数Person()返回结果为undefined(在非严格模式下,全局对象的name属性将被赋值为Nicholas)。而使用new操作符调用Person()的意图很明显是为了创建一个新对象。函数的双重角色问题一直困惑着开发者们,从而推进了ES6针对这个问题的改动。

首先,规范定义了两个不同的函数内部方法:[[Call]][[Construct]]。不使用new调用函数时,[[Call]]方法被执行,它将按照正常的上下文逻辑执行函数内部的代码。当使用new调用函数时,方法[[Construct]]被执行,它负责创建一个新对象,或者称为新目标,然后将this指向新对象后再执行函数内部的代码。具备[[Construct]]方法的函数被称为构造函数

需要注意的是,并非所有函数都具备[[Construct]]方法,也就是说并非所有函数都可以被new操作符调用。本章随后介绍的箭头函数便不具备[[Construct]]方法。

在ES5中,开发者们经常使用instanceof判断一个函数是否被new调用,如下:

function Person(name) {
    if (this instanceof Person) {
        this.name = name;   // using new
    } else {
        throw new Error("You must use new with Person.")
    }
}
var person = new Person("Nicholas");
var notAPerson = Person("Nicholas");  // throws error

函数Person()内的通过检查this是否指向的是Person的实例,如果检查通过就会执行if内部的逻辑。如果this不是Person的实例则会抛出错误。这样做的原理是[[Construct]]创建了Person的一个实例并将this指向它。但是这种检查并不完全可靠,因为即使不使用new调用Personthis仍然可能指向Person的实例,如下:

function Person(name) {
    if (this instanceof Person) {
        this.name = name;   // using new
    } else {
        throw new Error("You must use new with Person.")
    }
}
var person = new Person("Nicholas");
var notAPerson = Person.call(person, "Michael");    // works!

上述代码中Person.call()的第一个参数person是函数Person的一个实例,此时调用Person函数,其内部的this指向的是它的实例,从而绕过了instanceof检测。

为了弥补这种缺陷,ES6新增了元属性new.target当函数的[[Construct]]被执行时,new.target将指向new操作符调用的函数(也就是本例中的Person函数),也就是新创建实例的构造函数。如果[[Call]]被执行,new.target的取值为undefined。有了这种机制的支撑,我们便可以通过检测new.target是否为undefined来判断函数是否被new调用:

function Person(name) {
    if (typeof new.target !== "undefined") {
        this.name = name;   // using new
    } else {
        throw new Error("You must use new with Person.")
    }
}
var person = new Person("Nicholas");
var notAPerson = Person.call(person, "Michael");    // error!

上述代码使用new.target取代了原来的this instanceof Person,对于非new的调用抛出了错误,得到了预期的结果。

译者注:请注意new.target并非指向实例,而是指向实例的构造函数。这是它跟this的本质区别。

我们还通过new.target判断函数是否被特定的构造函数调用,如下:

function Person(name) {
    if (typeof new.target === Person) {
        this.name = name;   // using new
    } else {
        throw new Error("You must use new with Person.")
    }
}
function AnotherPerson(name) {
    Person.call(this, name);
}
var person = new Person("Nicholas");
var anotherPerson = new AnotherPerson("Nicholas");  // error!

上述代码中限制new.target必须指向Person。当执行new AnotherPerson("Nicholas")时,new.target指向AnotherPerson而非Person,所以随后的Person.call(this, name)将会抛出错误。

new.target只能在函数内部使用,否则会抛出语法错误

块级域函数

在ES3以及更早的版本中,函数是不能在一个块级代码内通过字面量语法声明的,否则会引起语法错误。尽管规范如此,但很多浏览器仍然支持这种错误的语法,并且不同浏览器之间的兼容性有细微的差别。因此建议开发者尽量避免在块级代码内使用字面量声明函数(使用赋值表达式声明函数并不会引起以上问题)。

为了避免不兼容性,ES5的严格模式中对块级代码内的函数字面量声明会抛出语法错误:

"use strict";
if (true) {
    // Throws a syntax error in ES5, not so in ES6
    function doSomething() {
        // ...
    }
}

上述代码在ES5环境中会抛出语法错误。在ES6环境中,函数doSomething()是一个块级域函数,它可以被所在块级域内的其他逻辑访问和调用。如下:

"use strict";
if (true) {
    console.log(typeof doSomething);        // "function"
    function doSomething() {
        // ...
    }
    doSomething();
}
console.log(typeof doSomething);            // "undefined"

块级域函数的声明被提升至当前块级域的顶部,所以函数声明语句前面的typeof doSomething返回"function"。同块级域变量一下,if块级代码执行完毕后,doSomething()函数就会被回收。

块级域函数与使用let赋值表达式声明的函数类似,一旦当前块级域执行完毕就会被回收。唯一的区别是:块级域函数会被声明提升至块级域顶部,但是let表达式声明的函数不会被提升。如下:

"use strict";
if (true) {
    console.log(typeof doSomething);        // throws error
    let doSomething = function () {
        // ...
    }
    doSomething();
}
console.log(typeof doSomething);

由于let并未被声明提升,上述代码的typeof doSomething会抛出运行错误。

开发者可以根据是否有声明提升的需求来决定使用哪一种声明方式。

ES6的块级域函数在非严格模式与严格模式下的表现有细微的差别。非严格模式下,块级域函数的声明并不会被提升至块级域顶部,而是被提升至函数作用域或者全局域的顶部。如下:

// ECMAScript 6 behavior
if (true) {
    console.log(typeof doSomething);        // "function"
    function doSomething() {
        // ...
    }
    doSomething();
}
console.log(typeof doSomething);            // "function"

上述代码中,doSomething()函数被提升至全局域的顶部,在if块级域外仍然可以访问它。ES6的这种行为是为了修复前文提到的不兼容性问题。

译者注:非严格模式下的块级域函数本质上已经不是块级域函数了,只是在块级代码内声明的普通函数。

箭头函数

箭头函数是ES6非常有趣并且非常重要的一个模块。顾名思义,箭头函数使用一个箭头=>声明。箭头函数与普通函数的区别主要有以下几点:

  • 语义绑定(Lexical this binding)—— 箭头函数内部的this由函数被声明的位置而不是被调用的位置决定。
  • 不能创建实例(Not newable)—— 箭头函数不存在[[Construct]]方法,它不能作为构造函数使用。如果使用new调用箭头函数将会抛出错误。
  • 不能改变内部的this指向(Can’t change this)—— 箭头函数内部的this指向不能被修改,它在整个函数生命周期内保持为固定值。
  • 没有arguments对象(No arguments object)—— 不能通过arguments对象访问箭头函数的参数,只能访问命名参数或者ES6规范的其他参数类型,比如剩余参数。

箭头函数的特性可以一定程度上改善JavaScript应用程序的表现,其中最重要的一点是关于this的修正。JavaScript代码中有很多问题是由this引起的。开发者们经常疏忽函数内部的this指向,从而引起各种意料之外的问题。箭头函数内部的this取值是固定的,贯穿整个函数的生命周期,这种机制可以令JavaScript引擎更容易地进行优化操作。

箭头函数与普通函数一样具有name属性。

语法

箭头函数的语法针对不同需求有很多变种。所有的变种遵循以下规范:参数=>函数体。参数和函数体可以根据需求变换不同的形式。如下例所示的箭头函数,接受一个参数并且返回此参数:

var reflect = value => value;
// 等价于:
var reflect = function(value) {
    return value;
};

如果箭头函数只有一个参数,可以直接使用这个参数而不需要额外的语法。箭头右侧的表达式将会被执行并返回,不需要使用return语句。

如果箭头函数有多个参数,则需要将参数包含在圆括号内。如下:

var sum = (num1, num2) => num1 + num2;
// 等价于:
var sum = function(num1, num2) {
    return num1 + num2;
};

sum()函数的作用是将两个参数做加法运算并返回运算值。与上例的唯一区别是,两个参数被包含在圆括号内。

如果箭头函数没有参数,则必须将一组空圆括号传入。如下:

var getName = () => "Nicholas";
// 等价于:
var getName = function() {
    return "Nicholas";
};

如果你想使箭头函数的函数体看起来更加接近普通函数,比如函数体包含不止一条表达式的情况下,只需要将箭头的右侧函数体用花括号包裹起来,并且定义明确的return语句即可。如下:

var sum = (num1, num2) => {
    return num1 + num2;
};
// 等价于:
var sum = function(num1, num2) {
    return num1 + num2;
};

使用花括号的箭头函数,除了上文提到的几点特性以外,与普通函数并无二异。

如果要定义一个空箭头函数,可以使用以下方式:

var doNothing = () => {};
// 等价于:
var doNothing = function() {};

需要注意上述代码中的,花括号用来定义函数体,而不是返回一个空对象。如果需要箭头函数返回一个对象,需要将此对象包裹在圆括号内,如下:

var getTempItem = id => ({ id: id, name: "Temp" });
// 等价于:
var getTempItem = function(id) {
    return {
        id: id,
        name: "Temp"
    };
};

包含在圆括号内部的花括号被认为是对象的字面量表达式,而不是函数体。

自执行函数-IIFEs

IIFEs创建一个立即执行的匿名函数并且不必生成引用。IIFEs生成的作用域完全独立,这种机制在JavaScript应用程序中被广泛使用。如下:

let person = function(name) {
    return {
        getName() {
            return name;
        }
    };
}("Nicholas");
console.log(person.getName());      // "Nicholas"

上述代码中的自执行函数创建了一个包含getName()方法的对象。getName()方法将参数name的值返回,并且name成为了IIFE返回对象的一个私有属性。

译者注:请注意“私有”一词,这个属性是完全隐藏的,只能通过person.getName()访问,而不能被其他方式访问,比如person.name将返回undefined。

使用箭头函数可以将上例的代码改写为以下格式:

let person = ((name) => {
    return {
        getName() {
            return name;
        }
    };
})("Nicholas");
console.log(person.getName());      // "Nicholas"

请注意上述代码中圆括号的位置,形参以及除实参"Nicholas"以外的函数体部分全部被包裹在内,实参"Nicholas"被单独包裹在一对圆括号内。

语义绑定(Lexical this binding)

函数内部this指向问题一直困扰着JavaScript开发者。this的指向取决于函数被调用的上下文关系,在处理多个对象时很容易产生混淆。如下:

var PageHandler = {
    id: "123456",
    init: function() {
        document.addEventListener("click", function(event) {
            this.doSomething(event.type);     // error
        }, false);
    },
    doSomething: function(type) {
        console.log("Handling " + type  + " for " + this.id);
    }
};

上述代码中的pageHandler对象用来处理页面的行为交互。init()函数设置click监听的响应this.doSomething()。然而上述代码的运行结果并非预想的顺利。当click事件触发this.doSomething()调用时,this的指向为点击事件的目标元素(本例中为document)而不是pageHandler对象。document对象没有doSomething方法,从而导致运行错误。

解决这个问题的一种方案是使用bind()函数将this指向绑定到pageHandler对象,如下:

var PageHandler = {
    id: "123456",
    init: function() {
        document.addEventListener("click", (function(event) {
            this.doSomething(event.type);     // no error
        }).bind(this), false);
    },
    doSomething: function(type) {
        console.log("Handling " + type  + " for " + this.id);
    }
};

上述代码虽然可以满足需求,但是有很多副作用。除了糟糕的可读性,使用bind(this)实际上创建了一个新函数并将新函数的this指向当前的this,也就是pageHandler,引起额外的性能花销。

箭头函数屏蔽了this的多变性。箭头函数内部的this指向与函数被定义的作用域this保持一致。如下:

var 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指向与init()this保持一致。这种机制保证上述代码与使用bind()一样可以满足需求。因为箭头函数内只有一条语句,所以不必包裹在花括号内。

箭头函数被定义为“用完即弃”的函数,它不能创建实例。箭头函数没有prototype属性,如果试图用new操作符创建箭头函数的实例将会抛出错误:

var MyType = () => {},
    object = new MyType();  // error - you can't use arrow functions with 'new'

另外,由于箭头函数的this是静态不变的,所以不能使用call()apply()或者bind()函数改变this的指向。

箭头函数简洁的语法可以为数组排序提供理想的解决方案。比如,一般情况下你可能会采用以下方法进行数组排序:

var result = values.sort(function(a, b) {
    return a - b;
});

上述代码的语法看起来非常繁琐,利用箭头函数,可以简写为如下形式:

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

任何可接收回调函数的数组相关函数(比如sort(), map()reduce())都可以使用箭头函数满足需求,并且代码更加简洁。

箭头函数被设计的初衷是在某些应用场景下取代匿名函数,它们不能作为构造函数使用,不具备很长的生命周期。箭头函数的最佳应用场景是作为常规函数的回调函数使用

语义参数绑定(Lexical arguments binding)

虽然箭头函数本身没有arguments对象,但是可以访问其容器函数的arguments对象。不论箭头函数何时被执行,arguments对象始终对其具有可访问性。如下:

function createArrowFunctionReturningFirstArg() {
    return () => arguments[0];
}
var arrowFunction = createArrowFunctionReturningFirstArg(5);
console.log(arrowFunction());       // 5

上述代码中的createArrowFunctionReturningFirstArg()函数内,arguments[0]被新创建的箭头函数引用。随后,箭头函数被执行时返回5,也就是createArrowFunctionReturningFirstArg()的第一个实参值。即使箭头函数并不是在其被创建的作用域内被执行,但是根据语义绑定机制,arguments对象仍然保持着可访问性。

译者注:所谓容器函数,是指箭头函数被定义位置的函数作用域。

如何识别箭头函数

尽管箭头函数的语法与普通函数不同,但是仍然可以使用常规的方法来判断它的类型:

var comparator = (a, b) => a - b;
console.log(typeof comparator);                 // "function"
console.log(comparator instanceof Function);    // true

使用typeofinstanceof判断箭头函数的返回结果与常规函数相同。

虽然箭头函数的this不会被改变,但是仍然可以使用call(),apply()和bind()调用箭头函数,如下:

var sum = (num1, num2) => num1 + num2;
console.log(sum.call(null, 1, 2));      // 3
console.log(sum.apply(null, [1, 2]));   // 3
var boundSum = sum.bind(null, 1, 2);
console.log(boundSum());                // 3

总结

ES6针对函数的改动并不是很多,每个改动都旨在改善JavaScript函数的开发工作。

默认参数允许指定参数的默认值,当形参没有被传入时不必进行额外的判断和赋值。

剩余参数将所有可选参数集合为一个独立的数组,比arguments对象的操作更灵活。

解构参数令函数的配置参数结构更加透明,增强API的可读性。

展开操作符是剩余参数的衍生行为,将参数数组分解为独立的参数传入函数。在ES6之前处理这种需求,要么手动拆解数组,要么使用apply()调用函数。使用展开操作符,开发者可以将参数作为数组传入任何函数,不必担心this的指向问题。

name属性可以更容易地辨别函数,以方便调试。此外,ES6修正了块级域函数的规范,以避免严格模式下的语法错误。

函数被常规调用时将触发内部方法[[Call]],当使用new生成函数实例时将触发内部方法[[Construct]]。新增的元属性new.target可以判断函数是否被new操作符调用。

箭头函数是ES6的一项重大改进。箭头函数的提出是为了取代匿名函数的应用场景,它有更加简洁的语法,this的语义绑定,并且没有arguments对象。箭头函数的this不能被修改,不能作为构造函数使用。