JavaScript中的call、apply与bind方法
一、JavaScript 中的 this 指向
在深入了解 call
、apply
与 bind
方法之前,我们先来回顾一下 JavaScript 中 this
的指向问题。this
的指向在函数定义的时候是确定不了的,只有函数执行的时候才能确定 this
到底指向谁,实际上 this
的最终指向的是那个调用它的对象。
1.1 全局作用域中的 this
在全局作用域中,this
指向全局对象。在浏览器环境中,全局对象是 window
;在 Node.js 环境中,全局对象是 global
。
console.log(this === window); // true
function logThis() {
console.log(this === window); // true
}
logThis();
1.2 函数作为对象方法调用时的 this
当函数作为对象的方法被调用时,this
指向该对象。
const person = {
name: 'John',
sayHello: function () {
console.log(`Hello, I'm ${this.name}`);
}
};
person.sayHello(); // Hello, I'm John
1.3 普通函数调用时的 this
当函数不是作为对象的方法调用时,也就是普通函数调用,在非严格模式下,this
指向全局对象(浏览器中是 window
);在严格模式下,this
是 undefined
。
// 非严格模式
function sayHi() {
console.log(this === window); // true
}
sayHi();
// 严格模式
function sayGoodbye() {
'use strict';
console.log(this); // undefined
}
sayGoodbye();
1.4 构造函数调用时的 this
当使用 new
关键字调用构造函数时,this
指向新创建的对象实例。
function Person(name) {
this.name = name;
this.sayName = function () {
console.log(`My name is ${this.name}`);
};
}
const tom = new Person('Tom');
tom.sayName(); // My name is Tom
二、call 方法
call
方法是 JavaScript 中所有函数都可以使用的方法,它的作用是改变函数内部 this
的指向,并立即执行该函数。
2.1 call 方法的语法
function.call(thisArg, arg1, arg2, ...)
thisArg
:在function
函数运行时指定的this
值。如果这个参数为空,在非严格模式下,则默认指向全局对象(浏览器中是window
),在严格模式下为undefined
。arg1, arg2, ...
:传递给function
函数的参数列表。
2.2 call 方法的示例
- 改变函数内部 this 指向
const person1 = {
name: 'Alice',
sayHello: function () {
console.log(`Hello, I'm ${this.name}`);
}
};
const person2 = {
name: 'Bob'
};
person1.sayHello.call(person2); // Hello, I'm Bob
在这个例子中,原本 person1.sayHello
函数内部的 this
指向 person1
,通过 call
方法,将 this
指向了 person2
,所以输出的是 Bob
。
- 实现继承
function Animal(name) {
this.name = name;
this.speak = function () {
console.log(`${this.name} makes a sound.`);
};
}
function Dog(name, breed) {
Animal.call(this, name);
this.breed = breed;
}
const myDog = new Dog('Buddy', 'Golden Retriever');
myDog.speak(); // Buddy makes a sound.
console.log(myDog.breed); // Golden Retriever
这里通过 call
方法,在 Dog
构造函数内部调用 Animal
构造函数,从而实现了继承。Animal
构造函数中的 this
被指向了新创建的 Dog
实例,这样 myDog
就拥有了 Animal
的属性和方法。
2.3 call 方法的原理
我们可以手动模拟实现一个 call
方法来深入理解它的原理。
Function.prototype.myCall = function (thisArg, ...args) {
if (typeof this!== 'function') {
throw new TypeError('this is not a function');
}
thisArg = thisArg || window;
const fnSymbol = Symbol('fn');
thisArg[fnSymbol] = this;
const result = thisArg[fnSymbol](...args);
delete thisArg[fnSymbol];
return result;
};
- 首先检查调用
myCall
的是否是一个函数,如果不是则抛出错误。 - 确定
thisArg
,如果没有传入则默认指向全局对象。 - 使用一个唯一的
Symbol
作为属性名,将调用myCall
的函数挂载到thisArg
上。 - 通过
thisArg[fnSymbol](...args)
执行函数,并传递参数,获取执行结果。 - 删除挂载在
thisArg
上的函数,避免污染thisArg
。 - 返回函数执行结果。
三、apply 方法
apply
方法和 call
方法类似,也是用来改变函数内部 this
的指向并立即执行函数,不同之处在于 apply
方法接收的第二个参数是一个数组(或者类数组对象)。
3.1 apply 方法的语法
function.apply(thisArg, [argsArray])
thisArg
:在function
函数运行时指定的this
值。如果这个参数为空,在非严格模式下,则默认指向全局对象(浏览器中是window
),在严格模式下为undefined
。argsArray
:一个数组或者类数组对象,其中的元素将作为单独的参数传给function
函数。
3.2 apply 方法的示例
- 改变函数内部 this 指向并传递数组参数
function sum(a, b) {
return a + b;
}
const numbers = [2, 3];
const result = sum.apply(null, numbers);
console.log(result); // 5
这里 sum
函数原本期望两个参数,通过 apply
方法,将数组 numbers
中的元素作为参数传递给 sum
函数,并且由于第一个参数为 null
,在非严格模式下,this
指向全局对象 window
。
- 使用 apply 方法求数组中的最大值
const numbersArray = [1, 5, 3, 9, 4];
const max = Math.max.apply(null, numbersArray);
console.log(max); // 9
Math.max
方法接受多个参数,通过 apply
方法,我们可以将数组展开作为参数传递给它,从而求出数组中的最大值。
3.3 apply 方法的原理
同样,我们可以模拟实现一个 apply
方法。
Function.prototype.myApply = function (thisArg, argsArray) {
if (typeof this!== 'function') {
throw new TypeError('this is not a function');
}
thisArg = thisArg || window;
const fnSymbol = Symbol('fn');
thisArg[fnSymbol] = this;
let result;
if (argsArray) {
result = thisArg[fnSymbol](...argsArray);
} else {
result = thisArg[fnSymbol]();
}
delete thisArg[fnSymbol];
return result;
};
- 首先检查调用
myApply
的是否是一个函数,如果不是则抛出错误。 - 确定
thisArg
,如果没有传入则默认指向全局对象。 - 使用一个唯一的
Symbol
作为属性名,将调用myApply
的函数挂载到thisArg
上。 - 判断是否有
argsArray
,如果有则展开数组作为参数执行函数,否则直接执行函数,获取执行结果。 - 删除挂载在
thisArg
上的函数,避免污染thisArg
。 - 返回函数执行结果。
四、bind 方法
bind
方法也是用来改变函数内部 this
的指向,但与 call
和 apply
不同的是,bind
方法不会立即执行函数,而是返回一个新的函数,这个新函数内部的 this
被绑定到指定的值。
4.1 bind 方法的语法
function.bind(thisArg, arg1, arg2, ...)
thisArg
:在返回的新函数中,将this
绑定到该值。如果这个参数为空,在非严格模式下,则默认指向全局对象(浏览器中是window
),在严格模式下为undefined
。arg1, arg2, ...
:(可选)预定义的参数,这些参数将被前置到新函数被调用时传入的参数之前。
4.2 bind 方法的示例
- 绑定 this 指向并返回新函数
const person = {
name: 'Charlie',
sayHello: function () {
console.log(`Hello, I'm ${this.name}`);
}
};
const boundSayHello = person.sayHello.bind({ name: 'David' });
boundSayHello(); // Hello, I'm David
这里通过 bind
方法,将 person.sayHello
函数内部的 this
绑定到一个新的对象 { name: 'David' }
,并返回一个新函数 boundSayHello
,调用新函数时输出的是 David
。
- 使用 bind 方法预定义参数
function multiply(a, b) {
return a * b;
}
const double = multiply.bind(null, 2);
console.log(double(5)); // 10
这里通过 bind
方法,将 multiply
函数的第一个参数预定义为 2
,返回一个新函数 double
,调用 double
时只需要传入第二个参数即可。
4.3 bind 方法的原理
下面是模拟实现 bind
方法的代码。
Function.prototype.myBind = function (thisArg, ...bindArgs) {
if (typeof this!== 'function') {
throw new TypeError('this is not a function');
}
const self = this;
return function (...args) {
if (this instanceof self) {
return new self(...bindArgs, ...args);
}
return self.apply(thisArg, bindArgs.concat(args));
};
};
- 首先检查调用
myBind
的是否是一个函数,如果不是则抛出错误。 - 保存调用
myBind
的函数self
。 - 返回一个新函数,在新函数内部:
- 如果新函数是通过
new
关键字调用的(即this instanceof self
为true
),说明是作为构造函数调用,那么使用new
关键字创建新实例,并将bindArgs
和新函数调用时传入的args
作为参数传递给原函数self
。 - 如果不是通过
new
关键字调用的,则使用apply
方法改变self
内部this
的指向为thisArg
,并将bindArgs
和新函数调用时传入的args
作为参数传递给self
。
- 如果新函数是通过
五、call、apply 与 bind 方法的区别
-
执行时机
call
和apply
方法会立即执行函数。bind
方法不会立即执行函数,而是返回一个新的函数,需要调用这个新函数才会执行。
-
参数传递方式
call
方法接受多个参数,第一个参数是this
的指向,后面的参数依次传递给函数。apply
方法接受两个参数,第一个参数是this
的指向,第二个参数是一个数组(或类数组对象),数组中的元素作为函数的参数。bind
方法第一个参数是this
的指向,后面可以跟任意数量的参数,这些参数会被前置到新函数调用时传入的参数之前。
-
返回值
call
和apply
方法返回的是函数执行的结果。bind
方法返回的是一个新的函数。
-
对原函数的影响
call
和apply
方法不会改变原函数本身,只是改变函数执行时this
的指向并执行函数。bind
方法同样不会改变原函数本身,而是返回一个新函数,新函数内部的this
被绑定到指定的值。
六、实际应用场景
-
继承 在实现继承时,
call
和apply
方法常用于在子类构造函数中调用父类构造函数,以实现属性和方法的继承。例如前面提到的Animal
和Dog
的例子。 -
函数借用 当一个对象没有某个方法,但其他对象有这个方法时,可以使用
call
或apply
方法借用其他对象的方法。
const arrayLike = { 0: 1, 1: 2, length: 2 };
const sum = Array.prototype.reduce.call(arrayLike, (acc, cur) => acc + cur, 0);
console.log(sum); // 3
这里 arrayLike
并不是一个真正的数组,但通过 call
方法借用了 Array.prototype.reduce
方法来计算其元素的总和。
- 事件处理函数中的 this 绑定
在事件处理函数中,
this
的指向可能不符合我们的预期,这时可以使用bind
方法来确保this
指向我们期望的对象。
const button = document.getElementById('myButton');
const person = {
name: 'Eve',
clickHandler: function () {
console.log(`${this.name} clicked the button.`);
}
};
button.addEventListener('click', person.clickHandler.bind(person));
这样在按钮点击时,clickHandler
函数内部的 this
就会指向 person
对象。
- 柯里化
bind
方法可以用于实现柯里化。柯里化是一种将多参数函数转换为一系列单参数函数的技术。
function add(a, b, c) {
return a + b + c;
}
const curriedAdd = add.bind(null, 1);
const result1 = curriedAdd(2, 3);
const curriedAdd2 = curriedAdd.bind(null, 2);
const result2 = curriedAdd2(3);
console.log(result1); // 6
console.log(result2); // 6
这里通过 bind
方法逐步固定 add
函数的参数,实现了柯里化。
七、总结与注意事项
-
总结
call
、apply
和bind
方法是 JavaScript 中非常强大的工具,它们允许我们灵活地控制函数内部this
的指向,从而实现继承、函数借用、事件处理等多种功能。call
和apply
方法用于立即执行函数并改变this
指向,区别在于参数传递方式;bind
方法用于返回一个新函数,新函数内部的this
被绑定到指定的值,且可以预定义部分参数。
-
注意事项
- 在使用
call
、apply
和bind
方法时,要注意thisArg
参数的取值,特别是在严格模式和非严格模式下的不同表现。 - 当使用
bind
方法返回的新函数作为构造函数时,this
的绑定会失效,新函数内部的this
会指向新创建的实例,这是需要特别注意的地方。 - 在传递参数时,要根据
call
、apply
和bind
方法不同的参数要求进行正确的传递,避免出现参数错误的情况。
- 在使用
通过深入理解和熟练运用 call
、apply
和 bind
方法,我们能够更加灵活地编写 JavaScript 代码,提高代码的复用性和可维护性。希望本文的介绍和示例能帮助你更好地掌握这些重要的方法。