想象你有一个通用的工具函数,但你希望在不同的上下文中使用它。JavaScript 的 call、apply 和 bind 方法就像是三个魔法师,可以让你精确地控制函数中 this 的指向,让你的代码更加灵活和可复用。
让我们先看一个简单的场景:
javascript
const person = {
name: "Alice",
age: 30,
introduce() {
return `Hi, I'm ${this.name} and I'm ${this.age} years old`;
},
};
const anotherPerson = {
name: "Bob",
age: 25,
};
console.log(person.introduce()); // "Hi, I'm Alice and I'm 30 years old"
// 如何让 person.introduce 在 anotherPerson 的上下文中执行?
console.log(person.introduce.call(anotherPerson)); // "Hi, I'm Bob and I'm 25 years old"通过 call 方法,我们成功让 introduce 函数在 anotherPerson 的上下文中执行了!
Call 方法:直接调用与参数传递
call 方法允许你调用一个函数,并明确指定函数运行时的 this 值以及参数。
基本语法
javascript
functionName.call(thisArg, arg1, arg2, ..., argN)thisArg:函数运行时使用的this值arg1, arg2, ..., argN:传递给函数的参数列表
使用场景
1. 函数借用
javascript
const arrayLike = {
0: "apple",
1: "banana",
2: "orange",
length: 3,
};
// 借用 Array.prototype.slice 方法
const realArray = Array.prototype.slice.call(arrayLike);
console.log(realArray); // ['apple', 'banana', 'orange']
console.log(Array.isArray(realArray)); // true2. 改变 this 指向
javascript
function greet(greeting, punctuation) {
return `${greeting}, ${this.name}${punctuation}`;
}
const person = {
name: "John",
};
console.log(greet.call(person, "Hello", "!")); // "Hello, John!"
console.log(greet.call(person, "Hi", ".")); // "Hi, John."3. 构造函数继承
javascript
function Parent(name) {
this.name = name;
this.colors = ["red", "blue", "green"];
}
function Child(name, age) {
// 继承 Parent 的属性
Parent.call(this, name);
this.age = age;
}
const child = new Child("Emma", 12);
console.log(child.name); // 'Emma'
console.log(child.colors); // ['red', 'blue', 'green']
console.log(child.age); // 12Apply 方法:数组参数的魔法
apply 方法与 call 类似,但它接受一个数组(或类数组对象)作为参数。
基本语法
javascript
functionName.apply(thisArg, [argsArray]);thisArg:函数运行时使用的this值argsArray:包含要传递给函数参数的数组或类数组对象
使用场景
1. 数组操作
javascript
const numbers = [5, 2, 8, 1, 9, 3];
// 找出最大值
const max = Math.max.apply(null, numbers);
console.log(max); // 9
// 找出最小值
const min = Math.min.apply(null, numbers);
console.log(min); // 12. 函数参数组装
javascript
function calculateTotal(price, tax, discount, shipping) {
return price + tax - discount + shipping;
}
const costs = [100, 8, 10, 15];
const total = calculateTotal.apply(null, costs);
console.log(total); // 1133. 链式调用
javascript
const methods = ["push", "pop", "shift", "unshift"];
const array = [1, 2, 3];
methods.forEach((method) => {
if (method === "push") {
Array.prototype[method].apply(array, [4]);
}
});
console.log(array); // [1, 2, 3, 4]Bind 方法:永久绑定的艺术
bind 方法创建一个新函数,当被调用时,其 this 值为 bind 的第一个参数,其余参数将作为新函数的参数供调用时使用。
基本语法
javascript
const newFunction = functionName.bind(thisArg, arg1, arg2, ..., argN)thisArg:新函数运行时使用的this值arg1, arg2, ..., argN:预设的参数
使用场景
1. 永久绑定 this
javascript
const counter = {
count: 0,
increment() {
this.count++;
console.log(this.count);
},
};
const increment = counter.increment;
// 不使用 bind,this 指向全局对象
increment(); // NaN (全局对象没有 count 属性)
// 使用 bind 绑定正确的 this
const boundIncrement = counter.increment.bind(counter);
boundIncrement(); // 1
boundIncrement(); // 22. 预设参数(柯里化)
javascript
function multiply(a, b) {
return a * b;
}
// 创建固定第一个参数的函数
const double = multiply.bind(null, 2);
const triple = multiply.bind(null, 3);
console.log(double(5)); // 10
console.log(triple(5)); // 15
// 预设多个参数
function discountPrice(price, discountRate, tax) {
return price * (1 - discountRate) + tax;
}
const applyTenPercentDiscount = discountPrice.bind(null, 0.1);
const finalPrice = applyTenPercentDiscount(100, 5);
console.log(finalPrice); // 953. 事件处理器中的 this
javascript
const button = {
text: "Click me",
clicks: 0,
init() {
const buttonElement = document.getElementById("myButton");
// 不使用 bind,this 指向 button 元素
buttonElement.addEventListener("click", function () {
this.clicks++; // 错误:this 指向 button 元素
console.log(this.text); // undefined
});
// 使用 bind 正确绑定 this
buttonElement.addEventListener(
"click",
function () {
this.clicks++;
console.log(`${this.text} clicked ${this.clicks} times`);
}.bind(this)
);
},
};4. 定时器中的 this
javascript
const timer = {
seconds: 0,
start() {
setInterval(
function () {
this.seconds++;
console.log(`Timer: ${this.seconds} seconds`);
}.bind(this),
1000
);
},
};
timer.start(); // Timer: 1 seconds, Timer: 2 seconds, ...Call vs Apply vs Bind 的对比
语法对比
javascript
function greet(greeting, punctuation) {
return `${greeting}, ${this.name}${punctuation}`;
}
const person = { name: "Sarah" };
// Call:逐个传递参数
console.log(greet.call(person, "Hello", "!")); // "Hello, Sarah!"
// Apply:通过数组传递参数
console.log(greet.apply(person, ["Hi", "."])); // "Hi, Sarah."
// Bind:创建新函数,可以后续调用
const boundGreet = greet.bind(person, "Hey");
console.log(boundGreet("?")); // "Hey, Sarah?"执行时机对比
javascript
function showTime() {
console.log(new Date().toLocaleTimeString());
}
// Call 和 Apply:立即执行
showTime.call(null); // 立即显示时间
showTime.apply(null); // 立即显示时间
// Bind:创建新函数,延迟执行
const delayedShowTime = showTime.bind(null);
setTimeout(delayedShowTime, 1000); // 1秒后显示时间高级应用技巧
1. 函数方法链
javascript
const calculator = {
value: 0,
add(num) {
this.value += num;
return this; // 返回自身以支持链式调用
},
multiply(num) {
this.value *= num;
return this;
},
subtract(num) {
this.value -= num;
return this;
},
getValue() {
return this.value;
},
};
const result = calculator.add(5).multiply(2).subtract(3).getValue();
console.log(result); // 72. 借用构造函数
javascript
function EventEmitter() {
this.events = {};
}
EventEmitter.prototype.on = function (event, callback) {
if (!this.events[event]) {
this.events[event] = [];
}
this.events[event].push(callback);
};
EventEmitter.prototype.emit = function (event, ...args) {
if (this.events[event]) {
this.events[event].forEach((callback) => callback.apply(this, args));
}
};
function User(name) {
EventEmitter.call(this); // 借用 EventEmitter 的构造函数
this.name = name;
}
// 继承原型方法
User.prototype = Object.create(EventEmitter.prototype);
User.prototype.constructor = User;
const user = new User("John");
user.on("login", function () {
console.log(`${this.name} logged in`);
});
user.emit("login"); // "John logged in"3. 数组操作工具函数
javascript
const arrayUtils = {
// 借用 Array.prototype 方法
slice: Array.prototype.slice,
map: Array.prototype.map,
filter: Array.prototype.filter,
// 转换类数组为真正数组
toArray(list) {
return this.slice.call(list);
},
// 过滤对象数组
filterByProperty(array, property, value) {
return this.filter.call(array, function (item) {
return item[property] === value;
});
},
};
const nodeList = document.querySelectorAll("div");
const divs = arrayUtils.toArray(nodeList);
const users = [
{ name: "Alice", age: 25, role: "admin" },
{ name: "Bob", age: 30, role: "user" },
{ name: "Charlie", age: 35, role: "admin" },
];
const admins = arrayUtils.filterByProperty(users, "role", "admin");
console.log(admins); // [{ name: 'Alice', age: 25, role: 'admin' }, { name: 'Charlie', age: 35, role: 'admin' }]性能考虑
1. 避免过度使用 bind
javascript
// 不推荐:每次渲染都创建新函数
function Component() {
this.handleClick = function () {
console.log("clicked");
}.bind(this);
// 每次 Component 实例化都会创建新的 handleClick 函数
}
// 推荐:在原型上定义方法
function Component() {
// 构造函数
}
Component.prototype.handleClick = function () {
console.log("clicked");
};
// 使用时绑定
const component = new Component();
element.addEventListener("click", component.handleClick.bind(component));2. 使用 apply 处理大量参数
javascript
// 处理可变数量的参数
function sum() {
return Array.prototype.reduce.call(
arguments,
function (total, num) {
return total + num;
},
0
);
}
// 使用 apply 传递参数数组
const numbers = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10];
const total = sum.apply(null, numbers);
console.log(total); // 55实际项目应用
1. React 组件中的事件处理
javascript
class Counter extends React.Component {
constructor(props) {
super(props);
this.state = { count: 0 };
// 在构造函数中绑定,避免每次渲染都创建新函数
this.increment = this.increment.bind(this);
this.decrement = this.decrement.bind(this);
}
increment() {
this.setState((prevState) => ({ count: prevState.count + 1 }));
}
decrement() {
this.setState((prevState) => ({ count: prevState.count - 1 }));
}
render() {
return (
<div>
<button onClick={this.increment}>+</button>
<span>{this.state.count}</span>
<button onClick={this.decrement}>-</button>
</div>
);
}
}2. 函数式编程中的组合
javascript
// 创建通用的函数组合器
function compose() {
const funcs = Array.prototype.slice.call(arguments);
return function () {
const args = Array.prototype.slice.call(arguments);
return funcs.reduceRight(function (acc, func) {
return func.apply(null, Array.isArray(acc) ? acc : [acc]);
}, args);
};
}
const add = (a, b) => a + b;
const multiply = (a, b) => a * b;
const square = (x) => x * x;
const addAndSquare = compose(square, add);
const multiplyAndAdd = compose(add, multiply);
console.log(addAndSquare(3, 4)); // 49 ((3 + 4) * (3 + 4))
console.log(multiplyAndAdd(3, 4)); // 12 (3 * 4 + 0)3. 借用 Array 方法处理字符串
javascript
String.prototype.reverse = function () {
return Array.prototype.reduce.call(
this,
function (acc, char) {
return char + acc;
},
""
);
};
String.prototype.uniqueChars = function () {
return Array.prototype.filter
.call(this, function (char, index, arr) {
return arr.indexOf(char) === index;
})
.join("");
};
console.log("hello".reverse()); // "olleh"
console.log("banana".uniqueChars()); // "ban"常见陷阱与解决方案
1. bind 不能被重新绑定
javascript
function greet() {
return this.name;
}
const person1 = { name: "Alice" };
const person2 = { name: "Bob" };
const boundGreet = greet.bind(person1);
console.log(boundGreet()); // "Alice"
// 尝试重新绑定会失败
const reboundGreet = boundGreet.bind(person2);
console.log(reboundGreet()); // "Alice",仍然是 Alice
// 解决方案:创建原始函数的新绑定
const newBoundGreet = greet.bind(person2);
console.log(newBoundGreet()); // "Bob"2. 构造函数中的 bind
javascript
function Person(name) {
this.name = name;
}
Person.prototype.sayHello = function () {
return `Hello, ${this.name}`;
};
const boundSayHello = Person.prototype.sayHello.bind({ name: "Stranger" });
const person = new Person("John");
console.log(boundSayHello.call(person)); // "Hello, Stranger",不会改变绑定的 this总结
call、apply 和 bind 是 JavaScript 中控制 this 指向的三大魔法师:
- call:立即执行函数,逐个传递参数
- apply:立即执行函数,通过数组传递参数
- bind:创建新函数,预设
this和参数,延迟执行
这三个方法为 JavaScript 函数式编程提供了强大的灵活性,让你能够:
- 借用其他对象的方法
- 改变函数执行上下文
- 预设函数参数
- 处理可变参数列表
- 实现函数组合和柯里化
掌握这三个方法,你就掌握了 JavaScript 中 this 指向控制的核心技能,能够写出更加灵活和可复用的代码。