✨深入理解JavaScript对象之对象属性

155 阅读6分钟

一、什么是对象

深入对象之前,首先我们要知道,什么是对象,在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: trueconfigurable:true, writable: falseconfigurable:false, writable: trueconfigurable:false, writable: false
修改属性值YYYN
赋值修改属性值YNYN
delete该属性YYNN
修改getter/setterYYNN

可能会有读者对第二列前两行结果不一样有疑问,这里举个例子说明:

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高级程序设计》