JavaScript中箭头函数和普通函数的区别

一、前言

我写的基本所有文章都是面试题引发的,这篇也不例外哈哈

关于ES6中的箭头函数相信很多人在项目中都用过,但是确实没有深入研究过和普通函数有什么区别,被面试毒打过的我慢慢养成了喜欢深究的习惯,然而不在像在学校一样,“凡事多问几个为什么”,现在变成了,“凡是多查为什么”

对JavaScript的了解越深入,越发现这门语言的魅力

二、 箭头函数

箭头函数(Arrow Function),最早接触这个还是在大四实习的时候,那时候学了一点vue语法边开始做公司vue的项目,项目用的 vue-element-admin 这套继承方案,才了解到ES6新特性,箭头函数 ()=>{} 、对象展开运算符 ... 然而因为项目进度催得紧,就没有仔细看,那时候对箭头函数的理解仅仅停留在方便定义函数。

当时埋下的雷,在面试时都踩了,泪奔。。

本文主要围绕箭头函数几个特点展开,分别是 书写格式 , this问题 , 不可作为构造函数 , yield , arguments对象

1. 书写形式

首先是我最粗浅的理解,也就是为了书写方便

首先箭头函数就是函数的缩写,如

1
2
3
var f = function(num) {
return num
}

这是普通函数写法,换成ES6中箭头函数的写法就是

三种写法是等效的

1
2
3
4
5
var f = (num) => { return num }
// 参数只有一个,因此可以省略括号
var f = num => { return num }
// 如果语句只包含一个return语句,可以省略大括号
var f = num => num;

2. this指向问题(最核心)

首先上结论!!!重点理解这句,理解了这句,箭头函数也就掌握了70%

箭头函数是个寄生虫,他不会创建自己的 this,它只会从自己的 作用域链 上找父级 执行上下文 的 this

箭头函数的this是创建时确定的,普通函数的this是执行时确定的

引用阮一峰在箭头函数的文章中的一个例子

下面是 Babel 转箭头函数产生的 ES5 代码,就能清楚地说明this的指向。

1
2
3
4
5
function foo() {
setTimeout(() => {
console.log('id:', this.id);
}, 100);
}

经过Babel编译后,变为这样

1
2
3
4
5
6
7
function foo() {
var _this = this;

setTimeout(function () {
console.log('id:', _this.id);
}, 100);
}

能充分说明箭头函数this指向,这里还能说明一个问题,如果foo函数作用域中的this发生了改变,箭头函数的this也会发生改变,看下面这个例子

1
2
3
4
5
6
7
8
9
10
function foo() {
setTimeout(() => {
console.log('id:', this.id);
}, 100);
}
var id = 21;
let obj = {
id: 42
}
foo.call(obj);

这个例子中通过call方法显式的改变了foo的this,箭头函数的this也就会随之发生改变,也指向obj对象, 再看一个我觉得没啥意义的例子,不过刚开始没仔细看,被绕进去了

1
2
3
4
5
6
7
8
9
10
11
12
13
14
function foo() {
return () => {
return () => {
return () => {
console.log('id:', this.id);
};
};
};
}
var f = foo.call({id: 1});

var t1 = f.call({id: 2})()(); // id: 1
var t2 = f().call({id: 3})(); // id: 1
var t3 = f()().call({id: 4}); // id: 1

首先通过call改变foo的this指向,使之指向一个匿名对象{id:1},然后将foo函数返回的箭头函数赋值给一个变量f,在通过call改变这个函数变量f的this

写的有点绕,我觉得可以从两个方面理解这个问题

第一种:其实f的本质是箭头函数,f保存的是箭头函数的引用,箭头函数不能通过call和apply改变this作用域,所以失败

第二种:箭头函数的作用域其实是指向foo函数执行上下文的this,只有改变foo函数this,箭头函数的this才会改变,而最后三行都在改变这个箭头函数的this,显然达不到目的,this也就没发生改变

看一个反例:

1
2
3
4
5
6
7
8
9
10
const cat = {
lives: 9,
jumps: () => {
this.lives--;
}
}
cat.jumps();
cat.jumps();
cat.jumps();
console.log(cat.lives); // 9

解释一下为什么?

3. 不可当作构造函数

其实这个和第一个点一样,都是和this有关,因为在通过new构造一个对象的时候,要改变this作用域,将this指向新创建的对象,然而构造函数没有自己的this,因此不可以,而且这么写代码会报错

1
2
3
4
var Person = () => {
this.name = "111";
}
var person = new Person(); // Uncaught TypeError: Person is not a constructor

还有一个需要注意的点,有人也把这个当成箭头函数和普通函数的区别,不过我认为和这个其实说的是一个意思,因为不能作为构造函数,也就不具有prototype,因为原型链的绑定就是通过new关键字(没深究过,觉得这句话应该没问题)

除了不能作为构造函数,在箭头函数中还不能使用super以及new.target

new.target是用来检测构造函数是否通过new关键字调用的,例:

1
2
3
4
5
6
7
8
9
function fun() {
if (new.target) {
console.log("是通过new构造的")
} else {
throw new Error("fun必须通过new调用")
}
}
var e = new fun(); // 是通过new构造的
var f = fun(); // Uncaught Error: fun必须通过new调用

super在class部分用的比较多,和java中的super关键字差不多,都是调用父类的方法,直接通过super()代表调用父类构造方法(java继承思想),super.test() 代表调用父类的test方法

4. 没有arguments

阮一峰在ES6系列文章中曾说不可以使用arguments对象,有些人认为使用会报错,其实不是,在箭头函数中可以使用arguments对象,只是arguments对象也指向外层的arguments

作为替代,可以使用rest参数,如下例:

1
2
3
4
5
6
function fun() {
return (...rest) => {
console.log(rest);
}
}
fun()(1, 2, 3, 4);

上述箭头函数中通过…rest接受了外界传递的参数,和arguments效果差不多,

5. yield问题

箭头函数中不能使用yield关键字,因此不能作为Generate函数

后续在补充

三、总结

虽然总结了五条不同点,但是最核心的应该还是this的问题,通过this问题可以抓出构造函数以及arguments的问题,yield目前用的还不太多,大概做个了解,后边在补充,在就是书写形式上比普通函数简化了很多