JavaScript进阶:面试必考的手写new以及arguments

40 阅读4分钟

前言

这篇文章把你所有想知道的都讲透了:

  • new 到底干了啥?
  • 手写 new 的最优写法 + 历史写法
  • Object.create 深度解析
  • arguments 的前世今生 + 在手写 new 中的全部用法
  • 面试官最爱追问的 10+ 细节

读完即满级!


一、new 运算符到底干了哪四件事?(官方规范原文)

function Person(name, age) {
  this.name = name
  this.age = age
}
Person.prototype.sayHi = function () {
  console.log(`Hi, 我是${this.name}`)
}

const p = new Person('小明', 18)

new 会自动完成以下四步(记住顺序!):

  1. 创建一个新对象 {}

  2. 建立原型链:新对象.__proto __ = Person.prototype

  3. 绑定 this 并执行构造函数:Person.call(新对象, '小明', 18)

  4. 返回值处理:

    • 构造函数显式返回 object / function → 用它
    • 其他情况(包括 return null / 原始值 / 无 return)→ 返回第 1 步创建的对象

二、终极手写 new

function create(Constructor, ...args) {
  // 1. 创建对象 + 正确原型链(推荐!)
  const obj = Object.create(Constructor.prototype)

  // 2. 执行构造函数
  const result = Constructor.apply(obj, args)

  // 3. 返回值判断(最容易被追问!)
  return result != null && (typeof result === 'object' || typeof result === 'function')
         ? result : obj
}

三、返回值为什么这么复杂?三大坑一次讲透

return result != null && (typeof result === 'object' || typeof result === 'function') ? result : obj

核心结论先说:

构造函数如果显式返回一个“对象”(包括 function),就用这个返回值;否则才返回我们自己创建的那个 obj。

返回值类型typeof是否返回它原因说明
{} / [] / new Date"object"正常
function () {}"function"是(重点!)高阶工厂、Logger 类常用返回函数
null"object"历史遗留 bug,必须排除
123 / "hi" / true不是 object原始值被忽略

面试官:为什么不能简单写 typeof result === 'object'?

:因为 null 也是 "object",会误判;而且构造函数返回函数时也应该返回那个函数,但 typeof function 是 "function",所以要额外判断。

面试官:那如果返回一个原始值呢?比如 return 123

:原始值会被忽略,new 依然返回我们创建的对象,这是规范规定的。

面试官:能举个实际开发中会返回函数的例子吗?

(淡定输出): 经典案例(返回函数):

class Logger {
  constructor(prefix) {
    return (...msg) => console.log(`[${prefix}]`, ...msg)
  }
}
const log = new Logger('ERROR')
log('服务器炸了')   // [ERROR] 服务器炸了

四、Object.create() 完全解析(手写 new 的灵魂)

Object.create(proto, [propertiesObject]) 是 ES5 引入的最优雅的创建对象的方式,号称“最纯正的原型继承”。

它只有一句话作用:

以 proto 作为原型,创建一个全新的对象

一、Object.create 的两大参数详解

Object.create(proto, propertiesObject)
参数类型说明
protoObject / null新对象的原型,必须是对象或 null,否则报错
propertiesObjectObject可选,用 Object.defineProperties 的格式给新对象定义属性(数据描述符)

示例:

const obj = Object.create(
  { x: 100 },                     // 原型对象
  {
    y: {
      value: 200,
      writable: false,    // 不可改
      enumerable: true,   // 可遍历
      configurable: false // 不可删除/不可改特性
    },
    z: {
      get() { return this.y * 2 },
      enumerable: true
    }
  }
)

console.log(obj.x)    // 100(继承)
console.log(obj.y)    // 200(自身)
console.log(obj.z)    // 400(getter)
obj.y = 999           // 静默失败(writable: false)

二、最基本的用法(替代 new + 构造函数)

// 传统写法(要写构造函数)
function Person(name) {
  this.name = name
}
Person.prototype.say = function() { console.log('hi') }

const p1 = new Person('小明')

// 用 Object.create 写法(一句话搞定)
const p2 = Object.create(Person.prototype)
p2.name = '小红'
p2.say = Person.prototype.say   // 手动抄一遍,太麻烦

真正优雅的做法是配合 Object.assign 或直接定义属性:

const p3 = Object.create(Person.prototype, {
  name: { value: '小刚', writable: true, enumerable: true, configurable: true },
  age:  { value: 18, writable: true }
})

但实际开发中我们更常用“工厂函数 + Object.create”的组合(这就是 Vue 2 里 mixin 的底层原理)。

三、Object.create(null) —— 最纯粹的“干净对象”

这是面试高频题!

const a = {}                     // 原型是 Object.prototype,有 toString、valueOf...
const b = Object.create(null)    // 原型是 null,啥继承的都没有!

console.log(a.toString)    // 存在
console.log(b.toString)    // undefined → 报错!

// 常用来做纯数据字典,避免 key 冲突
const map = Object.create(null)
map.__proto__ = '不会被覆盖'   // 普通对象这里会被当成 "__proto__" 污染
map.toString = 123             // 普通对象这里也会污染

实际应用场景:

  • 实现 Map 前的“字符串 key 不会被意外污染”
  • 做高性能的字典/缓存对象(没有原型链查找开销)

手写简化版 Object.create(面试加分)

function myCreate(proto) {
  if (proto !== null && typeof proto !== 'object') throw new TypeError()
  function F() {}
  F.prototype = proto
  return new F()
}

五、类数组:arguments

5.1 arguments 是什么?

function test(a, b, c) {
  console.log(arguments)
  // [Arguments] { '0': 1, '1': 2, '2': 3 }
}
test(1, 2, 3)
  • 是函数每次调用时自动创建的类数组对象
  • 包含函数调用时传入的所有实参
  • 有 length 和数字索引
  • 不是 Array 实例,不能直接用 map/reduce
  • 在非严格模式下和形参双向绑定(ES5 以前)

5.2 类数组 → 真数组的 4 种经典方法(面试必考)

// 1. ES6 最优雅
[...arguments]

// 2. Array.from
Array.from(arguments)

// 3. 经典 slice
Array.prototype.slice.call(arguments)

// 4. for 循环(最快)
const arr = []
for (let i = 0; i < arguments.length; i++) arr.push(arguments[i])

将arguments转为真数组后,我们就可以用arguments调用数组相关的方法了

5.3用 arguments实现一个累加函数

function add () {
  return [...arguments].reduce((pre, cur) => pre + cur, 0)
}
console.log(add(1, 2, 3, 4, 5, 6))  //21

基于这一个例子,你大概就明白何为包含函数调用时传入的所有实参

5.4 arguments 在手写 new 中的两大核心用法

场景:ES5 时代没有 ...rest 参数,只能靠 arguments

// 经典面经版(纯 ES5)
function create() {
  // 方式1:shift 取出构造函数(会修改 arguments)
  const Constructor = Array.prototype.shift.call(arguments)

  // 方式2:更稳妥(推荐!不破坏原对象)
  // const Constructor = arguments[0]
  // const args = Array.prototype.slice.call(arguments, 1)

  const obj = Object.create(Constructor.prototype)
  const result = Constructor.apply(obj, arguments)  // apply 直接支持类数组!

  return result != null && (typeof result === 'object' || typeof result === 'function')
         ? result : obj
}

关键点:

  • apply 第二个参数可以直接传 arguments(类数组)→ 最省事!
  • shift 会修改 arguments,建议用 slice(1) 更安全

六、ES5 版 vs ES6 版完整对比

项目ES5 版(arguments)ES6 版(推荐)
参数接收function create()function create(Constructor, ...args)
取构造函数shift.call(arguments)直接第一个参数
传参给构造函数Constructor.apply(obj, arguments)Constructor.apply(obj, args)
可读性★☆☆☆☆★★★★★
面试推荐能写就加分首选!

七、面试官最爱追问的 10 个问题(直接抄答案)

  1. new 的执行过程? → 四步背熟
  2. 为什么返回值要判断 function? → 可返回高阶函数
  3. Object.create(null) 和 {} 区别? → 原型不同,前者最干净
  4. apply 为什么能直接传 arguments? → 它支持类数组
  5. 构造函数返回 null 怎么办? → 被忽略
  6. arguments 和普通对象有什么区别? → 有 length + 数字索引 + callee(已废弃)
  7. 怎么把 arguments 转真数组? → 上面 4 种方法任选
  8. shift.call(arguments) 有副作用吗? → 会修改原对象
  9. 能手写 Object.create 吗? → 上面代码直接甩
  10. new new Foo() 合法吗? → 合法,new 优先级极高

八、30 秒满分面试回答模板

“new 做了四件事:创建对象 → 用 Object.create 建立原型链 → apply 绑定 this 执行构造函数 → 返回值判断(返回 object/function,否则返回创建的对象)。 现代项目用 rest 参数最清晰,老项目会用 arguments + apply 传参。 我写一下最新版……”

然后淡定写出第一段推荐代码,面试官直接打满分。