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

JavaScript属性特性对对象的影响

2023-07-077.9k 阅读

JavaScript属性特性基础介绍

在JavaScript中,对象的属性除了我们常见的属性值之外,还存在一些特性,这些特性决定了属性的一些行为和性质。属性特性主要分为两类:数据属性特性和访问器属性特性。

数据属性特性

  1. [[Value]]:这是属性实际存储的值。我们平时通过对象字面量或者赋值语句创建的属性,其值就存储在这个特性中。例如:
let person = {
    name: 'John'
};
// 这里name属性的[[Value]]就是'John'
  1. [[Writable]]:该特性决定了能否修改属性的值。如果[[Writable]]true,那么可以通过赋值语句修改属性值;若为false,尝试修改属性值在非严格模式下不会报错,但修改无效,在严格模式下会抛出错误。
let obj = {};
Object.defineProperty(obj, 'prop', {
    value: 10,
    writable: false
});
// 非严格模式下
obj.prop = 20;
console.log(obj.prop); // 输出10,修改无效
// 严格模式下
'use strict';
let newObj = {};
Object.defineProperty(newObj, 'newProp', {
    value: 30,
    writable: false
});
newObj.newProp = 40; // 抛出TypeError: Cannot assign to read only property 'newProp' of object '#<Object>'
  1. [[Enumerable]]:此特性决定了该属性是否会出现在对象的for...in循环或者Object.keys()的结果中。如果[[Enumerable]]true,属性会被枚举;为false则不会。
let myObj = {};
Object.defineProperty(myObj, 'visibleProp', {
    value: 'visible',
    enumerable: true
});
Object.defineProperty(myObj, 'hiddenProp', {
    value: 'hidden',
    enumerable: false
});
for (let key in myObj) {
    console.log(key); // 只会输出visibleProp
}
console.log(Object.keys(myObj)); // 输出['visibleProp']
  1. [[Configurable]]:这个特性决定了能否删除属性,以及能否修改除[[Value]]之外的其他特性。如果[[Configurable]]true,可以删除属性和修改其他特性;为false时,删除属性操作在非严格模式下不会报错,但删除无效,在严格模式下会抛出错误,同时也不能修改除[[Value]]之外的其他特性(修改[[Value]]不受此限制,前提是[[Writable]]true)。
let configObj = {};
Object.defineProperty(configObj, 'delProp', {
    value: 'to be deleted',
    configurable: true
});
Object.defineProperty(configObj, 'fixedProp', {
    value: 'fixed',
    configurable: false
});
// 删除delProp
delete configObj.delProp;
console.log(configObj.delProp); // 输出undefined,删除成功
// 删除fixedProp
delete configObj.fixedProp;
console.log(configObj.fixedProp); // 输出fixed,删除无效
// 严格模式下删除fixedProp
'use strict';
let strictObj = {};
Object.defineProperty(strictObj, 'nonConfigurableProp', {
    value: 'not configurable',
    configurable: false
});
delete strictObj.nonConfigurableProp; // 抛出TypeError: Cannot delete property 'nonConfigurableProp' of #<Object>

访问器属性特性

  1. [[Get]]:这是一个函数,当访问属性时会调用此函数。它没有参数,返回值就是属性访问表达式的值。例如:
let book = {
    _price: 10,
    get price() {
        return this._price;
    }
};
console.log(book.price); // 调用[[Get]]函数,输出10
  1. [[Set]]:也是一个函数,当给属性赋值时会调用此函数。它接受一个参数,即要赋的值。
let product = {
    _quantity: 5,
    get quantity() {
        return this._quantity;
    },
    set quantity(newValue) {
        if (typeof newValue === 'number' && newValue > 0) {
            this._quantity = newValue;
        }
    }
};
product.quantity = 10;
console.log(product.quantity); // 输出10
product.quantity = 'not a number';
console.log(product.quantity); // 输出10,因为赋值不符合条件,未修改
  1. [[Enumerable]][[Configurable]]:与数据属性中的这两个特性含义相同,决定属性是否可枚举以及是否可配置。
let enumerableObj = {};
Object.defineProperty(enumerableObj, 'accessorProp', {
    get: function() {
        return 'value';
    },
    enumerable: true,
    configurable: true
});
for (let key in enumerableObj) {
    console.log(key); // 输出accessorProp
}

