最近在看underscore源碼,涉及到js原型相關的知識,因而重溫了一遍,在此作下記錄。html
js原型是其語法的一個難點,也是一個重點,要深刻學習js必須掌握的點。要想讀懂別人的框架和庫,瞭解這些基礎知識是必不可少的。 js原型主要爲了提取公共屬性和方法,實現對象屬性和方法的繼承。說到原型,可能就有幾個相關的詞:prototype、__proto__、constructor、instanceof。下面經過這幾個關鍵詞來說解原型。數組
說到原型,先說一個概念,js裏函數(function)是一種特殊的對象,有個說法是js裏一切都是對象,這個說法不正確,要排除一些特殊類型,如:undefined, null (雖然null的typeof, toString返回類型是object,歷史遺留bug)。瀏覽器
js裏全部的對象都有__proto__屬性,能夠稱爲隱式原型,這個屬性在低版本ie瀏覽器中沒法讀取到。一個對象的隱式原型指向構造該對象的構造函數的原型,使得該對象可以繼承其構造函數原型上的屬性和方法。框架
函數對象除了具備__proto__這個屬性外,還有一個專有屬性prototype。這個屬性指向一個對象(命名a對象),從該函數實例化的對象(命名b對象)。b對象能共享a對象的全部屬性和方法。反過來a對象的constructor屬性又指向這個函數。函數
constructor 屬性是專門爲 function 而設計的,它存在於每個 function 對象的prototype 屬性中。這個 constructor 保存了指向 function 的一個引用。學習
constructor和prototype被設計時是構造函數和原型間相互指向,能夠當作互逆的,因爲實例繼承了prototype對象上的屬性(包括constructor),故實例的constructor也是指向構造函數。雖然他們倆是互逆,可是二者沒有必然聯繫,修改其中一個的指向,另外一個並不會變。因此在js中經過原型來繼承時,通常替換原型時,會附帶替換掉constructor的指向。測試
// constructor與prototype
Object.prototype === Object.prototype
Object.prototype.constructor === Object
// 實例指向構造函數
var a = new Object({});
a.constructor === Object;
// 修改一個指向
function a() {}
a.prototype = {
say: function () {
console.log('hello world');
}
}
a.prototype.constructor === Object //true
// 由於a.prototype從新賦值時,直接是賦值的一個對象,
// 這個對象沒有經過構造函數來生成,默認就會以new Object方式。故構造函數就是Object。
// 因此通常要手動從新指向構造函數
a.prototype.constructor = a;
複製代碼
constructor設計初是被用來判斷對象類型的,因爲其易變性,通常不使用它來作判斷,使用instanceof來替代它。ui
instanceof運算符,它用來判斷一個構造函數的prototype屬性所指向的對象是否存在另一個要檢測對象的原型鏈上。通常用來判斷一個實例是否從一個構造函數實例化過來,用一個函數模擬instanceof函數:this
function _instanceof(A, B) {
var O = B.prototype;
A = A.__proto__;
while (true) {
if (A === null) // 循環查找原型鏈,一直到Object.prototype.__proto__ = null
return false; // 退出循環
if (O === A)
return true;
A = A.__proto__;
}
}
複製代碼
說了這麼可能是不是以爲有點繞,拿出個人殺手鐗,祭出我收藏的一張圖,該圖很清晰的解釋了這些關係。看了這張圖後,瞬間理清了原型,廢話很少說,上圖:spa
看了上面的圖後相信整個原型比較清晰了,下面說說整個原型中幾個特殊對象。
js裏的內置對象Object、Array、Function、Date、Math、Number、String、Boolean、RegExp等都是構造函數對象,能夠經過new實例化對象出來。其__proto__屬性都指向Function.prototype。Function這個特殊對象,是上面其餘函數對象的構造函數。
這裏有一條鏈,以Array爲例:
js中上面寫的這些對象能夠當作是從Function構造函數new出來的對象(實例),只不過與Object,Array構造函數 new出來的對象有點不一樣,實例化出來的對象是函數對象。因此有如下等式成立。
Array.__proto__ === Function.prototype // true
Object.__proto__ === Function.prototype // true
Array.constructor === Function // true
Object.constructor === Function // true
複製代碼
因爲實例化的Array、Object等屬於函數對象,它就有prototype屬性,故給每一個函數對象配了個原型,如:Array.prototype、Object.prototype,從Array、Object等實例化的對象能夠完成一些相同的功能,故給這些對象內置了不少方法,讓全部實例化的對象都具有這些方法,故在原型上掛載了不少方法。好比Array.prototype的方法:push、shift、unshift、concat等。 還有個特例以下:
Function.__proto__ === Function.prototype
複製代碼
Function 這個函數既能夠當作構造函數,也能夠當作實例後的函數對象。
不論是構造函數、原型、仍是實例化對象,其都屬於對象,對象的原型最初來源都是Object.prototype這個原型對象,故:
Function.prototype.__proto__ === Object.prototype
Array.prototype.__proto__ === Object.prototype
Object.prototype.__proto__ === null
複製代碼
而Object.prototype這個對象的__proto__屬性就爲null了。 最後上一張我本身畫的關於原型的圖:
既然說到了原型鏈,來講一下幾個相關屬性,hasOwnProperty、isPrototypeOf、in。這幾個屬性在原型概念中常常用到。
js的原型主要實現了屬性和方法的繼承,既然有繼承,屬性和方法就有本身的和繼承來的之分。那麼怎麼去區分呢?
var a = {name: 'li'};
a.hasOwnProperty('hasOwnProperty'); // false
a.hasOwnProperty('name'); // true
複製代碼
hasOwnProperty屬性繼承自Object.prototype,故返回false, name則是建立時,自帶的,故返回false
var o = {}
var a = function () {}
var b = new a();
a.prototype = o;
var c = new a();
o.isPrototypeOf(b); // false
o.isPrototypeOf(c); // true
複製代碼
與hasOwnProperty不一樣的是,它會遍歷該對象上全部可枚舉屬性,包括原型鏈上的屬性,有就返回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);
}
}
上面的方法常常區分一個對象中的屬性是自身屬性仍是繼承屬性。
複製代碼
for...in 循環只遍歷可枚舉屬性,使用內置構造函數,像 Array、Object、Number、Boolean、String等構造函數的原型上的屬性都不可枚舉。 如:Object.prototype.toString方法。 固然若是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); // name, toString
}
複製代碼
可是在 IE < 9 瀏覽器中(萬惡的 IE),Object、Array等構造函數的原型上的屬性即便被重寫了,仍是不能被枚舉到。
(1)、說到可枚舉,你可能想到了一個函數,沒錯就是propertyIsEnumerable函數,他是Object.prototype上的一個方法,他能檢測一個屬性在其對象上是否能夠枚舉。 該方法只能檢測對象的自有屬性,對於其原型鏈上的屬性始終返回false,這一點要與for ... in 中的可枚舉區分開。
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)、對象的屬性分爲可枚舉和不可枚舉之分,說了這麼多,其實它們是由屬性的enumerable值決定的。如經過Object.defineProperty函數建立的屬性,能夠添加該字段來決定該屬性是否可枚舉。
var a = {name: 'xiao ming'}
Object.defineProperty(a, "gender", {
value: "male",
enumerable: false
});
a.propertyIsEnumerable('name') // true
a.propertyIsEnumerable('gender') // false
複製代碼
(3)、到此應該已經結束了,可是我仍是想提到一個函數,Object.keys。該函數返回一個對象的key的數組。看個例子
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']
複製代碼
說明該方法返回該對象自有的可枚舉屬性。
(4)、我還想說一下JSON.stringify方法,咱們都只到JSON.stringify方法能夠序列化對象。你有經過它克隆對象沒?
var a = {name: 'liming'}
var b = JSON.parse(JSON.stringify(a)) // {name: 'liming'}
複製代碼
沒錯對於簡單的對象,咱們能夠這樣克隆,可是他能保存對象裏的全部屬性嗎? 咱們來看一下:
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'}
複製代碼
顯然該方法並不能保存對象裏全部屬性,事實上stringify只能保存該對象本身的可枚舉屬性,不能保存其原型上的屬性,而且本身的屬性也必須知足如下要求: 一、stringify只能保存基礎類型的:數字、字符串、布爾值、null四種,不支持undefined。 二、stringify方法不支持函數; 三、除了RegExp、Error對象,JSON語法支持其餘全部對象; 關於其詳細內容,請看這篇文章傳送門
你可能感受文章後面說了一堆方法好像跟原型沒多大關聯,確實關聯性不是很大,可是它們方法內部都涉及到了對象的屬性遍歷,對象屬性遍歷天然就聯繫到原型鏈上的屬性是否可遍歷,屬性的可枚舉性等一系列概念,因此就把它們都提了一下。
本人能力有限,以上內容爲我的理解,若有錯誤,歡迎指正。