JavaScript面向對象的深入(含源碼)

什麼是面向對象

在編程的世界中,有一種思想是非常重要的,那就是——面向對象思想。掌握了這種思想,意味著你不再是一個編程菜鳥,已經開始朝著開發者的目標邁進。

JavaScript面向對象的深入(含源碼)

那麼,到底什麼是面向對象思想?說到這個可以給大家說說這個東西的由來。以前的編程,都是面向過程的。那麼什麼又是面向過程呢?以前在網上看到一個說法,覺得形容得很好,借用一下。

大體如下:如果現在有個人叫你把大象放到冰箱裡,我們使用面向過程如何做呢?我們可以分成以下幾個步驟:

1. 把冰箱門打開。

2. 把大象放到冰箱裡面。

3. 把冰箱門關上。

我們程序員就會把這三個步驟寫成三個函數:

1. openTheFridgeDoor()

2. putElephantIntoFridge()

3. closeTheFridgeDoor()

然後我們依次調用這幾個函數就行了。好了你以為這就可以下班了,但是過兩天你會發現,這個需求會衍變成很多個奇怪的走向,例如:

1. 請把獅子也放進冰箱。

2. 請把大象放進微波爐。

3. 請把猴子放進微波爐。

4. 把其他的動物也放進冰箱,但是門就別關了。

5. ……

諸如此類。此時要使用面向過程的方式實現這些需求,就要重新定義實現這些需求的函數。這無疑是讓人抓狂的一件事。但是老闆和客戶給錢了,你要做啊!於是你就要加班了,於是你就犧牲在了工作崗位上了……

所以為你的生命著想,你得想出一個不需要每次來需求都要重新定義實現的函數的辦法,那就是——面向對象。

我們的想法是:如果每次要變更需求,我們都不用自己去做這些過程,而是而是指揮別人去做,那不就萬事大吉了嗎?所以我們的面向對象的思想,第一個轉變就是做一個執行者,變成一個指揮者。

如果使用面向的思想完成把大象放進冰箱的需求。我們的做法變成這樣:

1. 找到冰箱,命令冰箱自己打開冰箱的門。

2. 找到大象,命令大象自己進入到冰箱裡面。

3. 再次命令冰箱自己把門關上。

所以實現這個需求需要的實體有: 大象、冰箱。我們就把實現需求中出現的實體稱為對象。大象要會自己進入到冰箱裡,冰箱要會自己開門和關門。進入冰箱、開門和關門我們稱為對象的能力,在編程中通常用方法表示。

所以做個總結:

1. 面向過程就是關注實現需求的第個步驟,任何的工作都需要自己去做。

2. 面向對象就是什麼事都交給能做這件事的對象去做。

那麼現在問題來了,如果需求變成了上文說的那些,面向對象要如何解決問題?現在我們要做的就是:分析需求中出現的對象(實體),然後分別賦予這對象相應的能力。

在新的需求中,要把大象、獅子、猴子這些[動物] 放進 冰箱、微波爐這些 [容器]中。此時這裡面出現的對象(實體)就有:動物、容器。動物要有的方法(能力)是:進入容器,容器要有的方法(能力)是:開門和關門。

所以上述的需求都可以變成:

1. [容器]開門。

2. [動物]進入[容器]。

3. [容器]關門(或者有的需求要不關門的,這個步驟就可以不要了)

所以這樣一來,我們就不用重複地定義函數來實現這些需求了。甚至將來需求又變成要把動物從容器中拿出來,我們也只要在動物對象上拓展動物從容器中出來的方法,就可以快速完成需求了。這個時候你犧牲在工作崗位上的機率就小很多了。

如何實現面向對象

說了那麼多,大家也能大概理解什麼是面向對象了。那麼我們在js裡面要怎麼寫代碼才能實現面向對象?

在JavaScript中,我們用構造函數來創建對象。

function Elephant(){

}

大象這種對象會有一些特有的數據,如大象的體重、品種、年齡等等。我們稱這些特有的數據為:屬性。每頭大象的這些數據都不一樣,這種差異在代碼中如何體現呢?

function Elephant(age,weight,type){

}

我們把每個數據都以形參的形式傳入構造函數,然後在創建的時候再決定每頭大象的實際數據。最終構造函數寫成:

