普通视图

发现新文章,点击刷新页面。
今天 — 2026年2月23日首页

前端构建工具:从Rollup到Vite

作者 wuhen_n
2026年2月23日 07:35

在 Vue.js 源码中,pnpm run build reactivity 这个命令背后究竟发生了什么?为什么 Vue3 选择 Rollup 作为构建工具?ViteRollup 又是什么关系?本文将深入理解 Rollup 的核心配置,探索 Vue3 的构建体系,并理清 ViteRollup 的渊源。

Rollup 基础配置解析

什么是 Rollup?

Rollup 是一个 JavaScript 模块打包器,它可以将多个模块打包成一个单独的文件。与 Webpack 不同,Rollup 专注于 ES 模块的静态分析,以生成更小、更高效的代码。

Rollup 的核心优势

  • treeShaking:基于 ES 模块的静态分析,自动移除未使用的代码
  • 支持输出多种模块格式(ESM、CJS、UMD、IIFE)
  • 配置文件简洁直观,学习成本低
  • 插件体系完善,可以处理各种场景

核心配置:input 与 output

Rollup 的配置文件通常是 rollup.config.js,它导出一个配置对象或数组:

input:入口文件配置

// rollup.config.js
export default {
    // 单入口(最常见)
    input: 'src/index.js',
    
    // 多入口(对象形式)
    input: {
        main: 'src/main.js',
        admin: 'src/admin.js',
        utils: 'src/utils.js'
    },
    
    // 多入口(数组形式)
    input: ['src/index.js', 'src/cli.js']
};

output:输出配置

output 配置决定了打包产物的形式和位置:

export default {
    input: 'src/index.js',
    
    // 单输出配置
    output: {
        file: 'dist/bundle.js',      // 输出文件
        format: 'esm',                // 输出格式
        name: 'MyLibrary',            // UMD/IIFE 模式下的全局变量名
        sourcemap: true,               // 生成 sourcemap
        banner: '/*! MyLibrary v1.0.0 */' // 文件头注释
    },
    
    // 多输出配置(数组形式,输出多种格式)
    output: [
        {
            file: 'dist/my-lib.cjs.js',
            format: 'cjs'              // CommonJS,适用于 Node.js
        },
        {
            file: 'dist/my-lib.esm.js',
            format: 'es'                // ES Module,适用于现代浏览器/打包工具
        },
        {
            file: 'dist/my-lib.umd.js',
            format: 'umd',              // UMD,适用于所有场景
            name: 'MyLibrary'
        },
        {
            file: 'dist/my-lib.iife.js',
            format: 'iife',              // IIFE,直接用于浏览器 script 标签
            name: 'MyLibrary'
        }
    ]
};
输出格式详解
格式 全称 适用场景 特点
es / esm ES Module 现代浏览器、打包工具 保留 import/export,支持 Tree Shaking
cjs CommonJS Node.js 环境 使用 require/module.exports
umd Universal Module Definition 通用(浏览器、Node.js) 兼容 AMD、CommonJS 和全局变量
iife Immediately Invoked Function Expression 直接在浏览器用 script 脚本引入 自执行函数,避免全局污染
amd Asynchronous Module Definition RequireJS 等 异步模块加载

插件系统:扩展 Rollup 的能力

Rollup 的核心功能很精简,大多数能力需要通过插件来扩展。插件通过 plugins 数组配置,可以是单个插件实例或包含多个插件的数组:

import { nodeResolve } from '@rollup/plugin-node-resolve';
import commonjs from '@rollup/plugin-commonjs';
import { terser } from 'rollup-plugin-terser';
import json from '@rollup/plugin-json';
import replace from '@rollup/plugin-replace';
import babel from '@rollup/plugin-babel';

export default {
    input: 'src/index.js',
    output: {
        file: 'dist/bundle.js',
        format: 'umd',
        name: 'MyLibrary'
    },
    plugins: [
        // 解析 node_modules 中的第三方模块[citation:1]
        nodeResolve(),
        
        // 将 CommonJS 模块转换为 ES 模块[citation:10]
        commonjs(),
        
        // 支持导入 JSON 文件
        json(),
        
        // 替换代码中的字符串(常用于环境变量)
        replace({
            'process.env.NODE_ENV': JSON.stringify('production')
        }),
        
        // 使用 Babel 进行代码转换
        babel({
            babelHelpers: 'bundled',
            exclude: 'node_modules/**'
        }),
        
        // 压缩代码(生产环境)
        terser()
    ]
};

external:排除外部依赖

当构建一个库时,我们通常不希望将第三方依赖(如 React、Vue、lodash)打包进最终的产物,而是将其声明为外部依赖:

export default {
    input: 'src/index.js',
    output: {
        file: 'dist/my-lib.js',
        format: 'umd',
        name: 'MyLibrary',
        // 为 UMD 模式提供全局变量名映射
        globals: {
            'react': 'React',
            'react-dom': 'ReactDOM',
            'lodash': '_'
        }
    },
    // 排除外部依赖
    external: [
        'react',
        'react-dom',
        'lodash',
        // 也可以使用正则表达式
        /^lodash\//  // 排除 lodash 的所有子模块
    ]
};

Tree Shaking

Rollup 最令人津津乐道的就是其 Tree Shaking 功能,它通过静态分析移除未使用的代码,减小打包体积:

export default {
    input: 'src/index.js',
    output: {
        file: 'dist/bundle.js',
        format: 'esm'
    },
    treeshake: {
        // 模块级别的副作用分析
        moduleSideEffects: false,
        
        // 属性访问分析(更精确的 Tree Shaking)
        propertyReadSideEffects: false,
        
        // 尝试合并模块
        tryCatchDeoptimization: false,
        
        // 未知全局变量分析
        unknownGlobalSideEffects: false
    }
};

// 更简单的用法:直接使用布尔值
treeshake: true // 开启默认的摇树优化[citation:1]

watch:监听模式

在开发过程中,我们可以开启监听模式,当文件变化时自动重新打包:

export default {
    input: 'src/index.js',
    output: {
        file: 'dist/bundle.js',
        format: 'esm'
    },
    watch: {
        include: 'src/**',      // 监听的文件
        exclude: 'node_modules/**', // 排除的文件
        clearScreen: false        // 不清除屏幕
    }
};

// 或者在命令行中开启
// rollup -c --watch
// rollup -c -w (简写)

Vue3 使用的关键 Rollup 插件

Vue3 的源码采用 monorepo 管理,使用 Rollup 进行构建。让我们看看 Vue3 在构建过程中使用了哪些关键插件:

@rollup/plugin-node-resolve

作用:允许 Rollup 从 node_modules 中导入第三方模块。

// 为什么需要这个插件?
import { reactive } from '@vue/reactivity'; // 这个模块在 node_modules 中
// 没有插件时,Rollup 无法解析这个路径

// Vue3 中的使用
import nodeResolve from '@rollup/plugin-node-resolve';

export default {
    plugins: [
        nodeResolve({
            // 指定解析的模块类型
            mainFields: ['module', 'main'], // 优先使用 module 字段[citation:5]
            extensions: ['.js', '.json', '.ts'], // 支持的文件扩展名
            preferBuiltins: false // 不优先使用 Node 内置模块
        })
    ]
};

@rollup/plugin-commonjs

作用:将 CommonJS 模块转换为 ES 模块,使得 Rollup 可以处理那些尚未提供 ES 模块版本的依赖:

import commonjs from '@rollup/plugin-commonjs';

export default {
    plugins: [
        commonjs({
            // 指定哪些文件需要转换
            include: 'node_modules/**',
            
            // 扩展名
            extensions: ['.js', '.cjs'],
            
            // 忽略某些模块的转换
            ignore: ['conditional-runtime-dependency']
        })
    ]
};

@rollup/plugin-replace

作用:在打包时替换代码中的字符串,常用于注入环境变量或特性开关(Feature Flags):

// Vue3 中的特性开关示例[citation:2]
// packages/compiler-core/src/errors.ts
export function createCompilerError(code, loc, messages, additionalMessage) {
    // __DEV__ 在构建时被替换为 true 或 false
    if (__DEV__) {
        // 开发环境才执行的代码
    }
}

// rollup 配置
import replace from '@rollup/plugin-replace';

export default {
    plugins: [
        replace({
            // 防止被 JSON.stringify 转义
            preventAssignment: true,
            
            // 定义环境变量
            __DEV__: process.env.NODE_ENV !== 'production',
            __VERSION__: JSON.stringify('3.2.0'),
            
            // 特性开关
            __FEATURE_OPTIONS_API__: true,
            __FEATURE_PROD_DEVTOOLS__: false
        })
    ]
};

@rollup/plugin-json

作用:支持从 JSON 文件导入数据:

import json from '@rollup/plugin-json';

export default {
    plugins: [
        json({
            // 指定 JSON 文件的大小限制,超过限制则作为单独文件引入
            preferConst: true,
            indent: '  '
        })
    ]
};

// 使用时
import pkg from './package.json';
console.log(pkg.version);

rollup-plugin-terser

作用:压缩代码,减小生产环境的包体积:

import { terser } from 'rollup-plugin-terser';

export default {
    plugins: [
        // 只在生产环境使用
        process.env.NODE_ENV === 'production' && terser({
            compress: {
                drop_console: true,      // 移除 console
                drop_debugger: true,      // 移除 debugger
                pure_funcs: ['console.log'] // 移除特定的函数调用
            },
            output: {
                comments: false           // 移除注释
            }
        })
    ]
};

@rollup/plugin-babel

作用:使用 Babel 进行代码转换,处理语法兼容性问题:

import babel from '@rollup/plugin-babel';

export default {
    plugins: [
        babel({
            // 排除 node_modules
            exclude: 'node_modules/**',
            
            // 包含的文件
            include: ['src/**/*.js', 'src/**/*.ts'],
            
            // Babel helpers 的处理方式
            babelHelpers: 'bundled', // 或 'runtime'
            
            // 扩展名
            extensions: ['.js', '.jsx', '.ts', '.tsx']
        })
    ]
};

@rollup/plugin-typescript

作用:支持 TypeScript 编译:

import typescript from '@rollup/plugin-typescript';

export default {
    plugins: [
        typescript({
            tsconfig: './tsconfig.json',
            declaration: true,      // 生成 .d.ts 文件
            declarationDir: 'dist/types'
        })
    ]
};

如何构建指定包(以 pnpm run build reactivity 为例)

Vue3 采用 monorepo 管理多个包,使用 pnpm 作为包管理器。理解 pnpm run build reactivity 背后的机制,能帮助我们更好地理解现代构建流程:

项目结构

