先讓我們前情提要一下,JavaScript 中的 this 取決於 Function 的執行情境與方式,而在 You don't know JS 中,詳細的說明了在執行了 Function後,JavaScript this 的五種繫結:
在上一篇中,我們介紹了 預設繫結 與 隱含繫結,接著我們就要介紹剩下的三個繫結。
還記得我們介紹過的 call
、apply
、bind
嗎,這三個 Function 函式的其中一個重要功能就是明確指定要執行 Function 中的 this 是什麼,因此他的套用情境很好辨別:
call
、apply
、bind
執行 Function 時,就會套用明確繫結而 this 的內容也顯而易見
call
、apply
、bind
中指定的第一個參數讓我們來看看範例:
call
、apply
var myString = 'hello global',
obj = { myString: 'hello object' };
function logMyString() {
console.log(this.myString);
}
logMyString.myString = 'hello function';
logMyString(); // "hello global"
logMyString.call(obj); // "hello Object"
logMyString.apply(obj); // "hello Object"
logMyString.call(null); // "hello global"
logMyString.apply(null); // "hello global"
在這個範例中,我們分別用 call
與 apply
分別執行 logMyString
,首先我們試著把 obj
作為第一個參數帶入,毫無意外的,我們可以看到答案皆為 "hello Object"
。
在上一篇中提到,當我們將 null
帶入 call
或 apply
的第一個參數時,會等於不套用指定任何物件為 this,也就代表回到預設繫結,就讓我們來測試看看。我們可以看到答案都是 "hello global"
,也與我們預期的相同。
從這個範例中可以看到 call
跟 apply
在指定 this 的能力上是一模一樣的,也就是說,call
與 apply
的使用時機還是要回到要以何種方式將參數帶入目標函式中來決定。更詳細可以參考這一篇。
bind
接下來輪到介紹 bind
:
var myString = 'hello global',
obj1 = { myString: 'hello object1' },
obj2 = { myString: 'hello object2' };
function logMyString() {
console.log(this.myString);
}
logMyString.myString = 'hello function';
var bindedFn = logMyString.bind(obj1);
bindedFn(); // "hello object1"
bindedFn.call(obj2); // "hello object1"
obj2.log = bindedFn;
obj2.log(); // "hello object1"
我們先對 logMyString
執行 bind
後將值賦予 bindedFn
,緊接著執行這個包裹函式 bindedFn()
。同樣的,因為我們明確的指定了 obj1
當作 this,因此答案自然也會是 "hello object1"
。
既然都一樣的話,為何要把 bind
跟 call
、apply
分開討論呢?
bind
的強制繫結有沒有想過當我們對被 bind
包裹的函式再進行一次明確指定 this 的話會怎麼樣?看看 bindedFn.call(obj2);
的結果會是甚麼,出乎意料的,bindedFn
的 this 竟然沒有被指定回 obj2
,而是依然保留在 obj1
。為什麼呢?就來回顧一下我們自製的簡易 bind polyfill 吧:
function bind(t, callback) {
var outerArgs = Array.from(arguments).slice(2);
return function() {
var innerArgs = Array.from(arguments);
return callback.apply(t, outerArgs.concat(innerArgs));
}
}
var bindedFn = bind(obj1, logMyString);
bindedFn(); // "hello object1"
bindedFn.call(obj2); // "hello object1"
obj2.log = bindedFn;
obj2.log(); // "hello object1"
來看看這個 bind
polyfill 是怎麼運作的。有注意到 callback.apply(t, outerArgs.concat(innerArgs))
的第一個參數 t
嗎?當我們執行 bind
後,會回傳一個匿名函式,而這個匿名函式再次被執行時,不論此時匿名函式中的 this 是甚麼,我們還是會把傳入 bind
的第一個參數明確指定給 callback
作為 this。再稍稍複習一下,這個匿名函式之所以還是能找到 t
並傳入 callback
還是多虧了 Closure 的威能!
也就是說,此時不論是再使用 隱含繫結 或是 明確繫結 都是無法再影響傳入 callback
的 this 的,因此 bindedFn.call(obj2);
與 obj2.log();
的答案才依然會是 "hello object1"
。
在 介紹 Function 時曾經說明過,new
也是執行 Function 的一種方式。與一般執行方式的最大差別在於 new
會把目標函式當作建構式使用。當作建構式的意思是以下幾樣事情會被執行:
有了這些觀念,new
繫結的 套用情境 與 內容 就顯而易見了
new
執行 Function 時,就會套用 new 繫結this 的內容會是 new
新建出來的物件本身
需要釐清的是,this 的內容會是新建出來的物件,而不是 Function 本身
現在用 new
把 logMyString
當作一個建構式使用,來看看 this 會指向何處:
var myString = 'hello global';
function logMyString() {
this.myString = 'hello new object';
}
logMyString.myString = 'hello function';
var newObj = new logMyString();
console.log(newObj.myString); // "hello new object"
在這個範例中,我們故意對 logMyString
這個函式本身也設定了一個 myString
屬性來確認 this 真正代表的位置。
在我們以 new logMyString();
執行 Function 後,就會隨即回傳一個被 new
出來的物件,我們把這個物件指派給 newObj
。印出 newObj.myString
後會得到 "hello new object"
。這可以驗證建構式中的 this 確實是 new 出來的新物件。
最後的一項細節是語彙繫結,這是利用 ES6 的新語法 () => {}
箭號函式來實現。
箭號函式可以當作是 匿名函式 的語法糖來看,但扯到 this 時,就不只是這樣了。之所以稱做 "語彙" 繫結,就是因為箭號函式的 this 會被繫結到包裹他的 Function 的 this,這個概念跟其他語言的 this 很雷同了。因此我們的 套用情境 與 this 內容會如下:
() => {}
執行 Function 時,就會套用語彙繫結接著來看看範例:
var myString = 'hello global',
obj = {};
function outer() {
this.myString = 'hello outer';
return () => {
this.myString = 'hello arrow function';
}
}
var arrowFn = outer.call(obj);
console.log(obj.myString); // "hello outer"
arrowFn();
console.log(window.myString); // "hello global"
console.log(obj.myString); // "hello arrow function"
為了更清楚看到實際發生的狀況,我們先把 outer
的 this 繫結明確設為 obj
,並用 arrowFn
這個變數接住回傳的箭號函式。此時印出 obj.myString
,確認有將 "hello outer"
成功設進 obj.myString
中。
當我們執行 arrowFn()
時,按照之前的概念,因為此時符合了預設繫結的規則:使用 ()
單獨呼叫函式,因此照道理匿名函式中的 this 的內容會是 global 物件,但事實上卻沒有,window.myString
依然是 "hello global"
。
這時使用上述語彙繫結的規則來看看,沒錯,arrowFn
的 this 之後永遠都會是 obj
了,因此此時印出 obj.myString
的答案就會是 "hello arrow function"
。
其實語彙繫結的功效即便不用 ES6 還是可以很容易地達成的,既然提到 語彙 ( Lexical Scope ) 自然就會 Closure 了。就讓我們再次借用 Closure 的威能吧!
在製作範例以前,先讓我們想想要怎麼樣才能 內部函式 永遠都可以拿到 外部函式的 this。明確的點出目標之後,其實答案就在不遠處了。想到了嗎?很簡單,我們只要把 this 指派給外層 Function 的一個變數,之後內層函式在使用這個變數的時候,就可以利用 Scope 查找到這個被存起來的外層 this 了!
就像這樣:
var myString = 'hello global',
obj = {};
function outer() {
var self = this;
self.myString = 'hello outer';
return function() {
self.myString = 'hello anonymous function';
}
}
var anonymousFn = outer.call(obj);
console.log(obj.myString); // "hello outer"
anonymousFn();
console.log(window.myString); // "hello global"
console.log(obj.myString); // "hello anonymous function"
我們執行 outer.call(obj)
後,我們立即把 this 指派給新的變數 self,往後所有關於 this 的操作,我們都以 self 來替代。
當執行 anonymousFn()
時,首先向 Scope 往上查找 self
,接著在 outer
的 Scope 中發現了它,而此時 self 的內容正是 outer
執行時被指派的 this:obj
。同樣的,往後 anonymousFn
的 this 都會是 obj
了,因為我們不再直接對 this 做操作,而是用新變數 self
來代為操勞了。因此 obj.myString
的內容會是 "hello arrow function"
。
本篇中,我們介紹了剩下的三個繫結:
明確繫結:
套用情境:
當我們使用 call
、apply
、bind
執行 Function 時,就會套用明確繫結
this 的內容:
this 的內容會是我們在 call
、apply
、bind
中指定的第一個參數
new 繫結
套用情境:
當我們使用 new
執行 Function 時,就會套用 new 繫結
this 的內容:
this 的內容會是 new
新建出來的物件本身
語彙繫結
套用情境:
當我們使用 ES6 的箭號函式 () => {}
執行 Function 時,就會套用語彙繫結
除此之外我們也可以用 Closure 來達成這個效果。
this 的內容:
與包裹他的 Function 的 this 相同
You Don't Know JS: this & object prototypes
您好,
請問這一句
var arrowFn = outer.call(obj);
這句可以讓原本是{}的obj可以被附上一個attribute myString = 'hello outer'
這個其中的原理可以稍作解釋嗎?第一次看到可以這樣附值,謝謝您
call
是 function 的原型函式,他的第一個參數可以指定 function 裡面的 this
。
因此以這個範例來看,執行 outer.call(obj)
時,outer
裡的 this
的值會變成 obj
,
而執行到self.myString = 'hello outer';
時會讓 obj
附上 myString
這個 attribute,其 value 就會是 'hello outer'
。
call
的詳細用法可以參考 MDN,也可以參考系列文中的另一篇文章 JavaScript - call,apply,bind~