【问题标题】:When should I use arrow functions in ECMAScript 6?我什么时候应该在 ECMAScript 6 中使用箭头函数?
【发布时间】:2014-05-21 06:32:51
【问题描述】:

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

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

  • 不适用于new,并且在创建prototype时不能使用
  • 修复了this 在初始化时绑定到作用域的问题

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

  • “它们工作的任何地方”,即函数不必与 this 变量无关的任何地方,而且我们没有创建对象。
  • 仅限“需要它们的任何地方”,即需要绑定到特定范围的事件侦听器、超时
  • 具有“短”功能,但不具有“长”功能
  • 仅适用于不包含其他箭头函数的函数

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

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

【问题讨论】:

  • 您认为Fixed this bound to scope at initialisation 是一种限制?
  • 这是一个优势,但如果您计划在原始上下文之外重用该函数,它也可能是一个限制。例如,当通过 Object.prototype 动态地将函数添加到类时。我所说的“限制”是指更改 this 的值是您可以使用常规函数但不能使用箭头函数的事情。
  • 老实说,我认为编码风格指南是相当固执的。不要误会我的意思,我认为它们很重要,但没有适合所有人的单一指南。
  • 我不认为Fixed this bound to scope at initialisation 是一个限制。 :) 看看这篇文章:exploringjs.com/es6/ch_arrow-functions.html
  • @thefourtheye,“限制”在这里的意思是“限制,因为一个愚蠢的自动代码翻译器不能盲目地用另一个替换一个并假设一切都会按预期运行”。

标签: javascript lambda ecmascript-6 ecmascript-harmony arrow-functions


【解决方案1】:

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

  • 在全局范围和Object.prototype 属性中使用function。
  • 将class 用于对象构造函数。
  • 在其他任何地方使用=>。

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

  1. 范围安全:当箭头函数被一致使用时,一切都保证使用相同的thisObject作为根。如果即使是单个标准函数回调与一堆箭头函数混合在一起,范围也可能会变得混乱。
  2. 紧凑性:箭头函数更易于读写。 (这可能看起来很自以为是,所以我将进一步举几个例子。)
  3. 清晰:当几乎所有东西都是箭头函数时,任何常规的function 都会立即突出来定义范围。开发人员可以随时查看下一个更高的 function 语句以了解 thisObject 是什么。

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

  1. 表示不应访问thisObject 的函数。
  2. window 对象(全局范围)最好显式处理。
  3. 许多Object.prototype 定义存在于全局范围内(想想String.prototype.truncate 等),而且这些通常必须是function 类型。在全局范围内始终使用function 有助于避免错误。
  4. 全局范围内的许多函数都是旧式类定义的对象构造函数。
  5. 函数可以命名为1。这有两个好处:(1) 写function foo(){} 比写const foo = () => {} 不那么尴尬——尤其是在其他函数调用之外。 (2) 函数名显示在堆栈跟踪中。虽然命名每个内部回调会很乏味,但命名所有公共函数可能是个好主意。
  6. 函数声明是hoisted,(意味着它们可以在声明之前被访问),这是静态实用函数中的一个有用属性。

对象构造函数

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

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

函数相对于箭头函数的一个关键优势是函数可以兼作对象构造函数:

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

但是,功能相同的2 ECMAScript Harmony draft class definition 几乎一样紧凑:

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

我希望最终不鼓励使用前一种表示法。对象构造函数符号可能仍被某些人用于简单的匿名对象工厂,其中对象是通过编程方式生成的,但其他情况不多。

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

箭头函数的可读性

坚持使用常规函数的最佳理由——该死的范围安全——可能是箭头函数的可读性不如常规函数。如果你的代码一开始就没有功能,那么箭头函数可能看起来就没有必要了,而且当箭头函数没有被一致使用时,它们看起来很丑。

自从 ECMAScript 5.1 为我们提供了函数式 Array.forEach、Array.map 和所有这些函数式编程特性,ECMAScript 发生了相当大的变化,这些特性让我们可以使用以前使用过 for 循环的函数.异步 JavaScript 已经取得了相当大的进展。 ES6 还将发布一个Promise 对象,这意味着更多的匿名函数。函数式编程没有回头路。在函数式 JavaScript 中,箭头函数优于常规函数。

以这段(特别令人困惑的)代码为例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));
}

虽然任何一个箭头函数都可以用标准函数代替,但这样做的好处很少。哪个版本更易读?我会说第一个。

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


