call/apply/bind方法的来源

call,apply,bind这三个方法其实都是继承自Function.prototype中的,属于实例方法。

console.log(Function.prototype.hasOwnProperty('call')) //true
console.log(Function.prototype.hasOwnProperty('apply')) //true
console.log(Function.prototype.hasOwnProperty('bind')) //true

上面代码中,都返回了true,表明三种方法都是继承自Function.prototype的。当然,普通的对象,函数,数组都继承了Function.prototype对象中的三个方法,所以这三个方法都可以在对象,数组,函数中使用。

apply、call

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

  • call() 方法在使用一个指定的this值和若干个指定的参数值的前提下调用某个函数或方法.

语法:

fun.call(thisArg[, arg1[, arg2[, ...]]])
  • apply() 方法在指定 this 值和参数(参数以数组或类数组对象的形式存在)的情况下调用某个函数。

语法:

fun.apply(thisArg[, argsArray])

apply、call 的区别

对于 apply、call 二者而言,作用完全一样,只是接受参数的方式不太一样。
call()方法接受的是若干个参数的列表,而apply()方法接受的是一个包含多个参数的数组。

var func = function(arg1, arg2) {
     
};

通过如下方式来调用:

func.call(this, arg1, arg2);
func.apply(this, [arg1, arg2])

当你的参数是明确知道数量时用 call ,而不确定的时候用 apply,然后把参数 push 进数组传递进去。当参数数量不确定时,函数内部也可以通过 arguments 这个伪数组来遍历所有的参数。

使用call方法调用函数并且指定上下文的’this’

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

使用call 和 apply 改变 this 指向

banana = {
    color: "yellow"
}
apple.say.call(banana);     //My color is yellow
apple.say.apply(banana);    //My color is yellow

使用call方法调用父构造函数

在一个子构造函数中,你可以通过调用父构造函数的 call 方法来实现继承。

下例中,使用 Food 构造函数创建的对象实例都会拥有在 Product 构造函数中添加的 name 属性和 price 属性,但 category 属性是在Food构造函数中定义的。

function Product(name, price) {
  this.name = name;
  this.price = price;

  if (price < 0) {
    throw RangeError('Cannot create product ' +
                      this.name + ' with a negative price');
  }
}

function Food(name, price) {
  Product.call(this, name, price); 
  this.category = 'food';
}

//等同于
function Food(name, price) { 
    this.name = name;
    this.price = price;
    if (price < 0) {
        throw RangeError('Cannot create product ' +
                this.name + ' with a negative price');
    }

    this.category = 'food'; 
}
var cheese = new Food('feta', 5);

使用call方法调用匿名函数

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);
}

result:
#0 Lion: King
VM695:9 #1 Whale: Fail

apply、call 的常见使用

Array之间追加

var array1 = [12 , "foo" , {name "Joe"} , -2458]; 
var array2 = ["Doe" , 555 , 100]; 
Array.prototype.push.apply(array1, array2); 
/* array1 值为  [12 , "foo" , {name "Joe"} , -2458 , "Doe" , 555 , 100] */

获取Array中的最大值和最小值

var  numbers = [5, 458 , 120 , -215 ]; 
var maxInNumbers = Math.max.apply(Math, numbers),   //458
    maxInNumbers = Math.max.call(Math,5, 458 , 120 , -215); //458

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

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

functionisArray(obj){ 
    return Object.prototype.toString.call(obj) === '[object Array]' ;
}

类(伪)数组使用Array方法

var domNodes = Array.prototype.slice.call(document.getElementsByTagName("*"));

notice

如果call和apply的第一个参数写的是null,那么this指向的是window对象

bind()

  • bind()方法会创建一个新函数。当这个新函数被调用时,bind()的第一个参数将作为它运行时的 this, 之后的一序列参数将会在传递的实参前传入作为它的参数。

语法:

fun.bind(thisArg[, arg1[, arg2[, ...]]])

创建绑定函数bind

this.x = 9; 
var module = {
  x: 81,
  getX: function() { return this.x; }
};

module.getX(); // 返回 81

