很多人在使用Javascript之前都至少使用過C++、C#或Java,面向對象的編程思想已經根深蒂固,恰好Javascript在語法上借鑒了Java,雖然方便了Javascript的入門,但要深入理解Javascript的時候,長期使用這些編程語言造成的思維定勢卻給使用Javascript帶來誤導。作者在學習Javascript的時候曾陷入了這個誤區,希望通過這篇文章讓新學者避免走這個彎路,迅速正確地掌握Javascript。
1. 要點
在面對Javascript時,要牢記以下幾點:
1.1 Javascript不是面向對象的編程語言
如果非要把Javascript歸到面向什么的話,Javascript准確的說是面向原型的編程語言,它是從self語言發展而來,除了語法上借鑒了Java,其它方面和Java什么關系都沒有,本質上更不同。簡單說,Javascript里面沒有類,全是對象。在使用Javascript的時候,應該時刻提醒自己:Javascript不是C++、C#或Java。
1.2 Javascript是解釋執行的語言
雖然這很顯而易見,但如果不時刻牢記這一點,而把Javascript和編譯型語言的運行方式混淆的話,也非常不利於理解Javascript。比如,如果你用C++、C#或Java中的局部變量的思想去理解Javascript函數中通過var定義的變量的話,就會有麻煩。
2. Javascript中的對象和類型
Javascript沒有傳統面向對象編程語言中的類,全部是對象。
Javascript中的對象是鍵值對的集合,鍵的類型是字符串,值可以是任意對象。創建新對象的方式有:new 函數()、{}語法、Object.create(原型對象)。
函數也是對象,是一種包含可運行的代碼的特殊對象,並且代碼能夠以函數調用的形式被執行。函數對象能通過function關鍵字定義或通過new關鍵字使用Function構造函數來創建。
下面的示例創建了一個函數對象foo,並為它設置三個屬性:一個數字,一個字符串,一個函數。從示例可以看到函數對象除了能被調用以外,其他行為和普通對象無異。
當通過“new 函數()”的語法創建對象時,函數被稱為構造函數。
在Javascript中,通常將構造函數稱為對象的類型。比如obj = new Fun(),那么稱obj的類型為Fun。
Javascript有Undefined, Null, Boolean, Number, String, Object和Function這幾種基本類型。Javascript運行時會對Undefined, Null, Boolean, Number, and String這五種基礎類型的對象以及Function類型的函數對象會以特定的方式解釋和處理。
因為函數是對象,函數除了可以通過function語法定義以外,也可以用創建對象的方式創建:fun = new Function("參數", "函數體"),Function本身是Javascript內建的函數。
基本類型Boolean, Number, String, Object, Array, Date等都是內建的函數。沒有Undefined和Null內建函數,而是有Undefined和Null類型的內建的唯一的對象,分別是undefined和null。
3. 執行上下文
Javascript中每一行代碼的執行都是在當前執行上下文中完成。
執行上下文的層級關系是由代碼的定義結構(也就是源代碼字面結構)決定的,與運行時函數的調用棧結構無關。
執行上下文由一個上下文變量容器和一個this綁定構成。上下文變量容器中包含當前執行上下文中定義的變量。this綁定表示當前執行上下文中this關鍵字指向的對象。
簡單來說,每次函數調用都進入一個新的執行上下文,同一函數遞的歸調用也會進入新的執行上下文。
每一個執行上下文都包含對創建它的上一級執行上下文的引用,因此所有執行上下文組成一棵樹。
在瀏覽器中,根執行上下文(即不在任何函數中運行的代碼)中沒有上下文變量容器,this綁定到全局對象window。因此在根執行上下文中定義變量時,使用var、不使用var和使用this.定義的變量都是全局對象window的屬性,因為沒有上下文變量容器。通過這一規定,Javascript運行過程中的所有執行上下文都能指向全局對象,形成一棵完整的樹。
在除了根執行上下文之外的其它執行上下文中,變量定義的規則如下:
var i = 1 | 變量i位於當前執行上下文的上下文變量容器中 |
j = 2 | 變量j是全局對象的屬性 |
this.k = 3 | 變量k是當前執行上下文的this對象的屬性 |
在表達式中使用一個變量時,Javascript將首先從當前執行上下文的上下文變量容器中查找該變量,如果不存在,則在上級執行上下文的上下文變量容器中查找,以此類推,直到根執行上下文,在根執行上下文中,將在全局對象中查找是否有同名的屬性。示例如下:
當前執行上下文的this綁定默認繼承自上級執行上下文,如果當前運行代碼所在的函數是通過obj.currentfun()的方式調用,也就是obj對象的方法,那么this將被綁定到obj。在使用時,this關鍵字必須明確寫出,因為this在邏輯上是與執行上下文有關的,與當前函數所屬的對象沒有直接因果關系。
垃圾回收機制:如果當前執行上下文及其中的代碼已經執行完成,且其所有下級執行上下文已經釋放,且沒有來自當前執行上下文之外的對當前執行上下文的上下文變量容器中的變量的引用,那么當前執行上下文被釋放,當前執行上下文的上下文變量容器中的變量指向的對象和this指向的對象的引用值減一(各引擎具體實現不同,這里描述的是原理)。
這也就是Javascript中常用到的閉包模式的原理。如果從傳統面向對象語言的角度去看,閉包模式看上去很詭異;但是在理解了Javascript的執行上下文后,閉包模式就是一種很自然的代碼編寫模式。
4. 原型
由於Javascript中沒有類的概念,因此也沒有面向對象中的繼承的概念。在完全沒有任何繼承機制的編程語言中,每個對象的方法都要明確設置一遍。假如Javascript沒有繼承機制,通過構造函數function A(name) {this.name= name;};創建了100個A的對象, 現在要為每個對象實現一個新的方法,就需要明確的為這100個對象都添加一個方法屬性,當每個對象都有很多方法的時候,會帶來巨大的性能開銷和非常差的編碼體驗,使得代碼編寫方式最終會倒退到面向過程的方式。
Javascript由面向原型的編程語言self發展而來,通過原型實現了另一種方式的繼承:屬性共享。如果為了和面向對象中的繼承區分,Javascript中通過原型實現的應該叫做共享。
Javascript中的原型在物理上是一個對象,可以是任意對象;原型這個詞也表示一種機制。
每個對象都可以擁有一個原型,同時這個原型本身還可以有自己的原型,形成一個原型鏈,直到其原型為null的原型。
當獲取對象的一個屬性值的時候,先在當前對象的鍵值對集合中查找這屬性,如果找到返回對應的值;如果沒有,則在原型對象的鍵值對集合中查找,如果還沒找到,則在原型的原型中查找,以此類推。
通過一個示例來演示原型的使用方法:
上面例子中,PersonPrototype就是原型對象,Person是構造函數,將Person的prototype屬性設置為PersonPrototype對象,那么通過new Person創建的所有對象的原型都是PersonPrototype。
通過示例可以看到,修改原型中showMyName方法的定義,p.showMyName()的輸出也會跟着發生變化,這個現象和上面原型的定義一致。
如果明確的為對象p設置一個showMyName方法,那么p對象的鍵值對集合中將也包含showMyName的定義,根據上面規則的描述的規則,p對象中定義的showMyName將被使用,忽略了原型中對showMyName的定義。這個時候,我們可以通過p.__proto__.showMyName來訪問原型中的showMyName方法,但是調用p.__proto__.showMyName的時候,this關鍵字綁定到了原型對象PersonPrototype,由於原型對象中沒有name屬性的定義,因此輸出“undefined!”。
接下來再定義幾個Person對象:
在上述代碼中,所有Person對象都共享了原型對象的方法,除非某個Person對象中重新定義了原型中同名的方法。
在對象中定義與原型中同名的屬性時,會在該對象中創建一個新的鍵值對,而不會覆蓋原型中同名屬性的值。
一般來說原型對象中通常定義方法和常量,不應該定義對象的狀態值,因為多數情況下所有的對象共享一個狀態值沒有實際意義。比如上面的代碼中,name屬性在邏輯上不應該被定義在原型中,因為每個人都有自己的名字。
方法是最適合定義在原型中的。共享方法給所有的對象,正是原型的主要作用。
5. 結論
本文介紹了Javascript的三個核心概念:對象、執行上下文和原型。這些是Javascript最重要的內容,理解這三點之后,很快就能掌握Javascript。
Javascript代碼中各類表達式與Java基本相同。
Javascript包括很多預定義的類型、對象和對象屬性,比如全局對象及其屬性,JSON類型和RegExp類型等等,這些查閱手冊即可。
當然,還有很多細節性的東西,例如:如果不明確設置Person的原型,該原型默認會是一個Person類型的對象;constructor屬性的作用。有機會將在后續章節中展開討論。