跳到主要內容
黯羽輕揚每天積累一點點

JS 學習筆記 2_面向對象

免費2015-04-06#JS#js面向对象

js 的 OOP 與 Java 之類的有很大區別,其核心是 prototype,本文詳細分析 js 的繼承與 OOP 中的其它部分

##1.對象的定義##

ECMAScript 中,對象是一個無序屬性集,這裡的「屬性」可以是基本值、對象或者函數

##2.數據屬性與訪問器屬性##

  • 數據屬性即有值的屬性,可以設置屬性只讀、不可刪除、不可枚舉等等

  • 訪問器屬性是用來設置 getter 和 setter 的,在屬性名前加上"_"(下劃線)表示該屬性只能通過訪問器訪問(私有屬性),但並不是說添個下劃線就把屬性變成私有的了,這只是習慣約定的一種命名方式而已。訪問器屬性沒什麼用,原因如下:

     var book={
       _year:2004,
       edition:1
     }
     Object.defineProperty(book,"year",{
            get:function(){
               return this._year;
            },
            set:function(newValue){
               if(newValue>2004){
                 this._year=newValue;
                 this.edition+=newValue-2004;
               }
            }
     });
     book.year=2005;
     alert(book.edition);
     /*
     for(var attr in book){
       showLine(attr + ' = ' + book[attr]);
     }
     */
     
    

高程中使用了上面的示例代碼,原理是 book 對象的屬性中_year 是數據屬性,而 year 是訪問器屬性,利用 gettter 和 setter 可以插入讀寫控制,聽起來不錯。

但問題是_year 和 edition 都是可枚舉的,也就是說用 for...in 循環可以看到,而訪問器屬性 year 卻是不可枚舉的。應該公開的訪問器不公開,卻把應該隱藏的私有屬性公開了。

除此之外,這種定義訪問器的方式並不是全瀏覽器兼容的,[IE9+] 才完整支持,當然,也有適用於舊瀏覽器的方式(__defineGetter__() 和__defineSetter__()),還是相當麻煩。

總之,訪問器屬性沒什麼用

##3.構造函數##

function Fun(){} var fun = new Fun();其中 Fun 是構造函數,與普通函數沒有任何聲明方式上的區別,只是調用方式不同(用 new 操作符調用)而已

構造函數可以用來定義自定義類型,例如:

function MyClass(){
  this.attr = value;//成員變量
  this.fun = function(){...}//成員函數
}

與 Java 的類聲明有些相似,但也有一些差異,比如 this.fun 只是一個函數指針,因此完全可以讓它指向可訪問的其它函數(如全域函數),但這樣做會破壞自定義對象的封裝性

##4.函數與原型 prototype##

  1. 聲明函數的同時也創建了一個原型對象,函數名持有該原型對象的引用(fun.prototype)

  2. 可以給原型對象添加屬性,也可以給實例對象添加屬性,區別是原型對象的屬性是該類型所有實例所共享的,而實例對象的屬性是非共享的

  3. 訪問實例對象的屬性時,先在該實例對象的作用域中查找,找不到才在原型對象的作用域中查找,因此實例的屬性可以屏蔽原型對象的同名屬性

  4. 原型對象的 constructor 屬性是一個函數指針,指向函數聲明

  5. 通過原型可以給原生引用類型(Object,Array,String 等)添加自定義方法,例如給 String 添加 Chrome 不支持但 FF 支持的 startsWith 方法:

    var str = 'this is script';
    //alert(str.startsWith('this'));//Chrome 中報錯
    String.prototype.startsWith = function(strTarget){
      return this.indexOf(strTarget) === 0;
    }
    alert(str.startsWith('this'));//不報錯了
    

注意:不建議給原生對象添加原型屬性,因為這樣可能會意外重寫原生方法,影響其它原生代碼(調用了該方法的原生代碼)

  1. 通過原型可以實現繼承,思路是讓子類的 prototype 屬性指向父類的實例,以增加子類可訪問的屬性,所以用原型鏈連接之後

    子類可訪問的屬性
    = 子類實例屬性 + 子類原型屬性
    = 子類實例屬性 + 父類實例屬性 + 父類原型屬性
    = 子類實例屬性 + 父類實例屬性 + 父父類實例屬性 + 父父類原型屬性
    = ...
    

最終父父...父類原型屬性被替換為 Object.prototype 指向的原型對象的屬性

具體實現是 SubType.prototype = new SuperType();可稱之為簡單原型鏈方式的繼承

##5.創建自定義類型的最佳方式(構造函數模式 + 原型模式)##

實例屬性用構造函數聲明,共享屬性用原型聲明,具體實現如下:

    function MyObject(){
      //實例屬性
      this.attr = value;
      this.arr = [value1, value2...];
    }
    MyObject.prototype = {
      constructor: MyObject,//保證子類持有正確的構造器引用,否則子類實例的 constructor 將指向 Object 的構造器,因為我們把原型改成匿名對象了
      //共享屬性
      fun: function(){...}
    }
    

##6.實現繼承的最佳方式(寄生組合式繼承)##

function object(obj){//返回原型為 obj 的沒有實例屬性的對象
  function Fun(){}
  Fun.prototype = obj;
  return new Fun();
}
function inheritPrototype(subType, superType){
  var prototype = object(superType.prototype);//建立原型鏈,繼承父類原型屬性,用自定義函數 object 處理是為了避免作為子類原型的父類實例具有實例屬性,簡單地說,就是為了切掉除多餘的實例屬性,可以對比組合繼承理解
  prototype.constructor = subType;//保證構造器正確,原型鏈會改變子類持有的構造器引用,建立原型鏈後應該再改回來
  subType.prototype = prototype;
}

function SubType(arg1, arg2...){
  SuperType.call(this, arg1, arg2...);//繼承父類實例屬性
  this.attr = value;//子類實例屬性
}
inheritPrototype(SubType, SuperType);

具體解釋見代碼註釋,這種方式避免了 SubType.prototype = new SuperType();簡單原型鏈的缺點:

  • 子類實例共享父類實例引用屬性的問題(因為原型引用屬性是所有實例共享的,建立原型鏈後父類的實例屬性就自然地成了子類的原型屬性)。

  • 創建子類實例時無法給父類構造函數傳參

##7.實現繼承的最常用方式(組合繼承)##

把上面的 inheritPrototype(SubType, SuperType);語句換成:

SubType.prototype = new SuperType();
SubType.prototype.constructor = SubType;

這種方式的缺點是:由於調用了 2 次父類的構造方法,會存在一份多餘的父類實例屬性,具體原因如下:

第一次 SuperType.call(this);語句從父類拷貝了一份父類實例屬性給子類作為子類的實例屬性,第二次 SubType.prototype = new SuperType();創建父類實例作為子類原型,此時這個父類實例就又有了一份實例屬性,但這份會被第一次拷貝來的實例屬性屏蔽掉,所以多餘。

評論

暫無評論,快來發表你的看法吧

提交評論