[15]面向對象與原型

ECMAScript 有兩種開發模式:1.函數式(過程化),2.面向對象(OOP)。面向對象的語言有一個標誌,那就是類的概念,而經過類能夠建立任意多個具備相同屬性和方法的對象。可是,ECMAScript 沒有類的概念,所以它的對象也與基於類的語言中的對象有所不一樣。javascript

一.建立對象

建立一個對象,而後給這個對象新建屬性和方法。css

var box = new Object(); //建立一個 Object 對象
box.name = 'Lee'; //建立一個 name 屬性並賦值
box.age = 100; //建立一個 age 屬性並賦值
box.run = function () { //建立一個 run()方法並返回值
    return this.name + this.age + '運行中...';
};
alert(box.run()); //輸出屬性和方法的值

上面建立了一個對象,而且建立屬性和方法,在 run()方法裏的 this,就是表明 box 對象自己。這種是 JavaScript 建立對象最基本的方法,但有個缺點,想建立一個相似的對象,就會產生大量的代碼。java

var box2 = box; //獲得 box 的引用
box2.name = 'Jack'; //直接改變了 name 屬性

alert(box2.run()); //用 box.run()發現 name 也改變了

var box2 = new Object();
box2.name = 'Jack';
box2.age = 200;
box2.run = function () {
    return this.name + this.age + '運行中...';
};

alert(box2.run()); //這樣才避免和 box 混淆,從而保持獨立

1.工廠模式

爲了解決多個相似對象聲明的問題,咱們可使用一種叫作工廠模式的方法,這種方法就是爲了解決實例化對象產生大量重複的問題。程序員

function createObject(name, age) { //集中實例化的函數
    var obj = new Object();
    obj.name = name;
    obj.age = age;
    obj.run = function () {
        return this.name + this.age + '運行中...';
    };
    return obj;
}

var box1 = createObject('Lee', 100); //第一個實例
var box2 = createObject('Jack', 200); //第二個實例
alert(box1.run());
alert(box2.run()); //保持獨立

工廠模式解決了重複實例化的問題,但還有一個問題,那就是識別問題,由於根本沒法搞清楚他們究竟是哪一個對象的實例。數組

alert(typeof box1); //Object
alert(box1 instanceof Object); //true

2.構造函數

ECMAScript 中能夠採用構造函數(構造方法)可用來建立特定的對象。類型於 Object 對象。瀏覽器

function Box(name, age) { //構造函數模式
    this.name = name;
    this.age = age;
    this.run = function () {
        return this.name + this.age + '運行中...';
    };
}

var box1 = new Box('Lee', 100); //new Box()便可
var box2 = new Box('Jack', 200);

alert(box1.run());
alert(box1 instanceof Box); //很清晰的識別他從屬於 Box

使用構造函數的方法,即解決了重複實例化的問題,又解決了對象識別的問題,但問題是,這裏並無 new Object(),爲何能夠實例化 Box(),這個是哪裏來的呢?安全

使用了構造函數的方法,和使用工廠模式的方法他們不一樣之處以下:
1.構造函數方法沒有顯示的建立對象(new Object());
2.直接將屬性和方法賦值給 this 對象;
3.沒有 renturn 語句。ruby

構造函數的方法有一些規範:
1.函數名和實例化構造名相同且大寫,(PS:非強制,但這麼寫有助於區分構造函數和普通函數);
2.經過構造函數建立對象,必須使用 new 運算符。bash

既然經過構造函數能夠建立對象,那麼這個對象是哪裏來的,new Object()在什麼地方執行了?執行的過程以下:
1.當使用了構造函數,而且 new 構造函數(),那麼就後臺執行了 new Object();
2.將構造函數的做用域給新對象,(即 new Object()建立出的對象),而函數體內的 this 就表明 new Object()出來的對象。
3.執行構造函數內的代碼;
4.返回新對象(後臺直接返回)。markdown

3.this

關於 this 的使用,this 其實就是表明當前做用域對象的引用。若是在全局範圍 this 就表明 window 對象,若是在構造函數體內,就表明當前的構造函數所聲明的對象。

var box = 2;
alert(this.box); //全局,表明 window

構造函數和普通函數的惟一區別,就是他們調用的方式不一樣。只不過,構造函數也是函數,必須用 new 運算符來調用,不然就是普通函數。

