本篇引用至 What-is-THIS-in-JavaScript ,主要為自己紀錄加深印象使用
This的定義
ECMAScript 對於 This 的定義為:「The this keyword evaluates to the value of the ThisBinding of the current execution context.」
MDN 對於 This 的定義為:
「In most cases, the value of this is determined by how a function is called.」
在大多數的情況下,this的值會由該 function 當時如何被呼叫而決定的
什麼是 This
- this 為 JavaScript 的一個關鍵字
- this 是 function 執行時,自動生成的一個內部物件
- 隨著 function 執行方式的不同,this 所指向的值也會有所不同
- 在大多數的情況下,this 表示呼叫 function 的物件 ( Owner Object of the function )
let getGender = function() { return person1.gender; } let person1 = { gender: 'female', getGender: getGender } let person2 = { gender: 'male', getGender: getGender } console.log(person1.getGender()); console.log(person2.getGender());
這時候的 console log 出來的結果都是 female,因為都被 return person1.gender 寫死了
那個如果把 getGender 的 function 改寫一下
那個如果把 getGender 的 function 改寫一下
let getGender = function() { return this.gender; } let person1 = { gender: 'female', getGender: getGender } let person2 = { gender: 'male', getGender: getGender } console.log(person1.getGender()); console.log(person2.getGender());
這時候的 console log 出來的結果分別是 female 和 male ,可以發現呼叫的方式不同,this 所指的值也不一樣
person1 的 this.gender 指向的是 person1 的 gender 屬性 (female);
person2 指向的是 person2 的 gender 屬性 (male)
由此可以知道 this 會因為執行方式的不同,而有不同的結果
屬性與方法是什麼
在 JavasScript 的世界裡面,原則上所有的東西都是物件,那麼在物件裡頭的東西可以分成屬性 ( property ) 和方法 ( method )簡單來說物件就是一堆 Key - Value 所組成的,不論是直接存放值 ( 布林、數值、字串等等),或是再存放個 Object ,這些都是屬於物件的「屬性」
但是如果存放的是函式 ( function ) 的話,那個就稱之為「方法」 ( method )
this 不等於 function
由上述可以知道, this 為 function 執行時呼叫(所屬)的物件,在 JavaScript 裡面,除了基本型別外所有的東西都是物件,那如果當 function 本身為物件時,如下:let foo = function() { this.count ++; } foo.count = 0; for( let i = 0 ; i < 5 ; i ++) { foo(); } console.log(foo.count);
這時候的 console log 會是 0 ,this 表示的是 function 執行時呼叫的物件
在範例裡頭,foo 是 function 同時也是一個 「全域變數」,在 JavaScript 的世界裡面,所有的全域變數都是全域物件的屬性,在瀏覽器裡面為 window ,在 Node 裡面為 global
因此,在呼叫 foo() 的時候,呼叫的物件為 window ,所以 this.count 的 this 也就是 window,所以跑了五次 ++ 的為 window.count
但是 window.count 並沒有被宣告,所以是 undefined ,對 undefined 的變數加了五次之後,會得到的是 NaN ( Not a Number),而 foo.count 沒有被動到,所以仍然為 0
所以,this 代表的是 function 執行時呼叫(所屬)的物件,而非 function 本身
再多幾個範例
在這邊 foo() 也是用 window 呼叫的,而 show 的 this 也是 window ,因此在 show 裡面的 this 也同樣是 window , 而 window.a 並沒有被宣告,因此 console log 會是 undefined
obj.showFoo() 呼叫 showFoo 的是 obj ,因此在 showFoo 裡面的 this 為 obj ,console log 就是
'foo in obj' ,而 showFoo() 呼叫的是 window ,所以是 undefined
在 function1 裡面的 this 為 obj 因此結果會是 true ,但是在 function1 裡面呼叫 function2 時因為沒有特別指名 this 的情況下,預設會是全域物件,因此會是 window,所以結果為 false
重點如下:
在範例裡頭,foo 是 function 同時也是一個 「全域變數」,在 JavaScript 的世界裡面,所有的全域變數都是全域物件的屬性,在瀏覽器裡面為 window ,在 Node 裡面為 global
因此,在呼叫 foo() 的時候,呼叫的物件為 window ,所以 this.count 的 this 也就是 window,所以跑了五次 ++ 的為 window.count
但是 window.count 並沒有被宣告,所以是 undefined ,對 undefined 的變數加了五次之後,會得到的是 NaN ( Not a Number),而 foo.count 沒有被動到,所以仍然為 0
所以,this 代表的是 function 執行時呼叫(所屬)的物件,而非 function 本身
再多幾個範例
let show = function() { console.log(this.a); }; let foo = function() { let a = 1; this.show(); }; foo();
在這邊 foo() 也是用 window 呼叫的,而 show 的 this 也是 window ,因此在 show 裡面的 this 也同樣是 window , 而 window.a 並沒有被宣告,因此 console log 會是 undefined
let foo = 'foo'; let obj = { foo: 'foo in obj' }; let showFoo = function() { console.log(this.foo); }; obj.showFoo = showFoo; obj.showFoo(); showFoo();
obj.showFoo() 呼叫 showFoo 的是 obj ,因此在 showFoo 裡面的 this 為 obj ,console log 就是
'foo in obj' ,而 showFoo() 呼叫的是 window ,所以是 undefined
巢狀迴圈中的 this
直接來個例子
let obj = { function1 = function() { console.log( this === obj ); let function2 = function() { console.log( this === obj ); } function2(); } } obj.function1();
在 function1 裡面的 this 為 obj 因此結果會是 true ,但是在 function1 裡面呼叫 function2 時因為沒有特別指名 this 的情況下,預設會是全域物件,因此會是 window,所以結果為 false
重點如下:
- 在 JavaScript中,用來切分變數的最小作用範圍( Scope ) 為 function
- 在沒有特別指名 this 的情況下,預設會是全域物件
但是如果是嚴格模式下,會禁止 this 自動指定為全域物件,因此在function2裡面的 this 會變成 undefined
Scope 是什麼
Scope 簡單來說就是變數以及函式能夠被存取的範圍,可以簡單分成全域範圍,任何地方都可以存取,以及區域範圍,只有該區域內能夠存取
let globalVal = 1; let show = function() { console.log(globalVal); let innerVal = 2; console.log(innerVal); } show(); console.log(innerVal);
在 show 裡面的兩個 console 分別會印出 1 和 2 ,因為 globalVal 為全域範圍變數,但是 innerVal 的僅限在該 function 內才可以存取,因此最後一行的 console 會報錯(變數未宣告)
小節重點
- this 不等於 function 本身
- this 也不是 function 的範圍 ( scope )
- this 取決於 function 被呼叫時的方式,與宣告無關
- this 是當 function 執行時,呼叫(所屬)的物件
- 當 function 是某一個物件的方法 ( method ) 時,this 為上層物件
- 全域變數的上層變數在瀏覽器中為 window,在 Node 中為 global
指定This的方法
假設現在有一個按鈕<button id="btn">按鈕</button>
在 JavaScript 裡面可以用「事件」( event ) 綁定來處理觸發事件,如下:
let btn = document.getElementById("btn"); btn.addEventListener("click", function(event){ console.log( this.textContent ); }, false);
首先透過Id取得HTML標籤的元素,接下來為他增加一個監聽事件,當收到 click 時要印出該的元素文字內容
addEventListener 的第三個參數為 useCapture 為一個布林值,建議為 false ,會在目標元素有祖先元素 ( ancestor element ) 的時候有所影響,如下:
<div id="outter"> <div id="inner"></div> </div>
當兩個 div 都有設置 click 的監聽事件時,觸發 inner 的事件同時也會觸發 outter 的事件,如果 useCapture 設為 true 時,會使用 Capture 的方式,由外而內觸發,也就是先觸發 outter 的事件,再觸發 inner 的事件
而 useCapture 設為 false 時,則會使用 Bubbling 的方式,與 Capture 相反,由內而外
如果兩個監聽事件設定的不一樣的話,則會先由外而內找到設為 True 的事件,再由內而外找設為 false 的事件
在上述的按鈕監聽事件裡面多加一些東西,如下:
let show = function(cb) { cb(); } let btn = document.getElementById("btn"); btn.addEventListener("click", function(event){ console.log( this.textContent ); show(function(){ console.log(this.textContent); }); }, false);
在這邊第二個 console 出來的會是 undefined ,因為 this 的值是由 function 執行時呼叫(所屬)的物件所決定
那最簡單、直觀的作法就是先把 this 存起來,如下:
btn.addEventListener("click", function(event){ let tmpThis = this; console.log( this.textContent ); show(function(){ console.log(tmpThis.textContent); }); }, false);
除了直接存取之外, bind() 可以被強制指定帶入的 this ,讓內部的 this 被指定成帶入的物件,如下:
btn.addEventListener("click", function(event){ console.log( this.textContent ); show(function(){ console.log(this.textContent); }.bind(this)); }, false);
來個簡單一點的範例:
let obj = { a : 1 }; let showA = function() { console.log(this.a); }; showA(); showA.bind(obj)();
第一個 showA 由 windows 所呼叫,因此會是 undefined
第二個 showA 則會由 bind 暫時將 showA 掛到 bind 裡所指定的物件底下,因此會變成由 obj 來呼叫 showA ,結果為 1
箭頭函數與this
從 ES6 開始新增「箭頭函數表示法」 ( Arrow Function expression ),具有 更短函數寫法 和 this綁定 的特性當今天一個簡單的 function 有一個參數,那麼箭頭函數可以將其簡化為
辨識符 ( Identifier ) => 表示法 ( Expression ) ,如下:
let show = showArray.forEach( function(element) { return outputFunction(element); }); let show = showArray.forEach( element => outputFunction(element) );
那如果有多個參數的話,如下:
let add = addArray.reduce( function(a,b) { return a + b; }, 0); let add = addArray.reduce( (a , b) => a + b , 0);
以上是更短函數寫法,而箭頭函數有隱含強制指定 this,所以上述按鈕監聽事件可以改成這樣:
btn.addEventListener("click", function(event){ console.log( this.textContent ); show(() => { console.log(this.textContent); }); }, false);
需要注意的是,使用箭頭函數的話,無論是使用 嚴謹模式或是 bind 都無法改變 this 的內容了,使用時也需要注意箭頭函數會強制指定 this 這件事情,如下:
btn.addEventListener("click", event => { console.log( this.textContent ); }, false);
這邊的 this 會變成 window 而非 btn 了
call() 和 apply()
假設有一個 function 如下:let showA = function() { console.log(this.a); };
可以直接呼叫,或是加上 call 和 apply 來呼叫,如下:
showA(); showA.call(); showA.apply();
call 和 apply 傳入的第一個參數,就是該 function 的 this 物件,兩者的差別在於後續參數的傳入方式不同,call 會以逗點分隔每個參數,而 apply 則是傳入陣列,如下:
let show = { number: 1, showUp: function(how) { console.log(this.number + ' show ' + how); } } let showA = { number: 2, }; show.showUp('up'); // 1 show up show.showUp.call(show , 'up'); // 1 show up show.showUp.apply(show , ['up']); // 1 show up show.showUp.call(showA , 'up'); // 2 show up show.showUp.apply(showA , ['up']); // 2 show up
三者的差異 - bind , call , apply
bind 為預先綁定,不論怎麼呼叫都有固定綁定的 thiscall 和 apply 則是呼叫時帶入,在呼叫當下即執行
this 綁定的基本原則
this 綁定的基本原則可以分為 4 種
- 預設綁定 ( Default Binding )
- 隱含式綁定 ( Implicit Binding )
- 顯式綁定 ( Explicit Binding )
- new 綁定
預設綁定是指當 function 在一般、沒有任何修飾的情況下被呼叫時,此時的 this 會自動綁定到全域物件 ( window ),但是如果使用嚴謹模式的話,會變成 undefined
隱含式綁定則是說,如果 function 是某個物件的屬性的話,那麼透過該物件呼叫,this 會綁定到該物件,如下:
function how() { console.log(this.number); } let show = { number: 1, showUp: how } showUp(); // undefined show.showUp(); // 1 let showUp2 = show.showUp; showUp2(); // undefined
第一個 showUp 會依據預設綁定原則,綁定到 window 底下
第二個 showUp 則是依照隱含式綁定原則,透過 show 呼叫,因此被綁定到 show 底下
第三個 showUp2 雖然是參照 show.showUp ,但是呼叫時沒有修飾,因此也是綁在 window 下
顯式綁定就是透過 bind / call / apply 這種方式直接指定 this 的綁定方式
new 綁定
在JavaScript的世界裡面,如果呼叫 function 時前面有一個 new 的話,會產生一個新的物件,並且 function 會被綁定至新物件底下,最後除非 function 內有 return 替代物件,否則都會自動回傳,如下:function makeA(a){ this.a = a; } let obj = new makeA(1); console.log(obj.a); // 1
上述中,透過 new 的方式建立一個新物件並回傳給「obj」,傳入的參數在建立物件時會變成新物件的屬性 a 的值
綁定的優先順序
當隱含式綁定與顯式綁定發生衝突時,會以顯式綁定為主總結
- 當 function 被呼叫時有加上 new 的話, this 就是建立時產生物件
- 當 function 被呼叫時有加上 call / bind / apply 的話,this 為指定的物件
- 當 function 被呼叫時屬於某個物件,this 為該物件
- 當 function 被呼叫時什麼都沒有,this 為全域物件
沒有留言:
張貼留言