1. 程式人生 > 其它 >C++泛型與多型(4):Duck Typing

C++泛型與多型(4):Duck Typing

Javascript是一種基於物件(object-based)的語言,你遇到的所有東西幾乎都是物件。但是,它又不是一種真正的面向物件程式設計(OOP)語言,因為它的語法中沒有class(類)—–es6以前是這樣的。所以es5只有使用函式模擬面向物件。

面向物件的三大基本特徵

  • 封裝:就是把客觀事物封裝成抽象的類,並且類可以把自己的資料和方法只讓可信的類或者物件操作,對不可信的進行資訊隱藏。我們平時所用的方法和類都是一種封裝,當我們在專案開發中,遇到一段功能的程式碼在好多地方重複使用的時候,我們可以把他單獨封裝成一個功能的方法,這樣在我們需要使用的地方直接呼叫就可以了。
  • 繼承:繼承的過程,就是從一般到特殊的過程。它可以使用現有類的所有功能,並在無需重新編寫原來的類的情況下對這些功能進行擴充套件。繼承在我們的專案開發中主要使用為子類繼承父類。通過繼承建立的新類稱為“子類”或“派生類”,被繼承的類稱為“基類”、“父類”或“超類”。
  • 多型:物件的多功能,多方法,一個方法多種表現形式。

三大特徵的優點

封裝:封裝的優勢在於定義只可以在類內部進行對屬性的操作,外部無法對這些屬性指手畫腳,要想修改,也只能通過你定義的封裝方法;

繼承:繼承減少了程式碼的冗餘,省略了很多重複程式碼,開發者可以從父類底層定義所有子類必須有的屬性和方法,以達到耦合的目的;

多型:多型實現了方法的個性化,不同的子類根據具體狀況可以實現不同的方法,光有父類定義的方法不夠靈活,遇見特殊狀況就捉襟見肘了。

建立物件的方式

1.原始模式

var person = new Object() //建立一個Object 物件
person.name = 'sun' //
建立一個name 屬性並賦值 person.age = 20 //建立一個age 屬性並賦值 person.say = function () { //建立一個say()方法並返回值 return this.name + this.age } alert(person.say()) //輸出屬性和方法的值

這樣的寫法有兩個缺點,一是如果想生成多個例項,寫起來就非常麻煩,而且會產生大量重複程式碼,這是不可取的;二是例項與原型之間,沒有任何辦法,可以看出有沒有什麼聯絡。

為了解決多個類似物件宣告的問題,我們可以使用一種叫做工廠模式的方法,這種方法就是為了解決例項化物件產生大量重複的問題。

2.工廠模式

工廠,簡單來說就是投入原料、加工、出廠。

通過工廠模式來生成物件,將重複的程式碼提取到一個函式裡面,避免像第一種方式寫大量重複的程式碼。這樣我們在需要這個物件的時候,就可以簡單地創建出來了。

function createPerson(name, age){
    var person = new Object()           
    person.name = name
    person.age = age      
    person.say = function(){
        alert("姓名:"+this.name+"年齡:"+this.age)
    }
    //出廠
    return person
}
//建立兩個物件
var p1 = createPerson("zhang", 22);
var p2 = createPerson("yu", 20);       
//呼叫物件方法
p1.say()
p2.say()

工廠模式解決了重複例項化的問題,但是它也有許多問題,建立不同物件時,其中的屬性和方法都會重複建立,消耗記憶體;還有函式識別問題等等。

3.建構函式模式

為了解決從原型物件生成例項的問題,Javascript提供了一個建構函式模式。 所謂”建構函式”,其實就是一個普通函式,但是內部使用了this變數。對建構函式使用new運算子,就能生成例項,並且this變數會繫結在例項物件上。 建構函式的方法有一些規範:
(1)函式名和例項化構造名相同且大寫(非強制,但這麼寫有助於區分建構函式和普通函式);
(2)通過建構函式建立物件,必須使用new 運算子。

function Person(name, age){
    this.name = name
    this.age = age
    this.say = function(){
        alert("姓名:"+this.name+"年齡:"+this.age)
    }
    //不需要自己return了
}             
//建立兩個物件
var p1 = new Person("zhang", 22)
var p2 = new Person("yu", 20)
alert(p1.say == p2.say) //false

