本文由淺到深,按部就班的將原型與繼承的抽象概念形象化,且每一個知識點都搭配相應的例子,儘量的將其通俗化,並且本文最大的優勢就是:長(爲了更詳細嘛)。git
首先,咱們先說說原型,但說到原型就得從函數提及,由於原型對象就是指函數所擁有的prototype
屬性(因此下文有時說原型,有時說prototype
,它們都是指原型)。github
說到函數,咱們得先有個概念:函數也是對象,和對象同樣擁有屬性,例如:瀏覽器
function F(a, b) {
return a * b;
}
F.length // 2 指函數參數的個數
F.constructor // function Function() { [native code] }
typeof F.prototype // "object"
複製代碼
從上面咱們能夠看出函數和對象同樣擁有屬性,咱們重點說的就是prototype
這個原型屬性。app
prototype
也是一個對象,爲了更形象的理解,我我的是把上述理解爲這樣的:函數
// F這個函數對象裏有個prototype對象屬性
F = {
prototype: {}
}
複製代碼
下面咱們就說說這個prototype
對象屬性。ui
prototype
是一個對象,裏面有個默認屬性constructor
,默認指向當前函數,咱們依舊使用F這個函數來講明:this
F = {
prototype: {
constructor: F // 指向當前函數
}
}
複製代碼
既然prototype
是個對象,那咱們也一樣能夠給它添加屬性,例如:spa
F.prototype.name = 'BetterMan';
// 那F就變成以下:
F = {
prototype: {
constructor: F,
name: 'BetterMan'
}
}
複製代碼
prototype
就先鋪墊到這,下面咱們來講說對象,而後再把它們串起來。prototype
建立對象有不少種方式,本文針對的是原型,因此就說說使用構造函數建立對象這種方式。上面的F
函數其實就是一個構造函數(構造函數默認名稱首字母大寫便於區分),因此咱們用它來建立對象。指針
let f = new F();
console.log(f) // {}
複製代碼
這時獲得了一個「空」對象,下面咱們過一遍構造函數建立對象的過程:
下面咱們修改一下F
構造函數:
function F(age) {
this.age = age;
}
複製代碼
再用F
來建立一個實例對象:
let f1 = new F(18); // 18歲,別來無恙
console.log(f1); // {age: 18}
複製代碼
其實咱們就獲得了一個f1
對象,裏面有一個age
屬性,但真的只有age
屬性嗎?上面咱們講到構造函數建立對象的過程,這裏的新建對象,而後給對象添加屬性,而後返回新對象,咱們都是看獲得的,還有一個過程,就是新對象的__proto__
屬性指向構造函數的ptototype
屬性。
咱們打印一下看看:
console.log(f1.__proto__); // {constructor: F}
複製代碼
這不就是F
構造函數的prototype
對象嗎?這個指向過程也就至關於f1.__proto__ === F.prototype
,理解這個很重要!
__proto__
咱們可稱爲隱式原型(不是全部瀏覽器都支持這個屬性,因此谷歌搞起),這個就厲害了,既然它指向了構造函數的原型,那咱們獲取到它也就能獲取到構造函數的原型了(但通常咱們不用這個方法獲取原型,後面會介紹其餘方法)。
前面咱們說了構造函數的prototype
對象中的constructor
屬性是指向自身函數的,那咱們用__proto__
來驗證一下:
console.log(f1.__proto__.constructor); // F(age) {this.age = age;}
// 由於f1.__proto__ === F.prototype,因此上述就是指F.prototype.constructor
複製代碼
嗯,不錯不錯,看來沒毛病!
目前來講應該仍是比較好理解的,那咱們再看看:
console.log(f1.constructor); // F(age) {this.age = age;}
複製代碼
額,這什麼鬼?難道實例對象f1
還有個constructor
屬性和構造函數原型的constructor
同樣都是指向構造函數?這就有點意思了。
其實不是,應該是說f1
的神祕屬性__proto__
指向了F.prototype
,這至關於一個指向引用,若是要形象點的話能夠把它理解爲把F.prototype
的屬性"共享"到了f1
身上,但這是動態的"共享",若是後面F.prototype
改變的話,f1
所"共享"到的屬性也會跟着改變。理解這個很重要!重要的事情說三遍!重要的事情說三遍!重要的事情說三遍!
那咱們再把代碼"形象化":
F = {
prototype: {
constructor: F
}
};
f1 = {
age: 18,
__proto__: { // 既然咱們已經把這個形象化爲"共享"屬性了,那就再形象一點
constructor: F
}
}
// 更形象化:
f1 = {
age: 18, // 這個是f1對象自身屬性
constructor: F // 這個是從原型上"共享"的屬性
}
複製代碼
既然咱們說的是動態"共享"屬性,那咱們改一改構造函數的prototype
屬性看看f1
會不會跟着改變:
// 沒改以前
console.log(f1.name); // undefined
// 修改以後
F.prototype.name = 'BetterMan';
console.log(f1); // {age: 18}
console.log(f1.name); // 'BetterMan'
複製代碼
A(讀A第二調)……,看來和想的一毛同樣啊,可是f1
上面沒看到name
屬性,那就是說咱們只是能夠從構造函數的原型上拿到name
屬性,而不是把name
變爲實例對象的自身屬性。說到這裏就得提提對象自身屬性和原型屬性(從原型上得來的屬性)了。
咱們所建立的實例對象f1
,有自身屬性age
,還有從原型上找到的屬性name
,咱們可使用hasOwnProperty
方法檢測一下:
console.log(f1.hasOwnProperty('age')); // true 說明是自身屬性
console.log(f1.hasOwnProperty('name')); // false 說明不是自身屬性
複製代碼
那既然是對象屬性,應該就能夠添加和刪除吧?咱們試試:
delete f1.age;
console.log(f1.age); // undefined
delete f1.name;
console.log(f1.name); // 'BetterMan'
複製代碼
額,age
屬性刪除成功了,但好像name
沒什麼反應,比較堅挺,這就說明了f1
對象能夠掌控自身的屬性,愛刪刪愛加加,但name
屬性是從原型上獲得的,是別人的屬性,你可沒有權利去修改。
其實咱們在訪問對象的name
屬性時,js引擎會依次查詢f1
對象上的全部屬性,可是找不到這個屬性,而後就會去建立f1
實例對象的構造函數的原型上找(這就歸功於神祕屬性__proto__了,是它把實例對象和構造函數的原型聯繫了起來),而後找到了(若是再找不到的話,還會往上找,這就涉及到原型鏈了,後面咱們會說到)。而找age
屬性時直接就在f1
上找到了,就不用再去其餘地方找了。
到如今你們應該對原型有了個大概的理解了吧,但它有什麼用呢? 用處大大的,能夠說咱們無時無刻都在使用它,下面咱們繼續。
講了原型,那確定是離不開繼承這個話題的,說到繼承就很熱鬧了,什麼原型模式繼承、構造函數模式繼承、對象模式繼承、屬性拷貝模式繼承、多重繼承、寄生式繼承、組合繼承、寄生組合式繼承……這什麼鬼?這麼多,看着是否是很頭疼?
我我的就把它們分爲原型方式、構造函數方式、對象方式這三個方式,而後其餘的繼承方式都是基於這三個方式的組合,固然這只是我我的的理解哈,下面咱們開始。
說到繼承,確定得說原型鏈,由於原型鏈是繼承的主要方法。
咱們先來簡單的回顧一下構造函數、原型和實例的關係:每一個構造函數都有一個原型對象,原型對象都包含一個指向構造函數的指針(constructor
),而實例包含一個指向原型對象的內部指針(__proto__
)。那麼,假如咱們讓原型對象等於另外一個實例對象,結果會怎麼樣呢?顯然,此時的原型對象將包含一個指向另外一個原型的指針(__proto__
),相應的,另外一個原型中也包含着一個指向另外一個構造函數的指針(constructor
)。那假如另外一個原型又是另外一個對象實例,那麼上述關係依然成立,如此層層遞進,就構成了實例與原型的鏈條。這就是所謂的原型鏈,如圖:
到這裏千萬不要亂,必定要理解了這段話再往下看,其實就是把別人的實例對象賦值給了咱們的構造函數的原型,這就是第一層,而後若是別人的實例對象的構造函數的原型又是另外一我的的實例對象的話,那不是同樣的道理嗎?這就是第二層,那若是再出現個第三者,那又是一層了,這就構成了一個層層連起來的原型鏈。
好了,若是你看到了這裏,說明已經理解了上述"鏈情",那咱們就開始搞搞繼承。
繼承有多重形式,咱們一個個來,分別對比一下其中的優缺點。
注:由於多數繼承都依賴於原型及原型鏈,因此當再依賴於其餘方式時,我就以這個方式來命名這個繼承方式,這樣看起來就不會那麼複雜。
1. 基於構造函數方式
咱們先定義三個構造函數:
// 構造函數A
function A() {
this.name = 'A';
};
A.prototype.say = function() {
return this.name;
};
// 構造函數B
function B() {
this.name = 'B';
};
// 構造函數C
function C(width, height) {
this.name = 'C';
this.width = width;
this.height = height;
this.getArea = function() {
return this.width * this.height;
};
};
複製代碼
下面咱們試試繼承:
B.prototype = new A();
C.prototype = new B();
複製代碼
上述是否是有點熟悉,是否是就是前面所提的原型鏈的概念:B構造函數的原型被賦上A構造函數的實例對象,而後C的原型又被賦上B構造函數的實例對象。
而後咱們用C構造函數來建立一個實例對象:
let c1 = new C(2, 6);
console.log(c1); // {name: "C", width: 2, height: 6, getArea: ƒ}
console.log(c1.name); // 'C'
console.log(c1.getArea()); // 12
console.log(c1.say()); // 'C'
複製代碼
c1
竟然有say
方法了,可喜可賀,它是怎麼作到的?讓咱們來捋捋這個過程:
C
新建了一個"空"對象;name
、width
、height
、getArea
這四個自身屬性;c1
實例對象;console.log(c1)
和console.log(c1.name)
,console.log(c1.getArea())
都好理解;console.log(c1.say())
,這就得去找say
方法了,js引擎先在c1
身上找,沒找到,而後c1.__proto__
這個神祕連接是指向C
構造函數的原型的,而後就去C.prototype
上找,而後咱們是寫有C.prototype = new B()
的,也就是說是去B
構造函數的實例對象上找,仍是沒有,那繼續,又經過new B().__proto__
去B
的原型上找,而後咱們是寫有B.prototype = new A();
,那就是去A
所建立的實例對象上找,沒有,那就又跑去A
構造函數的原型上找,OK!找到!這個過程就至關於這樣: c1 —→ C.prototype —→ new B() —→ B.prototype —→ new A() —→ A.prototype
這就是上述的一個基於構造函數方式的繼承過程,其實就是一個查找過程,可是你們有沒有發現什麼?
上述方式存在兩個問題:第一個問題就是constructor
的指向。
原本B.prototype
中的constructor
指向好好的,是指向B
的,但如今B.prototype
徹底被new A()
給替換了,那如今的B.prototype.constructor
是指向誰的?咱們看看:
console.log(B.prototype.constructor); // ƒ A() {}
let b1 = new B();
console.log(b1.constructor); // ƒ A() {}
複製代碼
此時咱們發現不只是B.prototype.constructor
指向A
,連b1
也是如此,別忘了b1
中的constructor
屬性也是由B.prototype
所共享的,因此老大(B
)改變了,小弟(b1
)固然也會跟着動態改變。
但如今它們爲何是指向A
的呢?由於B.prototype
被替換爲了new A()
,那new A()
裏有什麼?咱們再把B.prototype
和new A()
形象化來表示一下:
A = {
prototype:{
constructor: A
}
};
new A() = {
name: 'A',
say: function() {
return this.name;
},
constructor: A // 由__proto__的指向所共享獲得的
}
B = {
prototype:{
constructor: B
}
};
// 這時把B.prototype換爲new A(),那就變成了這樣:
B = {
prototype:{
name: 'A',
say: function() {
return this.name;
},
constructor: A // 因此指向就變成了A
}
};
複製代碼
因此咱們要手動修正B.prototype.constructor
的指向,同理C.prototype.constructor
的指向也是如此:
B.prototype = new A();
B.prototype.constructor = B;
C.prototype = new B();
C.prototype.constructor = C;
複製代碼
第一個問題解決了,到第二個問題:效率的問題。
當咱們用某一個構造函數建立對象時,其屬性就會被添加到this中去。而且當別添加的屬性其實是不會隨着實例改變時,這種作法會顯得沒有效率。例如在上面的實例中,A
構造函數是這樣定義的:
function A() {
this.name = 'A';
this.say = function() {
return this.name;
};
};
複製代碼
這種實現意味着咱們用new A()
建立的每一個實例都會擁有一個全新的name
屬性和say
屬性,並在內存中擁有獨立的存儲空間。因此咱們應該考慮把這些屬性放到原型上,讓它們實現共享:
// 構造函數A
function A() {};
A.prototype.name = 'A';
A.prototype.say = function() {
return this.name;
};
// 構造函數B
function B() {};
B.prototype.name = 'B';
// 構造函數C
function C(width, height) { // 此處的width和height屬性是隨參數變化的,因此就不須要改成共享屬性
this.width = width;
this.height = height;
};
C.prototype.name = 'C';
C.prototype.getArea = function() {
return this.width * this.height;
};
複製代碼
這樣一來,構造函數所建立的實例中一些屬性就再也不是私有屬性了,而是在原型中能共享的屬性,如今咱們來試試:
let test1 = new A();
let test2 = new A();
console.log(test1.say === test2.say); // true 沒改成共享屬性前,它們是不相等的
複製代碼
雖然這樣作一般更有效率,但也只是針對實例中不可變屬性而言的,因此在定義構造函數時咱們也要考慮哪些屬性適合共享,哪些適合私有(且必定要繼承後再對原prototype進行擴展和矯正constructor)。
2. 基於原型的方式
正如上面所作的,處於效率考慮,咱們應當儘量的將一些可重用的屬性和方法添加到原型中去,這樣的話咱們僅僅依靠原型就能夠完成繼承關係的構建了,因爲原型上的屬性都是可重用的,這也意味着從原型上繼承比在實例上繼承要好得多,並且既然須要繼承的屬性都放在了原型上,又何須生成實例下降效率,而後又從所生成的實例中繼承不須要的私有屬性呢?因此咱們直接拋棄實例,從原型上繼承:
// 構造函數A
function A() {};
A.prototype.name = 'A';
A.prototype.say = function() {
return this.name;
};
// 構造函數B
function B() {};
B.prototype = A.prototype; // 先繼承,再進行constructor矯正和B.prototype的擴展
B.prototype.constructor = B;
B.prototype.name = 'B';
// 構造函數C
function C(width, height) { // 此處的width和height屬性是隨參數變化的,因此就不須要改成共享屬性
this.width = width;
this.height = height;
};
C.prototype = B.prototype;
C.prototype.constructor = C; // 先繼承,再進行constructor矯正和C.prototype的擴展
C.prototype.name = 'C';
C.prototype.getArea = function() {
return this.width * this.height;
};
複製代碼
嗯,這樣感受效率高多了,也比較養眼,而後咱們試試效果:
let b2 = new B();
console.log(b2.say()); // 'C'
複製代碼
(⊙o⊙)…不是應該打印出B
的嗎?怎麼和我心裏的小完美不太同樣?
想必你們應該都看出來了,上面的繼承方式其實就至關於A、B、C
全都共享了同一個原型,那就形成了引用問題,在後面對C
原型上的name
屬性進行了修改,因此此時A
、B
、C
的原型的name屬性都爲'C',此時真的是受制於人啊。
有沒有一箭雙鵰的辦法,我又要效率,又不想受制於人,啪!把這兩個方法結合起來不就好了嗎?!
3. 結合構造函數方式和原型的方式
我既想快,又不想被別人管,搞個第三者來解決怎麼樣?(怎麼感受聽起來怪怪的)。咱們在它們中間使用一個臨時構造函數(因此也可稱爲臨時構造法)來作個橋樑,把小弟管大哥的關係斷掉(腿打斷),而後你們又能夠高效率的合做:
// 構造函數A
function A() {};
A.prototype.name = 'A';
A.prototype.say = function() {
return this.name;
};
// 構造函數B
function B() {};
let X = function() {}; // 新建一個"空"屬性的構造函數
X.prototype = A.prototype; // 將X的原型指向A的原型
B.prototype = new X(); // B的原型指向X建立的實例對象
B.prototype.constructor = B; // 記得修正指向
B.prototype.name = 'B'; // 擴展
// 構造函數C
function C(width, height) { // 此處的width和height屬性是隨參數變化的,因此就不須要改成共享屬性
this.width = width;
this.height = height;
};
// 同上
let Y = function() {};
Y.prototype = B.prototype;
C.prototype = new Y();
C.prototype.constructor = C;
C.prototype.name = 'C';
C.prototype.getArea = function() {
return this.width * this.height;
};
複製代碼
如今試試效果怎麼樣:
let c3 = new C;
console.log(c3.say()); // C
複製代碼
穩!這樣咱們既不是直接繼承實例上的屬性,而是繼承原型所共享的屬性,並且還能經過X
和Y
這兩個"空"屬性構造函數來把A和B
上的非共享屬性過濾掉(由於new X()
比起new A()
所生成的實例,由於X
是空的,因此生成的對象不會存在私有屬性,可是new A()
可能會存在私有屬性,既然是私有屬性,因此也就是不須要被繼承,因此new A()
會存在效率問題和多出不須要的繼承屬性)。
4. 基於對象的方式
這種基於對象的方式其實包括幾種方式,由於都和對象相關,因此我就統稱爲對象方式了,下面一一介紹:
①以接收對象的方式
function create(o) { // o是所要繼承的父對象
function F() {};
F.prototype = o;
return new F(); // 返回一個實例對象
};
let a = {
name: 'better'
};
console.log(create(a).name); // 'better'
複製代碼
這種方式是接受一個父對象後返回一個實例,進而達到繼承的效果,有沒有點似曾相識的感受?這不就是低配版的Object.create()
嗎?有興趣的能夠多去了解了解。因此這個方式其實也應該稱爲"原型繼承法",由於也是以修改原型爲基礎的,但又和對象相關,因此我就把它歸爲對象方式了,這樣比較好分類。
②以拷貝對象屬性的方式
// 直接將父原型的屬性拷貝過來,好處是Child.prototype.constructor沒被重置,但這種方式僅適用於只包含基本數據類型的對象,且父對象會覆蓋子對象的同名屬性
function extend(Child, Parent) { // Child, Parent都爲構造函數
let c = Child.prototype;
let p = Parent.prototype;
for (let i in p) {
c[i] = p[i];
}
};
複製代碼
// 這種直接拷貝屬性的方式簡單粗暴,直接複製傳入的對象屬性,但仍是存在引用類型的問題
function extendCopy(p) { // p是被繼承的對象
let c = {};
for (let i in p) {
c[i] = p[i];
}
return c;
};
複製代碼
// 上面的extendCopy可稱爲淺拷貝,沒有解決引用類型的問題,如今咱們使用深拷貝,這樣就解決了引用類型屬性的問題,由於無論你有多少引用類型,全都一個個拷過來
function deepCopy(p, c) { // c和p都是對象
c = c || {};
for (let i in p) {
if (p.hasOwnProperty[i]) { // 排除繼承屬性
if (typeof p[i] === 'object') { // 解決引用類型
c[i] = Array.isArray(p[i]) ? [] : {};
deepCopy[p[i], c[i]];
} else {
c[i] = p[i];
}
}
}
return c;
}
複製代碼
③拷貝多對象屬性的方式
// 這種方式就能夠一次拷貝多個對象屬性,也稱爲多重繼承
function multi() {
let n = {},
stuff,
j = 0,
len = arguments.length;
for (j = 0; j < len; j++) {
stuff = arguments[j];
for (let i in stuff) {
if (stuff.hasOwnProperty(i)) {
n[i] = stuff[i];
}
}
}
return n
};
複製代碼
④吸取對象屬性並擴展的方式
這種方式其實應該叫作"寄生式繼承",這名字乍看很抽象,其實也就那麼回事,因此也把它分到對象方式裏:
// 其實也就是在建立對象的函數中吸取了其它對象的屬性(寄生獸把別人的xx吸走),而後對其擴展並返回
let parent = {
name: 'parent',
toString: function() {
return this.name;
}
};
function raise() {
let that = create(parent); // 使用前面咱們寫過的create函數
that.other = 'Once in a blue moon!'; // 今天學的,醜顯唄一下
return that;
}
複製代碼
和對象相關的方式是否是有點多?但其實也都是圍繞着對象屬性的,理解這點就好理解了,下面繼續。
5. 構造函數借用法
這個方式其實也可歸爲構造函數方式,但比較溜,因此單獨拎出來溜溜(這是最後一個了,我保證)。
咱們再把以前定義的老函數A
拿出來炒炒:
// 構造函數A
function A() {
this.name = 'A';
};
A.prototype.say = function() {
return this.name;
};
// 構造函數D
function D() {
A.apply(this, arguments); // 這裏就至關於借用A構造函數把A中屬性建立給了D,即name和say屬性
};
D.prototype = new A(); // 這裏負責拿到A原型上的屬性
D.prototype.name = 'D'; // 繼承後再進行擴展
複製代碼
這樣兩個步驟是否是就把A的自身屬性和原型屬性都搞定了?簡單完美!
等等,看起來好像有點不對,A.apply(this, arguments)
已經完美的把A
自身屬性變爲了D
的自身屬性,可是D.prototype = new A()
又把A
的自身屬性繼承了一次,真是畫蛇添足,既然咱們只是單純的想要原型上的屬性,那直接拷貝不就完事了嗎?
// 構造函數A
function A() {
this.name = 'A';
};
A.prototype.say = function() {
return this.name;
};
// 以前定義的屬性拷貝函數
function extend2(Child, Parent) {
let c = Child.prototype;
let p = Parent.prototype;
for (let i in p) {
c[i] = p[i];
}
};
// 構造函數D
function D() {
A.apply(this, arguments); // 這裏就至關於借用A構造函數把A中屬性建立給了D,即name和say屬性
};
extend2(D, A); // 這裏就直接把A原型的屬性拷貝給了D原型
D.prototype.name = 'D'; // 繼承後在進行擴展
let d1 = new D();
console.log(d1.name); // 'A'
console.log(d1.__proto__.name) // undefined 這就說明了name屬性是新建的,而不是繼承獲得的
複製代碼
(⊙o⊙)…,其實還有其它的繼承方法,仍是不寫了,怕被打,但其實來來去去就是基於原型、構造函數、對象這幾種方式搞來搞去,我我的就是這麼給它們分類的,畢竟七秒記憶放不下,囧。
寫到這裏,終於嚥下了最後一口氣,呸,鬆了一口氣。也感謝你看到了最後,但願對你有所幫助,有寫得不對的地方還請多多指教,喜歡的就關注一波吧,後續會持續更新。