作者选择了 COVID-19 Relief Fund 作为 Write for DOnations 计划的一部分来接受捐赠。
介绍
ECMAScript 规范 (ES6) 的 2015 版在 JavaScript 语言中添加了 箭头函数表达式 。 箭头函数是一种编写匿名函数表达式的新方法,类似于一些其他编程语言中的 lambda 函数,例如 Python。
箭头函数在许多方面与传统函数不同,包括确定其作用域的方式和表达语法的方式。 因此,箭头函数在将函数作为参数传递给高阶函数时特别有用,例如当您使用 内置迭代器方法 循环 数组 时. 它们的语法缩写还可以让您提高代码的可读性。
在本文中,您将回顾函数声明和表达式,了解传统函数表达式和箭头函数表达式之间的区别,了解与箭头函数有关的词法作用域,并探索箭头函数允许的一些语法简写。
定义函数
在深入研究箭头函数表达式的细节之前,本教程将简要回顾传统的 JavaScript 函数,以便稍后更好地展示箭头函数的独特方面。
本系列前面的如何在JavaScript中定义函数教程介绍了函数声明和函数表达式的概念。 函数声明是使用 function
关键字编写的命名函数。 函数声明在任何代码运行之前加载到执行上下文中。 这称为 hoisting,这意味着您可以在声明之前使用该函数。
下面是一个 sum
函数的示例,它返回两个参数之和:
function sum(a, b) { return a + b }
可以在声明函数之前执行 sum
函数由于提升:
sum(1, 2) function sum(a, b) { return a + b }
运行此代码将给出以下输出:
Output3
您可以通过记录函数本身来找到函数的名称:
console.log(sum)
这将返回函数及其名称:
Outputƒ sum(a, b) { return a + b }
函数表达式是没有预加载到执行上下文中的函数,只有在代码遇到它时才会运行。 函数表达式通常赋值给一个变量,可以是anonymous,表示函数没有名字。
在此示例中,将相同的 sum
函数编写为匿名函数表达式:
const sum = function (a, b) { return a + b }
您现在已将匿名函数分配给 sum
常量。 在声明之前尝试执行该函数将导致错误:
sum(1, 2) const sum = function (a, b) { return a + b }
运行这将给出:
OutputUncaught ReferenceError: Cannot access 'sum' before initialization
另外,请注意该函数没有命名标识符。 为了说明这一点,编写分配给 sum
的相同匿名函数,然后将 sum
记录到控制台:
const sum = function (a, b) { return a + b } console.log(sum)
这将向您显示以下内容:
Outputƒ (a, b) { return a + b }
sum
的值是匿名函数,而不是命名函数。
您可以命名使用 function
关键字编写的函数表达式,但这在实践中并不流行。 您可能想要命名函数表达式的一个原因是使错误堆栈跟踪更易于调试。
考虑以下函数,如果缺少函数参数,它使用 if 语句 引发错误:
const sum = function namedSumFunction(a, b) { if (!a || !b) throw new Error('Parameters are required.') return a + b } sum();
突出显示的部分为函数命名,然后如果缺少任何一个参数,该函数使用 或 ||
运算符抛出错误 object。
运行此代码将为您提供以下信息:
OutputUncaught Error: Parameters are required. at namedSumFunction (<anonymous>:3:23) at <anonymous>:1:1
在这种情况下,命名函数可以让您快速了解错误所在。
arrow 函数表达式 是用“胖箭头”语法 (=>
) 编写的匿名函数表达式。
用箭头函数语法重写 sum
函数:
const sum = (a, b) => { return a + b }
与传统的函数表达式一样,箭头函数不会被提升,因此您不能在声明它们之前调用它们。 它们也总是匿名的——没有办法命名箭头函数。 在下一节中,您将探索箭头函数与传统函数之间在语法和实际方面的更多区别。
箭头函数行为和语法
箭头函数在它们的工作方式上有一些重要的区别,它们与传统函数不同,还有一些语法增强。 最大的功能差异是箭头函数没有自己的 this
绑定或原型,不能用作构造函数。 箭头函数也可以写成比传统函数更紧凑的替代方案,因为它们可以省略参数周围的括号并添加带有隐式返回的简洁函数体的概念。
在本节中,您将通过示例来说明每种情况。
词法 this
关键字 this
在 JavaScript 中通常被认为是一个棘手的话题。 文章 Understanding This, Bind, Call, and Apply in JavaScript 解释了 this
是如何工作的,以及如何根据程序是否在全局上下文,作为对象内的方法,作为函数或类的构造函数,或作为 DOM[X298X] 事件处理程序。
箭头函数有 lexical this,这意味着 this
的值由周围的范围(词法环境)决定。
下一个示例将演示传统函数和箭头函数处理 this
之间的区别。 在下面的 printNumbers
对象中,有两个属性:phrase
和 numbers
。 对象上还有一个方法,loop
,它应该打印 phrase
字符串和 numbers
中的当前值:
const printNumbers = { phrase: 'The current value is:', numbers: [1, 2, 3, 4], loop() { this.numbers.forEach(function (number) { console.log(this.phrase, number) }) }, }
人们可能期望 loop
函数在每次迭代的循环中打印字符串和当前数字。 但是,在运行函数的结果中,phrase
实际上是 undefined
:
printNumbers.loop()
这将给出以下内容:
Outputundefined 1 undefined 2 undefined 3 undefined 4
如图所示,this.phrase
是未定义的,表明传递给 forEach 方法 的匿名函数中的 this
不引用 printNumbers
对象。 这是因为传统的函数不会从环境的范围,也就是printNumbers
对象来确定它的this
值。
在旧版本的 JavaScript 中,您必须使用 bind
方法,该方法显式设置 this
。 在 ES6 出现之前,这种模式经常出现在一些早期版本的框架中,例如 React。
使用 bind
修复函数:
const printNumbers = { phrase: 'The current value is:', numbers: [1, 2, 3, 4], loop() { // Bind the `this` from printNumbers to the inner forEach function this.numbers.forEach( function (number) { console.log(this.phrase, number) }.bind(this), ) }, } printNumbers.loop()
这将给出预期的结果:
OutputThe current value is: 1 The current value is: 2 The current value is: 3 The current value is: 4
箭头函数提供了一种更直接的处理方式。 由于它们的 this
值是根据词法范围确定的,因此 forEach
中调用的内部函数现在可以访问外部 printNumbers
对象的属性,如下所示:
const printNumbers = { phrase: 'The current value is:', numbers: [1, 2, 3, 4], loop() { this.numbers.forEach((number) => { console.log(this.phrase, number) }) }, } printNumbers.loop()
这将给出预期的结果:
OutputThe current value is: 1 The current value is: 2 The current value is: 3 The current value is: 4
这些示例表明,在 forEach、map、filter 和 reduce 等内置数组方法中使用箭头函数可以更直观并且更易于阅读,使该策略更有可能实现预期。
箭头函数作为对象方法
虽然箭头函数作为传递给数组方法的参数函数非常出色,但它们作为对象方法并不有效,因为它们对 this
使用词法范围的方式。 使用与之前相同的示例,采用 loop
方法并将其转换为箭头函数,以了解它将如何执行:
const printNumbers = { phrase: 'The current value is:', numbers: [1, 2, 3, 4], loop: () => { this.numbers.forEach((number) => { console.log(this.phrase, number) }) }, }
在这种对象方法的情况下,this
应该引用 printNumbers
对象的属性和方法。 但是,由于对象不会创建新的词法范围,箭头函数将在对象之外查找 this
的值。
调用 loop()
方法:
printNumbers.loop()
这将给出以下内容:
OutputUncaught TypeError: Cannot read property 'forEach' of undefined
由于对象不创建词法作用域,箭头函数方法在外部作用域中查找 this
——本例中为 Window。 由于 numbers
属性在 Window
对象上不存在,因此会引发错误。 作为一般规则,默认情况下使用传统函数作为对象方法更安全。
箭头函数没有 constructor
或 prototype
本系列前面的 Understanding Prototypes and Inheritance in JavaScript 教程解释了函数和类具有 prototype
属性,这是 JavaScript 用作克隆和继承的蓝图。
为了说明这一点,创建一个函数并记录自动分配的 prototype
属性:
function myFunction() { this.value = 5 } // Log the prototype property of myFunction console.log(myFunction.prototype)
这会将以下内容打印到控制台:
Output{constructor: ƒ}
这表明在 prototype
属性中有一个带有 constructor
的对象。 这允许您使用 new
关键字来创建函数的实例:
const instance = new myFunction() console.log(instance.value)
这将产生您在首次声明函数时定义的 value
属性的值:
Output5
相反,箭头函数没有 prototype
属性。 创建一个新的箭头函数并尝试记录它的原型:
const myArrowFunction = () => {} // Attempt to log the prototype property of myArrowFunction console.log(myArrowFunction.prototype)
这将给出以下内容:
Outputundefined
由于缺少 prototype
属性,new
关键字不可用,您无法从箭头函数构造实例:
const arrowInstance = new myArrowFunction() console.log(arrowInstance)
这将给出以下错误:
OutputUncaught TypeError: myArrowFunction is not a constructor
这与我们之前的示例一致:由于箭头函数没有自己的 this
值,因此您将无法将箭头函数用作构造函数。
如此处所示,箭头函数有很多细微的变化,使得它们的操作方式与 ES5 及更早版本中的传统函数不同。 还有一些可选的语法更改,使编写箭头函数更快、更简洁。 下一节将展示这些语法更改的示例。
隐式返回
传统函数的主体包含在使用大括号 {}
的块中,并在代码遇到 return
关键字时结束。 下面是这个实现看起来像一个箭头函数:
const sum = (a, b) => { return a + b }
箭头函数引入了简洁的主体语法,或隐式返回。 这允许省略大括号和 return
关键字。
const sum = (a, b) => a + b
隐式返回对于在 map
、filter
和其他常见数组方法中创建简洁的单行操作很有用。 请注意,必须省略括号和 return
关键字。 如果您不能将主体编写为单行返回语句,那么您将不得不使用正常的块主体语法。
在返回对象的情况下,语法要求您将对象字面量括在括号中。 否则,括号将被视为函数体,不会计算 return
值。
为了说明这一点,请找到以下示例:
const sum = (a, b) => ({result: a + b}) sum(1, 2)
这将给出以下输出:
Output{result: 3}
省略单个参数周围的括号
另一个有用的语法增强功能是从函数中的单个参数周围删除括号的能力。 在以下示例中,square
函数仅对一个参数 x
起作用:
const square = (x) => x * x
因此,您可以省略参数周围的括号,它的工作原理是一样的:
const square = x => x * x square(10)
这将给出以下内容:
Output100
请注意,如果函数不带参数,则需要括号:
const greet = () => 'Hello!' greet()
调用 greet()
将按如下方式工作:
Output'Hello!'
一些代码库选择尽可能省略括号,而另一些代码库则选择始终在参数周围保留括号,特别是在使用 TypeScript 并需要有关每个变量和参数的更多信息的代码库中。 在决定如何编写箭头函数时,请查看您正在参与的项目的样式指南。
结论
在本文中,您回顾了传统函数以及函数声明和函数表达式之间的区别。 您了解到箭头函数总是匿名的,没有 prototype
或 constructor
,不能与 new
关键字一起使用,并确定 this
的值] 通过词法作用域。 最后,您探索了箭头函数可用的新语法增强功能,例如单参数函数的隐式返回和括号省略。
有关基本函数的回顾,请阅读 如何在 JavaScript 中定义函数。 要了解更多关于 JavaScript 中作用域和提升的概念,请阅读 了解 JavaScript 中的变量、作用域和提升。