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

显示模式

登录
ARCHIVE DOCUMENTJS

一道面试题引发的思考:理解 new 运算符

所属馆藏
JavaScript
文件格式
Markdown
原始路径
JavaScript/75-一道面试题引发的思考:理解 new 运算符
本文目录7 个章节
  1. 一、原题:为什么不是 'Tom'
  2. 二、new 到底做了什么
  3. 三、原文模拟实现的改进版
  4. 四、prototype、[[Prototype]] 与 constructor
  5. 五、new.target 和构造函数设计
  6. 六、容易混淆的细节
  7. 总结

一道面试题引发的思考:理解 new 运算符

Category(分类): JavaScript Status: 已整理

本文保留原文从一道面试题理解 new 的思路,并按 ECMAScript 当前术语修正“构造函数一定返回实例”等容易误导的说法。原文配图已下载到同目录 images 文件夹,图中文字属于历史示意。

一、原题:为什么不是 'Tom'

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

const person = new Person('Tom')
console.log(person) // Person { name: 'Tom' }
console.log(person instanceof Person) // true

Person 明确返回了字符串 'Tom',但 new Person('Tom') 的结果仍然是新创建的实例。因为使用 new 调用构造函数时,只有对象或函数返回值会替代默认实例;原始值(包括字符串、数字、布尔值、undefinednullsymbol 等)会被忽略。

原文配图:new 运算符返回值规则

如果不使用 new,那就是一次普通函数调用。普通调用的 this 由调用形式和严格模式决定,返回值则直接由 return 决定:

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

const receiver = {}
const result = Person.call(receiver, 'Tom')

console.log(result) // 'Tom'
console.log(receiver.name) // 'Tom'

原文直接写 Person('Tom') 在非严格的浏览器经典脚本中还可能把 this 指向全局对象,在严格模式、ES 模块或 Node.js 模块中则会得到不同结果,甚至抛出异常。因此不要用“直接调用构造函数”的结果推断 new 的行为。

二、new 到底做了什么

对一个可构造的函数执行 new Constructor(arg1, arg2),可以用下面的近似过程理解:

  1. 创建一个新对象。
  2. 将新对象的 [[Prototype]] 设置为构造函数的 prototype 属性(如果该属性不是对象,则按构造函数所属 realm 的规则回退到对应的 %Object.prototype%;普通同 realm 示例中就是 Object.prototype)。
  3. 以新对象作为 this 调用构造函数,并传入参数。
  4. 如果构造函数返回对象或函数,就把该返回值作为 new 表达式的结果;否则返回第 1 步创建的对象。

这是一种便于学习的模型,规范中的正式过程由 [[Construct]]GetPrototypeFromConstructorOrdinaryCreateFromConstructor 等内部操作共同完成。

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

const person = new Person('Fe')

console.log(Object.getPrototypeOf(person) === Person.prototype) // true
console.log(person.name) // Fe
console.log(person.constructor === Person) // true(默认 prototype 未被替换时)

返回对象、原始值和 null

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

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

function ReturnObject(name) {
  this.name = name
  return { kind: 'replacement' }
}

function ReturnNull(name) {
  this.name = name
  return null
}

const p1 = new NoReturn('aa')
const p2 = new ReturnPrimitive('bb')
const p3 = new ReturnObject('cc')
const p4 = new ReturnNull('dd')

console.log(p1.name) // aa
console.log(p2.name) // bb:字符串返回值被忽略
console.log(p3) // { kind: 'replacement' }
console.log(p4.name) // dd:null 也不会替代默认实例

这里“对象”应理解为规范中的 object value,函数也属于对象的一种。return function () {} 同样会替代默认实例。

function ReturnFunction() {
  return function replacement() {}
}

const value = new ReturnFunction()
console.log(typeof value) // 'function'

三、原文模拟实现的改进版

原文中的实现使用了 _proto_argumentscons.apply,可以帮助理解早期 JavaScript 的写法,但存在几个问题:

  • 规范属性名是 [[Prototype]]__proto__ 只是 Web 兼容遗留访问器,不应作为首选 API。
  • typeof ret === 'object' 会把 null 错误地当成可返回对象。
  • 没有检查第一个参数是否可构造。
  • cons.prototype 不是对象时,不能直接把它交给原型设置操作。
  • apply 版本不能完整模拟原生 new 的所有内部行为。

