2020年1月8日 星期三

JS學習: This


本篇引用至 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 改寫一下

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 本身


再多幾個範例

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 為預先綁定,不論怎麼呼叫都有固定綁定的 this
call 和 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 為全域物件


沒有留言:

張貼留言

JS學習:函數進階使用技巧

代理函數物件 根據不同的條件,代理函數物件可以指向不同的函數來實現動態改變,如下: function femaleFunction() { console.log('female'); } function maleFunction() { ...