建構函式可以建立物件執行的過程:
(1)當使用了建構函式,並且new 建構函式(),那麼就後臺執行了new Object();
(2)將建構函式的作用域給新物件(即new Object()創建出的物件),而函式體內的this 就代表new Object()出來的物件,而不是指向方法呼叫者;
(3)執行建構函式內的程式碼;
(4)返回新物件(後臺直接返回)。

建構函式方法很好用,解決了函式識別問題,但是存在一個浪費記憶體的問題。每一次生成一個例項,都會生成重複的內容,每個例項物件還是有自己的一套方法。這樣既不環保,也缺乏效率。

4.在原型(prototype)上進行擴充套件

我們建立的每個函式都有一個prototype(原型)屬性,這個屬性是一個物件,它的用途是包含可以由特定型別的所有例項共享的屬性和方法。邏輯上可以這麼理解:prototype是通過呼叫建構函式而建立的那個物件的原型物件。使用原型的好處是可以讓所有物件例項共享它所包含的屬性和方法。也就是說,不必在建構函式中定義物件資訊,而是可以直接將這些資訊新增到原型中。

我們經常把屬性(一些在例項化物件時屬性值改變的),定義在建構函式內;把公用的方法新增在原型上面,也就是混合方式構造物件(構造方法+原型方式)

function Person(name, age){
    //屬性:每個物件的屬性各不相同
    this.name = name
    this.age = age
}
//在原型上新增方法,這樣建立的所有物件都是用的同一套方法
Person.prototype.say= function(){
    alert("姓名:"+this.name+"年齡:"+this.age)
} 
//建立兩個物件
var p1 = new Person("zhang", 22)
var p2 = new Person("yu", 20)           
alert(p1.say == p2.say) //true
//這裡為什麼兩個物件的方法是相等的呢,原因如下
alert(p1.say == Person.prototype.say) //true

所以,它解決了消耗記憶體問題。通過prototype我們還可以很方便的擴充套件系統物件,按照自己的需求來擴充套件,而且又能適用於所有地方,又不會浪費資源。

總結:

例項物件的__proto__指向,其建構函式的原型;建構函式原型的constructor指向對應的建構函式。

有時某種原因constructor指向有問題,可以通過constructor:建構函式名;重新指向。

繼承

利用call()for in繼承 :

給物件的constructor.prototype新增方法屬性,物件就會繼承,如果要實現一個物件繼承其他物件,採用如下方法。

function Person(name,age){
    this.name = name
    this.age = age
}
Person.prototype.say = function(){
    console.log('說話')
}
function Man(name,age){
Person.call(this,name,age)
this.sex = "男" } for(var key in Person.prototype){ Man.prototype[key] = Person.prototype[key] } //新增自己的方法 Man.prototype.fn = function(){ console.log('aaa') } var m = new Man('s',20)

採用中介:

function ClassA(name){
    this.name = name
}
ClassA.prototype.showName = function(){
    console.log(this.name)
}
//中繼來做準備工作
function Ready(){}
Ready.prototype = ClassA.prototype //引用

//需要來繼承ClassA
function ClassB(name){
    ClassA.call(this,name)
}
ClassB.prototype = new Ready()
ClassB.prototype.constructor = ClassB
var b = new ClassB('小明')

es6繼承的書寫方法:

class Father {   
     constructor(name) {
         this._name = name
     }   
    //例項方法,通過例項物件呼叫
     getName() {
         console.log(this._name)
     }   
     // 靜態方法不會被繼承,並且是通過類名去呼叫的   
     static run() {        
          console.log("run")
     } 
}
class Son extends Father {
    constructor(name, age) {
        //例項化子類的時候把子類的資料傳給父類(這裡的super必須有,super裡的引數是所繼承的父類例項化所需要的資料)
        super(name)
        this._age = age
    } 
}
var p = new Father('人') Father.run()//run p.getName() // var m = new Son('小明',20) m.getName() //小明

特別提醒:繼承會繼承父類的例項屬性和例項方法,並不會繼承靜態屬性和靜態方法,並且靜態方法只能通過類名去呼叫。

多型

同一個方法,面對不同的物件有不同的表現形式就叫做多型。 實現多型,有兩種方式:方法過載,方法重寫。

方法過載:過載是指不同的函式使用相同的函式名,但是函式的引數個數或型別不同。呼叫的時候根據函式的引數來區別不同的函式

方法重寫:重寫(也叫覆蓋)是指在派生類中重新對基類中的虛擬函式(注意是虛擬函式)重新實現。即函式名和引數都一樣,只是函式的實現體不一樣