通过例子来理解 Javascript 中的 Call、Apply和 Bind

定义

让我们仔细地研究一下这几个函数的作用:

Call 函数可以改变函数调用的上下文。直白讲,就是将函数内部this值改变成任意你想要的值。

Apply 函数和call函数类似,唯一的区别在于apply允许将数组作为函数参数列表。

Bind 函数创建一个稍后执行的函数,这个新函数的执行上下文由 this 提供。

让我们先来看看call、apply和bind函数的例子,然后我们将来创建一个类似map的函数。

如何在JavaScript中使用call函数

call函数更改一个函数内部this的值,并且将传入的参数作为这个函数的执行参数。

call函数的语法如下:


func.call(thisObj, args1, args2, ...)

其中:

  • func 是通过不同this对象调用的函数
  • thisObj 是用来替换函数func内部 this关键字的对象或者值
  • args1, args2 args1, args2是参数,与改变后的this对象一起传递给调用的函数。

注意如果在不传入thisObj参数的情况下调用函数,JavaScript默认this值为全局对象。

现在我们已经了解了call函数的背景,让我们通过一些示例来进一步了解它。

JS中如何在不同的上下文调用函数

考虑下面的例子。这个例子中有三个类 – CarBrand1Brand2

function Car(type, fuelType){
	this.type = type;
	this.fuelType = fuelType;
}

function setBrand(brand){
	Car.call(this, "convertible", "petrol");
	this.brand = brand;
	console.log(`Car details = `, this);
}

function definePrice(price){
	Car.call(this, "convertible", "diesel");
	this.price = price;
	console.log(`Car details = `, this);
}

const newBrand = new setBrand('Brand1');
const newCarPrice = new definePrice(100000);

 

仔细看,你会发现我们在两个场景下通过call函数调用Car函数:一次是在setBrand函数调用;一次是在在 definePrice函数调用。

在这两个函数中, 我们都在this对象内调用Car函数,this对象分别代表了这两个函数。 例如在setBrand函数中,我们在代表函数上下文的this对象调用了Car函数,definePrice一样。

在JS中如何在不传入参数的情况下调用call函数

考虑下面的例子:

const newEntity = (obj) => console.log(obj);

function mountEntity(){
	this.entity = newEntity;
	console.log(`Entity ${this.entity} is mounted on ${this}`);
}

mountEntity.call(); //输出: Entity (obj) => console.log(obj) is mounted on [object Window]

在这个例子中,调用mountEntity时,thisObj参数为空。 这时,JavaScript会指向全局对象。

如何在JavaScript中使用apply函数

ApplyCall函数类似。callapply函数唯一的不同是传入的参数。

apply中,参数可以是一个数组的字面量或者一个新的数组对象。

apply函数的语法如下:

func.apply(thisObj, argumentsArray);

其中:

  • func 是通过不同this对象调用的函数
  • thisObj 是用来替换函数func内部 this关键字的对象或者值
  • argumentsArray 可以是参数数组、数组对象或者arguments关键字本身

如你所见,apply函数有不同的语法。

第一种语法很简单,你可以传入一个参数数组:

func.apply(thisObj, [args1, args2, ...]);

第二种语法可以传入一个新的数组对象:

func.apply(thisObj, new Array(args1, args2));

第三种语法可以传入arguments关键字:

func.apply(thisObj, arguments); 

arguments是函数中的一个特殊对象,包含传入函数的参数的值。你可以将这个关键字与apply函数一起使用,以接受任何数量的任意参数。

apply最棒的地方在于我们不需要关心传递给调用函数的参数的数量。由于动态性和多功能的特点,apply可以被应用到复杂情况。

我们用apply函数改写上文的例子:

function Car(type, fuelType){
	this.type = type;
	this.fuelType = fuelType;
}

function setBrand(brand){
	Car.apply(this, ["convertible", "petrol"]); //使用数组字面量的语法
	this.brand = brand;
	console.log(`Car details = `, this);
}

function definePrice(price){
	Car.apply(this, new Array("convertible", "diesel")); //使用数组构建函数的语法
	this.price = price;
	console.log(`Car details = `, this);
}

const newBrand = new setBrand('Brand1');
const newCarPrice = new definePrice(100000);

下面是使用arguments关键字的例子:

function addUp(){
		//使用参数捕获任意数量的输入
    const args = Array.from(arguments); 
    this.x = args.reduce((prev, curr) => prev + curr, 0);
    console.log("this.x = ", this.x);
}

function driverFunc(){
    const obj = {
        inps: [1,2,3,4,5,6]
    }
    addUp.apply(obj, obj.inps);
}

