谈谈call和apply

  每个函数都包含两个非继承而来的方法:apply()call()。这两个方法的用途都是在特定的作用域中调用函数,实际上等于设置函数体内this对象的值。 

  一、应用场景

  那就读读这篇文章——浅析读JS中的call和apply里的两个例子吧,这里我仅仅将第一个例子引用过来。

 function dwn(s) {
     document.write(s + "<br />");
 }
 
 window.onload = function () {
     var p = new Point(1, 2);
     var v = new Vector(-1, 2);
     var p1 = add.call(p, 2, 3);
     var p2 = add.apply(v, [2, 3]);
     dwn(p1);
     dwn(p2);
 }
 
 function Point(x, y) {
     this.x = x;
     this.y = y;
     this.toString = function () {
         return "[" + [x, y] + "]";
     }
 }
 
 function Vector(x, y) {
     this.x = x;
     this.y = y;
     this.toString = function () {
         return "(" + [x, y] + ")";
     }
 }
 
 function add(x, y) {
     return new this.constructor(this.x + x,this.y + y);
 }

  代码运行的结果:

       [3,5]
       (1,5)

  在add函数里面this表示的对象的属性值。

  OK,我们明白了,我们通过调用call和apply函数实现了改变函数赖以运行的作用域,这也是这两个方法的真正强大之处。从而改变了Point和Vector实例的自身属性值。

  我想读到这里,对call和apply的应用场景会有一个简单的认识了吧。

  细细观察这个例子,会发现整个过程是通过调用一个通用函数add这个接口来改变某个对象中的属性值。

  我们反向思考,我们通过对象中的某个方法来调用某个通用函数,在调用的过程当中,我们依旧保持当前作用域指向这个对象。

  看下面这个例子——

 1 // 首先我演示一下错误例子,因为这一点常常被我们忽略,导致浪费不少时间去找原因,
 2 // 剖析一下来源,日后事半功倍。
 3 (function ($) {
 4     /**
 5      * 这里例子里面,并没有传递回调函数所需要的执行上下文环境,
 6      * 自然也就无法使用call(apply)来扩充函数赖以运行的作用域了。
 7      */
 8     // 登录验证并执行回调
 9     function checkLogin(callback) {
10 
11         $.ajax({
12             url: baseUrl + "checkLogin",
13             type: 'get',
14             cache: false,
15             success: function (msg) {
16                 
17                 if (msg === 'ok') {
18                     
19                     if (typeof(callback) === 'function') {
20                         
21                         callback();
22                         
23                     }
24                     
25                 }
26                 
27             }
28         });
29         
30     };
31       
32     var obj = {
33           
34         //私有属性
35         _name: 'king',
36           
37         //出口函数
38         todo: function () {
39               
40             //如此调用,会产生什么结果?
41             checkLogin(this._callback);
42               
43         },
44           
45         //登录成功回调函数
46         _callback: function () {
47               
48             //这个例子里面的当前这个函数执行的作用域是window
49             //简单来讲,源于它的被调用者是window
50             //所以自然就无法改变当前obj对象的name属性值了
51             this._name = 'king' + '---callback';
52               
53         }
54           
55     }
56       
57     obj.todo();
58     
59 })(jQuery);

  OK,我们思考一下,怎么办才能达到我们的目的(在调用的过程当中,我们依旧保持当前作用域指向这个对象)。

  这个时候call(apply)就派上用场了——

 1 (function ($) {
 2     /**
 3      * 这里例子里面,传递了回调函数所需要的执行上下文环境
 4      */
 5     // 登录验证并执行回调
 6     function checkLogin(callback, context) {
 7 
 8         $.ajax({
 9             url: baseUrl + "checkLogin",
10             type: 'get',
11             cache: false,
12             success: function (msg) {
13                 
14                 if (msg === 'ok') {
15                     
16                     if (typeof(callback) === 'function') {
17                         
18                         context ? callback.call(context) : callback();
19                         
20                     }
21                     
22                 }
23                 
24             }
25         });
26         
27     };
28       
29     var obj = {
30           
31         //私有属性
32         _name: 'king',
33         
34         //出口函数
35         todo: function () {
36               
37             checkLogin(this._callback, this);
38               
39         },
40           
41         //登录成功回调函数
42         _callback: function () {
43               
44             //此时的this就指向当前这个对象了
45             this._name = 'king' + '---callback';
46               
47         }
48           
49     }
50       
51     obj.todo();
52     
53 })(jQuery);

  嗯,用着很不错,确实实现了需要的目标。但是有更好的方式来实现我们的目标。看下面这个例子——

 1 (function ($) {
 2     
 3     /**
 4      * 这里例子里面,同样没有传递回调函数所需要的执行上下文环境,
 5      */
 6     // 登录验证并执行回调
 7       function checkLogin(callback) {
 8 
 9         $.ajax({
10             url: baseUrl + "checkLogin",
11             type: 'get',
12             cache: false,
13             success: function (msg) {
14                 
15                 if (msg === 'ok') {
16                     
17                     if (typeof(callback) === 'function') {
18                         
19                         callback();
20                         
21                     }
22                     
23                 }
24                 
25             }
26         });
27         
28       };
29       
30       var obj = {
31           
32           //私有属性
33           _name: 'king',
34           
35           //出口函数
36           todo: function () {
37               
38               var that = this;
39               
40               //让传递的参数是一个匿名函数,让这个匿名函数来执行这个回调函数
41               checkLogin(function () {
42                   
43                   //那么也就是实现了让当前这个函数的作用域指向了这个对象
44                   that._callback();
45                   
46               });
47               
48           },
49           
50           //登录成功回调函数
51           _callback: function () {
52               
53               //此时的this就指向当前这个对象了
54               this._name = 'king' + '---callback';
55               
56           }
57           
58       }
59       
60       obj.todo();
61     
62 })(jQuery);

  可以看到,我们可以灵活的对call/apply的应用进行取舍。因此,我们需要深入的理解它们的应用场景,当然更基础的,我们需要理解好它的应用实质。

  二、应用实质

  从上面的两个例子中,我们可以看出来,使用call()(或者apply())来扩充作用域的最大好处是对象不需要与方法有任何耦合关系。

  有时候,可能恰好仅仅需要现有对象其中的一个或两个方法。在想要重用这些方法的同时,但是又确实不希望与源对象形成父-子(或者其他耦合)关系。也就是说,指向使用所需要的方法,而不希望继承所有哪些永远都不会用到的其他方法。在这种情况下,可以通过借用方法模式来实现,而这是收益与call()和apply()函数方法。

  举一个例子——借用数组的方法

  数组具有一些有用的方法,而形如arguments的类似数组的对象并不具有这些方法。因此,arguments可以借用数组的方法,比如slice()方法。如下所示:

function f() {
    //创建空数组的原因只是为了使用数组的方法。此外,
    //能够实现同样功能但是语句稍微长一点的方式是直接从Array的原型中借用方法,
    //即使用Array.prototype.slice.call()方法。这种方式需要输入更长一点的字符,
    //但是却可以节省创建一个空数组的工作。
    var args = [].slice.call(arguments, 1, 3);
    return args;
}

f(1, 2, 3, 4, 5, 6);    //[2, 3]

  仔细研究这个例子,便会发现,我们无需经历继承所带来的麻烦,因为我们仅仅借用数组Array.prototype中的一个方法(slice)罢了,更无需继承Array实例对象永远都不会用到的一些方法,可以仅仅临时性的借用方法slice()即可。

  如此,我们便明白了call和apply的应用实质无非是在借用方法罢了。不得不承认,call和apply的应用,大大增加了代码的重用性。

  三、call和apply的区别

  这两者之间的唯一区别在于其中一个可以接受传递给将被调用方法的参数数组,而另一个仅逐个的接受参数。

//call的例子
notMyObj.doSomething.call(myObj, param1, param2, param3);

//apply的例子
notMyObj.doSomething.call(myObj, [param1, param2, param3]);

  对于是使用apply()还是call(),完全取决于采取哪种给函数传递参数的方式最方便。

参考:

  1> 浅析读JS中的call和apply

  2> 《JavaScript高级程序设计-第2版》

  3> 《JavaScript模式》

 

原文地址:https://www.cnblogs.com/jinguangguo/p/3084269.html