DIV CSS 佈局教程網

 DIV+CSS佈局教程網 >> 網頁腳本 >> JavaScript入門知識 >> JavaScript基礎知識 >> javascript之面向對象程序設計(對象和繼承)
javascript之面向對象程序設計(對象和繼承)
編輯:JavaScript基礎知識     

總結的文章略長,甚點。

知識點預熱

  • 引用類型:引用類型的值(對象)是引用類型的一個實例。在ECMAScript中,引用類型是一種數據結構,用於將數據和功能組織在一起。在其他面向對象語言中被稱為類,雖然ECMAScript從技術上講也是一門面向對象語言,但它不具備傳統面向對象語言所支持的類和接口等基本結構而是通過別的形式實現類模板和繼承。引用類型描述的是一類對象所具有的屬性和方法。
  • 對象:對象是某個特定引用的實例,新對象是使用 new 操作符後跟一個構造函數來創建的。實例對象其實就是一組特定數據和具體功能的集合。
  • 構造函數:本身就是一個函數,只不過該函數是出於創建新對象的目的而定義的。通常用來為對象定義默認的屬性和方法。
  • 舉例: var person=new Object(); 保存對象(新實例)的變量 person 裡的內容為這個對象(新實例)在內存堆中的地址。我們知道c語言是可以直接用代碼訪問變量的物理內存,所以我就想javascript能不能也能直接訪問內存地址,谷歌關於javascript訪問內存地址的有效信息也寥寥,得知javascript這種高級語言是並不能直接通過代碼訪問物理內存,需要進行腳本擴展接口等等。參考(javascript能不能訪問物理內存?)。待以後學習深度加強專門研究一下。

 

Object類型

在ECMAScript中(就像在Java中的 java.lang.Object 對象一樣), Object 類型是所有它的實例的基礎, Object 類型所具有的任何屬性和方法也同樣可被更具體的對象所用。

JavaScript主要是通過原型鏈實現了面向對象中的實現繼承(區分接口繼承和實現繼承),所以每當構造一個實例對象時便繼承了 Object.prototype 上的方法,但這種原型鏈式的繼承並不是復制方法的副本,而是引用(指向)式的繼承。

  •  constructor: 保存著用於創建當前對象的函數,這裡構造函數就是Object(); person.constructor;// function Object() { [native code] }
  •  hasOwnProperty(propertyName): 用於檢查給定的屬性在當前對象的實例中(而不是在實例的原型中)是否存在,返回布爾值。屬性名以字符串形式給定。
    person.hasOwnProperty('constructor');// false
    Object.prototype.hasOwnProperty('constructor');// true
  •  isPrototypeOf(object): 用於檢查調用該方法的對象是否是傳入對象的原型。Object.prototype.isPrototypeOf(person);// true
  •  propertyIsEnumerbale(propertyName): 檢查給定的屬性是否能夠使用 for in 來枚舉,當然能枚舉的前提是該屬性的特性被設置為是可枚舉的。
  •  toLocaleString(): 返回對象的字符串表示,該字符串與執行環境的地區對應。
  •  toString(): 返回對象的字符串表示。
    //Number,String,Boolean類型返回新的字符串,其實是在包裝類的實例上調用toString或toLocaleString
    var number=10;
    number.toString();// "10"
    
    var str='xx';
    var strnew=str.toString();// "xx"
    str==strnew;// true 兩個string基本類型的字符串比較內容而已所以為true
    str+='add';// "xxadd" 修改str指向的內容,進一步確定toString()返回的是副本並不是str的引用
    strnew;// "xx"
    
    var bol=true;
    bol.toString();// "true"
    
    //引用類型構造函數及其實例對象調用toString/toLocaleString返回
    Object.toString();// "function Object() { [native code] }"
    var person=new Object();
    person.toString();// "[object Object]"
    
    Array.toString();// "function Array() { [native code] }"
    var a=new Array();
    a.toString();// "" 即調用數組每一項的toString()方法,然後拼接成字符串
    
    Functiom.toString();// "function Function() { [native code] }"
    new Function('console.log(1)').toString();//"function anonymous() {console.log(1)}"
    
    Boolean.toString();// "function Boolean() { [native code] }"
    new Boolean(true).toString();// "true"
    
    String.toString();// "function String() { [native code] }"
    new String('xx').toString();// "xx"
    
    Number.toString();// "function Number() { [native code] }"
    new Number(10).toString();// "10"
  •  valueOf(): 對於字符串,數值或布爾值這三個基本類型返回的是副本,對於包裝類的實例對象返回該實例的基本類型的表示,對於除過包裝類實例的引用類型則返回的是自身的引用。
    //基本類型Number,String,Boolean
    var num=1;
    var numnew=num.valueOf();
    num+=2;// 3
    numnew;// 1
    
    var str='xx';
    str.valueOf(); //"xx"
    
    var bol=true;
    bol.valueOf();// true;
    
    //包裝類的實例
    new Number().valueOf();// 0
    new String().valueOf();// ""
    new Boolean().valueOf();// false
    
    //其他引用類型返回自身的引用
    Object.valueOf()==Object;// true
    var o=new Object();
    o.valueOf()==o;// true

