在JavaScript編程中,
this
關鍵字總是讓初學者感到迷惑章咧,Function.prototype.call
和Function.prototype.apply
這兩個方法也有著廣泛的運用倦西。我們有必要在學習設計模式之前先理解這幾個概念。
- <a href="#no1">2.1 this</a>
- <a href="#no2">2.2 call 和 apply</a>
<a name="no1">2.1 this</a>
跟別的語言大相徑庭的是赁严,
JavaScript
的this
總是指向一個對象扰柠,而具體指向哪個對象是在運行時基于函數(shù)的執(zhí)行環(huán)境動態(tài)綁定的,而非函數(shù)被聲明時的環(huán)境疼约。
2.1.1 this
的指向
除去不常用的
with
和eval
的情況耻矮,具體到實際應用中,this的指向大致可以分為以下4種忆谓。
- 作為對象的方法調用裆装。
- 作為普通函數(shù)調用。
- 構造器調用倡缠。
-
Function.prototype.call
或Function.prototype.apply
調用哨免。
1. 作為對象的方法調用
當函數(shù)作為對象的方法被調用時,this
指向該對象:
var obj = {
a: 1,
getA: function(){
alert ( this === obj ); // 輸出:true
alert ( this.a ); // 輸出: 1
}
};
obj.getA();
2. 作為普通函數(shù)調用
當函數(shù)不作為對象的屬性被調用時昙沦,也就是我們常說的普通函數(shù)方式琢唾,此時的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
有時候我們會遇到一些困擾,比如在div
節(jié)點的事件函數(shù)內部丘损,有一個局部的callback
方法普办,callback
被作為普通函數(shù)調用時,callback
內部的this
指向了window
徘钥,但我們往往是想讓它指向該div
節(jié)點衔蹲,見如下代碼:
<html>
<body>
<div id="div1">我是一個div</div>
</body>
<script>
window.id = 'window';
document.getElementById( 'div1' ).onclick = function(){
alert ( this.id ); // 輸出:'div1'
var callback = function(){
alert ( this.id ); // 輸出:'window'
}
callback();
};
</script>
</html>
此時有一種簡單的解決方案,可以用一個變量保存div
節(jié)點的引用:
document.getElementById( 'div1' ).onclick = function(){
var that = this; // 保存div的引用
var callback = function(){
alert ( that.id ); // 輸出:'div1'
}
callback();
};
在ECMAScript 5的strict
模式下呈础,這種情況下的this
已經(jīng)被規(guī)定為不會指向全局對象舆驶,而是undefined
:
function func(){
"use strict"
alert ( this ); // 輸出:undefined
}
func();
3. 構造器調用
JavaScript中沒有類橱健,但是可以從構造器中創(chuàng)建對象,同時也提供了new
運算符沙廉,使得構造器看起來更像一個類拘荡。
除了宿主提供的一些內置函數(shù),大部分JavaScript函數(shù)都可以當作構造器使用撬陵。構造器的外表跟普通函數(shù)一模一樣珊皿,它們的區(qū)別在于被調用的方式。當用new
運算符調用函數(shù)時袱结,該函數(shù)總會返回一個對象亮隙,通常情況下途凫,構造器里的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
如果構造器不顯式地返回任何數(shù)據(jù)犀盟,或者是返回一個非對象類型的數(shù)據(jù)而晒,就不會造成上述問題:
var MyClass = function(){
this.name = 'sven'
return 'anne'; // 返回string類型
};
var obj = new MyClass();
alert ( obj.name ); // 輸出:sven
4. Function.prototype.call或Function.prototype.apply調用
跟普通的函數(shù)調用相比,用Function.prototype.call
或Function.prototype.apply
可以動態(tài)地改變傳入函數(shù)的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
call
和apply
方法能很好地體現(xiàn)JavaScript的函數(shù)式語言特性阅畴,在JavaScript中倡怎,幾乎每一次編寫函數(shù)式語言風格的代碼,都離不開call
和apply
贱枣。在JavaScript諸多版本的設計模式中监署,也用到了call
和apply
。
2.1.2 丟失的this
這是一個經(jīng)常遇到的問題纽哥,我們先看下面的代碼:
var obj = {
myName: 'sven',
getName: function() {
return this.myName;
}
};
console.log(obj.getName()); // 輸出:'sven'
var getName2 = obj.getName;
console.log(getName2()); // 輸出:undefined
當調用obj.getName
時钠乏,getName
方法是作為obj
對象的屬性被調用的,根據(jù)2.1.1節(jié)提到的規(guī)律春塌,此時的this
指向obj
對象晓避,所以obj.getName()
輸出'sven'
。
當用另外一個變量getName2
來引用obj.getName
只壳,并且調用getName2
時俏拱,根據(jù)2.1.2節(jié)提到的規(guī)律,此時是普通函數(shù)調用方式吼句,this
是指向全局window
的彰触,所以程序的執(zhí)行結果是undefined
。
再看另一個例子命辖,document.getElementById
這個方法名實在有點過長况毅,我們大概嘗試過用一個短的函數(shù)來代替它分蓖,如同prototype.js
等一些框架所做過的事情:
var getId = function( id ){
return document.getElementById( id );
};
getId( 'div1' );
我們也許思考過為什么不能用下面這種更簡單的方式:
var getId = document.getElementById;
getId( 'div1' );
現(xiàn)在不妨花1分鐘時間,讓這段代碼在瀏覽器中運行一次:
<html>
<body>
<div id="div1">我是一個div</div>
</body>
<script>
var getId = document.getElementById;
getId( 'div1' );
</script>
</html>
在Chrome尔许、Firefox么鹤、IE10中執(zhí)行過后就會發(fā)現(xiàn),這段代碼拋出了一個異常味廊。這是因為許多引擎的document.getElementById
方法的內部實現(xiàn)中需要用到this
蒸甜。這個this
本來被期望指向document
,當getElementById
方法作為document
對象的屬性被調用時余佛,方法內部的this
確實是指向document
的柠新。
但當用getId
來引用document.getElementById
之后,再調用getId
辉巡,此時就成了普通函數(shù)調用恨憎,函數(shù)內部的this
指向了window
,而不是原來的document
郊楣。
我們可以嘗試利用apply
把document
當作this
傳入getId
函數(shù)憔恳,幫助“修正”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
<a name="no2">2.2 call和apply</a>
Function.prototype.call
和Function.prototype.apply
都是非常常用的方法。它們的作用一模一樣净蚤,區(qū)別僅在于傳入?yún)?shù)形式的不同钥组。
2.2.1 call和apply的區(qū)別
apply
接受兩個參數(shù),第一個參數(shù)指定了函數(shù)體內this
對象的指向今瀑,第二個參數(shù)為一個帶下標的集合程梦,這個集合可以為數(shù)組,也可以為類數(shù)組橘荠,apply
方法把這個集合中的元素作為參數(shù)傳遞給被調用的函數(shù):
var func = function( a, b, c ){
console.log( [ a, b, c ] ); // 輸出 [ 1, 2, 3 ]
};
func.apply( null, [ 1, 2, 3 ] );
在這段代碼中屿附,參數(shù) 1、2砾医、3 被放在數(shù)組中一起傳入func
函數(shù)拿撩,它們分別對應func
參數(shù)列表中的a、b如蚜、c压恒。
call
傳入的參數(shù)數(shù)量不固定,跟apply
相同的是错邦,第一個參數(shù)也是代表函數(shù)體內的this
指向探赫,從第二個參數(shù)開始往后,每個參數(shù)被依次傳入函數(shù):
var func = function( a, b, c ){
console.log ( [ a, b, c ] ); // 輸出 [ 1, 2, 3 ]
};
func.call( null, 1, 2, 3 );
當調用一個函數(shù)時撬呢,JavaScript的解釋器并不會計較形參和實參在數(shù)量伦吠、類型以及順序上的區(qū)別,JavaScript的參數(shù)在內部就是用一個數(shù)組來表示的。從這個意義上說毛仪,apply
比call
的使用率更高搁嗓,我們不必關心具體有多少參數(shù)被傳入函數(shù),只要用apply
一股腦地推過去就可以了箱靴。
call
是包裝在apply
上面的一顆語法糖腺逛,如果我們明確地知道函數(shù)接受多少個參數(shù),而且想一目了然地表達形參和實參的對應關系衡怀,那么也可以用call
來傳送參數(shù)棍矛。
當使用call
或者apply
的時候,如果我們傳入的第一個參數(shù)為null
抛杨,函數(shù)體內的this
會指向默認的宿主對象够委,在瀏覽器中則是window
:
var func = function( a, b, c ){
alert ( this === window ); // 輸出true
};
func.apply( null, [ 1, 2, 3 ] );
但如果是在嚴格模式下,函數(shù)體內的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.2.2 call和apply的用途
前面說過真竖,能夠熟練使用call
和apply
脐雪,是我們真正成為一名JavaScript程序員的重要一步厌小,本節(jié)我們將詳細介紹call
和apply
在實際開發(fā)中的用途恢共。
1. 改變this
指向
call
和apply
最常見的用途是改變函數(shù)內部的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
當執(zhí)行getName.call( obj1 )
這句代碼時璧亚,getName
函數(shù)體內的this
就指向obj1
對象讨韭,所以此處的
var getName = function(){
alert ( this.name );
};
實際上相當于:
var getName = function(){
alert ( obj1.name ); // 輸出: sven
};
在實際開發(fā)中,經(jīng)常會遇到this
指向被不經(jīng)意改變的場景癣蟋,比如有一個div
節(jié)點透硝,div
節(jié)點的onclick
事件中的this
本來是指向這個div
的:
document.getElementById( 'div1' ).onclick = function(){
alert( this.id ); // 輸出:div1
};
假如該事件函數(shù)中有一個內部函數(shù)func
,在事件內部調用func
函數(shù)時疯搅,func
函數(shù)體內的this
就指向了window
濒生,而不是我們預期的div
,見如下代碼:
document.getElementById( 'div1' ).onclick = function(){
alert( this.id ); // 輸出:div1
var func = function(){
alert ( this.id ); // 輸出:undefined
}
func();
};
這時候我們用call
來修正func
函數(shù)內的this
幔欧,使其依然指向div
:
document.getElementById( 'div1' ).onclick = function(){
var func = function(){
alert ( this.id ); // 輸出:div1
}
func.call( this );
};
使用call
來修正this
的場景罪治,我們并非第一次遇到,在上一小節(jié)關于this
的學習中礁蔗,我們就曾經(jīng)修正過document.getElementById
函數(shù)內部“丟失”的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. Function.prototype.bind
大部分高級瀏覽器都實現(xiàn)了內置的Function.prototype.bind
,用來指定函數(shù)內部的this指向浴井,即使沒有原生的Function.prototype.bind
實現(xiàn)晒骇,我們來模擬一個也不是難事,代碼如下:
Function.prototype.bind = function(context) {
var self = this; // 保存原函數(shù)
return function() { // 返回一個新的函數(shù)
return self.apply(context, arguments); // 執(zhí)行新的函數(shù)的時候,會把之前傳入的context
// 當作新函數(shù)體內的this
}
};
var obj = {
name: 'sven'
};
var func = function() {
alert(this.name); // 輸出:sven
}.bind(obj);
func();`
我們通過Function.prototype.bind
來“包裝”func
函數(shù)洪囤,并且傳入一個對象context
當作參數(shù)徒坡,這個context
對象就是我們想修正的this
對象。
在Function.prototype.bind
的內部實現(xiàn)中瘤缩,我們先把func
函數(shù)的引用保存起來崭参,然后返回一個新的函數(shù)。當我們在將來執(zhí)行func
函數(shù)時款咖,實際上先執(zhí)行的是這個剛剛返回的新函數(shù)何暮。在新函數(shù)內部,self.apply( context, arguments )
這句代碼才是執(zhí)行原來的func
函數(shù)铐殃,并且指定context
對象為func
函數(shù)體內的this
海洼。
這是一個簡化版的Function.prototype.bind
實現(xiàn),通常我們還會把它實現(xiàn)得稍微復雜一點富腊,使得可以往func
函數(shù)中預先填入一些參數(shù):
Function.prototype.bind = function() {
var self = this, // 保存原函數(shù)
context = [].shift.call(arguments), // 需要綁定的this上下文
args = [].slice.call(arguments); // 剩余的參數(shù)轉成數(shù)組
return function() { // 返回一個新的函數(shù)
return self.apply(context, [].concat.call(args, [].slice.call(arguments)));
// 執(zhí)行新的函數(shù)的時候坏逢,會把之前傳入的context當作新函數(shù)體內的this
// 并且組合兩次分別傳入的參數(shù),作為新函數(shù)的參數(shù)
}
};
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);`
3. 借用其他對象的方法
我們知道赘被,杜鵑既不會筑巢是整,也不會孵雛,而是把自己的蛋寄托給云雀等其他鳥類民假,讓它們代為孵化和養(yǎng)育浮入。同樣,在JavaScript中也存在類似的借用現(xiàn)象羊异。
借用方法的第一種場景是“借用構造函數(shù)”事秀,通過這種技術,可以實現(xiàn)一些類似繼承的效果:
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'
借用方法的第二種運用場景跟我們的關系更加密切野舶。
函數(shù)的參數(shù)列表arguments
是一個類數(shù)組對象易迹,雖然它也有“下標”,但它并非真正的數(shù)組平道,所以也不能像數(shù)組一樣睹欲,進行排序操作或者往集合里添加一個新的元素。這種情況下一屋,我們常常會借用Array.prototype
對象上的方法窘疮。比如想往arguments
中添加一個新的元素,通常會借用Array.prototype.push
:
(function(){
Array.prototype.push.call( arguments, 3 );
console.log ( arguments ); // 輸出[1,2,3]
})( 1, 2 );
在操作arguments
的時候陆淀,我們經(jīng)常非常頻繁地找Array.prototype
對象借用方法考余。
想把arguments
轉成真正的數(shù)組的時候,可以借用Array.prototype.slice
方法轧苫;想截去arguments
列表中的頭一個元素時尖坤,又可以借用Array.prototype.shift
方法。那么這種機制的內部實現(xiàn)原理是什么呢摩瞎?我們不妨翻開V8的引擎源碼威沫,以Array.prototype.push
為例,看看V8引擎中的具體實現(xiàn):
function ArrayPush() {
var n = TO_UINT32( this.length ); // 被push的對象的length
var m = %_ArgumentsLength(); // push的參數(shù)個數(shù)
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
實際上是一個屬性復制的過程,把參數(shù)按照下標依次添加到被push
的對象上面,順便修改了這個對象的length
屬性滚躯。至于被修改的對象是誰,到底是數(shù)組還是類數(shù)組對象嘿歌,這一點并不重要掸掏。
由此可以推斷,我們可以把“任意”對象傳入Array.prototype.push
:
var a = {};
Array.prototype.push.call( a, 'first' );
alert ( a.length ); // 輸出:1
alert ( a[ 0 ] ); // first
這段代碼在絕大部分瀏覽器里都能順利執(zhí)行宙帝,但由于引擎的內部實現(xiàn)存在差異丧凤,如果在低版本的IE瀏覽器中執(zhí)行,必須顯式地給對象a
設置length
屬性:
var a = {
length: 0
};
前面我們之所以把“任意”兩字加了雙引號步脓,是因為可以借用Array.prototype.push
方法的對象還要滿足以下兩個條件愿待,從ArrayPush
函數(shù)的(1)處和(2)處也可以猜到,這個對象至少還要滿足:
- 對象本身要可以存取屬性靴患;
- 對象的length屬性可讀寫仍侥。
對于第一個條件,對象本身存取屬性并沒有問題鸳君,但如果借用Array.prototype.push
方法的不是一個object
類型的數(shù)據(jù)农渊,而是一個number
類型的數(shù)據(jù)呢? 我們無法在number
身上存取其他數(shù)據(jù),那么從下面的測試代碼可以發(fā)現(xiàn)相嵌,一個number類型的數(shù)據(jù)不可能借用到Array.prototype.push
方法:
var a = 1;
Array.prototype.push.call( a, 'first' );
alert ( a.length ); // 輸出:undefined
alert ( a[ 0 ] ); // 輸出:undefined
對于第二個條件腿时,函數(shù)的length
屬性就是一個只讀的屬性况脆,表示形參的個數(shù)饭宾,我們嘗試把一個函數(shù)當作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(){}