JavaScript中的 Call 和 Apply

1. call 和 apply的区别

Function.prototype.call 和 Function.prototype.apply都是非常常用的方法,它们的作用一模一样,区别仅在于传入参数形式的不同。

apply接受两个参数,第一个参数指定了函数体内 this 对象的指向,第二个参数为一个带下标的集合,这个集合可以为数组,也可能为类数组,apply 方法把这个集合中的元素作为参数传递给被调用的函数:

var func = function (x,y,z) {
    console.log([x,y,z]);
    // [1,2,3]
};
func.apply(null,[1, 2, 3]);

在这段代码中,参数1、2、3被放在数组中一起传入func函数,它们分别对应func参数列表中的x、y、z。

call传入的参数数量不固定,跟apply相同的是,第一个参数也是代表函数体内的this指向,从第二个参数开始往后,每个参数被依次传入函数:

var func = function (x,y,z) {
    console.log([x,y,z]);
    // [1,2,3]
};
func.call(null,1, 2, 3);

当调用一个函数时,JavaScript的解释器并不会计较形参和实参在数量、类型以及顺序上的区别,JavaScript的参数在内部就是用一个数组来表示的,从这个意义上来说,apply比call的使用率更高,我们不必关心具体有多少参数被传入函数,只要用apply一起推过去就完事了。

call是包装在apply上面的一颗语法糖,如果我们明确地知道函数接受多少个参数,而且想一目了然的表达形参和实参的对应关系,那么也可以用call来传递参数。

当使用call 或者 apply 的时候,如果我们传入的第一个参数为null,函数体内的this会指向默认的宿主对象,在浏览器中为window。

var func = function (x, y, z) {
    console.log(this==window);
    // true
};
func.call(null, 1, 2, 3);

但如果是在严格模式下,函数体内的this还是为null。

var func = function (x, y, z) {
    console.log(this===null);
    // true
};
func.call(null, 1, 2, 3);

有时候我们使用call或者apply的目的不在于指定this指向,而是另有有途,比如借用其也对象的方法,那么我们可以传入null来代替某个具体的对象:

var num = Math.max.apply(null,[1,2,3,4,5,6]);
console.log(num);
// 6

2. call和apply的用途

1. 临时改变this的指向,这是它们最常见的用途。下代码可以用来说明:

var user1= {
    name:"Augus"
};
var user2 = {
    name:"Yuki"
};
window.name="Window";
var getName = function(){
    console.log(this.name);
};
getName();
// Window
getName.call(user1);
// Augus
getName.call(user2);
// Yuki

当执行getName.call(user1)这行代码时,getName函数体内的this就指向user1对象,所以此处的:

var getName = function(){
    console.log(this.name);
};

实际相当于:

var getName = function(){
    console.log(user1.name);
};

在实际开发中,经常会遇到this指向被不经意改变的场景,比如有一个div的节点,div的节点的onclick事件中的this本来是指向这个div的。

document.getElementById("div").onclick = function(){
    console.log(this.id);
    // div
};

假如该事件函数中有一个内部函数func,在事件内部调用这个函数时,func函数体内的this就指向了window,而不是我们预期的div,请看如下代码:

document.getElementById("div").onclick = function(){
    console.log(this.id);
    // div
    var func = function(){
        console.log(this.id);
        // undefined
    };
    func();
};

这个时候我们可以用call来修正func函数的指向this,使其依然指向div。

document.getElementById("div").onclick = function(){
    console.log(this.id);
    // div
    var func = function(){
        console.log(this.id);
        // div
    };
    func.call(this);
};

另外在本博客的"JavaScript中this的理解"也用apply来修正this,代码如下:

<div id="user">我是一个用户</div>
<script>
    document.getElementById=(function(func){
        console.log(func);
        // document.getElementById()
        return function(){
            return func.apply(document,arguments);
        };
    })(document.getElementById);
    var getId = document.getElementById;
    var user = getId("user");
    console.log(user.id);
    // user
</script>

2. 永久绑定的 this 的 bind

大部分高级浏览器都实现了内置的Function.prototype.bind,用来指定函数内部的this指向,如下所示:

Function.prototype.bind = function(context){
    // 保存原函数,即调用时的函数
    var that = this;
    return function(){
        // 为调用函数传入指定的this对象及函数
        return that.apply(context,arguments);
    };
};

var user = {
    name:"Augus"
};

var func = function(){
    console.log(this.name);
    // Augus
}.bind(user);

func();