注意事项

  1. 已命名的箭头函数为deferred in the ES6 specification。它们可能仍会在未来的版本中添加。
  2. 根据草案规范,“类声明/表达式创建构造函数/原型对,与函数声明完全相同”,只要类不使用 extend 关键字。一个小的区别是类声明是常量,而函数声明不是。
  3. 关于单语句箭头函数中的块的注意事项:我喜欢在调用箭头函数时单独使用块来实现副作用(例如,赋值)。这样就很明显可以丢弃返回值了。

【讨论】:

  • 您想使用function 的其他时间是您不想绑定this,对吗?我最常见的场景是事件,您可能希望this 引用触发事件的对象(通常是 DOM 节点)。
  • 我实际上认为在示例 3 中,常规函数更具可读性。即使是非程序员也能猜到发生了什么。使用箭头,您需要确切地知道它们如何工作才能理解该示例。也许更多的换行符会帮助箭头示例,但我不知道。只是我的 2 美分,但箭头让我畏缩(但我还没有使用它们,所以我可能很快就会转变。)
  • @Spencer 这是一个公平的观点。根据我自己的经验,=> 随着时间的推移会变得更好。我怀疑非程序员对这两个示例的感受会大不相同。如果您正在编写 ES2016 代码,通常也不会使用这么多箭头函数。在此示例中,使用 async/await 和数组推导式,您最终会在 reduce() 调用中得到一个箭头函数。
  • 我完全同意 Spencer 的观点,即常规函数在该示例中更具可读性。
  • 好答案,谢谢!我个人也尽可能在全局范围内使用箭头。这让我几乎没有“功能”。对我来说,代码中的“函数”意味着需要突出并仔细考虑的特殊情况。
【解决方案2】:

根据the proposal,箭头旨在“解决和解决传统函数表达式的几个常见痛点”。他们打算通过在词法上绑定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);
};

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

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

但是,考虑一下代码是否使用了像 jQuery 这样的库,它的方法专门绑定了this。现在,有两个 this 值需要处理:

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

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

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

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

作者真的打算打电话给Book.prototype.reformat吗?还是他忘记绑定this,打算调用Reader.prototype.reformat?如果我们将处理程序更改为箭头函数,我们同样会怀疑作者是否想要动态的this,但却选择了一个箭头,因为它适合一行:

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

有人可能会提出:“箭头有时可能是错误的函数,这很奇怪吗?也许如果我们很少需要动态的this 值,那么大多数时候使用箭头仍然是可以的。”

但是问问自己这个问题:“调试代码并发现错误的结果是由‘极端情况’引起的‘值得’吗?”我宁愿避免麻烦,而不仅仅是大多数时间,但 100% 的时间。

有一个更好的方法:总是使用function(所以this总是可以动态绑定),并且总是通过变量引用this。变量是词法的并采用许多名称。将this 分配给变量将使您的意图更加明确:

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

此外,总是将this 分配给一个变量(即使只有一个this 或没有其他函数)可以确保即使在代码更改后仍能保持清晰的意图。

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

  • Mocha(昨天下载量约为 12 万次)通过 this 公开了其测试方法。
  • Grunt(昨天下载量约为 63k)通过 this 公开了构建任务的方法。
  • Backbone(昨天下载量约为 22k)定义了访问 this 的方法。
  • 事件 API(如 DOM)引用 EventTarget 和 this。
  • Prototypal 修补或扩展的 API 指的是带有 this 的实例。

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

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

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

关于简洁语法

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

x => x * x 比 function (x) { return x * x; }“更容易阅读”吗?也许是这样,因为它更有可能产生一个单一的、短的代码行。根据戴森的The influence of reading speed and line length on the effectiveness of reading from screen,

中等长度的行(每行 55 个字符)似乎支持以正常和快速的速度进行有效阅读。这产生了最高层次的悟性。 . .

条件(三元)运算符和单行 if 语句也有类似的理由。

然而,你真的在写简单的数学函数advertised in the proposal吗?我的领域不是数学的,所以我的子程序很少如此优雅。相反,我经常看到箭头函数打破列限制,并由于编辑器或样式指南而换行,这使 Dyson 定义的“可读性”无效。

有人可能会提出,“如果可能的话,只使用简短版本的简短功能怎么样?”。但现在一个风格规则与语言约束相矛盾:“尽量使用最短的函数符号,记住有时只有最长的符号才能像预期的那样绑定this。”这种混淆使箭头特别容易被误用。

