JavaScript開發教程之基礎知識學習

面向對象

JavaScript中沒有類的概念,因此JS的對象和基於類的語言中的對象不完全相同。


1 理解對象

如下兩種方法都可以創建對象:

//方法1——構造函數
var person = new Object();
person.name = "Name";
person.age = 20;
person.sayName = function(){
    alert(this.name);
};
//方法2——字面量語法
var person = {
    name : "Name";
    age : 20;
    sayName : function(){
        alert(this.name);
    }
};

1.1 屬性類型

屬性擁有一些用於和JS引擎打交道的特性,在JS中不能直接訪問,通常用兩對方括號包圍,例如[[Writable]]。
JS中有兩種屬性:數據屬性和訪問器屬性。


1.1.1 數據屬性

數據屬性包含一個數據值的位置,在這個位置可以讀取和寫入值。
數據屬性有4個特性:

特性 說明
[[Configurable]] true表示該屬性可以通過delete刪除並重新定義,可以修改屬性的特性,可以把屬性修改為訪問器屬性。直接在對象上定義的屬性,默認為true。
[[Enumerable]] true表示該屬性可以通過for-in循環返回。直接在對象上定義的屬性,默認為true。
[[Writable]] true表示該屬性的值可以修改。直接在對象上定義的屬性,默認為true。
[[Value]] 包含瞭該屬性的數據值。讀取該屬性的時候從這個位置讀,寫入的時候,新值保存在這個位置。默認為undefined。

要修改屬性默認的特性,必須使用Object.defineProperty()方法。

Object.defineProperty(objectName,propertyName,descriptor)

參數 說明
objectName 屬性所在的對象名
propertyName 要操作的屬性名
descriptor 要操作的特性,取值為4個特性的一個或多個,用散列表包裝。
如果操作將Configurable特性改為false,則再也無法改變為true。

1.1.2 訪問器屬性

訪問器屬性不包含數據值,而是包含一對getter和setter函數(這兩個函數不是必需的)。當讀取訪問器屬性的時候,會調用getter函數,負責返回有效的值,再寫入訪問器屬性時,會調用setter函數並傳入新值,負責決定如何處理數據。
訪問器屬性有4個特性:

特性 說明
[[Configurable]] true表示該屬性可以通過delete刪除並重新定義,可以修改屬性的特性,可以把屬性修改為數據屬性。直接在對象上定義的屬性,默認為true。
[[Enumerable]] true表示該屬性可以通過for-in循環返回。直接在對象上定義的屬性,默認為true。
[[Get]] 在讀取屬性時調用的函數。默認為undefined。
[[Set]] 在寫入屬性時調用的函數。缺省表示該屬性不能寫。默認為undefined。

訪問器屬性不能直接定義,必須使用Object.defineProperty()方法。

var book = {
    _year : 2016; //下劃線表示這是隻能通過對象方法訪問的屬性
    edition : 1
}; //定義一個很普通的對象

Object.defineProperty(book,"year",{
    get : function(){
        return this._year;
    },
    set : function(newValue){
        if (newValue > 2016){
            this._year = newValue;
            this.edition += newValue - 2016;
        }
    }
});//設置book對象的year屬性的get和set函數

對於不支持Object.defineProperty()方法的瀏覽器,可以使用__defineGetter__()和__defineSetter__()替代:

var book = {
    _year : 2016; //下劃線表示這是隻能通過對象方法訪問的屬性
    edition : 1
}; //定義一個很普通的對象

book.__defineGetter__("year",function(){
    return this._year;
});//設置book對象的year屬性的get函數

book.__defineSetter__("year",function(newValue){
    if (newValue > 2016){
        this._year = newValue;
        this.edition += newValue - 2016;
    }
});//設置book對象的year屬性的set函數

1.2 定義多個屬性

對象往往不止一個屬性,對於多個屬性的定義可以使用Object.defineProperties()方法。

Object.defineProperties(object,properties)

參數 說明
object 要添加和修改其屬性的對象
properties 與第一個參數的對象中要添加或修改的屬性一一對應的散列表

舉個例子:

var book = {};
Object.defineProperties(book,{
    _year : {
        value : 2016
    },
    edition : {
        value : 1
    },
    year : {
        get : function(){
            return this._year;
        },
        set : function(newValue){
            if (newValue > 2016){
                this._year = newValue;
                this.edition += newValue - 2016;
            }
        }
    }
});

1.3 讀取屬性的特性

Object.getOwnPropertyDescriptor()取得給定屬性的特性。