我们通过Function.prototype.bind来包装func函数,并且传入一个对象context作为参数,这个context对象就是我们想修正的this对象。

在Function.prototype.bind的内部实现中,我们先通过 var that = this 这行代码把func函数的引用保存起来,然后返回一个新的函数。当我们在执行func这个函数时,实际上先执行的是这个刚刚返回的新函数。在新函数的内部,that.apply(context , arugments) 这行代码才是执行的原来的func函数,并且指定context对象为func函数体内的this。

这是一个简化版的Function.prototype.bind实现,通常我们会把它实现的更为复杂一点,使得可以往函数中预定义一些参数。

Function.prototype.bind = function(){

    // 保存原函数,即调用时的函数
    var that = this;

    console.log(arguments);
    // Arguments(3) [{…}, 1, 2, callee: ƒ, Symbol(Symbol.iterator): ƒ]

    // 需要绑定this的上下文
    var context = [].shift.call(arguments);
    console.log(context);
    // {name:"Augus"}

    // 剩余的参数转成数组
    var args = [].slice.call(arguments);
    console.log(args);
    // [1,2]


    // 返回一个新的函数
    return function(){
        console.log(arguments);
        // Arguments(2) [3, 4, callee: ƒ, Symbol(Symbol.iterator): ƒ]

        // 执行新函数的时候,会把之前传入的context当作新函数体内的this
        // 并且组合两次分别传入的参数,作为新函数的参数
        return that.apply(context,
            [].concat.call(
                args,
                [].slice.call(arguments)
            )
        );
    };
};

var user = {name:"Augus"};

var func = function(a,b,c,d){
    console.log(this.name);
    // Augus
    console.log([a,b,c,d]);
    // [1,2,3,4]
}.bind(user,1,2);

func(3,4);

3.  改变this借用其他对象的方法

方法的第一种场景是"借用构造函数",通过这种技术,可以实现一些类似继承的效果,代码如下:

var userA = function(name){
    this.name = name;
};
var userB = function(){
    userA.apply(this,arguments);
};
userB.prototype.getName = function(){
    return this.name;  
};
var user = new userB("Augus");
console.log(user.getName());
// Augus

借用的第二种方法运用的场景跟我们的关系更加的密切。

函数的参数列表arguments是一个类数组对象,虽然它也有"下标",但它并不是真正的数组,所以也不能像数组一样,进行排序操作或者往集合里添加一个新的元素,这种情况下,我们常常会借用Array.prototype对象上的方法。 比如想往arguments中添加一个新的元素,通常会借用Array.prototype.push。  

(function(){
    Array.prototype.push.call(arguments,3);
    console.log(arguments);
    // Arguments(3) [1, 2, 3, callee: ƒ, Symbol(Symbol.iterator): ƒ]
})(1,2);

在操作arguments的时候,我们经常会频繁地找Array.prototype对象借用方法。

想把arguments转成真正的数组的时候,可以借用Array.prototype.slice的方法;想截取arguments列表中的头一个元素时,以可以借用Array.prototype.shift方法。那么这种机制的内部实现原理是什么呢?以Array.prototype.push为例,看看V8引擎中是如何实现的。

function ArrayPush(){
    // 被push对象的length
    var n=TO_UIN32(this.length);
    // push参数的个数
    var m=%_ArgumentsLength();

    for(var i=0;i<m;i++){
        // 复制元素
        this[i+n]= %_Arguments(i);
    };
    this.length = n+m;
    return this.length;
}

通过这段代码可以看到,Array.prototype.push 实际上是一个属性复制的过程,把参数按照下标依次添加到被push的对象上面,顺便修改了这个对象的length属性,至于修改的对象是数组还是类数组对象并不重要。

可以看出来,Array.prototype.push并不是数组的专属,对象也可以借用。

var obj = {};
Array.prototype.push.call(obj,'user');
console.log(obj.length);
// 1
console.log(obj[0]);
// user

上述代码在大部分浏览器中都可以正常跑通,如果在低版本的IE浏览中执行,必须显示的给对象obj设置length属性:

var obj = {} ; obj.length=0 ;

大家都在知道,在JavaScritp中一切皆对象,但并不是所有的对象都可以借用其它对象的方法,就像我和马云都是中国人,但我却不可能向他借到钱一样,以Arry.prototype.push方法为例,要借用到此方法,必须要满足两个条件:

1. 对象本身要可以存取属性,像number和str类型的数字是绝对不可能借到这个方法的。

2. 对象本身的length属性要可写,如果借用此方法的对象是一个function,就会产生报错。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

aiguangyuan

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值