我對javascript對象的理解

前言

JavaScript這門語言除了基本類型都是對象,能夠說JavaScript核心就是對象,所以理解JavaScript對象及其種種特性相當重要,這是內功。本文介紹了我對es5對象,原型, 原型鏈,以及繼承的理解javascript

注意(這篇文章特別長)這篇文章僅僅是我我的對於JavaScript對象的理解,並非教程。這篇文章寫於我剛瞭解js對象以後。文章確定有錯誤之處,還望讀者費心指出,在下方評論便可^-^html

什麼是JavaScript對象

var person = {   //person就是對象,對象都有各類屬性,每一個屬性又都對應着本身的值
    //鍵值對形式
    name: "Mofan",//能夠包含字符串
    age: 20,//數字
    parents: [  //數組
        "Daddy",
        "Mami",
    ]
    sayName: function(){  //函數
        console.log(this.name);
    },
    features: {   //對象
        height: "178cm",
        weight: "60kg",
    }
}複製代碼

js裏除了基本類型外全部事物都是對象:java

  • 函數是對象function sayName(){} ——sayName是函數對象web

  • 數組是對象var arr = new Array() ——arr是數組對象編程

爲何JavaScript要這麼設計呢?我以爲首先這樣一來,統一了數據結構,使JavaScript成爲一門編程風格很是自由化的腳本語言:不管定義什麼變量,通通var;其次,JavaScript對象都有屬性和方法,函數數組都是對象,調用引用就會很是靈活方便;再者,爲了構建原型鏈?數組

建立對象的幾種方式

  • Object()模式使用對象字面量:var obj={...}就像上面那樣或者使用原生構造函數Object():瀏覽器

var person = new Object();
    person.name = "Mofan";
    person.sayName = function(){
        console.log(this.name);
        };
    console.log(person.name);//Mofan
    obj.sayName();//Mofan
    複製代碼
  • 利用函數做用域使用自定義構造函數模式模仿類(構造器模式):數據結構

function Person(name,age){
        this.name = name;
        this.age = age;
        this.print = function(){
            console.log(this.name + this.age)
            };
    }
    var person = new Person("Mofan",19);
    console.log(person.name+person.age);//Mofan19
    person.print();//Mofan19複製代碼
  • 原型模式:wordpress

function Person(){}
    //能夠這樣寫
    /*Person.prototype.name = "Mofan"; Person.prototype.age = 19; Person.prototype.print = function(){ console.log(this.name+this.age); }*/
    //推薦下面這樣寫,但兩種方式不能混用!由於下面這種方式實際上重寫了
    //Person原型對象,若是二者混用,後面賦值方式會覆蓋前面賦值方式
    Person.prototype = {
        name:"Mofan",
        age:19,
        print:function(){
            console.log(this.name+this.age);
        }
    }
    var person = new Person();
    console.log(person.name+person.age);//Mofan19
    person.print();//Mofan19複製代碼
  • 組合構造函數模式和原型模式:函數

function Person(name,age){
        //這裏面初始化屬性
        this.name = name;
        this.age = age;
        ...
    }
    Person.prototype = {
        //這裏面定義公有方法
        print:function(){
            console.log(this.name+this.age);
        },
        ...
    }
    var person = new Person("Mofan",19);
    console.log(person.name+person.age);//Mofan19
    person.print();//Mofan19複製代碼
  • 動態建立原型模式:

function Person(name,age){
        //初始化屬性
        this.name = name;
        this.age = age;
        //在建立第一個對象(第一次被調用)時定義全部公有方法,之後再也不調用
        if(typeof this.print !="function"){
            Person.prototype.print =function(){
                    console.log(this.name+this.age);
                };
            Person.prototype.introduction=function(){
                    console.log("Hi!I'm "+this.name+",I'm "+this.age);
                };
                //若是採用對象字面量對原型添加方法的話,第一次建立的對象將不會有這些方法
            };
            
        
    }
    var person = new Person("Mofan",19);
    person.print();//Mofan19
    person.introduction();//Hi!I'm Mofan,I'm 19複製代碼

