天天看點

面向對象的程式設計

下述内容主要講述了《JavaScript進階程式設計(第3版)》第6章關于“面向對象的程式設計”。

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

一、了解對象

1. 屬性類型

ECMAScript中有兩種屬性:資料屬性和通路器屬性。

資料屬性包含一個值;通路器屬性不包含值而定義了一個當屬性被讀取時調用的函數(getter)和一個當屬性被寫入時調用的函數(setter)。

(1)資料屬性

特性 說明 描述
[[Configurable]] 可配置 能否删除、修改屬性的特性
[[Enumerable]] 可枚舉 能否通過for-in循環傳回屬性
[[Writable]] 可寫 能否修改屬性的值
[[Value]] 資料值 讀取、寫入值的位置

(2)通路器屬性

特性 說明 描述
[[Configurable]] 可配置 能否删除、修改屬性的特性
[[Enumerable]] 可枚舉 能否通過for-in循環傳回屬性
[[Get]] 屬性被讀取時調用的函數 預設值undefined
[[Set]] 屬性被寫入時調用的函數 預設值undefined

修改屬性預設的特性:

Object.defineProperty(屬性所在的對象, 屬性, 描述符對象)

示例:資料屬性

var person = {};
Object.defineProperty(person, "name", {
    configurable: true,
    enumerable: false,
    writable: false,
    value: "lg"
});

console.log(person.name);  // "lg"
for(var prop in person){
    console.log(prop);      // 未執行
}
person.name = "li";
console.log(person.name);  // "lg"           

複制

示例:通路器屬性

var person = {
    _name: "ligang"
};
Object.defineProperty(person, "name", {
    configurable: false,
    enumerable: true,
    set: function(name){
        /* 此處可以做其他操作 */
        this._name = name;
    },
    get: function(){
        /* 此處可以做其他操作 */
        return this._name;
    }
});
console.log(person.name);  // "lg"
for(var prop in person){
    console.log(prop);      // "_name"
}
person.name = "li";
console.log(person.name);  // "li"           

複制

可以通過

Object.getOwnPropertyDescriptor(屬性所在的對象, 屬性)

取得給定屬性的描述符;通過Object.defineProperties(屬性所在的對象, {屬性1:描述符對象1, 屬性2:描述符對象2})一次性定義多個屬性。

二、建立對象

1. 工廠模式

工廠模式抽象了建立具體對象的過程。

function createPerson(name, age){
    var obj = new Object();
    obj.name = name;
    obj.age = age;
    obj.sayName = function(){
        console.log(this.name);
    };
    return obj;
}
var p1 = createPerson("z3", 26);
var p2 = createPerson("l4", 27);           

複制

工廠模式可以解決建立多個相似對象的問題,但是會出現識别問題(即怎麼知道一個對象的類型)。

2. 構造函數模式

function Person(name, age){
    this.name = name;
    this.age = age;
    this.sayName = function(){
        console.log(this.name);
    };
}
var p1 = new Person("z3", 26);
var p2 = new Person("l4", 27);
console.log(p1 instanceof Person); // true           

複制

可以辨別類型(

p1.constructor ==> Person

),其方法都要在每個執行個體上重新建立一遍。

3. 原型模式

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

function Person(){}
Person.prototype.name = "lg";
Person.prototype.age = 26;
Person.prototype.sayName = function(){
    console.log(this.name);
};
var p1 = new Person();
p1.name = "z3";
console.log(p1.name);                       // "z3" 執行個體
console.log("name" in p1);                  // true
console.log(p1.hasOwnProperty("name"));     // true
delete p1.name;
console.log(p1.name);                       // "lg" 原型
console.log("name" in p1);                  // true
console.log(p1.hasOwnProperty("name"));     // false           

複制

當為對象添加一個屬性時,這個屬性就會屏蔽原型對象中儲存的同名屬性。hasOwnProperty()方法可以檢測一個屬性是否存在于執行個體中,還是存在于原型中;in操作符無論該屬性存在于執行個體中還是原型中。

示例:判斷屬性存在于原型中還是對象中

/* 方式一:函數封裝 */
function hasPrototypeProperty(object, name){
    return !object.hasOwnProperty(name) && (name in object);
}
hasPrototypeProperty(p1, "name");   // true 原型
/* 方式二:原型擴充 */
Object.prototype.hasPrototypeProperty = function(prop){
    return !this.hasOwnProperty(prop) && (prop in this);
};
p1.hasPrototypeProperty("name");    // true 原型           

複制

更簡單的原型文法:

function Person(){}
Person.prototype = {
    name: "lg",
    age: 26,
    friends: ["camile"],
    sayName: function(){
        console.log(this.name);
    }
};
// 修正構造函數指向,不可枚舉
Object.defineProperty(Person.prototype, "constructor", {
    enumerable: false,
    value: Person
});
var p1 = new Person();
var p2 = new Person();

p1.friends.push("Gavin");
console.log(p1.friends);    // ["camile", "Gavin"]
console.log(p2.friends);    // ["camile", "Gavin"]           

複制

如果我們的初衷就是所有執行個體共享一個數組,那麼其符合預期;若想每個執行個體都有屬于自己的全部屬性,會存在上述問題。

4. 組合使用構造函數模式和原型模式

