MK
摩柯社区 - 一个极简的技术知识社区
AI 面试

JavaScript属性特性的动态修改

2023-06-022.1k 阅读

JavaScript 属性特性概述

在JavaScript中,对象的属性不仅仅是简单的数据存储单元,每个属性都带有一些特性(attribute),这些特性决定了属性的行为和特征。属性特性主要分为两类:数据属性特性和访问器属性特性。理解这些特性对于深入掌握JavaScript对象的工作原理以及实现属性的动态修改至关重要。

数据属性特性

  1. value:这是属性实际存储的值。例如:
let person = {
    name: 'John'
};
// name属性的value就是'John'
  1. writable:该特性决定能否修改属性的值。默认情况下,直接在对象字面量中定义的属性writabletrue
let person = {
    name: 'John'
};
person.name = 'Jane';
console.log(person.name); // 输出'Jane'

但是,如果将writable设置为false,尝试修改属性值会失败,并且在严格模式下会抛出错误。

'use strict';
let person = {};
Object.defineProperty(person, 'name', {
    value: 'John',
    writable: false
});
person.name = 'Jane'; // 在严格模式下会抛出TypeError
console.log(person.name); // 仍然输出'John'
  1. enumerable:此特性决定属性是否会出现在对象的枚举中,比如使用for...in循环或Object.keys()方法。默认直接在对象字面量中定义的属性enumerabletrue
let person = {
    name: 'John',
    age: 30
};
for (let key in person) {
    console.log(key); // 会输出'name'和'age'
}
let keys = Object.keys(person);
console.log(keys); // 输出['name', 'age']

若将enumerable设置为false,属性将不会出现在这些枚举操作中。

let person = {};
Object.defineProperty(person, 'name', {
    value: 'John',
    enumerable: false
});
for (let key in person) {
    console.log(key); // 不会输出'name'
}
let keys = Object.keys(person);
console.log(keys); // 不会包含'name'
  1. configurable:该特性决定能否删除属性,以及能否修改除valuewritable之外的其他特性。默认直接在对象字面量中定义的属性configurabletrue
let person = {
    name: 'John'
};
delete person.name;
console.log(person.name); // 输出undefined

configurable设置为false时,删除属性操作会失败,并且尝试修改除valuewritable之外的特性也会失败,在严格模式下会抛出错误。

'use strict';
let person = {};
Object.defineProperty(person, 'name', {
    value: 'John',
    configurable: false
});
delete person.name; // 在严格模式下会抛出TypeError
// 尝试修改enumerable特性也会在严格模式下抛出TypeError
Object.defineProperty(person, 'name', {
    enumerable: true
});

访问器属性特性

  1. get:这是一个函数,当访问属性时会调用此函数。它没有参数,返回值就是属性访问表达式的值。
let person = {
    _age: 30,
    get age() {
        return this._age;
    }
};
console.log(person.age); // 输出30,调用了get age函数
  1. set:这是一个函数,当给属性赋值时会调用此函数。它接受一个参数,即要赋的值。
let person = {
    _age: 30,
    get age() {
        return this._age;
    },
    set age(newValue) {
        if (typeof newValue === 'number' && newValue > 0) {
            this._age = newValue;
        }
    }
};
person.age = 35;
console.log(person.age); // 输出35
person.age = 'twenty'; // 不会修改_age的值
console.log(person.age); // 仍然输出35
  1. enumerableconfigurable:与数据属性中的这两个特性含义相同,决定属性是否可枚举以及是否可配置。

动态修改属性特性的方法

在JavaScript中,可以使用Object.defineProperty()Object.defineProperties()方法来动态修改属性的特性。

Object.defineProperty()

该方法用于在一个对象上定义一个新属性,或者修改一个对象的现有属性,并返回该对象。其语法如下:

Object.defineProperty(obj, prop, descriptor)
  • obj:要在其上定义属性的对象。
  • prop:要定义或修改的属性的名称。
  • descriptor:将被定义或修改的属性的描述符。

例如,我们可以动态地将一个普通属性转换为访问器属性:

let person = {
    name: 'John'
};
// 将name属性转换为访问器属性
Object.defineProperty(person, 'name', {
    get: function () {
        return '访问器属性:' + this._name;
    },
    set: function (newValue) {
        this._name = newValue;
    },
    enumerable: true,
    configurable: true
});
person.name = 'Jane';
console.log(person.name); // 输出'访问器属性:Jane'

又如,修改现有属性的特性:

let person = {
    age: 30
};
// 修改age属性的writable为false
Object.defineProperty(person, 'age', {
    writable: false
});
person.age = 35;
console.log(person.age); // 在非严格模式下,不会修改age的值,仍然输出30

Object.defineProperties()

