Javascript語言是符合面向對象思想的。通常來講,面向對象思想須要知足如下三個基本要求:javascript
這裏的關鍵問題就是Javascript的原型繼承機制究竟是個啥玩意?java
有很大的可能性,Javascript在設計之初根本就沒有考慮那麼複雜。啥玩意麪向對象思想,跟我有半毛錢關係啊。公司就給我兩週時間,連設計帶編碼,我固然是怎麼簡單怎麼來了。若是說一門計算機語言必定要知足惟一的一個最最基本的設計思想,那必定不是面向對象,而應該是更簡單的:語言應支持功能的複用。若是一個對象功能不夠用了,那就再叫個幫手唄,因而Javascript硬性規定:任何對象都必須有一個原型對象。這下好了,全部的Javascript對象都是自帶祕書的,本身搞不定的就交給祕書去搞,祕書再搞不定的,就交給祕書的祕書去搞,這樣一路交接過去,直到完全搞不定了,那就只好報錯。這就是Javascript中的原型鏈檢索機制,是否是超簡單?瀏覽器
由於對象的原型是Javascript對象的基本構件,那麼首要的問題就是如何獲得對象的原型對象?大致來講有如下兩種方法:app
說了這麼多,來一段測試代碼吧:函數
test("獲得對象的原型", function() { var empty = {}; assert(empty.__proto__ === Object.getPrototypeOf(empty), "__proto__和Object.getPrototypeOf的等效性"); });
這個測試是說那兩種獲得對象原型對象的方法是一致的,爲了簡單,如下采起第一種方式。學習
再測試一下對象的原型鏈:測試
test("對象的原型鏈", function() { var p = {}; var pp = p.__proto__; var ppp = pp.__proto__; var q = new Object(); var qp = q.__proto__; var qpp = qp.__proto__; assert(pp === qp, "Object.__proto__"); assert(ppp === qpp && ppp === null, "Object.__proto__.__proto__"); });
這段測試須要咱們注意如下幾點:this
既然函數也是對象,那麼咱們來測試一下函數的原型鏈:編碼
test("函數的原型鏈", function() { function p() {}; var pp = p.__proto__; var ppp = pp.__proto__; function q() {}; var qp = q.__proto__; var qpp = qp.__proto__; assert(pp === qp, "Function.__proto__"); assert(ppp === qpp, "Function.__proto__.__proto__"); assert(ppp === {}.__proto__, "Function.__proto__.__proto__ === Object.__proto__"); });
這段測試須要咱們注意如下幾點:spa
總有一些對象是相似的,例如每一個person對象中都有name和age屬性,咱們能夠建立不少的相似的person1,person2等等,咱們但願可以把這種相似的對象的建立過程複用起來,直接封裝爲一個函數無疑是最簡單直接的:
test("構造函數的引入", function() { function Person(name, age) { this.name = name; this.age = age; this.say = function() { return this.name + " " + this.age; }; } var person1 = {}; Person.call(person1, "wgs", 18); assert(person1.name === "wgs" && person1.age === 18 && person1.say() === "wgs 18", "person1"); var person2 = {}; Person.call(person2, "www", 28); assert(person2.name === "www" && person2.age === 28 && person2.say() === "www 28", "person2"); });
利用函數的知識能夠很容易的解決這個問題。相似的咱們還能夠搞出Animal函數,Student函數等等。可是這裏的問題是這樣建立的任何對象都是Object類型的,咱們的需求是最好Person函數建立的對象是Person類型的,Animal函數建立的對象是Animal類型的,Student函數建立的對象是Student類型的等等。這就要求每一個函數對象必須有個函數描述對象。因而Javascript硬性規定:任何函數都必須有一個描述對象。這下就熱鬧了,當你你定義一個函數的時候,你覺得你只搞出了一個函數對象,實際上倒是兩個對象,一個函數對象,一個函數描述對象。後者是強制附送的,這情形就比如是說原本公司只打算招收了10位員工的,沒想到國家法律規定,必須夫妻倆一塊兒招,因而因而一會兒來了20位員工。這裏的好消息是咱們不用本身手動建立函數的描述對象,每當Javascript引擎解析完一個函數後,它會在建立函數對象的同時,自動建立好函數的描述對象。
那麼最直接的問題來了,怎麼獲得函數的描述對象?Javascript是這樣規定的:
不少翻譯把函數的描述對象翻譯爲原型對象,對象的原型對象,函數的原型對象,又一個函數的原型對象,這要把人繞暈嗎?我建議就直接用__proto__對象和prototype對象區分一下吧,二者根本不是一回事,儘管有時候,二者能夠是同一個對象。
全部的prototype對象也是有__proto__對象的,很簡單,那就是前面反覆提到過的Object.__proto__對象。
有人注意到一個有趣的現象沒有?Javascript這麼搞其實是把函數和類型合二爲一了,有一個函數就有一個類型,有一個類型就有一個函數。這下子玩得有點大啊,想一想在通常面嚮對象語言中,咱們定義一個類(在Javascript中其實它也是一個函數),咱們爲這個類定義幾個方法(在Javascript中其實它們也是幾個類型)。Javascript的內存使用率和運行效率確定不如C++,C#,Java這些語言,可是毫無疑問,Javascript的靈活性,那是至關的高。
因而咱們熟知的Javascript中的那幾個內置對象類型:Object、Array,Function,Regexp,Date,Error其實都是函數。那幾個包裝函數Number,String,Boolean其實也能夠看作是內置對象類型。這樣咱們就很容易理解Object.__proto__其實和Object函數是一對,Function.__proto__其實和Function函數是一對。
到如今爲止,咱們就沒有必要再糾結Object.__proto__,Function.__proto__這種奇怪的命名了,其實它們就是Object.prototype和Function.prototype
綜上所述,咱們獲得了下面的圖:
這就是那張號稱把無數Javascript初學者繞暈的經典圖。如今看起來是否是很清晰了呢?
再補點簡單的測試吧:
test("函數的prototype", function() { function Person() {}; assert(Person.prototype.constructor === Person, "Person.prototype.constructor === Person"); assert(Person.prototype.__proto__ === {}.__proto__, "Person.prototype.__proto__ === Object.__proto__"); });
還回到引入函數prototype對象的最初問題上來,如何實現Person函數建立的對象是Person類型的,Animal函數建立的對象是Animal類型的,Student函數建立的對象是Student類型的等等?道理上基本都懂了,如今欠缺只是一點編碼實踐,因而咱們來一個new關鍵字的模擬:
test("模擬構造函數", function() { function _new(Fun) { // 建立一個新對象 var this_ = {}; // 除了類型參數之外,咱們須要打包構造函數的參數 var args = Array.prototype.slice.call(arguments, 1); // 調用構造函數 Fun.apply(this_, args); // 將函數的prototype賦值給對象對的__proto__ this_.__proto__ = Fun.prototype; // 返回建立的新對象 return this_; } function Person(name, age) { this.name = name; this.age = age; } var person1 = new Person("wgs", 18); var person2 = _new(Person, "wgs", 18); assert(JSON.stringify(person1.__proto__) === "{}", "person1.__proto__ is {}"); assert(person1.__proto__.constructor === Person, "person1.__proto__.constructor is Person"); assert(person1.constructor === Person, "person1.constructor is Person"); assert(person1.name === "wgs" && person1.age === 18, "person1 name and age"); assert(JSON.stringify(person2.__proto__) === "{}", "person2.__proto__ is {}"); assert(person2.__proto__.constructor === Person, "person2.__proto__.constructor is Person"); assert(person2.constructor === Person, "person2.constructor is Person"); assert(person2.name === "wgs" && person2.age === 18, "person2 name and age"); });
new和_new只是調用語法上稍微有些不一樣,但結果實際上是如出一轍的。這下對構造函數的調用原理也不用糾結了吧,源碼在那裏,一共也沒有幾行,還不清楚的回去再多看幾遍。
再回頭看看那張經典的圖,在不考慮繼承的狀況下,咱們對類型的擴充其實就在prototype對象上
這裏咱們須要把全部內置對象類型的prototype顯示出來,以驗證咱們的想法:
<script type="text/javascript"> asserts(); function testCase(desc, proto) { // 獲得prototype的全部屬性名稱 var names = Object.getOwnPropertyNames(proto); for (var i = 0; i < names.length; i++) { var name = names[i]; // 由於Function的arguments和caller不可訪問, // 因此須要過濾掉這兩個屬性 if (name == "arguments" || name == "caller") { continue; } // 根據名稱獲得屬性的值 var value = proto[name]; // 顯示屬性及其內容 assert(true, "{0} : {1}".fmt(name, _varDesc(value))); } log(desc, proto); } test("Object.prototype", function() { testCase("Object.prototype:\n", Object.prototype); }); test("Array.prototype", function() { testCase("Array.prototype:\n", Array.prototype); }); test("Function.prototype", function() { testCase("Function.prototype:\n", Function.prototype); }); test("RegExp.prototype", function() { testCase("RegExp.prototype:\n", RegExp.prototype); }); test("Date.prototype", function() { testCase("Date.prototype:\n", Date.prototype); }); test("Error.prototype", function() { testCase("Error.prototype:\n", Error.prototype); }); test("Number.prototype", function() { testCase("Number.prototype:\n", Number.prototype); }); test("String.prototype", function() { testCase("String.prototype:\n", String.prototype); }); test("Boolean.prototype", function() { testCase("Boolean.prototype:\n", Boolean.prototype); }); </script>
代碼沒有多複雜,結果以下:
。。。
結果太多,就截一部分圖吧,看看這些熟悉的方法,是否是有種點進去看看每一個函數源碼的衝動?
關於Javascript的面向對象,這裏對繼承和多態沒有涉及,那將是另外一篇文檔的事情。不過若是看懂了本篇文章的內容,我保證,那些繼承和多態的東西還真沒什麼難度。