JavaScript中的面向?qū)ο缶幊?/h2>

學(xué)習(xí)JavaScript語言的過程中“面向?qū)ο蟆钡母拍罱^對是一道坎。JS沒有“類”這個(gè)概念,因此要用它來進(jìn)行面向?qū)ο缶幊叹托枰乱环Ψ颉R环矫?,我們需要把握J(rèn)avaScript語言自身的風(fēng)格和特點(diǎn);另一方面,我們需要重新理解面向?qū)ο蟮娜笠?。把“語言特性”和“三大要素”牢牢的記在心中,然后再去理解JavaScript面向?qū)ο缶幊叹鸵菀椎枚?。這篇文章將對JS面向?qū)ο缶幊痰暮诵臋C(jī)制進(jìn)行梳理和總結(jié)。歸納了幾種常見的定義類的方式以及如何建立類之間的繼承關(guān)系。

一. 兩個(gè)關(guān)鍵概念

“萬物皆對象”是JS的一個(gè)很重要的特點(diǎn),在JavaScript中,你定義的數(shù)組,鍵值對,整數(shù),字符串和浮點(diǎn)數(shù)等其實(shí)都是對象,連函數(shù)都是一個(gè)對象,所不同的只是它可以被調(diào)用而已。這一點(diǎn)跟Python就很像,這就是為什么你可以在JS數(shù)組中放入各種不同類型的元素的原因,這在強(qiáng)類型語言中是不可能做到的。請注意,理解這個(gè)關(guān)鍵概念對后面理解JavaScript面向?qū)ο缶幊?strong>至關(guān)重要:

  • 在JavaScript中,萬物皆對象;
  • 在JavaScript中,函數(shù)是一個(gè)可以被調(diào)用的特殊對象。

幾乎所有的面向?qū)ο笳Z言都有動(dòng)態(tài)引用當(dāng)前對象的方法,JS也不例外。這就是this關(guān)鍵字。this關(guān)鍵字與函數(shù)調(diào)用密切相關(guān),所不同于其他語言的是JS中的this關(guān)鍵字比較靈活,有很多種綁定規(guī)則,容易把人繞暈。而且this關(guān)鍵字在類的定義中頻繁用到,如果再加上JS因?yàn)闆]有“類”的概念而弄出的各種奇葩定義方式就更暈了。

要理解面向?qū)ο蟮腏S就必須先理解this關(guān)鍵字,要理解this關(guān)鍵字就必須把握一個(gè)關(guān)鍵法則,我稱它為“調(diào)用時(shí)法則”——直到函數(shù)調(diào)用的那一刻this關(guān)鍵字的綁定才被確定。有5種情況:

1. 函數(shù)作為方法

當(dāng)函數(shù)作為對象方法被調(diào)用時(shí),在調(diào)用的那一刻this關(guān)鍵字被綁定到當(dāng)前對象上:

var obj = {
  fn: function(a, b) {
    console.log(this);
  }
};
// this --> obj
obj.fn(3, 4);

2. 函數(shù)直接調(diào)用

當(dāng)函數(shù)被直接調(diào)用時(shí),this被綁定到global對象:

var fn = function(one, two) {
  console.log(this, one, two);
};
var g={}, b={};
// this --> global
fn(g, b);

3. Functional.prototype.call

如果我們希望函數(shù)在直接被調(diào)用時(shí)顯式指定this關(guān)鍵字的綁定,那么我們可以使用函數(shù)對象特有的call()方法:

var fn = function(one, two) {
  console.log(this, one, two);
};
var r={}, g={}, b={};
// this --> r
fn.call(r, g, b);

4. 作為回調(diào)函數(shù)

作為回調(diào)函數(shù)傳入其他函數(shù)中時(shí),由于其他函數(shù)會(huì)直接調(diào)用該回調(diào)函數(shù),因此this關(guān)鍵字仍然被綁定為global對象,就算傳入的是某個(gè)對象的方法也是如此——還記得嗎?this關(guān)鍵字的綁定只有在調(diào)用時(shí)才被確定:

// this --> global
setTimeout(r.method, 1000);

如果需要保留對象信息,可以采取這樣的方式,用一個(gè)匿名函數(shù)將其“包裹”起來:

setTimeout(function() { 
  r.method(); 
}, 1000);

5. 與new操作符一起使用

與new操作符一起使用時(shí),this關(guān)鍵字將被綁定為新創(chuàng)建的對象,在JS中定義類時(shí)會(huì)大量用到這樣的方法:

var Car = function(loc) {
  this.loc = loc;
}
// this --> a brand new object
var c = new Car(2);

二. 面向?qū)ο蟮腏avaScript

