(二)this、call和apply

在javascript中,this關鍵字總讓一些初學者迷惑,Function.prototype.call, Function.prototype.apply這兩個方法普遍的運用。咱們有必要理解這幾個概念。javascript

一:thishtml

跟別的語言截然不同的是,javascript的this老是指向一個對象,而具體指向那個對象在運行時基於函數的執行環境動態綁定的,非函數被聲明時的環境。java

(1).this的指向程序員

除去不經常使用的with和eval狀況,具體到實際的應用中,this的指向大體分爲下面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)做爲普通函數調用app

 當函數不做爲對象的屬性被調用時,也就是咱們常說的普通函數方式,此時的this老是指向全局對象。在瀏覽器裏,就是window對象函數

    window.name = 'globalName';

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

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

    var myObject = {
        name : 'seven',
        getNameA : function() {
            return this.name
        }
    }

    var a = myObject.getNameA
    console.log( a() )

有的時候咱們會碰見一些困擾,好比在div節點內部,有一個局部的callback方法,callback做爲普通的函數調用時 ,callback內部的this指向了window,但咱們每每想讓他指向該div節點。以下

<div id="div1">
    div1
</div>
<script type="text/javascript">
     document.getElementById("div1").onclick = function() {
         var that = this;
         console.log(this.id)
        var callback = function(){
            console.log(that.id)//this.id = xx
        }
        callback()
    }
</script>

注意:在ES5的strict模式下,this已經被規定不會指向全局對象,而是undefined

3)構造器調用

javascript目前沒有類,可是從構造器中建立對象,同時也提供了new運算符,使得構造器看起來更像一個類。
除了宿主提供的一些內置函數,大部分javascript函數均可以當作構造器來使用。構造器的外表跟普通函數如出一轍,他們的區別在於被調用的方式。當用new運算符調用函數時,該函數會返回一個對象,一般狀況下,構造器裏的this就指向返回的這個對象,以下

    var myClass = function( name , sex ) {
        this.name = name;
        this.sex = sex;
    };

    var newObj = new myClass('jj','sxxx');
    console.log(newObj.sex + newObj.name)

可是用new調用構造器時,還要注意一個問題,若是構造器顯式的返回了一個object類型的對象,那麼這次運算結果最終會返回這個對象,而不是咱們以前期待的this

    var myClass = function( name ) {
        this.name = name ;
        return {
            name : 'anam'
        }
    }
    
    var myObj = new myClass('jack')
    console.log(myObj.name) ;//anam

若是構造器不顯式的返回任何數據,或者是返回一個對象類型的數據,就不會形成上述問題

    var myClass = function( name ) {
        this.name = name ;
        name : 'anam'
    }
    
    var myObj = new myClass('jack')
    console.log(myObj.name) ;//jack

4)Function.prototype.call和Function.prototype.apply調用

跟普通的函數調用相比,用Function.prototype.call或Function.prototype.apply能夠動態的改變傳入函數的this

var obj1 = {
    name: 'seven',
    getName: function() {
        return this.name
    }
}

var obj2 = {
    name : 'jack'
}

console.log(obj1.getName()) ;//seven
console.log(obj1.getName.call(obj2)) //jack

call和apply能很好的體現javascript的函數語言特性,在javascript中,幾乎每一次編寫函數式語言風格代碼都離不開call和apply。在javascript諸多版本的設計模式中,也用到了call和apply。在之後咱們分析中會更多說到。

(2)丟失的this

這是一個常常遇到的問題

var obj = {
    myname : 'sven',
    getNname : function() {
        return this.myname;
    }
}

console.log(obj.getNname()) // sven

var getname2 = obj.getNname

console.log(getname2()) ;//undefined

當調用obj.getName時,getName方法是做爲obj對象的屬性被調用的。(本文1.1)此時,this指向obj對象。
因此obj.getName輸出 'sven'

當另一個變量getName2來引用obj.getName,而且調用getname2時,(本文1.2)提到的規律,此時是普通函數調用方式,this是指向全局window的,因此程序執行的是undefined.

咱們再來看一個例子。

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.ElementById 以後,再調用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');

console.log(div) ;//<div id="div1">div1</div>
console.log(div.id) ;//div1

二:call和apply

ES3給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 ){
        console.log([a,b,c]) ;//輸出[1, 2, 3]
    }
    console.log(func())
    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的參數在內部就是用一個數組來表示的。從這個意義上說,apply比call的使用率更高。咱們沒必要關心具體有多少參數被傳入函數,只要apply一股腦的推過去就行。

call是包裝在apply上面的一顆語法糖,若是咱們明確知道了函數接受多少個參數,並且想一目瞭然的表達形參和實參的對應關係。那麼也能夠用call來傳送參數。

當使用call或apply的時候,若是咱們傳入的第一個參數爲null,函數體內的this會指向默認的宿主對象。在瀏覽器中則是window.

var func = function( a, b, c ){
    console.log( this === window ) //true
};
func.apply(null,[1,2,3])

可是在嚴格模式下,函數體內的this仍是爲null

var func = function( a, b, c ){
    "use strict"
    console.log( this === null ) //true
};
func.apply(null,[1,2,3])

有時咱們使用call或者apply的目的不在於指定this指向,而是另有用途,好比借用其它的對象方法。那麼咱們能夠傳入null來代替某個具體對象。

Math.max.apply(null,[1,2,3,4,5,6,7]) //7

(2)call和apply的用途

前面說過,可以熟練使用call和apply,是成爲一名正真的javascript程序員的重要一步,下面咱們就來詳細說說call和apply在實際開發中的用途。

1).改變this的指向

call和apply最多見的用途就是改變this的指向,下面咱們來看個例子:

    var obj1 = {
        name : 'seven'
    }

    var obj2 = {
        name : 'anne'
    }

    window.name = 'window'

    var getName = function() {
        console.log(this.name)
    }

    getName();
    getName.call(obj1)
    getName.call(obj2)

當執行getName.call(obj1)時,getName函數體內的this就指向obj1對象,因此此處的

    var getName = function() {
        console.log(this.name)
    }

至關於:

    var getName = function() {
        console.log(obj.name)
    }

在實際開發中,常常會遇到this指向被不經意改變的場景,好比有一個div節點,func函數體內的this就指向的window,而不是咱們預期的div.

<div id="div1">div1</div>

<script type="text/javascript">
document.getElementById('div1').onclick = function(){
    console.log(this.id) //div1
}
</script>

假如該事件函數中有一個內部函數func,在事件的內部調用 func函數時,函數體內的this就指向了window,而不是咱們預期的div,見以下代碼:

document.getElementById('div1').onclick = function(){
    console.log(this.id) //div1
    function func(){
        console.log(this.id) //undefined
    }
    func()
}

這個時候,咱們用call來修正func函數內的this,使其依然指向div

document.getElementById('div1').onclick = function(){
    console.log(this.id) //div1
    function func(){
        console.log(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')
console.log(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 )
    }
};

var obj = {
    name : 'seven'
}

var getName = function() {
    console.log(this.name)
}.bind(obj)

getName() ; //seven

咱們經過Function.prototype.bind來「包裝」 func函數,而且傳入一個對象context當作參數,這個context對象就是咱們要修正的this對象。

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對象上的方法,好比:想往argumments中添加一個新的元素,一般會借用Array.prototype.push

(function(){
    Array.prototype.push.call( arguments, 3)
    console.log(arguments) //[12, 1, 1, 23, 3]
})(12,1,1,23)

在操做arguments時,咱們很是頻繁的找Array.prototype對象借用方法。

想把arguments轉成真正的數組的時候,能夠借用Array.prototype.slice方法,想截取arguments列表中的頭一個元素時,可使用Array.prototype.shift方法,這種機制的內部原理,咱們能夠翻開V8引擎源碼,以Array.prototype.push方法爲例。看看其實現

function ArrayPush() {
    var n = TO_UINT32( this.length ); //被push的對象的length
    var m = %_ArgumentsLength(); //push的參數個數

    for (var i = 0; i < m; i++) {
        this[i + n] = %_ArgumentsLength( 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, 'frist');
Array.prototype.push.call(a, 'second');
console.log(a.length) //2
console.log(a[0]) //frist

對於「任意對象」,咱們從ArrayPush()函數的(1)和(2)能夠猜到,這個對象還要知足:

  • 對象的自己能夠讀取屬性
  • 對象的lenth屬性可讀寫

(本文已經完結

上一篇文章:(一)面向對象的javascript  下一篇文章 (三)閉包和高階函數

相關文章
相關標籤/搜索