JavaScript建立對象、原型與繼承

      JavaScript是一門極其靈活的語言,爛七八糟的設計是它最大的優勢。不一樣於其餘嚴格類型的語言例如java,學習曲線比較友好。JavaScript我的感受上手基本不用費勁,要想上高度那就是一個悲催併且毀三觀的故事。特別是有面向對象語言基礎的人來講,JavaScript真像一個噩夢。JavaScript更加的零碎,封裝的不是很好。你必須理清脈絡深刻理解了,才能寫出來高大上的優雅的代碼。在下儘可能的用簡練易懂的語言,簡單的闡述一下我對JavaScript面向對象的一點粗淺的理解。java

1,要想面向對象先得建立對象

a,原始模式

var object = new Object();
object.name="huazi";
object.age="22";
object.sayHi = function(){
       console.log("hi"+this.name);
}

object.sayHi(); //hi huazi

首先經過一個名稱爲object的對象,爲其添加name和age屬性以及一個sayHi的方法。this.name將會被解析成object.name。這種方式的缺點顯而易見:使用同一個接口(Object)建立對象,產生大量的冗餘代碼。因此通常不會這麼使用。安全

b,工廠模式

function createObject(name,age){
      var obj = new Object();
      obj.name=name;
      obj.age=age;

      obj.sayHi=function(){
            console.log("hi "+this.name);
      }
      return obj;
}

