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

显示模式

登录
ARCHIVE DOCUMENTJS

面试官真的会问:new 的实现以及无 new 实例化

所属馆藏
JavaScript
文件格式
Markdown
原始路径
JavaScript/132-面试官真的会问:new的实现以及无new实例化
本文目录7 个章节
  1. new 实例化做了什么
  2. 实现一个简化版 new
  3. 构造函数显式 return
  4. 无 new 实例化
  5. jQuery 的原型转接思路
  6. instanceof 与 new.target
  7. 现代实践

面试官真的会问:new 的实现以及无 new 实例化

Category(分类): JavaScript Status: 未知

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

面试中经常会从“构造函数显式 return”追问到 new 的实现,以及如何设计一个不要求调用者书写 new 的工厂 API。本文保留原文的推导和 jQuery 设计思路,并补充现代标准 API 和实现边界。

new 实例化做了什么

对普通构造函数来说,new Constructor(...args) 大致会经历以下步骤:

  1. 创建一个新对象。
  2. 如果构造函数的 prototype 是对象,把新对象的 [[Prototype]] 指向它。
  3. 执行构造函数,把新对象作为 this,并传入参数。
  4. 如果构造函数返回非原始值,就返回该值;否则返回新对象。

下面的代码可以观察其中的前三步:

function Test() {
  console.log(JSON.stringify(this))
  console.log(Object.getPrototypeOf(this).constructor === Test)
  this.name = 'jack'
  this.age = 18
  console.log(JSON.stringify(this))
}

const value = new Test()
console.log(value.name) // jack
console.log(value.age) // 18

构造函数的 this 在执行过程中指向新对象;原型连接也在函数体执行前准备好了。__proto__ 可以用于观察,但新代码建议使用 Object.getPrototypeOf

实现一个简化版 new

下面的版本适用于普通函数构造器:

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.describe = function () {
  return `${this.name}:${this.age}`
}

const value = myNew(Test, '小明', 18)
console.log(value.describe()) // 小明:18
console.log(value instanceof Test) // true

原文中只写 func.call(obj, ...args); return obj,忽略了构造函数返回对象的情况。完整教学实现必须保留对象/函数返回值;但它仍不是原生 new 的完全等价物:class 不能用 Reflect.apply 调用,Proxy、跨 realm 和 new.target 等场景需要 Reflect.construct

生产代码通常直接使用原生语法,元编程场景可以使用:

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

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

构造函数显式 return

如果构造函数返回对象,这个对象会覆盖 new 创建的实例。对象不仅包括普通对象,也包括数组、日期、函数和 null 原型对象:

function ReturnObject() {
  this.name = 'jack'
  this.age = 18
  return {
    content: '我有 freestyle'
  }
}

const objectResult = new ReturnObject()
console.log(objectResult) // { content: '我有 freestyle' }
console.log(objectResult.name) // undefined

如果返回原始值,则原始值会被忽略:

function ReturnPrimitive() {
  this.name = 'jack'
  this.age = 18
  return '我有 freestyle'
}

const primitiveResult = new ReturnPrimitive()
console.log(primitiveResult.name) // jack
console.log(primitiveResult instanceof ReturnPrimitive) // true

如果构造函数返回函数,也会采用这个函数,因为函数也是非原始值:

function ReturnFunction() {
  return function result() {
    return 'function result'
  }
}

const fn = new ReturnFunction()
console.log(fn()) // function result

new 实例化

“无 new 实例化”通常是 API 设计层面的说法:调用者不需要写 new,但函数内部仍可能使用 new 或其他构造机制。jQuery 就采用过类似的工厂入口:

function Shadow() {
  this.name = 'jack'
  this.age = 18
}

function myQuery() {
  return new Shadow()
}

const object1 = myQuery()
const object2 = new myQuery()

console.log(object1 instanceof Shadow) // true
console.log(object2 instanceof Shadow) // true
console.log(object1.name) // jack
console.log(object2.name) // jack

myQuery()new myQuery() 在这个例子中都得到 Shadow 实例,是因为 myQuery 显式返回了 new Shadow() 的对象。它们的调用语义并不完全相同:普通调用不创建 myQuery 实例,构造调用会先创建临时实例,然后被显式返回对象覆盖。

