Class基本語法
概述
JavaScript語言的傳統方法是通過構造函數,定義並生成新對象。下面是一個例子。
function Point(x,y){ this.x = x; this.y = y; } Point.prototype.toString = function () { return '(' + this.x + ', ' + this.y + ')'; };
上面這種寫法跟傳統的面向對象語言(比如C++和Java)差異很大,很容易讓新學習這門語言的程序員感到困惑。
ES6提供了更接近傳統語言的寫法,引入了Class(類)這個概念,作為對象的模板。通過class
關鍵字,可以定義類。基本上,ES6的class可以看作只是一個語法糖,它的絕大部分功能,ES5都可以做到,新的class
寫法只是讓對象原型的寫法更加清晰、更像面向對象編程的語法而已。上面的代碼用ES6的“類”改寫,就是下面這樣。
//定義類 class Point { constructor(x, y) { this.x = x; this.y = y; } toString() { return '(' + this.x + ', ' + this.y + ')'; } }
上面代碼定義了一個“類”,可以看到里面有一個constructor
方法,這就是構造方法,而this
關鍵字則代表實例對象。也就是說,ES5的構造函數Point
,對應ES6的Point
類的構造方法。
Point類除了構造方法,還定義了一個toString
方法。注意,定義“類”的方法的時候,前面不需要加上function
這個關鍵字,直接把函數定義放進去了就可以了。另外,方法之間不需要逗號分隔,加了會報錯。
構造函數的prototype
屬性,在ES6的“類”上面繼續存在。事實上,類的所有方法都定義在類的prototype
屬性上面。
class Point { constructor(){ // ... } toString(){ // ... } toValue(){ // ... } } // 等同於 Point.prototype = { toString(){}, toValue(){} };
在類的實例上面調用方法,其實就是調用原型上的方法。
class B {} let b = new B(); b.constructor === B.prototype.constructor // true
類的內部所有定義的方法,都是不可枚舉的(non-enumerable)。
constructor方法
constructor
方法是類的默認方法,通過new
命令生成對象實例時,自動調用該方法。一個類必須有constructor
方法,如果沒有顯式定義,一個空的constructor
方法會被默認添加。
constructor
方法默認返回實例對象(即this
),完全可以指定返回另外一個對象。
class Foo { constructor() { return Object.create(null); } } new Foo() instanceof Foo // false
上面代碼中,constructor
函數返回一個全新的對象,結果導致實例對象不是Foo
類的實例。
類的實例對象
生成類的實例對象的寫法,與ES5完全一樣,也是使用new
命令。如果忘記加上new
,像函數那樣調用Class
,將會報錯。
// 報錯 var point = Point(2, 3); // 正確 var point = new Point(2, 3);
與ES5一樣,實例的屬性除非顯式定義在其本身(即定義在this
對象上),否則都是定義在原型上(即定義在class
上)。
與ES5一樣,類的所有實例共享一個原型對象。
這也意味着,可以通過實例的__proto__
屬性為Class添加方法。
var p1 = new Point(2,3); var p2 = new Point(3,2); p1.__proto__.printName = function () { return 'Oops' }; p1.printName() // "Oops" p2.printName() // "Oops" var p3 = new Point(4,2); p3.printName() // "Oops"
上面代碼在p1
的原型上添加了一個printName
方法,由於p1
的原型就是p2
的原型,因此p2
也可以調用這個方法。而且,此后新建的實例p3
也可以調用這個方法。這意味着,使用實例的__proto__
屬性改寫原型,必須相當謹慎,不推薦使用,因為這會改變Class的原始定義,影響到所有實例。
name屬性
由於本質上,ES6的Class只是ES5的構造函數的一層包裝,所以函數的許多特性都被Class繼承,包括name
屬性。
class Point {} Point.name // "Point"
name
屬性總是返回緊跟在class
關鍵字后面的類名。
Class表達式
與函數一樣,Class也可以使用表達式的形式定義。
const MyClass = class Me { getClassName() { return Me.name; } };
上面代碼使用表達式定義了一個類。需要注意的是,這個類的名字是MyClass
而不是Me
,Me
只在Class的內部代碼可用,指代當前類。
let inst = new MyClass(); inst.getClassName() // Me Me.name // ReferenceError: Me is not defined
上面代碼表示,Me
只在Class內部有定義。
如果Class內部沒用到的話,可以省略Me
,也就是可以寫成下面的形式。
const MyClass = class { /* ... */ };
采用Class表達式,可以寫出立即執行的Class。
let person = new class { constructor(name) { this.name = name; } sayName() { console.log(this.name); } }('張三'); person.sayName(); // "張三"
上面代碼中,person是一個立即執行的Class的實例。
不存在變量提升
Class不存在變量提升(hoist),這一點與ES5完全不同。
new Foo(); // ReferenceError class Foo {}
上面代碼中,Foo
類使用在前,定義在后,這樣會報錯,因為ES6不會把變量聲明提升到代碼頭部。這種規定的原因與下文要提到的繼承有關,必須保證子類在父類之后定義。
Class的繼承
基本用法
Class之間可以通過extends
關鍵字實現繼承,這比ES5的通過修改原型鏈實現繼承,要清晰和方便很多。
class ColorPoint extends Point {}
上面代碼定義了一個ColorPoint
類,該類通過extends
關鍵字,繼承了Point
類的所有屬性和方法。但是由於沒有部署任何代碼,所以這兩個類完全一樣,等於復制了一個Point
類。下面,我們在ColorPoint
內部加上代碼。
class ColorPoint extends Point { constructor(x, y, color) { super(x, y); // 調用父類的constructor(x, y) this.color = color; } toString() { return this.color + ' ' + super.toString(); // 調用父類的toString() } }
上面代碼中,constructor
方法和toString
方法之中,都出現了super
關鍵字,它在這里表示父類的構造函數,用來新建父類的this
對象。
子類必須在constructor
方法中調用super
方法,否則新建實例時會報錯。這是因為子類沒有自己的this
對象,而是繼承父類的this
對象,然后對其進行加工。如果不調用super
方法,子類就得不到this
對象。
class Point { /* ... */ } class ColorPoint extends Point { constructor() { } } let cp = new ColorPoint(); // ReferenceError
上面代碼中,ColorPoint
繼承了父類Point
,但是它的構造函數沒有調用super
方法,導致新建實例時報錯。
類的prototype屬性和__proto__屬性
大多數瀏覽器的ES5實現之中,每一個對象都有__proto__
屬性,指向對應的構造函數的prototype屬性。Class作為構造函數的語法糖,同時有prototype屬性和__proto__
屬性,因此同時存在兩條繼承鏈。
(1)子類的__proto__
屬性,表示構造函數的繼承,總是指向父類。
(2)子類prototype
屬性的__proto__
屬性,表示方法的繼承,總是指向父類的prototype
屬性。
class A { } class B extends A { } B.__proto__ === A // true B.prototype.__proto__ === A.prototype // true
上面代碼中,子類B
的__proto__
屬性指向父類A
,子類B
的prototype
屬性的__proto__
屬性指向父類A
的prototype
屬性。
這樣的結果是因為,類的繼承是按照下面的模式實現的。
class A { } class B { } // B的實例繼承A的實例 Object.setPrototypeOf(B.prototype, A.prototype); // B繼承A的靜態屬性 Object.setPrototypeOf(B, A);
Object.getPrototypeOf()
Object.getPrototypeOf
方法可以用來從子類上獲取父類。
Object.getPrototypeOf(ColorPoint) === Point // true
因此,可以使用這個方法判斷,一個類是否繼承了另一個類。
super關鍵字
super
這個關鍵字,有兩種用法,含義不同。
(1)作為函數調用時(即super(...args)
),super
代表父類的構造函數。
(2)作為對象調用時(即super.prop
或super.method()
),super
代表父類。注意,此時super
即可以引用父類實例的屬性和方法,也可以引用父類的靜態方法。
原生構造函數的繼承
原生構造函數是指語言內置的構造函數,通常用來生成數據結構。ECMAScript的原生構造函數大致有下面這些。
- Boolean()
- Number()
- String()
- Array()
- Date()
- Function()
- RegExp()
- Error()
- Object()
以前,這些原生構造函數是無法繼承的,ES6允許繼承原生構造函數定義子類,因為ES6是先新建父類的實例對象this
,然后再用子類的構造函數修飾this
,使得父類的所有行為都可以繼承。下面是一個繼承Array
的例子。
class MyArray extends Array { constructor(...args) { super(...args); } } var arr = new MyArray(); arr[0] = 12; arr.length // 1 arr.length = 0; arr[0] // undefined
上面代碼定義了一個MyArray
類,繼承了Array
構造函數,因此就可以從MyArray
生成數組的實例。這意味着,ES6可以自定義原生數據結構(比如Array、String等)的子類,這是ES5無法做到的。
上面這個例子也說明,extends
關鍵字不僅可以用來繼承類,還可以用來繼承原生的構造函數。因此可以在原生數據結構的基礎上,定義自己的數據結構。
Class的靜態方法
類相當於實例的原型,所有在類中定義的方法,都會被實例繼承。如果在一個方法前,加上static
關鍵字,就表示該方法不會被實例繼承,而是直接通過類來調用,這就稱為“靜態方法”。
class Foo { static classMethod() { return 'hello'; } } Foo.classMethod() // 'hello' var foo = new Foo(); foo.classMethod() // TypeError: undefined is not a function
上面代碼中,Foo
類的classMethod
方法前有static
關鍵字,表明該方法是一個靜態方法,可以直接在Foo
類上調用(Foo.classMethod()
),而不是在Foo
類的實例上調用。如果在實例上調用靜態方法,會拋出一個錯誤,表示不存在該方法。
父類的靜態方法,可以被子類繼承。
class Foo { static classMethod() { return 'hello'; } } class Bar extends Foo { } Bar.classMethod(); // 'hello'
上面代碼中,父類Foo
有一個靜態方法,子類Bar
可以調用這個方法。
靜態方法也是可以從super
對象上調用的。
new.target屬性
new
是從構造函數生成實例的命令。ES6為new
命令引入了一個new.target
屬性,(在構造函數中)返回new
命令作用於的那個構造函數。如果構造函數不是通過new
命令調用的,new.target
會返回undefined
,因此這個屬性可以用來確定構造函數是怎么調用的。