JavaScript夯實基礎系列(三):this

  在JavaScript中,函數的每次調用都會擁有一個執行上下文,經過this關鍵字指向該上下文。函數中的代碼在函數定義時不會執行,只有在函數被調用時才執行。函數調用的方式有四種:做爲函數調用做爲方法調用做爲構造函數調用以及間接調用,斷定this指向的規則跟函數調用的方式有關。
html

1、做爲函數的調用

  做爲函數調用是指函數獨立執行,函數沒有人爲指定的執行上下文。在有些狀況下,做爲函數調用的形式具備迷惑性,不只僅是簡單的函數名後面加括號來執行。
數組

一、明確的做爲函數調用

  明確的做爲函數調用是指形如func(para)形式的函數調用。做爲函數調用的狀況下this在嚴格模式下爲undefined,在非嚴格模式下指向全局對象(在瀏覽器環境下爲Window對象)以下代碼所示:
瀏覽器

var a = 1;
function test1 () {
    var a = 2
    return this.a
}
test1() // 1
複製代碼
'use strict'
var a = 1;
function test1 () {
    var a = 2
    return this.a
}
test1() // Uncaught TypeError
複製代碼

  以函數調用形式的函數一般不使用this,可是能夠根據this來判斷當前是不是嚴格模式。以下代碼所示,在嚴格模式下,this爲undefined,strict爲true;在非嚴格模式下,this爲全局對象,strict爲false。
app

var strict = (function () {
    return !this
})()
複製代碼

二、對象做爲橋樑找到方法

  經過對象調用的函數稱爲方法,可是經過對象找到方法並不執行屬於做爲函數調用的狀況。以下代碼所示:
ide

var a = 1;

function test() {
    console.log( this.a );
}

var obj = {
    a: 2,
    test: test
};

var func = obj.test;

func(); // 1
複製代碼

  上述代碼中,obj.test是經過obj對象找到函數test,並未執行,找到函數以後將變量func指向該函數。obj對象在這個過程當中只是起到一個找到test地址的橋樑做用,並不固定爲函數test的執行上下文。所以var func = obj.test;執行的結果僅僅是變量func和變量test指向共同的函數體而已,所以func()仍然是做爲函數調用,和直接調用test同樣。
  當傳遞迴調函數時,本質也是做爲函數調用。以下代碼所示:
函數

var a = 1

function func() {
    console.log( this.a );
}

function test(fn) {
    fn();
}

var obj = {
    a: 2,
    func: func
};

test( obj.func ); // 1
複製代碼

  函數參數是以值傳遞的形式進行的,obj.func做爲參數傳遞進test函數時會被複制,複製的僅僅是指向函數func的地址,obj在這個過程當中起到找到函數func的橋樑做用,所以test函數執行時,裏面的fn是做爲函數調用的。
  接收回調的函數是本身寫的仍是語言內建的沒有什麼區別,好比:
ui

var a = 1;

function test() {
    console.log( this.a );
}

var obj = {
    a: 2,
    test: test
};

setTimeout( obj.test, 1000 ); // 1
複製代碼

  setTimeout的第一個參數是經過obj對象找到的函數test,本質上obj依然是起到找到test函數的橋樑做用,所以test依然是做爲函數調用的。
this

三、間接調用傳遞null或undefined做爲執行上下文

  函數的間接調用是指經過call、apply或bind函數明確指定函數的執行上下文,當咱們指定null或者undefined做爲間接調用的上下文時,函數實際是做爲函數調用的。可是有一點須要注意:call()和apply()在嚴格模式下傳入空值則上下文爲空值,並非由於遵循做爲函數調用在嚴格模式下執行上下文爲全局對象的規則,而是由於在嚴格模式下call()和apply()的第一個實參都會變成this的值,哪怕傳入的實參是原始值甚至是null或undefined。
spa

var a = 1;

function test() {
    console.log( this.a );
}

test.call( null ); // 1
複製代碼

  間接調用的目的是爲了指定函數的執行上下文,那麼爲何要傳null或undefined使其做爲函數調用呢?這是由於咱們會用到這些方法的其餘性質:函數call中通常不傳入空值(null或undefined);函數apply傳入空值能夠起到將數組散開做爲函數參數的效果;函數bind能夠用來進行函數柯里化。在ES6中,新增了擴展運算符‘...’,將一個數組轉爲用逗號分隔的參數序列,能夠替代往apply函數傳空值的狀況。可是ES6中沒有增長函數柯里化的方法,所以往函數bind中傳空值的狀況將繼續使用。
  在使用apply或bind傳入空值的狀況,通常是不關心this值。可是若是函數中使用了this,在非嚴格模式下可以訪問到全局變量,有時會違背代碼編寫的本意。所以,使用一個真正空的值傳入其中可以避免這類狀況,以下代碼所示:
prototype

var empty = Object.create( null );
      
function foo(a,b) {
    console.log( "a:" + a + ", b:" + b );
}
      
foo.apply( empty, [1, 2] ); // a:1, b:2
複製代碼

