普通函数和箭头函数最容易被问到的区别,还是 this。但只说一句“箭头函数没有自己的 this”还不够,实际还会牵扯到 arguments、prototype、能不能被 new,以及适合用在哪里。
我一般会把它们当成两种不同定位的函数来记:普通函数更完整,可以作为方法、构造函数使用;箭头函数更轻,适合回调和保留外层 this。
this 指向不同
普通函数的 this 是调用时决定的:
const obj = {
name: 'Alice',
sayHi: function () {
console.log(this.name);
},
};
obj.sayHi(); // Alice
这里是 obj.sayHi() 调用,所以 this 指向 obj。
箭头函数没有自己的 this,它会捕获定义时外层作用域的 this:
const obj = {
name: 'Alice',
sayHi: () => {
console.log(this.name);
},
};
obj.sayHi(); // 通常不是 Alice
这也是为什么我不太会在对象方法里写箭头函数。它不会因为被 obj 调用就把 this 变成 obj。
箭头函数不能作为构造函数
普通函数可以配合 new 创建实例:
function User(name) {
this.name = name;
}
const user = new User('Alice');
箭头函数不行:
const User = (name) => {
this.name = name;
};
new User('Alice'); // TypeError: User is not a constructor
因为箭头函数没有自己的 this,也没有作为构造函数需要的内部机制。
prototype 不同
普通函数有 prototype,可以给实例挂原型方法:
function User(name) {
this.name = name;
}
User.prototype.sayHi = function () {
console.log(this.name);
};
箭头函数没有 prototype:
const fn = () => {};
console.log(fn.prototype); // undefined
所以如果我要写构造函数、原型方法,普通函数才是合适的选择。
arguments 不同
普通函数有自己的 arguments:
function fn() {
console.log(arguments);
}
fn(1, 2, 3);
箭头函数没有自己的 arguments,它拿到的是外层函数的 arguments:
function outer() {
const arrow = () => {
console.log(arguments);
};
arrow(1, 2);
}
outer(10, 20); // 输出的是 outer 的 arguments
如果箭头函数里要拿参数,我更倾向于直接用剩余参数:
const fn = (...args) => {
console.log(args);
};
事件监听里也要注意
如果需要在事件回调里通过 this 获取当前元素,普通函数更合适:
button.addEventListener('click', function () {
console.log(this); // button
});
箭头函数里的 this 不会指向按钮:
button.addEventListener('click', () => {
console.log(this); // 来自外层作用域
});
当然,如果用 event.currentTarget,就不一定依赖 this 了:
button.addEventListener('click', (event) => {
console.log(event.currentTarget);
});
实际项目里怎么选
我的习惯是:
- 对象方法、构造函数、原型方法:用普通函数。
- 需要动态
this的场景:用普通函数。 - 回调、短函数、数组方法:可以用箭头函数。
- 需要保留外层
this的场景:箭头函数更方便。
比如在类组件或者某些回调里,箭头函数可以避免手动 bind;但在对象方法和事件普通回调里,如果想依赖当前调用对象,就不要随便换成箭头函数。
所以它们不是谁替代谁,而是适合的场景不同。普通函数更完整,箭头函数更轻量。只要记住“普通函数的 this 看调用,箭头函数的 this 看外层”,大部分题就能判断出来。