第一部分-基礎知識(2)-this、call、apply

在JavaScript編程中,this關鍵字總是讓初學者感到迷惑,Function.prototype.callFunction.prototype.apply這兩個方法也有著廣泛的運用。我們有必要在學習設計模式之前先理解這幾個概念。

  • <a href="#no1">2.1 this</a>
  • <a href="#no2">2.2 call 和 apply</a>

<a name="no1">2.1 this</a>

跟別的語言大相徑庭的是,JavaScriptthis總是指向一個對象,而具體指向哪個對象是在運行時基于函數的執行環境動態綁定的,而非函數被聲明時的環境。

2.1.1 this的指向

除去不常用的witheval的情況,具體到實際應用中,this的指向大致可以分為以下4種。

  • 作為對象的方法調用。
  • 作為普通函數調用。
  • 構造器調用。
  • Function.prototype.callFunction.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

有時候我們會遇到一些困擾,比如在div節點的事件函數內部,有一個局部的callback方法,callback被作為普通函數調用時,callback內部的this指向了window,但我們往往是想讓它指向該div節點,見如下代碼:

<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節點的引用:

document.getElementById( 'div1' ).onclick = function(){
    var that = this;    // 保存div的引用
    var callback = function(){
        alert ( that.id );    // 輸出:'div1'
    }
    callback();
};

在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.callFunction.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

callapply方法能很好地體現JavaScript的函數式語言特性,在JavaScript中,幾乎每一次編寫函數式語言風格的代碼,都離不開callapply。在JavaScript諸多版本的設計模式中,也用到了callapply

2.1.2 丟失的this

這是一個經常遇到的問題,我們先看下面的代碼:

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對象的屬性被調用的,根據2.1.1節提到的規律,此時的this指向obj對象,所以obj.getName()輸出'sven'

當用另外一個變量getName2來引用obj.getName,并且調用getName2時,根據2.1.2節提到的規律,此時是普通函數調用方式,this是指向全局window的,所以程序的執行結果是undefined

再看另一個例子,document.getElementById這個方法名實在有點過長,我們大概嘗試過用一個短的函數來代替它,如同prototype.js等一些框架所做過的事情:

var getId = function( id ){
    return document.getElementById( id );
};

getId( 'div1' );

我們也許思考過為什么不能用下面這種更簡單的方式:

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

現在不妨花1分鐘時間,讓這段代碼在瀏覽器中運行一次:

<html>
    <body>
        <div id="div1">我是一個div</div>
    </body>
    <script>

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

    </script>
</html>

在Chrome、Firefox、IE10中執行過后就會發現,這段代碼拋出了一個異常。這是因為許多引擎的document.getElementById方法的內部實現中需要用到this。這個this本來被期望指向document,當getElementById方法作為document對象的屬性被調用時,方法內部的this確實是指向document的。

但當用getId來引用document.getElementById之后,再調用getId,此時就成了普通函數調用,函數內部的this指向了window,而不是原來的document

我們可以嘗試利用applydocument當作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

<a name="no2">2.2 call和apply</a>

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

2.2.1 call和apply的區別

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

var func = function( a, b, c ){
    console.log( [ 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 ){
    console.log ( [ a, b, c ] );    // 輸出 [ 1, 2, 3 ]
};

func.call( null, 1, 2, 3 );

當調用一個函數時,JavaScript的解釋器并不會計較形參和實參在數量、類型以及順序上的區別,JavaScript的參數在內部就是用一個數組來表示的。從這個意義上說,applycall的使用率更高,我們不必關心具體有多少參數被傳入函數,只要用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.2.2 call和apply的用途

前面說過,能夠熟練使用callapply,是我們真正成為一名JavaScript程序員的重要一步,本節我們將詳細介紹callapply在實際開發中的用途。

1. 改變this指向

callapply最常見的用途是改變函數內部的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的場景,我們并非第一次遇到,在上一小節關于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. 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);`

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(){}

資料來源

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 227,837評論 6 531
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 98,196評論 3 414
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 175,688評論 0 373
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 62,654評論 1 309
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 71,456評論 6 406
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 54,955評論 1 321
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,044評論 3 440
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 42,195評論 0 287
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 48,725評論 1 333
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 40,608評論 3 354
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 42,802評論 1 369
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,318評論 5 358
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,048評論 3 347
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,422評論 0 26
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,673評論 1 281
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 51,424評論 3 390
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 47,762評論 2 372

推薦閱讀更多精彩內容

  • 工廠模式類似于現實生活中的工廠可以產生大量相似的商品,去做同樣的事情,實現同樣的效果;這時候需要使用工廠模式。簡單...
    舟漁行舟閱讀 7,796評論 2 17
  • 單例模式 適用場景:可能會在場景中使用到對象,但只有一個實例,加載時并不主動創建,需要時才創建 最常見的單例模式,...
    Obeing閱讀 2,085評論 1 10
  • 本文源于本人關于《JavaScript設計模式與開發實踐》(曾探著)的閱讀總結。想詳細了解具體內容建議閱讀該書。 ...
    yozosann閱讀 269評論 0 1
  • 女人的幸福感是來自內心的,不是依靠外表光鮮亮麗,不是嫁給一個疼愛自己高富帥,不是把自己捧到高高在上位置,高貴地昂著...
    P尐c閱讀 2,292評論 0 0
  • 今天,iphone x發布,這個吸引了無數人眼球的發布會,被網友評論最多的就是人臉識別解鎖功能,男人們說不敢睡覺了...
    星光下的咖啡館閱讀 194評論 3 1