什么时候我应该在ECMAScript 6中使用箭头函数?

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

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

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

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

  • “在它们工作的任何地方”,即在函数不必对this变量不可知的任何地方,并且我们不是在创建对象。
  • 只有“需要它们的所有地方”,即事件监听器,超时,需要被绑定到某个范围
  • “短”函数,而不是“长”函数
  • 仅适用于不包含另一个箭头函数的函数

我正在寻找在未来版本的ECMAScript中选择适当的函数符号的指导方针。指导原则需要明确,以便可以在团队中教授给开发人员,并保持一致,以便不需要在一个函数符号和另一个函数符号之间来回不断地重构。

这个问题是针对那些在即将到来的ECMAScript 6 (Harmony)环境中思考过代码风格的人,以及已经使用过该语言的人。

137493 次浏览

不久前,我们的团队将所有代码(一个中等大小的AngularJS应用程序)迁移到使用Traceur 巴别塔编译的JavaScript。我现在对ES6及以上的函数使用以下的经验法则:

  • 在全局作用域和Object.prototype属性中使用function
  • 使用class作为对象构造函数。
  • 在其他地方使用=>

为什么几乎所有地方都使用箭头函数?

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

为什么总是在全局作用域或模块作用域中使用常规函数?

  1. 指示一个不应访问thisObject. xml文件的函数。
  2. window对象(全局作用域)最好显式寻址。
  3. 许多Object.prototype定义存在于全局作用域中(例如String.prototype.truncate等),而且这些定义通常必须为function类型。在全局作用域中一致地使用function有助于避免错误。
  4. 全局作用域中的许多函数都是旧式类定义的对象构造函数。
  5. 函数可以命名为__abc2。这样做有两个好处:(1)写__abc0比写const foo = () => {}不那么尴尬——尤其是在其他函数调用之外。(2)函数名显示在堆栈跟踪中。尽管为每个内部回调函数命名很乏味,但为所有公共函数命名可能是个好主意。
  6. 函数声明是< em > < / em >升起,(意味着它们可以在声明之前被访问),这是静态实用函数中的一个有用属性。

对象构造函数

尝试实例化箭头函数会抛出异常:

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

因此,函数相对于箭头函数的一个关键优势是函数可以同时用作对象构造函数:

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

然而,功能相同的__abc0 ECMAScript Harmony 类定义草案几乎同样紧凑:

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

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

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

箭头函数的可读性

坚持使用规则函数的最好理由可能是,箭头函数的可读性比规则函数差。如果你的代码从一开始就没有功能性,那么箭头函数似乎就没有必要了,当箭头函数没有被一致使用时,它们看起来就很难看。

自从ECMAScript 5.1给了我们函数式Array.forEachArray.map和所有这些函数式编程特性以来,ECMAScript已经发生了相当大的变化,这些特性让我们在以前使用循环的地方使用函数。异步JavaScript已经得到了很大的发展。ES6还将提供Promise对象,这意味着更多的匿名函数。函数式编程没有回头路。在函数式JavaScript中,箭头函数比常规函数更可取。

以代码__abc0为例(特别令人困惑):

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. 已命名的箭头函数已经在ES6规范中延迟。在未来的版本中可能还会添加这些功能。
  2. 根据规范草案,类声明/表达式创建的构造函数函数/原型对与函数声明完全相同。只要类不使用extend关键字。一个微小的区别是类声明是常量,而函数声明不是。
  3. 注意单语句箭头函数中的块:我喜欢在箭头函数单独用于副作用的地方使用块(例如,赋值)。这样,返回值显然可以被丢弃。

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

为了让each动态绑定this,我们必须使用function。这里不能用箭头函数。

处理多个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一样)引用带有thisEventTarget
  • 被修补或扩展的原型的 api引用带有this的实例。

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

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

有时需要词法this,但有时不需要;就像动态this有时是被期望的,但有时不是。值得庆幸的是,有一种更好的方法,它总是生成并传递预期的绑定。

关于简洁的语法

箭头函数成功地提供了“更短的句法形式”;的功能。但这些更短的职位会让你更成功吗?

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

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

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

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

有人可能会说,“如果可能的话,对于短函数只使用短版本如何?”但现在,一个风格规则与语言约束相矛盾:“尽量使用最短的函数符号,记住,有时只有最长的符号才能像预期的那样绑定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);
};

rest形参可以解析为展开运算符:

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

不过,如果编写(() => doSomething()());的目的是编写一个立即调用的函数表达式,则根本不会发生任何事情。

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

function的语法是非常一般化的。专门使用function意味着语言本身可以防止编写令人困惑的代码。要编写在所有情况下都能从语法上理解的过程,我选择function

关于指导方针

你要求一个需要“清晰”的指导方针。和“一致!”使用箭头函数最终会导致语法有效,逻辑无效的代码,两种函数形式交织在一起,有意义且任意。因此,我提出以下建议:

ES6函数表示法指南:

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

创建箭头功能是为了简化函数scope,并通过使其更简单来解决this关键字。它们使用=>语法,看起来像一个箭头。

备注:不替换已有功能。如果你用箭头函数替换所有的函数语法,它不会在所有情况下都有效。

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

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

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

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中呢?

这里this指的是window object而不是inner function,因为它是从scope中掉出来的。因为this总是引用它所在函数的所有者,在这种情况下——因为它现在超出了作用域——是window/global对象。

当它在object的方法内部时,function的所有者就是对象。因此关键字被绑定到对象上。然而,当它位于函数内部时,无论是单独存在还是在另一个方法中,它总是引用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. xml文件中。

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的arrow函数,我们可以用更简单的方式处理词法作用域问题。

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在创建时被绑定到外围的范围,并且不能被更改。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

