1.面向對象的編程
1.1.什么是面向對象編程
面向對象編程:即是把能夠完成獨立完成一部分功能的代碼封裝在一起,組成一個類。
舉個例子來說:
這里有一把槍, 槍的種類很多,有步槍,機關槍,阻擊槍....。但是無論怎么說,這些都是槍的概念,如果把這種抽象的概念剝離出來,就是我們經常說的“類”。那么槍有什么特點呢? 威力大小,型號,長度,子彈型號,能承載子彈的數量,槍口半徑......ok! 這些一切的一切都是為了描素槍,把這些抽象出來,就組成了“槍類”的屬性。槍又能干什么呢? 瞄准,開火,....這些描素都是槍的功能-----把這些抽象出來,即組成了一個類的方法。
所以,總體來說,面向對象的編程即是把一個程序模塊化,每個模塊承載一部分功能,各個模塊協同合作,維持程序的正常執行;
而所謂的類的組成,無外乎三個部分:這個類的名稱(對應着例子中的“槍”),這個類的屬性(對應着特點),這個類的方法(對應着功能)。
就像我們描素一個人一樣,無外乎,描素一個人的特點以及人的能力。所以,現實生活中的人,在程序中也可以抽象成類。
1.2.類,對象,實例的關系
- 類:是一個抽象概念,是對某一類相似對象的抽象。
- 對象:是類的一個實例化,因為類是一個抽象的概念,所以,在使用時必須落實到實物的身上。那么,對象就作為載體來完成某項功能。
- 實例:和對象是一個概念。一般說一個類的實例,指的就是這個類的某個對象。
舉個例子來說明三者之間的關系:
//1.Person是一個類的名字,定義的是一個人,對這個人的描述一般就是姓名,年齡。
var Person = function (name, age) {
//Person類的屬性 this.name = name; this.age = age; }
//Person類的方法 Person.prototype.greet = function () { console.log('hello'); }
//這是一個類的實例化過程,lisi這里就是Person類的一個對象,也可以說其是Person類的一個實例
var lisi=new Person("lisi",18);
1.3.面向對象的四個特點
- 封裝. 所謂的封裝就是把一個類的對象的屬性和方法封裝在類的內部。封裝的好處就是:類與類之間的屬性和方法相互獨立,互不干擾。
- 繼承. 所謂的繼承就是指的是一個類可以派生自另外一個類。比如,圖形類,可以派生出三角形,正方形,圓....
- 重載. 重載就是指一個類的方法可以名字可以相同(JS不支持重載).第二部分會給出解釋。
- 多態.多態指的是父類的方法,子類可以重寫該方法。那么,子類在調用該方法時調用的會是子類的方法。
請記住:面向對象,所有的一些都是為了代碼的復用。
2.面向對象的四個特點在JS中的實現
2.1.JS中的封裝
JS類的封裝即是把類的屬性和方法封裝在類的內部. 如果只是簡單的實現封裝,那么可以有多種方法。比如下面的兩種
//第一種方法
var Person = function (name, age) { this.name = name; this.age = age; this.greet = function () { console.log('hello'); } }
//第二種方法 var Person = function (name, age) { this.name = name; this.age = age; } Person.prototype.greet = function () { console.log('hello'); }
這兩種方法,雖然在使用效果上是一致的,但是在第一種方法中,每次new 一個對象的時候都要為該對象添加一個函數greet----這樣就沒有做到代碼的復用。所以在使用的時候,一般都是使用第二種方式---也就是所謂的組合式創建。所以一般我們也推薦用第二種方式。
2.2.JS中不存在重載
什么是重載呢? 重載的概念來源於強類型預言(C++,java,C#)中。我們先來看一些java中的重載
class Person{ //java語言, 定義一個Person類,該類中存在greet方法的重載。 public String name; public int age; Person(String name,int age){ this.name=name; this.age=age; } public void greet(){ System.out.println("I am "+ this.name); } public void greet(String message){ System.out.println("I am "+ this.name+ "\n This is your"+message); } }
所謂的重載,就是一個同一個方法名在一個類中被出現了多次。那么在該方法被調用的時候,編譯器如何區分具體調用哪個方法呢?
在強類型語言中,編譯器先根據函數的名字選擇函數,然后在根據調用時,形參和實參的類型,形參的個數和實參的個數是否一致來區分一個函數。
那么,問題來了....JS中的解釋器是符合區分一個函數呢? ok...JS中解釋器只是根據函數的名稱來選擇函數,而函數的形參並不在考慮的范圍----因為在編譯時無法根據確定形參的類型,更無法確定實參的類型。
既然,JS不支持重載,那么如果一個函數被重寫了,會出現什么情況呢?
var Person = function (name, age) { this.name = name; this.age = age; this.greet = function () { console.log('hello'); } } var Person = function (name, age) { this.name = name; this.age = age; } Person.prototype.greet = function () { console.log('我被覆蓋了'); } Person.prototype.greet = function (message) { console.log("我是重寫的方法"); } var person=new Person("zhangsan",18); person.greet(); //我是重寫的方法
根據上面的例子,可以看出,無論函數的參數是什么,只要函數同名,那么被調用的肯定是最后一次被寫的同名函數。
2.3.JS中的繼承
繼承這個概念的來源也是面向對象的編程。JS引薦強類型預言中的繼承做到這一點。所以我們要從強類型語言中的繼承來類推---JS中為什么要這么設計。
2.3.1.強類型語言中繼承的實現
在強類型語言中,在假設有兩個類 A 、B....A是B的父類。實現如下:
class A{//父類的構造函數 protected int x; A(int x){ this.x=x; } } class B extends A{ protected int y; B(int x,int y){//子類的構造函數 super(x); //在子類的構造函數中,第一句話總是先調用父類的構造函數, //如果不寫 則默認調用super();如果父類中不存在無參構造函數,則編譯時會報錯。
this.y=y;
}
public String getPoint(){
return "("+this.x+","+this.y+")"; //返回坐標(x,y)
}
}
從上面的這些我們可以看出什么呢? 就是對象初始化的順序...先初始化父類,在初始化子類。
初始化的時候順序為: 父類的屬性----》父類的方法-----》子類的屬性-----》子類的方法。(我們這里講的是排除了類中靜態數據和方法來說,因為靜態數據和方法的初始化,在類第一次被加載的時候就已經初始化完畢)
下面我們看下,JS中是怎么實現和上述一樣的功能的...
var A = function (x) { this.x = x; } var B = function (x, y) { A.call(this, x); //相當於第一種的super()函數。 this.y = y; }
//實現繼承 function extend(subClass, superClass) { var prototype = Object.create(superClass); subClass.prototype = prototype; subClass.constructor = subClass; } extend(B, A); B.prototype.getPoint = function () { return '(' + this.x + ',' + this.y + ')'; }
上面這兩段代碼,撇開語言的特性來說,他們實現的功能是等效的。只是第一種玩的是思想,第二種玩的是技巧。
OK!下面我們開始詳解JS的設計者為了JS語言能實現繼承所做的努力。
2.3.2.JS語言支持繼承的原理。
所有的函數均有一個prototype屬性。就是這個屬性幫我們做到了一些,首先要認識到一點這個屬性是一個對象。
用上面我們創建的一個Person函數詳解,那么這個函數的prototype屬性如下表示:
這是這個函數在剛開始被初始化時候的固有形式,后來執行了一句
Person.prototype.greet = function () { console.log('hello'); }
在這句執行完畢的時候,Person.prototype變化為
Person prototype | |
constructor | 指向Person函數 |
greet | (greet函數) |
解釋了這么多,貌似並沒有解釋繼承是怎么實現的是吧....別慌...慢慢來!!!
來看一下,當一個函數被實例化的時候發生了什么?
var lisi=new Person("lisi",18); //看看Person實例化的對象發生了什么?
到這里,我們看到了吧..當用new創建一個構造函數的對象的時候。這個對象會有一個【【__proto__】】內置屬性,指向函數的prototype。------這就是對象lisi傳說中的原型對象。
一個函數只有一個原型(prototype),這個函數在用new調用的時候會把這個原型賦值給當前對象的內置屬性。
當查詢一個對象的屬性的時候,首先查詢對象本身的屬性,如果沒有找到則根據對象內置屬性層層向上查找。
所以一切的一切都歸咎於,只要們修改一個函數的prototype屬性,那么就可以實現繼承。
下面圖解,B繼承A的過程。
//1. A類,和B類的構造函數
var A = function (x) { this.x = x; } var B = function (x, y) { A.call(this, x); //相當於第一種的super()函數。 this.y = y; }
//2.修改B的prototype,使其指向A的prototype
//實現繼承 function extend(subClass, superClass) { var prototype = Object.create(superClass); subClass.prototype = prototype; subClass.constructor = subClass; } extend(B, A); B.prototype.getPoint = function () { return '(' + this.x + ',' + this.y + ')'; }
如此,便實現B類繼承A類...關鍵點就在函數的prototype屬性上。----在下一篇中會詳解函數的prototype。
JS中實現多態
何謂多態?
首先一定要強調一點,只有在繼承存在的情況下才可能出現多態! 這是為什么呢..因為多態指的是子類覆蓋父類的方法.....子類覆蓋父類的方法,這種情況就是所謂的多態。
在java中的多態
public class Test { /** * @param args */ public static void main(String[] args) { // TODO Auto-generated method stub B b=new B(1,2); String result=b.getPoint(); System.out.println(result); } } class A{ protected int x; A(int x){ this.x=x; } public String getPoint(){ return "我是父類"; } } class B extends A{ protected int y; B(int x,int y){ super(x); this.y=y; }
//多態,父類覆蓋的方法 public String getPoint(){ return "我是子類"; } }
//輸出結果 :我是子類
在JS中的多態情況,也是指的的子類的方法覆蓋父類的方法。 上面的功能在JS中是這么實現的。
var A = function (x) { this.x = x; } A.prototype.getPoint = function () { return '我是子類'; } var B = function (x, y) { A.call(this, x); //相當於第一種的super()函數。 this.y = y; } //實現繼承 function extend(subClass, superClass) { var prototype = Object.create(superClass); subClass.prototype = prototype; subClass.constructor = subClass; } extend(B, A); B.prototype.getPoint = function () { return '我是子類'; } var b = new B(1, 2); b.getPoint();
//輸出結果 :我是子類
在上述代碼執行完畢后,函數B的結構如圖所示
B類在實例化的時候,B類的對象會擁有一個內部屬性指向B.prototype.當該實例調用函數的時候,會先在該對象內部查詢該函數是否存在,如果不存在則沿着內置屬性查詢原型對象,即B.prototype。如果找到此屬性,則停止查詢,否則會接着沿着內置屬性所指向的對象,一直找到最上級為止。