Object.getOwnPropertyDescriptor(object,propertyName)

參數 說明
object 屬性所在的對象
propertyName 屬性名

返回
包含該屬性所有特性的對象
對於之前的例子:

var descriptor = Object.getOwnPropertyDescriptor(book,"_year");
alert(descriptor.value); //2016
alert(descriptor.configurable); //false
alert(typeof descriptor.get); //"undefined"

var descriptor = Object.getOwnPropertyDescriptor(book,"year");
alert(descriptor.value); //"undefined"
alert(descriptor.enumerable); //false
alert(typeof descriptor.get); //"function"

2 創建對象

構造函數和對象字面量的缺點是使用同一個接口創建多個對象時會產生大量重復代碼。


2.1 工廠模式

工廠模式——用函數來封裝以特定接口創建對象的細節。

//工廠模式舉例
function createPerson(name,age,job){
    var o = new Object();
    o.name = name;
    o.age = age;
    o.job = job;
    o.sayName = function(){
        alert(this.name);
    };
    return o;
}

var person1 = createPerson("Andy",20,"student");
var person2 = createPerson("Bob",30,"doctor");

工廠模式解決瞭代碼重復的問題,但沒有解決識別對象的問題,無法知道一個對象是什麼類型。


2.2 構造函數模式

JS可以創建自定義的構造函數,從而定義對象類型的屬性和方法。

function Person(name,age,job){
    this.name = name;
    this.age = age;
    this.job = job;
    this.sayName = function(){alert(this.name);};
}

var person1 = new Person("Andy",20,"student");
var person2 = new Person("Bob",30,"doctor");

用Person()函數替代createPerson()函數,這個方法有是哪個特點:

沒有顯示地創建對象 直接將屬性和方法賦給瞭this對象 沒有return

這種方法實際上經歷瞭以下幾步:

創建一個新對象 將構造函數作用域賦給新對象,因此this就指向瞭這個新對象 執行構造函數中的代碼,為這個新對象添加屬性 返回新對象

上例中的person1和person2分別保存瞭Person的兩個實例,這兩個對象都有一個constructor(構造函數)屬性,該屬性指向Person。constructor屬性最初是用來標識對象類型的。這個例子創建的對象既是Object的實例也是Person實例。

alert(person1 instanceof Object); //true
alert(person1 instanceof Person); //true

與工廠模式相比,自定義構造函數意味著將來可以將它的實例標識為一種特定的類型

但構造函數也有缺點,每個方法都要在每個實例上重新創建一遍。


2.3 原型模式

我們創建的每個函數都有一個prototype屬性,這個屬性是一個指針,指向一個對象,該對象的用途是包含可以有特定類型的所有實例共享的屬性和方法。即prototype就是通過調用構造函數而創建的那個對象實例的原型對象。
其優點是可以讓所有對象實例共享他所包含的屬性和方法,不必在構造函數中定義對象實例的信息,而是將這些信息直接添加到原型對象中。這種方法同一對象的實例訪問的都是同一組屬性和同一個方法。

隻要創建瞭一個新函數,就會根據一組特定的規則為該函數創建一個prototype屬性,這個屬性指向函數的原型對象。所有原型對象都會獲得一個constructor屬性,包含一個指向prototype的指針。上例中Person.prototype.constructor指向Person。通過這個函數可以繼續為原型對象添加其他屬性和方法。
當為對象實例添加一個屬性時,這個屬性就會屏蔽原型對象中保存的那個同名屬性,delete操作符刪掉實例的屬性值可以解除屏蔽,對實例的修改不會影響原型。

hasOwnProperty(propertyName)方法可以確定什麼時候訪問的是實例屬性,什麼時候訪問的原型屬性。隻有實例重寫瞭屬性名才會返回true。

in操作符,有兩種使用方法——單獨使用和for-in循環。
in操作符會在通過對象能夠訪問給定屬性時返回true,無論該屬性在實例中還是原型中。

function Person(){
}

Person.prototype.name = "Andy";
Person.prototype.age = 20;
Person.prototype.sayName = function(){
    alert(this.name);
};

var person1 = new Person();
var person2 = new Person();

alert(person1.hasOwnProperty("name")); //false 沒有重寫name
alert("name" in person1); //true

person1,name = "Grey";
alert(person1.name); //"Grey"來自實例
alert(person1.hasOwnProperty("name")); //true 重寫瞭name
alert("name" in person1); //true

alert(person2.name); //"Andy"來自原型
alert(person2.hasOwnProperty("name")); //false 沒有重寫name
alert("name" in person2); //true

