手动实现new操作

使用new操作符可以实例化一个对象,那么,要模拟实现一个new,首先我们需要知道new在背后到底做了什么。

看下一个普通的类被实例化之后发生了什么变化。

function Person(name, age) {
  this.name = name;
  this.age = age;
  this.number = 123;
}
Person.prototype.getName = function () {
  return this.name;
};
Person.prototype.commonProperty = 'common property';

const person = new Person('leevare', 18);
console.log(person);

将上述这段代码放到浏览器中运行,可以看到输出的person实例中的agename等属性被赋予了具体传入的值,并且实例的[[Prototype]]上能找到类Person的原型方法。

直接实例化运行结果

分析这些现象,可以知道new使用我们传入的属性生成了一个新的实例对象,并且这个对象还能基于原型链找到类的原型方法和属性。所以,实现的基本思路找到了。

function myNew(...args) {
  const [Constructor, ...restArgs] = args;
  // 新创建一个对象,模拟每一个新创建的实例对象
  const obj = {};
  // 让obj的原型链上能找到Constructor的原型信息
  obj.__proto__ = Constructor.prototype;
  // 将传入的属性一一绑定到obj的实例上
  Constructor.call(obj, ...restArgs);
  return obj;
}
const person = myNew(Person, 'leevare', 18);
console.log(person);

受限于js,我们是不能够实现类似于new xxx()这种调用形式的,还是以函数调用的方式去实现。

那么,函数第一个参数为需要被实例化的类,剩余参数为类初始化时需要传递的参数。得益于js的arguments不定参数,我们可以传递不同类的不同参数(这里为了简单,我采用了es6的展开...运算符)。

第一步,需要创建一个新的对象,用于模拟每一个新创建的实例对象。然后再将这个新建对象的原型链指向正确,即设置其__proto__为类的prototype

之后,将传入的参数作为属性一一绑定为obj对象的实例属性,返回即可。

将上述代码拷贝到浏览器控制台执行一下看看吧,输出的结果和直接new是类似的。

初次实现结果

到这里,好像已经完全实现了new。先别着急,我们把Person类改写一下。

function Person(name, age) {
  this.name = name;
  this.age = age;
  this.number = 123;

  return {
    hello: 'world',
  };
}
Person.prototype.getName = function () {
  return this.name;
};
Person.prototype.commonProperty = 'common property';
const person = new Person('leevare', 18)
console.log(preson)

在浏览器控制台输出结果如下。

// 输出结果:
{ hello: 'world' }

返回的是我在构造函数中return的内容。这种情况涉及到一个概念。

如果构造函数返回非空对象,则返回该对象;否则,返回刚创建的新对象。
默认情况下,类构造函数会在执行之后返回 this 对象。构造函数返回的对象会被用作实例化的对象,如果没有什么引用新创建的 this 对象,那么这个对象会被销毁。不过,如果返回的不是 this 对象,而是其他对象,那么这个对象不会通过 instanceof 操作符检测出跟类有关联,因为这个对象的原型指针并没有被修改。

所以,还需要对上面new的实现进行一些改写,只需要判断构造函数中是否有显式返回的非空对象即可。

function Person(name, age) {
  this.name = name;
  this.age = age;
  this.number = 123;

  return {
    hello: 'word',
  };
}
Person.prototype.getName = function () {
  return this.name;
};
Person.prototype.commonProperty = 'common property';

function myNew(...args) {
  const [Constructor, ...restArgs] = args;
  // 新创建一个对象,模拟每一个新创建的实例对象
  const obj = {};
  // 让obj的原型链上能找到Constructor的原型信息
  obj.__proto__ = Constructor.prototype;
  // 将传入的属性一一绑定到obj的实例上
  const constructorReturns = Constructor.call(obj, ...restArgs);
  return typeof constructorReturns === 'object' ? constructorReturns : obj;
}
const person = myNew(Person, 'leevare', 18);
console.log(person);

再次运行,结果正确。

当然,实现的方式有很多种,这里只是其中的一种实现。比如上述声明obj和修改obj__proto__指向,我们还可以这样。

const obj = Object.create(Constructor.prototype);

看起来有点像之前文章中讲到的继承了。

本文完。😊

如果您觉得本文对您有用,欢迎捐赠或留言~
微信支付
支付宝

发表评论

您的电子邮箱地址不会被公开。