使用()=>{}和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上鼓掌或留下评论。

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

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

导出类C { N:数字; F1: number => number; F2: number => number; 构造函数(){ 这一点。N = 42; 这一点。F1 = (x:number) => x + this.n; 这一点。F2 =函数(x:number){返回x + this.n;}; } }

现在看看当我们使用来自不同模块的类C时会发生什么,就像这样:

let o = {f1: new C()。f1, f2: new C()。F2, n: "foo"}; 设n1: number = o.f1(1);// n1 = 43 Console.log (n1 === 43);/ /正确的 设n2: number = o.f2(1);// n2 = "1foo" Console.log (n2 === "1foo");// true,不是字符串!

正如您所看到的,类型检查器在这里失败了:f2应该返回一个数字,但它返回了一个字符串!

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

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

下面是可运行的非类型化版本,由Babel生成:

C类{ 构造函数(){ 这一点。N = 42; 这一点。F1 = x => x + this.n; 这一点。F2 =函数(x){返回x + this.n;}; } } let o = {f1: new C()。f1, f2: new C()。F2, n: "foo"}; 设n1 = o.f1(1);// n1 = 43 Console.log (n1 === 43);/ /正确的 设n2 = o.f2(1);// n2 = "1foo" Console.log (n2 === "1foo");// true,不是字符串!

简单来说,

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()如下所示。

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

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

关于词汇this

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

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

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

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

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

关于简洁的语法

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

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

关于指导方针

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

ES6函数表示法指南:

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

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

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

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

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

上面的代码片段将引用一个对象并打印出名称“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的函数中呢?

这里this指的是窗口对象,而不是内部函数,因为它超出了作用域。因此,总是引用它所在函数的所有者,在这种情况下——因为它现在超出了作用域——是window/global对象。

当它在对象的方法内部时,函数的所有者就是该对象。因此,this关键字被绑定到对象。然而,当它在函数内部时,无论是单独的还是在另一个方法中,它总是引用窗口/全局对象。

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

fn(); // [object Window]

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

通常你会在方法的内部函数之外创建一个变量。现在' forEach '方法可以访问这个对象,从而访问对象的属性及其值。

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关键字附加到该方法的内部函数。

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参数将与其外部函数相同。

对于箭头函数,它在创建时被绑定到外围作用域,并且不能更改。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();

演员。getName是用一个箭头函数定义的,但在调用时它警告为未定义,因为this.name是未定义的,因为上下文仍然是窗口。

发生这种情况是因为箭头函数在词法上将上下文绑定到窗口对象…也就是外部作用域。执行this.name相当于window.name,而window.name是未定义的。

对象原型

同样的规则也适用于在原型对象上定义方法。而不是使用箭头函数来定义sayCatName方法,这会带来一个不正确的上下文窗口:

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

调用构造函数

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

这是从封闭上下文(即外部作用域)中设置的,这使得它没有分配给新创建的对象。

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

带有动态上下文的回调

箭头函数在声明时静态地绑定上下文,不可能使其动态。将事件监听器附加到DOM元素是客户端编程中的常见任务。事件以此作为目标元素触发处理程序函数。

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

这是在全局上下文中定义的箭头函数中的窗口。当单击事件发生时,浏览器尝试调用带有按钮上下文的处理程序函数,但箭头函数不会更改其预定义上下文。这一点。innerHTML相当于window。innerHTML,没有任何意义。

你必须应用一个函数表达式,它允许改变这取决于目标元素:

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

当用户单击按钮时,处理程序函数中的这个就是按钮。因此这个问题。innerHTML = 'Clicked button'正确修改按钮文本,以反映单击状态。

参考文献

何时“不”使用箭头函数