006-call-apply-bind详解

call、apply、bind 深度解析

核心区别总结

特性 call apply bind
执行方式 立即执行 立即执行 返回绑定函数
参数格式 参数列表 (arg1, arg2) 参数数组 [arg1, arg2] 参数列表 + 后续参数
返回值 函数执行结果 函数执行结果 绑定this的新函数
使用场景 明确参数个数时 动态参数个数时 需要延迟执行时

详细对比与示例

1. call 方法

语法func.call(thisArg, arg1, arg2, ...)

1
2
3
4
5
6
7
8
function greet(message) {
console.log(`${message}, ${this.name}!`);
}

const user = { name: 'Alice' };

// 使用call立即调用,绑定user作为this
greet.call(user, 'Hello'); // "Hello, Alice!"

2. apply 方法

语法func.apply(thisArg, [argsArray])

1
2
3
4
5
6
7
8
9
10
11
function introduce(job, hobby) {
console.log(`我是${this.name}, 职业${job}, 爱好${hobby}`);
}

const user = { name: 'Bob' };

// 使用apply立即调用,参数通过数组传递
introduce.apply(user, ['工程师', '登山']); // "我是Bob, 职业工程师, 爱好登山"

// 数组最大值(经典应用)
Math.max.apply(null, [1, 5, 3]); // 5

3. bind 方法

语法const boundFunc = func.bind(thisArg, arg1, arg2, ...)

1
2
3
4
5
6
7
8
9
10
11
function showScore(subject) {
console.log(`${this.name}${subject}成绩:${this.score}`);
}

const student = { name: 'Carol', score: 95 };

// 创建绑定函数
const showMathScore = showScore.bind(student, '数学');

// 延迟执行
setTimeout(showMathScore, 1000); // "Carol的数学成绩:95"

bind 实现原理

基础版 bind 实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
Function.prototype.myBind = function(context, ...bindArgs) {
const self = this; // 保存原函数

return function(...callArgs) {
// 合并预置参数和调用时参数
const allArgs = bindArgs.concat(callArgs);

// 执行原函数并返回结果
return self.apply(context, allArgs);
};
};

// 测试
const boundFunc = greet.myBind(user, 'Hello');
boundFunc(); // "Hello, Alice!"

增强版(支持 new 操作)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
Function.prototype.myBind = function(context, ...bindArgs) {
const self = this;

const boundFunc = function(...callArgs) {
const allArgs = bindArgs.concat(callArgs);

// 判断是否通过new调用
const isNewCall = this instanceof boundFunc;

return self.apply(
isNewCall ? this : context,
allArgs
);
};

// 继承原型链(保持instanceof关系)
boundFunc.prototype = Object.create(self.prototype);

return boundFunc;
};

// 测试构造函数
function Person(name) {
this.name = name;
}
const BoundPerson = Person.myBind(null, 'Dave');
const p = new BoundPerson();
console.log(p instanceof Person); // true

bind 后的 this 能否修改?

关键结论

bind 创建的函数无法通过 call/apply 再次修改 this 绑定
原因:bind 返回的是绑定函数(Bound Function),其内部已固定 this 指向

验证实验

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
const obj1 = { name: '对象1' };
const obj2 = { name: '对象2' };

function showName() {
console.log(this.name);
}

// 绑定到obj1
const boundFunc = showName.bind(obj1);

// 尝试用call修改this
boundFunc.call(obj2); // 输出:"对象1"(未被修改)

// 尝试用apply修改this
boundFunc.apply(obj2); // 输出:"对象1"(未被修改)

// 尝试二次bind
const reboundFunc = boundFunc.bind(obj2);
reboundFunc(); // 输出:"对象1"(仍未被修改)

原理分析

  1. 绑定函数特性
    ECMAScript 规范规定:绑定函数在调用时会忽略传入的 this 值,始终使用创建时绑定的 this

  2. **内部属性 [[BoundThis]]**
    绑定函数内部维护一个 [[BoundThis]] 属性,存储原始绑定值

  3. 调用优先级
    绑定函数的执行优先级:[[BoundThis]] > call/apply 参数 > 默认绑定

特殊情况:new 操作符

1
2
3
4
5
6
7
8
9
10
function User(name) {
this.name = name;
}

const Admin = User.bind({ role: 'admin' });

// 通过new调用
const admin = new Admin('SuperUser');
console.log(admin.name); // "SuperUser"
console.log(admin.role); // undefined

原理
当使用 new 操作符调用绑定函数时:

  1. 创建新对象(忽略 [[BoundThis]])
  2. 执行构造函数初始化
  3. 新对象的原型指向绑定函数的 prototype

实际应用场景

场景1:事件处理(React)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
class Button extends React.Component {
constructor() {
super();
// 提前绑定避免每次渲染创建新函数
this.handleClick = this.handleClick.bind(this);
}

handleClick() {
console.log(this.props.id);
}

render() {
return <button onClick={this.handleClick}>点击</button>;
}
}

场景2:部分参数应用

1
2
3
4
5
6
7
8
9
10
11
12
// 创建通用日志函数
function log(level, message) {
console.log(`[${level}] ${message}`);
}

