JavaScript——call()、apply()和bind()

@TOC

在JavaScript中,每个函数都包含两个非继承而来的函数apply()和call(),这两个函数的作用是一样的,都是为了改变函数运行时的上下文而存在的,实际就是改变函数体内this的指向。

而bind()函数也可以达到这个目的,但是在处理方式上与call()函数和apply()函数有一定的区别,接下来我们就详细看下3者的使用方式。

1、call()

call()函数调用一个函数时,会将该函数的执行对象上下文改变为另一个对象。其语法如下所示。

js 复制代码
function.call(thisArg, arg1, arg2, ...)
  • function为需要调用的函数。
  • thisArg表示的是新的对象上下文,函数中的this将指向thisArg,如果thisArg为null或者undefined,则this会指向全局对象。
  • arg1,arg2,...表示的是函数所接收的参数列表。

可以通过下面的实例看看call()函数的用法。

js 复制代码
//定义一个add()函数
function add(x, y) {
    return x + y;
}

//通过call()函数进行add()函数的调用
function myAddCall(x, y) {
    //调用add()函数的call()函数
    return add.call(this, x, y);
}
console.log(myAddCall(10, 20));//30

myAddCall()函数自身是不具备运算能力的,但是我们在myAddCall()函数中,通过调用add()函数的call()函数,并传入this值,将执行add()函数的主体改变为myAddCall()函数自身,然后传入参数x和y,这就使得myAddCall()函数拥有add()函数计算求和的能力。在实际计算时,就为10 + 20 = 30。

2、apply()

apply()函数的作用域与call()函数是一致的,只是在传递参数的形式上存在差别。其语法格式如下。

js 复制代码
function.apply(thisArg, [argsArray])
  • function与thisArg参数与call()函数中的解释一样。·
  • argsArray\]表示的是参数会通过数组的形式进行传递,如果argsArray不是一个有效的数组或者arguments对象,则会抛出一个TypeError异常。

js 复制代码
//定义一个add()函数
function add(x, y) {
    return x + y;
}

//通过apply()函数进行add()函数的调用
function myAddApply(x, y) {
    //调用add()函数的apply()函数
    return add.apply(this, [x, y]);
}
console.log(myAddApply(10, 20));//30

与call()函数相比,apply()函数只需要将add()函数接收的参数使用数组的形式传递即可,即使用[x, y]的形式,运行后的结果为10 +20 = 30。

3、bind()

bind()函数创建一个新的函数,在调用时设置this关键字为提供的值,在执行新函数时,将给定的参数列表作为原函数的参数序列,从前往后匹配。其语法格式如下。

js 复制代码
function.bind(thisArg, arg1, arg2, ...)

事实上,bind()函数与call()函数接收的参数是一样的。其返回值是原函数的副本,并拥有指定的this值和初始参数。如果我们想要实现上面实例的效果,可以编写以下代码。

js 复制代码
//定义一个add()函数
function add(x, y) {
    return x + y;
}

//通过bind()函数进行add()函数的调用
function myAddBind(x, y) {
    //调用bind()函数得到一个新的函数
    let bindAddFn = add.bind(this, x, y);
    //执行新的函数
    return bindAddFn();
}
console.log(myAddBind(10, 20));//30

4、比较

三者的相同之处是:

  • 都会改变函数调用的执行主体,修改this的指向。

不同之处表现在以下两点。

  • 第一点是关于函数立即执行,call()函数与apply()函数在执行后会立即调用前面的函数,而bind()函数不会立即调用,它会返回一个新的函数,可以在任何时候进行调用。
  • 第二点是关于参数传递,call()函数与bind()函数接收的参数相同,第一个参数表示将要改变的函数执行主体,即this的指向,从第二个参数开始到最后一个参数表示的是函数接收的参数;而对于apply()函数,第一个参数与call()函数、bind()函数相同,第二个参数是一个数组,表示的是接收的所有参数,如果第二个参数不是一个有效的数组或者arguments对象,则会抛出一个TypeError异常。

5、使用案例

5.1、求数组中的最大项和最小项

Array数组本身没有max()函数和min()函数,无法直接获取到最大值和最小值,但是Math却有求最大值和最小值的max()函数和min()函数。我们可以使用apply()函数来改变Math.max()函数和Math.min()函数的执行主体,然后将数组作为参数传递给Math.max()函数和Math.min()函数。

js 复制代码
var arr = [3, 5, 7, 2, 9, 11];
// 求数组中的最大值
console.log(Math.max.apply(null, arr));  // 11
// 求数组中的最小值
console.log(Math.min.apply(null, arr));  // 2

apply()函数的第一个参数为null,这是因为没有对象去调用这个函数,我们只需要这个函数帮助我们运算,得到返回结果。

第二个参数是数组本身,就是需要参与max()函数和min()函数运算的数据,运算结束后得到返回值,表示数组的最大值和最小值。

5.2、类数组对象转换为数组对象

函数的参数对象arguments是一个类数组对象,自身不能直接调用数组的方法,但是我们可以借助call()函数,让arguments对象调用数组的slice()函数,从而得到一个真实的数组,后面就能调用数组的函数。

任意个数字的求和的代码如下所示。

