[JavaScriptWeird]No.28 物件、函式 與 this

前言

我們了解了函式是物件的一種,有屬性及許多其它的東西。記得課程剛開始時,提過的執行環境嗎?這堂課是物件、函式,以及 this 的探討。

當函式被呼叫

複習一下,當函式被呼叫時,會創造新的執行環境。當執行環境被創造,放進執行堆,這過程決定了程式怎樣執行。

每個執行環境都有自己的變數環境,也就是被創造在函式內的變數所在,並且可以參考到外部環境,能夠隨著範圍鏈一路往下找,直到全域執行環境。

而我們也知道,當函式被執行、執行環境被創造時 JavaScript 也會產生一個我們沒宣告過的特殊變數 this

this

this 會指向不同的物件,而這是根據函式是如何被呼叫的,這很重要,讓我們直接從簡單的範例了解吧。

直接觀察 this

1
console.log(this);

如果直接取用 this ,這個 this 會指向全域物件 window。

建立函式陳述句呼叫觀察 this :

1
2
3
4
function a (){  
console.log(this);
}
a();

直接呼叫 a 函式,於是函式 a 內的執行環境被創造、 this 也被創造,在這個情況下, this 會指向全域物件 window。

建立函式表示式呼叫觀察 this :

1
2
3
4
var b = function(){  
console.log(this);
}
b();

結果也是一樣的,因為我們仍然是直接呼叫變數 b 的函式。

從上面兩個小測試可以觀察到,無論我們使用表示式、陳述句在何處創造函式,並不會影響 this 指向全域物件,因為會影響到 this 的是函式如何被呼叫

小測試

而每一個執行環境都有自己的 this , 在上述兩個小測試中的 this 都指向同一個記憶體位址,也就是同個全域物件,所以我們可以再透過這個延伸例子觀察:

1
2
3
4
5
6
7
8
9
10
function a (){  
console.log(this);
this.newVariable = 'hello';
}
var b = function(){
console.log(this);
}
a();
console.log(newVariable);
b();

a 函式的 this 被創造之後,我們在這個 this 上利用點運算子新增一個屬性,將這個屬性連接到全域物件,所以在呼叫 a 函式之後,我們可以透過 console.log 觀察到 newVariable 的值。

可能我們會感到奇怪,為什麼取用 newVariable 變數的時候不需要使用點運算子,因為這時候的 this 指向全域物件,而任何連接到全域物件的變數都可以直接使用。這就相當於在全域執行環境時使用 var 宣告變數一樣,像這樣:

1
2
3
4
5
6
function a (){  
this.newVariable = 'hello';
}
a();
var c = '123';
console.log(window);

我們在全域執行環境中宣告了變數 c ,並且跟上面的例子一樣直接呼叫函式 a ,並在函式 a 的程式內新增全域物件的屬性,接著觀察 window 的輸出。


可以發現如果 this 指向全域物件時,使用點運算子增加屬性到全域物件上,這時的效果會同於直接在全域執行環境上使用 var 宣告變數。

物件實體內的方法

透過上面的範例,我們已經了解函式表示式、陳述句,因此這次我們在一個物件內建立一個函式。記得我們先前提的,物件是許多名稱 / 值配對而成的組合,當值是純值時稱為「屬性」;當值為函式時稱為「方法」。像這樣:

1
2
3
4
5
6
7
var c = {  
name: '這是 c 物件',
log: function(){
console.log(this);
}
}
c.log();

現在情況就有點不一樣了,並不是直接呼叫函式,而是呼叫被創造在物件時體內的函式,因此要取用物件內的成員,必須使用點運算子,並且加上()呼叫該函式,也就是 c 物件的 log 方法。

因為呼叫的方式改變了,在這個範例中 this 會指向有 log 方法的 c 物件,因此我們可以利用這個特性,在方法內修改 c 物件的 name 屬性,像這樣:

1
2
3
4
5
6
7
8
var c = {  
name: '這是 c 物件',
log: function(){
this.name = '更新 c 物件';
console.log(this);
}
}
c.log();

可以看到 name 屬性被修改了!

延伸範例

讓我們將範例混合起來觀察, this 是否仍然如我們所想的那樣:

1
2
3
4
5
6
7
8
9
10
11
12
13
var c = {  
name: '這是 c 物件',
log: function(){
this.name = '更新 c 物件';
console.log(this);
var setName = function(newName){
this.name = newName;
}
setName('再次更新 c 物件');
console.log(this);
}
}
c.log();

結果令人驚訝嗎?其實並不,這是可以解釋的。

log 方法內,我們雖然又新增了一個 setName 的函式,並且是直接的呼叫它,但是會影響 this 的是函式呼叫的方式,並非實際上程式碼的實體位置,因此雖然方法內的 this 是指向 c 物件本身,但在 setName 函式內的 this 仍然是指向全域物件 window

所以我們可以在全域物件 window 中找到剛剛新增的屬性

那麼,該如何修改才能符合預期呢?

其實很容易,我們說過物件是傳參考的,我們只需要創造一個變數,並把想保存的 this 利用等號運算子設定給該變數就可以了,像這樣:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
var c = {  
name: '這是 c 物件',
log: function(){
var self = this;
self.name = '更新 c 物件';
console.log(self);
var setName = function(newName){
self.name = newName;
}
setName('再次更新 c 物件');
console.log(self);
}
}
c.log();

這樣就不需要考慮每個時候的 this 究竟是指向誰,只需要知道要保存下來的 this 是指向誰就可以了。

在這個例子中,我希望保存 this 指向 c 物件的記憶體位址,因此用了變數 self 配合等號運算子,令其與 this 指向同樣的 c 物件的記憶體位址。這樣即使之後 this 變動,也已經 self 無關,我們仍然可以使用這個變數修改 c 物件。

額外補充

關於 this 部分還有很多例子可以細細觀察,這部分可以參考 卡斯伯的鐵人賽文章 - JavaScript 的 this 到底是誰?

當然 console.log(this) 隨時查一下 this 指向哪裡也是可以的,配合著這些觀念,會讓我們寫 code 更順利哦~

0%