一、什么是对象
深入对象之前,首先我们要知道,什么是对象,在ECMA-262标准中定义如下:
对象(object)是一个“属性的无序集合,每个属性存放一个原始值、对象或函数”。严格来说,这意味着对象是无特定顺序的值的数组。
二、对象属性
ECMA-262 使用一些内部特性来描述属性的特征。我们不能在JavaScript中直接访问到这些特性,为了与普通属性区分,一般会使用[[]]将属性名称括起来。
在JavaScript中,对象的属性特性分两种:1. 数据属性,2. 访问器属性
1、 数据属性
数据属性包含一个保存数据值的位置。值会从这个位置读取,也会写入到这个位置。数据属性有4个特性描述它们的行为
[[Configurable]]:表示属性是否可以通过 delete 删除并重新定义,是否可以修改它的特性,以及是否可以把它改为访问器属性。默认情况下,所有直接定义在对象上的属性的这个特性都是 true。[[Enumerable]]:表示属性是否可以通过 for-in 循环返回。默认情况下,所有直接定义在对象上的属性的这个特性都是 true。[[Writable]]:表示属性的值是否可以被修改。默认情况下,所有直接定义在对象上的属性的这个特性都是 true。[[Value]]:包含属性实际的值。这就是前面提到的那个读取和写入属性值的位置。这个特性的默认值为 undefined。
前面我们说过,我们不能在JavaScript中直接访问到这些属性,所以这里有对应的方法查看和修改这些特性。
Object.getOwnPropertyDescriptor(target, propertyName)方法可以查看对象中指定属性的特性,这一方法接收两个参数,目标对象和想要查看的对象属性名,返回一个描述符对象,即带有上面四个特性的对象。
let o = {
name: 'John',
age: 23
}
console.log(Object.getOwnPropertyDescriptor(o, "name"));
console.log(Object.getOwnPropertyDescriptor(o, "age"));
// { value: 'John', writable: true, enumerable: true, configurable: true }
// { value: 23, writable: true, enumerable: true, configurable: true }
Object.getOwnPropertyDescriptors(target)方法可以查看对象内所有属性的特性,这一个方法只接收目标对象这一个参数,返回一个对象,里面带有各属性的描述符对象。
let o = {
name: 'John',
age: 23
}
console.log(Object.getOwnPropertyDescriptors(o));
/** {
name: {
value: 'John',
writable: true,
enumerable: true,
configurable: true
},
age: { value: 23, writable: true, enumerable: true, configurable: true }
}
*/
Object.defineProperty(target, property, options)方法可以在对象上修改或定义属性及其特性,这一方法接收三个参数,目标对象,属性名,描述符对象,若属性名是目标对象上已有属性则修改此属性,若目标对象没有该属性则定义一个新属性,并且其特性为输入的描述符对象。
let o = {
name: "John",
age: 23,
};
Object.defineProperty(o, "name", {
value: "Jack",
});
console.log(o.name); // Jack
在严格模式下,修改只读属性的属性值时会抛出错误,同理,delete 不可配置属性也会抛出错误。
let o = {
name: 'John',
age: 23
}
/** 将name属性修改为只读属性 */
Object.defineProperty(o, "name", {
writable: false,
});
// 严格模式下会抛出错误,非严格模式会无视此次修改
o.name = "John" // TypeError: Cannot assign to read only property 'name' of object
Object.defineProperty(o, "age", {
configurable: false,
});
delete o.age; // TypeError: Cannot delete property 'age' of #<Object>
一旦某个属性定义为不可配置属性后,就将不再可配置,修改其属性特性除[[value]]以外的任何特性都会报错,可能会有读者发现[[writable]]特性也可以修改,这里笔者先暂且不表。
Object.defineProperty(o, "age", {configurable: false,});
Object.defineProperty(o, "age", {configurable: true,});
// TypeError: Cannot redefine property: age
Object.defineProperty(o, "age", { enumerable: false });
// TypeError: Cannot redefine property: age
o.age += 1;
console.log(o.age); // 24
[[writable]]特性相较其它特性有些特殊,它在[[configurable]]特性值为false,自身特性值为true时可修改为false,但若自身特性值为false时,修改为true时会抛出错误。
let o = {
name: "John",
age: 23,
};
Object.defineProperty(o, "age", {
configurable: false,
});
console.log(Object.getOwnPropertyDescriptor(o, "age"));
// { value: 23, writable: true, enumerable: true, configurable: false }
Object.defineProperty(o, "age", {
writable: false,
});
console.log(Object.getOwnPropertyDescriptor(o, "age"));
// { value: 23, writable: false, enumerable: true, configurable: false }
Object.defineProperty(o, "age", {writable: true,});
// TypeError: Cannot redefine property: age
下面是定义新属性,方法与修改属性类似,如果描述符对象中未给value赋值,默认值为undefined。
Object.defineProperty(o, "friend", {});
console.log(o); // {"name": "John", "age": 23}
console.log(o.friend); // undefined
console.log(Object.getOwnPropertyNames(o)); // [ 'name', 'age', 'friend' ]
Object.defineProperty(o, "friend", {
value: "Jack",
});
console.log(o.friend); // Jack
但是要注意,如果不显示指定除[[value]]特性以外的特性,默认都为false,我们查看上面例子的描述符对象
let o = {
name: "John",
age: 23,
};
Object.defineProperty(o, "friend", {});
console.log(Object.getOwnPropertyDescriptor(o, "friend"));
// {value: undefined, writable: false, enumerable: false, configurable: false}
2、访问器属性
访问器属性相较数据属性不包含数据值,即不包含[[value]]和[[writable]]特性,相对地,它包含一个getter函数和一个setter函数。与数据属性一样,访问器属性也只能通过Object.defineProperty方法定义。
[[Configurable]]:表示属性是否可以通过delete删除并重新定义,是否可以修改它的特性,以及是否可以把它改为数据属性。默认情况下,所有直接定义在对象上的属性的这个特性都是true。[[Enumerable]]:表示属性是否可以通过for-in循环返回。默认情况下,所有直接定义在对象上的属性的这个特性都是true。[[Get]]:getter函数,在读取属性时调用。默认值为undefined。[[Set]]:setter函数,在写入属性时调用。默认值为undefined。
下面是一个例子:
let student = {
name: "John",
_age: 22,
};
Object.defineProperty(student, "age", {
get() {
return this._age;
},
set(newVal) {
this._age = newVal;
},
});
console.log(student.age); // 22
student.age++;
console.log(student.age); // 23
需要注意的是,访问器属性与数据属性是一致的,在不显示定义[[configurable]]特性与[[enumerable]]特性时,默认值为false。
console.log(Object.getOwnPropertyDescriptor(student, "age"));
// {get: [Function: get], set: [Function: set],
// enumerable: false, configurable: false}
当然getter函数与setter函数不一定要同时设置,可以只设置getter函数或只设置setter函数,但只设立getter函数的属性表现类似只读属性,修改属性值会抛出错误,但读取只设立了setter函数的属性只会显示undefined(红宝书中说会抛出错误,但笔者实现不出这一结果,望评论区大佬指导)。
let student = {
name: "John",
_age: 22,
};
Object.defineProperty(student, "age", {
get() {
return this._age;
},
});
student.age++; // TypeError: Cannot set property age of #<Object> which has only a getter
Object.defineProperty(student, "age", {
set(newVal) {
this._age = newVal;
},
})
console.log(student.age) // undefined
总结
数据属性与访问器属性的组合行为表现可以总结为以下一个表格。
| configurable:true, writable: true | configurable:true, writable: false | configurable:false, writable: true | configurable:false, writable: false | |
|---|---|---|---|---|
| 修改属性值 | Y | Y | Y | N |
| 赋值修改属性值 | Y | N | Y | N |
| delete该属性 | Y | Y | N | N |
| 修改getter/setter | Y | Y | N | N |
可能会有读者对第二列前两行结果不一样有疑问,这里举个例子说明:
let o = {};
Object.defineProperty(o, "name", {
configurable: true,
writable: false,
value: "Jack",
});
console.log(o.name); // Jack
Object.defineProperty(o, "name", {
value: "John",
});
console.log(o.name); // John
o.name = "Mary"; // TypeError: Cannot assign to read only property 'name' of object
结尾语
写完后回看本文读感略显枯燥,但笔者还是认为这种基础概念性问题总归是show me the code,自己实际敲一遍就能理解并留有映象,还请多多包涵。也欢迎各位大佬交流与勘正。
参考资料
《JavaScript高级程序设计》