ECMAScript6(17):Class類

class聲明

class 是 ES6 模仿面嚮對象語言(C++, Java)提出的定義類的方法。形式相似 C++ 和 Java (各取所長), 下面例子展現了 class 是如何定義構造函數、對象屬性和對象動/靜態方法的:javascript

class Point{
  constructor(x, y){    //定義構造函數
    this.x = x;         //定義屬性x
    this.y = y;         //定義屬性y
  }                     //這裏沒有逗號
  toString(){           //定義動態方法,不須要 function 關鍵字
    return `(${this.x},${this.y})`;
  }
  static show(){        //利用 static 關鍵字定義靜態方法
    console.log("Static function!");
  }
}

var p = new Point(1,4);
console.log(p+"");               //(1,4)
console.log(typeof Point);       //"function"
console.log(Point.prototype.constructor === Point);    //true
console.log(Point.prototype.constructor === p.constructor);    //true
Point.show();      //"Static function!"

至關於傳統寫法:java

function Point(x, y){
  this.x = x;
  this.y = y;
}
Point.prototype.toString = function(){
  return `(${this.x},${this.y})`;
}
Point.show = function(){
  console.log("Static function!");
}
var p = new Point(1,4);
console.log(p+"");   //(1,4)

這裏不難看出,class 的類名就是 ES5 中的構造函數名,靜態方法就定義在其上,而類的本質依然是個函數。而 class 中除了 constructor 是定義的構造函數之外,其餘的方法都定義在類的 prototype 上,這都和 ES5 是一致的,這就意味着,ES5 中原有的那些方法均可以用, 包括但不限於:segmentfault

  • Object.keys(), Object.assign() 等等
  • 並且 class 也一樣支持表達式作屬性名,好比 Symbol
  • ES5 函數具備的屬性/方法:length、name、apply、call、bind、arguments 等等

但有些細節仍是有區別的,好比:數組

class Point{
  constructor(x, y){    //定義構造函數
    this.x = x;         //定義屬性x
    this.y = y;         //定義屬性y
  }                     //這裏沒有逗號
  toString(){           //定義動態方法,不須要 function 關鍵字
    return `(${this.x},${this.y})`;
  }
  getX(){
    return this.x;
  }
  getY(){
    return this.y;
  }
}
var p = new Point(1,4);
var keys = Object.keys(Point.prototype);
var ownKeys = Object.getOwnPropertyNames(Point.prototype);
console.log(keys);        //[]
console.log(ownKeys);     //["constructor", "toString", "getX", "getY"]
console.log(p.hasOwnProperty("toString"));                  //false
console.log(p.__proto__.hasOwnProperty("toString"));        //true
//ES5
function Point(x, y){
  this.x = x;
  this.y = y;
}
Point.prototype = {
  toString(){
    return `(${this.x},${this.y})`;
  },
  getX(){
    return this.x;
  },
  getY(){
    return this.y;
  }
}
var p = new Point(1,4);
var keys = Object.keys(Point.prototype);
var ownKeys = Object.getOwnPropertyNames(Point.prototype);
console.log(keys);        //["toString", "getX", "getY"]
console.log(ownKeys);     //["toString", "getX", "getY"]
console.log(p.hasOwnProperty("toString"));                  //false
console.log(p.__proto__.hasOwnProperty("toString"));        //true

這個例子說明,class 中定義的動態方法是不可枚舉的,而且 constructor 也是其自有方法中的一個。babel

使用 class 注意一下幾點:app

  • class 中默認是嚴格模式,即便不寫"use strict。關於嚴格模式能夠看:Javascript基礎(2) - 嚴格模式特色
  • 同名 class 不可重複聲明
  • class 至關於 object 而不是 map,不具備 map 屬性,也不具備默認的 Iterator。
  • constructor 方法在 class 中是必須的,若是沒有認爲指定,系統會默認生成一個空的 constructor
  • 調用 class 定義的類必須有 new 關鍵字,像普通函數那樣調用會報錯。ES5 不限制這一點。
TypeError: Class constructor Point cannot be invoked without 'new'
  • constructor 方法默認返回值爲 this,能夠認爲修改返回其餘的值,但這會致使一系列奇怪的問題:
class Point{
  constructor(x,y){
    return [x, y];
  }
}
new Point() instanceof Point;    //false
  • class 聲明類不存在變量提高
new Point();     //ReferenceError: Point is not defined
class Point{}

class 表達式

這個和麪向對象不同了,js 中函數能夠有函數聲明形式和函數表達式2種方式定義,那麼 class 同樣有第二種2種定義方式:class 表達式函數

var className1 = class innerName{
  //...
};
let className2 = class innerName{
  //...
};
const className3 = class innerName{
  //...
};

