JavaScript Weird Part (29) - 物件、函數與 this

函數是一種物件,具有屬性及其他特徵。回到執行環境來看,在函數被呼叫的時候,底層究竟發生什麼變化呢? 現在要繼續深究物件、函數以及有時讓人感到困惑的關鍵字 ”this” 之間的關係。

當函式被呼叫的時候

必須放在心中的是,當函數被呼叫的時候,會創造新的執行環境,放進執行堆,這決定了程式的執行方式。如果想一想執行環境,關注在函數物件的程式部分,當我執行程式屬性裡面的程式會怎麼樣?

我們知道執行環境被創造的時候,每個執行環境有自己的變數環境,也就是創建在函數裡面的變數所在的地方,它可以參照到外部詞彙環境,也就是它在程式碼當中的實際位置,讓它能夠隨著範圍鏈一路往下找,直到全域環境為止。

但是每當 在 JavaScript 引擎裡,每一次有一個執行環境創造的時候,每當函數被執行,JavaScript 引擎會給我們一個不曾宣告的this 變數。

this

this 會根據函數被呼叫的方式,而指向不同的物件,這是相當重要的觀念。

以直接取用的方式觀察 this

1
console.log(this)

在瀏覽器內執行,直接取用this , 它會指向全域物件 window 物件。因為 this 會指向全域物件,在瀏覽器內的全域物件就是 window 物件。

從函數陳述句觀察 this

現在看另外一個例子,有一個命名為 a 的函數,呼叫 a ,創造執行環境時,其中一部分就是創造 this 關鍵字。this 關鍵字會在執行環境裡面變成什麼呢?仍然會指向全域物件。

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

a();

從函數表示式觀察 this

在這個例子中的 this 會變成什麼呢?

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

呼叫 b 的結果,this 仍然指向 window 物件。因為我們還是直接呼叫變數 b 的函式。

所以無論何時我創造函數,用函數表示式或函數陳述句,都不會影響到this 指向記憶體中的同一個位址,也就是全域物件。因為會影響到 this 的因素是函式如何被呼叫。

延伸觀察

每一個執行環境都有自己的 this,上面兩個小觀察中的this 指向記憶體中的同一個全域物件。根據這種特性,我們可以繼續延伸觀察。

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

var b = function () {
console.log(this);
}
a();
console.log(newvariable);
b();

在呼叫 a 之後,當 a 函式的 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
8
var c = {
name = 'the c object',
log:function(){
console.log(this)
}
}

c.log();

現在所做的,不是直接呼叫函式,而是呼叫被創造在物件實體內的函式。所以要取用物件內的成員,就要使用點運算子,而且要加上 ()呼叫該函式,也就是 c物件的 log 方法。

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

1
2
3
4
5
6
7
8
9
var c = {
name : 'the c object',
log:function(){
this.name='update c object',
console.log(this)
}
}

c.log();

我們改變了擁有函數的物件的屬性。

這相當有用,可以使用 this 改變這個包含方法的物件,可以取用同一物件的方法或是屬性。很常用也很簡潔。

延伸範例

繼續看看混合範例裡面,this 仍然像我們想的那樣嗎?

1
2
3
4
5
6
7
8
9
10
11
12
13
14
var c = {
name: 'the c object',
log: function () {
this.name = 'update c object',
console.log(this);
var setname = function (newname) {
this.name = newname;
}
setname('update again! the c object');
console.log(this);
}
}

c.log();

這是為什麼?因為在 log 方法內,我們雖然新增了一個 setName函式,並且直接呼叫它,但是影響 this的是函式的呼叫方式,而非實際上程式碼的位置。所以即便這個方法內的 this 是指向 c 物件本身,但在 setName 函式內的 this 仍然是指向全域物件 window 。

常用解決模式

既然我們已經知道物件是用 by reference 設定的,

我們可以設定一個變數,並且把想保存 this 用等號運算子設定給該變數就可以了。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
    name: 'The c object',
log: function() {
var self = this;

self.name = 'Updated c object';
console.log(self);

var setname = function(newname) {
self.name = newname;
}
setname('Updated again! The c object');
console.log(self);
}
}

c.log();

如此就只要知道保存下來的 this 是指向誰就可以了。

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

Powered by Hexo and Hexo-theme-hiker

Copyright © 2013 - 2020 CYC'S BLOG All Rights Reserved.

UV : | PV :