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

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

不要在创建原型时使用新的和不能使用的 修正了初始化时绑定范围的问题

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

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

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

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


当前回答

在es6中引入了箭头函数或lambda。除了在最小语法上的优雅,最显著的函数差异是在箭头函数内的作用域

在正则函数表达式中,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()方法时,它通过将此正确地解析为objA来进行方法调用,但当定义为arrow=>函数时失败。这是因为在常规函数中,当它作为对象(objA)的方法调用时,它就是对象本身。

然而,在箭头函数的情况下,它在词法上绑定到它定义的封闭作用域的this(在我们的例子中是global / Window),并在objA上作为方法调用时保持不变。

在对象的方法中,箭头函数比常规函数有优势,但只有当它在定义时被固定和绑定时才有优势。

/* 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. log)的函数。id} -> {this.name}])异步地作为setTimeout上的回调,当箭头函数被用作回调时,该函数正确地解析为objB,但当回调被定义为常规函数时失败。

这是因为传递给setTimeout(()=>..)的arrow =>函数在词法上关闭了它的父函数,即调用定义它的objB.print()。换句话说,arrow =>函数传递给setTimeout(()==>…因为objB.print()的调用就是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 = 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. log])id} -> {this.name}]);}因为它不能被更改,并且将一直绑定到它所定义的外围作用域的this (global / Window)。

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

这些都是虚构的例子,但让我们考虑一些更真实的例子。如果必须将reduce()方法编写成类似于处理数组的方法,那么同样不能将其定义为lambda,因为它需要从调用上下文(即调用它的数组)推断出这一点。

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

此外,当框架或系统接受一个回调函数,并在以后使用动态上下文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这样的框架中,模板组件绑定方法是常规的函数/方法,因为它们的调用是由绑定函数的框架管理的。(Angular使用Zone.js来管理视图模板绑定函数调用的异步上下文。)

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


本文也可以在我的Medium出版物中找到。如果你喜欢这篇文章,或者有任何评论和建议,请在Medium上鼓掌或留下评论。

其他回答

在es6中引入了箭头函数或lambda。除了在最小语法上的优雅,最显著的函数差异是在箭头函数内的作用域

在正则函数表达式中,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()方法时,它通过将此正确地解析为objA来进行方法调用,但当定义为arrow=>函数时失败。这是因为在常规函数中,当它作为对象(objA)的方法调用时,它就是对象本身。

然而,在箭头函数的情况下,它在词法上绑定到它定义的封闭作用域的this(在我们的例子中是global / Window),并在objA上作为方法调用时保持不变。

在对象的方法中,箭头函数比常规函数有优势,但只有当它在定义时被固定和绑定时才有优势。

/* 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. log)的函数。id} -> {this.name}])异步地作为setTimeout上的回调,当箭头函数被用作回调时,该函数正确地解析为objB,但当回调被定义为常规函数时失败。

这是因为传递给setTimeout(()=>..)的arrow =>函数在词法上关闭了它的父函数,即调用定义它的objB.print()。换句话说,arrow =>函数传递给setTimeout(()==>…因为objB.print()的调用就是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 = 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. log])id} -> {this.name}]);}因为它不能被更改,并且将一直绑定到它所定义的外围作用域的this (global / Window)。

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

这些都是虚构的例子,但让我们考虑一些更真实的例子。如果必须将reduce()方法编写成类似于处理数组的方法,那么同样不能将其定义为lambda,因为它需要从调用上下文(即调用它的数组)推断出这一点。

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

此外,当框架或系统接受一个回调函数,并在以后使用动态上下文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这样的框架中,模板组件绑定方法是常规的函数/方法,因为它们的调用是由绑定函数的框架管理的。(Angular使用Zone.js来管理视图模板绑定函数调用的异步上下文。)

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


本文也可以在我的Medium出版物中找到。如果你喜欢这篇文章,或者有任何评论和建议,请在Medium上鼓掌或留下评论。

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

在全局作用域和Object中使用函数。原型属性。 为对象构造函数使用类。 在其他地方使用=>。

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

Scope safety: When arrow functions are used consistently, everything is guaranteed to use the same thisObject as the root. If even a single standard function callback is mixed in with a bunch of arrow functions there's a chance the scope will become messed up. Compactness: Arrow functions are easier to read and write. (This may seem opinionated so I will give a few examples further on.) Clarity: When almost everything is an arrow function, any regular function immediately sticks out for defining the scope. A developer can always look up the next-higher function statement to see what the thisObject is.

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

To indicate a function that should not access the thisObject. The window object (global scope) is best addressed explicitly. Many Object.prototype definitions live in the global scope (think String.prototype.truncate, etc.) and those generally have to be of type function anyway. Consistently using function on the global scope helps avoid errors. Many functions in the global scope are object constructors for old-style class definitions. Functions can be named1. This has two benefits: (1) It is less awkward to writefunction foo(){} than const foo = () => {} — in particular outside other function calls. (2) The function name shows in stack traces. While it would be tedious to name every internal callback, naming all the public functions is probably a good idea. Function declarations are hoisted, (meaning they can be accessed before they are declared), which is a useful attribute in a static utility function.

对象构造函数

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

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

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

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

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

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

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

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

箭头函数的可读性

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

自从ECMAScript 5.1给了我们函数数组之后,ECMAScript已经发生了很大的变化。forEach,数组。Map和所有这些函数式编程特性让我们在以前使用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关键字。


笔记

Named arrow functions have been deferred in the ES6 specification. They might still be added a future version. According to the draft specification, "Class declarations/expressions create a constructor function/prototype pair exactly as for function declarations" as long as a class does not use the extend keyword. A minor difference is that class declarations are constants, whereas function declarations are not. Note on blocks in single statement arrow functions: I like to use a block wherever an arrow function is called for the side effect alone (e.g., assignment). That way it is clear that the return value can be discarded.

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

关于词汇this

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

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

我认为有些人不会遇到这种情况,但即使遇到这种情况的人,他们也一定会发现自己在代码库中工作,每个文件都会出现100次这种情况,合理的人所希望的只是一点点(或很多)损害控制。所以箭头在某种程度上是好的,当它们让糟糕的情况变得更好的时候。

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

所有这些都是以下认识的序言:如果一个人不使用这个,那么箭头通常引起的关于这个的模糊性就变得无关紧要了。在这种情况下,箭头变得更加中性。

关于简洁的语法

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

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

关于指导方针

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

ES6函数表示法指南:

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

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

简单来说,

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将创建一个新的堆栈,并在全局上下文中执行内部函数,尽管有一个局部上下文中。

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

箭头可以解决这个问题。它们不采用全局上下文,而是采用本地上下文(如果存在的话)。在*给出的例子中,它将接受new ex()如下所示。

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