CC 咖啡猫的工作空间 Coding Space

JavaScript

语言基础

数据类型

7 种原始类型 + 1 种引用类型

// 原始类型(值传递,不可变)
undefined
null
boolean
number      // IEEE 754 双精度浮点(Number.MAX_SAFE_INTEGER = 2^53 - 1)
bigint      // BigInt(9007199254740991n)
string
symbol      // Symbol('description')

// 引用类型(引用传递,可变)
object      // Array, Function, Date, RegExp, Map, Set ...

typeof 与 instanceof

typeof undefined     // "undefined"
typeof null          // "object" (JS 历史遗留 bug)
typeof true          // "boolean"
typeof 42            // "number"
typeof 42n           // "bigint"
typeof 'hello'       // "string"
typeof Symbol()      // "symbol"
typeof {}            // "object"
typeof []            // "object"
typeof function(){}  // "function" (内置特殊处理)

// instanceof 检查原型链
[] instanceof Array       // true
[] instanceof Object      // true(Array.prototype 继承自 Object.prototype)

typeof null === 'object' 原因:JS 底层用 type tag(000: object),null 的表示为 0x00,恰好被识别为 object 类型。

类型转换

隐式转换规则表

运算 规则
+ 且一方为字符串 字符串拼接
+ 双方都不是字符串 转为 number
- * / % 全部转为 number
== 与 number 比较 另一侧转为 number
== 与 string 比较 另一侧转为 string
> < >= <= 都转为 number
if() && `

显式转换

String(123)            // "123"
Number('123')          // 123
Number('abc')          // NaN
Boolean(0)             // false
Boolean('')            // false
Boolean([])            // true(空数组是对象)
parseInt('10px', 10)   // 10
parseFloat('3.14em')   // 3.14

falsy 值(其他值都为 truthy):

false
0, -0, 0n
'' (空字符串)
null
undefined
NaN

== 的坑与 === 的推荐

// == 会进行类型转换
[] == false          // true([] → '' → 0, false → 0)
[] == 0              // true
' \t\r\n' == 0       // true(字符串转为 NaN?不,空串或空白转为 0)
null == undefined    // true

// 始终使用 === 和 !==(除了判断 null/undefined 时可用 == null)

声明提升与 TDZ

// var —— 声明提升到函数/全局作用域顶部,初始化为 undefined
console.log(a); // undefined(不会报错)
var a = 1;

// let / const —— 存在 TDZ(Temporal Dead Zone),提升但不初始化
console.log(b); // ReferenceError: Cannot access 'b' before initialization
let b = 2;

// function —— 声明+定义同时提升
foo(); // 正常运行
function foo() {}

TDZ(暂时性死区):从作用域开始到变量声明的区域,访问变量会抛出 ReferenceError。

let x = 1;
{
  console.log(x); // ReferenceError: x 在 TDZ 中
  let x = 2;
}

var / let / const 对比

特性 var let const
作用域 函数级 块级 块级
提升 声明提升(undefined) 声明提升(TDZ) 声明提升(TDZ)
重复声明 允许 不允许 不允许
全局声明 window 属性 非 window 属性 非 window 属性
重新赋值 允许 允许 不允许
定义时必须赋值

原型与继承

原型链

function Person(name) {
  this.name = name;
}
Person.prototype.sayHello = function() {
  console.log(`Hello, I'm ${this.name}`);
};

const alice = new Person('Alice');

// 关系链
alice.__proto__ === Person.prototype                        // true
Person.prototype.__proto__ === Object.prototype              // true
Object.prototype.__proto__ === null                          // true
Person.__proto__ === Function.prototype                      // true
Function.prototype.__proto__ === Object.prototype            // true

// 属性查找:alice.name → alice.__proto__.sayHello → alice.__proto__.__proto__

__proto__ vs prototype

属性 谁有 用途
prototype 函数 作为 new 调用的实例的原型对象
__proto__ 所有对象 指向构造函数的 prototype(原型链查找)

