深入理解 JavaScript 之 new 原理及模拟实现

前言

最近重新回顾 JavaScript 原型链和构造函数,发现 new 是一个很适合拿来串知识点的运算符。

js 复制代码
const person = new Person(18);

背后同时涉及:

  1. 对象是怎么创建出来的。
  2. 实例为什么能访问构造函数原型上的方法。
  3. 构造函数里的 this 为什么会指向实例。
  4. 构造函数主动 return 时,最终结果到底取哪个。
  5. 为什么手写 new 可以理解原理,但不能完全替代真正的 new

从一个例子开始

首先来看一段最常见的代码:

js 复制代码
function Person(age) {
  this.age = age;
}

Person.prototype.getAge = function () {
  console.log('年龄为:' + this.age);
};

const person = new Person(18);

console.log(person.age);
// 18

person.getAge();
// 年龄为:18

可以看到,person 至少具备两个能力:

  1. 可以访问构造函数 Person 内部通过 this 挂载的属性,比如 age
  2. 可以访问 Person.prototype 上的方法,比如 getAge

new 出来的实例,一方面拿到了构造函数里的实例属性,另一方面连上了构造函数的原型对象

接下来我们继续看几个返回值相关的例子。

构造函数没有 return 会发生什么

先看没有 return 的情况:

js 复制代码
function Person(age) {
  this.age = age;
}

const person = new Person(18);

console.log(person);
// Person { age: 18 }

从代码上看,Person 并没有显式返回任何值。

new Person(18) 最后还是得到了一个对象。这说明 new 在执行构造函数之前,已经先准备好了一个新对象。构造函数只是往这个对象上挂属性,最后如果构造函数没有主动返回对象,new 就会把这个新对象作为结果返回。

构造函数 return 对象会发生什么

如果构造函数主动返回一个对象呢?

js 复制代码
function Person(age) {
  this.age = age;

  return {
    name: '手动返回一个对象',
  };
}

const person = new Person(18);

console.log(person);
// { name: '手动返回一个对象' }

console.log(person.age);
// undefined

这个时候可以看到,this.age = age 确实执行了,但最终返回结果不是默认创建的实例对象,而是 return 后面的对象。

也就是说,构造函数如果返回的是对象类型,这个对象会覆盖 new 默认创建的实例对象

这里的"对象类型"不只普通对象,也包括数组、函数等非原始值:

js 复制代码
function Person(age) {
  this.age = age;

  return function sayHi() {
    console.log('hi');
  };
}

const person = new Person(18);

console.log(typeof person);
// function

这点在手写实现时很重要,因为很多简单实现会只判断 ret instanceof Object,但更准确的判断应该是:返回值是否为非 null 的 object,或者 function。

构造函数 return 基本类型会发生什么

如果构造函数返回的是基本数据类型:

js 复制代码
function Person(age) {
  this.age = age;

  return 1;
}

const person = new Person(18);

console.log(person);
// Person { age: 18 }

可以看到,返回数字没有影响最终结果。new 依然返回默认创建的实例对象。

null 也一样:

js 复制代码
function Person(age) {
  this.age = age;

  return null;
}

const person = new Person(18);

console.log(person);
// Person { age: 18 }

虽然 typeof null === 'object',但它不是一个真正可以作为实例结果返回的对象。实现时也需要把 null 排除掉。

new 的返回值规则:

构造函数返回值 new 的最终结果
没有 return 返回默认创建的新对象
return 基本类型 返回默认创建的新对象
return null 返回默认创建的新对象
return 普通对象、数组等 返回构造函数显式返回的对象
return 函数 返回构造函数显式返回的函数

new 到底做了什么

MDN 对 new 的描述可以概括为四步:

  1. 创建一个空对象。
  2. 把这个新对象的 [[Prototype]] 指向构造函数的 prototype
  3. 用这个新对象作为 this 执行构造函数。
  4. 如果构造函数返回对象,则返回这个对象;否则返回第一步创建的新对象。

如果结合 ECMAScript 规范看,new 表达式大致会走到 EvaluateNew

text 复制代码
new Person(18)

