ES6:箭头函数与普通函数的区别
Category(分类): JavaScript Status: 已更新
原文作者:长安曹公子 历史文章:ES6 - 箭头函数、箭头函数与普通函数的区别
本文保留原文“先介绍语法,再比较
this、arguments、构造函数和prototype”的结构。示例中的bash代码块、评论粘连和浏览器/Node.js 环境差异已修正。箭头函数属于 ECMAScript 2015;类字段等现代语法会明确标注其标准版本或运行环境。
一、基本语法
ES2015 允许使用箭头 => 定义函数。箭头函数的参数写在箭头左侧,表达式或函数体写在箭头右侧:
const greet = (name) => {
return `Hello ${name}!`
}
// 等同于
const greetAgain = function (name) {
return `Hello ${name}!`
}
箭头函数的参数有以下写法:
const noArgs = () => {
console.log('没有参数')
}
const oneArg = name => {
console.log(`Hello ${name}!`)
}
const manyArgs = (value1, value2, value3) => {
return [value1, value2, value3]
}
// 有默认值、解构参数或 rest 参数时必须使用括号
const withDefault = (name = '匿名') => name
const withRest = (...values) => values.length
const withDestructuring = ({ id }) => id
1. 表达式函数体和块级函数体
箭头函数只有一个表达式时,可以省略花括号和 return:
const identity = value => value
const sum = (a, b) => a + b
// 等同于
const sumAgain = function (a, b) {
return a + b
}
如果函数体需要多条语句,就使用花括号;只有需要返回值时才必须显式写 return:
const add = (a, b) => {
const result = a + b
return result
}
const logOnly = value => {
console.log(value)
// 没有 return,因此返回 undefined
}
2. 返回对象字面量
对象字面量要用圆括号包起来,否则箭头后的 {} 会被解析为函数体:
const getItem = id => ({
id,
name: 'Temp'
})
console.log(getItem(1)) // { id: 1, name: 'Temp' }
const wrong = id => { id: id }
console.log(wrong(1)) // undefined,不是一个对象字面量
原文还用 void 表达“不需要返回值”的场景:
const callWithoutResult = () => void doSomething()
void 会计算 doSomething(),但让整个表达式的结果变成 undefined。它不是箭头函数专属语法;如果只是调用副作用函数,下面的块体写法通常更直观:
const callWithoutResult = () => {
doSomething()
}
3. 常见回调写法
const squares = [1, 2, 3].map(value => value * value)
const sorted = [2, 5, 1, 4, 3].sort((a, b) => a - b)
const evenNumbers = [1, 2, 3, 4].filter(value => value % 2 === 0)
箭头函数也可以是异步函数:
const loadUser = async id => {
const response = await fetch(`/api/users/${id}`)
if (!response.ok) throw new Error(`HTTP ${response.status}`)
return response.json()
}
二、箭头函数与普通函数的区别
1. 语法更简洁,但不是普通函数的完全替代品
箭头函数适合短小的回调和需要捕获外层 this 的函数;普通函数仍然适合:
- 需要动态
this的方法或回调; - 需要
arguments、prototype或new的函数; - 需要写生成器
function*的函数; - 希望通过函数声明表达一个可构造的类型时。
“写得更短”不等于“语义完全相同”。
2. 箭头函数没有自己的 this
普通函数的 this 取决于调用方式;箭头函数没有自己的 this,会从定义它的词法环境中捕获 this。因此,调用箭头函数时改变调用形式不会重新绑定 this:
function createArrow() {
const arrow = () => this
return arrow
}
const arrow = createArrow.call({ name: 'outer' })
const result = arrow.call({ name: 'inner' })
console.log(result.name) // outer
arrow.call({ name: 'inner' }) 中的 call 仍然执行了函数,但传入的 this 不会覆盖箭头函数已经捕获的 this。
原文中 setTimeout 示例的边界
原文用下面的方式说明箭头函数可以保留外层方法的 this:
function Controller(id) {
this.id = id
}
Controller.prototype.start = function () {
setTimeout(() => {
console.log(this.id)
}, 0)
}
new Controller('Obj').start() // Obj
如果把回调改成普通函数,回调的 this 会由宿主的计时器 API 决定。在浏览器中通常是计时器所属的 Window,在 Node.js 中则可能是计时器对象;不要把这种环境行为概括成“普通函数永远指向 Window”。如果需要稳定的 this,可以使用箭头函数、保存变量或显式 bind:
Controller.prototype.startWithBind = function () {
setTimeout(function () {
console.log(this.id)
}.bind(this), 0)
}
3. 对象字面量不会形成新的 this 环境
下面的普通方法在以 obj.method() 调用时,this 是 obj;箭头属性则捕获定义它时外层环境的 this,不是对象字面量本身:
function createObject() {
return {
id: 'OBJ',
regular() {
return this.id
},
arrow: () => this.id
}
}
const obj = createObject.call({ id: 'OUTER' })
console.log(obj.regular()) // OBJ
console.log(obj.arrow()) // OUTER
如果箭头是在 ES module 顶层定义的,顶层 this 始终是 undefined;如果是在浏览器经典脚本顶层定义的,结果又可能与 globalThis 有关。因此,原文中直接写 GLOBAL 或 Window 只适用于特定的浏览器经典脚本环境,不能当作跨环境结论。
4. call、apply、bind 不能改变箭头函数的 this
对普通函数,这些方法可以改变调用时的 this:
function getName(prefix) {
return `${prefix}${this.name}`
}
const person = { name: 'Alice' }
console.log(getName.call(person, 'Hi ')) // Hi Alice
console.log(getName.apply(person, ['Hello '])) // Hello Alice
console.log(getName.bind(person, 'Welcome ')()) // Welcome Alice
对箭头函数,它们不能改变词法 this,但 call/apply 仍会传入参数,bind 仍会预先绑定参数:
function createGetter() {
return () => this.name
}
const getter = createGetter.call({ name: 'outer' })
console.log(getter.call({ name: 'inner' })) // outer
console.log(getter.bind({ name: 'another' })()) // outer
5. 箭头函数不能作为构造函数
使用 new 调用普通基类构造函数时,可以把过程简化为创建实例、设置原型并把构造调用的 this 指向实例;如果构造函数显式返回对象,或涉及派生类和 super(),还存在额外规则。箭头函数没有 [[Construct]] 内部方法,因此不能被 new 调用:
const Person = (name) => {
this.name = name
}
new Person('Alice') // TypeError: Person is not a constructor
如果需要构造实例,可以使用普通函数或 class:
class Person {
constructor(name) {
this.name = name
}
}
console.log(new Person('Alice').name) // Alice
6. 箭头函数没有自己的 arguments
箭头函数访问 arguments 时,会沿词法作用域向外寻找:
function outer(first, second) {
const arrow = () => arguments
return arrow()
}
const args = outer('a', 'b')
console.log(args[0], args[1]) // a b

