js繼承關係

  跟傳統面嚮對象語言比起來,js在繼承關係方面比較特別,若是第一次看恐怕會有些抓狂,偶就是這樣(又透露小白本質#=_=),從哪裏提及好呢?函數調用?php

  js中函數的調用方式大體可分如下幾種:java

  1. 普通函數,直接調用ios

    function Hi(){ alert(233); } Hi(); var f = function(){ alert(666); }; f();

  2. 做爲對象的方法調用c++

    var obj = { x:1,  m:function(){ alert("hello"); } }; obj.m();

  3. 構造函數調用,即new 表達式chrome

    var a = new Array(1,2,3);

  4. 間接調用,最經常使用應該是call或apply了數組

    function f(){
         alert(this.name);  
    }
    var obj = { name:"Apple" };
    f.call(obj);  // 間接調用函數f,輸出Apple

  顧名思義就是非直接的調用一個函數了,這裏的f.call(obj)的至關於obj.f(),若是直接寫obj.f()確定是錯誤的,因obj對象壓根沒定義這個方法,這即是奇妙的地方:obj對象經過call間接調用了f()函數。另外一個跟call類似的函數是apply,道理相同。瀏覽器

  仍是要牢記在js中除了null和undefined外都是對象,它們的基類都是Object,Object是全部對象的祖宗(比如六道仙人他媽),函數天然也是對象,並且js中全部的函數會自動繼承Function這個類(固然最終繼承自Object),在咱們寫js代碼時,環境中已經有了這些類,Function這個類裏面就定義了一些方法,其中就有call和apply,因此f才能打點這麼調。app

  call與apply方法的第一個參數是調用者所在的對象,好比這裏想達到讓obj來調用f()的目的,在f()函數中要對obj對象的屬性進行引用,那麼第一個參數就傳入obj這個對象,這樣f()函數中的上下文就是(context,好比全局做用域在瀏覽器中上下文是window對象,this是對它的引用)obj這個對象,因此在f()函數中this.name引用的是obj的name屬性,這個this即是對obj的引用。函數

  若是f()函數須要傳參數的話,用call這種間接調用的方式怎麼寫?這即是call與apply的不一樣之處了,假如如今f()是這樣的:f(name, age){...},那麼這兩種方式是:工具

  call寫法:f.call(obj, "Li", 23);

  apply寫法:f.apply(obj, ["Li", 23]);

  很明顯的區別:call以單個參數形式、逗號分隔傳入,有幾個寫幾個,放在第一個對象參數後面,apply則把全部要傳入的參數統一放在一個數組中。

  有時調用apply第一個對象參數沒有或沒必要要,能夠傳undefined或null,並且在全局做用域中用這種間接形式調用時,第一個參數能夠是全局對象引用this,或者直接寫undefined時,它會自動去把這個全局下文環境做爲第一個參數對象,所以咱們日常的寫的函數能夠有不少奇怪的寫法:

    // 通常寫法
    Math.max(1, 5, 7, 2); 
    // 奇怪寫法
    Math.max.call(Math, 1, 5, 7, 2);
    Math.max.call(window, 1, 5, 7, 2);
    Math.max.call(this, 1, 5, 7, 2);
    Math.max.call(undefined, 1, 5, 7, 2);
    Math.max.apply(Math, [1, 5, 7, 2]);  // max函數可傳入不定個數參數

  大概設計者想讓天下沒有難調的方法,因此出這麼一招,這樣的話,理論上任何對象的可調方法,幾乎都能被其餘無關的對象調用。

  js有面向對象,卻沒有直接定義的類(第一次聽很神奇),對象的實現由構造函數加調用表達式完成

    function Game(name){
        this.name = name;
    }
    var g = new Game("2048");

  在調用new的時候,對象已經造成,接着運行Game函數,Game函數內部的上下文就變成了這個實例化對象的環境,this即是對這個實例化對象的引用,因此Game構造函數裏邊的語句只是在對這個對象進行一下組裝,就比如:

    var g = {};
    g.name = "2048";  // 對象已生成,這裏對它設置一些屬性

  js的繼承方式大體分兩種,第一種就是像日常那些語言(php、c++、java等),好比class A extends B{...},class A : public B{...},經過對父類的繼承,稱爲類式繼承。通常來講這種方式會在子類構造函數中調用父類構造函數(不是必須),以初始化父類對象中的一些屬性、方法等,js同理也會這樣

    function A(){
        this.a = "A";
    }
    function B(){
        this.b = "B";
        A.call(this);  // 繼承A,調用它的構造函數
    }
    var bObj = new B();
    console.log(bObj.a); // A
    console.log(bObj.b); // B    

  這裏便用到了間接調用,並且通常也會這麼寫,由於須要傳入子對象的上下文this引用。父類構造函數A是個函數對象,它的調用只是在子類加了些東西進來而已,或者說初始化父類的屬性,它的效果跟下面同樣:

    function B(){
        this.b = "B";
        var self = this;
        function A(){ self.a = "A"; }  // 直接定義一個函數,爲這個對象添加屬性
        A();  // 調用它
    }
    var bObj = new B();
    console.log(bObj.a); // A
    console.log(bObj.b); // B            

  類式繼承就這麼簡單,調用了下就表示繼承了另外一個類,但並不經常使用。它的好處是能夠在子類實例化時直接給父類的構造函數傳參

    function A(platform){
        this.platform = platform;  
    }
    function B(name, platform){
        this.name = name;
        A.call(this, platform); // 傳參給父類
    }
    var bObj = new B("2048", "ios");

   第二種是原型鏈繼承,首先要大概知道原型(prototype)是什麼。在js中每個函數都關聯着一個prototype屬性,這是語言自己的機制,這個prototype會指向一個對象,這個對象存放着一些方法和屬性,若是B構造函數的prototype屬性被賦值爲A構造函數建立的實例,就完成了一次原型式繼承:B的實例繼承自A,相似下面child繼承Parent(例1

    function Parent(name){
        this.name = name;
    }
    function Child(age){
        this.age = age;  
    }
    Child.prototype = new Parent("Li");
    var child = new Child(25);
    console.log(child.name); // Li
    console.log(child.age);  // 25

  Child.prototype = new Parent("Li")就表示Child的實例化對象繼承了Parent這個類,prototype這種繼承形式被不少人詬病,但它確實有強大的一面。例如咱們都知道對象類型的變量是引用,引用是共享地址的,一個的屬性值改變了,另外一個的值跟着變,以下

    function Game(){
        this.name = "2048";
    }
    var o1 = new Game();
    console.log(o1.name);  // 2048
    var o2 = o1;
    o2.name = "gta";
    console.log(o1.name);  // gta

  當從Game建立對象時,兩個對象的方法是徹底分開的,各有一份

    function Game(){
        this.name = "2048";
        this.m = function(name){
            this.name = name;
        }
    }
    var o1 = new Game();
    var o2 = new Game();
    console.log(o1.m == o2.m);  // false,兩個對象的方法不相等
    o1.m("gta");
    o2.m("angry bird");
    console.log(o1.name);  // gta
    console.log(o2.name);  // angry bird,o一、o2各改各的值

  但在原型中定義的屬性

    // 以上例爲基礎
    Game.prototype.f = function(){ this.name = "unknown"; };
    console.log(o1.f === o2.f);  // true,o一、o2的f方法相等

  即在prototype中定義的屬性,是被全部實對象共享的,普通對象的屬性,每實例化一次,它們的屬性、方法就被複制一份存到各自的對象裏面去,但prototype中定義的只有一份,由於一個構造函數只關聯一個prototype,而全部的對象從這個構造函數實例化而來。故理論上全部函數都可做爲構造函數並用做原型繼承(固然還有一點限制),他們都有prototype屬性。

  那麼當具體訪問對象的某一屬性時,如何找到?好比o1.name,首先在o1對象中找,當它本身對象中沒有該屬性時,他會到它的構造函數的原型---Game.prototype所指向的對象中去找,而Game.prototype又是由另外一個構造函數設爲A,實例化獲得的對象,這個對象對應的構造函數A也有本身的prototype---A.prototype,當在Game.prototype中沒找到時,又會去A.prototype對中去找,直到最終的老祖宗Object的prototype---它是undefined,此時如還未找到就會報錯,找到了的話在哪一個裏面就用哪一個。這種一級一級的、回溯的prototype造成的鏈式結構被稱爲原型鏈,這種方式也被稱爲原型鏈繼承。

    function Animal(){
        this.name = 'animal';
    }
    function Dog(){
        this.type = 'dog';
    }
    Dog.prototype = new Animal();  // 原型繼承
    function Husky(){
        this.weight = 2.3;
    }
    Husky.prototype = new Dog();

    var o = new Husky();
    // 順着原型鏈查照
    console.log(o.name);
    console.log(o.type);
    console.log(o.weight);

  原型的第一大優勢就是屬性(方法)只保留一份,各對象共享,節省空間,還有個優勢是能夠動態的添加屬性。上例中,o一、o2已經實例化了,在函數對象Game的prototype屬性上添加其餘值時,對象依然能夠訪問到,緣由就是先找本身對象中的屬性,再順着原型鏈找,原型中有那就用它了。

   只有函數纔有prototype屬性且能夠訪問到,其餘對象實例是沒有的,但其餘對象實例有一個__proto__,聽說它是一個內在的指針,指向對象所繼承的類,不少瀏覽器都視其爲私有屬性而不可訪問,貌似火狐將其暴露了出來。在chrome的控制檯中能夠大體能夠看到這種風結構

        

  對象的__proto__指向Object,數組的__proto__指向Array,正由於如此,咱們才能調用到一些對象的方法,好比var a = [1,2,3]; a.push(4);,由於push已經被封裝在Array裏面了,它們在調用屬性、方法時也會順着__proto__這個鏈去找。

  原型或者說原型對象,是類的惟一標識,也就是說當兩個對象繼承自同一個原型對象時,它們才屬於同一個類的實例。當兩個構造函數的prototype是指向同一個對象時,由它們建立的實例纔是同類的,如何判斷對象是否是屬於某個類?可用instanceof運算符,如

      o instanceof Husky

      若是o繼承自Husky.prototype(注意不是Husky),則返回true。既然這樣爲什麼不直接寫 o instanceof Husky.prototype ?結果報錯了:instanceof右邊必須是個函數。另外一種方法是isPrototypeOf,如

  Husky.prototype.isPrototypeOf(o)

  若oHusky的實例化對象則返回true,Husky.prototype是o的原型。

   若是觀察了chrome的控制檯打印對象的話,特別是經過new表達式生成的對象,就會發如今對象的原型__proto__所指的對象中有一個constructor屬性,這個屬性的值就是它本身的構造函數(對象)

    

   對於一個完整的原型對象來講,它的constructor屬性是它自己的構造函數對象。因此更好的處理是這樣的

    function Animal(){
        this.name = "animal";
    }
    function Dog(){
        this.type = "dog";
    }
    Dog.prototype = new Animal();
    Dog.prototype.constructor = Dog;  // 將原型constructor屬性指向本身的構造函數 

  這裏有幾個容易讓人糊塗的東西:構造函數對象、實例化對象、原型對象。梳理一下:

  1. js中除了nullundefined外都是對象,函數固然也是對象,定義了一個函數function fun(){}(無論fun是否做爲構造函數使用),fun就是個函數對象;
  2. 實例化對象,經過new結合構造函數建立的對象,前面說過,相似function Game(){ this.name=」2048」; } var g = new Game(); ,當調用new的時候實例化對象已經生成了,而後再執行Game函數體代碼,函數的上下文變成了這個對象,this是對這個對象的引用,this.name只不過是給這個對象添加屬性,即構造函數內的代碼只是對已生成的對象進行一下組裝而已;
  3. 原型prototype是構造函數對象的一個屬性而已,只不過它剛好也是一個對象,經過原型鏈繼承的實例化對象,繼承自這個原型所指向的對象。這個prototype對象本身也有一些屬性,好比constructor,它的值又是本身的這個函數對象。以下圖

      

   稍微標準點的原型式繼承不是直接B.prototype = new A(),而是封裝到函數裏面,以下

    function inherit(obj){                // 返回繼承自對象obj的新實例
        if(obj === null) return null;
        if(Object.create)    // 使用ECMAScript5函數
            return Object.create(obj);
        var t = typeof obj;
        if(t != "object" && t != "function") return null;
        function f(){}  // 定義一個空函數
        f.prototype = obj;
        return new f();  // 返回繼承原型的實例對象
    }
     var obj = { x:5, m:function(){console.log(this.x);} };
     var subObject = inherit(obj); // 子對象

  這裏用到了Object.create()函數,它的第一個參數是對象,若是隻傳第一個參數如obj,它將返回指定原型爲obj對象的新對象。若是Object.create不存在,建立一個空函數,讓函數的prototype指向obj,再返回以這個空函數的實例對象,也完成了原型式繼承(有的把這個稱做寄生式繼承)。再看下例

    function A(){ this.name = "A"; }
    function B(){ this.type = "B";   }
    B.prototype = inherit(A.prototype);
    B.prototype.constructor = B;  // 將constructor指向本身的構造函數
    var o = new B();

  o是B的實例,B是A的子類,則需保證B的原型對象繼承自A的原型對象,若是不這樣,B的原型只繼承自Object.prototype,那麼o跟一個普通實例沒有差異。OK,這種寫法,o裏面有沒有屬性name?事實是沒有。前面在說類式繼承時,父類構造函數在子類構造函數裏面調用一下,其實至關因而給子類實例添加了屬性,因此子類實例才擁有這個屬性,這裏B的prototype只繼承自A的prototype,也不是前面的原型繼承時寫的B.prototype = new A()(這樣寫o.name是存在的),都是原型繼承,兩種不一樣寫法形成了這一差異。因此原型繼承的核心仍是在於:o的構造函數對象B的prototype原型到底包沒包含所需的屬性。

   針對以上類式繼承和原型式繼承各自的優缺點,又來了個新方式:組合繼承。組合固然是兼具類式繼承和原型式繼承的優勢了。

    function Parent(){
        this.name = "parent";
    }
    function Child(){
        this.type = "child";    
    }
    // 先處理原型鏈
    Child.prototype = new Parent();
    Child.prototype.constructor = Child;
    // 而後添加需繼承的屬性和方法
    Child.prototype.p = "2333";
    Child.prototype.f = function(){ console.log("hello"); };

    var o = new Child();

      以上只是零散的組合先設置好原型鏈,而後手動給原型添加屬性和方法,代替類式繼承的直接傳參,以此克服弊端。若是寫的標準點,首先準備個給原型添加屬性的小函數:

    // 將對象q的屬性添加到p中
    extend(p, q){
        if(p == null || q == null) return;
        if(typeof p != "object" || typeof q != "object") return;
        for(var prop in q)
            p[prop] = q[prop];
    }

  另外一個工具工具函數

   /*
    * 實現子類的繼承
    * @param superClass 父類構造函數對象
    * @param subClass 子類構造函數對象
    * @param methods 包含函數屬性的對象
    * @param props 包含屬性的對象
    * return function  組合繼承後的構造函數
    */
    function inheritClass(superClass, subClass, methods, props){
      // 使子類在原型上繼承自父類
      subClass.prototype = inherit(superClass.prototype);
      subClass.prototype.constructor = subClass;
      // 將所需方法添加至子類原型中
      if(typeof methods == "object") extend(subClass.prototype, methods);
      // 將所需屬性添加至子類原型中
      if(typeof props == "object") extend(subClass,.prototype, props);
      // 返回子類
      return subClass;
   }

  superClass是父類構造函數,subClass是子類構造函數,methods與props爲將要添加的方法和屬性的集合,以對象的方式包裝起來。借用了inherit和extend方法,inherit方法設置好原型鏈,爲了克服沒法直接傳參的弊端,將方法和屬性包裝好後,動態的添加到子類構造函數的prototype中。

  如inheritClass方法添加方法和屬性時,這樣寫extend(subClass, methods),將給subClass這個函數對象添加的是私有屬性,對象打點沒法訪問,只有構造函數打點能夠訪問,在其餘語言中,靜態變量或常量相似這種形式,因此這些屬性在js中稱爲類的私有屬性。

  測試代碼打印下看效果,依然在chrome控制檯

    function Parent(){
        this.p = "parent";
    }
    function Child(){
        this.c = "child";
    }
    // 添加的方法對象
    var mObj = { sayHello:function(){ console.log("hello") } };
    // 添加的屬性對象
    var pObj = { pos:"developer" };
    // 實現繼承
    var InheritedChild = inheritClass(Parent, Child, mObj, pObj);

    var obj = new InheritedChild();
    console.log(obj);

    

  console.table打印一個樹形結構,obj是對象,因此是__proto__,它關聯給本身實例化的構造函數的原型對象,表現上就是__proto__: Child,這個原型中有constructor屬性,值爲Child函數對象,還有添加進去的pos屬性和sayHello方法,這個原型又關聯這個Parent的prototype原型對象:__proto__:Parent,同理這個原型有本身的constructor屬性,並最終繼承自Object。

  繼承關係上大體就是這樣,亂七八糟一堆,也許一個$.extend()方法就解決了~

相關文章
相關標籤/搜索