call 和 apply方法解析
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方法解析