var obj = createObject("huazi",22);
console.log(obj.sayHi(); //hi huazi

此種方式建立對象叫作工廠模式,你給我屬性,我給你個包含這些屬性和方法的對象.這種模式建立的對象也有很大的問題:獲得的對象屬於什麼類型的是不肯定的.instanceof發現只能匹配到Object,不能匹配到createObject。app

c,構造器模式

function createObject(name,age){
    this.name=name;
    this.age=age;
    this.sayHi=function(){
        console.log("hi "+this.name);
    }
}
var obj = new createObject("huazi",22);
obj.sayHi(); //hi huazi
obj instanceof createObject;//true

構造器模式建立對象的過程分爲四步:1,建立一個Object類型對象.2,將執行環境交給這個對象(this指向).3,執行構造很熟.4,返回對象.此種方式解決了類型不肯定的問題.可是缺點是,在每次建立對象的過程當中,都會從新建立類如sayHi的對象.而這明顯是沒必要要的.修改以下:函數

function createObject(name,age){
    this.name=name;
    this.age=age;
    this.sayHi=sayHi;
}
function sayHi(){
    console.log("hi "+this.name);
}
var obj = new createObject("huazi",22);
obj.sayHi(); //hi huazi
obj instanceof createObject;//true

很簡單,只須要把函數放到全局做用域便可。但是問題又來了,全局做用域中的函數只能被某一個對象調用。這在邏輯上實在有點牽強。更嚴重的狀況是,每每咱們須要定義不少方法來實現一個對象。因此就會出現大量的全局函數,而且全局函數不能在其餘對象上使用。學習

d,原型模式

function createObject(){};
createObject.prototype.name = "huazi";
createObject.prototype.age=22;
createObject.prototype.sayHi=function(){
    console.log("hi "+this.name);
}

var obj1 = new createObject();
var obj2 = new createObject();
obj1.sayHi(); //hi huazi
obj1.sayHi===obj2.sayHi; //true

好了,如今好像解決了剛纔的問題,把屬性和方法都加到了原型中。這樣就不會出現全局屬性和重複函數對象了。這種模式的缺點也顯而易見:構造不能傳參,也就是說全部對象將長的如出一轍。還有就是內存共享的問題。屬性要是引用類型好比Array那麼就熱鬧了。牽一髮動全身。this

e,組合模式(構造+原型)

function createObject(name,age){
    this.name=name;
    this.age=age;
    this.array = [1,2,3];
}
createObject.prototype.sayHi = function(){
    console.log("hi "+this.name);
}

var obj1 = new createObject("huazi",22);
var obj2 = new createObject("huazi",22);
obj1.sayHi(); //hi huazi
obj1.array===obj2.array;//false

除了此種寫法以外,在給原型加方法的時候還可使用字面量的方式添加。可是須要注意的是使用字面量添加等於重寫了prototype了,因此須要顯示的申明,constructor的指向。spa

createObject.prototype={
    constructor:createObject,
    sayHi:function(){
        console.log("hi "+this.name);
    }
}

還有須要注意的一點是,在使用字面量以前不能建立對象。前面說過此種方式等因而重寫了prototype。因此以前建立的對象實例,不會更新新的屬性和方法。爲啥?自行腦補一下。prototype

f,動態原型模式

function createObject(name,age){
    this.name=name;
    this.age=age;
    if(typeof this.sayHi != "function"){  //不存在的狀況下添加方法
        createObject.prototype.sayHi = function(){
            console.log("hi "+this.name);
        }
    }
}

var obj1 = new createObject("huazi",22);
var obj2 = new createObject("saint",22);
obj1.sayHi(); //hi huazi
obj2.sayHi(); //hi saint

這種方式就算比較完美的了。可是還還須要注意,在構造內部不能使用字面量的方式去添加原型屬性。回溯到構造建立對象過程,咱們知道第一步就已經建立好對象了。因此使用字面量也會發生意外的事情。設計

g,寄生構造模式和穩妥模式

寄生構造模式的思路是:建立一個函數,這個函數的職責就是封裝一個對象所須要的全部屬性和方法,而後返回對象。從樣子上來看,此種方式幾乎與工廠模式無異。只是早建立對象的時候方式有些變化。就是使用new關鍵字來建立。指針

function createObject(name,age){
    var obj = new Object();
    obj.name=name;
    obj.age=age;
    obj.sayHi=function(){
        console.log("hi "+this.name);
    }
    return obj;
}
var o  = new createObject("huazi",22);
o.sayHi();

再回憶一下構造模式建立對象的最後一步,返回對象。寄生模式實際上是覆蓋了返回對象的那一步。同時此種模式也沒有擺脫類型不能肯定的問題。那麼此種模式在何時能夠用到呢?

function createObject(){
    var array = new Array();
    //Array構造中建立對象是使用push的
    array.push.apply(array,arguments);
    //添加新的方法
    array.toPipeString=function(){
        return this.join("|");
    }
    return array;
}
var o  = new createObject("huazi",22);
o.toPipeString(); //huazi|22

爲Array添加了一個方法,同時沒有修改Array的構造和原型。實際上就還對Array的二次包裝,添加新的方法。這種方式比較靠譜。安全性也比較好,夠封閉。

還有一種很是安全的建立對象模式叫作穩妥模式。與寄生模式很是的相似

function createObject(name,age){
    var obj = new Object();
    obj.sayHi=function(){
        console.log("hi "+name);
    }
    return obj;
}
var o  = new createObject("huazi",22);
o.sayHi(); //hi huazi

能夠發現,name值只能在sayHi中訪問,在構造中包裝的數據是絕對安全可靠的。這就有點private的意思了。sayHi是暴露給外部的接口。和寄生模式同樣採用穩妥模式建立的對象,類型是沒法肯定的。

2,原型

       不管何時只要是建立了一個新函數,隨之就會根據一組特定的規則爲該函數建立一個prototype屬性。在默認狀況下prototype屬性會自動得到一個constructor(構造函數)屬性,這個函數很是的特殊。包含一個指向prototype屬相所在函數的指針。

function createObject(name,age){}
var o  = new createObject("huazi",22);
console.log(createObject.prototype.constructor);//function createObject(name,age) {}
console.log(createObject.prototype)//createObject(name,age) {}
//判斷對象o是不是原型對象的派生。
console.log(createObject.prototype.isPrototypeOf(o));//true

      其實是這樣的。每一個對象都會有一個_proto_屬性,這個屬性指向的是函數原型createObject.prototype。而crateObject.prototype中存在一個constructor屬性,此屬性指向了createObject構造函數。等於指來指去,指出了一個迴路。isPrototypeOf函數的參數是函數對象實例,做用是判斷該實例的歸屬,是不是該原型對象的派生。使用hasOwnProperty()能夠檢測一個方法是存在於原型中仍是存在於實例中,固然前提是肯定能夠訪問這個方法或者這個方法存在才能肯定。使用delete操做符能夠刪除掉實例中的方法或者屬性。在原型中的屬性方法默認是不能被delete的。還有一個坑就是delete不存在的屬性或者方法也會返回true因此在使用delete的時候須要當心一些。

      在給prototype添加屬性以前建立了一個對象,那麼這個對象是否能夠引用新添加的原型上的屬性呢?答案是能夠的。在訪問屬性的時候首先會查看實例對象是否存在此屬性,否則就去原型找。而_proto_就是指向原型對象的。沒錯是指向,因此無論什麼時候更新原型屬性都是ok的。

3,繼承

      ECMAScript中描述了原型鏈的概念,並說明原型鏈將做爲實現繼承的主要方法。原型鏈顧名思義,就是講原型連接起來實現繼承。子類的prototype指向父類的實例對象就是很簡單的一種實現。

function superClass(){
    this.name="huazi";
}
superClass.prototype.getName=function(){
    console.log(this.name);
}
function childClass(){
    this.name="bob";
}
childClass.prototype = new superClass();

childClass.prototype.getName = function(){
    console.log(this.name+"v");
}

var instance = new childClass();
//重寫了父類方法和屬性
instance.getName();    //bobv
console.log(instance instanceof Object); //true
console.log(instance instanceof childClass);//true
console.log(instance instanceof superClass);//true

console.log(Object.prototype.isPrototypeOf(instance));//true
console.log(superClass.prototype.isPrototypeOf(instance));//true
console.log(childClass.prototype.isPrototypeOf(instance));//true

和其餘的狀況同樣,一旦使用到prototype,就不建議使用字面量的方式爲原型添加屬性了。腦補便可。

同時此種不加區分的繼承方式,任然保留內存共享的問題(引用類型屬性的問題)。同原型模式建立對象的問題是同樣的。固然解決這個問題的辦法和解決建立對象時的方法也是同樣的,那就是混合使用構造和原型來實現繼承。

function superClass(name){
    this.name=name;
    this.number=[1,2,4];
}
superClass.prototype.getName=function(){
    console.log(this.name);
}
function childClass(name,age){
    //繼承屬性
    superClass.call(this,name);
    this.age=age;
}
childClass.prototype = new superClass();

childClass.prototype.getName = function(){
    console.log(this.name+"v");
}

var instance = new childClass("huazi",22);
//重寫了父類方法和屬性
instance.getName();    //huaziv
instance.number.push("1");
instance.number===new childClass().number;  //false
instance.number//[1,2,4,'1']

此種方法的原則就是屬性靠構造,方法靠原型。能夠簡單地這麼理解一下。此種模式也有個問題就是效率沒有達到極致,由於屢次調用了父類構造。還有一個讓人不舒服的地方就是原型和實例上都包括有name和age屬性。這是不能接受的。

最後放一個大招,叫作寄生組合模式。其實也就是避免了上面這個方式的缺點,即繞過父類構造來繼承父類原型。

/**
 * 1,找到父類原型對象
 * 2,修改構造的指向
 * 3,父類原型 
 */
function inheritPrototype(superClass,childClass){
    var _prototype = new Object(superClass.prototype);
    _prototype.constructor = childClass;
    childClass.prototype=_prototype;
}

function superClass(name){
    this.name=name;
    this.number=[1,2,4];
}
superClass.prototype.getName=function(){
    console.log(this.name);
}
function childClass(name,age){
    //繼承屬性
    superClass.call(this,name);
    this.age=age;
}
//不一樣點就再這
inheritPrototype(superClass,childClass);

childClass.prototype.getName = function(){
    console.log(this.name+"v");
}

var instance = new childClass("huazi",22);
//重寫了父類方法和屬性
instance.getName();    //huaziv
instance.number.push("1");
instance.number===new childClass().number;  //false
instance.number//[1,2,4,'1']

噁心死我了,終於寫完了!

相關文章
相關標籤/搜索