原型基礎
每個對象都有一個原型prototype
對象,通過函數創建的對象也會擁有這個原型對象。
原型是一個指向對象的指針。
原型對象的作用:
存儲一些實例對象公用的方法或屬性,也就是說一個構造函數中的公共方法或屬性應該放入原型對象中
原型對象中的參數:
默認一個原型對象有一個方法
constructor
,即構造函數本身。原型對象和構造函數的關系:
構造函數怎么找到自己的原型對象:
使用屬性
prototype
即可找到該原型對象,你可以為其添加公共方法或屬性方便該構造函數的實例對象使用。實例對象怎么找到自己的原型對象:
使用屬性
__proto__
即可找到該實例對象的原型對象
使用字面量創建出的對象可以調用其原型對象中的方法。
<script>"use strict"; let array = [1, 2, 3]; console.log(array); </script>
構造函數,實例對象,原型對象的關系。
獲取原型對象
如果是一個構造函數,你想獲取到原型對象為其實例化的對象添加公共方法,可以使用屬性prototype
來獲取。
如果是一個已經實例化好的對象,你想獲取到其原型對象可以使用屬性__proto__
來進行獲取,也可以使用Object.getPrototypeOf()
方法來進行獲取。
<script>"use strict"; function User() { }; // 構造函數 console.log(User.prototype); let u1 = new User(); console.log(u1.__proto__); console.log(Object.getPrototypeOf(u1)); console.log(u1.__proto__ === User.prototype); // true console.log(u1.__proto__ === Object.getPrototypeOf(u1)); // true console.log(User.prototype === Object.getPrototypeOf(u1)); // true </script>
原型對象設置方法
函數擁有多個原型,prototype
用於實例對象使用,__proto__
用於函數自身當做對象時使用。
注意函數本身也是一個實例對象,所以當將函數作為對象使用時使用__proto__
為它設置方法。
當函數作為構造函數時其供實例使用的方法應該存儲在prototype
中,這是為了大幅度節省內存。
否則每一個實例對象都會創建出自己的方法。
<script>"use strict"; function User() { }; // 構造函數 User.__proto__.show = function (){ console.log("函數作為對象調用的方法..."); }; User.show(); // 函數作為對象調用的方法... // ============= User.prototype.show = function(){ console.log("該函數的實例對象調用的方法..."); }; let u1 = new User(); u1.show(); // 該函數的實例對象調用的方法... </script>
推薦使用prototype
來設置方法,因為將函數作為對象來使用的場景不多見。
設置方式有兩種,第一種在原有的原型對象基礎上增加新的方法,第二種是覆蓋原本的原型對象,但是要注意添加參數constructor
來指向構造函數。

<script>"use strict"; function User() { }; // 構造函數 // ========= 在原有的原型對象基礎上新增一個方法 User.prototype.show = function(){ console.log("該函數的實例對象調用的方法..."); }; let u1 = new User(); u1.show(); // 該函數的實例對象調用的方法... </script>

