在MDN中,new运算符的定义如下:

TIP

new 运算符创建一个用户定义的对象类型的实例或具有构造函数的内置对象的实例。

从上面的定义中,我们很难看出一个函数在使用new运算符调用时究竟发生了什么。我们还是来看代码:

function Person(name, age) {
  this.name = name;
  this.age = age;
}
Person.prototype.sex = "man"
var zhangsan = new Person("zhangsan", 18);
console.log(zhangsan.name);   //zhangsan 
console.log(zhangsan.age);    //18
console.log(zhangsan.sex);    //man

了解过javascript继承的人都知道,在使用new调用的函数被称为构造函数,调用之后会产生一个新的对象,被成为实例对象。实例对象可以访问构造函数中的属性和原型对象中的属性。这里我们可以总结一下,使用new运算符调用函数的时候,会创建一个对象,然后把构造函数中的this指向这个新对象,把新对象和构造函数的prototype进行绑定,经过一系列处理之后,返回这个新对象。这是我们暂时的理解,再来看一段代码:

function Person(name, age) {
  this.name = name;
  this.age = age;
  return {
    a: 1,
    b: 2
  }
}
Person.prototype.sex = "man"
var zhangsan = new Person("zhangsan", 18);
console.log(zhangsan.name);   //undefined
console.log(zhangsan.age);    //undefined
console.log(zhangsan.sex);    //undefined

从上面的代码我们可以看出,当构造函数有指定返回值的时候,这个新创建的对象好像“失效”了,返回的是这个指定的返回值。这时候“实例对象”不能再访问构造函数和原型对象中的属性了。
还没完,再看一段代码:

function Person(name, age) {
  this.name = name;
  this.age = age;
  return “ab”
}
Person.prototype.sex = "man"
var zhangsan = new Person("zhangsan", 18);
console.log(zhangsan.name);   //zhangsan
console.log(zhangsan.age);    //18
console.log(zhangsan.sex);    //name

通过上面的代码我们发现,当构造函数的指定返回值不是对象的时候,这个新创建的对象好像又“复活”了,具有了最开始时的特性。

那么这里我们来总结一下,当函数通过new运算符调用时,内部到底发生了什么

  • 创建了一个新对象
  • 构造函数中的this指向了这个对象
  • 这个对象和构造函数的prototype进行了绑定
  • 如果构造函数指定了对象类型的返回值,就不返回这个新创建的对象,返回指定的返回值
  • 如果构造函数没有指定返回值或者指定了非对象类型的返回值,就返回这个新创建的对象

下面我们根据上面的总结,一步一步来模拟实现new

# 第一版

我们无法模拟运算符,这里我们通过工厂函数的方式来模拟new。传入构造函数作为参数,返回实例对象。来看代码:

function myNew() {
  //传入的参数有多个,第一个参数是构造函数,后面的参数是传入构造函数的参数
  //这里使用shift来删除并获取arguments中的第一个值,也就是构造函数
  var constructor = Array.prototype.shift.call(arguments);
  //创建新的对象
  var newObj = {};
  //将构造函数中的this指定为newObj,使用apply模拟
  //这里注意,直接传入arguments,因为在上面已经使用shift删除了第一个参数
  constructor.apply(newObj, arguments);
  //返回创建的对象
  return newObj
}

function Person(name, age) {
  this.name = name;
  this.age = age;
}
var zhangsan = myNew(Person, "zhangsan", 18);
console.log(zhangsan.name);  //zhangsan
console.log(zhangsan.age);   //18

上面的代码有详细的注释,大家可以仔细查看,帮助理解。最终打印的值也是我们期望的结果。这里我们已经实现了部分功能,创建了新对象,将构造函数中的this指定为这个新对象,返回新对象。下面我们实现剩下的部分,绑定原型对象,给构造函数指定不同类型的返回值。

# 第二版

将实例对象绑定原型对象我们可以使用 _ proto _ , 判断类型则使用 typeof.

function myNew() {
  var constructor = Array.prototype.shift.call(arguments);
  var newObj = {};
  //绑定原型对象
  newObj.__proto__ = constructor.prototype;
  //接收返回值
  var result = constructor.apply(newObj, arguments);
  //根据返回值类型判断是返回newObj还是result
  return typeof result === 'object' ? result : newObj
}

大家可以对照注释理解代码,如果有问题可以留言讨论。下面来测试一下

function Person(name, age) {
  this.name = name;
  this.age = age;
}
Person.prototype.sex = "man"
var zhangsan = myNew(Person, "zhangsan", 18);
console.log(zhangsan.name);      //zhangsan
console.log(zhangsan.age);       //18
console.log(zhangsan.sex);       //man

没有返回值的情况,通过验证!

function Person(name, age) {
  this.name = name;
  this.age = age;
  return "ab"
}
Person.prototype.sex = "man"
var zhangsan = myNew(Person, "zhangsan", 18);
console.log(zhangsan.name);    //zhangsan
console.log(zhangsan.age);	   //18
console.log(zhangsan.sex);     //man

有返回值,但是返回值为非对象类型的情况,通过验证!

function Person(name, age) {
  this.name = name;
  this.age = age;
  return {
    a: 1,
    b: 2
  }
}
Person.prototype.sex = "man"
var zhangsan = myNew(Person, "zhangsan", 18);
console.log(zhangsan.name);       //undefined
console.log(zhangsan.age);        //undefined
console.log(zhangsan.sex);        //undefined
console.log(zhangsan.a);          //1
console.log(zhangsan.b);          //2

有返回值,且返回值为对象类型的情况,通过验证!