在 ECMAScript 6 中,我应该何时使用箭头函数?

有了,我们得到了两种非常相似的方式在ES6中编写函数。在其他语言中,lambda 函数通常通过匿名来区分自己,但在 ECMAScript 中,任何函数都可以是匿名的。这两种类型中的每一种都有唯一的使用域(即当需要显式绑定或显式不绑定时)。在这些域之间,有大量情况下,任何一种表示法都可以。() => {}function () {}this

ES6 中的箭头函数至少有两个限制:

  • 创建时不能使用,也不能使用newprototype
  • 初始化时固定绑定到作用域this

撇开这两个限制不谈,箭头函数理论上几乎可以在任何地方取代常规函数。在实践中使用它们的正确方法是什么?是否应该使用箭头函数,例如:

  • “无论它们在哪里工作”,即在任何地方,函数不必对变量不可知,我们也没有创建对象。this
  • 只有“在需要它们的地方”,即事件侦听器,超时,需要绑定到某个范围
  • 使用“短”函数,但不具有“长”函数
  • 仅使用不包含其他箭头函数的函数

我正在寻找一个指南,以便在 ECMAScript 的未来版本中选择适当的函数表示法。该指南需要明确,以便可以将其教授给团队中的开发人员,并保持一致,以便它不需要从一个函数表示法到另一个函数表示法的不断来回重构。

这个问题是针对那些在即将推出的 ECMAScript 6 (Harmony) 的上下文中思考过代码风格并且已经使用过该语言的人。


答案 1

不久前,我们的团队将其所有代码(一个中型的AngularJS应用程序)迁移到使用Traceur Babel编译的JavaScript。我现在对ES6及更高版本中的函数使用以下经验法则:

  • 在全局范围内使用,并用于属性。functionObject.prototype
  • 用于对象构造函数。class
  • 在其他任何地方使用。=>

