js设计模式基础篇(二)之this、call和apply

     在 JavaScript 编程中,this 关键字总是让初学者感到迷惑,Function.prototype.call 和Function.prototype.apply 这两个方法也有着广泛的运用。我们有必要在学习设计模式之前先理解这几个概念。

this

   Javascript的this总是指向一个对象,而具体指向哪个对象是在运行时基于函数的执行环境动态绑定的,而非函数被声明时的环境。                                                           

this的指向 

this的指向大致可以分为以下4种:

1.作为对象的方法调用

2.作为普通函数调用

3.构造器调用

4.Function.prototype.call或Function.prototype.apply调用。

下面我们分别进行介绍。 

 

  • 1.作为对象的方法调用

   当函数作为对象的方法被调用时,this 指向该对象

   var obj = { 
          a: 1, 
         getA: function(){ 
                  alert ( this === obj );          // 输出:true 
                  alert ( this.a );                // 输出: 1 

          }

   }

  obj.getA(); 

  • 2.作为普通函数被调用

    当函数不作为对象的属性被调用时,也就是我们常说的普通函数方式,此时的 this 总是指
向全局对象。在浏览器的 JavaScript 里,这个全局对象是 window 对象。

window.name = 'globalName'; 
var getName = function(){ 
     return this.name; 
}; 

console.log( getName() );         // 输出:globalName 
或者:

window.name = 'globalName'; 
var myObject = { 
      name: 'sven', 

      getName: function(){ 
          return this.name; 
      } 
}; 
var getName = myObject.getName;

console.log( getName() );        //  globalName 

在 ECMAScript 5 的 strict 模式下,这种情况下的 this 已经被规定为不会指向全局对象,而是 undefined:

 function func(){ 
      "use strict" 
       alert ( this );              // 输出:undefined 
  } 

 func(); 

 

  • 3. 构造器调用

    JavaScript 中没有类,但是可以从构造器中创建对象,同时也提供了 new 运算符,使得构造器看起来更像一个类。

    除了宿主提供的一些内置函数,大部分 JavaScript 函数都可以当作构造器使用。构造器的外表跟普通函数一模一样,它们的区别在于被调用的方式。当用 new 运算符调用函数时,该函数总会返回一个对象,通常情况下,构造器里的 this 就指向返回的这个对象,见如下代码:

var MyClass=function(){

    this.name='sven';

};

var obj=new MyClass();

alert( obj.name );           //  输出:sven

但用 new 调用构造器时,还要注意一个问题,如果构造器显式地返回了一个 object 类型的对象,那么此次运算结果最终会返回这个对象,而不是我们之前期待的 this:

var MyClass=function(){

      this.name='sven';

      return {      // 显示地返回一个对象

          name:'anne'

      }

};

var obj=new MyClass();

alert( obj.name );       // 输出:anne

如果构造器不显式地返回任何数据,或者是返回一个非对象类型的数据,就不会造成上述问题

var MyClass=function(){

     this.name='sven';

    return 'anne';    // 返回string类型

};

var obj=new MyClass();

alert( obj.name );        // 输出:sven

 

4.Function.prototype.call或Function.prototype.apply调用

跟普通的函数调用相比,用Function.prototype.call或Function.prototype.apply可以动态地改变传入函数的this:

var obj1={

    name:'sven',

    getName:function(){

            return  this.name;

    }

};

var obj2={

    name:'anne'

};

console.log( obj1.getName() );       // 输出:sven

console.log( obj1.getName.call(  obj2 ) );     // 输出 :anne

在JavaScript中,几乎每一次编写函数式语言风格的代码,都离不开call和apply,在JavaScript诸多版本的设计模式中,也会用到call和apply。

 

丢失的this

  我们来看一个例子,document.getElementById这个方法名实在有点长,我们用一个短函数来代替:

 var getId=function( id ){

        return document.getElementById( id);

}

getId( 'div1');

