什么时候应该在ECMAScript 6中使用箭头function?
这个问题针对的是那些在即将到来的ECMAScript 6(Harmony)中已经考虑过代码风格并且已经使用过该语言的人。
有了() => {}
和function () {}
我们得到了两种非常类似的方式来在ES6中编写函数。 在其他语言中,lambda函数通常是匿名的,但在ECMAScript中,任何函数都可以是匿名的。 这两种types中的每一种都具有唯一的使用领域(即,当this
需要显式地绑定或明确地不被绑定时)。 在这些领域之间有很多情况下,任何符号都可以做。
ES6中的箭头function至less有两个限制:
- 不要与
new
工作 - 修正了初始化时的范围限制
除了这两个限制外,箭头函数在理论上可以在几乎任何地方取代常规函数 在实践中使用它们的正确方法是什么? 如果使用箭头function,例如:
- “在任何地方工作”,即在任何地方,函数不必对
this
variables不可知,我们也不是创build一个对象。 - 只有“需要的地方”,即事件监听器,超时,需要绑定到某个范围
- 具有“短”function但不具有“长”function
- 仅限于不包含其他箭头function的function
我正在寻找的是在ECMAScript的未来版本中select适当的函数表示法的指南。 指导方针必须清楚,以便可以教给团队的开发人员,并保持一致,以便不需要不断地从一个函数表示法到另一个函数表示法的重构。
前一段时间,我们的团队将所有代码(一个中等大小的AngularJS应用程序)迁移到使用Traceur Babel编译的JavaScript中。 我现在使用下面的经验法则ES6和更高版本的function:
- 在全局范围和
Object.prototype
属性中使用function
。 - 使用对象构造函数的
class
。 - 在其他地方使用
=>
。
为什么几乎到处都使用箭头function
- 范围安全:当箭头函数一致使用时,一切都保证使用相同的
thisObject
作为根。 如果一个标准的函数callback和一堆箭头函数混合在一起,那么范围就会变得混乱。 - 紧凑性:箭头function更容易读取和写入。 (这看起来可能有些opinion持,所以我会再举几个例子)。
- 清晰度:当几乎所有东西都是箭头函数时,任何常规
function
立即伸出来定义范围。 开发人员总是可以查找下一个更高级的function
语句来查看thisObject
是什么。
为什么总是在全局范围或模块范围上使用常规函数?
- 指示不应该访问
thisObject
的函数。 -
window
对象(全局范围)最好是明确地解决。 - 许多
Object.prototype
定义存在于全局范围内(认为是String.prototype.truncate
等),而且这些定义通常必须是function
的types。 始终在全局范围内使用function
有助于避免错误。 - 全局范围内的许多函数都是旧式类定义的对象构造函数。
- 函数可以被命名为1 。 这有两个好处:(1)写
function foo(){}
比const foo = () => {}
更为尴尬 – 特别是在其他函数调用之外。 (2)函数名称显示在堆栈轨迹中。 虽然命名每个内部callback将是乏味的,但命名所有公共职能可能是一个好主意。 - 函数声明被挂起 (意味着它们可以在声明之前被访问),这是静态实用函数中的一个有用的属性。
对象构造函数
试图实例化一个箭头函数会引发一个exception:
var x = () => {}; new x(); // TypeError: x is not a constructor
与箭头函数相比,函数的一个关键优势是函数可以作为对象构造函数使用:
function Person(name) { this.name = name; }
然而,function相同的2 ES Harmony 草案类定义几乎一样紧凑:
class Person { constructor(name) { this.name = name; } }
我期望使用前面的符号最终会被阻止。 简单的匿名对象工厂可能仍然会使用对象构造函数表示法,其中对象是以编程方式生成的,但对其他方面来说则不是这样。
在需要对象构造函数的地方,应该考虑将函数转换为如上所示的class
。 该语法也适用于匿名函数/类。
箭头function的可读性
坚持常规function的可能最好的论据 – 范围的安全性是可恶的 – 会是箭头function比常规function更不可读。 如果你的代码在一开始并不起作用,那么箭头函数可能不是必须的,而且当箭头函数没有被一致地使用时,它们看起来很丑。
ECMAScript发生了很大的变化,因为ECMAScript 5.1为我们提供了函数Array.forEach
, Array.map
和所有这些函数编程特性,使得我们可以使用for循环之前使用的函数。 asynchronousJavaScript已经取得了相当大的进展。 ES6还将发布一个Promise
对象,这意味着更多的匿名函数。 function编程没有回头路可走。 在function性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的同一段代码:
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)); }
虽然任何一个箭头函数都可以用标准函数来替代,但是这样做可以获得很less的收益。 哪个版本更具可读性? 我会说第一个。
我认为,使用箭头函数还是常规函数的问题随着时间的推移会变得不那么重要。 大多数函数将会变成类方法,这会删除function
关键字,否则它们将成为类。 函数将继续用于通过Object.prototype
修补类。 同时,我build议保留function
关键字的任何应该真的是类方法或类。
笔记
- 命名的箭头函数在ES6规范中被推迟了 。 他们可能还会添加未来的版本。
- 根据规范草案“类声明/expression式创build一个构造函数/原型对完全一样的函数声明” ,只要一个类不使用
extend
关键字。 一个小的区别是类声明是常量,而函数声明不是。 - 注意单个语句中的块箭头函数:我喜欢在任何一个只为副作用调用箭头函数(例如赋值)的地方使用块。 这样很显然,返回值可以被丢弃。
根据提案 ,箭头旨在“解决和解决传统Function Expression
几个常见问题”。 他们打算通过词法绑定和提供简洁的语法来改善问题。
然而,
- 人们不能一致地把
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); };
箭头函数打算解决这个问题,我们需要在callback中访问这个属性。 已经有几种方法可以做到这一点:可以将this
分配给一个variables,使用bind
,或者使用Array
聚合方法中可用的第三个参数。 然而,箭头似乎是最简单的解决方法,所以方法可以像这样重构:
this.pages.forEach(page => page.draw(this.settings));
不过,考虑一下代码是否使用了像jQuery这样的库,它们的方法专门用来绑定this
库。 现在有两个值要处理:
Book.prototype.render = function () { var book = this; this.$pages.each(function (index) { var $page = $(this); book.draw(book.currentPage + index, $page); }); };
我们必须使用function
,以便each
dynamic地绑定this
。 我们不能在这里使用箭头function。
处理多个this
值也可能是令人困惑的,因为很难知道this
作者是在说什么:
function Reader() { this.book.on('change', function () { this.reformat(); }); }
作者是否打算打电话给Book.prototype.reformat
? 或者他忘了绑定this
,打算打电话给Reader.prototype.reformat
? 如果我们将处理程序更改为箭头函数,我们也会同样想知道作者是否想要dynamic的,但是select了一个箭头,因为它符合一行:
function Reader() { this.book.on('change', () => this.reformat()); }
有人可能会这样说:“箭是有时候是错误的function吗?也许如果我们很less需要dynamic的this
值的话,大多数时候使用箭头也是可以的。
但是问一下自己:“debugging代码是否值得,并且发现错误的结果是由一个”边缘案例“引起的?”“我宁愿避免大部分时间的麻烦,但是100%的时间。
有一个更好的方法:总是使用function
(所以this
总是可以dynamic地绑定),并且总是通过一个variables来引用this
。 variables是词汇和假设许多名字。 this
分配给variables将使您的意图清晰:
function Reader() { var reader = this; reader.book.on('change', function () { var book = this; book.reformat(); reader.reformat(); }); }
此外, 总是把this
分配给一个variables(即使有一个this
或没有其他的function),即使在代码改变之后,也可以保证其意图保持清晰。
此外,dynamicthis
并不是例外。 jQuery在超过5000万个网站上使用(截至2016年2月的这篇文章)。 以下是其他APIdynamic绑定的this
:
- 摩卡(昨天下载约120K)通过
this
testing公开了testing方法。 - Grunt(昨天约63k下载)通过
this
暴露了构build任务的方法。 - 骨干(昨天下载22K)定义了访问
this
方法。 - 事件API(如DOM)使用
this
引用EventTarget
。 - 修补或扩展的原型API引用具有
this
实例。
(通过http://trends.builtwith.com/javascript/jQuery和https://www.npmjs.com统计。);
你可能需要dynamic的this
绑定已经。
this
有时是一个词汇预期,但有时候不是; 就像有时预期的那样,但有时候不是。 值得庆幸的是,还有一个更好的方法,它总是产生和传达预期的约束。
关于简洁的语法
箭头函数成功地为函数提供了一个“较短的语法forms”。 但是这些较短的function会让你更成功吗?
x => x * x
“比function (x) { return x * x; }
更容易阅读” function (x) { return x * x; }
function (x) { return x * x; }
? 也许是这样,因为它更可能产生一个短的代码行。 对戴森来说,阅读速度和线条长度对阅读效果的影响 ,
中等长度的行(每行55个字符)似乎支持正常和快速的有效读取。 这产生了最高水平的理解。 。 。
对条件(三元)运算符和单行if
语句进行类似的certificate。
但是,你真的在写 这个提议中的简单math函数吗? 我的领域不是math的,所以我的子程序很less如此优雅。 相反,我通常会看到箭头函数打破了列限制,并且由于编辑器或样式指南而换行到另一行,从而使戴森的定义使“可读性”无效。
有人可能会这样说:“如果可能的话,使用简短版本的短版本怎么样?” 但是现在一个文体规则与语言约束相矛盾:“试着尽可能使用最短的函数符号,记住有时候只有最长的符号才会如预期的那样绑定this
符号。 这种混合使箭头特别容易被误用。
箭头函数语法有许多问题:
const a = x => doSomething(x); const b = x => doSomething(x); doSomethingElse(x);
这两个函数在语法上都是有效的。 但是doSomethingElse(x);
不在b
的身上,它只是一个不好的,顶级的陈述。
当扩展到块forms时,不再有隐式return
,哪一个可以忘记恢复。 但是这个expression可能只是为了产生一个副作用,所以谁知道是否需要明确的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参数可以被parsing为扩展运算符:
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 parens
块看起来像对象:
(id) => id // Returns `id` (id) => {name: id} // Returns `undefined` (it's a labeled statement) (id) => ({name: id}) // Returns an object
这是什么意思?
() => {}
作者是否打算创build一个no-op,或返回一个空对象的函数? (考虑到这一点,我们是否应该把它放在{
after =>
?我们是否应该仅限于expression式语法?这将进一步减less箭头的频率。)
=>
看起来像<=
和>=
:
x => 1 ? 2 : 3 x <= 1 ? 2 : 3 if (x => 1) {} if (x >= 1) {}
要立即调用箭头函数expression式,必须在外部放置()
,但放置()
在内部是有效的,可能是故意的。
(() => doSomething()()) // Creates function calling value of `doSomething()` (() => doSomething())() // Calls the arrow function
虽然,如果写(() => doSomething()());
为了编写一个立即调用的函数expression式,根本不会发生任何事情。
考虑到上述所有情况,很难说箭头函数“更容易理解”。 人们可以学习使用这种语法所需的所有特殊规则。 是不是真的值得吗?
function
的语法是无法一概而论的。 单独使用function
意味着语言本身可以防止编写混淆的代码。 为了编写在所有情况下应该被语法理解的程序,我selectfunction
。
关于指导方针
你要求一个需要“清晰”和“一致”的指导方针。 使用箭头函数最终将导致语法有效,逻辑无效的代码,两个函数forms交织在一起,有意义和任意。 因此,我提供以下内容:
ES6function符号指南:
- 总是用
function
创build程序。 - 始终将其分配给一个variables。 不要使用
() => {}
。
创build箭头函数是为了简化函数scope
并通过简化this
关键字来解决this
问题。 他们利用=>
语法,看起来像一个箭头。
注意:它并不取代现有的function。 如果用箭头函数replace每个函数的语法,它将不会在所有情况下工作。
让我们来看看现有的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
的所有者就是对象。 因此,这个关键字绑定到对象。 然而,当它在一个函数内部时,无论是独立的还是在另一个方法中,它总是会引用window/global
对象。
var fn = function(){ alert(this); } fn(); // [object Window]
在我们的ES5
本身中有办法解决这个问题,让我们来看看在深入ES6的箭头function之前如何解决它。
通常情况下,您可以在方法的内部函数之外创build一个variables。 现在, '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
箭头函数,我们可以更简单地处理lexical scoping
问题。
var Actor = { name: 'RajiniKanth', movies: ['Kabali', 'Sivaji', 'Baba'], showMovies: function() { this.movies.forEach((movie) => { alert(this.name + " has acted in " + movie); }); } }; Actor.showMovies();
Arrow functions
更像函数语句,只是它们bind
这个bind
到parent scope
。 如果arrow function is in top scope
,则this
参数将引用window/global scope
,而常规函数内的箭头函数将其参数与其外部函数相同。
使用arrow
函数在创build时绑定到封闭scope
,不能更改。 新的操作符,绑定,调用和应用对此没有影响。
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
在上面的例子中,我们失去了对此的控制。 我们可以通过使用此variables引用或使用bind
来解决上述示例。 使用ES6,将其作为lexical scoping
绑定变得更容易。
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
何时不用箭头function
在对象文字里面。
var Actor = { name: 'RajiniKanth', movies: ['Kabali', 'Sivaji', 'Baba'], getName: () => { alert(this.name); } }; Actor.getName();
Actor.showMovies
是用箭头函数定义的,但在调用时,它警告未定义,因为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
在一个构造调用中是新创build的对象。 当执行新的Fn()时, constructor Fn
的上下文是一个新的对象: this instanceof Fn === true
。
this
是从封闭的上下文,即外部范围,这使得它不被分配到新创build的对象设置。
var Message = (text) => { this.text = text; }; // Throws "TypeError: Message is not a constructor" var helloMessage = new Message('Hello World!');
用dynamic上下文callback
箭头函数在声明上静态地绑定context
,并且不可能使其变为dynamic的。 将事件监听器附加到DOM元素是客户端编程中的常见任务。 一个事件触发这个处理函数作为目标元素。
var button = document.getElementById('myButton'); button.addEventListener('click', () => { console.log(this === window); // => true this.innerHTML = 'Clicked button'; });
this
是在全局上下文中定义的箭头函数中的窗口。 当点击事件发生时,浏览器尝试用button上下文来调用处理函数,但是箭头函数不会改变其预定义的上下文。 this.innerHTML
相当于window.innerHTML
,没有任何意义。
你必须应用一个函数expression式,它允许根据目标元素来改变它:
var button = document.getElementById('myButton'); button.addEventListener('click', function() { console.log(this === button); // => true this.innerHTML = 'Clicked button'; });
当用户点击button时,处理函数中的这个button就是button。 因此, this.innerHTML = 'Clicked button'
修改正确的button文本,以反映点击状态。
参考文献: https : //rainsoft.io/when-not-to-use-arrow-functions-in-javascript/
我更喜欢在不需要访问本地的this
下使用箭头函数,因为箭头函数不会绑定自己的this,arguments,super或new.target 。
除了迄今为止的出色答案之外,我还想介绍一个非常不同的原因,为什么箭头函数在某种意义上比“普通的”JavaScript函数更好一些。 为了讨论起见,暂时假设我们使用TypeScript或Facebook的“Flow”types检查器。 考虑下面的玩具模块,它是有效的ECMAScript 6代码加上streamtypes的注释:(我将包括非types化的代码,这将真实地从巴别结果,在这个答案的结尾,所以它可以实际上运行。
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;}; } }
箭头function – 目前使用最广泛的ES6function…
用法:除以下情况外,所有ES5function都应该用ES6箭头function代替:
箭头function不应该被使用:
- 当我们想要function提升
- 因为箭头function是匿名的。
- 当我们想在一个函数中使用
this
/arguments
- 因为箭头函数没有自己的
this
/这些arguments
,所以它们依赖于它们的外部环境。
- 因为箭头函数没有自己的
- 当我们要使用命名函数
- 因为箭头function是匿名的。
- 当我们想要使用函数作为
constructor
- 如箭头function没有自己的
this
。
- 如箭头function没有自己的
- 当我们想添加函数作为对象字面值中的一个属性,并在其中使用对象
- 因为我们不能访问
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
如果我们需要它。
Variant 2 : When we want to pass ONLY one argument to a function and return some value from it.
ES5 version :
var double = function(a) { return a*2; }; console.log(double(2)); //4
ES6 version :
var doubleArrow = a => a*2; console.log(doubleArrow(2)); //4
Note: When passing only one argument we can omit parenthesis ()
.
Variant 3 : When we do NOT want to pass any argument to a function and do NOT want to return any value.
ES5 version :
var sayHello = function() { console.log("Hello"); }; sayHello(); //Hello
ES6 version :
var sayHelloArrow = () => {console.log("sayHelloArrow");} sayHelloArrow(); //sayHelloArrow
Variant 4 : When we want to explicitly return from arrow functions.
ES6 version :
var increment = x => { return x + 1; }; console.log(increment(1)); //2
Variant 5 : When we want to return an object from arrow functions.
ES6 version :
var returnObject = () => ({a:5}); console.log(returnObject());
Note: We need to wrap the object in parenthesis ()
otherwise JavaScript cannot differentiate between a block and an object.
Variant 6 : Arrow functions do NOT have arguments
(an array like object) of their own they depend upon outer context for arguments
.
ES6 version :
function foo() { var abc = i => arguments[0]; console.log(abc(1)); }; foo(2); // 2
Note: foo
is an ES5 function, with an arguments
array like object and an argument passed to it is 2
so arguments[0]
for foo
is 2.
abc
is an ES6 arrow function since it does NOT have it's own arguments
hence it prints arguments[0]
of foo
it's outer context instead.
Variant 7 : Arrow functions do NOT have this
of their own they depend upon outer context for this
ES5 version :
var obj5 = { greet: "Hi, Welcome ", greetUser : function(user) { setTimeout(function(){ console.log(this.greet + ": " + user); // "this" here is undefined. }); } }; obj5.greetUser("Katty"); //undefined: Katty
Note: The callback passed to setTimeout is an ES5 function and it has it's own this
which is undefined in use-strict
environment hence we get output:
undefined: Katty
ES6 version :
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
Note: The callback passed to setTimeout
is an ES6 arrow function and it does NOT have it's own this
so it takes it from it's outer context that is greetUser
which has this
that is obj6
hence we get output:
Hi, Welcome: Katty
Miscellaneous: We cannot use new
with arrow functions. Arrow functions do Not have prototype
property. We do NOT have binding of this
when arrow function is invoked through apply
or call
.
In a simple way,
var a =20; function a(){this.a=10; console.log(a);} //20, since the context here is window.
Another instance:
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: The console would print 20.
The reason being whenever a function is executed its own stack is created, in this example ex
function is executed with the new
operator so a context will be created, and when inner
is executed it JS would create a new stack and execute the inner
function a global context
though there is a local context.
So, if we want inner
function to have a local context which is ex
then we need to bind the context to inner function.
Arrows solve this problem, instead of taking the Global context
they take the local context
if exists any. In the given example,
it will take new ex()
as this
.
So, in all cases where binding is explicit Arrows solve the problem by defaults.