構造函數模式用于定義執行個體屬性,而原型模式用于定義方法和共享的屬性。每個執行個體都會有自己的一份執行個體屬性的副本,但同時又共享着方法的引用,最大限度地節省記憶體。

function Person(name, age, friends){
    this.name = name;
    this.age = age;
    this.friends = friends || [];
}
Person.prototype.sayName = function(){
    console.log(this.name);
};
var p1 = new Person("Gavin", 26);
var p2 = new Person("Camile", 26);
p1.friends.push(["James"]);
console.log(p1.friends);    // ["James"]
p2.friends.push(["Tom"]);
console.log(p2.friends);    // ["Tom"]           

複制

是目前ECMAScript中使用最廣泛、認同度最高的一種建立自定義類型的方法!!

5. 動态原型模式

将構造函數和原型結合,不再獨立。

function Person(name, age){
    this.name = name;
    this.age = age;
    // 不存在情況下,才會添加
    if(typeof this.sayName !== "function"){
        Person.prototype.sayName = function(){
            console.log(this.name);
        }
    }
}
var p1 = new Person("Gavin", 26);
p1.sayName();   // "Gavin"
// Person.prototype.sayName不會被執行
var p2 = new Person("Camile", 26);           

複制

注意:不能使用對象字面量重寫原型,其會切斷現有執行個體與新原型之間的聯系。

6. 寄生構造函數模式

其和典型的構造函數有略微的差別

function Person(name, age){
    var obj = new Object();
    obj.name = name;
    obj.age = age;
    obj.sayName = function(){
        console.log(this.name);
    };
    return obj;
}
var p1 = new Person("ligang", 26);
console.log(p1 instanceof Person);  // false
console.log(p1.constructor);        // Object           

複制

如果我們想建立一個具有額外方法的特殊屬性,使用上述模式會達到很好的效果!!

function SpecialArray(){
    var ary = new Array();
    ary.push.apply(ary, arguments);
    ary.toPipedString = function(){
        return this.join("|");
    };
    return ary;
}
var colors = new SpecialArray("red", "yellow", "blue");
colors.toPipedString();           

複制

三、繼承

JavaScript主要通過原型鍊實作繼承。

1. 原型鍊

每個構造函數都有一個原型對象(

prototype

),原型對象都包含一個指向構造函數的指針(

constructor

),而執行個體都包含一個指向原型對象的内部指針(

__proto__

)。

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

function Sub(){
    this.subProperty = false;
}
Sub.prototype = new Super();  // 将一個類型的執行個體賦給另一個構造函數的原型
Sub.prototype.getSubValue = function(){
    return this.subProperty;
};

var instance = new Sub();
console.log(instance.getSubValue());     // false
console.log(instance.getSuperValue());   // true
console.log(instance.constructor);       // Super
console.log(instance instanceof Sub);    // true
console.log(instance instanceof Super);  // true           

複制

面向對象的程式設計

問題:(1)包含引用類型值的原型,會被所有執行個體共享;(2)建立子類型的執行個體時,不能向父類型的構造函數中傳遞參數。

2. 借用構造函數

在子類構造函數的内部調用父類的構造函數。

function Super(name){
    this.name = name;
    this.colors = ["red"];
}

function Sub(name, age){
    // 繼承Super,可傳遞參數
    Super.call(this, name);
    this.age = age;
}

var instance = new Sub("Gavin", 26);
instance.colors.push("blue");
console.log(instance.name, instance.age);   // Gavin 26
console.log(instance.colors);   // ["red", "blue"] 獨立的colors副本
console.log(new Sub().colors);  // ["red"] 獨立的colors副本           

複制

問題:方法都在構造函數中定義,函數複用無從談起。

3. 組合繼承

将原型鍊和借用構造函數組合一起。使用原型鍊實作對原型屬性和方法的繼承,而通過借用構造函數來實作對執行個體屬性的繼承。

function Super(name){
    this.name = name;
    this.color = ["red"];
}
Super.prototype.sayName = function(){
    console.log(this.name);
};
function Sub(name, age){
    Super.call(this, name);
    this.age = age;
}
Sub.prototype = new Super();
Sub.prototype.sayAge = function(){
    console.log(this.age);
};

var instance1 = new Sub("Gavin", 26);
instance1.color.push("blue");
console.log(instance1.color);   // ["red", "blue"]
instance1.sayName();            // "Gavin"
instance1.sayAge();             // 26

var instance2 = new Sub("Camile", 26);
instance2.color.push("yellow");
console.log(instance2.color);   // ["red", "yellow"]
instance2.sayName();            // "Camile"
instance2.sayAge();             // 26           

複制

JavaScript中最常用的繼承模式!!!

4. 原型式繼承

function createObj(o){
    function F(){}
    F.prototype = o;    // 對o的一種淺複制
    return new F();
}           

複制

該方法等價于ECMAScript5中

Object.create()

方法隻傳入第一個參數。引用類型值的屬性會共享相應的值。

5. 寄生式繼承

在原型式繼承基礎上,繼續改造

function createAnother(original){
    var clone = createObj(original);
    clone.sayHi = function(){
        console.log("Hi");
    };
    return clone;
}
var person = {
    name: "LIGANG",
    age: 26
};
var anotherPerson = createAnother(person);
anotherPerson.sayHi();           

複制

對象方法不能被複用!