function Elephant(age,weight,type){

this.age = age;

this.weight = weight;

this.type = type;

}

現在如果要一頭大象,我們只要使用new的方式創建對象就行。

// 這是一頭2歲的非洲象,體重是200kg

var ele1 = new Elephant(2,'200kg','非洲象');

// 這是一頭3歲的美洲象,體重是250kg

var ele2 = new Elephant(3,'250kg','美洲象');

現在大象有,我們要教會大象進入容器的能力,這就是方法。初級的寫法是把這些方法寫到構造函數里面。

function Elephant(age,weight,type){

this.age = age;

this.weight = weight;

this.type = type;

this.enterContainer = function(){}

}

大象這類對象已經構建完畢了,接下來要做的是把冰箱也變成對象。我們也給冰箱寫一個構造函數。

function Fridge(){

}

同樣冰箱這種對象也是有它獨有的屬性(數據)的,比如冰箱的高度、寬度等等。我們也把這些屬性寫到構造函數里面。

function Fridge(width,height){

this.width = width;

this.height = height;

}

而現在冰箱按照需求,要有一個開門和一個關門的方法,我們也寫在構造函數上。

function Fridge(width,height){

this.width = width;

this.height = height;

this.openDoor = function(){};

this.closeDoor = function(){};

}

此時我們要完成“把大象放進冰箱”這個需求就需要大概如下代碼

// 1 找到一個冰箱對象,冰箱的寬高足夠放進大象

var fridge = new Fridge('4m','4m');

// 2 給冰箱發佈開門的指令

fridge.openDoor();

// 3 找到一個大象對象

var elephant = new Elephant(2,'200kg','非洲象');

// 4 給大象發佈進入冰箱的指令

elephant.enterContainer();

// 5 給冰箱發佈關門指令

fridge.closeDoor();

但是這個時候我們要現實把獅子放進冰箱裡面這個需求的時候,我們又要寫一段描述獅子的屬性和方法的代碼。並且這段代碼和描述大象的代碼幾乎一模一樣。

function Lion(age,weight,type){

this.age = age;

this.weight = weight;

this.type = type;

this.enterContainer = function(){}

}

這個時候我們分析一個,不管是大象還是獅子和猴子,都有一樣的屬性(年齡、體重、各類)和方法(進入容器),這些是我們在需求裡面的動物都有的,乾脆我們直接寫一段描述動物的代碼算了。

function Animal(age,weight,type){

this.age = age;

this.weight = weight;

this.type = type;

this.enterContainer = function(){}

}

當我們要把大象放進冰箱:

var ele = new Animal(2,'250kg','非洲象');

ele.enterContainer();

當我們要把獅子放進冰箱:

var lion = new Animal(2,'250kg','美洲獅');

lion.enterContainer();

此時就不需要重複地寫代碼來實現類似的需求了。但是這個時候有同學要說了,動物裡面猴子會爬樹,大象不會啊。如果是要做的需求是要猴子爬樹,我們難道直接給Animal構造函數加個爬樹的方法嗎?這明顯就來合理了啊!

當然不是!在解決這個同學的問題這前,我們先做個總結。剛才為國解決把動物放進冰箱的問題,我們把面向過程的做法變成了面向對象的做法。而以上代碼我們只是用到了面向對象思想中的第一個特性:封裝性。所謂的封裝性,即為把對象(需求中的實體)的屬性(特點)和方法(能力)都抽象出來,形成一個一個的分類,而在js中,在es6之前沒有類的概念,所以我們把每個分類都使用構造函數表示。抽象出來的對象,只要你想要使用的時候,只要把構造函數使用new操作,新創建一份即可。

繼承

接下來我們解決猴子要爬樹的問題。當然,要解決這個問題,我們要用到面向對象思想的另一個特性:繼承性。繼承性是指子類可以享有父類的屬性和方法(從這裡開始,不再解釋屬性這種類似的基本概念了)。那麼什麼是子類和父類呢?上文為了解決把動物放進冰箱中的問題,我們定義了一個動物的構造函數,我們把這個理解為父類,後面提出的問題:不是所有的動物都有一樣的方法。比猴子會爬樹,而大象不會,這個時候我們需要重新定義猴子這個構造函數了,我們把這個猴子理解為子類。

