javascript基础巩固(一)apply、call、bind函数

176 阅读7分钟

应用背景

apply,call,bind出现的目的都是切换/固定函数内部this的指向(即函数执行时所在的作用域)。

  • Function.prototype.call()
  • Function.prototype.apply()
  • Function.prototye.bind()

Function.prototype.call()

函数实例的call方法,可以指定函数内部this的指向(即函数执行时所在的作用域),然后在所指定的作用域中,调用该函数。例如:

var obj = {};
var f = function () {
    console.log(this);
}
f();//window
f.call(obj);//obj

上面的代码在控制台运行f()this指向全局环境window(不同的环境对应的全局环境不同,nodeglobal);调用f.call(obj)时,call函数会改变函数f内部的指向为obj对象。

call()函数参数f.call(thisArgs, arg1,arg2,...)

call函数的第一个参数是this指向的对象,1,如果thisArgs不是一个对象,则会自动包装为对应的对象; 2,如果thisArgs为空、nullundefined,则默认传入全局对象;arg1,arg2,...是传入函数的参数。例如:

var name = '李四';
var obj = {
    name: '张三'
};
var f = function (){
    console.log(this.name);
};
f();//李四
f.call();//李四
f.call(null);//李四
f.call(undefined);//李四
f.call(window);//李四
f.call(obj);//张三
f.call('王五');//undefined

call()函数的特殊应用--调用对象的原生方法

var obj = {};
obj.hasOwnProperty('toString');//false
// 覆盖掉obj继承的方法
obj.hasOwnProperty = function () {
    return true;
}
// 覆盖掉之后就得不到正确的结果,调用自己定义的hasOwnProperty方法
obj.hasOwnProperty('toString');//true
// 如下调用就可解决该问题,直接在obj对象调用原型链上的hasOwnProperty方法
Object.prototype.hasOwnProperty.call(obj, 'toString');//false

call()函数将hasOwnProperty方法的原始定义放到obj对象上执行,这样即使覆盖了hasOwnProperty方法也不会影响结果。

call()函数的特殊应用--实现继承

function a(){
    this.name = '张三';
    this.getInfo = function (){
        console.log('a:', this.name, this.age);
    }
}
function b(){
    this.age = 18;
    a.call(this);//this表示对象本身
}
var bt = new b();
console.log(bt);

运行结果:

结果
在使用new关键字实例化bt对象时会将所有this关键字的属性都生成了新对象的属性,也就是说new关键字会将所有的和this有关的属性都会转化为新对象的属性,那么当call方法将bthis对象赋值给a方法时,new也会检测到并且当做是b的属性一起实例化,所以实现继承的方式call只是起了个替换this对象的作用,主要工作还是在new 关键字,它能自动检测和this绑定的属性,全部添加到要实例化的对象上面。


Function.prototype.apply()

apply函数与call函数类似,唯一的区别是他接收一个数组作为执行时的参数,使用方式为:

func.apply(thisArgs, [arg1,arg2,...]);
//实例
function f(a,b){
    console.log(a*b);
}
f.call(null, 2,2);//4
f.apply(null,[2,2]);//4

第一个参数thisArgsthis指向的对象,也就是函数执行时所在的作用域;(1)如果thisArgsnull或者undedined,则与call函数一样,相当于传入了全局对象。

apply函数的特殊应用:

(1)寻找数组中最大的元素

var a = [3,8,10,5,2];
Math.max.apply(Math, a);//10
Math.max.apply(null, a);//10
Math.max.apply(undefined, a);//10

(2)转换数组的空元素为undefined
通过apply方法,利用Array构造函数将数组的空元素变为undefined

var a = ['a',,'b'];
var b = Array.apply(null, a);//???这里调用Array函数的时候发生了什么??
//不是应该用new调用吗?
console.log(b);//["a", undefined, "b"]

将数组空元素转换为undefined的用处:
数组的forEach方法会跳过空元素,但是不会跳过undefined(也不会跳过null),这样遍历数组会有不一样的结果。实例如下:

var a = ['a',,'b'];
var print = function (i) {
    console.log(i);
};
a.forEach(print);
//a
//b
Array.apply(null,a).forEach(print);
//a
//undefined
//b

(3)转换类数组对象为真正的数组--利用数组的slice方法

var a = {
    0: 1,
    1: 2,
    length: 2
};
var b = {
    0:1,
};
var c = {
    0: 1,
    length: 2,
};
var d = {
    length: 2,
};
Array.prototype.slice.apply(a);//[1,2]
Array.prototype.slice.apply(b);//[]
Array.prototype.slice.apply(c);//[1,](有一个空元素)
Array.prototype.slice.apply(d);//[,] (两个空元素)
Array.apply(null,Array.prototype.slice.apply(d));//[undefined, undefined]

由上可知,apply方法的参数都是对象,返回结果是数组,但是有两个需要注意的地方是:一是必须有length属性,以及对应的数字键,该方法才起作用;二是length属性值大于拥有的键值对时,会返回空元素,这个时候要想让其返回undefined,可参考上面的方法。
(3)绑定回调函数的对象
回调函数中的this问题:

var o = new Object();
o.f = function () {
    console.log(this === o);
};
//jQuery的写法
$('#button').on('click', o.f);//false

上面点击按钮后控制台显示false的原因就是点击的时候,由于f方法是在按钮对象的环境中调用的,因此this不再指向o对象,而是指向按钮的DOM对象,为了解决上述问题,我们可以使用apply或者call方法。如下:

var o = new Object();
o.f = function () {
    console.log(this === o);
};
var f = function () {
    o.f.apply(o);
    //或者o.f.call(o);
};
//jQuery的写法
$('#button').on('click', f);//true

由于调用o.f函数的时候显示绑定的该函数的运行环境,点击按钮以后控制台打印true;同时applycall函数都是会立即执行的,所以这里使用一个函数f进行包装。


Function.prototype.bind()

bind方法主要是将函数体内的this对象绑定到某个对象,然后返回一个新函数

var d = new Date();
d.getTime();//1561281958569
var print = d.getTime;
print();
//固定d.getTime函数内部的this变量
var prints = d.getTime.bind(d);
prints();//1561282104241

结果
如上,将d.getTime赋给变量print之后再调用该方法就报错(如上)。原因就是getTime方法内部的this原本指向Date对象的实例,赋值给print方法后,this不再指向Date对象的实例。如果用bind方法将getTime方法内部的this固定到Date对象的实例,就可以随心所欲的赋值给其他变量了。

var counter = {
    count: 0,
    inc: function () {
        this.count++;
        console.log(this === counter);
    }
};
var f = counter.inc.bind(counter);
f();//1,true
//this也可以绑定到其他的对象
var obj = {
    count: 30,
}
var ff = counter.inc.bind(obj);
ff();//31, false

bind函数参数

function.prototype.bind(thisArg, arg1, arg2,...) bind函数的参数和call函数一样,第一个参数thisArg是所要绑定的this的对象。arg1,arg2,...是目标函数被调用时,预先绑定到函数参数列表的参数。调用bind函数之后的返回值是:一个原函数的拷贝,并拥有指定的thsi值和初始参数。请参考MDN解释

var add = function (x,y) {
    return x *this.m + y * this.n;
};
var obj = {
    m: 1,
    n: 2,
};
var newAdd = add.bind(obj, 5);
newAdd(5);//15
var m = 3;
var n = 3;
var newAdds = add.bind(null, 5);
newAdds(5);//30
var newAddp = add.bind(undefined, 5);
newAddp(5);//30

如果bind函数的第一个参数是null或者undefined,和call,apply的规则是一样的,相当于将this绑定到了全局对象。

bind函数使用注意事项:

(1)bind用于事件监听

document.getElementById('button').addEventListener('click', o.m.bind(o));
document.getElementById('button').removeEventListener('click', o.m.bind(o));

上面给按钮绑定一个click事件的时候使用bind方法生成一个匿名函数,导致无法取消绑定,因此写法有误,正确的写法应该是:

var listener = o.m.bind(o);
document.getElementById('button').addEventListener('click', listener);
document.getElementById('button').removeEventListener('click', listener);

(2)bind函数与回调函数结合使用

var counter = {
    count: 0,
    inc: function () {
        'use strict';
        this.count++;
        conso.log(this === counter);
    }
};
var callIt = function (callback) {
    callback();
};
callIt(counter.inc.bind(counter));
console.log(counter.count);//1,true

上面callIt函数内部调用回调函数时,如果直接传入counter.inc,则counter.inc内部的this会指向全局对象,但是用bind方法绑定之后,counter.inc中的this总是指向counter
(3)数组的forEach中的使用

var obj = {
    name: 'john',
    times: [1, 2, 3],
    print: function () {
       this.times.forEach(function (n) {
           console.log(this.name);
           console.log(this === window);
       }) 
    },
}
obj.print();
//true
//true
//true

上面代码中,obj.print内部this.timesthis是指向obj的,这个没有问题。但是,forEach方法的回调函数内部的this.name却是指向全局对象,导致没有办法取到值,要解决这个问题就要使用bind函数固定this

obj.print = function () {
    this.times.forEach(function (n) {
        console.log(this.name);
        console.log(this === window);
    }.bind(this))
}
obj.print();
//john
//false
...

(4)结合call方法的使用

[1,2,3].slice(0,1);//[1]
Array.prototype.slice.call([1, 2, 3], 0, 1);//[1]

上面的代码中,数组的slice方法从数组按照指定位置和长度切分出另一个数组。这样做的本质是在数组上面调用Array.prototype.slice方法,因此可以用call方法表达这个过程。这里可以理解为call方法内部的this指向slice

call方法实质上是调用Function.prototype.call方法,因此上面的表达式可以用bind方法改写。

var slice = Function.prototype.call.bind(Array.prototype.slice);
slice([1, 2, 3], 0,1);//[1]
var push = Function.prototype.call.bind(Array.prototype.push);
var pop = Function.prototype.call.bind(Array.prototype.pop);
var a = [1,2,3];
push(4);
console.log(a);//[1,2,3,4]
a.pop();//4

上面代码的含义就是,将Array.prototype.slice变成Function.prototype.call方法所在的对象,调用时就变成了Array.prototype.slice.call,其他的两个push,pop也一样。如果再进一步,将Function.prototype.call方法绑定到Function.prototype.bind对象,就意味着bind的调用形式也可以改写。

function f() {
    console.log(this.v);
};
var obj = {
    v: 123
};
f.bind(obj)();
var bind = Function.prototype.call.bind(Function.prototype.bind);
bind(f,obj)();//123

上面代码的含义就是,将Function.prototype.bind方法绑定在Function.prototype.call上面,所以bind方法就可以直接使用,不需要在函数实例上使用。

总结