该截图是原文历史配图,仅用于辅助理解;当前结论以代码示例和 ECMAScript/MDN 规范为准。
如果箭头函数本身需要接收任意数量的参数,应使用 rest 参数:
const collect = (...values) => values
console.log(collect(1, 2, 3)) // [1, 2, 3]
原文中在全局箭头函数里直接访问 arguments 会报错,但错误原因取决于所在环境:ES module 顶层通常没有 arguments,CommonJS 模块或其他函数作用域可能存在外层绑定。准确的说法是“箭头函数没有自己的 arguments”。
7. 箭头函数没有自己的 prototype
const sayHi = () => {
console.log('Hello World')
}
console.log(sayHi.prototype) // undefined
console.log('prototype' in sayHi) // false
普通函数声明通常具有 prototype 属性,可以用作构造函数;箭头函数没有该属性,也不能通过它给实例设置原型。
8. 箭头函数不能作为生成器函数
生成器必须使用 function* 或对象/类中的生成器方法语法,箭头函数不能直接使用 yield:
function* numbers() {
yield 1
yield 2
}
console.log([...numbers()]) // [1, 2]
// const invalid = () => { yield 1 } // SyntaxError
原文中的 yeild 是拼写错误,应为 yield。箭头函数内部可以嵌套一个独立的生成器函数,但箭头函数自己不会因此变成生成器。需要异步逐项产出数据时可以使用异步生成器,但仍必须使用 async function* 或 async *method():
async function* values() {
yield 1
}
三、事件回调应该选哪一种?
1. 需要事件目标作为 this 时使用普通函数
addEventListener 调用普通函数监听器时,浏览器会把监听器的 this 设为触发事件的 EventTarget(严格模式下仍遵循事件监听器规则):
button.addEventListener('click', function () {
this.textContent = 'Clicked'
})
箭头监听器不会获得事件目标作为自己的 this:
button.addEventListener('click', () => {
// 这里的 this 是外层词法 this,不是 button
console.log(this)
})
但是,如果需要的是类实例的 this,箭头回调反而很方便:
class Counter {
count = 0
constructor(button) {
this.button = button
this.handleClick = this.handleClick.bind(this)
button.addEventListener('click', this.handleClick)
}
handleClick() {
this.count += 1
this.button.textContent = String(this.count)
}
destroy() {
this.button.removeEventListener('click', this.handleClick)
}
}
这里保存绑定后的函数引用是关键:每次调用 bind 都会返回新函数,不能在移除监听器时再次写 this.handleClick.bind(this)。
2. 类字段箭头函数是现代写法之一
类字段在 ECMAScript 2022 标准化。它可以让每个实例拥有一个捕获实例 this 的箭头函数:
class Counter {
count = 0
handleClick = () => {
this.count += 1
}
}
这和原型上的普通方法不同:箭头函数字段会为每个实例创建函数和闭包,可能占用更多内存;原型方法通常更节省,但需要手动绑定或在回调处使用箭头函数。应根据组件数量、性能和可读性选择,而不是笼统地说箭头函数“更好”。
原文使用的 React componentWillMount 已经被废弃,不应在新代码中继续使用。React 事件处理器通常直接使用类字段、构造器 bind 或函数组件闭包即可。
四、适用场景总结
优先考虑箭头函数
map、filter、reduce、Promise 等短小回调;- 需要捕获外层方法
this的定时器、事件或异步回调; - 函数不需要构造实例、
arguments或动态this。
优先考虑普通函数
- 对象方法、原型方法或需要动态
this的 API 回调; - 需要用
new创建实例的构造函数; - 需要
arguments、prototype或生成器能力; - 想让函数的调用方式明确表达“
this来自调用者”。