iT邦幫忙

2023 iThome 鐵人賽

DAY 20
0
Modern Web

超低腦容量學習法遇到javascript系列 第 20

如何在js實現傳統oop的class繼承

  • 分享至 

  • xImage
  •  

雖然js只有prottype,但如果想實現OOP裡的class間的繼承,其實也發展出相對應的作法:透過手動連結兩個建構函式的prototype chain,就可以實現一個constructor繼承另一個constructor。這邊說的手動,就是使用Object.create()。

透過constructor

先從一個Person建構函式開始,裡面寫了未來使用new來新建instance會建立的屬性,並在Person的prototype屬性建立了方法,如此一來,instance可以透過prototype chain取用這個方法,做到看起來像是繼承的prototypal inheritance。

const Person = function (firstName, birthYear) {
  this.firstName = firstName;
  this.birthYear = birthYear;
};

Person.prototype.calcAge = function () {
  return 2023 - this.birthYear;
};

接著我們建了另一個建構函式Student,想實現Student為Person的child class,也就是從Person那繼承原本就有的屬性與方法,另外又有自己獨特的屬性及方法(可依需求而變)。

在屬性的部分,我們製造兩者連結的方法是在Student呼叫Person,所以Person的屬性若有變更,Student的部份就會隨之而變。而在這邊使用call()的原因是,其實呼叫Person就是呼叫一個一般函式,所以Person裡的this指向的是undefined,故使用call()來手動設定this指向未來新增instance的空白物件(與this.major的this是同一個東西)。同時也可以視情況為child class新增屬性,這邊增加了主修科目。

const Student = function (firstName, birthYear, major) {
  Person.call(this, firstName, birthYear);
  this.major = major;
};

在方法的部分,則是使用Object.create()將Person.prototype裡的方法寫進Student.prototype,讓Student的instance能夠繼承。...才怪!!因為我們現在要做的是Student繼承Person啊,所以當然是寫在Student的[[prototype]]裡啊...才怪!!!
其實應該是要寫進Student.prototype的[[prototype]]才對啦!!

事情是這樣的,因為我們希望Student的instance也能使用Person的instance能用的方法,將Person.prototype寫進Student.prototype [[prototype]]才能建立正確的prototype chain。下圖要從紅房子往上看,也就是prototype chain的查找方向。

Student.prototype = Object.create(Person.prototype);

看一下以new建立Student的instance的結果,於是就達成class之間的繼承了。

const cindy = new Student("Cindy", 1996, "Finance");
console.log(cindy);
console.log(cindy.calcAge());

透過ES6 class

使用ES6 class來做OOP class的繼承,一切輕鬆簡單,如果沒有要增加新的屬性或方法,一行就做完了

class PersonCl {
  constructor(firstName, birthYear) {
    this.firstName = firstName;
    this.birthYear = birthYear;
  }
  calcAge() {
    return 2023 - this.birthYear;
  }
}

class StudentCl extends PersonCl {}

若要新增屬性,在constructor裡呼叫super(),然後在寫要多加的屬性。

class StudentCl extends PersonCl {
  constructor(firstName, birthYear, major) {
    super(firstName, birthYear);
    this.major = major;
  }
}

如果要新增或修改繼承來的方法,則直接在StudentCl裡加多方法即可。需要注意的是,這個新增或修改,其實是寫進子類別的[[prototype]],依prototype chain找的時候,會先找到自己的[[prototype]],而非修改了父類別那邊的方法。

class StudentCl extends PersonCl {
  constructor(firstName, birthYear, major) {
    super(firstName, birthYear);
    this.major = major;
  }
  greet() {
    return `I'm ${this.firstName} and my major is ${this.major}`;
  }
}

透過Object.create()

使用Object.create()來實現class間的繼承,所用的想法是把需要被繼承的東西直接寫進對方的[[prototype]]裡。像是想建立一個Person的instance,就直接把建立屬性的方法及其他方法寫進instance的[[prototype]],像是例子中的john。

const PersonProto = {
  createProperty(firstName, birthYear) {
    this.firstName = firstName;
    this.birthYear = birthYear;
  },
  calcAge() {
    return 2023 - this.birthYear;
  },
};

const john = Object.create(PersonProto);
john.createProperty("John", 1995);

下圖是從上面抓下來改寫的,但其實概念也都是一樣的,從紅房子開始往上看,就可以建立prototype chain,也就知道程式該怎麼寫

把PersonProto先寫進StudentProto的[[prototype]],再把StudentProto寫進instance from Student的[[prototype]]

const StudentProto = Object.create(PersonProto);
const cindy = Object.create(StudentProto);

如果只單純繼承屬性和方法而無新增或修改,其實這樣就能作用了

cindy.createProperty("Cindy", 2000);
console.log(cindy);

但一般而言還是會客製化一些子類別特有的屬性和方法,則是在instance的[prototype]新增方法,如此一來,當follow prototype chain查找時,會先找到第一層也就是有新增屬性的createProperty(),其效果就像是覆蓋了從父類別那邊來的方法。

StudentProto.createProperty = function (firstName, birthYear, major) {
  PersonProto.createProperty.call(this, firstName, birthYear);
  this.major = major;
};
const cindy = Object.create(StudentProto);
cindy.createProperty("Cindy", 2000, "Finance");

console.log(cindy);

今日總結

三種方法各有不同特性,constructor的方法讓人體會prototype的運作機制;ES6 class的結構真的友善很多,好寫也好讀;而Object.create()提供了更靈活的使用方法。

Reference

udemy-The Complete Javascript Course


上一篇
getter & setter 的應用
下一篇
同步&非同步
系列文
超低腦容量學習法遇到javascript30
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言