可以拆成:

  1. 先求出 Person 这个构造器。
  2. 收集参数列表 [18]
  3. 判断 Person 是否是构造器,也就是是否具备 [[Construct]] 能力。
  4. 调用 Construct(Person, [18])

注意一个细节:能被调用,不代表能被 new

比如箭头函数可以调用,但不能作为构造函数:

js 复制代码
const Person = () => {};

Person();
// 可以调用

new Person();
// TypeError: Person is not a constructor

所以,真正的 new 不是简单判断 typeof Constructor === 'function' 就完了。规范里会判断它有没有 [[Construct]] 这个内部方法。

不过我们平时手写模拟实现时,主要目标是理解普通构造函数的行为,不可能完全模拟所有内部槽和规范语义。

从流程图理解 new

一张图把流程串起来:
#mermaid-svg-je9NzqZjKtfTWYUx{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-je9NzqZjKtfTWYUx .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-je9NzqZjKtfTWYUx .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-je9NzqZjKtfTWYUx .error-icon{fill:#552222;}#mermaid-svg-je9NzqZjKtfTWYUx .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-je9NzqZjKtfTWYUx .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-je9NzqZjKtfTWYUx .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-je9NzqZjKtfTWYUx .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-je9NzqZjKtfTWYUx .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-je9NzqZjKtfTWYUx .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-je9NzqZjKtfTWYUx .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-je9NzqZjKtfTWYUx .marker{fill:#333333;stroke:#333333;}#mermaid-svg-je9NzqZjKtfTWYUx .marker.cross{stroke:#333333;}#mermaid-svg-je9NzqZjKtfTWYUx svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-je9NzqZjKtfTWYUx p{margin:0;}#mermaid-svg-je9NzqZjKtfTWYUx .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-je9NzqZjKtfTWYUx .cluster-label text{fill:#333;}#mermaid-svg-je9NzqZjKtfTWYUx .cluster-label span{color:#333;}#mermaid-svg-je9NzqZjKtfTWYUx .cluster-label span p{background-color:transparent;}#mermaid-svg-je9NzqZjKtfTWYUx .label text,#mermaid-svg-je9NzqZjKtfTWYUx span{fill:#333;color:#333;}#mermaid-svg-je9NzqZjKtfTWYUx .node rect,#mermaid-svg-je9NzqZjKtfTWYUx .node circle,#mermaid-svg-je9NzqZjKtfTWYUx .node ellipse,#mermaid-svg-je9NzqZjKtfTWYUx .node polygon,#mermaid-svg-je9NzqZjKtfTWYUx .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-je9NzqZjKtfTWYUx .rough-node .label text,#mermaid-svg-je9NzqZjKtfTWYUx .node .label text,#mermaid-svg-je9NzqZjKtfTWYUx .image-shape .label,#mermaid-svg-je9NzqZjKtfTWYUx .icon-shape .label{text-anchor:middle;}#mermaid-svg-je9NzqZjKtfTWYUx .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-je9NzqZjKtfTWYUx .rough-node .label,#mermaid-svg-je9NzqZjKtfTWYUx .node .label,#mermaid-svg-je9NzqZjKtfTWYUx .image-shape .label,#mermaid-svg-je9NzqZjKtfTWYUx .icon-shape .label{text-align:center;}#mermaid-svg-je9NzqZjKtfTWYUx .node.clickable{cursor:pointer;}#mermaid-svg-je9NzqZjKtfTWYUx .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-je9NzqZjKtfTWYUx .arrowheadPath{fill:#333333;}#mermaid-svg-je9NzqZjKtfTWYUx .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-je9NzqZjKtfTWYUx .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-je9NzqZjKtfTWYUx .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-je9NzqZjKtfTWYUx .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-je9NzqZjKtfTWYUx .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-je9NzqZjKtfTWYUx .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-je9NzqZjKtfTWYUx .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-je9NzqZjKtfTWYUx .cluster text{fill:#333;}#mermaid-svg-je9NzqZjKtfTWYUx .cluster span{color:#333;}#mermaid-svg-je9NzqZjKtfTWYUx div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-je9NzqZjKtfTWYUx .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-je9NzqZjKtfTWYUx rect.text{fill:none;stroke-width:0;}#mermaid-svg-je9NzqZjKtfTWYUx .icon-shape,#mermaid-svg-je9NzqZjKtfTWYUx .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-je9NzqZjKtfTWYUx .icon-shape p,#mermaid-svg-je9NzqZjKtfTWYUx .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-je9NzqZjKtfTWYUx .icon-shape .label rect,#mermaid-svg-je9NzqZjKtfTWYUx .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-je9NzqZjKtfTWYUx .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-je9NzqZjKtfTWYUx .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-je9NzqZjKtfTWYUx :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 否



