跳到主要内容

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)

参数

  • thisArg

    调用 func 时提供的 this 值。如果函数不处于严格模式,则 nullundefined 会被替换为全局对象,原始值会被转换为对象。

  • argsArray 可选

    一个类数组对象,用于指定调用 func 时的参数,或者如果不需要向函数提供参数,则为 nullundefined

返回值

使用指定的 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.targetundefined,从而类会抛出错误,因为它们不能在没有 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)

参数

  • thisArg

    在调用绑定函数时,作为 this 参数传入目标函数 func 的值。如果函数不在严格模式下,nullundefined 会被替换为全局对象,并且原始值会被转换为对象。如果使用 new 运算符构造绑定函数,则忽略该值。

  • arg1, …, argN 可选

    在调用 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 又会调用 fnfn 最终接收到的参数按顺序为: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 的对比

特性callapplybind
功能改变函数内部的 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

总结

  1. call apply 的主要区别在于参数传递方式
    • call:参数逐个传递。
    • apply:参数以数组形式传递。
  2. bind 创建一个绑定函数,不会立即执行
    • 常用于事件监听、预设参数等场景
  3. 都可以改变函数的 this 指向,这是它们最主要的用途。

参考

MDN Web