现代项目中也常见以下几种不要求 new 的方案:

  • 工厂函数:直接返回对象或 class 实例。
  • 静态 create() 方法:把创建过程命名出来。
  • 模块函数:返回带有私有闭包状态的 API。
  • class:明确要求 new,让错误在调用时尽早暴露。

jQuery 的原型转接思路

jQuery 1.x 的入口大致是下面的结构(为便于学习进行了删减):

const $ = function (selector, context) {
  return new $.fn.init(selector, context)
}

$.fn = $.prototype = {
  constructor: $,
  init(selector, context) {
    this.selector = selector
    this.context = context
  },
  text() {
    return this.selector
  }
}

// 取出 init 函数并挂回 fn,模拟 jQuery.fn.init 的结构。
$.fn.init = function (selector, context) {
  this.selector = selector
  this.context = context
}

// 关键:让 new $.fn.init() 创建的对象也能访问 $.prototype 上的方法。
$.fn.init.prototype = $.fn

const value = $('<div>123</div>')
console.log(value instanceof $) // true
console.log(value instanceof $.fn.init) // true
console.log(value.text()) // <div>123</div>

原文的核心代码是:

jQuery = function (selector, context) {
  return new jQuery.fn.init(selector, context)
}

jQuery.fn = jQuery.prototype = {
  // 原型方法和其他成员
}

jQuery.fn.init = function (selector, context) {
  // 创建实例的具体逻辑
}

jQuery.fn.init.prototype = jQuery.fn

jQuery.fnjQuery.prototype 的别名,主要为了书写简洁。真正关键的是 init.prototype = jQuery.fn:这样 new init() 产生的对象的原型就是 jQuery.prototype,从而可以使用 jQuery 的原型方法,并同时满足 instanceof jQuery 的判断。

原文配图 132-01

这种设计可以抽象成一个最小模块:

function myModule(params) {
  return new myModule.fn.init(params)
}

myModule.fn = myModule.prototype = {
  constructor: myModule,
  getValue() {
    return this.value
  }
}

myModule.fn.init = function (params) {
  this.value = params
}

myModule.fn.init.prototype = myModule.fn

const moduleValue = myModule('value')
console.log(moduleValue.getValue()) // value
console.log(moduleValue instanceof myModule) // true

instanceofnew.target

通过 instanceof 判断“是否使用 new”并不可靠,因为调用者可以把一个真实实例传给普通函数:

function Student(name) {
  if (this instanceof Student) {
    this.name = name
    return
  }

  throw new Error('必须通过 new 关键字来调用 Student')
}

const student = new Student('若')
// student 已经是 Student 实例,下面的普通调用会绕过这个检查。
const result = Student.call(student, '川')
console.log(result) // undefined
console.log(student.name) // 川

new.target 可以区分普通调用与构造调用:

function Student2(name) {
  if (new.target !== undefined) {
    this.name = name
    return
  }

  throw new Error('必须通过 new 关键字来调用 Student2')
}

const student2 = new Student2('若')
console.log(student2.name) // 若

try {
  Student2.call(student2, '川')
} catch (error) {
  console.log(error.message) // 必须通过 new 关键字来调用 Student2
}

new.target 在直接普通调用时是 undefined,在 new 调用中是构造目标。它比 instanceof 更适合检查调用方式,但在 Reflect.construct 显式指定 newTarget 的元编程场景中,仍应按具体设计理解。

现代实践

  • 能用工厂函数表达的创建过程,不必为了形式强行使用构造函数。
  • class 的构造器不能脱离 new 调用;需要“可调用也可构造”时,显式提供工厂入口。
  • 读取原型使用 Object.getPrototypeOf,创建继承对象使用 Object.create,不要把 __proto__ 赋值当作通用方案。
  • 手写 new 适合面试和理解规范,生产代码优先使用 newReflect.construct

原文配图 132-02

参考资料

  1. MDN:new 运算符
  2. MDN:Reflect.construct()
  3. MDN:new.target
  4. jQuery 1.12.4 源码

作者:程序员白彬

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

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

457 DOCUMENTS · 10 COLLECTIONS
ARCHIVE SEARCH457 篇文章

SEARCH GUIDE

输入关键词开始搜索

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

按分类浏览

10 COLLECTIONS