driverFunc(); //输出: this.x =  21

如何在JavaScript中使用bind函数

bind函数创建一个函数副本,并改变调用函数内部this的值。

bind函数的语法如下:

func.bind(thisObj, arg1, arg2, ..., argN);

其中:

  • func 是通过不同this对象调用的函数
  • thisObj 是用来替换函数func内部this关键字的对象或者值
  • arg1, arg2…argN – 和call函数类似,你可以传入一个或多个参数

bind函数返回一个新的函数,在这个函数中包含新的被调用函数内部this的值:

func(arg1, arg2);

然后函数func根据参数被执行

让我们一起来看一看如何在React类组件中使用bind函数:

class App extends React.Component {
  constructor(props) {
    super(props);
    this.state = {
      counter: 1
    };
  }
  handleCode() {
    console.log("HANDLE CODE THIS = ", this.state);
  }
  render() {
    return <button onClick={this.handleCode}>Click Me</button>;
  }
}

考虑上述App组件,有以下几个组成部分:

  • constructor 调用类的函数,通过new关键字实例化
  • render 是执行或渲染JSX的函数
  • handleCode 是打印组件的类方法

如果我们点击Click Me按钮,会得到报错Cannot read properties of undefined (reading 'state')

这为什么会发生? ??

因为handleCode是类的方法,所以你可能认为我们可以访问类的状态(state),但是这里存在的问题是:

  • handleCode中的this并不等同于类中的this
  • 在类中this是一个普通的对象,并且有非静态类方法作为属性, 但是handleCode中的this指代另一个上下文
  • 在这里this的值取决于函数被调用的位置,handleCode是在onClick事件中被调用
  • 调用时handleCode函数内部的this被设置为undefined
  • 我们尝试调用undefined的state属性,就导致了上文的报错

我们可以通过给handleCode方法的this指定上下文来解决这个问题,bind方法就派上用场了:

class App extends React.Component {
  constructor(props) {
    super(props);
    this.state = {
      counter: 1
    };
   this.handleCode = this.handleCode.bind(this); //绑定this函数
  }
  handleCode() {
    console.log("HANDLE CODE THIS = ", this.state);
  }
  render() {
    return <button onClick={this.handleCode}>Click Me</button>;
  }
}

bind会创建一个新函数,并且存储this在对象中,这个函数包含handleCode这个新属性。Bind确保类的this上下文被应用到 handleCode函数的this

如何自定义map函数

在了解所有必要知识之后,让我们来自己创建一个map函数,我们先来看看自定义map函数需要了解什么:

map函数的语法如下:

arr.map(func)

其中:

  • arr是map调用的数组
  • func 是数组上每一个元素需要执行的函数

map函数的基本功能很简单:

map函数遍历数组的每一个元素,并在每一个元素上调用传入的参数。返回值的类型是一个数组,数组的每一个元素都是应用func后的结果。

我们已经知道这个函数的要求了,就可以着手创建自己的map函数了,以下是新的map函数:

function newMap(func){
  let destArr = [];
  const srcArrLen = this.length;
  for(let i = 0; i < srcArrLen; i++){
    destArr.push(func.call(this, this[i]));
  }

  return destArr;
} 

让我们来一点一点解释上面的例子:

  • 函数接受名为func的参数。 这个参数就是需要在数组的每一个元素上调用的函数。
  • 代码的其他部分不言自明。我们主要聚焦在 destArr.push(func.call(this, this[i]));
  • 这行代码做了两件事:\
    1. 将变化推入destArr\
    2. 通过call方法执行funccall方法(如上文解释的那样)会执行func方法,并使用func方法内部this对象的新值。

让我们来看看newMap函数是如何执行的。不推荐下面这种给原始数据类型添加新方法的做法,我们这么做仅出于教学目的。

注:  不要在你的代码中使用下面的方法,不然会对你的工作造成影响。

Object.defineProperty(Array.prototype, 'newMap', {
  value: newMap
}); 

definePropertyArray.prototype创建了新的方法。

设定完毕后,我们就可以使用自己的函数了:

const arr = [1,2,3];
const newArr = arr.newMap(item => item + 1);
console.log(newArr); //输出:[2, 3, 4]

总结

本文通过示例展示了call、apply和bind的用法。

简单概括一下:

  • Call、apply和bind可以改变调用函数内部this关键字的上下文
  • 每个例子的调用方式不同 – apply通过一组数组执行,call执行结果类似但是参数由逗号隔开
  • 在React的类组件中,这些方法十分管用

感谢阅读!