class 表達式由不少特性和 ES5 同樣:this

  • 和函數表達式相似,這裏的innerName能夠省略,並且innerName只有類內部可見,實際的類名是賦值號前面的 className。
  • 這樣定義的類的做用域,由其所在位置和聲明關鍵字(var, let, const)決定
  • const申明的類是個常量,不能修改。
  • 其變量聲明存在提高,但初始化不提高
  • class 表達式也不能和 class 申明重名

ES5 中有當即執行函數,相似的,這裏也有當即執行類:prototype

var p = new class {
  constructor(x, y){
    this.x = x;
    this.y = y;
  }
  toString(){
    return `(${this.x},${this.y})`;
  }
}(1,5);   //當即生成一個對象
console.log(p+"");    //(1,5)

getter, setter 和 Generator 方法

getter 和 setter 使用方式和 ES5 同樣, 這裏很少說了,舉個例子一看就懂:設計

class Person{
  constructor(name, age, tel){
    this.name = name;
    this.age = age;
    this.tel = tel;
    this._self = {};
  }
  get id(){
    return this._self.id;
  }
  set id(str){
    if(this._self.id){
      throw new TypeError("Id is read-only");
    } else {
      this._self.id = str;
    }
  }
}
var p = new Person("Bob", 18, "13211223344");
console.log(p.id);                //undefined
p.id = '30010219900101009X';
console.log(p.id);                //'30010219900101009X'

var descriptor = Object.getOwnPropertyDescriptor(Person.prototype, 'id');
console.log('set' in descriptor);       //true
console.log('get' in descriptor);       //true

p.id = '110';                     //TypeError: Id is read-only

Generator 用法也和 ES6 Generator 部分同樣:

class Person{
  constructor(name, age, tel){
    this.name = name;
    this.age = age;
    this.tel = tel;
    this._self = {};
  }
  *[Symbol.iterator](){
    var keys = Object.keys(this);
    keys = keys.filter(function(item){
      if(/^_/.test(item)) return false;
      else return true;
    });
    for(let item of keys){
      yield this[item];
    }
  }
  get id(){
    return this._self.id;
  }
  set id(str){
    if(this._self.id){
      throw new TypeError("Id is read-only");
    } else {
      this._self.id = str;
    }
  }
}
var p = new Person("Bob", 18, "13211223344");
p.id = '30010219900101009X';
for(let info of p){
  console.log(info);   //依次輸出: "Bob", 18, "13211223344"
}

class 的繼承

這裏咱們只重點講繼承,關於多態沒有新的修改,和 ES5 中同樣,在函數內判斷參數便可。關於多態能夠閱讀Javascript對象、類與原型鏈中關於多態重構的部分。

此外,class 繼承屬於 ES5 中多種繼承方式的共享原型,關於共享原型也在上面這篇文章中講解過。

class 實現繼承能夠簡單的經過 extends 關鍵字實現, 而使用 super 關鍵字調用父類方法:

//定義 '有色點'' 繼承自 '點'
class ColorPoint extends Point{    //這裏延用了上面定義的 Point 類
  constructor(x, y, color){
    super(x, y);     //利用 super 函數調用父類的構造函數
    this.color = color;
  }
  toString(){
    return `${super.toString()},${this.color}`;     //利用 super 調用父類的動態方法
  }
}
var cp = new ColorPoint(1, 5, '#ff0000');
console.log(cp+"");      //(1,5),#ff0000
ColorPoint.show();       //"Static function!"     靜態方法一樣被繼承了
cp instanceof ColorPoint;   //true
cp instanceof Point;   //true

使用 extends 繼承的時候須要注意一下幾點:

  • super 不能單獨使用,不能訪問父類屬性,只能方法父類方法和構造函數(super自己)
  • 子類沒有本身的 this,須要藉助 super 調用父類構造函數後加工獲得從父類獲得的 this,子類構造函數必須調用 super 函數。這一點和 ES5 徹底不一樣。
  • 子類若是沒有手動定義構造函數,會自動生成一個構造函數,以下:
constructor(...args){
  super(...args);
}
  • 子類中使用 this 關鍵字以前,必須先調用 super 構造函數
  • 因爲繼承屬於共享原型的方式,因此不要在實例對象上修改原型(Object.setPrototypeOf, obj.__proto__等)
  • super 也能夠用在普通是對象字面量中:
var obj = {
  toString(){
    return `MyObj ${super.toString()}`;
  }
}
console.log(obj+"");    //MyObj [object Object]

prototype__proto__

在 class 的繼承中

  • 子類的 __proto__ 指向其父類
  • 子類 prototype 的 __proto__ 指向其父類的 prototype
class Point{
  constructor(x, y){
    this.x = x;
    this.y = y;
  }
}
class ColorPoint extends Point{
  constructor(x, y, color){
    super(x, y);
    this.color = color;
  }
}
ColorPoint.__proto__  === Point;   //true
ColorPoint.prototype.__proto__ === Point.prototype;   //true

