深入理解JavaScript属性描述符:从数据属性到存取器属性

一、属性描述符基础

1.1 什么是属性描述符?

属性描述符是一个普通对象,用于描述一个属性的相关信息。在JavaScript中,每个属性都有一个对应的属性描述符,它定义了属性的行为特征,比如是否可写、是否可枚举、是否可配置等。

通过Object.getOwnPropertyDescriptor()方法,我们可以获取一个对象某个属性的属性描述符:

js 复制代码
let obj = { name: 'John' };
let descriptor = Object.getOwnPropertyDescriptor(obj, 'name');
console.log(descriptor);
/*
{
  value: "John",
  writable: true,
  enumerable: true,
  configurable: true
}
*/

如果要获取对象所有属性的描述符,可以使用Object.getOwnPropertyDescriptors()方法:

js 复制代码
let descriptors = Object.getOwnPropertyDescriptors(obj);
console.log(descriptors);

1.2 属性描述符的类型

JavaScript中有两种类型的属性描述符:

  1. ​数据属性描述符(Data Property Descriptor)​:用于描述拥有值的属性
  2. ​存取器属性描述符(Accessor Property Descriptor)​:用于描述通过getter和setter函数访问的属性

两者的主要区别在于数据属性描述符直接存储值,而存取器属性描述符通过函数来控制值的获取和设置。

二、数据属性描述符详解

2.1 数据属性描述符的构成

一个数据属性描述符包含以下四个属性:

  1. ​value​​:属性的值,可以是任何JavaScript数据类型

  2. ​writable​​:布尔值,表示属性是否可写

    • true:属性的值可以被修改(默认值)
    • false:属性的值是只读的,不能被修改
  3. ​enumerable​​:布尔值,表示属性是否可枚举

    • true:属性可以通过for...in循环、Object.keys()等方法枚举出来(默认值)
    • false:属性不会被枚举出来
  4. ​configurable​​:布尔值,表示属性是否可配置

    • true:属性的描述符可以被修改,属性可以被删除(默认值)
    • false:属性的描述符不能被修改,属性不能被删除

2.2 如何定义数据属性描述符

我们可以使用Object.defineProperty()方法来定义或修改对象属性的描述符:

js 复制代码
const obj = {};
Object.defineProperty(obj, 'name', {
  value: '李四',
  writable: false,
  enumerable: true,
  configurable: false
});

console.log(obj.name); // 输出: 李四
obj.name = '王五'; // 尝试修改属性值,但无效
console.log(obj.name); // 输出: 李四 (属性值未被修改)
delete obj.name; // 尝试删除属性,但无效
console.log(obj.name); // 输出: 李四 (属性未被删除)

2.3 默认属性描述符

当我们直接给对象添加属性时,JavaScript会自动为该属性设置默认的描述符:

js 复制代码
let obj = { a: 2 };
// 等同于
Object.defineProperty(obj, 'a', {
  value: 2,
  writable: true,
  enumerable: true,
  configurable: true
});

2.4 数据属性描述符的应用场景

  1. ​保护属性​ ​:通过设置writable: false,可以防止属性被意外修改,保证数据的安全性

    js 复制代码
    const importantConfig = {};
    Object.defineProperty(importantConfig, 'apiKey', {
      value: '123-456-789',
      writable: false
    });
  2. ​隐藏属性​ ​:通过设置enumerable: false,可以隐藏属性,使其不被for...in循环等方法枚举出来,避免暴露内部实现细节

    js 复制代码
    function Cache() {
      Object.defineProperty(this, '_data', {
        value: {},
        enumerable: false
      });
    }
  3. ​控制属性的删除和配置​ ​:通过设置configurable: false,可以防止属性被删除或修改描述符,保证属性的稳定性

    js 复制代码
    class Constants {
      constructor() {
        Object.defineProperty(this, 'PI', {
          value: 3.1415926,
          writable: false,
          configurable: false
        });
      }
    }

三、存取器属性描述符详解

3.1 什么是存取器属性?

存取器属性是一种特殊的属性,它不直接存储值,而是通过getter和setter函数来控制属性的读取和设置。简单来说,存取器属性就像是对象属性的"门卫",你可以通过getter函数来定义读取属性时的行为,通过setter函数来定义设置属性时的行为。

3.2 存取器属性描述符的构成

存取器属性描述符主要关注以下两个属性:

  1. ​get​​:用于读取属性的getter函数

    • 当访问属性时调用
    • 不能接收参数
    • 应该返回一个值作为属性值
  2. ​set​​:用于设置属性的setter函数

    • 当给属性赋值时调用
    • 接收一个参数(赋的值)
    • 通常用于验证或转换值

