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

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 的唯一区别是参数形式

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

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]);
  };
};

底层原理解析:

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

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

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

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

javascript 复制代码
function deepClone(obj) {
  return JSON.parse(JSON.stringify(obj));
}

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

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...inObject.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 生产级优化:类型识别 + 可配置

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

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 的掌控力上了一个全新的台阶。

相关推荐
j7~1 个月前
【C++】STL--string类--拆析解剖string类的实现以及string类的底层详解(2)
开发语言·c++·浅拷贝·深拷贝·string类的实现·string拷贝构造·string赋值重载
一条泥憨鱼1 个月前
DTO、VO、PO、BO 到底该怎么区分?
java·数据库·状态模式·对象·印象笔记·对象类型
布兰妮甜2 个月前
前端AJAX封装回调this指向丢失:最简解决方案
this指向·ajax封装·js异步回调·this指向丢失·前端踩坑指南
布朗克1682 个月前
11 面向对象思想入门
java·对象
程序喵大人2 个月前
从内存/汇编角度看C与C++:指针、引用、对象的底层差异
c语言·汇编·c++·指针·引用·对象
Beginner x_u2 个月前
前端八股整理(手写 03)|防抖节流、深拷贝、函数柯里化
前端·深拷贝·防抖节流·手撕·函数柯里化
小贺儿开发2 个月前
Unity3D 编辑器对象锁定工具
unity·编辑器·编程·工具·对象·互动·拓展
A923A2 个月前
【javaScript 原型精讲】
javascript·原型·原型链
木斯佳4 个月前
前端八股文面经大全:快手前端一面 (2026-03-29)·面经深度解析
前端·宏任务·原型链·闭包