javascript技術難點(三)之this、new、apply和call詳解

發表於2014-12-10

講解this指標的原理是個很複雜的問題,如果我們從javascript裡this的實現機制來說明this,很多朋友可能會越來越糊塗,因此本篇打算換一個思路從應用的角度來講解this指標,從這個角度理解this指標更加有現實意義。

下面我們看看在java語言裡是如何使用this指標的,程式碼如下:

上面的程式碼執行後沒有任何問題,下面我修改下這個程式碼,加一個靜態的方法,靜態方法裡使用this指標呼叫類裡的屬性,如下圖所示:

我們發現IDE會報出語法錯誤“Cannot use this in a static context”,this指標在java語言裡是不能使用在靜態的上下文裡的。

在物件導向程式設計裡有兩個重要的概念:一個是類,一個是例項化的物件,類是一個抽象的概念,用個形象的比喻表述的話,類就像一個模具,而例項化物件就是通過這個模具製造出來的產品,例項化物件才是我們需要的實實在在的東西,類和例項化物件有著很密切的關係,但是在使用上類的功能是絕對不能取代例項化物件,就像模具和模具製造的產品的關係,二者的用途是不相同的。

有上面程式碼我們可以看到,this指標在java語言裡只能在例項化物件裡使用,this指標等於這個被例項化好的物件,而this後面加上點操作符,點操作符後面的東西就是this所擁有的東西,例如:姓名,工作,手,腳等等。

其實javascript裡的this指標邏輯上的概念也是例項化物件,這一點和java語言裡的this指標是一致的,但是javascript裡的this指標卻比java裡的this難以理解的多,究其根本原因我個人覺得有三個原因:

原因一:javascript是一個函式程式語言,怪就怪在它也有this指標,說明這個函式程式語言也是物件導向的語言,說的具體點,javascript裡的函式是一個高階函式,程式語言裡的高階函式是可以作為物件傳遞的,同時javascript裡的函式還有可以作為建構函式,這個建構函式可以建立例項化物件,結果導致方法執行時候this指標的指向會不斷髮生變化,很難控制。

原因二:javascript裡的全域性作用域對this指標有很大的影響,由上面java的例子我們看到,this指標只有在使用new操作符後才會生效,但是javascript裡的this在沒有進行new操作也會生效,這時候this往往會指向全域性物件window

原因三:javascript裡call和apply操作符可以隨意改變this指向,這看起來很靈活,但是這種不合常理的做法破壞了我們理解this指標的本意,同時也讓寫程式碼時候很難理解this的真正指向

上面的三個原因都違反了傳統this指標使用的方法,它們都擁有有別於傳統this原理的理解思路,而在實際開發裡三個原因又往往會交織在一起,這就更加讓人迷惑不解了,今天我要為大家理清這個思路,其實javascript裡的this指標有一套固有的邏輯,我們理解好這套邏輯就能準確的掌握好this指標的使用。

我們先看看下面的程式碼:

在script標籤裡我們可以直接使用this指標,this指標就是window物件,我們看到即使使用三等號它們也是相等的。全域性作用域常常會干擾我們很好的理解javascript語言的特性,這種干擾的本質就是:

在javascript語言裡全域性作用域可以理解為window物件,記住window是物件而不是類,也就是說window是被例項化的物件,這個例項化的過程是在頁面載入時候由javascript引擎完成的,整個頁面裡的要素都被濃縮到這個window物件,因為程式設計師無法通過程式語言來控制和操作這個例項化過程,所以開發時候我們就沒有構建這個this指標的感覺,常常會忽視它,這就是干擾我們在程式碼裡理解this指標指向window的情形。

干擾的本質還和function的使用有關,我們看看下面的程式碼:

上面是我們經常使用的兩種定義函式的方式,第一種定義函式的方式在javascript語言稱作宣告函式,第二種定義函式的方式叫做函式表示式,這兩種方式我們通常認為是等價的,但是它們其實是有區別的,而這個區別常常會讓我們混淆this指標的使用,我們再看看下面的程式碼:

這又是一段沒有按順序執行的程式碼,先看看ftn02,列印結果是undefined,undefined我在前文裡講到了,在記憶體的棧區已經有了變數的名稱,但是沒有棧區的變數值,同時堆區是沒有具體的物件,這是javascript引擎在預處理(群裡東方說預處理比預載入更準確,我同意他的說法,以後文章裡我都寫為預處理)掃描變數定義所致,但是ftn01的列印結果很令人意外,既然列印出完成的函式定義了,而且程式碼並沒有按順序執行,這隻能說明一個問題:

在javascript語言通過宣告函式方式定義函式,javascript引擎在預處理過程裡就把函式定義和賦值操作都完成了,在這裡我補充下javascript裡預處理的特性,其實預處理是和執行環境相關,在上篇文章裡我講到執行環境有兩大類:全域性執行環境和區域性執行環境,執行環境是通過上下文變數體現的,其實這個過程都是在函式執行前完成,預處理就是構造執行環境的另一個說法,總而言之預處理和構造執行環境的主要目的就是明確變數定義,分清變數的邊界,但是在全域性作用域構造或者說全域性變數預處理時候對於宣告函式有些不同,宣告函式會將變數定義和賦值操作同時完成,因此我們看到上面程式碼的執行結果。由於宣告函式都會在全域性作用域構造時候完成,因此宣告函式都是window物件的屬性,這就說明為什麼我們不管在哪裡宣告函式,宣告函式最終都是屬於window物件的原因了

關於函式表示式的寫法還有祕密可以探尋,我們看下面的程式碼:

執行結果我們發現ftn04雖然在ftn03作用域下,但是執行它裡面的this指標也是指向window,其實函式表示式的寫法我們大多數更喜歡在函式內部寫,因為宣告函式裡的this指向window這已經不是祕密,但是函式表示式的this指標指向window卻是常常被我們所忽視,特別是當它被寫在另一個函式內部時候更加如此。

其實在javascript語言裡任何匿名函式都是屬於window物件,它們也都是在全域性作用域構造時候完成定義和賦值,但是匿名函式是沒有名字的函式變數,但是在定義匿名函式時候它會返回自己的記憶體地址,如果此時有個變數接收了這個記憶體地址,那麼匿名函式就能在程式裡被使用了,因為匿名函式也是在全域性執行環境構造時候定義和賦值,所以匿名函式的this指向也是window物件,所以上面程式碼執行時候ftn04的this也是指向window,因為javascript變數名稱不管在那個作用域有效,堆區的儲存的函式都是在全域性執行環境時候就被固定下來了,變數的名字只是一個指代而已。

這下子壞了,this都指向window,那我們到底怎麼才能改變它了?

在本文開頭我說出了this的祕密,this都是指向例項化物件,前面講到那麼多情況this都指向window,就是因為這些時候只做了一次例項化操作,而這個例項化都是在例項化window物件,所以this都是指向window。我們要把this從window變成別的物件,就得要讓function被例項化,那如何讓javascript的function例項化呢?答案就是使用new操作符。我們看看下面的程式碼:

這是我上篇講到的關於this使用的一個例子,寫法一是我們大夥都愛寫的一種寫法,裡面的this指標不是指向window的,而是指向Object的例項,firebug的顯示讓很多人疑惑,其實Object就是物件導向的類,大括號裡就是例項物件了,即obj和otherObj。Javascript裡通過字面量方式定義物件的方式是new Object的簡寫,二者是等價的,目的是為了減少程式碼的書寫量,可見即使不用new操作字面量定義法本質也是new操作符,所以通過new改變this指標的確是不過攻破的真理。

下面我使用javascript來重寫本篇開頭用java定義的類,程式碼如下:

看this指標的列印,類變成了Person,這表明function Person就是相當於在定義一個類,在javascript裡function的意義實在太多,function既是函式又可以表示物件,function是函式時候還能當做建構函式,javascript的建構函式我常認為是把類和建構函式合二為一,當然在javascript語言規範裡是沒有類的概念,但是我這種理解可以作為建構函式和普通函式的一個區別,這樣理解起來會更加容易些

下面我貼出在《javascript高階程式設計》裡對new操作符的解釋:

new操作符會讓建構函式產生如下變化:

1.       建立一個新物件;

2.       將建構函式的作用域賦給新物件(因此this就指向了這個新物件);

3.       執行建構函式中的程式碼(為這個新物件新增屬性);

4.       返回新物件

關於第二點其實很容易讓人迷惑,例如前面例子裡的obj和otherObj,obj.show(),裡面this指向obj,我以前文章講到一個簡單識別this方式就是看方法呼叫前的物件是哪個this就指向哪個,其實這個過程還可以這麼理解,在全域性執行環境裡window就是上下文物件,那麼在obj裡區域性作用域通過obj來代表了,這個window的理解是一致的。

第四點也要著重講下,記住建構函式被new操作,要讓new正常作用最好不能在建構函式裡寫return,沒有return的建構函式都是按上面四點執行,有了return情況就複雜了,這個知識我會在講prototype時候講到。

Javascript還有一種方式可以改變this指標,這就是call方法和apply方法,call和apply方法的作用相同,就是引數不同,call和apply的第一個引數都是一樣的,但是後面引數不同,apply第二個引數是個陣列,call從第二個引數開始後面有許多引數。Call和apply的作用是什麼,這個很重要,重點描述如下:

Call和apply是改變函式的作用域(有些書裡叫做改變函式的上下文)

這個說明我們參見上面new操作符第二條:

將建構函式的作用域賦給新物件(因此this就指向了這個新物件);

Call和apply是將this指標指向方法的第一個引數。

我們看看下面的程式碼:

我們看到apply和call改變的是this的指向,這點在開發裡很重要,開發裡我們常常被this所迷惑,迷惑的根本原因我在上文講到了,這裡我講講表面的原因:

表面原因就是我們定義物件使用物件的字面表示法,字面表示法在簡單的表示裡我們很容易知道this指向物件本身,但是這個物件會有方法,方法的引數可能會是函式,而這個函式的定義裡也可能會使用this指標,如果傳入的函式沒有被例項化過和被例項化過,this的指向是不同,有時我們還想在傳入函式裡通過this指向外部函式或者指向被定義物件本身,這些亂七八糟的情況使用交織在一起導致this變得很複雜,結果就變得糊里糊塗。

其實理清上面情況也是有跡可循的,就以定義物件裡的方法裡傳入函式為例:

情形一:傳入的引數是函式的別名,那麼函式的this就是指向window

情形二:傳入的引數是被new過的建構函式,那麼this就是指向例項化的物件本身;

情形三:如果我們想把被傳入的函式物件裡this的指標指向外部字面量定義的物件,那麼我們就是用apply和call

我們可以通過程式碼看出我的結論,程式碼如下:

結果如下:

最後再總結一下:

如果在javascript語言裡沒有通過new(包括物件字面量定義)、call和apply改變函式的this指標,函式的this指標都是指向window

相關文章