vue-next/
├── packages/               # 所有子包
│   ├── reactivity/         # 响应式系统
│   │   ├── src/
│   │   ├── package.json    # 包级配置
│   │   └── ...
│   ├── runtime-core/       # 运行时核心
│   ├── runtime-dom/        # 浏览器运行时
│   ├── compiler-core/      # 编译器核心
│   ├── vue/                # 完整版本
│   └── ...
├── package.json            # 根配置
├── pnpm-workspace.yaml     # pnpm 工作区配置
└── rollup.config.js        # Rollup 配置文件

pnpm-workspace.yaml 配置

# pnpm-workspace.yaml
packages:
  - 'packages/*'  # 声明 packages 下的所有目录都是工作区的一部分

这个配置告诉 pnpm:packages 目录下的每个子目录都是一个独立的包,它们之间可以互相引用而不需要发布到 npm。

根 package.json 的脚本配置

// 根目录 package.json
{
  "private": true,
  "scripts": {
    "build": "node scripts/build.js",                 // 构建所有包
    "build:reactivity": "pnpm run build reactivity",  // 只构建 reactivity 包
    "dev": "node scripts/dev.js",                      // 开发模式
    "test": "jest"                                      // 运行测试
  }
}

pnpm run 的底层原理

当我们在命令行执行 pnpm run build reactivity 时,背后发生了以下步骤:

  1. 解析命令:pnpm run build reactivity
  2. 读取根目录 package.json 中的 scripts
  3. 找到 "build": node scripts/build.js
  4. 将参数 "reactivity" 传递给脚本
  5. 在 PATH 环境变量中查找 node
  6. 执行 node scripts/build.js reactivity
  7. 脚本根据参数决定构建哪个包

build.js 脚本分析

Vue3 的构建脚本会解析命令行参数,决定构建哪些包:

// scripts/build.js (简化版)
const fs = require('fs');
const path = require('path');
const execa = require('execa');
const { targets: allTargets } = require('./utils');

// 获取命令行参数
const args = require('minimist')(process.argv.slice(2));
const targets = args._; // 获取到的参数数组

async function build() {
    // 如果没有指定目标,构建所有包
    if (!targets.length) {
        await buildAll(allTargets);
    } else {
        // 只构建指定的包
        await buildSelected(targets);
    }
}

async function buildSelected(targets) {
    for (const target of targets) {
        await buildPackage(target);
    }
}

async function buildPackage(packageName) {
    console.log(`开始构建: @vue/${packageName}`);
    
    // 切换到包目录
    const pkgDir = path.resolve(__dirname, '../packages', packageName);
    
    // 使用 rollup 构建该包
    await execa(
        'rollup',
        [
            '-c',                                      // 使用配置文件
            '--environment',                           // 设置环境变量
            `TARGET:${packageName}`,                   // 告诉 rollup 要构建哪个包
            '--watch'                                   // 开发模式时可能开启
        ],
        {
            stdio: 'inherit',                          // 继承输入输出
            cwd: pkgDir                                 // 在包目录执行
        }
    );
}

build();

Rollup 配置如何区分不同的包

// rollup.config.js (简化版)
import { createRequire } from 'module';
import path from 'path';
import fs from 'fs';

// 获取所有包
const packagesDir = path.resolve(__dirname, 'packages');
const packages = fs.readdirSync(packagesDir)
    .filter(f => fs.statSync(path.join(packagesDir, f)).isDirectory());

// 根据环境变量决定构建哪个包
const target = process.env.TARGET;

function createConfig(packageName) {
    const pkgDir = path.resolve(packagesDir, packageName);
    const pkg = require(path.join(pkgDir, 'package.json'));
    
    // 为每个包生成不同的配置
    return {
        input: path.resolve(pkgDir, 'src/index.ts'),
        output: [
            {
                file: path.resolve(pkgDir, pkg.main),
                format: 'cjs',
                sourcemap: true
            },
            {
                file: path.resolve(pkgDir, pkg.module),
                format: 'es',
                sourcemap: true
            }
        ],
        plugins: [
            // 共用插件
        ],
        external: [
            ...Object.keys(pkg.dependencies || {}),
            ...Object.keys(pkg.peerDependencies || {})
        ]
    };
}

// 如果指定了 target,只构建那个包
if (target) {
    module.exports = createConfig(target);
} else {
    // 否则构建所有包
    module.exports = packages.map(createConfig);
}

包级 package.json 的配置

每个包都有自己的 package.json,定义了该包的元信息和构建产物的入口:

// packages/reactivity/package.json
{
  "name": "@vue/reactivity",
  "version": "3.2.0",
  "main": "dist/reactivity.cjs.js",     // CommonJS 入口
  "module": "dist/reactivity.esm.js",    // ES Module 入口
  "unpkg": "dist/reactivity.global.js",  // 直接引入的 UMD 版本
  "types": "dist/reactivity.d.ts",       // TypeScript 类型定义
  "dependencies": {
    "@vue/shared": "3.2.0"
  }
}

Vite 与 Rollup 的关系

为什么需要 Vite?

虽然 Rollup 很优秀,但在开发大型应用时,它和 Webpack 一样面临着性能瓶颈:随着项目变大,启动开发服务器的时间越来越长。

Vite 的双引擎架构

Vite 在开发环境和生产环境使用不同的引擎:

  • 开发环境:利用浏览器原生 ES 模块 + esbuild 预构建
  • 生产环境:使用 Rollup 进行深度优化打包

开发环境:利用原生 ES 模块

<!-- Vite 开发服务器的原理 -->
<script type="module">
    // 浏览器直接请求模块,服务器实时编译返回
    import { createApp } from '/node_modules/.vite/vue.js'
    import App from '/src/App.vue'
    
    createApp(App).mount('#app')
</script>

esbuild 使用 Go 编写,比 JS 编写的打包器快 10-100 倍,可以预构建依赖,并转换 TypeScript/JSX。

生产环境:使用 Rollup 打包

Vite 在生产环境构建时,会使用 Rollup 进行打包。Vite 的插件系统也是与 Rollup 兼容的,这意味着绝大多数 Rollup 插件也可以在 Vite 中使用:

// vite.config.js
import { defineConfig } from 'vite';
import vue from '@vitejs/plugin-vue';
import { resolve } from 'path';

export default defineConfig({
    plugins: [
        vue()  // 这个插件同时支持开发环境和生产环境
    ],
    
    // 构建配置
    build: {
        // 底层是 Rollup 配置
        rollupOptions: {
            input: {
                main: resolve(__dirname, 'index.html'),
                nested: resolve(__dirname, 'nested/index.html')
            },
            output: {
                // 代码分割配置
                manualChunks: {
                    vendor: ['vue', 'vue-router']
                }
            }
        },
        
        // 输出目录
        outDir: 'dist',
        
        // 生成 sourcemap
        sourcemap: true,
        
        // 压缩配置
        minify: 'terser' // 或 'esbuild'
    }
});

Vite 与 Rollup 的配置对比

配置项 Rollup Vite
入口文件 input build.rollupOptions.input
输出目录 output.file / output.dir build.outDir
输出格式 output.format build.rollupOptions.output.format
外部依赖 external build.rollupOptions.external
插件 plugins plugins (同时支持 Vite 和 Rollup 插件)
开发服务器 无(需配合 rollup -w) 内置,支持 HMR

何时选择 Vite,何时选择 Rollup?

使用 Rollup

  • 开发 JavaScript/TypeScript 库
  • 需要精细控制打包过程
  • 项目不复杂,不需要开发服务器
  • 已有基于 Rollup 的构建流程

使用 Vite

  • 开发应用(Vue/React 项目)
  • 需要快速启动的开发服务器
  • 需要 HMR 热更新
  • 希望简化配置

两者结合

  • 库开发时使用 Rollup
  • 应用开发时使用 Vite
  • Vite 内部使用 Rollup 构建生产环境

总结

Rollup 的核心优势

  • 简洁性: 配置直观,学习成本低
  • TreeShaking: 基于ES模块的静态分析,产出代码极小
  • 多格式输出: 支持输出多种模块格式,适用于不同环境
  • 插件生态: 丰富的插件,可以处理各种场景
  • 源码可读性: 打包后的代码保持较好的可读性

Vite 的创新之处

  • 开发体验: 利用原生ES模块,实现极速启动和热更新
  • 双引擎架构: 开发用 esbuild,生产用 Rollup,各取所长
  • 配置简化: 内置常用配置,开箱即用
  • 插件兼容: 兼容 Rollup 插件生态

构建工具是现代前端开发的基石,深入理解它们不仅能帮助我们写出更高效的代码,还能在遇到问题时快速定位和解决。对于文章中错误的地方或者有任何问题,欢迎在评论区留言讨论!

昨天以前首页

JavaScript 防抖与节流进阶:从原理到实战

作者 wuhen_n
2026年2月16日 06:12

当用户疯狂点击按钮、疯狂滚动页面、疯狂输入搜索关键词时,应用还能流畅运行吗?防抖(Debounce)和节流(Throttle)是应对高频事件的终极武器。本文将从源码层面深入理解它们的差异,并实现一个支持立即执行、延迟执行、取消功能、记录参数的完整版本。

前言:高频事件带来的挑战

我们先来看一些简单的场景:

window.addEventListener('resize', () => {
    // 窗口大小改变时重新计算布局
    recalcLayout(); // 一秒可能触发几十次!
});

searchInput.addEventListener('input', () => {
    // 用户每输入一个字符就发起搜索请求
    fetchSearchResults(input.value); // 浪费大量请求!
});

window.addEventListener('scroll', () => {
    // 滚动时加载更多数据
    loadMoreData(); // 滚动一下触发几十次!
});

在这些场景中,当事件触发频率远高于我们需要的处理频率,就会出现卡顿、闪屏等现象,这就是防抖和节流要解决的核心问题。

理解防抖与节流的本质差异

核心概念对比

类型 防抖 节流
概念 将多次高频操作合并为一次,仅在最后一次操作后的延迟时间到达时执行 保证在单位时间内只执行一次,稀释执行频率
场景示例 电梯关门:等最后一个人进来后才关门,中间如果有人进来就重新计时 地铁安检:无论多少人排队,每秒钟只能通过一个人
执行次数 只执行最后一次 定期执行,不保证最后一次
频率 N次高频调用 → 1次执行 N次高频调用 → N/间隔时间次执行

适用场景对比

防抖场景

  • 搜索框输入(用户停止输入后才搜索)
  • 窗口大小调整(窗口调整完成后重新计算)
  • 表单验证(用户输完才验证)
  • 自动保存(停止编辑后保存)
  • 按钮防连点(避免重复提交)

节流场景

  • 滚动加载更多(滚动过程中定期检查)
  • 动画帧(控制动画执行频率)
  • 游戏循环(固定帧率)
  • 鼠标移动事件(实时位置但不过度频繁)
  • DOM元素拖拽(平滑移动)

