先记住这个答案
写成 handle = () => {} 的类字段会在每次创建实例时生成一个函数,作为实例自有属性保存。箭头函数捕获初始化时的 this,即使单独传给回调,仍指向原实例。写成 handle() {} 的方法位于类的原型上,多个实例共用同一个函数;它的 this 取决于调用方式。大量实例都要同一行为时,原型方法通常节省函数分配;需要稳定回调时,箭头函数字段更省心。
- 箭头函数字段属于实例,每个实例各有一份
- 原型方法由实例共享,调用时决定
this - 提取为回调时,箭头函数字段保留原实例
- 大量实例默认优先考虑原型方法
看函数放在哪、是否共享
同一个类创建两个按钮模型,分别取出它们的方法比较。箭头函数字段每个实例会初始化一次,所以引用不同;原型方法只定义一次,所以引用相同。这并不意味着箭头函数一定造成可见的性能问题,但在大量实例上会多分配函数和闭包。
class ButtonModel {
onClick = () => this.label // 每个实例各建一份
readLabel() { return this.label } // 定义在原型上
constructor(label) { this.label = label }
}
const a = new ButtonModel('保存')
const b = new ButtonModel('取消')
console.log(a.onClick === b.onClick)
console.log(a.readLabel === b.readLabel)查看输出与解释
false
true可以再看属性位置:Object.hasOwn(a, 'onClick') 是 true,而 readLabel 要沿原型链查找。实例字段覆盖了同名原型属性时,普通的原型方法调用也可能被它遮住。理解存放位置后,内存和覆盖行为就容易推出来。
把方法交给回调时的差异
如果写 setTimeout(model.readLabel, 0),这个函数作为回调被调用时已脱离 model,不能再指望它的 this 自动指向原对象。类方法在严格模式下执行,独立调用时 this 可能是 undefined,读取属性就会报错。model.onClick 则是初始化时捕获了 model 的箭头函数,传出去仍能访问它的字段。
class ButtonModel {
onClick = () => this.label
readLabel() { return this.label }
constructor(label) { this.label = label }
}
const model = new ButtonModel('保存')
const arrowCallback = model.onClick // 已捕获实例
const boundCallback = model.readLabel.bind(model) // 显式绑定
console.log(arrowCallback())
console.log(boundCallback())查看输出与解释
保存
保存在组件事件、定时器等需要频繁传方法引用的地方,箭头函数字段能少写一次 bind()。但也要看到,手动 bind() 同样会创建新函数;性能比较应看创建实例的规模和实际调用方式,而不是只凭语法长短下结论。
继承与初始化时机
原型方法允许子类通过同名方法覆盖,也可以用 super.method() 调用父类实现。箭头函数字段是实例属性,不是原型方法;不能把它当作父类原型上的 super.method() 来访问。若子类也声明同名字段,还要注意父类与子类字段各自的初始化顺序,后初始化的字段会替换先前的实例属性。
基类的实例字段会在构造器正文之前初始化,派生类字段在 super() 返回后初始化。原稿说箭头函数字段“可以在构造函数前捕获 this”,容易误导为任何时候都能提前使用 this。对子类来说,super() 之前 this 还不可访问;字段箭头函数也不可能在此之前初始化。
容易答错的地方
- 以为箭头函数写在类里就是原型方法
onClick = () => {}使用的是类字段语法。它创建的是实例属性,两个实例的函数引用不同。真正的原型方法是onClick() {}。两者在方法提取、共享和继承上都不同。- 以为原型方法传出去后总能记住实例
- 原型方法没有自动绑定原实例。写
model.method()时接收者是model;单独传model.method后,调用方可能以别的方式调用它。需要稳定接收者时,用bind()、包装箭头函数或实例箭头函数字段。
面试官还会怎么问?
给原型方法调用 bind() 后,还省内存吗?
原型上的原始函数仍由实例共享,但每次调用 bind() 都会产生新的绑定函数。如果每个实例都在构造器里绑定一次,分配开销就接近实例箭头函数字段;应按实际使用方式比较。
子类能用 super.onClick() 调父类的箭头函数字段吗?
不能按原型方法那样调用。父类箭头函数字段初始化在实例上,不在父类原型上;super.onClick 查找的是父类原型。需要可覆盖且可通过 super 复用的方法,应采用原型方法。
call() 能把箭头函数字段的 this 改成别的对象吗?
不能。箭头函数没有自己的动态 this 绑定;call()、apply() 或 bind() 传入的新接收者不会替换它已捕获的实例。原型方法则可以用这些方法显式指定接收者。
参考资料
示例用于理解所注明的运行环境与边界;延伸学习可结合原文中的更多案例。