一篇文章圖文並茂地帶你輕鬆學完 JavaScript 繼承
技術標籤:JavaScriptjavascriptjsinheritance
JavaScript 繼承
在閱讀本文章之前,已經預設你瞭解了基礎的 JavaScript
語法知識,基礎的 ES6
語法知識 。
繼承種類
簡單的繼承種類可以分為
- 建構函式繼承
- 原型鏈繼承
- class繼承
- 寄生繼承
其中 class
繼承是 ES6
後提供的一種語法糖,方便其他面嚮物件語言的程式設計師更好的接受 JavaScript
中的繼承,本質上還是原型鏈繼承。
1. 建構函式繼承
function Person() {
this.name = "name";
this.eat = function() {
console.log("eat");
}
}
function Student() {
Person.call(this); // 繼承
this.age = 20;
}
const student = new Student();
console.log(student);
2. 原型鏈繼承
原型與原型鏈前置相關內容可以參考 點這裡
function Person() {
this.name = "name";
}
function Student() {
this .age = 20;
}
Student.prototype = new Person();
Student.prototype.constructor = Student; // 指利用 Student 建構函式 進行例項初始化
const stu = new Student();
console.log(stu.name); // "name"
console.log(stu);
利用在原型和原型鏈所學知識
Student
例項物件的 __proto__
將會指向 Person
例項,從而實現繼承的效果
stu
:
3. class繼承
constructor
是建構函式,可以結合原型鏈中的 constructor
class People {
constructor() {
this.name = "name";
}
}
class Student extends People {
constructor() {
super()
this.age = 20;
}
}
console.log(new Student())
可以發現,其實就是基於原型鏈繼承,只不過 constructor
是 class Student
4. 寄生繼承
在 JavaScript
設計模式中,有 工廠模式
,具體可以上網查詢
工廠模式
意味著只要傳入適當的引數 (加工),就會給予一個例項,就像工廠製造東西一樣。
而寄生繼承,用的就是工廠模式的思想
function People() {}
People.prototype.eat = function() {
console.log("eat");
}
function createInstance() {
const obj = Object.create(People.prototype)
Object.assign(obj, ...arguments);
return obj;
}
const stu1 = createInstance({ age: 20 });
console.log(stu1);
const stu2 = createInstance({ age: 30 });
console.log(stu2);
下面是 stu1
的列印結果
繼承優化
1. 建構函式繼承
利用 Student
構造出來的例項,屬性和方法是不共享的
function People(name) {
this.name = name;
this.eat = function () {
console.log("eat");
};
}
function Student(name) {
People.call(this, name);
this.age = 20;
}
const stu1 = new Student("huro");
const stu2 = new Student("lero");
console.log(stu1.name === stu2.name); // false
console.log(stu1.eat === stu2.eat); // false
對於方法來說我們希望是共享的,否則實際上浪費了很多記憶體。
2. 組合繼承
基於原型的方法是例項共享的,我們將方法放入原型,而屬性放在建構函式內,這樣就叫做組合繼承,組合繼承可以解決浪費多餘記憶體的問題。
function People(name) {
this.name = name;
}
People.prototype.sayName = function() {
console.log(this.name);
}
function Student() {
People.call(this);
this.age = "20";
}
Student.prototype = new People();
Student.prototype.constructor = Student;
const stu1 = new Student();
const stu2 = new Student();
console.log(stu1.sayName === stu2.sayName);
然而,還是有個缺點,我們列印 stu1
在 __proto__
中 有個 name
屬性,這個屬性其實我們是不需要的,我們希望每個例項能夠獨享屬性,這個 name
屬性的存在不但加大了記憶體開銷,還導致當 delete stu1.name
的時候,出現還能使用 stu1.name
的情況,這是我們不想要的
3. 組合寄生繼承
顧名思義,組合寄生繼承就是結合組合繼承和寄生繼承
function People(name) {
this.name = name;
}
People.prototype.sayName = function() {
console.log(this.name);
}
function Student() {
People.call(this);
this.age = "20";
}
Student.prototype = Object.create(People.prototype); // 實際上只變化這一行
Student.prototype.constructor = Student;
const stu1 = new Student();
const stu2 = new Student();
console.log(stu1.sayName === stu2.sayName);
通過這種方式創造的繼承,彌補了組合繼承的不足,節省了記憶體,並且使得例項共享方法獨享屬性。
那麼 ES6
語法提供的 class
是否也有這種 “聰明” 的設計呢?如果有的話,我們直接利用 class
就可以了
class
繼承
class People {
constructor() {
this.name = "name";
}
eat() {
console.log("eat");
}
}
class Student extends People {
constructor() {
super()
this.age = 20;
}
}
const stu1 = new Student();
const stu2 = new Student();
console.log(stu1.eat === stu2.eat); // true
extends
繼承的是原型鏈的方法
super
繼承的是獨享的屬性和方法
可以發現其實是和組合寄生繼承類似的
哦哦,那肯定啊,不然 ES6
不被噴死啊。
繼承優勢 (選擇)
用 ES6
的 class
語法有什麼優勢呢?
- 最大的優勢是在於可以繼承原生建構函式
原生建構函式
- Boolean
- Number
- String
- Array
- Date
- Function
- RegExp
- Error
- Object
在 ES5
語法中,你無法原生建構函式的屬性,你可能會嘗試這樣寫
const MyArray() {
Array.apply(this, arguments);
}
MyArray.prototype = Object.create(Array.prototype);
MyArray.prototype.constructor = MyArray;
當用這種方式繼承的時候,你會發現他與 Array
這個類的行為完全不一致
const names = new MyArray();
names[0] = "huro";
console.log(names.length); // 0
原生建構函式無法繫結 this
而 class繼承
可以
class MyArray extends Array {}
const names = new MyArray();
names[0] = "huro";
console.log(names.length); // 1
- 是否一定具有
__proto__
在原型和原型鏈章節中,我們說到例項的 __proto__
指向建構函式的 prototype
實際上並不是所有瀏覽器都是支援 __proto__
的,而 class
作為建構函式的語法糖,一定有這兩個屬性。
- 更嚴格的控制
function People(name) {
this.name = name;
this.eat = function () {
console.log("eat");
};
}
function Student(name) {
People.call(this, name);
this.age = 20;
}
const stu1 = Student("huro"); // new?
console.log(stu1);
利用建構函式例項化物件的時候,如果忘傳了 new
會怎麼樣,這個時候顯然也成立,因為會被當做一個函式看待,由於是全域性呼叫,因此 this
在瀏覽器環境下就是 window
這樣相當於給 window
賦值了 name
和 eat
這個時候直譯器也不會報錯,因為沒有任何方法可以區分一個函式是否是建構函式,因此可能出現意想不到的錯誤。
而用 class
方式繼承,好處就是如果不寫 new
直接報錯。
class MyArray extends Array {}
const names = MyArray(); // class constructor MyArray cannot be invoked without "new"
除此之外,在繼承的建構函式裡,如果沒寫 super
關鍵字或 super
不在建構函式頂部也會報錯
class MyArray extends Array {
constructor(){
// Must call super constructor in derived class before accessing 'this' or returning from derived constructor
}
}
總結
更嚴格的語法檢查,更多的優化,使得 class繼承
應該是目前來看最為優質的繼承方式。 為了能看懂他人的程式碼,以及更好的相容性,其他的繼承方式也要有所瞭解。