<script>"use strict"; function User() { }; // 構造函數 // ========= // 設置新的原型對象 User.prototype = { constructor: User, // 必須添加該參數,指向構造函數。 show() { console.log("方法1"); }, test() { console.log("方法2"); } }; let u1 = new User(); u1.show(); // 方法1 u1.test(); // 方法2 </script>
原型鏈關系圖
原型對象也有自己的原型,最終的原型對象都是Object.prototype
<script>"use strict"; function User() { }; // 構造函數 User.prototype.show = function () { console.log("該函數的實例對象調用的方法..."); }; let u1 = new User(); console.log("User的實例對象的原型對象--->", u1.__proto__); </script>
<script>"use strict"; function User() { }; // 構造函數 User.prototype.show = function () { console.log("該函數的實例對象調用的方法..."); }; let u1 = new User(); // 驗證上圖關系 console.log(u1.__proto__.__proto__ === Object.prototype); // true </script>
原型對象與構造函數
原型對象中有一個constructor
的方法,即指向構造函數。
<script>"use strict"; function User() { }; // 構造函數 console.log(User.prototype.constructor === User); // true </script>
更改原型對象
使用Object.setPrototypeOf()
可設置對象的原型對象。
也可使用Object.create()
來設置對象的原型對象,這個方法下面會介紹到。
<script> "use strict"; function User() { }; // 構造函數 function Admin() { }; // 構造函數 User.prototype.show = function () { console.log("User中的show"); }; Admin.prototype.show = function () { console.log("Admin中的show"); }; let a1 = new Admin(); Object.setPrototypeOf(a1, User.prototype); // 將a1的原型對象設置為User的原型對象 console.log(Object.getPrototypeOf(a1)); // {show: ƒ, constructor: ƒ} a1.show(); // User中的show let a2 = new Admin(); a2.show(); // Admin中的show </script>
原型檢測
使用instanceof
檢測構造函數的 prototype
屬性是否出現在某個實例對象的原型鏈上
使用isPrototypeOf
檢測一個對象是否是另一個對象的原型鏈中
<script> "use strict"; function User() { }; // 構造函數 let u1 = new User(); console.log(u1 instanceof User); // true u1的原型鏈中包含User的原型對象嗎? console.log(User.prototype.isPrototypeOf(u1)); // true User的原型對象在u1的原型鏈中嗎? </script>
屬性遍歷
使用in
檢測原型鏈上是否存在屬性,使用 hasOwnProperty()
只檢測當前對象的原型對象。
使用for/in
會按照原型鏈遍歷。
<script> "use strict"; function User() { }; // 構造函數 User.prototype = { constructor: User, show() { console.log("User原型的show..."); } } let u1 = new User(); console.log("show" in u1); // true 會沿着原型鏈查找 console.log(u1.hasOwnProperty("show")); // false 只檢測自己 for (let key in u1) { // for/in會遍歷所有原型鏈 if (key === "show") { console.log("存在"); // 存在 } } </script>
原型借用
我們可以借用另一個原型對象中的方法,使用call()
或者apply()
來改變this
指向與傳遞參數即可。
如下示例,對象借用了數組中的排序方法對成績進行排序,這里實在想不到太好的例子。所以就用這個了。
<script> "use strict"; let obj = { Html: 76, Css: 88, Js: 100, Python: 96, Linux: 77, }; // call傳遞一個新的this指向 let res = Array.prototype.sort.call(Object.entries(obj), function (v1, v2) { return v2[1] - v1[1]; }); obj = {}; // 清空對象 for (let i = 0; i < res.length; i++) { let [key, value] = res[i]; Object.assign(obj,{[key]:value}) }; console.log(obj); // {Js: 100, Python: 96, Css: 88, Linux: 77, Html: 76} </script>
this
this
不受原型繼承影響,this
指向調用屬性時使用的對象。
<script> "use strict"; function User(username) { this.username = username; }; // 構造函數 User.prototype = { constructor: User, show() { console.log(this.username); } } let u1 = new User("u1"); let u2 = new User("u2"); u1.show(); // u1 u2.show(); // u2 </script>
Object.create
該方法可以立即返回一個對象,參數1指定其原型對象,參數2可設置其屬性或方法及其特征。
<script> "use strict"; // 無原型的對象 let obj_1 = Object.create(null, { username: { value: "雲崖" } }); console.log(obj_1); // username: "雲崖" // 有原型的對象,該對象原型指向為Array對象的原型 let obj_2 = Object.create(Array.prototype, { username: { value: "雲崖" } }); console.log(obj_2); // Array {username: "雲崖"} </script>
__proto__原理
__proto__
其實它並非一個真正意義上的屬性而是使用getattr
以及setattr
進行實現的。
建議使用 Object.setPrototypeOf
與Object.getProttoeypOf
替代 __proto__
。
以下示例將展示__proto__
原理。
<script> "use strict"; function User(username) { this.username = username; Object.defineProperties(this, { __proto__: { get() { return User.prototype; }, set(value) { Object.setPrototypeOf(this, value); }, }, }); }; // 構造函數 </script>
繼承與多態
Js
的繼承是原型上的繼承。Js
只有單繼承,沒有多繼承,即一個對象只能有一個原型。
當一個對象開始找方法時不斷的向上使用__proto__
來尋找方法。
調用相同方法,產生不同結果,這就是多態的體現。
繼承實現
注意!Js
的繼承是原型對象的繼承,並不是類的繼承。
當一個實例對象要找方法時會一層一層向上找,如果找到了方法就不再繼續向上找了。