防抖函数实现

基础防抖实现

function debounce(fn, delay) {
  let timer = null;

  return function (...args) {
    // 每次调用都清除之前的定时器
    if (timer) {
      clearTimeout(timer);
    }

    // 设置新的定时器
    timer = setTimeout(() => {
      fn.apply(this, args);
      timer = null;
    }, delay);
  };
}

支持立即执行的防抖

function debounceEnhanced(fn, delay, immediate = false) {
  let timer = null;
  let lastContext = null;
  let lastArgs = null;
  let lastResult = null;
  let callCount = 0;

  return function (...args) {
    lastContext = this;
    lastArgs = args;

    // 第一次调用且需要立即执行
    if (immediate && !timer) {
      lastResult = fn.apply(lastContext, lastArgs);
      callCount++;
      console.log(`立即执行 (调用 #${callCount})`);
    }

    // 清除之前的定时器
    if (timer) {
      clearTimeout(timer);
    }

    // 设置延迟执行
    timer = setTimeout(() => {
      // 如果不是立即执行模式,或者已经执行过立即执行
      if (!immediate) {
        lastResult = fn.apply(lastContext, lastArgs);
        callCount++;
        console.log(`延迟执行 (调用 #${callCount})`);
      }

      // 清理
      timer = null;
      lastContext = null;
      lastArgs = null;
    }, delay);

    return lastResult;
  };
}

完整版防抖(支持取消、取消、参数记录)

class DebouncedFunction {
  constructor(fn, delay, options = {}) {
    this.fn = fn;
    this.delay = delay;
    this.immediate = options.immediate || false;
    this.maxWait = options.maxWait || null;

    this.timer = null;
    this.lastArgs = null;
    this.lastContext = null;
    this.lastResult = null;
    this.lastCallTime = null;
    this.lastInvokeTime = null;

    // 参数历史记录
    this.history = [];
    this.maxHistory = options.maxHistory || 10;

    // 调用次数统计
    this.stats = {
      callCount: 0,
      invokedCount: 0,
      canceledCount: 0
    };
  }

  /**
   * 执行函数
   */
  _invoke() {
    const time = Date.now();
    this.stats.invokedCount++;
    this.lastInvokeTime = time;

    // 记录参数历史
    if (this.lastArgs) {
      this.history.push({
        args: [...this.lastArgs],
        timestamp: time,
        type: this.timer ? 'delayed' : 'immediate'
      });

      // 限制历史记录数量
      if (this.history.length > this.maxHistory) {
        this.history.shift();
      }
    }

    // 执行原函数
    this.lastResult = this.fn.apply(this.lastContext, this.lastArgs);

    // 清理
    this.lastArgs = null;
    this.lastContext = null;

    return this.lastResult;
  }

  /**
   * 调用防抖函数
   */
  call(...args) {
    const now = Date.now();
    this.stats.callCount++;
    this.lastArgs = args;
    this.lastContext = this;
    this.lastCallTime = now;

    // 立即执行模式处理
    if (this.immediate && !this.timer) {
      this._invoke();
    }

    // 清除现有定时器
    if (this.timer) {
      clearTimeout(this.timer);
    }

    // 最大等待时间处理
    if (this.maxWait && this.lastInvokeTime) {
      const timeSinceLastInvoke = now - this.lastInvokeTime;
      if (timeSinceLastInvoke >= this.maxWait) {
        this._invoke();
        return this.lastResult;
      }
    }

    // 设置新的定时器
    this.timer = setTimeout(() => {
      // 非立即执行模式,或者已经执行过立即执行
      if (!this.immediate) {
        this._invoke();
      }
      this.timer = null;
    }, this.delay);

    return this.lastResult;
  }

  /**
   * 取消当前待执行的防抖
   */
  cancel() {
    if (this.timer) {
      clearTimeout(this.timer);
      this.timer = null;
      this.stats.canceledCount++;
    }

    this.lastArgs = null;
    this.lastContext = null;
  }

  /**
   * 立即执行并取消后续
   */
  flush() {
    if (this.lastArgs) {
      this._invoke();
      this.cancel();
    }
    return this.lastResult;
  }

  /**
   * 判断是否有待执行的任务
   */
  pending() {
    return this.timer !== null;
  }

  /**
   * 获取调用历史
   */
  getHistory() {
    return [...this.history];
  }

  /**
   * 获取统计信息
   */
  getStats() {
    return { ...this.stats };
  }

  /**
   * 重置状态
   */
  reset() {
    this.cancel();
    this.history = [];
    this.stats = {
      callCount: 0,
      invokedCount: 0,
      canceledCount: 0
    };
    this.lastResult = null;
    this.lastCallTime = null;
    this.lastInvokeTime = null;
  }
}

节流函数实现

基础节流实现

function throttleTimer(fn, interval) {
  let timer = null;

  return function (...args) {
    if (!timer) {
      timer = setTimeout(() => {
        fn.apply(this, args);
        timer = null;
      }, interval);
    }
  };
}

完整版节流(支持首尾执行)

class ThrottledFunction {
  constructor(fn, interval, options = {}) {
    this.fn = fn;
    this.interval = interval;
    this.leading = options.leading !== false; // 是否立即执行
    this.trailing = options.trailing !== false; // 是否最后执行

    this.timer = null;
    this.lastArgs = null;
    this.lastContext = null;
    this.lastResult = null;
    this.lastInvokeTime = 0;

    // 参数历史
    this.history = [];
    this.maxHistory = options.maxHistory || 10;

    // 统计信息
    this.stats = {
      callCount: 0,
      invokedCount: 0,
      throttledCount: 0
    };
  }

  /**
   * 执行函数
   */
  _invoke() {
    const now = Date.now();
    this.lastInvokeTime = now;
    this.stats.invokedCount++;

    // 记录历史
    if (this.lastArgs) {
      this.history.push({
        args: [...this.lastArgs],
        timestamp: now,
        type: 'executed'
      });

      if (this.history.length > this.maxHistory) {
        this.history.shift();
      }
    }

    // 执行函数
    this.lastResult = this.fn.apply(this.lastContext, this.lastArgs);
    this.lastArgs = null;
    this.lastContext = null;
  }

  /**
   * 调用节流函数
   */
  call(...args) {
    const now = Date.now();
    this.stats.callCount++;
    this.lastArgs = args;
    this.lastContext = this;

    // 检查是否在节流期内
    const timeSinceLastInvoke = now - this.lastInvokeTime;
    const isThrottled = timeSinceLastInvoke < this.interval;

    if (isThrottled) {
      this.stats.throttledCount++;

      // 如果需要尾部执行
      if (this.trailing) {
        // 清除现有的尾部执行定时器
        if (this.timer) {
          clearTimeout(this.timer);
        }

        // 设置尾部执行定时器
        const remainingTime = this.interval - timeSinceLastInvoke;
        this.timer = setTimeout(() => {
          if (this.lastArgs) {
            this._invoke();
          }
          this.timer = null;
        }, remainingTime);
      }

      return this.lastResult;
    }

    // 不在节流期内
    if (this.leading) {
      // 头部执行
      this._invoke();
    } else if (this.trailing) {
      // 延迟执行
      if (this.timer) {
        clearTimeout(this.timer);
      }
      this.timer = setTimeout(() => {
        if (this.lastArgs) {
          this._invoke();
        }
        this.timer = null;
      }, this.interval);
    }

    return this.lastResult;
  }

  /**
   * 取消尾部执行
   */
  cancel() {
    if (this.timer) {
      clearTimeout(this.timer);
      this.timer = null;
    }
    this.lastArgs = null;
    this.lastContext = null;
  }

  /**
   * 立即执行并取消尾部执行
   */
  flush() {
    if (this.lastArgs) {
      this._invoke();
      this.cancel();
    }
    return this.lastResult;
  }

  /**
   * 判断是否有尾部待执行
   */
  pending() {
    return this.timer !== null;
  }

  /**
   * 获取历史记录
   */
  getHistory() {
    return [...this.history];
  }

  /**
   * 获取统计信息
   */
  getStats() {
    return { ...this.stats };
  }

  /**
   * 重置状态
   */
  reset() {
    this.cancel();
    this.history = [];
    this.stats = {
      callCount: 0,
      invokedCount: 0,
      throttledCount: 0
    };
    this.lastInvokeTime = 0;
    this.lastResult = null;
  }
}

进阶实现与组合优化

支持最大等待时间的防抖

支持最大等待时间的防抖,就是确保函数至少每隔 maxWait 时间执行一次:

function debounceMaxWait(fn, delay, maxWait) {
  let timer = null;
  let lastArgs = null;
  let lastContext = null;
  let lastInvokeTime = null;
  let maxTimer = null;

  const invoke = () => {
    lastInvokeTime = Date.now();
    fn.apply(lastContext, lastArgs);
    lastArgs = null;
    lastContext = null;
  };

  const startMaxWaitTimer = () => {
    if (maxTimer) clearTimeout(maxTimer);

    maxTimer = setTimeout(() => {
      if (lastArgs) {
        console.log('达到最大等待时间,强制执行');
        invoke();
      }
    }, maxWait);
  };

  return function (...args) {
    lastArgs = args;
    lastContext = this;

    // 清除现有延迟定时器
    if (timer) {
      clearTimeout(timer);
    }

    // 设置最大等待时间定时器
    if (maxWait && !lastInvokeTime) {
      startMaxWaitTimer();
    }

    // 设置新的延迟定时器
    timer = setTimeout(() => {
      invoke();
      timer = null;

      if (maxTimer) {
        clearTimeout(maxTimer);
        maxTimer = null;
      }
    }, delay);
  };
}

动态调整延迟时间的防抖

根据调用频率动态调整等待时间:

function debounceAdaptive(fn, baseDelay, options = {}) {
  const {
    minDelay = 100,
    maxDelay = 1000,
    factor = 0.8
  } = options;

  let timer = null;
  let lastArgs = null;
  let lastContext = null;
  let callTimes = [];
  let currentDelay = baseDelay;

  const calculateDelay = () => {
    // 计算最近1秒内的调用频率
    const now = Date.now();
    callTimes = callTimes.filter(t => now - t < 1000);
    const frequency = callTimes.length;

    // 根据频率调整延迟
    if (frequency > 10) {
      // 高频调用,增加延迟
      currentDelay = Math.min(currentDelay * (1 + frequency / 100), maxDelay);
    } else if (frequency < 2) {
      // 低频调用,减少延迟
      currentDelay = Math.max(currentDelay * factor, minDelay);
    }

    return currentDelay;
  };

  return function (...args) {
    callTimes.push(Date.now());
    lastArgs = args;
    lastContext = this;

    if (timer) {
      clearTimeout(timer);
    }

    const delay = calculateDelay();
    console.log(`  当前延迟: ${Math.round(delay)}ms (调用频率: ${callTimes.length}/秒)`);

    timer = setTimeout(() => {
      fn.apply(lastContext, lastArgs);
      timer = null;
    }, delay);
  };
}

