对象的属性类型、语法及解构
# 理解对象
对象是一组没有特定顺序的值,对象的每个属性或方法都由一个名称来表示,这个名称映射到一个值。
# 属性类型
ECMA-262规范用[[ ]]
将一些内部特性的名称括起来,开发者不能在JavaScript中直接访问这些特性。属性分两种:
# 数据属性
该属性包含一个保存数据值的位置,用于值的读取和写入。数据属性有4个特性用来描述它们的行为:
- [[Configurable]]:表示是否可以通过delete删除并重新定义、是否可以修改它的特性、以及是否可以把它改为访问器属性,默认直接定义在对象上的属性时该特性为true
- [[Enumerable]]:表示属性是否可以通过for-in循环返回,默认直接定义在对象上的属性时该特性为true
- [[Writable]]:表示属性的值是否可以被修改,默认直接定义在对象上的属性时该特性为true
- [[Value]]:包含属性实际的值,默认为undefined
要修改属性的默认特性,就必须使用Object.defineProperty()
方法。这个方法接收3个参数:对象名、属性名和一个描述对象,描述对象上的属性可以包含:configurable、enumerable、writable 和 value,跟相关特性的名称一一对应。根据要修改的特性,可以设置其中一个或多个值。
let person = {};
Object.defineProperty(person, "name", {
writable: false,
value: "Nicholas"
});
console.log(person.name); // "Nicholas"
person.name = "Greg";
console.log(person.name); // "Nicholas"
// 因为设置了writable为false,所以无法修改该属性的值
2
3
4
5
6
7
8
9
注:
- 一个属性被定义为不可配置之后,就 不能再变回可配置的了,且再次调用Object.defineProperty()并修改任何非value属性会导致错误(如果此前writable为true则修改有效,否则无效)
- 在调用Object.defineProperty()时,configurable、enumerable 和 writable 的值如果不 指定,则都默认为 false
# 访问器属性
访问器属性不包含数据值,它包含一个getter
函数和一个setter
函数。在读取访问器属性时,会调用 getter 函数,这个函数用于返回一个有效的值;在写入访问器属性时,会调用 setter 函数并传入新值,这个函数决定对数据做出什么修改。(其中 getter 和 setter 是概念,get 和 set 是具体实现方法)访问器属性的4个特性:
- [[Configurable]]:表示是否可以通过delete删除并重新定义、是否可以修改它的特性、以及是否可以把它改为数据属性,默认直接定义在对象上的属性时该特性为true
- [[Enumerable]]:表示属性是否可以通过for-in循环返回,默认直接定义在对象上的属性时该特性为true
- [[Get]]:获取函数,在读取属性时调用,默认值为undefined
- [[Set]]:设置函数,在写入属性时调用,默认值为undefined
访问器属性时不能直接定义的,必须使用Object.defineProperty()
。
// 定义一个对象,包含伪私有成员 year_和公共成员 edition
let book = {
year_: 2017, // 下划线常用来表示该属性并不希望在对象方法的外部被访问
edition: 1
};
Object.defineProperty(book, "year", {
get() {
return this.year_;
},
set(newValue) {
if (newValue > 2017) {
this.year_ = newValue;
this.edition += newValue - 2017;
}
}
});
book.year = 2018;
console.log(book.edition); // 2
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
这是访问器属性的经典使用场景,即设置一个属性值会导致一些其他变化发生。其中 get 和 set 函数不一定都要定义,如只定义 get 意味着属性是只读的。 在不支持 Object.defineProperty()的浏览器中没有办法修改[[Configurable]]或[[Enumerable]]。
数据属性是实际存储属性值的属性,而访问器属性不实际存储数据,专门提供对其他数据/变量的保护。如果不使用Object.defineProperty()
或者Object.defineProperties()
以及指定get和set等特殊方法定义的对象属性,默认都是数据属性。
# 定义多个属性
ES提供了一个可以同时定义多个属性的方法Object.defineProperties('要操作的对象','一个描述符对象')
,不过通过字面量直接定义也可以,但不能字面量形式不能直接定义访问器属性。其中通过 Object.defineProperties()定义的数据属性的前三个特性都是false。
let book = {};
Object.defineProperties(book, {
year_: {
value: 2017
},
edition: {
value: 1
},
year: {
get() {
return this.year_;
},
set(newValue) {
if (newValue > 2017) {
this.year_ = newValue;
this.edition += newValue - 2017;
}
}
}
});
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
# 读取属性的特性
Object.getOwnPropertyDescriptor('操作对象', '属性名')
,返回值是一个对象,包含对应属性的4个特性。
//采用上一段代码的例子
let descriptor = Object.getOwnPropertyDescriptor(book, "year_");
console.log(descriptor.value); // 2017
console.log(descriptor.configurable); // false
console.log(typeof descriptor.get); // "undefined"
let descriptor = Object.getOwnPropertyDescriptor(book, "year");
console.log(descriptor.value); // undefined
console.log(descriptor.enumerable); // false
console.log(typeof descriptor.get); // "function"
2
3
4
5
6
7
8
9
ES2017新增了Object.getOwnPropertyDescriptors()
静态方法。这个方法实际上就是在每个自有属性上调用 Object.getOwnPropertyDescriptor() 并在一个新对象中返回它们。
console.log(Object.getOwnPropertyDescriptors(book));
/* {
edition: {
configurable: false,
enumerable: false,
value: 1,
writable: false
},
year: {
configurable: false,
enumerable: false,
get: f(),
set: f(newValue),
},
year_: {
configurable: false,
enumerable: false,
value: 2017,
writable: false
}
}*/
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
# 合并对象
合并对象就是把原对象所有本地属性一起复制到目标对象上。ES6提供了合并对象的方法Object.assign()
,这个方法接收一个目标对象和一个或多个源对象作为参数,然后将每个源对象中可枚举(Object.propertyIsEnumerable() 返回 true)和自有(Object.hasOwnProperty() 返回 true)属性复制到目标对象。以字符串和符号为键的属性会被复制。对每个符合条件的属性,这个方法会使用源对象上的[[Get]]
取得属性的值,然后使用目标对象上的[[Set]]
设置属性的值。
let dest, src, result;
/**
* 简单复制
*/
dest = {};
src = { id: 'src' };
result = Object.assign(dest, src);
// Object.assign 修改目标对象
// 也会返回修改后的目标对象
console.log(dest === result); // true
console.log(dest !== src); // true
console.log(result); // { id: src }
console.log(dest); // { id: src }
/**
* 多个源对象
*/
dest = {};
result = Object.assign(dest, { a: 'foo' }, { b: 'bar' });
console.log(result); // { a: foo, b: bar }
/**
* 获取函数与设置函数
*/
dest = {
set a(val) {
console.log(`Invoked dest setter with param ${val}`);
}
};
src = {
get a() {
console.log('Invoked src getter');
return 'foo';
}
};
Object.assign(dest, src);
// 调用 src 的获取方法
// 调用 dest 的设置方法并传入参数"foo"
// 因为这里的设置函数不执行赋值操作
// 所以实际上并没有把值转移过来
console.log(dest); // { set a(val) {...} }
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
Object.assign() 实际上对每个源对象执行的是浅复制。如果多个源对象都有相同的属性,则使用最后一个复制的值。不能在两个对象间转移获取函数和设置函数。如果赋值期间出错,则操作会中止并退出,完成部分复制,同时抛出错误。
let dest, src, result;
/**
* 错误处理
*/
dest = {};
src = {
a: 'foo',
get b() {
// Object.assign()在调用这个获取函数时会抛出错误
throw new Error();
},
c: 'bar'
};
try {
Object.assign(dest, src);
} catch(e) {}
// Object.assign()没办法回滚已经完成的修改
// 因此在抛出错误之前,目标对象上已经完成的修改会继续存在:
console.log(dest); // { a: foo }
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
# 对象标识及相等判定
在ES6之前,有些特殊情况即使是===
操作符也无能为力。
// 这些是===符合预期的情况
console.log(true === 1); // false
console.log({} === {}); // false
console.log("2" === 2); // false
// 这些情况在不同 JavaScript 引擎中表现不同,但仍被认为相等
console.log(+0 === -0); // true
console.log(+0 === 0); // true
console.log(-0 === 0); // true
// 要确定 NaN 的相等性,必须使用极为讨厌的 isNaN()
console.log(NaN === NaN); // false
console.log(isNaN(NaN)); // true
2
3
4
5
6
7
8
9
10
11
为了改善这一情况,ES6新增了Object.is()
,这个方法和===
很像,但也考虑了上面这些情况。这个方法必须接收两个参数:
console.log(Object.is(true, 1)); // false
console.log(Object.is({}, {})); // false
console.log(Object.is("2", 2)); // false
// 正确的 0、-0、+0 相等/不等判定
console.log(Object.is(+0, -0)); // false
console.log(Object.is(+0, 0)); // true
console.log(Object.is(-0, 0)); // false
// 正确的 NaN 相等判定
console.log(Object.is(NaN, NaN)); // true
2
3
4
5
6
7
8
9
# 增强的对象语法
ES6新增了很多有用的语法糖,这些特性没有改变现有引擎的行为,但提升了处理对象的方便程度,这些对象语法同样适用与ES6的类。
属性值简写
let name = 'Matt'; let person = { name // 与 name: name等价 }; console.log(person); // { name: 'Matt' }
1
2
3
4
5注:简写属性名只要使用变量名就会被自动解释为同名的属性键,如果没有找到同名变量,则会抛出ReferenceError(经测试,如果没有找到同名变量,会被解释成空串,可能是后面规范修改的)
可计算属性
在引入可计算属性之前,如果想使用变量的值作为属性,那么必须先声明对象,然后使用中括号语法来添加属性。换句话说,不能在对象字面量中直接动态命名属性。有了可计算属性,就可以直接在对象字面量中完成动态属性赋值。
const nameKey = 'name'; const ageKey = 'age'; const jobKey = 'job'; // 在没引入可计算属性之前 let person = {}; person[nameKey] = 'Matt'; person[ageKey] = 27; person[jobKey] = 'Software engineer'; // 引入可计算属性之后 let person = { [nameKey]: 'Matt', [ageKey]: 27, [jobKey]: 'Software engineer' }; console.log(person); // { name: 'Matt', age: 27, job: 'Software engineer' }
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15可计算属性本身可以是复杂的表达式,在实例化时再求值。可计算属性表达式中抛出任何错误都会中断对象创建。如果计算属性的表达式有副 作用,那就要小心了,因为如果表达式抛出错误,那么之前完成的计算是不能回滚的。
简写方法名
// 在给对象定义方法时,通常都要写一个方法名、冒号,然后再引用一个匿名函数表达式 let person = { sayName: function(name) { console.log(`My name is ${name}`); } }; // 简写方法 let person = { sayName(name) { console.log(`My name is ${name}`); } }; person.sayName('Matt'); // My name is Matt // 简写方法名对获取和设置函数也适用 let person = { name_: '', get name() { return this.name_; }, set name(name) { this.name_ = name; }, sayName() { console.log(`My name is ${this.name_}`); } }; person.name = 'Matt'; person.sayName(); // My name is Matt // 简写方法名与可计算属性键相互兼容 const methodKey = 'sayName'; let person = { [methodKey](name) { console.log(`My name is ${name}`); } } person.sayName('Matt'); // My name is Matt
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38简写方法名对ES6的类更有用。
# 对象解构
对象解构就是使用与对象匹配的结构来实现对象属性赋值。
let person = {
name: 'Matt',
age: 27
};
// 不使用对象解构
let personName = person.name, personAge = person.age;
// 使用对象解构
let { name: personName, age: personAge } = person;
// 或者
let personName, personAge;
({name: personName, age: personAge} = person);// 如果是给实现声明的变量赋值,则赋值表达式必须包含在一对括号中
console.log(personName); // Matt
console.log(personAge); // 27
// 如果想让变量直接使用属性的名称,可以使用简写语法
let { name, age } = person;
console.log(name); // Matt
console.log(age); // 27
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
- 解构赋值不一定与对象的属性匹配。赋值的时候可以忽略某些属性,而如果引用的属性不存在,则该变量的值就是 undefined。也可以在解构赋值的同时定义默认值。
- 解构在内部会使用函数 ToObject()(不能在运行时环境中直接访问)把源数据结构转换为对象。这意味着在对象解构的上下文中,原始值会被当成对象。这也意味着(根据 ToObject()的定义),null 和 undefined 不能被解构,否则会抛出错误。
- 在外层属性没有定义的情况下不能使用嵌套解构。
- 如果解构赋值出错,则整个解构赋值只会完成一部分。
- 在函数参数列表中也可以进行解构赋值。
let { length } = 'foobar';
console.log(length); // 6
let { constructor: c } = 4;
console.log(c === Number); // true
let { _ } = null; // TypeError
let { _ } = undefined; // TypeError
2
3
4
5
6