<script> "use strict"; function A() { }; // 構造函數 A.prototype.f1 = function () { console.log("A的f1方法"); }; function B() { }; // 構造函數 Object.setPrototypeOf(B.prototype, A.prototype); // B的原型對象繼承於A的原型對象 B.prototype.f2 = function () { console.log("B的f2方法"); }; function C() { }; // 構造函數 Object.setPrototypeOf(C.prototype, B.prototype); // C的原型對象繼承於B的原型對象 C.prototype.f3 = function () { console.log("C的f3方法"); } let c1 = new C(); console.dir(c1); c1.f1(); c1.f2(); c1.f3(); </script>
以下示例不是在原型對象上繼承,故是一種錯誤的做法。

<script> "use strict"; function A() { }; A.prototype.f1 = function () { console.log("A的f1方法"); }; function B() { }; Object.setPrototypeOf(B, A.prototype); B.prototype.f2 = function () { console.log("B的f2方法"); }; function C() { }; Object.setPrototypeOf(C, B.prototype); C.prototype.f3 = function () { console.log("C的f3方法"); } let c1 = new C(); console.dir(c1); // 異常 c1.f1(); c1.f2(); c1.f3(); </script>
方法覆寫
由於查找順序是由下而上,所以我們在最近的原型對象中寫入同名方法就不會繼續向上查找了。
<script> "use strict"; function A() { }; // 構造函數 A.prototype.show = function () { console.log("A的show方法"); }; function B() { }; // 構造函數 Object.setPrototypeOf(B.prototype, A.prototype); // B的原型對象繼承於A的原型對象 B.prototype.show = function () { console.log("B的show方法"); }; let b1 = new B(); b1.show(); // B的show方法 </script>
多態體現
同樣的方法運用在不同的對象身上會產生不同的結果,這就是多態的體現。
<script> "use strict"; function User() { } User.prototype.show = function () { console.log(this.description()); // 調用相同方法,產生不同結果,這就是多態的體現 }; function Admin() { } Admin.prototype = Object.create(User.prototype); // Object.create() 也是可以改變對象的原型 Admin.prototype.description = function () { return "管理員在此"; }; function Member() { } Member.prototype = Object.create(User.prototype); Member.prototype.description = function () { return "我是會員"; }; function Enterprise() { } Enterprise.prototype = Object.create(User.prototype); Enterprise.prototype.description = function () { return "企業帳戶"; }; for (const obj of [new Admin(), new Member(), new Enterprise()]) { obj.show(); } </script>
深究繼承
繼承是為了復用代碼,繼承的本質是將原型指向到另一個對象。
構造函數
如果多個構造函數在功能上極其相似,我們希望進行復用代碼則可以利用其它構造函數來進行函數的構建。但是要注意如下問題:
此時 this
指向了window,無法為當前對象聲明屬性。
<script> "use strict"; function User(username) { this.username = username; // 嚴格模式拋出異常!此時的this指向在window } User.prototype = { constructor: User, show() { console.log(`this指向-->${this}`); console.log(this.username); }, } function Admin(username) { User(username) } Object.setPrototypeOf(Admin.prototype, User.prototype); let a1 = new Admin("雲崖"); a1.show(); </script>
解決上面的問題是使用 call()/apply()
方法改變this
指向,從而為每個生成的對象設置屬性。
<script> "use strict"; function User(username) { this.username = username; } User.prototype = { constructor: User, show() { console.log(`this指向-->${this}`); // this指向-->[object Object] console.log(this.username); // 雲崖 }, } function Admin(username) { User.call(this, username) // 解決辦法 } Object.setPrototypeOf(Admin.prototype, User.prototype); let a1 = new Admin("雲崖"); a1.show(); </script>
原型工廠
原型工廠是將繼承的過程封裝,使用繼承業務簡單化。

<script> "use strict"; function extend(sub, sup) { // 原型工廠代碼封裝 Object.setPrototypeOf(sub.prototype,sup.prototype); // 使sub的原型對象繼承於sup的原型對象 } function User(username) { this.username = username; } User.prototype = { constructor: User, show() { console.log(`this指向-->${this}`); // this指向-->[object Object] console.log(this.username); // 雲崖 }, } function Admin(username) { User.call(this, username) } extend(Admin,User); // 使用原型工廠封裝 let a1 = new Admin("雲崖"); a1.show(); </script>
對象工廠
在原型繼承基礎上,將對象的生成使用函數完成,並在函數內部為對象添加屬性或方法。

<script> "use strict"; function User(name, age) { this.name = name; this.age = age; } User.prototype.show = function () { console.log(this.name, this.age); }; function Admin(name, age) { let instance = Object.create(User.prototype); // 創建了一個新對象 User.call(instance, name, age); instance.role = function () { console.log('admin.role'); } return instance; } let hd = Admin("管理員", 19); hd.show(); </script>
Mixin機制
由於Js
不支持多繼承,所以想添加功能必須在某一個原型對象上不斷的增加功能,這勢必會讓其本來的原型顯得混亂不堪。
這種時候就可以使用Mixin
機制來實現。
注意:Minin
類應該當做工具箱來使用,而不應該作為其他類的父類被繼承下來。

<script> "use strict"; function extend(sub, sup) { // 更改原型對象的函數 Object.setPrototypeOf(sub.prototype, sup.prototype); // 使sub的原型對象繼承於sup的原型對象 } function Vehicle(name) { // 交通工具 this.name = name; } Vehicle.prototype = { constructor: Vehicle, whistle() { console.log(`${this.name}在鳴笛`); // 公用方法放父類中 }, } function Aircraft(name) { // 飛機 Vehicle.call(this, name); } extend(Aircraft, Vehicle) // 飛機的原型對象繼承於交通工具。因此飛機具有了鳴笛方法 function Car(name) { // 汽車 Vehicle.call(this, name); } extend(Car, Vehicle) // 汽車的原型對象繼承於交通工具。因此汽車具有了鳴笛方法 let Flyable_Mixin = { // 飛行器的功能 fly() { console.log(`${this.name}在飛`); }, outer() { console.log("其他功能..."); }, }; Object.assign(Aircraft.prototype, Flyable_Mixin); //給飛機添加上飛機Mixin的功能 let Car_Mixin = { // 汽車的功能 reversing() { console.log(`${this.name}正在倒車入庫`); }, outer() { console.log("其他功能..."); }, }; Object.assign(Car.prototype, Car_Mixin); //給汽車添加汽車Mixin的功能 let c1 = new Car("法拉利"); let a1 = new Aircraft("波音747"); c1.whistle(); // 法拉利在鳴笛 c1.reversing(); // 法拉利正在倒車入庫 a1.whistle(); // 波音747在鳴笛 a1.fly(); // 波音747在飛 </script>
super
super
會在其原型對象上找。
<script> "use strict"; let a = { username: "雲崖" }; let b = { __proto__: a, show() { console.log(super.username); // super會去找__proto__,相當於拿到a.username }, }; b.show(); // 雲崖 </script>
總結
其實Js
的繼承處理的和其他語言還是有所不同,構造函數相當於父親,這個父親有一個背包就是原型對象。當他的兒子要去用方法時就去找父親的背包,父親的背包沒找到就找爺爺的背包。
而在這個背包中有一張字條,就是父親的名字。
以上就是原型對象與構造函數的關系。
使用繼承時應當把公共方法丟給背包而不是父親本身,這是與別的語言比較大的區別。