原型基礎
每個對象都有一個原型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的繼承處理的和其他語言還是有所不同,構造函數相當於父親,這個父親有一個背包就是原型對象。當他的兒子要去用方法時就去找父親的背包,父親的背包沒找到就找爺爺的背包。
而在這個背包中有一張字條,就是父親的名字。
以上就是原型對象與構造函數的關系。
使用繼承時應當把公共方法丟給背包而不是父親本身,這是與別的語言比較大的區別。