還有一些模式用的場景比較少

這些模式的應用場景

怎麼會有這麼多的建立模式?實際上是由於js語言太靈活了,所以前輩們總結出這幾種建立方式以應對不一樣的場景,它們各有利弊。

  • 第一種方式,使用字面量或者使用構造函數Object()經常使用於建立普通對象存儲數據等。它們的原型都是Object,彼此之間沒有什麼關聯。事實上,下面建立方式都是同樣的:

var o1 = {};//字面量的表現形式
    var o2 = new Object;
    var o3 = new Object();
    var o4 = new Object(null);
    var o5 = new Object(undefined);
    var o6 = Object.create(Object.prototype);//等價於 var o = {};//即以 Object.prototype 對象爲一個原型模板,新建一個以這個原型模板爲原型的對象複製代碼

  • 第二種方式,利用函數做用域模仿類,這樣就能夠在建立對象時傳參了,能夠建立不一樣屬性值得對象,實現對象定製。不過print方法也定義在了構造函數裏面,若是要把它當作公有方法的話,這樣每new一個對象,都會有這個方法,太浪費內存了。能夠這樣修改一下構造器模式:

//構造器方法2
    function print(){      //定義一個全局的 Function 對象,把要公有的方法拿出來
         console.log(this.name + this.age);
    }
    
    function Person(name,age){
        this.name = name;
        this.age = age;
   
        this.print = print.bind(this);//每一個 Person 對象共享同一個print 方法版本(方法有本身的做用域,不用擔憂變量被共享)
    }
    var person = new Person("Mofan",19);
    console.log(person.name+person.age);//Mofan19
    person.print();//Mofan19
    複製代碼

然而這樣看起來很亂,也談不上類的封裝性。仍是使用原型吧

  • 第三種方式,純原型模式,不論是屬性仍是方法都添加到原型裏面去了,這樣作好處是很省內存,可是應用範圍就少了,更多的對象 內部的屬性是須要定製的,並且一旦更改原型,全部這個原型實例都會跟着改變。所以能夠結合構造函數方式來實現對對象的定製,因而就有了第四種方式——組合構造函數模式與原型模式,能夠定製的放在構造器裏,共有的放在原型裏,這也符合構造器和原型的特性。「這是es5中使用最普遍、認同度最高的建立自定義類型的方法」---《JavaScript高級程序設計》第三版

  • 第五種方式,動態原型模式,出現這種方式是由於有些面向對象開發人員習慣了類構造函數,因而對這種獨立出來的構造函數和原型感到困惑和不習慣。因而,就出現了把定義原型也寫進構造函數裏的動態原型模式。上面在動態原型模式程序裏面講「若是採用對象字面量對原型添加方法的話,第一次建立的對象將不會有這些方法」這是由於在if語句執行之前,第一個對象已經被建立了,而後執行if裏面的語句,若是採用對象字面量給原型賦值,就會致使原型在實例建立以後被重寫,建立的第一個實例就會失去與原型的連接,也就沒有原型裏的方法了。不過之後建立的對象就可使用原型裏的方法了,由於它們都是原型被修改後建立的。

原型是什麼

在JavaScript中,原型就是一個對象,不必把原型和其餘對象區別對待,只是經過它能夠實現對象之間屬性的繼承。任何一個對象也能夠成爲原型。之因此常常說對象的原型,實際上就是想找對象繼承的上一級對象。對象與原型的稱呼是相對的,也就是說,一個對象,它稱呼繼承的上一級對象爲原型,它本身也能夠稱做原型鏈下一級對象的原型。

一個對象內部的[[Prototype]]屬性生來就被建立,它指向繼承的上一級對象,稱爲原型。函數對象內部的prototype屬性也是生來就被建立(只有函數對象有prototype屬性),它指向函數的原型對象(不是函數的原型!)。當使用var instance = new Class();這樣每new一個函數(函數被當作構造函數來使用)建立實例時,JavaScript就會把這個原型的引用賦值給實例的原型屬性,因而實例內部的[[Prototype]]屬性就指向了函數的原型對象,也就是prototype屬性。