继承方式演进

// 1. 原型链继承 —— 引用类型共享问题
function Parent() { this.names = ['a', 'b']; }
function Child() {}
Child.prototype = new Parent();
const c1 = new Child(), c2 = new Child();
c1.names.push('c');
console.log(c2.names); // ['a', 'b', 'c'] —— 被污染

// 2. 构造函数继承 —— 方法不能复用
function Parent(name) { this.name = name; }
function Child(name) {
  Parent.call(this, name); // 复制父类属性
}
// 方法需要写在 Parent.prototype 上,但 Child 无法访问

// 3. 组合继承(原型链+构造函数)—— 两次调用 Parent
function Parent(name) { this.name = name; }
Parent.prototype.say = function() { console.log(this.name); };
function Child(name, age) {
  Parent.call(this, name);  // 第一次
  this.age = age;
}
Child.prototype = new Parent(); // 第二次
Child.prototype.constructor = Child;

// 4. 寄生组合继承(推荐)—— 只调用一次 Parent
function inherit(Child, Parent) {
  Child.prototype = Object.create(Parent.prototype);
  Child.prototype.constructor = Child;
}
function Parent(name) { this.name = name; }
Parent.prototype.say = function() { console.log(this.name); };
function Child(name, age) {
  Parent.call(this, name);
  this.age = age;
}
inherit(Child, Parent);
Child.prototype.sayAge = function() { console.log(this.age); };

class 语法糖本质

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

  // 原型方法
  sayHello() { console.log(`Hi, ${this.name}`); }

  // 静态方法(Person.create 上)
  static create(name) { return new Person(name); }

  // 私有字段(ES2022+)
  #privateField = 42;

  // getter/setter
  get fullName() { return this.name; }
  set fullName(val) { this.name = val; }
}

// 本质:function 的语法糖
typeof Person // "function"
Person.prototype.sayHello // function

// 继承
class Student extends Person {
  constructor(name, grade) {
    super(name); // 必须先调用 super 才可使用 this
    this.grade = grade;
  }
  sayHello() {
    super.sayHello(); // 调用父类方法
    console.log(`Grade: ${this.grade}`);
  }
}

new 运算符原理

function _new(Constructor, ...args) {
  // 1. 创建空对象,原型指向 Constructor.prototype
  const obj = Object.create(Constructor.prototype);

  // 2. 执行构造函数,this 指向 obj
  const result = Constructor.apply(obj, args);

  // 3. 若构造函数返回对象则用该对象,否则用 obj
  return result instanceof Object ? result : obj;
}

闭包与作用域

词法环境与执行上下文

执行上下文:JS 引擎执行代码时的环境,分为三种:

  • 全局执行上下文
  • 函数执行上下文(每次调用创建)
  • Eval 执行上下文

每个执行上下文包含:

ExecutionContext = {
  ThisBinding: <this>,
  LexicalEnvironment: {            // let/const 存放处
    EnvironmentRecord: { bindings },
    Outer: <outer reference>
  },
  VariableEnvironment: {           // var 存放处(其他同 LexicalEnvironment)
    EnvironmentRecord: { bindings },
    Outer: <outer reference>
  }
}

执行上下文栈

function a() {
  function b() {
    console.log('inner');
  }
  b();
}
a();

// 栈变化:
// 1. global EC (压栈)
// 2. a EC (压栈)
// 3. b EC (压栈)→ 执行 b → b EC 出栈
// 4. a EC 继续 → 执行完成 → a EC 出栈

闭包形成原理

function outer() {
  let count = 0;          // outer 的局部变量
  return function inner() {
    count++;              // inner 引用了 outer 作用域中的 count
    return count;
  };
}

const counter = outer();  // outer 执行后,其执行上下文本该被销毁
console.log(counter());   // 1 —— 但 inner 保持了对 count 的引用
console.log(counter());   // 2 —— count 仍存在且可变

闭包形成条件:函数嵌套 + 内部函数引用外部函数的变量。

