1 原型是什麼?
原型是什麼?有許多JavaScript初學者都不明白原型。
說白了,原型就是一個對象中內置的特殊屬性_proto_,它是該對象對於其他對象的引用。
每個對象在創建時都會被賦予一個非空的私有屬性_proto_指向它的原型對象(任何對象都能做原型對象),該原型對象同樣是對象,既然是對象,那麼它內部也有一個非空的私有屬性_proto_,那麼這個_proto_屬性指向的是什麼呢?這個_proto_指向的是這個原型對象的原型對象。層層向上直到有一個對象的原型爲"null"爲止。"null"沒有原型,作爲原型鏈(由一層又一層相互關聯的原型指向組成)的最後一個環節。
一般而言在JavaScript中的對象都是位於原型頂端"Object"的實例,因此很多對象纔會具有"hasOwnProperty(...)" "toString(...)" "valueOf(...)"等等的方法。
這麼說有點抽象,我們來試試用例子來表示。
var foo={};
我們創建了一個對象"foo",這個函數被創建時帶有非空私有屬性_proto_,這個屬性指向的是"foo"對象的原型對象。
於是"foo.prototype=Object",接下來"Object.prototype"指向的是"null",這就是原型鏈。
原型鏈:
foo.prototype------>Object.prototype----->null
注意!!![[Prototype]]與_pro_相同,都是一個對象的內置私有屬性,在創建對象時,自動創建,代表的含義是:指向當前對象的原型對象。而prototype是函數特殊的屬性(此處的prototype與上文對象的prototype不一樣)指向的是一個對象,但是使用"new"構造調用函數創建對象時,"對象._pro_===函數.prototype"。
prototype是函數特有的,_pro_([[prototype]])是對象的內置私有屬性,函數又是可調用的對象,所以它也具有_pro_。
1.1 基於原型鏈的繼承
這種原型鏈看似缺點很多,但是卻非常容易構建面向對象編程中的繼承
1.1.1繼承屬性
①訪問屬性
讓我們來回顧一下,我們在《深入理解Java之對象》https://blog.csdn.net/qq_41889956/article/details/83716216中提到的[[Get]]查找。
- [[Get]]是查找對象屬性觸發的操作
- 它首先會在對象中查找是否存在同名屬性,找到返回,找不到進行下一步
- 開始查找對象本身的原型鏈,層層向上,直到查找到該屬性。
- 若仍是找不到,返回"undefined"
var foo={
a:2
}; //原型鏈:foo.prototype---->Object.prototype---->null
console.log(foo.a); //2 訪問對象的屬性,開始在對象foo中查找屬性a找到則返回該屬性的值。
var bar=Object.create(foo); //使用Object.create創建對象,此方法能將創建的對象與foo對象強行關聯到一起
//原型鏈:bar.prototype---->foo.prototype---->Object.prototype---->null
console.log(bar.a); //2 在bar上開始查找屬性a,發現查找不到,於是通過原型鏈查找foo,在foo中查找到屬性a,於是輸出
console.log(bar.b); //undefined 同樣在查找bar本身之後遍歷bar相關的原型鏈,發現不存在屬性b,輸出undefined
"foo.a"時,首先在"foo"中查找屬性"a",找到並返回"a"的值。
我們利用"Object.create(...)"創建了一個新對象"bar",讓它與對象"foo"的原型關聯起來,實則就是"bar._proto_=foo"。這時的原型鏈變爲:bar.prototype---->foo.prototype---->Object.prototype---->null。
"bar.a"在對象"bar"中查找無果,向原型鏈遍歷,直到在"foo"中查找到"a",於是返回"a"。
"bar.b"在對象"bar"中查找無果,向原型鏈遍歷,直到遍歷原型鏈結束了以後,仍沒有查找到"b",於是返回"undefined"。
注意在當前對象查找屬性,只會從此對象自身的原型鏈開始查找,即向上查找,而不是向下查找。
在上面的代碼中假設添加這麼一條語句"bar.c=3;" "console.log(foo.c)" 輸出結果是undefined,爲什麼呢?因爲訪問對象"foo"的屬性"c"於是一開始在自身查找該屬性,查找不到便開始遍歷原型鏈,"foo.prototype---->Object.prototype---->null",查找無果輸出undefined。
②屏蔽屬性
在屬性中因爲原型鏈的存在讓我們得以繼承屬性,實現面向對象中的繼承特性,那麼當我們在對象自身中定義了一個與原型鏈上的屬性同名的屬性,會發生什麼事呢?
var foo={
a:2
}; //原型鏈:foo.prototype---->Object.prototype---->null
console.log(foo.a); //2
var bar=Object.create(foo); //使用Object.create創建對象,此方法能將創建的對象與foo對象強行關聯到一起
//原型鏈:bar.prototype---->foo.prototype---->Object.prototype---->null
console.log(bar.a); //2
bar.a=3;
console.log(bar.a); //3 發生了屏蔽
console.log(foo.a); //2
當我們在對象自身中定義了一個與原型鏈上的屬性同名的屬性,就會發生屏蔽,然而並不是所有情況下都會發生屏蔽
之前講了[[Get]]操作,那麼我們現在來講講[[Put]]操作設置屬性。
我們向一個對象中添加一個屬性,會發生怎楊的操作呢?
- 當對象屬性中存在這個同名屬性,那麼修改它
- 當對象屬性不存在此屬性,原型鏈上存在此屬性,根據種種情況會發生屏蔽與不屏蔽
- 當對像屬性不存在此屬性,且原型鏈上不存在此屬性,那麼創建它並賦值
下面我們就第二種情況進行分析,“當對象屬性不存在此屬性,原型鏈上存在此屬性”。
①當對象屬性不存在,且原型鏈上存在此屬性,該屬性還是一個普通的“屬性描述符”(這裏涉及到對象內容的知識,不會的話看https://blog.csdn.net/qq_41889956/article/details/83716216)"writable"爲"true"時,那麼發生屏蔽
var foo={
a:2 //普通的屬性描述符
}; //原型鏈:foo.prototype---->Object.prototype---->null
console.log(foo.a); //2
var bar=Object.create(foo); //使用Object.create創建對象,此方法能將創建的對象與foo對象強行關聯到一起
//原型鏈:bar.prototype---->foo.prototype---->Object.prototype---->null
console.log(bar.a); //2
bar.a=3;
console.log(bar.a); //3 發生了屏蔽
console.log(foo.a); //2
②當對象屬性不存在,且原型鏈上存在此屬性,該屬性還是一個普通的“屬性描述符”,但是與①不同"writable"爲"false"時,不發生屏蔽,如果是在嚴格模式下,還會報錯"TypeError",因爲嚴格模式不允許修改一個只讀的屬性。如果不是嚴格模式就會忽略此語句。
var foo={
}; //原型鏈:foo.prototype---->Object.prototype---->null
Object.defineProperty(foo,"a",{
writable:false,
value:2
});
console.log(foo.a); //2
var bar=Object.create(foo); //使用Object.create創建對象,此方法能將創建的對象與foo對象強行關聯到一起
//原型鏈:bar.prototype---->foo.prototype---->Object.prototype---->null
console.log(bar.a); //2
bar.a=3;
console.log(bar.a); //2 不發生屏蔽
③當對象中不存在該屬性,且原型鏈上存在此屬性,該屬性還是一個"setter",那就會調用這個setter,此屬性不會添加到對象中,也就是不會發生屏蔽,這個setter也不會被重新定義。
總的來說只有第一種情況:對象中不存在該屬性,且原型鏈中存在該屬性,該屬性還是一個普通屬性描述符"writable"爲"true",纔會發生屏蔽。
③繼承方法
在JavaScript中沒有其他語言中定義的方法,與之稱之爲方法不如說是函數,函數在JavaScript中是一個可調用的對象,那麼繼承函數與繼承屬性沒太大的區別,唯一的區別在於,函數中如果存在this的話,this綁定的對象或許會發生改變。
當繼承的函數被調用時,this指向的時當前被繼承的對象,而不是繼承的函數所在的原型對象。
var o={
a:2,
foo:function () {
return this.a;
}
}; //原型鏈 o.prototype---->Object.prototype
console.log(o.foo()); //2
var m=Object.create(o); //構建原型鏈 m.prototype---->Object.prototype---->null
m.a=3;
console.log(m.foo()); //3 this綁定到了m上面
可以看到this綁定的對象由"o"變成了"m"。
2、創建對象以及生成原型鏈
前面我們已經知道了使用"Object.create(...)"可使創建對象與參數對象相關聯,從而構建原型鏈,難麼除此之外還有哪些方法呢?
2.1 語法結構創建對象生成原型鏈
創建普通對象,它的原型對象是"Object"。所以它具有的屬性是來自於"Object",其中包括:hasOwnProperty()
var o= {
a: 2,
}; //原型鏈:o.prototype---->Object.prototype---->null
創建數組對象,數組對象的原型都是"Array","Array.prototype"又指向"Object"。因此數組對象具有"forEach(...),indexOf(...)"等方法
var o=["a",1,"2"];
//原型鏈:o.prototype---->Array.prototype---->Object.prototype---->null
創建函數,儘管我們都在說函數是“可調用的”對象,但是它的原型對於普通對象有點不一樣,它的原型對象是"Function","Function.prototype"又指向"Object"。因此它具有"call(...)、bind(...)"等方法
function o() {
console.log("this a function");
} //原型鏈: o.prototype---->Function.prototype---->Object.prototype---->null
2.2 構造器創建的對象生成原型鏈
回顧下我們之前所說的"new",在JavaScript中是不存在構造方法的,所以當我們使用"new"創建一個對象時,是構造調用,對於一個函數的構造調用。
使用new關鍵字來創建對象的過程,大體分爲以下三步。
- 創建一個對象
- 這個函數被執行原型鏈
- 這個對象被綁定到函數調用的this上
- 如果函數中沒有返回這個對象,那麼new表達式中的函數會自動返回這個新對象
function foo() {
}
foo.prototype={ //相當於foo的原型對象,因爲bar._pro_===foo.prototype,所以它們共享foo.prototype中的屬性和方法
a:2,
tostring:function (a) {
return this.a=a;
}
};
var bar=new foo();
console.log(bar.a);
console.log(bar.tostring(3));
console.log(bar.__proto__===foo.prototype); //證明bar的原型對象確實是foo.prototype
console.log(Object.getPrototypeOf(bar)===foo.prototype);
從上述結果可以看出,對象._proto_===使用new創建該對象的函數.prototype,於是它們共享一個對象中的屬性和方法。具體的_proto_與prototype與[[Prototype]]區別可參考https://blog.csdn.net/qq_41889956/article/details/84234001
那麼按照如此上述使用new創建對象便可分爲一下步驟
- 創建一個對象
- 創建對象._proto_=構造調用函數.prototype
- 這個對象被綁定到函數調用的this上
- 如果函數中沒有返回這個對象,那麼new表達式中的函數會自動返回這個新對象
2.3 Object.create(...)創建對象生成原型鏈
這是強制使創建對象於參數對象相關聯,創建對象的原型對象=參數對象,形成原型鏈
var o={
a:2
};
var m=Object.create(o); //原型鏈: m.prototype---->o.prototype---->Object.prototype---->null
console.log(m.a); //2
3 性能
在原型鏈上查找屬性比較耗費時間,有損性能,比如你在查找一個對象的屬性時,當這個屬性不存在此對象中,就會遍歷整條原型鏈(當然從此對象的原型對象開始)。
如果你只是像看看再這個對象中是否存在此屬性,那麼可以使用"hasOwnProperty(...)"
"hasOwnPrototype(...)"是JavaScript中僅有的兩個不會遍歷原型鏈屬性的方法之一,另一個是"Object.key(...)"。
4、Object.getPrototypeOf(...)
此方法的作用是,得到傳入對象的原型對象,與對象foo"foo._pro_"相同。