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

发布时间:2026/7/24 16:37:23
原型链、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 类型的 key6.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.myCallfunction(context,...args){// 1. 处理 context 为 null/undefined 时指向全局对象constctxcontextnull||contextundefined?globalThis:Object(context);// 2. 用 Symbol 创建唯一 key避免覆盖原对象属性constfnKeySymbol(fn);// 3. 将当前函数this作为 ctx 的临时方法ctx[fnKey]this;// 4. 执行函数并传入参数constresultctx[fnKey](...args);// 5. 删除临时属性deletectx[fnKey];returnresult;};关键点解析使用Object(context)确保基本类型如数字、字符串被包装为对象。用Symbol作为属性名避免与目标对象原有属性冲突。this指向调用myCall的函数本身这正是“借用”的体现。四、手撕 apply与 call 的唯一区别是参数形式apply与call功能完全一致唯一区别是参数以数组或类数组形式传入。Function.prototype.myApplyfunction(context,argsArray){constctxcontextnull||contextundefined?globalThis:Object(context);constfnKeySymbol(fn);ctx[fnKey]this;// 关键区别判断参数是否为数组/类数组letresult;if(argsArraytypeofargsArrayobjectSymbol.iteratorinObject(argsArray)){resultctx[fnKey](...argsArray);}elseif(argsArrayundefined||argsArraynull){resultctx[fnKey]();}else{thrownewTypeError(第二个参数必须是数组或类数组对象);}deletectx[fnKey];returnresult;};这里使用了Symbol.iterator来判断是否为可迭代对象既支持数组也支持arguments等类数组。五、手撕 bind闭包 柯里化的经典结合bind与前两者最大的不同是它不会立即执行函数而是返回一个新的函数且新函数的 this 被永久绑定到指定对象。实现 bind 需要结合闭包和柯里化Function.prototype.myBindfunction(context,...boundArgs){// 保存原函数thisconstoriginalFnthis;// 返回一个新函数returnfunctionboundFn(...callArgs){// 关键如果 boundFn 被 new 调用则 this 指向实例不绑定 contextif(new.target){returnneworiginalFn(...boundArgs,...callArgs);}// 普通调用使用 context 作为 thisreturnoriginalFn.apply(context,[...boundArgs,...callArgs]);};};底层原理解析闭包boundArgs和originalFn被返回的函数所“记住”形成了闭包。柯里化bind时传入的部分参数boundArgs与调用时传入的参数callArgs合并执行。new 优先级当boundFn作为构造函数被new调用时this指向新创建的实例而不是绑定的 context。这是bind实现中最容易被忽略的细节。六、深拷贝完整版从“浅”到“深”的进阶之路深拷贝的实现是对 JavaScript 数据类型、引用、循环引用等底层特性的全面考察。6.1 基础版JSON 方法有缺陷functiondeepClone(obj){returnJSON.parse(JSON.stringify(obj));}缺陷无法拷贝undefined、Symbol、函数、Date、RegExp、循环引用等。6.2 完整版递归 WeakMap 解决循环引用functiondeepClone(obj,hashnewWeakMap()){// 基本类型或 null 直接返回if(objnull||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(leti0;iobj.length;i){arrCopy[i]deepClone(obj[i],hash);}returnarrCopy;}// 处理普通对象constclonedObjObject.create(Object.getPrototypeOf(obj));hash.set(obj,clonedObj);// 拷贝所有自有属性包括不可枚举属性用 getOwnPropertyDescriptorsconstdescriptorsObject.getOwnPropertyDescriptors(obj);for(constkeyindescriptors){constdescriptordescriptors[key];if(descriptor.value!undefined){descriptor.valuedeepClone(descriptor.value,hash);}// 处理 getter/setter直接赋值Object.defineProperty(clonedObj,key,descriptor);}returnclonedObj;}6.3 处理 Symbol 类型的 keySymbol 类型的 key 无法通过for...in或Object.keys遍历需要使用Reflect.ownKeysfunctiondeepClone(obj,hashnewWeakMap()){// ... 前面的类型判断保持不变 ...constclonedObjObject.create(Object.getPrototypeOf(obj));hash.set(obj,clonedObj);// 使用 Reflect.ownKeys 获取所有 key包括 SymbolconstallKeysReflect.ownKeys(obj);for(constkeyofallKeys){constdescriptorObject.getOwnPropertyDescriptor(obj,key);if(descriptor.value!undefined){descriptor.valuedeepClone(descriptor.value,hash);}Object.defineProperty(clonedObj,key,descriptor);}returnclonedObj;}6.4 生产级优化类型识别 可配置对于生产环境通常需要支持更多类型Map、Set、Error、ArrayBuffer等functiondeepClone(obj,hashnewWeakMap()){// 处理基本类型if(objnull||typeofobj!object){returnobj;}if(hash.has(obj))returnhash.get(obj);// 处理内置对象类型constconstructorobj.constructor;consttypeObject.prototype.toString.call(obj);letclonedObj;switch(type){case[object Date]:returnnewDate(obj);case[object RegExp]:returnnewRegExp(obj.source,obj.flags);case[object Map]:clonedObjnewMap();hash.set(obj,clonedObj);obj.forEach((value,key){clonedObj.set(deepClone(key,hash),deepClone(value,hash));});returnclonedObj;case[object Set]:clonedObjnewSet();hash.set(obj,clonedObj);obj.forEach(value{clonedObj.add(deepClone(value,hash));});returnclonedObj;case[object Array]:clonedObj[];break;case[object Object]:clonedObjObject.create(Object.getPrototypeOf(obj));break;default:// 其他类型如 Error、ArrayBuffer可扩展returnobj;}hash.set(obj,clonedObj);constallKeysReflect.ownKeys(obj);for(constkeyofallKeys){constdescriptorObject.getOwnPropertyDescriptor(obj,key);if(descriptor.value!undefined){descriptor.valuedeepClone(descriptor.value,hash);}Object.defineProperty(clonedObj,key,descriptor);}returnclonedObj;}七、总结底层原理的融会贯通通过手写这几个核心 API我们实际上走通了一条 JavaScript 底层能力进阶路线实现核心依赖考察能力myCall对象属性动态赋值、Symbolthis 指向、属性操作myApply迭代器判断、参数展开类型判断、类数组处理myBind闭包、柯里化、new.target作用域链、构造函数优先级deepClone递归、WeakMap、Reflect.ownKeys引用类型、循环引用、数据完整性这些手写实现不是为了“背诵代码”而是为了让你理解 JavaScript 最核心的三个底层机制——原型链函数借用、作用域闭包状态保持、引用传递深拷贝。当你真正理解它们时你会发现自己对 JavaScript 的掌控力上了一个全新的台阶。