最近在看underscore源碼,涉及到j(luò)s原型相關(guān)的知識(shí),于是重溫了一遍,再次做下記錄。
js原型是其語法的一個(gè)難點(diǎn),也是一個(gè)重點(diǎn),要深入學(xué)習(xí)js必須掌握的點(diǎn)。要想讀懂別人的框架和庫,了解這些基礎(chǔ)知識(shí)是必不可少的。
js原型主要為了提取公共屬性和方法,實(shí)現(xiàn)對(duì)象屬性和方法的繼承。說到原型,可能就有幾個(gè)相關(guān)的詞:prototype、__proto__、constructor、instanceof。下面通過這幾個(gè)關(guān)鍵詞來一一講解原型。
說到原型,先說一個(gè)概念,js里函數(shù)(function)是一種特殊的對(duì)象,有個(gè)說法是js里一切都是對(duì)象,這個(gè)說法不正確,要排除一些特殊類型,如:undefined, null (雖然null的typeof, toString返回類型是object,歷史遺留bug)。
js里所有的對(duì)象都有proto屬性,可以稱為隱式原型,這個(gè)屬性在低版本ie瀏覽器中無法讀取到。一個(gè)對(duì)象的隱式原型指向構(gòu)造該對(duì)象的構(gòu)造函數(shù)的原型,使得該對(duì)象能夠繼承其構(gòu)造函數(shù)原型上的屬性和方法。
函數(shù)對(duì)象除了具有proto這個(gè)屬性外,還有一個(gè)專有屬性prototype。這個(gè)屬性指向一個(gè)對(duì)象(命名a對(duì)象),從該函數(shù)實(shí)例化的對(duì)象(命名b對(duì)象)。b對(duì)象能共享a對(duì)象的所有屬性和方法。反過來a對(duì)象的constructor屬性又指向這個(gè)函數(shù)。
constructor 屬性是專門為 function 而設(shè)計(jì)的,它存在于每一個(gè) function 對(duì)象的prototype 屬性中。這個(gè) constructor 保存了指向 function 的一個(gè)引用。
constructor和prototype被設(shè)計(jì)時(shí)是構(gòu)造函數(shù)和原型間相互指向,可以看成互逆的,由于實(shí)例繼承了prototype對(duì)象上的屬性(包括constructor),故實(shí)例的constructor也是指向構(gòu)造函數(shù)。雖然他們倆是互逆,但是兩者沒有必然聯(lián)系,修改其中一個(gè)的指向,另一個(gè)并不會(huì)變。所以在js中通過原型來繼承時(shí),一般替換原型時(shí),會(huì)附帶替換掉constructor的指向。
// constructor與prototype
Object.prototype === Object.prototype
Object.prototype.constructor === Object
// 實(shí)例指向構(gòu)造函數(shù)
var a = new Object({});
a.constructor === Object;
// 修改一個(gè)指向
function a() {}
a.prototype = {
say: function () {
console.log('hello world');
}
}
a.prototype.constructor === Object //true
// 因?yàn)閍.prototype重新賦值時(shí),直接是賦值的一個(gè)對(duì)象,
// 這個(gè)對(duì)象沒有通過構(gòu)造函數(shù)來生成,默認(rèn)就會(huì)以new Object方式。故構(gòu)造函數(shù)就是Object。
// 所以一般要手動(dòng)重新指向構(gòu)造函數(shù)
a.prototype.constructor = a;
constructor設(shè)計(jì)初是被用來判斷對(duì)象類型的,由于其易變性,一般不使用它來做判斷,使用instanceof來替代它。
instanceof運(yùn)算符,它用來判斷一個(gè)構(gòu)造函數(shù)的prototype屬性所指向的對(duì)象是否存在另外一個(gè)要檢測(cè)對(duì)象的原型鏈上。一般用來判斷一個(gè)實(shí)例是否從一個(gè)構(gòu)造函數(shù)實(shí)例化過來,用一個(gè)函數(shù)模擬instanceof函數(shù):
function _instanceof(A, B) {
var O = B.prototype;
A = A.__proto__;
while (true) {
if (A === null) // 循環(huán)查找原型鏈,一直到Object.prototype.__proto__ = null
return false; // 退出循環(huán)
if (O === A)
return true;
A = A.__proto__;
}
}
說了這么多是不是覺得有點(diǎn)繞,拿出我的殺手锏,祭出我收藏的一張圖,該圖很清晰的解釋了這些關(guān)系。看了這張圖后,瞬間理清了原型,廢話不多說,上圖:

