讀《javaScript高級程序設計-第6章》之封裝類

第6章我一共寫了3篇總結,下面是相關連接:
讀《javaScript高級程序設計-第6章》之理解對象
讀《javaScript高級程序設計-第6章》之繼承java

工廠模式

所謂的工廠模式就是,把建立具體對象的過程抽象成了一個函數,每次調用這個函數都會返回一個類似的對象。chrome

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("Nicholas", 29, "Software Engineer");
var person2 = createPerson("Greg", 27, "Doctor");
person1.sayName();   //"Nicholas"
person2.sayName();   //"Greg"

工廠模式雖然解決了建立多個類似對象的問題,但卻沒有解決對象識別的問題(即怎樣知道一個對象的類型)。segmentfault

構造函數模式

js裏常常如此寫var obj=new Object();var arr=new Array();ObjectArray就是構造函數,使用new操做符能夠建立相應類型的對象,使用instanceof能夠驗證對象的類型,例如:
alert(arr instance Array);      //true
構造函數模式就是,自定義像ArrayObject等這樣的構造函數,並使用new操做符調用它來建立自定義類型對象的方法。
例如:數組

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

var person1 = new Person("Nicholas", 29, "Software Engineer");
var person2 = new Person("Greg", 27, "Doctor");

person1.sayName();   //"Nicholas"
person2.sayName();   //「Greg」
  • new操做符

使用new操做符調用,Person就是一個構造函數
要建立Person的新實例,必須使用new操做符。以這種方式調用構造函數實際上會經歷一下4個步驟:
    (1)建立一個新對象
    (2)將構造函數的做用域賦給新對象,即把構造函數的this指向這個新對象
    (3)執行構造函數中的代碼(爲這個新對象添加屬性)
    (4)返回新對象瀏覽器

若是不使用new,Person就是一個普通的函數,能夠正常調用。例如:函數

//做爲普通函數在全局做用域下調用
Person("Greg", 27, "Doctor");  //adds to window
window.sayName();   //「Greg"
//做爲普通函數在另外一個對象中調用
var o = new Object();
Person.call(o, "Kristen", 25, "Nurse");
o.sayName();    //"Kristen"
  • 檢測類型

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

綜上,建立自定義的構造函數,意味着未來能夠將它的實例標識爲一種特定的類型(相似於Array類型,Number類型);而這正是構造函數模式賽過工廠模式的地方。可是構造函數模式也存在缺點。spa

  • 構造函數模式的問題

使用構造函數的主要問題就是,每一個方法都要在每一個實例上從新建立一遍(實例化一次Function對象),浪費內存。例如,person1和person2都有一個sayName()的方法,但建立person1和person2時候,定義sayName這個方法時都實例化了一個函數對象,所以person1.sayName和person2.sayName是不相等的,而事實上它們又是作的一樣的事情。或者也能夠這麼說,person1和person2的sayName()方法作一樣的事情,但卻在建立對象時被實例化了兩次,也就佔用了兩倍內存。
雖然能夠解決,但並不完美,例如:firefox

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

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