new Constructor(...args)
Constructor 是否可构造?
抛出 TypeError
创建一个新对象
设置新对象的 \[Prototype]
以新对象作为 this 执行 Constructor
构造函数返回值是否是对象?
返回构造函数显式返回的对象
返回默认创建的新对象

手搓实现

new 是关键字,不能直接覆盖。这里我们用 create 来模拟:

js 复制代码
function create() {
  const obj = new Object();
  const Constructor = [].shift.call(arguments);

  obj.__proto__ = Constructor.prototype;

  const result = Constructor.apply(obj, arguments);

  return result instanceof Object ? result : obj;
}

这版代码可以跑通最基本的例子:

js 复制代码
function Person(age) {
  this.age = age;
}

Person.prototype.getAge = function () {
  console.log('年龄为:' + this.age);
};

const person = create(Person, 18);

console.log(person.age);
// 18

person.getAge();
// 年龄为:18
  1. new Object() 创建一个空对象。
  2. [].shift.call(arguments) 取出第一个参数,也就是构造函数。
  3. obj.__proto__ = Constructor.prototype 让实例能访问构造函数原型上的属性。
  4. Constructor.apply(obj, arguments) 让构造函数里的 this 指向 obj
  5. 判断构造函数返回值,如果返回对象,就用返回对象;否则返回 obj

这已经能说明 new 的核心思路了。

但这版实现有几个问题:

  1. 直接使用 __proto__ 不推荐。
  2. arguments 可读性一般。
  3. result instanceof Object 对跨 realm 对象不够稳,且表达意图不如直接判断类型。
  4. 没处理 Constructor.prototype 不是对象的情况。
  5. 没说明它和真正 new 的差距。

再优化一版。

更稳妥的模拟实现

先写一个工具函数,用来判断返回值是否可以作为对象结果:

js 复制代码
function isObject(value) {
  return (
    value !== null && (typeof value === 'object' || typeof value === 'function')
  );
}

然后实现 create

js 复制代码
function create(Constructor, ...args) {
  if (typeof Constructor !== 'function') {
    throw new TypeError('Constructor must be a function');
  }

  const prototype = isObject(Constructor.prototype)
    ? Constructor.prototype
    : Object.prototype;

  const instance = Object.create(prototype);
  const result = Constructor.apply(instance, args);

  return isObject(result) ? result : instance;
}

这里改动主要有三点。

先用 Object.create(prototype) 创建对象。

Object.create 的作用就是创建一个新对象,并把这个新对象的原型指向传入对象。相比直接改 __proto__,这样更清晰。

再对 Constructor.prototype 做了兜底。

根据规范里的 OrdinaryCreateFromConstructor,创建对象时会尝试从构造器的 prototype 属性上拿原型;如果这个值不是对象,就使用默认原型。

可以用代码验证:

js 复制代码
function Person(name) {
  this.name = name;
}

Person.prototype = 1;

const p = new Person('Jack');

console.log(Object.getPrototypeOf(p) === Object.prototype);
// true

所以模拟实现里也做了类似处理:

js 复制代码
const prototype = isObject(Constructor.prototype)
  ? Constructor.prototype
  : Object.prototype;

结尾,返回值判断不再用 instanceof Object

更直接的判断是:

js 复制代码
value !== null && (typeof value === 'object' || typeof value === 'function');

这样可以覆盖普通对象、数组、函数,也可以排除 null 和基本类型。

测试一下手搓代码

没有 return

js 复制代码
function Person(age) {
  this.age = age;
}

const person = create(Person, 18);

console.log(person.age);
// 18

console.log(person instanceof Person);
// true

return 对象

js 复制代码
function Person(age) {
  this.age = age;

  return {
    name: '手动返回对象',
  };
}

const person = create(Person, 18);