其等價的 ES5 是這樣的:

function Point(){
  this.x = x;
  this.y = y;
}
function ColorPoint(){
  this.x = x;
  this.y = y;
  this.color = color;
}
Object.setPrototypeOf(ColorPoint.prototype, Point.prototype);    //繼承動態方法屬性
Object.setPrototypeOf(ColorPoint, Point);                        //繼承靜態方法屬性

ColorPoint.__proto__  === Point;                      //true
ColorPoint.prototype.__proto__ === Point.prototype;   //true

這裏咱們應該理解一下3種繼承的 prototype 和 __proto__

  1. 沒有繼承
class A{}
A.__proto__  === Function.prototype;          //true
A.prototype.__proto__ === Object.prototype;   //true
  1. 繼承自 Object
class A extends Object{}
A.__proto__  === Object;                      //true
A.prototype.__proto__ === Object.prototype;   //true
  1. 繼承自 null
class A extends null{}
A.__proto__  === Function.prototype;        //true
A.prototype.__proto__ === undefined;        //true

判斷類的繼承關係:

class A{}
class B extends A{}
Object.getPrototypeOf(B) === A;     //true

子類的實例的 __proto____proto__ 指向其父類實例的 __proto__

class A{}
class B extends A{}
var a = new A();
var b = new B();
B.__proto__.__proto__ === A.__proto__;        //true

所以,能夠經過修改子類實例的 __proto__.__proto__ 改變父類實例的行爲。建議:

  • 老是用 class 取代須要 prototype 的操做。由於 class 的寫法更簡潔,更易於理解。
  • 使用 extends 實現繼承,由於這樣更簡單,不會有破壞 instanceof 運算的危險。

此外存取器和 Generator 函數均可以很理想的被繼承:

class Person{
  constructor(name, age, tel){
    this.name = name;
    this.age = age;
    this.tel = tel;
    this._self = {};
  }
  *[Symbol.iterator](){
    var keys = Object.keys(this);
    keys = keys.filter(function(item){
      if(/^_/.test(item)) return false;
      else return true;
    });
    for(let item of keys){
      yield this[item];
    }
  }
  get id(){
    return this._self.id;
  }
  set id(str){
    if(this._self.id){
      throw new TypeError("Id is read-only");
    } else {
      this._self.id = str;
    }
  }
}

class Coder extends Person{
  constructor(name, age, tel, lang){
    super(name, age, tel);
    this.lang = lang;
  }
}

var c = new Coder("Bob", 18, "13211223344", "javascript");
c.id = '30010219900101009X';
for(let info of c){
  console.log(info);   //依次輸出: "Bob", 18, "13211223344", "javascript"
}
console.log(c.id);     //'30010219900101009X'
c.id = "110";          //TypeError: Id is read-only

多繼承

多繼承指的是一個新的類繼承自已有的多個類,JavaScript 沒有提供多繼承的方式,因此咱們使用 Mixin 模式手動實現:

function mix(...mixins){
  class Mix{}
  for(let mixin of mixins){
    copyProperties(Mix, mixin);                         //繼承靜態方法屬性
    copyProperties(Mix.prototype, mixin.prototype);     //繼承動態方法屬性
  }

  return Mix;

  function copyProperties(target, source){
    for(let key of Reflect.ownKeys(source)){
      if(key !== 'constructor' && key !== "prototype" && key !== "name"){
        if(Object(source[key]) === source[key]){
          target[key] = {};
          copyProperties(target[key], source[key]);       //遞歸實現深拷貝
        } else {
          let desc = Object.getOwnPropertyDescriptor(source, key);
          Object.defineProperty(target, key, desc);
        }
      }
    }
  }
}

//使用方法:
class MultiClass extends mix(superClass1, superClass2, /*...*/){
  //...
}

因爲 mixin 模式使用了拷貝構造,構造出的子類的父類是 mix 函數返回的 class, 所以 prototype 和 __proto__ 與任一 superClass 都沒有直接的聯繫,instanceof 判斷其屬於 mix 函數返回類的實例,一樣和任一 superClass 都沒有關係。能夠這麼說:咱們爲了實現功能破壞了理論應該具備的原型鏈。

原生構造函數繼承

在 ES5 中,原生構造函數是不能繼承的,包括: Boolean(), Number(), Date(), String(), Object(), Error(), Function(), RegExp()等,好比咱們這樣實現:

function SubArray(){}
Object.setPrototypeOf(SubArray.prototype, Array.prototype);    //繼承動態方法
Object.setPrototypeOf(SubArray, Array);                        //繼承靜態方法

var arr = new SubArray();
arr.push(5);
arr[1] = 10;
console.log(arr.length);     //1  應該是2
arr.length = 0;
console.log(arr);            //[0:5,1:10]  應該爲空

