对象、类与面向对象编程
ECMA-262 将对象定义为一组属性的无序集合。严格来说,这意味着对象就是一组没有特定顺序的值。对象的每个属性或方法都由一个名称来标识,这个名称映射到一个值,值可以是数据或者函数。
理解对象
创建自定义对象的通常方式是创建 Object 的一个新实例,然后再给它添加属性和方法,如下例所示:
const person = new Object();
person.name = 'Joker';
person.age = 18;
person.sayName = function () {
console.log(this.name);
};
早期 JavaScript 开发者频繁使用这种方式创建新对象。几年后,对象字面量变成了更流行的方式。如下等价写法:
const person = {
name: 'Joker',
age: 18,
sayName() {
console.log(this.name);
},
};
属性的类型
ECMA-262 使用一些内部特性来描述属性的特征。这些特性是由为 JavaScript 实现引擎的规范定义的。因此,开发者不能在 JavaScript 中直接访问这些特性。为了将某个特性标识为内部特性,规范会用两个中括号把特性的名称括起来,比如 [[Enumerable]]
。
属性分两种:数据属性和访问器属性。
数据属性
数据属性包含一个保存数据值的位置。值会从这个位置读取,也会写入到这个位置。数据属性有 4 个特性描述它们的行为。
-
[[Configurable]]
表示属性是否可以通过 delete
删除并重新定义,是否可以修改它的特性,以及是否可以把它改为访问器属性。默认值为 true
。
-
[[Enumerable]]
表示属性是否可以通过 for-in 循环返回。默认值为 true
。
-
[[Writable]]
表示属性的值是否可以被修改。默认值为 true
。
-
[[Value]]
包含属性实际的值。这就是前面提到的那个读取和写入属性值的位置。默认值为 undefined
。
在创建对象时,进行显式添加属性,会默认给该属性赋予如上四个特性值。要修改属性的默认特性,就必须使用 Object.defineProperty()
方法。
Object.defineProperty()
方法接收 3 个参数:对象(要定义或修改属性的对象)、属性名(要定义或修改的属性的名称)、属性描述符对象(一个描述属性特性的对象,可以包含以下可选属性:value
、writable
、enumerable
、configurable
)
// 对已定义属性进行修改
let person = {
name: 'Joker',
};
Object.defineProperty(person, 'name', {
// 不可配置
configurable: false,
});
// 删除被忽略
delete person.name;
console.log(person.name); // joker
// 添加新属性
Object.defineProperty(person, 'age', {
value: 18,
});
console.log(person.age); // 18
这个例子把 configurable 设置为 false,意味着这个属性不能从对象上删除。非严格模式下对这个属性调用 delete 没有效果,严格模式下会抛出错误。此外,一个属性被定义为不可配置之后,就不能再变回可配置的了。再次调用 Object.defineProperty()
两种情况会导致抛出错误:
- 尝试将一个不可配置的属性的
configurable
特性改为true
。 - 尝试将一个不可配置的访问器属性的
enumerable
特性改为false
,或者将get
或set
属性改为undefined
。
Object.defineProperties()
是 Object.defineProperty()
的扩展版本,允许同时定义或修改多个属性。它接收两个参数:对象(要定义或修改属性的对象)、属性描述符对象(一个包含多个属性描述符的对象)。
属性描述符对象:是一个对象,其中每个键表示要定义或修改的属性的名称,每个值必须是且只能是数据描述符或访问器描述符之一。
const obj = {};
Object.defineProperties(obj, {
property1: {
value: true,
writable: true,
},
property2: {
value: 'Hello',
writable: false,
},
});
访问器属性
访问器属性是对象属性的一种类型,与数据属性不同,它不包含数据值,而是包含一个获取(getter)函数和一个设置(setter)函数。这两个函数可以自定义对象属性的读取和写入行为。但是,这两个函数并不是必需的,可以只定义其中一个。当读取访问器属性时,会调用获取函数,而在写入访问器属性时,则会调用设置函数,并将新值作为参数传递给设置函数。访问器属性有四个特性描述它们的行为,分别是:
-
get
获取函数,用于定义读取属性时的行为。当访问该属性时,会调用这个函数来获取属性值。
-
set
设置函数,用于定义写入属性时的行为。当对属性赋值时,会调用这个函数,并将新值作为参数传递给它。
-
enumerable
-
configurable
enumerable
和 configurable
与上述作用相同,不再赘述。
访问器属性是不能直接定义的,必须使用 Object.defineProperty()
。
// 定义一个对象,以下划线开头的为私有成员
const person = {
_age: 0,
};
Object.defineProperty(person, 'age', {
get: function () {
return this._age;
},
set: function (value) {
if (value < 0 || value > 100) {
throw new Error('Invalid age');
}
this._age = value;
},
});
person.age = 20; // 成功设置
console.log(person.age); // 20
get
函数和 set
函数不一定都要定义。只定义 get
函数意味着属性是只读的,尝试修改属性会被忽略。在严格模式下,尝试写入只定义了 get
函数的属性会抛出错误。类似地,只有一个 set
函数的属性是不能读取的,非严格模式下读取会返回 undefined
,严格模式下会抛出错误。
读取属性的特性
使用 Object.getOwnPropertyDescriptor()
方法可以取得指定属性的属性描述符。
Object.getOwnPropertyDescriptor()
是一个用于获取对象属性描述符的方法。它接收两个参数:对象(要获取属性描述符的对象)和属性名(要获取描述符的属性的名称)。
调用该方法返回一个属性描述符对,包含了指定属性的所有特性,包括 value
、writable
、enumerable
和 configurable
。如果指定的属性不存在,则返回 undefined
。
// 省略定义属性特性,照搬如上 person 对象
const person = {
_age: 0,
};
let descriptor = Object.getOwnPropertyDescriptor(person, 'age');
console.log(descriptor);
/* 输出结果:
{
get: [Function: get],
set: [Function: set],
enumerable: false,
configurable: false
}
*/
ECMAScript 2017 新增了 Object.getOwnPropertyDescriptors()
静态方法。这个方法实际上会在每个自有属性上调用 Object.getOwnPropertyDescriptor()
并在一个新对象中返回它们。
Object.getOwnPropertyDescriptors()
是一个用于获取对象所有属性描述符的方法。返回对象的所有属性的描述符,而不仅仅是指定属性的描述符。它接收一个参数:对象(要获取属性描述符的对象)。
const book = {
name: 'JavaScript',
time: '2024年3月15',
};
let descriptor = Object.getOwnPropertyDescriptors(book);
console.log(descriptor);
/* 输出结果:
{
name: {
value: 'JavaScript',
writable: true,
enumerable: true,
configurable: true
},
time: {
value: '2024年3月15',
writable: true,
enumerable: true,
configurable: true
}
}
*/
合并对象
ECMAScript 6 专门为合并对象提供了 Object.assign()
方法。用于把源对象所有的本地属性一起复制到目标对象上。
Object.assign()
方法用于合并对象的属性。它接收一个目标对象和一个或多个源对象作为参数,并将每个源对象中可枚举和自有的属性复制到目标对象中。
可枚举属性:Object.propertyIsEnumerable()
返回 true
自有的属性:Object.hasOwnProperty()
返回 true
// 定义目标对象
const target = { a: 1, b: 2 };
// 定义源对象1
const source1 = { b: 3, c: 4 };
// 定义源对象2
const source2 = { d: 5 };
// 使用 Object.assign() 合并属性到目标对象中
const result = Object.assign(target, source1, source2);
// 如果源对象中存在同名属性,则会覆盖目标对象的属性值。
console.log(result); // 输出: { a: 1, b: 3, c: 4, d: 5 }
console.log(target === result); // true
Object.assign()
实际上对每个源对象执行的是浅复制。如果多个源对象都有相同的属性,则使用最后一个复制的值。此外,从源对象访问器属性取得的值,比如获取函数,将其返回值赋给目标对象,而不是获取函数本身。
let dest = {
set id(x) {
console.log(x);
},
};
Object.assign(dest, { id: 'first' }, { id: 'second' }, { id: 'third' });
// first
// second
// third
let desc = {};
let source = { name: {} };
Object.assign(desc, source);
console.log(desc.name === source.name); // true
如果赋值期间出错,则操作会中止并退出,同时抛出错误。Object.assign()
没有“回滚”之前赋值的概念,因此它是一个尽力而为、可能只会完成部分复制的方法。
let desc = {};
let source = {
a: 'foo',
get b() {
// Object.assign()在调用这个获取函数时会抛出错误
throw new Error();
},
c: 'bar',
};
try {
Object.assign(desc, source);
} catch (e) {}
// 因此在抛出错误之前,目标对象上已经完成的修改会继续存在
console.log(desc); // { a: foo }
对象标识及相等判定
ECMAScript 6 之前,有些特殊情况即使是 ===
操作符也无能为力:
// 这些是===符合预期的情况
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
为改善这类情况,ECMAScript 6 规范新增了 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
要检查超过两个值,递归地利用相等性传递即可:
/*
每次递归调用时都会检查剩余参数中的第一个参数与剩余参数中的其余参数是否相等,
直到剩余参数数组为空或只有一个参数。
*/
function recursivelyCheckEqual(x, ...rest) {
return (
Object.is(x, rest[0]) && (rest.length < 2 || recursivelyCheckEqual(...rest))
);
}
对象的增强语法
ECMAScript 6 为定义和操作对象新增了很多极其有用的语法糖特性。这些特性都没有改变现有引擎的行为,但极大地提升了处理对象的方便程度。
属性值简写
在给对象添加变量的时候,如果属性名和变量名是一样的,可以使用如下写法:
let name = 'Joker';
let person = {
name,
};
console.log(person); // { name: 'Joker' }