ECMAScript 2015 -繼承

by vivid 25. 一月 2017 17:59

NET Magazine國際中文電子雜誌
作 者:許薰尹
審 稿:張智凱
文章編號: N170117902
出刊日期: 2017/01/25

本文介紹ECMAScript 2015繼承的語法。

繼承(Inheritance)

在ECMAScript 5通常採用Prototype Chaining方式來實作物件導向的繼承,繼承的動作有些煩瑣。Prototype Chaining實作繼承的定義步驟如下:

  • 定義父建構函式,以及父prototype。
  • 定義子建構函式。
  • 設定子建構函式的prototype屬性為父物件實體。
  • 將子建構函式的prototype.constructor屬性指回自己。

參考以下範例程式碼,使用Prototype Chaining繼承的範例,Sales繼承自Employee,需要改寫Sales的prototype,為一個Employee物件,並將Sales.prototype.constructor指回Sales:

var Employee = function ( id, name ) {
  this.id = id;
  this.name = name;
}

Employee.prototype.printInfo = function () {
  return ( this.id + " , " + this.name );
}

var Sales = function ( id, name, bonus ) {
  this.id = id;
  this.name = name;
  this.bonus = bonus;
}

Sales.prototype = new Employee();
Sales.prototype.constructor = Sales;
   
var aSale = new Sales( 1, "Mary",  1000);
console.log( aSale.id ); // 1
console.log( aSale.name ); // Mary
console.log( aSale.bonus ); // 1000
console.log( aSale.printInfo() ); / /1 , Mary

在ECMAScript 5,若要繼承屬性,通常會在Sales建構函式(子)之中,利用call方法叫用Employee建構函式(父)來繼承屬性,這些語法相當不親切,常常使得初學者無所適從,參考以下範例程式碼,:

var Employee = function ( id, name ) {
  this.id = id;
  this.name = name;
}

Employee.prototype.printInfo = function () {
  return ( this.id + " , " + this.name );
}

var Sales = function ( id, name, bonus ) {
  //繼承屬性
  Employee.call(this, id, name);
  this.bonus = bonus;
}

Sales.prototype = new Employee();
Sales.prototype.constructor = Sales;

var aSale = new Sales( 1,"Mary",  1000 );
console.log( aSale.id ); //1
console.log( aSale.name ); //Mary
console.log( aSale.bonus ); //1000
console.log( aSale.printInfo() ); //1 , Mary


 

ECMAScript 2015繼承

ECMAScript 2015讓繼承的語法變得較為簡單,新增一個extends關鍵字,來指定要繼承的父類別,這樣JavaScript引擎會自動設定適當的prototype,接著你還可以在子類別中使用super()方法來存取父類別的成員。

參考以下範例程式碼,Sales類別使用extends關鍵字繼承Employee類別,只要Sales類別繼承了Employee類別,雖然Sales類別沒有包含任何程式碼,但建立Sales物件之後,便可以叫用定義在Employee類別中的printInfo()方法,並存取到id與name屬性:

class Employee {
  constructor( id, name ) {
    this.id = id;
    this.name = name;
  }
  printInfo() {
    return ( this.id + " , " + this.name );
  }
}

class Sales extends Employee {
}

let s = new Sales( 1, "mary" );
console.log( s.id ); // 1
console.log( s.name ); // mary
console.log( s.printInfo() ); //1 , mary

console.log( s instanceof Sales ); //true
console.log( s instanceof Employee ); //true
console.log( Object.getPrototypeOf(Sales) === Employee ); //true

 

子類別的prototype會指向父prototype物件,因此「Object.getPrototypeOf(Sales) === Employee」程式的比較結果會回傳true。

若在子類別中定義了建構函式,則子類別的建構函式必需呼叫super()方法,否則會發生錯誤,若沒有指定建構函式,例如以下範例程式碼:

class Sales extends Employee {

}

上述這段程式碼就等同於以下範例程式碼,會自動使用super()方法傳入與父建構函式(constructor)相同個數的引數,來呼叫父類別的建構函式(constructor)程式:

class Sales extends Employee {
  constructor( id, name ) {
    super( id, name )
  }
}

 

 

上例程式也可以修改如下,在Sales類別建構函式中,使用super()方法,明確地呼叫父類别的建構函式,並傳入與父建構函式相同個數的引數做參數:

class Employee {
  constructor( id, name ) {
    this.id = id;
    this.name = name;
  }
  printInfo() {
   return ( this.id + " , " + this.name );
  }
}

class Sales extends Employee {
  constructor( id, name ) {
    super( id, name )
  }
}

