Class 類

class聲明

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

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!"

相當于傳統寫法:

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 中原有的那些方法都可以用, 包括但不限于:

  • 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 也是其自有方法中的一個。

使用 class 注意一下幾點:

  • class 中默認是嚴格模式,即使不寫"use strict。關于嚴格模式可以看:嚴格模式特點
  • 同名 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 一樣:

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

ES5 中有立即執行函數,類似的,這里也有立即執行類:

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 中一樣,在函數內判斷參數即可。關于多態可以閱讀對象、類與原型鏈中關于多態重構的部分。

此外,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

關于抽象類這里解釋一下,要一個類不能實例化只能繼承用什么用?

在繼承中產生歧義的原因有可能是繼承類繼承了基類多次,從而產生了多個拷貝,即不止一次的通過多個路徑繼承類在內存中創建了基類成員的多份拷貝。抽象類的基本原則是在內存中只有基類成員的一份拷貝。舉個例子,一個類叫"動物",另有多各類繼承自動物,比如"胎生動物"、"卵生動物",又有多個類繼承自哺乳動物, 比如"人", "貓", "狗",這個例子好像復雜了,不過很明顯,被實例化的一定是一個個體,比如"人", "貓", "狗"。而"胎生動物",不應該被實例化為一個個體,它僅僅是人類在知識領域,為了分類世間萬物而抽象的一個分類。但是面向對象設計要求我們把共性放在一起以減少代碼,因此就有了抽象類。所以胎生動物都會運動,都可以發出聲音,這些就應該是共性放在"胎生動物"類中,而所以動物都會呼吸,會新陳代謝,這些共性就放在動物里面,這樣我們就不需要在"人", "貓", "狗"這樣的具體類中一遍遍的實現這些共有的方法和屬性。

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容

  • class的基本用法 概述 JavaScript語言的傳統方法是通過構造函數,定義并生成新對象。下面是一個例子: ...
    呼呼哥閱讀 4,129評論 3 11
  • ES6 class類知識點梳理 大概從幾個方面來講解ES6 class類和傳統的構造函數的區別。 必須要有cons...
    sunny519111閱讀 486評論 0 1
  • 1.簡單介紹 JavaScript傳統的方法是通過構造函數定義并生成新對象;且ES5里面的繼承是通過原型鏈來實現的...
    _花閱讀 296評論 0 1
  • 注:此篇文章是我參考阮一峰老師的[ECMAScript 6 入門]文章,自己記錄的筆記,詳細的內容請移步阮一峰老師...
    HW_____T閱讀 531評論 0 1
  • 突如其來的慵懶聲音,讓得牧塵面龐上的神情一點點的凝固下來,片刻后,他終于是有些回過神來,但依舊還是有些難以置信的喃...
    混沌天書閱讀 196評論 0 0