JS OBJECT 2 属性描述符 get set

113 阅读4分钟

为什么会有JS的面向对象编程

JavaScript 面向对象编程(OOP)是一种通过“对象”组织代码、封装数据和行为的编程范式。它让你的代码更模块化、可维护、可扩展。


什么是“面向对象编程”(OOP)

面向对象编程是一种通过**对象(Object)**来组织程序的方式,核心三大特性是:

特性说明
✅ 封装将数据和操作数据的方法绑定在一起(隐藏内部细节)
✅ 继承对象可以继承另一个对象的属性和方法(代码复用)
✅ 多态相同方法在不同对象中表现不同(扩展性强)

JavaScript 中的对象(Object)

在 JS 中,几乎一切都是对象,常见的定义对象方式有:

const person = {
  name: "Alice",
  speak() {
    console.log(`Hi, I'm ${this.name}`);
  }
};
person.speak();

JavaScript 面向对象编程方式

1️ 构造函数方式(传统)

function Person(name) {
  this.name = name;
}
Person.prototype.speak = function () {
  console.log(`Hi, I'm ${this.name}`);
};

const p1 = new Person("Alice");
p1.speak();

2️ class 类方式(现代 ES6+)

class Person {
  constructor(name) {
    this.name = name;
  }
  speak() {
    console.log(`Hi, I'm ${this.name}`);
  }
}
const p1 = new Person("Alice");
p1.speak();

JS 面向对象核心概念

