1. 程式人生 > >ES6 Class 類

ES6 Class 類

從ES6(ES2015)開始,JS提出了類(Class)概念,JS中的類只是JS現有的、基於原型的繼承的一種語法包裝(語法糖),它能讓我們用理簡明的語法實現繼承。

定義類

ES6中的類實際就是一個函式,且正如函式的定義方式有函式宣告和函式表示式兩種方式一樣,類的定義也有兩種方式,分別為:

  • 類宣告
  • 類表示式

類宣告

類宣告是定義類的一種方式,使用class關鍵字後跟一個類名,就可以定義一個類。如下:

class Foo {
    constructor() {
        // ..
    }
}

不存在變數提升(hoist)

類宣告和函式宣告不同的一點是,函式宣告存在變數提升現象,而類宣告不會。即,類必須先宣告,然後才能使用,否則會丟擲ReferenceError

異常。

var foo = new Foo(); // Uncaught ReferenceError: Foo is not defined(...)
class Foo {
    // ...
}

這種規定的原因與類的繼承有關,必須保證子類在父類之後定義。

let Foo = class {};

class Bar extends Foo {
}

上面的程式碼不會報錯,因為class Bar繼承Foo時,Foo已經有定義了。但是,如果存在Class提升,上面程式碼就會報錯,因為Class Bar會被提升到程式碼頭部,而表示式式Foo是不會提升的,所以導致Class Bar繼承Foo

的時候,Foo還沒有定義。

類表示式

類表示式就定義類的另外一種方式,就像函式表示式一樣,在類表示式中,類名是可有可無的。若定義的類名,則該類名只有的類的內部才可以訪問到。

// 方式一
const MyClass = class {};

// 方式二:給出類名
const MyClass = class Me {
    getClassName() {
        return Me.name;
    }
};

上面方式二定義類的同時給出了類名,此時,Me類名只可以在Class的內部程式碼可用,指代當前類。MyClass的name屬性值為給出的類名。

let my = new
MyClass(); my.getClassName(); // Me Me.name; // Uncaught ReferenceError: Me is not defined(…) MyClass.name; // Me

採用類表示式,可以寫出立即執行的Class。如下:

let person = new class {
    constructor(name) {
        this.name = name;
    }

    sayName() {
        console.log(this.name);
    }
}('Zhang San');

person.sayName(); // Zhang San

類體和方法定義

類的成員需要定義在一對大括號內{},大括號內的程式碼的大括號本身組成了類體。類成員包括類構造器和類方法(包括靜態方法和例項方法)。

嚴格模式

類體中的程式碼都強制在嚴格模式中執行,即預設”use strict”。考慮到未來所有的程式碼,其實都是執行在模組之中,所以ES6實際上把整個語言升級到了嚴格模式。

構造器(constructor方法)

constructor方法是一個特殊的類方法,它既不是靜態方法也不是例項方法,它僅在例項化的時候被呼叫。一個類只能擁有一個名為constructor的方法,否則會丟擲SyntaxError異常。

如果沒有定義constructor方法,這個方法會被預設新增,即,不管有沒有顯示定義,任何一個類都有constructor方法。

子類必須在constructor方法中呼叫super方法,否則新建例項時會報錯。因為子類沒有自己的this物件,而是繼承父類的this物件,然後對其進行加工,如果不呼叫super方法,子類就得不到this物件。

class Point {}

class ColorPoint extends Point {
    constructor() {}
}

let cp = new ColorPoint(); // ReferenceError

上面程式碼中,ColorPoint繼承了父類Point,但是它的建構函式沒有呼叫super方法,導致新建例項時報錯。

原型方法

定義類的方法時,方法名前面不需要加上function關鍵字。另外,方法之間不需要用逗號分隔,加了會報錯。

class Bar {
    constructor() {}

    doStuff() {}

    toString() {}

    toValue() {}
}

類的所有方法都是定義在類的prototype屬性上的,上面的寫法等同於下面:

Bar.prototype = {
    doStuff() {},
    toString() {},
    toValue() {}
};