箭头函数语法有很多问题:

const a = x =>
    doSomething(x);

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

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

当扩展到块形式时,不再有隐含的return,人们可能会忘记恢复它。但是该表达式可能仅旨在产生副作用,所以谁知道未来是否需要明确的return?

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);
};

什么可能作为一个休息参数可以被解析为扩展运算符:

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

这是什么意思?

() => {}

作者是打算创建一个空操作,还是一个返回空对象的函数? (考虑到这一点,我们是否应该将{ 放在=> 之后?我们是否应该仅限于表达式语法?这会进一步降低箭头的频率。)

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

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

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

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

function 的语法非常通用。专门使用function 意味着语言本身可以防止人们编写混乱的代码。要编写在所有情况下都应在语法上理解的程序,我选择function。

关于指南

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

ES6 中的函数表示法指南:

  • 始终使用function 创建过程。
  • 始终将this 分配给变量。不要使用() =&gt; {}。

【讨论】:

  • 关于函数式程序员对 JavaScript 的看法的有趣文章。我不确定我是否同意私有变量的论点。 IMO 很少有人真正需要它们;那些这样做的人可能还需要其他合同功能,并且无论如何都会选择像 TypeScript 这样的语言扩展。我当然可以看到 self 而不是 this 的吸引力。您所说的箭头函数陷阱也都是有效的,并且与其他可以不用大括号的语句相同的标准也绝对适用于此;否则,我认为根据您的论点,人们也可以在任何地方提倡箭头功能。
  • “有多种做事方式会在工作场所和语言社区中为争论和分歧创造不必要的载体。如果语言语法不允许我们做出错误的选择会更好。”非常同意。不错的文案!我认为箭头函数实际上是退一步。在另一个主题上,我希望我的同事不要再尝试使用一系列 .prototype 定义将 JavaScript 转换为 C#。那真令人恶心。我应该匿名链接你的帖子:)
  • 写得真好!虽然我不同意你的大部分观点,但考虑相反的观点很重要。
  • 不是箭头函数,而是this 的怪异行为是 Javascript 的问题。 this 不应被隐式绑定,而应作为显式参数传递。
  • "始终使用函数(因此 this 始终可以动态绑定),并且始终通过变量引用 this。"。 我完全不同意!
【解决方案3】:

创建Arrow functions 是为了简化函数scope 并通过使其更简单来解决this 关键字。他们使用=&gt; 语法,看起来像一个箭头。

注意:它不会取代现有的功能。如果将每个函数语法都替换为箭头函数,则并非在所有情况下都有效。

让我们看看现有的 ES5 语法。如果 this 关键字在对象的方法(属于对象的函数)中,它指的是什么?

var Actor = {
  name: 'RajiniKanth',
  getName: function() {
     console.log(this.name);
  }
};
Actor.getName();

上面的 sn-p 将引用 object 并打印出名称 "RajiniKanth"。让我们探索下面的 sn-p,看看这里会指出什么。

var Actor = {
  name: 'RajiniKanth',
  movies: ['Kabali', 'Sivaji', 'Baba'],
  showMovies: function() {
   this.movies.forEach(function(movie) {
     alert(this.name + " has acted in " + movie);
   });
  }
};

Actor.showMovies();

现在如果this 关键字在method’s function 内部呢?

这里指的是window object,而不是inner function,因为它脱离了scope。因为this,总是引用它所在函数的所有者,对于这种情况 — 因为它现在超出范围 — 窗口/全局对象。

当它在 object 的方法内时 — function 的所有者就是对象。因此 this 关键字被绑定到对象。然而,当它在函数内部时,无论是独立的还是在另一个方法中,它总是会引用 window/global 对象。

var fn = function(){
  alert(this);
}

fn(); // [object Window]

在我们的 ES5 本身中有解决这个问题的方法。在深入研究 ES6 箭头函数如何解决它之前,让我们先研究一下。

通常你会在方法的内部函数之外创建一个变量。现在‘forEach’ 方法可以访问this 和object’s 属性及其值。

var Actor = {
  name: 'RajiniKanth',
  movies: ['Kabali', 'Sivaji', 'Baba'],
  showMovies: function() {
   var _this = this;
   this.movies.forEach(function(movie) {
     alert(_this.name + " has acted in " + movie);
   });
  }
};

Actor.showMovies();

使用bind 将引用该方法的this 关键字附加到method’s inner function。

var Actor = {
  name: 'RajiniKanth',
  movies: ['Kabali', 'Sivaji', 'Baba'],
  showMovies: function() {
   this.movies.forEach(function(movie) {
     alert(this.name + " has acted in " + movie);
   }.bind(this));
  }
};

Actor.showMovies();

现在有了 ES6 箭头函数,我们可以用更简单的方式处理词法作用域问题。

var Actor = {
  name: 'RajiniKanth',
  movies: ['Kabali', 'Sivaji', 'Baba'],
  showMovies: function() {
   this.movies.forEach((movie) => {
     alert(this.name + " has acted in " + movie);
   });
  }
};

Actor.showMovies();

箭头函数更像函数语句,除了它们绑定 this到父范围。如果 箭头函数在顶部范围内,this 参数将引用 窗口/全局范围,而常规函数内的箭头函数将具有它的 this参数与其外部函数相同。

使用箭头 函数this 在创建时绑定到封闭的范围,并且无法更改。 new 运算符、bind、call 和 apply 对此没有影响。

var asyncFunction = (param, callback) => {
  window.setTimeout(() => {
  callback(param);
  }, 1);
};

// With a traditional function if we don't control
// the context then can we lose control of `this`.
var o = {
  doSomething: function () {
  // Here we pass `o` into the async function,
  // expecting it back as `param`
  asyncFunction(o, function (param) {
  // We made a mistake of thinking `this` is
  // the instance of `o`.
  console.log('param === this?', param === this);
  });
  }
};

o.doSomething(); // param === this? false

在上面的例子中,我们失去了对此的控制。我们可以通过使用this的变量引用或使用bind来解决上面的例子。使用 ES6,管理this 变得更容易,因为它绑定到词法作用域。

var asyncFunction = (param, callback) => {
  window.setTimeout(() => {
  callback(param);
  }, 1);
};

var o = {
  doSomething: function () {
  // Here we pass `o` into the async function,
  // expecting it back as `param`.
  //
  // Because this arrow function is created within
  // the scope of `doSomething` it is bound to this
  // lexical scope.
  asyncFunction(o, (param) => {
  console.log('param === this?', param === this);
  });
  }
};

o.doSomething(); // param === this? true

何时不使用箭头函数

在对象字面量内。

var Actor = {
  name: 'RajiniKanth',
  movies: ['Kabali', 'Sivaji', 'Baba'],
  getName: () => {
     alert(this.name);
  }
};

Actor.getName();

Actor.getName 是用箭头函数定义的,但在调用时它会发出未定义警报,因为 this.name 是 undefined,因为上下文仍然是 window。

这是因为箭头函数在词法上将上下文与window object... 即外部作用域绑定在一起。执行this.name等价于window.name,未定义。

对象原型

在prototype object 上定义方法时适用相同的规则。而不是使用箭头函数来定义 sayCatName 方法,这会带来不正确的context window:

function Actor(name) {
  this.name = name;
}
Actor.prototype.getName = () => {
  console.log(this === window); // => true
  return this.name;
};
var act = new Actor('RajiniKanth');
act.getName(); // => undefined

调用构造函数

this 在构造调用中是新创建的对象。执行 new Fn() 时,constructor Fn 的上下文是一个新对象:this instanceof Fn === true。

this 是从封闭上下文设置的,即外部范围,使其不分配给新创建的对象。

var Message = (text) => {
  this.text = text;
};
// Throws "TypeError: Message is not a constructor"
var helloMessage = new Message('Hello World!');

动态上下文回调

箭头函数在声明时静态绑定context,并且不可能使其成为动态的。将事件侦听器附加到 DOM 元素是客户端编程中的常见任务。一个事件以 this 作为目标元素触发处理函数。

var button = document.getElementById('myButton');
button.addEventListener('click', () => {
  console.log(this === window); // => true
  this.innerHTML = 'Clicked button';
});

this 是在全局上下文中定义的箭头函数中的窗口。当单击事件发生时,浏览器会尝试使用按钮上下文调用处理函数,但箭头函数不会更改其预定义的上下文。 this.innerHTML 等价于window.innerHTML,没有意义。

你必须应用一个函数表达式,它允许根据目标元素改变它:

var button = document.getElementById('myButton');
button.addEventListener('click', function() {
  console.log(this === button); // => true
  this.innerHTML = 'Clicked button';
});

当用户单击按钮时,处理函数中的this 就是按钮。因此this.innerHTML = 'Clicked button' 正确地修改了按钮文本以反映点击状态。

参考文献

【讨论】:

  • 好吧,我必须承认,“最好的在中间”。赞成声明,箭头函数将不涵盖任何可能的函数用例。它们实际上只是为了解决一部分常见问题。完全切换到它们将是一种矫枉过正。
  • @DmitriPavlutin:查看我更新的帖子,它收集了很多东西......也许我应该发布一个参考。
  • “使用 bind 将引用方法的 this 关键字附加到方法的内部函数”行之后的代码。有错误。您是否测试了其余的示例?
  • using bind to attach the this keyword that refers to the method to the method’s inner function. 有语法错误。
  • 应该是var Actor = { name: 'RajiniKanth', movies: ['Kabali', 'Sivaji', 'Baba'], showMovies: function() { this.movies.forEach(function(movie){ alert(this.name + ' has acted in ' + movie); }.bind(this)) } }; Actor.showMovies();
【解决方案4】:

箭头函数 - 迄今为止使用最广泛的 ES6 功能 ...

用法:所有 ES5 函数都应替换为 ES6 箭头函数,以下情况除外:

箭头函数应该 不 被使用:

  1. 当我们想要函数提升时
    • 因为箭头函数是匿名的。
  2. 当我们想在函数中使用this/arguments
    • 由于箭头函数没有自己的this/arguments,它们依赖于它们的外部上下文。
  3. 当我们想使用命名函数时
    • 因为箭头函数是匿名的。
  4. 当我们想将函数用作constructor
    • 因为箭头函数没有自己的this。
  5. 当我们想在对象字面量中添加函数作为属性并在其中使用对象时
    • 因为我们无法访问this(应该是对象本身)。

让我们了解一些箭头函数的变体以便更好地理解:

变体 1:当我们想将多个参数传递给函数并从中返回一些值时。

ES5 版本:

var multiply = function (a, b) {
    return a*b;
};
console.log(multiply(5, 6)); // 30

ES6 版本:

var multiplyArrow = (a, b) => a*b;
console.log(multiplyArrow(5, 6)); // 30

注意:

function 关键字不是必需的。 =&gt; 是必需的。 {} 是可选的,当我们不提供 {} 时 return 由 JavaScript 隐式添加,当我们提供 {} 时,如果需要,我们需要添加 return。

变体 2:当我们想只向函数传递一个参数并从中返回一些值时。

ES5 版本:

var double = function(a) {
    return a*2;
};
console.log(double(2)); // 4

ES6 版本:

var doubleArrow  = a => a*2;
console.log(doubleArrow(2)); // 4

注意:

当只传递一个参数时,我们可以省略括号,()。

变体 3:当我们不想要将任何参数传递给函数并且不想要返回任何值时。 p>

ES5 版本:

var sayHello = function() {
    console.log("Hello");
};
sayHello(); // Hello

ES6 版本:

var sayHelloArrow = () => {console.log("sayHelloArrow");}
sayHelloArrow(); // sayHelloArrow

变体 4:当我们想要从箭头函数显式返回时。

ES6 版本:

var increment = x => {
  return x + 1;
};
console.log(increment(1)); // 2

变体 5:当我们想从箭头函数返回一个对象时。

ES6 版本:

var returnObject = () => ({a:5});
console.log(returnObject());

注意:

我们需要将对象括在括号中,()。否则,JavaScript 无法区分块和对象。

变体 6:箭头函数不有自己的arguments(类似对象的数组)。它们依赖于arguments 的外部上下文。

ES6 版本:

function foo() {
  var abc = i => arguments[0];
  console.log(abc(1));
};
foo(2); // 2

注意:

foo 是一个 ES5 函数,有一个类似对象的 arguments 数组,传递给它的参数是 2 所以 arguments[0] 对于 foo 是 2。

abc 是一个 ES6 箭头函数,因为它没有有自己的arguments。因此,它会打印 arguments[0] of foo 的外部上下文。

变体 7:箭头函数不拥有自己的 this,它们依赖于 this 的外部上下文

ES5 版本:

var obj5 = {
  greet: "Hi, Welcome ",
  greetUser : function(user) {
        setTimeout(function(){
        console.log(this.greet + ": " +  user); // "this" here is undefined.
        });
     }
};

obj5.greetUser("Katty"); //undefined: Katty

注意:

传递给 setTimeout 的回调是一个 ES5 函数,它有自己的 this,在 use-strict 环境中未定义。因此我们得到输出:

undefined: Katty

ES6 版本:

var obj6 = {
  greet: "Hi, Welcome ",
  greetUser : function(user) {
    setTimeout(() => console.log(this.greet + ": " +  user));
      // This here refers to outer context
   }
};

obj6.greetUser("Katty"); // Hi, Welcome: Katty

注意:

传递给setTimeout 的回调是一个ES6 箭头函数,它确实没有有自己的this,所以它从外部上下文greetUser 获取它,它有@987654367 @。那是obj6,因此我们得到了输出:

Hi, Welcome: Katty

其他:

  • 我们不能将new 与箭头函数一起使用。
  • 箭头函数不有prototype属性。
  • 当通过apply 或call 调用箭头函数时,我们确实没有绑定this。

【讨论】:

    【解决方案5】:

    我仍然坚持我在此线程中my first answer 中写的所有内容。但是,从那时起,我对代码风格的看法有所发展,因此我在上一个问题的基础上对这个问题有了新的答案。

    关于词法this

    在我的上一个回答中,我故意避开了我对这种语言的基本信念,因为它与我提出的论点没有直接关系。尽管如此,在没有明确说明的情况下,我可以理解为什么很多人在发现箭头非常有用时,会简单地拒绝我不使用箭头的建议。

    我的信念是:我们不应该首先使用this。因此,如果一个人故意避免在他的代码中使用this,那么箭头的“词汇this”特性就没有什么价值了。另外,在this是坏事的前提下,Arrow对this的处理也算不上什么“好事”;相反,它更像是针对另一个糟糕的语言功能的一种损害控制形式。

    我认为这要么不会发生在某些人身上,但即使对于那些发生过的人来说,他们也必须总是发现自己在代码库中工作,this 在每个文件中出现一百次,而且很少(或很多) ) 的损害控制是一个合理的人所希望的。因此,在某种意义上,箭可以使糟糕的情况变得更好。

    即使使用带有箭头的this 编写代码比不使用箭头更容易,使用箭头的规则仍然非常复杂(参见:当前线程)。因此,正如您所要求的那样,指导方针既不“清晰”也不“一致”。即使程序员知道箭头的歧义,我认为他们还是会耸耸肩接受它们,因为词法 this 的价值让他们黯然失色。

    所有这一切都是以下认识的前言:如果不使用this,那么箭头通常会导致的关于this 的歧义变得无关紧要。在这种情况下,箭头变得更加中性。

    关于简洁语法

    当我写第一个答案时,我认为即使是一味地坚持最佳实践也是值得付出的代价,如果这意味着我可以编写更完美的代码。但我最终意识到,简洁可以作为一种抽象形式,也可以提高代码质量——足以证明有时偏离最佳实践是合理的。

    换句话说:该死,我也想要单行函数!

    关于准则

    鉴于this-neutral 箭头函数的可能性,以及值得追求的简洁性,我提供以下更宽松的指导方针:

    ES6 中的函数表示法指南:

    • 不要使用this。
    • 对要按名称调用的函数使用函数声明(因为它们已被提升)。
    • 对回调使用箭头函数(因为它们往往更简洁)。

    【讨论】:

    • 100% 同意底部的“ES6 中函数表示法指南”部分 - 特别是对于提升和内联回调函数。很好的答案!
    【解决方案6】:

    除了迄今为止的出色答案之外,我想提出一个非常不同的原因,说明箭头函数在某种意义上从根本上优于“普通”JavaScript 函数。

    为了便于讨论,我们暂时假设我们使用了一个类型检查器,比如TypeScript 或 Facebook 的“Flow”。考虑以下玩具模块,它是有效的 ECMAScript 6 代码加上流类型注释(我将在此答案的末尾包含实际上由 Babel 产生的无类型代码,因此它实际上可以运行):

    export class C {
      n : number;
      f1: number => number;
      f2: number => number;
    
      constructor(){
        this.n = 42;
        this.f1 = (x:number) => x + this.n;
        this.f2 = function (x:number) { return  x + this.n;};
      }
    }

    现在看看当我们使用来自不同模块的类 C 时会发生什么,如下所示:

    let o = { f1: new C().f1, f2: new C().f2, n: "foo" };
    let n1: number = o.f1(1); // n1 = 43
    console.log(n1 === 43); // true
    let n2: number = o.f2(1); // n2 = "1foo"
    console.log(n2 === "1foo"); // true, not a string!

    如您所见,类型检查器失败:f2 应该返回一个数字,但它返回了一个字符串!

    更糟糕的是,似乎没有可以想象的类型检查器可以处理普通(非箭头)JavaScript函数,因为f2的“this”并没有出现在f2的参数列表中,所以需要“this”的类型不可能作为注释添加到 f2。

    这个问题是否也会影响不使用类型检查器的人?我认为是这样,因为即使我们没有静态类型,我们也会认为它们就在那里。 (“第一个参数必须是数字,第二个参数必须是字符串”等)隐藏的“this”参数可能会或可能不会在函数体中使用,这让我们的心理记录变得更加困难。

    这是可运行的无类型版本,将由 Babel 生成:

    class C {
        constructor() {
            this.n = 42;
            this.f1 = x => x + this.n;
            this.f2 = function (x) { return x + this.n; };
        }
    }
    
    let o = { f1: new C().f1, f2: new C().f2, n: "foo" };
    let n1 = o.f1(1); // n1 = 43
    console.log(n1 === 43); // true
    let n2 = o.f2(1); // n2 = "1foo"
    console.log(n2 === "1foo"); // true, not a string!

    【讨论】:

      【解决方案7】:

      我更喜欢在不需要访问本地 this 的任何时候都使用箭头函数,因为箭头函数 do not bind their own this, arguments, super, or new.target。

      【讨论】:

      • 是“超级”字面意思吗?
      【解决方案8】:

      箭头函数或lambdas,是在 ES 6 中引入的。除了它在最小语法上的优雅之外,最显着的函数区别在于 this 在箭头函数内

      在正则函数表达式中,this 关键字根据调用它的上下文绑定到不同的值。

      在箭头函数中,this 是词法绑定的,这意味着它从定义箭头函数的范围(父范围)关闭this ),并且无论在何处以及如何调用/调用它都不会改变。

      箭头函数作为对象方法的局限性

      // this = global Window
      let objA = {
        id: 10,
        name: "Simar",
        print () { // same as print: function()
          console.log(`[${this.id} -> ${this.name}]`);
        }
      }
      
      objA.print(); // logs: [10 -> Simar]
      
      objA = {
        id: 10,
        name: "Simar",
        print: () => {
          // Closes over this lexically (global Window)
          console.log(`[${this.id} -> ${this.name}]`);
        }
      };
      
      objA.print(); // logs: [undefined -> undefined]
      

      在objA.print() 的情况下,当print() 方法使用常规function 定义时,它通过将this 正确解析为objA 来进行方法调用,但在定义为箭头=&gt; 函数时失败。这是因为 this 在作为对象 (objA) 上的方法调用时,是对象本身。

      但是,在箭头函数的情况下,this 在词法上绑定到定义它的封闭范围的this(在我们的例子中是全局/窗口),并在调用过程中保持不变objA 上的方法。

      在对象的方法中,箭头函数优于常规函数,但仅当 this 预计在定义时被固定和绑定时。

      /* this = global | Window (enclosing scope) */
      
      let objB = {
        id: 20,
        name: "Paul",
        print () { // Same as print: function()
          setTimeout( function() {
            // Invoked async, not bound to objB
            console.log(`[${this.id} -> ${this.name}]`);
          }, 1)
        }
      };
      
      objB.print(); // Logs: [undefined -> undefined]'
      
      objB = {
        id: 20,
        name: "Paul",
        print () { // Same as print: function()
          setTimeout( () => {
            // Closes over bind to this from objB.print()
            console.log(`[${this.id} -> ${this.name}]`);
          }, 1)
        }
      };
      
      objB.print(); // Logs: [20 -> Paul]
      

      在 objB.print() 的情况下,print() 方法被定义为调用 console.log([${this.id} -> {this.name}]) 作为回调的函数setTimeout , this 在使用箭头函数作为回调时正确解析为 objB,但在将回调定义为常规函数时失败。

      这是因为箭头 =&gt; 函数,传递给 setTimeout(()=&gt;..),从其父级词法上关闭了 this,即定义它的 objB.print() 的调用。换句话说,箭头 =&gt; 函数传递到 setTimeout(()==&gt;... 绑定到 objB 作为其 this,因为 objB.print() this 的调用是 objB 本身。

      我们可以轻松地使用Function.prototype.bind() 使定义为常规函数的回调工作,方法是将其绑定到正确的this。

      const objB = {
        id: 20,
        name: "Singh",
        print () { // The same as print: function()
          setTimeout( (function() {
            console.log(`[${this.id} -> ${this.name}]`);
          }).bind(this), 1)
        }
      }
      
      objB.print() // logs: [20 -> Singh]
      

      但是,箭头函数派上用场,并且在异步回调的情况下更不容易出错,我们知道 this 在它获取并应该绑定到的函数定义时。

      this 需要跨调用更改的箭头函数的限制

      任何时候,我们都需要一个this可以在调用时改变的函数,我们不能使用箭头函数。

      /* this = global | Window (enclosing scope) */
      
      function print() {
        console.log(`[${this.id} -> {this.name}]`);
      }
      
      const obj1 = {
        id: 10,
        name: "Simar",
        print // The same as print: print
      };
      
      obj.print(); // Logs: [10 -> Simar]
      
      const obj2 = {
        id: 20,
        name: "Paul",
      };
      
      printObj2 = obj2.bind(obj2);
      printObj2(); // Logs: [20 -> Paul]
      print.call(obj2); // logs: [20 -> Paul]
      

      以上都不适用于箭头函数const print = () =&gt; { console.log([${this.id} -> {this.name}]);} 因为this 无法更改,并且将保持绑定到@987654370 @ 定义它的封闭范围(全局/窗口)。

      在所有这些示例中,我们使用不同的对象(obj1 和 obj2)一个接一个地调用相同的函数,这两个对象都是在声明 print() 函数之后创建的。

      这些都是人为的例子,但让我们考虑一些更真实的例子。如果我们必须编写类似于在 arrays 上工作的 reduce() 方法,我们再次不能将其定义为 lambda,因为它需要从调用上下文中推断出 this,即其所在的数组它被调用了。

      因此,构造函数 函数永远不能定义为箭头函数,因为构造函数的this 不能在其声明时设置。每次使用 new 关键字调用构造函数时,都会创建一个新对象,然后绑定到该特定调用。

      此外,当框架或系统接受稍后使用动态上下文 this 调用的回调函数时,我们不能再次使用箭头函数 this 可能需要在每次调用时更改。这种情况通常出现在 DOM 事件处理程序中。

      'use strict'
      var button = document.getElementById('button');
      
      button.addEventListener('click', function {
        // web-api invokes with this bound to current-target in DOM
        this.classList.toggle('on');
      });
      
      var button = document.getElementById('button');
      
      button.addEventListener('click', () => {
        // TypeError; 'use strict' -> no global this
        this.classList.toggle('on');
      });
      

      这也是为什么在 Angular 2+ 和 Vue.js 等框架中,模板组件绑定方法是常规函数/方法,如 this因为它们的调用由绑定函数的框架管理。 (Angular 使用 Zone.js 来管理调用视图模板绑定函数的异步上下文。)

      另一方面,在 React 中,当我们想将组件的方法作为事件处理程序传递时,例如 &lt;input onChange={this.handleOnchange} /&gt;,我们应该将 handleOnchanage = (event)=&gt; {this.props.onInputChange(event.target.value);} 定义为箭头函数,如每次调用。我们希望它与为渲染的 DOM 元素生成 JSX 的组件实例相同。


      这篇文章也可以在my Medium 出版物中找到。如果您喜欢这篇文章,或者有任何 cmets 和建议,请拍手或在Medium留下 cmets .

      【讨论】:

        【解决方案9】:

        简单来说,

        var a = 20; function a() {this.a = 10; console.log(a);}
        //20, since the context here is window.
        

        另一个例子:

        var a = 20;
        function ex(){
            this.a = 10;
            function inner(){
                console.log(this.a); // Can you guess the output of this line?
            }
            inner();
        }
        var test = new ex();
        

        Ans:控制台会打印 20。

        原因是每当执行一个函数时都会创建自己的堆栈,在此示例中,ex 函数使用 new 运算符执行,因此将创建一个上下文,并且当执行 inner 时,JavaScript 会创建一个新堆栈并在 global context 中执行 inner 函数,尽管存在本地上下文。

        所以,如果我们希望inner 函数有一个本地上下文,即ex,那么我们需要将上下文绑定到内部函数。

        Arrows 解决了这个问题。他们没有使用Global context,而是使用local context(如果有的话)。在*给出的示例中,它将把new ex() 当作this。

        因此,在所有显式绑定的情况下,箭头默认解决问题。

        【讨论】:

          猜你喜欢
          • 1970-01-01
          • 2022-01-19
          相关资源
          最近更新 更多