解锁 ES6 访问器属性 Getter & Setter

在 JavaScript ES6 及其后续版本中,可以使用 访问器属性(Accessor Properties) 来定义 gettersetter,从而拦截对象属性的访问和赋值。这种机制不仅提供了更灵活的属性管理方式,还能增强数据的封装性和安全性。 

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)",但它 不会直接操作属性描述符,而是按以下步骤进行:

  1. 读取(触发 getter)
    1. Object.assign() 先通过 source 读取所有可枚举的自有属性。
    2. 如果某个属性是 访问器属性(getter/setter),它会调用 getter 并获取返回值。
  2. 写入(普通赋值,不复制 setter)
    1. Object.assign() 在 target 上 创建一个普通的数据属性,并将 getter 返回的值赋给它。
    2. 这个新属性不是访问器属性,因此 setter 不会被复制。
    3. setter 本质上是一种行为,而不是一个值,所以不会被 Object.assign() 处理。
  3. 再次调用(不触发 getter)
    1. 直接读取 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):

  1. source.name 触发 getter。
  2. return this.name时,由于 this.name 仍然指向 source.name,所以 getter 再次被调用。
  3. 进入无限递归,最终导致栈溢出(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,不支持 ProxyVue 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 / setcomputed 也可以使用 get / set
适用场景适用于封装数据逻辑适用于 Vue 组件中的动态计算数据

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值