深入剖析Object.create(),为与之相关的理解形成闭环

3,471 阅读6分钟

今天是国庆节的第8天,尽管假期结束,但也不打破自己的学习计划,今天的主题是:总结Object.create()

总结此文的原因是:碰到继承的时候经常用到它,引发我去深入了解其用法。

此文能收货:

  • new Object() 和 Object.create()的区别
  • 操作原型对象(prototype)的方法
    • Object.create()
    • Object.setPrototypeOf
    • Object.getPrototypeOf()
  • 不同属性对应不同的继承方法
    • 如果只是拷贝自身可枚举属性
    • 如果要拷贝原型上的属性
    • 如果要拷贝get /set 属性

需要了解详情,请看下文。

一、Object.create()

  • 描述:该方法创建一个新对象,使用现有的对象来提供新创建的对象的__proto__

  • 语法:Object.create(proto, [propertiesObject])

    • proto:必须。表示新建对象的原型对象,即该参数会被赋值到目标对象(即新对象,或说是最后返回的对象)的原型上。该参数可以是null、对象、函数的prototype属性 (注:创建空的对象时需传null , 否则会抛出TypeError异常)。
    • propertiesObjec:可选。 添加到新创建对象的可枚举属性(即其自身的属性,而不是原型链上的枚举属性)对象的属性描述符以及相应的属性名称。这些属性对应Object.defineProperties()的第二个参数
  • 返回值:在指定原型对象上添加新属性后的对象

二、new Object()

var objB = new Object();
// var objB = Object();
objB.name = 'b';
objB.sayName = function() {
    console.log(`My name is ${this.name} !`);
}
objB.sayName();
console.log(objB.__proto__ === Object.prototype); // true
console.log(objB instanceof Object); // true

new操作符其实做了以下四步:


function F() {};
var func = new F();

var obj = new Object(); // 创建一个空对象
obj.__proto__ = F.prototype; // obj的__proto__指向构造函数的prototype
var result = F.call(obj); // 把构造函数的this指向obj,并执行构造函数把结果赋值给result
if (typeof(result) === 'object') {
    func = result; // 构造函数F的执行结果是引用类型,就把这个引用类型的对象返回给objB
} else {
    func = obj; // 构造函数F的执行结果是值类型,就返回obj这个空对象给objB
}

三、new Object() 和 Object.create()的区别

1. 创建对象的方式不同

new Object() 方式:通过构造函数来创建对象, 添加的属性是在自身实例下

let a = { fruit : 'apple' }
let b = new Object(a) 
console.log(b) // {fruit: "apple"}
console.log(b.__proto__) // {}
console.log(b.fruit) // apple

Object.create() 方式:继承一个对象, 添加的属性是在原型下

let a = { fruit: 'apple' }
let b = Object.create(a)
console.log(b)  // {}
console.log(b.__proto__) // {fruit: "apple"}
console.log(b.fruit) // apple

2. 创建对象属性描述符不同

Object.create用第二个参数创建非空对象的属性描述符默认是为false的,不可写,不可枚举,不可配置

let obj = Object.create({}, { age: { value: 18 } })
console.log(Object.getOwnPropertyDescriptors(obj))

>
{
  age: {
    value: 18,
    writable: false,
    enumerable: false,
    configurable: false
  }
}
obj.age = 24
obj.age
> 18

obj.q = 12
for (var prop in obj) {
   console.log(prop)
}
> "q"

delete obj.age
> false

字面量方法创建的对象属性的描述符默认为true

let obj = Object.create(null)
obj.age = { value: 18 }
console.log(Object.getOwnPropertyDescriptors(obj))


{
  age: {
    value: { value: 18 },
    writable: true,
    enumerable: true,
    configurable: true
  }
}

构造函数创建的对象属性的描述符默认为true

let age = { value: 18 }
console.log(Object.getOwnPropertyDescriptors(age))
{
  value: { 
  	value: 18,
    writable: true, 
    enumerable: true, 
    configurable: true 
  }
}

3. 创建空对象时,是否有原型属性不同

当用构造函数或对象字面量方法创建空对象时,对象时有原型属性的,即有_proto_

console.dir(new Object()) //{}
>
Object
__proto__:

当用Object.create()方法创建空对象时,对象是没有原型属性的

console.dir(Object.create(null)) 
> 
Object
No properties

四、操作原型对象(prototype)的方法

Object.create()

描述:该方法创建一个新对象,使用现有的对象来提供新创建的对象的__proto__

格式:Object.create(proto, [propertiesObject])

用法:如果用传统的方法要给一个对象的原型上添加属性和方法,是通过prototype 实现的

let proto = {
    age: 18,
    name: 'hannie',
    show(){}
};
let obj = Object.create(proto);
console.dir(obj)
> 
Object
  __proto__: 
    age: 18
    name: "hannie"
    show: ƒ show()
    __proto__: Object
var a = Object.create(null);
console.dir(a); // {}
console.log(a.__proto__); // undefined
console.log(a.__proto__ === Object.prototype); // false
console.log(a instanceof Object); // false 没有继承`Object.prototype`上的任何属性和方法,所以原型链上不会出现Object

如果是不用Object.create()方法,我们是通过构造函数或者类给对象原型添加属性和方法的?

let Person = function(){}
Person.prototype.age = 18
Person.prototype.name = "hannie"
Person.prototype.show = function() {}
//通过构造函数创建实例
var p = new Person();
console.log(p.__proto__ === Person.prototype) // true
console.dir(p)
>
Person
  __proto__: 
    age: 18
    name: "hannie"
    show: ƒ show()
    __proto__: Object