// 绑定第一个参数
const logError = log.bind(null, 'ERROR');
const logInfo = log.bind(null, 'INFO');

// 直接使用
logError('数据库连接失败'); // [ERROR] 数据库连接失败
logInfo('用户登录成功'); // [INFO] 用户登录成功

场景3:定时器回调

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
class Timer {
constructor() {
this.seconds = 0;
// 绑定保证回调中的this正确
this.tick = this.tick.bind(this);
}

start() {
setInterval(this.tick, 1000);
}

tick() {
this.seconds++;
console.log(`已运行:${this.seconds}秒`);
}
}

原型链关系图

1
2
3
4
5
6
7
8
9
10
graph TD
A[原生函数] --> B[bind创建]
B --> C[绑定函数]
C --> D[[内部属性]]
D --> E[[BoundThis]]
D --> F[[BoundArguments]]
D --> G[[BoundTargetFunction]]
H[new操作] --> I[创建新对象]
I --> J[忽略BoundThis]
J --> K[执行构造函数]

面试要点总结

  1. bind 后的函数特性

    • 无法通过 call/apply 修改 this 绑定
    • 参数可以部分应用(柯里化)
    • 使用 new 调用时忽略绑定的 this
  2. 设计原理

    • 绑定函数内部维护 [[BoundThis]] 属性
    • 执行时优先使用 [[BoundThis]]
    • 规范定义的绑定函数行为不可变
  3. 最佳实践

    • 类组件中在构造函数提前绑定
    • 避免在渲染中创建绑定函数(性能优化)
    • 利用参数绑定简化函数调用

007-闭包的概念和使用场景

闭包:概念解析与应用场景全解

闭包核心概念

定义

闭包(Closure) 是指能够访问其外部函数作用域中变量的函数,即使外部函数已经执行结束。闭包由两部分组成:

  1. 函数
  2. 创建该函数时所在的作用域环境

关键特性

特性 说明
作用域保留 闭包会保留其外部函数的作用域链,即使外部函数已执行完毕
变量持久化 外部函数的变量不会被垃圾回收,而是被闭包长期引用
状态封装 通过闭包可以创建私有变量,实现数据封装

闭包经典应用场景

场景1:模块化开发(私有变量封装)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
// 计数器模块
const counter = (function() {
// 私有变量
let count = 0;

// 公共接口
return {
increment() {
return ++count;
},
decrement() {
return --count;
},
getValue() {
return count;
}
};
})();

console.log(counter.getValue()); // 0
counter.increment();
counter.increment();
console.log(counter.getValue()); // 2
counter.decrement();
console.log(counter.getValue()); // 1

场景2:函数工厂(动态函数创建)

1
2
3
4
5
6
7
8
9
10
11
12
13
// 创建幂函数工厂
function powerFactory(exponent) {
return function(base) {
return Math.pow(base, exponent);
};
}

// 创建特定幂函数
const square = powerFactory(2);
const cube = powerFactory(3);

console.log(square(4)); // 16 (4^2)
console.log(cube(3)); // 27 (3^3)

场景3:事件处理(保留上下文)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
// 按钮列表事件处理
function setupButtons() {
const buttons = document.querySelectorAll('.btn');

for (var i = 0; i < buttons.length; i++) {
// 使用闭包保留每个按钮的索引
(function(index) {
buttons[index].addEventListener('click', function() {
console.log(`按钮 ${index + 1} 被点击`);
});
})(i);
}
}

// 替代方案(使用let块级作用域)
function setupButtonsModern() {
const buttons = document.querySelectorAll('.btn');

for (let i = 0; i < buttons.length; i++) {
buttons[i].addEventListener('click', function() {
console.log(`按钮 ${i + 1} 被点击`);
});
}
}

场景4:函数柯里化

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// 柯里化函数
function curry(fn) {
return function curried(...args) {
if (args.length >= fn.length) {
return fn.apply(this, args);
} else {
return (...nextArgs) => curried.apply(this, [...args, ...nextArgs]);
}
};
}

// 使用柯里化
const add = curry((a, b, c) => a + b + c);
const addFive = add(2)(3);
console.log(addFive(5)); // 10 (2+3+5)

场景5:状态管理

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
// 状态管理器
function createState(initialValue) {
let state = initialValue;

return {
get() {
return state;
},
set(newValue) {
state = newValue;
},
// 高级功能:状态变更订阅
subscribe(callback) {
const listener = () => callback(state);
// 模拟订阅机制
document.addEventListener('stateChange', listener);

// 返回取消订阅函数
return () => {
document.removeEventListener('stateChange', listener);
};
}
};
}

// 使用状态管理器
const userState = createState({ name: 'Alice', age: 30 });
console.log(userState.get()); // {name: 'Alice', age: 30}
userState.set({ name: 'Bob', age: 25 });
console.log(userState.get()); // {name: 'Bob', age: 25}

