本文能够更好的帮助大家理解箭头函数,它和原来的匿名函数相比,获得了哪些好处等等…

原文地址:https://www.taniarascia.com/understanding-arrow-functions-in-javascript/

介绍

ES6中新增了一个语言特性:箭头函数。箭头函数是一种新的编写匿名函数表达式的方式,这和其他语言(如Python)中的lambda函数很类似。
箭头函数和传统的函数有很多地方存在区别,包括函数作用域和表达式语法。箭头函数在把函数作为一个参数传递给一个高阶函数的使用起来非常便捷,尤其在通过数组内置的iterator(map、reduce、forEach等等)来循环遍历的时候,箭头函数的简写方式也可以让你的代码更易读。
在这篇文章中,我们会一起回顾一下函数声明和函数表达式,来重新学习一下传统函数和箭头函数的区别、箭头函数适用的词法作用域,并探索一些箭头函数允许的语法速记方式。

函数声明

在深入研究箭头函数之前,本文将会先回顾一下传统的javascript函数,会更方便我们后续和箭头函数进行对比。先前的系列文章《How To Define Functions in JavaScript》介绍了函数声明函数表达式的概念。函数声明是一个通过关键词 function 声明的具名函数表达式。函数声明在代码运行之前会被加载到javascript运行上下文之中。这被称做声明提升hoisting),它意味着你可以在函数定义之前对函数进行调用。
下面是关于一个sum函数的例子,函数返回参数a和b的和:

  1. function sum(a, b) {
  2. return a + b
  3. }

你可以在函数声明之前调用sum方法(由于声明提升的作用)

  1. sum(1, 2) // 返回3
  2. function sum(a, b) {
  3. return a + b
  4. }

如果你打印sum函数

  1. console.log(sum)
  2. // 输出下面的函数
  3. ƒ sum(a, b) {
  4. return a + b
  5. }

函数表达式则不会在执行上下文中预加载,只会在代码执行到函数表达式的位置才会运行。函数表达式通常会赋值给一个变量,并且可以是匿名函数(意味着函数可以不定义名称)

  1. const sum = function (a, b) {
  2. return a + b
  3. }

如果你尝试在sum声明之前调用sum函数,则会抛出异常:

  1. sum(1, 2) // Uncaught ReferenceError: Cannot access 'sum' before initialization
  2. const sum = function (a, b) {
  3. return a + b
  4. }

当然,由于匿名函数没有定义名字,在控制态中我们输出sum函数,你会看到:

ƒ (a, b) {
  return a + b
}

sum 函数的值是一个匿名函数,而不是一个具名函数。

你可以在使用function关键字的同时,给函数一个名称,但这种方式并不常见。如果说给函数表达式定义一个函数名称有什么特别作用,那么他就是在错误堆栈异常中能变得更容易跟踪和调试。举个例子:

const sum = function namedSumFunction(a, b) {
  if (!a || !b) throw new Error('Parameters are required.')

  return a + b
}

sum()
// Uncaught Error: Parameters are required.
    at namedSumFunction (<anonymous>:3:23) // 有函数名更容易跟踪
    at <anonymous>:1:1

箭头函数是一种使用(=>)语法编写的匿名函数表达式。我们可以使用箭头函数重写sum函数:

const sum = (a, b) => {
  return a + b
}

和传统的函数表达式类似,箭头函数不会被声明提升,你也无法在声明之前调用它。箭头函数一直都是匿名的,并且无法命名的。
在下一个章节,我们会深入挖掘箭头函数和传统函数不一样的特性。

箭头函数

箭头函数和传统函数相比,除了简洁的语法之外,还有一些重要的差别。最大的不同在于,箭头函数没有自己的this、prototype,并且不能被作为一个构造函数使用。箭头函数也可以作为传统函数的一种更紧凑的替代方法来编写,因为它们允许省略参数周围的括号,并添加具有隐式返回表达式,让函数体更简洁。
在这个章节中,你会更深入的了解到相关的例子:

关键字this

关键字 this 在javascript中是一个非常难对付的话题。《 Understanding This, Bind, Call, and Apply in JavaScript 》这篇文章解释来 this 是如何工作的,以及 this 可以根据程序是否在全局上下文中使用它来隐式推断:做为对象中的一个方法,做为一个构造函数,或者DOM 事件的处理函数。
箭头函数可以使用this,并且this的值是由它外部的作用域来决定的。
下面的例子描述了传统函数和箭头函数里this关键字不同处理方式。在下面的printNumbers对象中,包含两个属性:phrase和numbers. 这里也有另一个对象:loop——用于打印当前numbers数组中的phrase的值:

const printNumbers = {
  phrase: 'The current value is:',
  numbers: [1, 2, 3, 4],

  loop() {
    this.numbers.forEach(function (number) {
      console.log(this.phrase, number)
    })
  },
}

如果我们直接调用

printNumbers.loop()
// 输出如下
// undefined 1
// undefined 2
// undefined 3
// undefined 4

