iT邦幫忙

第 12 屆 iThome 鐵人賽

DAY 20
1
Modern Web

JavaScript基本功修煉系列 第 20

JavaScript基本功修練:Day20 - this的運作

this所指向是物件是看我們如何呼叫函式。不論在全域或函式裏,都會自動帶上this這個關鍵詞,我們可以調用它來指向某個物件。當我們在全域使用this,它會指向window這個物件。但當我們呼叫函式時,this會因應我們「如何呼叫」函式而被影響。

影響this的幾種情況:

  • 物件裏的方法
  • 簡單呼叫(不建議)
  • bind, apply, call方法
  • new 建構函式
  • 箭頭函式 (先不在本文討論)

我們為什麼需要用this呢?其中一個常見的例子就是我們有一個多層的物件,而我們想在物件較內部的執行環境可以取到較外部的資料。

物件裏的方法 (最常用)

關於this的課題,最常見的情況是在物件裏調用函式,而該函式的this會指向該物件:

var name = 'Hong Kong'

function travel(){
    console.log(this.name)
}

var city = {
    name: 'Taipei',
    travel: travel
}

city.travel() //Taipei
travel() //Hong kong

以上例子可見我們有2種不同方式來呼叫travel(),結果都是不同。我們只需要看travel()這個函式是如何被呼叫。第一個,我們是在city物件裏呼叫,所以this會指向city物件,name就是在city物件裏的Taipei。

可是,當在全域呼叫函式travel,這時候,travel裏的this就會指向全域物件(window)的name屬性,因此回傳Hong Kong

沒錯,travel是在全域宣告,但我們不用理會函式travel是在哪裏宣告。只需要理會我們如何呼叫travel

那麼如果我們把city.travel()這個呼叫方式賦予到一個變數裏,再執行該變數,我們也會得出Taipei嗎?就像以下的做法:

var name = 'Hong Kong'

function travel(){
    console.log(this.name)
}

var city = {
    name: 'Taipei',
    travel: travel
}

//拷貝該函式到變數a
var a = city.travel
a()//Hong Kong

答案是不會。因為這裏的a只是拷貝了city.travel裏的那個函式,即是travel。所以a()是指我們直接呼叫travel函式。

簡單呼叫(不建議)

簡單呼叫(Simple call)就是直接調用函式,函式裏的this會指向全域的window。但是一般而言都不建議使用這個方法,在文章較後的部分會再解釋。

以下的例子很直接,我們直接呼叫traveltravel裏的this是指向全域的window,因此會回傳全域變數name的值,Hong Kong。

var name = 'Hong Kong'

function travel(){
    console.log(this.name)
}

travel(); //Hong Kong

再看看以下例子,雖然有兩層函式,但我們都是直接呼叫travel,所以結果同樣是指向全域的Taipei。

var name = 'Taipei'

function travel(){
    console.log(this.name)

    function innerTravel(){
        console.log(this.name)
    }
    innerTravel() 
}

travel()

//Taipei
//Taipei

以閉包函式為例,道理也是相同的,我們也是直接在全域呼叫函式,this會指向window物件。

var myScore = 3000

function calculate(score){
    var myScore = score;
    return function(addScore){
        myScore += addScore;
        console.log(myScore) //500+1000 = 1500
        console.log(this.myScore) //3000(全域)
    }
}

var result = calculate(500);
result(1000) //也是直接呼叫函式

除此之外,在Callback function的情況中,函式裏所指向的this同樣都會是全域window物件,所謂的callback function就是把一個函式傳到另一個函式裏,並在該函式裏執行,例如我們經常會用到的forEach方法:

var x = [1,2,3]
x.forEach(function(num){
    console.log(this) //window物件 x3
})

然而,其實不建議用簡易呼叫的方式去調用this,因為在嚴格模式下會出現錯誤,例如以下例子:

var a = 100

function test(){
    'use strict'
    console.log(this.a)
}

//這樣寫會報錯
test() 
//Uncaught TypeError: Cannot read property 'a' of undefined