属性特性的获取与设置

我们可以使用Object.getOwnPropertyDescriptor()方法来获取对象属性的特性描述符,它会返回一个对象,包含属性的各种特性。

let propObj = {};
Object.defineProperty(propObj, 'testProp', {
    value: 'test value',
    writable: true,
    enumerable: true,
    configurable: true
});
let descriptor = Object.getOwnPropertyDescriptor(propObj, 'testProp');
console.log(descriptor);
// 输出 { value: 'test value', writable: true, enumerable: true, configurable: true }

Object.defineProperty()方法不仅可以创建新属性,还可以修改现有属性的特性。

let modifyObj = {
    originalProp: 'original'
};
// 修改originalProp的特性
Object.defineProperty(modifyObj, 'originalProp', {
    writable: false,
    enumerable: false
});
let newDescriptor = Object.getOwnPropertyDescriptor(modifyObj, 'originalProp');
console.log(newDescriptor);
// 输出 { value: 'original', writable: false, enumerable: false, configurable: true }

原型链上属性特性的影响

在JavaScript中,对象可以通过原型链继承属性。当访问对象的属性时,如果对象本身没有该属性,会沿着原型链向上查找。属性特性在原型链上也有其特殊的影响。

不可枚举属性在原型链上的表现

如果原型对象上的属性[[Enumerable]]false,即使通过原型链继承到了该属性,在for...in循环或者Object.keys()操作时也不会被枚举。

function Animal() {}
Animal.prototype.species = 'Mammal';
Object.defineProperty(Animal.prototype, 'hiddenSpecies', {
    value: 'Internal Species',
    enumerable: false
});
let dog = new Animal();
for (let key in dog) {
    console.log(key); // 只输出species
}
console.log(Object.keys(dog)); // 输出['species']

不可配置属性在原型链上的限制

原型对象上[[Configurable]]false的属性,在继承的对象上不能被删除或修改其除[[Value]](前提是[[Writable]]true)之外的特性。

function Shape() {}
Shape.prototype.type = 'Generic Shape';
Object.defineProperty(Shape.prototype, 'fixedType', {
    value: 'Fixed Shape',
    configurable: false
});
let circle = new Shape();
// 删除fixedType
delete circle.fixedType;
console.log(circle.fixedType); // 输出Fixed Shape,删除无效
// 严格模式下删除fixedType
'use strict';
function StrictShape() {}
StrictShape.prototype.strictFixedType = 'Strict Fixed Shape';
Object.defineProperty(StrictShape.prototype,'strictFixedType', {
    configurable: false
});
let square = new StrictShape();
delete square.strictFixedType; // 抛出TypeError: Cannot delete property'strictFixedType' of #<Object>

访问器属性在原型链上的继承

访问器属性在原型链上同样可以被继承。当继承的对象访问或设置继承来的访问器属性时,会调用原型对象上定义的[[Get]][[Set]]函数。

function Person() {}
Person.prototype._age = 0;
Object.defineProperty(Person.prototype, 'age', {
    get: function() {
        return this._age;
    },
    set: function(newValue) {
        if (typeof newValue === 'number' && newValue >= 0) {
            this._age = newValue;
        }
    }
});
let tom = new Person();
tom.age = 25;
console.log(tom.age); // 输出25,调用原型上的[[Get]]函数

对对象序列化和克隆的影响

属性特性对于对象的序列化和克隆操作有着重要的影响。

序列化时的属性特性

在进行对象序列化(如使用JSON.stringify())时,只有[[Enumerable]]true的数据属性会被序列化。访问器属性不会被序列化,因为JSON.stringify()只处理数据。

let serialObj = {
    _secret: 'private',
    publicProp: 'public',
    get secret() {
        return this._secret;
    }
};
Object.defineProperty(serialObj, 'nonEnumProp', {
    value: 'not enumerable',
    enumerable: false
});
let serialized = JSON.stringify(serialObj);
console.log(serialized); // 输出{"publicProp":"public"}

克隆对象时的属性特性处理

当克隆对象时,如果想要完整地复制属性及其特性,可以使用Object.getOwnPropertyDescriptors()结合Object.create()来实现。

