JS getter (`get`) 与 setter (`set`):控制属性的读写访问

各位观众,晚上好!我是你们的老朋友,今天咱们聊聊 JavaScript 里的 getset,也就是 getter 和 setter。这俩哥们儿,可以说是 JavaScript 面向对象编程里的一对黄金搭档,能让你对对象的属性进行更细致的控制。 别怕,听起来高大上,其实就是给属性设置“读”和“写”的关卡。

开场白:属性的“读”与“写”

在JavaScript里,我们经常会直接访问和修改对象的属性,比如:

const person = {
  name: '张三',
  age: 30
};

console.log(person.name); // 输出: 张三
person.age = 31;
console.log(person.age); // 输出: 31

这看起来很直接,也很方便。但是,有时候我们可能需要对属性的访问和修改进行一些额外的控制,比如:

  • 数据校验: 确保赋给属性的值符合特定的规则。
  • 计算属性: 属性的值不是直接存储的,而是通过计算得到的。
  • 只读属性: 禁止外部修改属性的值。
  • 副作用: 在访问或修改属性时执行一些额外的操作。

这时候,getset 就派上用场了。

什么是 Getter 和 Setter?

简单来说:

  • Getter ( get ): 是用来读取属性值的“拦截器”。当你尝试访问一个属性时,Getter 函数会被调用,它决定了实际返回什么值。
  • Setter ( set ): 是用来设置属性值的“拦截器”。当你尝试给一个属性赋值时,Setter 函数会被调用,它决定了如何处理这个新值。

你可以把它们想象成属性的“门卫”,负责检查进出属性的值。

语法:如何定义 Getter 和 Setter

Getter 和 Setter 有两种定义方式:

  1. 对象字面量 (Object Literal) 方式: 直接在对象定义时声明。

    const person = {
      firstName: '张',
      lastName: '三',
      get fullName() {
        return this.firstName + ' ' + this.lastName;
      },
      set fullName(name) {
        const parts = name.split(' ');
        this.firstName = parts[0];
        this.lastName = parts[1];
      }
    };
    
    console.log(person.fullName); // 输出: 张 三 (调用 getter)
    person.fullName = '李 四'; // 调用 setter
    console.log(person.firstName); // 输出: 李
    console.log(person.lastName); // 输出: 四

    在这个例子中,fullName 属性并没有直接存储值,而是通过 get fullName() 方法动态计算得到的。 当你访问 person.fullName 时,实际上是调用了 get fullName() 函数。 同样,当你设置 person.fullName 时,实际上调用了 set fullName(name) 函数,它会将新的名字拆分成 firstNamelastName

  2. Object.defineProperty() 方法: 在对象创建后动态添加或修改属性的特性。

    const person = {
      firstName: '王',
      lastName: '五'
    };
    
    Object.defineProperty(person, 'fullName', {
      get: function() {
        return this.firstName + ' ' + this.lastName;
      },
      set: function(name) {
        const parts = name.split(' ');
        this.firstName = parts[0];
        this.lastName = parts[1];
      }
    });
    
    console.log(person.fullName); // 输出: 王 五 (调用 getter)
    person.fullName = '赵 六'; // 调用 setter
    console.log(person.firstName); // 输出: 赵
    console.log(person.lastName); // 输出: 六

    Object.defineProperty() 提供了更强大的控制能力,可以设置属性的 configurable(是否可配置)、enumerable(是否可枚举)等特性。

Getter 的使用场景

Getter 最常见的用途包括:

  • 计算属性: 属性的值依赖于其他属性,需要动态计算。
  • 数据转换: 在读取属性时对数据进行格式化或转换。
  • 隐藏内部状态: 暴露一个只读的属性,但底层实现可能很复杂。

示例 1:计算属性

const rectangle = {
  width: 10,
  height: 5,
  get area() {
    return this.width * this.height;
  }
};

console.log(rectangle.area); // 输出: 50
rectangle.width = 20;
console.log(rectangle.area); // 输出: 100 (area 会自动更新)

area 属性的值依赖于 widthheight,每次访问 rectangle.area 时都会重新计算。

示例 2:数据转换

const temperature = {
  celsius: 25,
  get fahrenheit() {
    return this.celsius * 9 / 5 + 32;
  },
  set fahrenheit(value){
    this.celsius = (value - 32) * 5 / 9;
  }
};

console.log(temperature.fahrenheit); // 输出: 77
temperature.fahrenheit = 86;
console.log(temperature.celsius); // 输出: 30

fahrenheit 属性会将摄氏度转换为华氏度。

Setter 的使用场景

Setter 最常见的用途包括:

  • 数据验证: 在设置属性值之前进行验证,确保值的有效性。
  • 副作用: 在设置属性值时执行一些额外的操作,例如更新其他属性或触发事件。
  • 控制属性的修改: 限制属性的修改方式或禁止修改。

示例 1:数据验证

const circle = {
  radius: 1,
  set radius(value) {
    if (value <= 0) {
      throw new Error('半径必须大于 0');
    }
    this._radius = value; // 使用 _radius 存储实际的值
  },
  get radius(){
    return this._radius;
  }
};

circle.radius = 5; // 正常赋值
console.log(circle.radius); // 输出 5

try {
  circle.radius = -1; // 抛出错误
} catch (error) {
  console.error(error.message); // 输出: 半径必须大于 0
}

在这个例子中,Setter 函数会检查 radius 的值是否大于 0,如果不是,则抛出一个错误。 注意,我们使用 _radius 来存储实际的半径值,避免 Setter 函数无限循环调用自身。这是很常见的做法,用一个下划线开头的变量来表示“私有”属性。

示例 2:副作用

const counter = {
  _count: 0,
  get count() {
    return this._count;
  },
  set count(value) {
    this._count = value;
    console.log('计数器已更新为:', value); // 副作用:打印日志
  }
};

counter.count = 10; // 输出: 计数器已更新为: 10
console.log(counter.count); // 输出: 10

在这个例子中,Setter 函数在设置 count 的值后,会打印一条日志。

示例 3:只读属性

const obj = {
  _name: "只读属性",
  get name() {
    return this._name;
  },
};

console.log(obj.name); // 输出 "只读属性"
obj.name = "尝试修改"; // 静默失败,不会报错,但值不会改变
console.log(obj.name); // 仍然输出 "只读属性"

这个例子中,我们只定义了 name 属性的 getter,没有定义 setter。这意味着外部无法修改 name 属性的值。

注意事项

  • 避免无限循环: Getter 和 Setter 函数内部访问或修改自身属性时,一定要小心,避免无限循环调用。 通常的做法是使用一个不同的变量来存储实际的值,例如使用下划线前缀 _

  • 性能影响: 过度使用 Getter 和 Setter 可能会对性能产生一定影响,因为每次访问或修改属性都需要调用函数。 所以,要权衡使用的必要性。

  • 配合 Object.defineProperty() 使用: Object.defineProperty() 除了可以定义 Getter 和 Setter,还可以设置属性的其他特性,例如 configurableenumerable,可以更精细地控制属性的行为。

Object.defineProperty() 的更多用法

让我们更深入地了解 Object.defineProperty()

const myObject = {};

Object.defineProperty(myObject, 'myProperty', {
  value: 'Hello', // 属性的初始值
  writable: false, // 是否可写 (只读)
  enumerable: true, // 是否可枚举 (是否可以通过 for...in 循环访问)
  configurable: false // 是否可配置 (是否可以删除或修改属性的特性)
});

console.log(myObject.myProperty); // 输出: Hello
myObject.myProperty = 'World'; // 尝试修改,但无效,因为 writable: false
console.log(myObject.myProperty); // 输出: Hello (值没有改变)

for (let key in myObject) {
  console.log(key); // 输出: myProperty (因为 enumerable: true)
}

// 尝试删除属性 (会抛出错误,因为 configurable: false)
// delete myObject.myProperty; // 严格模式下会报错, 非严格模式下静默失败

// 尝试重新定义属性 (会抛出错误,因为 configurable: false)
// Object.defineProperty(myObject, 'myProperty', { value: 'New Value' }); // 严格模式下会报错, 非严格模式下静默失败
特性 描述
value 属性的值。
writable 布尔值,指示属性是否可写。如果为 false,则该属性是只读的。
enumerable 布尔值,指示属性是否可枚举。如果为 true,则该属性可以通过 for...in 循环、Object.keys() 等方法访问。
configurable 布尔值,指示属性是否可配置。如果为 false,则无法删除该属性,也无法修改其特性(例如 writableenumerableconfigurable 本身)。 注意:一旦设置为 false,就无法再改回 true
get 一个函数,作为属性的 getter。当属性被访问时,该函数会被调用。
set 一个函数,作为属性的 setter。当属性被赋值时,该函数会被调用。

更高级的用法:使用 Symbol 作为属性名

有时候,我们希望定义一些“私有”属性,不希望被外部访问。虽然 JavaScript 没有真正的私有属性,但我们可以使用 Symbol 来模拟:

const _age = Symbol('age'); // 创建一个 Symbol

const person = {
  name: '李明',
  [_age]: 25, // 使用 Symbol 作为属性名
  get age() {
    return this[_age];
  },
  set age(value) {
    if (typeof value !== 'number' || value < 0) {
      throw new Error('年龄必须是大于等于0的数字');
    }
    this[_age] = value;
  }
};

console.log(person.name); // 输出: 李明
console.log(person.age); // 输出: 25 (通过 getter 访问)

// 无法直接访问 _age 属性 (除非你知道 Symbol 的值)
// console.log(person._age); // undefined

// 尝试使用 Object.keys() 也无法访问到 Symbol 属性
console.log(Object.keys(person)); // 输出: [ 'name', 'age' ]

person.age = 30;
console.log(person.age); // 输出: 30

// 尝试通过 Object.getOwnPropertySymbols() 可以访问到 Symbol 属性
console.log(Object.getOwnPropertySymbols(person)); // 输出: [ Symbol(age) ]

// 但是仍然无法直接修改
person[Object.getOwnPropertySymbols(person)[0]] = 100; // 非常不推荐
console.log(person.age); // 仍然输出: 30

Symbol 属性不会被 for...in 循环或 Object.keys() 访问到,只能通过 Object.getOwnPropertySymbols() 获取。 虽然这并不能完全阻止外部访问,但可以有效地隐藏属性,提高代码的可维护性。

总结

Getter 和 Setter 是 JavaScript 中非常有用的特性,可以让你对对象的属性进行更细致的控制。 它们可以用于数据验证、计算属性、副作用处理等多种场景。 配合 Object.defineProperty() 和 Symbol,你可以构建更健壮、更易于维护的代码。

记住,没有银弹。 Getter 和 Setter 虽然强大,但也需要适度使用,避免过度设计和性能问题。 合理地运用它们,可以让你写出更优雅、更安全的代码。

今天就到这里,希望大家有所收获! 咱们下次再见!

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注