test.call(this) //100

我們知道這裏的this是指向window,但是有時在嚴格模式下,我們用簡易呼叫的方式,this會被當成undefined而報錯。我們需要用到callapply等方法,刻意把this傳進去,即把window物件帶進去。依照call的方法,現在函式test裏的this就可以成功指向window

bind, apply, call方法

bindapplycall的方法非常相似,這些方法的第一個参數this會指向的物件

差異點在於:

  • callbind第二及之後的参數都是一個個值
  • apply第二個参數必須是陣列
  • callapply會立即執行,但bind需要在被呼叫後才會執行

應用例子:

var num = 999

var obj = {
    num: 10
}

function showNum(a,b){
    console.log(this.num, a, b)
}

//this是window,回傳是在全域的num
showNum(20,30) //999,20,30

//call方法
//this是obj,回傳是obj的num
showNum.call(obj,20,30) //10,20,30

//apply方法
showNum.apply(obj,[20,30]) //10,20,30

//bind方法
//不會自動執行,需要被呼叫
var newNum = showNum.bind(obj,20,30); 
newNum() //10,20,30

使用bind時要注意,當我們用bind方法傳入参數後,我們要手動呼叫這個方法。當我們呼叫它時並不能再次傳入参數,例如以下情況,newNum裏的2000和3000會被無視:

var newNum = showNum.bind(obj,20,30); 
newNum(2000,3000) 
//最後回傳結果仍然是 10,20,30

另外,如果第一個参數是undefinednullthis就會指向window物件,看看以下例子:

var obj = {
    num: 10
}

function showNum(a,b){
    console.log(this.num, a, b)
}

showNum.call(null,10,20) //undefined, 10, 20
showNum.call(undefined,50,70)//undefined, 10, 20

showNum.apply(null,[90,100])//undefined, 90, 100

var x = showNum.bind(undefined, 40,40)
x() //undefined, 40, 40

當我們把nullundefined傳進去函式當作this参考的物件時,this就會指向window,因為全域並沒有num這個變數,所以我們會得出undefined。試想想,如果我們剛好有一個全域變數var num = 3,那麼全部結果都會回傳3了。

如果第一個参數是基本型別值,這個值會被封裝成物件

function showNum(a,b){
    console.log(this, a, b)
}

showNum.call(200,1,2) //Number {200}, 1, 2
showNum.call('200',1,2) //String {"200"}, 1, 2

new 建構函式

在原型鏈的章節中,我們知道可以用建構函式產生出一個實體物件。在建構函式中的this,會指向那個新物件:

//建構函式
function createObj(){
    this.prop1 = 123
    this.prop2 = 'hello'
}

//實體物件
var obj = new createObj();
console.log(obj) //{prop1: 123, prop2: "hello"}

在以上例子可見,建構函式裏的this不是指向window,而是指向obj這個剛剛被建立出來的物件。因此,該新物件就會被設定prop1prop2屬性和值。

總結

  • 在全域調用this,會指向window物件
  • 影響this的幾種情況:物件裏的方法、簡單呼叫(不建議用)、bind, apply, call方法、new 建構函式、箭頭函式(先不在文章討論)
  • 嚴格模式下,簡易呼叫函式時,函式裏的this會是undefined
  • bindcallapply的方法觀念相近,同樣是在第一個参數傳入一個物件,函式裏this將會指向該物件
  • bind不會自動執行,需要被呼叫
  • 建構函式裏的this會指向由該建構函式產生的實體物件

参考資料

JS 原力覺醒 Day17 - this 的四種繫結
鐵人賽:JavaScript 的嚴格模式 "use strict"
鐵人賽:JavaScript 的 this 到底是誰?
JS核心篇(六角學院)


上一篇
JavaScript基本功修練:Day19 - 設定物件屬性裏的特徵
下一篇
JavaScript基本功修練:Day21 - 箭頭函式
系列文
JavaScript基本功修煉31

尚未有邦友留言

立即登入留言