let sourceObj = {
    prop1: 'value1',
    prop2: 20
};
Object.defineProperty(sourceObj, 'prop3', {
    value: 'configurable value',
    writable: false,
    enumerable: true,
    configurable: true
});
let descriptors = Object.getOwnPropertyDescriptors(sourceObj);
let clonedObj = Object.create(Object.getPrototypeOf(sourceObj), descriptors);
console.log(clonedObj.prop1); // 输出value1
// 尝试修改prop3
clonedObj.prop3 = 'new value';
console.log(clonedObj.prop3); // 输出configurable value,修改无效,因为writable为false

对对象扩展性和封装性的影响

属性特性对对象的扩展性和封装性有着关键的作用。

对对象扩展性的影响

  1. [[Configurable]]特性:如果对象的属性[[Configurable]]false,那么这个属性就不能被删除,也不能轻易改变其特性(除了[[Value]],在[[Writable]]true时可改)。这在一定程度上限制了对象的扩展性。例如,在一个库中定义了一些基础对象和属性,将这些属性设置为不可配置,可以防止用户意外地删除或修改关键属性,破坏库的正常功能。
// 模拟一个库中的基础对象
function LibraryBase() {}
LibraryBase.prototype.baseProp = 'default value';
Object.defineProperty(LibraryBase.prototype, 'baseProp', {
    configurable: false
});
let libraryInstance = new LibraryBase();
// 用户尝试删除baseProp
delete libraryInstance.baseProp;
console.log(libraryInstance.baseProp); // 仍然输出default value,删除无效
  1. [[Enumerable]]特性:通过设置[[Enumerable]]false,可以隐藏一些内部使用的属性,不被常规的枚举操作(如for...in)发现。这样可以让对象的公开接口更加清晰,避免用户误操作这些内部属性,同时也为对象的未来扩展留下空间。例如,一个对象可能有一些用于内部计算的属性,这些属性不应该被外部代码直接访问或修改。
let utilityObj = {
    calculate: function() {
        this._tempValue = 10;
        // 进行一些计算
        return this._tempValue * 2;
    }
};
Object.defineProperty(utilityObj, '_tempValue', {
    enumerable: false
});
for (let key in utilityObj) {
    console.log(key); // 不会输出_tempValue
}

对对象封装性的影响

  1. 数据属性的[[Writable]]特性:将数据属性的[[Writable]]设置为false,可以实现只读属性,这是封装的一种方式。外部代码只能读取属性值,而不能修改它,从而保护了对象内部数据的一致性。例如,一个表示用户信息的对象,其中的用户ID可能不应该被随意修改。
let user = {};
Object.defineProperty(user, 'userId', {
    value: '12345',
    writable: false
});
user.userId = '67890';
console.log(user.userId); // 输出12345,修改无效
  1. 访问器属性的[[Get]]和[[Set]]特性:访问器属性通过[[Get]][[Set]]函数可以对属性的访问和赋值进行控制,实现更高级的封装。可以在[[Set]]函数中添加数据验证逻辑,确保赋给属性的值是符合要求的。例如,一个表示温度的对象,温度值必须在一定范围内。
let temperature = {
    _value: 0,
    get value() {
        return this._value;
    },
    set value(newValue) {
        if (typeof newValue === 'number' && newValue >= -273.15 && newValue <= 100) {
            this._value = newValue;
        }
    }
};
temperature.value = 50;
console.log(temperature.value); // 输出50
temperature.value = 200;
console.log(temperature.value); // 仍然输出50,因为200超出范围

对性能的影响

属性特性在一定程度上也会影响性能,特别是在涉及大量对象操作或者频繁访问属性的场景中。

[[Enumerable]]特性对枚举性能的影响

当使用for...in循环或者Object.keys()等枚举操作时,如果对象中有大量[[Enumerable]]true的属性,尤其是在原型链上也存在大量可枚举属性时,枚举操作的性能会受到影响。因为引擎需要遍历并确定哪些属性应该包含在枚举结果中。

// 创建一个有大量可枚举属性的原型对象
function BigPrototype() {}
for (let i = 0; i < 10000; i++) {
    Object.defineProperty(BigPrototype.prototype, `prop${i}`, {
        value: i,
        enumerable: true
    });
}
let bigObject = new BigPrototype();
// 进行for...in循环
let start = Date.now();
for (let key in bigObject) {
    // 这里可以进行一些操作,但为了测试性能,暂不做具体操作
}
let end = Date.now();
console.log(`for...in loop took ${end - start} ms`);