關(guān)鍵概念鋪墊完了,下面我們正式進(jìn)入正題,看看JS是怎么定義類的。以上我們了解了語言特性,下面我們就來復(fù)習(xí)一下什么是“面向?qū)ο蟆薄D敲磫栴}來了,當(dāng)我們說“面向?qū)ο蟆钡臅r(shí)候,我們究竟在說什么?

1. 三大要素

面向?qū)ο缶幊逃腥笠兀悍庋b,繼承和多態(tài)。“封裝(encapsulation)”就是將數(shù)據(jù)和與數(shù)據(jù)有關(guān)的操作封裝起來,定義成一個(gè)“類”,以實(shí)現(xiàn)對問題領(lǐng)域的建模。比如定義一個(gè)類Car代表汽車,封裝一些常見的數(shù)據(jù),比如速度,重量和長度等,形成屬性,再封裝一些操作,比如“啟動(dòng)”,“行駛”和“停止”等,形成方法。那么汽車有很多種,有貨車,卡車,公交車和轎車等等,不同的類型有一些自己特有的屬性,那么就需要通過“繼承(inheritance)”來模擬這種關(guān)系,“繼承”也是代碼重用的一種有效的抽象方式。雖然都是汽車,都具有“啟動(dòng)”,“行駛”和“停止”等方法,但不同種類車輛有自己的操作方式,它們共享同樣的“接口”(比如都具有“啟動(dòng)”的方法)但實(shí)現(xiàn)方式不一樣,我們稱這種多樣的行為為“多態(tài)(polymorphism)”。

以上就是掌握J(rèn)S面向?qū)ο缶幊痰乃嘘P(guān)鍵概念,再次強(qiáng)調(diào),這些概念對于理解下面要介紹的內(nèi)容至關(guān)重要??偨Y(jié)一下,有如下4點(diǎn):

  • 在JavaScript中,萬物皆對象;
  • 在JavaScript中,函數(shù)是一個(gè)可以被調(diào)用的特殊對象;
  • this關(guān)鍵字直到調(diào)用時(shí)才被確定綁定到哪個(gè)對象;
  • 面向?qū)ο笕兀悍庋b,繼承和多態(tài);

2. JavaScript中定義類的模式

深刻理解這4個(gè)概念之后JS面向?qū)ο缶幊讨兄T多晦澀難懂的概念將迎刃而解。簡單的說,對象其實(shí)就是鍵值對,類似與Python中的dict,Go語言中的map,因此最簡單的情況下,如果我需要一個(gè)對象,那么其實(shí)我不需要定義類,直接定義對象就好了:

var amy = {loc: 1};
amy.loc++;
var ben = {loc: 9};
ben.loc++;

但是對象如果有成千上百個(gè),這么定義也不是個(gè)事兒,要寫很多重復(fù)代碼。要實(shí)現(xiàn)最基本的代碼重用,就需要把創(chuàng)建對象的過程抽象出來。在JS中這種抽象往往通過定義一個(gè)函數(shù)來實(shí)現(xiàn),這也可以說是JS特有的一類構(gòu)造型模式了。比如“對象修飾器模式(object decorator pattern)”。

1)對象修飾器模式

對象修飾器模式的一大特點(diǎn)是修飾器本身并不創(chuàng)建對象。我們將創(chuàng)建好的對象傳遞給修飾器,讓修飾器為對象添加新的屬性然后返回:

var carLike = function(obj, loc) {
  obj.loc = loc;
  obj.move = function() {
    this.loc++; // *根據(jù)this關(guān)鍵字綁定規(guī)則,它將被綁定到被調(diào)用的對象上
  };
  return obj;
}

這種創(chuàng)建對象的方式還不夠省心,如果我們要求函數(shù)自己創(chuàng)建對象并返回,那么它就不叫修飾器了,叫“函數(shù)類(functional class)”。

2)函數(shù)類模式

函數(shù)類模式定義的函數(shù)一般大寫,以顯示它的特殊性:它會(huì)創(chuàng)建對象,添加屬性,然后返回:

var Car = function(loc) {
  var obj = {};
  obj.loc = loc;
  obj.move = function() {
    obj.loc++;
  };
  return obj;
};

如果將定義好的函數(shù)和new運(yùn)算符結(jié)合起來使用,我們還可以把代碼寫的更簡單些,這就誕生了一個(gè)新的模式,叫做“構(gòu)造器模式(constructor pattern)”。

3)構(gòu)造器模式

使用new運(yùn)算符時(shí),JS解釋器會(huì)默默地在函數(shù)一頭一尾處分別幫你添加一行代碼,這樣代碼看起來就更簡潔,更像一般意義上的構(gòu)造函數(shù)定義:

var Car = function(loc) {
  // this = Object.create(Car.prototype); <-- 解釋器隱式添加
  this.loc = loc;
  this.move = function() {
    this.loc++;
  };
  // return this; <-- 解釋器隱式添加
};

以上三種模式都有個(gè)共同的弊端——每次創(chuàng)建新對象時(shí)都會(huì)創(chuàng)建新的函數(shù)對象move,但函數(shù)的功能是相同的,其實(shí)沒必要反復(fù)拷貝,因此這兩種模式只做到了代碼重用,并沒有做到真正的effective code reuse。創(chuàng)建的對象多了開銷會(huì)很大。如果要讓創(chuàng)建的對象共享屬性和方法,可以通過“原型模式(prototype pattern)”實(shí)現(xiàn)。

4)原型模式

原型模式通過將屬性和方法都定義到構(gòu)造函數(shù)的prototype對象上來實(shí)現(xiàn):

function Car() {}
Car.prototype.loc = 1;
Car.prototype.move = function() {
    this.loc++;
};
var c = new Car();
var d = new Car();

此時(shí)對象c和對象d都共享同一套屬性和方法:loc屬性和move方法。確切地說是所有由Car()創(chuàng)建的對象都會(huì)擁有同一個(gè)loc屬性和move方法,這就避免了重復(fù)拷貝同一個(gè)函數(shù)對象。原型模式分別與函數(shù)類模式、構(gòu)造器模式組合形成了“原型類模式(prototype class)”和“偽類模式(pseudo class)”。

5)原型類模式

原型類模式是函數(shù)類模式和原型模式的結(jié)合。我們用函數(shù)類來創(chuàng)建實(shí)例并添加屬性,然后用原型對象來存儲(chǔ)所有的共享方法,并把所創(chuàng)建的實(shí)例委托給原型對象:

var Car = function(loc) {
  var obj = Object.create(Car.prototype);
  obj.loc = loc;
  reurn obj;
};
Car.prototype.move = function() {
  this.loc++;
};

這樣一來,每個(gè)實(shí)例都有自己的loc屬性,但它們都通過原型對象共享同一個(gè)函數(shù)對象move。如果你仔細(xì)對比上面這段代碼和前面介紹的構(gòu)造器模式里的代碼,就會(huì)發(fā)現(xiàn)它們其實(shí)是大同小異的。只需要一點(diǎn)點(diǎn)改進(jìn)和優(yōu)化,原型類模式就變成了偽類模式。

6)偽類模式

使用原型類模式定義的類是這樣的:

var Car = function(loc) {
  var obj = Object.create(Car.prototype);
  obj.loc = loc;
  reurn obj;
};
Car.prototype.move = function() {
  this.loc++;
};

而使用構(gòu)造器模式定義的類是這樣的:

var Car = function(loc) {
  // this = Object.create(Car.prototype); <-- 解釋器隱式添加
  this.loc = loc;
  this.move = function() {
    this.loc++;
  };
  // return this; <-- 解釋器隱式添加
};

注釋是關(guān)鍵!可以看到在使用new運(yùn)算符創(chuàng)建對象時(shí),解釋器自動(dòng)幫我們生成了這兩行代碼,因此,我們可以把原型類模式的代碼改寫成下面這樣:

var Car = function(loc) {
  this.loc = loc;
};
Car.prototype.move = function() {
  this.loc++;
};

當(dāng)我們使用var car = new Car(5);來定義新的實(shí)例時(shí),該實(shí)例就自動(dòng)將move方法委托給了Car.prototype對象,當(dāng)我們調(diào)用car.move()時(shí),由于car本身不包括move()方法(還記得嗎,對象只不過是鍵值對而已),此時(shí)就會(huì)到Car.prototype中去查找,找到了,于是便執(zhí)行該函數(shù)。結(jié)合上面的討論可以知道此時(shí)this關(guān)鍵字被綁定給了car對象,因此改變的就是對象car的loc屬性。這種對象之間通過原型對象形成鏈?zhǔn)疥P(guān)系,逐步向上查找的機(jī)制就是JS中的“原型鏈(prototype chain)”。它就像一個(gè)鏈表,JS中所有對象都有一個(gè)頂層原型對象Object.prototype它包括了所有對象共享的方法,比如toString()。根據(jù)“萬物皆對象”的特性,所有的數(shù)組都是特殊對象,數(shù)組共享的方法委托給了Array.prototype原型;所有的函數(shù)也都是特殊對象,函數(shù)共享的方法委托給了Function.prototype。

