先记住这个答案
区别在调用方式和存放位置。普通方法 toF() 是一个值为函数的数据属性,要写 t.toF() 调用;getter 和 setter 伪装成普通属性,读 t.f 就调用 getter,写 t.f = 212 就调用 setter。class 里的 get f() 和 set f() 会在 Temperature.prototype 上定义一个访问器属性,它的描述符是 { get, set, enumerable: false, configurable: true },和用 Object.defineProperty 定义的访问器是同一种东西,只是 class 语法帮你把 enumerable 和 configurable 设好了。
- getter 读属性时调用,普通方法要手动加括号调用
class的 getter 定义在原型上,不在实例上- 访问器描述符有
get、set等字段 - 只写 getter 不写 setter,赋值在严格模式下抛错
看代码:getter 像属性,方法像函数
用摄氏度和华氏度换算做例子,实例里只存摄氏度,华氏度通过 getter 算出来。
class Temperature {
constructor(c) {
this._c = c
}
get f() {
return this._c * 1.8 + 32 // 读 t.f 时自动调用
}
set f(value) {
this._c = (value - 32) / 1.8 // 执行 t.f = 212 时自动调用
}
toF() {
return this._c * 1.8 + 32 // 普通方法,要加括号调用
}
}
const t = new Temperature(0)
console.log(t.f, t.toF())
t.f = 212
console.log(t._c, Object.keys(t).join())
const d = Object.getOwnPropertyDescriptor(Temperature.prototype, 'f')
console.log(typeof d.get, typeof d.set, d.enumerable, d.configurable)
console.log(Object.hasOwn(t, 'f'))查看输出与解释
32 32
100 _c
function function false true
false第二行说明 t.f = 212 并没有在实例上新建一个 f 属性,而是调用了 setter,改的是 _c。实例自身的键只有 _c。第三行就是答案的核心:原型上的 f 是访问器属性,描述符里没有 value 和 writable,只有 get 和 set。最后一行再确认一次,f 不在实例上。
访问器属性和数据属性的区别
属性分两种。数据属性存一个值,描述符有 value 和 writable;访问器属性不存值,描述符有 get 和 set 两个函数。两种属性都有 enumerable 和 configurable。一个属性只能是其中一种,同时写 value 和 get 会抛 TypeError。
普通方法属于数据属性,只不过值碰巧是函数。所以 t.toF 拿到的是函数本身,而 t.f 拿到的是 getter 的返回值,你没有办法通过 t.f 拿到 getter 函数,只能通过描述符拿。
什么时候用 getter 而不是方法
getter 适合「看起来是属性、实际需要计算」的场景,比如派生值 fullName、area,或者给内部字段加一层只读外壳。调用方不用关心它是存的还是算的,以后把存储字段改成计算字段也不用改调用处。
但 getter 应该便宜、没有副作用。每次读取都会重新执行,如果里面有耗时计算、网络请求,或者每次返回新对象,调用方很容易在循环里无意中重复触发。这类逻辑写成方法更诚实,括号本身就在提醒「这里会做事」。Vue 的 computed 就是在 getter 的基础上加了缓存,解决的正是重复计算的问题。
容易答错的地方
- 以为 getter 定义在实例上
class里的 getter 在原型上,所有实例共享。Object.keys(t)、JSON.stringify(t)都看不到它,要序列化得自己写toJSON。对象字面量里的 getter 才在对象自身,而且是可枚举的。- 在 setter 里给同名属性赋值
set f(v) { this.f = v }会再次触发 setter,无限递归直到栈溢出。要把值存到另一个名字上,比如_f或者私有字段#f。- 以为
Object.defineProperty的默认值和class一样 Object.defineProperty(obj, 'f', { get() {} })没写的特性全部默认为false,既不可枚举也不可配置,之后不能删也不能重新定义。class的 getter 默认可配置。
面试官还会怎么问?
实例上已经有同名数据属性,getter 还会生效吗?
不会。读属性时先找实例自身,找到数据属性 f 就直接返回,不会继续到原型上找 getter。常见于类字段写了 f = 0,同时又定义了 get f(),字段会在实例上把 getter 挡住。
只有 getter 没有 setter,赋值会怎样?
赋值找到的是原型上 set 为 undefined 的访问器,写入失败。严格模式下抛 TypeError,非严格模式静默失败。ES 模块和 class 的方法体都是严格模式,所以现代项目里通常会直接看到报错。
静态 getter 放在哪里?
static get count() 定义在类本身,也就是构造函数对象上,用 Temperature.count 访问,实例访问不到。
参考资料
示例用于理解所注明的运行环境与边界;延伸学习可结合原文中的更多案例。