实际应用场景实战

搜索框自动补全

class SearchAutoComplete {
  constructor(options = {}) {
    this.searchAPI = options.searchAPI || this.mockSearchAPI;
    this.minLength = options.minLength || 2;
    this.debounceDelay = options.debounceDelay || 300;
    this.maxResults = options.maxResults || 10;
    this.cacheResults = options.cacheResults !== false;

    // 搜索缓存
    this.cache = new Map();

    // 创建防抖搜索函数
    this.debouncedSearch = this.createDebouncedSearch();

    // 请求计数器
    this.requestCount = 0;
    this.cacheHitCount = 0;
  }

  /**
   * 模拟搜索API
   */
  async mockSearchAPI(query) {
    // 模拟网络延迟
    await new Promise(resolve => setTimeout(resolve, 200));

    // 模拟搜索结果
    const results = [];
    const prefixes = ['apple', 'banana', 'orange', 'grape', 'watermelon'];

    for (let i = 1; i <= 5; i++) {
      results.push({
        id: i,
        text: `${query} 结果 ${i}`,
        category: prefixes[i % prefixes.length]
      });
    }

    return results;
  }

  /**
   * 创建防抖搜索函数
   */
  createDebouncedSearch() {
    const searchFn = async (query) => {
      // 检查缓存
      if (this.cacheResults && this.cache.has(query)) {
        this.cacheHitCount++;
        return this.cache.get(query);
      }

      // 执行真实搜索
      this.requestCount++;
      console.log(`  🌐 [请求#${this.requestCount}] "${query}"`);

      try {
        const results = await this.searchAPI(query);

        // 存入缓存
        if (this.cacheResults) {
          this.cache.set(query, results);

          // 限制缓存大小
          if (this.cache.size > 50) {
            const oldestKey = this.cache.keys().next().value;
            this.cache.delete(oldestKey);
          }
        }

        return results;
      } catch (error) {
        console.error(`搜索失败: ${query}`, error);
        return [];
      }
    };

    // 使用完整版防抖
    return debounceComplete(searchFn, this.debounceDelay, {
      immediate: false,
      maxWait: 1000
    });
  }

  /**
   * 用户输入处理
   */
  onInput(query) {

    // 忽略空查询
    if (!query || query.length < this.minLength) {
      console.log('  查询太短,忽略');
      return Promise.resolve([]);
    }

    // 执行防抖搜索
    return this.debouncedSearch(query)
      .then(results => {
        const limited = results.slice(0, this.maxResults);
        console.log(`返回 ${limited.length} 条结果`);
        this.renderResults(limited);
        return limited;
      })
      .catch(error => {
        console.error('搜索失败:', error);
        return [];
      });
  }

  /**
   * 渲染搜索结果
   */
  renderResults(results) {
    // 实际项目中这里会更新DOM
    console.log(' 搜索结果:');
    results.slice(0, 3).forEach((result, i) => {
      console.log(`    ${i + 1}. ${result.text}`);
    });
    if (results.length > 3) {
      console.log(`... 等 ${results.length} 条`);
    }
  }

  /**
   * 清空缓存
   */
  clearCache() {
    this.cache.clear();
    this.cacheHitCount = 0;
    console.log('搜索缓存已清空');
  }

  /**
   * 获取统计信息
   */
  getStats() {
    return {
      requestCount: this.requestCount,
      cacheHitCount: this.cacheHitCount,
      cacheSize: this.cache.size,
      pending: this.debouncedSearch.pending(),
      debounceStats: this.debouncedSearch.getStats?.()
    };
  }
}

无限滚动加载

console.log('\n=== 无限滚动加载 ===\n');

class InfiniteScroll {
  constructor(options = {}) {
    this.loadMoreAPI = options.loadMoreAPI || this.mockLoadMoreAPI;
    this.throttleInterval = options.throttleInterval || 200;
    this.threshold = options.threshold || 200;
    this.pageSize = options.pageSize || 20;

    this.currentPage = 0;
    this.hasMore = true;
    this.isLoading = false;
    this.items = [];

    // 创建节流滚动处理函数
    this.throttledScroll = this.createThrottledScroll();

    // 记录最后一次滚动位置
    this.lastScrollPosition = 0;
    this.scrollHistory = [];
  }

  /**
   * 模拟加载更多数据
   */
  async mockLoadMoreAPI(page, pageSize) {
    // 模拟网络延迟
    await new Promise(resolve => setTimeout(resolve, 300));

    // 模拟数据
    const start = page * pageSize;
    const items = [];

    for (let i = 0; i < pageSize; i++) {
      items.push({
        id: start + i,
        title: `项目 ${start + i}`,
        content: `这是第 ${start + i} 个项目的内容`,
        timestamp: Date.now()
      });
    }

    // 模拟没有更多数据
    const hasMore = page < 10;

    return { items, hasMore };
  }

  /**
   * 创建节流滚动处理函数
   */
  createThrottledScroll() {
    const scrollHandler = async (scrollTop, clientHeight, scrollHeight) => {
      const distanceFromBottom = scrollHeight - scrollTop - clientHeight;

      console.log(`滚动位置: ${scrollTop}, 距离底部: ${distanceFromBottom}px`);

      // 记录滚动位置
      this.lastScrollPosition = scrollTop;
      this.scrollHistory.push({
        position: scrollTop,
        timestamp: Date.now()
      });

      // 限制历史记录大小
      if (this.scrollHistory.length > 20) {
        this.scrollHistory.shift();
      }

      // 检查是否需要加载更多
      if (distanceFromBottom < this.threshold) {
        await this.loadMore();
      }
    };

    return throttleComplete(scrollHandler, this.throttleInterval, {
      leading: true,
      trailing: true
    });
  }

  /**
   * 处理滚动事件
   */
  onScroll(event) {
    const target = event.target;
    const scrollTop = target.scrollTop || target.scrollingElement?.scrollTop || 0;
    const clientHeight = target.clientHeight || window.innerHeight;
    const scrollHeight = target.scrollHeight || document.documentElement.scrollHeight;

    this.throttledScroll(scrollTop, clientHeight, scrollHeight);
  }

  /**
   * 加载更多数据
   */
  async loadMore() {
    if (this.isLoading || !this.hasMore) {
      console.log(`${this.isLoading ? '正在加载中' : '没有更多数据'}`);
      return;
    }

    this.isLoading = true;
    this.currentPage++;

    console.log(`加载第 ${this.currentPage} 页数据...`);

    try {
      const result = await this.loadMoreAPI(this.currentPage, this.pageSize);

      this.hasMore = result.hasMore;
      this.items.push(...result.items);

      console.log(`加载完成,当前总条目: ${this.items.length}`);
      console.log(`还有更多: ${this.hasMore}`);

      this.renderItems(result.items);
    } catch (error) {
      console.error('加载失败:', error);
      this.currentPage--; // 回退页数
    } finally {
      this.isLoading = false;
    }
  }

  /**
   * 渲染新加载的项目
   */
  renderItems(newItems) {
    // 实际项目中这里会更新DOM
    console.log('新增项目:');
    newItems.slice(0, 3).forEach((item, i) => {
      console.log(`${item.id}. ${item.title}`);
    });
    if (newItems.length > 3) {
      console.log(`... 等 ${newItems.length} 条`);
    }
  }

  /**
   * 重置到顶部
   */
  reset() {
    this.currentPage = 0;
    this.hasMore = true;
    this.isLoading = false;
    this.items = [];
    this.scrollHistory = [];
    console.log('滚动列表已重置');
  }

  /**
   * 获取滚动统计
   */
  getScrollStats() {
    if (this.scrollHistory.length < 2) {
      return { avgSpeed: 0 };
    }

    const recent = this.scrollHistory.slice(-10);
    let totalSpeed = 0;

    for (let i = 1; i < recent.length; i++) {
      const distance = recent[i].position - recent[i - 1].position;
      const timeDiff = recent[i].timestamp - recent[i - 1].timestamp;
      const speed = distance / timeDiff; // px/ms
      totalSpeed += speed;
    }

    return {
      avgSpeed: totalSpeed / (recent.length - 1),
      scrollCount: this.scrollHistory.length,
      lastPosition: this.lastScrollPosition
    };
  }
}

最佳实践指南

防抖最佳实践

  • 默认延迟时间:300-500ms(用户输入)、200-300ms(窗口调整)、1000ms(自动保存)
  • 搜索框建议使用防抖,避免频繁请求
  • 表单验证使用防抖,用户输完再验证
  • 提交按钮使用防抖,防止重复提交
  • 需要立即反馈的操作设置 immediate: true

节流最佳实践

  • 滚动加载:200-300ms(平衡响应性和性能)
  • 拖拽事件:16-33ms(约30-60fps)
  • 窗口大小调整:100-200ms
  • 游戏循环:使用 requestAnimationFrame 替代定时器节流
  • 频繁的状态更新:考虑使用 requestAnimationFrame

内存管理实践

  • 组件卸载时取消未执行的防抖/节流
  • 避免在全局作用域创建过多的防抖/节流函数
  • 使用缓存时注意设置最大缓存大小
  • 定期清理过期的缓存数据

调试技巧

  • 添加日志追踪函数调用
  • 记录调用历史便于回溯问题
  • 使用 Stats 统计调用次数和节流情况
  • 开发环境设置更短的延迟时间便于测试

防抖节流选择决策树

是否需要处理高频事件?
        │
        ├─→ 是
        │   │
        │   ├─→ 是否需要关注最后一次执行?
        │   │   │
        │   │   ├─→ 是 → 使用防抖
        │   │   │   │
        │   │   │   ├─→ 搜索建议、自动保存、表单验证
        │   │   │   └─→ 窗口调整、拖拽结束
        │   │   │
        │   │   └─→ 否 → 使用节流
        │   │       │
        │   │       ├─→ 滚动加载、拖拽中、动画帧
        │   │       └─→ 游戏循环、鼠标移动
        │   │
        │   └─→ 是否需要立即执行?
        │       │
        │       ├─→ 是 → immediate: true
        │       │   │
        │       │   ├─→ 按钮提交(防止双击)
        │       │   └─→ 数据埋点
        │       │
        │       └─→ 否 → immediate: false
        │           │
        │           ├─→ 搜索建议(避免每个字符都请求)
        │           └─→ 自动保存(停止编辑后保存)
        │
        └─→ 否 → 不需要特殊处理

