Object 对象
Object 的作用
在 JavaScript 中,Object 有两个主要作用
- 作为键值对的容器,用于存储和管理数据
- 作为一个构造函数,用于创建对象实例
对象包装类型
在 JavaScript 中,当我们将基本类型作为参数传入 Object() 构造函数时,会根据参数的类型不同,生成对应的包装类型对象。
// null 和 undefined 没有对应的包装类型,传入会直接返回
const obj1 = new Object(undefined);
const obj2 = new Object(null);
// 数字类型会被包装成 Number 对象
const num = new Object(123);
console.log(num); // Number {123}
// 字符串类型会被包装成 String 对象
const str = new Object('abc');
console.log(str); // String {'abc'}
// Symbol 类型会被包装成 Symbol 对象
const sym = new Object(Symbol('foo'));
console.log(sym); // Symbol {Symbol(foo)}
需要注意的是,如果传入 Object() 的参数本身就是一个对象,那么会直接返回这个对象,不会进行二次包装。
const originObj = { x: 1 };
const wrapperObj = Object(originObj);
console.log(originObj === wrapperObj); // true
const originArr = [1, 2, 3];
const wrapperArr = Object(originArr);
console.log(originArr === wrapperArr); // true
构造器标识 constructor
每个由 Object 创建的对象实例,都有一个 constructor 属性,指向它的构造函数。
console.log(Object.prototype.constructor === Object); // true
function Person(name) {
this.name = name;
}
const p = new Person('Tom');
console.log(p.constructor === Person); // true
有个有趣的现象是,看起来 constructor 属性是可以被修改的。
let str = 'abc';
str.constructor = Number;
console.log(str.constructor); // ƒ String() { [native code] }
实际上,这里并没有真正修改 str 的 constructor。因为 str 是一个原始值,本身是没有属性的。 当我们尝试给 str 设置 constructor 属性时
- 引擎会先将 str 包装成一个临时的 String 对象
- 在这个临时对象上设置 constructor 属性为 Number
- 随后这个临时对象就被销毁,str 变量仍然指向原始的字符串值
- 所以之后再访问 str.constructor 时,引擎又会重新创建一个 String 对象,因此得到的还是最初的 String 构造函数
new 关键字
使用 new 关键字调用构造函数时,会经历以下步骤
- 执行构造函数中的代码
- 创建一个空对象,作为 this 对象
- 将 this 对象的 proto 指向构造函数的 prototype 属性
- 返回 this 对象(如果构造函数没有返回其他对象)
所以,以下两种写法是完全等价的
function Test() {
// ...
}
const test1 = new Test();
const test2 = new Test();
原型链与继承
在 JavaScript 中,继承是通过原型链实现的。每个对象都有一个内部属性 [[Prototype]],指向它的原型对象。 当访问一个对象的属性时,如果该对象本身没有这个属性,就会沿着原型链一直向上查找,直到找到这个属性或者到达原型链的顶端。
function Animal(name) {
this.name = name;
}
Animal.prototype.sayHi = function () {
console.log(`Hello, I'm ${this.name}`);
};
function Dog(name, breed) {
Animal.call(this, name);
this.breed = breed;
}
Dog.prototype = Object.create(Animal.prototype);
Dog.prototype.constructor = Dog;
Dog.prototype.bark = function () {
console.log('Woof!');
};
const dog = new Dog('Buddy', 'Golden Retriever');
dog.sayHi(); // Hello, I'm Buddy
dog.bark(); // Woof!
在上面的例子中,Dog 通过将它的 prototype 指向一个 Animal 的实例,继承了 Animal 的属性和方法。 同时 Dog 还可以定义自己特有的属性和方法,实现了在继承的基础上进行扩展。
关于 proto 和 [[Prototype]] 的区别
- proto 是早期浏览器支持的一个属性,允许我们直接读写[[Prototype]]。但它从未成为正式标准,现在已被废弃。
- [[Prototype]] 是 JavaScript 规范中定义的一个内部属性,代表对象的原型。外部代码无法直接访问这个属性。
- 现在推荐使用 Object.getPrototypeOf() 和 Object.setPrototypeOf() 来操作对象的原型,而不是使用 proto。
原型链的顶端是 Object.prototype。它的 [[Prototype]] 指向 null。
Object.prototype.__proto__ = null; // 不会报错
Object.prototype.__proto__ = {};
// Uncaught TypeError: Cannot set prototype of #<Object> to an object
将 Object.prototype.proto 设置为 null 不会报错,因为这不会真正改变原型链。 而将其设置为一个对象会报错,因为这会破坏原型链结构,JavaScript 为了避免这种情况而抛出了异常。
属性描述符
通过 Object.defineProperty() 可以精确地定义或修改对象的属性。
const obj = {};
Object.defineProperty(obj, 'a', {
value: 1,
writable: false,
enumerable: false,
configurable: false,
});
console.log(obj.a); // 1
obj.a = 2;
console.log(obj.a); // 1
这里定义了一个属性 a,它的值为 1,且不可写、不可枚举、不可配置。 所以后面尝试修改 a 的值并不会生效。
Object.defineProperty() 还可以定义 getter 和 setter 函数,实现属性的读取和设置拦截。
let val = 1;
const obj = {};
Object.defineProperty(obj, 'a', {
get() {
console.log('Getting value:', val);
return val;
},
set(newVal) {
console.log('Setting value:', newVal);
val = newVal;
},
});
obj.a; // Getting value: 1
obj.a = 2; // Setting value: 2
obj.a; // Getting value: 2
我们可以使用以下方法来获取属性的描述符
- Object.getOwnPropertyDescriptor(obj, 'prop') 获取某个属性的描述符
- Object.getOwnPropertyDescriptors(obj) 获取所有属性的描述符
- Object.getOwnPropertyNames(obj) 获取所有自有属性的键名(包括不可枚举的)
- Object.keys(obj) 获取所有可枚举的自有属性的键名
- Object.getOwnPropertySymbols(obj) 获取所有 Symbol 类型的属性键名
- obj.propertyIsEnumerable('prop') 判断某个属性是否可枚举
对象的不变性
JavaScript 提供了几种方法来限制对象的可变性。
- Object.preventExtensions(obj) 禁止向对象添加新属性
- Object.seal(obj) 禁止添加/删除属性,已有属性标记为不可配置
- Object.freeze(obj) 禁止添加/删除/修改属性,已有属性标记为不可配置,writable 设为 false
与之对应,有如下方法检查对象的状态
- Object.isExtensible(obj)
- Object.isSealed(obj)
- Object.isFrozen(obj)
需要注意,这些方法只能影响对象本身的属性,不会影响到原型链上的属性。 而且,它们也只能阻止属性的添加/删除/修改,并不能防止属性值本身被修改。
const obj = { a: { b: 1 } };
Object.freeze(obj);
obj.a = { c: 2 }; // 不生效
obj.a.b = 2; // 生效,因为 a 属性指向的对象没有被冻结
对象的合并
Object.assign() 方法可以将一个或多个源对象的所有可枚举属性复制到目标对象,并返回修改后的目标对象。
const target = { a: 1 };
const source1 = { b: 2 };
const source2 = { c: 3 };
const result = Object.assign(target, source1, source2);
console.log(result === target); // true
console.log(target); // { a: 1, b: 2, c: 3 }
Object.assign() 的几个特点
- 如果目标对象与源对象有同名属性,后面的源对象会覆盖前面的
- 如果源对象是基本类型,会先转换成对象再合并
- 对于 undefined 和 null 会被忽略,不会报错
- 只复制源对象的自有可枚举属性,不复制继承属性和不可枚举属性
- String 和 Symbol 类型的属性都会被复制
const v1 = 'abc';
const v2 = true;
const v3 = 123;
const v4 = Symbol('foo');
const obj = Object.assign({}, v1, v2, v3, v4);
console.log(obj);
// { '0': 'a', '1': 'b', '2': 'c', [Symbol(foo)]: true }
Object.assign() 执行的是浅拷贝,如果源对象某个属性的值是对象,那么目标对象拷贝得到的是这个对象的引用。
const source = { a: { b: 1 } };
const target = {};
Object.assign(target, source);
source.a.b = 2;
console.log(target.a.b); // 2
所以在实际项目中使用 Object.assign() 进行对象合并时,一定要注意是否有嵌套的对象存在,以免出现意料之外的修改。