JavaScript辨别apply,call,bind 用法与性能

用法

在JavaScript中,我们经常使用apply,call,bind来绑定this的指向,这是this绑定中的硬绑定,即绑定后无法修改。(下面有些代码会涉及this绑定的内容,有关this绑定的内容可以看看我的另一篇博客-->理解掌握JavaScript的this的指向

首先我们看看下面的代码

function fn(num1, num2, num3) {
    console.log(`a:  ${this.a} sum: ${num1+num2+num3}`);
}

var a = 1;

var obj = {
    a: 2
}

fn(1, 2, 3);//a:  1 sum: 6

fn.call(obj, 1, 2, 3);//a:  2 sum: 6

fn.apply(obj, [1, 2, 3]);//a:  2 sum: 6

fn.bind(obj, 1, 2, 3)();//a:  2 sum: 6

可以看到,我们在方法fn中打印出this.a,且把参数相加打印出来,我们来分析一下上面三个方法的使用和它们的打印结果。

首先先看看全局环境下使用fn方法,因为是在全局中调用的,不是使用new构造的,也并非硬绑定和软绑定,所以是默认绑定,在非严格模式下this.a指向了全局环境下的a,所以fn(1,2,3)最后得到的结果是a:   1 sum:  6。

好,知道了全局环境下直接使用该方法的结果后,我们来看看这三个方法,可以注意

到,三个方法的第一个参数都是对象obj,而最后打印出的结果正式obj.a的值,即是说,在第一个参数上,这三个方法都是一样的,是方法中this要绑定的对象。而后面的参数,是方法的参数,只是形式不一样而已。

我们可以看到,call和bind方法后面的参数都是以逗号分隔开的参数形式,而apply是以数组形式来存放参数的。这就是它们的第一个区别,即参数的传递形式。正是由于apply可以将数组内的成员作为函数的参数传递,所以我们很经常使用apply来把数组内的成员作为参数传递给一个方法。(ES6中提供了扩展运算符用于替代apply的这一用法)

var arr=[1,2,3]

function add(n1,n2,n3){
    console.log(n1,n2,n3);
}

//apply的使用

add.apply(null,arr)//1 2 3

//扩展运算符

add(...arr)//1 2 3

在这里要提出apply的第二个参数实际上不一定要是一个数组,一个带有length的对象也是可以的

function add(n1,n2,n3){
    console.log(n1,n2,n3);
}
add.apply(null,{length:3})
// undefined undefined undefined

//这里实际上可以将第二个参数看为[undefined,undefined,undefined]

接下来继续看三个方法的另一个区别,可以看到,bind后面多了一个括号,即call和apply返回的是函数的调用,而bind返回的是函数,需要再使用括号进行调用。既然如此,那原方法的参数是否可以放在后面的括号里呢,答案是可以的。

fn.bind(obj)(1, 2, 3);//a:  2 sum: 6

fn.bind(obj, 1)(2, 3);//a:  2 sum: 6

fn.bind(obj, 1, 2)(3);//a:  2 sum: 6

这里可以把bind看成是为部分参数提供默认值,利用bind的这一用法,可以很简便地实现函数的柯里化,当然,也可以用来实现thunk。

性能

除了用法之外,还有性能上的区别

这三者的性能排序从高到低(时间消耗从少到多)如下

  1. call
  2. bind
  3. apply

我写了一个demo结合performance来进行测试

<body>
    <button onclick="_call()">call</button>
    <button onclick="_apply()">apply</button>
    <button onclick="_bind()">bind</button>
    <script>
        function fn(a, b) {
            return a + b
        }

        function _call() {
            for (let i = 0; i < 1000000; i++) {
                fn.call(null, 1, 2)
            }
        }

        function _apply() {
            for (let i = 0; i < 1000000; i++) {
                fn.apply(null, [1, 2])
            }
        }

        function _bind() {
            for (let i = 0; i < 1000000; i++) {
                fn.bind(null)(1, 2)
            }
        }
    </script>
</body>

然后在浏览器中,在不同的时间点点击按钮,查看performance中script的时间消耗,结果如图

可以明显看到,apply消耗的时间最多(黄色部分高度最高),接下来我们看到其中具体的时间来比较

这里call和bind消耗的时间差不多,实际上,call和bind在我测试的时候,有时call消耗时间多,有时bind消耗时间多,所以这两个性能上其实区别不大

但是我们可以看到,apply和这两个区别就大了,是十倍的时间,所以在一般情况下,我们尽可能地去使用call和bind,但是有一个问题,如果我们当前的参数在一个数组里,是不是直接用apply比较好呢,在以前,可能是的,但是在ES6之后,有了解构赋值,我们可以将数组的成员解构传参,这里我们修改一下_call函数,使用解构赋值,看性能上是否还是优于apply(因为call和bind差不多,所以我就只测试call做解构赋值处理了),修改函数如下

function _call() {
    for (let i = 0; i < 1000000; i++) {
        fn.call(null, ...[1, 2])
    }
}

可以看到,时间的消耗比apply多,所以如果我们需要传入数组,还是以apply会比较好

 

总的来说,三个方法的区别就有三个

1.第一个参数后面的参数的形式不同,call和bind后面是逗号分隔的参数形式,apply后面是数组形式

2.返回的内容不同,call和apply返回函数调用,bind返回函数

3.性能上的区别,按各自用法call和bind的性能比apply好,但是结合解构赋值,apply性能更好

理解这三个方法的区别并在硬绑定时合理使用,会有很大的便利性。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值