javascript高級程序設計第三版 第六章 面向對象的程序設計

6 面向對象的程序設計

6.1 理解對象

6.1.1 屬性類型

分兩種:數據屬性和訪問器屬性
js引擎使用,js不能直接訪問。
4個描述其行爲的特性。
[[Configurable]] 能否通過delete刪除屬性從而重新定義屬性,能否修改屬性的特性,或者能否把屬性修改爲訪問器屬性。默認true
[[Enumerable]] 能否通過for-in循環返回屬性,默認true
[[Writable]] 能否修改屬性的值 默認true
[[Value]] 包含這個屬性的數據值。默認undefined

var person={};
Object.defineProperty(person,"name",{
    writable:false,
    value:"Nicholas"
});
alert(person.name);//"Nicholas"
person.name="Greg";
alert(person.name);//"Nicholas"

採用Object.defineProperty()時,如果不指定,configurable,enumerable和writable默認false

訪問器屬性
[[Get]] 讀取屬性時調用的函數,默認undefined
[[Set]] 設置屬性時調用的函數,默認undefined

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

book.year=2005;
alert(book.edition); //2

6.1.2 定義多個屬性

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

6.1.3 讀取屬性的特性

var descriptor=Object.getOwnPropertyDescriptor(book,"_year");
alert(descriptor.value);//2004
alert(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(){
        alert(this.name);
    }
    return o;
}

var person1=createPerson("a",10,"engineer");
var person2=createPerson("b",20,"teacher");

優點:解決了創建多個相似對象的問題
缺點:沒有解決對象識別的問題,即怎樣知道一個對象的類型。只有person1 instanceof Object爲true

6.2.2 構造函數模式

//沒有顯示創建對象
//直接將屬性和方法賦值給this對象
//沒有return語句
function Person(name,age,job){
    this.name=name;
    this.age=age;
    this.job=job;
    this.sayName=function(){
        alert(this.name);
    }
}

var person1=new Person("a",10,"engineer");
var person2=new Person("b",20,"teacher");

alert(person1.constructor == Person);//true
alert(person1 instanceof Object);//true
alert(person1 instanceof Person);//true

必須使用new操作符調用構造函數,經歷4個步驟:
1、創建一個新對象
2、將構造函數的作用域賦值給新對象,因此this指向新對象
3、執行構造函數的代碼
4、返回新對象

如果把構造函數當做函數調用,跟普通函數一樣

//構造函數
var person=new Person("a",10,"engineer");
person.sayName();//"a"

//普通函數
Person("a",10,"engineer");
window.sayName();//"a"

//在另一個對象的作用域中調用
var o=new Object();
Person.call(o,"a",10,"engineer");
o.sayName();//"a"

構造函數的問題
person1和person2都有一個名爲sayName()的方法,但不是同一個Function的實例,會浪費內存。ECMAScript中的函數是對象,因此每定義一個函數,也就是實例化了一個對象。

function Person(name,age,job){
    this.name=name;
    this.age=age;
    this.job=job;
    this.sayName=new Function("alert(this.name);");//與聲明函數在邏輯上是等價的
}

alert(person1.sayName == person2.sayName);//false

解決方法

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

function sayName(){
    alert(this.name);
}

新問題:在全局作用域中定義的函數實際上只能被某個對象調用,如果對象需要定義很多方法,就需要定義很多全局函數,不合理。

6.2.3 原型模式

每個函數都有一個prototype(原型)屬性,是一個指針,指向一個對象,用途是包含可以由特定類型的所有實例共享的屬性和方法。好處是讓所有對象實例共享原型對象所包含的屬性和方法。

function Person(){
}
Person.prototype.name="a";
Person.prototype.sayName=function(){
    alert(this.name);
}
var person1=new Person();
person1.sayName();
var person2=new Person();
person2.sayName();
alert(person1.name == person2.name);//true

1、理解原型對象

默認情況,所有原型對象都會自動獲得一個constructor(構造函數)屬性,指向prototype屬性所在函數的指針。

//isPrototypeOf()
alert(Person.prototype.isPrototypeOf(person1));//true

//getPrototypeOf()
alert(Object.getPrototypeOf(person1) == Person.prototype);//true

//先查找實例是否有name屬性,然後查找原型,即使實例屬性設置爲null,也不會恢復指向原型屬性,只能通過delete刪除實例屬性
alert(person1.name);

//hasOwnProperty()檢查是否有實例屬性
person1.name="a";
alert(person1.hasOwnProperty("name"));//true

delete person1.name;
alert(person1.hasOwnProperty("name"));//false

2、原型與in操作符

in操作符會在通過對象能夠訪問給定屬性時返回true,無論屬性在實例還是原型中。

//檢查原型是否有該屬性
function hasPrototypeProperty(object,name){
    return !object.hasOwnProperty(name) && (name in object);
}

//獲取對象上所有可枚舉實例屬性
var keys = Object.keys(Person.prototype);
alert(keys);//"name,obj,job,sayName"

