這是本系列的第 5 篇文章。javascript
還記得上一篇文章中的閉包嗎?點擊查看文章 理解 JavaScript 閉包 。前端
在聊 this 以前,先來複習一下閉包:java
var name = 'Neil'; var person = { name: 'Leo', sayHi: function() { return function () { return 'Hi! My name is ' + this.name; } } }; person.sayHi()(); // "Hi! My name is Neil"
上一篇文章說,咱們能夠把閉包簡單地理解爲函數返回函數。因此這裏的閉包結構是:segmentfault
// ... function () { return 'Hi! My name is ' + this.name; } // ...
可是你有沒有發現,這個函數執行的結果是 「Hi! My name is Neil」 。等等,我不是叫 Leo 嗎?怎麼給我改了個名字?!瀏覽器
我一分析,原來是 this 在其中做祟,且聽我慢慢道來這「更名的由來」。閉包
首先,你得確保你已經清楚執行棧與執行上下文的知識。點擊查看文章 理解 JavaScript 執行棧 。app
ECMAScript 5.1 中定義 this 的值爲執行上下文中的 ThisBinding。而 ThisBinding 簡單來講就是由 JS 引擎建立並維護,在執行時被設置爲某個對象的引用。函數
在 JS 中有三種狀況能夠建立上下文:初始化全局環境、eval() 和執行函數。this
var num = 1; function getName () { return "Leo"; } this.num; // 1 this.getName(); // Leo this == window; // true
當咱們在瀏覽器中運行這段代碼,JS 引擎會將 this 設置爲 window 對象。而聲明的變量和函數被做爲屬性掛載到 window 對象上。固然,在嚴格模式下,全局中 this 的值設置爲 undefined。spa
"use strcit"; var num = 1; function getName () { return "Leo"; } this.num; // TypeError this.getName(); // TypeError this == undefined; // true
開啓嚴格模式後,全局 this 將指向 undefined,因此調用 this.num 會報錯。
eval() 不被推薦使用,我如今對其也不太熟悉,這裏嘗試着說一下。初學者能夠直接跳到下一節。
結合所查閱的資料,目前我對 eval() 的理解以下:
eval(...) 直接調用,被理解爲是一個 lvalue,也有說是 left unchanged,字面理解爲餘下不變。什麼是「餘下不變」?我理解爲直接調用 eval(...),其中代碼的執行環境不變,依舊爲當前環境,this 也依舊指向當前環境中的調用對象。
而使用相似 (1, eval)(...) 的代碼,被稱爲間接調用。(1, eval) 是一個表達式,你能夠這樣認爲 (true && eval) 或者 (0 : 0 ? eval)。間接調用的 eval 始終認爲其中的代碼執行在全局環境,將 this 綁定到全局對象。
var x = 'outer'; (function() { var x = 'inner'; // "direct call: inner" eval('console.log("direct call: " + x)'); // "indirect call: outer" (1, eval)('console.log("indirect call: " + x)'); })();
關於 eval(),如今不敢肯定,若有錯誤,歡迎指正。
首先,咱們須要明確的是,在 JS 中函數也屬於對象,它能夠擁有屬性,this 就是函數在執行時得到的屬性。通常狀況下,在全局環境中直接調用函數,函數中的 this 會在調用時被 JS 引擎設置爲全局對象 window(一樣在嚴格模式下爲 undefined)。
var name = "Leo"; function getName() { var name = "Neil"; console.log(this); // [object Window] return this.name; } getName(); // Leo
函數能夠做爲對象的方法被該對象調用,那麼這種狀況 this 會被設置爲該對象。
var name = 'Neil'; var person = { name: 'Leo', sayHi: function() { console.log(this); // person return 'Hi! My name is ' + this.name; } }; person.sayHi(); // "Hi! My name is Leo"
當 person 對象調用 sayHi() 方法時,this 被指向 person。
JS 還提供了一種供開發者自定義 this 的方式,它提供了 3 種方式。
咱們能夠經過設置 thisArg 的值,來自定義函數中 this 的指向。
var leo = { name: 'Leo', sayHi: function () { return "Hi! My name is " + this.name; } } var neil = { name: 'Neil' }; leo.sayHi(); // "Hi! My name is Leo" leo.sayHi.call(neil); // "Hi! My name is Neil"
這裏,咱們經過 call() 將 sayHi() 中 this 的指向綁定爲 neil 對象,從而取代了默認 的 this 指向 leo 對象。
關於函數的 call(), apply(), bind() 我將在後面另寫一篇文章,敬請期待。
經過前面的介紹,我想你對 this 已經有了初步的印象。那麼,回到文章開頭的問題,this 是怎麼改變了個人名字?換句話說,this 在閉包的影響下指向發生了怎樣的變更?
再看一下代碼:
var name = 'Neil'; var person = { name: 'Leo', sayHi: function() { return function () { return 'Hi! My name is ' + this.name; } } }; person.sayHi()(); // "Hi! My name is Neil"
經過上一篇文章 理解 JavaScript 閉包,函數返回函數會造成閉包。在這種狀況下,閉包每每所執行的環境與所定義的環境不一致,而 this 的值倒是在執行時決定的。因此,當上面代碼中的閉包在執行時,它所在的執行上下文是全局環境,this 將被設置爲 window(嚴格模式下爲 undefined)。
怎麼解決?咱們能夠利用 call / apply / bind 來修改 this 的指向。
var name = 'Neil'; var person = { name: 'Leo', sayHi: function() { return function () { return 'Hi! My name is ' + this.name; } } }; person.sayHi().call(person); // "Hi! My name is Leo"
這裏利用 call() 將 this 指向 person。OK,個人名字回來了,「Hi! My name is Leo」 ^^
固然,咱們還有第二種解決方法,閉包的問題就讓閉包本身解決。
var name = 'Neil'; var person = { name: 'Leo', sayHi: function() { var that = this; // 定義一個局部變量 that return function () { return 'Hi! My name is ' + that.name; // 在閉包中使用 that } } }; person.sayHi()(); // "Hi! My name is Leo"
在 sayHi() 方法中定義一個局部變量,閉包能夠將這個局部變量保存在內存中,從而解決問題。
在回調函數中 this 的指向也會發生變化。
var name = 'Neil'; var person = { name: 'Leo', sayHi: function() { return 'Hi! My name is ' + this.name; } }; var btn = document.querySelector('#btn'); btn.addEventListener('click', person.sayHi); // "Hi! My name is undefined"
這裏 this 既不指向 person,也不指向 window。那它指向什麼?
btn 對象,它是一個 DOM 對象,有一個 onclick 方法,在這裏定義爲 person.sayHi。
{ // ... onclick: person.sayHi // ... }
因此,當咱們執行上面的代碼,this.name 的值爲 undefined,由於 btn 對象上沒有定義 name 屬性。咱們給 btn 對象自定義一個 name 屬性來驗證一下。
var btn = document.querySelector('#btn'); btn.name = 'Jackson'; btn.addEventListener('click', person.sayHi); // "Hi! My name is Jackson"
緣由說清楚了,解決方案一樣可用過 call / apply / bind 來改變 this 的指向,使其綁定到 person 對象。
btn.addEventListener('click', person.sayHi.bind(person)); // "Hi! My name is Leo"
var name = 'Neil'; var person = { name: 'Leo', sayHi: function() { return 'Hi! My name is ' + this.name; } }; person.sayHi(); // "Hi! My name is Leo" var foo = person.sayHi; foo(); // "Hi! My name is Neil"
當把 person.sayHi() 賦值給一個變量,這個時候 this 的指向又發生了變化。由於 foo 執行時是在全局環境中,因此 this 指向 window(嚴格模式下指向 undefined)。
一樣,咱們能夠經過 call / apply / bind 來解決,這裏就不貼代碼了。
在 JS 中,咱們聲明一個類,而後 new 一個實例。
function Person(name) { this.name = name; } var her = Person('Angelia'); console.log(her.name); // TypeError var me = new Person('Leo'); console.log(me.name); // "Leo"
若是咱們直接把調用這個函數,this 將指向全局對象,Person 在這裏就是一個普通函數,沒有返回值,默認 undefined,而嘗試訪問 undefined 的屬性就會報錯。
若是咱們使用 new 操做符,那麼 new 其實會生成一個新的對象,並將 this 指向這個新的對象,而後將其返回,因此 me.name 能打印出 「Leo」。
關於 new 的原理,我會在後面的文章分享,敬請期待。
你看,this 是否是變幻無窮。可是咱們得以不變應萬變。
在這麼多場景下,this 的指向萬變不離其宗:它必定是在執行時決定的,指向調用函數的對象。在閉包、回調函數、賦值等場景下咱們均可以利用 call / apply / bind 來改變 this 的指向,以達到咱們的預期。
接下來,請期待文章《理解 JavaScript call/apply/bind》。
Be Good. Sleep Well. And Enjoy.
原文發佈在個人公衆號 camerae,點擊查看。
前端技術 | 我的成長