场景6:性能优化(记忆化)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
// 记忆化函数
function memoize(fn) {
const cache = new Map();

return function(...args) {
// 创建缓存键
const key = JSON.stringify(args);

// 检查缓存
if (cache.has(key)) {
return cache.get(key);
}

// 计算并缓存结果
const result = fn.apply(this, args);
cache.set(key, result);
return result;
};
}

// 使用记忆化
const fibonacci = memoize(function(n) {
if (n <= 1) return n;
return fibonacci(n - 1) + fibonacci(n - 2);
});

console.log(fibonacci(10)); // 55(首次计算)
console.log(fibonacci(10)); // 55(从缓存读取)

React/Vue 中的闭包应用

React Hooks 示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
function Counter() {
// useState 内部使用闭包保存状态
const [count, setCount] = useState(0);

// useEffect 使用闭包保留回调函数
useEffect(() => {
const timer = setInterval(() => {
// 闭包保留最新count值
setCount(c => c + 1);
}, 1000);

return () => clearInterval(timer);
}, []); // 依赖数组为空,只运行一次

return <div>计数: {count}</div>;
}

Vue Composition API 示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
<script setup>
import { ref, onMounted, onUnmounted } from 'vue';

// ref 使用闭包保存响应式状态
const count = ref(0);

// 定时器使用闭包访问最新值
onMounted(() => {
const timer = setInterval(() => {
count.value++;
}, 1000);

// 清理函数使用闭包
onUnmounted(() => {
clearInterval(timer);
});
});
</script>

<template>
<div>计数: {{ count }}</div>
</template>

闭包常见问题与解决方案

问题1:循环中的闭包陷阱

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
// 错误示例
for (var i = 0; i < 5; i++) {
setTimeout(function() {
console.log(i); // 全部输出5
}, 100);
}

// 解决方案1:IIFE创建闭包
for (var i = 0; i < 5; i++) {
(function(j) {
setTimeout(function() {
console.log(j); // 0,1,2,3,4
}, 100);
})(i);
}

// 解决方案2:使用let块级作用域
for (let i = 0; i < 5; i++) {
setTimeout(function() {
console.log(i); // 0,1,2,3,4
}, 100);
}

问题2:内存泄漏

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
// 大型对象保留在闭包中
function createHeavyObjectHolder() {
const largeObject = new Array(1000000).fill('data');

return {
getData() {
return largeObject[0];
}
};
}

// 解决方案:不再需要时解除引用
function createSafeHolder() {
let largeObject = new Array(1000000).fill('data');

const api = {
getData() {
return largeObject[0];
}
};

// 提供清理方法
api.cleanup = () => {
largeObject = null;
};

return api;
}

// 使用
const holder = createSafeHolder();
// 使用完成后清理
holder.cleanup();

闭包性能优化技巧

  1. 避免不必要的闭包:只在需要封装状态时使用
  2. 最小化闭包范围:减少闭包中引用的变量数量
  3. 及时解除引用:不再需要的闭包手动设置为null
  4. 使用模块模式:替代多个小型闭包
  5. 避免循环中创建闭包:改用块级作用域变量

闭包与垃圾回收

1
2
3
4
5
6
graph LR
A[函数执行完毕] --> B{内部变量是否被引用}
B -->|被闭包引用| C[保留在内存中]
B -->|未被引用| D[标记为可回收]
C --> E[闭包不再被引用]
E --> D

经典面试题解析

题目1:闭包输出问题

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
function createFunctions() {
var result = [];

for (var i = 0; i < 3; i++) {
result[i] = function() {
return i;
};
}

return result;
}

var funcs = createFunctions();
console.log(funcs[0]()); // 3
console.log(funcs[1]()); // 3
console.log(funcs[2]()); // 3

解析:所有闭包共享同一个变量i,循环结束时i=3

题目2:闭包计数器

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
function createCounter() {
let count = 0;
return {
increment: () => count++,
get: () => count,
reset: () => { count = 0; }
};
}

const c = createCounter();
c.increment();
c.increment();
console.log(c.get()); // 2
c.reset();
console.log(c.get()); // 0

解析:通过闭包实现私有变量count的封装

总结

闭包核心价值

应用方向 价值体现
数据封装 创建私有变量,实现信息隐藏
状态保持 长期保存数据,不受外部函数执行结束影响
函数工厂 动态生成特定配置的函数
模块化 构建独立作用域,避免全局污染
高阶函数 实现函数组合、柯里化等高级功能
异步编程 在回调中保持上下文一致性

使用原则

  1. 必要性原则:只在需要封装状态时使用闭包
  2. 最小化原则:保持闭包作用域尽可能小
  3. 可控性原则:提供清理机制避免内存泄漏
  4. 可维护性原则:避免过度嵌套的闭包结构

闭包是JavaScript最强大的特性之一,合理使用闭包可以:

  • ✅ 创建优雅的模块化代码
  • ✅ 实现高效的状态管理
  • ✅ 封装复杂的业务逻辑
  • ✅ 构建可复用的函数工厂

但同时要注意:

  • ❌ 避免不必要的内存占用
  • ❌ 防止意外的变量共享
  • ❌ 警惕循环引用导致的内存泄漏