console.dir(p.__proto__)
>
Object
  age: 18
  name: "hannie"
  show: ƒ show()
  __proto__: Object

Object.setPrototypeOf

描述:该方法的作用与 __proto__ 相同,用来设置一个对象的 prototype 对象,返回参数对象本身。 ES6 正式推荐的设置原型对象的方法。

格式:Object.setPrototypeOf(object, prototype)

let person = {
  age: 18,
  name: "hannie"
};
let o = { sex: '女' };
Object.setPrototypeOf(o, person);
console.dir(o)
>
Object
  sex: "女"
  __proto__: 
    age: 18
    name: "hannie"
    __proto__: Object

输出结果中看出,添加的方法是在原型上的。就类似于

obj.__proto__ = proto;

Object.getPrototypeOf()

描述:用于读取一个对象的原型对象;

格式:Object.getPrototypeOf(obj);

let person = {
  age: 18,
  name: "hannie"
};
let o = { sex: '女' };
Object.setPrototypeOf(o, person);
console.log(Object.getPrototypeOf(o))
>
{
  age: 18, 
  name: "hannie",
  __proto__: Object  //其实这个除了Object.create()方法创建的空对象,其他每个对象都会有
}


Object.getPrototypeOf('foo') === String.prototype 
>true
Object.getPrototypeOf(true) === Boolean.prototype 
>true

以上方法引出原型属性的继承

五、原型属性的拷贝(继承)

在原型上定义方法

function Person(age){
  this.age = age
}
Person.prototype.name = 'hannie'
Person.prototype.call = function(phone){
  console.log('The phone is:',phone)
}
let like = {a: 1, b: 2, c: 3};

Object.assign(Person.prototype,like)
let person = new Person(18)
console.log(person);

> 
Person 
  age: 18
  __proto__:
    a: 1
    b: 2
    c: 3
    call: ƒ (phone)
    name: "hannie"
    constructor: ƒ Person(age)
    __proto__: Object

拷贝实例上的方法:Object.assign

let person1 = Object.assign({},person)
console.log(person1.age); // 能拷贝到实例上的方法 
>18
console.log(person1.a); // 不能拷贝到原型上的方法
>undefined

怎么才能拷贝原型上的方法呢?

方法一:Object.getPrototypeOf + Object.create + Object.assign

let originProto = Object.getPrototypeOf(person)
let originProto2 = Object.create(originProto);

let person1 = Object.assign(originProto2, person)

console.log(person1.age); 
>18
console.log(person1.a);  //可以拷贝原型上的方法
>1

方法二(推荐):Object.getPrototypeOf + Object.getOwnPropertyDescriptors + Object.create

Object.create()的参数理解为:第一个参数是放在新对象的原型上的,第二个参数是放在新对象的实例上的。

推荐原因:因为Object.assign() 方法不能正确拷贝 get ,set 属性

let originProto = Object.getPrototypeOf(person)
let descriptor = Object.getOwnPropertyDescriptors(person)
let person1 = Object.create(originProto, descriptor);
console.log(person1.age); 
>18
console.log(person1.a);  //可以拷贝原型上的方法
>1

下面对比两种方法,哪方法种能正确拷贝 get ,set 属性

Object.defineProperty(person,'ageGet', {
  enumerable: true, // 设为可枚举,不然 Object.assign 方法会过滤该属性
  get(){
      return "Could get: " + this.age
  }
});
let originProto = Object.getPrototypeOf(person)
let descriptor = Object.getOwnPropertyDescriptors(person)
let person1 = Object.create(originProto, descriptor);
console.log(person1); //18
> 
Person 
  age: 18
  ageGet: "Could it return 18"
  get ageGet: ƒ get()
  __proto__: Object
Object.defineProperty(person,'ageGet', {
  enumerable: true, // 设为可枚举,不然 Object.assign 方法会过滤该属性
  get(){
      return "Could get: " + this.age
  }
});
let originProto = Object.getPrototypeOf(person)
let originProto2 = Object.create(originProto);
let person1 = Object.assign(originProto2, person)
> 
Person
  age: 18
  ageGet: "Could get: 18"
  __proto__: Object
  
结果中没有get ageGet: ƒ get()

得出结论:

方法一不能正确拷贝 get ,set 属性;
方法二能正确拷贝 get ,set 属性

虽然说实际开发上很少要去修改 get 描述符,但是多知道一种方法,遇到情况时就知道怎么去解决了。

再实例化新对象,发现没ageGet,证明get也不是原型上的方法

let person2 = new Person(18)
console.log(person2);
> 
Person
  age: 18
  __proto__: Object

综合以上,原型属性的继承可以有以下几种方法:

let originProto = Object.getPrototypeOf(person)

//方法1
const obj = Object.create(originProto);
obj.other = 123;

//方法2
const obj = Object.assign(
  Object.create(originProto),
  {
    other: 123,
  }
);

/方法3
const obj = Object.create(originProto,Object.getOwnPropertyDescriptors({ other: 123 }));

总结

new Object() 和 Object.create()的区别:

  • 创建对象的方式不同
  • 创建对象属性描述符不同
  • 创建空对象时,是否有原型属性不同

操作原型对象(prototype)的方法:

  • Object.create()
  • Object.setPrototypeOf
  • Object.getPrototypeOf()

属性的拷贝(继承):

  • 如果只是拷贝自身可枚举属性,就可以只用 Object.assign 方法;
  • 如果要拷贝原型上的属性,使用 Object.assign + Object.create + Object.getPrototypeOf 或者 Object.getPrototypeOf + Object.getOwnPropertyDescriptors + Object.create
  • 如果要拷贝get /set 属性,使用Object.getPrototypeOf + Object.getOwnPropertyDescriptors + Object.create