1. 程式人生 > 其它 >一篇文章圖文並茂地帶你輕鬆學完 JavaScript 繼承

一篇文章圖文並茂地帶你輕鬆學完 JavaScript 繼承

技術標籤:JavaScriptjavascriptjsinheritance

JavaScript 繼承

在閱讀本文章之前,已經預設你瞭解了基礎的 JavaScript 語法知識,基礎的 ES6 語法知識 。

繼承種類

簡單的繼承種類可以分為

  1. 建構函式繼承
  2. 原型鏈繼承
  3. class繼承
  4. 寄生繼承

其中 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())

在這裡插入圖片描述

可以發現,其實就是基於原型鏈繼承,只不過 constructorclass 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 就可以了

  1. 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 不被噴死啊。

繼承優勢 (選擇)

ES6class 語法有什麼優勢呢?

  1. 最大的優勢是在於可以繼承原生建構函式

原生建構函式

  1. Boolean
  2. Number
  3. String
  4. Array
  5. Date
  6. Function
  7. RegExp
  8. Error
  9. 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
  1. 是否一定具有 __proto__

在原型和原型鏈章節中,我們說到例項的 __proto__ 指向建構函式的 prototype

實際上並不是所有瀏覽器都是支援 __proto__ 的,而 class 作為建構函式的語法糖,一定有這兩個屬性。

  1. 更嚴格的控制
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 賦值了 nameeat

這個時候直譯器也不會報錯,因為沒有任何方法可以區分一個函式是否是建構函式,因此可能出現意想不到的錯誤。

而用 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繼承 應該是目前來看最為優質的繼承方式。 為了能看懂他人的程式碼,以及更好的相容性,其他的繼承方式也要有所瞭解。