var box = new Box('Lee', 100); //構造模式調用
alert(box.run());

Box('Lee', 20); //普通模式調用,無效

var o = new Object();
Box.call(o, 'Jack', 200) //對象冒充調用
alert(o.run());

探討構造函數內部的方法(或函數)的問題,首先看下兩個實例化後的屬性或方法是否相等。

var box1 = new Box('Lee', 100); //傳遞一致
var box2 = new Box('Lee', 100); //同上

alert(box1.name == box2.name); //true,屬性的值相等
alert(box1.run == box2.run); //false,方法其實也是一種引用地址
alert(box1.run() == box2.run()); //true,方法的值相等,由於傳參一致

能夠把構造函數裏的方法(或函數)用 new Function()方法來代替,獲得同樣的效果,更加證實,他們最終判斷的是引用地址,惟一性。

function Box(name, age) { //new Function()惟一性
    this.name = name;
    this.age = age;
    this.run = new Function("return this.name + this.age + '運行中...'");
}

咱們能夠經過構造函數外面綁定同一個函數的方法來保證引用地址的一致性,但這種作法沒什麼必要,只是加深學習瞭解:

function Box(name, age) {
    this.name = name;
    this.age = age;
    this.run = run;
}

function run() { //經過外面調用,保證引用地址一致
    return this.name + this.age + '運行中...';
}

雖然使用了全局的函數 run()來解決了保證引用地址一致的問題,但這種方式又帶來了一個新的問題,全局中的 this 在對象調用的時候是 Box 自己,而看成普通函數調用的時候,this 又表明 window。

二.原型

咱們建立的每一個函數都有一個 prototype(原型)屬性,這個屬性是一個對象,它的用途是包含能夠由特定類型的全部實例共享的屬性和方法。邏輯上能夠這麼理解:prototype 經過調用構造函數而建立的那個對象的原型對象。使用原型的好處可讓全部對象實例共享它所包含的屬性和方法。也就是說,沒必要在構造函數中定義對象信息,而是能夠直接將這些信息添加到原型中。

function Box() {} //聲明一個構造函數

Box.prototype.name = 'Lee'; //在原型裏添加屬性
Box.prototype.age = 100;
Box.prototype.run = function () { //在原型裏添加方法
    return this.name + this.age + '運行中...';
};

比較一下原型內的方法地址是否一致:

var box1 = new Box();

var box2 = new Box();
alert(box1.run == box2.run); //true,方法的引用地址保持一致

爲了更進一步瞭解構造函數的聲明方式和原型模式的聲明方式,咱們經過圖示來了解一下:

構造函數方式:
這裏寫圖片描述
原型模式方式:
這裏寫圖片描述

在原型模式聲明中,多了兩個屬性,這兩個屬性都是建立對象時自動生的。__proto__屬性是實例指向原型對象的一個指針,它的做用就是指向構造函數的原型屬性constructor。經過這兩個屬性,就能夠訪問到原型裏的屬性和方法了。

PS:IE 瀏覽器在腳本訪問__proto__會不能識別,火狐和谷歌瀏覽器及其餘某些瀏覽器均能識別。雖然能夠輸出,但沒法獲取內部信息。

alert(box1.__proto__); //[object Object]

判斷一個對象是否指向了該構造函數的原型對象,可使用 isPrototypeOf()方法來測試。

alert(Box.prototype.isPrototypeOf(box)); //只要實例化對象,即都會指向

原型模式的執行流程:
1.先查找構造函數實例裏的屬性或方法,若是有,馬上返回;
2.若是構造函數實例裏沒有,則去它的原型對象裏找,若是有,就返回;

雖然咱們能夠經過對象實例訪問保存在原型中的值,但卻不能訪問經過對象實例重寫原型中的值。

var box1 = new Box();
alert(box1.name); //Lee,原型裏的值
box1.name = 'Jack';
alert(box.1name); //Jack,就近原則,

var box2 = new Box();
alert(box2.name); //Lee,原型裏的值,沒有被 box1 修改

若是想要 box1 也能在後面繼續訪問到原型裏的值,能夠把構造函數裏的屬性刪除便可,具體以下:

delete box1.name; //刪除屬性
alert(box1.name);

如何判斷屬性是在構造函數的實例裏,仍是在原型裏?可使用 hasOwnProperty()函數來驗證:

alert(box.hasOwnProperty('name')); //實例裏有返回 true,不然返回 false

