💎一站式轻松地调用各大LLM模型接口,支持GPT4、智谱、星火、月之暗面及文生图 广告
[TOC] # 13. 箭头函数 ## 13.1 概览 箭头函数有两个好处。 首先,没有传统函数那么啰嗦: ```js const arr = [1, 2, 3]; const squares = arr.map(x => x * x); // Traditional function expression: const squares = arr.map(function (x) { return x * x }); ``` 其次,他们的`this`是从父作用域(*lexical*)中获取的。因此,你不再需要使用`bind()` 或者 `that = this`。 ```js function UiComponent() { const button = document.getElementById('myButton'); button.addEventListener('click', () => { console.log('CLICK'); this.handleClick(); // lexical `this` }); } ``` 以下的变量在箭头函数中属于词法的: - `arguments` - `super` - `this` - `new.target` ## 13.2 因为 this ,传统的函数都是糟糕的非方法函数 在JavaScript中,传统函数有三种类型: 1. 非方法函数 2. 方法 3. 构造函数 它们之间存在冲突: 因为第2种和第3种中, 函数总是有自己的 `this`变量。但在回调函数内部却无法获得其外部方法中的`this`(第1种)。 可以在下面的ES5代码中看到: ```js function Prefixer(prefix) { this.prefix = prefix; } Prefixer.prototype.prefixArray = function (arr) { // (A) 'use strict'; return arr.map(function (x) { // (B) // Doesn’t work: return this.prefix + x; // (C) }); }; ``` 在C行,我们想访问 `this.prefix`,但是由于行 B 的函数在作用域链上覆盖了来自于行 A 方法的 `this` ,因此是无法访问的。在严格模式下,非方法的函数中`this`的值为`undefined`,所以我们访问`Prefixer`会出错。 ```js > var pre = new Prefixer('Hi '); > pre.prefixArray(['Joe', 'Alex']) TypeError: Cannot read property 'prefix' of undefined ``` 在 ECMAScript 5 中有三种解决方法。 ### 13.2.1 方案 1: `that = this` 你可以将`this` 赋值给一个不会被屏蔽的变量。这就是下面行 A 所做的: ```js function Prefixer(prefix) { this.prefix = prefix; } Prefixer.prototype.prefixArray = function (arr) { var that = this; // (A) return arr.map(function (x) { return that.prefix + x; }); }; ``` 现在`Prefixer` 如预期一样工作: ```js > var pre = new Prefixer('Hi '); > pre.prefixArray(['Joe', 'Alex']) [ 'Hi Joe', 'Hi Alex' ] ``` ### 13.2.2 方案 2: 为`this`指定一个值 有很少的几个数组方法会有额外的参数用于指定在调用回调函数的时候的 `this`值。这就是下面行 A 最后一个参数: ```js function Prefixer(prefix) { this.prefix = prefix; } Prefixer.prototype.prefixArray = function (arr) { return arr.map(function (x) { return this.prefix + x; }, this); // (A) }; ``` ### 13.2.3 方案3:`bind(this) ` 可以使用方法 bind() 转换一下在调用的时候才决定的 this 值(通过 call() ,函数调用,方法调用,等等),将其变为固定的值。这就是下面行 A 所做的: ```js function Prefixer(prefix) { this.prefix = prefix; } Prefixer.prototype.prefixArray = function (arr) { return arr.map(function (x) { return this.prefix + x; }.bind(this)); // (A) }; ``` ### 13.2.4 ECMAScript 6解决方案:箭头函数 箭头函数非常像方案3。但是,最好将它们看作是一种不会词法上屏蔽`this`的新函数。也就是说,它们与常规函数不同(甚至可以说它们做的更少)。它们不是一般的函数加上绑定。 使用箭头函数,示例代码如下: ```js function Prefixer(prefix) { this.prefix = prefix; } Prefixer.prototype.prefixArray = function (arr) { return arr.map((x) => { return this.prefix + x; }); }; ``` 如果要把上述代码完全 ES6 化,就要使用类和更加紧凑多样的箭头函数: ```js class Prefixer { constructor(prefix) { this.prefix = prefix; } prefixArray(arr) { return arr.map(x => this.prefix + x); // (A) } } ``` 在行 A ,调整了一下箭头函数两边的部分,节省了几个字符: * 如果只有一个参数,并且这个参数是一个标识符,那么可以省略小括号。 * 在箭头后面跟一个表达式会返回该表达式的值。 ## 13.3 箭头函数语法 选用“胖”箭头`=>`(相对于瘦箭头`->`)的原因是与 CoffeeScript 兼容,两者非常类似。 指定参数: ~~~ () => { ... } // no parameter x => { ... } // one parameter, an identifier (x, y) => { ... } // several parameters ~~~ 指定函数体: ```js x => { return x * x } // block x => x * x // expression, equivalent to previous line ``` 这些语句块表现得像普通的函数体。例如,需要用 `return` 返回一个值。如果只有一个表达式,那么这个表达式的值会被隐式返回。 注意,只有一个表达式的箭头函数省略了多少啰嗦的东西。比较: ```js const squares = [1, 2, 3].map(function (x) { return x * x }); const squares = [1, 2, 3].map(x => x * x); ``` ### 13.3.1 省略单个参数的括号 只有在包含单个标识符的情况下,才能省略参数周围的括号: ```js > [1,2,3].map (x => 2 * x) [2,4,6] ``` 其他情况下,你必须输入括号,即使只有一个参数。例如,如果您重构了单个参数,则需要括号: ```js > [[1,2], [3,4]].map(([a,b]) => a + b) [ 3, 7 ] ``` 如果单个参数具有默认值,则需要括号(`undefined`会触发默认值!): ```js > [1, undefined, 3].map((x='yes') => x) [ 1, 'yes', 3 ] ``` ## 13.4 词汇变量 ### 13.4.1 传播变量值:静态的和动态的 以下是可以传播变量值的两种方式。 首先,静态地(词法上的):变量可访问的方式由程序的结构决定。在作用域内声明的变量在其内部嵌套的所有作用域内都是可访问的(除非被屏蔽)。例如: ```js const x = 123; function foo(y) { return x; // value received statically } ``` 第二,动态地:可以通过函数调用传播变量值。例如: ```js function bar(arg) { return arg; // value received dynamically } ``` ### 13.4.2 箭头函数中的词法变量 this 的来源是一个区分箭头函数的重要方面: * 传统函数有一个动态的 this ,它的值取决于函数如何调用。 * 箭头函数有一个词法的 this ,它的值取决于父作用域。 从词法上来确定值的[全部变量列表](http://www.ecma-international.org/ecma-262/6.0/#sec-arrow-function-definitions-runtime-semantics-evaluation)是: ~~~ arguments super this new.target ~~~ ## 13.5 语法陷阱 有一些与语法有关的细节有时会让你出错 ### 13.5.1 箭头功能的绑定非常松散 如果你把`=>`看作一个操作符,你可以说它的优先级较低,因为它是松散绑定的。也就是说:如果它可能会与其他操作符产生冲突,通常其他操作符会比其优先级高。 因为JS允许表达体“连接在一起”,所以以下代码会难理解且产生问题: ```js const f = x => (x % 2) === 0 ? x : 0; ``` 换句话说,如果我们想要`===`和`?`优先级高,而`=>`比较低。我们希望它被解释如下: ```js const f = x => ((x % 2) === 0 ? x : 0); ``` 如果希望`=>`优先级高,就像这样: ```js const f = (x => (x % 2)) === 0 ? x : 0; ``` 如果`=>`优先级比`?`高、比`===`低,它会是这样的: ```js const f = (x => ((x % 2) === 0)) ? x : 0; ``` 因此,如果与其他运算符竞争,则必须将括号中的箭头函数括起来。例如: ```js console.log(typeof () => {}); // SyntaxError console.log(typeof (() => {})); // OK ``` 另一方面,可以使用 `typeof`作为一个表达式体,不用放在大括号中: ```js const f = x => typeof x; ``` ### 13.5.2 箭头函数参数后面不能进行换行 ES6 禁止在参数定义和箭头函数的箭头之间换行: ```js const func1 = (x, y) // SyntaxError => { return x + y; }; const func2 = (x, y) => // OK { return x + y; }; const func3 = (x, y) => { // OK return x + y; }; const func4 = (x, y) // SyntaxError => x + y; const func5 = (x, y) => // OK x + y; ``` 在参数定义内进行换行是允许的: ```js const func6 = ( // OK x, y ) => { return x + y; }; ``` 这种限制换行的理由是,为了保留该选项,因为可能在未来会使用“无头”箭头函数(即:当您定义了没有参数的箭头函数时,您可以省略括号)。 ### 13.5.3 不能用语句作为表达式体 #### 13.5.3.1表达式与语句 快速复习(参阅《 Speaking JavaScript 》获取更多[相关信息](http://speakingjs.com/es5/ch07.html#expr_vs_stmt)): 表达式产生(执行得到)值。例子: ```js 3 + 4 foo(7) 'abc'.length ``` 语句做一些操作。例子: ```js while (true) { ··· } return 123; ``` 大多数表达式可以被用作语句,简单地将它们放在语句的位置上: ```js function bar() { 3 + 4; foo(7); 'abc'.length; } ``` #### 3.5.3.2 箭头函数体 如果箭头函数体是一个表达式,那么就可以不需要花括号了: ```js asyncFunc.then(x => console.log(x)); ``` 但是,语句一定要放在花括号里面: ```js asyncFunc.catch(x => { throw x }); ``` ### 13.5.4 返回对象字面量 JavaScript语法的一些部分是不明确的。以下列代码为例。 ```js { bar: 123 } ``` 它可能是: * 具有单个属性`bar`的对象字面量。 * 具有标签`bar`和表达式语句`123`的块。 假定一个箭头函数的函数体可以是一个表达式或一个语句。如果你想让函数体是一个对象字面量,就必须将其放在圆括号中: ```js > const f1 = x => ({ bar: 123 }); > f1() { bar: 123 } ``` 为了比较,这里的箭头函数的函数体是一个块: ```js > const f2 = x => { bar: 123 }; > f2() undefined ``` ## 13.6 立即调用箭头函数 还记得[立即调用函数表达式( IIFEs )](###SJ—第16章)吗?在 ECMAScript 5 中用于模拟块级作用域和值返回块,看起来像下面这样: ```js (function () { // open IIFE // inside IIFE })(); // close IIFE ``` 如果您使用**立即调用的箭头函数**(**IIAF**),可以节省字符: ```js (() => { return 123 })(); ``` ### 13.6.1 分号 和 IIFEs 类似,应该在 IIAFs 结尾加上分号(或者使用一个等价的措施),以避免两个连续的 IIAFs 被解释成一个函数调用(第一个是函数,第二个是参数)。 ### 13.6.2 带有块体的括号内的箭头函数 即使IIAF有一个块体,也必须用括号括起来,因为它不能被(直接地)函数调用。这种语法约束的原因是与箭头函数的一致性,这些函数的主体是表达式(如下所述)。 因此,括号必须括起箭头函数。相比之下,你可以选择IIFEs——你可以用括号括起整个表达式: ```js (function () { ··· }()); ``` 或者,只是用括号括起函数表达式: ```js (function () { ··· })(); ``` 考虑到箭头函数的工作方式,现在起,我们应该优先考虑第二种的立即调用方式。 ### 13.6.3 带有表达式体的括号内的箭头函数 如果您想了解为什么不能通过在其后面加上括号来调用箭头函数,则必须了解表达式体的工作方式:表达式体之后的括号应该是表达式的一部分,而不是整个箭头函数的调用。这与前面的小节中解释的箭头函数松绑定有关。 看一个例子: ```js const value = () => foo(); ``` 应该被解释为这样: ```js const value = () => (foo()); ``` 而不是: ```js const value = (() => foo)(); ``` 进一步阅读:[12. ECMAScript 6中的可调用实体](22.md),里面有着关于ES6 中IIFEs 和 IIAFs的更多信息。透露一下:你会很少使用它们,因为ES6有更好的选择。 ## 13.7 箭头函数与bind() ES6箭头函数通常是`Function.prototype.bind()`的令人关注的替代方案。 ### 3.7.1 提取方法 如果提取的方法是作为一个回调,你必须指定一个固定的`this`,否则它将被作为函数进行调用(`this`将是`undefined`或为全局对象)。例如: ```js obj.on('anEvent', this.handleEvent.bind(this)); ``` 另一种方法是使用箭头函数: ```js obj.on('anEvent', event => this.handleEvent(event)); ``` ### 13.7.2 `this`通过参数 以下代码演示了一个简单的技巧:对于某些方法,比如`filter()`,您不需要`bind()`这样的回调,因为它们允许您通过附加参数指定`this`值: ```js const as = new Set([1, 2, 3]); const bs = new Set([3, 2, 4]); const intersection = [...as].filter(bs.has, bs); // [2, 3] ``` 但是,如果使用箭头函数,此代码更容易理解: ```js const as = new Set([1, 2, 3]); const bs = new Set([3, 2, 4]); const intersection = [...as].filter(a => bs.has(a)); // [2, 3] ``` ### 13.7.3 部分求值(Partial evaluation) `bind()`使您能够进行部分求值,您可以通过填充现有函数的参数来创建新函数: ```js function add(x, y) { return x + y; } const plus1 = add.bind(undefined, 1); ``` 再次,我发现箭头函数更容易理解: ```js const plus1 = y => add(1, y); ``` ## 13.8 箭头函数与常规函数 一个箭头函数与一个普通的函数在两个方面不一样: * 下列变量的构造是词法的: `arguments` , `super` , `this` , `new.target` * 不能被用作构造函数:没有内部方法 `[[Construct]]` (该方法允许普通的函数通过 `new` 调用),也没有 `prototype` 属性。因此, `new (() => {})` 会抛出错误。 除此之外,箭头函数和普通的函数没有明显的区别。例如,`typeof` 和 `instanceof` 产生同样的结果: ```js > typeof (() => {}) 'function' > () => {} instanceof Function true > typeof function () {} 'function' > function () {} instanceof Function true ``` 参阅[12. ECMAScript 6中的可调用实体](22.md)的那一章,获取更多关于什么时候使用箭头函数和什么时候使用传统函数的信息。 ## 13.9 常见问题:箭头函数 ### 13.9.1 为什么ES6 中有“胖”箭头函数(`=>`),但没有“瘦”箭头函数(`->`)? ECMAScript 6 有具有词法(静态的)`this`的函数语法,即所谓的箭头函数。但是,具有动态`this`的函数没有箭头语法。这种遗漏是经过考虑的; 方法定义涵盖了大部分瘦箭头的用例。如果你真的需要动态的this,你仍然可以使用传统的函数表达式。