1. 程式人生 > >call 和 apply方法解析

call 和 apply方法解析

ray ntb 方法 綁定 推斷 都是 還要 new int32

ECAMScript 3給Function的原型定義了兩個方法,它們是Function.prototype.call和Function. prototype.apply。在實際開發中,特別是在一些函數式風格的代碼編寫中,call 和 apply 方法尤為有用。在 JavaScript 版本的設計模式中,這兩個方法的應用也非常廣泛,能熟練運用這兩個方 法,是我們真正成為一名 JavaScript 程序員的重要一步。

1.call和apply的區別

Function.prototype.call 和 Function.prototype.apply 都是非常常用的方法。它們的作用一模 一樣,區別僅在於傳入參數形式的不同。

apply 接受兩個參數,第一個參數指定了函數體內 this 對象的指向,第二個參數為一個帶下 標的集合,這個集合可以為數組,也可以為類數組,apply 方法把這個集合中的元素作為參數傳 遞給被調用的函數:

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

在這段代碼中,參數 1、2、3 被放在數組中一起傳入 func 函數,它們分別對應 func 參數列 表中的 a、b、c。

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 

2.call和apply的用途

2.1 改變 this 指向

call 和 apply 最常見的用途是改變函數內部的 this 指向,我們來看個例子:

var obj1 = {
   name: ‘sven‘
};
var obj2 = {
   name: ‘anne‘
};
window.name = ‘window‘;
var getName = function(){
  alert ( this.name );
};
getName(); // 輸出: window
getName.call( obj1 ); // 輸出: sven
getName.call( obj2 ); // 輸出: anne 

當執行 getName.call( obj1 )這句代碼時,getName 函數體內的 this 就指向 obj1 對象,所以 此處的

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

實際上相當於:

var getName = function(){
  alert ( obj1.name ); // 輸出: sven
}; 

在實際開發中,經常會遇到 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 = function(){
   var func = function(){
   alert ( this.id ); // 輸出:div1
 }
 func.call( this );
}; 

使用 call 來修正 this 的場景,比如修正 document.getElementById 函數內部“丟失”的 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 

2.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(); 

我們通過 Function.prototype.bind 來“包裝”func 函數,並且傳入一個對象 context 當作參數,這個 context 對象就是我們想修正的 this 對象。

在 Function.prototype.bind 的內部實現中,我們先把 func 函數的引用保存起來,然後返回一 個新的函數。當我們在將來執行 func 函數時,實際上先執行的是這個剛剛返回的新函數。在新 函數內部,self.apply( context, arguments )這句代碼才是執行原來的 func 函數,並且指定 context 對象為 func 函數體內的 this。

這是一個簡化版的 Function.prototype.bind 實現,通常我們還會把它實現得稍微復雜一點, 使得可以往 func 函數中預先填入一些參數:

Function.prototype.bind = function(){
 var self = this, // 保存原函數
 context = [].shift.call( arguments ), // 需要綁定的 this 上下文
 args = [].slice.call( arguments ); // 剩余的參數轉成數組
 return function(){ // 返回一個新的函數
 return self.apply( context, [].concat.call( args, [].slice.call( arguments ) ) );
 // 執行新的函數的時候,會把之前傳入的 context 當作新函數體內的 this
 // 並且組合兩次分別傳入的參數,作為新函數的參數
 }
 };
var obj = {
 name: ‘sven‘
};
var func = function( a, b, c, d ){
 alert ( this.name ); // 輸出:sven
 alert ( [ a, b, c, d ] ) // 輸出:[ 1, 2, 3, 4 ]
}.bind( obj, 1, 2 );
func( 3, 4 ); 

2.3 借用其他對象的方法

我們知道,杜鵑既不會築巢,也不會孵雛,而是把自己的蛋寄托給雲雀等其他鳥類,讓它們 代為孵化和養育。同樣,在 JavaScript 中也存在類似的借用現象。

借用方法的第一種場景是“借用構造函數”,通過這種技術,可以實現一些類似繼承的效果:

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 方法。那麽這種機制的內部實現原理是什麽呢?我們不妨翻開 V8 的引擎源碼,以 Array.prototype.push 為例,看看 V8 引 擎中的具體實現:

function ArrayPush() {
  var n = TO_UINT32( this.length ); // 被 push 的對象的 length
  var m = %_ArgumentsLength(); // push 的參數個數
  for (var i = 0; i < m; i++) {
       this[ i + n ] = %_Arguments( i ); // 復制元素 (1)
  }
  this.length = n + m; // 修正 length 屬性的值 (2)
  return this.length;
}; 

通過這段代碼可以看到,Array.prototype.push 實際上是一個屬性復制的過程,把參數按照 下標依次添加到被 push 的對象上面,順便修改了這個對象的 length 屬性。至於被修改的對象是誰,到底是數組還是類數組對象,這一點並不重要。

由此可以推斷,我們可以把“任意”對象傳入 Array.prototype.push:

var a = {};
Array.prototype.push.call( a, ‘first‘ );
alert ( a.length ); // 輸出:1
alert ( a[ 0 ] ); // first

這段代碼在絕大部分瀏覽器裏都能順利執行,但由於引擎的內部實現存在差異,如果在低版 本的 IE 瀏覽器中執行,必須顯式地給對象 a 設置 length 屬性:

var a = {
   length: 0
}; 

前面我們之所以把“任意”兩字加了雙引號,是因為可以借用 Array.prototype.push 方法的對象還要滿足以下兩個條件,從 ArrayPush 函數的(1)處和(2)處也可以猜到,這個對象至少還要滿足:

  • 對象本身要可以存取屬性
  • 對象的 length 屬性可讀寫

對於第一個條件,對象本身存取屬性並沒有問題,但如果借用 Array.prototype.push 方法的不是一個 object 類型的數據,而是一個 number 類型的數據呢? 我們無法在 number 身上存取其他數據,那麽從下面的測試代碼可以發現,一個 number 類型的數據不可能借用到 Array.prototype. push 方法:

var a = 1;
Array.prototype.push.call( a, ‘first‘ );
alert ( a.length ); // 輸出:undefined
alert ( a[ 0 ] ); // 輸出:undefined 

對於第二個條件,函數的 length 屬性就是一個只讀的屬性,表示形參的個數,我們嘗試把 一個函數當作 this 傳入 Array.prototype.push:

var func = function(){};
Array.prototype.push.call( func, ‘first‘ );
alert ( func.length );
// 報錯:cannot assign to read only property ‘length’ of function(){} 

call 和 apply方法解析