call、apply、bind 的基本概念
前言
科学的尽头是神学,this 关键字是 JS 中最复杂的机制之一。在 React 中,bind 常用于绑定组件的实例方法,特别是使用类组件时。如果不使用 bind,this 会在事件调用时丢失,导致报错。到底三剑客有啥威力?今天,就让我们来揭开它那神秘的面纱。🧐
call 的概念
定义
Function
实例的 call()
方法会以给定的 this
值和逐个提供的参数调用该函数。
语法
call(thisArg)
call(thisArg, arg1)
call(thisArg, arg1, arg2)
call(thisArg, arg1, arg2, /* …, */ argN)
参数
返回值
使用指定的 this
值和参数调用函数后的结果。
描述
备注: 这个函数几乎与 apply()
相同,只是函数的参数以列表的形式逐个传递给 call()
,而在 apply()
中它们被组合在一个对象中,通常是一个数组——例如,func.call(this, "eat", "bananas")
与 func.apply(this, ["eat", "bananas"])
。
通常,在调用函数时,函数内部的 this
值是访问该函数的对象。使用 call()
,你可以在调用现有函数时将任意值分配给 this
,而无需首先将函数附加到对象上作为属性。这样可以将一个对象的方法用作通用的实用函数。
警告: 不要使用 call()
来链式调用构造函数(例如,实现继承)。这会将构造函数作为普通函数调用,这意味着 new.target
的值为 undefined
,而类会抛出错误,因为它们不能在没有 new
的情况下被调用。请改用 Reflect.construct()
或 extends
。
例子
/**
* call 的定义
* 1、是一个方法,是函数方的方法;
* 2、只能被函数调用,立即执行;
* 3、可以改变 this 指向;
* 4、第一个参数是指定函数内部 this 的指向对象
*/
/**
* 只能被函数调用
*/
function fun() {
console.log('Hello')
}
fun.call(); // 'Hello'
/**
* 以改变 this 指向
*/
function updatePointTo() {
console.log(this.name)
}
const boy = {
name: 'LiHua',
}
updatePointTo.call(boy); // 'LiHua'
const girl = {
name: 'Afla',
sayName() {
console.log(`I'm ${this.name}`)
},
eat(food) {
console.log(`I like eat ${food}`)
}
}
girl.sayName(); // I'm Afla
girl.sayName.call(boy); // I'm LiHua
girl.eat('Apple'); // I like eat Apple
girl.eat.call(boy, 'Mango'); // I like eat Mango
apply 概念
定义
Function
实例的 apply()
方法会以给定的 this
值和作为数组(或类数组对象)提供的 arguments
调用该函数。
语法
apply(thisArg)
apply(thisArg, argsArray)
参数
-
调用
func
时提供的this
值。如果函数不处于严格模式,则null
和undefined
会被替换为全局对象,原始值会被转换为对象。 -
一个类数组对象,用于指定调用
func
时的参数,或者如果不需要向函数提供参数,则为null
或undefined
。
返回值
使用指定的 this
值和参数调用函数的结果。
描述
备注: 这个函数与 call()
几乎完全相同,只是函数参数在 call()
中逐个作为列表传递,而在 apply()
中它们会组合在一个对象中,通常是一个数组——例如,func.call(this, "eat", "bananas")
与 func.apply(this, ["eat", "bananas"])
。
通常情况下,在调用函数时,函数内部的 this
的值是访问该函数的对象。使用 apply()
,你可以在调用现有函数时将任意值分配给 this
,而无需先将函数作为属性附加到对象上。这使得你可以将一个对象的方法用作通用的实用函数。
你还可以使用任何类数组对象作为第二个参数。实际上,这意味着它需要具有 length
属性,并且整数(“索引”)属性的范围在 (0..length - 1)
之间。例如,你可以使用一个 NodeList
,或者像 { 'length': 2, '0': 'eat', '1': 'bananas' }
这样的自定义对象。你还可以使用 arguments
,例如:
function wrapper() {
return anotherFn.apply(null, arguments);
}
function wrapper(...args) {
return anotherFn(...args);
}
一般而言,fn.apply(null, args)
等同于使用参数展开语法的 fn(...args)
,只是在前者的情况下,args
期望是类数组对象,而在后者的情况下,args
期望是可迭代对象。
警告: 不要使用 apply()
进行构造函数链式调用(例如,实现继承)。这会将构造函数作为普通函数调用,这意味着 new.target
是 undefined
,从而类会抛出错误,因为它们不能在没有 new
的情况下调用。请改用 Reflect.construct()
或 extends
。
例子
/**
* apply 的概念
* 1、用法和 call 差不多,只是参数是数组的方式传入;(this, args)
*/
function updatePointTo() {
console.log(this.name)
}
const dog = {
name: 'H8'
}
updatePointTo.call(dog); // H8
updatePointTo.apply(dog); // H8
const cat = {
name: 'R9',
eat(food1, food2, food3) {
console.log(`my name is ${this.name}; I like eat ${food1} and ${food2} and ${food3}`);
}
}
cat.eat('a','b','c'); // my name is R9; I like eat a and b and c
cat.eat.call(dog, 'd', 'e', 'f'); // my name is H8; I like eat d and e and f
cat.eat.apply(dog, ['g', 'h', 'j']); // my name is H8; I like eat g and h and j
bind 概念
定义
Function
实例的 bind()
方法创建一个新函数,当调用该新函数时,它会调用原始函数并将其 this
关键字设置为给定的值,同时,还可以传入一系列指定的参数,这些参数会插入到调用新函数时传入的参数的前面。
语法
bind(thisArg)
bind(thisArg, arg1)
bind(thisArg, )
bind(thisArg, arg1, arg2, /* …, */ argN)
参数
-
在调用绑定函数时,作为
this
参数 传入目标函数func
的值。如果函数不在严格模式下,null
和undefined
会被替换为全局对象,并且原始值会被转换为对象。如果使用new
运算符构造绑定函数,则忽略该值。 -
在调用
func
时,插入到传入绑定函数的参数前的参数。
返回值
使用指定的 this
值和初始参数(如果提供)创建的给定函数的副本。
描述
bind()
函数创建一个新的绑定函数(bound function)。调用绑定函数通常会执行其所包装的函数,也称为目标函数(target function)。绑定函数将绑定时传入的参数(包括 this
的值和前几个参数)提前存储为其内部状态。而不是在实际调用时传入。通常情况下,你可以将 const boundFn = fn.bind(thisArg, arg1, arg2)
和 const boundFn = (...restArgs) => fn.call(thisArg, arg1, arg2, ...restArgs)
构建的绑定函数的调用效果视为等效(但就构建 boundFn
的过程而言,不是二者等效的)。
绑定函数可以通过调用 boundFn.bind(thisArg, /* more args */)
进一步进行绑定,从而创建另一个绑定函数 boundFn2
。新绑定的 thisArg
值会被忽略,因为 boundFn2
的目标函数是 boundFn
,而 boundFn
已经有一个绑定的 this
值了。当调用 boundFn2
时,它会调用 boundFn
,而 boundFn
又会调用 fn
。fn
最终接收到的参数按顺序为:boundFn
绑定的参数、boundFn2
绑定的参数,以及 boundFn2
接收到的参数。
例子
/**
* bind 的概念
* 1、是 Function.prototype 提供的方法,用于创建一个新的函数,并绑定一个指定的 this 值和部分参数。新创建的函数不会立即执行,而* 是返回一个可以稍后调用的函数。
* 2、和 call 差不多,bind(thisArg, arg1, arg2) 也可以用 bind(thisArg, ...args)
*/
function updatePointTo() {
console.log(this.name)
}
const dog = {
name: 'H8'
}
updatePointTo.call(dog); // H8
updatePointTo.apply(dog); // H8
const cat = {
name: 'R9',
eat(food1, food2, food3) {
console.log(`my name is ${this.name}; I like eat ${food1} and ${food2} and ${food3}`);
}
}
cat.eat('a','b','c'); // my name is R9; I like eat a and b and c
cat.eat.call(dog, 'd', 'e', 'f'); // my name is H8; I like eat d and e and f
cat.eat.apply(dog, ['g', 'h', 'j']); // my name is H8; I like eat g and h and j
const newEat = cat.eat.bind(dog, 'k', 'l', 'z');
newEat(); // my name is H8; I like eat k and l and z
call、apply、bind 的对比
特性 | call | apply | bind |
---|---|---|---|
功能 | 改变函数内部的 this 值,并立即执行函数 | 改变函数内部的 this 值,并立即执行函数 | 改变函数内部的 this 值,返回一个绑定后的新函数 |
参数传递 | 以逗号分隔的形式逐个传递参数 | 参数以数组或类数组对象的形式传递 | 可绑定部分参数,返回的函数调用时可继续传递剩余参数 |
返回值 | 函数调用的结果 | 函数调用的结果 | 一个新的函数 |
调用时机 | 立即执行 | 立即执行 | 稍后调用 |
使用场景
1 、改变 this 指向
call、apply 和 bind 最常见的用途就是改变函数内部的 this 指向。
const obj = { name: 'Alice' };
function greet(greeting) {
console.log(`${greeting}, my name is ${this.name}`);
}
greet.call(obj, 'Hello'); // Hello, my name is Alice
greet.apply(obj, ['Hi']); // Hi, my name is Alice
const boundGreet = greet.bind(obj, 'Hey');
boundGreet(); // Hey, my name is Alice
2、借用其他对象的方法
当一个对象没有某个方法时,可以通过 call 或 apply 借用其他对象的方法。
示例:借用数组的方法
const obj = { 0: 'a', 1: 'b', 2: 'c', length: 3 };
// 借用数组的 `slice` 方法
const result = Array.prototype.slice.call(obj);
console.log(result); // ['a', 'b', 'c']
3、为函数绑定默认参数
使用 bind 可以为函数绑定一些默认参数,实现函数柯里化的效果。
function multiply(a, b) {
return a * b;
}
const double = multiply.bind(null, 2); // 固定第一个参数为 2
console.log(double(5)); // 10
console.log(double(10)); // 20
4 、 在事件监听中绑定 this
当在事件监听器中需要访问类的方法时,bind 可以用来绑定正确的 this。
class Counter {
constructor() {
this.count = 0;
}
increment() {
this.count++;
console.log(this.count);
}
}
const counter = new Counter();
const button = document.querySelector('button');
// 绑定 this,确保 increment 中的 this 指向 Counter 实例
button.addEventListener('click', counter.increment.bind(counter));
5、 实现继承
在构造函数继承中,可以使用 call 来调用父类构造函数并绑定子类的 this。
function Parent(name) {
this.name = name;
}
function Child(name, age) {
Parent.call(this, name); // 调用父类构造函数
this.age = age;
}
const child = new Child('Alice', 10);
console.log(child); // { name: 'Alice', age: 10 }
call、apply 和 bind 的注意事项
1、this 值为 null 或 undefined
-
在非严格模式下,call 和 apply 的 this 会默认绑定为全局对象(浏览器中是 window)。
-
在严格模式下,this 会保持为 null 或 undefined。
function test() {
console.log(this);
}
test.call(null); // 浏览器中会输出: window
2、箭头函数不受 call、apply 和 bind 影响
- 箭头函数的 this 是在定义时绑定的,无法通过 call、apply 或 bind 修改。
const obj = { name: 'Alice' };
const arrowFunc = () => {
console.log(this.name);
};
arrowFunc.call(obj); // undefined
3、使用 bind 创建的新函数不能被重复绑定 this
- 如果对一个绑定函数再次使用 bind,新的 this 值将被忽略。
function greet() {
console.log(this.name);
}
const boundGreet = greet.bind({ name: 'Alice' });
const reBoundGreet = boundGreet.bind({ name: 'Bob' });
reBoundGreet(); // Alice
总结
- call 和 apply 的主要区别在于参数传递方式
- call:参数逐个传递。
- apply:参数以数组形式传递。
- bind 创建一个绑定函数,不会立即执行:
- 常用于事件监听、预设参数等场景
- 都可以改变函数的 this 指向,这是它们最主要的用途。