//獲取對象上所有實例屬性,無論是否可枚舉
var keys = Object.getOwnPropertyNames(Person.prototype);
alert(keys);//"constructor,name,obj,job,sayName"

3、更簡單原型語法

function Person(){
}
//這種語法本質上重寫了默認的prototype對象,所以constructor屬性不再指向Person
Person.prototype = {
    //可手動添加constructor:Person,
    name:"a",
    sayName:function(){
        alert(this.name);
    }
};
var person1=new Person();
alert(person1 instanceof Person);//true
alert(person1.constructor == Person);//false

4、原型的動態性

//對原型對象所做的任何修改能夠立即從實例上反應出來,即使是先創建實例後修改原型
var p=new Person();
Person.prototype.sayHi=function(){
    alert("hi");
}
p.sayHi();//沒問題

重寫修改了構造函數的原型屬性的指針指向的對象

//但是重寫就不行
var p=new Person();
Person.prototype={
    constructor:Person,
    name:"a",
    sayName:function(){
        alert(this.name);
    }
};

p.sayName();//error

5、原生對象的原型

如Object,Array,String等的方法都是在其構造函數的原型上定義方法。

6、原型對象的問題

對象的屬性一般定義在構造函數,因爲定義在原型上會被共享。

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

//構造函數模式用於定義實例屬性,原型模式用於定義方法和共享屬性。使用最廣泛
function Person(name,age,job){
    this.name=name;
    this.age=age;
    this.job=job;
    this.friends=["a","b"];
}

Person.prototype={
    constructor:Person,
    sayName:function(){
        alert(this.name);
    }
}

6.2.5 動態原型模式

有其他OO語言經驗的開發人員看到獨立的構造函數和原型時,可能會非常困惑。動態原型模式正是致力於解決這個問題,即把所有信息封裝在構造函數中。

function Person(name,age,job){
    this.name=name;
    this.age=age;
    this.job=job;
    this.friends=["a","b"];

    //初次調用構造函數時纔會執行
    if(typeof this.sayName != "function"){
        Person.prototype.sayName=function(){
            alert(this.name);
        };
    }
}

6.2.6 寄生構造函數模式

//僅僅封裝創建對象的代碼,然後返回新對象
function SpecialArray(){
    var values = new Array();
    values.push.apply(values,arguments);
    //不修改Array構造函數,增加額外方法,但是會每創建一次,就創建一個函數對象,浪費內存?
    values.toPipedString = function(){
        return this.join("|");
    };
    //構造函數在不返回值的情況下,默認會返回新對象實例,而通過在構造函數的末尾添加一個return語句,可以重寫調用構造函數時返回的值
    return values;
}

var colors = new SpecialArray("reb","green","blue");
var colors2 = new SpecialArray("reb","green","blue");

alert(colors.toPipedString());
alert(colors instanceof SpecialArray);//false
alert(colors instanceof Array);//true
alert(colors instanceof Object);//true
alert(colors.toPipedString ==  colors2.toPipedString);//false

此模式下,返回的對象與構造函數或與構造函數原型屬性之間沒有關係,也就是說和在構造函數外部創建的對象沒什麼不同,不能使用instanceof確定對象類型,可以用其他模式就不要使用這種模式。

6.2.7 穩妥構造函數模式

穩妥對象指的是沒有公共屬性,其方法也不引用this對象,也不使用new操作符調用構造函數。
參考鏈接:https://www.zhihu.com/question/25101735/answer/36695742

function Person(name,age,job){
    var o = new Object();

    //可以在這裏定義私有變量和函數
    //凡是想設爲 private 的成員都不要掛到 Person 返回的對象 o 的屬性上面,掛上了就是 public 的了。當然,這裏的 private 和 public 都是從形式上類比其他 OO 語言來說的,其實現原理還是 js 中作用域、閉包和對象那一套。感覺實現得挺巧妙的。
    var name2=name;

    o.sayName=function(){
        alert(name);
    };

    o.sayName2=function(){
        alert(name2);
    };

    return o;
}

var friend = Person("Nicholas",29,"Software Engineer");
friend.sayName();//"Nicholas"
friend.sayName2();//"Nicholas"
alert(friend.name2);//undefined

這樣保存的是一個穩妥對象,除了調用sayName()方法外,沒有別的方式可以訪問其數據成員。即使有其他代碼會給這個對象添加方法或數據成員,但也不可能有別的辦法訪問傳入到構造函數中的原始數據。

6.3 繼承

ECMAScript中只支持實現繼承,而且其實現繼承主要是依靠原型鏈來實現的。

6.3.1 原型鏈

實現本質是重寫原型對象,代之以一個新類型的實例。

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

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

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

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

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

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

