对象、类和面向对象编程
对象
对象是数个属性无序的集合。
ECMA-262使用一些内部特性来描述属性的特征(对象的属性的特性)。
属性分为数据属性(定义属性时使用)和访问器属性(获取或设置属性值时使用):
数据属性
数据属性包含一个保存数据值的位置。值会从这个位置读取,也会写入到这个位置。数据属性有 4个特性描述它们的行为。
[[Configurable]]:表示属性是否可以通过 delete 删除并重新定义,是否可以修改它的特性,以及是否可以把它改为访问器属性。默认情况下,所有直接定义在对象上的属性的这个特性都是 true,如前面的例子所示。
[[Enumerable]]:表示属性是否可以通过 for-in 循环返回。默认情况下,所有直接定义在对象上的属性的这个特性都是 true,如前面的例子所示。
[[Writable]]:表示属性的值是否可以被修改。默认情况下,所有直接定义在对象上的属性的这个特性都是 true,如前面的例子所示。
[[Value]]:包含属性实际的值。这就是前面提到的那个读取和写入属性值的位置。这个特性的默认值为 undefined。
将属性显式添加到对象之后,[[Configurable]]、[[Enumerable]]和[[Writable]]都会被默认设置为 true,而[[Value]]特性会被设置为指定的值。
访问器属性
[[Configurable]]:表示属性是否可以通过 delete 删除并重新定义,是否可以修改它的特性,以及是否可以把它改为访问器属性。默认情况下,所有直接定义在对象上的属性的这个特性都是 true,如前面的例子所示。
[[Enumerable]]:表示属性是否可以通过 for-in 循环返回。默认情况下,所有直接定义在对象上的属性的这个特性都是 true,如前面的例子所示。
[[Get]]:获取函数,在读取属性时调用。默认值为 undefined。
[[Set]]:设置函数,在写入属性时调用。默认值为 undefined。
// 定义一个对象,包含伪私有成员 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
year_中的下划线常用来表示该属性并不希望在对象方法的外部被访问。
这是访问器属性的典型使用场景,即设置一个属性值会导致一些其他变化发生。
获取函数和设置函数不一定都要定义。只定义获取函数意味着属性是只读的,尝试修改属性会被忽略。在严格模式下,尝试写入只定义了获取函数的属性会抛出错误。类似地,只有一个设置函数的属性
是不能读取的,非严格模式下读取会返回 undefined,严格模式下会抛出错误。
Object.defineProperty(给其添加属性的对象,属性名称,描述符对象):用来修改对象某一个属性的特性,如果省略第三个参数则会把属性特性的值设置为false;
Object.defineProperties(要为之添加或修改属性的对象,描述符对象):通过多个描述符一次性定义多个属性;
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;
}
}
}
});
唯一的区别是所有属性都是同时定义的,并且数据属性的configurable、enumerable 和 writable 特性值都是 false。
Object.getOwnPropertyDescriptor(属性所在的对象,要取得其描述符的属性名):取得指定属性的属性描述符;
Object.getOwnPropertyDescriptors(属性所在的对象):取得指定对象的所有属性的属性描述符;
Object.assign(一个目标对象,数个源对象):合并对象(merge或mixin),把源对象所有的本地属性一起复制到目标对象上。
将每个源对象中可枚举(Object.propertyIsEnumerable()返回 true)和自有(Object.hasOwnProperty()返回 true)属性复制到目标对象。
对每个符合条件的属性,这个方法会使用源对象上的[[Get]]取得属性的值,然后使用目标对象上的[[Set]]设置属性的值。
Object.assign 修改目标对象,也会返回修改后的目标对象;
Object.assign()实际上对每个源对象执行的是浅复制,两个对象的属性值是相互影响的,深拷贝两个对象的属性值是不相互影响。如果多个源对象都有相同的属性,则使用最后一个复制的值。
* 获取函数与设置函数
*/
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) {...} }
相等判定
// 在全等操作符中0、-0、+0之间是true
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
es6新增了Object.is(arg1, arg2)
// 0和+0返回true, 0和-0返回false,
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
可计算属性
在引入可计算属性之前,不能在对象字面量中直接动态命名属性:
const nameKey = 'name';
let person = {};
person[nameKey] = 'Matt';
有了可计算属性,就可以在对象字面量中完成动态属性赋值,可计算属性本身可以是复杂的表达式:
const nameKey = 'name';
const methodKey = 'sayName';
let person = {
[nameKey]: 'Matt',
[methodKey](name) {
console.log(`My name is ${name}`);
}
};
对象解构
对象解构就是使用与对象匹配的嵌套层级结构来实现对象属性赋值。
let personHeight ;
let person = {
name: 'Matt',
age: 27,
height:"178cm"
};
let { name: personName, age,job,sex='Software engineer' } = person;
console.log(job); // undefined({height: personHeight} = person);
console.log(sex); // Software engineer
解构在内部使用函数 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
在外层属性没有定义的情况下不能使用嵌套解构。