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

显示模式

登录
ARCHIVE DOCUMENTJS

面试官问:能否模拟实现 JS 的 new 操作符

所属馆藏
JavaScript
文件格式
Markdown
原始路径
JavaScript/134-面试官问:能否模拟实现JS的new操作符
本文目录8 个章节
  1. 前言
  2. new 做了什么
  3. 构造函数的返回值
  4. 模拟实现 new
  5. Object.create() 用法举例
  6. 现代补充:class、new.target 与 Reflect.construct
  7. 最后总结
  8. 参考资料

面试官问:能否模拟实现 JS 的 new 操作符

Category(分类): JavaScript Status: 未知

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

前言

你好,我是若川。这是“面试官问”系列的第一篇,旨在帮助读者提升 JavaScript 基础知识,涉及 newcallapplythis 和继承。

面试官问系列还包括:

  1. 面试官问:能否模拟实现 JS 的 new 操作符
  2. 面试官问:能否模拟实现 JS 的 bind 方法
  3. 面试官问:能否模拟实现 JS 的 call 和 apply 方法
  4. 面试官问:JS 的 this 指向
  5. 面试官问:JS 的继承

Vue 2 时代常见的入口是 new Vue(options)

// Vue 2 的历史写法
const vm = new Vue({
  el: '#app',
  mounted() {}
})

Vue 3 使用应用工厂 createApp,不再要求通过 new Vue 创建根应用:

// Vue 3 的现代写法
const app = createApp(App)
app.mount('#app')

面试官可能会继续追问:new 到底做了什么,能否用 JavaScript 模拟实现?

new 做了什么

例子 1:创建新对象

function Student() {}

const student = new Student()
console.log(typeof student) // object
console.log(Object.prototype.toString.call(student)) // [object Object]
console.log(Object.getPrototypeOf(student) === Student.prototype) // true
console.log(student.constructor === Student) // true
console.log(typeof Student) // function
console.log(typeof Object) // function

一个函数使用 new 调用后,会产生一个新对象。Student 和内置的 Object 都是函数,但 Student 是用户定义的构造器,Object 是内置构造器。

new Object()Object() 在很多常见输入下都会产生对象,但两者仍是不同的调用形式;对于原始值参数,Object(value) 会返回对应的包装对象,不能把它简单理解成“总是创建空对象”。日常代码通常使用对象字面量 {},而不是 new Object()

原文配图 134-01

new Student() 创建的对象上,原型关系可以这样观察:

console.log(Object.getPrototypeOf(student) === Student.prototype) // true
console.log(Student.prototype.constructor === Student) // true
console.log(Object.getPrototypeOf(Student.prototype) === Object.prototype) // true
console.log(Object.getPrototypeOf(Object.prototype)) // null

原文使用 __proto__ 查看原型。它是为了 Web 兼容而保留的访问器属性,不建议作为新代码的主要 API;读取原型优先使用 Object.getPrototypeOf

小结 1

从这个简单例子可以观察到:

  1. new 创建一个全新的对象。
  2. 新对象的 [[Prototype]] 通常会指向构造函数的 prototype 属性。

例子 2:构造函数中的 this

function Student(name) {
  console.log('赋值前 this', this)
  this.name = name
  console.log('赋值后 this', this)
}

const student = new Student('若川')
console.log(student.name) // 若川
console.log(Object.getPrototypeOf(student) === Student.prototype) // true

构造函数中的 this 指向 new Student() 创建的新对象。构造函数执行前,原型连接和 this 绑定已经由构造调用准备好。

小结 2

  1. 构造函数执行时,其 this 指向 new 创建的新对象。

例子 3:实例共享原型方法

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

Student.prototype.doSth = function () {
  console.log(this.name)
}

const student1 = new Student('若')
const student2 = new Student('川')

student1.doSth() // 若
student2.doSth() // 川
console.log(student1.doSth === student2.doSth) // true
console.log(Object.getPrototypeOf(student1) === Student.prototype) // true
console.log(Object.getPrototypeOf(student2) === Student.prototype) // true

doSth 只存储在 Student.prototype 上,两个实例沿原型链找到同一个函数。调用 student1.doSth() 时,函数中的 this 仍然是 student1,而不是 Student.prototype

原文配图 134-02

原文配图 134-03

小结 3

通过 new Student() 创建的每个对象,会把自己的 [[Prototype]] 连接到当时的 Student.prototype 对象。注意,如果之后重新赋值 Student.prototype,已经创建的实例仍然指向旧对象,新实例才会指向新对象。

构造函数的返回值

如果构造函数没有显式返回值,或返回原始值,new 会返回新创建的对象:

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

const primitiveResult = new ReturnPrimitive('若川')
console.log(primitiveResult.name) // 若川
console.log(primitiveResult instanceof ReturnPrimitive) // true

下面这些值都是原始值,返回时会被忽略:

function ReturnNull() {
  this.kind = 'instance'
  return null
}

function ReturnSymbol() {
  this.kind = 'instance'
  return Symbol('ignored')
}

console.log(new ReturnNull().kind) // instance
console.log(new ReturnSymbol().kind) // instance

如果构造函数返回非原始值,则该值成为 new 表达式的结果。对象不仅包括普通对象,也包括函数、数组、日期、正则表达式、错误对象和 null 原型对象:

function ReturnObject() {
  this.kind = 'instance'
  return { kind: 'returned' }
}

function ReturnFunction() {
  return function returnedFunction() {}
}

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