注意:存取器属性描述符不能同时包含valuewritable属性,否则会抛出错误。

3.3 如何定义存取器属性

定义存取器属性有两种主要方式:

3.3.1 使用Object.defineProperty()方法

这是最常见也是最灵活的方式:

js 复制代码
const person = {};
Object.defineProperty(person, 'fullName', {
  get: function() {
    return this.firstName + ' ' + this.lastName;
  },
  set: function(value) {
    const parts = value.split(' ');
    this.firstName = parts[0];
    this.lastName = parts[1];
  },
  enumerable: true,
  configurable: true
});

person.firstName = 'John';
person.lastName = 'Doe';
console.log(person.fullName); // 输出: John Doe
person.fullName = 'Jane Smith';
console.log(person.firstName); // 输出: Jane
console.log(person.lastName); // 输出: Smith

3.3.2 使用get和set关键字(ES6语法)

ES6引入了更简洁的语法来定义存取器属性:

js 复制代码
const person = {
  firstName: 'John',
  lastName: 'Doe',
  get fullName() {
    return this.firstName + ' ' + this.lastName;
  },
  set fullName(value) {
    const parts = value.split(' ');
    this.firstName = parts[0];
    this.lastName = parts[1];
  }
};

console.log(person.fullName); // 输出: John Doe
person.fullName = 'Jane Smith';
console.log(person.firstName); // 输出: Jane
console.log(person.lastName); // 输出: Smith

3.4 存取器属性的妙用

存取器属性的应用场景非常广泛,以下是一些常见的例子:

  1. ​数据验证​​:可以在set函数中对传入的值进行验证,确保数据的有效性

    js 复制代码
    const circle = {
      _radius: 1,
      get radius() {
        return this._radius;
      },
      set radius(value) {
        if (value <= 0) {
          throw new Error('Radius must be positive.');
        }
        this._radius = value;
      }
    };
    
    circle.radius = 5; // OK
    // circle.radius = -1; // 抛出错误
  2. ​计算属性​​:可以根据其他属性的值动态计算出新的属性值

    js 复制代码
    const rectangle = {
      width: 10,
      height: 5,
      get area() {
        return this.width * this.height;
      }
    };
    
    console.log(rectangle.area); // 输出: 50
  3. ​实现数据绑定​​:访问器属性可以与框架或库结合使用,实现数据绑定功能,当属性值发生变化时,自动更新UI

  4. ​日志记录​​:可以在getter和setter中添加日志记录功能,跟踪属性的访问和修改

    js 复制代码
    class Model {
      constructor() {
        this._data = null;
      }
      
      get data() {
        console.log('Getting data');
        return this._data;
      }
      
      set data(value) {
        console.log('Setting data to', value);
        this._data = value;
      }
    }
  5. ​懒加载​​:可以在getter中实现属性的懒加载,即第一次访问时才计算或获取值

    js 复制代码
    class ExpensiveObject {
      constructor() {
        this._value = null;
      }
      
      get value() {
        if (this._value === null) {
          console.log('Calculating expensive value...');
          this._value = this._calculateValue();
        }
        return this._value;
      }
      
      _calculateValue() {
        // 模拟耗时计算
        let result = 0;
        for (let i = 0; i < 100000000; i++) {
          result += Math.sqrt(i);
        }
        return result;
      }
    }

3.5 存取器属性的注意事项

虽然存取器属性非常强大,但也需要注意一些潜在的陷阱:

  1. ​无限循环​ ​:如果在getter或setter中直接访问自身属性,可能会导致无限循环。因此,通常需要使用一个内部变量(例如_radius_count)来存储实际的值。

    js 复制代码
    // 错误示例 - 会导致无限循环
    const obj = {
      get value() {
        return this.value; // 会再次调用getter,导致无限循环
      },
      set value(v) {
        this.value = v; // 会再次调用setter,导致无限循环
      }
    };
    
    // 正确做法
    const obj = {
      _value: null,
      get value() {
        return this._value;
      },
      set value(v) {
        this._value = v;
      }
    };
  2. ​性能问题​​:频繁调用getter和setter可能会影响性能,尤其是在复杂的计算场景下。需要根据实际情况进行权衡。

  3. ​兼容性​ ​:虽然现代浏览器都支持存取器属性,但在一些老旧浏览器中可能需要使用Object.defineProperty的polyfill。

四、属性描述符的高级应用

4.1 控制对象状态