arr --> Array.prototype --> Object.prototype
fn --> Function.prototype --> Object.prototype

偽類模式是所有模式中最高效簡潔的模式,因此推薦使用它來定義類,但是光有類,沒有繼承關(guān)系肯定也不行,下面我們就來重點(diǎn)看看偽類模式下如何建立類之間的繼承關(guān)系。

3. 偽類模式下實(shí)現(xiàn)類之間的繼承關(guān)系

比如現(xiàn)在我們要定義一個(gè)類Van表示貨車,它是Car的子類,擁有自己特有的方法grab??梢酝ㄟ^4個(gè)分解步驟來實(shí)現(xiàn):

  1. 繼承超類的屬性;
  2. 繼承超類的方法;
  3. 恢復(fù)原型的構(gòu)造函數(shù)屬性;
  4. 定義自己特有的方法;

1)繼承超類的屬性

我們需要在子類的上下文環(huán)境中以一種特殊的方式去調(diào)用超類構(gòu)造函數(shù)來繼承超類的屬性。方法就是使用Function.prototype.call():

var Van = function(loc) {
  Car.call(this, loc);
};

根據(jù)上面介紹的綁定規(guī)則,執(zhí)行new Van(5);時(shí)this關(guān)鍵字被綁定到新創(chuàng)建的Van實(shí)例上,然后該實(shí)例被傳給了Car函數(shù),在Car函數(shù)中的this即Van實(shí)例,因此Van的實(shí)例從Car那里繼承了loc屬性。

2)繼承超類的方法

回想一下,超類Car的方法被委托給了Car.prototype對象,因此才實(shí)現(xiàn)了所有Car的實(shí)例共享同一套函數(shù)對象,當(dāng)調(diào)用實(shí)例方法時(shí),會(huì)通過原型鏈機(jī)制鏈?zhǔn)讲檎液瘮?shù)對象并調(diào)用。因此如果Van想要繼承Car的方法,那么它也需要將自己的原型對象委托給Car.prototype。怎么委托呢?通過Object.create()方法:

var Van = function(loc) {
  Car.call(this, loc);
};
Van.prototype = Object.create(Car.prototype);

這樣便建立了鏈?zhǔn)疥P(guān)系,Van的實(shí)例現(xiàn)在可以通過原型鏈機(jī)制調(diào)用move函數(shù)了,你可能會(huì)很好奇,到底Object.create()是怎么建立這層關(guān)系的呢?看一下Crockford大神的Object.create()實(shí)現(xiàn)你就知道了:

 if (typeof Object.create !== 'function') {
    Object.create = function (o) {
        // 臨時(shí)構(gòu)造器
        function F() {
        }
?        // 所有F構(gòu)造器創(chuàng)建的實(shí)例都將自己的原型委托給對象o
        F.prototype = o;
        // 返回F類的對象
        return new F();
    };
}

這下明白了,原來Van.prototype = Object.create(Car.prototype);就是將Van.prototype替換成了一個(gè)新的對象f,而f將自己的原型委托給了Car.prototype對象。一旦有v.move()這樣的調(diào)用,就會(huì)先在v這個(gè)鍵值對中查找move對象,發(fā)現(xiàn)找不到,于是通過Van自己的prototype對象到Car.prototype中去查找,找到了!于是執(zhí)行。

3)恢復(fù)原型的構(gòu)造函數(shù)屬性

上面這段代碼有個(gè)小問題,仔細(xì)看看Object.create()的實(shí)現(xiàn)就會(huì)發(fā)現(xiàn),此時(shí)我們的Van.prototype實(shí)際上是一個(gè)新對象f,而f除了有一個(gè).prototype屬性指向Car.prototype之外就什么都沒有了,缺了什么?缺了構(gòu)造函數(shù)!原來Van.prototype是有一個(gè)constructor屬性的,該屬性存儲(chǔ)的就是Van本身——通過它我們可以知道Van的對象都是由哪個(gè)構(gòu)造函數(shù)實(shí)現(xiàn)的?,F(xiàn)在沒有了,意味著根據(jù)原型鏈法則,我們會(huì)到Car.prototype中去尋找構(gòu)造函數(shù),是Car()函數(shù)本身,這是不對的,因此我們需要恢復(fù)自己的構(gòu)造函數(shù):

var Van = function(loc) {
  Car.call(this, loc);
};
Van.prototype = Object.create(Car.prototype);
Van.prototype.constructor = Van;

4)定義自己特有的方法

關(guān)鍵步驟完成之后,我們終于可以定義自己特有的方法了:

var Van = function(loc) {
  Car.call(this, loc);
};
Van.prototype = Object.create(Car.prototype);
Van.prototype.constructor = Van;
Van.prototype.grab = function() {
  /**/
};