創建Object實例:不論用哪種方式效果是一樣的

       

  1. new 構造函數
    var o=new Object();
    o.name="xx";
    o.say=function(){
      console.log('hi')
    }
    
    //如果不傳參,可以省略圓括號,但不推薦
    var o=new Object;
  2. 對象字面量
  3. var o={
      name:'xx',
      say:function(){
        console.log('hi');
      }
    }

    ECMAScript中表達式上下文的定義:該上下文期待的一個值(表達式)。在這個例子中,左邊的花括號 ({) 表示對象字面量的開始 ,因為它出現在表達式上下文中。賦值操作符表示後面是一個值,所以左花括號在這裡表示一個表達式的開始。
    ECMAScript中語句上下文的定義:同樣的花括號,例如跟在if條件語句後面,則表示一個語句塊的開始。
    在通過對象字面量定義的對象時,實際上不會調用 Object 構造函數。此話出自JavaScript高級程序設計第三版,不明白不調用 Object 構造函數那是通過什麼幕後形式創建的對象,js引擎?
    屬性名可以使用字符串定義,也可以像本代碼一樣簡寫,JavasScript會自動轉化為字符串。

    for(var i in o){
      console.log(typeof i);//string
    }

    對象字面量也是向函數傳遞大量可選的參數的首選方式。

    function displayInfo(args){
         var output="";
         for(var i in args){
          if(args.hasOwnProperty(i)){
                if(typeof args[i]=='function'){ 
                  output+=args[i](); 
                }
                else{
                   output+=args[i]+' ';
                }        
             }
         }
        console.log(output); 
    };
    
    displayInfo({
        name:'xx',
        say:function(){
          return 'hi';
        }
    });
    方括號語法的主要優點是通過可以通過變量來訪問屬性,如果屬性名中包含會導致語法錯誤的字符,或者屬性名使用的是關鍵字或保留字,也可以使用方括號表示法訪問。將要訪問的屬性以字符串形式放在方括號中。

     

ECMA-262把對象定義為

無序屬性的集合,其屬性可以包含基本值,對象,函數。相當於說對象是一組沒有特定順序的值。對象的每個屬性或方法都有一個名字,而每個名字都映射到一個值,可以把ECMAScript中的對象想象成散列表,無非就是一組名值對,其中的值可以是數據或函數。每個對象都是基於一個引用類型創建的,這個引用類型可以是原生類型也可以是自定義類型。

創建對象:雖然 Object 構造函數或對象字面量都可以用來創建單個對象,但這些方法明顯有缺點:使用同一個接口創建很多對象,會產生大量重復的代碼。所以產生如下七種模式。

1.工廠模式:抽象了創建具體對象的過程,考慮到在ECMAScript中無法創建類,所以就用函數封裝代碼實現特定功能。實質就是用函數封裝以特定接口來創建對象的細節(細節是指構造對象實例的方式)。實際想想在軟件工程領域中我們經常這樣做,將一個功能封裝起來只給外界提供接口。

function createPerson(name,age,job){
  var o=new Object();
  o.name=name;
  o.age=age;
  o.job=job;
  o.say=function(){
    console.log(this.name); 
  }; 
  return o;
}

var person1=createPerson('xx',20,'student');
var person2=createPerson('mm',22,'student');
person1==person2;// false

有點感覺了,像java/C++中類裡面的構造函數有沒有,可以返回同功能的不同的多個實例。

缺點:沒有解決對象識別問題(即怎樣知道一個對象的類型),雖然你可以用代碼試探實例是那種類型(比如可以用 person1.__proto__ 的方法),但卻沒法直觀地知道 person1 和 person2 到底是哪種類型的實例。

2.構造函數模式:ECMAScript中的原生構造函數可以用來創建特定類型的對象,此外也可以創建自定義構造函數,從而定義自定義對象類型的屬性和方法。

  • function Person(name,age,job){
      this.name=name;
      this.age=age;
      this.job=job;
      this.say=function(){
         console.log(this.name);
      }; 
    }
    
    var p1=new Person('xx',20,'student');
    var p2=new Person('mm',22,'student');
    p1==p2;// false
    
    p1.constructor==Person;//true
     Person 中的代碼與工廠模式中 createPerson 中的代碼相比:
    1.並沒有顯式創建對象
    2.直接將屬性和方法賦值給了 this 對象
    3.沒有 return 語句
    小tip:構造函數命名規范,始終以一個大寫字母開頭,而非構造函數則以一個小寫字母開頭。這個做法借鑒自其他OO語言,主要為了區別於ECMAScript中其他函數。
    通過 new 操作符的方式調用構造函數實際上會經歷以下四個步驟:JavaScript面向對象之我見 中提到函數的 prototype 屬性的值被作為原型對象來可克隆出新對象(實際上是後面說的寄生式繼承),可以把 new 運算符想象成一個方法,這個方法的功能
  • 創建一個新對象
  • 將構造函數的作用域賦給新對象(因此 this 就指向了這個新對象)
  • 執行構造函數中的代碼(為這個新對象添加屬性)
  • 返回新對象

 p1 和 p2 這兩個對象都有一個 constructor (構造器)屬性(javascript高程上這麼說其實並不准確, constructor 其實並不是 p1 和 p2 自身有的屬性而是通過原型鏈繼承來的屬性), constructor 指向 Person 。

這裡所創建的 p1 和 p2 既是 Object 的實例,又是 Person 的實例,原因是原型鏈上繼承關系,後面有說。 p1 instanceof Object;// true

p1 instanceof Person;// true 

創建自定義的構造函數意味著將來可以將它的實例標識為一種特定的類型,明確的知道實例的類型這正是構造函數模式勝於工廠模式的地方。

在另一個對象的作用域中調用:在某個特殊對象的作用域中調用 Person 函數。

var o=new Object();
Person.call(o,'xx',20,'student');
o.say();// xx

缺點: say 這個方法要在每個實例上都創建一遍, p1.say==p2.say;// false p1和p2上的say方法雖然內容一樣但卻是完全不同的兩個 Function 類型實例, this.say=function(){ console.log(this.name);};  相當於 this.say=new Function("console.log(this.name)"); 以這種方式創建函數,會導致不同的作用域鏈和標識符解析(變量,函數,屬性,參數的名字),但創建 Function 新實例的機制仍然是相同的。

解決方案:鑒於 say 函數對於 p1 和 p2 完成的功能一樣,那麼可以不用在執行代碼前就把該函數綁定到特定對象上面,通過把完成特定功能的函數單獨拿出來,而讓實例對象的 "say" 屬性指內存堆裡同一個函數來解決問題。這樣 p1 和 p2 就共享了在全局作用域中定義的同一個函數。

function Person(name,age,job){
  this.name=name;
  this.age=age;
  this.job=job;
  this.say=say;
};
function say(){
  console.log(this.name); 
}

var p1=new Person('xx',20,'student');
var p2=new Person('mm',20,'student');

缺點:

  1. 全局作用域中調用的函數只能被某個對象調用才會體現出該函數的功能,這讓全局作用域有點名不副實。
  2. 如果對象需要定義很多個方法,那麼就需要定義很多全局函數,於是我們這個自定義的引用類型就沒有封裝性可言了。

3.原型模式:JavaScript中每個函數都有一個 prototype (原型)屬性,這個屬性是一個指針,指向一個對象。 prototype 是這個屬性的名字,這個指針的內容就是該函數原型屬性對象在堆內存中的地址。這個原型對象的作用就是包含可以由特定類型的所有實例共享的屬性和方法。也可以這麼理解, prototype 就是通過調用構造函數而創建的那個對象實例的原型對象。使用原型好處就是可以讓所有對象實例共享它所包含的屬性和方法。即不必在構造函數中定義對象實例的信息,而是可以將這些信息添加到原型對象中。

function Person(){}
Person.prototype.name='xx';
Person.prototype.age=29;
Person.prototype.job="student";
Person.prototype.say=function(){
    console.log(this.name);
};
var p1=new Person();
p1.name;// 'xx'

var p2=new Person();
p1==p2;// false
p1.say==p2.say;// true
  • 理解原型對象:每當代碼讀取某個對象的某個屬性時,都會執行一次搜索具有給定名字的屬性。搜索先從本對象實例開始,如果在對象實例中找到具有給定名字的屬性則返回該屬性的值。如果沒找到,則繼續搜索指針指向的原型對象,在原型對象中查找具有給定名字的屬性。這正是多個對象實例共享原型所保存的屬性和方法的基本原理。使用delete操作符可以完全刪除實例屬性從而恢復原型鏈繼承,前提是屬性的 configurable 特性為 true 。
  • 原型與in操作符:兩種方法使用 in 操作符(單獨使用, for-in 循環)
    單獨使用: in 操作符會在通過對象能夠訪問給定屬性時返回 true ,無論該屬性存在於實例還是原型中。 'job' in p1;// true
    for in :返回的是所有能夠通過對象訪問的,可枚舉的屬性,其中既包括存在實例中的屬性,也包括原型中的屬性。 Object.prototype 上的屬性都是不可枚舉的,因為它們的 enumerable 特性默認為 false 。 for(i in Object.prototype){ console.log(i) };// undefined 。但我們可以為實例定義同名的方法覆蓋原型對象上方法的不可枚舉性。因為通過字面量對象上添加屬性和構造函數創建的對象上的屬性們默認都是可枚舉的,注意通過 Object.defineProperty 定義對象屬性是不可枚舉的。
    //字面量創建對象屬性
    var p={
      name:'xx',
      say:function(){}
    }
    Object.getOwnPropertyDescriptor(p,'name');// Object {value: "xx", writable: true, enumerable: true, configurable: true}
    
    //直接在對象上添加屬性
    var o=new Object();
    o.name='xx';// "xx"
    Object.getOwnPropertyDescriptor(o,'name');// Object {value: "xx", writable: true, enumerable: true, configurable: true}
    
    //Object.definedProperty定義屬性
    var p={};
    Object.defineProperty(p,'name',{
      value:'xx'
    });// Object {name: "xx"} 一旦定義某一屬性後就不能通過這種方式再次定義該屬性,因為此時writable默認為false,除非當時顯式聲明為true
    Object.getOwnPropertyDescriptor(p,name);// Object {value: "xx", writable: false, enumerable: false, configurable: false}
    var o={
      toString:function(){
        console.log('我是實例上的toString');
      }
    };
    for(var i in o){
      console.log(i);
    };// toString

    注:上面的覆寫 toString 代碼會在IE早期版本中出現bug,不會打印出 toString ,因為IE認為原型的 toString 方法被打上了值為 false 的 [[Enumerable]] 標記,因此會跳過該屬性。
    解決方案:
    1.改變原型上方法的可枚舉性

    Object.defineProperty(Object.prototype,"valueOf",{enumerable:true });
    
    Object.defineProperty(o,"valueOf",{enumerable:true
    });
    
    Object.getOwnPropertyDescriptor(Object.prototype,'valueOf');// Object {writable: true, enumerable: true, configurable: true}
    for(var i in o){
      console.log(o[i]);
    }

    2.如果你想得到所有實例屬性而不論它們是否都可枚舉,使用 Object.getOwnPropertyNames() ,返回類型為 "[object Object]" 類數組:

  1. Object.getOwnPropertyNames(Person.prototype);// ["constructor", "name", "age", "job", "say"]
    
    Object.getOwnPropertyNames(Object.prototype);/* ["constructor", "toString", "toLocaleString", "valueOf", "hasOwnProperty", "isPrototypeOf",
     "propertyIsEnumerable", "__defineGetter__", "__lookupGetter__", "__defineSetter__", "__lookupSetter__", "__proto__"] */

    ECMAScript5中 Object.keys() 方法可以取得對象上所有可枚舉的實例屬性,這樣就不用 for-in 和 hasOwnProperty 篩選判斷了:參數為對象,返回一個包含所有可枚舉屬性的字符串數組

    Object.keys(Person.prototype);// ["name", "age", "job", "say"]
    
    p1.sex='女';
    Object.keys(p1);// ["sex"]
  • 更簡單的原型語法:為了更好的封裝原型的功能,將原型上定義的屬性和方法用對象字面量重寫整個原型

  1. Person.prototype={
       name:'xx',
       age:22,
       say:function(){console.log(this.name)}
    }

    但這樣做法會使原型上的 constructor 屬性不見了, Object.getOwnPropertyNames(Person.prototype);// ["name", "age", "say"] ,為什麼呢?因為這種方式是在重定義原型屬性的內容(即讓 prototype 指向了別處對象),而之前的方式是在默認的原型屬性上添加新屬性而已。我們知道,每創建一個函數,就會同時創建它的 prototype 對象,這個對象也就會自動獲得 constructor 屬性,但現在我們讓 prototype 指向了別處新對象,原來帶 constructor 的那個對象在內存中就沒人引用了,如果也沒有實例對象的引用情況下就等待垃圾處理機制的回收。有意思的是雖說 Person.prototype 沒有 construcor 屬性了,但是再次訪問 Person.prototype.constructor;// Object() { [native code] } 什麼鬼?不是說沒有這個屬性不能訪問到了嗎?原來現在的 constructor 其實是在自己對象上搜索不到的屬性,便順著原型鏈繼承自 Object.prototype.constructor 來的。此時已經不能用 constructor 來判斷實例對象的類型了。

    Person.prototype.hasOwnProperty('constructor');// false
    Object.prototype.hasOwnProperty('constructor');// true

    如果需要 constructor ,可以特意將 constructor 設回適當的值。

    Person.prototype={
       name:'xx',
       age:22,
       say:function(){console.log(this.name);},
       constructor:Person  
    }

    但是這樣會將它的 [[Enumerable]] 特性被設置為 true 。當然再加一步,通過 Object.defineProperty() 將特性重新賦為 false 。 

  • 原型的動態性:實例與原型間的松散連接關系可以讓實例對象訪問後來在原型上添加的方法。因為實例與原型之間的連接只不過是一個指針,而非一個副本。
    注意:如果修改了整個原型對象,那麼就要慎重訪問了,順序尤為重要。我們知道調用構造函數時會為實例添加一個指向最初原型的 [[prototype]] 的指針,而現在把構造函數的原型修改為指向別處,那麼這時候實例創建的順序對訪問結果有很大影響。出現錯誤的原因就是 p.__proto__ 和 Person.prototype 指向的不是一個對象。
  1. 原生對象的原型可以取得所有默認方法的引用,而且也可以定義新方法。但是不推薦在原型上添加方法可能會導致命名沖突。
  2. 原型對象的問題:
    它省略了為構造函數傳遞參數這一部分導致所有實例在默認情況下都取得相同屬性值。
    原型中所有屬性是被很多實例共享的,這種共享對於函數非常合適。但對於包含引用類型值的屬性問題就比較突出了。
    function Person(){}
    Person.prototype={
      constructor:Person,
      name:'xx',
      hobby:['a','b']
    };
    var p1=new Person();
    var p2=new Person();
    p1.hobby.push('c');
    p1.hobby;// ['a','b','c']
    p2.hobby;// ['a','b','c']
    p1.hobby===p2.hobby

4.組合使用構造函數模式和原型模式:構造函數模式用於定義實例上的屬性,原型模式定義方法和共享的屬性。這樣每個實例都會有自己的一份實例屬性的副本,但同時又共享著方法的引用,節省了內存。

function Person(name,age){
   this.name=name;
   this.age=age;
   this.hobby=['a','b'];
}

Person.prototype={
  constructor:Person,
  say:function(){
   console.log(this.name);
  }
}

var p1=new Person('xx',20);
var p2=new Person('mm',20);
p1.hobby.push('c');
p2.hobby;// ['a','b'];

 5.動態原型模式(在構造函數中初始化原型):動態原型是把所有信息都封裝在了構造函數中,這樣構造函數和原型就不獨立開了,符合了OO語言中功能在一塊的習慣。通過在構造函數中初始化原型(僅在必要的情況下)又保持了同時使用構造函數和原型的優點。換句話說可以通過檢查某個應該存在的方法是否有效來決定是否需要初始化原型。

function Person(name,age){
   this.name=name;
   this.age=age;
   if(typeof this.say!='function'){ //或用instanceof判斷
      Person.prototype.say=function(){
         console.log(this.name);
      }  
   }
}

var f=new Person("xx",20);
f.say();// 'xx'

這段代碼只會初次調用構造函數時才會執行,此後原型已經完成初始化,需要再做什麼改變了。 if 語句檢查的可以是初始化之後應該存在的任何屬性或方法。不必用一大堆 if 語句檢查每個屬性和每個方法,只要檢查其中一個就好。

缺點:不能用對象字面量的形式重寫原型,因為實例先於修改原型創建,執行 new 時,調用構造函數,為實例添加一個指向默認原型的 [[prototype]] ,然後再初始化,所以是在修改原型之前。

6.寄生構造函數模式:工廠模式和構造函數模式的結合。用一個函數封裝創建創建對象的代碼然後返回新創建的對象。除了使用 new 操作符並把使用的包裝函數叫構造函數外,這個模式和工廠模式其實一樣。這裡涉及到JavaScript中構造函數的返回值

  • 當構造函數沒有返回值時,則按照其他語言一樣返回實例化對象
    當構造函數有返回值時,若返回值為非引用類型如(String,Number,Boolean,undefined,null)則與無返回值相同,返回實例對象。若返回值為引用類型,則實際返回值為這個引用類型。
    function Person(name,age){
      var o=new Object();
      o.name=name;
      o.age=age;
      o.say=function(){
        console.log(this.name);
      }
      return  o;
    }
    var p=new Person('xx',20);
    p.say();

    應用:假設想創建一個具有額外方法的特殊數組。由於不能直接修改Array構造函數,因此可以使用這個模式

    function SpecialArray(){
       var values=new Array();
       values.push.apply(values,arguments);//初始化數組
       values.toPipedString=function(){
           return values.join('|');
       };
       return values;
    }
    var hobbits=new SpecialArray('a','b','c');
    hobbits.toPipedString();// "a|b|c"

    關於寄生構造函數模式,說明一點,返回的對象與構造函數或者與構造函數的原型屬性之間沒有關系。即構造函數返回的對象與在構造函數外部創建的對象沒什麼區別,為此不能依賴instanceof操作符來確定對象類型。此種模式不推薦。

7.穩妥構造函數模式:
穩妥對象:沒有公共屬性,而且其方法也不引用 this 的對象。穩妥對象最適合在一些安全的環境中(這些環境會禁止使用 this 和 new )或者防止數據被其他應用程序改動時使用。
穩妥構造函數遵循與寄生構造函數類似的模式,有兩點不同:一是創建對象的實例方法不引用 this ,二是不使用 new 操作符調用構造函數。

function Person(name,age){
   var o=new Object();
   //在這裡定義私有變量和函數
   o.say=function(){
     console.log(name);
   } 
  return o;
}

var p=Person('xx',20);
p.say();// 'xx'

注意這種模式創建的對象中,除了使用say方法外沒有別的方法可以訪問其數據成員。即使有其他代碼會給這個對象添加方法或數據成員,但也不可能有別的方法訪問傳入到構造函數中的原始數據。使用穩妥構造函數模式創建的對象與構造函數之間也沒有什麼關系,因此也不能用 instanceof 判斷類型。 

 

 

繼承

許多OO語言都支持兩種繼承方式,接口繼承和實現繼承。接口繼承只繼承方法名,實現繼承繼承實際的方法。由於函數沒有簽名,在ECMAMScript中無法實現接口繼承。ECMAScript只支持實現繼承,而且其實現繼承主要是依靠原型鏈來實現的。

1.原型鏈:作為實現繼承的主要方法。基本思想是利用原型讓一個引用類型繼承另一個引用類型的屬性和方法。假如讓原型對象等於另一個類型的實例,此時的原型對象將包含一個指向另一個原型的指針,相應地,另一個原型中也包含著一個指向另一個構造函數的指針。假如另一個原型又是另一個類型的實例,那麼上述關系依然成立,如此層層遞進,就構成了實例與原型的鏈條。

function SuperType(){
  this.property=true;
}

SuperType.prototype.getSuperValue=function(){
  return this.property;
}

function SuberType(){
  this.subproperty=false;
}

SuberType.prototype=new SuperType();

SuberType.prototype.getSubValue=function(){
  return this.subproperty;
}
var instance=new SuberType();
instance.getSuperValue();// true 

調用 insatnce.getSuperValue() 會經歷:搜索實例;搜索 SubType.prototype ;搜索 SuperType.prototype ,最後一步才會找到該方法。

  1. 別忘記默認的原型:所有引用類型默認都繼承了Object。所有函數的默認原型都是Object的實例,因此默認原型都會包含一個內部指針,指向Object.prototype。這也正是所有自定義類型都會繼承toString和valueOf等默認方法的根本原因。
  2. 確定原型和實例的關系:
    instanceof操作符:測試實例與原型鏈中出現過的構造函數,結果返回true。
    instance instanceof Object;// true
    instance instanceof SuperType;// true
    instance instanceof SuberType;// true
    isPrototypeOf()方法:只要是原型鏈中出現過的原型,都可以說是該原型鏈所派生的實例的原型,因此isPrototypeOf()也會返回true。
    Object.prototype.isPrototypeOf(instance);// true
    SuperType.prototype.isPrototypeOf(instance);// true
    SuberType.prototype.isPrototypeOf(instance);// true
  3. 謹慎地定義方法:謹慎使用對象字面量創建原型方法,這樣做會重寫原型鏈。
  4. 原型鏈的問題:最主要的問題是包含引用類型值的原型。在創建子類型的實例時,不能向超類型的構造函數傳遞參數(但是Java的 super() 函數就可以實現啊)。也就是說沒有辦法在不影響所有對象實例的情況下給超類型的構造函數傳遞參數。所以實踐中很少會單獨使用原型鏈。

2.借用構造函數:解決了原型中包含引用類型值所帶來的問題。即在子類型的構造函數中調用超類型的構造函數(看來也是借鑒了Java中的 super() ),至於是怎麼實現的?JavaScript中函數只不過是在特定環境中執行代碼的對象,因此通過 apply 和 call 方法可以在將來新創建的對象上執行構造函數。

//父類
function SuperType(){
   this.colors=["a","b"];
}
//子類
function SuberType(){
   SuperType.call(this);
}

var instance1=new SuberType();
instance1.colors.push('c');// 3 colors值為["a","b","c"];

var instance2=new SuberType();
instance2.colors;// ["a","b"];

通過使用 call 方法或 apply 方法,我們實際是在(未來將要)新創建的 SuberType 實例的環境下調用 SuperType 構造函數,畢竟每當 new 一個實例的時候是先將構造函數的作用域賦給新對象( this 指向確定)。這樣一來,就會在新的 SuberType 對象上執行 SuperType 函數中定義的所有對象初始化代碼。這樣, SuberType 的每個實例就都會有自己的 colors 屬性的副本了。
優點:可以在子類的構造函數中向超類構造函數傳遞參數(這點Java的 super() 傳遞參數也可做到)

function SuperType(name){
  this.name=name;
}

function SuberType(age){
  //繼承了SuperType還傳遞了參數
  SuperType.call(this,"xx");
  //實例的其他屬性
  this.age=age;
}

var instance=new SuberType(20);
instance.name;// "xx"
instance.age;// 20

為了確保 SuperType 構造函數不會重寫子類的屬性,所以在子類中定義的屬性寫在調用的後面。
缺點:如果僅僅使用構造函數來完成繼承,那麼也無法避免構造函數模式中存在的問題,即方法都在構造函數中定義,就沒有函數的復用了。在超類原型中定義的方法,對子類型而言也是不可見的,結果所有類型就只能使用構造函數模式。考慮到這些,借用構造函數的技術也很少單獨使用。
3.組合繼承:原型鏈和構造函數的技術結合起來,使用原型鏈實現實現對原型屬性和方法的繼承,借用構造函數實現對實例屬性的繼承。

//父類
function SuperType(name){
  this.name=name;
  this.colors=["a","b"];
}
 
SuperType.prototype.sayName=function(){
  console.log(this.name);
}

//子類
function SuberType(name,age){
  SuperType.call(this,name);
  this.age=age;
}

SuberType.prototype=new SuperType();

SuberType.prototype.constructor=SuberType;
SuberType.prototype.sayAge=function(){
  console.log(this.age);
}
var a1=new SuberType('xx',20);

a1.colors.push('c');
a1.colors;// ["a","b","c"]
a1.sayAge();// 20;
a1.sayName();// xx

這種方式 instanceof 和 isPrototypeOf 同樣可用。但注意到 SuberType.prototype 有個 name 和 colors 的無用屬性。
缺點:會調用兩次超類的構造函數,一次是在創建子類原型的時候,另一次是在子類構造函數內部的 call 或 apply 。子類型最終會包含超類型對象的全部實例屬性,但我們不得不在調用子類型構造函數時重寫這些屬性。解決辦法是寄生組合繼承。
4.原型式繼承:這種方法並沒有使用嚴格意義上的構造函數,而是借助原型可以基於已有的對象創建新對象,同時還不必因此創建自定義類型。

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

先創建一個臨時性的構造函數,然後將傳入的對象作為這個構造函數的原型,返回了臨時類型的一個新實例。

var person={
  name:"xx",
  friends:["aa","bb","cc"]
};

var p1=object(person);
p1.name;// "xx" 原型繼承
p1.name="xixi";
p1.friends.push("dd");
person.friends;// ["aa", "bb", "cc", "dd"] 

var p2=object(person);// 再次調用object(),雖然是重新執行了F.prototype但是o參數仍指向原來的person
p2.name="xuxu";
p2.friends.push("ee");
person.friends;// ["aa", "bb", "cc", "dd", "ee"]

ECMA5新增的 Object.create() 規范化了原型式繼承,兩個參數,一個用作新對象原型的對象,(可選的)為一個新對象定義額外屬性的對象。在傳入一個參數情況下, Object.create() 和 object() 沒什麼區別。

var person={
  name:"xx",
  friends:["aa","bb","cc"]
};

var p1=Object.create(person);
p1.name="xixi";
p1.friends.push("dd");

var p2=Object.create(person);
p2.name="xuxu";
p2.friends.push("ee");

person.friends;// ["aa", "bb", "cc", "dd", "ee"]

 Object.create() 方法的第二個參數與 Object.defineProperties() 方法的第二個參數格式相同,每個屬性都是通過自己的描述符定義的。以這種方式指定的任何屬性都會覆蓋原型對象上的同名屬性。

var person={
  name:'xx',
  friends:["aa","bb","cc"]
};

var p1=Object.create(person,{
  name:{
     value:"xixi"
  }
});

p1.name;// "xixi"
person.name;// "xx"

在沒有必要創建構造函數,而只是想讓一個對象與另一個對象保持類似情況下,原型式繼承是完全可以的。不過缺點還是在的,比如包含引用類型值得屬性始終都是共享的。
5.寄生式繼承:是一種與原型式繼承緊密相關的思路。寄生式繼承的思路與寄生構造函數和工廠模式類似,即創建一個僅用於封裝繼承過程的函數,該函數在內部以某種方式增強對象,最後再像是它做了所有工作一樣返回對象。

function createAnother(original){
   var clone=object(original); //調用函數創建一個新對象
   clone.say=function(){  //增強這個對象
     console.log("hi");
   }
   return clone; 
}
var person={
  name:'xx',
  friends:["aa","bb","cc"]
}

var p1=createAnother(person);
p1.name;// "xx"
p1.friends;// ["aa", "bb", "cc"]
p1.say();// hi

在主要考慮對象而不是自定義類型的構造函數情況下,寄生式繼承也是一種有用方法,而且 object() 函數也不是必須的,任何能夠返回新對象的函數都適用此模式。
缺點:由於是增強對象給對象添加函數,所以不能函數復用。這點與構造函數模式相似。

6.寄生組合式繼承:通過構造函數繼承屬性,原型鏈的混成繼承方法。說白了就是不必為了指定子類型的原型而調用超類型的構造函數而造成子類型原型上出現一些用不到的屬性,我們所要的無非就是超類型原型的一個副本實現原型鏈之間的繼承關系而已。那麼為何不考慮結合寄生式繼承因為寄生式繼承可以為一個對象指定原型啊。這樣使用寄生式繼承來繼承超類型的原型,然後再將返回的結果指定給子類原型。

function inheritPrototype(suberType,superType){
  var prototype=object(superType.prototype);// 創建對象
  prototype.constructor=suberType;// 增強對象
  suberType.prototype=prototype;// 指定對象
}
function SuperType(name){
  this.name=name;
  this.colors=["aa","bb","cc"];
} 
SuperType.prototype.say=function(){
  console.log(this.name); 
}

function SuberType(age,name){
  SuperType.call(this,name);
  this.age=age;
}

inheritPrototype(SuberType,SuperType);
SuberType.prototype.say=function(){
  console.log(this.age);
}
只調用一次父類構造函數不僅提高了效率,這樣做還能正常地使用 instanceof 和 isPrototypeOf 。寄生組合式繼承是引用類型最理想的繼承范式。YUI的 YAHOO.lang.extend 就用到了寄生組合繼承(https://yui.github.io/yui2/docs/yui_2.3.0/docs/Lang.js.html)

 

參考 :《JavaScript高級程序設計》

XML學習教程| jQuery入門知識| AJAX入門| Dreamweaver教程| Fireworks入門知識| SEO技巧| SEO優化集錦|
Copyright © DIV+CSS佈局教程網 All Rights Reserved