let s = new Sales( 1, "mary" );
console.log( s.printInfo() ); //1 , mary

console.log( s instanceof Sales ); //true
console.log( s instanceof Employee ); //true

 

子類別也可以定義自己的建構函式,傳入子類別所需引數,引數個數不一定和父建構函式的一樣,同時在子類別的建構函式或方法之中,也可以使用super()關鍵字來存取父類別,參考以下範例程式碼,Sales類別的建構函式定義id、name與bonus三個引數,叫用super()傳入id與name當參數,叫用相同引數個數的父建構函式 :

class Employee {
  constructor( id, name ) {
    this.id = id;
    this.name = name;
  }
  printInfo() {
    return ( this.id + " , " + this.name );
  }
}

class Sales extends Employee {
  constructor( id, name, bonus ) {
    super( id, name );
    this.bonus = bonus;
  }
}

let s = new Sales( 1, "mary", 2000 );
console.log( s.bonus ); // 2000
console.log( s.printInfo() ); // 1 , mary

console.log( s instanceof Sales ); //true
console.log( s instanceof Employee ); //true

 

 

特別注意,只有在子類別中才能使用super()方法,否則會產生例外錯誤,例如以下範例程式碼,在沒有使用extends關鍵字定義繼承的Employee類別建構函式中使用super()方法,程式將無法執行:

class Employee {
  constructor(id, name) {
    super(); //Error
    this.id = id;
    this.name = name;
  }
  printInfo() {
    return (this.id + " , " + this.name);
  }
}

 

另外在子類別的建構函式之中,需要先叫用super()方法,才能夠使用this關鍵字,否則會產生例外錯誤。super()方法主要負責初始化this,參考以下範例程式碼,在Sales類別的建構函式中使用this,this會指向Sales :

class Sales extends Employee {
  constructor(id, name, bonus) {
    this.bonus = bonus; //Error
    super(id, name);
  }
}

 

改寫方法

若繼承下來的方法不適用,你可以改寫父類別的方法,只要在子類別定義一個和父類別同名的方法。定義在子類別中的方法,永遠會蓋掉父類別同名的方法。

參考以下範例程式碼,Sales繼承了Employee類別,若呼叫Sales物件的printInfo()方法,將會叫用到定義在Sales類別protototype中的printInfo()方法,而不會叫用到Employee.prototype.printInfo()方法:

 

class Employee {
  constructor( id, name ) {
    this.id = id;
    this.name = name;
  }
  printInfo() {
    return ( this.id + " , " + this.name );
  }
}

class Sales extends Employee {
  constructor( id, name, bonus ) {
    super( id, name )
    this.bonus = bonus;
  }

  printInfo() {
    return this.id + " , " + this.name + " , " + this.bonus;
  }
}

let s = new Sales( 1, "mary", 2000 );
console.log( s.printInfo() ); // 1 , mary , 2000
console.log( s instanceof Sales ); // true
console.log( s instanceof Employee ); // true

 

在子類別的方法中,也可以使用super()呼叫父類別的方法,參考以下範例程式碼,printInfo()方法中叫用父類別同名的printInfo()方法取得id與name屬性:

class Employee {
  constructor( id, name ) {
    this.id = id;
    this.name = name;
  }
  printInfo() {
    return ( this.id + " , " + this.name );
  }
}

class Sales extends Employee {
  constructor( id, name, bonus ) {
    super( id, name );
    this.bonus = bonus;
  }

  printInfo() {
    return super.printInfo() + " , " + this.bonus;
  }
}

let s = new Sales( 1, "mary",2000 );
console.log( s.printInfo() ); // 1 , mary , 2000
console.log( s instanceof Sales ); // true
console.log( s instanceof Employee ); // true

 

繼承靜態成員

若父類別包含靜態成員,可直接在子類別存取到此成員。因為子類別的prototype會被設定為父prototype,父prototype包含一個constructor屬性,指向父constructor,因此靜態方法也會被自動繼承下來,參考以下範例程式碼,定義一個靜態getCompanyName方法,我們可以使用「Sales.getCompanyName()」直接叫用此方法:

class Employee {
  constructor( id, name ) {
    this.id = id;
    this.name = name;
  }
  printInfo() {
    return ( this.id + " , " + this.name );
  }
  //靜態方法(Static Method)
  static getCompanyName() {
    return "UUU";
  }
}

class Sales extends Employee {
}

console.log( Sales.getCompanyName() ); //UUU

 

同樣的,也可以在子類別中,利用super()方法呼叫父靜態方法,參考以下範例程式碼,子類別Sales中包含一個getCompanyName()靜態方法,而此方法則叫用Employee類別的getCompanyName()靜態方法:

class Employee {
  constructor( id, name ) {
    this.id = id;
    this.name = name;
  }
  printInfo() {
    return ( this.id + " , " + this.name );
  }
  //靜態方法(Static Method)
  static getCompanyName() {
    return "UUU";
  }
}

class Sales extends Employee {

  static getCompanyName() {
    return super.getCompanyName() + " inc.";
  }
}

 

函式運算式繼承

ECMAScript 2015也可以繼承自函式運算式(Function Expression)語法,同樣使用extends關鍵字,這樣的好處是,你可以動態地決定要繼承的物件是誰。參考以下範例程式碼,Employee建構函式使用的是ECMAScript 5的語法定義,而Sales類別使用extends關鍵字繼承自Employee建構函式:

function Employee( id, name ) {
  this.id = id;
  this.name = name;
}

Employee.prototype.printInfo = function () {
  return ( this.id + " , " + this.name );
};

class Sales extends Employee {
  constructor( id, name, bonus ) {
    super( id, name );
    this.bonus = bonus;
  }

  printInfo() {
    return super.printInfo() + " , " + this.bonus;
  }
}

let s = new Sales( 1, "mary", 2000 );
console.log( s.printInfo() ); // 1 , mary ,2000
console.log( s instanceof Sales ); // true
console.log( s instanceof Employee ); // true   

 

由於繼承自函式運算式(Function Expression)意味著你可以動態決定要繼承的型別。參考以下範例程式碼,parentClass變數在執行階段被指定為Employee,而Sales 類別extends關鍵字之後,接parentClass變數,表示其值可以在執行階段指定:

function Employee( id, name ) {
  this.id = id;
  this.name = name;
}

Employee.prototype.printInfo = function () {
  return ( this.id + " , " + this.name );
};

let parentClass = Employee;

class Sales extends parentClass {
  constructor( id, name, bonus ) {
    super( id, name );
    this.bonus = bonus;
  }

  printInfo() {
    return super.printInfo() + " , " + this.bonus;
  }
}

let s = new Sales( 1, "mary", 2000 );
console.log( s.printInfo() ); // 1 , mary ,2000
console.log( s instanceof Sales ); // true
console.log( s instanceof Employee ); // true

extends關鍵字之後可以接任何的運算式,但繼承之後不一定能夠產生一個有效的類別。

在建構函式中使用new.target

你可以在類別的建構函式中使用new.target屬性,它預設會指向類別的建構函式(Constructor Function),因為class一定要搭配new關鍵字使用,不能直接當方法呼叫,因此new.target屬性一定會有值。參考以下範例程式碼,當使用new關鍵字建立Employee物件時,new.target的值被等同於Employee constructor :

class Employee {
  constructor( id, name ) {
    console.log( new.target == Employee ); //true
    this.id = id;
    this.name = name;
  }
  printInfo() {
    return ( this.id + " , " + this.name );
  }
}


let s = new Employee( 1, "mary" );
console.log( s.printInfo() ); // 1 , mary

但在繼承的情況下,new.target的值可能會有不同,參考以下範例程式碼,當Sales繼承Employee後,使用new建立Sales物件時,Employee類別建構函式中new.target的值會指向Sales的建構函式 :

class Employee {
  constructor( id, name ) {
    console.log( new.target == Employee ); //false
    console.log( new.target == Sales ); //true

    this.id = id;
    this.name = name;
  }
  printInfo() {
    return ( this.id + " , " + this.name );
  }
}

class Sales extends Employee {
  constructor( id, name, bonus ) {
    super( id, name )
    this.bonus = bonus;
  }

  printInfo() {
    return this.id + " , " + this.name + " , " + this.bonus;
  }
}

let s = new Sales( 1, "mary", 1000 );
console.log( s.printInfo() ); //1 , mary

Tags:

.NET Magazine國際中文電子雜誌 | JavaScript | 許薰尹Vivid Hsu

新增評論




  Country flag
biuquote
  • 評論
  • 線上預覽
Loading






NET Magazine國際中文電子雜誌

NET Magazine國際中文電子版雜誌,由恆逸資訊創立於2000,自發刊日起迄今已發行超過500篇.NET相關技術文章,擁有超過40000名註冊讀者群。NET Magazine國際中文電子版雜誌希望藉於電子雜誌與NET Developer達到共同學習與技術新知分享,歡迎每一位對.NET 技術有興趣的朋友們多多支持本雜誌,讓作者群們可以有持續性的動力繼續爬文。<請加入免費訂閱>

月分類Month List