淺談JavaScript的面向對象和它的封裝、繼承、多態

寫在前面

既然是淺談,就不會從原理上深度分析,只是幫助咱們更好地理解...php

面向對象與面向過程

面向對象和麪向過程是兩種不一樣的編程思想,剛開始接觸編程的時候,咱們大都是從面向過程起步的,畢竟像我同樣,你們接觸的第一門計算機語言大機率都是C語言,C語言就是一門典型的面向過程的計算機語言。
面向過程主要是以動詞爲主,解決問題的方式是按照順序一步一步調用不一樣的函數。
面向對象是以名詞爲主,將問題抽象出具體的對象,而這個對象有本身的屬性和方法,在解決問題的時候,是將不一樣的對象組合在一塊兒使用。java

//面向過程裝大象
1.開(冰箱)
2.(大象)裝進(冰箱)
3.關(冰箱)
//面向對象裝大象
1. 冰箱.開門()
2. 冰箱.裝進(大象)
3. 冰箱.關門()

從這個例子能夠看出,面向對象是以主謂爲主,將主謂堪稱一個一個的對象,而後對象有本身的屬性和方法。
面向對象是以功能來劃分問題的,而不是步驟。功能上的統一保證了面向對象設計的可擴展性,解決了代碼重用性的問題。
這也是在漫長的程序設計的發展過程當中獲得的驗證結果,面向對象的編程思想較之於面向過程較好一點python

封裝

面向對象有封裝、繼承和多態三大特性。
封裝:就是把事物封裝成,隱藏事物的屬性和方法的實現細節,僅對外公開接口。es6

在ES5中,並無class的概念,可是因爲js的函數級做用域(函數內部的變量函數外訪問不到)。因此咱們能夠模擬class。在es5中,類其實就是保存了一個函數的變量,這個函數有本身的屬性和方法。將屬性和方法組成一個類的過程就是封裝。編程

1.經過構造函數添加

JavaScript提供了一個構造函數(Constructor)模式,用來在建立對象時初始化對象。構造函數其實就是普通的函數,只不過有如下的特色ruby

①首字母大寫(建議構造函數首字母大寫,即便用大駝峯命名,非構造函數首字母小寫)
②內部使用this
③使用new生成實例

經過構造函數添加屬性和方法實際上也就是經過this添加的屬性和方法。由於this老是指向當前對象的,因此經過this添加的屬性和方法只在當前對象上添加,是該對象自身擁有的。因此咱們實例化一個新對象的時候,this指向的屬性和方法都會獲得相應的建立,也就是會在內存中複製一份,這樣就形成了內存的浪費。函數

function Cat(name,color){
    this.name = name;
    this.color = color;
    this.eat = (() => {
        console.log("fish!")
    })
}

//生成實例
var cat1 = new Cat("tom", "gray")

經過this定義的屬性和方法,咱們實例化對象的時候鬥湖從新複製一份this

2.經過原型prototype封裝

在類上經過this的方式添加屬性和方法會致使內存浪費的現象,有什麼辦法可讓實例化的類所使用的屬性和方法 直接使用指針 指向同一個屬性和方法。es5

這就是原型的方法prototype

JavaScript規定,每個構造函數都有一個prototype屬性,指向另外一個對象。這個對象的全部屬性和方法,都會被構造函數的實例繼承。
也就是說,對於那些不變的屬性和方法,咱們能夠直接將其添加在類的prototype對象上。
function Cat(name,color){
            this.name = name;
            this.color = color;
        }
        Cat.prototype.type = "英短";
        Cat.prototype.eat = ( () => {
            alert("fish!")
        } )
          
        //生成實例
        var cat1 = new Cat('Tom', 'gray');
        var cat2 = new Cat('Kobe', 'purple');
        console.log(cat1.type); //英短
        cat2.eat(); //fish!

這時全部實例的type屬性和eat()方法,其實都是同一個內存地址,指向prototype對象,所以就提升了運行效率。
可是這樣作也有弊端,由於實例化的對象的原型都是指向同一內存地址,改動其中一個對象的屬性可能會影響到其餘的對象

es6中的類和封裝

es6聲明一個類
①構造器:構造器內建立自有屬性
②方法:聲明類實例具備的方法

class Cat {
    //等價於Cat構造器
    constructor(name) {
        this.name = name;
    }
    //更加簡單的聲明類的內部函數
    //等價於 Cat.prototype.eat
    eat() {
        console.log("fish!");
    }
}

//生成實例
var cat1 = new Cat("tom");
cat1.eat(); //fish!
console.log(cat1 instanceof Cat); //true
console.log(cat1 instanceof Object); //true
console.log(typeof Cat); //function
console.log(typeof Cat.prototype.eat); //function

