跳至主要内容

[JS] 淺談this

TL;DR

這個就是this啊~

參考資料

相關連結


function基本介紹

傳入function的參數

開始前,先介紹function

var name = '全域'
function fn(params) {
console.log(params, this, window, arguments);
// debugger;
}

fn(1,2,3);

執行結果如下: 印出函式包含哪些傳入參數 可以看到我們雖然傳入了三個參數,但是實際上fn接收到的只有第一個的1
arguments內則會是我們完整傳入的參數。

後面三個(this,window,arguments)參數是函式預設就會傳入的,不需要我們手動傳入。

傳統函式的this

obj.fn內的this

創建一個物件,並且將上述的function放在物件內

var name = '全域'
var obj = {
name: '小明',
fn: function(params) {
console.log(params, this, window, arguments);
// debugger;
}
}
obj.fn(1,2,3);

執行結果如下: 印出的this會有所不同 可以注意到這邊的this指向變得不一樣了,變成是obj物件本身

simple call內的this

我們再來看一個範例:

var someone = '全域';
function callSomeone() {
var someone="function內";
console.log(this.someone);
}
callSomeone();

執行結果: 會輸出全域 這種直接執行的方式稱為simple call

如果使用let,const結果會有所不同

需要注意,上面的範例是使用var宣告。
如果今天是用ES6的let or const去宣告,結果會不一樣

let someone = '全域';
function callSomeone() {
var someone="function內";
console.log(this.someone);
}
callSomeone();

執行結果: 使用let宣告會印出undefined 使用let或是const宣告全域都會輸出undefined,這是因為使用var宣告會變成全域的一個屬性,可以使用點記法去取值。

this到底指向哪?

延伸上一個範例,如果今天不使用simple call,而是另外定義一個物件:

var someone = '全域'; 
function callSomeone() {
const someone="function內";
console.log(this.someone);
}

var obj = {
someone: '物件',
callSomeone() {
console.log(this.someone);
}
}
obj.callSomeone();

obj.callSomeone使用物件內的函式縮寫形式

執行結果如下: 執行後會印出物件的字串 執行後會印出物件的字串

傳統韓式的this只與調用方法有關。

證明方法如下,我們今天撰寫程式碼:

var someone = '全域'; 

function callSomeone() {
const someone="function內";
console.log(this.someone);
}

var obj2 = {
someone: '物件2',
callSomeone
}

obj2.callSomeone();

上述程式碼我們在 Line:3 宣告了一個具名函式,並且在 Line:10 的地方使用縮寫的方式帶入。
接著我們使用obj2.callSomeone()去調用它。執行結果如下: 印出��物件2

也就是說,雖然我們調用的callSomeone是 Line:3 所定義的具名函式,但是因為我們是透過obj2去調用他的,所以最後this的指向會是指向obj2

更多範例(內外層物件)

延伸上面的範例,程式碼如下:

var someone = '全域'; 
function callSomeone() {
const someone="function內";
console.log(this.someone);
}

var wrapObj = {
someone: '外層物件',
callSomeone,
innerObj: {
someone: '內層物件',
callSomeone,
}
}

wrapObj.callSomeone();
wrapObj.innerObj.callSomeone();

我們在 Line:2 定義了一個callSomeone的具名函式,並且同樣在 Line:9 以及 Line:12 使用縮寫的方式將該函式加入物件中。
經過 Line:16,17 執行的結果後,結果如下: 執行結果為分別印出外層物件與內層物件

因為wrapObj.callSomeone();的調用是透過wrapObj,所以this就會指向wrapObj,這時候透過函式執行的 Line:4 this.someone就會是wrapObj下的someone這個屬性內的值,也就是外層物件

相對的,如果今天是透過wrapObj.innerObj.callSomeone去調用this,則this就會指向warpObj.innerOjb,所以this.someone就會是 Line:11內層物件

物件內的simple call

程式碼如下:

var someone = '全域'; 
function callSomeone() {
const someone="function內";
console.log(this.someone);
}

var obj3 = {
someone: '物件 3',
fn() {
callSomeone(); // 通常平常不會這樣去取用 this
}
}

obj3.fn();

這題有點小陷阱,雖然我們是執行obj3.fn(),但是因為在fn()內才會執行callSomeone();
Line:10 內的callSomeone();前面沒有看到其他物件,所以這裡的callSomeone屬於simple call

不要再Simple call內使用this

一般來說我們不會在Simple call內使用this。
因為Simple call內this指向通常會不如我們預期。

Callback function內的this

程式碼:

var someone = '全域'; 

var obj4 = {
someone: '物件 4',
fn() {
setTimeout(function () {
console.log(this.someone);
},0);
}
}

obj4.fn();

運行結果如下: 會印出全域 執行結果會印出 Line:1 所定義的全域

這是因為setTimeout屬於callback function,通常大部分的callback funtion屬於simple call的形式,所以他的this會指向window
少部分callback functionthis會重新定義。


箭頭函式基本介紹

ES6新增了箭頭函式,但是他的運作上會跟傳統函式有所不同。最大的不同就是箭頭函式沒有自己的this

傳統函式改寫箭頭函式

以下範例為傳統函式寫法:

const arr = [1, 2, 3, 4, 5];
const filterArr=arr.filter(function(item){
return item%2;
})
console.log(filterArr);

