apply、call、bind

4,355 阅读9分钟

编写时间:2019-08-21
更新时间:2021-03-24

作者:鬼小妞

备注: 本文整理了js改变函数的this对象的指向的apply、call、bind

状态:整理中2021-03-24

apply、call、bind

image.png

image.png

apply、call、bind共同点

  • apply 、 call 、bind 三者都是用来改变函数的this对象的指向的;

  • apply 、 call 、bind 三者第一个参数都是this要指向的对象,也就是想指定的上下文;

  • apply 、 call 、bind 三者都可以利用后续参数传参;

apply、call、bind不同点

  • bind是偏函数型,返回对应函数,便于稍后调用;apply、call则是立即调用 。

  • call 立即调用,需要把参数按顺序传递进去,而 apply 则是把参数放在数组里。例如:func.call(this, arg1, arg2);。(非严格模式下)当参数数量不确定时,函数内部也可以通过 arguments 这个数组来遍历所有的参数。

  • apply 立即调用,把参数放在数组里。例如:func.apply(this, [arg1, arg2])

某个函数的仅仅需要在被调用时改变this指向,使用bind

某个函数需要立即执行时,参数是明确知道数量时用 call , 而不确定的时候用 apply,然后把参数 push进数组传递进去

bind

Function.prototype.bind(thisArg [, arg1 [, arg2, …]]) 是ES5新增的函数扩展方法,bind()返回一个新的函数对象,该函数的this被绑定到thisArg上,并向事件处理器中传入参数

MDN的解释是:

bind() 方法创建一个新的函数,在 bind() 被调用时,这个新函数的 this 被指定为 bind() 的第一个参数,而其余参数将作为新函数的参数,供调用时使用。

bind调用实例

image.png image.png

this.height = '155';    // 在浏览器中,this 指向全局的 "window" 对象
let user = {
  height: '170',
  getHeight: function() { return this.height; }
};

// 自身调用
user.getHeight(); //user调用getX,此时getX里的this指的是user
console.log(user.getHeight()); // '170'


// 赋值调用
let tianZhen = user.getHeight; //  相当于  tianZhen = function() { return this.height; }
tianZhen(); // tianZhen函数是在全局作用域中调用的,相当于window.tianZhen(),此时tianZhen = function() { return this.height; }里的this指window
console.log(tianZhen()); // '155'

// 赋值绑定调用
let bindGetH = tianZhen.bind(user); // 把 'this' 绑定到 user 对象,此时bindGetH未立即执行。相当于 tianZhen = function() { return user.height; }
bindGetH(); //  即使在全局作用域直接调用,window.tianZhen()。function() { return user.height; },得到user里的height
console.log(bindGetH()); // '170'

bind实际用途

在常见的单体模式中,通常我们会使用 _this , that , self 等保存 this ,这样我们可以在改变了上下文之后继续引用到它,vue中组件dom绑定很少会用bind,但是在react中,使用React.createClass会自动绑定每个方法的this到当前组件,但使用ES6 class或纯函数时,就要靠手动绑定this了,而此时bind就非常合适。 像这样:

1.在dom添加方法时使用bind,并传入this(需要绑定this的上下文), <button onClick={this.test.bind(this)}>点我一下</button>

image.png

import React, {Component} from 'react'
class Greeter extends Component{
   constructor (props) {
        super(props)
        this.state = {}
    }

    test (value) {
       console.log(value)
    }

    render () {
        return (
            <div>
                <button onClick={this.test.bind(this,3)}>点我一下</button>
            </div>
        )
    }
}

或者这样

2.在构造函数 constructor 内绑定this,好处是仅需要绑定一次,无需在dom中绑定,避免每次渲染时都要重新绑定,函数在别处复用时也无需再次绑定。constructor (props) { super(props) this.test=this.test.bind(this) }

image.png

import React, {Component} from 'react'
class Greeter extends Component{
   constructor (props) {
        super(props)
        this.state = {}
        this.test=this.test.bind(this)
    }

    test (value) {
       console.log(value)
    }

    render () {
        return (
            <div>
                <button onClick={this.test(3)}>点我一下</button>
            </div>
        )
    }
}