最终建议

  1. 不要盲目使用防抖/节流,先评估是否真的需要
  2. 根据用户体验选择合理的延迟时间
  3. 为防抖/节流函数命名时标明其特性
  4. 在类组件中绑定this时注意上下文
  5. 优先使用成熟的库实现(lodash、underscore)
  6. 理解原理,但不一定需要每次都自己实现
  7. 监控实际效果,根据数据持续优化

结语

防抖和节流是前端性能优化的基本工具,掌握它们不仅能提升应用性能,还能优化用户体验。对于文章中错误的地方或者有任何问题,欢迎在评论区留言讨论!

JavaScript 深拷贝的完全解决方案

作者 wuhen_n
2026年2月14日 05:24

当我们说"深拷贝"时,我们到底在说什么?为什么简单的 JSON.parse(JSON.stringify(obj)) 不够用?如何优雅地处理循环引用、特殊对象、函数引用?本文将深入深拷贝的每一个角落,构建一个真正"完全"的深拷贝函数。

前言:一个看似简单的问题

我们先来看一个最简单的深拷贝:

const obj = { name: '张三', age: 25 };
const copy = JSON.parse(JSON.stringify(obj));
console.log(copy); // { name: '张三', age: 25 }

但这真的够用吗?如果我们有一个复杂对象:

const complexObj = {
  date: new Date(),
  regex: /test/gi,
  func: () => console.log('hello'),
  undef: undefined,
  inf: Infinity,
  nan: NaN,
  map: new Map([['key', 'value']]),
  set: new Set([1, 2, 3]),
  symbol: Symbol('test'),
  error: new Error('错误')
};

这时候应该如何处理呢?这就是为什么我们需要一个真正完善的深拷贝解决方案。

深拷贝的基础概念

深拷贝 vs 浅拷贝

  • 深拷贝:完全独立的副本,修改拷贝对象的属性值,不会影响原对象
  • 浅拷贝:只复制一层,修改拷贝对象的属性值,会影响原对象

浅拷贝示例

const original = {
  name: '张三',
  address: {
    city: '北京',
    street: '长安街'
  }
};

// 浅拷贝示例
const shallowCopy1 = Object.assign({}, original);
const shallowCopy2 = { ...original };

shallowCopy1.address.city = '上海';
console.log('original.address.city:', original.address.city); // 上海
console.log('shallowCopy1.address.city:', shallowCopy1.address.city); // 上海

深拷贝示例

const original = {
  name: '张三',
  address: {
    city: '北京',
    street: '长安街'
  }
};
function simpleDeepCopy(obj) {
  return JSON.parse(JSON.stringify(obj));
}

const deepCopy = simpleDeepCopy(original);
deepCopy.address.city = '广州';
console.log('original.address.city:', original.address.city); // 北京
console.log('deepCopy.address.city:', deepCopy.address.city); // 广州

为什么要深拷贝?

  • 状态管理要求不可变数据
  • 撤销/重做功能需要保存历史快照
  • 复杂表单的草稿保存
  • 数据处理的隔离环境
  • 跨线程/跨进程通信

深拷贝的挑战

  • 循环引用:对象相互引用导致无限递归
  • 特殊对象:需要保留原型链和构造函数
  • 函数:通常不拷贝,但需要处理
  • Symbol:作为属性名和值的处理
  • 不可枚举属性:需要遍历所有属性描述符
  • 原型链:是否需要继承
  • 性能:大量数据的拷贝效率

JSON 方法的全面评估

JSON 序列化的局限性

  1. undefined 会被忽略
  2. symbol 会被忽略
  3. function 会被忽略
  4. 特殊数值会变成 null
  5. Date / RegExp / Error 等对象会转成字符串
  6. Map / Set / WeakMap / WeakSet 等会变成空对象
  7. TypedArray / ArrayBuffer 会变成对象
  8. 循环引用会导致错误

JSON 方法的适用场景

  • 纯数据对象(只有普通对象、数组、字符串、数字、布尔值)
  • 与后端 API 通信的数据交换
  • 简单的本地存储(localStorage)
  • 不需要保持原对象类型的临时副本
  • 数据结构已知且可控的内部模块

完整深拷贝要考虑的问题

1. 基础功能

  • 支持所有原始类型
  • 支持普通对象和数组
  • 处理循环引用
  • 处理原型链

2. 内置对象

  • Date:保持 Date 对象
  • RegExp:保持正则表达式
  • Map:保持键值对结构
  • Set:保持集合结构
  • Error:保留错误信息
  • Promise:处理状态
  • Symbol:作为值和属性名

3. 二进制数据

  • ArrayBuffer
  • TypedArray 所有类型
  • DataView
  • SharedArrayBuffer

4. 其他特性

  • 支持不可枚举属性
  • 支持属性 getter/setter
  • 支持冻结/密封对象
  • 支持自定义类实例
  • 性能优化

递归实现与循环引用检测

基础递归实现

function basicDeepCopy(source) {
  // 原始类型直接返回
  if (source === null || typeof source !== 'object') {
    return source;
  }

  // 数组或对象
  const target = Array.isArray(source) ? [] : {};

  // 递归复制每个属性
  for (let key in source) {
    if (source.hasOwnProperty(key)) {
      target[key] = basicDeepCopy(source[key]);
    }
  }

  return target;
}

循环引用检测

function deepCopyWithCycleDetection(source, cache = new WeakMap()) {
  // 处理原始类型
  if (source === null || typeof source !== 'object') {
    return source;
  }

  // 检测循环引用
  if (cache.has(source)) {
    console.log('检测到循环引用,返回已缓存的对象');
    return cache.get(source);
  }

  // 创建目标对象
  const target = Array.isArray(source) ? [] : {};

  // 缓存当前对象
  cache.set(source, target);

  // 递归复制属性
  for (let key in source) {
    if (source.hasOwnProperty(key)) {
      target[key] = deepCopyWithCycleDetection(source[key], cache);
    }
  }

  return target;
}

性能优化版本

function optimizedDeepCopy(source, cache = new Map()) {
  // 快速路径:原始类型
  if (source === null || typeof source !== 'object') {
    return source;
  }

  // 快速路径:Date
  if (source instanceof Date) {
    return new Date(source);
  }

  // 快速路径:RegExp
  if (source instanceof RegExp) {
    return new RegExp(source.source, source.flags);
  }

  // 循环引用检测
  if (cache.has(source)) {
    return cache.get(source);
  }

  // 根据类型创建目标对象
  let target;

  if (Array.isArray(source)) {
    target = [];
  } else if (source instanceof Map) {
    target = new Map();
  } else if (source instanceof Set) {
    target = new Set();
  } else if (source instanceof WeakMap || source instanceof WeakSet) {
    // WeakMap/WeakSet 无法遍历,返回新实例
    return new source.constructor();
  } else {
    // 普通对象:使用原对象的构造函数
    target = Object.create(Object.getPrototypeOf(source));
  }

  // 缓存当前对象
  cache.set(source, target);

  // 处理数组
  if (Array.isArray(source)) {
    for (let i = 0; i < source.length; i++) {
      target[i] = optimizedDeepCopy(source[i], cache);
    }
    return target;
  }

  // 处理 Map
  if (source instanceof Map) {
    for (let [key, value] of source) {
      target.set(
        optimizedDeepCopy(key, cache),
        optimizedDeepCopy(value, cache)
      );
    }
    return target;
  }

  // 处理 Set
  if (source instanceof Set) {
    for (let value of source) {
      target.add(optimizedDeepCopy(value, cache));
    }
    return target;
  }

  // 处理普通对象
  const keys = [...Object.keys(source), ...Object.getOwnPropertySymbols(source)];

  for (let key of keys) {
    const descriptor = Object.getOwnPropertyDescriptor(source, key);

    if (descriptor) {
      // 复制属性描述符
      Object.defineProperty(target, key, {
        ...descriptor,
        value: optimizedDeepCopy(descriptor.value, cache)
      });
    }
  }

  return target;
}

内置对象的深拷贝

class BuiltInCopier {
  // Date 对象
  static copyDate(date) {
    return new Date(date.getTime());
  }

  // RegExp 对象
  static copyRegExp(regexp) {
    const flags =
      (regexp.global ? 'g' : '') +
      (regexp.ignoreCase ? 'i' : '') +
      (regexp.multiline ? 'm' : '') +
      (regexp.dotAll ? 's' : '') +
      (regexp.unicode ? 'u' : '') +
      (regexp.sticky ? 'y' : '');

    return new RegExp(regexp.source, flags);
  }

  // Error 对象
  static copyError(error) {
    const copy = new error.constructor(error.message);
    copy.stack = error.stack;
    copy.name = error.name;
    return copy;
  }

  // Map 对象
  static copyMap(map, copyFn) {
    const result = new Map();
    map.forEach((value, key) => {
      result.set(copyFn(key), copyFn(value));
    });
    return result;
  }

  // Set 对象
  static copySet(set, copyFn) {
    const result = new Set();
    set.forEach(value => {
      result.add(copyFn(value));
    });
    return result;
  }

  // WeakMap 对象
  static copyWeakMap(weakMap) {
    // WeakMap 不可遍历,返回空实例
    return new WeakMap();
  }

  // WeakSet 对象
  static copyWeakSet(weakSet) {
    // WeakSet 不可遍历,返回空实例
    return new WeakSet();
  }

  // ArrayBuffer 对象
  static copyArrayBuffer(arrayBuffer) {
    const copy = arrayBuffer.slice(0);
    return copy;
  }

  // TypedArray 对象
  static copyTypedArray(typedArray) {
    return new typedArray.constructor(typedArray);
  }

  // DataView 对象
  static copyDataView(dataView) {
    return new DataView(
      this.copyArrayBuffer(dataView.buffer),
      dataView.byteOffset,
      dataView.byteLength
    );
  }

  // Promise 对象
  static copyPromise(promise) {
    // Promise 无法复制,返回新的 pending Promise
    return new Promise(() => { });
  }
}

处理自定义类和原型链

自定义类的深拷贝

function copyCustomClass(instance, cache = new WeakMap()) {
  if (cache.has(instance)) {
    return cache.get(instance);
  }

  // 获取构造函数
  const Constructor = instance.constructor;

  // 创建新实例
  let copy;

  try {
    // 尝试使用构造函数创建新实例
    copy = Object.create(Constructor.prototype);
    Constructor.apply(copy, []);
  } catch (error) {
    // 如果构造函数需要参数,则使用 Object.create
    copy = Object.create(Constructor.prototype);
  }

  cache.set(instance, copy);

  // 复制所有属性
  const allKeys = Reflect.ownKeys(instance);

  for (const key of allKeys) {
    const descriptor = Object.getOwnPropertyDescriptor(instance, key);

    if (descriptor) {
      if (descriptor.value !== undefined) {
        descriptor.value = comprehensiveDeepCopy(descriptor.value, cache);
      }
      Object.defineProperty(copy, key, descriptor);
    }
  }

  return copy;
}

原型链的完整处理

function deepCopyWithPrototype(source, cache = new WeakMap()) {
  if (source === null || typeof source !== 'object') {
    return source;
  }

  if (cache.has(source)) {
    return cache.get(source);
  }

  let target;

  // 获取完整的原型链
  const getPrototypeChain = (obj) => {
    const chain = [];
    let proto = Object.getPrototypeOf(obj);
    while (proto && proto !== Object.prototype) {
      chain.unshift(proto);
      proto = Object.getPrototypeOf(proto);
    }
    return chain;
  };

  // 重建原型链
  const buildPrototypeChain = (obj, chain) => {
    if (chain.length === 0) {
      return obj;
    }

    let current = obj;
    for (let i = 0; i < chain.length; i++) {
      const proto = chain[i];
      const protoCopy = Object.create(Object.getPrototypeOf(proto));

      // 复制原型上的属性
      const keys = Reflect.ownKeys(proto);
      for (const key of keys) {
        const descriptor = Object.getOwnPropertyDescriptor(proto, key);
        if (descriptor) {
          if (descriptor.value !== undefined) {
            descriptor.value = deepCopyWithPrototype(descriptor.value, cache);
          }
          Object.defineProperty(protoCopy, key, descriptor);
        }
      }

      Object.setPrototypeOf(current, protoCopy);
      current = protoCopy;
    }

    return obj;
  };

  // 处理不同类型
  if (source instanceof Date) {
    target = new Date(source);
  } else if (source instanceof RegExp) {
    target = new RegExp(source);
  } else if (Array.isArray(source)) {
    target = [];
  } else if (source instanceof Map) {
    target = new Map();
  } else if (source instanceof Set) {
    target = new Set();
  } else {
    // 普通对象:先创建空对象,再设置原型链
    target = {};
  }

  cache.set(source, target);

  // 获取并重建原型链
  const protoChain = getPrototypeChain(source);
  buildPrototypeChain(target, protoChain);

  // 复制自身属性
  const allKeys = Reflect.ownKeys(source);
  for (const key of allKeys) {
    const descriptor = Object.getOwnPropertyDescriptor(source, key);
    if (descriptor) {
      if (descriptor.value !== undefined) {
        descriptor.value = deepCopyWithPrototype(descriptor.value, cache);
      }
      Object.defineProperty(target, key, descriptor);
    }
  }

  return target;
}

最终完整解决方案


// 深拷贝配置选项
class DeepCopyOptions {
  constructor({
    copySymbols = true,
    copyNonEnumerables = true,
    preservePrototype = true,
    copyFunctions = false,
    copyWeakCollections = false,
    maxDepth = Infinity,
    onError = (error, key, value) => console.warn(`拷贝 ${key} 时出错:`, error)
  } = {}) {
    this.copySymbols = copySymbols;
    this.copyNonEnumerables = copyNonEnumerables;
    this.preservePrototype = preservePrototype;
    this.copyFunctions = copyFunctions;
    this.copyWeakCollections = copyWeakCollections;
    this.maxDepth = maxDepth;
    this.onError = onError;
  }
}

// 最终版深拷贝
function cloneDeep(source, options = new DeepCopyOptions(), depth = 0, cache = new WeakMap()) {
  // 深度限制
  if (depth >= options.maxDepth) {
    return source;
  }

  // 处理原始类型
  if (source === null || typeof source !== 'object') {
    // 处理函数(可选)
    if (typeof source === 'function' && options.copyFunctions) {
      // 简单的函数复制,不保证完全等价
      return new Function('return ' + source.toString())();
    }
    return source;
  }

  // 循环引用检测
  if (cache.has(source)) {
    return cache.get(source);
  }

  let target;

  try {
    // 根据类型创建目标对象
    const constructor = source.constructor;

    // 内置对象处理
    switch (constructor) {
      case Date:
        target = new Date(source);
        break;

      case RegExp:
        target = new RegExp(source.source, source.flags);
        target.lastIndex = source.lastIndex;
        break;

      case Error:
        target = new source.constructor(source.message);
        target.stack = source.stack;
        target.name = source.name;
        break;

      case Map:
        target = new Map();
        cache.set(source, target);
        source.forEach((value, key) => {
          target.set(
            cloneDeep(key, options, depth + 1, cache),
            cloneDeep(value, options, depth + 1, cache)
          );
        });
        return target;

      case Set:
        target = new Set();
        cache.set(source, target);
        source.forEach(value => {
          target.add(cloneDeep(value, options, depth + 1, cache));
        });
        return target;

      case WeakMap:
        target = options.copyWeakCollections ? new WeakMap() : source;
        cache.set(source, target);
        return target;

      case WeakSet:
        target = options.copyWeakCollections ? new WeakSet() : source;
        cache.set(source, target);
        return target;

      case ArrayBuffer:
        target = source.slice(0);
        break;

      case DataView:
        target = new DataView(
          cloneDeep(source.buffer, options, depth + 1, cache),
          source.byteOffset,
          source.byteLength
        );
        break;

      default:
        // 检查 TypedArray
        if (ArrayBuffer.isView(source) && !(source instanceof DataView)) {
          target = new constructor(
            cloneDeep(source.buffer, options, depth + 1, cache),
            source.byteOffset,
            source.length
          );
          break;
        }

        // 普通对象或数组
        if (constructor === Object || constructor === Array) {
          target = Array.isArray(source) ? [] : {};
        } else if (options.preservePrototype) {
          // 保持原型链
          target = Object.create(constructor.prototype);
        } else {
          target = {};
        }
        break;
    }
  } catch (error) {
    options.onError(error, 'constructor', source);
    target = Array.isArray(source) ? [] : {};
  }

  // 缓存当前对象
  cache.set(source, target);

  // 处理数组
  if (Array.isArray(source)) {
    for (let i = 0; i < source.length; i++) {
      try {
        target[i] = cloneDeep(source[i], options, depth + 1, cache);
      } catch (error) {
        options.onError(error, i, source[i]);
        target[i] = undefined;
      }
    }
    return target;
  }

  // 获取所有属性键
  const allKeys = [];

  if (options.copySymbols) {
    allKeys.push(...Object.getOwnPropertySymbols(source));
  }

  if (options.copyNonEnumerables) {
    allKeys.push(...Object.getOwnPropertyNames(source));
  } else {
    allKeys.push(...Object.keys(source));
  }

  // 复制属性
  for (const key of allKeys) {
    try {
      const descriptor = Object.getOwnPropertyDescriptor(source, key);

      if (descriptor) {
        if (descriptor.value !== undefined) {
          descriptor.value = cloneDeep(descriptor.value, options, depth + 1, cache);
        }
        Object.defineProperty(target, key, descriptor);
      }
    } catch (error) {
      options.onError(error, key, source[key]);
    }
  }

  return target;
}

// 便利函数
const deepClone = {
  // 快速克隆(适用于大多数场景)
  quick: (obj) => cloneDeep(obj),

  // 完整克隆(保留所有特性)
  full: (obj) => cloneDeep(obj, new DeepCopyOptions({
    copySymbols: true,
    copyNonEnumerables: true,
    preservePrototype: true,
    copyFunctions: false,
    copyWeakCollections: false
  })),

  // 严格克隆(尽可能完整)
  strict: (obj) => cloneDeep(obj, new DeepCopyOptions({
    copySymbols: true,
    copyNonEnumerables: true,
    preservePrototype: true,
    copyFunctions: true,
    copyWeakCollections: true
  })),

  // 数据克隆(只保留可序列化的数据)
  data: (obj) => cloneDeep(obj, new DeepCopyOptions({
    copySymbols: false,
    copyNonEnumerables: false,
    preservePrototype: false,
    copyFunctions: false,
    copyWeakCollections: false
  }))
};

结语

最好的深拷贝是不需要深拷贝。通过良好的架构设计、使用不可变数据、避免深层嵌套等方式,可以减少对深拷贝的需求。当必须使用时,选择合适的实现方案,既满足需求又不过度设计。对于文章中错误的地方或者有任何问题,欢迎在评论区留言讨论!

JavaScript 手写 new 操作符:深入理解对象创建

作者 wuhen_n
2026年2月13日 09:10

当我们使用 new 关键字时,背后到底发生了什么?这个看似简单的操作,实际上完成了一系列复杂的步骤。理解 new 的工作原理,是掌握 JavaScript 面向对象编程的关键。

前言:从 new 的神秘面纱说起

function Person(name, age) {
    this.name = name;
    this.age = age;
}

Person.prototype.greet = function() {
    return `你好,我是${this.name},今年${this.age}岁`;
};

const person = new Person('张三', 25);

上述代码中 new 到底创建了什么?为什么 this 指向了新对象?原型链是怎么建立的?如果构造函数有返回值会怎样?我们将通过本篇文章揭开 new 的神秘面纱,从零实现一个自己的 new 操作符。

理解 new

new 的四个核心步骤:

  1. 创建一个空对象
  2. 将对象的原型设置为构造函数的 prototype 属性
  3. 将构造函数的 this 绑定到新对象,并执行构造函数
  4. 判断返回值类型:如果构造函数返回一个对象(包括函数),则返回该对象;否则返回新创建的对象

手写实现 new 操作符

基础版本实现

function myNew(constructor, ...args) {

  // 1. 创建一个空对象
  const obj = {};

  // 2. 将对象的原型设置为构造函数的 prototype 属性
  Object.setPrototypeOf(obj, constructor.prototype);

  // 3. 将构造函数的 this 绑定到新对象,并执行构造函数
  const result = constructor.apply(obj, args);

  // 4. 判断返回值类型
  // 如果构造函数返回一个对象(包括函数),则返回该对象
  // 否则返回新创建的对象
  const isObject = result !== null && (typeof result === 'object' || typeof result === 'function');

  return isObject ? result : obj;
}

处理边界情况

function myNewEnhanced(constructor, ...args) {

  // 边界情况1:constructor 不是函数
  if (typeof constructor !== 'function') {
    throw new TypeError(`${constructor} is not a constructor`);
  }

  // 边界情况2:箭头函数(没有 prototype)
  if (!constructor.prototype) {
    throw new TypeError(`${constructor.name || constructor} is not a constructor`);
  }

  // 1. 创建新对象(改进方法):使用 Object.create 更优雅地设置原型
  const obj = Object.create(constructor.prototype);

  // 2. 调用构造函数
  let result;
  try {
    result = constructor.apply(obj, args);
  } catch (error) {
    // 如果构造函数抛出异常,直接传播
    throw error;
  }

  // 3. 处理返回值
  // 注意:null 也是 object 类型,但需要特殊处理
  const resultType = typeof result;
  const isObject = result !== null && (resultType === 'object' || resultType === 'function');

  return isObject ? result : obj;
}

完整实现与原型链优化

function myNewComplete(constructor, ...args) {
  // 1. 参数验证
  if (typeof constructor !== 'function') {
    throw new TypeError(`Constructor ${constructor} is not a function`);
  }

  // 2. 检查是否为可构造的函数:箭头函数和部分内置方法没有 prototype
  if (!constructor.prototype && !isNativeConstructor(constructor)) {
    throw new TypeError(`${getFunctionName(constructor)} is not a constructor`);
  }

  // 3. 创建新对象并设置原型链
  const proto = constructor.prototype || Object.prototype;
  const obj = Object.create(proto);

  // 4. 绑定 constructor 属性
  obj.constructor = constructor; 

  // 5. 执行构造函数
  const result = Reflect.construct(constructor, args, constructor);

  // 6. 处理返回值
  // Reflect.construct 已经处理了返回值逻辑
  // 但我们还是实现自己的逻辑以保持一致
  return processConstructorResult(result, obj, constructor);
}

// 辅助函数:检查是否为原生构造函数
function isNativeConstructor(fn) {
  // 一些内置构造函数如 Symbol、BigInt 没有 prototype
  const nativeConstructors = [
    'Number', 'String', 'Boolean', 'Symbol', 'BigInt',
    'Date', 'RegExp', 'Error', 'Array', 'Object', 'Function'
  ];

  return nativeConstructors.some(name =>
    fn.name === name || fn === globalThis[name]
  );
}