为什么几乎到处都使用箭头函数?

  1. 范围安全:当箭头函数一致使用时,保证所有内容都与根函数相同。如果将单个标准函数回调与一堆箭头函数混合在一起,则范围可能会变得混乱。thisObject
  2. 紧凑性:箭头函数更易于读取和写入。(这似乎很固执己见,所以我将进一步给出几个例子。
  3. 清晰度:当几乎所有内容都是箭头函数时,任何常规都会立即用于定义范围。开发人员始终可以查找下一个更高的语句以查看其内容。functionfunctionthisObject

为什么总是在全局范围或模块范围上使用常规函数?

  1. 指示不应访问 的函数。thisObject
  2. 最好显式寻址对象(全局作用域)。window
  3. 许多定义存在于全局范围内(思考等),无论如何,这些定义通常必须是类型的。在全局范围内一致使用有助于避免错误。Object.prototypeString.prototype.truncatefunctionfunction
  4. 全局范围内的许多函数都是旧式类定义的对象构造函数。
  5. 函数可以命名为1。这有两个好处:(1)编写起来比编写更不尴尬,特别是在其他函数调用之外。(2) 函数名称显示在堆栈跟踪中。虽然命名每个内部回调都很繁琐,但命名所有公共函数可能是一个好主意。function foo(){}const foo = () => {}
  6. 函数声明被提升(意味着在声明之前可以访问它们),这是静态实用程序函数中的一个有用属性。

对象构造函数

尝试实例化箭头函数会引发异常:

var x = () => {};
new x(); // TypeError: x is not a constructor

因此,与箭头函数相比,函数的一个关键优势是函数兼作对象构造函数:

function Person(name) {
    this.name = name;
}

但是,功能相同的2 ECMAScript Harmony 草稿类定义几乎同样紧凑:

class Person {
    constructor(name) {
        this.name = name;
    }
}

我预计,使用前一种符号最终将被劝阻。对象构造函数表示法可能仍被某些人用于简单的匿名对象工厂,其中对象是以编程方式生成的,但不适用于其他任何用途。

如果需要对象构造函数,则应考虑将函数转换为 a,如上所示。该语法也适用于匿名函数/类。class

箭头函数的可读性

坚持常规函数(范围安全该死)的最佳论据可能是箭头函数的可读性低于常规函数。如果你的代码一开始就不起作用,那么箭头函数可能看起来没有必要,当箭头函数没有被一致地使用时,它们看起来很丑陋。

自从 ECMAScript 5.1 为我们提供了函数式和所有这些函数式编程特性以来,ECMAScript 已经发生了很大的变化,所有这些特性让我们使用函数,而 for 循环以前会被使用。异步 JavaScript 已经起飞了很多。ES6还将发布一个Promise对象,这意味着更多的匿名函数。函数式编程没有回头路可走。在函数式JavaScript中,箭头函数比常规函数更可取。Array.forEachArray.map

以这段(特别令人困惑的)代码3为例:

function CommentController(articles) {
    this.comments = [];

    articles.getList()
        .then(articles => Promise.all(articles.map(article => article.comments.getList())))
        .then(commentLists => commentLists.reduce((a, b) => a.concat(b)));
        .then(comments => {
            this.comments = comments;
        })
}

具有常规函数的同一段代码:

function CommentController(articles) {
    this.comments = [];

    articles.getList()
        .then(function (articles) {
            return Promise.all(articles.map(function (article) {
                return article.comments.getList();
            }));
        })
        .then(function (commentLists) {
            return commentLists.reduce(function (a, b) {
                return a.concat(b);
            });
        })
        .then(function (comments) {
            this.comments = comments;
        }.bind(this));
}

虽然任何一个箭头函数都可以替换为标准函数,但这样做几乎没有什么好处。哪个版本更具可读性?我会说第一个。

我认为,随着时间的推移,使用箭头函数还是常规函数的问题将变得不那么重要。大多数函数要么成为类方法,取消关键字,要么它们将成为类。函数将继续用于通过 修补类。同时,我建议将关键字保留给任何真正应该是类方法或类的内容。functionObject.prototypefunction


笔记

  1. 命名箭头函数在 ES6 规范中已延迟。它们可能仍会添加到将来的版本。
  2. 根据规范草案,“类声明/表达式创建构造函数/原型对与函数声明完全相同”,只要类不使用关键字即可。一个细微的区别是类声明是常量,而函数声明不是。extend
  3. 关于单语句箭头函数中的块的注意事项:我喜欢在仅针对副作用(例如,赋值)调用箭头函数的地方使用块。这样就可以清楚地丢弃返回值。

答案 2

根据该提案,箭头旨在“解决和解决传统功能表达的几个常见痛点”。他们打算通过绑定词汇和提供简洁的语法来改善问题。this

然而

  • 一个人不能始终如一地在词汇上绑定this
  • 箭头函数语法细腻且模棱两可

因此,箭头函数会造成混淆和错误,应该从 JavaScript 程序员的词汇表中排除,代之以专用。function

关于词汇this

this有问题:

function Book(settings) {
    this.settings = settings;
    this.pages = this.createPages();
}
Book.prototype.render = function () {
    this.pages.forEach(function (page) {
        page.draw(this.settings);
    }, this);
};

Arrow 函数旨在解决我们需要访问回调内部属性的问题。已经有几种方法可以做到这一点:可以分配给变量,使用,或者使用聚合方法上可用的第三个参数。然而,箭头似乎是最简单的解决方法,因此该方法可以像这样重构:thisthisbindArray

this.pages.forEach(page => page.draw(this.settings));

但是,请考虑代码是否使用了类似 jQuery 的库,其方法专门绑定。现在,有两个值需要处理:thisthis

Book.prototype.render = function () {
    var book = this;
    this.$pages.each(function (index) {
        var $page = $(this);
        book.draw(book.currentPage + index, $page);
    });
};

我们必须使用,以便动态绑定。我们不能在这里使用箭头函数。functioneachthis

处理多个值也可能令人困惑,因为很难知道作者在谈论哪个:thisthis

function Reader() {
    this.book.on('change', function () {
        this.reformat();
    });
}

作者真的打算打电话吗?还是他忘了绑,打算打电话?如果我们将处理程序更改为箭头函数,我们同样会想知道作者是否想要动态,但选择了一个箭头,因为它适合一行:Book.prototype.reformatthisReader.prototype.reformatthis

function Reader() {
    this.book.on('change', () => this.reformat());
}

有人可能会说:“箭头有时可能是错误的功能,这是否是例外?也许如果我们很少需要动态值,那么大多数时候使用箭头仍然是可以的。this

但是问问自己:“调试代码并发现错误的结果是由'边缘情况'引起的,这是否'值得'?我宁愿避免麻烦,不仅大部分时间,而且100%的时间。

有一个更好的方法:始终使用(因此始终可以动态绑定),并且始终通过变量引用。变量是词法的,并假定许多名称。赋值到变量将明确您的意图:functionthisthisthis

function Reader() {
    var reader = this;
    reader.book.on('change', function () {
        var book = this;
        book.reformat();
        reader.reformat();
    });
}

此外,始终赋值给变量(即使存在单个函数或没有其他函数)可确保即使在代码更改后,其意图仍然清晰。thisthis

此外,动态也并非异常。jQuery在超过5000万个网站上使用(截至2016年2月撰写本文时)。以下是动态绑定的其他 API:thisthis

  • Mocha(昨天下载量约为120k)通过.this
  • Grunt(昨天下载量约为63k)通过公开了构建任务的方法。this
  • Backbone(昨天下载量约为 22k)定义了访问 的方法。this
  • 事件 API(如 DOM 的)引用 with .EventTargetthis
  • 原型修补或扩展的 API 是指带有 的实例。this

(通过 http://trends.builtwith.com/javascript/jQuery 和 https://www.npmjs.com 进行统计。

您可能已经需要动态绑定。this

有时需要一个词汇,但有时不是;就像动态有时是预期的,但有时不是。值得庆幸的是,有一种更好的方法,它总是产生和传达预期的绑定。thisthis

关于简洁的语法

箭头函数成功地为函数提供了“更短的语法形式”。但是,这些较短的功能会让你更成功吗?

“更容易阅读”比?也许是这样,因为它更有可能产生一行短代码。根据戴森的《阅读速度和行长对屏幕阅读效果的影响》x => x * xfunction (x) { return x * x; }

中等行长(每行 55 个字符)似乎支持在正常和快速速度下的有效读取。这产生了最高水平的理解力。 。 。

对于条件(三元)运算符和单行语句,也进行了类似的论证。if

但是,你真的在写提案中宣传的简单数学函数吗?我的域不是数学的,所以我的子例程很少这么优雅。相反,我经常看到箭头函数打破了列限制,并由于编辑器或样式指南而换行到另一行,这根据戴森的定义使“可读性”无效。

有人可能会说,“如果可能的话,只使用短版本来做短功能怎么样?但现在,文体规则与语言约束相矛盾:“尝试使用尽可能短的函数表示法,请记住,有时只有最长的表示法才能按预期绑定。这种混杂使得箭头特别容易被误用。this

箭头函数语法存在许多问题:

const a = x =>
    doSomething(x);

const b = x =>
    doSomething(x);
    doSomethingElse(x);

这两个函数在语法上都是有效的。但不在正文中。它只是一个缩进不良的顶级语句。doSomethingElse(x);b

当扩展到块形式时,不再有一个隐式的,人们可能会忘记恢复。但是这个表达式可能只是为了产生副作用,所以谁知道未来是否需要显式表达呢?returnreturn

const create = () => User.create();

const create = () => {
    let user;
    User.create().then(result => {
        user = result;
        return sendEmail();
    }).then(() => user);
};

const create = () => {
    let user;
    return User.create().then(result => {
        user = result;
        return sendEmail();
    }).then(() => user);
};

可能用作 rest 参数的内容可以解析为 spread 运算符:

processData(data, ...results => {}) // Spread
processData(data, (...results) => {}) // Rest

赋值可能与默认参数混淆:

const a = 1;
let x;
const b = x => {}; // No default
const b = x = a => {}; // "Adding a default" instead creates a double assignment
const b = (x = a) => {}; // Remember to add parentheses

块看起来像对象:

(id) => id // Returns `id`
(id) => {name: id} // Returns `undefined` (it's a labeled statement)
(id) => ({name: id}) // Returns an object

这是什么意思?

() => {}

作者是否打算创建一个 no-op,或者一个返回空对象的函数?(考虑到这一点,我们是否应该在之后放置?我们是否应该将自己限制在表达式语法上?这将进一步降低箭头的频率。{=>

=>看起来像和:<=>=

x => 1 ? 2 : 3
x <= 1 ? 2 : 3

if (x => 1) {}
if (x >= 1) {}

要立即调用箭头函数表达式,必须放在外面,但放在里面是有效的,可能是有意的。()()

(() => doSomething()()) // Creates function calling value of `doSomething()`
(() => doSomething())() // Calls the arrow function

虽然,如果一个人写的目的是编写一个立即调用的函数表达式,那么什么都不会发生。(() => doSomething()());

考虑到上述所有情况,很难说箭头函数“更容易理解”。人们可以学习使用此语法所需的所有特殊规则。真的值得吗?

的语法是无可比拟的概括。独占使用意味着语言本身可以防止人们编写令人困惑的代码。要编写在所有情况下都应在语法上理解的过程,我选择 。functionfunctionfunction

关于指南

您要求制定一个需要“清晰”和“一致”的指南。使用箭头函数最终将导致语法上有效、逻辑上无效的代码,两种函数形式相互交织,有意义且任意。因此,我提供以下服务:

ES6 中的函数表示法指南:

  • 始终使用 创建过程。function
  • 始终赋值给变量。请勿使用 。this() => {}