在講完了作用域之後,我們終於可以來講關於 this。
this
可能是許多人一直困惑的點,他究竟是什麼?
我們可以這樣說:他是一個總是存在於所有作用域中的特殊關鍵字,總是指向一個對象。
那問題來了,他為什麼要存在,他又到底指向誰?
這兩個問題其實是緊密相連的。
怎麼說?this
實際上會隨著被 調用 的時機(Call-site)指向不同的對象,即使是同一個語句中的 this
,都有可能因為被使用的方式不同,得到不同的結果。
指向對象的則是透過一連串的規則來決定。
很明顯,這種靈活的指向解決的問題就是高複用性和彈性。
function hello(){
return `${this.name} says hello!`;
}
let friend1 = { name: 'Ken'};
let friend2 = { name: 'Ryu'};
console.log(hello.call(friend1)); // Ken says hello!
console.log(hello.call(friend2)); // Ryu says hello!
在上面的例子我們可以看到,this
在 hello
中指向了 friend1
和 friend2
。
即使在 hello 函世中我們本身並沒有定義 name
的屬性,透過了 this
關鍵字,增加了這個函式的靈活性。
以下的介紹順序具有順序性,最先介紹的優先度最低。
這邊關於綁定的名稱來自 YDKJS。
規格外 : ES 6 箭頭函式
在所有情況都不符合的情況下,就會套用默認綁定,獨立調用函式也會是這種方式。
在嚴格模式下,默認綁定 this
會變成 undefined
。
非嚴格模式下,this
則會指向全域。
function foo(){
console.log(this.a);
}
a = 10
foo();//
function bar(){
"use strict"
console.log(this.b);
}
b = 10;
bar();//Uncaught TypeError: Cannot read properties of undefined (reading 'b')
當調用點具備相對應的對象的時候。
function foo(){
console.log(this.a);
}
let obj = {
a:10,
foo:foo
};
obj.foo();//10
上面的例子 .foo()
作為 obj
的屬性被呼叫。
實際調用點是 .foo()
而不是 foo:foo
,foo:foo
僅僅只指向位址,並未調用。
一般這種適用於方法作為物件屬性被呼叫時套用。
function foo(){
console.log(this.a);
}
let obj2 = {
a:20,
foo:foo
}
let obj = {
a:10,
obj2:obj2
}
obj.obj2.foo();//20
這個情況的調用點發生在 obj2.foo()
上,所以回傳的會是 obj2
上的 20。
function foo() {
console.log( this.a );
}
function doFoo(fn) {
fn();
}
let obj = {
a: 'obj a',
foo: foo
};
let a = "global a";
doFoo( obj.foo );//global a
上面這個 obj.foo
看起來很像我們說的物件上的方法,但如 foo:foo
一樣,他只是一個位址,並不是方法。
上面的例子的調用發生在 doFoo
中 fn()
這個語句,因為 doFoo()
是以全域方法的方式被呼叫,所以是套用 1.默認綁定 規則,往外查找到了全域的 a
。
這段想再次強調什麼是調用點,什麼不是,this
只關注物件被調用的地方來決定指向對象。
前面兩種綁定的行為看起來都是有點背後規則的感覺,一如他們的名字 默認,隱含,在明確綁定中,我們要討論的是如何指名 this 的指向對象。
function foo(){
console.log(this.a);
}
let obj = {
a : 'obj a'
};
let a = 'global a';
foo.call(obj);//obj a
foo.apply(obj);//obj a
foo.call()
的方法就是第一種明確綁定的方式,方法簽名是 Function.prototype.call(thisArg)
(也能接收更多的參數作為函式的參數)。透過這個方法,被執行的函式的 this
會指第一個被傳入的參數 thisArg
。
以上面的例子,傳入 obj
,因此印出了 obj
裡面的 a
而不是全域的 a
。
Function.prototype.apply(thisArg)
的使用情境上和 Function.prototype.call(thisArg)
主要在帶入函式的參數方式,apply
以陣列方式帶入,call
以明確參數序依序帶入 -- 但在 this
的綁定上,他們皆屬明確綁定,都會將 this
綁定到第一個傳入的參數上。
明確綁定讓綁定的意圖更明顯,能夠簡單的找到 this
指向的對象,但缺點是仍有可能遇到 this
被修改或遺失的狀況。
這時有種設計模式可以解決這個問題,讓 this
不會被改動,稱作 硬綁定(Hard Binding)。
function foo(word) {
console.log( this.a, word );
return this.a + " " + word;
}
let obj = {
a: 'obj a'
};
let bar = function() {
return foo.apply( obj, arguments );
};
let a = 'global a'
let b = bar('parm'); //"obj a", "param"
console.log(b); //"obj a param"
bar(3)
是調用點,此時 bar
內部的 this
套用默認綁定規則,應該會吃到 a = 'global a'
。
但進入 bar
後的 foo
透過 apply
明確綁定了 this
為全域的 obj
物件。
接著執行了 foo('param')
時 this.a
指向的是 obj.a
,所以印出了 "obj a", "param"
。
因為 bar
的內部透過 apply
做了明確綁定,因此外部的 b
(由 bar
建立的物件),不管如何執行,都保證 foo
所使用的的 this
必定為 obj
,且無法從外部更動這個綁定行為。(即使外部對 bar
使用 bar.call
也不影響,因為實際上使用 this
的是 foo
,無法從外部被改動)。
因為此種設計模式的常用,出現了與之對應的語法關鍵字 bind
,這個關鍵字自 ES 5 就存在。
function foo(word) {
console.log( this.a, word );
return this.a + " " + word;
}
let obj = {
a: 'obj a'
};
let bar = foo.bind(obj);
let a = 'global a'
let b = bar('parm'); //"obj a", "param"
console.log(b); //"obj a param"
這樣的寫法效果與硬綁定第一種一樣,也是硬綁定,因為綁定行為建立在 bar
裡,確保 foo
的 this
能總是指向希望的對象且不被更改。
new
綁定指的是使用 new
關鍵字時對生成物件的 this
綁定行為。
當透過 new
建構出了一個新的物件,該物件內部的所有 this
都會指向生成的物件本身。
function foo(a) {
this.a = a;
}
let a = 'global a';
let bar = new foo('input a');
console.log(bar);//{a: "input a",}
bar
是透過 new
建立的 foo
物件,因此 foo
物件中方法定義的 this
都會指向 bar
這個物件。
如同建構時傳入的 'input a'
,掛到了 bar
本身的屬性上。(this.a = a
語句的結果,這邊的 this
指的就是 bar
)。
雖然說了優先順序上是 new 綁定 > 明確綁定 > 隱含綁定 > 默認綁定,但身為工程師,總是要有實證的精神。
默認綁定是最不優先的,在上面的例子應該看得很清楚了,所以我們先驗證明確綁定和隱含綁定的優先序。
function foo(){
console.log(this.a);
}
let obj1 = {
a : 'obj1 a',
foo : foo
};
let obj2 = {
a : 'obj2 a',
foo : foo
};
let a = 'global a';
obj1.foo();//'obj1 a' 隱含綁定
obj2.foo();//'obj2 a' 隱含綁定
obj1.foo.call(obj2);//'obj2 a' 明確綁定
obj2.foo.call(obj1);//'obj1 a' 明確綁定
前兩行 .foo()
確認了隱含綁定的生效,表示調用物件上的方法時隱含綁定如預期的發生。
但是如果我們透過 .call()
方法,來指定物件上方法的 thisArg
,可以看到後兩行 this
都指向了指定的 thisArg
,由此可知,明確綁定是優先於隱含綁定的。
接著,我們該來驗證的是 new
綁定規則是否優先於明確綁定。
因為語法上的限制,new
和 call
無法一起使用,所以我們要使用應綁定的關鍵字 bind
來證明這件事。
function foo(input) {
this.a = input;
}
let obj1 = {};
let bar = foo.bind(obj1);
bar('input bar');
console.log( obj1.a );//"input bar"
let baz = new bar('input baz');
console.log( obj1.a );//"input bar"
console.log( baz.a );//"input baz"
透過 bar('input bar')
foo.bind(obj1)
,我們指定了 foo
的 this
指向 obj1
,並把他的 a 放為 'input bar'
,這是硬綁定,外部無法直接更改 foo
綁定 obj1
的行為。
這時候我們使用 new
關鍵字來從 bar
上建構新的物件baz
,這邊 new
綁定的規則是把 this
指向物件 baz
,覆寫物件的a
為 'input baz'
,結果 new
綁定的複寫生效, baz.a
印出的結果是 new
時傳進去的字串。而 obj1.a
上的物件若硬綁定生效的話,應該預期被改為 input baz
,但仍維持著 input bar
。
由此可見即使是硬綁定,透過 new
關鍵字仍能覆寫其 this
綁定。
綜上所述,確認一個物件的 this
關鍵字指向誰的判定順序,由上至下找到第一個符合的結果
new
創建,則該物件的 this
指向建構出的物件call
, apply
, bind
,則該物件的 this
指向傳入的 thisArg
this
視調用點而定,若直接調用 obj.func()
則 this
指向 obj
本身this
指向undefined
,非嚴格模式 this
指向全域以上概括了大部分的 this
規則 -- 除了一個特例, ES 6 引入的箭頭函式,這個我們放在明天討論 function 的時候來討論。
希望以上內容已經足以讓你對 this
是一個怎麼樣的屬性,如何指向有了足夠的了解。