console.log(person);
// { name: '手动返回对象' }

console.log(person.age);
// undefined

return 基本类型

js 复制代码
function Person(age) {
  this.age = age;

  return 1;
}

const person = create(Person, 18);

console.log(person.age);
// 18

return 函数

js 复制代码
function Person(age) {
  this.age = age;

  return function sayHi() {
    console.log('hi');
  };
}

const person = create(Person, 18);

console.log(typeof person);
// function

为什么不建议用 __proto__

很多手写 new 的文章会这么写:

js 复制代码
obj.__proto__ = Constructor.prototype;

这行代码确实能把 obj 的原型指向 Constructor.prototype,它不适合作为推荐写法。

原因很简单:真正想表达的是"创建一个指定原型的新对象",而不是"先创建一个对象,再修改它的原型"。从意图上看,Object.create(Constructor.prototype) 更直接。

也就是说:

js 复制代码
const instance = Object.create(Constructor.prototype);

比下面这样更适合出现在模拟实现里:

js 复制代码
const instance = {};
instance.__proto__ = Constructor.prototype;

更何况我们还需要处理 Constructor.prototype 不是对象的情况,所以最后写成:

js 复制代码
const prototype = isObject(Constructor.prototype)
  ? Constructor.prototype
  : Object.prototype;

const instance = Object.create(prototype);

这样读代码时,意图会更明确。

new 和 Object.create 的区别

这两个 API 很容易放在一起,但它们不是一回事。

js 复制代码
const obj = Object.create(Person.prototype);

这行代码只做了一件事:创建一个新对象,并把它的原型指向 Person.prototype

它不会执行 Person 构造函数。

也就是说:

js 复制代码
function Person(age) {
  this.age = age;
}

Person.prototype.getAge = function () {
  console.log(this.age);
};

const p1 = Object.create(Person.prototype);

console.log(p1.age);
// undefined

p1.getAge();
// undefined

new Person(18) 做得更多:

js 复制代码
const p2 = new Person(18);

console.log(p2.age);
// 18

p2.getAge();
// 18

所以可以简单记:

操作 是否创建对象 是否链接原型 是否执行构造函数
Object.create()
new

手写 new 时使用 Object.create,只是借它完成"创建对象 + 链接原型"这一步。

模拟实现的边界

上面的 create 能帮助我们理解 new 的普通场景,但它并不是完整 polyfill。

接下来几个边界情况,反而更能帮助我们看清真正的 new 做了什么。

1. 箭头函数不能被 new

箭头函数没有自己的 this,也不能作为构造函数:

js 复制代码
const Person = () => {};

new Person();
// TypeError: Person is not a constructor

但上面手搓只判断了 typeof Constructor === 'function'。箭头函数也是 function,所以这一点无法完全模拟。

如果用 create(Person),它不会和真实 new Person() 完全一致。

这是因为真正的 new 判断的是函数是否具备 [[Construct]] 内部方法,而不是简单判断类型。

2. class 构造函数不能直接 apply

js 复制代码
class Person {
  constructor(age) {
    this.age = age;
  }
}

const person = new Person(18);

console.log(person.age);
// 18

但如果用手搓的 create

js 复制代码
create(Person, 18);
// TypeError: Class constructor Person cannot be invoked without 'new'

原因是 class 构造函数必须通过 new 调用,不能像普通函数一样通过 apply 调用。

这也是手写 new 的一个天然边界:用 apply 只能模拟普通函数构造器,不能模拟 class 构造器的完整语义

3. new.target 无法被模拟出来

ES6 里可以通过 new.target 判断函数是普通调用,还是通过 new 调用:

js 复制代码
function Person(age) {
  console.log(new.target === Person);
  this.age = age;
}

new Person(18);
// true

但如果用 create

js 复制代码
create(Person, 18);
// false

因为 create 里实际执行的是:

js 复制代码
Constructor.apply(instance, args);

这本质上还是一次普通函数调用,不会设置 new.target

如果确实需要动态调用构造函数,并保留更接近 new 的语义,可以使用 Reflect.construct

js 复制代码
function Person(age) {
  console.log(new.target === Person);
  this.age = age;
}

const person = Reflect.construct(Person, [18]);
// true