JavaScript提供了3种方法,用来精确控制一个对象的读写状态,防止对象被改变:

  1. ​Object.preventExtensions​​:阻止为对象添加新的属性

    • 可以使用Object.isExtensible()检查对象是否可扩展
  2. ​Object.seal​​:阻止为对象添加新的属性,同时也无法删除旧属性

    • 等价于Object.preventExtensions(obj)并设置现有的所有属性为configurable:false
    • 可以使用Object.isSealed()检查对象是否被密封
  3. ​Object.freeze​​:阻止为一个对象添加新属性、删除旧属性、修改属性值

    • 效果相当于调用了Object.seal(obj)并设置所有属性为writable: false
    • 可以使用Object.isFrozen()检查对象是否被冻结
js 复制代码
// 示例代码
var obj1 = {};
console.log(Object.isExtensible(obj1)); // true
Object.preventExtensions(obj1);
console.log(Object.isExtensible(obj1)); // false

var obj2 = {};
console.log(Object.isSealed(obj2)); // false
Object.seal(obj2);
console.log(Object.isSealed(obj2)); // true

var obj3 = {};
console.log(Object.isFrozen(obj3)); // false
Object.freeze(obj3);
console.log(Object.isFrozen(obj3)); // true

4.2 创建常量属性

通过组合writable: falseconfigurable: false,可以创建一个不能修改、重新定义或删除的对象常量属性:

js 复制代码
var myObject = {};
Object.defineProperty(myObject, "FAVORITE_NUMBER", {
  value: 42,
  writable: false,
  configurable: false
});

myObject.FAVORITE_NUMBER = 100; // 修改无效
console.log(myObject.FAVORITE_NUMBER); // 42

delete myObject.FAVORITE_NUMBER; // 删除无效
console.log(myObject.FAVORITE_NUMBER); // 42

4.3 实现私有属性模式

虽然JavaScript没有真正的私有属性,但我们可以利用属性描述符模拟私有属性:

js 复制代码
function createPrivateProperty(obj, name, initialValue) {
  let value = initialValue;
  
  Object.defineProperty(obj, name, {
    get: function() {
      console.log('Getting private property');
      return value;
    },
    set: function(newValue) {
      console.log('Setting private property');
      value = newValue;
    },
    enumerable: false,
    configurable: false
  });
}

const obj = {};
createPrivateProperty(obj, '_secret', 'initial');
console.log(obj._secret); // "initial"
obj._secret = 'modified';
console.log(obj._secret); // "modified"

// 但这种方式并不是真正的私有,仍然可以通过obj._secret访问

4.4 属性描述符与原型链

当访问一个对象的属性时,JavaScript会沿着原型链查找。属性描述符在这过程中有一些特殊行为:

  1. 如果一个属性在原型链上被定义为只读(writable:false),你不能在子对象上创建同名属性
  2. 如果一个属性在原型链上是存取器属性,那么对该属性的读取或设置会调用原型上的getter/setter
js 复制代码
const proto = {
  get name() {
    return 'Proto';
  }
};

const obj = Object.create(proto);
obj.name = 'Obj';
console.log(obj.name); // 仍然是"Proto",因为原型上的name是只读的存取器属性

五、总结

  1. ​数据属性描述符​ 直接存储值,并通过valuewritableenumerableconfigurable控制属性行为
  2. ​存取器属性描述符​通过getter和setter函数控制属性的访问和修改,不直接存储值
  3. 使用Object.definePropertyObject.defineProperties可以精细控制属性行为
  4. Object.preventExtensionsObject.sealObject.freeze提供了不同级别的对象保护
  5. 属性描述符是实现高级特性如数据验证、计算属性、数据绑定的基础
相关推荐
蓝天白云下遛狗18 分钟前
goole chrome变更默认搜索引擎为百度
前端·chrome
come1123441 分钟前
Vue 响应式数据传递:ref、reactive 与 Provide/Inject 完全指南
前端·javascript·vue.js
musk12121 小时前
electron 打包太大 试试 tauri , tauri 安装打包demo
前端·electron·tauri
万少2 小时前
第五款 HarmonyOS 上架作品 奇趣故事匣 来了
前端·harmonyos·客户端
OpenGL2 小时前
Android targetSdkVersion升级至35(Android15)相关问题
前端
rzl023 小时前
java web5(黑马)
java·开发语言·前端
Amy.Wang3 小时前
前端如何实现电子签名
前端·javascript·html5
今天又在摸鱼3 小时前
Vue3-组件化-Vue核心思想之一
前端·javascript·vue.js
蓝婷儿3 小时前
每天一个前端小知识 Day 21 - 浏览器兼容性与 Polyfill 策略
前端
百锦再3 小时前
Vue中对象赋值问题:对象引用被保留,仅部分属性被覆盖
前端·javascript·vue.js·vue·web·reactive·ref