我们也许思考过为什么不能用下面这种更简单的方式:

var getId = document.getElementById; 
getId( 'div1' ); 

很明显这种方式会报错,document.getElementById 方法的内部实现中需要用到 this。这个 this 本来被期望指向document,当 getElementById 方法作为 document 对象的属性被调用时,方法内部的 this 确实是指向 document 的,但当用 getId 来引用document.getElementById 之后,再调用 getId,此时就成了普通函数调用,函数内部的 this 指向了 window,而不是原来的 document。

我们可以尝试利用 apply 把 document 当作 this 传入 getId 函数,帮助“修正”this:

document.getElementById=( function (  func ) {

        return function(){

            return func.apply(document,arguments);

      }

})( document.getElementById );

var getId=document.getElementById;

var div=getId( 'div1' );

alert( div.id );          // 输出:div1

 

  • call和apply的区别

  call和apply的区别在于传参的形式不同

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

var func = function( a, b, c ){ 
      alert ( [ a, b, c ] );           // 输出 [ 1, 2, 3 ] 
}; 
func.apply( null, [ 1, 2, 3 ] ); 

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

var func = function( a, b, c ){ 
 alert ( [ a, b, c ] );           // 输出 [ 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( a, b, c ){ 
 alert ( this === window );     // 输出 true 

}; 
func.apply( null, [ 1, 2, 3 ] ); 

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

var func = function( a, b, c ){ 
  "use strict"; 
  alert ( this === null ); // 输出 true 

func.apply( null, [ 1, 2, 3 ] ); 

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

Math.max.apply( null, [ 1, 2, 5, 3, 4 ] )    // 输出:5

 

  • call和apply的用途

   1. 改变 this 指向

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

document.getElementById( 'div1' ).onclick = function(){ 
     alert( this.id ); // 输出:div1 
}; 

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

document.getElementById( 'div1' ).onclick = function(){ 
       alert( this.id ); // 输出:div1 
       var func = function(){ 
            alert ( this.id );           // 输出:undefined 
      } 
      func(); 
 }; 

 这时候我们用 call 来修正 func 函数内的 this,使其依然指向 div:
document.getElementById( 'div1' ).onclick = fuction(){ 
         var func = function(){ 
             alert ( this.id ); // 输出:div1 
          } 
         func.call( this ); 
}; 

2. Function.prototype.bind

大部分高级浏览器都实现了内置的 Function.prototype.bind,用来指定函数内部的 this指向,
即使没有原生的 Function.prototype.bind 实现,我们来模拟一个也不是难事,代码如下:

Function.prototype.bind = function( context ){ 
    var self = this;               // 保存原函数
    return function(){                 // 返回一个新的函数
        return self.apply( context, arguments );         // 执行新的函数的时候,会把之前传入的 context,当作新函数体内的 this } 
    }

}

var obj = { 
 name: 'sven' 
}; 

var func = function(){ 
     alert ( this.name );      // 输出:sven 
}.bind( obj);

func(); 

 

3.借用其他对象的方法

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

var A = function( name ){ 
    this.name = name; 
}; 

var B = function(){ 
   A.apply( this, arguments ); 
}; 

B.prototype.getName = function(){ 
     return this.name; 
}; 

var b = new B( 'sven' ); 
console.log( b.getName() );   // 输出: 'sven

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

  (function(){ 
         Array.prototype.push.call( arguments, 3 ); 
         console.log ( arguments );         // 输出[1,2,3] 
   })( 1, 2 ); 

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

想把 arguments 转成真正的数组的时候,可以借用 Array.prototype.slice 方法;想截去
arguments 列表中的头一个元素时,又可以借用 Array.prototype.shift 方法。

 

小结:call 和 apply 方法能很好地体现 JavaScript 的函数式语言特性,在 JavaScript 诸多版本的设计模式中,也
用到了 call 和 apply。

你可能感兴趣的:(js设计模式基础篇(二)之this、call和apply)