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一樣,在類內部可以使用get
和set
關鍵字,對某個屬性設定取值和賦值方法。
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
迴圈會自動呼叫這個遍歷器。