使用 () => {}
和 function () {}
,我们得到了两种非常相似的在 ES6 中编写函数的方法。在其他语言中,lambda 函数通常通过匿名来区分自己,但在 ECMAScript 中,任何函数都可以是匿名的。这两种类型中的每一种都有唯一的使用域(即当 this
需要显式绑定或显式不绑定时)。在这些域之间,有大量的情况可以使用任何一种表示法。
ES6 中的箭头函数至少有两个限制:
不要使用 new 并且在创建原型时不能使用
在初始化时修复了这个绑定到范围的问题
除了这两个限制之外,箭头函数理论上几乎可以在任何地方取代常规函数。在实践中使用它们的正确方法是什么?是否应该使用箭头函数,例如:
“它们工作的所有地方”,即函数不必对 this 变量不可知的任何地方,并且我们没有创建对象。
只有“需要它们的任何地方”,即需要绑定到某个范围的事件监听器、超时
具有“短”功能,但不具有“长”功能
仅适用于不包含另一个箭头函数的函数
我正在寻找在未来版本的 ECMAScript 中选择适当函数表示法的指南。该指南需要清晰,以便可以教授给团队中的开发人员,并且要保持一致,这样就不需要不断地从一个函数符号到另一个函数符号来回重构。
这个问题是针对那些在即将到来的 ECMAScript 6 (Harmony) 的上下文中考虑过代码风格并且已经使用过该语言的人。
Fixed this bound to scope at initialisation
是一种限制吗?
this
的值是您可以使用常规函数但不能使用箭头函数执行的操作。
Fixed this bound to scope at initialisation
是一个限制。 :) 看看这篇文章:exploringjs.com/es6/ch_arrow-functions.html
不久前,我们的团队将其所有代码(一个中型 AngularJS 应用程序)迁移到使用 Traceur Babel 编译的 JavaScript。我现在对 ES6 及更高版本中的函数使用以下经验法则:
在全局范围和 Object.prototype 属性中使用函数。
将类用于对象构造函数。
在其他任何地方使用 =>。
为什么几乎到处都使用箭头函数?
范围安全:当箭头函数被一致使用时,一切都保证使用相同的 thisObject 作为根。如果即使是单个标准函数回调与一堆箭头函数混合在一起,范围也可能会变得混乱。紧凑性:箭头函数更容易读写。 (这可能看起来很自以为是,所以我将进一步举几个例子。) 清晰性:当几乎所有东西都是箭头函数时,任何常规函数都会立即用于定义范围。开发人员总是可以查找下一个更高的函数语句以查看 thisObject 是什么。
为什么总是在全局范围或模块范围内使用常规函数?
指示不应访问 thisObject 的函数。窗口对象(全局范围)最好显式处理。许多 Object.prototype 定义存在于全局范围内(想想 String.prototype.truncate 等),而且它们通常必须是函数类型。在全局范围内一致地使用函数有助于避免错误。全局范围内的许多函数都是旧式类定义的对象构造函数。函数可以命名为 1。这有两个好处:(1) 编写function foo(){} 比编写 const foo = () => {} 更不尴尬——尤其是在其他函数调用之外。 (2) 函数名显示在堆栈跟踪中。虽然命名每个内部回调会很乏味,但命名所有公共函数可能是一个好主意。函数声明被提升,(意味着它们可以在声明之前被访问),这是静态实用函数中的一个有用属性。
对象构造函数
尝试实例化箭头函数会引发异常:
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
关键字。
笔记
命名箭头函数已在 ES6 规范中被推迟。他们可能仍会被添加到未来的版本中。根据规范草案,“类声明/表达式创建构造函数/原型对,与函数声明完全相同”,只要类不使用扩展关键字。一个小的区别是类声明是常量,而函数声明不是。关于单语句箭头函数中的块的注意事项:我喜欢在单独调用箭头函数的地方使用块(例如,赋值)。这样一来,很明显可以丢弃返回值。
根据the proposal,箭头旨在“解决和解决传统函数表达式的几个常见痛点”。他们打算通过在词法上绑定 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(昨天下载量约为 120k)通过 this 公开了其测试方法。
Grunt(昨天下载了约 63k 次)通过它公开了构建任务的方法。
Backbone(昨天下载了约 22k 次)定义了访问它的方法。
事件 API(如 DOM)通过 this 引用 EventTarget。
修补或扩展的原型 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
虽然,如果写 (() => doSomething()());
的目的是编写一个立即调用的函数表达式,那么什么都不会发生。
考虑到上述所有情况,很难说箭头函数“更容易理解”。人们可以学习使用这种语法所需的所有特殊规则。是不是真的值得吗?
function
的语法非常通用。仅使用 function
意味着语言本身可以防止编写令人困惑的代码。要编写在所有情况下都应在句法上理解的过程,我选择 function
。
关于指导方针
您要求的指南需要“清晰”和“一致”。使用箭头函数最终将导致语法有效、逻辑无效的代码,两种函数形式相互交织,有意义且任意。因此,我提供以下内容:
ES6 中的函数表示法指南:
始终创建具有功能的程序。
始终将其分配给变量。不要使用 () => {}。
self
而不是 this 的吸引力。您陈述的箭头函数陷阱也都是有效的,并且与其他可以不使用大括号的语句相同的标准也绝对适用于此;否则,我认为根据您的论点,人们也可以在任何地方提倡箭头功能。
this
的奇怪行为是 Javascript 的问题。 this
不应被隐式绑定,而应作为显式参数传递。
创建 Arrow functions 是为了简化函数 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
内会怎样?
这里指的是 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参数与其外部函数相同。
arrow 函数 this
在创建时绑定到封闭的 scope 并且不能更改。 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'
正确地修改了按钮文本以反映点击状态。
参考
何时“不”使用箭头功能
using bind to attach the this keyword that refers to the method to the method’s inner function.
有语法错误。
addEventListener
,即使使用箭头函数,您仍然可以使用 event.target
获取点击范围。想法?
箭头函数 - 迄今为止使用最广泛的 ES6 特性......
用法:所有 ES5 函数都应替换为 ES6 箭头函数,以下情况除外:
不应使用箭头函数:
当我们想要函数提升时,箭头函数是匿名的。当我们想在函数中使用 this/arguments 作为箭头函数没有自己的 this/arguments 时,它们取决于它们的外部上下文。当我们想使用命名函数作为箭头函数是匿名的。当我们想使用函数作为构造函数时,箭头函数没有自己的this。当我们想在对象字面量中添加函数作为属性并在其中使用对象时,我们无法访问 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
,因此 foo
的 arguments[0]
是 2。
abc
是一个 ES6 箭头函数,因为它没有有自己的 arguments
。因此,它改为打印 foo
的 arguments[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 与箭头函数一起使用。
箭头函数没有原型属性。
当通过 apply 或 call 调用箭头函数时,我们没有 this 的绑定。
我仍然坚持我在此线程中 my first answer 中写的所有内容。但是,从那时起,我对代码风格的看法有所发展,所以我对这个问题有了一个新的答案,这个答案建立在我上一个问题的基础上。
关于词汇this
在我的上一个回答中,我故意避开了我对这种语言的基本信念,因为它与我提出的论点没有直接关系。尽管如此,在没有明确说明的情况下,我可以理解为什么许多人在发现箭头非常有用时,会简单地拒绝我不使用箭头的建议。
我的信念是:我们不应该首先使用 this
。因此,如果一个人故意避免在他的代码中使用 this
,那么箭头的“词法 this
”特性就没有什么价值了。此外,在 this
是坏事的前提下,arrow 对 this
的处理也不是“好事”;相反,它更像是对另一个糟糕的语言功能的一种损害控制形式。
我认为这要么不会发生在某些人身上,但即使对那些发生过的人来说,他们也必须总是发现自己在代码库中工作,其中 this
每个文件出现一百次,并且有一点(或很多)损坏控制是一个理性的人所能期望的。因此,在某种意义上,箭可以使糟糕的情况变得更好。
即使使用带有箭头的 this
编写代码比不使用箭头更容易,使用箭头的规则仍然非常复杂(参见:当前线程)。因此,如您所要求的,指导方针既不“清晰”也不“一致”。即使程序员知道箭头的歧义,我认为他们仍然耸耸肩并接受它们,因为词法 this
的价值掩盖了它们。
所有这一切都是以下认识的序言:如果不使用 this
,那么箭头通常会导致的关于 this
的歧义变得无关紧要。在这种情况下,箭头变得更加中性。
关于简洁的语法
当我写第一个答案时,我认为即使是一味地坚持最佳实践也是值得付出的代价,如果这意味着我可以编写更完美的代码。但我最终意识到,简洁可以作为一种抽象形式,也可以提高代码质量——足以证明有时偏离最佳实践是合理的。
换句话说:该死,我也想要单线函数!
关于指导方针
考虑到 this
-neutral 箭头函数的可能性,以及值得追求的简洁性,我提供以下更宽松的指导方针:
ES6 中的函数表示法指南:
不要使用这个。
对按名称调用的函数使用函数声明(因为它们已被提升)。
使用箭头函数进行回调(因为它们往往更简洁)。
除了到目前为止的出色答案之外,我还想提出一个非常不同的原因,为什么箭头函数在某种意义上从根本上优于“普通”JavaScript 函数。
为了便于讨论,让我们暂时假设我们使用像 TypeScript 或 Facebook 的“Flow”这样的类型检查器。考虑以下玩具模块,它是有效的 ECMAScript 6 代码加上 Flow 类型注释(我将在此答案的末尾包含实际上由 Babel 产生的无类型代码,因此它实际上可以运行):
导出类 C { n : number; f1:数字=>数字; f2:数字=>数字;构造函数(){ this.n = 42; this.f1 = (x:number) => x + this.n; this.f2 = function (x:number) { return x + this.n;}; } }
现在看看当我们使用来自不同模块的类 C 时会发生什么,如下所示:
让 o = { f1: 新 C().f1, f2: 新 C().f2, n: "foo" };让 n1: number = o.f1(1); // n1 = 43 console.log(n1 === 43); // true let n2: number = o.f2(1); // n2 = "1foo" console.log(n2 === "1foo"); // 是的,不是字符串!
如您所见,类型检查器在这里失败: f2 应该返回一个数字,但它返回了一个字符串!
更糟糕的是,似乎没有任何可以想象的类型检查器可以处理普通(非箭头)JavaScript函数,因为f2的“this”没有出现在f2的参数列表中,所以不可能添加“this”所需的类型作为 f2 的注释。
这个问题是否也会影响不使用类型检查器的人?我认为是这样,因为即使我们没有静态类型,我们也会认为它们就在那里。 (“第一个参数必须是数字,第二个参数必须是字符串”等)隐藏的“this”参数可能会或可能不会在函数体中使用,这使我们的心理簿记变得更加困难。
这是可运行的无类型版本,将由 Babel 生成:
类 C { 构造函数() { this.n = 42; this.f1 = x => x + this.n; this.f2 = function (x) { return x + this.n; }; } } 让 o = { f1: 新 C().f1, f2: 新 C().f2, n: "foo" };让 n1 = o.f1(1); // n1 = 43 console.log(n1 === 43); // true let n2 = o.f2(1); // n2 = "1foo" console.log(n2 === "1foo"); // 是的,不是字符串!
我更喜欢在不需要访问本地 this
的任何时候都使用箭头函数,因为箭头函数 do not bind their own this, arguments, super, or new.target。
箭头函数或 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
来进行方法调用,但在定义为箭头=>
函数时失败。这是因为当作为对象 (objA
) 的方法调用时,常规函数中的 this
是对象本身。
但是,在箭头函数的情况下,this
会在词法上绑定到定义它的封闭范围的 this
(在我们的例子中是全局 / Window),并在调用期间保持它作为 { 3}。
与对象方法中的常规函数相比,箭头函数具有优势,但仅当 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
,但当回调定义为时失败作为常规功能。
这是因为传递给 setTimeout(()=>..)
的箭头 =>
函数从其父级词法上关闭了 this
,即调用定义它的 objB.print()
。换句话说,传递给 setTimeout(()==>...
的箭头 =>
函数绑定到 objB
作为它的 this
,因为 objB.print()
this
的调用是 objB
本身。
通过将 Function.prototype.bind()
绑定到正确的 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 = 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.id} -> {this.name}]);}
作为 this
无法更改,并将保持绑定到定义它的封闭范围的 this
(全局/窗口)。
在所有这些示例中,我们使用不同的对象(obj1
和 obj2
)一个接一个地调用相同的函数,这两个对象都是在声明 print()
函数之后创建的。
这些都是人为的例子,但让我们考虑一些更真实的例子。如果我们必须编写类似于在 arrays
上工作的 reduce()
方法,我们再次不能将其定义为 lambda,因为它需要从调用上下文推断 this
,即在其上的数组它被调用了。
因此,constructor 函数永远不能定义为箭头函数,因为构造函数的 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 中,当我们想将组件的方法作为事件处理程序传递时,例如 <input onChange={this.handleOnchange} />
,我们应该将 handleOnchanage = (event)=> {this.props.onInputChange(event.target.value);}
定义为箭头函数,就像每次调用一样。我们希望它与为渲染的 DOM 元素生成 JSX 的组件实例相同。
这篇文章也可以在 my Medium 出版物中找到。如果您喜欢这篇文章,或有任何意见和建议,请在 Medium 上鼓掌或留下评论 .
以一种简单的方式,
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
。
因此,在绑定是显式的所有情况下,箭头默认解决问题。
function
的其他时间是当您不希望this
被绑定时,对吗?我最常见的场景是事件,您可能希望this
引用触发事件的对象(通常是 DOM 节点)。=>
看起来会更好。我怀疑非程序员对这两个示例的感受会大不相同。如果您正在编写 ES2016 代码,通常也不会使用这么多箭头函数。在此示例中,使用 async/await 和数组推导式,您最终会在reduce()
调用中得到一个箭头函数。