|

圖片來(lái)源于 DigitalOcean 1. 什么是類(lèi)
在說(shuō) JavaScript 的面向?qū)ο蟮膶?shí)現(xiàn)方法之前,我們先來(lái)看面向?qū)ο缶幊痰囊粋€(gè)核心概念——類(lèi)(class)。類(lèi)是對(duì)擁有同樣屬性(property)和行為的一系列對(duì)象(object)的抽象。 這里說(shuō)的“行為”,在基于類(lèi)的面向?qū)ο蟮恼Z(yǔ)言中通常叫做類(lèi)的方法(method)。而在 JavaScript 里,函數(shù)也是“一等公民”,可以被直接賦值給一個(gè)變量或一個(gè)對(duì)象的屬性,因此在本文后續(xù)的討論中,把“行為”也歸入“屬性”的范疇。 2. JavaScript 對(duì)“類(lèi)”的實(shí)現(xiàn)JavaScript 一開(kāi)始是被設(shè)計(jì)成在網(wǎng)頁(yè)上對(duì)表單進(jìn)行校驗(yàn)或者對(duì)網(wǎng)頁(yè)上的元素進(jìn)行操縱的一種腳本語(yǔ)言,沒(méi)有像 C++ 和 Java 那樣用 class、private、protected 等關(guān)鍵字來(lái)定義類(lèi)的語(yǔ)法。JavaScript 采用的是一種更簡(jiǎn)單的實(shí)現(xiàn)方式:既然類(lèi)就是擁有同樣屬性的一系列對(duì)象,那么只要通過(guò)一種方式能使某一些對(duì)象擁有同樣的屬性就行了。 
JavaScript 規(guī)定每一個(gè)對(duì)象都可以有一個(gè)原型([[prototype]] 內(nèi)部屬性)。(在實(shí)現(xiàn) ECMAScript 5.1 規(guī)范以前,除了 Object.prototype 以外的對(duì)象都必須有一個(gè)原型。)每個(gè)對(duì)象都“共享”其原型的屬性:在訪問(wèn)一個(gè)對(duì)象的屬性時(shí),如果該對(duì)象本身沒(méi)有這個(gè)屬性,則 JavaScript 會(huì)繼續(xù)試圖訪問(wèn)其原型的屬性。這樣,就可以通過(guò)指定一些對(duì)象的原型來(lái)使這些對(duì)象都擁有同樣的屬性。從而我們可以這樣認(rèn)為,在 JavaScript 中,以同一個(gè)對(duì)象為原型的對(duì)象就是屬于同一個(gè)類(lèi)的對(duì)象。
2.1 JavaScript 中對(duì)象的原型的指定方式那么 JavaScript 中的對(duì)象與其原型是怎樣被關(guān)聯(lián)起來(lái)的呢?或者說(shuō),JavaScript 中的對(duì)象的原型是怎樣被指定的呢? 2.1.1 new 操作符JavaScript 有一個(gè) new 操作符(operator),它基于一個(gè)函數(shù)來(lái)創(chuàng)建對(duì)象。這個(gè)用 new 操作符創(chuàng)建出來(lái)的對(duì)象的原型就是 new 操作符后面的函數(shù)(稱為“構(gòu)造函數(shù)”)的 prototype 屬性。例如: var a = {'aa': 1}; function B() {} B.prototype = a; var b = new B();
此時(shí) b 對(duì)象的原型就是 a 對(duì)象。我在另一篇文章中介紹了 new 操作符的具體實(shí)現(xiàn)邏輯,供大家參考。 2.1.2 Object.create 方法Object.create 方法直接以給定的對(duì)象作為原型創(chuàng)建對(duì)象。一個(gè)代碼例子: var a = {'aa': 1}; var b = Object.create(a);
此時(shí) b 對(duì)象的原型就是 a 對(duì)象。關(guān)于 Object.create 方法的實(shí)現(xiàn)細(xì)節(jié),大家可參考我的這篇文章。 2.1.3 Object.setPrototypeOf 方法new 操作符和 Object.create 方法都是在創(chuàng)建一個(gè)對(duì)象的同時(shí)就指定其原型。而 Object.setPrototypeOf 方法則是指定一個(gè)已被創(chuàng)建的對(duì)象的原型。代碼例子: var a = {'aa': 1}; var b = Object.create(a); // 此時(shí) b 的原型是 a var c = {'cc': 2}; Object.setPrototypeOf(b, c); // 此時(shí) b 的原型變?yōu)?nbsp;c 了
2.1.4 隱式指定
數(shù)字、布爾值、字符串、數(shù)組和函數(shù)在 JavaScript 中也是對(duì)象,而它們的原型是被 JavaScript 隱式指定的: 1. 數(shù)字(例如 1、1.1、NaN、Infinity)的原型是 Number.prototype; 2. 布爾值(true 和 false)的原型為 Boolean.prototype; 3. 字符串(例如 ''、'abc')的原型為 String.prototype; 4. 函數(shù)(例如 function () {}、function (a) { return a + '1'; }) 的原型為 Function.prototype; 5. 數(shù)組(如 []、[1, '2'])的原型是 Array.prototype; 6. 用花括號(hào)直接定義的對(duì)象(如 {}, {'a': 1})的原型是 Object.prototype。 
2.2 JavaScript 中定義類(lèi)的代碼示例
下面給出定義一個(gè)類(lèi)的一段 JavaScript 代碼的示例。它定義一個(gè)名為 Person 的類(lèi),它的構(gòu)造函數(shù)接受一個(gè)字符串的名稱,還一個(gè)方法 introduceSelf 會(huì)輸出自己的名字。 // ----==== 類(lèi)定義開(kāi)始 ====---- function Person(name) { this.name = name; } Person.prototype.introduceSelf = function () { console.log('My name is ' + this.name); }; // ----==== 類(lèi)定義結(jié)束 ====---- // 下面實(shí)例化一個(gè) Person 類(lèi)的對(duì)象 var someone = new Person('Tom'); // 此時(shí) someone 的原型為 Person.prototype someone.introduceSelf(); // 輸出 My name is Tom
如果轉(zhuǎn)換為 ECMAScript 6 引入的類(lèi)聲明(class declaration)語(yǔ)法,則上述 Person 類(lèi)的定義等同于: class Person { constructor(name) { this.name = name; } introduceSelf() { console.log('My name is ' + this.name); } }
2.3 對(duì)“構(gòu)造函數(shù)”的再思考
在上面的例子中,假如我們不通過(guò) Person.prototype 來(lái)定義 introduceSelf 方法,而是在構(gòu)造函數(shù)中給對(duì)象指定一個(gè) introduceSelf 屬性: function Person(name) { this.name = name; this.introduceSelf = function () { console.log('My name is ' + this.name); }; } var someone = new Person('Tom'); someone.introduceSelf(); // 也會(huì)輸出 My name is Tom
雖然這種方法中,通過(guò) Person 構(gòu)造函數(shù) new 出來(lái)的對(duì)象也都有 introduceSelf 屬性,但這里 introduceSelf 變成了 someone 自身的一個(gè)屬性而不是 Person 類(lèi)的共有的屬性: function Person1(name) { this.name = name; } Person1.prototype.introduceSelf = function () { console.log('My name is ' + this.name); }; var a = new Person1('Tom'); var b = new Person1('Jerry'); console.log(a.introduceSelf === b.introduceSelf); // 輸出 true delete a.introduceSelf; a.introduceSelf(); // 仍然會(huì)輸出 My name is Tom,因?yàn)?nbsp;introduceSelf 不是 a 自身的屬性,不會(huì)被 delete 刪除 b.introduceSelf = function () { console.log('I am a pig'); }; Person1.prototype.introduceSelf.call(b); // 輸出 My name is Jerry // 即使 b 的 introduceSelf 屬性被覆蓋,我們?nèi)匀豢梢酝ㄟ^(guò) `Person1.prototype` 來(lái)讓 b 執(zhí)行 Person1 類(lèi)規(guī)定的行為。
function Person2(name) { this.name = name; this.introduceSelf = function () { console.log('My name is ' + this.name); }; } a = new Person2('Tom'); b = new Person2('Jerry'); console.log(a.introduceSelf === b.introduceSelf); // 輸出 false // a 的 introduceSelf 屬性與 b 的 introduceSelf 屬性是不同的對(duì)象,分別占用不同的內(nèi)存空間。 // 因此這種方法會(huì)造成內(nèi)存空間的浪費(fèi)。 delete a.introduceSelf; a.introduceSelf(); // 會(huì)拋 TypeError b.introduceSelf = function () { console.log('I am a pig'); }; // 此時(shí) b 的行為已經(jīng)與 Person2 類(lèi)規(guī)定的脫節(jié),對(duì)象 a 和對(duì)象 b 看起來(lái)已經(jīng)不像是同一個(gè)類(lèi)的對(duì)象了
但是這種方法也不是一無(wú)是處。例如我們需要利用閉包來(lái)實(shí)現(xiàn)對(duì) name 屬性的封裝時(shí): function Person(name) { this.introduceSelf = function () { console.log('My name is ' + name); }; } var someone = new Person('Tom'); someone.name = 'Jerry'; someone.introduceSelf(); // 輸出 My name is Tom // introduceSelf 實(shí)際用到的 name 屬性已經(jīng)被封裝起來(lái),在 Person 構(gòu)造函數(shù)以外的地方無(wú)法訪問(wèn) // name 相當(dāng)于 Person 類(lèi)的一個(gè)私有(private)成員屬性
3. JavaScript 的類(lèi)繼承
類(lèi)的繼承實(shí)際上只需要實(shí)現(xiàn): 1. 子類(lèi)的對(duì)象擁有父類(lèi)定義的所有成員屬性; 2. 子類(lèi)的任何一個(gè)構(gòu)造函數(shù)都必須在開(kāi)頭調(diào)用父類(lèi)的構(gòu)造函數(shù)。 實(shí)現(xiàn)第 2 點(diǎn)的方式比較直觀。而怎樣實(shí)現(xiàn)第 1 點(diǎn)呢?其實(shí)我們只需要讓子類(lèi)的構(gòu)造函數(shù)的 prototype 屬性 (子類(lèi)的實(shí)例對(duì)象的原型) 的原型是父類(lèi)的構(gòu)造函數(shù)的 prototype 屬性 (父類(lèi)的實(shí)例對(duì)象的原型),簡(jiǎn)而言之就是:把父類(lèi)實(shí)例的原型作為子類(lèi)實(shí)例的原型的原型。這樣在訪問(wèn)子類(lèi)的實(shí)例對(duì)象的屬性時(shí),JavaScript 會(huì)沿著原型鏈找到子類(lèi)規(guī)定的成員屬性,再找到父類(lèi)規(guī)定的成員屬性。而且子類(lèi)可在子類(lèi)構(gòu)造函數(shù)的 prototype 屬性中重載(override)父類(lèi)的成員屬性。 3.1 代碼示例下面給出一個(gè)代碼示例,定義一個(gè) ChinesePerson 類(lèi)繼承上文中定義的 Person 類(lèi): function ChinesePerson(name) { Person.apply(this, name); // 調(diào)用父類(lèi)的構(gòu)造函數(shù) } ChinesePerson.prototype.greet = function (other) { console.log(other + '你好'); }; Object.setPrototypeOf(ChinesePerson.prototype, Person.prototype); // 將 Person.prototype 設(shè)為 ChinesePerson.prototype 的原型
var someone = new ChinesePerson('張三'); someone.introduceSelf(); // 輸出“My name is 張三” someone.greet('李四'); // 輸出“李四你好”
上述定義 ChinesePerson 類(lèi)的代碼改用 ECMAScript 6 的類(lèi)聲明語(yǔ)法的話,就變成: class ChinesePerson extends Person { constructor(name) { super(name); }
greet(other) { console.log(other + '你好'); } }
3.1.1 重載父類(lèi)成員屬性的代碼示例
你會(huì)不會(huì)覺(jué)得上面代碼示例中,introduceSelf 輸出半英文半中文挺別扭的?那我們讓 ChinesePerson 類(lèi)重載 introduceSelf 方法就好了: ChinesePerson.prototype.introduceSelf = function () { console.log('我叫' + this.name); }; var someone = new ChinesePerson('張三'); someone.introduceSelf(); // 輸出“我叫張三”
var other = new Person('Ba Wang'); other.introduceSelf(); // 輸出 My name is Ba Wang // ChinesePerson 的重載并不會(huì)影響父類(lèi)的實(shí)例對(duì)象
|