概念说明
构造函数创建对象的函数
原型所有对象都继承自原型(__proto__
类(class)ES6 引入的语法糖,更易于构造面向对象代码
this当前对象的引用
继承extendssuper() 实现子类继承

OOP 示例:父类 + 子类

class Animal {
  constructor(name) {
    this.name = name;
  }
  speak() {
    console.log(`${this.name} makes a sound`);
  }
}

class Dog extends Animal {
  speak() {
    console.log(`${this.name} barks`);
  }
}

const d = new Dog("Buddy");
d.speak(); // Buddy barks

总结一句话:

JavaScript 的面向对象编程,是通过构造函数、原型或类 class 来实现对象的封装、继承与多态,让程序结构更清晰、更强大。


JS object的创建

var obj = {
    name: 'wei',
    age: 18,
    heigth: 1.88
}
  • 优点:
    • 简单,直接添加到属性的内部
  • 缺点:
    • 不能对这个属性进行一些限制(是否可以通过delete删除,是否可以for-in遍历)
var obj = {
  name: 'why',
  age: 18
}
//获取属性
console.log(obj.name)
//给属性赋值
obj.name = 'kobe'
console.log(obj.name)
//删除属性
delete obj.name
console.log(obj)

使用属性描述符

当你创建一个对象的属性时,不仅仅是简单地存储了一个值。每个属性(无论是数据属性还是访问器属性)都有一组与之关联的内部特性,这些特性定义了该属性的行为。这些特性的集合就被称为属性描述符

属性描述符主要有两种类型:

  1. 数据描述符 (Data Descriptor) : 包含一个数据值的属性。
  2. 访问器描述符 (Accessor Descriptor) : 由 getter 和 setter 函数定义的属性。

一个属性描述符只能是这两种类型中的一种,不能同时是两者。


属性描述符的特性 (Attributes)

1. 数据描述符 (Data Descriptor) 具有以下可选特性:
  • value:

    • 属性的值。可以是任何有效的 JavaScript 值(数字、对象、函数等)。
    • 默认值: undefined (如果使用 Object.defineProperty 创建时未提供)。
  • writable:

    • 一个布尔值,指示属性的 value 是否可以被赋值运算符 (=) 修改。
    • true: 值可以被修改。
    • false: 值是只读的。
    • 默认值: false (如果使用 Object.defineProperty 创建时未提供)。
  • enumerable:

    • 一个布尔值,指示属性是否会出现在对象的属性枚举中。例如,在 for...in 循环或 Object.keys()、Object.values()、Object.entries()、JSON.stringify() 中是否会包含该属性。
    • true: 属性是可枚举的。
    • false: 属性是不可枚举的。
    • 默认值: false (如果使用 Object.defineProperty 创建时未提供)。
  • configurable:

    • 一个布尔值,指示属性是否可以被删除,以及除了 value 和 writable 之外的其他特性 (enumerable, configurable, 以及是否可以在数据和访问器描述符之间切换) 是否可以被修改。
    • true: 属性可以被删除,特性可以被修改。
    • false: 属性不能被删除,并且大多数特性不能被修改 (有例外:如果 writable 为 true,则 value 仍然可以修改,并且 writable 可以改为 false;但一旦 configurable 为 false,就不能再将其改回 true)。如果 configurable 为 false 且 writable 为 false,那么 value 也不能再被修改。
    • 默认值: false (如果使用 Object.defineProperty 创建时未提供)。
2. 访问器描述符 (Accessor Descriptor) 具有以下可选特性:
  • get:

    • 一个函数,当读取属性值时被调用,不需要参数。其返回值将作为属性的值。如果没有定义 get,读取属性会返回 undefined。
    • 默认值: undefined。
  • set:

    • 一个函数,当给属性赋值时被调用,会接收唯一一个参数(即赋给属性的值)。如果没有定义 set,尝试给属性赋值在非严格模式下会被忽略,在严格模式下会抛出 TypeError。
    • 默认值: undefined。
  • enumerable:

    • 与数据描述符中的 enumerable 含义相同。
    • 默认值: false (如果使用 Object.defineProperty 创建时未提供)。
  • configurable:

    • 与数据描述符中的 configurable 含义相同。如果为 false,则属性不能被删除,get、set 函数以及 enumerable 特性也不能被修改。
    • 默认值: false (如果使用 Object.defineProperty 创建时未提供)。

默认特性值

需要特别注意的是:

  • 直接在对象字面量中定义属性通过简单的赋值操作添加属性 (obj.prop = value 或 obj['prop'] = value) 时,这些属性的 writable、enumerable 和 configurable 特性默认为 true

    const obj = { name: "Alice" }; // name 属性描述符: { value: "Alice", writable: true, enumerable: true, configurable: true }
    
    obj.age = 30;                  // age 属性描述符: { value: 30, writable: true, enumerable: true, configurable: true }
        
    
  • 使用 Object.defineProperty()、Object.defineProperties() 或 Object.create() 的第二个参数定义属性时,如果没有显式指定,writable、enumerable 和 configurable 特性默认为 false。这是一个常见的混淆点!

    const obj = {};
    Object.defineProperty(obj, 'readOnlyProp', {
      value: 100
      // writable, enumerable, configurable 默认为 false
    });
    console.log(Object.getOwnPropertyDescriptor(obj, 'readOnlyProp'));
    // { value: 100, writable: false, enumerable: false, configurable: false }
        
    
描述符configurableenumerablevaluewritablegetset
数据描述符可以可以可以可以不可以不可以
存储描述符可以可以不可以不可以可以可以
总结:

value 和 writeable 与 get 和 set 不能同时存在


操作属性描述符的方法

JavaScript 提供了几个内建方法来操作对象的属性描述符:

  1. Object.defineProperty(obj, prop, descriptor)

    • 在一个对象上定义新属性修改现有属性,并返回该对象。
    • obj: 要在其上定义属性的对象。
    • prop: 要定义或修改的属性的名称(字符串或 Symbol)。
    • descriptor: 要定义或修改的属性描述符对象。
    const person = {};
    
    // 定义一个数据属性
    Object.defineProperty(person, 'name', {
      value: "Bob",
      writable: false,    // 不可写
      enumerable: true,     // 可枚举
      configurable: false   // 不可配置 (不能删除, 不能改 enumerable/configurable, 不能从数据转访问器)
    });
    
    console.log(person.name); // "Bob"
    person.name = "Charlie"; // 尝试修改 (非严格模式下静默失败,严格模式下报错 TypeError)
    console.log(person.name); // "Bob" (值未改变)
    console.log(Object.keys(person)); // ["name"] (因为 enumerable: true)
    // delete person.name; // 尝试删除 (会失败或报错 TypeError, 因为 configurable: false)
    
    // 定义一个访问器属性
    let internalTemp = 25;
    Object.defineProperty(person, 'temperature', {
      get() {
        console.log("Getting temperature");
        return internalTemp + "°C";
      },
      set(value) {
        console.log("Setting temperature");
        internalTemp = parseFloat(value);
      },
      enumerable: true,
      configurable: true // 可以删除或修改 get/set
    });
    
    console.log(person.temperature); // "Getting temperature", "25°C"
    person.temperature = 30;        // "Setting temperature"
    console.log(person.temperature); // "Getting temperature", "30°C"
        
    
  2. Object.defineProperties(obj, props)

    • 在一个对象上定义一个或多个新属性修改现有属性,并返回该对象。
    • obj: 要在其上定义属性的对象。
    • props: 一个对象,其自身的键是属性名,值是对应的属性描述符对象。
    const car = {};
    Object.defineProperties(car, {
      'make': {
        value: 'Toyota',
        writable: true,
        enumerable: true
        // configurable 默认为 false
      },
      'model': {
        value: 'Camry',
        writable: false,
        enumerable: true
        // configurable 默认为 false
      },
      'year': {
        value: 2022,
        // writable, enumerable, configurable 默认为 false
      }
    });
    
    console.log(car.make);  // Toyota
    car.make = 'Honda';    // OK, 因为 writable: true
    console.log(car.make);  // Honda
    // car.model = 'Accord'; // Fails silently or throws TypeError
    console.log(Object.keys(car)); // ["make", "model"] (year 是 non-enumerable)
        
    
  3. Object.getOwnPropertyDescriptor(obj, prop)

    • 返回指定对象上一个自有属性(非继承属性)对应的属性描述符。如果属性不存在,则返回 undefined。
    • obj: 需要查找的目标对象。
    • prop: 属性名称。
    const book = { title: "The Hobbit" };
    Object.defineProperty(book, 'author', {
      value: "J.R.R. Tolkien",
      writable: false,
      enumerable: false,
      configurable: true
    });
    
    console.log(Object.getOwnPropertyDescriptor(book, 'title'));
    // { value: 'The Hobbit', writable: true, enumerable: true, configurable: true }
    
    console.log(Object.getOwnPropertyDescriptor(book, 'author'));
    // { value: 'J.R.R. Tolkien', writable: false, enumerable: false, configurable: true }
    
    console.log(Object.getOwnPropertyDescriptor(book, 'publisher')); // undefined
    console.log(Object.getOwnPropertyDescriptor(book, 'toString')); // undefined (toString 是继承来的)
        
    
  4. Object.getOwnPropertyDescriptors(obj) (ES2017)

    • 返回一个对象,包含了指定对象所有自有属性的属性描述符。键是属性名,值是对应的描述符对象。
    const gadget = {};
    Object.defineProperties(gadget, {
      name: { value: 'Smartphone', enumerable: true },
      price: { value: 999, writable: true },
      _id: { value: 'XYZ123', configurable: false } // enumerable 默认为 false
    });
    
    const descriptors = Object.getOwnPropertyDescriptors(gadget);
    console.log(descriptors);
    /*
    {
      name: { value: 'Smartphone', writable: false, enumerable: true, configurable: false },
      price: { value: 999, writable: true, enumerable: false, configurable: false },
      _id: { value: 'XYZ123', writable: false, enumerable: false, configurable: false }
    }
    */
    
    // 这个方法对于精确地复制对象(包括 getter/setter 和非枚举属性)很有用
    const shallowCloneWithDescriptors = Object.create(
        Object.getPrototypeOf(gadget),
        Object.getOwnPropertyDescriptors(gadget)
    );
    console.log(Object.getOwnPropertyDescriptor(shallowCloneWithDescriptors, 'price').writable); // true
        
    
总结

属性描述符提供了对对象属性行为的底层控制:

  • 创建只读属性: 设置 writable: false。

  • 创建不可枚举属性: 设置 enumerable: false (常用于隐藏内部状态或辅助方法)。

  • 创建不可配置/不可删除属性: 设置 configurable: false (用于锁定属性)。

  • 实现 getter 和 setter: 用于创建计算属性、执行验证逻辑或触发副作用。

  • 精确的对象复制/克隆: 结合 Object.getOwnPropertyDescriptors 和 Object.create 或 Object.defineProperties。

get和set函数使用

好的,我们来详细探讨 JavaScript 对象中的 get (getter) 和 set (setter)。它们是 访问器属性 (Accessor Properties) 的组成部分,允许你为对象的属性定义自定义的读取和写入行为,而不是简单地存储一个静态值。

这与 数据属性 (Data Properties) 不同,数据属性直接存储一个值,并具有 value 和 writable 特性。访问器属性则不包含 value 或 writable,而是包含 get 和/或 set 函数。


1. Getter (get)

目的: 定义当读取属性值时要执行的函数。

语法:
在对象字面量或类定义中,使用 get 关键字后跟你想定义的属性名,然后是一个无参数的函数体。

  const obj = {
  _internalValue: 10, // 通常用下划线表示内部使用的变量

  // 定义一个名为 'value' 的 getter
  get value() {
    console.log("Getter for 'value' was called.");
    // getter 必须返回一个值,这个值就是读取 obj.value 时得到的结果
    return this._internalValue * 2;
  }
};

// 读取属性时,getter 函数会自动执行
console.log(obj.value); // 输出: "Getter for 'value' was called.",然后是 20
console.log(obj.value); // 输出: "Getter for 'value' was called.",然后是 20

// 注意:你不能直接给 getter 属性赋值 (除非同时定义了 setter)
// obj.value = 5; // 如果没有 setter,严格模式下会报错 TypeError,非严格模式下静默失败
    

关键点:

  • Getter 看起来像一个普通属性,但访问它时会执行一个函数。
  • Getter 函数不接收参数
  • Getter 函数必须 return 一个值,这个值就是属性的读取结果。
  • 常用于计算属性(基于其他属性动态计算值)、提供对内部状态的受控访问。

2. Setter (set)

目的: 定义当设置 (赋值) 属性值时要执行的函数。

语法:
在对象字面量或类定义中,使用 set 关键字后跟你想定义的属性名,然后是一个接收一个参数的函数体,该参数就是赋给属性的值。

  const obj = {
  _internalValue: 10,
  _log: [], // 用于记录日志

  get value() {
    return this._internalValue;
  },

  // 定义一个名为 'value' 的 setter
  set value(newValue) {
    console.log(`Setter for 'value' was called with ${newValue}.`);

    // Setter 通常用于验证、转换数据或触发副作用
    if (typeof newValue !== 'number' || isNaN(newValue)) {
      console.error("Invalid value provided. Must be a number.");
      return; // 阻止无效赋值
    }

    if (newValue !== this._internalValue) {
       this._log.push(`Value changed from ${this._internalValue} to ${newValue}`);
       this._internalValue = newValue; // 更新内部值
    }
  },

  get history() {
    return this._log.join('\n');
  }
};

console.log(obj.value); // 10 (通过 getter)

// 给属性赋值时,setter 函数会自动执行
obj.value = 25;         // 输出: "Setter for 'value' was called with 25."
console.log(obj.value); // 25

obj.value = "hello";    // 输出: "Setter for 'value' was called with hello."
                        // 输出: "Invalid value provided. Must be a number."
console.log(obj.value); // 25 (值未改变,因为验证失败)

obj.value = 30;         // 输出: "Setter for 'value' was called with 30."
console.log(obj.history); // 输出: Value changed from 10 to 25
                          //       Value changed from 25 to 30
    

关键点:

  • Setter 看起来像一个普通属性,但给它赋值时会执行一个函数。
  • Setter 函数必须接收一个参数,这个参数就是赋值运算符 (=) 右侧的值。
  • Setter 函数通常不需要 return 值 (其返回值会被忽略)。它的主要目的是执行操作(如修改内部状态、验证输入等)。
  • 常用于数据验证、格式转换、触发依赖更新(如在框架中)。

3. Getters 和 Setters 结合使用

通常,getter 和 setter 是成对出现的,用于控制对同一个逻辑属性的访问。它们经常操作一个内部的、通常被认为是“私有”的变量(按照约定,常以下划线 _ 开头)。

重要: 不要在 getter 或 setter 内部直接读写它们自身对应的那个公共属性名,否则会造成无限递归调用导致栈溢出!

// 错误示例:无限递归
const badObj = {
  get name() {
    // 错误!读取 name 会再次调用 get name()
    // return this.name;
    console.log('Getting name'); // 永远不会执行到 return
    return 'something'; // 假设有个返回值
  },
  set name(value) {
    // 错误!设置 name 会再次调用 set name()
    // this.name = value;
     console.log('Setting name'); // 永远不会执行到完成
  }
}
// console.log(badObj.name); // Uncaught RangeError: Maximum call stack size exceeded
// badObj.name = 'Test';    // Uncaught RangeError: Maximum call stack size exceeded

// 正确示例:使用内部变量
class Person {
  constructor(firstName, lastName) {
    this._firstName = firstName;
    this._lastName = lastName;
  }

  // Getter for 'fullName'
  get fullName() {
    console.log("Calculating full name...");
    return `${this._firstName} ${this._lastName}`;
  }

  // Setter for 'fullName'
  set fullName(name) {
    console.log(`Setting full name to '${name}'...`);
    const parts = String(name).split(' ');
    if (parts.length >= 2) {
      this._firstName = parts[0];
      this._lastName = parts.slice(1).join(' '); // 处理可能有中间名的情况
    } else {
      console.warn("Full name should contain at least first and last name separated by space.");
      // 可以选择只设置 firstName 或保持不变
      this._firstName = name;
      this._lastName = '';
    }
  }

  // 如果需要单独访问 first/last name,也可以提供 getter/setter
  get firstName() { return this._firstName; }
  set firstName(name) { this._firstName = name; }
  // ... lastName 类似
}

const person = new Person("Jane", "Doe");

// 使用 getter
console.log(person.fullName); // 输出: "Calculating full name...", "Jane Doe"

// 使用 setter
person.fullName = "John Adam Smith"; // 输出: "Setting full name to 'John Adam Smith'..."

// 再次使用 getter,访问的是更新后的内部值
console.log(person.firstName); // "John"
console.log(person._lastName); // "Adam Smith" (访问内部变量)
console.log(person.fullName); // 输出: "Calculating full name...", "John Adam Smith"
    

4. 定义 Getters/Setters 的其他方式

除了在对象字面量和 class 中直接定义,还可以使用 Object.defineProperty() 或 Object.defineProperties()。

 const user = {
  _email: ''
};

Object.defineProperty(user, 'email', {
  // 定义 getter
  get: function() {
    console.log("Accessing email via descriptor getter");
    return this._email;
  },

  // 定义 setter
  set: function(value) {
    console.log("Setting email via descriptor setter");
    if (value.includes('@')) { // 简单验证
      this._email = value;
    } else {
      console.error("Invalid email format");
    }
  },

  enumerable: true,    // 让 'email' 属性可枚举
  configurable: true   // 允许后续修改或删除 'email' 属性的定义
});

user.email = "test@example.com"; // "Setting email via descriptor setter"
console.log(user.email);       // "Accessing email via descriptor getter", "test@example.com"
user.email = "invalid-email";  // "Setting email via descriptor setter", "Invalid email format"
console.log(user.email);       // "Accessing email via descriptor getter", "test@example.com" (值未变)
    

总结

Getters 和 Setters 提供了一种强大的机制来:

  1. 实现计算属性: 属性值是动态计算出来的,而不是静态存储的。
  2. 数据验证与修正: 在设置属性值之前进行检查或处理。
  3. 触发副作用: 当属性被读取或写入时执行额外的逻辑(如日志记录、通知、UI 更新)。
  4. 提供更友好的 API: 隐藏内部复杂性,对外暴露简单直观的属性访问接口。
  5. 向后兼容: 在不破坏现有代码的情况下,为现有属性添加新的逻辑。

它们是 JavaScript 面向对象编程中实现封装和抽象的重要工具。