// 辅助函数:获取函数名
function getFunctionName(fn) {
  if (fn.name) return fn.name;
  const match = fn.toString().match(/^function\s*([^\s(]+)/);
  return match ? match[1] : 'anonymous';
}

// 辅助函数:处理构造函数返回值
function processConstructorResult(result, defaultObj, constructor) {
  // 如果 result 是 undefined 或 null,返回 defaultObj
  if (result == null) {
    return defaultObj;
  }

  // 检查 result 的类型
  const type = typeof result;

  // 如果是对象或函数,返回 result
  if (type === 'object' || type === 'function') {
    // 额外检查:如果 result 是构造函数本身的实例,确保原型链正确
    if (result instanceof constructor) {
      return result;
    }
    return result;
  }

  // 原始值,返回 defaultObj
  return defaultObj;
}

深入原型链与继承

原型链的建立过程

// 父构造函数
function Animal(name) {
  this.name = name;
}
// 父类方法
Animal.prototype.speak = function () {
  return `${this.name} 叫了`;
};
// 子构造函数
function Dog(name, breed) {
  Animal.call(this, name);
  this.breed = breed;
}
// 建立原型链
Dog.prototype = Object.create(Animal.prototype);
Dog.prototype.constructor = Dog;
// 子类方法
Dog.prototype.bark = function () {
  return `${this.name} 汪汪叫`;
};
// 创建实例
const myDog = new Dog('旺财', '金毛');
console.log(myDog.speak()); // 旺财 叫了
console.log(myDog.bark());  // 旺财 汪汪叫

ES6 类与 new 的关系

ES6 类的本质还是基于原型的语法糖:

ES6 基本写法

class Person {
  constructor(name, age) {
    this.name = name;
    this.age = age;
  }

  greet() {
    return `你好,我是${this.name}`;
  }
}
const person = new Person('张三', 30);

对应 ES5 的写法

function PersonES5(name, age) {
  // 类构造器中的代码
  if (!(this instanceof PersonES5)) {
    throw new TypeError("Class constructor Person cannot be invoked without 'new'");
  }

  this.name = name;
  this.age = age;
}

// 实例方法(添加到原型)
PersonES5.prototype.greet = function () {
  return `你好,我是${this.name}`;
};
const personES5 = new PersonES5('李四', 25);

类的重要特性

  1. 类必须用 new 调用
  2. 类方法不可枚举
  3. 类没有变量提升

ES6 实现继承的完整示例

class Animal {
  constructor(name) {
    this.name = name;
  }

  speak() {
    console.log(this.name + ' 叫了');
  }
}
class Dog extends Animal {
  constructor(name) {
    super(name);
  }

  speak() {
    console.log(this.name + ' 汪汪叫');
  }
}

ES6 继承的本质

ES6 通过 extends 关键字实现继承,就等价于 ES5 的寄生组合继承:

function AnimalES5(name) {
  this.name = name;
}

AnimalES5.prototype.speak = function () {
  console.log(this.name + ' 叫了');
};

function DogES5(name) {
  AnimalES5.call(this, name);
}

// 设置原型链
DogES5.prototype = Object.create(AnimalES5.prototype);
DogES5.prototype.constructor = DogES5;

DogES5.prototype.speak = function () {
  console.log(this.name + ' 汪汪叫');
};

特殊场景与高级应用

单例模式与 new

方法1:使用静态属性

class SingletonV1 {
  static instance = null;

  constructor(name) {
    if (SingletonV1.instance) {
      return SingletonV1.instance;
    }

    this.name = name;
    SingletonV1.instance = this;
  }

  static getInstance(name) {
    if (!this.instance) {
      this.instance = new SingletonV1(name);
    }
    return this.instance;
  }
}

方法2:使用闭包

const SingletonV2 = (function () {
  let instance = null;

  return class Singleton {
    constructor(name) {
      if (instance) {
        return instance;
      }

      this.name = name;
      instance = this;
    }
  };
})();

方法3:代理模式

function createSingletonProxy(Class) {
  let instance = null;

  return new Proxy(Class, {
    construct(target, args) {
      if (!instance) {
        instance = Reflect.construct(target, args);
      }
      return instance;
    }
  });
}

实现 Object.create 的 polyfill

if (typeof Object.create !== 'function') {
  Object.create = function (proto, propertiesObject) {
    // 参数验证
    if (typeof proto !== 'object' && typeof proto !== 'function') {
      throw new TypeError('Object prototype may only be an Object or null');
    }

    // 核心实现:使用空函数作为中间构造函数
    function F() { }
    F.prototype = proto;

    // 创建新对象,原型指向proto
    const obj = new F();

    // 处理第二个参数(属性描述符)
    if (propertiesObject !== undefined) {
      Object.defineProperties(obj, propertiesObject);
    }

    // 处理 null 原型
    if (proto === null) {
      obj.__proto__ = null;
    }
    // 返回新对象
    return obj;
  };
}

常见面试问题与解答

问题1:new 操作符做了什么?

  1. 创建一个新的空对象',
  2. 将这个空对象的原型设置为构造函数的 prototype 属性',
  3. 将构造函数的 this 绑定到这个新对象,并执行构造函数',
  4. 如果构造函数返回一个对象(包括函数),则返回该对象;否则返回新创建的对象

问题2:如果构造函数有返回值会怎样?

  • 返回对象(包括函数):忽略 this 绑定的对象,返回该对象
  • 返回原始值(number, string, boolean等):忽略返回值,返回 this 绑定的对象
  • 没有 return 语句:隐式返回 undefined,返回 this 绑定的对象

问题3:如何判断函数是否被 new 调用?

  • ES5:检查 this instanceof Constructor'
  • ES6+:使用 new.target(更准确)
  • 箭头函数:不能作为构造函数,没有 new.target

结语

通过深入理解 new 操作符的工作原理,我们不仅能在面试中脱颖而出,还能在实际开发中做出更明智的设计决策。对于文章中错误的地方或者有任何问题,欢迎在评论区留言讨论!

JavaScript 手写 call、apply、bind:深入理解函数上下文绑定

作者 wuhen_n
2026年2月13日 06:57

当面试官让我们手写 call、apply、bind 时,他们真正考察的是什么?这三个方法看似简单,却隐藏着 JavaScript 函数执行上下文、原型链、参数处理等核心概念。本文将从零实现,并深入理解它们的差异和应用场景。

前言:为什么需要 call、apply、bind?

const obj = {
  name: '张三',
  sayHello() {
    console.log(`你好,我是${this.name}`);
  }
};

const sayHelloFunc = obj.sayHello;
obj.sayHello();     // "你好,我是张三" - 正确
sayHelloFunc();     // "你好,我是undefined" - this丢失了!

上述代码,出现问题根源是:函数的 this 在调用时才确定,取决于调用方式。那如何解决呢?使用call、apply、bind 显式绑定 this 。

call 方法的实现

call 的基本使用

call 方法用于调用一个函数,并显式指定函数的 this 值和参数列表。

function greet(message) {
  console.log(`${message}, ${this.name}!`);
}

const person = { name: 'zhangsan' };

// 原生 call 的使用
greet.call(person, '你好'); // "你好, zhangsan!"

call 的工作原理

  1. 将函数设为对象的属性
  2. 使用该对象调用函数
  3. 删除该属性

基础版本实现

Function.prototype.myCall = function (context, ...args) {
  // 如果context是null或undefined,则绑定到全局对象
  if (context == null) {
    context = globalThis;
  }
  // 给context对象添加一个临时属性,值为当前函数
  const fnKey = Symbol('fn'); // 使用Symbol避免属性名冲突
  context[fnKey] = this; // this指向调用myCall的函数
  // 使用context对象调用函数
  const result = context[fnKey](...args);
  // 删除临时属性
  delete context[fnKey];
  return result;
};

处理边界情况

Function.prototype.myCallEnhanced = function (context, ...args) {
  // 处理undefined和null
  if (context == null) {
    context = globalThis;
  }

  // 原始值需要转换为对象,否则不能添加属性
  const contextType = typeof context;
  if (contextType === 'string' ||
    contextType === 'number' ||
    contextType === 'boolean' ||
    contextType === 'symbol' ||
    contextType === 'bigint') {
    context = Object(context); // 转换为包装对象
  }

  // 使用更安全的Symbol作为key
  const fnKey = Symbol('fn');
  context[fnKey] = this;

  try {
    const result = context[fnKey](...args);
    return result;
  } finally {
    // 确保总是删除临时属性
    delete context[fnKey];
  }
};

完整实现与性能优化

Function.prototype.myCallFinal = function (context = globalThis, ...args) {
  // 1. 类型检查:确保调用者是函数
  if (typeof this !== 'function') {
    throw new TypeError('Function.prototype.myCallFinal called on non-function');
  }

  // 2. 处理Symbol和BigInt(ES6+)
  const contextType = typeof context;
  let finalContext = context;

  // 3. 处理原始值(非严格模式下的自动装箱)
  if (contextType === 'string') {
    finalContext = new String(context);
  } else if (contextType === 'number') {
    finalContext = new Number(context);
  } else if (contextType === 'boolean') {
    finalContext = new Boolean(context);
  } else if (contextType === 'symbol') {
    // Symbol不能通过new创建,使用Object
    finalContext = Object(context);
  } else if (contextType === 'bigint') {
    // BigInt不能通过new创建,使用Object
    finalContext = Object(context);
  }
  // null和undefined已经通过默认参数处理

  // 4. 使用Symbol创建唯一key,避免属性冲突
  const fnSymbol = Symbol('callFn');

  // 5. 将函数绑定到上下文对象
  // 使用Object.defineProperty确保属性可配置
  Object.defineProperty(finalContext, fnSymbol, {
    value: this,
    configurable: true,
    writable: true,
    enumerable: false
  });

  // 6. 执行函数并获取结果
  let result;
  try {
    result = finalContext[fnSymbol](...args);
  } finally {
    // 7. 清理临时属性
    try {
      delete finalContext[fnSymbol];
    } catch (error) {
      // 如果上下文不可配置,忽略错误
      console.warn('无法删除临时属性:', error.message);
    }
  }

  return result;
};

apply 方法的实现

apply 的基本使用

apply 和 call 的功能基本相同,唯一的区别在于参数的传递方式:

  • call 接受参数列表
  • apply 接受参数数组
function sum(a, b, c) {
  return a + b + c;
}
// apply:参数以数组形式传递
sum.apply(null, [1, 2, 3]);

基础版本实现

Function.prototype.myCall = function (context, args) {
  // 如果context是null或undefined,则绑定到全局对象
  if (context == null) {
    context = globalThis;
  }
  // 给context对象添加一个临时属性,值为当前函数
  const fnKey = Symbol('fn'); // 使用Symbol避免属性名冲突
  context[fnKey] = this; // this指向调用myCall的函数
  // 使用context对象调用函数
  const result = context[fnKey](...args);
  // 删除临时属性
  delete context[fnKey];
  return result;
};

完整实现与性能优化

Function.prototype.myApply = function (context = globalThis, argsArray) {
  // 1. 类型检查
  if (typeof this !== 'function') {
    throw new TypeError('Function.prototype.myApply called on non-function');
  }

  // 2. 参数处理:确保argsArray是数组或类数组对象
  let args = [];
  if (argsArray != null) {
    // 检查是否为数组或类数组
    if (typeof argsArray !== 'object' ||
      (typeof argsArray.length !== 'number' && argsArray.length !== undefined)) {
      throw new TypeError('第二个参数必须是数组或类数组对象');
    }

    // 将类数组转换为真实数组
    if (!Array.isArray(argsArray)) {
      args = Array.from(argsArray);
    } else {
      args = argsArray;
    }
  }

  // 3. 使用Symbol作为唯一key
  const fnSymbol = Symbol('applyFn');

  // 4. 处理原始值(与call相同)
  const contextType = typeof context;
  let finalContext = context;

  if (contextType === 'string') {
    finalContext = new String(context);
  } else if (contextType === 'number') {
    finalContext = new Number(context);
  } else if (contextType === 'boolean') {
    finalContext = new Boolean(context);
  } else if (contextType === 'symbol') {
    finalContext = Object(context);
  } else if (contextType === 'bigint') {
    finalContext = Object(context);
  }

  // 5. 绑定函数到上下文
  Object.defineProperty(finalContext, fnSymbol, {
    value: this,
    configurable: true,
    writable: true,
    enumerable: false
  });

  // 6. 执行函数
  let result;
  try {
    result = finalContext[fnSymbol](...args);
  } finally {
    // 7. 清理
    try {
      delete finalContext[fnSymbol];
    } catch (error) {
      // 忽略删除错误
    }
  }

  return result;
};

bind 方法的实现

bind 的基本使用

bind 方法创建一个新的函数,当这个新函数被调用时,它的 this 值会被绑定到指定的对象,并且可以预先传入部分参数。

function greet(greeting, name) {
  console.log(`${greeting}, ${name}! 我是${this.role}`);
}

const context = { role: '管理员' };

// bind:创建新函数,稍后执行
const boundGreet = greet.bind(context, '你好');
boundGreet('李四'); 

bind 的核心特性:

  1. 返回一个新函数
  2. 可以预设参数(柯里化)
  3. 绑定this值
  4. 支持new操作符(特殊情况)

基础版本实现

Function.prototype.myBind = function (context, ...bindArgs) {
  const fn = this;
    return function(...newArgs) {
        return fn.apply(context, [...args, ...newArgs]);
    };
};

处理 new 操作符的特殊情况

Function.prototype.myBindEnhanced = function (context = globalThis, ...bindArgs) {
  const originalFunc = this;

  if (typeof originalFunc !== 'function') {
    throw new TypeError('Function.prototype.myBindEnhanced called on non-function');
  }

  // 内部函数,用于判断是否被new调用
  const boundFunc = function (...callArgs) {
    // 关键判断:this instanceof boundFunc
    // 如果使用new调用,this会是boundFunc的实例
    const isConstructorCall = this instanceof boundFunc;

    // 确定最终的上下文
    // 如果是构造函数调用,使用新创建的对象作为this
    // 否则使用绑定的context
    const finalContext = isConstructorCall ? this : Object(context);

    // 合并参数
    const finalArgs = bindArgs.concat(callArgs);

    // 执行原函数
    // 如果原函数有返回值,需要特殊处理
    const result = originalFunc.apply(finalContext, finalArgs);

    // 构造函数调用的特殊处理
    // 如果原函数返回一个对象,则使用该对象
    // 否则返回新创建的对象(this)
    if (isConstructorCall) {
      if (result && (typeof result === 'object' || typeof result === 'function')) {
        return result;
      }
      return this;
    }

    return result;
  };

  // 维护原型链
  // 方法1:直接设置prototype(有缺陷)
  // boundFunc.prototype = originalFunc.prototype;

  // 方法2:使用空函数中转(推荐)
  const F = function () { };
  F.prototype = originalFunc.prototype;
  boundFunc.prototype = new F();
  boundFunc.prototype.constructor = boundFunc;

  // 添加一些元信息(可选)
  boundFunc.originalFunc = originalFunc;
  boundFunc.bindContext = context;
  boundFunc.bindArgs = bindArgs;

  return boundFunc;
};

完整实现与性能优化

Function.prototype.myBindFinal = (function () {
  // 使用闭包保存Slice方法,提高性能
  const ArraySlice = Array.prototype.slice;

  // 空函数,用于原型链维护
  function EmptyFunction() { }

  return function myBindFinal(context = globalThis, ...bindArgs) {
    const originalFunc = this;

    // 严格的类型检查
    if (typeof originalFunc !== 'function') {
      throw new TypeError('Function.prototype.bind called on non-function');
    }

    // 处理原始值的上下文(非严格模式)
    let boundContext = context;
    const contextType = typeof boundContext;

    // 原始值包装(与call/apply保持一致)
    if (contextType === 'string') {
      boundContext = new String(boundContext);
    } else if (contextType === 'number') {
      boundContext = new Number(boundContext);
    } else if (contextType === 'boolean') {
      boundContext = new Boolean(boundContext);
    } else if (contextType === 'symbol') {
      boundContext = Object(boundContext);
    } else if (contextType === 'bigint') {
      boundContext = Object(boundContext);
    }

    // 创建绑定函数
    const boundFunction = function (...callArgs) {
      // 判断是否被new调用
      const isConstructorCall = this instanceof boundFunction;

      // 确定最终上下文
      let finalContext;
      if (isConstructorCall) {
        // new调用:忽略绑定的context,使用新实例
        finalContext = this;
      } else if (boundContext == null) {
        // 非严格模式:使用全局对象
        finalContext = globalThis;
      } else {
        // 普通调用:使用绑定的context
        finalContext = boundContext;
      }

      // 合并参数
      const allArgs = bindArgs.concat(callArgs);

      // 调用原函数
      const result = originalFunc.apply(finalContext, allArgs);

      // 处理构造函数调用的返回值
      if (isConstructorCall) {
        // 如果原函数返回对象,则使用该对象
        if (result && (typeof result === 'object' || typeof result === 'function')) {
          return result;
        }
        // 否则返回新创建的实例
        return this;
      }

      return result;
    };

    // 维护原型链 - 高性能版本
    // 避免直接修改boundFunction.prototype,使用中间函数
    if (originalFunc.prototype) {
      EmptyFunction.prototype = originalFunc.prototype;
      boundFunction.prototype = new EmptyFunction();
      // 恢复constructor属性
      boundFunction.prototype.constructor = boundFunction;
    } else {
      // 处理没有prototype的情况(如箭头函数)
      boundFunction.prototype = undefined;
    }

    // 添加不可枚举的原始函数引用(用于调试)
    Object.defineProperty(boundFunction, '__originalFunction__', {
      value: originalFunc,
      enumerable: false,
      configurable: true,
      writable: true
    });

    // 添加不可枚举的绑定信息
    Object.defineProperty(boundFunction, '__bindContext__', {
      value: boundContext,
      enumerable: false,
      configurable: true,
      writable: true
    });

    Object.defineProperty(boundFunction, '__bindArgs__', {
      value: bindArgs,
      enumerable: false,
      configurable: true,
      writable: true
    });

    // 设置适当的函数属性
    Object.defineProperty(boundFunction, 'length', {
      value: Math.max(0, originalFunc.length - bindArgs.length),
      enumerable: false,
      configurable: true,
      writable: false
    });

    Object.defineProperty(boundFunction, 'name', {
      value: `bound ${originalFunc.name || ''}`.trim(),
      enumerable: false,
      configurable: true,
      writable: false
    });

    return boundFunction;
  };
})();

面试常见问题与解答

问题1:手写call的核心步骤是什么?

  1. 步骤1: 将函数设为上下文对象的属性
  2. 步骤2: 执行该函数
  3. 步骤3: 删除该属性
  4. 步骤4: 返回函数执行结果
  5. 关键点:
    • 使用Symbol避免属性名冲突
    • 处理null/undefined上下文
    • 处理原始值上下文
    • 使用展开运算符处理参数

问题2:bind如何处理new操作符?

  1. 通过 this instanceof boundFunction 判断是否被new调用
  2. 如果是new调用,忽略绑定的上下文,使用新创建的对象作为this
  3. 需要正确设置boundFunction的原型链,以支持instanceof
  4. 如果原构造函数返回对象,则使用该对象,否则返回新实例

问题3:call、apply、bind的性能差异?

  1. call通常比apply快,因为apply需要处理数组参数
  2. bind创建新函数有开销,但多次调用时比重复call/apply高效

结语

通过深入理解call、apply、bind的实现原理,我们不仅能更好地回答面试问题,还能在实际开发中编写出更优雅、更高效的JavaScript代码。对于文章中错误的地方或者有任何问题,欢迎在评论区留言讨论!

❌
❌