栗子最实在:用实例来说明call、apply和bind方法

前言

call,apply和bind方法,之前使用的时候,总会有蒙圈的时候。这次顺着用实例来说明this的含义之后,就一次也把这三个方法整理明白吧。

一、call和apply的比较

ECMAScript标准中Function原型具备有call方法 Function.prototype.call和apply方法Function.prototype.apply,因此JavaScript中每个函数都有call方法和apply方法。
  call方法和apply方法,实现的效果作用是一样的,区别就在于参数的形式和数量上不同。具体而言是:

  • call方法接收的参数数量可以是多个。第一个参数是函数执行上下文的对象,后面的参数可以是多个
<script>
        var a={
            name:'hyh'
        }
        var name='hyhaa'
        function exp(a,b){
            console.log(a+this.name+b)
        }
        exp.call(null,'happy ',' day') //happy hyhaa day
        exp.call(a,'happy ',' day') //happy hyh day
    </script>
  • apply方法接收的参数数量是两个。第一个参数也是函数执行上下文的对象,第二个参数是多个传入的函数参数组成的类数组。
<script>
        var a={
            name:'hyh'
        }
        var name='hyhaa'
        function exp(a,b,c){
            console.log(this.name+a+b+c)
        }
        exp.apply(null,[' happy ',' every',' day']) //hyhaa happy  every day
    </script>

call和apply方法的使用,其实就是换一种参数传递的形式来达到使用函数方法的目的。比如有些时候,我们手头有的参数形式是数组(类数组),直接传递给某个函数方法,但其不接受,那我们就借位,把函数方法换个apply写法,这样就能接受我们的参数形式。

参数为数组的情况下,Math.max方法的使用示例和对比

所以,鉴于apply和call方法的作用是一样的,那么如何挑选哪种写法合适,就看具体情况了。简单来说,就是参数是数组形式,就使用apply方法,参数是单独多个的话,就使用call方法。
  注意:如果第一个参数不需要写,不需要改动函数上下文的对象的话,记得写上null值来占位。如上面例子中的exp.apply(null,[' happy ',' every',' day'])写法

二、call和apply的作用

2.1 改变this的指向

接上面所说的,call方法和apply方法的第一个参数都是作为函数执行上下文的对象,也就是说call方法和apply方法改变了函数被调用时this的指向

    <script>
        var value=100
        var obj5={
            value:200
        }
        function fn4(a,b){
            console.log(this.value+a+b)
        }
        fn4(3,4) //107
        fn4.call(obj5,3,4) //207
        fn4.apply(obj5,[3,4]) //207
    </script>

这个例子中,等同于obj5这个对象传递给了函数fn4,函数fn4的this就指向obj5。等同于实现:

function fn4(a,b){
  console.log(obj5.value+a+b)
}

再举个复杂点的例子:

<script>
    var fruits={
     name:'apple',
     saleprice:function(price){
           console.log(this.name+' price is '+price +' yuan')
         }
     }
     var vegtable={
         name:'tomato'
     }
     fruits.saleprice('20')  //apple price is 20 yuan
     fruits.saleprice.call(vegtable,'12')  //tomato price is 12 yuan
</script>

在这个例子中,fruits.saleprice.call(vegtable,'12')是将vegtable传给到fruits对象中,因此this.name就等同于vegtable的name属性'tomato'。

2.2 立即执行函数

这两个方法的使用,会让函数立即执行。

function exp(){
  console.log(1)
}
exp.call()
exp.apply()

不过因为例如exp()的写法也能让函数立即执行,所以常见的是这种更简便的写法exp()。

三、call和apply常见的应用写法

3.1 数组的扩充

   <script>
        var a=[123,'2fds','3sdf',34]
        var b=['aa','bb',343]
        Array.prototype.push.apply(a,b)
        console.log(a)  //[123, "2fds", "3sdf", 34, "aa", "bb", 343]
   </script>

3.2 字符串的拼接

function expjoin(arguments){
  var a=Array.prototype.join.apply(arguments);
  console.log(a);
}
expjoin([123,'2fds','3sdf',34]) //"123,2fds,3sdf,34"

这个写法也等同于

[].join.apply([123,'2fds','3sdf',34])  //"123,2fds,3sdf,34"

3.3 最大/小值的获取

var arr=[34,54,656,877]  //undefined
Math.max.apply(Math,arr)  //877
Math.max.apply(null,arr)  //877
Math.max.call(null,34,54,656,877)  //877
Math.min.call(null,34,54,656,877)  //34
Math.min.apply(null,arr)  //34
Math.min.apply(Math,arr)  //34

3.4 验证是否为数组

<script>
        function isArray(obj){
            return Object.prototype.toString.call(obj) ==='[object Array]'
        }
        console.log(isArray(343));
        console.log(isArray([4343,565]));
</script>

3.5 把一个类数组转换为真正的数组

var arr= Array.prototype.slice.call([2,4,5]);
arr  //[2, 4, 5]
arr.push('33')  //[2, 4, 5, "33"],能够具备并使用数组的方法例如push()
domNodes.push(3434)  //[2, 4, 5, "33", 3434]

四、call和apply写法的转换

如果看了感觉会晕,那么可以换种方式来看call和apply的写法

Array.prototype.join.apply(arguments)等同于arguments.join()
Array.prototype.slice.call(arguments)等同于arguments.slice()