delete person1.name; //刪除person1的name屬性
alert(person1.name); //"Andy"來自原型
alert(person1.hasOwnProperty("name")); //false 沒有重寫name
alert("name" in person1); //true

組合使用二者,
當in返回true,hasOwnProperty()返回false,表示屬性存在於原型
當in返回true,hasOwnProperty()返回true,表示屬性存在於實例

for-in循環時,返回的是所有能夠通過對象訪問的,可以枚舉的屬性,既包括實例中的也包括原型中的。

要取得對象上所有的可枚舉實例屬性,可以使用Object.keys()方法:

// 接上例
var keys = Object.keys(Person.prototype);
alert(keys); //"name,age,sayName"

var p1 = new Person();
p1.name = "Bob";
p1.age = 100;
var p1keys = Object.keys(p1);
alert(p1keys); //"name,age"

要取得所有實例屬性,無論是否可以枚舉,可以使用Object.getOwnPropertyNames()方法。

盡管可以隨時為原型添加屬性和方法,並且修改可以立即在所有對象實例中反映出來,但是如果重寫整個原型對象,則相當於把原型修改為另外一個對象,也就切斷瞭構造函數和最初原型之間的關系。實例中的指針指向原型,而不是構造函數


2.3.1 原生對象的原型

原生的引用類型也是采用的這種模式創建的。可以像修改自定義對象的原型一樣修改原生對象的原型。

String.prototype.startWith  = function(text){
    return this.indexOf(text) == 0;
};

var msg = "My name is Andy.";
alert(msg.startWith("My")); //true

上例,為String引用類型添加瞭一個startWith()方法。所有當前環境下的字符串都可以調用這個方法。


2.3.2 原型對象的缺點

原型中的所有屬性是被很多實例共享的,這種共享對於函數是有利的,對於基本類型的屬性,也可以通過賦新值來改變,但是對於引用類型的屬性,多個實例的操作都會反映在一個原型上,導致每個實例之間不是相互獨立的。
如下例:

function Person(){
}

Person.prototype = {
    name : "Andy";
    age : 20;
    friends : ["Bob","Candy"];
    sayName = function(){
        alert(this.name);
    }
};

var person1 = new Person();
var person2 = new Person();

person1.friends.push("Doggy"); //對person1實例修改

alert(person1.friends); //"Bob,Candy,Doggy"
alert(person2.friends); //"Bob,Candy,Doggy" 對person1的修改對person2產生瞭影響
alert(person1.friends == person2.friends); //true

2.4 構造函數模式與原型模式組合

用構造函數模式定義實例屬性,用原型模式定義方法和共享的屬性。
這樣每個實例都有獨立的實例屬性的空間,又共享瞭方法。
還是同樣的例子:

function Person(name,age){
    this.name = name;
    this,age = age;
    this.friends = ["Bob","Candy"];
}//用構造函數模式定義屬性

Person.prototype = {
    constructor : Person,
    sayName : function(){
        alert(this.name);
    }
}//用原型模式定義方法

var person1 = new Person("Andy",20);
var person2 = new Person("Eric",2);

person1.friends.push("Doggy");
alert(person1.friends); //"Bob,Candy,Doggy"
alert(person2.friends); //"Bob,Candy" 不受到person1的影響
alert(person1.friends === person2.friends); //false 屬性各自獨立
alert(person1.sayName === person2.sayName); //true 共享方法

2.5 動態原型模式

上述方法仍然不夠簡潔和美觀,動態原型模式把所有信息都封裝在構造函數中,通過在構造函數中初始化原型,又保持瞭同時使用二者的優點。通過檢查某個應該存在的方法是否有效來決定是否需要初始化原型。

fucntion Person(name,age){
    this.name = name;
    this.age = age;

    if(typeof this.sayName != "function"){
        Person.prototype.sayName = function(){
            alert(this.name);
        };
    }
}

2.6 寄生構造函數模式

原理工廠模式其實是一樣的,一個函數創建一個對象,並給該對象賦予屬性和方法,再將這個對象返回。唯一的不同,是使用new操作符新建對象,把工廠函數成為構造函數而已。

//在不修改Array構造函數的情況下,創建一個具有特殊方法的數組

function SpecialArray(){
    var values = new Array();//創建一個普通的數組
    values.push.apply(values,arguments); //給對象添加值
    values.toPopeString = function(){
        return this.join("|");
    };//給對象添加方法

    return values;  
}