闭包原理:JS 引擎在函数创建时保存外部作用域的引用([[Scopes]] 属性),使该作用域不会被 GC 回收。

闭包应用

// 1. 模块化(IIFE 模块)
const CounterModule = (function() {
  let count = 0;         // 私有变量
  return {
    increment() { count++; return this; },
    decrement() { count--; return this; },
    getCount() { return count; },
  };
})();

// 2. 柯里化
function curry(fn) {
  return function curried(...args) {
    if (args.length >= fn.length) {
      return fn.apply(this, args);
    }
    return (...next) => curried(...args, ...next);
  };
}

// 3. 防抖/节流 —— 见高阶技巧

// 4. 私有变量
function createPerson(name) {
  let _name = name;                // 私有
  return {
    getName() { return _name; },
    setName(n) { _name = n; },
  };
}

闭包的内存问题

// ❌ 内存泄漏:闭包长期持有大对象
function leak() {
  const heavyData = new Array(1000000).fill('*');
  return function() {
    console.log('闭包持有 heavyData'); // heavyData 永远不会被 GC
  };
}

// ✅ 改进:用完后置空
function noLeak() {
  const heavyData = new Array(1000000).fill('*');
  const result = function() {
    console.log('闭包持有 heavyData');
  };
  heavyData = null; // 闭包内不再持有
  return result;
}

this 指向

五种绑定规则

// 1. 默认绑定(非严格模式 → window/globalThis,严格模式 → undefined)
function foo() { console.log(this); }
foo(); // window(非严格)或 undefined(严格)

// 2. 隐式绑定(谁调用指向谁)
const obj = { name: 'obj', foo };
obj.foo(); // obj

// 隐式丢失
const bar = obj.foo;
bar(); // window(引用丢失)

// 3. 显式绑定(call / apply / bind)
foo.call(obj, arg1, arg2);
foo.apply(obj, [arg1, arg2]);
const bound = foo.bind(obj); // 返回新函数,永久绑定 this

// 4. new 绑定
function Person(name) { this.name = name; }
const p = new Person('Alice'); // this → 新对象

// 5. 箭头函数(无独立 this,继承外层词法 this)
const arrow = () => { console.log(this); };
arrow(); // 外层 this

优先级判断

new 绑定 > 显式绑定 > 隐式绑定 > 默认绑定
箭头函数:无视以上规则,取定义时的外层 this
// bind 不能改变箭头函数的 this
const obj = { name: 'obj' };
const fn = () => { console.log(this); };
fn.call(obj);      // window(箭头函数无视 call)

// bind 返回的函数不能被 new 改变 this
function Person() { console.log(this); }
const BoundPerson = Person.bind({ fake: true });
new BoundPerson(); // Person 实例(new 优先于 bind)

常见陷阱

// 定时器中的 this
const obj = {
  count: 0,
  start() {
    setInterval(function() {
      this.count++; // this → window
    }, 1000);
  },
  startCorrect() {
    setInterval(() => {
      this.count++; // this → obj(箭头函数)
    }, 1000);
  }
};

异步编程

Promise

三种状态pendingfulfilled / rejected(状态不可逆)。

const p = new Promise((resolve, reject) => {
  // 异步操作
  setTimeout(() => resolve('done'), 1000);
  // 或 reject(new Error('fail'));
});

p.then(
  (value) => console.log(value),     // onFulfilled
  (reason) => console.error(reason)  // onRejected(可选)
).catch((err) => {
  // 处理上方 then 中抛出的错误
}).finally(() => {
  // 无论成功失败都会执行
});

Promise 静态方法

// all —— 全部成功则返回结果数组,任一失败则立即 reject
Promise.all([p1, p2, p3])
  .then(([r1, r2, r3]) => {})
  .catch(err => {});

// allSettled —— 等待所有结束,返回 {status, value/reason} 数组
Promise.allSettled([p1, p2, p3])
  .then(results => {
    results.forEach(r => {
      if (r.status === 'fulfilled') console.log(r.value);
      else console.error(r.reason);
    });
  });