原型真正意義上指的是一個對象內部的[[Prototype]]屬性,而不是函數對象內部的prototype屬性,這二者之間沒有關係!對於一個對象內部的[[Prototype]]屬性,不一樣瀏覽器有不一樣的實現:

var a = {}; 
 
     //Firefox 3.6+ and Chrome 5+ 
     Object.getPrototypeOf(a); //[object Object] 
     
     //Firefox 3.6+, Chrome 5+ and Safari 4+ 
    a.__proto__; //[object Object] 
     
     //all browsers 
     a.constructor.prototype; //[object Object]複製代碼

之因此函數對象內部存在prototype屬性,而且能夠用這個屬性建立一個原型,是由於這樣以來,每new一個這樣的函數(函數被當作構造函數來使用)建立實例,JavaScript就會把這個原型的引用賦值給實例的原型屬性,這樣以來,在原型中定義的方法等都會被全部實例共用,並且,一旦原型中的某個屬性被定義,就會被全部實例所繼承(就像上面的例子)。這種操做在性能和維護方面其意義是不言自明的。這也正是構造函數存在的意義(JavaScript並無定義構造函數,更沒有區分構造函數和普通函數,是開發人員約定俗成)。下面是一些例子:

var a = {}    //一個普通的對象
    function fun(){}   //一個普通的函數
    //普通對象沒有prototype屬性
    console.log(a.prototype);//undefined
    console.log(a.__proto__===Object.prototype);//true
    
    //只有函數對象有prototype屬性
    console.log(fun.prototype);//Object
    console.log(fun.__proto__===Function.prototype);//trueconsole.log(fun.prototype.__proto__===Object.prototype);//true
    console.log(fun.__proto__.__proto__===Object.prototype);//true
    console.log(Function.prototype.__proto__===Object.prototype);//true
    console.log(Object.prototype.__proto__);//null複製代碼

當執行console.log(fun.prototype);輸出爲能夠看到,每建立一個函數,就會建立prototype屬性,這個屬性指向函數的原型對象(不是函數的原型),而且這個原型對象會自動得到constructor屬性,這個屬性是指向prototype屬性所在函數的指針。而__proto__屬性是每一個對象都有的。

接着上面再看:

function Person(){}//構造函數,約定首字母大寫
    var person1 = new Person();//person1爲Person的實例console.log(person1.prototype);//undefined
    console.log(person1.__proto__===Person.prototype);//true
    console.log(person1.__proto__.__proto__===Object.prototype);//true
    console.log(person1.constructor);//function Person(){}
    
    //函數Person是Function構造函數的實例
    console.log(Person.__proto__===Function.prototype);//true
    //Person的原型對象是構造函數Object的實例
    console.log(Person.prototype.__proto__===Object.prototype);//true複製代碼

person1和上面那個普通的對象a有區別,它是構造函數Person的實例。前面講過:

當使用var instance = new Class();這樣每new一個函數(函數被當作構造函數來使用)建立實例時,JavaScript就會把這個原型的引用賦值給實例的原型屬性,因而實例內部的[[Prototype]]屬性就指向了函數的原型對象,也就是prototype屬性。

所以person1內部的[[Prototype]]屬性就指向了Person的原型對象,而後Person的原型對象內部的[[Prototype]]屬性再指向Object.prototype,至關於在原型鏈中加了一個對象。經過這種操做,person1就有了構造函數的原型對象裏的方法。

另外,上面代碼console.log(person1.constructor);//function Person(){}中,person1內部並無constructor屬性,它只是順着原型鏈往上找,在person1.__proto__裏面找到的。

能夠用下面這張圖理清原型、構造函數、實例之間的關係:

繼承

JavaScript並無繼承這一現有的機制,但能夠利用函數、原型、原型鏈模仿。下面是三種繼承方式:

類式繼承