此時(shí)如果我們覺得Car原先實(shí)現(xiàn)的move函數(shù)不滿足Van的實(shí)際情況,我們也可以定義一個(gè)move函數(shù):

var Van = function(loc) {
  Car.call(this, loc);
};
Van.prototype = Object.create(Car.prototype);
Van.prototype.constructor = Van;
Van.prototype.grab = function() {
  /**/
};
Van.prototype.move = function() {
  /* new implementation of move() */ 
};

這樣一來,根據(jù)原型鏈法則,當(dāng)調(diào)用v.move()時(shí)會(huì)先在v身上查找,發(fā)現(xiàn)找不到,于是到Van.prototype中查找,找到了!就不會(huì)繼續(xù)向上回溯到Car.prototype。這不就是“多態(tài)”嗎?對了,JS的多態(tài)就是通過原型鏈機(jī)制和this關(guān)鍵字的調(diào)用時(shí)綁定實(shí)現(xiàn)的。與此相關(guān)的是instanceof運(yùn)算符。你常常會(huì)在代碼中看到car instanceof Car這樣的調(diào)用。instanceof運(yùn)算符的工作機(jī)制是:它會(huì)檢查Car.prototype有沒有出現(xiàn)在car的原型鏈中,如果有就返回true,否則就返回false。

三. 也談理解和把握復(fù)雜概念的方法

一生俯首拜陽明

《傳習(xí)錄》中曾記錄過一個(gè)著名的公案,叫做“巖中花”:

先生游南鎮(zhèn)。一友指巖中花樹問曰“天下無心外物。如此花樹,在深山中自開自落,于我心亦何相關(guān)?”
先生曰:“你未看此花時(shí),此花與汝心同歸于寂。你來看此花時(shí),則此花顏色一時(shí)明白起來。便知此花不在你的心外?!?/p>

你沒有看到這朵花時(shí),它對你是沒有意義的,你甚至都不知道它的存在。而當(dāng)你看到這朵花時(shí),它的形狀,顏色就一下子出現(xiàn)在你的心中了,你就知道,哦!這是一朵花。

陸王心學(xué)講究“心外無物”,“心即理”,“圣人之道,吾性自足”,“吾心即宇宙,宇宙即吾心”。這講的其實(shí)是一種“價(jià)值存在”而不是“物理存在”。我們當(dāng)然知道,如果我們沒有看到這朵花,或者沒有接觸前端開發(fā)的知識(shí),那么這朵花或者說這些面向?qū)ο髾C(jī)制仍然是存在的,只是它的存在對我們沒有價(jià)值意義而已。而一旦接觸它們,它們就具有了價(jià)值,就不在我們的心外。所以我們要去“致良知”,要去弄懂它到底是怎么運(yùn)作的,抓住它的關(guān)鍵和本質(zhì):JS其實(shí)就是用鍵值對去模擬面向?qū)ο蟮姆庋b,繼承和多態(tài)!一旦抓住這個(gè)中心,復(fù)雜的概念就迎刃而解了——所謂八仙過海各顯神通,那些千奇百怪的實(shí)現(xiàn)方式只不過都是想方設(shè)法通過鍵值對來建立面向?qū)ο蟮臋C(jī)制罷了,萬變不離其宗!

由此可以產(chǎn)生一個(gè)觀念上的轉(zhuǎn)變,即我們做任何事情,要堅(jiān)持價(jià)值判斷而不是利益判斷,這樣我們才能走的更遠(yuǎn)。拿打王者榮耀來舉例,如果我們心里揣著的是利益判斷,那么我們追逐的就是個(gè)人利益,就會(huì)糾結(jié)自己能拿對方多少個(gè)人頭,會(huì)不會(huì)被殺死很多次,這局打完能不能獲得MVP什么的,在玩游戲的時(shí)候要么過于沖動(dòng),要么不敢把握機(jī)會(huì),最后反而輸?shù)目赡苄愿?。而如果我們心里揣著的是價(jià)值判斷,那么我們會(huì)更多地思考如何配合隊(duì)友,怎樣提高自己的技戰(zhàn)術(shù),進(jìn)而多花心思了解對方,了解自己,做到知己知彼,反而可能贏的概率更高。這就是思維的轉(zhuǎn)變,這就是心學(xué)的力量。

白起

身在黑暗,心向光明。

四. 參考資料

  1. 面向?qū)ο蟮?Javascript 編程
  2. Object-oriented JavaScript for beginners
  3. Inheritance in JavaScript
  4. OOP In JavaScript: What You NEED to Know
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容