// race —— 返回第一个完成的(无论成功或失败)
Promise.race([p1, p2]).then(v => {});

// any —— 返回第一个成功的,全失败则 AggregateError
Promise.any([p1, p2]).then(v => {}).catch(err => {
  console.error(err.errors); // 所有失败原因
});

手写 Promise 核心步骤

function MyPromise(executor) {
  this.state = 'pending';
  this.value = undefined;
  this.reason = undefined;
  this.onFulfilledCallbacks = [];
  this.onRejectedCallbacks = [];

  const resolve = (value) => {
    if (this.state !== 'pending') return;
    this.state = 'fulfilled';
    this.value = value;
    this.onFulfilledCallbacks.forEach(fn => fn());
  };

  const reject = (reason) => {
    if (this.state !== 'pending') return;
    this.state = 'rejected';
    this.reason = reason;
    this.onRejectedCallbacks.forEach(fn => fn());
  };

  try {
    executor(resolve, reject);
  } catch (err) {
    reject(err);
  }
}

// then 返回新的 Promise,支持链式调用
MyPromise.prototype.then = function(onFulfilled, onRejected) {
  // 值穿透
  onFulfilled = typeof onFulfilled === 'function' ? onFulfilled : v => v;
  onRejected = typeof onRejected === 'function' ? onRejected : r => { throw r; };

  const promise2 = new MyPromise((resolve, reject) => {
    const handleFulfilled = () => {
      queueMicrotask(() => {
        try {
          const x = onFulfilled(this.value);
          resolvePromise(promise2, x, resolve, reject);
        } catch (e) {
          reject(e);
        }
      });
    };
    // ... 类似 handleRejected
    if (this.state === 'fulfilled') handleFulfilled();
    else if (this.state === 'pending') {
      this.onFulfilledCallbacks.push(handleFulfilled);
    }
  });
  return promise2;
};

// resolvePromise 处理 x 可能为 Promise 的情况(递归拆解)
function resolvePromise(promise2, x, resolve, reject) {
  if (promise2 === x) return reject(new TypeError('Chaining cycle'));
  if (x instanceof MyPromise) {
    x.then(y => resolvePromise(promise2, y, resolve, reject), reject);
  } else {
    resolve(x);
  }
}

async/await

// async 函数总是返回 Promise
async function fetchData() {
  // 值 → Promise.resolve(值)
  return 'data'; // 等价于 return Promise.resolve('data')
}

// await 暂停执行直到 Promise 完成
async function getUser(id) {
  try {
    const user = await api.getUser(id);      // 等待
    const posts = await api.getPosts(user.id); // 串行
    return { user, posts };
  } catch (err) {
    console.error('请求失败:', err);
    throw err; // 让调用方继续处理
  }
}

错误处理

// 方式一:try/catch(推荐)
async function safe() {
  try {
    await risky();
  } catch (err) {
    // 处理错误
  }
}

// 方式二:链式 catch
async function safe2() {
  const result = await risky().catch(err => {
    console.error(err);
    return fallbackValue; // 降级值
  });
}

// 方式三:全局错误(未被捕获的 Promise reject)
window.addEventListener('unhandledrejection', (event) => {
  console.error('未捕获的 Promise 错误:', event.reason);
});

并发控制

// 串行(性能差但有依赖关系时必需)
const r1 = await task1();
const r2 = await task2(r1); // 依赖 r1

// 并行(无依赖关系)
const [a, b] = await Promise.all([taskA(), taskB()]);

// 限流并发(控制并发数)
async function parallelLimit(tasks, limit) {
  const results = [];
  const executing = [];

  for (const task of tasks) {
    const p = Promise.resolve().then(() => task());
    results.push(p);

    if (limit <= tasks.length) {
      const e = p.then(() => executing.splice(executing.indexOf(e), 1));
      executing.push(e);
      if (executing.length >= limit) {
        await Promise.race(executing);
      }
    }
  }

  return Promise.all(results);
}

Generator

function* generateSequence() {
  yield 1;
  yield 2;
  yield 3;
}

const gen = generateSequence();
gen.next(); // { value: 1, done: false }
gen.next(); // { value: 2, done: false }
gen.next(); // { value: 3, done: true }
gen.next(); // { value: undefined, done: true }

用 Generator 实现异步流程控制

function asyncGenerator(genFn) {
  return function(...args) {
    const gen = genFn.apply(this, args);
    return new Promise((resolve, reject) => {
      function step(key, arg) {
        try {
          const { value, done } = gen[key](arg);
          if (done) return resolve(value);
          Promise.resolve(value).then(
            v => step('next', v),
            e => step('throw', e)
          );
        } catch (err) {
          reject(err);
        }
      }
      step('next');
    });
  };
}

// 使用:类似 async/await
const fetchUser = asyncGenerator(function*(id) {
  const user = yield api.getUser(id);  // yield Promise → await
  const posts = yield api.getPosts(user.id);
  return { user, posts };
});

ES6+

解构赋值

// 数组解构
const [a, b = 10, ...rest] = [1, 2, 3, 4];
// a=1, b=2, rest=[3,4]

// 对象解构
const { name, age, ...others } = { name: 'Alice', age: 25, city: 'NY' };
// 重命名 + 默认值
const { name: userName = 'Guest' } = obj;

// 嵌套解构
const { address: { city, zip } } = person;

// 函数参数解构
function render({ title = '', width = 100, height = 100 } = {}) {}

展开与剩余

// 展开数组
const arr = [1, 2, 3];
const copy = [...arr];
const merged = [...arr, 4, 5];
Math.max(...arr);

// 展开对象(浅拷贝)
const obj = { a: 1, b: 2 };
const copy = { ...obj };
const merged = { ...obj, c: 3 };

// 剩余参数(代替 arguments)
function sum(...nums) {
  return nums.reduce((a, b) => a + b, 0);
}

Symbol

const sym1 = Symbol('desc');
const sym2 = Symbol('desc');
sym1 === sym2 // false(唯一)

// 内置 Symbol
Symbol.iterator    // 可迭代对象
Symbol.toStringTag  // Object.prototype.toString
Symbol.hasInstance  // instanceof
Symbol.species     // 派生对象构造函数

Map / Set / WeakMap / WeakSet

// Map —— key 可以是任意类型
const map = new Map();
map.set(obj, 'value');  // 对象作为 key
map.get(obj);
map.has(obj);
map.delete(obj);
map.size;

// 与 Object 对比:
// - Object key 只能是 string/Symbol,Map key 可以是任意类型
// - Map 有序迭代,Object 无序
// - Map size 直接取,Object 需 Object.keys().length
// - Map 频繁增删性能更优

// Set —— 值唯一
const set = new Set([1, 2, 3, 1, 2]);
// set: {1, 2, 3}

// WeakMap —— key 必须是对象,弱引用,不阻止 GC
let obj = { data: 'large' };
const weakMap = new WeakMap();
weakMap.set(obj, 'metadata');
obj = null; // weakMap 中对应的键值对会被 GC(不可预测时间)

// WeakSet —— 同理,值必须是对象,弱引用

// 用途:DOM 节点关联数据,不会因 Map 持有引用造成内存泄漏

Proxy / Reflect

const target = { name: 'Alice', age: 25 };

const proxy = new Proxy(target, {
  // 拦截读取
  get(target, prop, receiver) {
    console.log(`GET ${String(prop)}`);
    // return target[prop];   // 推荐用 Reflect
    return Reflect.get(target, prop, receiver);
  },
  // 拦截写入
  set(target, prop, value, receiver) {
    console.log(`SET ${String(prop)} = ${value}`);
    return Reflect.set(target, prop, value, receiver);
  },
  // 拦截 in 操作
  has(target, prop) {
    return Reflect.has(target, prop);
  },
  // 拦截 delete
  deleteProperty(target, prop) {
    return Reflect.deleteProperty(target, prop);
  },
  // 拦截函数调用
  apply(target, thisArg, args) {
    console.log('函数被调用');
    return Reflect.apply(target, thisArg, args);
  },
});

