函数进阶
递归与堆栈
递归
一种编程模式,将一个任务可以自然地拆分成多个相同类型但更简单的任务。
简单来说就是将任务拆分为最基础情况和调用自身,通过不断调用自身到达最基础情况。
function pow(x, n) {
if (n == 1) {
return x;
} else {
return x * pow(x, n - 1);
}
}
console.info( pow(2, 3) ); // 8
当执行时会被分为2个分支:
if n==1 = x
/
pow(x, n) =
\
else = x * pow(x, n - 1)
递归会不断调用自身,直到到达最基础的递归,即上述的 if n==1。
最大的嵌套调用次数(包括首次)被称为 递归深度。
最大递归深度受限于 JavaScript 引擎。
引擎在最大迭代深度为 10000 及以下时是可靠的,有些引擎可能允许更大的最大深度,
但是对于大多数引擎来说,100000 可能就超出限制了。
有一些自动优化能够帮助减轻这种情况(尾部调用优化),但目前它们还没有被完全支持,
只能用于简单场景。
循环迭代思路:
function pow(x, n) { let result = 1; // 在循环中,用 x 乘以 result n 次 for (let i = 0; i < n; i++) { result *= x; } return result; } console.info( pow(2, 3) ); // 8
执行上下文与上下文堆栈(Call Stack)
-
执行上下文(Execution Context)
每个被调用的函数都会创建一个执行上下文,内部保存:- 当前局部变量
- 所在代码行(恢复位置)
this等运行时信息
-
上下文堆栈(Call Stack)
JavaScript 引擎使用“栈”结构管理这些上下文:- 压栈:发生嵌套调用时,当前上下文暂停并压入栈,新建子上下文并置顶。
- 出栈:子调用返回后,其上下文被弹出,原上下文恢复执行。
-
递归示例
pow(2, 3)
调用链与栈变化(→ 表示栈顶):pow(2,3) → {x:2, n:3, line 5}计算
x * pow(2,2),压入新上下文:pow(2,2) → {x:2, n:2, line 5} pow(2,3) → {x:2, n:3, line 5}继续
pow(2,1):pow(2,1) → {x:2, n:1, line 1} // n==1 命中返回 2 pow(2,2) → {x:2, n:2, line 5} pow(2,3) → {x:2, n:3, line 5}pow(2,1)返回后弹出,继续计算并逐层弹出,最终得到8。 -
内存与性能
- 递归深度 = 栈最大长度;每次递归都占用一份上下文内存。
- 循环版本只需一份上下文,内存恒定,通常更快。
- 任何递归都可改写成循环,但递归代码往往更短、可读性更高;在性能瓶颈不明显时优先选择可维护性。
递归遍历(Recursive Traversal)
1. 适用场景
当数据结构“部分复制自身”——即对象里又嵌套相同结构的对象/数组时,用循环写 for/while 会迅速出现 3-4 级嵌套,难以维护。递归把“复杂节点”自动拆成“同构子节点”,代码简洁且能应对任意深度。
2. 公司薪资汇总示例
function sumSalaries(dept) {
if (Array.isArray(dept)) { // 叶子:员工数组
return dept.reduce((s, p) => s + p.salary, 0);
}
// 分支:子部门对象 → 递归合并
return Object.values(dept)
.reduce((s, sub) => s + sumSalaries(sub), 0);
}
console.info(sumSalaries(company)); // 7700
调用过程形成一棵“递归树”:对象节点继续分叉,数组节点立即给出结果,最后层层汇总。
3. 递归结构(Recursive Data Structure)
定义里“包含自身”的结构,例如:
- 公司部门 = 人员数组 | {子部门…}
- HTML 元素 = 文本 | 注释 | 其他 HTML 元素…
遍历算法与结构定义一一对应,天然适合递归。
链表(Singly Linked List)
1. 节点定义
node = { value: any, next: node | null }
2. 优势
- 插入/删除只需修改相邻节点的
next,O(1),不像数组需批量搬移元素。 - 可动态拼接/拆分,无需重新分配连续内存。
3. 基本操作
let list = { value: 1, next: { value: 2, next: { value: 3, next: null } } };
// 头部插入
list = { value: "new", next: list };
// 删除第 2 个节点
list.next = list.next.next;
4. 劣势
- 随机访问 O(n),必须从头顺
next遍历;数组为 O(1)。 - 每个节点额外存储指针,内存开销略高。
5. 扩展
- 双向链表:增加
prev指针,支持倒序遍历与尾部删除。 - 循环链表:尾节点指向头节点,用于轮询场景。
- 尾指针
tail:在尾部插入/删除时可保持 O(1)。
总结:链表在“频繁头尾插入/删除、很少随机下标访问”的场景(如队列、LRU 缓存)中优于数组;数组在随机读取与缓存局部性上更强。根据实际需求选择或组合使用。
Rest 与 Spread
Rest
即为不定长参数。
语法:...变量名
将会声明一个数组并指定其名称,其中存有剩余的参数
function showName(firstName, lastName, ...titles) {
console.info( firstName + ' ' + lastName ); // Julius Caesar
// 剩余的参数被放入 titles 数组中
// i.e. titles = ["Consul", "Imperator"]
console.info( titles[0] ); // Consul
console.info( titles[1] ); // Imperator
console.info( titles.length ); // 2
}
showName("Julius", "Caesar", "Consul", "Imperator");
注意:Rest 参数必须放到参数列表的末尾。
历史遗留:arguments
一个名为 arguments 的特殊类数组对象可以在函数中被访问,该对象以参数在参数列表中的索引作为键,存储所有参数。
过去不支持rest语法,因此JavaScript使用一个类数组对象arguments来存储参数。
但其只能保存全部参数,不能自动进行截断。
function showName() {
console.info( arguments.length );
console.info( arguments[0] );
console.info( arguments[1] );
// 它是可遍历的
// for(let arg of arguments) console.info(arg);
}
// 依次显示:2,Julius,Caesar
showName("Julius", "Caesar");
// 依次显示:1,Ilya,undefined(没有第二个参数)
showName("Ilya");
注意:箭头函数没有arguments,其所能访问的为外部函数的arguments
Spread
作用是自动展开可迭代对象。
内部使用迭代器来收集元素,与for of一致。
let arr1 = [1, -2, 3, 4];
let arr2 = [8, 3, -8, 1];
console.info( Math.max(1, ...arr1, 2, ...arr2, 25) ); // 25
let str = "Hello";
console.info( [...str] ); // H,e,l,l,o
快速进行浅拷贝
比
Object.assign更加方便
数组:
let arr = [1, 2, 3];
let arrCopy = [...arr]; // 将数组 spread 到参数列表中
// 然后将结果放到一个新数组
// 两个数组中的内容相同吗?
console.info(JSON.stringify(arr) === JSON.stringify(arrCopy)); // true
// 两个数组相等吗?
console.info(arr === arrCopy); // false(它们的引用是不同的)
// 修改我们初始的数组不会修改副本:
arr.push(4);
console.info(arr); // 1, 2, 3, 4
console.info(arrCopy); // 1, 2, 3
对象:
let obj = { a: 1, b: 2, c: 3 };
let objCopy = { ...obj }; // 将对象 spread 到参数列表中
// 然后将结果返回到一个新对象
// 两个对象中的内容相同吗?
console.info(JSON.stringify(obj) === JSON.stringify(objCopy)); // true
// 两个对象相等吗?
console.info(obj === objCopy); // false (not same reference)
// 修改我们初始的对象不会修改副本:
obj.d = 4;
console.info(JSON.stringify(obj)); // {"a":1,"b":2,"c":3,"d":4}
console.info(JSON.stringify(objCopy)); // {"a":1,"b":2,"c":3}
作用域
代码块
每一个代码块{}都是一个独立的环境,使用const与let声明的变量只在内部可见。
// 代码块一
{
// 显示 message
let message = "Hello";
console.info(message);
}
// 代码块二
{
// 显示另一个 message
let message = "Goodbye";
console.info(message);
}
如果没有代码块进行隔离,那么会出现变量重复定义错误
在for (let i = 0; i < 3; i++)中的let i也属于代码块内部。
代码块嵌套:
内部代码块可以访问外部代码块的变量数据
function makeCounter() {
let count = 0;
return function() {
return count++;
};
}
let counter = makeCounter();
console.info( counter() ); // 0
console.info( counter() ); // 1
console.info( counter() ); // 2
词法环境
Step 1. 变量
- 每个脚本 / 代码块 / 函数都有一个与之关联的词法环境
- 词法环境 = 环境记录(存局部变量等属性) + 外部引用(指向外层词法环境;全局为
null) - 变量就是环境记录的属性;读写变量即读写该内部对象属性
- 执行流程中环境记录状态变化:
- 预填充所有
let变量 → 初始“未初始化” - 执行到
let行 → 变为undefined - 后续赋值 / 修改 → 值更新
- 预填充所有
Step 2. 函数声明
- 函数也是值,保存在环境记录
- 函数声明在词法环境建立阶段立即完成初始化,因此可先调用后声明
- 函数表达式(如
let f = function...)不具备此特性
Step 3. 内部与外部词法环境
- 函数被调用时新建内部词法环境存放参数与局部变量
- 内部环境通过“外部引用”链向全局(或外层)环境
- 变量查找顺序:内部 → 外部 → ... → 全局;找不到则严格模式报错
- 全局词法环境定义的变量为全局变量,其余词法环境定义的变量为局部变量
Step 4. 返回函数(闭包)
- 内部函数“诞生”时获得隐藏属性
[[Environment]],永久保存其创建时的词法环境引用 - 当内部函数被调用:
- 新建当前调用环境
- 当前环境的外部引用 = 被调函数的
[[Environment]]
- 因此即使外部函数已结束,内部函数仍能访问并更新外部变量
- 多次调用同一闭包,均在同一外部环境记录上操作,变量持续累加
闭包(Closure)
- 定义
一个函数能够记住并访问其外部变量的能力,称为闭包。 - JavaScript 中的实现
- 所有函数在“诞生”时自动通过隐藏属性
[[Environment]]保存对创建位置词法环境的引用。 - 因此所有函数天生都是闭包(唯一例外:
new Function语法)。
- 效果
即使外部函数已执行完毕,内部函数仍能通过[[Environment]]链访问并更新外部变量,形成“变量常驻”现象。
垃圾回收
与正常垃圾一样,仅在词法环境可达(存在外部引用)的情况下才会被保留在内存当中,
否则将会被删除。
注意:
JavaScript 引擎会试图优化它。会导致在调试过程中变得困难。
运行以下代码,暂停后输入console.info(value)
会输出 No such variable 错误
function f() {
let value = Math.random();
function g() {
debugger; // 在 Console 中:输入 console.info(value); No such variable!
}
return g;
}
let g = f();
g();
Var
无块级作用域
var会忽略代码块,因此无块级作用域,仅拥有函数作用域和全局作用域
if (true) {
var test = true; // 使用 "var" 而不是 "let"
}
console.info(test); // true,变量在 if 结束后仍存在
同理,对于循环局部作用域也不支持
for (var i = 0; i < 10; i++) {
var one = 1;
// ...
}
console.info(i); // 10,"i" 在循环结束后仍可见,它是一个全局变量
console.info(one); // 1,"one" 在循环结束后仍可见,它是一个全局变量
声明区别
允许重新声明:
会自动忽略已声明变量,只进行赋值操作。
var user = "Pete";
var user = "John"; // 这个 "var" 无效(因为变量已经声明过了)
// ……不会触发错误
console.info(user); // John
允许声明前使用:
在不考虑嵌套的情况下,var声明的变量会被自动移至函数开头,因此允许:
function sayHi() {
phrase = "Hello";
console.info(phrase);
var phrase;
}
sayHi();
但是只是声明被提升,赋值并没有
function sayHi() {
console.info(phrase); // undefined
var phrase = "Hello";
}
sayHi();
由于不存在块级作用域,并且自动提升至函数开头,因此会出现以下情况:
function sayHi() {
phrase = "Hello"; // (*)
if (false) {
var phrase;
}
console.info(phrase);// 正常打印
}
sayHi();
使用立即调用函数模拟块级作用域(借助函数作用域):
(function() { var message = "Hello"; console.info(message); // Hello })();
Var 特性速览
| 特性 | var |
|---|---|
| 块级作用域 | ❌ 无视 if / for / {} 块,只在函数或全局层面有效 |
| 重复声明 | ✅ 同一作用域内可重复声明,后续视为赋值 |
| 声明提升 | ✅ 声明自动提升到函数顶部,赋值留在原地 |
| 暂时性死区 | ❌ 不存在,提升后可在声明前使用(值为 undefined) |
| 全局挂载 | ✅ 在全局作用域声明的变量会成为 window 的属性 |
var vs. let vs. const 对比表
| 维度 | var | let | const |
|---|---|---|---|
| 作用域 | 函数 / 全局 | 块级 {} | 块级 {} |
| 重复声明 | 允许 | 禁止 | 禁止 |
| 声明提升 | 完全提升 | 提升但不可访问(TDZ) | 提升但不可访问(TDZ) |
| 暂时性死区 | 无 | 有 | 有 |
| 是否只读 | 否 | 否 | 是(绑定不可重新赋值) |
| 是否需初始化 | 否 | 否 | 是(声明时必须赋值) |
| 全局对象属性 | 是 | 否 | 否 |
“暂时性死区”(TDZ,Temporal Dead Zone)是 ES6 引入
let/const后出现的一种**语法保护期> **:从块级作用域开始到变量声明语句真正执行前的这段区域 / 时间,变量虽然已存在于词法环境,但任何访问或赋值都会抛
ReferenceError。
只有执行完声明语句,变量才“解冻”,进入可正常使用状态。{ console.log(a); // ReferenceError:处于 TDZ let a = 1; // 声明执行,TDZ 结束 console.log(a); // 1 }
对比 var 的“声明提升+默认 undefined”,TDZ 让“先使用后声明”成为显式错误,避免隐藏 bug。
全局对象
全局对象提供可在任何地方使用的变量和函数。默认情况下,这些全局变量内建于语言或环境中。
比如:
console.info("Hello");
// 等同于
window.console.info("Hello");
Browser与Node
- 在Browser(浏览器)中,全局对象名称为
window - 在Node中,全局对象名称为
global - 在最新标准中,都支持全局对象
globalThis// 在浏览器中 console.info(globalThis == window ); // true // 在Node中 console.info(global == globalThis); // true
全局属性挂载
支持为全局对象增加新的属性和方法
要注意:使用
var声明的变量会自动挂载到全局对象当中var gVar = 5; console.info(globalThis.gVar); // 5(成为了全局对象的属性)
// 将当前用户信息全局化,以允许所有脚本访问它
globalThis.currentUser = {
name: "John"
};
// 代码中的另一个位置
console.info(currentUser.name); // John
// 或者,如果我们有一个名为 "currentUser" 的局部变量
// 从 globalThis 显式地获取它(这是安全的!)
console.info(globalThis.currentUser.name); // John
自定义polyfills
if (!globalThis.Promise) {
globalThis.Promise = ... // 定制实现现代语言功能
}
函数对象 与 NFE
在 JavaScript 中,函数的类型是对象。不仅可以调用它们,还能把它们当作对象来处理:增/删属性,按引用传递等。
名称属性 name
一个函数的名字可以通过属性 “name” 来访问。规范中把这种特性叫做「上下文命名」,在没有具体提供时,会自动根据上下文进行匹配。当无法进行匹配时,则置为空。
function sayHi() {
console.info("Hi");
}
console.info(sayHi.name); // sayHi
// 或者
let sayHi = function() {
console.info("Hi");
};
console.info(sayHi.name); // sayHi(有名字!)
// 甚至
function f(sayHi = function() {}) {
console.info(sayHi.name); // sayHi(生效了!)
}
f();
入参个数 length
要注意:rest参数不参与计数
function f1(a) {}
function f2(a, b) {}
function many(a, b, ...more) {}
console.info(f1.length); // 1
console.info(f2.length); // 2
console.info(many.length); // 2
可以利用其实现函数多态:
function ask(question, ...handlers) {
let isYes = console.info(question);
for(let handler of handlers) {
if (handler.length == 0) {
if (isYes) handler();
} else {
handler(isYes);
}
}
}
// 对于肯定的回答,两个 handler 都会被调用
// 对于否定的回答,只有第二个 handler 被调用
ask("Question?", () => console.info('You said yes'), result => console.info(result));
自定义属性
属性不是变量,变量不是属性,二者毫不相干。
可以借助函数属性实现闭包功能。
闭包的变量无法在外部访问,但函数属性可以在外部访问。
function makeCounter() {
// 不需要这个了
// let count = 0
function counter() {
return counter.count++;
};
counter.count = 0;
return counter;
}
let counter = makeCounter();
console.info( counter() ); // 0
console.info( counter() ); // 1
NFE(命名函数表达式)
NFE特性:
- 允许函数在内部引用自己。
- 在函数外是不可见的。
此处的func即为一个NFE
let sayHi = function func(who) {
if (who) {
console.info(`Hello, ${who}`);
} else {
func("Guest"); // 使用 func 再次调用函数自身
}
};
sayHi(); // Hello, Guest
// 但这不工作:
func(); // Error, func is not defined(在函数外不可见)
为何在嵌套时不使用sayHi呢:
因为当发生将sayHi赋值给其他变量时,同时将sayHi置空,则嵌套的调用就会失效
let sayHi = function(who) {
if (who) {
console.info(`Hello, ${who}`);
} else {
sayHi("Guest"); // Error: sayHi is not a function
}
};
let welcome = sayHi;
sayHi = null;
welcome(); // Error,嵌套调用 sayHi 不再有效!
因此采用NFE,即可避免此问题发生。
new Function
支持从字符串创建函数,但
- 词法环境指向全局,无法访问内部变量
- 经过压缩程序压缩后的代码,变量名称会变化,通过字符串创建的函数则无法直接进行访问
语法:
let func = new Function ([arg1, arg2, ...argN], functionBody);
使用示例:
本质返回一个函数,进行正常调用即可。
new Function('a', 'b', 'return a + b'); // 基础语法
new Function('a,b', 'return a + b'); // 逗号分隔
new Function('a , b', 'return a + b'); // 逗号和空格分隔
延时执行
不希望一个函数立即执行,而是等待特定时间后执行,称为计划调用。
setTimeout
将函数推迟到一段时间间隔之后再执行。
签名
let timerId = setTimeout(func|code, [delay], [arg1], [arg2], ...)
保存返回的
timerId为可选操作
参数
| 参数 | 说明 |
|---|---|
func / code | 要执行的函数或代码字符串(历史遗留,强烈建议用函数) |
delay | 延迟毫秒数,默认 0;实际最小间隔约为 4 ms |
arg1, arg2… | 传给 func 的实参列表(IE9 以下不支持) |
正确 vs 错误示例
function sayHi() {
console.info('Hello');
}
// ✅ 传函数引用
setTimeout(sayHi, 1000, "Hello", "John");
// ✅ 箭头函数
setTimeout(() => console.info('Hello'), 1000);
// ❌ 立即执行,实际传入 undefined
setTimeout(sayHi(), 1000);
返回值
timerId —— 用于稍后 clearTimeout(timerId) 取消调度。
setInterval
重复运行一个函数,从一段时间间隔之后开始运行,之后以该时间间隔连续重复运行该函数。
签名
let timerId = setInterval(func|code, [delay], [arg1], [arg2], ...)
保存返回的
timerId为可选操作
参数
| 参数 | 说明 |
|---|---|
func / code | 要周期性执行的函数或代码字符串(推荐用函数) |
delay | 间隔毫秒数,默认 0;实际最小间隔约为 4 ms |
arg1, arg2… | 传给 func 的实参列表(IE9 以下不支持) |
正确 vs 错误示例
// ✅ 传函数引用
let timerId = setInterval(sayHi, 2000, "Hello", "John");
// ✅ 箭头函数
setInterval(() => console.info('tick'), 1000);
// ❌ 立即执行,实际传入 undefined
setInterval(sayHi(), 1000);
返回值
timerId —— 用于稍后 clearInterval(timerId) 取消循环调度。
取消调度:clearTimeout 与 clearInterval
-
语法
let timerId = setTimeout(...); // 或 setInterval(...) clearTimeout(timerId); // 取消单次 clearInterval(timerId); // 取消循环 -
作用
clearTimeout:在延迟到期前取消已排队的单次任务;取消后回调不会执行。clearInterval:取消正在循环的定时任务;后续周期不再触发。
-
定时器标识符
- 浏览器:返回一个数字。
- Node.js:返回一个定时器对象(含方法)。
标识符类型无统一规范,但均可直接传给对应的clear*方法。
-
示例
let t1 = setTimeout(() => console.info("never happens"), 1000); let t2 = setInterval(() => console.info("tick"), 1000); clearTimeout(t1); // 立即取消单次任务 clearInterval(t2); // 立即停止循环 console.info(t1); // 仍是原标识符(不会变 null)
装饰器与转发,call/apply
装饰器
增强原有函数的功能,所具有的优势:
- 装饰器功能可重用,复用到其他方法上
- 装饰器逻辑独立,没有增加被增强函数的原有复杂度
- 可以使用多个装饰器进行组合调用
// 假设这是一个耗时计算函数
function heavyCal(x) {
return x*x;
}
// heavyCal(2);
function cacheFunc(func) {
let caches = new Map();
return function(key, ...args) {
if ( caches.has(key) ) {
console.info(`has: ${caches.get(key)}`);
}
else {
let res = func(key, ...args);
caches.set(key, res);
console.info(res);
}
};
}
// 获得带有缓存功能的函数
let easyCal = cacheFunc(heavyCal);
easyCal(2); // 4
easyCal(3); // 9
easyCal(2); // has: 4
注意:
装饰器包装后的函数不再具有原函数的内置属性,
因此尽量避免使用函数属性来记录数据,而使用变量。
上下文绑定 func.call
对于对象方法直接使用装饰器进行增强会导致this丢失
let worker = {
someMethod() {
return 1;
},
heavyCal(x) {
return x* this.someMethod(); // (*)
}
};
console.info( worker.heavyCal(1) ); // 原始方法有效
worker.heavyCal = cacheFunc(worker.heavyCal); // 现在对其进行缓存
console.info( worker.heavyCal(2) ); // Error: Cannot read property 'someMethod' of undefined
原因为cacheFunc中执行到let res = func(key, ...args);时,func即为worker.heavyCal,
但worker.heavyCal内部调用了this.someMethod(),此处的this发生了丢失,导致someMethod()方法调用失败,导致出错。
func.call(context, …args)
允许调用一个显式设置 this 的函数。在运行func,提供的第一个参数作为 this,后面的作为参数
function sayHi(phrase) {
console.info(this.name + ': ' + phrase);
}
let user = { name: "John" };
let admin = { name: "Admin" };
// 使用 call 将不同的对象传递为 "this"
sayHi.call( user, "Hello"); // John
sayHi.call( admin, "Hi"); // Admin
修复后的装饰器:
let worker = {
someMethod() {
return 1;
},
heavyCal(x) {
return x* this.someMethod(); // (*)
}
};
console.info( worker.heavyCal(1) ); // 原始方法有效
function cacheFunc(func) {
let caches = new Map();
return function(key, ...args) {
if ( caches.has(key) ) {
console.info(`has: ${caches.get(key)}`);
}
else {
let res = func.call(this, key, ...args);
caches.set(key, res);
console.info(res);
}
};
}
worker.heavyCal = cacheFunc(worker.heavyCal); // 现在对其进行缓存
worker..heavyCal(2);
func.apply
可以替代func.call,唯一的语法区别是,call 期望一个参数列表,而 apply 期望一个包含这些参数的类数组对象。
将所有参数连同上下文一起传递给另一个函数被称为“呼叫转移(call forwarding)”
对于即可迭代又是类数组的对象,使用apply会更快,引擎内部进行了优化。
语法:
func.apply(context, args)
使用apply的装饰器:
function cacheFunc(func, hash) {
let caches = new Map();
return function(...args) {
let key = hash(args);
if ( caches.has(key) ) {
console.info(`has: ${caches.get(key)}`);
}
else {
let res = func.apply(this, args);
caches.set(key, res);
console.info(res);
}
};
}
function hash(...args) {
console.info( args.join(",") ); // 1,2
}
方法借用
对于可迭代以及类数组对象,需要使用数组的内建方法时,可以:
// 以函数内建数据arguments为例(包含函数所有参数)
function hash() {
console.info( [].join.call(arguments) ); // 1,2
}
函数绑定
一、为什么需要“绑定”
- JavaScript 的“this 绑定”是运行时决定的:
obj.method()→ this = obj
const f = obj.method; f()→ this = undefined(严格模式)或 window - 把对象方法当回调(setTimeout、事件监听器、Promise 等)时,调用位置不再位于对象之后,造成“this 丢失”。
- 目标:把 this 固化到指定对象,同时可选地预设部分参数,得到“可安全传递”的新函数。
二、原生工具:Function.prototype.bind
签名:
const boundFunc = func.bind(context[, arg1, arg2, ...])
行为:
- 返回一个“外来对象”,内部按以下规则转发:
- this = context(不可再被 call/apply 改变)
- 预设参数 = arg1, arg2...(排在最前)
- 其余参数由调用时继续追加
- 立即生成,不执行原函数;可多次绑定形成链。
- 无原型(boundFunc.prototype 未定义),因此不能当构造函数用(new 会抛 TypeError)。
典型范式
// 1. 只锁 this
const log = console.log.bind(console);
log('hello'); // this 永远指向 console
// 2. 锁 this + 部分参数(柯里化雏形)
function mul(a, b) { return a * b; }
const double = mul.bind(null, 2); // 乘数固定为 2
double(7); // 14
三、bind 解决 this 丢失的三种场景(含丢→错→修)
| 场景 | 丢失现场 | 结果 | 修复代码(bind) |
|---|---|---|---|
| ① setTimeout | setTimeout(user.greet, 1000) | Hi, undefined | setTimeout(user.greet.bind(user), 1000) |
| ② DOM 事件 | button.addEventListener('click', obj.inc) | NaN(this=button) | button.addEventListener('click', obj.inc.bind(obj)) |
| ③ 解构导出 | const { get } = api; get() | TypeError 找不到 base | api.get = api.get.bind(api); 再导出 |
完整前后对比示例
// ① 异步回调
let user = { name: 'John', greet() { console.log('Hi, ' + this.name); } };
setTimeout(user.greet, 1000); // → Hi, undefined
setTimeout(user.greet.bind(user), 1000); // → Hi, John (this 永固)
// ② 事件监听
let counter = { val: 0, inc() { this.val++; console.log(this.val); } };
button.addEventListener('click', counter.inc); // 点一次 NaN
button.addEventListener('click', counter.inc.bind(counter)); // 点一次 1 2 3...
// ③ 批量导出
let api = { base: 'https://api.io', get(p) { return this.base + p; } };
const { get } = api; // 解构后 this 丢失
get('/users'); // TypeError
// 批量绑定后任意用
for (const k in api) if (typeof api[k] === 'function') api[k] = api[k].bind(api);
get('/users'); // https://api.io/users
四、partial —— “只锁参数,不锁 this”
当不想固定上下文,只想预设参数时,原生 bind 强制传 context,手写 partial 更干净:
function partial(fn, ...preset) {
return function(...args) {
return fn.call(this, ...preset, ...args); // 保留当前 this
};
}
let user = {
firstName: "John",
say(time, phrase) {
console.info(`[${time}] ${this.firstName}: ${phrase}!`);
}
};
// 示例:把当前时间注入日志,但保留运行时 this
user.sayNow = partial(user.say, `${Date.now()}`);
user.sayNow('ms') // [1639...] John: ms!
lodash 的 _.partial 同理。
五、bind vs 箭头函数
| 维度 | bind | 箭头函数 |
|---|---|---|
| this 来源 | 显式指定一次,不可再变 | 词法捕获(声明处外层 this) |
| 是否创建新对象 | 是( exotic function ) | 否(原始函数形态) |
| 能否 new | ❌ | ❌ |
| 参数预设 | ✅ 原生支持 | ❌ 需再包一层 |
| 多次重用 | 同一段逻辑可生成多个绑定实例 | 每次写 => 都是新函数 |
| 性能 | 多一次包装,内存略高 | 无运行时成本,但闭包变量常驻 |
口诀:
- 要固定 this 或预设参数 →
bind - 只要词法 this 且代码简洁 →
=> - 只想预设参数、保留动态 this →
partial
六、常见误区 & 最佳实践
-
在构造函数里绑定方法
class Counter { constructor() { this.inc = this.inc.bind(this); } // 每个实例共享同一逻辑 }保证把实例方法当回调时 this 不丢,且所有子类继承后仍有效。
-
不要过度绑定
只在“方法会被脱离对象调用”时绑;否则增加内存(每个实例一份新函数)。 -
React / Vue 事件
- 类组件:构造函数内绑定 或 类字段箭头函数
- 函数组件:直接用箭头函数定义事件处理器即可
-
记得清理
绑定后的函数无法通过removeEventListener(fn)卸载(因为是新引用),需保留同一变量:const boundClick = handler.bind(this); element.addEventListener('click', boundClick); // 卸载时 element.removeEventListener('click', boundClick);
七、一句话记住
“this 弄丢 → bind 回;参数想留 → partial 走;词法不变 → 箭头就够!”
箭头函数详解
1. 没有自己的 this
访问 this 时沿词法作用域向外查找,因此嵌套在对象方法里仍可正确使用外部 this。
let group = {
title: "Our Group",
students: ["John", "Pete", "Alice"],
showList() {
this.students.forEach(st => console.log(this.title + ': ' + st));
}
};
group.showList(); // Our Group: John ...
2. 没有 arguments 对象
内部 arguments 也是沿词法链从外部获取,适合透明转发。
function defer(f, ms) {
return function (...args) {
setTimeout(() => f.apply(this, args), ms);
};
}
function sayHi(who) { console.log('Hello ' + who); }
let sayHiDeferred = defer(sayHi, 1000);
sayHiDeferred('World'); // 1s 后 Hello World
3. 不能当构造函数
由于缺失 this,不能用 new 调用。
const Foo = (name) => this.name = name;
new Foo('Tom'); // TypeError: Foo is not a constructor
4. 对比 .bind(this)
.bind(this)显式创建绑定了this的新函数- 箭头函数单纯“没有
this”,查找方式与普通变量一致,更简洁且无需保存中间变量
function normal() { console.log(this.id); }
const bound = normal.bind({ id: 42 });
bound(); // 42
const arrow = () => console.log(this.id);
arrow.call({ id: 99 }); // 依旧取外部 this,无效化 call
5. 典型场景
回调、迭代器、延迟执行等需要保留当前上下文和参数时,用箭头函数可直接使用外部 this 与 arguments,省去 let self = this 或额外闭包变量。