1. 程式人生 > >JavaScrpt中的this指向規則

JavaScrpt中的this指向規則

首先,JavaScript的this指向問題並非傳說中的那麼難,不難的是機制並不複雜,而被認為不好理解的是邏輯關係和容易混淆的執行上下文。這篇部落格也就會基於這兩個不好理解的角度來展開,如要要嚴格的來對this的指向來分類的話,有三類不同的情況,一種是獨立函式執行的指向機制,第二種就是引用指向機制,第三種是new機制下的this指向。然後建立在這三個指向機制的基礎上來剖析一些this的常見問題,下面進入正文解析this指向機制:

一、獨立函式執行的指向機制

在JavaScript中函式執行可以分為兩種情況,一種是函式純粹的執行,另一種是被某個物件引用執行。函式純粹的執行也通常被描述為獨立函式執行,這種情況的函式執行內部this指向全域性物件,但是在嚴格模式下獨立函式執行this會指向undefined。注意,機制的本身非常簡單,但是容易出錯的卻在函式被呼叫的機制上,如果再在這個問題上深入的追溯問題的根源的話,其本身的問題是出在JavaScript物件與物件的屬性和方法的歸屬關係問題。

1.1指向全域性與指向undefined

function foo(){
    console.log(this.a);
}
var a = 2;
foo();//2

這個執行結果證明了獨立的函式執行的this指向了全域性物件,接下來我們再看看嚴格模式下的獨立函式的this的指向。

function foo(){
    "use strict";
    console.log(this.a);
}
var a = 2;
foo();//TypeError: Cannot read property 'a' of undefined

從錯誤提示可以看出來,嚴格模式下的this指向是undefined。但是這兩個函式的執行能不能就代表全部的純函式執行就是這個機制呢?

1.2this向與賦值機制

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函式被宣告在全域性,那下面來看看下面這段修改後的程式碼:

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指向機制。

二、引用函式執行的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指向問題,這部分我寫到柯里化那部分部落格的時候,再考慮是在這篇部落格上新增還是在柯里化部分的部落格上擴充套件。

三、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的詳細內容部落格。