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. 最佳实践

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