在 ECMASCript 6 之前精绎,使用構(gòu)造函數(shù)模式與原型模式以及它們的組合來模擬類的行為 递宅。但是這幾種策略都有自己的問題任斋,也有相應(yīng)的妥協(xié)。而實現(xiàn)繼承也會顯得非常冗長和混亂露筒。因此呐伞,ECMASCript5 新引入了 class
關(guān)鍵字來定義類,但實際上背后使用的仍然是原型和構(gòu)造函數(shù)的概念邀窃。
類定義
類是 “特殊的函數(shù)”荸哟,因此定義類也有兩種方式。第一種定義類的方式是聲明類瞬捕。
class Person {}
另一種定義類的方式是類表達(dá)式鞍历。
const Person = class {};
類表達(dá)式的名稱是可選的。在把類表達(dá)式賦值給變量后肪虎,可以通過 name
屬性取得類表達(dá)式的名稱字符串劣砍。但不能在類表達(dá)式作用域外部訪問這個標(biāo)識符。
let Person = class PersonName {
identify() {
console.log(Person.name, PersonName.name);
}
}
let p = new Person();
p.identify(); // PersonName PersonName
console.log(Person.name); // PersonName
console.log(PersonName); // ReferenceError: PersonName is not defined
二者之間有重要區(qū)別扇救。函數(shù)聲明可以提升刑枝,類聲明不會提升。
let p = new Person(); // ReferenceError: Cannot access 'Person' before initialization
class Person {}
let s = new Student(); // ReferenceError: Cannot access 'Student' before initialization
let Student = class {};
類可以包含構(gòu)造函數(shù)方法迅腔、實例方法装畅、獲取函數(shù)、設(shè)置函數(shù)和靜態(tài)類方法沧烈,但這些都不是必需的掠兄。空的類定義照樣有效。默認(rèn)情況下蚂夕,類定義中的代碼都在嚴(yán)格模式下執(zhí)行迅诬。
class Person {
// 構(gòu)造函數(shù)
constructor() {}
// 獲取函數(shù)
get name() {}
// 靜態(tài)方法
static of() {}
}
構(gòu)造方法
constructor
方法是類定義中的構(gòu)造方法。當(dāng)類創(chuàng)建實例時婿牍,會調(diào)用這個方法侈贷。構(gòu)造方法的定義不是必需的,不定義構(gòu)造方法相當(dāng)于將構(gòu)造函方法定義為空函數(shù)等脂。
當(dāng)在類中定義了 constructor
方法時俏蛮,使用 new
實例對象時,會調(diào)用 constructor
方法進(jìn)行實例化上遥,并且執(zhí)行如下操作嫁蛇。
- 在內(nèi)存中創(chuàng)建一個新對象。
- 這個新對象內(nèi)部的
[[Prototype]]
指針被賦值為構(gòu)造函數(shù)的prototype
屬性露该。 - 構(gòu)造函數(shù)內(nèi)部的
this
指向新對象睬棚。 - 執(zhí)行構(gòu)造函數(shù)內(nèi)部的代碼。
- 如果構(gòu)造函數(shù)返回非空對象解幼,則返回該對象抑党;否則,返回剛創(chuàng)建的新對象撵摆。
class Person {
constructor(name) {
console.log(arguments.length);
this.name = name || null;
}
}
class Student {
constructor() {
this.name = "default name";
}
}
類實例化時傳入的參數(shù)會用作構(gòu)造函數(shù)的參數(shù)底靠。如果不需要參數(shù),則類名后面的括號也是可選的特铝。
let p1 = new Person; // 0
let p2 = new Person(); // 0
let p3 = new Person("小剛"); // 1
console.log(p1.name); // null
console.log(p2.name); // null
console.log(p3.name); // 小剛
let s2 = new Student();
console.log(s2.name); // default name
默認(rèn)情況下暑中,類構(gòu)造函數(shù)會在執(zhí)行之后返回 this
對象。構(gòu)造函數(shù)返回的對象會被用作實例化的對象鲫剿,如果沒有什么引用新創(chuàng)建的 this
對象鳄逾,那么這個對象會被銷毀。不過灵莲,如果返回的不是this
對象雕凹,而是其他對象,那么這個對象不會通過 instanceof
操作符檢測出跟類有關(guān)聯(lián)政冻,因為這個對象的原型指針并沒有被修改枚抵。
class Person {
constructor(override) {
this.name = '小齊';
if (override) {
return {
nickname: "昵稱"
};
}
}
}
let p1 = new Person(),
p2 = new Person(true);
console.log(p1); // Person { name: '小齊' }
console.log(p1 instanceof Person); // true
console.log(p2); // { nickname: '昵稱' }
console.log(p2 instanceof Person); // false
類構(gòu)造函數(shù)與構(gòu)造函數(shù)的主要區(qū)別是,調(diào)用類構(gòu)造函數(shù)必須使用 new
操作符明场。而普通構(gòu)造函數(shù)如果不使用 new
調(diào)用汽摹,那么就會以全局的 this
(通常是 window
)作為內(nèi)部對象。調(diào)用類構(gòu)造函數(shù)時如果忘了使用 new
則會拋出錯誤:
function Person() {}
class Student {}
// 把window 作為this 來構(gòu)建實例
let p = Person();
let a = Student(); // TypeError: Class constructor Student cannot be invoked without 'new'
類構(gòu)造函數(shù)沒有什么特殊之處苦锨,實例化之后逼泣,它會成為普通的實例方法(但作為類構(gòu)造函數(shù)嫌套,仍然要使用 new
調(diào)用)。因此圾旨,實例化之后可以在實例上引用它:
class Person {}
// 使用類創(chuàng)建一個新實例
let p1 = new Person();
p1.constructor(); // TypeError: Class constructor Person cannot be invoked without 'new'
// 使用對類構(gòu)造函數(shù)的引用創(chuàng)建一個新實例
let p2 = new p1.constructor(); // 這里可以通過
在 ECMAScript 中,使用 class
定義的類魏蔗,通過 typeof
來檢測砍的,其實質(zhì)是一個函數(shù):
class Person {}
console.log(Person); // [class Person]
console.log(typeof Person); // function
類標(biāo)識符有 prototype
屬性,而這個原型也有一個 constructor
屬性指向類自身:
class Person{}
console.log(Person.prototype); // Person {}
console.log(Person === Person.prototype.constructor); // true
與普通構(gòu)造函數(shù)一樣莺治,可以使用 instanceof
操作符檢查構(gòu)造函數(shù)原型是否存在于實例的原型鏈中:
class Person {}
let p = new Person();
console.log(p instanceof Person); // true
類是 JavaScript 的一等公民廓鞠,因此可以像其他對象或函數(shù)引用一樣把類作為參數(shù)傳遞:
// 類可以像函數(shù)一樣在任何地方定義,比如在數(shù)組中
let classes = [
class {
constructor(id) {
this.id = id;
console.log(`instance ${this.id}`);
}
}
];
function tryInstance(classDefinition, id) {
return new classDefinition(id);
}
let instance = tryInstance(classes[0], 3.14); // instance 3.14
與立即調(diào)用函數(shù)表達(dá)式相似谣旁,類也可以立即實例化:
// 因為是一個類表達(dá)式床佳,所以類名是可選的
let p = new class Person {
constructor(x) {
console.log(x);
}
}('小強(qiáng)'); // 小強(qiáng)
console.log(p); // Person {}
實例方法
每次創(chuàng)建實例時,都會執(zhí)行類構(gòu)造方法榄审。而在類的構(gòu)造方法中砌们,通過 this
可以為該類添加 實例屬性。每個實例都對應(yīng)一個唯一的成員對象搁进,這意味著所有成員都不會在原型上共享:
class Person {
constructor() {
// 這個例子先使用對象包裝類型定義一個字符串
// 為的是在下面測試兩個對象的相等性
this.name = new String("小麗");
this.sayName = () => console.log(this.name);
this.nicknames = ['小節(jié)', '小點']
}
}
let p1 = new Person(),
p2 = new Person();
p1.sayName(); // 小麗
p2.sayName(); // 小麗
console.log(p1.name === p2.name); // false
console.log(p1.sayName === p2.sayName); // false
console.log(p1.nicknames === p2.nicknames); // false
p1.name = p1.nicknames[0];
p2.name = p2.nicknames[1];
p1.sayName(); // 小節(jié)
p2.sayName(); // 小點
靜態(tài)屬性或原型的數(shù)據(jù)屬性必須定義在類定義的外面浪感。
class Person {
sayName() {
console.log(`${Person.greeting} ${this.name}`);
}
}
// 在類上定義數(shù)據(jù)成員
Person.greeting = 'My name is';
// 在原型上定義數(shù)據(jù)成員
Person.prototype.name = '柯林';
let p = new Person();
p.sayName(); // My name is 柯林
<small>注意:類定義中之所以沒有顯示支持添加數(shù)據(jù)成員,是因為在共享目標(biāo)上添加可變數(shù)據(jù)成員是一種反模式饼问。一般來說影兽,對象實例應(yīng)該獨(dú)自擁有通過 this
引用的數(shù)據(jù)。</small>
原型方法
為了在實例間共享方法莱革,類定義語法把在類塊中定義的方法作為原型方法峻堰。
class Person {
constructor() {
// 添加到this 的所有內(nèi)容都會存在于不同的實例上
this.locate = () => console.log('instance');
}
// 在類塊中定義的所有內(nèi)容都會定義在類的原型上
locate() {
console.log('prototype');
}
}
let p = new Person();
p.locate(); // instance
Person.prototype.locate(); // prototype
靜態(tài)方法
可以在類上可以使用 staitc
關(guān)鍵字定義靜態(tài)方法。調(diào)用靜態(tài)方法不需要實例化該類盅视,但不能通過一個類實例調(diào)用靜態(tài)方法捐名。在靜態(tài)方法中,this
引用類自身闹击。
class Person {
constructor() {
// 添加到this 的所有內(nèi)容都會存在于不同的實例上
this.locate = () => console.log('instance', this);
}
// 定義在類的原型對象上
locate() {
console.log('prototype', this);
}
// 定義在類本身上
static locate() {
console.log('class', this);
}
}
let p = new Person();
p.locate(); // instance Person { locate: [Function] }
Person.prototype.locate(); // prototype Person {}
Person.locate(); // class [class Person]
迭代器與生成器
類定義語法支持在原型和類本身上定義生成器方法:
class Person {
// 在原型上定義生成器方法
* createNicknameIterator() {
yield '杰克狗';
yield '杰克鼠';
yield '杰克貓';
}
// 在類上定義生成器方法
static* createJobIterator() {
yield '邦德一';
yield '邦德二';
yield '邦德三';
}
}
let jobIter = Person.createJobIterator();
console.log(jobIter.next().value); // 邦德一
console.log(jobIter.next().value); // 邦德二
console.log(jobIter.next().value); // 邦德三
let p = new Person();
let nicknameIter = p.createNicknameIterator();
console.log(nicknameIter.next().value); // 杰克狗
console.log(nicknameIter.next().value); // 杰克鼠
console.log(nicknameIter.next().value); // 杰克貓
因為支持生成器方法桐筏,所以可以通過添加一個默認(rèn)的迭代器,把類實例變成可迭代對象:
class Person {
constructor() {
this.nicknames = ['杰克狗', '杰克鼠', '杰克貓'];
}
*[Symbol.iterator]() {
yield *this.nicknames.entries();
}
}
let p = new Person();
for (let [idx, nickname] of p) {
console.log(nickname);
}
// 杰克狗
// 杰克鼠
// 杰克貓
也可以只返回迭代器實例:
class Person {
constructor() {
this.nicknames = ['杰克狗', '杰克鼠', '杰克貓'];
}
[Symbol.iterator]() {
return this.nicknames.entries();
}
}
let p = new Person();
for (let [idx, nickname] of p) {
console.log(nickname);
}
// 杰克狗
// 杰克鼠
// 杰克貓
繼承
ECMAScript 6 通過 extends
關(guān)鍵字提供的語法糖來與任何擁有 [[Construct]]
和原型的對象實現(xiàn)的類繼承機(jī)制拇砰。
class Person {}
class Student extends Person{}
let s = new Student();
console.log(s instanceof Student); // true
console.log(s instanceof Person); // true
但是梅忌,這種繼承方式也可以繼承普通的構(gòu)造函數(shù)。
function Person() {}
class Student extends Person{}
let s = new Student();
console.log(s instanceof Student); // true
console.log(s instanceof Person); // true
這種繼承方式可以用在類表達(dá)式上除破。
let Student = class extends Person {}
子類還可以通過 super
關(guān)鍵字引用它們的原型牧氮,而且只能在子類中使用。子類中定義了構(gòu)造方法瑰枫,必須調(diào)用 super()
之后踱葛,才能使用 this
丹莲。這里調(diào)用 super()
會調(diào)用父類的構(gòu)造方法,并將返回的實例賦值給 this
尸诽。
class Person {
constructor(name) {
this.name = name;
}
}
class Student extends Person{
constructor(name) {
super(name);
console.log(this);
}
}
let s = new Student("小王"); // Student { name: '小王' }
console.log(s.name); // 小王
在靜態(tài)方法中甥材,可以通過 super
調(diào)用父類上定義的靜態(tài)方法:
class Person {
constructor(name) {
this.name = name;
}
static of(name) {
return new Person(name);
}
}
class Student extends Person{
constructor(name) {
super(name);
console.log(this);
}
static of(name) {
return super.of(name);
}
}
let s = Student.of("小軍");
console.log(s); // Person { name: '小軍' }
console.log(s instanceof Student); // false
console.log(s instanceof Person); // true
ECMAScript 可以通過 new.target
實現(xiàn)一個供其他類繼承,但本身不會被實例化的抽象基類性含。new.target
保存通過 new
關(guān)鍵字調(diào)用的類或函數(shù)洲赵。通過在實例化時檢測new.target
是不是抽象基類,可以阻止對抽象基類的實例化:
class Person {
constructor(name) {
if (new.target === Person) {
throw new Error("Person不能直接被實例化");
}
this.name = name;
}
}
class Student extends Person {
constructor(name) {
super(name);
}
}
let s = new Student("小萍");
let p = new Person("小玲"); // Error: Person不能直接被實例化
也可以在抽象基類的構(gòu)造方法中檢查子類是否定義了某個方法商蕴。因為原型方法在調(diào)用類構(gòu)造方法之前就已經(jīng)存在了叠萍,所以可以通過 this
來檢查相應(yīng)的方法:
class Person {
constructor(name) {
if (new.target === Person) {
throw new Error("Person不能直接被實例化");
}
if (!this.action) {
throw Error("繼承的類必須定義action()方法");
}
this.name = name;
}
}
class Student extends Person {
constructor(name) {
super(name);
}
action() {
console.log("學(xué)生行為");
}
}
class Employee extends Person {
constructor(name) {
super(name);
}
}
let s = new Student("小萍");
let e = new Employee("小洪"); // Error: 繼承的類必須定義action()方法
ES6 新增的 class
和 extends
可以很順暢的為內(nèi)置引用類型擴(kuò)展功能。
class MoreArray extends Array {
first() {
return this[0];
}
last() {
return this[this.length - 1];
}
}
let arr = new MoreArray(20, 92, 15, 40);
console.log(arr.first()); // 20
console.log(arr.last()); // 40
類混入
ECMAScript 6 支持單繼承绪商,但是可以通過現(xiàn)有特性模擬多重繼承苛谷。
extends
關(guān)鍵字后面可以跟一個 JavaScript 表達(dá)式。任何可以解析為一個類或一個構(gòu)造函數(shù)的表達(dá)式都是有效的格郁。
class Person {}
function getPerson() {
console.log("對象操作");
return Person;
}
class Student extends getPerson() {} // 對象操作
混入模式可以通過在一個表達(dá)式中連綴多個混入元素來實現(xiàn)腹殿,這個表達(dá)式最終會解析為一個可以被繼承的類。如果 Person
類需要組合A例书、B赫蛇、C,則需要某種機(jī)制實現(xiàn) B 繼承 A雾叭,C 繼承 B悟耘,而 Person
再繼承 C,從而把 A织狐、B暂幼、C 組合到 Person
中。實現(xiàn)這種模式有不同的策略移迫。
一個策略是定義一組 “可嵌套” 的函數(shù)旺嬉,每個函數(shù)分別接收一個父類作為參數(shù),而將混入類定義為這個參數(shù)的子類厨埋,并返回這個類邪媳。這些組合函數(shù)可以連綴調(diào)用,最終組合成超類表達(dá)式:
class Person {}
let Action = (Superclass) => class extends Superclass {
action() {
console.log('動作');
}
};
let Face = (Superclass) => class extends Superclass {
face() {
console.log('表情');
}
};
let Sex = (Superclass) => class extends Superclass {
sex() {
console.log('性別');
}
};
class Student extends Sex(Face(Action(Person))) {}
let s = new Student();
s.action(); // 動作
s.face(); // 表情
s.sex(); // 性別
也可以通過寫一個輔助函數(shù)荡陷,可以把嵌套調(diào)用展開:
通過寫一個輔助函數(shù)雨效,可以把嵌套調(diào)用展開:
class Person {}
let Action = (Superclass) => class extends Superclass {
action() {
console.log('動作');
}
};
let Face = (Superclass) => class extends Superclass {
face() {
console.log('表情');
}
};
let Sex = (Superclass) => class extends Superclass {
sex() {
console.log('性別');
}
};
function mix(BaseClass, ...Mixins) {
return Mixins.reduce((accumulator, current) => current(accumulator), BaseClass);
}
class Student extends mix(Person, Action, Face, Sex) {}
let s = new Student();
s.action(); // 動作
s.face(); // 表情
s.sex(); // 性別
<small>注意:很多JavaScript框架已經(jīng)拋棄混入模式,轉(zhuǎn)向組合模式废赞。這反映了 “組合勝過繼承” 的軟件設(shè)計原則徽龟,且提供了很大的靈活性。<small>
總結(jié)
ECMAScript 6 新增的類語法很大程度上是基于語言既有的原型機(jī)制來實現(xiàn)的語法糖唉地。但這種語法可以優(yōu)雅地定義向后兼容的類据悔,既可以繼承內(nèi)置類型传透,也可以繼承自定義類型。類有效地跨越了對象實例极颓、原型和類之間的鴻溝朱盐。
更多內(nèi)容請關(guān)注公眾號「海人為記」