技术知识文章集合TECHNICAL ARCHIVE · 457 DOCUMENTS

显示模式

登录
ARCHIVE DOCUMENTJS

重学 JS 系列:聊聊 new 操作符

所属馆藏
JavaScript
文件格式
Markdown
原始路径
JavaScript/129-重学 JS 系列:聊聊 new 操作符
本文目录5 个章节
  1. new 的作用
  2. new 大致做了什么
  3. 自己实现一个简化版 new
  4. new.target 与“无 new”调用
  5. 最后

重学 JS 系列:聊聊 new 操作符

Category(分类): JavaScript Status: 未知

配图来自原文页面,已下载并转换为本地 WebP;原始授权未核实,发布前请人工确认。

这是重学 JS 系列的第一篇文章,写这个系列的初衷也是为了夯实自己的 JavaScript 基础。既然是重学,肯定不会从零开始介绍一个知识点;下面保留原文的推导过程,并补充现代规范中的边界。

new 的作用

我们先通过例子了解 new 的作用:

function Test(name) {
  this.name = name
}

Test.prototype.sayName = function () {
  console.log(this.name)
}

const t = new Test('yck')
console.log(t.name) // yck
t.sayName() // yck
console.log(Object.getPrototypeOf(t) === Test.prototype) // true

从上面的例子中可以得出这些结论:

  • new 通过构造函数 Test 创建出来的实例,可以访问构造函数中写入 this 的属性。
  • 实例的 [[Prototype]] 通常会指向 Test.prototype,因此可以访问构造函数原型上的方法。
  • Test.prototype 和实例的 [[Prototype]] 是同一个对象引用,但 prototype[[Prototype]] 是两个不同的概念。

构造函数返回原始值

当构造函数没有显式返回值时,new 返回新创建的实例。如果构造函数返回原始值,这个值会被忽略:

function ReturnPrimitive(name) {
  this.name = name
  return 1
}

const value = new ReturnPrimitive('yck')
console.log(value.name) // yck
console.log(value instanceof ReturnPrimitive) // true

这里的“原始值”包括数字、字符串、布尔值、nullundefinedsymbolbigint。原始值不会覆盖 new 创建的实例。

构造函数返回对象

如果构造函数返回对象(包括数组、日期、函数以及 Object.create(null) 创建的对象),这个对象会成为整个 new 表达式的结果:

function ReturnObject(name) {
  this.name = name
  return { age: 26 }
}

const value = new ReturnObject('yck')
console.log(value) // { age: 26 }
console.log(value.name) // undefined
console.log(value instanceof ReturnObject) // false

“对象”这里不能用 result instanceof Object 简单判断,因为 Object.create(null) 没有 Object.prototype,但它仍然是非原始值,应当被 new 采用:

function ReturnNullPrototypeObject() {
  return Object.create(null)
}

const value = new ReturnNullPrototypeObject()
console.log(Object.getPrototypeOf(value)) // null

这两个例子告诉我们:普通构造函数通常不需要显式返回值。返回原始值没有覆盖效果,返回对象则会让 new 返回另一个对象。

new 大致做了什么

对于普通的、可构造的函数,可以把 new Constructor(...args) 抽象为以下步骤:

  1. 创建一个新对象。
  2. 如果 Constructor.prototype 是对象,则把新对象的 [[Prototype]] 设置为它;如果不是对象,则使用当前 realm 的默认对象原型。
  3. 执行构造函数,并把新对象作为 this,传入调用参数。
  4. 如果构造函数返回非原始值,就返回该值;否则返回新对象。

规范内部使用 [[Construct]] 等内部方法实现这一过程。这里的伪代码只是教学模型,不能覆盖 Proxy、类、跨 realm 对象和 new.target 的全部细节。

自己实现一个简化版 new

首先回顾几个要点:

  • 需要创建一个对象。
  • 需要让它访问构造函数原型上的属性。
  • 需要把它作为 this 传给构造函数。
  • 需要忽略构造函数返回的原始值,但保留返回的对象或函数。

下面的实现适合普通函数构造器的教学演示:

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

  const prototype = Constructor.prototype
  const instance = Object.create(
    prototype !== null && (typeof prototype === 'object' || typeof prototype === 'function')
      ? prototype
      : Object.prototype
  )

  const result = Reflect.apply(Constructor, instance, args)
  const isObject = result !== null && (typeof result === 'object' || typeof result === 'function')

  return isObject ? result : instance
}

使用它验证行为:

function Test(name, age) {
  this.name = name
  this.age = age
}

Test.prototype.sayName = function () {
  console.log(this.name)
}

const a = myNew(Test, 'yck', 26)
console.log(a.name) // yck
console.log(a.age) // 26
a.sayName() // yck
console.log(a instanceof Test) // true

原文使用 Con.apply(obj, args) 也能表达“绑定 this 并调用”的意图;这里使用标准的 Reflect.apply,并明确区分了“返回函数也是非原始值”的情况。

这个模拟实现的边界

  • Reflect.apply 不能调用 class 构造器,因为 class 只能通过 newReflect.construct 调用。原生 new 可以构造 class,而上述教学实现不行。
  • 箭头函数、普通方法和某些内置函数不可构造;它们没有可用的构造行为,原生 new 会抛出 TypeError
  • Proxy 可能通过 construct trap 改变构造过程;跨 realm 的默认原型也不能只用 Object.prototype 完全模拟。
  • 生产代码不要为了替代 new 而修改 Function.prototype。如果只是需要以函数形式调用构造能力,优先使用原生 newReflect.construct

生产代码中的 Reflect.construct

Reflect.construct(target, argumentsList, newTarget) 是函数形式的构造操作。前两个参数可以理解为 new target(...argumentsList),第三个参数允许指定 new.target,适合元编程或代理场景:

function Person(name) {
  this.name = name
}

const person = Reflect.construct(Person, ['Ada'])
console.log(person.name) // Ada
console.log(Object.getPrototypeOf(person) === Person.prototype) // true

new.target 与“无 new”调用

普通函数可以同时支持直接调用和构造调用,通过 new.target 区分当前调用方式:

function Person(name) {
  if (!new.target) {
    return { name, calledAsFunction: true }
  }

  this.name = name
  this.calledAsConstructor = true
}

console.log(Person('Ada')) // { name: 'Ada', calledAsFunction: true }
console.log(new Person('Ada')) // Person { name: 'Ada', calledAsConstructor: true }

new.target 在直接调用时是 undefined,在构造调用中是构造目标(在绑定函数和部分 Reflect.construct 场景中还要结合规范理解)。class 不能直接调用:

class User {
  constructor(name) {
    this.name = name
  }
}

try {
  User('Ada')
} catch (error) {
  console.log(error instanceof TypeError) // true
}

最后

new 不只是“调用函数并返回对象”,它同时建立实例与原型之间的关系、准备构造调用的 this,并处理构造函数的返回值。阅读旧代码时,还要注意它可能使用 __proto__Function.prototype.apply 或手写的 new 模拟;现代代码应优先使用 Object.getPrototypeOfObject.createReflect.applyReflect.construct 等标准 API。

原文配图 129-01

参考资料

  1. MDN:new 运算符
  2. MDN:Reflect.construct()
  3. ECMAScript:The new Operator

作者:yck

链接:https://juejin.cn/post/6844903789070123021

来源:稀土掘金。著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

457 DOCUMENTS · 10 COLLECTIONS
ARCHIVE SEARCH457 篇文章

SEARCH GUIDE

输入关键词开始搜索

支持搜索文章标题、所属分类和原始文档路径。

按分类浏览

10 COLLECTIONS