此外,你还可以在dom添加事件时使用箭头函数进行绑定,如<button onClick={()=>{this.test()}}>点我一下</button>,这里不再详解。

bind()的连续调用

我们可以思考一个问题,就是:如果连续 bind() 两次,亦或者是连续 bind() 三次那么输出的值是什么呢?

在下述代码里,bind() 创建了一个函数,当这个事件绑定在被调用的时候,它的 this 关键词会被设置成被传入的值(这里指调用bind()时传入的第一个参数设为thisArg)。因此,这里我们传入想要的上下文 this到 bind() 函数中。然后,当回调函数被执行的时候, this 便指向thisArg。再来一个简单的栗子:

image.png

image.png

let foo = { x: 3 }
let bar = function(){
   console.log(this.x);
}
bar(); // undefined
let func = bar.bind(foo);
func(); // 3

这里我们创建了一个新的函数 func,当使用 bind() 创建一个绑定函数之后,它被执行的时候,它的 this 会被设置成 foo , 而不是像我们调用 bar() 时的全局作用域。 有个有趣的问题,如果连续 bind() 两次,亦或者是连续 bind() 三次那么输出的值是什么呢?像这样:

image.png

image.png

let foo = { x: 3 };
let sed = { x: 4 };
let fiv = { x: 5 };
let bar = function(){
   console.log(this.x);
}
bar(); // undefined

let func = bar.bind(foo).bind(sed);
func(); // 3

let func2 = bar.bind(foo).bind(sed).bind(fiv);
func2(); // 3

答案是,两次都仍将输出 3 ,而非期待中的 4 和 5 。

原因是,在Javascript中,多次 bind() 是无效的。

更深层次的原因, bind() 的实现,相当于使用函数在内部包了一个 call / apply ,第二次 bind() 相当于再包住第一次 bind() ,故第二次以后的 bind 是无法生效的。 可以看官网的Function.prototype.bind的 Polyfill实现

apply、call

在 javascript 中,call 和 apply 都是为了改变某个函数运行时的上下文(context)而存在的,换句话说,就是为了改变函数体内部 this 的指向。

JavaScript 的一大特点是,函数存在「定义时上下文」和「运行时上下文」以及「上下文是可以改变的」这样的概念。

先来一个例子:

image.png image.png

function fruits() {}
  fruits.prototype = {
  color: "red",
  say: function() {
    console.log("My color is " + this.color);
  }
}
let apple = new fruits;
apple.say(); //My color is red

但是如果我们有一个对象banana= {color : "yellow"} ,我们不想对它重新定义 say 方法,那么我们可以通过 call 或 apply 用 apple 的 say 方法:

image.png image.png

function fruits() {}
  fruits.prototype = {
  color: "red",
  say: function() {
    console.log("My color is " + this.color);
  }
}
let apple = new fruits;
apple.say(); //My color is red

let banana = {
color: "yellow"
}

apple.say.call(banana); //My color is yellow
apple.say.apply(banana); //My color is yellow


所以,可以看出 call 和 apply 是为了动态改变 this 而出现的,当一个 object 没有某个方法(本栗子中banana没有say方法),但是其他的有(本栗子中apple有say方法),我们可以借助call或apply用其它对象的方法来操作。

apply、call 的区别

对于 apply、call 二者而言,作用完全一样,只是接受参数的方式不太一样。

  • call 立即调用,需要把参数按顺序传递进去,而 apply 则是把参数放在数组里。例如:func.call(this, arg1, arg2);。(非严格模式下)当参数数量不确定时,函数内部也可以通过 arguments 这个数组来遍历所有的参数。

  • apply 立即调用,把参数放在数组里。例如:func.apply(this, [arg1, arg2])

例如,有一个函数定义如下:

image.png

image.png

let func = function(arg1, arg2) {
  console.log(arg1, arg2)
};

func.call(this, '1', '2');
func.apply(this, ['1', '2'])

其中 this 是你想指定的上下文,他可以是任何一个 JavaScript 对象(JavaScript 中一切皆对象),call 需要把参数按顺序传递进去,而 apply 则是把参数放在数组里。 JavaScript 中,某个函数的参数数量是不固定的,因此要说适用条件的话,当你的参数是明确知道数量时用 call 。 而不确定的时候用 apply,然后把参数 push 进数组传递进去。当参数数量不确定时,函数内部也可以通过 arguments 这个数组来遍历所有的参数。 为了巩固加深记忆,下面列举一些常用用法:

数组之间追加

image.png

image.png


let array1 = [12 , "foo" , { name: "Joe" } , -2458];
let array2 = ["Doe" , 555 , 100];
Array.prototype.push.apply(array1, array2);

console.log(array1); // [12, "foo", {name: "Joe"}, -2458, "Doe", 555, 100]
console.log(array2); // ["Doe", 555, 100]

获取数组中的最大值和最小值

image.png image.png


let numbers = [5, 458 , 120 , -215 ];
let maxInNumbers1 = Math.max.apply(Math, numbers);
let maxInNumbers2 = Math.max.call(Math,5, 458 , 120 , -215);
 // 或者let maxInNumbers2 = Math.max.call(Math, ...numbers);

console.log(maxInNumbers1); //458
console.log(maxInNumbers2); //458


number 本身没有 max 方法,但是 Math 有,我们就可以借助 call 或者 apply 使用其方法。

验证是否是数组(前提是toString()方法没有被重写过)

在JavaScript里使用typeof判断数据类型,只能区分基本类型,即:number、string、undefined、boolean、object。 对于null、array、function、object来说,使用typeof都会统一返回object字符串。 要想区分对象、数组、函数、单纯使用typeof是不行的。

在JS中,可以通过Object.prototype.toString方法,判断某个对象之属于哪种内置类型。 分为null、string、boolean、number、undefined、array、function、object、date、math等。

当然也可以直接使用instanceof 操作符判断。

如下使用call演示,验证是否是数组

image.png

image.png


function isArray(obj){
  return Object.prototype.toString.apply(obj) === '[object Array]' ;
  // 或者 return Object.prototype.toString.call(obj) === '[object Array]' ;
}

let arrFlag = isArray(['7', '6']);
console.log(arrFlag); // true


类(伪)数组使用数组方法

Javascript中存在一种名为伪数组的对象结构。比较特别的是 arguments 对象,还有像调用 getElementsByTagName , document.childNodes 之类的,它们返回NodeList对象都属于伪数组。不能应用 Array下的 push , pop数组 等方法。

但是我们能通过 Array.prototype.slice.call 转换为真正的数组的带有 length 属性的对象,这样 domNodes 就可以应用 Array 下的所有方法了。

通过 Array.prototype.slice.call 或 Array.prototype.slice.apply 转化为标准数组

image.png

image.png

伪类数组是无法直接使用Array的内置方法,所以会抛错 image.png


<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta http-equiv="X-UA-Compatible" content="IE=edge">
  <meta height="viewport" content="width=device-width, initial-scale=1.0">
  <title>Document</title>
</head>
<body>
  <div>
    <span>1</span>
    <span>2</span>
  </div>
</body>
<script>

let domNodes1 = Array.prototype.slice.call(document.getElementsByTagName('span'));
// 或者
let domNodes2 = Array.prototype.slice.apply(document.getElementsByTagName('span'));

console.log(domNodes1);
console.log(domNodes2);

apply、call、bind比较

那么 apply、call、bind 三者相比较,之间又有什么异同呢?

何时使用 apply、call,何时使用 bind 呢。

简单的一个栗子:

var obj = {x: 81,
};
var foo = {
    getX: function() {
        return this.x;
    }
}
console.log(foo.getX.bind(obj)()); //81
console.log(foo.getX.call(obj)); //81
console.log(foo.getX.apply(obj)); //81

三个输出的都是81,但是注意看使用 bind() 方法的,他后面多了对括号。

也就是说,区别是,当你希望改变上下文环境之后并非立即执行,而是回调执行的时候,使用 bind() 方法。而 apply/call 则会立即执行函数。

再总结一下:

apply 、 call 、bind 三者都是用来改变函数的this对象的指向的; apply 、 call 、bind 三者第一个参数都是this要指向的对象,也就是想指定的上下文; apply 、 call 、bind 三者都可以利用后续参数传参;

bind是返回对应函数,便于稍后调用;apply、call则是立即调用 。