console.log(person.age);
// 18

Reflect.construct(Constructor, args) 可以理解成函数形式的 new Constructor(...args)

4. 内置对象可能有内部槽

再来看一个更容易忽略的例子:

js 复制代码
const date = new Date();

console.log(date instanceof Date);
// true

console.log(date.getTime());
// 正常返回时间戳

如果用上面手搓的 create 去模拟:

js 复制代码
const fakeDate = create(Date);

console.log(fakeDate instanceof Date);
// true

console.log(fakeDate.getTime());
// TypeError: this is not a Date object

这里看起来很反直觉:fakeDate instanceof Datetrue,但 getTime() 依然报错。

原因是 instanceof 主要检查原型链,而真正的 Date 实例内部还有日期相关的内部槽。手动 Object.create(Date.prototype) 只能伪造原型链,不能创建这些引擎内部的数据结构。

所以,手写 new 最适合用来理解普通构造函数,不适合拿来模拟所有内置对象。

new 和原型链的关系

js 复制代码
function Person(age) {
  this.age = age;
}

Person.prototype.getAge = function () {
  console.log(this.age);
};

const person = new Person(18);

这段代码执行完后,关系大概是:
#mermaid-svg-7zLmlfRRwJ479hvb{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-7zLmlfRRwJ479hvb .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-7zLmlfRRwJ479hvb .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-7zLmlfRRwJ479hvb .error-icon{fill:#552222;}#mermaid-svg-7zLmlfRRwJ479hvb .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-7zLmlfRRwJ479hvb .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-7zLmlfRRwJ479hvb .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-7zLmlfRRwJ479hvb .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-7zLmlfRRwJ479hvb .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-7zLmlfRRwJ479hvb .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-7zLmlfRRwJ479hvb .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-7zLmlfRRwJ479hvb .marker{fill:#333333;stroke:#333333;}#mermaid-svg-7zLmlfRRwJ479hvb .marker.cross{stroke:#333333;}#mermaid-svg-7zLmlfRRwJ479hvb svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-7zLmlfRRwJ479hvb p{margin:0;}#mermaid-svg-7zLmlfRRwJ479hvb .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-7zLmlfRRwJ479hvb .cluster-label text{fill:#333;}#mermaid-svg-7zLmlfRRwJ479hvb .cluster-label span{color:#333;}#mermaid-svg-7zLmlfRRwJ479hvb .cluster-label span p{background-color:transparent;}#mermaid-svg-7zLmlfRRwJ479hvb .label text,#mermaid-svg-7zLmlfRRwJ479hvb span{fill:#333;color:#333;}#mermaid-svg-7zLmlfRRwJ479hvb .node rect,#mermaid-svg-7zLmlfRRwJ479hvb .node circle,#mermaid-svg-7zLmlfRRwJ479hvb .node ellipse,#mermaid-svg-7zLmlfRRwJ479hvb .node polygon,#mermaid-svg-7zLmlfRRwJ479hvb .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-7zLmlfRRwJ479hvb .rough-node .label text,#mermaid-svg-7zLmlfRRwJ479hvb .node .label text,#mermaid-svg-7zLmlfRRwJ479hvb .image-shape .label,#mermaid-svg-7zLmlfRRwJ479hvb .icon-shape .label{text-anchor:middle;}#mermaid-svg-7zLmlfRRwJ479hvb .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-7zLmlfRRwJ479hvb .rough-node .label,#mermaid-svg-7zLmlfRRwJ479hvb .node .label,#mermaid-svg-7zLmlfRRwJ479hvb .image-shape .label,#mermaid-svg-7zLmlfRRwJ479hvb .icon-shape .label{text-align:center;}#mermaid-svg-7zLmlfRRwJ479hvb .node.clickable{cursor:pointer;}#mermaid-svg-7zLmlfRRwJ479hvb .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-7zLmlfRRwJ479hvb .arrowheadPath{fill:#333333;}#mermaid-svg-7zLmlfRRwJ479hvb .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-7zLmlfRRwJ479hvb .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-7zLmlfRRwJ479hvb .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-7zLmlfRRwJ479hvb .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-7zLmlfRRwJ479hvb .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-7zLmlfRRwJ479hvb .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-7zLmlfRRwJ479hvb .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-7zLmlfRRwJ479hvb .cluster text{fill:#333;}#mermaid-svg-7zLmlfRRwJ479hvb .cluster span{color:#333;}#mermaid-svg-7zLmlfRRwJ479hvb div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-7zLmlfRRwJ479hvb .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-7zLmlfRRwJ479hvb rect.text{fill:none;stroke-width:0;}#mermaid-svg-7zLmlfRRwJ479hvb .icon-shape,#mermaid-svg-7zLmlfRRwJ479hvb .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-7zLmlfRRwJ479hvb .icon-shape p,#mermaid-svg-7zLmlfRRwJ479hvb .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-7zLmlfRRwJ479hvb .icon-shape .label rect,#mermaid-svg-7zLmlfRRwJ479hvb .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-7zLmlfRRwJ479hvb .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-7zLmlfRRwJ479hvb .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-7zLmlfRRwJ479hvb :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} own property
prototype method
person
Person.prototype
Object.prototype
null
age: 18
getAge()