这个方法允许在一个对象上一次性定义多个新的属性或修改多个现有属性,并返回该对象。语法为:

Object.defineProperties(obj, props)
  • obj:要在其上定义或修改属性的对象。
  • props:一个对象,其属性描述符将被定义或修改。每个属性的键是要定义或修改的属性名,值是属性描述符。

例如:

let person = {};
Object.defineProperties(person, {
    name: {
        value: 'John',
        writable: true,
        enumerable: true,
        configurable: true
    },
    age: {
        value: 30,
        writable: false,
        enumerable: true,
        configurable: false
    }
});
console.log(person.name); // 输出'John'
// 尝试修改age会失败,因为writable为false
person.age = 35;
console.log(person.age); // 仍然输出30

动态修改属性特性的应用场景

  1. 数据验证和封装:通过使用访问器属性和修改属性特性,可以实现数据的验证和封装。例如,在设置属性值时进行类型检查和范围验证。
let rectangle = {
    _width: 0,
    _height: 0,
    get width() {
        return this._width;
    },
    set width(newValue) {
        if (typeof newValue === 'number' && newValue > 0) {
            this._width = newValue;
        }
    },
    get height() {
        return this._height;
    },
    set height(newValue) {
        if (typeof newValue === 'number' && newValue > 0) {
            this._height = newValue;
        }
    }
};
rectangle.width = 10;
rectangle.height = 5;
console.log(rectangle.width * rectangle.height); // 输出50
// 尝试设置非法值
rectangle.width = 'ten';
console.log(rectangle.width); // 仍然是之前合法设置的值10
  1. 实现只读属性:将writable设置为false可以创建只读属性。这在很多场景下很有用,比如定义一些常量性质的属性。
let mathConstants = {};
Object.defineProperty(mathConstants, 'PI', {
    value: 3.14159,
    writable: false,
    enumerable: true,
    configurable: false
});
mathConstants.PI = 3.14;
console.log(mathConstants.PI); // 仍然输出3.14159
  1. 隐藏属性:通过将enumerable设置为false,可以使属性不被枚举,从而实现一定程度的隐藏。这对于一些内部使用的属性很有帮助,不希望外部代码随意访问和修改。
let user = {};
Object.defineProperty(user, '_password', {
    value:'secret',
    enumerable: false,
    writable: true,
    configurable: true
});
for (let key in user) {
    console.log(key); // 不会输出'_password'
}
let keys = Object.keys(user);
console.log(keys); // 不会包含'_password'
  1. 属性劫持(Property Interception):通过将普通属性转换为访问器属性,可以劫持属性的访问和赋值操作。这在实现数据绑定、代理等功能时非常有用。
let data = {
    value: 10
};
let proxy = {};
Object.defineProperty(proxy, 'value', {
    get: function () {
        console.log('访问属性value');
        return data.value;
    },
    set: function (newValue) {
        console.log('设置属性value为', newValue);
        data.value = newValue;
    }
});
console.log(proxy.value); // 输出'访问属性value',然后输出10
proxy.value = 20; // 输出'设置属性value为 20'

注意事项

  1. 继承属性的特性:当从原型链上继承属性时,属性的特性也会被继承。但是,在实例对象上直接修改继承属性的特性,不会影响原型对象上的属性特性。
function Person() {}
Person.prototype.name = 'Default Name';
let john = new Person();
// 虽然john继承了name属性,但修改john.name的特性不会影响原型上的name属性
Object.defineProperty(john, 'name', {
    writable: false
});
john.name = 'John Doe';
console.log(john.name); // 仍然输出'Default Name'
// 原型上的name属性仍然是可写的
Person.prototype.name = 'New Default Name';
console.log(john.name); // 输出'New Default Name'
  1. 兼容性:虽然Object.defineProperty()Object.defineProperties()是ECMAScript 5引入的标准方法,但在一些较旧的浏览器中可能不支持。如果需要兼容旧浏览器,可以使用一些垫片(polyfill)来模拟这些功能。
  2. 性能影响:频繁地动态修改属性特性可能会对性能产生一定的影响,特别是在性能敏感的应用场景中。例如,在一个循环中多次使用Object.defineProperty()来修改属性特性,可能会导致性能瓶颈。因此,在实际应用中需要权衡利弊,尽量减少不必要的动态属性特性修改。

在JavaScript编程中,深入理解和灵活运用属性特性的动态修改,能够使代码更加健壮、灵活和安全。无论是开发大型应用程序还是小型脚本,掌握这些技术都能为开发者提供更多的手段来实现复杂的功能需求。通过合理地设置属性的特性,如数据验证、封装、只读属性等,可以有效地提高代码的质量和可维护性。同时,了解动态修改属性特性的应用场景和注意事项,能够避免在开发过程中出现一些潜在的问题,确保程序的稳定运行。