在 JavaScript ES6 及其后续版本中,可以使用 访问器属性(Accessor Properties) 来定义 getter 和 setter,从而拦截对象属性的访问和赋值。这种机制不仅提供了更灵活的属性管理方式,还能增强数据的封装性和安全性。
1. 访问器属性(Getters 和 Setters)
在 ES6 的 class 语法中,可以使用 get 和 set 关键字定义访问器属性,这样可以像访问普通属性一样调用方法,而不需要加 ()。
1.1 基本用法
class Person {
constructor(name, age) {
this._name = name; // 内部存储,通常用下划线命名
this._age = age;
}
// Getter: 读取时触发
get name() {
console.log('调用了 getter');
return this._name;
}
// Setter: 赋值时触发
set name(newName) {
console.log('调用了 setter');
if (typeof newName !== 'string') {
throw new Error('名字必须是字符串');
}
this._name = newName;
}
}
const p = new Person('张三', 25);
console.log(p.name); // 调用了 getter -> 张三
p.name = '李四'; // 调用了 setter
console.log(p.name); // 调用了 getter -> 李四
在 class 内部:
- get name() 让 name 变成一个计算属性,调用 p.name 时自动执行 get 逻辑。
- set name(newName) 赋值 p.name = '新名字' 时,自动执行 set 逻辑,而不会直接修改 this._name。
1.2 高级用法
1、只读属性(只定义 get,不定义 set)
class Circle {
constructor(radius) {
this.radius = radius;
}
get area() {
return Math.PI * this.radius ** 2;
}
}
const c = new Circle(10);
console.log(c.area); // 314.1592653589793
c.area = 500; // 无效赋值
console.log(c.area); // 314.1592653589793
注意:避免无意义的 set 操作。如果 set 只是赋值而不做额外逻辑,建议直接使用普通属性。
2、计算属性
class User {
constructor(name) {
this._name = name;
}
get upperCaseName() {
return this._name.toUpperCase();
}
}
const user = new User('hello');
console.log(user.upperCaseName); // HELLO
3、访问器属性 vs Object.defineProperty
在 ES5 时代,可以使用 Object.defineProperty() 手动定义访问器:
const person = {};
Object.defineProperty(person, 'name', {
get() {
return '张三';
},
set(value) {
console.log('新值:', value);
},
});
console.log(person.name); // 张三
person.name = '李四'; // 新值: 李四
但 class 里,get / set 语法更加简洁。
1.3 注意 !important
1、访问器属性不会出现在 Object.keys() 和 for...in 循环中(不可枚举)。
举个 🌰
class User {
constructor(name) {
this._name = name;
}
get name() {
return this._name;
}
}
const user = new User('Alice');
console.log(Object.keys(user)); // [ '_name' ]
_name 是一个普通的实例属性,而 name 是通过 getter 定义的访问器属性。
2、访问器属性的优先级比实例属性高
因为 JavaScript 在对象上查找属性时,会先检查原型链,如果在原型链上找到了 getter 或 setter,那么它就会被触发,而不会直接在实例对象上创建新的属性。
当访问器属性和实例属性同名时,赋值操作会触发访问器的 setter,而不会直接设置实例属性。因此,在 constructor 中使用 this.name = name 时,实际上会调用访问器的 setter 方法,而不是直接设置实例的 _name。
举个 🌰
class User {
constructor(name) {
console.log('构造函数被调用');
this.name = name; // 这里调用了 setter
}
get name() {
console.log('访问器属性的 getter 被调用');
return this._name;
}
set name(value) {
console.log('访问器属性的 setter 被调用');
this._name = value;
}
}
const user = new User('Alice');
console.log(Object.getOwnPropertyDescriptor(user, 'name')); // undefined
console.log(Object.getOwnPropertyDescriptor(Object.getPrototypeOf(user), 'name'));
console.log(user._name);
console.log(user.name);
user.name = 'Bob';
console.log(user._name);
console.log(user.name);
执行结果:
构造函数被调用
访问器属性的 setter 被调用
undefined
{
get: [Function: get name],
set: [Function: set name],
enumerable: false,
configurable: true
}
Alice
访问器属性的 getter 被调用
Alice
访问器属性的 setter 被调用
Bob
访问器属性的 getter 被调用
Bob
Object.getOwnPropertyDescriptor 查看 name 的属性描述符,看看访问器属性和实例属性的区别,如上。
3、getter 里不要使用 this 访问未定义的属性
如果在 getter 里访问 this 上不存在的属性,可能会导致 死循环 或 意外错误。
const obj = {
get value() {
return this.value + 1; // ❌ 这里会导致递归调用自身
}
};
console.log(obj.value); // ❌ RangeError: Maximum call stack size exceeded
❗ 原因:访问 obj.value 时,getter 被调用,它又去访问 obj.value,导致无限递归。
正确操作
const obj = {
_value: 10,
get value() {
return this._value + 1; // ✅ 正确使用私有属性
}
};
console.log(obj.value); // 11
4、Object.assign() 处理 getter/setter 的行为(细节)
const source = {
_name: '张三',
get name() {
console.log('触发 getter');
return this._name;
},
set name(value) {
console.log('触发 setter');
this._name = value;
}
};
const target = Object.assign({}, source);
console.log(target.name); // ✅ '张三',触发 getter
target.name = '李四'; // ❌ 只是普通赋值,不会触发 setter
console.log(target.name); // ❌ '李四',不会触发 getter
返回结果:
触发 getter
张三
李四
为什么会出现上面的情况呢?
Object.assign() 只会 触发 getter 取值,而不会复制 setter,这是因为 Object.assign() 的工作方式是基于 "数据属性" 复制值,而不是基于属性描述符。
Object.assign() 的工作机制
Object.assign() 仅复制 "可枚举的自有属性(own enumerable properties)",但它 不会直接操作属性描述符,而是按以下步骤进行:
- 读取(触发 getter)
- Object.assign() 先通过 source 读取所有可枚举的自有属性。
- 如果某个属性是 访问器属性(getter/setter),它会调用 getter 并获取返回值。
- 写入(普通赋值,不复制 setter)
- Object.assign() 在 target 上 创建一个普通的数据属性,并将 getter 返回的值赋给它。
- 这个新属性不是访问器属性,因此 setter 不会被复制。
- setter 本质上是一种行为,而不是一个值,所以不会被 Object.assign() 处理。
- 再次调用(不触发 getter)
- 直接读取 target.name,它是一个普通属性,所以直接返回,不会触发 getter。
如何解决呢?
const target = Object.defineProperties({}, Object.getOwnPropertyDescriptors(source));
console.log(target.name); // ✅ '张三',触发 getter
target.name = '李四'; // ✅ 触发 setter
console.log(target.name); // ✅ '李四',仍然触发 getter
// 触发 getter
// 张三
// 触发 setter
// 触发 getter
// 李四
原因:
- Object.getOwnPropertyDescriptors(source) 获取的是完整的属性描述符,包括 getter/setter。
- Object.defineProperties(target, ...) 把 getter/setter 复制到 target 上,所以 target.name 仍然是访问器属性,而不是普通属性。
Tip:第一次遇到这个问题,如果不对的话,请指出Thanks♪(・ω・)ノ。
5、访问器属性和原生属性重名
这个情况会导致无限递归(死循环),原因是 getter 和 setter 内部错误地访问了自身的 name,导致无限调用自身。
举个 🌰
const source = {
name: '张三', // ❶ 普通属性
get name() {
// ❷ getter
console.log('触发 getter');
return this.name; // 这里访问了自身的 name
},
set name(value) {
// ❸ setter
console.log('触发 setter');
this.name = value; // 这里访问了自身的 name
},
};
console.log(source.name); // 触发 getter 死循环
source.name = '李四'; // 触发 setter 死循环
执行 console.log(source.name):
- source.name 触发 getter。
- return this.name时,由于 this.name 仍然指向 source.name,所以 getter 再次被调用。
- 进入无限递归,最终导致栈溢出(Maximum call stack size exceeded)。
2. 访问器属性结合 Proxy 代理
Proxy 是 ES6 提供的另一种数据劫持方式,它可以拦截对象的所有操作,如 get/set、has、deleteProperty 等。结合 Proxy 和访问器属性,可以实现更强大的数据代理。
const handler = {
get(target, prop) {
console.log(`获取属性 ${prop} 值: ${target[prop]}`);
return target[prop];
},
set(target, prop, value) {
console.log(`修改属性 ${prop} 为: ${value}`);
target[prop] = value;
return true;
},
};
const person = new Proxy(
{
firstName: '李',
lastName: '四',
},
handler,
);
person.firstName; // 获取属性 firstName 值: 李
person.lastName = '五'; // 修改属性 lastName 为: 五
对比:
特性 | 访问器属性(getter/setter) | Proxy |
---|---|---|
作用范围 | 仅对特定属性生效 | 能劫持整个对象的所有属性 |
性能 | 适用于小范围封装 | 可能带来性能开销,尤其是深层嵌套对象 |
灵活性 | 需要手动定义每个属性的 getter/setter | 可用于整个对象,自动代理所有属性 |
Vue 兼容性 | Vue 2 依赖 Object.defineProperty,不支持 Proxy | Vue 3 基于 Proxy 构建,原生支持 |
3. getter / setter 在 Vue 组件中的使用
在 Vue 组件中,getter/setter 主要用于 computed 计算属性 和 Vue 3 响应式系统。
<template>
<p>姓名: {{ state.name }}</p>
</template>
<script setup>
import { reactive } from 'vue';
const state = reactive({
_name: ' Alice ',
get name() {
return this._name.toUpperCase();
},
set name(newName) {
this._name = newName.trim();
}
});
</script>
访问器属性 vs Vue 计算属性
特性 | 访问器属性(get / set) | Vue 计算属性(computed) |
---|---|---|
作用范围 | 仅在对象上生效 | 适用于 Vue 组件 |
缓存机制 | 每次访问都会重新执行 | 依赖不变时,不会重复计算 |
可读写性 | 需要手动定义 get / set | computed 也可以使用 get / set |
适用场景 | 适用于封装数据逻辑 | 适用于 Vue 组件中的动态计算数据 |