现代 JavaScript 中,可以用 Reflect.apply 写一个更接近规范模型的教学实现:

function isObject(value) {
  return (typeof value === 'object' && value !== null) || typeof value === 'function'
}

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

  const prototype = isObject(Constructor.prototype)
    ? Constructor.prototype
    : Object.prototype
  const instance = Object.create(prototype)
  const result = Reflect.apply(Constructor, instance, args)

  return isObject(result) ? result : instance
}

function Person(name) {
  this.name = name
  return { replacement: true }
}

const person = newLike(Person, 'Fe')
console.log(person) // { replacement: true }

newLike 主要是教学模型,不是对引擎 [[Construct]] 的完整替代。它只检查了“可调用”,没有实现规范的 IsConstructor;因此传入箭头函数时会普通调用并错误地返回实例,而真实的 new Arrow() 必须抛出 TypeError。类构造器、Map/Set 等内置构造器、new.target、代理和跨 realm 构造器也不能用 Reflect.apply 完整模拟。

const Arrow = () => {}

try {
  new Arrow()
} catch (error) {
  console.log(error instanceof TypeError) // true
}

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

console.log(new User('Ada').name) // Ada

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

四、prototype[[Prototype]]constructor

构造函数的 prototype 是一个普通属性。用 new 创建对象时,构造函数的 prototype 如果是对象,新对象通常会以它为原型;对象自身的内部 [[Prototype]] 可以通过 Object.getPrototypeOf 观察:

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

const person = new Person('Ada')

console.log(Person.prototype === Object.getPrototypeOf(person)) // true
console.log(Person.prototype.constructor === Person) // true

如果整个 prototype 被替换,需要手动决定是否保留 constructor 信息;constructor 只是原型上的普通属性,不是引擎识别实例类型的可靠依据:

function Person() {}

Person.prototype = { kind: 'person' }
const person = new Person()

console.log(Object.getPrototypeOf(person) === Person.prototype) // true
console.log(person.constructor === Object) // true:constructor 从 Object.prototype 继承而来

Object.defineProperty(Person.prototype, 'constructor', {
  value: Person,
  writable: true,
  configurable: true,
  enumerable: false,
})

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

判断原型关系时,优先使用 instanceofObject.getPrototypeOf 或明确的品牌检查,而不要只依赖可被覆盖的 constructor 属性。

五、new.target 和构造函数设计

new.target 可以判断函数是否通过构造方式调用,也可用于阻止基类直接实例化:

function AbstractModel() {
  if (new.target === AbstractModel) {
    throw new TypeError('AbstractModel cannot be constructed directly')
  }
}

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

Object.setPrototypeOf(User.prototype, AbstractModel.prototype)

const user = new User('Ada')
console.log(user.name) // Ada

在实际项目中,优先使用 class、明确的工厂函数或普通对象组合,而不是为了模拟其他语言的类层次结构而堆叠多层构造函数。new 只解决“创建并初始化对象”的一种调用形式,不会自动解决状态共享、继承设计或资源释放问题。

六、容易混淆的细节

1. 箭头函数不能作为构造器

箭头函数没有自己的 prototype,也没有 [[Construct]],所以不能使用 new

2. 类构造器必须使用 new

类的构造器在没有 new 的情况下调用会抛出 TypeError。这与传统普通函数可以被直接调用不同。

3. new 的返回值不是“永远是实例”

如果构造函数返回一个对象或函数,返回值会替代实例。若构造函数返回原始值或 null,才会保留默认实例。

4. prototype 不是实例的原型本身

Person.prototype 是构造函数的一个属性;实例的内部原型是 Object.getPrototypeOf(instance)。两者经常相等,但概念不同。

总结

原文面试题的答案不是 'Tom',而是 Person 实例,因为字符串是原始值,不能替代 new 创建的对象。记住下面的判断即可:

const result = new Constructor(...args)
// Constructor 返回对象或函数:result 是返回值
// Constructor 返回原始值、null 或没有返回:result 是新实例

参考资料:

作者:fe。原文链接:一道面试题引发的思考:理解 new 运算符

457 DOCUMENTS · 10 COLLECTIONS
ARCHIVE SEARCH457 篇文章

SEARCH GUIDE

输入关键词开始搜索

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

按分类浏览

10 COLLECTIONS