首先,JavaScript的this指向問題並不是傳說中的那麼難,不難的是機制並不複雜,而被認爲很差理解的是邏輯關係和容易混淆的執行上下文。這篇博客也就會基於這兩個很差理解的角度來展開,如要要嚴格的來對this的指向來分類的話,有三類不一樣的狀況,一種是獨立函數執行的指向機制,第二種就是引用指向機制,第三種是new機制下的this指向。而後創建在這三個指向機制的基礎上來剖析一些this的常見問題,下面進入正文解析this指向機制:數組
1、獨立函數執行的指向機制app
在JavaScript中函數執行能夠分爲兩種狀況,一種是函數純粹的執行,另外一種是被某個對象引用執行。函數純粹的執行也一般被描述爲獨立函數執行,這種狀況的函數執行內部this指向全局對象,可是在嚴格模式下獨立函數執行this會指向undefined。注意,機制的自己很是簡單,可是容易出錯的卻在函數被調用的機制上,若是再在這個問題上深刻的追溯問題的根源的話,其自己的問題是出在JavaScript對象與對象的屬性和方法的歸屬關係問題。函數
1.1指向全局與指向undefined學習
function foo(){ console.log(this.a); } var a = 2; foo();//2
這個運行結果證實了獨立的函數執行的this指向了全局對象,接下來咱們再看看嚴格模式下的獨立函數的this的指向。this
function foo(){ "use strict"; console.log(this.a); } var a = 2; foo();//TypeError: Cannot read property 'a' of undefined
從錯誤提示能夠看出來,嚴格模式下的this指向是undefined。可是這兩個函數的執行能不能就表明所有的純函數執行就是這個機制呢?spa
1.2this向與賦值機制code
function foo(){ console.log(this.a); } var obj = { a:2, foo:foo } var bar = obj.foo; var a = 1; bar();//1
執行結果爲何是1?由於bar是一個純函數執行啊。很難理解這部分機制的大多都是學習後臺強屬性語言,由於在強屬性語言中,屬性的歸屬永遠都是一個對象的,可是在JavaScript中全部的函數都是一個獨立的個體,它不屬於任何對象,又能夠是任何對象的方法。當函數在不被任何對象引用執行的時候它就算是一個獨立函數,可能有的人會理解爲var bar = obj.foo是對象引用啊,記住在JavaScript中,這行代碼的函數是賦值,bar得到是foo函數的堆內存地址,不會記錄obj的關聯性。而在強屬性語言中,這種賦值就不僅是把函數賦給一個變量了,同時還會帶着這個函數的關聯對象的關係一塊兒賦給這個變量,因此這就是JavaScript的賦值機制給this帶來的問題。對象
關於個賦值機制這段代碼可能還不能徹底說服你,由於foo函數被聲明在全局,那下面來看看下面這段修改後的代碼:blog
var obj = { a:2, foo:function(){ console.log(this.a); } } var bar = obj.foo; var a = 1; bar();//1
引用方法賦值行爲,接收方法的變量不會接收引用關係,得到的是一個獨立函數的純粹堆內存引用,這對理解this引用很重要。並且還有值得咱們注意的一個地方就是經過參數的傳值行爲本質上也只是函數的純粹賦值而已,不會帶着引用關係傳遞到形參上的。看下面這段代碼來理解這種機制:繼承
var obj = { a:2, foo:function(){ console.log(this.a); } } function bar(fn){ fn(); } var a = 1; bar(obj.foo);//1
JavaScript傳值、賦值本質上都只是將函數的堆內存地址賦給變量而已,而上面這段代碼有說明了另外一個問題就是無論在什麼地方,函數純粹的執行它的this指向都是全局(fn嵌套在bar內,可是this仍是指向了全局對象),而後這裏又會延伸出一個新的問題,嵌套函數內嚴格模式,這裏有一個細節值得注意。
var obj = { a:2, foo:function(){ console.log(this.a); } } function bar(fn){ "use strict"; fn(); } var a = 1; bar(obj.foo);//1
不是說嚴格模式下的this指向undefined嗎?怎麼這裏的this仍是指向了全局對象呢?
不錯,在嚴格模式下this指向會被修改成undefined,可是必須是當前做用域被設置了嚴格模式,看下面這段代碼來理解:
var obj = { a:2, foo:function(){ "use strict"; console.log(this.a); } } function bar(fn){ fn(); } var a = 1; bar(obj.foo);// Cannot read property 'a' of undefined
關於純函數的執行this指向已經所有解析完,接下來咱們繼續引用函數執行的this指向機制。
2、引用函數執行的this指向機制
關於引用函數執行的this指向機制比起純函數執行來講,要簡單的多,惟一存在容易混淆不清的地方就是對象屬性與做用域,不少時候咱們都把做用域當作是對象,但實際上不是,他只是在某些狀況下有些特性與對象相似而已。
function foo(){ console.log(this.a); } var obj = { a:1, foo:foo } var a = 3; obj.foo();//1
其實經過上面的示例咱們能夠看到函數內的this指向了引用函數執行的對象。其實從這個例子咱們也能夠反推出一個邏輯,那就是純函數執行其實質並不是是純粹的函數執行,而是當函數沒有被指定的對象引用執行的時候,函數其實質上是被全局對象隱式的引用執行,在嚴格模式下是被undefined隱式的引用執行。
2.1引用函數執行的this機制與賦值
function foo(){ console.log(this.a); } var obj = { a:1, foo:foo } var obj1 = { a:2, obj:obj } var a = 3; obj1.obj.foo();//1
咱們在前面對函數賦值機制作了深刻剖析,再來看看對象賦值。將一個對象賦給另外一個對象,再經過鏈式調用對象的方法,其本質上this仍是遵循了引用執行機制,this指向直接調用函數的對象,再來看下一個示例就會更清楚了:
function foo(){ console.log(this.a); } var obj = { foo:foo } var obj1 = { a:2, obj:obj } var a = 3; obj1.obj.foo();//undefined
當引用執行函數的對象沒有函數執行須要的參數時,這個參數的值會默認爲undefined,必定要注意,這種賦值調用,對象與對象之間並非繼承關係,僅僅只是一個調用關係。在對象原型鏈的博客裏我會詳細剖析引用執行函數的對象this指向及內部原型鏈的查詢機制。
2.2引用函數執行的this指向與call和apply
function foo(){ console.log(this.a); } var obj = { a:1 } foo.call(obj);//1
上面的示例證實函數this指向了傳入call方法的實參obj上了,而且會當即執行這個函數,call方法其本質上是一個很是簡單的操做,只不過是實現了對象動態添加方法而且當即執行的一個行爲而已。因此上面的代碼能夠顯示的用下列代碼靜態添加方法並引用執行來完成:
function foo(){ console.log(this.a); } var obj = { a:1, foo:foo } obj.foo();//1
上面這兩段代碼從本質上來將是徹底對等的操做,只不過經過call方法實現了動態添加執行,在obj對象上實質上是找不到foo這個方法的。關於call的this指向解析清楚之後,關於apply就很容易了,這兩個方法的核心功能實際上是同樣的,都是將方法動態的綁定到指定的對象上而後引用執行這個方法。既然是方法要執行就必然會涉及到參數傳遞,這兩個方法的差別就在於參數的傳遞有寫差別,其餘的徹底一致。
function foo(a,b,c){ console.log(this.a + ";參數:" + a + b + c); } var obj = { a:1 } var a = "a", b = "b", c = "c", arr = ["a","b","c"]; foo.call(obj,a,b,c);//1;參數:abc foo.apply(obj,arr);//1;參數:abc
call的參數傳遞和普通的函數傳參一致,都是單個一一傳入,apply的參數傳遞方式是將實參打包成一個數組傳入,數組的下標與形參的順序一一對應,由於call和apply方法的第一個參數須要傳入函數執行的引用對象,因此函數執行的參數都是從第二位開始傳入。
關於函數執行的this指向解析所有解析完,還記得在博客開頭的我有提到關於this指向的執行上下文混淆的問題嗎?這個問題主要出在當引用對象是一個回調函數的時候,就會容易混淆this的指向,這是由於咱們常常把執行性上下單純的當作一個對象來對待,這樣的觀點致使咱們對this的指向很容易混淆不清,下面咱們來看一段代碼:
function foo(){ var a = 1; function bar(){ var a = 2 console.log(this.a); } return bar; } var a = 3; function baz(){ console.log(this.a); } baz.call(foo());//undefined
有點小驚訝吧,不是2,也不是3,而是undefined,這裏有幾個誤區:
1.一般咱們都把call和apply兩個方法的第一個參數成爲執行上下文,這不徹底正確。
2.由於全局做用域會把變量和函數轉成自身屬性(即全局對象的屬性),可是其餘的函數的做用域不能。
3.一般咱們把做用域成爲執行上下文當作是一個對象,誤認爲把做用域內的變量和方法及調用執行方法的對象的屬性都歸爲執行上下文對象的屬性,這個誤區能夠算是上面兩個誤區的增強版。
這裏咱們先要試着理解執行上下文究竟是個什麼東西?而後才能弄清楚this指向的究竟是什麼?
咱們一般所表達的執行上下文其實質上包含了三個部分:引用方法執行的對象,方法自身執行的做用域,除自身做用域的全部上層做用域。而this指向的是引用方法執行的對象。
上面的代碼中,baz.call(foo())能夠理解爲時bar.baz()這樣的引用執行方式,可是由於bar上沒有baz這個方法,這樣寫會報錯,而call和apply實質上在調用函數以前給引用執行方法的對象臨時的作了一個添加方法的處理,只是這個方法會執行完之後就會被刪除,表面上咱們能夠這麼理解,可是引擎內部是不回作這種損耗效率的事情,call和apply的內部機制應該是一種又有效的動態的添加執行行爲,這部分沒有深刻研究,有興趣的朋友能夠在評論區一塊兒討論。
其實這裏應該還有一個關於bind的柯里化的this指向問題,這部分我寫到柯里化那部分博客的時候,再考慮是在這篇博客上添加仍是在柯里化部分的博客上擴展。
3、new機制下的this指向
關於new機制的this指向相對來講是最簡單的,由於這個機制下的this是一個固定指向,只出如今經過function實例化對象的時候,不會出如今程序邏輯中。由於涉及一些對象實例化因此會在這裏擴展一點對象實例化的內容,由於是一個交叉知識點,後期還會在對象原型機制的時候再作管理解析。
function Car(name,height,lang,weight,health){ this.name = name; this.height = height; this.lang = lang; this.weight = weight; this.health = health; this.run = function(){ this.health --; } } var BMW = new Car("BMW",1400,4900,1400,100); console.log(BMW);
在new機制下的this,指向新建立的對象。這個新建立的對象的描述是一個很是模糊的說法,好比從字面量的層面來理解,上面的示例中,能夠說this是指向BMW,若是對JavaScript的堆棧存儲關係有所瞭解就會知道這個描述能夠說是錯誤的,由於當咱們var一個新的變量,而後將BMW的值賦給這個新的變量,這個新的變量並不會記憶BWM的引用賦值關係,而是直接將BMW指向的堆內存當成本身的,這時候這個新的變量和BMW就同時平等的關聯着這個同一個對象的堆內存地址。
從上面的思考能夠看出,這個新建立的對象從嚴格意義上來說並不能說是指向了某個變量,還有一種狀況就是隻經過new關鍵字示例化了對象,但並無給某個變量賦值操做:
new Car("BMW",1400,4900,1400,100);
這個不作賦值操做的對象實例化,在實際開發中咱們雖然不會這麼作,可是本質上確實是實例化了一個對象,這就進一步說明前面的思考是值得探討的。
咱們連這個新對象是誰都不知道,就說this指向了新建立的對象,這有點太不負責任了吧!!!
因此這裏,重點須要探討的是這個新的對象究竟是誰?下面我經過一個流程圖來描述function的new機制實例化對象的全過程來講明這個問題:
經過上圖基本上就能夠徹底理解function構造對象實例化的內部機制了,基本上就是一下幾個步驟:
1.函數執行的前一刻建立變量對象後,在變量對象上隱式的生成一個屬性this,並賦值{}。
2.變量提高參數統一,函數聲明提高完成後開始執行,經過this.xxx的方式給this對象添加屬性,而後執行賦值;(而且還會隱式的添加原型指向:__proto__指向Object,在原型上的constructor的值賦爲構造函數)。
3.函數執行完的前一刻隱式的執行return this操做。
以上就是this指向規則的所有剖析內容,ES6的胖箭頭this詞法不在這裏分析,後期會有關於ES6的詳細內容博客。