這篇文章是基于 <<JavaScript 高級程序設(shè)計>>第六章面向?qū)ο蟮某绦蛟O(shè)計 6.2.3原型模式
我們每創(chuàng)建一個函數(shù), 都有一個 prototype
(原型)屬性, 這個屬性是一個指針, 指向一個對象,這個對象的 用途: 包含可以由指定類型的所有實例共享的屬性和方法
所以 prototype
就是通過調(diào)用構(gòu)造函數(shù)而創(chuàng)建的哪個對象實例的原型對象
function Person() {};
Person.prototype.name = 'TonNi';
Person.prototype.age = 19;
Person.prototype.sayName = function () {
alert ( this. name );
}
var person1 = new Person();
person1.sayName() // TonNi;
// prototype.isPrototypeOf 判斷 person1 實例的原型是不是 Person
console.log (Person.prototype.isPrototypeOf(person1)) // true
// Object.getPrototypeOf 該方法獲取一個對象的原型
console.log(Object.getPrototypeOf(person1) === Person.prototype) // true
console.log(Object.getPrototypeOf(person1).name) //TonNi
// hasOwnProperty 檢測一個實例屬性是不是存在與實例中 , 存在返回true
person1.sing = 'i can sing';
console.log(person1.hasOwnProperty('name')) // false
console.log(person1.hasOwnProperty('sing')) // true
1: 什么是實例, 什么是原型 ?
以上面的代碼為例,
原型: 就是 大寫的 Person 函數(shù)
實例: 就是以 Person為原型 從新 new 出來的person; 上例中的 person1就是實例
這里我們將
name, age, sayName()
等屬性和方法 直接添加到了Person
的prototype
中, 這樣所有 new 出來的實例 都擁有同樣的 屬性和方法, 可以被訪問或調(diào)用.var person2 = new Person()
person2 和 person1 一樣擁有同樣的屬性和方法
2: constructor
只要創(chuàng)建一個新的函數(shù), 就會根據(jù)特定規(guī)則為該函數(shù)創(chuàng)建一個 prototype
屬性, 此屬性指向函數(shù)的原型對象
而在默認情況下, 所有的原型對象都會自動獲得一個 constructor
(構(gòu)造函數(shù)) 屬性, 這個屬性是一個指向 prototype
屬性所在函數(shù)的指針.
可以看下圖, Person.prototype.constructor 指向的就是 Person
作用:
通過這個構(gòu)造函數(shù), 我們還可以繼續(xù)為原型對象添加其他屬性和方法
3: 如果實例中和原型都有同名的屬性或者方法, 調(diào)用哪個呢?
當代碼讀取某個屬性或者方法時, 都會經(jīng)行一次搜索, 搜索目標是給定的屬性或方法的名字. 搜索順序是, 先從實例查找 , 如果實例中有, 則返回屬性值或者調(diào)用方法, 如果沒有, 則搜索指針指向原型對象, 如果有同上, 如果沒有, 屬性就返回 undefind
, 方法則報錯, xxx is not a function
function Person() {};
Person.prototype.name = 'TonNi';
Person.prototype.age = 19;
Person.prototype.sayName = function () {
alert ( this. name );
}
var person1 = new Person();
console.log(person1.name) // 'TonNi' 實例中沒有則去原型中找, 找到后返回屬性值,
person1.name = 'Mike'
console.log(person1.name) // Mike 實例中有了name 就直接拿來用了
//在實例化一個person2
var person2 = new Person()
console.log(person2 .name) // 'TonNi' 實例中沒有則去原型中找, 找到后返回屬性值, 不會因為 person1添加了name收到影響
// 刪除person1中的name
delete person1.name;
console.log(person1.name) // 'TonNi' 刪除后 實例中沒有則去原型中找, 找到后返回屬性值,
故當我們?yōu)閷嵗砑右粋€屬性時,此屬性會屏蔽掉原型中對象中的同名屬性, 但是只會阻止我們訪問對象原型屬性, 并不會修改
4: 關(guān)于判斷屬性或方法是存在實例還是原型中
-
hasOwnProperty()
只有屬性或方法 在實例中才為true
...上面的例子
person1.hasOwnproperty('name'); // false 不在實例中
person1.name = 'Mike';
person1.hasOwnProperty('name'); // true 在實例中, 返回true
-
in
操作符
無論該屬性在實例中還是原型中, 只要能訪問到就返回true
...上上面的例子
'name' in person1 // true;
person1.hasOwnproperty('name'); // false 不在實例中
person1.name = 'Mike';
'name' in person1 // true;
person1.hasOwnProperty('name'); // true 在實例中, 返回true
'run' in person1; // false 實例和原型都沒有
所以呢, 同時使用 hasOwnProperty()
和 in
就可以確定該屬性到底存在對象還是原型中了
function hasPrototypeProperty (object, name) {
return !object.hasOwnProperty(name) && (name in object);
}
由于 in
操作符只要通過對象能夠訪問到屬性就能返回 true
, hasOwnProperty()
只要屬性存在實例中才能返回 true
, 所以只要 in
返回 true
而 hasOwnProperty()
返回 false
就說明屬性在原型中
5: 更簡單的原型語法
由于上面例子每添加一個屬性就敲一次 Person.prototype
很麻煩, 為了減少不必要的輸入, 如下:
function Person () {}
Person.prototype = {
name: 'Toni',
age: 19,
sayName: function () {
console.log(this.name);
}
}
缺點1:
按照上面的對象字面量來重寫整個原型對象, 最終結(jié)果相同, 但是, constructor
屬性不在指向 Person
, 這里重寫了后, constructor
指向了新對象 Object構(gòu)造函數(shù)
不在指向 Person
, 盡管 instanceof
還能返回正確的結(jié)果, 但是 constructor
已經(jīng)無法確定對象類型了
如:
//上例
var friend = new Person();
console.log(friend instanceof Object)// true
console.log(friend instanceof Person)// true
console.log(friend.constructor == Person) // false 正常的此處應(yīng)為true
console.log(friend.constructor == Object) // true
改進:
如果你的確需要 constructor
屬性的話, 可以這樣:
function Person () {}
Person.prototype = {
constructor: Person, //這里
name: 'Toni',
age: 19,
sayName: function () {
console.log(this.name);
}
}
或者 使用 Object.defineProperty()
function Person () {}
Person.prototype = {
name: 'Toni',
age: 19,
sayName: function () {
console.log(this.name);
}
}
Object.defineProPerty (Person.prototype, 'constructor', {
enumerable: false,
value: Person
})
- 第一種方法: 有個問題就是, 把原本
constructor
屬性的Enumerable
設(shè)置成了true
成了可枚舉的屬性, 原生的是false
不可枚舉- 第二中方法, 則是重構(gòu)構(gòu)造函數(shù), 直接設(shè)置了
enumerable: false
, 但是 只適應(yīng)于ECMAScript 5
兼容的瀏覽器
缺點2或者是需注意:
看例子:
例子1
var friend = new Person(); // 先實例化一個Person原型
Person.prototype.sayHi = function () {
console.log('Hi')
}; // 添加方法
friend.sayHi(); // Hi 沒什么問題
例子2:
function Person () {}; //原型
var friend = new Person(); // 緊接著實例化 (注意這里實例的順序)
Person.prototype = {
constructor: Person,
name: 'Toni',
age: 19,
sayName: function () {
console.log(this.name);
}
} // 然后在添加屬性方法
friend.sayName() // 報錯 friend.sayName is not a function
例子2正確的順序是:
function Person () {}; //原型
Person.prototype = {
constructor: Person,
name: 'Toni',
age: 19,
sayName: function () {
console.log(this.name);
}
} // 然后在添加屬性方法
var friend = new Person(); // 實例化
friend.sayName() // 'Toni';
例子錯誤原因
例子1中, 因為實例與原型中鏈接的是一個指針, 而非一個副本, 所以不論實例化原型與添加屬性方法的順序如何, 都是可以在原型中獲取到
例子2中, 我們先創(chuàng)建一個Person
的一個實例, 然后又重寫了其原型對象,
但是, 重寫原型對象就切斷了現(xiàn)有原型與任何之前已經(jīng)存在的對象實例之間的聯(lián)系
6: 原型對象的問題
原型對象的缺點 , 所有的實例都在默認情況下獲取相同的屬性值, 即共享本性, 雖然這也是其優(yōu)點, 但是這中共享對于函數(shù)非常適合, 對于包含基本值(即 name: 'tom',)的屬性還算可以, 但是對于引用類型(Object, Array,String, ...)的屬性問題就比較突出了
如:
function Person () {};
Person.prototype = {
constructor: Person,
name: 'Toni',
age: 19,
friends: ['Tom', 'mike', 'Anny'],
sayName: function () {
console.log(this.name);
}
}
var friend1 = new Person();
var friend2 = new Person();
console.log (friend2 .friends) //'Tom', 'mike', 'Anny' 正常顯示
friend1 .friends.push('Masa'); // 使用實例1 在數(shù)組中添加一個人名
console.log (friend2 .friends) //'Tom', 'mike', 'Anny' ,'Masa' , person2受到了影響
當然了如果你要的結(jié)果就是這樣, 那倒無所謂, 如果不是, 都需要單獨的屬性的話, 就有很大問題,
問題原因自然是由于實例與原型中鏈接的是一個指針, 指向同一個數(shù)組, friend1 對原型的修改, friend2 也會反映出來
7: 所以最好組合是
組合使用 構(gòu)造函數(shù)模式 和原型模式,
構(gòu)造函數(shù)模式用于定義實例屬性,
原型模式用于定義方法和共享屬性,
// 構(gòu)造函數(shù), 定義實例屬性, this將作用域指向新對象
function Person (name,age) {
this.name = name;
this.age = age;
this.friends = ['Tom', 'Mike'];
}
//原型模式用于定義方法和共享屬性,
Person.prototype = {
constructor: Person,
sayName: function () {
console.log(this.name)
}
}
var person1 = new Person ('Toni', 19) ;
var person2 = new Person ('Anny', 18);
person1.friends.push('An');
console.log(person1.friends); // 'Tom', 'Mike','An'
console.log(person2.friends) ; // 'Tom', 'Mike',
例子中, 實例屬性都是在構(gòu)造函數(shù)中定義的, 而由所有實例共享的屬性 construction
和 方法 sayName
都是在原型中定義的, 因而, 修改 person1.friends
數(shù)組, 只是修改實例中的數(shù)組, 沒有修改共有屬性,, 所以其他實例不會受到影響