面试题之手写new apply call bind
4 分钟阅读
手写 new / call / apply / bind
手写 new、call、apply、bind 是面试中出现频率最高的手写题之一。这四个 API 考察的是同一个核心知识点:JS 中 this 的绑定规则。理解了原理,实现只是水到渠成的事。本文按"原理 → 实现 → 边界情况"的顺序逐个实现。
1. call:显式绑定 this
call 的作用是:以指定的 this 和参数列表调用函数。
实现思路很直接——把函数临时挂载到目标对象上,以对象方法的形式调用,this 自然就指向了该对象,调用完再删掉:
javascript
Function.prototype.myCall = function(context, ...arg) {
if (typeof this !== 'function') {
// 排除 Function.prototype.myCall.call(sthNotAFunction) 这种情况
throw new TypeError('Error')
}
// 使用 Symbol 避免覆盖 context 上已有的同名属性
const symbolFn = Symbol('fn')
context[symbolFn] = this
const result = arg.length ? context[symbolFn](...arg) : context[symbolFn]()
delete context[symbolFn]
return result
}两个设计细节:
- 为什么用
Symbol做 key:如果直接用字符串 key(比如context.fn = this),当 context 上恰好已有同名属性时会被覆盖,delete时还会误删对象原本的属性和方法。Symbol的唯一性保证不会污染目标对象。 - 为什么要
delete:挂载是手段不是目的,调用完成后必须清理,否则会在目标对象上留下垃圾属性。
这里简化处理了 context 为基础类型或 null/undefined 的边界情况(原生 call 会将其包装为对象或替换为全局对象),理解原理即可。
2. apply:数组形式的 call
apply 与 call 的唯一区别是参数以数组形式传入。有了 myCall,实现只需一行:
javascript
Function.prototype.myApply = function(context, arg) {
// 把数组展开交给 myCall,不需要重复实现
return this.myCall(context, ...arg)
}3. bind:返回一个绑定了 this 的新函数
bind 不会立即执行,而是返回一个"预绑了 this 和部分参数"的新函数。它有一个容易被忽略的边界:返回的函数可能被当作构造函数使用。
javascript
Function.prototype.myBind = function(context, ...arg) {
if (typeof this !== 'function') {
throw new TypeError('Error')
}
const _this = this
return function F(...args) {
if (this instanceof F) {
// 被 new 调用时,bind 指定的 this 失效,
// 此时 this 指向新创建的实例,直接按构造函数处理
return new _this(...arg, ...args)
}
// 普通调用:拼接 bind 时的预设参数和调用时的新参数
return _this.call(context, ...arg, ...args)
}
}两个关键点:
this instanceof F判断:原生bind返回的函数被new调用时,预设的this会被忽略(这正是"new 绑定优先级高于显式绑定"的 this 绑定优先级规则)。实现里必须检测并处理这种情况,否则预设参数会丢。- 参数拼接用展开而不是 concat:网上很多实现用
arg.concat(args)再交给apply,当参数本身是数组时会被意外展开——[].concat([1, 2], [2, 3])得到[1, 2, 2, 3]。用展开运算符逐个传递参数不会有这个问题。
4. new:四步创建一个实例
new 运算符背后做了四件事:
- 创建一个新的空对象
- 将新对象的
__proto__链接到构造函数的prototype - 以新对象为
this执行构造函数 - 如果构造函数返回了一个对象,则返回它;否则返回新创建的对象
翻译成代码:
javascript
function myNew(constructor, ...arg) {
let obj = {}
// 链接原型,让实例能访问构造函数 prototype 上的方法
obj.__proto__ = constructor.prototype
// 绑定 this 并执行
let result = constructor.apply(obj, arg)
// 构造函数显式返回对象时以它为准,否则返回新对象
return result instanceof Object ? result : obj
}第 4 步是最容易被遗漏的:如果构造函数 return 了一个引用类型,new 表达式的结果会是那个对象而不是新实例——这也是"构造函数里一般不写 return"这条经验的原因。
5. 小结
四个 API 的实现都围绕 this 绑定展开:
| API | 核心机制 | 关键边界 |
|---|---|---|
call |
挂载为对象方法再调用 | 不污染目标对象(Symbol + delete) |
apply |
参数为数组的 call | 直接复用 call 展开即可 |
bind |
闭包保存 this 和预设参数 | 被 new 调用时 this 失效、参数正确拼接 |
new |
建对象、链原型、绑 this、处理返回值 | 构造函数显式返回对象的情况 |
掌握这几道题的价值不在于背出实现,而在于把 this 绑定规则、原型链、闭包这些基础知识串成一条线——面试官真正想考察的正是这个。
相关文章
前后篇
← NEWER浅谈LHS和RHSOLDER →用PM2做自动部署 评论