程式碼先宣告一個陣列arr,接著使用filter的方法去判斷是否是奇數。 執行結果如下: 回傳1,3,5的陣列

filter內是一個callback function,只要判斷為true就會回傳該item出來,最後組成一個新的陣列。

我們可以對filter內的callback function改寫成箭頭函式如下:

const arr = [1, 2, 3, 4, 5];
const filterArr=arr.filter((item)=>{
return item%2;
})
console.log(filterArr);

因為這邊沒有使用到this,所以改寫成箭頭函式的寫法後,執行結果不變。

箭頭函式的縮寫

我們以上述程式碼filter內的callback function為例:

const arr = [1, 2, 3, 4, 5];
const filterArr=arr.filter((item)=>{
return item%2;
})
console.log(filterArr);

可以縮寫將大括號給移除,並且會自動補上return,所以也可以將return給移除。(這邊需要將程式碼寫在同一行不然會出錯,也需要將分號給移除。)

結果如下:

const arr = [1, 2, 3, 4, 5];
const filterArr=arr.filter((item)=> item%2)
console.log(filterArr);

另外在傳入的變數只有一個的時候,也可以再將小括號給移除:

const arr = [1, 2, 3, 4, 5];
const filterArr=arr.filter(item=> item%2)
console.log(filterArr);

整體可以變得非常簡潔。

移除箭頭函式變數的小括號

只有在變數為一個的時候才可以移除小括號。沒有傳入變數或是變數在兩個(包含)以上時,都不可以移除小括號!

箭頭函式的this

箭頭函式的this會跟外層函式的this相同

這邊撰寫一段程式碼如下:

var name = '全域'
const person = {
name: '小明',
callName: function () {
console.log('1', this.name); // 1
setTimeout(function () {
console.log('2', this.name); // 2
console.log('3', this); // 3
}, 10);
},
}

person.callName();

在執行 Line:13 以後,會得到以下結果: 程式碼執行結果

因為是使用person.callName()去調用這個function的,所以functionthis的指向會是person這個物件

在執行callName這個function時,裡面又有一個setTimeout。在前面也有提到,setTimeout是一個callback function,所以他的this指向是指到Window的,所以這邊的this.name就會取到 Line:1 所宣告的全域。並且this指向Window

我們將上述範例的setTimeout改成箭頭函式的寫法:

var name = '全域'
const person = {
name: '小明',
callName: function () {
console.log('1', this.name); // 1
setTimeout(() => {
console.log('2', this.name); // 2
console.log('3', this); // 3
}, 10);
},
}

person.callName();

接著觀看執行結果: 箭頭函式的this指向會不同

mark:1 的地方可以看到this.name仍然維持是小明

但是 mark:2mark:3 的部分因為我們使用了箭頭函式去改寫,所以他的this指向會不一樣。
因為箭頭函式沒有自己的this,所以他的this會跟外層的function相同
也就是說在執行setTimeout的時候,this的指向會與執行 Line:4callName時的this相同。

所以根據 Line:13 ,執行callName是使用person去調用的,this就會指向person這個物件本身。 this指向往外層找

箭頭函式陷阱

程式碼如下:

var name = '全域'
const person = {
name: '小明',
callName: () => {
console.log(this.name); // 請尋找箭頭所在的作用域為何?
},
}

person.callName();

這邊在 Line:9 執行之後會輸出全域

雖然我們是透過perosn.callName();去調用這個function的,但是因為callName這個function是一個箭頭函式,我們在尋找他的this的時候要往外面的函式去尋找。但是在這個範例中,往外層是看不到任何函式的,所以這邊的this就會指向全域(Windos)

更多箭頭函式this範例

程式碼如下:

var name = '全域'
const person = {
name: '小明',
callMe() {
const callName = () => {
console.log(this.name); // 請尋找箭頭所在的作用域為何?
};
callName();
}
}

person.callMe();

在執行 Line:12 之後,答案是會印出小明

Line:5 ~ Line:7是一個箭頭函式,因為箭頭函式沒有自己的this,所以往外層尋找會找到callMe這個function
因為在 Line:12 調用的時候是透過person物件去調用的,所以callMe這個functionthis指向就會指向person物件

示意圖: this的指向跟callMe相同

實戰中的this

this在框架中該如何使用

在使用框架的時候,很常會運用到this
為了讓this可以正確指向,通常有兩種方法:

  1. 另外宣告一個變數指向this
  2. 使用箭頭函式

使用vm

vm在vue中是指ViewModel的意思。 使用方法如下:

var someone = '全域';
function callSomeone() {
console.log(this.someone);
}

var obj4 = {
someone: '物件 4',
fn() {
const vm = this; // vm 在 Vue 中意指 ViewModel
setTimeout(function () {
console.log(vm.someone);
});
}
}

obj4.fn();

輸出結果: 輸出物件4fn這個function內宣告一個變數vm,這樣就算在setTimeoutcallback function內使用vm這個變數的時候,也會正確指向obj4這個物件。

使用箭頭韓式

因為箭頭函式沒有自己的this,所以將上面程式碼片段修改如下也可以達到一樣的效果:

var someone = '全域';
function callSomeone() {
console.log(this.someone);
}


var obj4 = {
someone: '物件 4',
fn() {
setTimeout(() => {
console.log(this.someone);
});
}
}

obj4.fn();

輸出結果: 輸出物件4