前端学习之路--call、apply、bind详细解析

307 阅读6分钟

call、apply、bind详细解析

语法

MDN中对三种方法进行了详细的说明:

func.call(thisArg,arg1,arg2,...)

thisArg:可选,在func函数运行时使用的this值。在非严格模式下,指定为null或undefined时自动指向全局对象。

arg1,arg2,...:指定的参数列表

func.apply(thisArg,[argsArray])

thisArg:与call中一致。

argsArray:可选,一个数组或者类数组对象,其中的数组元素将作为单独的参数传给func函数。如果该参数值为null或undefined,则表示不需要传入参数。

func.bind(thisArg,arg1,arg2,...)

参数与call一致

可以发现,三种方法各自有相似的地方,又各自有不同之处。具体差别在哪里我们总结一下:

共同点:

  1. 三个都是挂在Function原型上的方法,只有函数才能调用。

  2. 作用都是用来改变函数执行时的this指向。

区别:

1.call和apply的区别:

  call和apply的唯一区别就是其传给函数的参数形式不同,call的参数是一串参数组成的序列,apply的参数是一个数组或者类数组,其实就相当于把call的参数放到了数组中。

2.call和bind的区别:

  • call和bind的语法除了方法名不一样外,其他的一模一样。但是区别在于其返回不一样。call的返回和apply一样,都是返回调用指定this和参数的函数执行的结果;而bind的返回则是原函数的拷贝,并拥有制定的this值和初始参数。
  • call在改变this上下文后立即执行函数,bind则是返回拷贝的函数,原函数不执行,因而后面还需单独调用函数执行。

应用

1. 改变作用域(改变this指向)

在《JavaScript高级程序设计》中,对call和apply用途是这样描述的:这两个方法的用途都是在特定的作用域中调用函数,实际上等于设置函数体内this对象的值。也就是俗称的改变this指向。

这一应用也是call、apply、bind最重要最常用的应用了。通过改变this指向,可以借用其他对象上的方法,实现代码复用。

例子如下:

window.name = "zhang"
function Test(){
  console.log(this.name);
}
let obj = {
  name: 'joury'
}
Test()          //zhang
Test.call(obj)  //joury
Test.bind(obj)() //joury

对bind来说,bind()会创建一个this改变的不会立即执行的绑定函数,这个函数也可以用来作为构造函数来用。

2.判断数据类型

  其实这种应用主要靠的也是改变this指向来实现的。大家都知道typeof运算符可以判断数据类型,但是typeof用来判断一些基本数据类型还算方便,在引用类型上判断就不那么灵光。我们先回顾一下typeof的返回:

  • undefined ---如果这个值未定义
  • boolean ---如果这个值是布尔值
  • string ---如果这个值是字符串
  • number ---如果这个值是数值
  • object ---如果这个值是对象或null
  • function ---如果这个值是函数

  可以看到,typeof的返回值在null上有些令人迷惑,但是技术上也并无错误,毕竟null被认为是一个空对象引用,但是在严格判断上未免不尽人意。

  为了更好的判断数据类型,我们可以采用Object.prototype.toString.call()的方法,call的这种改变this指向的功能则可以实现判断多种数据类型,几乎可以判断所有类型的数据。

  注意:Object.prototype.toString.call()返回的是[Object 类型]的格式,为了单独提取出类型字符串,一般采用Object.prototype.toString.call().slice(8,-1)将类型字符串截取出来。

  这种方式可以判断String、Number、Boolean、Null、Undefined、Object、Array、Function、Date、RegExp、Map、Set、HTMLDivElement(DOM元素)、WeakMap、Window、Error、Arguments等数据类型,相比typeof强大很多。

3.使Math方法可以直接跟数组

  在进行数据分析、比较时,大家一般都把数据放在一个数组中,数组自身的方法就有很多,但是有些方法就不方便操作,比如获取最大值最小值等,需要使用Math.max等方法,但是Math.max方法后面跟的是数值,不能跟数组,怎么办呢?

  刚才讲过apply传递参数刚好是数组类型,同时还可以改变this指向,因此可以采用Math.max.apply(Math,array)的方法来对数组进行操作。

 let arr = [1,2,3,5,7,9];
 let max = Math.max.apply(Math,arr);
 console.log(max);//9

  这里多说一句,在利用Math.max来操作数组上,ES6新加了spread操作符也可以做到,spread操作符是由三个点组成(...),很像Rest操作符,但是意义相反,是为了展开操作,将可迭代的对象进行展开。

let arr1 = [1,5,9,6,4,7];
let arr2 = [2,0,8,...arr1,10]; //[2,0,8,1,5,9,6,4,7,10]
let max = Math.max(...arr2); //10

4.指定默认参数(偏函数)

这个主要是bind的应用,用来初始化部分参数。

function mul(a,b) {
  return a * b;
};
let double = mul.bind(null,2);
let triple = mul.bind(null,3);
alert(double(4));    // = mul(2, 4) = 6
alert( triple(4) );  // = mul(3, 4) = 12

当然,偏函数也可以用call、apply来实现,但是为了不立即执行,一般会采用bind来创建绑定函数。偏函数可以引出 “柯里化” 这一概念,在此文中不细述。

5.多重嵌套函数this保存

在多重嵌套的函数中,我们经常会遇到保存上级作用域this的情况,尤其是在绑定事件、定时器中。一般都采用let That = this之类的方式来保存this,但是学过bind之后,可以采用另一种方法来保存:

//常用的用变量保存this的方法
btn.click(function(){
  let _this = this;
  lis.each(function(){
    this;  //lis
    _this; //btn
  })
})
//用bind保存this的方法
btn.click(function(){
  lis.each(function(){
    this;  //btn
  }).bind(this)()
})

手写call、apply、bind

1.手写call

以下方法来自于OBKoro1大佬的js基础-面试官想知道你有多理解call,apply,bind?[不看后悔系列]

//call是函数的一个方法,所以需要写在Function的原型上,而不是单纯写一个具有call功能的函数。
Function.prototype.myCall = function (context, ...arr) {
    if (context === null || context === undefined) {
       // 指定为 null 和 undefined 的 this 值会自动指向全局对象(浏览器中为window)
        context = window 
    } else {
        context = Object(context) // 值为原始值(数字,字符串,布尔值)的 this 会指向该原始值的实例对象
    }
    const specialPrototype = Symbol('特殊属性Symbol') // 用于临时储存函数
    context[specialPrototype] = this; // 函数的this指向隐式绑定到context上
    let result = context[specialPrototype](...arr); // 通过隐式绑定执行函数并传递参数
    delete context[specialPrototype]; // 删除上下文对象的属性
    return result; // 返回函数执行结果
};

2.手写apply

待补充,稍后更新

参考文献

JavaScript高级程序设计(第3版)

现代 JavaScript 教程

js基础-面试官想知道你有多理解call,apply,bind?[不看后悔系列]

简要谈谈javascript bind 方法

理解JavaScript Call()函数原理