//父類
    function SuperClass(){
        this.superValue = "super";
    }
    SuperClass.prototype.getSuperValue = function(){
        return this.superValue;
​
    };
    //子類
    function SubClass(){
        this.subValue = "sub";
    }
    //類式繼承,將父類實例賦值給子類原型,子類原型和子類實例能夠訪問到父類原型上以及從父類構造函數中複製的屬性和方法
    SubClass.prototype = new SuperClass();
    //爲子類添加方法
    SubClass.prototype.getSubValue = function(){
        return this.subValue;
    }
    
    //使用
    var instance = new SubClass();
    console.log(instance.getSuperValue);//super
    console.log(instance.getSubValue);//sub複製代碼

這種繼承方式有很明顯的兩個缺點:

  • 實例化子類時沒法向父類構造函數傳參

  • 若是父類中的共有屬性有引用類型,就會在子類中被全部實例所共用,那麼任何一個子類的實例更改這個引用類型就會影響其餘子類實例,可使用構造函數繼承方式解決這一問題

構造函數繼承

//父類
    function SuperClass(id){
        this.superValue = ["big","large"];//引用類型
        this.id = id;
    }
    SuperClass.prototype.getSuperValue = function(){
        return this.superValue;
​
    };
    //子類
    function SubClass(id){
        SuperClass.call(this,id);//調用父類構造函數並傳參
        this.subValue = "sub";
    }
     var instance1 = new SubClass(10);//能夠向父類傳參
     var instance2 = new SubClass(11);
     
    instance1.superValue.push("super");
    console.log(instance1.superValue);//["big", "large", "super"]
    console.log(instance1.id);//10
    console.log(instance2.superValue);["big", "large"]
    console.log(instance2.id);//11
    console.log(instance1.getSuperValue());//error複製代碼

這種方式是解決了類式繼承的缺點,不過在代碼的最後一行你也看到了,沒有涉及父類原型,所以違背了代碼複用的原則。因此組合它們:

組合繼承

function SuperClass(id){
        this.superValue = ["big","large"];//引用類型
        this.id = id;
    }
    SuperClass.prototype.getSuperValue = function(){
        return this.superValue;
​
    };
    //子類
    function SubClass(id,subValue){
        SuperClass.call(this,id);//調用父類構造函數並傳參
        this.subValue = subValue;
    }
     SubClass.prototype = new SuperClass();
      SubClass.prototype.getSubValue = function(){
        return this.subValue;
    }
    
     var instance1 = new SubClass(10,"sub");//能夠向父類傳參
     var instance2 = new SubClass(11,"sub-sub");
​
    instance1.superValue.push("super");
    console.log(instance1.superValue);//["big", "large", "super"]
    console.log(instance1.id);//10
    console.log(instance2.superValue);["big", "large"]
    console.log(instance2.id);//11
    console.log(instance1.getSuperValue());["big", "large", "super"]
    console.log(instance1.getSubValue());//sub
    console.log(instance2.getSuperValue());//["big", "large"]
    console.log(instance2.getSubValue());//sub-sub複製代碼

嗯,比較完美了,可是有一點,父類構造函數被調用了兩次,這就致使第二次調用也就是建立實例時重寫了原型屬性,原型和實例都有這些屬性,顯然性能並很差。先來看看克羅克福德的寄生式繼承:

function object(o){
        function F(){};
        F.prototype = o;
        return new F();
   }
    function createAnnther(original){
        var clone = object(original);
        clone.sayName = function(){
            console.log(this.name);
        }
        return clone;
   }
    var person = {
        name:"Mofan",
        friends:["xiaoM","Alice","Neo"],
   };
    var anotherPerson = createAnnther(person);
    anotherPerson.sayName();//"Mofan"
}複製代碼

就是讓一個已有對象變成新對象的原型,而後再在createAnother函數裏增強。你也看到了,person就是一個普通對象,因此這種寄生式繼承適合於根據已有對象建立一個增強版的對象,在主要考慮經過已有對象來繼承而不是構造函數的狀況下,這種方式的確很方便。但缺點也是明顯的,createAnother函數不能複用,我若是想給另一個新建立的對象定義其餘方法,還得再寫一個函數。仔細觀察一下,其實寄生模式就是把原型給了新對象,對象再增強。

等等,寫到這個地方,我腦子有點亂,讓咱們回到原點:繼承的目的是什麼?應該繼承父類哪些東西?我以爲取決於咱們想要父類的什麼,我想要父類所有的共有屬性(原型裏)而且能夠自定義繼承的父類私有屬性(構造函數裏)!前面那麼多模式它們的缺點主要是由於這個:

SubClass.prototype = new SuperClass();複製代碼

那爲何要寫這一句呢?是隻想要繼承父類的原型嗎?若是是爲何不這麼寫:

SubClass.prototype = SuperClass.prototype;複製代碼

這樣寫是能夠繼承父類原型,可是風險極大:SuperClass.prototype屬性它是一個指針,指向SuperClass的原型,若是把這個指針賦給子類prototype屬性,那麼子類prototype也會指向父類原型。對SubClass.prototype任何更改,就是對父類原型的更改,這顯然是不行的。

寄生組合式繼承

但出發點沒錯,能夠換種繼承方式,看看上面的寄生式繼承裏的object()函數,若是把父類原型做爲參數,它返回的對象實現了對父類原型的繼承,沒有調用父類構造函數,也不會對父類原型產生影響,堪稱完美。

function object(o){
        function F(){};
        F.prototype = o;
        return new F();
   }
    function inheritPrototype(subType,superType){
        var proto = object(superType.prototype);
        proto.constructor = subType;//矯正一下construcor屬性
        subType.prototype = proto;
   }
​
   function SuperClass(id){
        this.superValue = ["big","large"];//引用類型
        this.id = id;
    }
    SuperClass.prototype.getSuperValue = function(){
        return this.superValue;
​
    };
    //子類
    function SubClass(id,subValue){
        SuperClass.call(this,id);//調用父類構造函數並傳參
        this.subValue = subValue;
    }
   inheritPrototype(SubClass,SuperClass);//繼承父類原型
    SubClass.prototype.getSubValue = function(){
        return this.subValue;
    }
    var instance1 = new SubClass(10,"sub");//能夠向父類傳參
     var instance2 = new SubClass(11,"sub-sub");
​
    instance1.superValue.push("super");
    console.log(instance1.superValue);//["big", "large", "super"]
    console.log(instance1.id);//10
    console.log(instance2.superValue);//["big", "large"]
    console.log(instance2.id);//11
    console.log(instance1.getSuperValue());//["big", "large", "super"]
    console.log(instance1.getSubValue());//sub
    console.log(instance2.getSuperValue());//["big", "large"]
    console.log(instance2.getSubValue());//sub-sub複製代碼

解決了組合繼承的問題,只調用了一次父類構造函數,並且還能保持原型鏈不變,爲何這麼說,看對寄生組合的測試:

console.log(SubClass.prototype.__proto__===SuperClass.prototype);//ture
    console.log(SubClass.prototype.hasOwnProperty("getSuperValue"));//false複製代碼

所以,這是引用類型最理想的繼承方式。

總結

建立用於繼承的對象最理想的方式是組合構造函數模式和原型模式(或者動態原型模式),就是讓可定義的私有屬性放在構造函數裏,共有的放在原型裏;繼承最理想的方式是寄生式組合,就是讓子類的原型的[[prototype]]屬性指向父類原型,而後在子類構造函數裏調用父類構造函數實現自定義繼承的父類屬性。

JavaScript對象總有一些讓我困惑的地方,不過我還會繼續探索。我在此先把我瞭解的記錄下來,與各位共勉。錯誤的地方請費心指出,我將感謝您的批評指正。

本文爲做者原創,轉載請註明本文連接,做者保留權利。

參考文獻:[1] www.cnblogs.com/chuaWeb/p/5…[2] www.cnblogs.com/xjser/p/496…[3] javascriptweblog.wordpress.com/2010/06/07/…

相關文章
相關標籤/搜索