call、apply、bind与this的用法

之前一直对这4个货都是一知半解,今天有必要把它们拎出来做一个了断了

简单的解释

  • this指向函数运行时的对象(谁调用这个函数,this就指向谁)
  • applycallbind都是用来改变函数的this指向的
  • callapply的不同是call传递参数时是一个一个传递, apply传递参数时是以数组的形式传递
  • callapply都是立即调用,bind是返回一个函数,便于稍后调用

this

一般函数调用的时候,有以下几种方式:

func(a, b) //直接调用一个函数
obj.foo(a, b)//调用对象中的方法
func.call(context, a, b)//使用call/apply调用函数
new Foo()//通过new方法来调用构造函数

1.在全局的函数调用中,this指向全局对象,在浏览器中为window

console.log(this);//window

function foo(){
    console.log(this);
}
foo()//window

严格模式下,全局函数中的this指向undefined
2. 调用对象中的方法, this指向这个对象

var obj = {name:'bla'};

function foo(){
   console.log(this);
}
obj.foo = foo;  

obj.foo();//bla

3. call/apply/bind this指向调用的第一个参数

var obj = { num: 2 };

function foo(a, b) {
    console.log(this.num + a + b);
}

foo.call(obj, 3, 4);//9
foo.apply(obj, [5, 6]);//13
var fun = foo.bind(obj, 7, 8)
fun()//17

4. 构造函数中, this指向该函数实例化的时候的对象

var name = 'bob'
function Obj() {
    this.name = 'bla';
}
var o = new Obj()
console.log(o.name)//bla   this并没有改变全局变量的name

前两种调用函数的方式我们也可以这样理解为第三种方式的语法糖:

func(a, b) 等价于func.call(window, a, b)(严格模式下为 func.call(undefined, a, b)
obj.foo(a, b)等价于obj.foo.call(obj, a, b)
那么前三种方式可以合并为一种:

func.call(context,  a,  b)

this就是上面代码中的context

callapply

先看一个例子

function Fruit() {
    this.color = 'red';
}

Fruit.prototype = {
    say: function () { 
        console.log("My fruit color is " + this.color);
    }
}

var apple = new Fruit;
apple.say(); //My fruit color is red

这里我们定义了一个构造函数Fruit,在它的原型上定义了一个say方法,好了,现在我们又有一个对象banana: { color: 'yellow'},我们不想对它重新定义 say 方法,那么我们可以通过 call 或 apply 用 apple 的 say 方法:

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

可以看出 call 和 apply 是为了动态改变 this 而出现的,当一个对象想借用其他对象上的方法的时候就可以用call或者apply来实现

call与apply的区别仅仅在于传参的方式不同,当函数个数确定的情况下用call,不确定的情况下用apply

apply 更多的是用在将数组转化为参数列表

就这么简单,下面看几个实际使用中的例子

  1. 求数组中的极值
    var nums = [3, 56, 16, 99];
    Math.max.apply(Math, nums)//99
    
  2. 合并两个数组
    var list1 = [0, 1, 2];
    var list2 = [3, 4, 5];
    [].push.apply(list1, list2);
    //上面等价于Array.prototype.push.apply(list1, list2)
    //合并数组在ES6语法中也可以这样:list1.push(...list2) 
    console.log(list1); //[0, 1, 2, 3, 4, 5]
    
  3. 将类数组对象/集合转换为数组
exam(1, 2, 3)
function exam(a, b, c, d, e) {
    console.log(arguments);//{ '0': 1, '1': 2, '2': 3 }
    // 使用call/apply将arguments转换为数组, 返回结果为数组,arguments自身不会改变
    var arg = [].slice.call(arguments);
    console.log(arg);//[ 1, 2, 3 ]
}

//也常常使用该方法将DOM中的nodelist转换为数组
[].slice.call( document.getElementsByTagName('li') );

详见MDN上slice的用法

  1. 继承的实现
//可以看作父类
function Animal (name, color) {
    this.name = name;
    this.color = color;
    this.msg = function () {
        console.log(`${this.name}, ${this.color}`)
    }
}
//可以看作子类
function Cat (name, color) {
    Animal.call(this, name, color)
}

var xixi = new Cat('xixi', 'white')
xixi.say()//xixi, white

上面代码中, 将父类Animal中的this指向了子类Cat,所以Cat实例化后的对象xixi就有了父类中的方法,其中Cat函数等同于:

function Cat (name, color, size) {
    this.name = name;
    this.color = color;
    this.msg = function () {
        console.log(`${this.name}, ${this.color}`)
    }
}

bind

bind()方法会创建一个新函数。当这个新函数被调用时,bind()的第一个参数将作为它运行时的 this

传入 bind() 方法的第二个以及以后的参数加上绑定函数运行时本身的参数按照顺序作为原函数的参数来调用原函数
看下面这个例子:

var obj = { num: 2 };

function foo(a, b, c, d) {
    console.log(this.num, a, b, c, d);
}
var fun = foo.bind(obj, 7, 8)
fun(3,1)//2 7 8 3 1

实际使用时

我们来看一个例子

var obj = {
    a: 20,
    handleClick: function () {
        $('someClass').addEventListener('someEvent', this.say, false)
    },
    delay: function() {
        setTimeout(this.say, 1000)
    },
    say: function () {
        console.log(this.a)
    }
}

obj.delay()//undefined
//或者someclass被点击的时候//undefined

因为setTimeout()是window下的方法,所以this会指向window,addEventListener()中的this会指向绑定该事件的节点,我们期望的是将this指向obj对象,通常我们会使用 _this , that , self 等保存 this

在看一个例子:

var obj = {
    a: 20,
    delay: function() {
        setTimeout(function () {
            console.log(this.a)
        }, 1000)
    },

}

obj.delay()//undefined

这里输出undefined的原因是因为闭包里的匿名函数不属于任何对象,所以就指向了全局对象window,如果还不理解的话往下看:

function callback(){}  
setTimeout(callback,2000);

我们可以把上面代码理解为:

function setTimeout(fn,delay) {
   // 等待delay 毫秒
   fn(); // <-- 调用位置!
}

可以看到fn()是属于全局对象window的了吧

现在我们有了bind()方法就可以像这样:

var obj = {
    a: 20,
    handleClick: function () {
        $('someClass').addEventListener('someEvent', this.say.bind(this), false)
    },
    delay: function() {
        setTimeout(this.say.bind(this), 1000)
    },
    say: function () {
        console.log(this.a)
    }
}

obj.delay()//20

箭头函数中的this

所有的箭头函数都没有自己的this,都指向外层,即父级作用域
更多关于箭头函数的内容看MDN上的解释
上面的代码我们可以简单的改写为:


var obj = {
    a: 20,
    delay: function() {
        setTimeout(() => {
            console.log(this.a)
        }, 1000)
    },
}

obj.delay()//20

用call/apply实现bind

在知道了以上知识后,我们自己用apply实现一个简单的bind方法,以加深理解

Function.prototype.bind = function (obj) {
    var self = this; // 保存原函数
    return function () { // 返回一个新的函数  
         self.apply(obj, arguments); // 执行新的函数的时候,会把之前传入的obj当作新函数体内的this
    }
};

var obj = {
    name: 'jack'
};
var func = function () {
    console.log(this.name)
}.bind(obj);

func();//jack

当然如果用箭头函数的话,Function.prototype.bind方法可以改写为:

Function.prototype.bind = function (obj) {
    return () => {
        this.apply(obj, arguments);
    }
}

这里bind()方法不能传递额外的参数,我们把它稍微改进一下:

//升级版
Function.prototype.bind = function () {
    var self = this, // 保存原函数
        context = [].shift.call(arguments), // 获取bind参数的第一个元素作为this要指向的对象
        args = [].slice.call(arguments); // 剩余的参数转成数组
    return function () { 
         self.apply(context, [].concat.call(args, [].slice.call(arguments)));
        // 将bind方法中的参数与fn中的参数合并成为新函数的参数
    }
};

var obj = {
    name: 'jack'
};
var func = function (a, b, c, d) {
    console.log(this.name, a, b, c, d) 
}.bind(obj, 3, 4);

func(5, 6);//jack 3 4 5 6

ES7中的::运算符

在ES7中有一个新的语法糖:“函数绑定”(function bind)运算符,函数绑定运算符是并排的两个双冒号(::),双冒号左边是一个对象,右边是一个函数。该运算符会自动将左边的对象,作为上下文环境(即this对象),绑定到右边的函数上面。

foo::bar;
// 等同于
bar.bind(foo);

foo::bar(...arguments);
// 等同于
bar.apply(foo, arguments);

如果双冒号左边为空,右边是一个对象的方法,则等于将该方法绑定在该对象上面

var method = obj::obj.foo;
// 等同于
var method = ::obj.foo;

let log = ::console.log;
// 等同于
var log = console.log.bind(console);

由于双冒号运算符返回的还是原对象,因此可以采用链式写法。
关于双冒号运算符的更多用法看ECMAScript 6 入门

参考资料

mode_edit