聊聊 new 操作符以及和 Object.create 的区别

1,803 阅读5分钟

JS 中经常使用 new 操作符调用构造函数来返回一个实例对象,这个实例对象能够拥有构造函数上的属性和共享构造函数原型链上的属性。当构造函数当作普通函数调用时(不使用 new 调用时),函数内部的 this 指向全局的 window。使用 new 和不使用 new 差别这么大,那 new 操作符到底干了什么呢?

new 的特性

我们可以先通过几个例子来了解一下 new 的作用。

  • 构造函数没有返回值

    function Foo() {
      this.age = 11
    }
    Foo.prototype.sayAge = function() {
      console.log(this.age)
    }
    
    const o = new Foo()
    o instanceof Foo // true,看过 instanceof 的文章就可以知道其实是 o.__proto__ === Foo.prototype
    console.log(o) // { age: 11 }
    o.sayAge() // 11
    

    通过上面的例子,可以看出来,通过 new 生成的实例对象可以访问构造函数中的属性并且还可以访问到构造函数原型链上的属性。Instanceof 相关文章

  • 构造函数返回基本类型值

    function Foo() {
      this.age = 11
      return 1
    }
    
    const o = new Foo()
    console.log(o) // { age: 11 }
    

    虽然构造函数有返回值,但是这个返回结果并没有任何用处,根本没有影响到后面的执行结果。

  • 构造函数返回对象类型值

    function Foo() {
      this.age = 11
      return {
        name: 'keer'
      }
    }
    
    const o = new Foo()
    console.log(o) // { name: 'keer' }
    

    上面的例子可以看到,o 的值为函数 Foo 的返回值,因此也不能再访问 Foo 的属性和它原型链上的属性了。所以构造函数返回对象时会导致 new 操作符失去作用。

所以可以总结出来 new 的特性如下:

  • new 创建出来的实例可以访问到构造函数中的属性
  • new 创建出来的实例可以访问到构造函数原型链中的属性,也就是说通过 new 操作符,实例与构造函数通过原型链连接了起来
  • 构造函数如果返回原始值,这个返回值没有意义
  • 构造函数如果返回对象,创建的实例为返回值的结果

模拟实现 new 操作符

要模拟 new 操作符,我们看一下规范上对 new 操作符的定义。

11.2.2 The new Operator

The production NewExpression : new NewExpression is evaluated as follows:

  1. Let ref be the result of evaluating NewExpression.
  2. Let constructor be GetValue(ref).
  3. If Type(constructor) is not Object, throw a TypeError exception.
  4. If constructor does not implement the [[Construct]] internal method, throw a TypeError exception. // 如果构造函数未实现 [[Construct]] 内部方法,则引发TypeError异常。比如 new Symbol() 或者 new 箭头函数就会报错。
  5. Return the result of calling the [[Construct]] internal method on constructor, providing no arguments (that is, an empty list of arguments). // 返回在构造函数上调用 [[Construct]] 内部方法的结果,不提供任何参数(即,空的参数列表)。(当然也可以传递参数)

可以看到返回的是调用构造函数内部 [[Construct]] 方法的结果,那我们来看一下 [[Construct]] 这个内部方法都执行了哪些步骤:

13.2.2 [[Construct]]

When the [[Construct]] internal method for a Function object F is called with a possibly empty list of arguments, the following steps are taken:

  1. Let obj be a newly created native ECMAScript object. // 创建 ECMAScript 原生对象 obj
  2. Set all the internal methods of obj as specified in 8.12. // 给 obj 设置原生对象的内部属性;
  3. Set the [[Class]] internal property of obj to "Object". // 设置 obj 的内部属性 [[Class]]Object
  4. Set the [[Extensible]] internal property of obj to true. // 设置 obj 的内部属性 [[Extensible]]true
  5. Let proto be the value of calling the [[Get]] internal property of F with argument "prototype". // 将 proto 的值设置为 Fprototype 属性值
  6. If Type(proto) is Object, set the [[Prototype]] internal property of obj to proto. // 如果 proto 是对象类型,则设置 obj 的内部属性 [[Prototype]] 值为 proto
  7. If Type(proto) is not Object, set the [[Prototype]] internal property of obj to the standard built-in Object prototype object as described in 15.2.4. // 如果 proto 不是对象类型,则设置 obj 的内部属性 [[Prototype]] 值为内建构造函数 Objectprototype
  8. Let result be the result of calling the [[Call]] internal property of F, providing obj as the this value and providing the argument list passed into [[Construct]] as args. // 调用函数 F,将其返回值赋给 result;其中,F 执行时的实参为传递给 [[Construct]](即 F 本身) 的参数,F 内部 this 指向 obj
  9. If Type(result) is Object then return result. // 如果 resultObject 类型,返回 result
  10. Return obj. // 如果 F 返回的不是对象类型(第 9 步不成立),则返回创建的对象 obj

用比较简洁的方式描述一下就是

  1. 创建一个新对象 obj
  2. 给新对象的内部属性赋值,关键是给[[Prototype]]属性赋值。如果构造函数的 prototype 属性是 Object 类型,则 proto 指向构造函数的 prototype,否则指向 Object 对象的prototype
  3. 执行构造函数,并将 this 指向 obj
  4. 构造函数如果有返回结果,如果结果是对象类型,直接返回该对象,否则返回上面创建的 obj

所以根据上面的描述,我们可以简单的实现代码如下:

function create(F, ...args) {
  const obj = {}
  Object.setPrototypeOf(obj, F.prototype)
  const result = F.apply(obj, args)
  return result instanceof Object ? result : obj
}

// 测试下
const a = create(Foo)
a.sayAge() // 12
a instanceof Foo // true

Object.create 和 new 有什么区别

我们先来了解一下 Object.create 的作用:

const dog = {
  eat: function() {
    console.log(this.eatFood)
  }
}

const maddie = Object.create(dog)
console.log(dog.isPrototypeOf(maddie)) //true
maddie.eatFood = 'NomNomNom'
maddie.eat() //NomNomNom

Object.create 创建了一个全新的对象 maddie,这个对象的原型为dog,并且可以访问 dog 中的 eat 方法。

简单模拟下 Object.create 如下:

function create(obj) {
  let F = function () {}
  F.prototype = obj
  return new F()
}

你可能会想,Object.create 和 new 到底有什么区别呢,它们看起来好像都在做同一个事,它们都创建了一个新兑现并且继承了一个原型。

下面可以看一下例子:

function Dog(){
  this.pupper = 'Pupper'
};
Dog.prototype.pupperino = 'Pups.'

const maddie = new Dog()
const buddy = Object.create(Dog.prototype)

// Using Object.create()
console.log(buddy.pupper) // Output is undefined
console.log(buddy.pupperino) // Output is Pups.

//Using New Keyword
console.log(maddie.pupper); //Output is Pupper
console.log(maddie.pupperino); //Output is Pups.

从上面的例子可以看到, buddy.pupper 的值为 undefined,即使 Object.create 将其原型设置为 Dog,buddy 也无法在构造函数中访问 this.pupper。这是由于 new Dog 实际上运行了构造函数的代码,而 Object.create 不执行构造函数。