原型链、this 指向闭包底层:手撕 bind/call/apply、深拷贝完整版

Hi,我是前端人类学

JavaScript 中有一道“传说级”面试题,几乎贯穿了从初级到高级的整个进阶路径:手写 call、apply、bind,并实现一个完整的深拷贝。这道题考察的绝不仅仅是 API 记忆,而是对原型链、this 绑定、闭包底层机制的综合理解。

本文不满足于“实现功能”,而是从底层原理出发,带你一步步理解“为什么这样写”,最终完成生产级别的函数实现。


文章目录

    • 一、原型链:一切函数和对象的“根”
    • 二、this 指向的本质:调用位置决定一切
    • 三、手撕 call:给函数加一个“临时属性”的巧妙思路
    • 四、手撕 apply:与 call 的唯一区别是参数形式
    • 五、手撕 bind:闭包 + 柯里化的经典结合
    • 六、深拷贝完整版:从“浅”到“深”的进阶之路
      • 6.1 基础版:JSON 方法(有缺陷)
      • 6.2 完整版:递归 + WeakMap 解决循环引用
      • 6.3 处理 Symbol 类型的 key
      • 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 绑定到指定对象,并立即执行该函数

实现思路非常巧妙——将函数作为目标对象的一个临时属性,调用完后删除:

Function.prototype.myCall=function(context,...args){// 1. 处理 context 为 null/undefined 时指向全局对象constctx=context===null||context===undefined?globalThis:Object(context);// 2. 用 Symbol 创建唯一 key,避免覆盖原对象属性constfnKey=Symbol('fn');// 3. 将当前函数(this)作为 ctx 的临时方法ctx[fnKey]=this;// 4. 执行函数,并传入参数constresult=ctx[fnKey](...args);// 5. 删除临时属性deletectx[fnKey];returnresult;};

关键点解析:

  • 使用Object(context)确保基本类型(如数字、字符串)被包装为对象。
  • Symbol作为属性名,避免与目标对象原有属性冲突。
  • this指向调用myCall的函数本身,这正是“借用”的体现。

四、手撕 apply:与 call 的唯一区别是参数形式

applycall功能完全一致,唯一区别是参数以数组(或类数组)形式传入

Function.prototype.myApply=function(context,argsArray){constctx=context===null||context===undefined?globalThis:Object(context);constfnKey=Symbol('fn');ctx[fnKey]=this;// 关键区别:判断参数是否为数组/类数组letresult;if(argsArray&&typeofargsArray==='object'&&Symbol.iteratorinObject(argsArray)){result=ctx[fnKey](...argsArray);}elseif(argsArray===undefined||argsArray===null){result=ctx[fnKey]();}else{thrownewTypeError('第二个参数必须是数组或类数组对象');}deletectx[fnKey];returnresult;};

这里使用了Symbol.iterator来判断是否为可迭代对象,既支持数组,也支持arguments等类数组。

五、手撕 bind:闭包 + 柯里化的经典结合

bind与前两者最大的不同是:它不会立即执行函数,而是返回一个新的函数,且新函数的 this 被永久绑定到指定对象。

实现 bind 需要结合闭包柯里化

Function.prototype.myBind=function(context,...boundArgs){// 保存原函数(this)constoriginalFn=this;// 返回一个新函数returnfunctionboundFn(...callArgs){// 关键:如果 boundFn 被 new 调用,则 this 指向实例,不绑定 contextif(new.target){returnneworiginalFn(...boundArgs,...callArgs);}// 普通调用:使用 context 作为 thisreturnoriginalFn.apply(context,[...boundArgs,...callArgs]);};};

底层原理解析:

  1. 闭包boundArgsoriginalFn被返回的函数所“记住”,形成了闭包。
  2. 柯里化bind时传入的部分参数(boundArgs)与调用时传入的参数(callArgs)合并执行。
  3. new 优先级:当boundFn作为构造函数被new调用时,this指向新创建的实例,而不是绑定的 context。这是bind实现中最容易被忽略的细节。

六、深拷贝完整版:从“浅”到“深”的进阶之路

深拷贝的实现是对 JavaScript 数据类型、引用、循环引用等底层特性的全面考察。

6.1 基础版:JSON 方法(有缺陷)

functiondeepClone(obj){returnJSON.parse(JSON.stringify(obj));}

缺陷:无法拷贝undefinedSymbol、函数、DateRegExp、循环引用等。

6.2 完整版:递归 + WeakMap 解决循环引用

functiondeepClone(obj,hash=newWeakMap()){// 基本类型或 null 直接返回if(obj===null||typeofobj!=='object'){returnobj;}// 处理循环引用if(hash.has(obj)){returnhash.get(obj);}// 处理日期if(objinstanceofDate){returnnewDate(obj);}// 处理正则if(objinstanceofRegExp){returnnewRegExp(obj.source,obj.flags);}// 处理数组if(Array.isArray(obj)){constarrCopy=[];hash.set(obj,arrCopy);for(leti=0;i<obj.length;i++){arrCopy[i]=deepClone(obj[i],hash);}returnarrCopy;}// 处理普通对象constclonedObj=Object.create(Object.getPrototypeOf(obj));hash.set(obj,clonedObj);// 拷贝所有自有属性(包括不可枚举属性?用 getOwnPropertyDescriptors)constdescriptors=Object.getOwnPropertyDescriptors(obj);for(constkeyindescriptors){constdescriptor=descriptors[key];if(descriptor.value!==undefined){descriptor.value=deepClone(descriptor.value,hash);}// 处理 getter/setter:直接赋值Object.defineProperty(clonedObj,key,descriptor);}returnclonedObj;}

6.3 处理 Symbol 类型的 key

Symbol 类型的 key 无法通过for...inObject.keys遍历,需要使用Reflect.ownKeys

functiondeepClone(obj,hash=newWeakMap()){// ... 前面的类型判断保持不变 ...constclonedObj=Object.create(Object.getPrototypeOf(obj));hash.set(obj,clonedObj);// 使用 Reflect.ownKeys 获取所有 key(包括 Symbol)constallKeys=Reflect.ownKeys(obj);for(constkeyofallKeys){constdescriptor=Object.getOwnPropertyDescriptor(obj,key);if(descriptor.value!==undefined){descriptor.value=deepClone(descriptor.value,hash);}Object.defineProperty(clonedObj,key,descriptor);}returnclonedObj;}

6.4 生产级优化:类型识别 + 可配置

对于生产环境,通常需要支持更多类型(MapSetErrorArrayBuffer等):

functiondeepClone(obj,hash=newWeakMap()){// 处理基本类型if(obj===null||typeofobj!=='object'){returnobj;}if(hash.has(obj))returnhash.get(obj);// 处理内置对象类型constconstructor=obj.constructor;consttype=Object.prototype.toString.call(obj);letclonedObj;switch(type){case'[object Date]':returnnewDate(obj);case'[object RegExp]':returnnewRegExp(obj.source,obj.flags);case'[object Map]':clonedObj=newMap();hash.set(obj,clonedObj);obj.forEach((value,key)=>{clonedObj.set(deepClone(key,hash),deepClone(value,hash));});returnclonedObj;case'[object Set]':clonedObj=newSet();hash.set(obj,clonedObj);obj.forEach(value=>{clonedObj.add(deepClone(value,hash));});returnclonedObj;case'[object Array]':clonedObj=[];break;case'[object Object]':clonedObj=Object.create(Object.getPrototypeOf(obj));break;default:// 其他类型(如 Error、ArrayBuffer)可扩展returnobj;}hash.set(obj,clonedObj);constallKeys=Reflect.ownKeys(obj);for(constkeyofallKeys){constdescriptor=Object.getOwnPropertyDescriptor(obj,key);if(descriptor.value!==undefined){descriptor.value=deepClone(descriptor.value,hash);}Object.defineProperty(clonedObj,key,descriptor);}returnclonedObj;}

七、总结:底层原理的融会贯通

通过手写这几个核心 API,我们实际上走通了一条 JavaScript 底层能力进阶路线:

实现核心依赖考察能力
myCall对象属性动态赋值、Symbolthis 指向、属性操作
myApply迭代器判断、参数展开类型判断、类数组处理
myBind闭包、柯里化、new.target作用域链、构造函数优先级
deepClone递归、WeakMap、Reflect.ownKeys引用类型、循环引用、数据完整性

这些手写实现不是为了“背诵代码”,而是为了让你理解 JavaScript 最核心的三个底层机制——原型链(函数借用)、作用域闭包(状态保持)、引用传递(深拷贝)。当你真正理解它们时,你会发现自己对 JavaScript 的掌控力上了一个全新的台阶。