天天看點

JavaScript進階程式設計——第6章 了解對象

ECMA-262對對象的定義:無序屬性的集合,其屬性可以包含基本值、對象或者函數。

6.1 了解對象

建立對象

//早起建立對象方法
var person = new Object();
person.name = "Greg";
person.age = ;
person.sayName = function(){
    console.log(this.name);
};

//字面量方法

var person = {
    name:"Greg",
    age:,
    sayName:function(){
        console.log(this.name);
    }
}; 
           

6.1.1屬性類型

隻有内部才用的特性。規範:例如[[Enumerable]]

資料屬性
  • 資料屬性包含一個資料值的位置,在這個位置可以讀取和寫入值。
特性 描述
[[Enumerable]] 能否通過for-in循環傳回屬性,例子中直接在對象上定義的屬性,預設true
[[Configurable]] 能否通過delete删除屬性進而重新定義屬性,能否修改屬性特性,能否把屬性修改為通路器屬性,例子中直接在對象上定義的屬性,預設true
[[Writable]] 能否修改屬性的值,例子中直接在對象上定義的屬性,預設true
[[Value]] 包含這個屬性的資料值,預設undefined

例如,前面person對象的name屬性的資料屬性分别是true,true,true,“Greg”。

* 修改屬性預設的特性 Object.defineProperty(object,property,descriptor)

該函數接收三個參數:屬性所在的對象、屬性的名字和一個描述符對象。描述符對象的屬性必須是enumerable、configurable、writable和value中的一個或多個。

var person = new Object();
Object.defineProperty(person,"name",{
    writable:false,
    value:"Jane"
});
person.age = ;
person.sayName = function(){
    console.log(this.name);
};
person.sayName();  //Jane
person.name = "Greg";
person.sayName();  //Jane
           

如果嘗試為name屬性指定新值,非嚴格模式下,指派被忽略;嚴格模式下,抛出錯誤。

注:可以多次修改同一屬性,但是修改Configurable特性後,隻能再修改writable特性。

通路器屬性
  • 通路器屬性不包含資料值,有一對getter和setter函數
特性 描述
[[Enumerable]] 能否通過for-in循環傳回屬性,例子中直接在對象上定義的屬性,預設true
[[Configurable]] 能否通過delete删除屬性進而重新定義屬性,能否修改屬性特性,能否把屬性修改為通路器屬性,例子中直接在對象上定義的屬性,預設true
[[Get]] 讀取屬性時候調用的函數,預設undefined
[[Set]] 寫入屬性時候調用的函數,預設undefined

* 通路器屬性的定義:Object.defineProperty(object,property,descriptor)

//使用通路器屬性常見方式:設定一個屬性的值,導緻其他屬性發生變化
var book = {
    _year:, //隻能通過對象方法通路的屬性
    edition:
};
Object.defineProperty(book,"year",{
    get:function(){
        return this._year;
    },
    set:function(newValue){
        if(newValue > ){
            this._year = newValue;
            this.edition += newValue-;
        }
    }
});
book.year = ;
console.log(book.edition); //2
           

6.1.2 定義多個屬性

Object.defineProperties(object,properties)

該函數接收兩個參數,第一個是要添加和修改其屬性的對象;第二個對象的屬性與要添加的修改的對象的屬性一一對應。

var book = {};
Object.defineProperties(book,{
    //資料屬性
    _year:{value:},
    edition:{value:},
    //通路器屬性
    year:{
        get: function () {
            return this._year;
        },
        set: function (newValue) {
            if (newValue > ) {
                this._year = newValue;
                this.edition += newValue - ;
            }
        }
    }
});
           

6.1.3 讀取屬性的特性

Object.getOwnPropertyDescription()接收兩個參數:屬性所在的對象和要讀取其描述符的屬性名稱。

var descriptor = Object.getOwnPropertyDescriptor(book,"_year");
console.log(descriptor.value);         //2004
console.log(descriptor.configurable);  //false
           

6.2 建立對象

6.2.1 工廠模式

用函數來封裝以特定接口建立對象的細節。

function createPerson(name,age,job){
    var o = new Object();
    o.name = name;
    o.age = age;
    o.job = job;
    o.sayName = function(){
        console.log(this.name);
    };
    return o;
}
var person1 = createPerson("Jane",,"teacher");
           

弊端:沒有解決對象識别問題(怎樣知道一個對象的類型)

6.2.2 構造函數模式

function Person(name,age,job){
    this.name = name;
    this.age = age;
    this.job = job;
    this.sayName = function(){
        console.log(this.name);
    };
}
var person1 = new Person("Jack",,"worker");
var person2 = new Person("Tom",,"actor");
           

與上面一種方法比較:

* 沒有顯式地建立對象,

* 直接将屬性和方法賦給this

* 沒有return語句

* 建立執行個體的時候需要用new操作符

弊端:每個方法都要在每個執行個體上建立一遍,比如上面的person1和person2的sayName是不相等的。有this屬性在,沒必要在執行代碼前面就把函數綁定到特定對象上面。如果把函數定義轉移到構造函數外部,又影響封裝性。

6.2.3 原型模式