// 响应式原理(Vue3)
function reactive(obj) {
  const deps = new Map();
  return new Proxy(obj, {
    get(target, key, receiver) {
      track(deps, key); // 收集依赖
      return Reflect.get(target, key, receiver);
    },
    set(target, key, value, receiver) {
      const result = Reflect.set(target, key, value, receiver);
      trigger(deps, key); // 触发更新
      return result;
    },
  });
}

Optional Chaining / Nullish Coalescing

// Optional Chaining(?.)
user?.address?.city;        // 链中任一为 null/undefined 则返回 undefined
users?.[0];                 // 数组访问
obj?.method?.();            // 方法调用

// Nullish Coalescing(??)
const value = input ?? 'default';  // 仅当 input 为 null/undefined 时使用默认
// vs ||
const value = input || 'default';  // input 为 '' / 0 / false 也使用默认

模块化

CommonJS vs ES Module

// ========== CommonJS(Node.js,同步,运行时加载) ==========
// 导出
module.exports = { foo, bar };
exports.foo = foo; // 相当于 module.exports.foo

// 导入
const module = require('./module');
const { foo } = require('./module');

// 特性:运行时动态加载,值拷贝(基本类型)

// ========== ES Module(标准,异步,静态解析) ==========
// 导出
export const name = 'value';
export function foo() {}
export default class App {}
export { name, foo };

// 导入
import { name, foo } from './module.js';
import defaultExport from './module.js';
import * as all from './module.js';
import { name as alias } from './module.js';

// 动态导入(运行时)
const module = await import('./module.js');

// 特性:
// - 静态分析支持 Tree Shaking
// - 值引用(模块内的变量变化会反映在导入方)
// - 必须使用 .mjs 或 package.json type: "module"

区别总结

特性 CommonJS ES Module
加载时机 运行时 编译时(静态分析)
加载方式 同步 异步
值传递 值拷贝(基本类型) 值引用(动态绑定)
Tree Shaking 不支持 支持
循环依赖 部分支持(可拿到未完成的 exports) 支持(有 TDZ 机制)
顶层 this module.exports undefined

Tree Shaking 原理

Tree Shaking 依赖于 ES Module 的静态结构(import/export 在编译时确定),构建工具(webpack/rollup)通过:

  1. 标记未使用的导出(/*#__PURE__*/ 标记纯函数调用)
  2. 移除无用代码(terser 等压缩工具执行)

