快速了解 ES6 的類定義

JavaScript類定義.png

在 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í)行如下操作嫁蛇。

  1. 在內(nèi)存中創(chuàng)建一個新對象。
  2. 這個新對象內(nèi)部的 [[Prototype]] 指針被賦值為構(gòu)造函數(shù)的 prototype 屬性露该。
  3. 構(gòu)造函數(shù)內(nèi)部的 this 指向新對象睬棚。
  4. 執(zhí)行構(gòu)造函數(shù)內(nèi)部的代碼。
  5. 如果構(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 新增的 classextends 可以很順暢的為內(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)注公眾號「海人為記

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市菠隆,隨后出現(xiàn)的幾起案子兵琳,更是在濱河造成了極大的恐慌,老刑警劉巖浸赫,帶你破解...
    沈念sama閱讀 206,839評論 6 482
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異赃绊,居然都是意外死亡既峡,警方通過查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,543評論 2 382
  • 文/潘曉璐 我一進(jìn)店門碧查,熙熙樓的掌柜王于貴愁眉苦臉地迎上來运敢,“玉大人,你說我怎么就攤上這事忠售〈荩” “怎么了?”我有些...
    開封第一講書人閱讀 153,116評論 0 344
  • 文/不壞的土叔 我叫張陵稻扬,是天一觀的道長卦方。 經(jīng)常有香客問我,道長泰佳,這世上最難降的妖魔是什么盼砍? 我笑而不...
    開封第一講書人閱讀 55,371評論 1 279
  • 正文 為了忘掉前任,我火速辦了婚禮逝她,結(jié)果婚禮上浇坐,老公的妹妹穿的比我還像新娘。我一直安慰自己黔宛,他們只是感情好近刘,可當(dāng)我...
    茶點故事閱讀 64,384評論 5 374
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著臀晃,像睡著了一般觉渴。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上徽惋,一...
    開封第一講書人閱讀 49,111評論 1 285
  • 那天疆拘,我揣著相機(jī)與錄音,去河邊找鬼寂曹。 笑死哎迄,一個胖子當(dāng)著我的面吹牛戈咳,可吹牛的內(nèi)容都是我干的俯画。 我是一名探鬼主播,決...
    沈念sama閱讀 38,416評論 3 400
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼聂沙!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起套媚,我...
    開封第一講書人閱讀 37,053評論 0 259
  • 序言:老撾萬榮一對情侶失蹤驾锰,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后白华,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體慨默,經(jīng)...
    沈念sama閱讀 43,558評論 1 300
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 36,007評論 2 325
  • 正文 我和宋清朗相戀三年弧腥,在試婚紗的時候發(fā)現(xiàn)自己被綠了厦取。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 38,117評論 1 334
  • 序言:一個原本活蹦亂跳的男人離奇死亡管搪,死狀恐怖虾攻,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情更鲁,我是刑警寧澤霎箍,帶...
    沈念sama閱讀 33,756評論 4 324
  • 正文 年R本政府宣布,位于F島的核電站澡为,受9級特大地震影響漂坏,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜媒至,卻給世界環(huán)境...
    茶點故事閱讀 39,324評論 3 307
  • 文/蒙蒙 一樊拓、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧塘慕,春花似錦筋夏、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,315評論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至蛤织,卻和暖如春赴叹,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背指蚜。 一陣腳步聲響...
    開封第一講書人閱讀 31,539評論 1 262
  • 我被黑心中介騙來泰國打工乞巧, 沒想到剛下飛機(jī)就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人摊鸡。 一個月前我還...
    沈念sama閱讀 45,578評論 2 355
  • 正文 我出身青樓绽媒,卻偏偏與公主長得像蚕冬,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子是辕,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 42,877評論 2 345

推薦閱讀更多精彩內(nèi)容