-
1 # Web開發汪
-
2 # 黑馬程式設計師
什麼是面向物件
在程式設計的世界中,有一種思想是非常重要的,那就是——面向物件思想。掌握了這種思想,意味著你不再是一個程式設計菜鳥,已經開始朝著開發者的目標邁進。
那麼,到底什麼是面向物件思想?說到這個可以給大家說說這個東西的由來。以前的程式設計,都是面向過程的。那麼什麼又是面向過程呢?以前在網上看到一個說法,覺得形容得很好,借用一下。
大體如下:如果現在有個人叫你把大象放到冰箱裡,我們使用面向過程如何做呢?我們可以分成以下幾個步驟:
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屬性的。這個屬性就是我們所說的原型。
展開這個原型屬性,我們發現平時使用的陣列的方法都是從這個原型上來的。也就是說原型的方法是可以被例項物件所共享的。
那麼接下來我們就用原型來解決猴子的程式碼重複太多的問題。我們發現,在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();
此時進入容器的方法enterContainer已經可以共享了。但是這種寫法有個缺點,我們寫的方法都是寫在構造函數里面的,這會在我們每次new物件的時候都會在記憶體中宣告一個函式,而這個函式的程式碼每次都是一樣的。這就很沒有必要。
var m1 = new Monkey(1,"15kg","長臂猴");
var m2 = new Monkey(2,"25kg","大馬猴");
console.log(m1,m2);
我們仿照原生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("小猴子正在爬樹");
}
首先從記憶體上來分析,在物件上已經沒有物件的方法了
再從控制檯中觀察
var m1 = new Monkey(1,"15kg","長臂猴");
var m2 = new Monkey(2,"25kg","大馬猴");
console.log(m1,m2);
m1.climbingTree();
m2.climbingTree();
這是因為我們把方法寫在了原型上,而原型上的方法是可以例項所共享的。m1和m2這兩個物件都是Monkey的例項,是可以呼叫爬樹的方法的。
借用建構函式實現屬性繼承
那麼到目前為止,我們已經解決了一部分程式碼的重用問題。我們發現還有一部分程式碼還是重複的,這部分程式碼是物件的屬性。
在js中,我們可以使用借用建構函式實現屬性的繼承。
什麼是借用呢?這其實是所有函式都可以呼叫的一個方法:call方法。其他作用是可以修改函式執行時的this指向。舉個簡單的例子:
function fn(){
console.log(this);
}
fn();
這個程式碼在正常情況下的結果是輸出window物件。
但是如果我們使用了借用這個方法:
function fn(){
console.log(this);
}
fn.call({name:"小明"});
在控制檯中輸出的是:我們在使用call方法的第一個引數。利用這個特點,我們可以把構造函數借用下。
具體程式碼如下
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);
所以最終我們的程式碼寫在了這個樣子
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在未來的勢頭還非常好的。
回覆列表
面相物件其實算是一種開發方式,一種開發思想。js語言可以說是基於物件,但是由於一些原型和函式特性也可以實現繼承多型這些面向物件的語言的一些特徵。es6出來了class 不過不同於java裡面的類,其實算種語法糖。