function Monkey(age,weight,type){

this.age = age;

this.weight = weight;

this.type = type;

this.climbingTree = function(){};

this.enterContainer = function () {}

}

猴子和大象一樣有年齡、體重、各類這幾個同樣的屬性,和大象一樣會進入容器的方法,與此同時,猴子自己會一個爬樹的方法。此時我們發現,在這個新的構造函數中,只有爬樹這個代碼是新的,其他的代碼我們都寫過一次了。如果每個動物的構造函數都這麼寫的話,會有很多重複的代碼。此時我們就要用到繼承的思想。

原型和繼承

使用原型實現方法繼承

在js中,使用原型來實現繼承。

首先來說一個什麼是原型。原型是js中實現繼承的必要存在,是每個構造函數的一個屬性,同時也是一個對象,他的作用就是在原型上的屬性和方法可以構造函數的實例對象所分享。

我們先看看原型對象的原型。在js中,任何的構造函數都有一個屬性: prototype。我們先在控制檯中輸出一個構造函數:

console.dir(Array);

此時在控制檯中我們可以看到,Array構造函數是有個prototype屬性的。這個屬性就是我們所說的原型。

JavaScript面向對象的深入(含源碼)

展開這個原型屬性,我們發現平時使用的數組的方法都是從這個原型上來的。也就是說原型的方法是可以被實例對象所共享的。

JavaScript面向對象的深入(含源碼)

那麼接下來我們就用原型來解決猴子的代碼重複太多的問題。我們發現,在Animal構造函數和Monkey構造函數中,都而要一個進入容器的函數enterContainer,為了去除這部分重複的代碼,我們中在Animal這個相當父類的構造函數中聲明,而把Monkey的原型指向Animal的實例即可。

function Animal(age, weight, type) {

this.age = age;

this.weight = weight;

this.type = type;

this.enterContainer = function () {

console.log('進入了容器');

}

}

function Monkey(age,weight,type){

this.age = age;

this.weight = weight;

this.type = type;

this.climbingTree = function(){}

}

Monkey.prototype = new Animal();

此時我們new一個Monkey的實例,發現這個實例是可以調用進入容器方法的。

var monkey = new Monkey(2,'25kg','金絲猴');

monkey.enterContainer();

JavaScript面向對象的深入(含源碼)

此時進入容器的方法enterContainer已經可以共享了。但是這種寫法有個缺點,我們寫的方法都是寫在構造函數里面的,這會在我們每次new對象的時候都會在內存中聲明一個函數,而這個函數的代碼每次都是一樣的。這就很沒有必要。

JavaScript面向對象的深入(含源碼)

var m1 = new Monkey(1,'15kg','長臂猴');

var m2 = new Monkey(2,'25kg','大馬猴');

console.log(m1,m2);

JavaScript面向對象的深入(含源碼)

我們仿照原生js的方式,把方法寫到原型上解決這個問題

function Animal(age, weight, type) {

this.age = age;

this.weight = weight;

this.type = type;

}

Animal.prototype.enterContainer = function () {

console.log('進入了容器');

}

function Monkey(age,weight,type){

this.age = age;

this.weight = weight;

this.type = type;

}

Monkey.prototype = new Animal();

Monkey.prototype.climbingTree = function(){

console.log('小猴子正在爬樹');

}

首先從內存上來分析,在對象上已經沒有對象的方法了

JavaScript面向對象的深入(含源碼)

再從控制檯中觀察

var m1 = new Monkey(1,'15kg','長臂猴');

var m2 = new Monkey(2,'25kg','大馬猴');

console.log(m1,m2);

m1.climbingTree();

m2.climbingTree();

JavaScript面向對象的深入(含源碼)

這是因為我們把方法寫在了原型上,而原型上的方法是可以實例所共享的。m1和m2這兩個對象都是Monkey的實例,是可以調用爬樹的方法的。

借用構造函數實現屬性繼承

那麼到目前為止,我們已經解決了一部分代碼的重用問題。我們發現還有一部分代碼還是重複的,這部分代碼是對象的屬性。

在js中,我們可以使用借用構造函數實現屬性的繼承。

