前言
这篇文章把你所有想知道的都讲透了:
- 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 会自动完成以下四步(记住顺序!):
-
创建一个新对象 {}
-
建立原型链:新对象.__proto __ = Person.prototype
-
绑定 this 并执行构造函数:Person.call(新对象, '小明', 18)
-
返回值处理:
- 构造函数显式返回 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)
| 参数 | 类型 | 说明 |
|---|---|---|
| proto | Object / null | 新对象的原型,必须是对象或 null,否则报错 |
| propertiesObject | Object | 可选,用 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 个问题(直接抄答案)
- new 的执行过程? → 四步背熟
- 为什么返回值要判断 function? → 可返回高阶函数
- Object.create(null) 和 {} 区别? → 原型不同,前者最干净
- apply 为什么能直接传 arguments? → 它支持类数组
- 构造函数返回 null 怎么办? → 被忽略
- arguments 和普通对象有什么区别? → 有 length + 数字索引 + callee(已废弃)
- 怎么把 arguments 转真数组? → 上面 4 种方法任选
- shift.call(arguments) 有副作用吗? → 会修改原对象
- 能手写 Object.create 吗? → 上面代码直接甩
- new new Foo() 合法吗? → 合法,new 优先级极高
八、30 秒满分面试回答模板
“new 做了四件事:创建对象 → 用 Object.create 建立原型链 → apply 绑定 this 执行构造函数 → 返回值判断(返回 object/function,否则返回创建的对象)。 现代项目用 rest 参数最清晰,老项目会用 arguments + apply 传参。 我写一下最新版……”
然后淡定写出第一段推荐代码,面试官直接打满分。