建立的每個函數都有一個prototype屬性,該屬性是一個指針,指向一個對象,這個對象的用途是包含可以由特定類型的所有執行個體共享的屬性和方法。

function Person(){}
Person.prototype.name = "Tom";
Person.prototype.age = ;
Person.prototype.sayName = function(){
    console.log(this.name);
};
var person1 = new Person();
person1.sayName();  //Tom
var person2 = new Person();
person2.sayName();  //Tom
console.log(person1.sayName == person2.sayName);  //true
           
  • 原型對象

任何時候建立一個新函數時,就會為該函數建立一個prototype屬性,這個屬性指向函數的原型對象。預設情況下,所有原型對象都會自動獲得一個constructor(構造函數)屬性,這個屬性包含一個指向prototype屬性所在函數的指針。調用函數建立一個新的執行個體,該執行個體的内部将包含一個指針(内部屬性),指向構造函數的原型對象。ES5稱之為[[Prototype]]。

JavaScript進階程式設計——第6章 了解對象

①Person.prototype指向原型對象

②Person.prototype.constructor指回Person

③Person的每個執行個體的内部屬性隻指向Person.prototype,執行個體與構造函數沒有直接關系

确定對象和原型之間是否有關系,有以下兩種方法

console.log(Person.prototype.isPrototypeOf(person1));             //true
console.log(Object.getPrototypeOf(person1) == Person.prototype); //true
           

每當代碼讀取一個對象的某個屬性時,先搜尋對象執行個體有沒有這個屬性,有就傳回該屬性的值;如果沒找到,繼續搜尋指針指向的原型對象,若在原型對象找到該屬性,則傳回該屬性的值。

注:不能通過對象執行個體重寫原型中的值,執行個體中與原型對象重名的屬性,會屏蔽原型的屬性。

person1.name = "Jack";
person1.sayName(); //Jack——來自執行個體
person2.sayName(); //Tom——來自原型
           

使用delete可以完全删除執行個體屬性,同時原型屬性不受影響

delete person1.name;
person1.sayName(); //Tom——來自原型
           

檢測一個屬性是否存在于執行個體中:hasOwnProperty()

  • in操作符

單獨的in操作符,property in object,通過對象能通路到屬性,傳回true。不管屬性是存在于執行個體中,還是原型中。

person1.name = "Jack";
console.log("name" in person1); //true
delete person1.name;
console.log("name" in person1); //true
           

使用for-in循環:傳回所有能通過對象通路的、可枚舉的(enumerated)屬性,包括存在在執行個體中、原型中的屬性。屏蔽了原型中不可枚舉的屬性([[Enumerable]]标記為false)的執行個體屬性也會在for-in中傳回,因為開發人員定義的屬性都是可枚舉的。【此處IE有bug】

ES5中的Object.keys()方法接受一個參數對象,傳回一個包含所有可枚舉屬性的字元串數組,Object.getOwnPropertyNames()擷取所有屬性。

function Person(){}
Person.prototype.name = "Tom";
Person.prototype.age = ;
Person.prototype.sayName = function(){
    console.log(this.name);
};
var p1 = new Person();
p1.name = "Jack";
var key1 = Object.keys(Person.prototype); //[ 'name', 'age', 'sayName' ]
var key2 = Object.keys(p1);               //[ 'name' ]
var key3 = Object.getOwnPropertyNames(Person.prototype); //[ 'constructor', 'name', 'age', 'sayName' ]
                                                          //包括不可枚舉的constructor
           
  • 更簡單的原型屬性

    對象字面量重寫Person原型對象:

function Person(){}
Person.prototype = {
    name:"Tom",
    age:,
    sayName:function(){
        console.log(this.name);
    }
};
           

注意,constructor不再指向Person,這裡的文法本質上重寫了prototype屬性,constructor屬性變成新的對象的constructor屬性(指向Object)。instanceof操作符還能正确傳回結果。

var friend = new Person();
console.log(friend instanceof Person);     //true
console.log(friend instanceof Object);    //true
console.log(friend.constructor == Object); //true
console.log(friend.constructor == Person); //false
           

可以讓constructor重新指向Person:

function Person(){}
Person.prototype = {
    /************看這裡*************/
    constructor:Person,
    name:"Tom",
    age:,
    sayName:function(){
        console.log(this.name);
    }
};
           

以上方法會使constructor的[[Enumerable]]特性被設定為true。ES5相容的浏覽器可以這麼幹:

//重設構造函數
Object.defineProperty(Person.prototype,"constructor",{
    enumerable:false,
    value:Person
});
           
  • 原型的動态性

在原型中查找值的過程是一次搜尋,對原型對象作出的任何修改能立刻從執行個體中反映出來。

* 原生對象的原型

所有原生引用類型(Object、Array、String…)都在其構造函數的原型上定義的方法。可以為原生對象的原型添加方法

  • 原型對象的問題

有時候共享屬性不是我們想要的。比如向前文的Person.prototype添加:

friend:["Jane","Tod"];
           

對person1執行 person.friend.push(“Andy”); 結果同樣反映到person2上。

解決方法:組合使用構造函數模式和原型模式

