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 不按签名区分重载;后定义的同名函数覆盖前者。
常见"重载"策略
- 参数个数/类型分派
- 可选对象参数(推荐,可扩展性强)
- 重载表(按规则挑处理器)
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
变量名提升为undefined
,let/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/reduce
、compose/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) arguments
与 callee
arguments
:类数组、动态实参集合;不建议新代码依赖。arguments.callee
:严格模式禁用,不推荐使用(可用命名函数表达式替代)。
javascript
function show() {
console.log(arguments.length); // 实参个数
}
(2) this
绑定规则优先级
new
绑定(构造调用)- 显式绑定:
call/apply/bind
- 隐式绑定:作为对象方法调用
obj.fn()
- 默认绑定:非严格
this===window
;严格模式undefined
- 箭头函数:词法 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)
常见来源
- 全局变量/意外挂到全局
- 未清理的定时器/订阅/事件监听
- 闭包长期持有大对象/DOM 引用
- 脱离文档的 DOM 被引用(detached DOM)
- 无界缓存(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 Module :
export
/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 校验)。 - 性能:频繁创建闭包/绑定函数有成本,复用或上移到外层作用域。