评论

JavaScript 的 new 操作符到底干了什么

前几天无意间发现了刚开始学JavaScript时在知乎写的一些回答,有一个就是讲new操作符到底干了什么


叔叔,你能给我 new 一个对象吗?


前几天无意间发现了刚开始学JavaScript时在知乎写的一些回答,有一个就是讲new操作符到底干了什么。从现在的视角看我当时的回答虽然是正确的,但是在对原理的剖析和细节的理解上还相去甚远。所以借此机会,就想重新梳理一下这一年多来对new操作符理解的变化与成长。

模拟new操作符


第一次去了解new操作符,是在看《JS高级程序设计(第三版)》这本书时,P145是这样写到的。


要创建Person的新实例,必须使用new操作符。以这种方式调用构造函数实际上会经历以下4个步骤:


  1. 创建一个新对象;

  1. 将构造函数的作用域赋给新对象(因此this就指向了这个新对象);

  1. 执行构造函数中的代码(为这个新对象添加属性);

  1. 返回新对象;


尽管书中是这样描述,但是并没有给出实践的代码,所以我就按照这个步骤自己去实践模拟一个new操作符。代码如下:


var mockNew = function (constructor) {  var o = {} // 创建一个新对象
  constructor.apply(o, Array.prototype.slice.call(arguments, 1)) // 赋作用域 执行代码
  return o // 返回新对象}


然后我们用这个模拟new操作符的函数来创造一个对象试试。


var Person = function (name, age) {  this.name = name
  this.age = age}Person.prototype.sayName = function () {  console.log(this.name)
}var person1 = mockNew(Person, 'MeloGuo', 22)console.log(person1.name) // 'MeloGuo'console.log(person1.age) // 22person1.sayName() // Uncaught TypeError: person1.sayName is not a function


看起来我的模拟函数虽然能访问实例中的属性,但是却不能访问sayName方法,而且当我使用instanceof操作符检测时却得到了这样的结果。


console.log(person1 instanceof Person) // falseconsole.log(person1 instanceof Object) // true

改进的模拟函数


可见person1并不是Person的实例。当时的我还不知道问题出在哪里,直到学习到原型链时我才直到mockNew函数缺少了一个步骤,即绑定构造函数原型。所以person1实例是无法访问到Person原型中的sayName方法,同时instanceof操作符的结果也为false。因为instanceof操作符是用来检测一个对象在其原型链中是否存在一个构造函数的prototype属性的,而person1的原型链中并不存在Person.prototype,所以返回值为false。因此,我们改造mockNew函数如下:


var mockNew = function (constructor) {  var o = {}  o.__proto__ = constructor.prototype // 绑定构造函数原型,但是生产代码中千万别用.__proto__
  constructor.apply(o, Array.prototype.slice.call(arguments, 1))  return o}


这时我们再创建实例,然后使用instanceof操作符检测一下,同时调用下sayName方法。


var person1 = mockNew(Person, 'MeloGuo', 22)console.log(person1 instanceof Person) // trueconsole.log(person1 instanceof Object) // trueperson1.sayName() // 'MeloGuo'


这时看似mockNew函数已经完全模拟了new操作符了,但是当我们尝试下面这种情况时,又出现了问题。


function Person (name) {  this.name = name
  return { age: 22 }
}var person1 = new Person('MeloGuo')var person2 = mockNew(Person, 'MeloGuo')console.log(person1) // {age: 22}console.log(person2) // Person {name: "MeloGuo"}console.log(person1 instanceof Person) // falseconsole.log(person2 instanceof Person) // true


什么!!!用new操作符调用的Person构造函数并没有按照预期返回带有name属性并且在Person.prototype上的对象,而是返回了我们手动return的带有age属性的对象,但是我们的mockNew函数是正常返回了。所以我们的mockNew函数中肯定又丢失了一些细节,为了弄清楚,只好硬着头皮去读ECMA-262规范了。看到规范中的steps后才恍然大悟了new操作符的整个执行流程。


完善模拟函数


简单来说我们在返回对象前缺失了判断返回值类型的步骤。


  • 如果构造函数的返回值是值类型,那么就丢弃掉,依然返回构造函数创建的实例。

  • 如果构造函数的返回值是引用类型,那么就返回这个引用类型,丢弃构造函数创建的实例。


注:如果没有显示return,那么相当于隐式返回了undefined,则丢弃它。


吸取了规范中的内容,并且加入ES6语法后,我们新的mockNew函数如下:


function mockNew (constructor, ...args) {  const isPrimitive = result => {    // 如果result为值类型则返回true
    // 如果result为引用类型则返回false    
  }  const o = Object.create(constructor.prototype)  const result = constructor.apply(o, args)  
  return isPrimitive(result) ? o : result}


这时的mockNew函数可以说是较好的模拟了new操作符的功能。

总结


其实new操作符就是一个语法糖。在传统的面向类的语言中,“构造函数”是类中的一些特殊方法,使用new初始化类时会调用类中的构造函数。而JavaScript中的new其实是用来告诉函数,我要以“构造”的方式调用你了,从而向mockNew函数中的流程一样,得到我们的实例。所以本质上来说JS是没有所谓构造函数的,有的应该是构造调用。这样的称呼能让我们更清楚认识JS中的new操作符。


本文作者 郭梓梁,首次发布于 MeloGuo Blog,转载请保留以上链接。


最后一次编辑于  05-16  (未经腾讯允许,不得转载)
点赞 6
收藏
评论

2 个评论

  • Luck
    Luck
    05-17

     好文

    05-17
    赞同 4
    回复
  • 惠嘉伟-Javey
    惠嘉伟-Javey
    07-28

    通透

    07-28
    赞同
    回复