構造函數實例屬性和原型屬性示意圖:
這裏寫圖片描述

in 操做符會在經過對象可以訪問給定屬性時返回 true,不管該屬性存在於實例中仍是原
型中。

alert('name' in box); //true,存在實例中或原型中

咱們能夠經過hasOwnProperty()方法檢測屬性是否存在實例中,也能夠經過 in 來判斷實例或原型中是否存在屬性。那麼結合這兩種方法,能夠判斷原型中是否存在屬性。

function isProperty(object, property) { //判斷原型中是否存在屬性
    return !object.hasOwnProperty(property) && (property in object);
}

var box = new Box();
alert(isProperty(box, 'name')) //true,若是原型有

爲了讓屬性和方法更好的體現封裝的效果,而且減小沒必要要的輸入,原型的建立可使用字面量的方式:

function Box() {};
    Box.prototype = { //使用字面量的方式
    name : 'Lee',
    age : 100,
    run : function () {
        return this.name + this.age + '運行中...';
    }
};

使用構造函數建立原型對象和使用字面量建立對象在使用上基本相同,但仍是有一些區別,字面量建立的方式使用 constructor 屬性不會指向實例,而會指向 Object,構造函數建立的方式則相反。

var box = new Box();
alert(box instanceof Box);
alert(box instanceof Object);
alert(box.constructor == Box); //字面量方式,返回 false,不然,true
alert(box.constructor == Object); //字面量方式,返回 true,不然,false

若是想讓字面量方式的 constructor 指向實例對象,那麼能夠這麼作:

Box.prototype = { constructor : Box, //直接強制指向便可 };

PS:字面量方式爲何 constructor 會指向 Object?由於 Box.prototype={};這種寫法其實就是建立了一個新對象。而每建立一個函數,就會同時建立它 prototype,這個對象也會自動獲取 constructor 屬性。因此,新對象的 constructor 重寫了 Box 原來的 constructor,所以會指向新對象,那個新對象沒有指定構造函數,那麼就默認爲 Object。

原型的聲明是有前後順序的,因此,重寫的原型會切斷以前的原型。

function Box() {};

Box.prototype = { //原型被重寫了
    constructor : Box,
    name : 'Lee',
    age : 100,
    run : function () {
        return this.name + this.age + '運行中...';
    }
};

Box.prototype = {
    age = 200
};

var box = new Box(); //在這裏聲明
alert(box.run()); //box 只是最初聲明的原型

原型對象不只僅能夠在自定義對象的狀況下使用,而 ECMAScript 內置的引用類型均可以使用這種方式,而且內置的引用類型自己也使用了原型。

alert(Array.prototype.sort); //sort 就是 Array 類型的原型方法
alert(String.prototype.substring); //substring 就是 String 類型的原型方法

String.prototype.addstring = function () { //給 String 類型添加一個方法
    return this + ',被添加了!'; //this 表明調用的字符串
};

alert('Lee'.addstring()); //使用這個方法

PS:儘管給原生的內置引用類型添加方法使用起來特別方便,但咱們不推薦使用這種方法。由於它可能會致使命名衝突,不利於代碼維護。

原型模式建立對象也有本身的缺點,它省略了構造函數傳參初始化這一過程,帶來的缺點就是初始化的值都是一致的。而原型最大的缺點就是它最大的優勢,那就是共享。原型中全部屬性是被不少實例共享的,共享對於函數很是合適,對於包含基本值的屬性也還能夠。但若是屬性包含引用類型,就存在必定的問題:

function Box() {};

Box.prototype = {
    constructor : Box,
    name : 'Lee',
    age : 100,
    family : ['父親', '母親', '妹妹'], //添加了一個數組屬性
    run : function () {
        return this.name + this.age + this.family;
    }
};

var box1 = new Box();

box1.family.push('哥哥'); //在實例中添加'哥哥'
alert(box1.run());

var box2 = new Box();
alert(box2.run()); //共享帶來的麻煩,也有'哥哥'了

PS:數據共享的緣故,致使不少開發者放棄使用原型,由於每次實例化出的數據須要保留本身的特性,而不能共享。

爲了解決構造傳參和共享問題,能夠組合構造函數+原型模式:

function Box(name, age) { //不共享的使用構造函數
    this.name = name;
    this.age = age;
    this. family = ['父親', '母親', '妹妹'];
};

Box.prototype = { //共享的使用原型模式
    constructor : Box,
    run : function () {
        return this.name + this.age + this.family;
    }
};

PS:這種混合模式很好的解決了傳參和引用共享的大難題。是建立對象比較好的方法。

原型模式,無論你是否調用了原型中的共享方法,它都會初始化原型中的方法,而且在聲明一個對象時,構造函數+原型部分讓人感受又很怪異,最好就是把構造函數和原型封裝到一塊兒。爲了解決這個問題,咱們可使用動態原型模式。

function Box(name ,age) { //將全部信息封裝到函數體內
    this.name = name;
    this.age = age;
    if (typeof this.run != 'function') { //僅在第一次調用的初始化
        Box.prototype.run = function () {
            return this.name + this.age + '運行中...';
        };
    }
}

var box = new Box('Lee', 100);
alert(box.run());

當第一次調用構造函數時,run()方法發現不存在,而後初始化原型。當第二次調用,就不會初始化,而且第二次建立新對象,原型也不會再初始化了。這樣及獲得了封裝,又實現了原型方法共享,而且屬性都保持獨立。

if (typeof this.run != 'function') {
    alert('第一次初始化'); //測試用
    Box.prototype.run = function () {
        return this.name + this.age + '運行中...';
    };
}

var box = new Box('Lee', 100); //第一次建立對象
alert(box.run()); //第一次調用
alert(box.run()); //第二次調用

var box2 = new Box('Jack', 200); //第二次建立對象
alert(box2.run());
alert(box2.run());

PS:使用動態原型模式,要注意一點,不能夠再使用字面量的方式重寫原型,由於會切斷實例和新原型之間的聯繫。

以上講解了各類方式對象建立的方法,若是這幾種方式都不能知足需求,可使用一開始那種模式:寄生構造函數

function Box(name, age) {
    var obj = new Object();
    obj.name = name;
    obj.age = age;
    obj.run = function () {
        return this.name + this.age + '運行中...';
    };
    return obj;
}

寄生構造函數,其實就是工廠模式+構造函數模式。這種模式比較通用,但不能肯定對象關係,因此,在可使用以前所說的模式時,不建議使用此模式。

在什麼狀況下使用寄生構造函數比較合適呢?假設要建立一個具備額外方法的引用類型。因爲以前說明不建議直接 String.prototype.addstring,能夠經過寄生構造的方式添加。

function myString(string) {
    var str = new String(string);
    str.addstring = function () {
        return this + ',被添加了!';
    };
    return str;
}

var box = new myString('Lee'); //比直接在引用原型添加要繁瑣好多
alert(box.addstring());

在一些安全的環境中,好比禁止使用 this 和 new,這裏的 this 是構造函數裏不使用 this,這裏的 new 是在外部實例化構造函數時不使用 new。這種建立方式叫作穩妥構造函數

function Box(name , age) {
    var obj = new Object();
    obj.run = function () {
        return name + age + '運行中...'; //直接打印參數便可
    };
    return obj;
}

var box = Box('Lee', 100); //直接調用函數
alert(box.run());

PS:穩妥構造函數和寄生相似。

三.繼承

1.原型鏈繼承

繼承是面向對象中一個比較核心的概念。其餘正統面嚮對象語言都會用兩種方式實現繼承:一個是接口實現,一個是繼承。而 ECMAScript 只支持繼承,不支持接口實現,而實現繼承的方式依靠原型鏈完成。

function Box() { //Box 構造
    this.name = 'Lee';
}

function Desk() { //Desk 構造
    this.age = 100;
}

Desk.prototype = new Box(); //Desc 繼承了 Box,經過原型,造成鏈條

var desk = new Desk();
alert(desk.age);
alert(desk.name); //獲得被繼承的屬性

function Table() { //Table 構造
    this.level = 'AAAAA';
}

Table.prototype = new Desk(); //繼續原型鏈繼承

var table = new Table();
alert(table.name); //繼承了 Box 和 Desk

原型鏈繼承流程圖:
這裏寫圖片描述

若是要實例化 table,那麼 Desk 實例中有 age=100,原型中增長相同的屬性 age=200,最後結果是多少呢?

Desk.prototype.age = 200; //實例和原型中均包含 age

PS:以上原型鏈繼承還缺乏一環,那就是 Obejct,全部的構造函數都繼承自 Obejct。而繼承 Object 是自動完成的,並不須要程序員手動繼承。

通過繼承後的實例,他們的從屬關係會怎樣呢?

alert(table instanceof Object); //true
alert(desk instanceof Table); //false,desk 是 table 的超類
alert(table instanceof Desk); //true
alert(table instanceof Box); //true

在 JavaScript 裏,被繼承的函數稱爲超類型(父類,基類也行,其餘語言叫法),繼承的函數稱爲子類型(子類,派生類)。繼承也有以前問題,好比字面量重寫原型會中斷關係,使用引用類型的原型,而且子類型還沒法給超類型傳遞參數。

2.對象冒充

爲了解決引用共享和超類型沒法傳參的問題,咱們採用一種叫借用構造函數的技術,或者成爲對象冒充(僞造對象、經典繼承)的技術來解決這兩種問題。

function Box(age) {
    this.name = ['Lee', 'Jack', 'Hello']
    this.age = age;
}

function Desk(age) {
    Box.call(this, age); //對象冒充,給超類型傳參
}

var desk = new Desk(200);
alert(desk.age);
alert(desk.name);
desk.name.push('AAA'); //添加的新數據,只給 desk
alert(desk.name);

3.組合繼承

借用構造函數雖然解決了剛纔兩種問題,但沒有原型,複用則無從談起。因此,咱們須要原型鏈+借用構造函數的模式,這種模式成爲組合繼承

function Box(age) {
    this.name = ['Lee', 'Jack', 'Hello']
    this.age = age;
}

Box.prototype.run = function () {
    return this.name + this.age;
};

function Desk(age) {
    Box.call(this, age); //對象冒充
}

Desk.prototype = new Box(); //原型鏈繼承

var desk = new Desk(100);
alert(desk.run());

4.原型式繼承

還有一種繼承模式叫作:原型式繼承;這種繼承藉助原型並基於已有的對象建立新對象,同時還沒必要所以建立自定義類型。

function obj(o) { //傳遞一個字面量函數
    function F() {} //建立一個構造函數
    F.prototype = o; //把字面量函數賦值給構造函數的原型
    return new F(); //最終返回出實例化的構造函數
}

var box = { //字面量對象
    name : 'Lee',
    arr : ['哥哥','妹妹','姐姐']
};

var box1 = obj(box); //傳遞
alert(box1.name);

box1.name = 'Jack';
alert(box1.name);

alert(box1.arr);
box1.arr.push('父母');
alert(box1.arr);

var box2 = obj(box); //傳遞
alert(box2.name);
alert(box2.arr); //引用類型共享了

5.寄生式繼承

寄生式繼承把原型式+工廠模式結合而來,目的是爲了封裝建立對象的過程。

function create(o) { //封裝建立過程
    var f= obj(o);
    f.run = function () {
        return this.arr; //一樣,會共享引用
    };
    return f;
}

組合式繼承是 JavaScript 最經常使用的繼承模式;但,組合式繼承也有一點小問題,就是超類型在使用過程當中會被調用兩次:一次是建立子類型的時候,另外一次是在子類型構造函數的內部。

function Box(name) {
    this.name = name;
    this.arr = ['哥哥','妹妹','父母'];
}

Box.prototype.run = function () {
    return this.name;
};

function Desk(name, age) {
    Box.call(this, name); //第二次調用 Box
    this.age = age;
}

Desk.prototype = new Box(); //第一次調用 Box

6.寄生組合繼承

以上代碼是以前的組合繼承,那麼寄生組合繼承,解決了兩次調用的問題。

function obj(o) {
    function F() {}
    F.prototype = o;
    return new F();
}

function create(box, desk) {
    var f = obj(box.prototype);
    f.constructor = desk;
    desk.prototype = f;
}

function Box(name) {
    this.name = name;
    this.arr = ['哥哥','妹妹','父母'];
}

Box.prototype.run = function () {
    return this.name;
};

function Desk(name, age) {
    Box.call(this, name);
    this.age = age;
}

inPrototype(Box, Desk); //經過這裏實現繼承

var desk = new Desk('Lee',100);
desk.arr.push('姐姐');
alert(desk.arr);
alert(desk.run()); //只共享了方法

var desk2 = new Desk('Jack', 200);
alert(desk2.arr); //引用問題解決
相關文章
相關標籤/搜索