所以,在類的例項上呼叫方法,實際上就是呼叫原型上的方法。

class B {}
let b = new B();

b.constructor === B.prototype.constructor; // true

上面程式碼中,b是B類的例項,它的constructor方法就是B類原型的constructor方法。
由於類的方法都是定義在prototype上面,所以類的新方法可以新增在prototype物件上面。Object.assign方法可以很方便地一次向類新增多個方法。

class Point {
    constructor() {
        // ...
    }
}

Object.assign(Point.prototype, {
    toString() {},
    toValue() {}
});

另外,類的內部所有定義的方法,都是不可列舉的(non-enumerable)。

class Point {
    constructor(x, y) {
        // ...
    }

    toString() {
        return '(' + x + ', ' + y + ')';
    }
}

Object.keys(Point.prototype); // []
Object.getOwnPropertyNames(Point.prototype); // ["constructor", "toString"]
Object.getOwnPropertyDescriptor(Point, 'toString');
// Object {writable: true, enumerable: false, configurable: true}

靜態方法

static關鍵字用來定義類的靜態方法。靜態方法是指那些不需要對類進行例項化,使用類名就可以直接訪問的方法。靜態方法經常用來作為工具函式。

class Point {
    constructor(x, y) {
        this.x = x;
        this.y = y;
    }

    static distance(a, b) {
        const dx = a.x - b.x;
        const dy = a.y - b.y;

        return Math.sqrt(dx*dx + dy*dy);
    }
}

const p1 = new Point(5, 5);
const p2 = new Point(10, 10);

console.log(Point.distance(p1, p2));

靜態方法不可以被例項繼承,是通過類名直接呼叫的。但是,父類的靜態方法可以被子類繼承。

class Foo {
  static classMethod() {
    return 'hello';
  }
}

class Bar extends Foo {
}

Bar.classMethod(); // "hello"

靜態方法也可以用super關鍵字呼叫。

class Foo {
  static classMethod() {
    return 'hello';
  }
}

class Bar extends Foo {
  static classMethod() {
    return super.classMethod() + ', too';
  }
}

Bar.classMethod(); // "hello too"

extends關鍵字

extends關鍵字用於實現類之間的繼承。子類繼承父類,就繼承了父類的所有屬性和方法。
extends後面只可以跟一個父類。

super 關鍵字

super關鍵字可以用來呼叫其父類的構造器或方法。

class Cat { 
  constructor(name) {
    this.name = name;
  }

  speak() {
    console.log(this.name + ' makes a noise.');
  }
}

class Lion extends Cat {
  speak() {
    super.speak();
    console.log(this.name + ' roars.');
  }
}

類的Getter和Setter方法

與ES5一樣,在類內部可以使用getset關鍵字,對某個屬性設定取值和賦值方法。

class Foo {
    constructor() {}

    get prop() {
        return 'getter';
    }

    set prop(val) {
        console.log('setter: ' + val);
    }
}

let foo = new Foo();

foo.prop = 1;
// setter: 1

foo.prop;
// "getter"

上面程式碼中,prop屬性有對應 的賦值和取值方法,因此賦值和讀取行為都被自定義了。
存值和取值方法是設定在屬性的descriptor物件上的。

var descriptor = Object.getOwnPropertyDescriptor(Foo.prototype, 'prop');

"get" in descriptor // true
"set" in descriptor // true

上面程式碼中,存值和取值方法是定義在prop屬性的描述物件上的,這與ES5一致。

類的Generator方法

如果類的某個方法名前加上星號(*),就表示這個方法是一個Generator函式。

class Foo {
  constructor(...args) {
    this.args = args;
  }
  * [Symbol.iterator]() {
    for (let arg of this.args) {
      yield arg;
    }
  }
}

for (let x of new Foo('hello', 'world')) {
  console.log(x);
}
// hello
// world

上面程式碼中,Foo類的Symbol.iterator方法前有一個星號,表示該方法是一個Generator函式。Symbol.iterator方法返回一個Foo類的預設遍歷器,for...of迴圈會自動呼叫這個遍歷器。