通過實現原型鏈,本質上擴展了原型搜索機制。
搜索屬性時:
1,搜索實例
2,搜索SubType.prototype
3,搜索SuperType.prototype
4、搜索Object.prototype
直到找到爲止,在找不到屬性或方法時,搜索過程是要一環一環地前行到原型鏈末端纔會停下來。

1、別忘記默認的原型

所有引用類型默認都繼承了Object,而這個繼承也是通過原型鏈實現的。
所有函數的默認原型都是Object的實例,因此默認原型都會包含一個內部指針,指向Object.prototype。這是所有自定義類型都會繼承toString等默認方法的根本原因。

2、確定原型和實例的關係

alert(instance instanceof Object);//true
alert(instance instanceof SuperType);//true
alert(instance instanceof SubType);//true

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

只要原型鏈中出現過的原型,都可以說是該原型鏈所派生的實例的原型。

3、謹慎地定義方法

給原型添加方法的代碼一定要放在替換原型的語句之後。

4、原型鏈的問題

1、在通過原型來實現繼承時,原型實際上會變成另一個類型的實例。原先的實例屬性變成現在的原型屬性。如果是包含引用類型值的原型,一個實例修改會影響另外一個實例。
2、在創建子類型的實例時,不能向超類型的構造函數中傳遞參數。
因此,實踐中很少會單獨使用原型鏈。

6.3.2 借用構造函數

有時也叫僞造對象或經典繼承

function SuperType(){
    this.color = ["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"

1、傳遞參數

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

function SubType(){
    SuperType.call(this,"a");
    this.age=29;
}

var instance = new SubType();
alert(instance.name);
alert(instance.age);
console.log(instance);

在SubType構造函數內部調用SuperType構造函數時,實際上是爲SubType的實例設置了name屬性。爲了確保SuperType構造函數不會重寫子類型的屬性,可以在調用超類型構造函數後,再添加應該在子類型中定義的屬性。

2、借用構造函數的問題

如果僅僅是借用構造函數,那麼也將無法避免構造函數模式存在的問題——方法都在構造函數中定義,因此函數複用就無從談起。而且在超類型的原型中定義的方法,對子類型而言也是不可見的。因此很少單獨使用。

6.3.3 組合繼承

將原型鏈和借用構造函數的技術組合在一塊,其背後思路是使用原型鏈實現對原型屬性和方法的繼承,而通過借用構造函數來實現對實例屬性的繼承。

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

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);
}

組合繼承避免了原型鏈和借用構造函數的缺陷,融合了它們的優點,成爲javascript中最常用的繼承模式。instanceof和isPrototypeOf()也能夠識別基於組合繼承創建的對象。

6.3.4 原型式繼承

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

本質上講,object()對傳入其中的對象執行了一次淺複製。
ECMAScript5通過新增Object.create()方法規範化了原型式繼承。包含引用類型值的屬性始終都會共享相應的值,和原型鏈有同樣的問題。

6.3.5 寄生式繼承

寄生式繼承的思路與寄生構造函數和工廠模式類似。

function createAnother(original){
    var clone = object(original);//通過調用函數創建一個新對象
    clone.sayHi = function(){//以某種方式來增強這個對象
        alert("hi");
    };
    return clone;//返回這個對象
}

問題是不能做到函數複用而降低效率。

6.3.6 寄生組合式繼承

組合繼承是Javascript最常用的繼承模式,問題是無論什麼情況下,都會調用兩次超類型構造函數:一次是在創建子類型原型的時候,另一次是在子類型構造函數內部。子類型最終會包含超類型對象的全部實例屬性,但我們不得不在調用子類型構造函數時重寫這些屬性。

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);//第二次調用SuperType()

    this.age = age;
}

SubType.prototype = new SuperType();//第一次調用SuperType()
SubType.prototype.constructor = SubType;
SubType.prototype.sayAge = function(){
    alert(this.age);
}

最終,有兩組name和age屬性,一組在實例上,一組在Subtype原型中。解決方法是使用寄生組合式繼承。
寄生組合式繼承,即通過借用構造函數來繼承屬性,通過原型鏈的混成形式來繼承方法。其背後的基本思路是:不必爲了指定子類型的原型而調用超類型的構造函數,我們所需要的無非就是超類型原型的一個副本而已。本質上,就是使用寄生式繼承來繼承超類型的原型,然後再將結果指定給子類型的原型。

//兩個參數:子類型構造函數,父類型構造函數
function inheritPrototype(subType,superType){
    var prototype = object(superType.prototype);//創建對象
    prototype.constructor = subType;//增強對象,彌補重寫原型而失去的默認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);

    this.age = age;
}

inheritPrototype(SubType,SuperType);

SubType.prototype.sayAge = function(){
    alert(this.age);
}

高效率體現在它只調用了一次SuperType構造函數,並且因此避免了在SubType.prototype上面創建不必要的、多餘的屬性。與此同時,原型鏈還能保持不變。因此能正常使用instanceof和isPrototypeOf()。寄生組合式繼承是引用類型最理想的繼承範式。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章