所以:

js 复制代码
person.age;

先在 person 自身找,能找到 age

而:

js 复制代码
person.getAge();

先在 person 自身找,找不到,再沿着原型链到 Person.prototype 上找,于是找到了 getAge

这就是 new 和原型链之间的核心关系:new 不会把原型方法复制到实例上,而是让实例通过原型链访问这些方法

constructor 属性

真正发生的是:实例的 [[Prototype]] 指向了构造函数的 prototype 对象。而 prototype 对象上默认有一个 constructor 属性,指回构造函数。

比如:

js 复制代码
function Person() {}

const person = new Person();

console.log(person.constructor === Person);
// true

看起来像是 person 自己有 constructor,但其实不是:

js 复制代码
console.log(person.hasOwnProperty('constructor'));
// false

console.log(Person.prototype.hasOwnProperty('constructor'));
// true

也就是说,person.constructor 是沿着原型链找到的。

如果我们手动重写 Person.prototype

js 复制代码
function Person() {}

Person.prototype = {
  getName() {},
};

const person = new Person();

console.log(person.constructor === Person);
// false

因为新的 Person.prototype 对象里没有手动补回 constructor

通常会这样写:

js 复制代码
Person.prototype = {
  constructor: Person,
  getName() {},
};

所以,new 的关键不是设置实例的 constructor,而是设置实例的原型链。

最终版代码

最后把代码整理一下:

js 复制代码
function isObject(value) {
  return (
    value !== null && (typeof value === 'object' || typeof value === 'function')
  );
}

function create(Constructor, ...args) {
  if (typeof Constructor !== 'function') {
    throw new TypeError('Constructor must be a function');
  }

  const prototype = isObject(Constructor.prototype)
    ? Constructor.prototype
    : Object.prototype;

  const instance = Object.create(prototype);
  const result = Constructor.apply(instance, args);

  return isObject(result) ? result : instance;
}

这个版本可以覆盖普通构造函数下最核心的 new 行为:

  1. 创建一个新对象。
  2. 设置新对象的原型。
  3. 绑定 this 执行构造函数。
  4. 根据构造函数返回值决定最终结果。

但也要记住它的边界:

  1. 不能完整判断一个函数是否具备 [[Construct]]
  2. 不能模拟 class 构造函数。
  3. 不能模拟 new.target
  4. 不能创建内置对象需要的内部槽。
  5. 不能替代真正的 new,只能作为学习原理的实现。

总结

new 的核心其实可以压缩成一句话:

创建一个新对象,把它连到构造函数的原型上,再用它作为 this 执行构造函数,最后根据构造函数返回值决定返回谁。

但真正写模拟实现时,细节就会冒出来:

  1. 原型链接不是复制方法,而是设置 [[Prototype]]
  2. 构造函数返回对象时,会覆盖默认创建的实例。
  3. 返回基本类型和 null 时,会被忽略。
  4. __proto__ 可以帮助理解,但实现上更推荐 Object.create
  5. instanceof Object 不是最好的返回值判断方式。
  6. 手写 new 理解普通函数构造器足够,但不要误以为它能覆盖 classnew.target 和所有内置对象。

参考文章和规范