var colors = new SpecialArray("red","blue","green");
alert(colors.toPipeString); //"red|blue|green"

3 繼承

同其他面向對象語言一樣,JS也有繼承。
JS的繼承主要依靠原型鏈來實現。


3.1 原型鏈

這個概念很重要!!!
基本思想是利用原型讓每一個引用類型繼承另一個引用類型的屬性和方法。

每個構造函數都有一個原型對象 每個原型對象都包含一個指向構造函數的指針 每個實例都包含一個指向原型對象的內部指針

如果讓一個原型對象等於另一個類型的實例,則原型對象將包含一個指向另一個原型的指針,相應的,另一個原型也包含一個指向另一個構造函數的指針,如果另一個原型又是另一個類型的實例,同樣滿足上述關系,層層遞進,成瞭一個“鏈”。

實現原型鏈的基本模式:

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

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

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

SubType.prototype = new SuperType(); //SubType繼承瞭SuperType

SubType.prototype.getSubValue = function(){
    return this.subproperty;
};

var instance = new SubType();
alert(instance.getSuperValue()); //true

繼承的關鍵一步是新建的SuperType的實例賦給SubType的原型,其本質是重寫瞭SubType的原型對象,所以原來存在於SuperType的實例中的所有屬性和方法現在也存在於SubType.prototype中。

下圖較為直觀的表現瞭這二者的關系。
這裡寫圖片描述

最終結果就是SubType的實例instance指向SubType的原型,SubType的原型又指向SuperType的原型。getSuperValue()方法作為原型方法還在SuperType.prototype中,property作為實例屬性已經位於SubType.prototype中。

原型搜索機制——當讀取模式訪問一個實例屬性是,首先會在實例中搜索該屬性,如果沒有,則會繼續搜索實例的原型。"311-默認的原型">3.1.1 默認的原型

每個引用類型都默認繼承瞭Object,而這個繼承也是通過原型鏈實現的。所有函數的默認原型都是Object實例,因此默認原型都會包含一個內部指針指向Object.prototype。

這裡寫圖片描述


3.1.2 確定原型和實例的關系

有兩種方法:

instanceof操作符,隻要用這個操作符來測試實例與原型鏈中出現過的構造函數,就返回true isPrototypeOf()方法,隻要是原型鏈中出現過的原型,就返回true

alert(Object.prototype.isPrototypeOf(instance)); //true
alert(SuperType.prototype.isPrototypeOf(instance)); //true
alert(SubType.prototype.isPrototypeOf(instance)); //true

3.1.3 謹慎的定義方法

子類型重寫父類型的方法或添加父類沒有的方法時,相關的代碼要放在替換原型的語句之後。

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

SuperType.prototype.getSuperValue = function(){
    return this.property;
};//父類方法

function SubType(){
    this.subproperty = false;
}//子類

SubType.prototype = new SuperType(); //SubType繼承瞭SuperType

SubType.prototype.getSubValue = function(){
    return this.subproperty;
};//添加新方法

SubType.prototype.getSuperValue = function(){
    return false;
};//重寫父類方法

var instance = new SubType();
alert(instance.getSuperValue()); //false 調用的是重寫後的getSuperValue()

特別重要的一點是,原型鏈實現繼承時,不能使用對象字面量創建原型方法。因為這樣做就會重寫原型鏈,導致繼承失效。


3.1.4 原型鏈的缺點

和原型對象的缺點一樣,原型鏈的問題來自包含引用類型值的原型。問題還是出在共享屬性上。


3.2 借用構造函數

為瞭解決上述問題,提出瞭一種借用構造函數。
即在子類型構造函數的內部調用父類型構造函數。

function SuperType(){
    this.colors = ["red","blue","green"];
}

function SubType(){
    //繼承瞭SuperType
    SuperType.call(this); //在子類的構造函數中調用父類的構造函數
}

var instance1 = new Subtype();
instance1.colors.push("black");
alert(instance1.colors); //"red,blue,green,black"

var instance2 = new SubType();
alert(instance2.colors); //"red,blue,green" 兩個實例相互獨立 屬性不影響

3.2.1 傳遞參數

相對於原型鏈,借用構造函數優勢在於可以在子類構造函數中向父類構造函數傳遞參數。

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

function SubType(){
    SuperType.call(this,"Andy"); //實際上是給SubType添加瞭一個name屬性

    this.age = 20;
}

var instance = new SubTyoe();
alert(instance.name); //"Andy"
alert(instance.age); //20

3.2.2 借用構造函數的缺點

缺點和構造函數模式是相似的。