我们看到this.phrase输出的是 undefined,在forEach循环中传入的匿名函数内部指向的this,并没有关联到printNumbers对象。这是因为传统的函数中的this不会指向执行上下文中的对象(如例子中的printNumbers)。
在老版本的JavaScript中,我们可以通过使用 bind 方法来设置当前this上下文。在一些老版本的框架之中,我们经常会发现这种写法。

使用 bind 来改进函数

const printNumbers = {
  phrase: 'The current value is:',
  numbers: [1, 2, 3, 4],
  loop: function () {
    // Bind the `this` from printNumbers to the inner forEach function
    this.numbers.forEach(
      function (number) {
        console.log(this.phrase, number)
      }.bind(this),
    )
  },
}
printNumbers.loop()
// 将会输出
The current value is: 1
The current value is: 2
The current value is: 3
The current value is: 4

箭头函数可以让我们用更简单的方式处理this。由于this的值是由词法作用域决定的,在forEach内部的调用的函数,可以获取到外部的printNumbers对象:

const printNumbers = {
  phrase: 'The current value is:',
  numbers: [1, 2, 3, 4],
  loop: function () {
    this.numbers.forEach((number) => {
      console.log(this.phrase, number)
    })
  },
}
printNumbers.loop()
// 将会获得预期的输出
The current value is: 1
The current value is: 2
The current value is: 3
The current value is: 4


箭头函数用作对象方法

箭头做为数组方法的参数传入时,会非常便捷,但用作对象方法的时候则相反。使用前面类似的例子:

const printNumbers = {
  phrase: 'The current value is:',
  numbers: [1, 2, 3, 4],
  loop: () => {
    this.numbers.forEach((number) => {
      console.log(this.phrase, number)
    })
  },
}
printNumbers.loop()
// Uncaught TypeError: Cannot read property 'forEach' of undefined

由于Object对象并没有自己的作用域,箭头函数在查找this的时候,会向外层寻找(这里找到来最外层的window),而numbers对象并不存在于window对象中,所以这里抛了一个异常。一般来说,用作对象方法时,使用传统的函数会更安全一点。

箭头函数没有 constructorprototype

先前的系列文章《 Understanding Prototypes and Inheritance in JavaScript 》解释了函数和类都包含一个 prototype 属性。
你可以通过下面的方法来查看相关的prototype:

function myFunction() {
  this.value = 5
}
// Log the prototype property of myFunction
console.log(myFunction.prototype)
// 打印出:
// {constructor: ƒ}

它表示在prototype属性中,包含一个构造函数constructor。你可以使用new关键字去创建一个函数的实例。

const instance = new myFunction()
console.log(instance.value)
// 输出 5

相反,箭头函数则没有原型属性,创建一个箭头函数并打印它的protoytpe,结果是undefined,也无法使用new关键字进行创建实例。

const myArrowFunction = () => {}

// Attempt to log the prototype property of myArrowFunction
console.log(myArrowFunction.prototype)

// 输出undefined

new myArrowFunction()
// 使用new关键字也会报错
// Uncaught TypeError: myArrowFunction is not a constructor

如上面我们见到的,箭头函数有很多细微的变化,这使得它们的操作方式与ES5和更早版本中的传统函数不同。也有一些可选的语法变化,使编写箭头函数更快、更少冗长。下一节将展示这些语法更改的示例。

隐性return

我么可以用下面的方式简写我们的代码

const sum = (a, b) => {
  return a + b
}
// 改成
const sum = (a, b) => a + b

隐性自动添加return,在一些场景会让我们的代码变的更简洁,比如一些数据处理函数的组合:

// 比如一个filter + map的组合 
var result = [{a:1,b:1},{a:2,b:2}].filter(item => item.a === 1).map(item => item.a + item.b)

如果要最终返回一个对象,记得使用括号包裹起来

const sum = (a, b) => ({result: a + b})
sum(1, 2)
// {result: 3}

省略单个函数两边的括号

另一个很好用的语法简写是,单函数只有一个参数时,可以省略函数的括号:

const square = (x) => x * x

// 可以简写成
const square = x => x * x

square(10) // 输出100

需要注意的是:如果不包含任何参数,那么括号则是必要的

const greet = () => 'Hello!'
greet()

一些代码规范会要求在参数周围保留括号,尤其在TypeSciprt中需要对参数提供更多的变量信息的时候。在编写箭头函数的时候,请先确保写法是符合相应的代码规范。

总结

本文一开始对比了函数声明和函数表达式,然后我们了解到了箭头函数始终是匿名函数,并且不包含prototypeconstructor,并且不能和关键字new一起使用。箭头函数内部的this的值是由词法作用域决定的。最后我们了解了一些箭头函数带来的语法简写:隐性return和单个参数括号省略。
如果想要了解一些基础的函数的概念,可以阅读《 How To Define Functions in JavaScript》。想了解作用域以及声明提升的概念,可以参考T《 Understanding Variables, Scope, and Hoisting in JavaScript