JS 中的 new 到底做了什么

这篇小短文里,我们来聊聊 js 里的 new 运算符到底干了什么。

预备知识:你需要对 this 有所熟悉,需要对原型链知识有所熟悉。

开始聊前,先放热身代码,大家瞅下下面代码的运行结果会是什么呢?

function Person(name) {
  this.name = name;
}
Person.prototype.say = function() {
  console.log(this.name)
}

function FrozenPerson(name) {
  this.name = "SnowMan"
}
FrozenPerson.prototype.shake = function() {
  console.log(this.name, 'shake shake');
}

const alice = new Person("alice")
Person.prototype = FrozenPerson.prototype
const alice2 = new Person("alice2")

console.log(alice.name);
console.log(alice2.name);
try { alice.say();} catch (e) {}
try { alice2.say();} catch (e) {}
try { alice2.shake();} catch (e) {}

说出上面代码的运行内容似乎也没必要知道 new 运算符具体干了啥,仅凭借对原型链和 this 的了解就能说出来了,所以例子并不太严谨哈 😅,但如果对 new 了解,那肯定会更加自信自己的答案。

下面进行正题 😉:

new在 js 里是一个运算符/操作符(operator),既然是运算符,自然有它的“运算规则”,那么它具体是怎么“运算”的呢?

每个运算符都有它的作用对象(operand),比如加法运算符+的作用对象是它两边的内容,可以是数字,可以是字符串等。而 new 运算符的作用对象则必须是一个函数或一个类,这里暂不讨论类,不过类的本质还是函数,它就是一个语法糖而已。

下面来手动实现一个自己的 new 运算符从而直观理解它的“运算规则”到底是什么样的,由于 js 里没有类似 c++ 的运算符重载,所以得用函数来模拟实现,由于 new 是 js 的保留关键字,所以这里使用 myNew 作为函数名,详见下面代码:

// 这是我们的构造函数
function Person(name) {
  this.name = name;
}
// 调用自定义的new运算符
const alice = myNew(Person, "alice");

// 自定义new运算符的具体实现
function myNew(constructor, ...args) {
  // 第1步:建立一个空对象
  const obj = {};
  // 第2步:设置上面空对象的原型
  Object.setPrototypeOf(obj, constructor.prototype);
  //  第3步:执行(构造)函数,并绑定其this为第1步中建立的空对象,同时传入函数参数
  const customObj = constructor.apply(obj, args);
  // 第4.1步:如果上面(构造)函数返回的是一个对象,那么new运算会直接返回该对象
  if(typeof customObj === 'object')
    return customObj
  // 第4.2步:如果返回的不是对象,比如是个字符串、数字、undefined等原始类型,则new运算会返回第一步中创建的那个对象
  else
    return obj;
}

看完上面代码中的注释,再重新思考开头的例子,是否能更加清楚些呢。