必要条件

  • ES Module(import/export
  • 生产模式
  • 使用副作用标记("sideEffects": false"sideEffects": ["./*.css"]

高阶技巧

防抖(Debounce)

// 防抖:最后一次触发后 delay ms 执行
function debounce(fn, delay = 300) {
  let timer = null;
  return function(...args) {
    if (timer) clearTimeout(timer);
    timer = setTimeout(() => {
      fn.apply(this, args);
      timer = null;
    }, delay);
  };
}

// 立即执行版(第一次立即触发,之后等待)
function debounceImmediate(fn, delay = 300) {
  let timer = null;
  return function(...args) {
    const callNow = !timer;
    if (timer) clearTimeout(timer);
    timer = setTimeout(() => {
      timer = null;
    }, delay);
    if (callNow) fn.apply(this, args);
  };
}

// 使用
const handleSearch = debounce((e) => {
  api.search(e.target.value);
}, 500);

节流(Throttle)

// 节流:固定时间间隔内只执行一次
function throttle(fn, interval = 300) {
  let lastTime = 0;
  return function(...args) {
    const now = Date.now();
    if (now - lastTime >= interval) {
      lastTime = now;
      fn.apply(this, args);
    }
  };
}

// 尾调用版(停止触发后还会执行最后一次)
function throttleTrailing(fn, interval = 300) {
  let timer = null;
  return function(...args) {
    if (!timer) {
      timer = setTimeout(() => {
        fn.apply(this, args);
        timer = null;
      }, interval);
    }
  };
}

// 完整版(头尾都执行)
function throttleFull(fn, interval = 300) {
  let lastTime = 0, timer = null;
  return function(...args) {
    const now = Date.now();
    const remaining = interval - (now - lastTime);
    if (remaining <= 0) {
      if (timer) { clearTimeout(timer); timer = null; }
      lastTime = now;
      fn.apply(this, args);
    } else if (!timer) {
      timer = setTimeout(() => {
        lastTime = Date.now();
        timer = null;
        fn.apply(this, args);
      }, remaining);
    }
  };
}

防抖 vs 节流

场景 方案
搜索输入框 防抖(用户停之后再搜)
无限滚动 节流(固定间隔检查位置)
窗口 resize 防抖(停止变化后重排)
拖拽/鼠标移动 节流(固定频率更新位置)

深拷贝

// 1. JSON 方法(简单但有限制)
const copy = JSON.parse(JSON.stringify(obj));
// 限制:undefined/Symbol/function → 丢失;Date → 字符串;RegExp → {};循环引用 → 报错

// 2. 递归深拷贝(支持循环引用)
function deepClone(obj, map = new WeakMap()) {
  if (obj === null || typeof obj !== 'object') return obj;

  // 处理循环引用
  if (map.has(obj)) return map.get(obj);

  // 处理特殊对象
  if (obj instanceof Date) return new Date(obj);
  if (obj instanceof RegExp) return new RegExp(obj);
  if (obj instanceof Map) {
    const copy = new Map();
    map.set(obj, copy);
    obj.forEach((v, k) => copy.set(deepClone(k, map), deepClone(v, map)));
    return copy;
  }
  if (obj instanceof Set) {
    const copy = new Set();
    map.set(obj, copy);
    obj.forEach(v => copy.add(deepClone(v, map)));
    return copy;
  }

  const copy = Array.isArray(obj) ? [] : {};
  map.set(obj, copy);

  // 遍历自身属性(包含 Symbol 属性)
  for (const key of [...Object.keys(obj), ...Object.getOwnPropertySymbols(obj)]) {
    copy[key] = deepClone(obj[key], map);
  }

  return copy;
}

函数柯里化

// 柯里化:将多参数函数转为接受单参数的函数链
function curry(fn) {
  return function curried(...args) {
    // 参数够了就调用
    if (args.length >= fn.length) {
      return fn.apply(this, args);
    }
    // 不够就返回新函数继续收集
    return (...args2) => curried(...args, ...args2);
  };
}

const add = (a, b, c) => a + b + c;
const curriedAdd = curry(add);
curriedAdd(1)(2)(3); // 6
curriedAdd(1, 2)(3); // 6
curriedAdd(1)(2, 3); // 6

偏函数(Partial Application)

// 偏函数:固定部分参数
function partial(fn, ...fixedArgs) {
  return function(...remainingArgs) {
    return fn.apply(this, [...fixedArgs, ...remainingArgs]);
  };
}

const add = (a, b, c) => a + b + c;
const add5 = partial(add, 5);
add5(3, 2); // 10

compose / pipe

// compose: 从右到左组合函数
function compose(...fns) {
  return function(x) {
    return fns.reduceRight((acc, fn) => fn(acc), x);
  };
}

// pipe: 从左到右组合函数(更常见)
function pipe(...fns) {
  return function(x) {
    return fns.reduce((acc, fn) => fn(acc), x);
  };
}

const add1 = (x) => x + 1;
const double = (x) => x * 2;
const toString = (x) => `${x}`;

const process = pipe(add1, double, toString);
process(5); // "12"(先加1得6,再乘2得12,再转字符串)

const process2 = compose(toString, double, add1);
process2(5); // "12"(执行顺序从右到左:5+1=6, 6*2=12, "12")