很明顯這已經不是那個咱們熟悉的數組了!咱們能夠用 class 試試:

class SubArray extends Array{}
var arr = new SubArray();
arr.push(5);
arr[1] = 10;
console.log(arr.length);     //2
arr.length = 0;
console.log(arr);            //[]

仍是熟悉的味道,對吧!這就和以前提到的繼承差別有關了,子類沒有本身的 this,須要藉助 super 調用父類構造函數後加工獲得從父類獲得的 this,子類構造函數必須調用 super 函數。而 ES5 中先生成子類的 this,而後把父類的 this 中的屬性方法拷貝過來,咱們都知道,有的屬性是不可枚舉的,而有的屬性是 Symbol 名的,這些屬性不能很好的完成拷貝,就會致使問題,好比 Array 構造函數的內部屬性 [[DefineOwnProperty]]

利用這個特性,咱們能夠定義本身的合適的類, 好比一個新的錯誤類:

class ExtendableError extends Error{
  constructor(message){
    super(message);
    this.stack = new Error().stack;
    this.name = this.constructor.name;
  }
}
throw new ExtendableError("test new Error");   //ExtendableError: test new Error

靜態屬性

爲什麼靜態屬性須要單獨寫,而靜態方法直接簡單帶過。由於這是個兼容性問題,目前 ES6 的靜態方法用 static 關鍵字,可是靜態屬性和 ES5 同樣,須要單獨定義:

class A{}
A.staticProperty = "staticProperty";
console.log(A.staticProperty);      //"staticProperty"

不過 ES7 提出能夠在 class 內部實現定義,惋惜目前不支持,可是還好有 babel 支持:

class A{
  static staticProperty = "staticProperty";   //ES7 靜態屬性
  instanceProperty = 18;                      //ES7 實例屬性
}
console.log(A.staticProperty);                //"staticProperty"
console.log(new A().instanceProperty);        //18

new.target 屬性

new 原本是個關鍵字,但 ES6 給它添加了屬性——target。該屬性只能在構造函數中使用,用來判斷構造函數是否做爲構造函數調用的, 若是構造函數被 new 調用返回構造函數自己,不然返回 undefined:

function Person(){
  if(new.target){
    console.log("constructor has called");
  } else {
    console.log("function has called");
  }
}

new Person();     //"constructor has called"
Person();         //"function has called"

這樣咱們能夠實現一個構造函數,只能使用 new 調用:

function Person(name){
  if(new.target === Person){
    this.name = name;
  } else {
    throw new TypeError("constructor must be called by 'new'");
  }
}

new Person('Bob');     //"constructor has called"
Person();              //TypeError: constructor must be called by 'new'
Person.call({});       //TypeError: constructor must be called by 'new'

這裏須要注意:父類構造函數中的 new.target 會在調用子類構造函數時返回子類,所以使用了該屬性的類不該該被實例化,只用於繼承,相似於 C++ 中的抽象類。

class Person{
  constructor(name){
    if(new.target === Person){
      this.name = name;
    } else {
      throw new TypeError("constructor must be called by 'new'");
    }
  }
}
class Coder extends Person{}
new Coder('Bob');     //TypeError: constructor must be called by 'new' 這不是咱們想要的
//抽象類實現
class Person{
  constructor(name){
    if(new.target === Person){
      throw new TypeError("This class cannot be instantiated");
    }
    this.name = name;
  }
}
class Coder extends Person{}
var c = new Coder('Bob');
console.log(c.name);   //'Bob'
new Person('Bob');     //TypeError: This class cannot be instantiated

關於抽象類這裏解釋一下,要一個類不能實例化只能繼承用什麼用?

在繼承中產生歧義的緣由有多是繼承類繼承了基類屢次,從而產生了多個拷貝,即不止一次的經過多個路徑繼承類在內存中建立了基類成員的多份拷貝。抽象類的基本原則是在內存中只有基類成員的一份拷貝。舉個例子,一個類叫"動物",另有多各種繼承自動物,好比"胎生動物"、"卵生動物",又有多個類繼承自哺乳動物, 好比"人", "貓", "狗",這個例子好像複雜了,不過很明顯,被實例化的必定是一個個體,好比"人", "貓", "狗"。而"胎生動物",不該該被實例化爲一個個體,它僅僅是人類在知識領域,爲了分類世間萬物而抽象的一個分類。可是面向對象設計要求咱們把共性放在一塊兒以減小代碼,所以就有了抽象類。因此胎生動物都會運動,均可以發出聲音,這些就應該是共性放在"胎生動物"類中,而因此動物都會呼吸,會新陳代謝,這些共性就放在動物裏面,這樣咱們就不須要在"人", "貓", "狗"這樣的具體類中一遍遍的實現這些共有的方法和屬性。
相關文章
相關標籤/搜索