原型模式
原型模式
我們創建的每個函式都有一個 prototype(原型) 屬性,這個屬性是一個指標,指向一個物件,而這個物件的用途是包含可以有特定型別的所有實體共享的屬性和方法,如果按照字面意思來理解,那么 prototype 就是通過呼叫建構式而創建的那個物件實體的原型物件,使用原型物件的好處是可以讓所有物件實體共享它所包含的屬性和方法,換句話說,不必在建構式中定義物件實體的資訊,而是可以將這些資訊直接添加到原型物件中,如下面的例子所示:
function Person() {}
Person.prototype.name = '小明'
Person.prototype.age = 22
Person.prototype.sex = '男'
Person.prototype.sleep = function() { alert(this.name + '睡覺了') }
var person1 = new Person()
person1.sleep() // '小明睡覺了'
var person2 = new Person()
person2.sleep() // '小明睡覺了'
console.log(person1.sleep === person2.sleep) // true
在這里,我們將 sleep() 方法和所有屬性直接添加到了 Person 的 prototype 屬性中,建構式變成了空函式,即使如此,也仍可以通過建構式來創建新物件,而且新物件還具有相同的屬性和方法,但和建構式模式不同的是,新物件的這些屬性和方法是由所有實體共享的,換句話說, person1 person2 訪問的是同一組屬性和同一個 sleep() 函式,要理解原型模式的作業原理,必須先理解 ES5 中原型物件的性質,
理解原型物件
? 無論什么時候,只要創建了一個新函式,就會根據一組特定的規則為該函式創建一個 prototype 屬性,這個屬性指向函式的原型物件,在默認情況下,所有原型物件都會自動獲得一個 constructor (建構式)屬性,這個屬性包含一個指向 prototype 屬性所在函式的指標,就拿前面的例子來說, Person.prototype.constructor 指向 Person ,而通過這個建構式,我們還可繼續為原型物件添加其他屬性和方法,
? 創建了自定義的建構式之后,其原型物件默認只會取得 constructor 屬性;至于其他方法,則都是從 Object 繼承而來的,當呼叫建構式創建一個新實體后,該實體的內部將包含一個指標(內部屬性),指向建構式的原型物件,ECMA-262第5版中管這個指標叫 [[Prototype]],雖然在腳本中沒有標準的訪問方式訪問 [[Prototype]] ,但是在 Firefox、 Safari 和 Chrome 在每個物件上都支持一個屬性 __proto__;而在其它實作中,這個屬性對腳本則是完全不可見的,不過要明確的真正重要的一點就是,在這個連接存在于實體與建構式的原型物件之間,而不是存在于實體與建構式之間,
? 以前面使用 Person 建構式和 Person.prototype 創建實體的代碼為例,下圖展示了各個物件之間的關系,

? 上圖展示了 Person 建構式、Person 的原型屬性以及 Person 現有的兩個實體之間的關系,在此,Person.prototype 指向了原型物件,而 Person.prototype.constructor 又指回了 Person,原型物件中除了包含 constructor 屬性之外,還包括后來添加的其他屬性, Person的每個實體—— person1 和 person2 都包含了一個內部屬性,該屬性僅僅指向了 Person.prototype,換句話說,它們與建構式沒有直接的關系,此外,要格外注意的是,雖然這兩個實體都不包含屬性和方法,但我們卻可以呼叫 person1.sleep(),這是通過查找物件屬性的程序來實作的,
? 雖然在所有實作中都無法訪問到 [[Prototype]],但可以通過 isPrototypeOf() 方法來確定物件之間是否存在這種關系,從本質上講,如果 [[Prototype]] 指向呼叫 isPrototype() 方法的物件(Person.prototype),那么這個方法就回傳 true:
console.log(Person.prototype.isPrototypeOf(person1)) // true
console.log(Person.prototype.isPrototypeOf(person2)) // true
? 這里,用原型物件的 isPrototype() 方法測驗了 person1 和 person2,因為它們內部都有一個指向 Person.prototype 的指標,因此都回傳了 true,
ES55 增加了一個新方法,叫 Object.getPrototypeOf(),在所有支持的實作中,這個方法回傳 [[Prototype]] 的值,例如:
console.log(Object.getPrototype(person1) === Person.prototype) // true
console.log(Object.getPrototype(person1).name) // 小明
? 這里的第一行代碼只是確定 Object.getProtitypeOf() 回傳的物件實際就是這個物件的原型,這第二行代碼取得了原型物件中的 name 屬性的值,也就是 ‘小明’ ,使用 Object.getPrototypeOf() 可以方便地取得一個物件得原型,而這在利用原型實作繼承得情況下是非常重要的,
? 每當代碼讀取某個物件得某個屬性時,都會執行一次搜索,目標是具有給定名字得屬性,搜索首先從物件實體本身開始,如果在實體中找到了具有給定名字得屬性,則回傳該屬性得值;如果沒有找到,則繼續搜索指標指向得原型物件,在原型物件中查找具有給定名字得屬性,如果在原型物件中找到了這個屬性,則回傳該屬性得值,也就是說,在我們呼叫 person1.name 的時候,會先后執行兩次搜索,
person1.name
這句代碼的內部執行程序如下:
graph TD START([開始])-->A[搜索實體 person1 的屬性] A-->B{找到name屬性}-->|是|C[回傳name屬性值]-->END([結束]) B-->|否|E[搜索實體 person1 的原型屬性] E-->F{找到name屬性}-->|是|C F-->|否|G[回傳undefined]-->END首先,決議器會尋找實體 person1 的屬性 name
找到了,它就回傳 name 對應的屬性值
沒找到,它就會執行第二次搜索,搜索實體 person1 的內部屬性 [[Prototype]] 指向的原型物件是否存在 name 屬性
找到了,回傳 name 對應的屬性值
沒找到,回傳 undefined,
如果實體 person1 存在 name 屬性且它內部 [[Prototype]] 指向的原型物件同樣存在 name 屬性,它會先尋找實體本身的屬性 name,找到了后就不會再次執行第二次對原型物件搜索了,即若實體本身存在我們給定的屬性名,就算它指向的原型物件存在該屬性名,也只會回傳實體本身的屬性值,代碼如下:
function Person() {}
Person.prototype.name = '小明'
var person1 = new Person()
person1.name = '小紅'
console.log(person1.name) // 小紅
var perspn2 = new Person()
console.log(person2.name) // 小明
? 雖然可以通過物件實體訪問保存在原型中的值,但卻不能通過物件實體重寫原型中的值,如果我們在實體中添加了一個屬性,而該屬性與實體原型中的一個屬性同名,那我們就在實體中創建該屬性,該屬性將會屏蔽原型中的那個屬性,
? 不過,使用 delete 運算子則可以完全洗掉實體屬性,從而讓我們重新訪問原型中的屬性,如下所示:
function() {}
Person.prototype.name = '小明'
var person1 = new Person()
var person2 = new Person()
person1.name = '小紅'
console.log(person1.name) // 小紅 來自實體本身的 name 屬性
console.log(person2.name) // 小明 來自原型物件的 name 屬性
delete person1.name // 移除了實體 person1 的屬性 name
console.log(person1.name) // 小明 來自實體本身的 name 屬性
? 在這個修改的例子中,使用了 delete 運算子 移除了 person1.name,之前它保存的實體 name 屬性 屏蔽了同名的原型屬性,把它移除后,就恢復了對原型屬性 name 的連接,因此,接下來再呼叫 person1.name 時,回傳的就是原型中的 name 的值了,
? 使用 hasOwnProperty() 方法可以檢測一個屬性是存在于實體中,還是存在于原型中,這個方法(從Object繼承而來的)只在給定屬性存在于物件實體中時,才會回傳 true,
function Person() {}
Person.prototype.name = '小明'
var person1 = new Person()
var person2 = new Person()
person2.name = '小紅'
console.log(person1.hasOwnProperty('name')) // false 非實體本身的屬性
console.log(person2.hasOwnProperty('name')) // true 實體本身的屬性
delete person2.name // 移除實體 person2 的屬性 name
console.log(person2.hasOwnProperty('name')) // false 非實體本身的屬性
? 通過使用 hasOwnProperty() 方法,訪問的是實體屬性還是原型物件的屬性就一清二楚了,
原型與 in 運算子
? 有兩種方式使用 in 運算子:單獨使用和在 for-in 回圈中使用,在單獨使用時,in 運算子會在通過物件能夠訪問給定屬性時回傳 true,無論該屬性存在于實體中還是原型中,
function Person() {}
Person.prototype.name = '小明'
var person1 = new Person()
var person2 = new Person()
person2.name = '小紅'
console.log(person1.hasOwnProperty('name')) // false name 屬性來自原型物件
console.log(person2.hasOwnProperty('name')) // true name 屬性來自實體本身
console.log('name' in person1) // true person1 實體本身 或者 原型物件有 name 屬性
console.log('name' in person2) // true person2 實體本身 或者 原型物件有 name 屬性
delete person2.name // 移除了實體 person2 的屬性 name
console.log('name' in person2) // true person2 實體本事的屬性被移除,但是原型物件仍舊有屬性 name
? 在以上代碼執行的整個程序中, name 屬性要么在實體本身訪問到的,要么在原型物件上訪問到的,無論該屬性存在于實體中還是原型物件中,使用 ’name‘ in person2 始終都會回傳 true,
? 同使使用 hasOwnProperty() 方法和 in 運算子,就可以確定該屬性到底時存在于物件中,還是存在于原型中:
function hasPrototypeOrProperty(object, propertyName){
// 判斷該物件以及它的原型物件是否包含該屬性
if(propertyName in object){
// 若存在則判斷它是否為實體本身屬性
if(object.hasOwnProperty(propertyName)){
// 若為實體屬性則:
console.log(propertyName + '是' + '實體本身的屬性')
// 否則:
}else {
console.log(propertyName + '是' + '原型物件的屬性')
}
// 若該物件不存在該屬性
}else {
console.log('該物件不存在屬性' + propertyName)
}
}
function Person(){}
Person.prototype.name = '小明'
var person1 = new Person()
var person2 = new Person()
person2.name = '小紅'
hasPrototypeOrProperty(person1, 'name') // name是原型物件的屬性
hasPrototypeOrProperty(person2, 'name') // name是實體本身的屬性
hasPrototypeOrProperty(person1, 'abcd') // 該物件不存在屬性abcd
? 在使用 for-in 回圈時,回傳的是所有能夠通過物件訪問的、可列舉的屬性,其中既包括存在于實體中的屬性,也包括存在于原型中的屬性,屏蔽了原型中不可列舉的屬性(即將[[Enumerable]] 標記為 false 的屬性)的實體屬性也會在 for-in 回圈中回傳,因為根據規定,所有開發人員定義的屬性都是可被列舉的——只有在 IE8 以及更早的版本中例外,
? IE 早期版本的實作中存在一個 bug,即屏蔽不可列舉的屬性的實體依舊不會在 for-in 回圈中,例如:
var obj = {
toString: function() {}
}
for(var prop in obj){
if(prop === 'toString') {
console.log('找到了toString') // 在IE中不會顯示
}
}
? 當以上代碼運行時,應該在控制臺中輸出 ‘hello’ 的字串,實體中的屬性 toString 屏蔽了原型中(不可被列舉)的 toString 屬性,在 IE 中,由于其實作認為原型的 toString 屬性被打上了 [[Enumerable]] 的標記,因此跳過了該屬性,該 bug 會影響默認不可被列舉的所有屬性和方法,包括:hasOwnProperty(),hasOwnProperty(),propertyIsEnumerable(),toLocaleString(),toString(),valueOf(),ES55 也將 constructor 和 prototype 屬性的 [[Enumerble]] 特性設定為 false,但并不是所有瀏覽器都照此實作,
? 要取得物件上所有可列舉的實體屬性,可以使用 ES55 的 Object.keys() 方法,這個方法接收一個物件作為引數,回傳一個包含所有可列舉的字串陣列,例如:
function Person() {}
Person.prototype.name = '小明'
Person.prototype.age = 22
Person.prototype.sex = '男'
var person = new Person()
console.log(Object.keys(Person.prototype)) // ["name", "age", "sex"]
// 并不是所有的瀏覽器都能訪問到 __proto__ 屬性
console.log(Object.keys(person.__proto__)) // ["name", "age", "sex"]
person.name = '小紅'
person.age = 22
console.log(Object.keys(person)) // ["name", "age"]
? 如果想要得到所有實體屬性,無論它是否可列舉,都可以使用 Object.getOwnPropertyNames() 方法?,以下方法中回傳的陣列包含了不可被列舉的屬性 constructor,
var keys = Object.getOwnPropertyNames(Persone.prototype)
console.log(keys) // ["constructor", "name", "age", "sex"]
更簡單的原型語法
? 前面的例子每添加一個屬性和方法就要敲一遍 Person.prototype,為了減少不必要的輸入,也為了從視覺上更好地封裝原型的功能,更常見的做法時用一個包含所有屬性和方法的物件字面量來重寫整個原型物件,如下面所示:
function Person(){}
Person.prototype = {
name: '小明',
age: 22,
sex: '男',
sleep: function() { alert(this.name + '睡覺了') }
}
? 在上面的代碼中,將 Person.protoyepe 設定成一個新的物件,但是這種方式生成的原型物件中的屬性 constructor 屬性將不再指向 Person 了,每一次創建一個函式時,就會同使創建它的 prototype 物件,該物件也會自動獲得 constructor 屬性,這個屬性指向了新創建的那個函式,在這里也就時指向了 Person,當我們用字面量的方式,其實就是完全覆寫掉了原來的 prototype 物件,因此 constructor 屬性也就變成了新物件的 constructor 屬性(指向了 Object 建構式),不再指向 Person 函式,此時,盡管 instanceof 運算子還能回傳正確的結果,但是通過 constructor 已經無法確定物件的型別了,如下所示:
var person = new Person()
console.log(person instanceof Object) // true
console.log(person instanceof Person) // true
console.log(person.constructor === Person) // false
console.log(person.constructor === Object) // true
? 在此,用 instanceof 運算子測驗了 Object 和 Person 仍然回傳 true,但是 constructor 屬性則等于 Object 而不等于 Person 了,為了解決這種問題,可以在字面量里面定義它的屬性 constructor 的指向:
function Person() {}
Person.prototype = {
constructor: Person,
name: '小明',
age: 22,
sex: '男',
sleep: function() { alert(this.name + '睡覺了') }
}
? 以上代碼特意包含了一個 constructor 屬性,并將它的值設定為 Person,從而確保了通過該屬性能夠訪問到正確的值,
? 注意,以這種方式重設 constructor 屬性會導致它的 [[Enumerable]] 特性被設定為 true,默認情況下,原生的 constructor 是不可被列舉的,因此如果兼容 ES5 的 JavaScript 引擎的話,可以這么做:
function Person() {}
Person.prototype = {
name: '小明',
age: 22,
sex: '男',
sleep: function() { alert(this.name + '睡覺了') }
}
Object.defineProperty(Person.prototype, 'constructor', {
enumerable: false,
value: Person
})
原型的動態性
? 由于在原型中查找值得程序是一次搜索,因此我們對原型物件所有的任何修改都能夠立即從實力上反映出來——即使是先創建了實體后修改原型也照樣如此:
var person = new Person()
Person.prototype.sleep = function() { alert('月亮不睡我不睡') }
person.sleep() // 月亮不睡我不睡
? 以上代碼先創建了 Person 的一個實體,并將其保存在 person 中,然后,下一條陳述句在 Person.prototype 中添加了一個方法 sleep(), 即使 **person **是在添加新方法之前創建的,但它仍然可以訪問這個新定義的方法,其原因可以歸結為實體與原型之間的松散連接關系,當我們呼叫 person.sleep() 時,首先會在實體中搜索名為 sleep 屬性,在沒有找到的情況下,會繼續搜索原型,因為實體與原型之間的連bi接不過只是一個指標,而非一個副本,因此就可以在原型找到新的 sleep 屬性并回傳保存在那里的函式,
? 盡管可以隨時為原型添加屬性和方法,并且修改能夠立即在所有物件實體中反映出來,但如果是重寫整個原型物件,那么情況就不一樣了,我們知道,呼叫建構式時會為實體添加一個指向最初原型的 [[Prototype]] 指標,而把原型修改為另外一個物件就等于切斷了建構式與最初原型之間的聯系,
function() {}
var person = new Person()
Person.prototype = {
constructor: Person,
name: '小明'
}
console.log(person.name) // undefined
? 在這個例子中,先創建了 Person 的一個實體,然后又重寫了其原型物件,然后再呼叫 person.name 時無法找到該屬性,因為 person 指向的原型中不包含以該名字命名的屬性,
重寫原型物件之前:

重寫原型物件之后:

? 從上圖可以看出,重寫原型物件切斷了現有原型與任何之前已經存在的物件實體之間的聯系,它指向仍舊是最初的原型物件,
原生物件的原型
? 原型模式的重要性不僅體現再創建自定義型別方面,就連所有原生的參考型別,都是采用這種模式創建的,所有原生參考型別(Object、Array、Srting等等) 都在其建構式的原型上定義了方法,例如,再 **Array.prototype **中可以找到 sort() 方法,而在 String.prototype 中可以找到 subtring() 方法,如下所示:
console.log(typeof Array.prototype.sort) // function
console.log(typeof String.prototype.substring) // function
? 通過原生物件的原型,不僅可以取得所有默認方法的參考,而且也可以定義新方法,可以修改自定義物件的原型一樣修改原生物件的原型,因此可以隨時添加方法,下面的代碼就給基本包裝型別 String 添加了一個 startWidth() 的方法,
String.prototype.startWidth = function(text) {
return this.indexOf(text) === 0
}
var msg = 'Hello world!'
console.log(msg.startWidth('Hello')) // true
? 這里新定義的 startWidth() 方法會在傳入的文本位于一個字串開始時回傳 true,既然方法被添加給了 String.prototype,那么當前環境中的所有字串都可以呼叫它,由于 msg 是字串,而且后臺會呼叫 String 基本包裝函式創建這個字串,因此通過 **msg **就可以呼叫 startWIdth() 方法,
? 盡管可以這樣做,但并不推薦在產品化的程式中修改原生物件的原型,如果因某個實作中缺少某個方法,就在原生物件的原型中添加這個方法,那么當在另一個支持該方法的實作中運行代碼時,就可能導致命名沖突,而且,這樣做也可能意外地重寫原生方法,
原型物件的問題
? 原型物件也不是沒有缺點,首先,它省略了建構式船體初始化引數這一環節,結果所有實體在默認情況下都將取得相同的屬性值,雖然這會在某種程度上帶來一些不方便,但還不是原型的最大的問題,原型模式的最大問題時由其共享的本性所導致的,
? 原型中所有屬性是被很多實體共享的,這種共享對于函式非常合適,對于那些包含基本值得屬性倒也說得過去,畢竟(如前面得例子所示),通過在實體上添加一個同名屬性,可以隱藏原型中的對應屬性,然而,對于包含參考型別值的屬性來說,問題就比較突出了,
function Person(){}
Person.prototype = {
constructor: Person,
name: '小明',
age: 22,
friends: ['小紅', '小美'],
sleep: function() { alert(this.name + '睡覺了') }
}
var person1 = new Person()
var person2 = new Person()
person1.friends.push('小敏')
console.log(person1.friends) // ["小紅", "小美", "小敏"]
console.log(person2.friends) // ["小紅", "小美", "小敏"]
console.log(person1.friends === person2.friends) // true
? 在此,Person.prototype 物件有一個名為 friends 的屬性,該屬性包含了一個字串陣列,然后,創建了 Person 的兩個實體,接著,修改了 person1.friendsp 參考的陣列,想陣列中添加了一個字串,由于 friends 陣列存在于 Person.prototype 而非 person1中,所以剛剛提到的修改也會通過 person2.friends (與 person1.friends 指向同一個陣列)反映出來,加入我們的初衷就是像這樣在所有實體中共享一個陣列,那么沒什么問題,可是,實體一般都是要有屬于自己的全部屬性的,而這個問題正是很少看到有人單獨使用原型模式的原因所在,
轉載請註明出處,本文鏈接:https://www.uj5u.com/qiye/253893.html
標籤:JavaScript
上一篇:溫習資料演算法—js滑塊驗證碼
下一篇:建構式+原型模式