2、做爲方法調用

  當函數掛載到一個對象上,做爲對象的屬性,則稱該函數爲對象的方法。若是經過對象來調用函數時,該對象就是本次調用的上下文,被調用函數的this也就是該對象。以下代碼所示:

var obj = {
    a: 1,
    test: test
};

function test() {
    console.log( this.a );
}

obj.test(); // 1
複製代碼

  在JavaScript中,對象能夠擁有對象屬性,對象屬性有又能夠擁有對象或者方法。函數做爲方法調用時,this指向直接調用該方法的對象,其餘對象僅僅是爲了找到this指向的這個對象而已。以下代碼所示:

function test() {
    console.log( this.a );
}

var obj2 = {
    a: 2,
    test: test
};

var obj1 = {
    a: 1,
    obj2: obj2
};

obj1.obj2.test(); // 2
複製代碼

  當方法的返回值時一個對象時,這個對象還能夠再調用它的方法。當方法不須要返回值時,最好直接返回this,若是一個對象中的全部方法都返回this,就能夠採用鏈式調用對象中的方法。以下代碼所示:

function add () {
    this.a++;
    return this;
}

function minus () {
    this.a--;
    return this;
}

function print() {
    console.log( this.a );
    return this;
}

var obj = {
    a: 1,
    print: print,
    minus: minus,
    add: add
};

obj.add().minus().add().print(); // 2
複製代碼

3、做爲構造函數調用

  在JavaScript中,構造函數沒有任何特殊的地方,任何函數只要是被new關鍵字調用該函數就是構造函數,任何不被new關鍵字調用的都不是構造函數。
  當使用new關鍵字來調用函數時,會經歷如下四步:

一、建立一個新的空對象。
二、這個空對象繼承構造函數的prototype屬性。
三、構造函數將新建立的對象做爲執行上下文來進行初始化。
四、若是構造函數有返回值而且是對象,則返回構造函數的返回值,不然返回新建立的對象。

  約定俗成的是:在編寫構造函數時函數名首字母大寫,且構造函數不寫返回值。所以通常來講,new關鍵字調用構造函數建立的新對象做爲構造函數的this。以下代碼所示:

function foo() {
    this.a = 1;
}

var bar = new foo();
console.log( bar.a ); // 1
複製代碼

4、間接調用

  在JavaScript中,對象中的方法屬性僅僅存儲的是一個函數的地址,函數與對象的耦合度沒有想象中的高。經過對象來調用函數,函數的執行上下文(this指向)就是該對象。若是經過對象來找到函數的地址,就能指定函數的執行上下文,可使用call()、apply()和bind()方法來實現。換而言之,任何函數能夠做爲任何對象的方法來調用,哪怕函數並非那個對象的方法。

一、call()和apply()

  每一個函數都call()和apply()方法,函數調用這兩個方法是能夠明確指定執行上下文。從綁定上下文的角度來講這兩個方法是同樣的,第一個參數傳遞的都是指定的執行上下文。所不一樣的在於call()方法剩餘的參數將會做爲函數的實參來使用,能夠有多個;apply()則最多隻接收兩個參數,第一個是執行上下文,第二個是一個數組,數組中的每一個元素都將做爲函數的實參。以下代碼所示:

var a = 1
function test(b,c) {
    console.log(`a:${this.a},b:${b},c:${c}`)
}
var obj = {
    a:2
}
test.call(obj,3,4) // a:2,b:3,c:4

var d = 11
function test2(b,c) {
    console.log(`b:${b},c:${c},d:${this.d}`)
}
var obj2 = {
    d:12
}
test2.apply(obj2,[13,14]) // b:13,c:14,d:12
複製代碼

  在非嚴格模式下,call()、apply()的第一個參數傳入null或者undefined時,函數的執行上下文被替代爲全局對象,若是傳入的是基礎類型,則爲替代爲相應的包裝對象。在嚴格模式下,遵循的規則是傳入的值即爲執行上下文,不替換,不自動裝箱。以下代碼所示:

var a = 1
function test1 () {
    console.log(this.a)
}
test1.call(null) // 1
test1.call(undefined) // 1
test1.apply(null) // 1
test1.apply(undefined) // 1
複製代碼

'use strict'
function test1 () {
    console.log(this)
}
test1.call(null) // null
test1.call(undefined) // undefined
test1.call(1) // 1
test1.apply(null) // null
test1.apply(undefined) // undefined
test1.apply(1) // 1
複製代碼

  apply()有一個較爲常見的用法:將數組轉化成函數的參數序列。ES6中增長了擴展運算符「...」來實現該功能。以下代碼所示:

var arr = [1,19,4,54,69,9]

var a = Math.max.apply(null,arr)
console.log(a) // 69

var b = Math.max(...arr)
console.log(b) // 69
複製代碼

二、bind()

  bind()函數能夠接收多個參數,返回一個功能相同、執行上下文肯定、參數通過初始化的函數。其中第一個參數爲要綁定的執行上下文,剩餘參數爲返回函數的預約義值。bind()函數的做用有兩點:一、爲函數綁定執行上下文;二、進行函數柯里化。以下代碼所示:

var a = 1

function func(b,c) {
    console.log(`a:${this.a},b:${b},c:${c}`)
}