var retrieveX = module.getX;
retrieveX(); // 返回 9, 在这种情况下,"this"指向全局作用域

// 创建一个新函数,将"this"绑定到module对象
// 新手可能会被全局的x变量和module里的属性x所迷惑
var boundGetX = retrieveX.bind(module);
boundGetX(); // 返回 81

bind()的优雅使用

在常见的单体模式中,通常我们会使用 _this , that , self 等保存 this

var foo = {
    bar : 1,
    eventBind: function(){
        var _this = this;
        $('.someClass').on('click',function(event) {
            /* Act on the event */
            console.log(_this.bar);     //1
        });
    }
}

使用 bind() 可以更加优雅的解决这个问题:

var foo = {
    bar : 1,
    eventBind: function(){
        $('.someClass').on('click',function(event) {
            /* Act on the event */
            console.log(this.bar);      //1
        }.bind(this));
    }
}

多次调用bind()

var bar = function(){
    console.log(this.x);
}
var foo = {
    x:3
}
var sed = {
    x:4
}
var func = bar.bind(foo).bind(sed);
func(); //?
 
var fiv = {
    x:5
}
var func = bar.bind(foo).bind(sed).bind(fiv);
func(); //?

答案是,两次都仍将输出 3 ,而非期待中的 4 和 5 。原因是,在Javascript中,多次 bind() 是无效的。更深层次的原因, bind() 的实现,相当于使用函数在内部包了一个 call / apply ,第二次 bind() 相当于再包住第一次 bind() ,故第二次以后的 bind 是无法生效的。

Polyfill(兼容旧浏览器)

bind 函数在 ECMA-262 第五版才被加入;它可能无法在所有浏览器上运行。你可以部份地在脚本开头加入以下代码,就能使它运作,让不支持的浏览器也能使用 bind() 功能。

if (!Function.prototype.bind) {
  Function.prototype.bind = function (oThis) {
    if (typeof this !== "function") {
      // closest thing possible to the ECMAScript 5
      // internal IsCallable function
      throw new TypeError("Function.prototype.bind - what is trying to be bound is not callable");
    }

    var aArgs = Array.prototype.slice.call(arguments, 1), 
        fToBind = this, 
        fNOP = function () {},
        fBound = function () {
          return fToBind.apply(this instanceof fNOP
                                 ? this
                                 : oThis || this,
                               aArgs.concat(Array.prototype.slice.call(arguments)));
        };

    fNOP.prototype = this.prototype;
    fBound.prototype = new fNOP();

    return fBound;
  };
}

我手写 bind1 函数

var a = 10
var obj = {
    a: 20
}
function fn(b, c) {
    console.log('this', this)
    console.log(b, c)
    return this.a
}

Function.prototype.bind1 = function() {
    // 将参数解析为数组
    const args = Array.prototype.slice.call(arguments)
    // 获取 this (取出数组第一项,数组剩余的就是传递的参数)
    const t = args.shift()
    // 当前函数, 即 fn.bind1(...) 中的 fn
    const self = this
    // 返回一个函数
    return function() {
        // 执行原函数,并返回结果
        return self.apply(t, args)
    }
    
}

// fn() // 10

var b = fn.bind1(obj,'peng','you')
b() // 20

1、以上 bind1() 函数中的 arguments 是一个类数组对象

arguments = {
    0: {a: 20},
    1: 'peng',
    2:  'you',
    length: 3
}

2、将类数组对象转化成数组的方法

const args = Array.prototype.slice.call(arguments)
const args = Array.prototype.slice.call(arguments, 0)
const args = [].slice.call(arguments)
const args = Array.from(arguments)  // ES6

以上几个方法都等价

3、args 是一个数组

args = [{a: 20}, 'peng', 'you']

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

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

Summary

apply 、 call 、bind 三者都是用来改变函数的this对象的指向的;
apply 、 call 、bind 三者第一个参数都是this要指向的对象,也就是想指定的上下文;
apply 、 call 、bind 三者都可以利用后续参数传参;
bind 是返回对应函数,便于稍后调用;apply 、call 则是立即调用 。

更多-more