什麼是借用呢?這其實是所有函數都可以調用的一個方法:call方法。其他作用是可以修改函數執行時的this指向。舉個簡單的例子:

function fn(){

console.log(this);

}

fn();

這個代碼在正常情況下的結果是輸出window對象。

JavaScript面向對象的深入(含源碼)

但是如果我們使用了借用這個方法:

function fn(){

console.log(this);

}

fn.call({name:'小明'});

在控制檯中輸出的是:我們在使用call方法的第一個參數。利用這個特點,我們可以把構造函數借用下。

JavaScript面向對象的深入(含源碼)

具體代碼如下

function Animal(age, weight, type) {

this.age = age;

this.weight = weight;

this.type = type;

}

function Monkey(age,weight,type){

Animal.call(this,age,weight,type);

}

此時Monkey裡的重複的屬性代碼就沒有了。那麼我們試試Monkey的實例是否會有這些屬性。

var m1 = new Monkey(1,'15kg','長臂猴');

var m2 = new Monkey(2,'25kg','大馬猴');

console.log(m1,m2);

JavaScript面向對象的深入(含源碼)

所以最終我們的代碼寫在了這個樣子

function Animal(age, weight, type) {

this.age = age;

this.weight = weight;

this.type = type;

}

Animal.prototype.enterContainer = function () {

console.log('進入了容器');

}

function Monkey(age,weight,type){

Animal.call(this,age,weight,type);

}

Monkey.prototype = new Animal();

Monkey.prototype.climbingTree = function(){

console.log('小猴子正在爬樹');

}

此時如果是所有動物的方法,我們只要回到Animal.prototype上,如果是猴子自己獨有的方法,就寫到Mokey.prototype上。這就是在js中要實現面向的過程。以上的兩個方式實現繼承的方式我們稱為:組合繼承。

更簡易的語法實現面向對象

上述的寫法是我們在es5的標準下實現面向對象的過程。這個過程稍稍有點麻煩。在es6的新標準下,我們有更簡易的語法實現面向對象。

Class關鍵字

首先了解下es6裡面的一個新的關鍵字:class,這個關鍵字可以快速地讓我們實現類的定義。語法如下:

class 類名{

}

然後在裡面寫該類的構造函數

class 類名{

constructor(){

}

}

比如我們定義一個動物類

class Animal{

constructor(age,weight,type){

this.age = age;

this.weight = weight;

this.type = type;

}

}

當我而要一個動物實例的時候,也只要new即可。

var a1 = new Animal(2,'200kg','非洲象');

console.log(a1);

這個語法的本質也是使用函數和原型實現的,所以我要實現之前的動物和冰箱的問題,只要以這種新語法的方式實現就會更快了。

Extends關鍵字

在新語法的情況下,如果要實現繼承,我們也只要使用一個新的關鍵字“extends”即可。語法如下:

class 子類 extends 父類{

constructor(){}

}

但是要流量的是,在這個新語法下實現的繼承,要在子類的構造函數里面先調用父類的構造函數。

class 子類 extends 父類{

constructor(){

super();

}

}

所以現在要實現Mokey對Animal的繼承,我們要這麼寫

class Animal{

constructor(age,weight,type){

this.age = age;

this.weight = weight;

this.type = type;

}

enterContainer(){

console.log('進入了容器');

}

}

class Mokey extends Animal{

constructor(age,weight,type){

super(age,weight,type);

}

climbingTree(){

console.log('小猴子正在爬樹');

}

}

結語

面向對象這種思想其實並不難,基本所有的高級語言都遵循了這種思想。即為我們在使用語言的時候大部分都是調用語言的API,這些API基本都是用在調用對象的方法或屬性。而要調用對象的方法或者屬性,必然要先找到對象,其實找到對象,然後調用對象的方法和屬性的行為就是我們在使用面向對象的思想解決問題。

所以我對面向對象的問題就是:所謂的面向對象,就是找到對象,然後使用對象的方法和屬性。

而不同的語言實現的過程不一樣,在js裡面還有es5和es6兩種不同的實現方式,這既是js的好的地方,也是js不好的地方。好處在於不斷的有新的語法標準出來,證明這個語言還在蓬勃發展,不好的是這樣對初學者不友好。但是值得肯定的是,js在未來的勢頭還非常好的。


分享到:


相關文章: