理解原型對(duì)象
創(chuàng)建一個(gè)函數(shù)剥懒,就會(huì)根據(jù)一組規(guī)則為該函數(shù)創(chuàng)建一個(gè) prototype 屬性奖地,這個(gè)屬性指向函數(shù)的原型對(duì)象。
function Person() {
}
Person.prototype.name = 'Zhang san';
Person.prototype.sayName = function() {
console.log(this.name);
}
console.log(Person.prototype); // Person { name: 'Zhang san' }
console.log(Person.prototype.constructor === Person); // true
const person = new Person();
console.log(person.__proto__); // Person { name: 'prototype' }
在默認(rèn)情況下赎瞎,所有原型對(duì)象都會(huì)自動(dòng)獲得一個(gè) constructor 屬性,這個(gè)屬性指向構(gòu)造函數(shù)巧号。自定義構(gòu)造函數(shù)的原型對(duì)象默認(rèn)只會(huì)獲得 constructor 屬性族奢,其他的方法和屬性都是從 Object 繼承而來(lái)。
當(dāng)調(diào)用構(gòu)造函數(shù)創(chuàng)建一個(gè)實(shí)例時(shí)丹鸿,實(shí)例內(nèi)部會(huì)包含一個(gè) [[Prototype]]
屬性越走,指向構(gòu)造函數(shù)的原型對(duì)象,在瀏覽器中以 __proto__
表示靠欢。
[image:48EDB027-8752-4216-94D9-8AC4BA23B791-30221-00025A8850310CC5/WechatIMG245.png]
對(duì)象和原型之間的關(guān)系可以通過(guò) isPrototypeOf() 方法來(lái)檢測(cè)廊敌。
console.log(Person.prototype.isPrototypeOf(person)); // true
對(duì)象有兩種方式可以獲取到它的原型。
console.log(Object.getPrototypeOf(person) === Person.prototype); // true
console.log(person.__proto__ === Person.prototype); // true, 官方不推薦使用這種方式
當(dāng)讀取一個(gè)對(duì)象的屬性或方法時(shí)门怪,會(huì)先在對(duì)象實(shí)例上搜索骡澈,如果實(shí)例具有給定名稱的屬性,則返回該屬性值掷空。如果沒(méi)有找到肋殴,則繼續(xù)從原型中搜索。
雖然實(shí)例可以訪問(wèn)保存在原型中的值坦弟,但卻不能通過(guò)對(duì)象實(shí)例重寫(xiě)原型中的值护锤。如果我們?cè)趯?shí)例中添加一個(gè)屬性,而該屬性與實(shí)例原型中的一個(gè)屬性同名酿傍,那么該實(shí)例的屬性會(huì)屏蔽原型中的對(duì)應(yīng)的屬性烙懦。
function Person() {
}
Person.prototype.name = 'Zhang san';
let person1 = new Person();
let person2 = new Person();
person2.name = 'Li si';
console.log(person1.name); // Zhang san
console.log(person2.name); // Li si
person2 重寫(xiě) name 屬性后原型的 name 屬性值被覆蓋。person1 的 name 屬性不受影響赤炒。
通過(guò) delete 操作符能夠使得 person2 重新獲得原型上的值氯析。
delete person2.name;
console.log(person2.name); // Zhang san
使用對(duì)象的 hasOwnProperty() 方法檢測(cè)實(shí)例中的屬性。
function Person() {
}
Person.prototype.name = 'Zhang san';
let person1 = new Person();
let person2 = new Person();
person2.name = 'Li si';
console.log(person1.hasOwnProperty('name')); // false
console.log(person2.hasOwnProperty('name')); // true
person2 因?yàn)橹貙?xiě)了 name 屬性莺褒,所以返回 true掩缓;而 person1 沒(méi)有該實(shí)例屬性,所以返回 false.
原型與 in 操作符
有兩種方式使用 in 操作符:
- 單獨(dú)使用
- 在 for-in 循環(huán)中使用
單獨(dú)使用 in 操作符時(shí)遵岩,用于檢測(cè)對(duì)象能夠訪問(wèn)的屬性拾因,不管是在實(shí)例中還是原型中。
console.log('name' in person1); // true
console.log('name' in person2); // true
前面的例子中 person1 的 name 屬性在原型上旷余,person2 的 name 屬性在實(shí)例上,兩者都返回 true扁达。
使用 for-in 循環(huán)時(shí)正卧,返回的是所有能夠通過(guò)對(duì)象訪問(wèn)的、可枚舉的屬性跪解,其中包括在實(shí)例中和原型中的屬性炉旷。
function Person() {
}
Person.prototype.name = 'Zhang san';
Person.prototype.sayName = function() {
console.log(this.name);
}
Object.defineProperty(Person.prototype, 'age', {enumrable: false, value: 18})
let person2 = new Person();
person2.job = 'Engineer';
for (const prop in person2) {
console.log(prop); // job, name, sayName
}
job 存在于對(duì)象中,name 和 sayName() 存在于對(duì)象原型,都被正常的枚舉窘行,而
age 被定義為不可枚舉饥追,所以沒(méi)有返回。
既然 in 操作符可以檢測(cè)對(duì)象能夠訪問(wèn)的屬性罐盔,而前面講到 hasOwnProperty() 只能返回存在于對(duì)象實(shí)例上的屬性但绕,那我們可以自定義方法來(lái)檢測(cè)一個(gè)屬性是否存在于原型上。
function hasPrototypeProperty(object, name) {
return !Object.hasOwnProperty(name) && (name in object);
}
在 ES5 中新增了 Object.keys() 方法惶看,這個(gè)方法接收一個(gè)對(duì)象作為參數(shù)捏顺,返回一個(gè)包含所有可枚舉屬性的字符串?dāng)?shù)組。
function Person() {
}
Person.prototype.name = 'Zhang san';
Person.prototype.sayName = function() {
console.log(this.name);
}
Object.defineProperty(Person.prototype, 'age', {enumrable: false, value: 18})
let person2 = new Person();
person2.job = 'Engineer';
person2.sayName2 = function() {
console.log(this.name);
}
const pKeys = Object.keys(person2);
console.log(pKeys); // ['job', 'sayName2']
我們可以發(fā)現(xiàn) Object.keys() 方法只返回包含在對(duì)象上的屬性和方法纬黎,不包含原型上的幅骄,并且不可枚舉的屬性也不會(huì)被返回。
如果想要得到實(shí)例的所有屬性本今,無(wú)論是否可枚舉拆座,可以使用 Object.getOwnPropertyNames() 方法。
const keys = Object.getOwnPropertyNames(Person.prototype);
console.log(keys); // [ 'constructor', 'name', 'sayName', 'age' ]
原型的動(dòng)態(tài)性
在原型中訪問(wèn)屬性值其實(shí)是一次搜索的過(guò)程冠息,因此我們?cè)谠蛯?duì)象上所做的任何修改都能立即從實(shí)例上反映出來(lái)挪凑,即使先創(chuàng)建實(shí)例后修改原型。
const friend = new Person();
Person.prototype.sayHi = function() {
console.log('hi');
}
friend.sayHi(); // hi
因?yàn)閷?shí)例與原型之間的連接只是一個(gè)引用铐达,而非是一個(gè)副本岖赋,因此實(shí)例對(duì)象可以在原型中找到新添加的 sayHi 屬性。
但是如果重寫(xiě)整個(gè)對(duì)象的原型瓮孙,情況就不一樣了唐断。使用構(gòu)造函數(shù) new 一個(gè)對(duì)象,會(huì)為該實(shí)例添加一個(gè)指向原型的指針杭抠,而把原型修改為另一個(gè)對(duì)象就會(huì)切斷構(gòu)造函數(shù)與最初原型之間的聯(lián)系脸甘。
function Person() {
}
const friend = new Person();
Person.prototype = {
constructor: Person,
name: 'Li si',
sayName: function() {
console.log(this.name);
}
}
friend.sayName(); // error
原生對(duì)象的原型
原生對(duì)象(Object, Array, String, …)的方法都是在其構(gòu)造函數(shù)的原型上定義的。比如 Array 的 sort() 方法偏灿。
console.log(Array.prototype.sort); // [Function: sort]
因此我們也可以使用這種方式為原生對(duì)象添加自定義的方法丹诀。下面的代碼為 String 添加一個(gè)名為 startWith() 的方法。
String.prototype.startWith = function(text) {
return this.indexOf(text) === 0;
}
const msg = 'Hello world';
console.log(msg.startWith('Hello')); // true
為 String.prototype 添加屬性翁垂,當(dāng)前環(huán)境下的所有字符串都可以調(diào)用铆遭。這么做會(huì)有風(fēng)險(xiǎn),如果其他地方添加了相同名稱的屬性沿猜,就會(huì)造成命名沖突枚荣。
原型對(duì)象的問(wèn)題
原型對(duì)象實(shí)現(xiàn)了實(shí)例之間屬性的共享,但也存在一個(gè)明顯的缺點(diǎn)啼肩。共享的屬性如果是一個(gè)引用類型的對(duì)象橄妆,在修改的時(shí)候會(huì)導(dǎo)致所有實(shí)例都受影響衙伶。
function Person() {
}
Person.prototype = {
constructor: Person,
name: 'Li si',
friends: ['Wang wu', 'Zhang san'],
}
const person1 = new Person();
const person2 = new Person();
person1.friends.push('Li li');
console.log(person1.friends); // [ 'Wang wu', 'Zhang san', 'Li li' ]
console.log(person2.friends); // [ 'Wang wu', 'Zhang san', 'Li li' ]
Person.prototype 上定義的 friends 屬性,在修改 person1 實(shí)例時(shí)害碾, person2 也被修改了矢劲。
所以,原型對(duì)象一般只用來(lái)添加方法慌随,而對(duì)象屬性直接添加在實(shí)例上芬沉。
用原型創(chuàng)建一個(gè)實(shí)例
前面講到通過(guò)構(gòu)造函數(shù) new 一個(gè)實(shí)例,該實(shí)例的 [[prototype]]
屬性指向構(gòu)造函數(shù)的原型儒陨。還有一種方法是直接以一個(gè)對(duì)象原型來(lái)創(chuàng)建實(shí)例花嘶。
const person = {
name: 'Zhang San',
sayName: function() {
console.log(this.name);
}
};
const me = Object.create(person);
me.sayName(); // Zhang san
我們以 person 為原型創(chuàng)建了 me 實(shí)例,沒(méi)有顯示的用到構(gòu)造函數(shù)蹦漠。實(shí)際上內(nèi)部是創(chuàng)建了一個(gè) Object 實(shí)例颁湖,并將其 [[prototype]]
指針指向了 person善炫。
總結(jié)
- 如何檢測(cè)對(duì)象是否存在某個(gè)屬性赶么;
- 如何遍歷對(duì)象實(shí)例和原型中的屬性瘟仿;
- 如何檢測(cè)是屬性在對(duì)象中還是原型中;
- 如何為原生對(duì)象添加自定義方法研铆;
- 如何以一個(gè)對(duì)象為原型埋同,創(chuàng)建實(shí)例對(duì)象。
本文內(nèi)容多數(shù)為《JavaScript 高級(jí)程序設(shè)計(jì)》閱讀筆記棵红。