天天看點

JS學習筆記2_面向對象

1.對象的定義

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

2.資料屬性與通路器屬性

  • 資料屬性即有值的屬性,可以設定屬性隻讀、不可删除、不可枚舉等等
  • 通路器屬性是用來設定getter和setter的,在屬性名前加上”_”(下劃線)表示該屬性隻能通過通路器通路(私有屬性),但并不是說添個下劃線就把屬性變成私有的了,這隻是習慣約定的一種命名方式而已。通路器屬性沒什麼用,原因如下:
1 var book={
 2   _year:2004,
 3   edition:1
 4 }
 5 Object.defineProperty(book,"year",{
 6        get:function(){
 7           return this._year;
 8        },
 9        set:function(newValue){
10           if(newValue>2004){
11             this._year=newValue;
12             this.edition+=newValue-2004;
13           }
14        }
15 });
16 book.year=2005;
17 alert(book.edition);
18 /*
19 for(var attr in book){
20   document.write(attr + ' = ' + book[attr] + ';');
21 }
22 */      

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

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

除此之外,這種定義通路器的方式并不是全浏覽器相容的,[IE9+]才完整支援,當然,也有适用于舊浏覽器的方式(__defineGetter__()和__defineSetter__()),還是相當麻煩。

總之,通路器屬性沒什麼用。

3.構造函數

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

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

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

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

4.函數與原型prototype

  1. 聲明函數的同時也建立了一個原型對象,函數名持有該原型對象的引用(fun.prototype)
  2. 可以給原型對象添加屬性,也可以給執行個體對象添加屬性,差別是原型對象的屬性是該類型所有執行個體所共享的,而執行個體對象的屬性是非共享的
  3. 通路執行個體對象的屬性時,先在該執行個體對象的作用域中查找,找不到才在原型對象的作用域中查找,是以執行個體的屬性可以屏蔽原型對象的同名屬性
  4. 原型對象的constructor屬性是一個函數指針,指向函數聲明
  5. 通過原型可以給原生引用類型(Object,Array,String等)添加自定義方法,例如給String添加Chrome不支援但FF支援的startsWith方法:
1 var str = 'this is script';
2 //alert(str.startsWith('this'));//Chrome中報錯
3 String.prototype.startsWith = function(strTarget){
4   return this.indexOf(strTarget) === 0;
5 }
6 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次父類的構造方法,會存在一份多餘的父類執行個體屬性,具體原因如下: