深入解析Object.create的应用与实践

450 阅读3分钟

语法

Object.create() 静态方法以一个现有对象作为原型,创建一个新对象

Object.create(proto,[propertiesObject])

  • proto:新创建对象的原型对象
  • propertiesObject是可选的,如果该参数被指定且不为 undefined,则该传入对象可枚举的自有属性将为新创建的对象添加具有对应属性名称的属性描述符。

使用 Object.create(),我们可以创建一个原型为 null 的对象。在字面量初始化对象语法中,相当于使用 __proto__ 键,即:

o = Object.create(null);
// 等价于:
o = { __proto__: null };

{} 🆚 Object.create(null)

你知道 {} 和 Object.create(null) 的区别是什么吗?

我们在控制台打印出来看看:

image.png

从结果可以看到,通过字面量 {} 创建的对象,会继承Object原型链上的方法,如hasOwnProperty、isPrototypeOf、toString、valueof等;而通过Object.create(null)创建的新对象不会继承Object中的任何属性

那使用Object.create方法如何创建一个与 {} 等价的新对象呢?Object.create(Object.prototype)

image.png

Object.create({}) 🆚 Object.create(Object.prototype)

是否想过 Object.create({}) 与 Object.create(Object.prototype) 创建的新对象之间的区别是什么?

image.png

可以看到,多一层[[Prototype]],当我们在调用 Object.create({}) 时,创建了一个新的空对象,其原型指向这个空对象{},而这个空对象的原型指向Object.prototype

Object.create({})
// 对应的原型链:newObj -> {} -> Object.prototype -> null

Object.create(Object.prototype)
// 对应的原型链:newObj -> Object.prototype -> null

模仿 new 运算符

使用 Object.create() 来模仿 new 运算符的行为

function Constructor() {}
o = new Constructor();

// 在 Constructor 函数没有实际的初始化代码的情况下,等价于:
o = Object.create(Constructor.prototype);

特别注意 在Constructor函数没有实际的初始化代码的情况下, 那为什么有初始化代码就不等价?

结合上一篇图解指南:继承中的Prototype、__proto__与Constructor之间的关系文章中提到了使用Object.create类式继承与使用class extends继承存在的差异,举个例子:

function Parent() {
  this.name = 'parent';
}
Parent.staticValue = 'static value';
Parent.prototype.getName = function () {
  return `getName ${this.name}`;
};

function Children() {
  this.age = 2;
}

// 使 Children 的实例能够访问 Parent 原型上的属性和方法
Children.prototype = Object.create(Parent.prototype);

const child = new Children();

console.log(child.name); // undefined
console.log(child.getName()); // getName undefined
console.log(Children.staticValue); // undefined
console.log(Children.constructor === Children); // false
console.log(Children.__proto__ === Parent); // false
console.log(Children.prototype.__proto__ === Parent.prototype); // true

结合以上例子表现出的差异,我们该如何弥补?

  1. 构造函数调用
function Children() {
  Parent.call(this); //实例继承:确保子类实例能继承父类构造函数内定义的属性和方法
}
  1. 静态属性和方法继承
Object.assign(Children, Parent);
  1. 修复构造器指向
Children.prototype.constructor = Children;
  1. 在构造函数之间建立类似的“继承”关系
Object.setPrototypeOf(Children, Parent); // 使 Children.__proto__ === Parent

静态属性与实例属性

静态属性和实例属性在定义和访问方式上有本质的区别

静态属性属于构造函数本身,只能通过构造函数来访问;实例属性属于构造函数创建的实例,只能通过实例来访问。

  1. 静态属性是直接添加在构造函数上的属性。它们不属于构造函数创建的任何实例,而是属于构造函数本身。
function A() {} 
A.staticProperty = 'static value'; 
console.log(A.staticProperty); // static value

const instance = new A(); 
console.log(instance.staticProperty); // undefined
  1. 实例属性是在构造函数内部使用 this 关键字添加的属性。这些属性在构造函数通过 new 关键字创建新实例时,会被添加到每个实例上。
function A() { 
  this.staticProperty = 'static value';
}

const instance = new A(); 
console.log(instance.staticProperty); // static value
console.log(A.staticProperty); // undefined

参考文章

MDN-Object.create()