從上面class聲明的Cat爲例:Cat類是一個具備構造函數行爲的函數,其中內部方法eat實際上就是Cat.prototype.eat()
因此說es6的class封裝類,本質上是es5實現方式的語法糖
最主要的區別在於,class類的屬性是不可從新賦值和不可枚舉的,Cat.prototype就是一個只讀屬性

class和自定義類型的區別
(1)class的聲明不會提高,與let相似
(2)class的聲明自動運行於嚴格模式之下
(3)class聲明的方法不可枚舉
(4)class的內部方法沒有 constructor 屬性,沒法new
(5)調用class的構造函數必須new
(6)class內部方法不能同名

class類的使用
class做爲js中的一級公民,能夠被看成值來直接使用

//1.類名做爲參數傳入函數
function createObj (ClassName) {
    return new ClassName()
}

//2.當即執行,實現單例模式
let cat1 = new class{
    constructor (name) {
        this.name = name
    }
    eat() {
        console.log("fish!")
    }
}("tom」)
cat1.eat() //fish!

繼承

繼承就是子類可使用父類的全部功能,而且對這些功能進行擴展。繼承的過程,就是從通常到特殊的過程。

1.類式繼承

所謂的類式繼承就是使用的原型的方式,將方法添加在父類的原型上,而後子類的原型是父類的一個實例化對象。

//聲明父類
var SuperClass = function(){
    let id = 1;
    this.name = ['java'];
    this.superValue = function() {
        console.log('this is superValue!')
    }
}

//爲父類添加共有方法
SuperClass.prototype.getSuperValue = function () {
    return this.superValue();
};

//聲明子類
var SubClass = function() {
    this.subValue = (() => {
        console.log('this is subValue!')
    })
}

//繼承父類
SubClass.prototype = new SuperClass();

//爲子類添加共有方法
SubClass.prototype.getSubValue = function() {
    return this.subValue()
}


//生成實例
var sub1 = new SubClass();
var sub2 = new SubClass();

sub1.getSuperValue(); //this is superValue!
sub1.getSubValue(); //this is subValue!

console.log(sub1.id); //undefined
console.log(sub1.name); //["java"]

sub1.name.push("php"); 
console.log(sub1.name); //["java", "php"]
console.log(sub2.name); //["java", "php"]

其中最核心的是SubClass.prototype = new SuperClass();
類的原型對象prototype對象的做用就是爲類的原型添加共有的方法的,可是類不能直接訪問這些方法,只有將類實例化以後,新建立的對象複製了父類構造函數的屬性和方法,並將原型 proto 指向了父類的原型對象。這樣子類就能夠訪問父類的屬性和方法,同時,父類中定義的屬性和方法不會被子類繼承。

but使用類繼承的方法,若是父類的構造函數中有引用數據類型,就會在子類中被全部實例共用,所以一個子類的實例若是更改了這個引用數據類型,就會影響到其餘子類的實例。

構造函數繼承

爲了克服類繼承的缺點,纔有了構造函數繼承,構造函數繼承的核心思想就是SuperClass.call(this, id),直接改變this的指向,使經過this建立的屬性和方法在子類中複製一份,由於是單獨複製的,因此各個實例化的子類互不影響。but會形成內存浪費的問題

//構造函數繼承
//聲明父類

var SuperClass = function(id){
    var name = 'java'
    this.languages = ['java', 'php', 'ruby'];
    this.id = id 
}

//聲明子類
var SubClass = function(id){
    SuperClass.call(this, id)
}

//生成實例
var sub1 = new SubClass(1);
var sub2 = new SubClass(2);


console.log(sub2.id); // 2
console.log(sub1.name); //undefined

sub1.languages.push("python");
console.log(sub1.languages); // ['java', 'php', 'ruby', 'python']
console.log(sub2.languages); // ['java', 'php', 'ruby']
組合式繼承

組合式繼承是汲取了二者的優勢,既避免了內存浪費,又使得每一個實例化的子類互不影響。

//組合式繼承
//聲明父類

var SuperClass = function(name){
    this.languages = ['java', 'php', 'ruby'];
    this.name = name;
}

 //聲明父類原型方法
SuperClass.prototype.showLangs = function () {
    console.log(this.languages);
}

//聲明子類
var SubClass = function(name){
    SuperClass.call(this, name)
}

//子類繼承父類(鏈式繼承)
SubClass.prototype = new SuperClass();

//生成實例
var sub1 = new SubClass('python');
var sub2 = new SubClass('go');

sub2.showLangs(); //['java', 'php', 'ruby']
sub1.languages.push(sub1.name);
console.log(sub1.languages);//["java", "php", "ruby", "python"]
console.log(sub2.languages);//['java', 'php', 'ruby']

but警告:組合式繼承方法當然好,可是會致使一個問題,父類的構造函數會被建立兩次(call()的時候一遍,new的時候又一遍)

寄生組合繼承

組合式繼承的缺點的關鍵是 父類的構造函數在類繼承和構造函數繼承的組合形式被建立了兩邊,可是在類繼承中咱們並不須要建立父類的構造函數,咱們只要子類繼承父類的原型便可。
因此咱們先給父類的原型建立一個副本,而後修改子類的 constructor 屬性,最後在設置子類的原型就能夠了

//原型式繼承
//原型式繼承其實就是類式繼承的封裝,實現的功能返回一個實例,該實例的原型繼承了傳入的o對象
function inheritObject(o) {
    //聲明一個過渡函數
    function F() {}

    //過渡對象的原型鏈繼承父對象
    F.prototype = o;

    //返回一個過渡對象的實例,該實例的原型繼承了父對象
    return new F();
}

//寄生式繼承
//寄生式繼承就是對原型繼承的第二次封裝,使得子類的原型等於父類的原型。而且在第二次封裝的過程當中對繼承的對象進行了擴展

function inheritPrototype(subClass, superClass){
    //複製一份父類的原型保存在變量中,使得p的原型等於父類的原型
    var p = inheritObject(superClass.prototype);
    //修正由於重寫子類原型致使子類constructor屬性被修改
    p.constructor = subClass;
    //設置子類的原型
    subClass.prototype = p;
}

//定義父類
var SuperClass = function(name) {
    this.name = name;
    this.languages = ["java", "php", "python"]
}
//定義父類原型方法
SuperClass.prototype.showLangs = function() {
    console.log(this.languages);
}

//定義子類
var SubClass = function(name) {
    SuperClass.call(this,name)
}

inheritPrototype(SubClass, SuperClass);

var sub1 = new SubClass('go');
es6中的繼承
class SuperClass {
    constructor(name) {
        this.name = name
        this.languages = ['java', 'php', 'go']; 
    }
    showLangs() {
        console.log(this.languages);
    }
}

class SubClass extends SuperClass {
    constructor(name) {
        super(name)
    }

    //重寫父類中的方法
    showLangs() {
        this.languages.push(this.name)
        console.log(this.languages);
    }
}

//生成實例
var sub = new SubClass('韓二虎');

console.log(sub.name); //韓二虎

sub.showLangs(); //["java", "php", "go", "韓二虎"]

多態

多態其實是不一樣對象做用與同一操做產生不一樣的效果。多態的思想其實是把 「想作什麼」 和 「誰去作」 分開。
多態的好處在於,你沒必要再向對象詢問「你是什麼類型」後根據獲得的答案再去調用對象的某個行爲。你儘管去調用這個行爲就是了,其餘的一切能夠由多態來負責。規範來講,多態最根本的做用就是經過吧過程化的條件語句轉化爲對象的多態性,從而消除這些條件分支語句。
因爲JavaScript中提到的關於多態的詳細介紹並很少,這裏簡單的經過一個例子來介紹就好

//非多態     
var hobby = function(animal){
    if(animal == 'cat'){
        cat.eat()
    }else if(animal == 'dog'){
        dog.eat()
    }
}

var cat = {
    eat: function() {
        alert("fish!")
    }
}

var dog = {
    eat: function() {
        alert("meat!")
    }
}

console.log(123);
hobby('cat'); //fish!
hobby('dog'); //meat!

從上面的例子能看到,雖然 hobby 函數目前保持了必定的彈性,但這種彈性很脆弱的,一旦須要替換或者增長成其餘的animal,必須改動hobby函數,繼續往裏面堆砌條件分支語句。咱們把程序中相同的部分抽象出來,那就是吃某個東西。而後再從新編程。

//多態     
var hobby = function(animal){
    if(animal.eat instanceof Function){
        animal.eat();
    }
}

var cat = {
    eat: function() {
        alert("fish!")
    }
}

var dog = {
    eat: function() {
        alert("meat!")
    }
}

如今來看這段代碼中的多態性。當咱們向兩種 animal 發出 eat 的消息時,會分別調用他們的 eat 方法,就會產生不一樣的執行結果。對象的多態性提示咱們,「作什麼」「怎麼去作」是能夠分開的,這樣代碼的彈性就加強了不少。即便之後增長了其餘的animal,hobby函數仍舊不會作任何改變。

//多態     
var hobby = function(animal){
    if(animal.eat instanceof Function){
        animal.eat();
    }
}

var cat = {
    eat: function() {
        alert("fish!")
    }
}

var dog = {
    eat: function() {
        alert("meat!")
    }
}

var aoteman = {
    eat: function(){
        alert("lil-monster!")
    }
}

hobby(cat); //fish!
hobby(dog); //meat!
hobby(aoteman); //lil-monster!

快樂面向對象😁

相關文章
相關標籤/搜索