Hi,我是前端人类学 !
JavaScript 中有一道"传说级"面试题,几乎贯穿了从初级到高级的整个进阶路径:手写 call、apply、bind,并实现一个完整的深拷贝 。这道题考察的绝不仅仅是 API 记忆,而是对原型链、this 绑定、闭包底层机制的综合理解。
本文不满足于"实现功能",而是从底层原理出发,带你一步步理解"为什么这样写",最终完成生产级别的函数实现。
文章目录
-
- 一、原型链:一切函数和对象的"根"
- [二、this 指向的本质:调用位置决定一切](#二、this 指向的本质:调用位置决定一切)
- [三、手撕 call:给函数加一个"临时属性"的巧妙思路](#三、手撕 call:给函数加一个“临时属性”的巧妙思路)
- [四、手撕 apply:与 call 的唯一区别是参数形式](#四、手撕 apply:与 call 的唯一区别是参数形式)
- [五、手撕 bind:闭包 + 柯里化的经典结合](#五、手撕 bind:闭包 + 柯里化的经典结合)
- 六、深拷贝完整版:从"浅"到"深"的进阶之路
-
- [6.1 基础版:JSON 方法(有缺陷)](#6.1 基础版:JSON 方法(有缺陷))
- [6.2 完整版:递归 + WeakMap 解决循环引用](#6.2 完整版:递归 + WeakMap 解决循环引用)
- [6.3 处理 Symbol 类型的 key](#6.3 处理 Symbol 类型的 key)
- [6.4 生产级优化:类型识别 + 可配置](#6.4 生产级优化:类型识别 + 可配置)
- 七、总结:底层原理的融会贯通
一、原型链:一切函数和对象的"根"
在动手实现之前,必须先理解一个核心事实:JavaScript 中的函数也是对象 。每个函数都有一个 prototype 属性(指向原型对象),同时函数实例也有 __proto__(或 [[Prototype]])指向 Function.prototype。
当我们执行 fn.call() 时,实际上是在 Function.prototype 上查找 call 方法------因为 fn.__proto__ === Function.prototype。
理解这个链条的意义在于:我们手写的 call/apply/bind,本质上是给所有函数添加一个"临时方法",通过修改 this 指向来实现借用。
二、this 指向的本质:调用位置决定一切
this 的指向不是在定义时确定的,而是在函数被调用时根据调用方式动态绑定。核心规则如下:
| 调用方式 | this 指向 |
|---|---|
| 普通函数调用 | 全局对象(严格模式下为 undefined) |
| 对象方法调用 | 该对象 |
call/apply/bind |
显式指定的对象 |
| 箭头函数 | 外层作用域的 this(无法被 bind 改变) |
new 调用 |
新创建的实例对象 |
我们手写 call/apply/bind 的核心,就是模拟"显式指定 this"这个能力。
三、手撕 call:给函数加一个"临时属性"的巧妙思路
call 的核心功能是:将函数的 this 绑定到指定对象,并立即执行该函数。
实现思路非常巧妙------将函数作为目标对象的一个临时属性,调用完后删除:
javascript
Function.prototype.myCall = function(context, ...args) {
// 1. 处理 context 为 null/undefined 时指向全局对象
const ctx = context === null || context === undefined ? globalThis : Object(context);
// 2. 用 Symbol 创建唯一 key,避免覆盖原对象属性
const fnKey = Symbol('fn');
// 3. 将当前函数(this)作为 ctx 的临时方法
ctx[fnKey] = this;
// 4. 执行函数,并传入参数
const result = ctx[fnKey](...args);
// 5. 删除临时属性
delete ctx[fnKey];
return result;
};
关键点解析:
- 使用
Object(context)确保基本类型(如数字、字符串)被包装为对象。 - 用
Symbol作为属性名,避免与目标对象原有属性冲突。 this指向调用myCall的函数本身,这正是"借用"的体现。
四、手撕 apply:与 call 的唯一区别是参数形式
apply 与 call 功能完全一致,唯一区别是参数以数组(或类数组)形式传入。
javascript
Function.prototype.myApply = function(context, argsArray) {
const ctx = context === null || context === undefined ? globalThis : Object(context);
const fnKey = Symbol('fn');
ctx[fnKey] = this;
// 关键区别:判断参数是否为数组/类数组
let result;
if (argsArray && typeof argsArray === 'object' && Symbol.iterator in Object(argsArray)) {
result = ctx[fnKey](...argsArray);
} else if (argsArray === undefined || argsArray === null) {
result = ctx[fnKey]();
} else {
throw new TypeError('第二个参数必须是数组或类数组对象');
}
delete ctx[fnKey];
return result;
};
这里使用了 Symbol.iterator 来判断是否为可迭代对象,既支持数组,也支持 arguments 等类数组。
五、手撕 bind:闭包 + 柯里化的经典结合
bind 与前两者最大的不同是:它不会立即执行函数,而是返回一个新的函数,且新函数的 this 被永久绑定到指定对象。
实现 bind 需要结合闭包 和柯里化:
javascript
Function.prototype.myBind = function(context, ...boundArgs) {
// 保存原函数(this)
const originalFn = this;
// 返回一个新函数
return function boundFn(...callArgs) {
// 关键:如果 boundFn 被 new 调用,则 this 指向实例,不绑定 context
if (new.target) {
return new originalFn(...boundArgs, ...callArgs);
}
// 普通调用:使用 context 作为 this
return originalFn.apply(context, [...boundArgs, ...callArgs]);
};
};
底层原理解析:
- 闭包 :
boundArgs和originalFn被返回的函数所"记住",形成了闭包。 - 柯里化 :
bind时传入的部分参数(boundArgs)与调用时传入的参数(callArgs)合并执行。 - new 优先级 :当
boundFn作为构造函数被new调用时,this指向新创建的实例,而不是绑定的 context。这是bind实现中最容易被忽略的细节。
六、深拷贝完整版:从"浅"到"深"的进阶之路
深拷贝的实现是对 JavaScript 数据类型、引用、循环引用等底层特性的全面考察。
6.1 基础版:JSON 方法(有缺陷)
javascript
function deepClone(obj) {
return JSON.parse(JSON.stringify(obj));
}
缺陷: 无法拷贝 undefined、Symbol、函数、Date、RegExp、循环引用等。
6.2 完整版:递归 + WeakMap 解决循环引用
javascript
function deepClone(obj, hash = new WeakMap()) {
// 基本类型或 null 直接返回
if (obj === null || typeof obj !== 'object') {
return obj;
}
// 处理循环引用
if (hash.has(obj)) {
return hash.get(obj);
}
// 处理日期
if (obj instanceof Date) {
return new Date(obj);
}
// 处理正则
if (obj instanceof RegExp) {
return new RegExp(obj.source, obj.flags);
}
// 处理数组
if (Array.isArray(obj)) {
const arrCopy = [];
hash.set(obj, arrCopy);
for (let i = 0; i < obj.length; i++) {
arrCopy[i] = deepClone(obj[i], hash);
}
return arrCopy;
}
// 处理普通对象
const clonedObj = Object.create(Object.getPrototypeOf(obj));
hash.set(obj, clonedObj);
// 拷贝所有自有属性(包括不可枚举属性?用 getOwnPropertyDescriptors)
const descriptors = Object.getOwnPropertyDescriptors(obj);
for (const key in descriptors) {
const descriptor = descriptors[key];
if (descriptor.value !== undefined) {
descriptor.value = deepClone(descriptor.value, hash);
}
// 处理 getter/setter:直接赋值
Object.defineProperty(clonedObj, key, descriptor);
}
return clonedObj;
}
6.3 处理 Symbol 类型的 key
Symbol 类型的 key 无法通过 for...in 或 Object.keys 遍历,需要使用 Reflect.ownKeys:
javascript
function deepClone(obj, hash = new WeakMap()) {
// ... 前面的类型判断保持不变 ...
const clonedObj = Object.create(Object.getPrototypeOf(obj));
hash.set(obj, clonedObj);
// 使用 Reflect.ownKeys 获取所有 key(包括 Symbol)
const allKeys = Reflect.ownKeys(obj);
for (const key of allKeys) {
const descriptor = Object.getOwnPropertyDescriptor(obj, key);
if (descriptor.value !== undefined) {
descriptor.value = deepClone(descriptor.value, hash);
}
Object.defineProperty(clonedObj, key, descriptor);
}
return clonedObj;
}
6.4 生产级优化:类型识别 + 可配置
对于生产环境,通常需要支持更多类型(Map、Set、Error、ArrayBuffer 等):
javascript
function deepClone(obj, hash = new WeakMap()) {
// 处理基本类型
if (obj === null || typeof obj !== 'object') {
return obj;
}
if (hash.has(obj)) return hash.get(obj);
// 处理内置对象类型
const constructor = obj.constructor;
const type = Object.prototype.toString.call(obj);
let clonedObj;
switch (type) {
case '[object Date]':
return new Date(obj);
case '[object RegExp]':
return new RegExp(obj.source, obj.flags);
case '[object Map]':
clonedObj = new Map();
hash.set(obj, clonedObj);
obj.forEach((value, key) => {
clonedObj.set(deepClone(key, hash), deepClone(value, hash));
});
return clonedObj;
case '[object Set]':
clonedObj = new Set();
hash.set(obj, clonedObj);
obj.forEach(value => {
clonedObj.add(deepClone(value, hash));
});
return clonedObj;
case '[object Array]':
clonedObj = [];
break;
case '[object Object]':
clonedObj = Object.create(Object.getPrototypeOf(obj));
break;
default:
// 其他类型(如 Error、ArrayBuffer)可扩展
return obj;
}
hash.set(obj, clonedObj);
const allKeys = Reflect.ownKeys(obj);
for (const key of allKeys) {
const descriptor = Object.getOwnPropertyDescriptor(obj, key);
if (descriptor.value !== undefined) {
descriptor.value = deepClone(descriptor.value, hash);
}
Object.defineProperty(clonedObj, key, descriptor);
}
return clonedObj;
}
七、总结:底层原理的融会贯通
通过手写这几个核心 API,我们实际上走通了一条 JavaScript 底层能力进阶路线:
| 实现 | 核心依赖 | 考察能力 |
|---|---|---|
myCall |
对象属性动态赋值、Symbol | this 指向、属性操作 |
myApply |
迭代器判断、参数展开 | 类型判断、类数组处理 |
myBind |
闭包、柯里化、new.target |
作用域链、构造函数优先级 |
deepClone |
递归、WeakMap、Reflect.ownKeys |
引用类型、循环引用、数据完整性 |
这些手写实现不是为了"背诵代码",而是为了让你理解 JavaScript 最核心的三个底层机制------原型链(函数借用)、作用域闭包(状态保持)、引用传递(深拷贝)。当你真正理解它们时,你会发现自己对 JavaScript 的掌控力上了一个全新的台阶。