const objectResult = new ReturnObject()
const functionResult = new ReturnFunction()
const nullPrototypeResult = new ReturnNullPrototypeObject()

console.log(objectResult.kind) // returned
console.log(typeof functionResult) // function
console.log(Object.getPrototypeOf(nullPrototypeResult)) // null

小结 4

  1. 构造函数没有返回值或返回原始值时,new 返回新对象。
  2. 构造函数返回对象或函数时,new 返回该非原始值。
  3. 判断返回值不能只写 result instanceof Object,因为 Object.create(null) 是对象但不继承 Object.prototype

模拟实现 new

把上面的行为整理起来,普通函数构造器的教学实现可以写成:

function newOperator(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
}

逐步说明:

  1. 检查第一个参数是否是函数。
  2. 创建对象,并在可能时把它的原型设置为 Constructor.prototype
  3. 使用 Reflect.apply 把对象作为 this,执行构造函数。
  4. 返回构造函数产生的对象或函数,否则返回最初创建的实例。
function Student(name, age) {
  this.name = name
  this.age = age
}

Student.prototype.doSth = function () {
  return `${this.name}:${this.age}`
}

const student1 = newOperator(Student, '若', 18)
const student2 = newOperator(Student, '川', 18)

console.log(student1.doSth()) // 若:18
console.log(student2.doSth()) // 川:18
console.log(student1 instanceof Student) // true
console.log(Object.getPrototypeOf(student1) === Student.prototype) // true

原文曾写 newOperator.target = ctor,这不是 new.targetnew.target 是 JavaScript 运行时提供的元属性,只能在函数或 class 的调用上下文中读取,不能通过给普通函数增加 target 属性来模拟。上面的实现也不是原生 new 的完全替代品:

  • class 不能用 Reflect.apply 调用,只能通过 newReflect.construct 构造;
  • Proxy 可以通过 construct trap 改变构造行为;
  • 跨 realm 对象的默认原型不能简单固定为当前 realm 的 Object.prototype
  • 内置对象可能需要特殊内部槽,手写函数不能任意模拟 MapDate 等全部构造语义。

生产代码直接使用 new;元编程场景使用标准的 Reflect.construct

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

const user = Reflect.construct(User, ['Ada'])
console.log(user.name) // Ada

Object.create() 用法举例

Object.create(proto, propertiesObject) 创建一个新对象,并把 proto 作为新对象的 [[Prototype]]。第二个参数是可选的属性描述符对象:

const anotherObject = {
  name: '若川'
}

const myObject = Object.create(anotherObject, {
  age: {
    value: 18,
    enumerable: true,
    writable: true,
    configurable: true
  }
})

console.log(Object.getPrototypeOf(anotherObject) === Object.prototype) // true
console.log(Object.getPrototypeOf(myObject) === anotherObject) // true
console.log(Object.hasOwn(myObject, 'name')) // false
console.log(Object.hasOwn(myObject, 'age')) // true
console.log(myObject.name) // 若川
console.log(myObject.age) // 18

原文中的 value:18 使用了中文冒号,会造成语法错误;属性描述符的键值应使用英文冒号。属性描述符中的 enumerablewritableconfigurable 如果省略,默认值为 false,这也是 Object.create 示例中经常被忽略的细节。

历史上的 Object.create polyfill

现代浏览器和 Node.js 都已经支持 Object.create,不需要再添加 polyfill。下面保留一个 ES5 时代的简化思路,但它不支持 null 原型和第二个属性描述符参数,不能作为完整标准实现:

function createLegacy(proto) {
  if (proto !== null && typeof proto !== 'object' && typeof proto !== 'function') {
    throw new TypeError('Object prototype may only be an Object or null')
  }

  function Empty() {}
  Empty.prototype = proto
  return new Empty()
}

const value = createLegacy({ kind: 'legacy' })
console.log(value.kind) // legacy

真正的兼容层还需要处理 null 原型、属性描述符、不可扩展对象和各类错误;不要覆盖现代运行时已有的标准实现。

现代补充:class、new.targetReflect.construct

class 语法仍然建立在原型机制之上,但 class 不能像普通函数一样直接调用:

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

const person = new Person('Ada')
console.log(person.name) // Ada

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

普通函数可以使用 new.target 区分直接调用和构造调用:

function Factory(name) {
  if (!new.target) {
    return { name, fromFactory: true }
  }

  this.name = name
  this.fromConstructor = true
}

console.log(Factory('Ada')) // { name: 'Ada', fromFactory: true }
console.log(new Factory('Ada')) // Factory { name: 'Ada', fromConstructor: true }

最后总结

  1. new 创建新对象,并在适用时把新对象的 [[Prototype]] 指向构造函数的 prototype
  2. 构造函数执行时,this 指向新对象。
  3. 构造函数返回原始值时,原始值会被忽略;返回对象或函数时,该非原始值会成为表达式结果。
  4. Object.getPrototypeOfObject.createReflect.applyReflect.construct 是理解或实现相关机制时更清晰的标准 API。
  5. 手写 new 适合面试和学习,不要声称它可以替代所有内置构造器、class、Proxy 和跨 realm 语义。

参考资料

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

作者:若川

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

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

457 DOCUMENTS · 10 COLLECTIONS
ARCHIVE SEARCH457 篇文章

SEARCH GUIDE

输入关键词开始搜索

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

按分类浏览

10 COLLECTIONS