前面例子中的fruits.saleprice.call(vegtable,'12')等同于vegtable.saleprice('12')

这样就不难理解,诸如下面的写法:

function log(){
    console.log.apply(console, arguments); //console.log(arguments)
}
log(1,2,3,4,5)  //arguments会默认使用[ ]运算符获取参数,结果为:1 2 3 4 5

对上面这个例子使用到了arguments,再来引申讲下arguments

  • 在函数调用时,会自动在该函数内部生成一个名为 arguments的隐藏对象

  • 该对象类似于数组,可以使用[ ]运算符获取函数调用时传递的实参

  • 只有函数被调用时,arguments对象才会创建,未调用时其值为null

 function fn5(name, age){
     console.log(arguments); //["Byron", 20]
     name = 'XXX';
     console.log(arguments); //["XXX", 20]
     arguments[1] = 30;
     console.log(arguments); //["XXX", 30]
 }
 fn5('Byron', 20);

五、bind方法

终于写到bind了~~bind的用法稍微迂回点,需要稍长点耐心理解。
call方法和apply方法是ECMAScript3标准定义的,而bind方法是更晚的ECMAScript5标准定义的方法。

5.1 bind、call和apply方法的相同之处

前面我们已经说了call和apply的作用相同,区别就在参数的形式不同。bind方法的作用也是改变this的指向。bind方法的参数要求和call方法一样,第一个参数是函数执行上下文的对象,后面的参数可以是多个。
  但bind又有自己的特别之处,下面来说说bind的用法上的区别:

5.2 bind方法的用法上的区别

区别一: bind()方法是会创建一个新函数,当调用这个新函数时,会以创建新函数时传入 bind()方法的第一个参数作为 this。
看栗子来体会:

var name='xxx'; 
var obj = {
    name: 'yyy'
}
function func() {
    console.log(this.name);
}
var func1 = func.bind(obj);
func1();   //yyy
func();     //xxx

func1等于通过bind方法创建的和func函数相同的新函数。func1的this对象为传入的obj,所以this.name就只等于obj对象的name值‘yyy’。
  因为是新生成的函数,所以原来函数func()并不受影响,所以直接执行func(),this指向全局window,所以this.name为'xxx'。

区别二: 参数的使用上,call方法是将第二个及之后的参数,作为实参传递到函数中。
  而 bind() 方法的第二个以及以后的参数,再加上新函数运行时的参数,按照顺序作为实参传递到新函数中。
  这样看文字解释,真的太绕了,直接看栗子,就很好理解:

function func(a, b, c) {
    console.log(a, b, c);
}
var func1 = func.bind(null,'aaa');

func('A', 'B', 'C');            // A B C
func1('A', 'B', 'C');           // aaa A B
func1('B', 'C');                // aaa B C
func.call(null, 'aaa');      // aaa undefined undefined

区别三: 最后一个不同,就是bind函数不是立即调用的,实现生成新的处理函数,然后再执行。而call和apply方法都是立即执行。所以写法上,bind函数需要再加个()
  还是看栗子吧,改写下前面的例子:(一样的实现效果,不一样的执行写法)

var name='xxx'; 
var obj = {
    name: 'yyy'
}
function func() {
    console.log(this.name);
}
console.log(func.bind(obj());  //yyy
console.log(func.call(obj));    //yyy
console.log(func.apply(obj));  //yyy
5.3 bind方法常见的应用写法

例子1:改变setTimeout()方法的this指向
  上一篇专门写this的使用实例的时候,讲到一个setTimeout()的例子。上一篇的例子中是使用var _this=this的方式来使setTimeout()的this不指向window。现在学了bind,同样也可以用bind来达到同样效果:

<body>
    <button class="bindtest">dianwo</button>
    <script>
    /*bind的用法--和使用var _this=this达到同样的目的*/
        $bindtest=document.querySelector('.bindtest')
        $bindtest.addEventListener('click',function(){
            console.log(this) //<button class="domtest">dianwo</button>
            setTimeout(function(){
                console.log(this)   //<button class="domtest">dianwo</button>
            }.bind(this),300)
        })
    </script>
</body>

前面说了,bind的作用是得到一个新的函数,而新函数的this就是bind传递的第一个参数。这里bind方法是和setTimeout方法同级的,属于$bindtest对象的点击事件下的,所以this是$bindtest对象。

例子2:字符串拼接的bind方法实现
  前面call方法的使用例子中实现的字符串拼接,现在换成用bind方法来实现:
  常见的是,比如我需要一个join操作,但是我没有这个方法,那么我就需要借助数组Array原型的join方法来借位使用。

function joinStr(){
      var joins=Array.prototype.join.bind(arguments);
      console.log(joins('-'))
}
joinStr('a','b','c')

例子中的写法等同于['a','b','c'].join('-')

六、结语

写了这么一大通,结合实例来理解,真的事半功倍。我列举的call,apply和bind方法的常见使用的例子,只是我目前看到的常见写法。更多的变化写法,就待实践和学习,来扩充积累啦。

参考文章:

https://segmentfault.com/a/1190000009650716
http://book.jirengu.com/fe/%E5%89%8D%E7%AB%AF%E8%BF%9B%E9%98%B6/%E9%9D%A2%E5%90%91%E5%AF%B9%E8%B1%A1/this.html
https://github.com/lin-xin/blog/issues/7
https://segmentfault.com/a/1190000006993545

推荐阅读更多精彩内容