1.概念javascript
JavaScript並不提供一個class的實現,在ES6中提供class關鍵字,可是這個只是一個語法糖,JavaScript仍然是基於原型的。JavaScript只有一種結構:對象。每一個對象都有一個私有屬性:_proto_,這個屬性指向它構造函數的原型對象(Prototype)。它的原型對象也有一個屬於本身的原型對象,這樣層層向上只至這個原型對象的屬性爲null。根據定義null沒有本身的原型對象,它是這個原型鏈中的最後一個環節。css
幾乎全部的JavaScript中的對象都是位於原型鏈頂端的Object的實例。html
2.基於原型鏈的繼承java
JavaScript對象是動態的屬性「包」(指其本身的屬性)。JavaScript對象有一個指向原型對象的鏈。當訪問一個對象的屬性時,它不只僅在對象上搜尋,還會試圖搜尋對象的原型,以及該對象原型的原型,依次層層向上搜索,直至找到一個名字匹配的屬性或者到達原型鏈的頂端爲止。es6
在ECMA標準中,someObject.[[Prototype]]符號是表示指向someObject的原型。從ES6開始,[[Prototype]]能夠經過Object.getPrototypeOf()和Object.setPrototype()訪問器來訪問。這個是JavaScript的非標準api,可是不少瀏覽器都實現了__proto__,兩者做用等同。注意瀏覽器沒有實現對象的object.Prototype這樣的屬性,即沒有實現對象實例的Prototype屬性,只有構造函數.prototype屬性。ajax
可是[[Prototype]]和構造函數func的prototype屬性不一樣,不要弄混。構造函數建立的實例對象的[[prototype]]指向func的prototype屬性。Object.prototype屬性表示Object的原型對象。api
這裏咱們舉一個例子,假設咱們有一個對象o,它有本身的屬性a, b,o 的原型 o.__proto__有屬性 b 和 c, 最後, o.__proto__.__proto__ 是 null,JavaScript代碼以下:
數組
var o = {a: 1, b: 2}; o.__proto__ = {b: 3, c: 4}; console.log(Object.getPrototypeOf(o)); console.log(o.__proto__); console.log(Object.getPrototypeOf(Object.getPrototypeOf(o))); console.log(o.__proto__.__proto__); console.log(Object.getPrototypeOf(Object.getPrototypeOf(Object.getPrototypeOf(o)))); console.log(o.__proto__.__proto__.__proto__);
輸出結果以下:瀏覽器
第一句:定義一個對象o,對象有屬性a,b緩存
第二句:設置對象o的原型爲一個新的對象{b: 3, c: 4}
第三句:使用ES6方法Object.getPrototypeOf獲取對象o的原型,輸出{b: 3, c: 4}
第四句:使用瀏覽器實現的原型屬性__proto__獲取對象o的原型,輸出{b: 3, c: 4}
第五句:使用ES6的方法Object.getPrototypeOf獲取對象o的原型的原型,是原型鏈頂端Object的實例
第六句:使用瀏覽器實現的原型屬性__proto__獲取對象o的原型的原型,是原型鏈頂端Object的實例
第七句:使用ES6的方法Object.getPrototypeOf獲取對象o的原型的原型的原型,是null
第八句:使用瀏覽器實現的原型屬性__proto__獲取對象o的原型的原型的原型,null
3.繼承方法
JavaScript沒有其餘基於類的語言中定義的「方法」。在JavaScript裏,任何函數均可以添加到對象上做爲對象的屬性。函數的繼承與其餘的屬性繼承沒有任何區別,包括「屬性遮蔽」(這至關於其餘語言的方法重寫)。
當繼承的函數被調用時,this指向的當前繼承的對象,而不是繼承的函數所在的原型對象。看下面的例子:
var o = { a: 2, m: function () { return this.a + 1; } }; // 當調用o.m()的時候,this指向了o console.log(o.m()); // 建立一個對象p,p.__proto__是o,p是一個繼承自o的對象 var p = Object.create(o); // 下面兩句和上面的效果同樣 // var p = {}; // p.__proto__ = o; // 建立p自身的屬性a p.a = 4; // 調用p.m()函數時this指向了p,p繼承o的m函數此時this.a,即p.a指向p自身的屬性a,最後獲得5 console.log(p.m());
上面代碼中,調用p對象的m()方法時,m()方法中this.a指向p對象的a屬性,而不是它的父對象o的屬性a,有點相似英語語法中的「就近原則」,即先從自身屬性開始找,而不是它的原型對象。
4.__proto__和prototype的關係
上面提到「JavaScript中只有一種結構,就是對象」,在JavaScript任何數據結構歸根結底都是對象類型,他們都有對象的共同特色,即都有私有屬性__proto__,基本上全部的瀏覽器都實現了這個屬性,可是不建議在代碼中使用這個屬性,因此它使用了一個比較怪異的名字__proto__,表示只能在內部使用,也叫隱式屬性,意思是一個隱藏起來的屬性。__proto__屬性指向當前對象的構造函數的原型,它保證了對象實例可以訪問在構造函數原型中定義的全部屬性和方法。
JavaScript中的方法除了和其餘對象同樣有隱式屬性__proto__以外,還有本身特有的屬性prototype,這個屬性是一個指針,prototype指向原型對象,這個對象包含全部實例共享的屬性和方法,咱們把prototype屬性叫作原型屬性。prototype指向的原型對象又有一個屬性constructor,這個屬性也是一個指針,指回原構造函數,即這個方法。
下面咱們來看一張圖:
1.構造函數Foo()的原型屬性Foo.prototype指向了原型對象,在原型對象中有共有的方法,構造函數聲明的實例f1,f2都共享這個方法。
2.原型對象Foo.prototype保存着實例的共享的方法,它又有一個指針constructor,指回到構造函數,即函數Foo()。
3.f1,f2是Foo這個構造函數的兩個實例,這兩個對象的屬性__proto__,指向構造函數的原型對象,這樣就能夠訪問原型對象的全部方法。
4.構造函數Foo()是方法,也是對象,它的__proto__屬性指向它的構造函數的原型對象Function.prototype,這個對象中有共有屬性call(),bind()等。
5.Foo()的原型對象Function.prototype是對象,它的__proto__屬性指向它的構造函數的原型對象,即Object.prototype,這個對象中共有共有屬性length,is()等。
6.Function.prototype的prototype屬性指向原型對象function Function(),該原型對象的constructor屬性指向Function.prototype自己。
7.Function.prototype的__proto__屬性指向它構造函數的原型對象Object.prototype。
8.function object()的__proto__屬性指向構造函數的原型對象Function.prototype,這個對象包含object實例共享的屬性和方法。
9.function ojbect()的prototype屬性指向原型對象Object.prototype。
9.最後Object.prototype對象的__proto__指向null。
10.對象有屬性__proto__,指向該對象的構造函數的原型對象。
11.方法除了有屬性__proto__,還有屬性prototype,指向該方法的原型對象。
5. 使用不一樣的方法來建立對象和生成原型鏈
5.1 語法結構建立的對象
var o = { a: 1 };這是一個定義對象的語法,這個語句使對象o繼承了Object.prototype上全部的屬性,o自己沒有名爲hasOwenProperty的屬性,hasOwnProperty是Object.property的屬性,所以對象o繼承了Object.prototype的hasOwnProperty屬性方法。Object.property的原型爲null,原型鏈以下:o -> Object.prototype -> null,截圖以下:
var a = ["yo", "whadup", "?"]; 這是一個定義數組的語法,數組都繼承於Array.prototype,Array.prototype中包含indexOf,forEach等方法,原型鏈以下:a -> Array.prototype -> Object.prototype -> null,截圖以下:
function f() = { return 2; } 這是一個定義函數的語法,函數都繼承於Function.prototype,Function.prototype中包含call,bind等方法,原型鏈以下:f -> Function.prototype -> Object.prototype -> null,使用console.log方法輸出f,console.log(f)只能把函數的內容輸出,並不能看到函數的原型,函數的原型的原型,只能看到這個方法體,目前本人尚未搞清楚這個問題。截圖以下:
5.2 使用構造器建立的對象
在JavaScript中,構造器(構造方法)其實就是一個普通的函數。當使用new操做符來做用這個函數時,它就能夠被稱爲成爲構造方法或者構造函數。看下面的代碼:
function Graph() { this.vertices = [] this.edges = [] } Graph.prototype = { addVertice: function (v) { this.vertices.push(v); } } var g = new Graph(); console.log(g);
輸出以下:
g是使用構造方法new Graph()生成的對象,它有本身的屬性‘vertices’和‘edges’,還有從本身的原型對象中繼承的addVertice方法,在g被實例化時,g.[[Prototype]]指向了Graph.prototype
5.3 Object.create建立的對象
ECMAScript5中引入了一個新的方法:Object.create()。能夠調用這個方法來建立一個新對象。新對象的原型就是調用create方法時傳入的第一個參數。咱們來看下面的例子:輸出結果以下:
var a = {a: 1}; var b = Object.create(a); console.log(b.a); var c = Object.create(b); console.log(c); console.log(c.a); var d = Object.create(null); console.log(d.hasOwnProperty);
輸出結果以下:
第一句:定義對象a,它有屬性a
第二句:使用Object.Create(a)建立對象b,b的原型是a
第三句:輸出b.a,如今對象b上查找屬性a,沒有,而後在b的原型上找,值是1,輸出1
第四句:使用Object.Create(b)建立對象c,c的原型是b
第五句:輸出對象c,它的原型的原型上有一個屬性c,值爲1
第六句:輸出c.a,如今對象c的屬性中查找a,沒有,在c的原型b上查找屬性a,沒有,在b的原型a上查找屬性a,有,值爲1,輸出1
第七句:使用Object.Create(null)建立對象d,注意null沒有原型
第八句:輸出d.hasOwnProperty方法,在d的方法中找,沒有,在d的原型null中找,也沒有,最後輸出undefined
5.4 class關鍵字建立對象
es6引入一套新的關鍵字來實現class。使用基於類的語言對這些結構會很熟悉,但它們是不一樣的。JavaScript是基於原型的。這些新的關鍵字包括class,constructor,static,extends和super。來看下面的例子:
class Polygon { constructor(height, width) { this.width = width; this.height = height; } } class Square extends Polygon { constructor(sideLength) { super(sideLength, sideLength); } get area() { return this.height * this.width; } set sideLength(sideLength) { this.height = sideLength; this.width = sideLength; } } var square = new Square(2); writeStr(square.area);
輸出結果以下:
在原型鏈上查找屬性比較耗時,對性能有反作用,試圖訪問不存在的屬性的時候會遍歷整個原型鏈。遍歷對象的屬性時,原型鏈上每一個可枚舉屬性都會被枚舉出來。要檢查對象是否有一個本身定義的屬性,而不是從原型鏈上繼承的屬性,可使用從Object.prototype上繼承的hasOwnPrototype方法。hasOwnPrototype是JavaScript中處理屬性但不會遍歷原型鏈的方法之一,另外可使用Object.keys()方法。注意這個並不能解決一切問題,沒有這個屬性的時候hasOwnPrototype會返回undefined,可能該屬性存在,可是它的值就是undefined。
常用的一個錯誤作法是擴展Object.prototype或其餘內置原型,這種技術會破壞封裝,儘管一些流行的框架例如Prototype.js在使用該技術,可是仍然沒有足夠好的理由使用附加的非標準方法來混入內置原型。擴展內置原型惟一的理由是支持JavaScript引擎的新特性,例如Array.forEach,固然在es6中這個特性已經存在。
6. JavaScript中的繼承
6.1 先看看如何封裝
上面咱們講到建立對象的方式,有了對象以後就會有封裝,在JavaScript中封裝一個類很容易。經過構造器建立對象時,在構造函數(類)的內部經過對this(函數內部自帶的變量,用於指向當前這個對象)添加屬性或者方法來實現添加屬性或方法。代碼以下:
// 類的封裝 function Book1 (id, bookname, price) { this.id = id; this.bookname = bookname this.price = price } var Book2 = function (id, bookname, price) { this.id = id; this.bookname = bookname this.price = price }
也能夠經過在構造函數類(對象)的原型對象上添加屬性和方法。有兩種方式,一種是爲原型對象賦值,另外一種是將一個對象賦值給類的原型對象。以下:
// 方式一 Book.prototype.display = function () { } // 方式二 Book.prototype = { display: function () { } }
須要訪問類的屬性和方法時不能直接使用Book類,例如Book.name,Book.display(),而要用new關鍵字來建立新的對象,而後經過點語法來訪問。
經過this添加的屬性,方法是在當前函數對象上添加的,JavaScript是一種基於原型prototype的語言,因此每次經過一個構造函數建立對象的時候,這個對象都有一個原型prototype指向其繼承的屬性,方法。因此經過prototype繼承來的屬性和方法不是對象自身的,可是在使用這些屬性和方法的時候須要經過prototype一級一級向上查找。
經過this定義的屬性或方法是該函數對象自身擁有的,每次經過這個函數建立新對象的時候this指向的屬性和方法都會相應的建立,而經過prototype繼承的屬性或者方法是經過prototype訪問到的,每次經過函數建立新對象時這些屬性和方法不會再次建立,也就是說只有單獨的一份。
面向對象概念中「私有屬性」,「私有方法」,「公有屬性」,「公有方法」,「保護方法」在JavaScript中又是怎麼實現的呢?
私有屬性,私有方法:因爲JavaScript函數級做用域,聲明在函數內部的變量和方法在外界是訪問不到的,經過這個特性能夠建立類的私有變量以及私有方法。
公有屬性,公有方法:在函數內部經過this建立的屬性和方法,在類建立對象時,沒有對象自身都擁有一份而且能夠在外部訪問到,所以經過this建立的屬性,方法能夠看作對象公有屬性和對象公有方法。類經過prototype建立的屬性或方法在實例的對象中經過點語法訪問到,因此能夠將prototype對象中的屬性和方法也稱爲類的公有屬性,類的公有方法。
特權方法:經過this建立的方法,不但能夠訪問這些對象的共有屬性,方法,並且能夠訪問到類或者對象自身的私有屬性和私有方法,權利比較大,因此能夠看作是特權方法。
類構造器:在對象建立時能夠經過特權方法實例化對象的一些屬性,所以這些在建立對象時調用的特權方法能夠看作類的構造器。
靜態共有屬性,靜態共有方法:經過new關鍵字和方法名來建立新對象時,因爲函數外面經過點語法(函數名.xxx)添加的屬性和方法沒有執行到,因此新建立的對象中沒法使用它們,可是能夠經過類名來使用。所以在類外面經過點語法來建立的屬性,方法能夠被稱爲類的靜態共有屬性和類的靜態共有方法。
參考下面的代碼:
var Book = function (id, name, price) { // 私有屬性 var num = 1; // 私有方法 function checkId() { }; // 特權方法 this.getName = function () { }; this.getPrice = function () { }; this.setName = function () { }; this.setPrice = function () { }; // 對象公有屬性 this.id = id; // 對象公有方法 this.copy = function () { }; // 構造器 this.setName(name); this.setPrice(price); } // 類靜態公有屬性(對象不能訪問) Book.isChinese = true; // 類靜態公有方法(對象不能訪問) Book.resetTime = function () { console.log('new Time'); }; Book.prototype = { // 公有屬性 isJSBook: false, //公有方法 display: function () { } };
經過new關鍵字建立對象的本質是對新對象的this不斷的賦值,並將prototype指向類的prototype所指向的對象,而在類的構造函數外面經過點語法定義的屬性,方法不會添加在新的對象上。所以要想在新建立的對象上訪問isChinese就得經過Book類而不能經過this,如Book.isChinese,類的原型上定義的屬性在新對象裏能夠直接使用,這是由於新對象的prototype和類(Boo()方法)的prototype指向同一個對象。
類的私有屬性num以及靜態公有屬性isChiese在新建立的對象裏是訪問不到的,而類的公有屬性isJSBook在對象中能夠經過點語法訪問到。看下面實例代碼,注意這段代碼是在上面的實例代碼基礎上寫的:
var b = new Book(11, 'Javascript', 50); console.log(b.num); // undefined console.log(b.isJSBook); // false console.log(b.id); // 11 console.log(b.isChinese); // undefined console.log(Book.isChinese); // true Book.resetTime(); // new Time
第一句,使用new關鍵字建立對象b,對Book函數對象內的this指定的屬性賦值,而且將b的原型指向Book.prototype
第二句,輸出b.num,由於num是類的私有屬性,對象訪問不到,在Book.prototype上也找不到,因此輸出undefined
第三句,輸出b.isJSBook,在構造函數內沒有這個屬性,在Book.prototype上有,因此輸出false
第四句,輸出b.id,在構造函數中有這個屬性,它是共有屬性,值爲11
第五句,輸出b.isChinese,這個是類的靜態屬性,在類的對象上是找不到的,輸出undefined
第六句,輸出Book.isChinese,這個是類的靜態屬性,使用類名直接訪問,輸出true
第七句,調用Book類的resetTime()方法,這個是類的靜態屬性,輸出new time
new關鍵字的做用能夠看作對當前對象的this不停地賦值,若是沒有指定new關鍵字則this默認指向當前全局變量,通常是window。
6.2 子類的原型對象繼承—類式繼承
// 類式繼承 // 申明父類 function SuperClass() { this.superValue = true } //爲父類添加共有方法 SuperClass.prototype.getSuperValue = function () { return this.superValue; } // 申明子類 function SubClass() { this.subValue = false; } // 繼承父類 SubClass.prototype= new SuperClass() // 爲子類添加共有方法 SubClass.prototype.getSubValue = function () { return this.subValue; } let sup = new SuperClass(); let sub = new SubClass(); console.log(sup.getSuperValue()); //true console.log(sup.getSubValue()); //Uncaught TypeError: sup.getSubValue is not a function console.log(sub.getSubValue()); // false console.log(sub.getSuperValue()); // true console.log(sub instanceof SubClass); // true console.log(sub instanceof SuperClass); // true console.log(sup instanceof SubClass); // false console.log(sup instanceof SuperClass); // true console.log(SubClass instanceof SuperClass); // false console.log(SubClass.prototype instanceof SuperClass); // true console.log(SubClass.prototype instanceof SuperClass.prototype); // Uncaught TypeError: Right-hand side of 'instanceof' is not callable console.log(sub.prototype instanceof SuperClass); // false
1. 申明父類(函數)SuperClass()
2. 在SuperClass的原型對象上設置共有方法getSuperValue
3. 申明子類(函數)SubClass()
4. 設置子類SubClass的原型對象是父類SuperClass的一個實例,子類繼承了父類 的屬性和方法,以及父類的原型對象上的屬性和方法。
5. 在子類SubClass的原型對象設置共有方法getSubValue
6. 定義父類對象sup
7. 定義子類對象sub
8. 調用父類對象sup的getSuperValue()方法獲得true
9. 調用父類對象sup的方法getSubValue(),它沒有這個方法,報錯了
10. 調用子類對象sub的getSubValue()方法,在它的內部找不到,在它的原型對象上找,有這個方法,返回this.subValue,返回false
11. 調用子類對象的getSuperValue()方法,在它的內部找不到,原型對象上找不到,繼承的父類的原型對象上有,返回this.superValue,值爲true
12. 子類對象sub是子類SubClass的一個實例
13. 子類對象sub是父類SuperClass的一個實例
14. 父類對象sup不是子類SubClass的一個實例
15. 父類對象sup是父類SuperClass的一個實例
16. 子類SubClass不是父類SuperClass的實例
17. 子類的原型對象SubClass.property是父類SuperClass的一個實例
18. 子類的原型對象SubClass.property不是父類原型對象SuperClass.property的實例,由於父類的原型對象不是一個類,而是一個對象
19. 子類的原型對象sub.property不是父類SuperClass的一個實例,而是指向一個父類對象
類的原型對象用來爲類添加共有方法,可是不能直接添加,訪問這些屬性和方法,必須經過原型prototype來訪問。新建立的對象複製了父類構造函數的屬性和方法,並將原型__proto__指向父類的原型對象,這樣就擁有了父類的原型對象上的屬性和方法,這個新建立的對象能夠直接訪問到父類原型對象上的屬性和方法。
這種繼承方式有2個缺點,其一,子類經過其原型對父類實例化,繼承了父類。若是父類中的共有屬性是引用類型的話,全部子類的實例會公用這個共有屬性,任何一個子類實例修改了父類屬性(引用類型),會直接影響到全部子類和這個父類。看下面代碼:
function SuperClass() { this.books = ['javascript', 'html']; } function SubClass() {} SubClass.prototype = new SuperClass(); var instance1 = new SubClass(); var instance2 = new SubClass(); console.log(instance1.books); //["javascript", "html"] instance2.books.push('java'); console.log(instance1.books); //["javascript", "html", "java"] console.log(instance2.books); //["javascript", "html", "java"] console.log(SuperClass.books);//undefined var sup1 = new SuperClass(); var sup2 = new SuperClass(); sup2.books.push('css'); console.log(sup1.books); // ["javascript", "html"] console.log(sup2.books); // ["javascript", "html", "css"]
1. 申明父類(函數)SuperClass,內部有共有引用屬性books
2. 申明子類(函數)SubClass,函數內部沒有內容
3. 子類的原型對象設置爲父類的一個對象,子類繼承了父類的屬性,方法和父類原型對象上的屬性,方法
4. 定義子類對象instance1,instance2,它們繼承了父類的屬性,方法以及父類原型對象上的屬性,方法
5. 輸出子類instance1的屬性books,在子類對象的內部沒有,在在父類上有這個屬性輸出["javascript", "html"]
6. 在子類對象instance2上找books屬性,它來自繼承的父類內部,而且是一個引用屬性,修改這個屬性,添加一個元素「java」
7. 輸出子類對象instance1的book屬性,她來自繼承的父類內部,已經被修改,輸出["javascript", "html", "java"]
8. 輸出子類對象instance2的book屬性,她來自繼承的父類內部,已經被修改,輸出["javascript", "html", "java"]
9. 在父類函數SuperClass上訪問它內部的屬性books,找不到這個屬性,輸出undefined
10. 定義父類對象sup1,和sup2,他們調用父類函數,初始化共有屬性books
11. 給父類對象sup2的引用屬性books添加一個元素「css」
12. 輸出父類對象sup1的屬性books,輸出["javascript", "html"],這個books屬性和sup2的books屬性是沒有關係的
13. 輸出父類對象sup2的屬性books,輸出["javascript", "html", "css"]
上面例子中instance2修改了父類的books屬性,添加了一個「java」,結果instance1的books屬性也有了個新的元素「java」。注意SubClass.prototype = new SuperClass();這一句中new操做符會複製一份父類的屬性和方法,var sup = new SuperClass();也會複製一份父類的屬性和方法,可是他們是不一樣的,相互後者不會影響。而且只有前者纔會出現這種引用類型被無心修改的狀況,前者是經過設置SubClass的原型對象添加的屬性和方法。
其二,因爲子類實現繼承是靠其原型prototype對父類的實例化實現的,所以在(實例化子類時會建立父類,就是這一句:let sub = new SubClass();)建立父類的時候是沒法向父類傳遞參數的,所以在實例化父類的時候沒法調用父類的構造函數進而對父類構造函數內部的屬性初始化。
6.3 構造函數繼承—call方法建立繼承
// 構造函數繼承 // 申明父類 function SuperClass(id) { // 引用類型共有屬性 this.books = ['javascript', 'html', 'css']; // 值型共有屬性 this.id = id; } // 父類申明原型方法 SuperClass.prototype.showBooks = function () { console.log(this.books); } // 申明子類 function subClass(id) { // 繼承父類 SuperClass.call(this, id); } // 建立兩個實例 var instance1 = new subClass(10); var instance2 = new subClass(11); instance1.books.push('java'); console.log(instance1.books); // ["javascript", "html", "css", "java"] console.log(instance1.id); // 10 console.log(instance2.books); // ["javascript", "html", "css"] console.log(instance2.id); // 11 instance1.showBooks(); // Uncaught TypeError: instance1.showBooks is not a function instance2.showBooks(); // Uncaught TypeError: instance1.showBooks is not a function // 申明父類實例 var instance3 = new SuperClass(12); instance3.showBooks(); // ["javascript", "html", "css"]
1. 申明父類方法SuperClass,方法內部有共有屬性books,id
2. 給父類的原型對象上申明共有方法showBooks()
3. 申明子類方法SubClass(),在子類方法中使用call調用父類SuperClass方法,在當前子類中執行父類方法,給this賦值,這樣子類就繼承了父類內部的方法和屬性(id,books),可是子類不會繼承父類的原型對象中的屬性和方法(showBooks())
4. 申明兩個子類實例instance1,instance2,並分別傳參給父類10,11
5. 修改子類實例instance1的books屬性,這是一個引用屬性,給數組添加一個元素「java」
6. 輸出子類實例instance1的books屬性,「java」已經被添加上去了
7. 輸出子類實例instance1的id屬性是10
8. 輸出子類實例instance2的books屬性,這裏是沒有「java」元素的,由於它是在調用call方法的時候直接複製的一份,和instance1的是兩個徹底不一樣的數組對象
9. 輸出子類實例instance2的books屬性是11
10. 調用子類實例instance1的showBooks()方法,在子類中找不到,子類的原型對象中找不到,在子類繼承的父類中找不到(這裏不會在子類繼承的父類的原型對象中找這個方法),所以報錯
11. 調用子類實例instance2的showBooks()方法,在子類中找不到,子類的原型對象中找不到,在子類繼承的父類中找不到(這裏不會在子類繼承的父類的原型對象中找這個方法)所以報錯
12. 申明父類實例instance3,傳入參數12
13. 調用父類實例instance3的showBooks()方法,在父類內部找不到這個方法,在父類的原型對象中有這個方法,輸出books對象,注意這個對象並無被子類實例instance1修改,全部子類實例都有一份本身單獨的屬性和方法
注意SuperClass.call(this, id);這句是構造函數式繼承的關鍵。call方法能夠改變函數的做用環境,在子類中對SuperClass調用這個方法就是將子類中的變量在父類中執行一遍,因爲父類是給this綁定屬性的,所以子類就繼承了父類的共有屬性。因爲這種類型的繼承沒有涉及原型,因此父類的原型中的方法和屬性不會被子類繼承,要想被子類繼承就必須放在構造函數中,這樣建立的實例會單獨擁有一份父類的屬性和方法,而不是共用,這樣違背了代碼複用的原則。
6.4 組合繼承
組合繼承又叫「僞經典繼承」,是指將原型鏈和構造函數技術組合在一塊兒的一種繼承方式,下面看一個例子:
// 申明父類 function SuperClasss(name) { // 值類型共有屬性 this.name = name; // 引用類型共有屬性 this.books = ['html', 'css', 'Javascript']; } // 父類原型共有方法 SuperClasss.prototype.getName = function () { console.log(this.name); } // 申明子類 function SubClass(name, time) { // 構造函數式繼承父類name屬性 SuperClasss.call(this, name); // 子類的共有屬性 this.time = time; } // 類式繼承,子類原型繼承父類 SubClass.prototype = new SuperClasss(); // 子類原型方法 SubClass.prototype.getTime = function () { console.log(this.time); } var instance1 = new SubClass('js book', 2014); instance1.books.push('java'); console.log(instance1.books); // ['html', 'css', 'Javascript', 'java'] instance1.getName(); // 'js book' instance1.getTime(); // 2014 var instance2 = new SubClass('css book', 2013); console.log(instance2.books); // ['html', 'css', 'Javascript'] instance2.getName(); // 'css book' instance2.getTime(); // 2013
1. 申明父類方法SuperClass(),方法內部有共有屬性
2. 在父類方法的原型對象上定義共有方法getname(),輸出當前屬性name
3. 申明子類方法SubClass(),在子類方法中使用call調用父類方法,在當前子類中執行父類方法給this賦值,這樣子類就繼承了父類內部的方法和屬性(name,books),可是子類不會繼承父類的原型對象中的屬性和方法。子類方法中有共有屬性time
4. 子類的原型對象設置爲父類的一個實例對象,子類繼承了父類的屬性,方法和父類原型對象上的屬性,方法。
5. 在子類的原型對象上定義共有方法getTime(),輸出當前對象的屬性time
6. 定義子類對象實例instance1,分別向父類構造函數「js book」,子類構造函數傳遞參數2014
7. 訪問子類對象實例instance1的books屬性,在子類方法中找不到books屬性,在子類對象實例的原型對象的構造函數內有這個屬性,給這個引用屬性添加一個元素「java」
8. 訪問子類對象實例instance1的getName()方法,在子類方法構造函數中找不到,在子類原型對象中有這個方法,輸出「js book」
9. 訪問子類對象實例instance1的getTIme()方法,在子類方法構造函數中找不到,在子類原型對象中有這個方法,輸出2014
10. 定義子類對象實例instance2,分別向父類構造函數「css book」,子類構造函數傳遞參數2013
11. 訪問子類對象實例instance2的books屬性,在子類方法中找不到books屬性,這裏是構造函數繼承,在子類對象實例的原型對象的構造函數內有這個屬性,這個屬性是從父類構造函數中拷貝的一份,它和instance1的books屬性是不一樣的,相互沒有影響
12. 訪問子類對象實例instance2的getName()方法,子類構造函數中找不到,父類構造函數中找不到,父類原型對象上有這個方法,輸出當前對象的name屬性,所以輸出「css book」
13. 訪問子類對象實例instance2的getTime()方法,子類構造函數中找不到,子類原型對象中有這個方法,輸出當前對象的time屬性,所以輸出2013
注意這裏經過call方式繼承父類後,訪問方法的前後順序是:
1. 子類方法中的共有方法SubClass.this.getName,
2. 父類方法中的共有方法SuperClasss.this.getName,
3. 子類原型對象中的共有方法SubClass.prototype.getName,
4. 父類原型對象中的共有方法SuperClasss.prototype.getName
訪問屬性books的時候也是這個順序,因此優先考慮經過call方法給當前this賦值獲得的books,而不是經過原型對象繼承的books。
在子類構造函數中執行父類構造函數,在子類原型上實例化父類就是組合模式。經過this將引用屬性books定義在父類的共有屬性中,每次實例化子類都會單獨拷貝一份,所以在子類的實例中更改父類繼承下來的引用類型屬性books不會影響到其餘實例,而且子類實例化過程當中又能將參數傳遞到父類的構造函數中。
這種方式也有缺點,在使用構造函數繼承時執行了一次父類的構造函數,而在實現子類原型的類式繼承時又調用了一遍父類的構造函數,父類的構造函數調用了兩次。
6.5 簡潔的繼承—原型式繼承
原型式繼承的思想是藉助prototype根據已有的對象建立一個新的對象,同時沒必要建立新的自定義對象類型。代碼以下:
// 原型式繼承 function inheritObject(o) { // 申明一個過渡函數對象 function F() {} // 過渡對象的原型繼承父對象 F.prototype = o; // 返回過渡對象的一個實例,該實例的原型繼承了父對象 return new F(); } var book = { name: 'js book', alikeBook: ['css book', 'html book'] } var newBook = inheritObject(book); newBook.name = 'ajax book'; newBook.alikeBook.push('xml book'); var otherBook = inheritObject(book) otherBook.name = 'flash book'; otherBook.alikeBook.push('as book'); console.log(newBook.name); // ajax book console.log(newBook.alikeBook); // ["css book", "html book", "xml book", "as book"] console.log(otherBook.name); // flash book console.log(otherBook.alikeBook); // ["css book", "html book", "xml book", "as book"] console.log(book.name); // js book console.log(book.alikeBook); // ["css book", "html book", "xml book", "as book"]
1. 定義原型式繼承方法,在方法內部申明過渡類,設置類的原型對象爲傳入的參數,訪問這個對象實例,這個實例繼承了父類對象
2. 定義book對象,對象內有name屬性和alikeBook屬性
3. 定義子類對象newBook,調用原型式繼承方法,繼承book對象中的屬性
4. 訪問子類對象newBook的name屬性,賦值爲「ajax book」,子類對象的原型對象中有這個屬性,而且是一個值類屬性
5. 訪問子類對象newBook的alikeBook屬性,添加元素「xml book」,子類對象的原型對象中有這個屬性,而且是一個引用屬性
6. 定義子類對象otherBook,調用原型式繼承方法,繼承book對象中的屬性
7. 訪問子類對象otherBook的name屬性,賦值爲「ajax book」,子類對象的原型對象中有這個屬性,而且是一個引用類型變量
8. 訪問子類對象otherBook的alikeBook屬性,添加元素「as book」,子類對象的原型對象中有這個屬性,而且是一個引用類型變量
9. 輸出newBook的name屬性,值是「ajax book」
10. 輸出newBook的books屬性,它是從父類原型對象上繼承來的,是同一個變量,這個數組內被添加了 「as book」,輸出['css book', 'html book', 'xml book', 'as book']
11. 輸出other的name屬性,值是「flash book」
12. 輸出oterBook的books屬性,它是從父類原型對象上繼承來的,是同一個變量,這個數組內被添加了 「as book」,輸出['css book', 'html book', 'xml book', 'as book']
13. 輸出父類對象的name屬性,值是「js book」
14. 輸出父類對象book的的alikeBook屬性,它是從父類原型對象上繼承來的,是同一個變量,這個數組被修改過了,添加了「as book」,輸出['css book', 'html book', 'xml book', 'as book']
和類式繼承同樣,父類對象book中的值類型被複制,引用類型屬性被共用,它也有類式繼承的缺點,即修改修改子類中從父類繼承來的引用類型屬性,會影響到其餘子類中的同名屬性,他們是同一個屬性。這種方法的優勢是F()函數內部沒有什麼內容,開銷比較小,還能夠將F過渡類緩存起來。也可使用新的語法Object.create()來代替這一句。不過建立子類實例的時候是能夠向父類構造函數傳參的,這裏再也不展開介紹。
6.6 寄生式繼承—加強版的原型式繼承
// 原型式繼承 function inheritObject(o) { // 申明一個過渡函數對象 function F() {} // 過渡對象的原型繼承父對象 F.prototype = o; // 返回過渡對象的一個實例,該實例的原型繼承了父對象 return new F(); } var book = { name: 'js book', alikeBook: ['css book', 'html book'] } function createBook(obj) { // 經過原型繼承方式建立對象 var o = new inheritObject(obj); // 拓展對象 o.getName = function () { console.log(obj.name); } // 返回拓展後的新對象 return o; } var newBook = createBook(book); newBook.name = 'ajax book'; newBook.alikeBook.push('xml book'); var otherBook = createBook(book); otherBook.name = 'flash book'; otherBook.alikeBook.push('as book'); console.log(newBook.name); // ajax book newBook.getName(); // js book console.log(newBook.alikeBook); // ["css book", "html book", "xml book", "as book"] console.log(otherBook.name); // flash book console.log(otherBook.alikeBook); // ["css book", "html book", "xml book", "as book"] otherBook.getName(); // js book console.log(book.name); // js book console.log(book.alikeBook); // ["css book", "html book", "xml book", "as book"]
1. 聲明原型式繼承方法inheritObject,實現原型式繼承
2. 定義父類對象book
3. 聲明建立Book對象的方法createBook,方法內部使用new表達式建立一個繼承自傳遞參數的對象o,在這個對象上擴展屬性,最後返回建立的對象
4. 使用createBook方法建立對象newBook,傳遞參數是book對象
5. 訪問對象newBook的name屬性,在它的原型對象上有這個屬性,從新賦值爲「ajax book」
6. 訪問對象newBook的alikeBook屬性,在它的原型對象上有這個屬性,添加元素「xml book」,這樣會影響全部繼承自這個對象的對象
7. 使用createBook方法建立對象otherBook,傳遞參數是book對象
8. 訪問對象otherBook的name屬性,在它的原型對象上有這個屬性,從新賦值爲「flash book」
9. 訪問對象otherBook的alikeBook屬性,在它的原型對象上有這個屬性,添加元素「as book」,這樣會影響全部繼承自這個對象的對象
10. 訪問newBook的屬性name,雖然繼承自它的原型對象的,可是這個屬性是值類型,已經被修改爲「ajax book」
11. 訪問newBook的getName方法,這個方法是經過在原型對象上擴展的方法繼承的,輸出傳入參數的name屬性,值爲「js book」
12. 訪問newBook的alikeBook屬性,這個屬性是繼承自原型對象的,而且是一個引用類型,已經被修改爲["css book", "html book", "xml book", "as book"]
13. 訪問otherBook的屬性name,雖然繼承自它的原型對象的,可是這個屬性是值類型,已經被修改爲「flash book」
14. 訪問otherBook的alikeBook屬性,這個屬性是繼承自原型對象的,而且是一個引用類型,已經被修改爲["css book", "html book", "xml book", "as book"]
15. 訪問otherBook的getName方法,這個方法是經過在原型對象上擴展的方法繼承的,輸出傳入參數的name屬性,值爲「js book」
16. 訪問父對象book的name屬性,它仍然是「js book」
17. 訪問父類對象book的alikeBook屬性,這個屬性已經被經過原型對象繼承book對象的子類對象修改了,已經被修改爲["css book", "html book", "xml book", "as book"]
寄生式繼承是對原型繼承的二次封裝,並在二次封裝過程當中對繼承的對象進行了拓展,這樣新建立的對象不只僅繼承了父類中的屬性和方法,並且還添加了新的屬性和方法。之因此叫寄生式繼承,是指能夠像寄生蟲同樣寄託於某個對象的內部生長,寄生式繼承這種加強新建立對象的繼承方式是依託於原型繼承模式。
從上面的測試代碼能夠看出,這種方式仍然會有全部子類共用一個引用實例的問題。
6.7 寄生組合式繼承-改造組合繼承
上面介紹的組合繼承是把類式繼承和構造函數繼承組合使用,這種方式有一個問題,就是子類不是父類的實例,而子類的原型是父類的實例,因此纔有了這裏要說的寄生組合繼承。寄生繼承依賴於原型繼承,原型繼承又與類式繼承很像,寄生繼承有些特殊,它處理的不是對象,而是對象的原型。
組合繼承中,經過構造函數繼承的屬性和方法是沒有問題的,這裏主要探討經過寄生式繼承從新繼承父類的的原型。咱們須要繼承的僅僅是父類的原型,再也不須要調用父類的構造函數,也就是在構造函數繼承中咱們已經調用了父類的構造函數。所以咱們須要的就是父類的原型對象的一個副本,而這個副本咱們經過原型繼承能夠獲得,可是這麼直接賦值給子類會有問題的,由於對父類原型對象複製獲得的複製對象p中的constructor指向的不是subClass子類對象,所以在寄生式繼承中要對複製對象p作一次加強處理,修復它的constructor屬性指向不正確的問題,最後獲得的複製對象p賦值給子類的原型,這樣子類的原型就繼承了父類的原型而且沒有執行父類的構造函數。測試代碼以下:
/** * 原型式繼承 * @param o 父類 * */ function inheritObject(o) { // 申明一個過渡函數對象 function F() {} // 過渡對象的原型繼承父對象 F.prototype = o; // 返回過渡對象的一個實例,該實例的原型繼承了父對象 return new F(); } /** * 寄生式繼承,繼承原型 * @param subClass 子類 * @param superClass 父類 */ function inheritPrototype(subClass, superClass) { // 複製一份父類的原型副本保存在變量中 var p = inheritObject(superClass.prototype); // 修正由於重寫子類原型而致使子類的constructor屬性被修改 p.constructor = subClass; // 設置子類的原型 subClass.prototype = p; } // 定義父類 function SuperClass(name) { this.name = name; this.colors = ['red', 'blue', 'green']; } // 定義父類原型方法 SuperClass.prototype.getName = function () { console.log(this.name); }; // 定義子類 function SubClass(name, time) { // 構造函數式繼承 SuperClass.call(this, name); // 子類新增屬性 this.time = time; } // 寄生式繼承父類原型 inheritPrototype(SubClass, SuperClass); // 子類新增原型方法 SubClass.prototype.getTime = function () { console.log(this.time); } var instance1 = new SubClass('js book', 2014); var instance2 = new SubClass('css book', 2013); instance1.colors.push('black'); console.log(instance1.colors); //["red", "blue", "green", "black"] console.log(instance2.colors); //["red", "blue", "green"] instance2.getName(); //css book instance2.getTime(); //2013 console.log(SubClass instanceof SuperClass); // false console.log(SubClass.prototype instanceof SuperClass); // true console.log(SubClass.prototype instanceof SuperClass.prototype); // Right-hand side of 'instanceof' is not callable console.log(instance2 instanceof SubClass); // true console.log(instance2 instanceof SuperClass); // true
1. 定義原型式繼承方法inheritObject,經過過渡對象返回一個經過原型對象繼承自傳入自參數的實例
2. 定義寄生式繼承方法inheritPrototype,傳入父類和子類。複製一份父類原型的副本保存在變量中,修正由於重寫子類原型而致使子類的constructor屬性問題,設置子類的原型爲這個對象。
3. 定義父類方法SuperClass,內部有本身的屬性
4. 訪問父類的原型對象,添加getName方法,輸出當前屬性name
5. 定義子類方法SubClass,在子類中調用call方法,在子類中執行父類的構造方法,給子類的this賦值。定義子類本身的屬性time
6. 調用寄生式繼承方法inheritPrototype,先拷貝父類原型對象賦值給變量p,修改它的constructor屬性,讓它指向子類構造函數subClass,設置子類的原型對象爲這個新的對象p
7. 訪問子類SubClass的原型對象,設置共有方法getTime,輸出當前對象time
8. 定義子類對象實例instance1,傳入兩個參數,第一個「js book」傳遞給父類方法,第二個2014用於對象本身的共有屬性
9. 定義子類對象實例instance2,傳入兩個參數,第一個「css book」傳遞給父類方法,第二個2013用於對象本身的共有屬性
10. 訪問對象instance1的colors屬性,它是經過call方法從父類構造函數中單獨拷貝的,給colors屬性新加一個元素「black」
11. 訪問對象instance1的colors屬性,輸出的是新增「black」元素以後的數組
12. 訪問對象instance2的colors屬性,它是經過call方法從父類構造函數中單獨拷貝的,這個沒有被修改過
13. 訪問對象instance2的getName方法,它是經過父類的原型對象繼承來的,輸出當前對象的name屬性「css book」
14. 訪問對象instance2的getTime方法,它是經過子類對象的原型對象繼承來的,輸出當前對象的time屬性2013
15. SubClass子類不是父類SuperClass的實例
16. 子類原型對象SubClass.prototype是父類SuperClass的實例
17. 子類原型對象SubClass.prototype不是父類原型對象SuperClass.prototype的實例
18.子類對象instance2是子類SubClass的實例
19. 子類對象instance2是父類SuperClass的實例
最大的改變就是對子類原型的處理,被賦予父類原型的一個引用,這是一個對象,所以這裏有一點要注意的就是子類再想添加原型方法必須經過prototype對象,經過點語法的方式一個一個添加方法了,不然直接賦予對象就會覆蓋掉從父類原型繼承的對象。
從上面的例子來看,寄生組合繼承還解決了子類共用父類中引用類型屬性的問題,子類中繼承的引用類型實例互不影響。還有子類也繼承了父類原型中的屬性和方法。