var person1 = new Person("Nicholas", 29, "Software Engineer");
var person2 = new Person("Greg", 27, "Doctor」);
alert(person1.sayName == person2.sayName);  //true 

可是若是共享方法有不少,就須要定義不少個全局函數,那麼咱們的自定義的引用類型就絲毫沒有封裝性可言了。好在,這些問題能夠經過使用原型模式解決。prototype

原型模式

(1)理解原型對象

不管何時,只要建立了一個新函數,就會根據一組特定的規則爲該函數建立一個prototype屬性,這個屬性就是該函數的原型對象。每一個函數都有一個原型對象,全部原型對象都會自動得到constructor屬性,constructor指向該函數(擁有該prototype屬性的函數)。
例如,Person.prototype.constructor指向Person
建立構造函數後,其原型對象默認只會取得constructor屬性;至於其餘的方法都是從Object繼承來的(__proto__)。當調用構造函數建立一個新實例後,該實例內部將包含一個指針(__proto__),指向構造函數的原型對象。(ECMA-262第5版中管這個指針叫[[Prototype]],但在腳本中沒有標準的方式訪問它。在chrome,safari和firefox中都支持一個屬性__proto__,但在其餘實現中__proto__對腳本是不可見的)。因此和實例有直接關係的是構造函數的原型對象,而不是構造函數。

上圖展現了Person構造函數、Person的原型對象和Person現有的兩個實例之間的關係。

(2)實例屬性和原型屬性:

原型屬性即構造函數的原型對象的屬性;實例屬性即在實例對象上直接添加的屬性。
例如:person1.name=「Jone」。
經過點運算符能夠訪問到實例的實例屬性和原型屬性。實例訪問屬性時,腳本會先搜索實例屬性,若是找到了,則中止搜索返回實例屬性的值;若是沒找到就繼續搜索原型屬性。因此若是實例屬性和原型屬性同名,那麼原型屬性就會被屏蔽掉,沒法訪問到。
須要注意的是:實例沒法修改他的原型屬性的值,也沒法修改原型對象(即不能修改、刪除和增長一個原型屬性)
(注意:實例不能修改的是原型屬性的值,可是若是原型屬性指向一個引用類型,原型屬性的值是存儲這個引用類型的地址,即不能修改原型屬性指向另外一個對象,但卻能修改原型屬性指向的對象裏的屬性。下面原型對象的問題裏還會再講到)
若是person1.name=「Jone」這樣寫,腳本只會在實例屬性裏建立或修改一個name=「Jone」的屬性,delete person1.name 只會刪除person1的實例屬性name(就算實例沒有name的實例屬性,也不會刪除實例的原型屬性)。

(3)和原型對象有關的幾個方法

  • isPrototypeOf()

alert(Person.prototype.isPrototypeOf(person1));    //true
若是person1[[prototype]]  (即__proto__)指向調用isPrototypeOf的對象即Person.prototype就會返回true
即判斷Person.prototype是不是person1[[prototype]]

  • Object.getPrototypeOf()

alert(Object.getPrototypeOf(person1)==Person.prototype);   //true
返回person1這個對象的原型[[prototype]]

  • hasOwnProperty()

person1.hasOwnProperty(「name」);     若是person1.name是來自於person1的實例屬性,返回true;若是來自於person1的原型屬性,則返回false

(4)原型與in操做符

有兩種方式使用in操做符:
單獨使用in:alert(「name」 in person1);   //true
在經過person1可以訪問給定屬性是返回true,不管屬性是實例屬性仍是原型屬性。
在for-in循環中使用:返回的是全部可以經過對象訪問的、可枚舉的屬性,其中包括實例屬性也包括原型屬性。

  • Object.keys()

接受一個對象做爲參數,返回一個包含對象的全部可枚舉屬性的字符串數組。
若是對象是一個實例,則只返回實例的實例屬性而不包含原型屬性

  • Object.getOwnPropertyNames()
 var keys = Object.getOwnPropertyNames(Person.prototype);
 alert(keys);   //"constructor,name,age,job,sayName」

獲得對象的全部實例屬性,不管它是否可枚舉
  

(5)更簡單的原型語法

所謂的更簡單的原型寫法就是用字面量的形式來定義構造函數的原型對象,以下:

function Person(){
}

Person.prototype = {
    name : "Nicholas",
    age : 29,
    job: "Software Engineer",
    sayName : function () {
        alert(this.name);
    }

};

var friend = new Person();

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

這樣定義完了以後,Person.prototype這個對象就被重寫了,致使它的constructor這個屬性的指向變成了Object,而不是Person
(解釋:Person.prototypeObject的一個實例,因此它有一個原型屬性constructor指向ObjectPerson被建立時,它的原型對象Person.prototype自動得到了一個constructor的屬性,指向Person,這個屬性是對象的實例的實例屬性,因此會屏蔽掉對象的原型屬性,因此說Person.prototype.constructor是指向Person的。可是用字面量重寫了Person.prototype後,Person.prototype還是Object的一個實例,因此它有一個原型屬性constructor指向Object,但它沒有了指向Person的實例屬性constructor,因此在訪問Person.prototype.constructor時,就是訪問了Person.prototype對象的原型屬性,指向了Object)。
但咱們能夠再把它定義進這個對象字面量裏手動指向Person,即給Person.prototype這個對象的實例加一個實例屬性constructor,指向Person。以下:

function Person(){
}

Person.prototype = {
    constructor: Person,
    name : "Nicholas",
    age : 29,
    job: "Software Engineer",
    sayName : function () {
        alert(this.name);
    }
};

咱們知道如此定義對象,對象的屬性的[[enumerable]]特性默認是true。而默認狀況下,原聲的原型對象的constructor屬性是不可枚舉的,所以若是你使用兼容ES5的javaScript引擎,可使用Object.defineProperty()來設置constructor屬性。以下:

//重設構造函數,只適用於ES5兼容的瀏覽器
Object.difineProperty(Person.prototype,」constructor」,{
    enumerable:false,
    value:Person
});

(6)原型的動態性

簡單點來講,就是實例的[[prototype]]是指向構造函數的原型對象,而不是構造函數。只要你明白這一點,原型的動態性就好理解了。
第一種狀況:Person.prototype能夠在任意地方增長修改或刪除屬性,實例能夠實時的訪問最新的原型屬性。由於每次實例訪問屬性,都是一次搜索的過程,搜索原型屬性時是到實例的[[prototype]]指向的對象裏查找。實例的[[prototype]]是一個指針,Person.prototype也是一個指針,指向的是同一個地址,也就是說修改和查找都在同一個地方,那麼查找到的值天然就是最新實時的了。

function Person(){
}
var friend = new Person();
Person.prototype.sayHi = function(){
    alert("hi");
};
friend.sayHi();   //"hi"

第二種狀況:在實例被建立以後,Person.prototype被重寫了

function Person(){
}

var friend = new Person();
       
Person.prototype = {
    constructor: Person,
    name : "Nicholas",
    age : 29,
    job : "Software Engineer",
    sayName : function () {
        alert(this.name);
    }
};
friend.sayName();   //error

這種狀況是由於:實例一旦被建立,實例的[[prototype]]存儲的地址就肯定了,指向的對象地址就肯定了,若是你改變這個地址裏的對象,實例均可以訪問的到。可是若是在實例被建立以後,重寫Person.prototype,就至關因而把Person.prototype指向了一個新的對象,而實例的[[prototype]]仍是指向原來的對象,因此實例訪問的原型屬性仍是要在原來的對象裏查找,原來的對象裏並無sayName這個方法,所以會報錯。

(7)原生對象的原型

咱們用原型模式建立自定義類型,讓自定義類型和原生類型同樣使用。其實全部的原生的對象(Object、Array、String,等等)也是採用的原型模式建立的。全部原生的引用類型都在其構造函數的原型上定義了方法。
例如,在Array.prototype中能夠找到sort()方法,而在String.prototype中能夠找到substring()方法。
經過原生對象的原型,不只能夠取得全部默認方法的引用,也能夠定義新的方法。能夠像修改自定義對象的原型同樣修改原生對象的原型,所以能夠隨時添加方法。可是不建議如此作(在支持該方法的實現中運行代碼時會致使命名衝突,或者意外重寫了原生方法)

(8)原型對象的問題

**首先,原型模式省略了爲構造函數傳遞參數,初始化實例的環節,使得全部實例默認時都是同樣的。
其次,原型模式的共享本性使得全部的實例都能共享它的屬性。**
若是屬性值是函數或者是基本值時,實例不能修改原型屬性的值,只會爲該實例增長一個同名屬性,而後屏蔽掉同名原型屬性,這樣其它的實例都不會受到影響,使用的仍然是原型屬性原來的值。
若是屬性值是引用類型,實例雖不能修改原型屬性的值(這個值就是指向的對象的地址),即實例不能讓這個原型屬性從新指向另外一個對象,可是卻能夠修改指向的對象的屬性,這就會致使其它實例再訪問這個對象時,對象已被修改了。
例如:

function Person(){
}

Person.prototype = {
    constructor: Person,
    name : "Nicholas",
    age : 29,
    job : "Software Engineer",
    friends : ["Shelby", "Court"],
    sayName : function () {
        alert(this.name);
    }
};

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

person1.friends.push("Van");

alert(person1.friends);    //"Shelby,Court,Van"
alert(person2.friends);    //"Shelby,Court,Van"
alert(person1.friends === person2.friends);  //true

這樣就違反了咱們但願實例擁有屬於本身的所有屬性的初衷

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

綜合前面所說的,咱們發現構造函數模式優勢在於能向構造函數傳遞,定義屬於實例本身的實例屬性。原型模式優勢在於共享着對方法的引用,原型屬性是全部實例所共享的。
因此建立自定義類型的最多見方式,就是組合使用構造函數模式與原型模式
例如:

function Person(name, age, job){
    this.name = name;
    this.age = age;
    this.job = job;
    this.friends = ["Shelby", "Court"];
}

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

var person1 = new Person("Nicholas", 29, "Software Engineer");
var person2 = new Person("Greg", 27, "Doctor");

person1.friends.push("Van");

alert(person1.friends);    //"Shelby,Court,Van"
alert(person2.friends);    //"Shelby,Court"
alert(person1.friends === person2.friends);  //false
alert(person1.sayName === person2.sayName);  //true

動態原型模式

這一小節,私覺得了解了解就好,只要你理解了上面所說的構造函數模式和原型模式的原理,那麼原型屬性的定義你能夠爲所欲爲,只要符合你的預期就好。你高興就好,代碼高興就好。

寄生構造函數模式

與工廠模式的區別是使用new 調用。不使用new調用,它就是工廠模式。
這一小節,私覺得了解了解就好。

穩妥構造函數模式

與工廠模式的區別是對象定義的方法不使用this,構造函數傳進來的參數不向外直接暴露。
這一小節,私覺得了解了解就好。

好了,封裝類的幾種方式已經介紹完了。個人觀點是理解了對象和構造函數模式以及原型模式,就能夠隨機應變了。不須要記住什麼什麼各類模式的,無非就是使用對象的場景不一樣。要理解對象和構造函數以及原型對象,靈活變換,無招勝有招纔好。

這是我讀《javaScript高級程序設計》這本書的第6章面向對象的程序設計,作的筆記,在本篇以前還有一篇理解對象的筆記,後面還有一篇繼承的筆記。發現問題的小夥伴歡迎指出。

相關文章
相關標籤/搜索