JavaScript 是基於原型的物件導向典範,然而,模擬類別的需求一直都在,只是各自有不同的模擬方式與風格,這就造成了不同風格間要互動合作時的不便。
另一方面,雖然基於原型的 JavaScript 可以很有彈性地模擬不同風格的類別,然而,有些特性的模擬有其困難,像是在繼承時能透過 super 之類的方式呼叫父類方法等。
在基本的類別模擬需求上,為了能提供一致的風格基礎,也為了直接在語法上提供功能,以便能解決過去模擬類別時遇到的一些困難,ES6 提供了類別語法,若它的語法能解決需求就會建議採用,當然,若需要的類別特性無法使用 ES6 類別語法來實現,基於原型的方式仍然適用。
以〈模擬類別的封裝與繼承〉中看到的例子來說:
function Person(name, age) {
this.name = name;
this.age = age;
}
Person.prototype.toString = function() {
return '[' + this.name + ',' + this.age + ']';
};
var p = new Person('Justin', 35);
console.log(p.name); // Justin
在 ES6 中可以寫為:
class Person {
constructor(name, age) {
this.name = name;
this.age = age;
}
toString() {
return `[${this.name}, ${this.age}`;
}
}
let p = new Person('Justin', 35);
console.log(p.name); // Justin
如果你熟悉其他語言中的類別語法,看到這個 ES6 類別,應該馬上就能理解它的意義,本質上來說,上面的類別語法很大的成份,可以視為前一個基於原因的範例之語法蜜糖。
對 ES6 類別來說,Person 本身是個 Function 實例,toString 則是定義在 Person.prototype 上的一個特性,而 Person.prototype.constructor 參考的就是 Person,這些都與 ES5 中對應的定義相同,如果透過 Person.prototype 添加特性,那麼 Person 的實例也會找得到該特性;你也可以直接將 toString 參考的函式指定給某個變數,或者是指定為另一物件的特性,透過該物件來呼叫函式,該函式的 this 一樣是依呼叫者而決定;每個透過 new Person(...) 建構出來的實例,本身的原型(__proto__)也都是參考至 Person.prototype。
然而不同的是,使用 class 定義的 Person 只能使用 new 來建立實例,直接使用 Person(...)、Person.call(...) 或 Person.apply(...) 都會發生 TypeError,而 Person 也不會是定義在全域物件上的一個特性,另外,class 定義的名稱,就像 let 宣告的名稱那樣,不會有 Hoist 的效果,因此在定義 Person 類別之前,就嘗試 new Person(...),會發生 ReferenceError。
另一方面,class 中定義的方法,雖然是等同於定義在 prototype 上,然而特性描述器上的 enumerable 為 false,因此 for in、Object.hasOwnProperty、Object.keys 無法列舉,只能透過 Object.getOwnPropertyNames,因為這個函式會一併取得可列舉與無法列舉的特性名稱。
在類別中,constructor 定義了建構式,如果類別中沒有撰寫 constructor,也會自動加入一個無參數的 constructor() {},constructor 最後隱含地 return this,如果在 constructor 明確地 return 某個物件,那麼 new 的結果就會是該物件。
在 ES6 的類別中也可以使用 [] 來定義方法,[] 中可以是字串、運算式的結果或者是 Symbol,例如:
class Range {
constructor(start, end) {
this.start = start;
this.end = end;
}
[Symbol.iterator]() {
let i = this.start;
let end = this.end;
return {
next() {
return i < end ?
{value: i++, done: false} :
{value: undefined, done: true}
}
};
}
toString() {
return `Range [${this.start}...${this.end - 1}]`;
}
}
let range = new Range(1, 4);
for(let i of range) {
console.log(i); // 顯示 1 2 3
}
console.log(range.toString()); // 顯示 Range [1...3]
你也可以在 class 中定義產生器函式:
class Range {
constructor(start, end) {
this.start = start;
this.end = end;
}
*[Symbol.iterator]() {
for(let i = this.start; i < this.end; i++) {
yield i;
}
}
toString() {
return `Range [${this.start}...${this.end - 1}]`;
}
}
let range = new Range(1, 4);
for(let i of range) {
console.log(i); // 顯示 1 2 3
}
console.log(range.toString()); // 顯示 Range [1...3]
在 ES6 的類別語法下,定義一個特性的 setter、getter 變得比較容易了,例如:
class Person {
constructor(name, age) {
this.__name__ = name;
this.__age__ = age;
}
toString() {
return `[${this.__name__}, ${this.__age__}`;
}
get name() {
return this.__name__;
}
get age() {
return this.__age__;
}
}
var p = new Person('Justin', 35);
console.log(p.name); // Justin
如果要定義 getter 的話,在方法前加上 get,若是定義 setter 的話,在方法前使用 set,上頭模擬了物件的私有成員,在 ES6 類別中,並沒有定義私有成員的語法,你還是要以模擬的方式來定義。
在 ES6 的類別中,若方法前加上 static,那麼該方法會是個靜態方法,也就是以類別名稱為名稱空間的一個函式:
class Foo {
static orz() {
}
}
與自行定義 Foo 函式,然而在函式上定義 orz 特性不同的是,在 class 上定義的靜態方法,子類別可以便於找到而使用,例如若有個 class Foo2 extends Foo {},那麼 Foo2.orz() 是可以呼叫的,ES6 中並沒有定義靜態特性的方式,因此仍只能使用 Foo.CONT = 123 這樣的方式來模擬。
類別也可以使用運算式的方式來建立,必要時也可以給予名稱:
> let clz = class {
... constructor(name) { this.name = name; }
... };
undefined
> new clz('xyz');
clz { name: 'xyz' }
> var clz2 = class Xyz {
... constructor(name) { this.name = name; }
... };
undefined
> new clz2('xyz');
Xyz { name: 'xyz' }
>
在 ES6 中新增了 new.target,如果函式或類別的建構式中有 new.target,在使用 new 建構實例時,new.target 代表了建構式或類別本身,否則就會是 undefined,因此,在傳統的建構式定義時,可以如下檢查,以達到強制使用 new 來建構物件的效果:
function Person(name, age) {
if (new.target === Person) {
this.name = name;
this.age = age;
} else {
throw new TypeError(
"Constructor Person cannot be invoked without 'new'");
}
}
在方法中,如果要遞迴呼叫,必須在方法前加上 this,明確指定是呼叫自身方法,否則會嘗試呼叫範圍內可找到的函式,若找不到就是 ReferenceError。