js 复制代码
// 任意个数字的求和
function sum() {
   // 将传递的参数转换为数组
   var arr = Array.prototype.slice.call(arguments);
   // 调用数组的reduce()函数
   return arr.reduce(function (pre, cur) {
       return pre + cur;
   }, 0)
}

sum(1, 2);       // 3
sum(1, 2, 3);    // 6
sum(1, 2, 3, 4); // 10

5.3、用于继承

js 复制代码
// 父类
function Animal(age) {
   // 属性
   this.age = age;
   // 实例函数
   this.sleep = function () {
       return this.name + '正在睡觉!';
   }
}
// 子类
function Cat(name, age) {
   // 使用call()函数实现继承
   Animal.call(this, age);
   this.name = name || 'tom';
}

var cat = new Cat('tony', 11);
console.log(cat.sleep());  // tony正在睡觉!
console.log(cat.age);  // 11

其中关键的语句是子类中的Animal.call(this, age),在call()函数中传递this,表示的是将Animal构造函数的执行主体转换为Cat对象,从而在Cat对象的this上会增加age属性和sleep函数,子类实际相当于如下代码。

js 复制代码
function Cat(name, age) {
   // 来源于对父类的继承
  this.age = age;
   this.sleep = function () {
       return this.name + '正在睡觉!';
   };
   // Cat自身的实例属性
   this.name = name || 'tom';
}

5.4、执行匿名函数

假如存在这样一个场景,有一个数组,数组中的每个元素是一个对象,对象是由不同的属性构成,现在我们想要调用一个函数,输出每个对象的各个属性值。

我们可以通过一个匿名函数,在匿名函数的作用域内添加print()函数用于输出对象的各个属性值,然后通过call()函数将该print()函数的执行主体改变为数组元素,这样就可以达到目的了。

js 复制代码
var animals = [
   {species: 'Lion', name: 'King'},
   {species: 'Whale', name: 'Fail'}
];
for (var i = 0; i < animals.length; i++) {
   (function (i) {
       this.print = function () {
           console.log('#' + i + ' ' + this.species + ': ' + this.name);
       };
       this.print();
   }).call(animals[i], i);
}

在上面的代码中,在call()函数中传入animals[i]​,这样匿名函数内部的this就指向animals[i]​,在调用print()函数时,this也会指向animals[i]​,从而能输出speices属性和name属性。

5.5、bind()函数配合setTimeout

在默认情况下,使用setTimeout()函数时,this关键字会指向全局对象window。当使用类的函数时,需要this引用类的实例,我们可能需要显式地把this绑定到回调函数以便继续使用实例。

js 复制代码
// 定义一个函数
function LateBloomer() {
   this.petalCount = Math.ceil(Math.random() * 12) + 1;
}
// 定义一个原型函数
LateBloomer.prototype.bloom = function () {
   // 在一秒后调用实例的declare()函数,很关键的一句
   window.setTimeout(this.declare.bind(this), 1000);
};
// 定义原型上的declare()函数
LateBloomer.prototype.declare = function () {
   console.log('I am a beautiful flower with ' + this.petalCount + ' petals!');
};
// 生成LateBloomer的实例
var flower = new LateBloomer();
flower.bloom();  // 1秒后,调用declare()函数

在上面的代码中,关键的语句在bloom()函数中,我们期望通过一个定时器,设置在1秒后,调用实例的declare()函数。很多人可能会写出下面这样的代码。

js 复制代码
LateBloomer.prototype.bloom = function () {
   window.setTimeout(this.declare, 1000);
};

此时,当我们调用setTimeout()函数时,由于其调用体是window,因此在setTimeout()函数内部的this指向的是window,而不是对象的实例。这样在1秒后调用declare()函数时,其中的this将无法访问到petalCount属性,从而返回"undefined"​,输出结果如下所示。

js 复制代码
I am a beautiful flower with undefined petals!

因此我们需要手动修改this的指向,而通过bind()函数能够达到这个目的。

通过bind()函数传入实例的this值,这样在setTimeout()函数内部调用declare()函数时,declare()函数中的this就会指向实例本身,从而就能访问到petalCount属性。

js 复制代码
LateBloomer.prototype.bloom = function () {
   window.setTimeout(this.declare.bind(this), 1000);
};
复制代码
I am a beautiful flower with 4 petals!
相关推荐
平凡灵感码头2 小时前
C语言 printf 数据打印格式速查表
c语言·开发语言·算法
好雨知时节t2 小时前
sleep 函数在React项目中的运用
javascript
好雨知时节t2 小时前
关于Scheduler 类,一个并发控制调度器
javascript
xw-busy-code2 小时前
Prettier 学习笔记
javascript·笔记·学习·prettier
毕设源码-郭学长2 小时前
【开题答辩全过程】以 基于Web的网上问诊系统的设计与实现为例,包含答辩的问题和答案
前端
电商API&Tina2 小时前
电商数据采集API接口||合规优先、稳定高效、数据精准
java·javascript·数据库·python·json
兮℡檬,2 小时前
答题卡识别判卷
开发语言·python·计算机视觉
酉鬼女又兒2 小时前
零基础快速入门前端DOM 操作核心知识与实战解析(完整汇总版)(可用于备赛蓝桥杯Web应用开发)
开发语言·前端·javascript·职场和发展·蓝桥杯·js
kyle~2 小时前
C++----函数指针与函数指针类型 返回值类型 (*类型名)(参数列表)
开发语言·c++