坐臥不安的寫下這篇文章。用JavaScript實現繼承模型,已是很是成熟的技術,各類大牛也已經寫過各式的經驗總結和最佳實踐。在這裏,我只能就我所能,寫下我本身的思考和總結。java
在閱讀以前,咱們先假設幾個在面向對象編程中的概念是你們熟悉的:git
因爲講解這些概念是十分複雜的,因此還請參閱其餘資料。github
面向對象是當代編程的主流思想。不管是C++仍是Java,都是面向對象的。嚴格上來說,JavaScript並非面向對象的,而是「基於對象的」(Object-based),由於它的確缺少面向對象裏的不少特性,例如:編程
但再另外一方面,JavaScript是基於原型(Prototype)的對象系統。它的繼承體系,叫作原型鏈繼承。不一樣於繼承樹形式的經典對象系統,基於原型的對象系統中,對象的屬性和方法是從一個對象原型(或模板)上拷貝或代理(Delegation)的。JavaScript也不是惟一使用這種繼承方法的編程語言,其餘的例子如:瀏覽器
那麼,prototype
在哪裏呢?app
// 訪問Array的原型 Array.prototype
// 訪問自定義函數Foo的原型 var Foo = function() {} Foo.prototype
__proto__
不是標準屬性,可是被大多數瀏覽器支持框架
var a = {} a.__proto__;
使用ES5的Object.getPrototypeOf
:編程語言
Object.getPrototypeOf([]) === Array.prototype;
再來點繞彎的:
[].constructor.prototype === Array.prototype
new
關鍵字大多數面嚮對象語言,都有new
關鍵字。他們大多和一個構造函數一塊兒使用,可以實例化一個類。JavaScript的new
關鍵字是殊途同歸的。
等等,不是說JavaScript不支持經典繼承麼!的確,其實new
的含義,在JavaScript中,嚴格意義上是有區別的。
當咱們,執行
new F()
其實是獲得了一個從F.prototype
繼承而來的一個對象。這個說法來自Douglas的很早以前的一篇文章1。在現在,若是要理解原型繼承中new
的意義,仍是這樣理解最好。
若是咱們要描述new
的工做流程,一個接近的可能流程以下:
constructor
和F.prototype
上的各式方法、屬性。注意,這裏執行的並非拷貝,而是代理。後文會講解這點。this
指向這個對象),並執行構造函數咱們來定義一個簡單的「類」和它的原型:
var Foo = function() {}; Foo.prototype.bar = function() { console.log("haha"); }; Foo.prototype.foo = function() { console.log("foo"); };
咱們在原型上定義了一個bar
方法。看看咱們怎麼使用它:
var foo = new Foo(); foo.bar(); // => "haha" foo.foo(); // => "foo"
咱們要繼承Foo
:
var SuperFoo = function() { Foo.apply(this, arguments); }; SuperFoo.prototype = new Foo(); SuperFoo.prototype.bar = function() { console.log("haha, haha"); }; var superFoo = new SuperFoo(); superFoo.foo(); // => "foo" superFoo.bar(); // => "haha, haha"
注意到幾個要點:
SuperFoo
中,咱們執行了父級構造函數SuperFoo
中,咱們讓然能夠調用foo
方法,即便SuperFoo
上沒有定義這個方法。這是繼承的一種表現:咱們能夠訪問父類的方法SuperFoo
中,咱們從新定義了bar
方法,實現了方法的重載咱們仔細想一想第二點和第三點。咱們新指定的bar
方法到底保存到哪裏了?foo
方法是如何找到的?
要回答上面的問題,必需要介紹原型鏈這個模型。相比樹狀結構的經典類型系統,原型繼承採起了另外一種線性模型。
當咱們要在對象上查找一個屬性或方法時:
prototype
對象上查找,若是沒有找到進行下一步prototype
對象做爲當前對象;若是當前對象存在prototype
,就能繼續,不然不存在則查找失敗,退出;在該對象上查找,若是沒有找到,將前面提到的「當前對象」做爲起始對象,重複步驟3這樣的遞歸查找終究是有終點的,由於:
Object.prototype.__proto__ === null
也就是Object構造函數上,prototype
這個對象的構造函數上已經沒有prototype
了。
咱們來看以前Foo
和SuperFoo
的例子,咱們抽象出成員查找的流程以下:
superFoo自己 => SuperFoo.prototype => Foo.prototype => Object.prototype
解讀原型鏈的查找流程:
superFoo自己
意味着superFoo
這個實例有除了可以從原型上獲取屬性和方法,自己也有存儲屬性、方法的能力。咱們稱其爲own property
,咱們也有很多相關的方法來操做:SuperFoo.prototype
:SuperFoo.prototype = new Foo();
,也就是說SuperFoo.prototoye
就是這個新建立的這個Foo類型的對象Foo.prototype
上的方法和屬性了Foo.prototype
:SuperFoo.prototype
的值,回想上一條Object.prototype
Object.prototype
這個對象的原型是null
,咱們沒法繼續查找 toString
那麼,當在SuperFoo
上添加bar
方法呢?這時,JavaScript引擎會在SuperFoo.prototype
的本地添加bar
這個方法。當你再次查找bar
方法時,按照咱們以前說明的流程,會優先找到這個新添加的方法,而不會找到再原型鏈更後面的Foo.prototype.bar
。
也就是說,咱們既沒有刪掉或改寫原來的bar
方法,也沒有引入特殊的查找邏輯。
基本到這裏,繼承的大部分原理和行爲都已經介紹完畢了。可是如何將這些看似簡陋的東西封裝成最簡單的、可重複使用的工具呢?本文的後半部分將一步一步來介紹如何編寫一個大致可用的對象系統。
準備幾個小技巧,以便咱們在後面使用。
若是要以一個對象做爲原型,建立一個新對象:
function beget(o) { function F() {} F.prototype = o; return new F(); } var foo = beget({bar:"bar"}); foo.bar === "bar"; //true
理解這些應該困難。咱們構造了一個臨時構造函數,讓它的prototype
指向咱們所指望的原型,而後返回這個構造函數所建立的實例。有一些細節:
A.prototype = B.prototype
這樣的事情,由於你對子類的修改,有可能直接影響到父類以及父類的全部實例。大多數狀況下這不是你想看到的結果F
的實例,建立了一個本地對象
,能夠持有(own)自身的屬性和方法,即可以支持以後的任意修改。回憶一下superFoo.bar
方法。若是你使用的JavaScript引擎支持Object.create
,那麼一樣的事情就更簡單:
Object.create({bar:"bar"});
要注意Object.create
的區別:
Object.create(null)
Object.create
的文檔2 JavaScript的函數能夠在運行時很方便的獲取其字符串表達:
var f = function(a) {console.log("a")}; f.toString(); // 'function(a) {console.log("a")};'
這樣的能力其實時很強大的,你去問問Java和C++工程師該如何作到這點吧。
這意味着,咱們能夠去分析函數的字符串表達來作到:
this
JavaScript中的this
是在運行時綁定的,咱們每每須要用到這個特性,例如:
var A = function() {}; A.methodA = function() { console.log(this === A); }; A.methodA();// => true
以上這段代碼有以下細節:
A.methodA()
運行時,其上下文對象指定的是A
,因此this
指向了A
this
引用到類(構造函數)自己單純實現一個extend
方法:
var extend = function(Base) { var Class = function() { Base.apply(this, arguments); }, F; if(Object.create) { Class.prototype = Object.create(Base.prototype); } else { F = function() {}; F.prototype = Base.prototype; Class.prototype = new F(); } Class.prototype.constructor = Class; return Class; }; var Foo = function(name) { this.name = name; }; Foo.prototype.bar = function() { return "bar"; }; var SuperFoo = extend(Foo); var superFoo = new SuperFoo("super"); console.log(superFoo.name);// => "super" console.log(superFoo.bar());// => "bar"
因爲過於簡單,我就不作講解了。
XObject
extend
,可是會有修改var extend = function(Base) { var Class = function() { Base.apply(this, arguments); }, F; if(Object.create) { Class.prototype = Object.create(Base.prototype); } else { F = function() {}; F.prototype = Base.prototype; Class.prototype = new F(); } Class.prototype.constructor = Class; return Class; }; var merge = function(target, source) { var k; for(k in source) { if(source.hasOwnProperty(k)) { target[k] = source[k]; } } return target; }; // Base Contstructor var XObject = function() {}; XObject.extend = function(props) { var Class = extend(this); if(props) { merge(Class.prototype, props); } // copy `extend` // should not use code like this; will throw at ES6 // Class.extend = arguments.callee; Class.extend = XObject.extend; return Class; }; var Foo = XObject.extend({ bar: function() { return "bar"; }, name: "foo" }); var SuperFoo = Foo.extend({ name: "superfoo", bar: function() { return "super bar"; } }); var foo = new Foo(); console.log(foo.bar()); // => "bar" console.log(foo.name); // => "foo" var superFoo = new SuperFoo(); console.log(superFoo.name); // => "superfoo" console.log(superFoo.bar()); // => "super bar"
上面的例子中,
XObject
是咱們對象系統的根類XObject.extend
能夠接受一個包含屬性和方法的對象來定義子類XObject
的全部子類,都沒有定義構造函數邏輯的機會!真是難以接受的:init
方法來初始化對象,而將構造函數自己最簡化super
屬性、mixin
特性等咱們解決了一部分問題,又發現了一些新問題。但本文的主要內容在這裏就結束了。一個更具實際意義的對象系統,實際隨處可見,Ember
和Angular
中的根類。他們都有更強大的功能,例如:
可是,這些框架中對象系統的出發點都在本文所闡述的內容之中。若是做爲教學,John Resig在2008年的一篇博客中3,總結了一個現代JavaScript框架中的對象系統的雛形。我建立了docco代碼註解來當即這段代碼,本文也會結束在這段代碼的註解。
還有一些更高級的話題和技巧,會在另一篇文章中給出。