3.3 組合繼承

將上述兩種方法合二為一。

原型鏈實現實現對原型屬性和方法的繼承,實現函數復用 借用構造函數來實現對實例屬性的繼承,保證每個實例有自己的屬性

function SuperType(name){
    this.name = name;
    this.colors = ["red","blue","green"];
}//父類的構造函數,父類有2個屬性
SuperType.prototype.sayName = function(){
    alert(this.name);
};//父類原型方法

function SubType(name,age){
    //繼承屬性
    SuperType.call(this, name);
    this.age = age; //子類特有的屬性
}//子類的構造函數

SubType.prototype = new SuperType();//繼承方法

SubType.prototype.sayAge = function(){
    alert(this.age);
};//子類添加的方法

var instance1 = new SubType("Andy",20);
instance1.colors.push("black");
alert(instance1.colors);//"red,blue,green,black"
instance1.sayName(); //"Andy"
instance1.sayAge(); //20

var instance2 = new SubType("Bob",2);
alert(instance2.colors);//"red,blue,green"
instance2.sayName(); //"Bob"
instance2.sayAge(); //2

組合繼承避免瞭原型鏈和借用構造函數模式的缺點,是JS中最常用的集成模式。


3.4 原型式繼承

這種方法沒有嚴格意義上的構造函數,其想法是借助原型可以基於已有對象創建新對象,並且不必因此創建自定義類型。

function object(o){
    function P(){} //在函數內部先創建一個臨時的構造函數,
    P.prototype = o; //將傳入的對象作為這個構造函數的原型
    return new P(); //返回這個臨時類型的一個新實例
}
//本質是對傳入參數的一次淺復制

JS通過Object.create()方法規范化瞭原型式繼承。
這種方法在於方便,但同樣存在原型模式的缺點。


3.5 寄生式繼承

這種方法與工廠模式類似,創建一個僅用於封裝繼承過程的函數。

function createAnother(){
    var clone = object(original); //通過調用函數創建新對象
    clone.sayHi = function(){ //給對象增加方法
        alert("hi");
    };
    return clone; //返回該對象
}

在主要考慮對象而不是自定義類型和構造函數的情況下,寄生式繼承也是一種有用的模式。
同樣存在多個實例多次定義函數的情況,執行效率會降低。


3.6 寄生組合式繼承

組合繼承有自己的缺點,無論什麼情況下,都會調用兩次父類的構造函數,創建子類原型的時候和子類構造函數內部。

function SuperType(name){
    this.name = name;
    this.colors = ["red","blue","green"];
}//父類的構造函數
SuperType.prototype.sayName = function(){
    alert(this.name);
};//父類原型方法

function SubType(name,age){
    SuperType.call(this, name); //第二次調用父類構造函數 在新對象上創建瞭實例屬性name,colors
    this.age = age; 
}//子類的構造函數

SubType.prototype = new SuperType(); //第一次調用父類構造函數 SubType得到name和colors屬性
SubType.prototyp.constructor = SubType;
SubType.prototype.sayAge = function(){
    alert(this.age);
};//子類添加的方法

寄生組合式繼承

通過借用構造函數繼承屬性 通過原型鏈的混成形式來繼承方法

本質就是使用寄生式繼承來繼承父類的原型,再將結果指定給子類型的原型

基本模式如下:

function inheritPrototype(subType,superType){
    var prototype = object(superType.prototype); //創建對象 創建父類原型的一個副本
    prototype.constructor = subType; //增強對象 為創建的副本添加constructor屬性 彌補因重寫原型而失去的默認的constructor屬性
    subType.prototype = prototype; //指定對象 將新創建的對象賦值給子類型的原型
}

使用寄生組合式繼承後,原來的例子可以修改為如下形式:

function SuperType(name){
    this.name = name;
    this.colors = ["red","blue","green"];
}//父類的構造函數
SuperType.prototype.sayName = function(){
    alert(this.name);
};//父類原型方法

function SubType(name,age){
    SuperType.call(this, name); //第二次調用父類構造函數 在新對象上創建瞭實例屬性name,colors
    this.age = age; 
}//子類的構造函數

inheritPrototype(subType,superType);
SubType.prototype.sayAge = function(){
    alert(this.age);
};//子類添加的方法

這種繼承方式隻調用瞭一次父類構造函數,避免在子類原型上創建不必要的屬性,原型鏈也保持瞭原樣,可以用instanceof和isPrototypeOf()判別類型,是引用類型最理想的繼承范式。

發佈留言