文章目录
一. 基本语法
ES6标准中新增了一种更加简洁的匿名函数书写方式 —— 箭头函数(Arrow Function),一般用做函数表达式或者回调函数。基本语法是:
( 形参列表 ) => { 执行体 }
// 变量 = 函数表达式
let add = (a,b) => {
return a + b;
};
// 回调函数
["zevin",21,"code"].forEach(item => console.log(item));
——————OUTPUT——————
zevin
21
code
二. 简写规则
箭头函数最特色的当然是它的简写规则:
- 当函数参数只有一个,圆括号
( )
可以省略;但是没有参数时,圆括号( )
不可以省略。
var num = a => {
return a + 1;
};
var index = () => {
return true;
};
- 当执行体只有一行return语句时,可以省略大括号
{ }
;有多行代码时大括号{ }
就不能省略。
var num = a => {
return a + 1;
};
// 等价于
var num = a => a + 1;
var index = () => {
var str = "hello world";
return str;
};
// {}不可省略
结合一下上述的两个极端条件:只有一个参数x,返回x值。你就可以看到最最简洁的写法:
var a = x => x;
- 如果返回一个对象,需要特别注意,如果是单表达式要返回自定义对象,不写括号会报错,因为和函数体的{ … }有语法冲突。
注意,用小括号包含大括号则是对象的定义,而非函数主体。
x => {key: x} // 报错
x => ({key: x}) // 正确
三. 内部属性(this,arguments)的指向
很多人都关注到了箭头函数带来的语法上的简洁,其实箭头函数最大的设计痛点是解决了以往函数this
动态绑定带来的编程问题。还记得今年前半年在家自己做小游戏的时候,也遇到了这个问题,当时也只是百度知道了解决的办法,但是其中的原理并不清楚。
截一段当时的代码:
这段代码作用是在父节点上添加一个预制体的子节点,当时就是this的指向问题一直报错,最后找到了hack写法:
var that = this;
或者图片上的.bind(this)
的写法。对于这个问题,箭头函数 =>
就是一个很好的替代。
接下来步入正题:
3.1 内部属性取决于词法作用域(上下文)
箭头函数本身并没有内部属性(this,arguments),具体的this
和arguments
取决于词法作用域,由上下文确定,总是指向外部调用者。
来比较一下传统函数和箭头函数的区别:
const outObj = {
name: 'zevin',
anonFunction: function() {
const inAnon = function() {
console.log(this.name);
console.log(arguments);
console.log(this);
};
return inAnon();
},
arrowFunction: function() {
const inArrow = () => {
console.log(this.name);
console.log(arguments);
console.log(this);
};
return inArrow();
}
};
上述代码中,我们在外部对象outObj
中定义了了两个方法,分别返回内部函数的执行结果。区别在于anonFunction
方法中的inAnon
函数使用传统函数表达式定义,而arrowFunction
方法中的inArrow
函数使用箭头函数表达式定义。
现在我们用同样的参数来分别调用这两个方法:
outObj.anonFunction("hello","world");
——————OUTPUT——————
undefined
[Arguments] {}
Object [global] {
global: [Circular],
clearInterval: [Function: clearInterval],
clearTimeout: [Function: clearTimeout],
setInterval: [Function: setInterval],
setTimeout: [Function: setTimeout] {
[Symbol(nodejs.util.promisify.custom)]: [Function]
},
queueMicrotask: [Function: queueMicrotask],
clearImmediate: [Function: clearImmediate],
setImmediate: [Function: setImmediate] {
[Symbol(nodejs.util.promisify.custom)]: [Function]
}
}
outObj.arrowFunction("hello","world");
——————OUTPUT——————
zevin
[Arguments] { '0': 'hello', '1': 'world' }
{
name: 'zevin',
anonFunction: [Function: anonFunction],
arrowFunction: [Function: arrowFunction]
}
可以看到,两者的输出结果截然不同:
传统函数表达式定义的inAnon
函数——内部this
指向全局对象global
(我这里是在Node.js环境下测试的所以全局对象为global
,浏览器环境下的话就为Window
),也就没有了创建它时调用的参数(arguments
输出为空对象),全局对象下没有定义name
变量,理所应当输出undefined
。
箭头函数表达式定义的inArrow
函数——内部属性this
和arguments
都继承自了它的父函数arrowFunction
。也就证明了箭头函数中的this
和arguments
取决于词法作用域,由上下文确定,总是指向外部调用者。
3.2 封闭的词法作用域
箭头函数的this按照词法作用域绑定好之后就固定了,之后无法再通过call()
,apply()
或者bind()
修改this的值了,这些方法传入的新作用域参数会被无视。
这里就以call()
为例:
const outObj = {
name: 'zevin',
anonFunction: function() {
const inAnon = function() {
console.log(this.name);
};
return inAnon.call({name:"code"});
},
arrowFunction: function() {
const inArrow = () => {
console.log(this.name);
};
return inArrow.call({name:"code"});
}
};
这里都通过call()
方法重新指定了新的作用域{name:"code"}
,可以看到传统函数表达式定义的inAnon
函数作用域被修改了,而使用箭头函数表达式的inArrow
函数依旧输出了zevin
,说明新指定的作用域被无视了。
outObj.anonFunction();
outObj.arrowFunction();
——————OUTPUT——————
code
zevin
四. 正确使用场景
4.1 各种接收回调函数作为参数的API
就比如之前各种数组遍历相关的API,至于这部分API的详细讲解请看那篇数组专题:
【JavaScript笔记(六)】Array全家桶(引用数据类型中的数组 / Array对象 / Array.prototype)
这里就以大家 “挺熟悉” 的Array.prototype.map()
为例,哈哈哈
console.log([1,2,3].map((item,index) => item * index));
——————OUTPUT——————
[ 0, 2, 6 ]
这里传入的匿名函数作用为返回每一项数组元素(item)与其位置下标(index)的乘积。从这里就能看出箭头函数的代码之简洁。
当然选择map()
方法为例,主要是他还可以接受第二个参数:回调函数的this作用域。
我们刚才讲了箭头函数的this指向在代码编写阶段就已经确定,不允许后期更改。这里就再检验一下:
let arr = ['a','b','c']
console.log([1,2,3].map(function(){return this},arr));
console.log([1,2,3].map(() => this,arr));
——————OUTPUT——————
[ [ 'a', 'b', 'c' ], [ 'a', 'b', 'c' ], [ 'a', 'b', 'c' ] ]
[ {}, {}, {} ]
输出结果看,普通回调函数每次遍历的this指向都已经变成了arr数组;而箭头函数内部的this
并没有改变,还是空对象(也验证了箭头函数中没有this
的结论)。
4.2 Promise和Promise链
在编写异步程序的时候,箭头函数也会让代码更加直观和简洁。Promise
可以更简单的编写异步程序。虽然实际中还是async/await
用的多,但是promise
作为基础中的基础也需要好好理解。
promise
中需要我们去定义大量的代码执行完成之后的回调函数,这里就需要箭头函数大展身手了。特别是如果回调函数是有状态的,同时想引用对象中的某些内容。
this.doSomethingAsync().then((result) => {
this.storeResult(result);
});
4.3 对象转换
箭头函数的另一个常见而且十分有用的地方就是用于封装的对象转换。 例如在Vue.js
中,有一种通用模式,就是使用mapState
将Vuex
存储的各个部分,直接包含到Vue
组件中。 这涉及到定义一套mappers
,用于从原对象到完整的转换输出,这在组件问题中实十分有必要的。 这一系列简单的转换,使用箭头函数是最合适不过的。比如:
export default {
computed: {
...mapState({
results: state => state.results,
users: state => state.users,
});
}
}
五. 使用雷区
有时候一个事物的优点恰恰也可能是它的缺点。箭头函数也是如此,正是因为它鲜明的特性——this
的非动态绑定和语法简洁,可能就会造成 this
提前绑定和代码难以理解 的问题。
5.1 需要动态上下文的回调函数
箭头函数中的this指向相对固定,所以在动态的上下文环境下,相较常规的this
动态绑定,箭头函数中被提前绑定了作用域的this
就显得很麻烦了。
比如很常见的事件绑定操作:
(这里以原生JS中的DOM操作和jQuery中的操作对比为例)
我们简单的设定一个需求:点击按钮替换按钮上的文字。先看最基础的DOM操作:
<body>
<button id="anonButton">传统函数</button>
<button id="arrowButton">箭头函数</button>
<script>
var $anonButton = document.getElementById("anonButton");
var $arrowButton = document.getElementById("arrowButton");
$anonButton.onclick = function(event){
console.log(this);
this.innerText = "hello world";
};
$arrowButton.onclick = (event) => {
console.log(this);
this.innerText = "hello world";
};
</script>
</body>
可以看到传统函数的按钮的文字替换成功了,是因为事件函数内部this
动态绑定了当前目标按钮(控制台this
打印的是当前dom
对象);而使用箭头函数的按钮并没有完成需求,此时的this
还是提前绑定的全局对象window
,与按钮点击事件无关。
再来看jQuery中的操作,也是大同小异:
<body>
<button id="anonButton">传统函数</button>
<button id="arrowButton">箭头函数</button>
<!-- 引入jQuery -->
<script src="https://cdn.bootcdn.net/ajax/libs/jquery/1.12.4/jquery.js"></script>
<script>
$(function(){
$('#anonButton').click(function(){
console.log(this);
this.innerText = "hello world";
});
$('#arrowButton').click(() => {
console.log(this);
this.innerText = "hello world";
});
})
</script>
</body>
结果仍然是只有传统函数表达式完成了需求,需要注意的是这里由于jQuery
对于原生DOM
操作的封装,箭头函数的内部this
指向#document
根节点。
同理之后在vue
中, methods
和 computed
中的 this
指向的是vue
的组件。
5.2 不可用于构造函数定义
声明构造函数时的this
指向新创建的对象实例。以下是正常的function
关键字声明的构造函数:
function Parent (name) {
this.name = name;
};
var child = new Parent("zevin");
console.log(child.name);
——————OUTPUT——————
zevin
如果换成箭头函数呢 ?
var Parent = (name) => {
this.name = name;
};
var child = new Parent("zevin");
console.log(child.name);
——————OUTPUT——————
TypeError: Parent is not a constructor
这种情况会报错,原因也是构造函数在创建的时候this
就绑定了,不会再指向实例对象了。
5.3 语句简单但逻辑复杂的函数
这样的情形下,并不是说不能用箭头函数,只是这样的代码虽然看着很简洁,但是后期这样的代码会让维护人员非常头秃。来看一个例子:
let num = (a,b) => b === undefined ? b => a * b : a * b;
let oneItem = num(3);
console.log(oneItem.toString());
console.log(oneItem(4));
console.log(num(3,4));
——————OUTPUT——————
b => a * b
12
12
这里的num函数作用是传入一个参数时返回一个可接受一个参数b返回a,b乘积;传入两个参数时直接返回乘积。写成常规写法就是:
let num = function(a,b){
if(b === undefined){
return function(b){
return a*b;
};
}else{
return a*b;
};
};
六. 总结
箭头函数虽然写起来很苏福,但是BUG也是真难改。所以箭头函数也要学会正确使用,切忌不可一味的全用箭头函数。