「译」理解ECMAScript 6中的arrow函数

September 13 2013 , Category: JavaScript

原文地址, 作者: NCZ, 翻译:Fantasyshao.

ECMAScript 6中最有意思的新内容是「arrow函数」。顾名思义,arrow函数就是一种使用叫做「arrow」(=>)的新语法来定义函数的方法。不过,arrow函数在以下几方面和「传统」的JavaScript函数表现的不同:

我们很难解释为什么会存在这些差异。首先,对this的绑定在JavaScript中引发诸多错误的罪魁祸首. 在函数体内我们会发现很难「追踪」this的值,并且因此会很轻易地产生很多意想不到的后果.其次,通过arrow函数来保证运行的函数只有一个单一的this值,JavaScript引擎可以更容易的优化这些操作(相对于那些被用来做构造函数或者经常修改的常规的函数).

语法

Arrow函数的语法可以有多种多样的形式,这还需要取决于你想要尝试实现的是什么.所有的变化都是以函数参数开始,接着是arrow,接着就是函数体.用法不同,参数和函数体就可以以不同形式出现.比如,下面这个arrow函数只有一个参数并简单的返回之:

var reflect = value => value;
// 相比以下相同作用的函数,显得更为高效:
var reflect = function(value) {
    return value;
};

当arrow函数只有一个参数的时候,那个参数可以直接被使用而不需要其他的多余的语法元素.Arrow紧接着参数,而arrow右边的表达式会被计算并返回.尽管没有明确的return语句,这个arrow函数会返回第一个传入的参数.

如果传入了两个及以上的参数,那么就必须用括号将参数包含起来以表示一个参数列表. 如:

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

sum()函数简单的将两个数字相加并返回其结果.唯一不同的地方就在于参数用逗号分隔,并封闭在括号内(就像传统的函数一样).同样的,没有参数的函数必须使用空括号来声明一个函数:

var sum = () => 1 + 2;
// 等同于:
var sum = function() {
    return 1 + 2;
};

当你需要提供一个更为「传统」的函数体时,也许由多个表达式组成,那么就需要用大括号将函数体包裹起来并显示的声明返回值,如:

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

除了不能使用arguments以外,我们可以像「传统」函数一样在arrow函数的大括号内进行任意操作.

因为大括号是用来表示函数体的,倘若一个arrow函数要在函数体外部返回一个对象字面量(Object literal),则必须在函数体外部添加一对括号,如:

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

    return {
        id: id,
        name: "Temp"
    };
};

将对象字面量用括号括起来,就意味着大括号内的是在函数体内部的一个对象字面量.

用法

JavaScript中最常见的一种错误就是由于在函数体内对this值的绑定. 由于函数中的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()方法是用来初始化页面的交互方法 – 给文档增加了一个事件监听器用来调用this.doSomething().但是这段代码实际上并不会起作用.由于this在事件处理函数中指向的是一个全局对象而不是PageHandler,对this.doSomething()的引用就因此破坏了.如果试着去运行这段代码,那么在事件绑定函数运行时就会产生错误,因为this.doSomething()并不存在于全局对象上.

你可以使用bind()方法显式地将this值绑定到PageHandler上:

var PageHandler = {

    id: "123456",

    init: function() {
        document.addEventListener("click", (function(event) {
            this.doSomething(event.type);     // error
        }).bind(this), false);
    },

    doSomething: function(type) {
        console.log("Handling " + type  + " for " + this.id);
    }
};

现在代码就能如期运行了,但是这样看起来会有点奇怪.因为调用bind(this)时就会产生一个将this值绑定到现在的this上的函数 – PageHandler().现在的代码运行起来就能如同你所预期的一样了,尽管需要创建另外一个函数来完成这个任务.

因为arrow函数在词法上对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.doSomething()的arrow函数.this的值在init()函数中都是一致的,所以这个版本的例子可以像上面那个使用bind()函数的一样.尽管doSomething()方法没有返回任何值,但它仍然是函数体内唯一一个被执行的语句,所以没有必要在外面增加大括号.

Arrow函数简洁明了的语法使得它们变得像其他函数中的参数一样靠谱.举例而言,如果你想要用ES5对一个数组进行排序,很有可能会这样写:

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

对于这种简单的程序步骤而言还是显得太为繁琐了.以下是更为紧凑的arrow函数版本:

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

数组方法可以接受的回调函数如sort(), map(), 以及reduce()等都可以通过arrow函数将复杂的过程转换为简单的代码.

其他

Arrow函数和「传统」的函数有很多不同之处,但是还是有很多地方适合它们相似的,如:

最大的不同之处在于,arrow函数不能使用new,如果尝试着对arrow函数使用之,就会抛出一个错误.

总结

Arrow函数是ECMAScript 6中十分有趣的特性,并且是到现在为止十分稳固的特性.随着将函数作为参数越来越流行,使用一种更为简洁的定义函数的方法是广受好评的改变,并将永远改变人们编程的方式.对于this的绑定能够解决开发者长久以来的痛楚,并且通过JavaScript引擎优化可以对性能有额外的加分.如果你想要尝试一下arrow函数,那么就快去更新你的FireFox吧!因为FireFox是第一个官方发布ECMAScript6实现的的浏览器!

BTW, Chrome(包括Chrome Canary 30+)到现在还不支持Arrow函数 (查看ECMAScript 6的浏览器兼容性), 所以要使用ECMAScript 6中的新特性还是必须用FireFox.

另,最近空闲事件不多,但是会在这些时间内抽出来翻译一些大师的文章,长点姿势. 个人感觉有些地方翻译的还是比较生硬,英语水平还有待提高 (囧)

–EOF–

支持作者 | 文章采用 CC BY-NC-SA 4.0,转载请注明出处