在上面的例子中,我们失去了对它的控制。我们可以通过使用thisbind变量引用来解决上面的例子。在ES6中,当this绑定到词法作用域时,管理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.nameundefined,因为上下文仍然是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元素是客户端编程中的常见任务。事件以此作为目标元素触发处理程序函数。

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'正确地修改按钮文本以反映被单击的状态。

参考文献

除了到目前为止最棒的答案之外,我还想提出一个非常不同的理由,为什么箭头函数在某种意义上从根本上优于“普通”函数。JavaScript函数。

为了便于讨论,让我们暂时假设我们使用类似打印稿或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函数,因为"this"的f2不会出现在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!

我更喜欢在任何不需要访问本地this的时候使用箭头函数,因为箭头函数不绑定自己的this,参数,super,或new.target. c。

简单来说,

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

答:控制台会打印20个。

原因是无论何时函数被执行,它自己的堆栈都会被创建,在这个例子中,ex函数是用new操作符执行的,因此将创建一个上下文,当inner被执行时,JavaScript将创建一个新的堆栈并在global context中执行inner函数,尽管有一个本地上下文。

因此,如果我们想要inner函数有一个局部上下文,即ex,那么我们需要将上下文绑定到内部函数。

箭头可以解决这个问题。他们不取Global context,而是取local context(如果存在的话)。在*给出的例子中,它将new ex()作为this

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

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

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

__abc0 __abc1 __abc2

  1. 当我们想要函数提升时
    • 因为箭头函数是匿名的。
  2. 当我们想在函数中使用this/arguments
    • 由于箭头函数本身没有this/arguments,它们依赖于它们的外部上下文。
  3. 当我们想使用命名函数时
    • 因为箭头函数是匿名的。
  4. 当我们想使用函数作为constructor
    • 因为箭头函数没有自己的this
  5. 当我们想在object literal中添加function作为属性并在其中使用object时
    • 因为我们不能访问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关键字是必需的。 =>是必需的。 {}是可选的,当我们不提供{}时,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:当我们做想要将任何参数传递给函数,做想要返回任何值时。

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,因此fooarguments[0]为2。

abc是ES6的一个箭头函数,因为它的有自己的arguments。因此,它将输出fooarguments[0]作为它的外部上下文。

变体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中获取它,该上下文具有this。它是obj6,因此我们得到输出:

Hi, Welcome: Katty

杂项:

  • 不能将new用于箭头函数。
  • 箭头函数具有prototype属性。
  • 当通过applycall调用箭头函数时,确实绑定了this

箭头函数或λ在es6中引入。除了在最小语法上的优雅,最著名的函数差异是作用域 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来进行方法调用,但当定义为arrow=>函数时失败。这是因为在常规函数中,当作为对象的方法(objA)调用时,this就是对象本身。

然而,对于箭头函数,this在词法上绑定到定义它的外围作用域的this(在我们的例子中是global / Window),并在作为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. this. c]的函数。id}→{this.name}])异步地作为setTimeout的回调,当箭头函数被用作回调时,this正确地解析为objB,但当回调被定义为常规函数时失败。

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

通过将回调函数绑定到正确的this,可以很容易地使用Function.prototype.bind()使定义为常规函数的回调工作。

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 = () => { console.log([${this. this]。id}→{this.name}]);}作为this不能被更改,并且将始终绑定到定义它的封闭作用域的this (global / Window)。

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

这些都是虚构的例子,但让我们考虑一些更真实的例子。如果我们必须将reduce()方法编写为类似于在arrays上工作的方法,我们同样不能将其定义为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');
});

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

另一方面,在反应中,当我们想要将组件的方法作为事件处理程序传递时,例如,<input onChange={this.handleOnchange} />,我们应该将handleOnchanage = (event)=> {this.props.onInputChange(event.target.value);}定义为每次调用的箭头函数。我们希望这是为呈现的DOM元素生成JSX的组件的同一个实例。


本文也可在我的媒体出版物中找到。如果你喜欢这篇文章,或者有任何意见和建议,请< em > < / em >鼓掌或在媒介上留下< em > < / em >的评论

我仍然坚持我在这个线程中我的第一个回答中所写的一切。然而,从那时起,我对代码风格的看法有所发展,所以我对这个问题有了一个新的答案,它建立在我上一个问题的基础上。

关于词法this

在我最后的回答中,我故意回避了我对这种语言的潜在信念,因为它与我所做的论点没有直接关系。尽管如此,如果没有明确地说明这一点,我可以理解为什么许多人在发现箭头如此有用时,只是拒绝我的不使用箭头的建议。

我的信念是:我们不应该首先使用this。因此,如果一个人故意避免在他的代码中使用this,那么箭头的“词法this”特性几乎没有价值。同样,在this是坏事的前提下,arrow对this的处理并不是一件“好事”;相反,它更像是另一种糟糕语言特性的损害控制形式。

我认为这要么不会发生在一些人身上,但即使对那些发生这种情况的人来说,他们必须总是发现自己在每个文件this出现100次的代码库中工作,并且一点(或很多)损害控制是一个合理的人所能希望的。所以箭头在某种程度上是好的,当它们让糟糕的情况变得更好的时候。

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

所有这些都是以下实现的序言:如果不使用this,则箭头通常导致的this的模糊性变得无关紧要。在这种情况下,箭头变得更加中性。

关于简洁的语法

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

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

关于指导方针

鉴于__abc0中立箭头函数的可能性,以及简洁性值得追求,我提供了以下更宽松的指导原则:

ES6函数表示法指南:

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