閱讀目錄
JavaScript 是我接觸到的第二門編程語言,第一門是 C 語言。然後才是 C++、Java 還有其它一些什麼。所以我對 JavaScript 是非常有感情的,畢竟使用它有十多年了。早就想寫一篇關於 JavaScript 方面的東西,從入門的學習筆記到高手的心得體會一應俱全,不管我怎麼寫,都難免落入俗套,所以遲遲沒有動筆。另外一個原因,也是因為在 Ubuntu 環境中一直沒有找到很好的 JavaScript 開發工具,這種困境直到 Node.js 和 Visual Studio Code 的出現才完全解除。
十年前,對 JavaScript 的介紹都是說他是基於對象的編程語言,而從沒有哪本書會說 JavaScript 是一門面向對象的編程語言。基於對象很好理解,畢竟在 JavaScript 中一切都是對象,我們隨時可以使用點號操作符來調用某個對象的方法。但是十年前,我們編寫 JavaScript 程序時,都是像 C 語言那樣使用函數來組織我們的程序的,只有在論壇的某個角落中,有少數的高手會偶爾提到你可以通過修改某個對象的prototype
來讓你的函數達到更高層次的復用,直到 Flash 的 ActionScript 出現時,才有人系統介紹基於原型的繼承。十年後的現在,使用 JavaScript 的原型鏈和閉包來模擬經典的面向對象程序設計已經是廣為流傳的方案,所以,說 JavaScript 是一門面向對象的編程語言也絲毫不為過。
我喜歡 JavaScript,是因為它非常具有表現力,你可以在其中發揮你的想象力來組織各種不可思議的程序寫法。也許 JavaScript 語言並不完美,它有很多坑和陷阱,而正是這些很有特色的語言特性,讓 JavaScript 的世界出現了很多奇技淫巧。
JavaScript面向對象編程指南 PDF書簽版 http://www.linuxidc.com/Linux/2016-04/130052.htm
JavaScript權威指南(第6版) PDF中文版+英文版+源代碼 http://www.linuxidc.com/Linux/2013-10/91056.htm
JavaScript 是一門基於對象的編程語言,在 JavaScript 中一切都是對象,包括函數,也是被當成一等一的對象對待,這正是 JavaScript 極其富有表現力的原因。在 JavaScript 中,創建一個對象可以這麼寫:
var someThing = new Object();
這和其它面向對象語言的使用某個類的構造函數創建一個對象是一樣一樣的。但是在 JavaScript 中,這不是最推薦的寫法,使用對象字面量來定義一個對象更簡潔,如下:
var anotherThing = {};
這兩個語句其本質是一樣的,都是生成一個空對象。對象字面量也可以用來寫數組以及更加復雜的對象,這樣:
var weekDays = ["Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday", "Sunday"];
這樣:
var person = {
name : "youxia",
age : 30,
gender : "male",
sayHello : function(){ return "Hello, my name is " + this.name; }
}
甚至這樣數組和對象互相嵌套:
var workers = [{name : "somebody", speciality : "Java"}, {name : "another", speciality : ["HTML", "CSS", "JavaScript"]}];
需要注意的是,對象字面量中的分隔符都是逗號而不是分號,而且即使 JavaScript 對象字面量的寫法和 JSON 的格式相似度很高,但是它們還是有本質的區別的。
在我們搗鼓 JavaScript 的過程中,工具是非常重要的。我這裡介紹的第一個工具就是 Chromium 浏覽器中自帶的 JavaScript 控制台。在 Ubuntu 中安裝 Chromium 浏覽器只需要一個命令就可以搞定,如下:
sudo apt-get install chromium
啟動 Chromium 浏覽器後,只需要按 F12 就可以調出 JavaScript 控制台。當然,在菜單中找出來也可以。下面,讓我把上面的示例代碼輸入到 JavaScript 控制台中,一是可以看看我們寫的代碼是否有語法錯誤,二是可以看看 JavaScript 對象的真面目。如下圖:
對於廣大的前端攻城獅來講,Chromium 的 JavaScript 控制台已經是一個爛大街的工具了,在控制台中寫console.log("Hello, World!");
就像是在 C 語言中寫printf("Hello, World!");
一樣成為了入門標配。在控制台中輸入 JavaScript 語句後,一按 Enter 該行代碼就立即執行,如果要輸入多行代碼怎麼辦呢?一個辦法就是按 Shift+Enter 進行換行,另外一個辦法就是在別的編輯器中寫好然後復制粘貼。不過在 Chromium 的 JavaScript 控制台中還有一些不那麼廣泛流傳的小技巧,比如使用console.dir()
函數輸出 JavaScript 對象的內部結構,如下圖:
從圖中,可以很容易看出每一個對象的屬性、方法和原型鏈。
和其它的面向對象編程語言不同, JavaScript 不是基於類的代碼復用體系,它選擇了一種很奇特的基於原型的代碼復用機制。通俗點說,如果你想創建很多對象,而這些對象有某些相同的屬性和行為,你為每一個對象編寫單獨的代碼肯定是不合算的。在其它的面向對象編程語言中,你可以先設計一個類,然後再以這個類為模板來創建對象。我這裡稱這種方式為經典的面向對象體系。而在 JavaScript 中,解決這個問題的方式是把一個對象作為另外一個對象的原型,擁有相同原型的對象自然擁有了相同的屬性和行為。對象擁有原型,原型又有原型的原型,最終構成一個原型鏈。在現代 JavaScript 模式中,硬是用函數、閉包和原型鏈模擬了經典的面向對象體系。
原型這個概念本身並不復雜,復雜的是 JavaScript 中的隱式原型和函數對象。什麼是隱式原型,就是說在 JavaScript 中不管你以什麼方式創建一個對象,它都會自動給你生成一個原型對象,我們的對象中,有一個隱藏的__proto__
屬性,它指向這個自動生成的原型對象;並且在 JavaScript 中不管你以什麼方式創建一個對象,它最終都是從構造函數生成的,以對象字面量構造的對象也有構造函數,它們分別是Object()
和Array()
,每一個構造函數都有一個自動生成的prototype
屬性,它也指向那個自動生成的原型對象。而且在 JavaScript 中一切都是對象,構造函數也不例外,所以構造函數既有prototype
屬性,又有__proto__
屬性。再而且,自動生成的原型對象也是對象,所以它也應該有自己的原型對象。你看,說起來都這麼拗口,理解就更加不容易了,更何況 JavaScript 中還內置了Object()
、Array()
、String()
、Number()
、Boolean()
、Function()
這一系列的構造函數。看來不畫個圖是真的理不順了���下面我們來抽絲剝繭。
先考察空對象someThing
,哪怕它是以對象字面量的方式創建的,它也是從構造函數Object()
構造出來的。這時,JavaScript 會自動創建一個原型對象,我們稱這個原型對象為Object.prototype
,構造函數Object()
的prototype
屬性指向這個對象,對象someThing
的__proto__
屬性也指向這個對象。也就是說,構造函數Object()
的prototype
屬性和對象someThing
的__proto__
屬性指向的是同一個原型對象。而且,這個原型對象中有一個constructor
屬性,它又指回了構造函數Object()
,這樣形成了一個環形的連接。如下圖:
要注意的是,這個圖中所顯示的關系是對象剛創建出來的時候的情況,這些屬性的指向都是可以隨意修改的,改了就不是這個樣子了。下面在 JavaScript 控制台中驗證一下上圖中的關系:
請注意,構造函數Object()
的prototype
屬性和__proto__
屬性是不同的,只有函數對象才同時具有這兩個屬性,普通對象只有__proto__
屬性,而且這個__proto__
屬性是隱藏屬性,不是每個浏覽器都允許訪問的,比如 IE 浏覽器。下面,我們來看看 IE 浏覽器的開發者工具:
這是一個反面教材,它既不支持console.dir()
來查看對象,也不允許訪問__proto__
內部屬性。所以,在後面我講到繼承時,需要使用特殊的技巧來避免在我們的代碼中使用__proto__
內部屬性。上面的例子和示意圖中,都只說構造函數Object()
的prototype
屬性指向原型對象,沒有說構造函數Object()
的__proto__
屬性指向哪裡,那麼它究竟指向哪裡呢?這裡先留一點懸念。
下一步,我們自己創建一個構造函數,然後使用這個構造函數創建一個對象,看看它們之間原型的關系,代碼是這樣的:
functionPerson(name, age, gender){
this.name = name;
this.age = age;
this.gender = gender;
}
Person.prototype.sayHello = function(){ return "Hello, my name is " + this.name; };
var somebody = new Person("youxia", 30, "male");
輸入到 Chromium 的 JavaScript 控制台中,然後使用console.dir()
分別查看構造函數Person()
和對象somebody
,如下兩圖:
用圖片來表示它們之間的關系,應該是這樣的:
我使用藍色表示構造函數,黃色表示對象,如果是 JavaScript 自帶的構造函數和 prototype 對象,則顏色深一些。從上圖中可以看出,構造函數Person()
有一個prototype
屬性和一個__proto__
屬性,__proto__
屬性的指向依然留懸念,prototype
屬性指向Person.prototype
對象,這是系統在我們定義構造函數Person()
的時候,自動創建的一個和構造函數Person()
相關聯的原型對象,請注意,這個原型對象是和構造函數Person()
相關聯的原型對象,而不是構造函數Person()
的原型對象。當我們使用構造函數Person()
創建對象somebody
時,somebody
的原型就是這個系統自動創建的原型對象Person.prototype
,就是說對象somebody
的__proto__
屬性指向原型對象Person.prototype
。而這個原型對象中有一個constructor
屬性,又指回構造函數Person()
,形成一個環。這和空對象和構造函數Object()
是一樣的。而且原型對象Person.prototype
的__proto__
屬性指向Object.prototype
。如果在這個圖中把空對象和構造函數Object()
加進去的話,看起來是這樣的:
有點復雜了,是嗎?不過這還不算最復雜的,想想看,如果把JavaScript 內置的Object()
、Array()
、String()
、Number()
、Boolean()
、Function()
這一系列的構造函數以及與它們相關聯的原型對象都加進去,會是什麼情況?每一個構造函數都有一個和它相關聯的原型對象,Object()
有Object.prototype
,Array()
有Array.prototype
,依此類推。其中最特殊的是Function()
和Function.prototype
,因為所有的函數和構造函數都是對象,所以所有的函數和構造函數都有構造函數,而這個構造函數就是Function()
。也就是說,所有的函數和構造函數都是由Function()
生成,包括Function()
本身。所以,所有的構造函數的__proto__
屬性都應該指向Function.prototype
,前面留的懸念終於有答案了。如果只考慮構造函數Person()
、Object()
和Function()
及其關聯的原型對象,在不解決懸念的情況下,圖形是這樣的:
可以看到,每一個構造函數和它關聯的原型對象構成一個環,而且每一個構造函數的__proto__
屬性無所指。通過前面的分析我們知道,每一個函數和構造函數的__proto__
屬性應該都指向Function.prototype
。我用紅線標出這個關系,結果應該如下圖:
如果我們畫出前面提到過的所有構造函數、對象、原型對象的全家福,會是個什麼樣子呢?請看下圖:
暈菜了沒?歡迎指出錯誤。把圖一畫,就發現其實 JavaScript 中的原型鏈沒有那麼復雜,有幾個內置構造函數就有幾個配套的原型對象而已。我這裡只畫了六個內置構造函數和一個自定義構造函數,還有幾個內置構造函數沒有畫,比如Date()
、Math()
、Error()
、RegExp()
,但是這不影響我們理解。寫到這裡,是不是應該介紹一下我使用的畫圖工具了?
在我的 Linux 系列中,有一篇介紹畫圖工具的文章,不過我這次使用的工具是另辟蹊徑的 Graphviz,據說這是一個由貝爾實驗室的幾個牛人開發和使用的畫流程圖的工具,它使用一種腳本語言定義圖形元素,然後自動進行布局和生成圖片。首先,在 Ubuntu 中安裝 Graphiz 非常簡單,一個命令的事兒:
sudo apt-get install graphviz
然後,創建一個文本文件,我這裡把它命名為sample.gv
,其內容如下:
digraph GraphvizDemo{
Alone_Node;
Node1 -> Node2 -> Node3;
}
這是一個最簡單的圖形定義文件了,在 Graphviz 中圖形僅僅由三個元素組成,它們分別是:1、Graph,代表整個圖形,上面源代碼中的digraph GraphvizDemo{}
就定義了一個 Graph,我們還可以定義 SubGraph,代表子圖形,可以用 SubGraph 將圖形中的元素分組;2、Node,代表圖形中的一個節點,可以看到 Node 的定義非常簡單,上面源碼中的Alone_Node;
就是定義了一個節點;3、Edge,代表連接 Node 的邊,上面源碼中的Node1 -> Node2 -> Node3;
就是定義了三個節點和兩條邊,可以先定義節點再定義邊,也可以直接在定義邊的同時定義節點。然後,調用 Graphviz 中的dot
命令,就可以生成圖形了:
dot -Tpng sample.gv > sample.png
生成的圖形如下:
上面的圖形中都是用的默認屬性,所以看起來效果不咋地。我們可以為其中的元素定義屬性,包括定義節點的形狀、邊的形狀、節點之間的距離、字體的大小和顏色等等。比如下面是一個稍微復雜點的例子:
digraph GraphvizDemo{
nodesep=0.5;
ranksep=0.5;
node [shape="record",,color="black",fillcolor="#f4a582",fontname="consolas",fontsize=15];
edge [,color="#053061"];
root [label="<l>left|<r>right"];
left [label="<l>left|<r>right"];
right [label="<l>left|<r>right"];
leaf1 [label="<l>left|<r>right"];
leaf2 [label="<l>left|<r>right"];
leaf3 [label="<l>left|<r>right"];
leaf4 [label="<l>left|<r>right"];
root:l:s -> left:n;
root:r:s -> right:n;
left:l:s -> leaf1:n;
left:r:s -> leaf2:n;
right:l:s -> leaf3:n;
right:r:s -> leaf4:n;
}
在這個例子中,我們使用了nodesep=0.5;
和ranksep=0.5
設置了 Graph 的全局屬性,使用了node [shape=...];
和[edge [style=...];
這樣的語句設置了 Node 和 Edge 的全局屬性,並且在每一個 Node 和 Edge 後面分別設置了它們自己的屬性。在這些屬性中,比較特別的是 Node 的shape
屬性,我將它設置為record
,這樣就可以很方便地利用 Node 的label
屬性來繪制出類似表格的效果了。同時,在定義 Edge 的時候還可以指定箭頭的起始點。
執行dot
命令,可以得到這樣的圖形:
是不是漂亮了很多?雖然以上工作使用任何文本編輯器都可以完成,但是為了提高工作效率,我當然要祭出我的神器 Eclipse 了。在 Eclipse 中可以定義外部工具,所以我寫一個 shell 腳本,將它定義為一個外部工具,這樣,每次編寫完圖形定義文件,點一下鼠標,就可以自動生成圖片了。使用 Eclipse 還可以解決預覽的問題,只需要編寫一個 html 頁面,該頁面中只包含生成的圖片,就可以利用 Eclipse 自帶的 Web 浏覽器預覽圖片了。這樣,每次改動圖形定義文件後,只需要點一下鼠標生成圖片,再點一下鼠標刷新浏覽器就可以實時預覽圖片了。雖然不是所見即所得,但是工作效率已經很高了。請看動畫:
Graphviz 中可以設置的屬性很多,具體內容可以查看 Graphviz官網 上的文檔。
更多詳情見請繼續閱讀下一頁的精彩內容: http://www.linuxidc.com/Linux/2016-09/135356p2.htm