跳到主要内容

Object 对象

Object 的作用

在 JavaScript 中,Object 有两个主要作用

  1. 作为键值对的容器,用于存储和管理数据
  2. 作为一个构造函数,用于创建对象实例

对象包装类型

在 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 属性时

  1. 引擎会先将 str 包装成一个临时的 String 对象
  2. 在这个临时对象上设置 constructor 属性为 Number
  3. 随后这个临时对象就被销毁,str 变量仍然指向原始的字符串值
  4. 所以之后再访问 str.constructor 时,引擎又会重新创建一个 String 对象,因此得到的还是最初的 String 构造函数

new 关键字

使用 new 关键字调用构造函数时,会经历以下步骤

  1. 执行构造函数中的代码
  2. 创建一个空对象,作为 this 对象
  3. 将 this 对象的 proto 指向构造函数的 prototype 属性
  4. 返回 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() 进行对象合并时,一定要注意是否有嵌套的对象存在,以免出现意料之外的修改。