不希望(不需要)共享的屬性,在構造函數中定義;constructor和共享的方法在原型中定義。

* 動态原型模式

function Person(name,age,job){
    //屬性
    this.name = name;
    this.age = age;
    this.job = job;
    //方法
    if(typeof this.sayName != "function"){
        Person.prototype.sayName = function(){
            console.log(this.name);
        };
    }
}
           

方法部分的代碼隻在初次調用構造函數時執行

* 寄生構造函數模式

基本思想:建立一個函數,函數的作用僅僅是封裝建立對象的代碼,然後傳回新建立的對象。

function Person(name,age,job){
    var o = new Object();
    o.name = name;
    o.age = age;
    o.job = job;
    o.sayName = function(){
        console.log(this.name);
    };
    return o;
}
           
  • 穩妥構造函數模式

穩妥對象:沒有公共屬性,其方法也不引用this。适合在一些安全的環境中(禁用new和this)或者在防止資料被改動時使用。

function Person(name,age,job){
    var o = new Object();
    o.name = name;
    o.age = age;
    o.job = job;
    o.sayName = function(){
        console.log(name);
    };
    return o;
}
           

以上例子,除了sayName方法,沒有其他辦法通路name的值。

6.3 繼承

ES隻支援實作繼承。

6.3.1 原型鍊

function SuperType(){
    this.property = true;
}
SuperType.prototype.getSuperValue = function(){
    return this.property;
};
function SubType(){
    this.prototype = false;
}
SubType.prototype = new SuperType();
SubType.prototype.getSubValue = function(){
    return this.property;
};
var instance = new SubType();
console.log(instance.getSuperValue()); //true
           

關系圖:

JavaScript進階程式設計——第6章 了解對象

①實作的本質是重寫原型對象。原本存在于SuperType的執行個體中的所有屬性和方法,現在也存在于SubType.prototype中。

②property屬性從SuperType“變到”SubType.prototype中。因為property是一個執行個體屬性,getSuperValue是一個原型屬性。SubType.prototype是SuperType的執行個體,那麼property就位于該執行個體中。

③instance.constructor現在指向SuperType

④不能使用字面量方法重寫SubType.prototype,會切斷原型鍊

* 預設的原型

所有引用類型預設繼承Object

JavaScript進階程式設計——第6章 了解對象

* 确定原型和執行個體的關系

instanceof操作符、isPrototypeOf()方法:隻要是原型鍊中出現過的原型,結果都傳回true。

console.log(instance instanceof Object); //true
console.log(SuperType.prototype.isPrototypeOf(SubType.prototype)); //true
           
  • 原型鍊的問題:SuperType的執行個體屬性會變成SubType.prototype的屬性,同樣有共享原型屬性的問題。

6.3.2 借用構造函數

在子類型構造函數的内部調用超類型構造函數。使用apply()/call()在(将來)新建立的對象上執行構造函數。

function Product(name,price){
    this.name = name;
    this.price = price;
}
function Food(name,price){
    Product.call(this,name,price);
    this.category = "food";
}

//等同于
function Food(name, price) {
    this.name = name;
    this.price = price;
    this.category = 'food';
}

var f1 = new Food("apple",);
var f2 = new Food("corn",);
console.log(f1); //Food { name: 'apple', price: 5.8, category: 'food' }
console.log(f2); //Food { name: 'corn', price: 3, category: 'food' }
           

6.3.3 組合繼承

使用原型鍊實作對原型屬性和方法的繼承,通過借用構造函數來實作對執行個體屬性的繼承。【最常用】

function Product(name,price){
    this.name = name;
    this.price = price;
}
Product.prototype.printName = function(){
    console.log(this.name);
};
function Food(name,price){
    Product.call(this,name,price);//繼承屬性
    this.category = "food";
}
Food.prototype = new Product();//繼承方法
Food.prototype.constructor = Product;
Food.prototype.printCategory = function(){
    console.log(this.category);
};
           

6.3.4 原型式繼承

ES5的Object.create()

6.3.5 寄生式繼承

建立一個僅用于封裝繼承過程的函數,在該函數内部以某種方式增強對象,最後傳回對象。

function createAnother(original){
    var clone = object(original); //通過調用函數建立一個新對象
    clone.sayHi = function(){    //以某種方式增強這個對象
        console.log("Hi";)
    };
    return clone;                //傳回對象
}
           

6.3.6 寄生組合式繼承

組合繼承中,調用了兩次超類型構造函數,第二次調用時重寫了執行個體屬性。

寄生組合:借用構造函數繼承屬性,通過原型鍊的混成形式來繼承方法。

function Product(name,price){
    this.name = name;
    this.price = price;
}
Product.prototype.printName = function(){
    console.log(this.name);
};
function Food(name,price){
    Product.call(this,name,price);//繼承屬性
    this.category = "food";
}
inheritPrototype(Food,Product);
Food.prototype.printCategory = function(){
    console.log(this.category);
};

function inheritPrototype(subType,superType){
    var prototype = Object(superType.prototype); //建立對象
    prototype.constructor = superType;           //增強對象
    subType.prototype = prototype;               //指定對象
}
           

繼續閱讀