JS 函数终极指南:this、闭包、递归、尾调用、柯里化,一次性吃透

1. 箭头函数 vs 普通函数

核心差异

  • 没有自己的 this / arguments / super / new.target(都词法绑定,取自外层作用域)。
  • 不能 做构造函数:new (()=>{}) 会抛错。
  • prototype 属性不存在。
  • 返回值:单表达式可省略 {}return;要返回对象字面量需用括号包裹。

坑点

  • 对象方法不要随手写成箭头函数,否则 this 指向外层而非对象本身。
  • 需要 arguments 时,用 rest 参数 (...args) 代替。

示例

javascript 复制代码
const counter = {
  n: 0,
  // ❌ 用箭头写方法会拿不到对象的 this
  incBad: () => ++this.n,           // this 来自外层,非 counter
  // ✅ 正确:普通函数做方法
  inc() { return ++this.n; },
};

const add = (a, b) => a + b;
const makeUser = (name) => ({ name });  // 返回对象需用括号包住

建议

  • 事件/类方法/需要 this 的场景用普通函数;回调与组合函数用箭头函数。

2. 函数名(Function.prototype.name

用途

  • 调试堆栈 & 日志。
  • 自递归(命名函数表达式)。

细节

  • 赋给变量的匿名函数会推断名字const f = function(){}; f.name === "f"
  • 绑定函数:bound 前缀------f.bind(obj).name === "bound f"(实现相关)。
javascript 复制代码
function foo() {}
const bar = function baz() {};
console.log(foo.name); // "foo"
console.log(bar.name); // "baz"(仅在函数体内能引用 baz 递归)

3. 理解参数(JS 没有"真正重载")

现状

  • JS 不按签名区分重载;后定义的同名函数覆盖前者。

常见"重载"策略

  1. 参数个数/类型分派
  2. 可选对象参数(推荐,可扩展性强)
  3. 重载表(按规则挑处理器)
javascript 复制代码
// 1) 个数/类型分派
function area(shape, a, b) {
  if (shape === 'circle') return Math.PI * a * a;
  if (shape === 'rect')   return a * b;
  throw new TypeError('unknown shape');
}

// 2) options 对象
function fetchUser({ id, withPosts = false } = {}) { /* ... */ }

// 3) 重载表
const overloads = {
  string: (x) => x.toUpperCase(),
  number: (x) => x * x,
};
function process(x) {
  const f = overloads[typeof x];
  if (!f) throw new TypeError('unsupported');
  return f(x);
}

4. 箭头函数中的参数

写法速览

javascript 复制代码
const id = x => x;                 // 单参省略括号
const add = (a, b) => a + b;       // 多参
const sum = (...nums) => nums.reduce((p, c) => p + c, 0); // rest
const greet = ({name} = {}) => `Hi, ${name||'Guest'}`;    // 解构+默认

5. "没有重载"意味着什么

  • 同名函数只保留最后一次定义
  • API 设计宜避免同名多义,使用不同名字或 options。
javascript 复制代码
function f(a) {}
function f(a, b) {} // 覆盖前面的 f(a)

6. 默认参数值

规则

  • 只有当实参为 undefined 才会触发默认值(null 不会)。
  • 默认值在调用时求值,可引用前面参数。
  • 与解构组合很强大。

示例

javascript 复制代码
function hello(name = 'Guest') { return `Hi, ${name}`; }
hello();         // "Hi, Guest"
hello(null);     // "Hi, null"(不会触发默认)

function f(a, b = a * 2) { return b; }
f(3); // 6

function draw({x = 0, y = 0} = {}) { /* ... */ } // 支持缺参与缺字段

必传参数技巧

javascript 复制代码
const required = (n) => { throw new Error(`${n} is required`); };
function connect(url = required('url')) { /*...*/ }

7. 默认参数与"暂时性死区"(TDZ)

  • 默认参数形成独立作用域;在其作用域内存在 TDZ。
  • 默认值表达式可用之前的参数 ,不可用之后的
javascript 复制代码
let x = 1;
function f(a = x, x = 2) {  // 形参 x 遮蔽外层 x
  console.log(a, x);        // a=1, x=2
}

function g(a = b, b = 2) {} // ❌ ReferenceError,b 尚未初始化

8. 参数扩展与收集(spread / rest)

(1) 扩展参数(spread)

  • 用在调用/字面量 中"展开"可迭代对象;浅拷贝
ini 复制代码
const arr = [1, 2, 3];
Math.max(...arr);                 // 3

const a = [1, 2]; const b = [3, 4];
const ab = [...a, ...b];          // [1,2,3,4]

const o1 = {a:1}, o2 = {b:2, a:9};
const o = { ...o1, ...o2 };       // {a:9, b:2}(后者覆盖前者)

(2) 收集参数(rest)

  • 用在形参最后位置,收集剩余实参为真数组。
scss 复制代码
function sum(first, ...rest) {
  return rest.reduce((p, c) => p + c, first);
}
sum(10, 1, 2, 3); // 16

对比 arguments

  • arguments 类数组,strict 下与形参不再联动 ;rest 是真数组、更推荐。

9. 函数声明 vs 函数表达式

区别

  • 声明 (Function Declaration)整体提升:可在声明前调用。
  • 表达式 (Function Expression)赋值给变量;var 变量名提升为 undefinedlet/const 有 TDZ。
  • 命名函数表达式仅在函数体内可见其名(便于自递归)。

示例

scss 复制代码
foo();               // ✅
function foo() {}

bar();               // ❌ ReferenceError(若使用 let/const)
const bar = function() {};

const baz = function qux(n){ return n ? qux(n-1) : 0; };

10. 函数作为值 & 柯里化(Currying)

概念

  • 柯里化:把 f(a,b,c) 变为 f(a)(b)(c)(或更灵活地累积参数)。
  • 用途:参数复用、延迟求值、函数组合、偏应用、构建 DSL。

通用柯里化实现

scss 复制代码
const curry = (fn, arity = fn.length) =>
  function curried(...args) {
    return args.length >= arity
      ? fn.apply(this, args)
      : (...rest) => curried.apply(this, args.concat(rest));
  };

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

实战

  • 事件处理器"预置参数":onClick={handle(type)}
  • 日志封装:const logNs = ns => (...a)=>console.log(ns, ...a)
  • React/FP:与 map/filter/reducecompose/pipe 搭配。

偏应用(partial)对比

scss 复制代码
const partial = (fn, ...bound) => (...rest) => fn(...bound, ...rest);
const add = (a,b,c)=>a+b+c;
partial(add, 1, 2)(3); // 6

11. 函数内容

(1) argumentscallee

  • arguments:类数组、动态实参集合;不建议新代码依赖。
  • arguments.callee严格模式禁用,不推荐使用(可用命名函数表达式替代)。
javascript 复制代码
function show() {
  console.log(arguments.length); // 实参个数
}

(2) this

绑定规则优先级

  1. new 绑定(构造调用)
  2. 显式绑定:call/apply/bind
  3. 隐式绑定:作为对象方法调用 obj.fn()
  4. 默认绑定:非严格 this===window;严格模式 undefined
  5. 箭头函数:词法 this(创建时确定,无法改)
javascript 复制代码
function who() { console.log(this.tag); }
const o = { tag:'O', who };
who();        // undefined / window
o.who();      // 'O'
who.call({tag:'X'}); // 'X'

const arrow = () => console.log(this.tag);
arrow.call({tag:'Y'}); // 仍来自外层 this

(3) caller

  • function.caller非标准、严格模式下受限,不可依赖。

(4) new.target

  • 判断是否通过 new 调用;在 class 中可做"抽象类"限制。
javascript 复制代码
function Person() {
  if (!new.target) throw new Error('use new');
}

class Shape {
  constructor() {
    if (new.target === Shape) throw new Error('abstract');
  }
}

12. 函数属性与方法:call / apply / bind

共性 :改变 this

  • call(thisArg, ...args)立即调用,参数散列。
  • apply(thisArg, argsArray)立即调用,参数数组(适合已有数组)。
  • bind(thisArg, ...args)返回新函数 ,可部分应用 & 固定 this

示例

javascript 复制代码
function greet(g, p) { console.log(`${g}, ${this.name}${p}`); }
const u = { name:'Ada' };

greet.call(u, 'Hi', '!');        // Hi, Ada!
greet.apply(u, ['Hello', '!!!']); // Hello, Ada!!!

const hiAda = greet.bind(u, 'Hi'); // 预置 g 与 this
hiAda('?');                        // Hi, Ada?

实战场景

  • 借用数组方法:[].slice.call(arguments)(老法,现在用 Array.from
  • DOM 事件里预绑定处理器 this/参数
  • 函数组合与偏应用

小心

  • bind 返回的函数不可再被 call/apply 更改 this
  • 频繁 bind 会增开销,优先在构造时 一次性绑定或用箭头函数持有外层 this

13. 函数表达式与"声明提升"

  • 函数声明整体提升。
  • 函数表达式 不会提升实现体;若用 let/const,在声明前访问触发 TDZ
scss 复制代码
foo();      // ok
function foo(){}

bar();      // ReferenceError
const bar = function(){};

14. 递归(以阶乘为例)

javascript 复制代码
function factorial(n) {
  if (n < 0) throw new RangeError('n>=0');
  if (n <= 1) return 1;
  return n * factorial(n - 1);
}

注意

  • n 可能栈溢出;可改成循环或"尾递归+蹦床(trampoline)"。

蹦床示意:

ini 复制代码
const trampoline = (f) => (...args) => {
  let res = f(...args);
  while (typeof res === 'function') res = res();
  return res;
};

const factT = trampoline(function step(n, acc=1){
  if (n<=1) return acc;
  return () => step(n-1, n*acc);
});

factT(10000); // 不会爆栈

15. 尾调用优化(TCO)

概念

  • "尾调用"是函数返回位置直接返回另一个调用的结果。
  • "尾递归"是递归调用位于尾位置。
  • 理论上可节省栈帧。

现实

  • 主流引擎目前并未实现规范化 TCO (不要依赖它避免爆栈)。生产中请用循环蹦床

尾递归写法(语义正确,但不指望优化)

javascript 复制代码
'use strict';
function factorialTR(n, acc = 1) {
  if (n <= 1) return acc;
  return factorialTR(n - 1, n * acc); // 尾位置
}

16. 尾调用的判定条件(理论)

  • 调用必须在返回语句 的尾位置(return f(...))。
  • 不能在 try/finally 等会产生额外工作的位置。
  • 不能对返回值再做运算(如 1 + f() 不是尾调用)。
  • 严格模式(规范讨论语境)。

实战:当成编码风格理解,不依赖优化。


17. 闭包(Closure)

定义

  • 函数"记住"其创建时的词法作用域,即使在外层函数已执行完后仍可访问到。

常见用途

  • 私有状态、函数工厂、缓存/记忆化、模块封装。

示例:计数器

scss 复制代码
function makeCounter() {
  let n = 0;
  return () => ++n;
}
const c = makeCounter();
c(); c(); // 1, 2

坑点

  • 循环里 var 会共享一个变量;用 let 或 IIFE 捕获值。
javascript 复制代码
for (var i=0;i<3;i++){
  setTimeout(()=>console.log(i),0); // 3,3,3
}
for (let j=0;j<3;j++){
  setTimeout(()=>console.log(j),0); // 0,1,2
}

18. this 对象(再补充要点)

  • 优先级new > 显式 > 隐式 > 默认;箭头函数跳出规则,直接词法绑定
  • 类字段里的箭头 常用于把回调里的 this 固定为实例:
kotlin 复制代码
class View {
  constructor() { this.count = 0; }
  onClick = () => { this.count++; } // 在实例上创建,this 永远是实例
}

19. 内存泄漏(Memory Leak)

常见来源

  1. 全局变量/意外挂到全局
  2. 未清理的定时器/订阅/事件监听
  3. 闭包长期持有大对象/DOM 引用
  4. 脱离文档的 DOM 被引用(detached DOM)
  5. 无界缓存(Map/Object 不清理)

示例与修复

javascript 复制代码
// 1) 全局
function bad() { leaky = new Array(1e6).fill('*'); } // ❌ 隐式全局(漏写 var/let/const)
function good() { const safe = new Array(1e6).fill('*'); }

// 2) 定时器/监听
const id = setInterval(()=>{/*...*/}, 1000);
// 当组件卸载或不需要时
clearInterval(id);

const onScroll = () => {/*...*/};
window.addEventListener('scroll', onScroll);
// ...
window.removeEventListener('scroll', onScroll);

// 3) 缓存
const cache = new Map();
function get(key, create) {
  if (!cache.has(key)) cache.set(key, create());
  return cache.get(key);
}
// 若 key 可能只被临时对象引用,考虑 WeakMap 以便 GC:
const wcache = new WeakMap();

建议

  • 打开浏览器内存快照找"保留对象"。
  • 组件/模块成对清理资源(定时器、监听、订阅)。
  • 能用 WeakMap/WeakRef 的场景尽量用。

20. IIFE(立即调用函数表达式)

作用

  • 立即执行并创建私有作用域;历史上用于"模块化"(在 ES 模块前)。

示例

javascript 复制代码
(function(){
  const secret = 42;
  console.log('IIFE run');
})();

// 异步 IIFE(常见于顶层 await 替代)
(async () => {
  const data = await Promise.resolve(123);
  console.log(data);
})();

21. 私有变量(基于闭包)

csharp 复制代码
function Counter() {
  let n = 0;                    // 私有
  return {
    inc: () => ++n,
    get: () => n
  };
}
const c = Counter();
c.inc(); c.get();               // 1

特点 :无法从外部直接访问/修改 n,天然私有。


22. 静态私有变量

方式 A:类的私有静态字段(#)

arduino 复制代码
class IdGen {
  static #next = 1;                // 静态私有
  static alloc() { return this.#next++; }
}
IdGen.alloc(); // 1

方式 B:模块作用域变量(文件级私有)

javascript 复制代码
let next = 1;                      // 仅模块内可见
export function alloc() { return next++; }

23. 模块模式(Module Pattern / Revealing Module Pattern)

思想

  • 用 IIFE 封装私有变量与函数,只暴露公共 API。
kotlin 复制代码
const Store = (function(){
  const data = new Map();                 // 私有
  function set(k, v){ data.set(k, v); }
  function get(k){ return data.get(k); }
  function has(k){ return data.has(k); }
  return { set, get, has };               // Revealing:名称一致
})();
Store.set('a', 1);

提示

  • 现代项目优先使用 ES Moduleexport / import
  • 但在无打包器或老环境,模块模式仍很实用。

24. 增强模块模式(Augmented / Hybrid Module)

用途

  • 在原模块基础上扩展注入依赖,支持可测试性/可插拔性。
javascript 复制代码
// 基础模块
const Base = (function(){
  const list = [];
  return {
    add(x){ list.push(x); },
    all(){ return list.slice(); }
  };
})();

// 增强:添加 reset、filter 功能
const Enhanced = (function(mod){
  mod.reset = function(){ mod._reset?.() ?? (function(){
    // 通过内部 API 或暴露的方式清空(演示用)
    while (mod.all().length) mod.all().pop(); // 这里只为演示,实际应在 Base 提供 clear
  })(); };

  mod.filter = function(pred){ return mod.all().filter(pred); };
  return mod;
})(Base);

// 依赖注入式增强(Hybrid)
const WithLogger = (function(mod, logger){
  const oldAdd = mod.add;
  mod.add = (x)=>{ logger('add', x); oldAdd(x); };
  return mod;
})(Base, console.log);

建议

  • 设计时预留最小必要的扩展点 (如 clear/onChange)。
  • 现代 ESM 下可通过组合导出插件机制实现增强。

额外实用补充

  • 函数属性length(形参个数,不含 rest/有默认值之后的参数),name
  • Function 构造器new Function(code) 动态创建函数(绕过作用域封闭,慎用)。
  • 参数校验 :运行时校验(typeof/Array.isArray/小型 schema 校验)。
  • 性能:频繁创建闭包/绑定函数有成本,复用或上移到外层作用域。
相关推荐
Zestia10 分钟前
页面点击跳转源代码?——element-jumper插件实现
前端·javascript
前端小白199510 分钟前
面试取经:工程化篇-webpack性能优化之优化loader性能
前端·面试·前端工程化
PineappleCoder10 分钟前
大小写 + 标点全搞定!JS 如何精准统计单词频率?
前端·javascript·算法
zhangbao90s12 分钟前
Web组件:使用Shadow DOM
前端
hhy前端之旅12 分钟前
语义版本控制:掌握版本管理的艺术
前端
coding随想12 分钟前
深入浅出DOM操作的隐藏利器:Range(范围)对象——掌控文档的“手术刀”
前端
前端小白199513 分钟前
面试取经:工程化篇-webpack性能优化之减少模块解析
前端·面试·前端工程化
一枚前端小能手13 分钟前
🏗️ 项目越来越大维护不动了,微前端架构了解一下
前端
文艺理科生22 分钟前
Nuxt.js入门指南-Vue生态下的高效渲染技术
前端·vue.js·nuxt.js
夏小花花26 分钟前
vue3 ref和reactive的区别和使用场景
前端·javascript·vue.js·typescript