相比之下,如果将一些不需要枚举的属性设置为[[Enumerable]]: false,可以提高枚举性能。

[[Writable]]和[[Configurable]]特性对属性操作性能的影响

在频繁修改属性值或者尝试删除属性的场景中,[[Writable]][[Configurable]]特性会影响性能。如果[[Writable]]false,每次尝试修改属性值都会进行额外的检查,虽然在非严格模式下不报错但操作无效,这会消耗一定的性能。同样,[[Configurable]]false时,删除属性的无效操作也会带来性能开销,尤其是在严格模式下抛出错误的情况。

let writableObj = {};
Object.defineProperty(writableObj, 'prop1', {
    value: 10,
    writable: false
});
let startWrite = Date.now();
for (let i = 0; i < 100000; i++) {
    writableObj.prop1 = i;
}
let endWrite = Date.now();
console.log(`Writing to non - writable prop took ${endWrite - startWrite} ms`);

因此,在设计对象和属性特性时,需要根据实际的使用场景,合理设置这些特性,以平衡功能和性能。

实际应用场景

  1. 数据模型的定义:在开发应用程序时,经常需要定义数据模型。通过设置属性特性,可以确保数据的完整性和一致性。例如,在一个电商应用中,商品的价格属性可以设置为[[Writable]]: false,一旦价格确定后就不能随意修改,防止价格被误操作更改。
function Product(name, price) {
    this.name = name;
    Object.defineProperty(this, 'price', {
        value: price,
        writable: false
    });
}
let phone = new Product('Smartphone', 500);
phone.price = 400;
console.log(phone.price); // 输出500,修改无效
  1. 库和框架的开发:在库和框架的开发中,属性特性用于保护内部实现和提供稳定的接口。例如,一个UI库中的组件对象,一些内部属性设置为[[Enumerable]]: false[[Configurable]]: false,防止用户意外访问或修改这些属性,破坏组件的正常工作。
// 模拟一个UI库的组件
function UIComponent() {
    this._internalState = 'initial';
    Object.defineProperty(this, '_internalState', {
        enumerable: false,
        configurable: false
    });
    this.getState = function() {
        return this._internalState;
    };
}
let component = new UIComponent();
for (let key in component) {
    console.log(key); // 不会输出_internalState
}
// 尝试删除_internalState
delete component._internalState;
console.log(component.getState()); // 仍然输出initial,删除无效
  1. 对象的代理和拦截:在使用Proxy进行对象代理和拦截时,属性特性也起着重要作用。可以根据属性特性来决定如何拦截和处理属性的访问、赋值、删除等操作。例如,对于不可写的属性,可以在代理中抛出更友好的错误提示。
let target = {
    readonlyProp: 'value'
};
Object.defineProperty(target,'readonlyProp', {
    writable: false
});
let handler = {
    set(target, prop, value) {
        if (!Object.getOwnPropertyDescriptor(target, prop).writable) {
            throw new Error('Cannot set read - only property');
        }
        target[prop] = value;
        return true;
    }
};
let proxy = new Proxy(target, handler);
proxy.readonlyProp = 'new value'; // 抛出Error: Cannot set read - only property

综上所述,JavaScript的属性特性对对象的行为、功能、性能以及应用开发的各个方面都有着深远的影响。深入理解和合理运用这些特性,能够帮助开发者编写出更健壮、高效且易于维护的代码。无论是在小型脚本还是大型应用程序的开发中,属性特性都是不可忽视的重要组成部分。通过正确设置和利用属性特性,可以实现对象的合理封装、有效扩展以及与其他代码模块的良好交互。在实际开发过程中,根据具体的需求和场景,精心设计对象的属性特性,是提升代码质量和开发效率的关键一环。例如,在构建复杂的业务逻辑时,合理运用属性的可配置性和可写性,可以确保数据的安全性和一致性;在进行性能敏感的操作时,注意属性的可枚举性对遍历性能的影响,能够优化程序的执行效率。总之,熟练掌握JavaScript属性特性对对象的影响,是成为一名优秀JavaScript开发者的必备技能。