這是前端面試題系列的第 4 篇,你可能錯過了前面的篇章,能夠在這裏找到:javascript
在前端的面試中,常常會問到有關 this 的指向問題。最近,朋友Z 向我求助說,他一看到 this 的題目就犯難,搞不清楚 this 究竟指向了誰。我爲他作了解答,並整理成了這篇文章,但願能幫到有須要的同窗。html
朋友Z 給我看了這樣一道題:前端
var length = 10; function fn () { console.log(this.length); } var obj = { length: 5, method: function (fn) { fn(); arguments[0](); } }; obj.method(fn, 1);
問:瀏覽器的輸出結果是什麼?java
它的答案是:先輸出一個 10
,而後輸出一個 2
。git
讓咱們來解析一下緣由:github
window
,因此輸出了 10。arguments[0]();
這條語句並不常見,可能你們有疑惑的點在這裏。 其實,arguments 是一種特殊的對象。在函數中,咱們無需指出參數名,就能訪問。能夠認爲它是一種,隱式的傳參形式。再來,很多同窗對 this 的指向感到疑惑,是由於 this 並無指向咱們預期的那個對象。面試
就像這道題,從語義上來看,咱們指望 fn() 輸出的是 obj 本身的 length,也就是 5,而不是 10。那麼若是要獲得 5 的結果,咱們該如何修改這段代碼呢?segmentfault
其實只要多作一步處理就好。就是讓 this 指向 obj 本身。這裏,咱們能夠用 call 來改變 this 的指向,像下面這樣:瀏覽器
var length = 10; function fn () { console.log(this.length); } var obj = { length: 5, method: function (fn) { // 在這裏用call 將 this 指向 obj 本身 fn.call(this); } }; obj.method(fn);
輸出的結果就是 5 了,搞定。數據結構
看吧,this 也沒那麼複雜吧,咱們只須要一些簡單的操做,就能控制 this 的指向了。那麼,問題來了,爲何有時候 this 會失控呢?
其實,這與 this 機制背後的原理有關。不過別急,讓咱們從理解 this 的基本概念開始,先來看看 this 究竟是什麼?
this 是 JavaScript 中的一個關鍵字。它一般被運用於函數體內,依賴於函數調用的上下文條件,與函數被調用的方式有關。它指向誰,則徹底是由函數被調用的調用點來決定的。
因此,this,是在運行時綁定的,而與編寫時的綁定無關。隨着函數使用場合的不一樣,this 的值也會發生變化。可是有一個總的原則:那就是this 總會指向,調用函數的那個對象。
從概念上理解起來,彷佛有點費勁。那咱們爲何還要使用 this 呢?用了 this 會帶來什麼好處?
讓咱們先看下面這個例子:
function identify() { return this.name.toUpperCase(); } var me = { name: "Kyle" }; var you = { name: "Reader" }; identify.call( me ); // KYLE identify.call( you ); // READER
一開始咱們可能太不明白爲什麼這樣輸出。那不如先換個思路,與使用 this 相反,咱們能夠明確地將環境對象,傳遞給 identify()。像這樣:
function identify(context) { return context.name.toUpperCase(); } identify( you ); // READER
在這個簡單的例子中,結果是同樣的。咱們能夠把環境對象直接傳入函數,這樣看來比較直觀。可是,當模式愈加複雜時,將執行環境做爲一個明確的參數傳遞給函數,就會顯得很是混亂了。
而 this 機制,能夠提供一種更優雅的方式,來隱含地「傳遞」一個對象的引用,這會使得 API 的設計更加地乾淨,複用也會變得容易。
明白了 this 的概念以後,不經讓我好奇,爲什麼 this 指向的就是函數運的執行環境呢?
以前,看到了 阮老師 的一篇文章,十分透徹地分析了 this 的原理。我根據本身的理解,整理以下。
不少教科書會告訴你,this 指的是函數運行時所在的環境。可是,爲何會這樣?也就是說,函數的運行環境究竟是怎麼決定的?
理解 this 的原理,有助於幫咱們更好地理解它的用法。JavaScript 語言之因此有 this 的設計,跟內存裏面的數據結構有關係。
來看一個簡單的示例:
var obj = { foo: 5 };
上面的代碼將一個對象賦值給變量 obj。JavaScript 引擎會先在內存裏面,生成一個對象 { foo: 5 },而後把這個對象的內存地址賦值給變量 obj。
也就是說,變量 obj 其實只是一個地址。後面若是要讀取 obj.foo,引擎先從 obj 拿到內存地址,而後再從該地址讀出原始的對象,返回它的 foo 屬性。
這樣的結構很清晰,但若是屬性的值是一個函數,又會怎麼樣呢?好比這樣:
var obj = { foo: function () {} };
這時,JavaScript 引擎會將函數單獨保存在內存中,而後再將函數的地址賦值給 foo 屬性的 value 屬性。
能夠看到,函數是一個單獨的值(以地址形式賦值),因此才能夠在不一樣的環境中執行。
又由於,JavaScript 容許在函數體內部,引用當前環境的其餘變量。因此須要有一種機制,可以在函數體內部得到當前的運行環境(context)。因此,this就出現了,它的設計目的就是在函數體內部,指代函數當前的運行環境。
在理解了 this 的原理以後,咱們用下面的 5 種狀況,來討論 this 的用法。
這是函數的最一般用法,屬於全局性調用,所以 this 就表明全局對象 window。
function test(){ this.x = 1; console.log(this.x); } test(); // 1
函數做爲某個對象的方法調用,這時 this 就指這個上級對象。
function test(){ console.log(this.x); } var o = {}; o.x = 1; o.m = test; o.m(); // 1
所謂構造函數,就是經過這個函數生成一個新對象(object)。這時,this 就指這個新對象。
function test(){ this.x = 1; } var o = new test(); console.log(o.x); // 1
apply() 是函數對象的一個方法,它的做用是改變函數的調用對象,它的第一個參數就表示改變後的調用這個函數的對象。所以,this 指的就是這第一個參數。
var x = 0; function test() { console.log(this.x); } var o = {}; o.x = 1; o.m = test; o.m.apply(); //0
apply() 的參數爲空時,默認調用全局對象。所以,這時的運行結果爲0,證實this指的是全局對象。
它與上文中提到的 call 的做用是同樣的,只是寫法上略有區別。因爲篇幅緣由,我會另啓一篇,來詳述它們的用法。
ES6 中的箭頭函數,在大部分狀況下,使得 this 的指向,變得符合咱們的預期。但有些時候,它也不是萬能的,一不當心的話,this 一樣會失控。
由於篇幅內容較多,我會另寫一篇文章來介紹。
最後,讓咱們來鞏固一下 this 的概念和用法。來看一道面試題:
window.val = 1; var obj = { val: 2, dbl: function () { this.val *= 2; val *= 2; console.log('val:', val); console.log('this.val:', this.val); } }; // 說出下面的輸出結果 obj.dbl(); var func = obj.dbl; func();
答案是輸出:2 、 4 、 8 、 8
。
解析:
this 指代了函數當前的運行環境,依賴於函數調用的上下文條件,在運行時纔會進行綁定。請牢記總原則:this 總會指向,調用函數的那個對象。
PS:歡迎關注個人公衆號 「超哥前端小棧」,交流更多的想法與技術。