看了上面的圖后相信整個(gè)原型比較清晰了,下面說說整個(gè)原型中幾個(gè)特殊對(duì)象。
第一個(gè)特殊的對(duì)象就是Function。
js里的內(nèi)置對(duì)象Object、Array、Function、Date、Math、Number、String、Boolean、RegExp等都是構(gòu)造函數(shù)對(duì)象,可以通過new實(shí)例化對(duì)象出來。其proto屬性都指向Function.prototype。Function這個(gè)特殊對(duì)象,是上面其他函數(shù)對(duì)象的構(gòu)造函數(shù)。
這里有一條鏈,以Array為例:

js中上面寫的這些對(duì)象可以看成是從Function構(gòu)造函數(shù)new出來的對(duì)象(實(shí)例),只不過與Object,Array構(gòu)造函數(shù) new出來的對(duì)象有點(diǎn)不同,實(shí)例化出來的對(duì)象是函數(shù)對(duì)象。所以有以下等式成立。
Array.__proto__ === Function.prototype // true
Object.__proto__ === Function.prototype // true
Array.constructor === Function // true
Object.constructor === Function // true
由于實(shí)例化的Array、Object等屬于函數(shù)對(duì)象,它就有prototype屬性,故給每個(gè)函數(shù)對(duì)象配了個(gè)原型,如:Array.prototype、Object.prototype,從Array、Object等實(shí)例化的對(duì)象可以完成一些相同的功能,故給這些對(duì)象內(nèi)置了很多方法,讓所有實(shí)例化的對(duì)象都具備這些方法,故在原型上掛載了很多方法。比如Array.prototype的方法:push、shift、unshift、concat等。
還有個(gè)特例如下:
Function.__proto__ === Function.prototype
Function 這個(gè)函數(shù)既可以看成構(gòu)造函數(shù),也可以看成實(shí)例后的函數(shù)對(duì)象。
第二個(gè)個(gè)特殊的對(duì)象就是Object.prototype
不管是構(gòu)造函數(shù)、原型、還是實(shí)例化對(duì)象,其都屬于對(duì)象,對(duì)象的原型最初來源都是Object.prototype這個(gè)原型對(duì)象,故:
Function.prototype.__proto__ === Object.prototype
Array.prototype.__proto__ === Object.prototype
Object.prototype.__proto__ === null
而Object.prototype這個(gè)對(duì)象的proto屬性就為null了。
最后上一張我自己畫的關(guān)于原型的圖:

既然說到了原型鏈,來說一下幾個(gè)相關(guān)屬性,hasOwnProperty、isPrototypeOf、in。這幾個(gè)屬性在原型概念中經(jīng)常用到。
js的原型主要實(shí)現(xiàn)了屬性和方法的繼承,既然有繼承,屬性和方法就有自己的和繼承來的之分。那么怎么去區(qū)分呢?
1、hasOwnProperty()方法用來判斷某個(gè)對(duì)象是否含有指定的自身屬性。不包括原型鏈上的屬性
var a = {name: 'li'};
a.hasOwnProperty('hasOwnProperty'); // false
a.hasOwnProperty('name'); // true
hasOwnProperty屬性繼承自O(shè)bject.prototype,故返回false,
name則是創(chuàng)建時(shí),自帶的,故返回false
2、isPrototypeOf方法測(cè)試一個(gè)對(duì)象是否存在另一個(gè)對(duì)象的原型鏈上。
var o = {}
var a = function () {}
var b = new a();
a.prototype = o;
var c = new a();
o.isPrototypeOf(b); // false
o.isPrototypeOf(c); // true
3、in方法也是檢測(cè)一個(gè)對(duì)象中是否有每個(gè)屬性。
與hasOwnProperty不同的是,它會(huì)遍歷該對(duì)象上所有可枚舉屬性,包括原型鏈上的屬性,有就返回true,沒有就返回false;
function a() {
this.name = 'li'
}
a.prototype = {age: 20};
var b = new a();
for (var key in b) {
if (b.hasOwnProperty(key)) {
console.log('自身屬性'+ key);
} else {
console.log('繼承屬性'+ key);
}
}
上面的方法經(jīng)常區(qū)分一個(gè)對(duì)象中的屬性是自身屬性還是繼承屬性。
for...in 循環(huán)只遍歷可枚舉屬性,使用內(nèi)置構(gòu)造函數(shù),像 Array、Object、Number、Boolean、String等構(gòu)造函數(shù)的原型上的屬性都不可枚舉。
如:Object.prototype.toString方法。
當(dāng)然如果toString方法被重寫,還是可以遍歷的,如:
function animal() {
this.name = 'lilei'
}
animal.prototype.toString = function () {
console.log('animal say');
}
var cat = new animal();
for (var key in cat) {
console.log(key);
}
但是在 IE < 9 瀏覽器中(萬惡的 IE),Object、Array等構(gòu)造函數(shù)的原型上的屬性即使被重寫了,還是不能被枚舉到。
(1)、說到可枚舉,你可能想到了一個(gè)函數(shù),沒錯(cuò)就是propertyIsEnumerable函數(shù),他是Object.prototype上的一個(gè)方法,他能檢測(cè)一個(gè)屬性在其對(duì)象上是否可以枚舉。
該方法只能檢測(cè)對(duì)象的自有屬性,對(duì)于其原型鏈上的屬性始終返回false,這一點(diǎn)要與for ... in 中的可枚舉區(qū)分開。
function a() {
this.name = 'liming';
}
a.prototype.say = function () {
console.log(1);
}
var b = new a();
b.propertyIsEnumerable('name') // true
b.propertyIsEnumerable('say') // false
(2)、對(duì)象的屬性分為可枚舉和不可枚舉之分,說了這么多,其實(shí)它們是由屬性的enumerable值決定的。如通過Object.defineProperty函數(shù)創(chuàng)建的屬性,可以添加該字段來決定該屬性是否可枚舉。
var a = {name: 'xiao ming'}
Object.defineProperty(a, "gender", {
value: "male",
enumerable: false
});
a.propertyIsEnumerable('name') // true
a.propertyIsEnumerable('gender') // false
(3)、到此應(yīng)該已經(jīng)結(jié)束了,但是我還是想提到一個(gè)函數(shù),Object.keys。該函數(shù)返回一個(gè)對(duì)象的key的數(shù)組。看個(gè)例子
function q() {
this.name = 'lilei'
}
q.prototype.say = function () {
console.log('say');
}
var a = new q();
Object.defineProperty(a, "gender", {
value: "male",
enumerable: false
});
Object.keys(a) // ['name']
說明該方法返回該對(duì)象自有的可枚舉屬性。
(4)、我還想說一下JSON.stringify方法,我們都只到JSON.stringify方法可以序列化對(duì)象。你有通過它克隆對(duì)象沒?
var a = {name: 'liming'}
var b = JSON.parse(JSON.stringify(a)) // {name: 'liming'}
沒錯(cuò)對(duì)于簡(jiǎn)單的對(duì)象,我們可以這樣克隆,但是他能保存對(duì)象里的所有屬性嗎?
我們來看一下:
function f() {
this.name = 'lilei';
this.like= undefined;
this.say = function () {}
}
f.prototype.age = 20;
var a = new f();
Object.defineProperty(a, "gender", {
value: "male",
enumerable: false
});
var b = JSON.parse(JSON.stringify(a)) // {name: 'lilei'}
顯然該方法并不能保存對(duì)象里所有屬性,事實(shí)上stringify只能保存該對(duì)象自己的可枚舉屬性,不能保存其原型上的屬性,并且自己的屬性也必須滿足以下要求:
1、stringify只能保存基礎(chǔ)類型的:數(shù)字、字符串、布爾值、null四種,不支持undefined。
2、stringify方法不支持函數(shù);
3、除了RegExp、Error對(duì)象,JSON語法支持其他所有對(duì)象;
關(guān)于其詳細(xì)內(nèi)容,請(qǐng)看這篇文章傳送門
結(jié)語:你可能感覺文章后面說了一堆方法好像跟原型沒多大關(guān)聯(lián),確實(shí)關(guān)聯(lián)性不是很大,但是它們方法內(nèi)部都涉及到了對(duì)象的屬性遍歷,對(duì)象屬性遍歷自然就聯(lián)系到原型鏈上的屬性是否可遍歷,屬性的可枚舉性等一系列概念,所以就把它們都提了一下。
本人能力有限,以上內(nèi)容為個(gè)人理解,如有錯(cuò)誤,歡迎指正。