var obj = {
    a: 2
}

var test = func.bind(obj,3)

test(4) // a:2,b:3,c:4
複製代碼

  bind()方法是ES5加入的,可是咱們能夠很輕易的在ES3中經過apply()模擬出來,下面代碼是MDN上的bind()的polyfill。

if (!Function.prototype.bind) {
    Function.prototype.bind = function(oThis) {
        if (typeof this !== "function") {
            // 可能的與 ECMAScript 5 內部的 IsCallable 函數最接近的東西,
            throw new TypeError( "Function.prototype.bind - what " +
                "is trying to be bound is not callable"
            );
        }

        var aArgs = Array.prototype.slice.call( arguments, 1 ),
            fToBind = this,
            fNOP = function(){},
            fBound = function(){
                return fToBind.apply(
                    (this instanceof fNOP &&oThis ? this : oThis),
                    aArgs.concat( Array.prototype.slice.call( arguments ) )
                );
            };

        fNOP.prototype = this.prototype;
        fBound.prototype = new fNOP();

        return fBound;
    };
}
複製代碼

5、規則的優先級

  函數的調用有時不僅一種,那麼不一樣調用方式的規則的優先級就最終決定了this的指向。那就讓咱們來比較不一樣調用方式的規則優先級。以下代碼所示,當函數做爲方法調用的時候,this指向調用方法的對象,看成爲函數調用時,this指向在非嚴格模式下指向全局對象,在嚴格模式下指向undefined。所以,方法調用的優先級高於函數調用。

var a = 1

var obj = {
    a:2,
    test:test
}

function test () {
    console.log(this.a)
}

var b = obj.test

obj.test() // 2
b() // 1
複製代碼

  以下代碼所示是函數做爲方法調用分別和間接調用、構造函數調用做對比。由代碼可知:函數做爲方法調用優先級分別小於間接調用和構造函數調用。

function test(para) {
    this.a = para
}

var obj1 = {
    test: test
}

var obj2 = {}
      
obj1.test( 2 )
console.log( obj1.a ) // 2

obj1.test.call( obj2, 3 )
console.log( obj2.a ) // 3

var bar = new obj1.test( 4 )
console.log( obj1.a ) // 2
console.log( bar.a ) // 4
複製代碼

  new關鍵字後面是一個函數,而call()和apply()並非返回一個函數,而是依照傳入參數來執行函數,所以形如new foo.call(obj)的代碼是不被容許的。ES5中的bind()返回的是一個函數,能夠與new關鍵字同時使用。以下代碼所示,bind()返回的函數用做構造函數,將忽略傳入bind()的this值,原始函數會以構造函數的形式調用,傳入的參數也會原封不動的傳入原始函數。

function test(something) {
    this.a = something;
}

var obj = {};

var bar = test.bind( obj );
bar( 2 );
console.log( obj.a ); // 2

var baz = new bar( 3 );
console.log( obj.a ); // 2
console.log( baz.a ); // 3
複製代碼

  總之,構造函數的優先級大於間接調用,間接調用的優先級大於方法調用,方法調用的優先級大於函數調用。

6、詞法this

  this關鍵字沒有做用域限制,函數的this指向調用該函數的對象,在嵌套函數匯中,若是想訪問外層函數的this值,能夠將外層函數的this賦值給一個變量,用詞法做用域來代替傳統的this機制。以下代碼所示:

function foo() {
    var self = this // 詞法上捕獲`this`
    setTimeout( function(){
        console.log( self.a )
    }, 1000 )
}

var obj = {
    a: 2
};

foo.call( obj ) // 2
複製代碼

  ES6新增了箭頭函數,箭頭函數體內的this對象,就是定義時所在的對象,而不是使用時所在的對象。以下代碼所示,箭頭函數可以將this固化,箭頭函數內部沒有綁定this的機制,其內部的this就是外層代碼塊的this。傳統的this機制讓不少人與詞法做用域混淆,所以有了將this賦值給變量的行爲,ES6只是將這種行爲加以標準化而已。

var a = 21

function test() {
    setTimeout(() => {
        console.log('a:', this.a)
    }, 1000)
}

test.call({ a: 42 }) // 42
複製代碼

7、總結

  JavaScript中的this機制跟詞法做用域沒有關係,根據函數調用的方式不一樣,肯定this指向的規則也不相同。在肯定this指向時能夠遵循如下步驟:

一、函數是否爲構造函數調用,即函數跟在new關鍵字後面,若是是,this就是新構建的對象。
二、函數是否爲間接調用,即經過call()、apply()或者bind()調用,若是是,this就是明確指定的對象。
三、函數是否爲做爲方法調用,即經過對象來調用函數,若是是,this就是該對象。
四、不然,即爲做爲函數的調用,在非嚴格模式下,this指向全局對象,在嚴格模式下,this爲undefined。

  能夠將外層函數的this賦值給一個變量,使得內層函數以詞法做用域的規則來訪問該this。ES6新增的箭頭函數即是使用詞法做用域來決定this綁定的。
如需轉載,煩請註明出處:www.cnblogs.com/lidengfeng/…

相關文章
相關標籤/搜索