1、原型的概念
首先我們創建一個對象時,此對象都會默認創建一個內置的私有屬性_proto_([[Prototype]]),此屬性的值指向的是該對象的原型對象,對象會繼承來自原型的屬性和方法,
函數是可調用的對象,但是當我們創建一個函數時它不僅會有內置私有屬性_proto_還有屬於函數的特有的內置私有屬性prototype,指向的是,構造函數調用創建對象的原型對象。
既然原型對象是對象,只要是對象都會有原型對象,那麼它也有原型對象,如此一層接一層,這就是原型鏈。
原型鏈有沒有結束的地方呢?有的,Object.prototype是原型鏈的最後一環節,準確來說是"null",Object.prototype---->null。
在JavaScript中所有對象都是Object的實例。
1.1 普通對象原型
在一個對象創建時,會存在一個私有的內置屬性_proto_(實際上正確叫法應該是[[Prototype]]),此屬性的值指向的是該對象的原型對象,但是並不建議使用_proto_來訪問對象的原型對象,我們可以通過Object.getPrototypeOf(...)來訪問原型對象。
對象以原型爲模板,從原型繼承方法和屬性,通過原型鏈的方式,上游的原型鏈的屬性和方法傳遞給下游的原型鏈對象https://blog.csdn.net/qq_41889956/article/details/84172745(這是我寫的原型鏈的文章)。
因爲所有對象都是"Object"的實例,所以纔會擁有"valueOf(...)"、"hasOwnProperty(...)"、"toString(...)"方法。
舉個例子
var bar={
a:2
}; //原型鏈: bar.prototype---->Object.prototype---->null
console.log(bar.__proto__); //Object的屬性和方法
console.log(bar.hasOwnProperty("a")); //true 從Object中繼承過來的方法
我們通過創建對象,繼承了bar的原型對象的方法"hasOwnProperty(...)",
var a=3;
function foo() {
return this.a
}; //原型鏈: foo.prototype---->Function.prototype---->Object.prototype---->null
console.log(foo.call(this)); //3
因爲foo()函數的原型對象是"Function" ,所以它擁有"call(...)"方法。
var foo=["a","bas",11]; //原型鏈: foo.prototype---->Array.prototype---->Object.prototype---->null
console.log(foo.length); //3
因爲"foo"數組繼承與"Array",所以它擁有"length"屬性。
var foo={
a:2
};
var bar=Object.create(foo); //將foo作爲bar的原型
console.log(foo.a); //2
console.log()
console.log(bar.__proto__); //得到bar的原型對象
1.1.1 訪問對象屬性
當我們訪問對象中的屬性時,會發生[[Get]]操作,此操作爲訪問對象屬性時默認觸發。
它涉及到的操作是:
- 首先在對象中檢索同名屬性,查找到則返回,查找不到則下一步
- 遍歷上游的原型鏈,在原型鏈上查找此屬性,找到則返回,找不到則下一步
- 返回"undefined"
值得一提的[[Get]]操作只會遍歷此對象上游的原型鏈
var foo={
a:2
};
var bar=Object.create(foo); //原型鏈:bar.prototype--->foo.prototype--->Object.prototype--->null
var baz=Object.create(bar); //原型鏈:baz.prototype--->bar.prototype--->foo.prototype--->Object.prototype--->null
baz.b=3;
console.log(bar.a) ; //2 bar中不存在a屬性,於是從bar的原型對象foo開始遍歷原型鏈
console.log(bar.b); //undefined
// bar中不存在a屬性,於是從bar的原型對象foo開始遍歷原型鏈,因爲b屬性在下游的baz中,於是不能查找到
而與[[Get]]操作相同的是"for...in"循環,此循環會將再原型鏈上的可枚舉屬性一一枚舉出來,使用in操作符會將原型鏈上的屬性(無論是否被枚舉)枚舉出來。
1.1.3 屏蔽
在我們訪問對象的屬性時,會觸發[[Get]]操作,那麼當我們在修改對象屬性時,也會觸發[[Put]]操作。
讓我們來看看在涉及到原型的情況下,[[Put]]會如何操作
- 當對象中存在該屬性,修改它
- 當對象中不存在該屬性,原型鏈上存在該屬性,根據種種情況發生屏蔽或者不屏蔽
- 當對象中不存在該屬性,且原型鏈上不存在該屬性,那麼在該對象中創建此屬性並賦值
舉個例子:當對象中存在該屬性時
var foo={
a:2
};
foo.a=3; //[[Put]]操作
console.log(foo.a); //3
當對象中存在該屬性時,修改此屬性的值,
接下來我們對第二種情況進行分析
①當對象中不存在該屬性,且該屬性存在原型鏈中,它的屬性描述符(writable:true),則發生屏蔽
var foo={
a:2
};
var bar=Object.create(foo); //默認創建的bar,它的屬性描述符都是true
//原型鏈:bar.prototype--->foo.ptototype--->Object.prototype
console.log(bar.a); //2
bar.a=3;
console.log(bar.a); //3 原型鏈上的屬性在下游發生了屏蔽!
console.log(foo.a) //2 foo中的a沒有發生屏蔽
可以看到原型鏈上游的屬性"a"被下游的屏蔽了。
②當對象中不存在該屬性,且該屬性存在原型鏈中,它的屬性描述符(writable:false),則不發生屏蔽,如果在嚴格模式下還會報錯
var foo={};
Object.defineProperty(foo,"a",{
wrarable:false,
value:2
});
var bar=Object.create(foo);
bar.a=3;
console.log(foo.a); //2
console.log(bar.a); //2 沒有發生屏蔽
③當對象中不存在該屬性,且原型鏈上存在此屬性,該屬性還是一個"setter",那就會調用這個setter,此屬性不會添加到對象中,也就是不會發生屏蔽,這個setter也不會被重新定義。
總的來說只有第一種情況:對象中不存在該屬性,且原型鏈中存在該屬性,該屬性還是一個普通屬性描述符"writable"爲"true",纔會發生屏蔽。
1.2 可調用的對象函數原型
總所周知,函數是可調用的對象,是比較特殊的對象,它除了擁有對象都會擁有的屬性._proto_以外,還有函數纔會擁有的屬性prototype,此屬性同樣指向一個值,不同的是,此值指向的是構造調用函數創建的對象的原型對象。
這聽起來有點拗口,舉個例子:
function foo() {
a=2
};
var bar=new foo(); //new構造調用
console.log(bar.__proto__===foo.prototype); //true
可以看到,bar的原型對象(bar._proto_訪問)===foo.prototype。所以
函數foo的屬性prototype指向的對象是new創建對象bar的原型對象,
那麼,此對象是否就是函數foo的原型對象呢?檢驗出真理,在以上代碼中我們添加上
console.log(bar._proto_===foo._proto_) //false
此對象不是函數foo的原型對象。
那麼這樣做的原型鏈是怎樣的呢?我們來實驗一下,通過控制檯輸出各對象的原型對象
function foo() {
a=2
};
var bar=new foo();
console.log(bar.__proto__); //bar的原型對象
console.log(foo.__proto__); //foo的原型對象Function
console.log(bar.__proto__.__proto__); //Object
console.log(foo.__proto__.__proto__); //Object
結果:
完整的原型鏈是
1.2.1 繼承的屬性和方法
函數也是對象,那麼在訪問對象的屬性時,同樣會觸發[[Get]]操作,過程與之前對象中的一致。
在原型鏈中並沒有將原型對象的屬性和方法複製給創建的對象,而是通過原型鏈進行引用
舉個例子:
function foo() {
a=2
};
var bar=new foo();
bar.b=3;
console.log(bar.hasOwnProperty("b")); //true
在bar中沒有"hasOwnProperty(...)"方法,此方法作用是在對象中查找參數屬性並返回布爾值,此方法也並不存在bar的原型對象中,而是存在bar的原型對象的原型對象中,也就是Object中。
1.2.2 Prototype屬性:繼承成員被定義的地方
但是這是時候問題來了,Object中有不少的屬性和方法,爲什麼只有部分能夠被原型鏈使用呢?
其實在Object中只有帶有"Object.prototype"前綴的方法才能使能下游的原型鏈繼承。爲什麼呢?
在MDZ中使這麼解釋的:
繼承的屬性和方法都是定義在prototype屬性之上的,你可以稱之爲子命名空間-----那些以"Object.prototype"開頭的屬性,而非僅僅以"Object."開頭的屬性。"prototype"屬性的值是一個對象,我們希望被原型鏈下游的對象繼承的屬性和方法,都儲存在其中。
簡單來說,只有"Object.prototype"的屬性才能被繼承,比如"Object.prototype.valueOF(...)",而"Object."得屬性只能被"Object()"本身使用,比如"Object.keys(...)"
1.2.3 constructor屬性
“其實每個實例對象都從原型中"繼承"了一個constructor屬性”許多的JavaScript初學者會認爲時如此,但是我們暫時這麼理解,隨後再解釋。
該屬性指向了構造此對象的函數。
function foo() {
var a=3;
};
var bar=new foo();
console.log(bar.constructor); //返回foo()這個構造器
值得一提的是,不僅是通過構造函數調用創建的對象bar有constructor屬性,bar的原型對象bar._proto_或者說是foo.prototype也具有屬性constructor,且都指向foo函數本身。
function foo() {
};
var bar=new foo();
console.log(bar.constructor===foo); //true
console.log(foo.prototype.constructor===foo); //true
一個小技巧是:你可以在constructor後加一個小括號,從而用這個函數創建一個對象,
var baz=new bar.constructor()
正常工作下,你不會用到此方法,但是在某些情況下,不知道該構造函數,但是又想要通過此構造函數構造新的實例。這種方法就很有用了。
你會本能認爲bar.constructor是由foo函數構造時生成的,但是很遺憾並不是,在bar對象中不存在.constructor屬性,此屬性是通過默認的[[Prototype]]委託被bar使用的。同樣foo.prototype.constructor也是通過委託使用的。
舉例來說,foo.prototype的.constructor屬性是foo函數在聲明時的默認屬性。如果你創建了一個新對象並替換掉了函數默認的.prototype引用的話,那麼新對象不會獲得.constructor屬性。
試驗一下:
function foo() {
a:2
};
foo.prototype={ //創建一個新的原型對象
b:3
};
var bar=new foo();
console.log(bar.constructor===foo); //false
console.log(foo.prototype.constructor===foo); //false
console.log(bar.b); //3 依舊可以使用foo.prototype中的屬性
如果按照之前的理解,constructor是從原型中繼承了foo函數的constructor屬性,那麼此時"bar.constructor===foo"應該是true,bar的構造函數是foo纔對。但是輸出的卻是"false"。
bar並沒有constructor屬性,所以它會委託原型鏈上的foo.prototype。但是這個對象中也沒有"constructor"(默認的foo.prototype存在此屬性,但是由於這是新創建的原型對象所以不存在此屬性),所以它會繼續委託,這次委託給原型鏈頂端的"Object.prototype"。這個對象有constructor,指向內置的Object(...)函數。
試驗一下,我們在上述代碼中添加
console.log(bar.constructor===Object); //true
可見bar.constructor指向的是Object。
其實即使是創建的普通對象都會從Object中委託得到屬性construtor,指向的是Object對象。
創建的函數對象是不會從Object中得到屬性constructor,函數對象的原型對象是"Function",它的constructor屬性由此而來
function foo() {
};
var bar={};
console.log(bar.constructor===Object); //true
console.log(foo.constructor===Function); //false
總結:
不能簡單的把constructor簡單理解爲指向構造函數,這會讓你吃大虧。
對象中是不存在constructor屬性的,它能夠引用,是因爲原型鏈上游的原型對象的委託(繼承)。
普通對象的constructor是來自於Object的委託(繼承)(在Object中存在指向內置的Object函數),指向的是Object
函數對象的constructor是來自於Function的委託(在Function中存在指向內置的Function函數)。指向的是Function。
所以最好不要輕易的使用constructor屬性。
2 修改原型
2.1.1 直接修改
當我們想要修改原型的話,比如往裏面添加一些屬性和方法,那該怎麼做呢?
實際上我們可以通過"對象._proto_"往原型內添加屬性和方法,或者通過構造函數調用"函數.prototype"
舉個例子:
function foo() {
var a=2
};
foo.prototype={ //顯式往bar的原型對象中添加屬性b
b:3
};
var bar=new foo();
var baz={
c:4
};
baz.__proto__={ //顯式往baz的原型對象中添加屬性d
d:5
};
var bax=Object.create(baz);
console.log(bax.d); //5
console.log(bar.b); //3
事實上我們在構造函數中定義屬性,在原型對象中定義一個又一個的方法,提高代碼的可讀性。
這裏的構造函數定義屬性,實際上是用到了創建對象屬性的其中一種,構造函數創建。
舉個例子:
function foo(a,b) { //定義屬性
this.a=a;
this.b=b;
};
foo.prototype.x=function () { //定義方法x
return this.a+this.b;
};
foo.prototype.y=function () { //定義方法y
return this.a*this.b;
};
var bar=new foo(2,3);
console.log(bar.a); //2 證明在構造調用時,已經存在了a這個屬性
console.log(bar.b); //3 證明在構造調用時,已經存在了b這個屬性
console.log(bar.x()); //5
console.log(bar.y()); //6
2.1.2 Object.create(...)
前面我們已經接觸到了,我們可以使用Object.create來將兩個對象關聯到一起形成原型鏈。
舉個簡單的例子:
var foo={
a:2
};
var bar=Object.create(foo); //原型鏈:bar.prototype--->foo.prototype--->Object.prototype--->null
console.log(bar.a); //2
可以看到,我們使用Object.create創建的對象的原型對象是foo。因此它可以輸出foo的屬性a。
其實Object.create還有更復雜的用法:
function foo(name) {
this.name=name;
};
foo.prototype.myName=function () {
return this.name
};
function bar(name,label) {
foo.call(this,name); //顯式綁定foo中的this指向當前對象bar
this.label=label;
};
//創建了一個新的bar.prototype對象且關聯到foo.prototype
//原型鏈:bar.prototype--->foo.prototype--->Object.prototype--->null
bar.prototype=Object.create(foo.prototype); //此作用是“繼承”foo中的name以及foo.prototype.myName(...)
bar.prototype.myLabel=function () {
return this.label;
};
var a=new bar("a","obj");
console.log(a.myName); //a
console.log(a.myLabel); //obj
此代碼中最爲核心的代碼是"bar.prototype=Object.create(foo.prototype)",創建一個新的對象bar.prototype,並把foo.prototype作爲它的原型對象,這麼做有什麼好處呢?在我們創建實例時,根據原型鏈的規則,這個實例自動擁有原型鏈上游的屬性和方法,就本例來說,創建實例a時會自動繼承bar.prototype中的屬性"name,label"和方法"myLabel",因爲我們使用"bar.prototype=Object.create(foo.prototype)"所以我們同樣擁有"foo.prototype"的屬性和方法。
注意以下的兩種方式,試圖達到"bar.prototype=Object.create(foo.prototype)"但是都失敗了。
bar.prototype=foo.prototype //達不到想要的效果,只是對foo.prototype的引用,當你執行.myLabel語句時,會直接修改 //foo.prototype本身
bar.prototype=new foo.prototype(); //基本能達到你的目的但是會有副作用
2.1.3 Object.setPrototypeOf(...)
在ES6之前,我們無法準確的獲得對象的原型對象,只能通過"_proto_"這種不標準且部分瀏覽器不兼容的屬性,但是在ES6正式推出了訪問對象的原型對象的方法"Object.getPrototypeOf(...)"。
所以我們一般訪問對象的原型對象時,採用的方法是"Object.getPrototypeOf(...)",而與之對應的是設置對象的原型對象的方法"Object.setPrototypeOf(...)"。
此方法於"Object.create(...)"不同之處在於它不會創建一個新的原型對象,捨棄舊的原型對象。
就以上的代碼來說:
bar.prototype=Object.create(foo.prototype); //拋棄之前的bar.prototype
bar.prototype=Object.setPrototypeOf(foo.prototype); //直接修改現有的bar.prototype
在何時用着兩種方法都合適,但只是第一種會帶來輕微的性能損耗(拋棄掉的對象要進行垃圾回收)。
3 查看原型對象
假如在一個代碼裏,我們如何知道一個對象的原型對象呢?
通常在面向對象類語言中檢查一個實例(JavaScript中的對象)的繼承祖先(JavaScript中的原型鏈又叫做委託關聯)通常稱爲內省(或者稱之爲反射)
3.1 instanceof
此方法侷限比較多,只能判斷對象和可調用對象之間的關係。如果要判斷兩個普通對象之間的關係,那就要用到別的方法了。
function foo() {
};
foo.prototype.a=2;
var bar=new foo();
console.log(bar instanceof foo); //判斷bar的原型鏈中是否存在一個對象指向foo.prototype
instanceof只能站在“類”的角度來判斷,它的左邊是一個普通對象,右邊是一個函數,此關鍵字回答的問題是:在bar的整條原型鏈中是否有指向foo.prototype的對象?
注意!!!如果使用內置的.bind函數來生成一個硬綁定函數的話,該函數是沒有prototype屬性的。在這樣的函數上使用instanceof,目標函數的prototype會替換硬綁定函數的prototype。
關於關鍵字"instanceof"更多的細節可以移步至https://www.ibm.com/developerworks/cn/web/1306_jiangjj_jsinstanceof/
3.2 isPrototypeOf方法
不同於"instanceof",此方法更加簡潔,而且能夠判斷兩個普通對象之間的關係。
此方法是檢測對象A是否存在於對象B的上游原型鏈中,對象B.prototype----->對象A.prototype。
對象A.isPrototypeOf(對象B)=true------>A對象是B對象的原型對象。
對象A.isPrototypeOf(對象B)=false------>A對象不是B對象的原型對象。此情況分爲兩種情況,一是A對象不存在對象B的原型鏈上,二是A對象存在B的原型鏈上,但是A對象是處於下游的原型鏈對象,A.prototype--->B.prototype
值得一提的是此方法只能檢測A對象是否在原型鏈上而不能檢測A是否就是B的直接原型對象。
例如:B對象.prototype--->C對象.prototype--->A對象.prototype。
這時 A對象.isPrototypeOf(B對象)=true
function foo() {
};
foo.prototype.a=2;
var bar=new foo();
var baz=new foo();
var bax=Object.create(bar);
console.log(bar.isPrototypeOf(foo)); //false foo與bar不處在同一條原型鏈上
console.log(baz.isPrototypeOf(bar)); //false bar與baz的原型對象都是foo.prototype
console.log(foo.prototype.isPrototypeOf(bar)); //true
console.log(bax.isPrototypeOf(bar)); //false
console.log(bar.isPrototypeOf(bax)); //true
從以上代碼可以看出,isPrototypeOf(...)方法不僅能證明對象和函數的關係,還能證明兩個普通對象之間的關係。
3.3 Object.getPrototype(...)
這個方法是用於直接得到參數對象的原型對象,是直接原型對象。
var foo={
a:2
};
var bar=Object.create(foo); //原型鏈:bar.prototype--->foo.prototype--->Object.prototype--->null
var bax=Object.create(bar); //原型鏈:bax.prototype--->bar.prototype--->foo.prototype--->Object.prototype--->null
console.log(
Object.getPrototypeOf(bar)===foo //true
);
console.log(Object.getPrototypeOf(bax)===foo); //false
從結果中可以看出,bar的原型對象是foo,於是"Object.getPrototypeOf(bar)===foo"輸出"true",我們新創建了一個對象bax,它的原型對象是bar,我們使用"Object.getPrototype(bax)===foo",輸出"false"。證明Object.getPrototype(...)能夠直接獲取參數對象的原型對象。
3.4 _proto_屬性
_proto_屬性不是JavaScript中的標準屬性,但是絕大多數瀏覽器都能夠實現,它就如同對象中的[[Prototype]]一樣,屬性指向的對象是當前對象的原型對象。你可以直接用"對象._proto_"就能代表該對象的原型對象。
var foo={
a:2
};
var bar=Object.create(foo);
console.log(bar.__proto__===foo); //true
但是就像之前說的"constructor"一樣,此屬性並不存在於對象中,而是像常用的函數(toString(...)、valueOf(...))一樣存在於內置的Object.prototype中。
_proto_看上去更像一個setter和getter,它是一個訪問描述符,讓我們來看看它在Object中的定義
Object.defineProperty( Object.prototype, "__proto__", {
get: function() {
return Object.getPrototypeOf( this );
},
set: function(o) {
// ES6 中的 setPrototypeOf(..)
Object.setPrototypeOf( this, o );
return o;
}
} );
因此,當你使用"對象._proto_"時,實際上是調用了"對象._proto_( )"調用了getter函數。
_proto_代碼中具有setter函數,證明_proto_是可以設置的,但是不建議你修改已創建對象。
4 原型的作用
通常來說,原型以及原型的作用是:當對象中查找不到相關屬性時,會到對象的原型對象,相關的原型鏈上查找。更爲通俗的講的話,原型就相當於對象的“備用”庫,是處理對象“缺失”屬性和方法的一種備用選項。但是,你要在設計上更好的“包裝”它,纔會讓你的代碼清晰明瞭。
看以下代碼:
var foo={
cool:function () {
console.log("cool");
}
};
var bar=Object.create(foo);
bar.cool(); //cool
由於原型以及原型鏈的作用,你的代碼能夠正常工作,但是你這樣寫只是爲了讓"bar"能夠在無法處理屬性或方法時可以使用備用的"foo",那麼你的軟件就會變得有點神奇,難以理解和維護。
JavaScript中有一種高級功能叫做“代理”,實現的是方法無法找到時的行爲
當你設計軟件時,需要用到foo.cool()這個方法,如果bar中不存在cool()這條語句也可正常工作時,那你的API設計就會很奇怪,對於維護你軟件的開發者來說這可能不太好理解。比如上面Negev例子,你在foo中表示有cool方法,bar中沒有cool方法,但是你的bar卻能使用cool方法!!!
但是你可以讓你的API設計不那麼奇怪,同時仍能使用原型鏈。
var foo={
cool:function () {
console.log("cool");
}
};
var bar=Object.create(foo);
bar.doCool=function () { //建立了一個內部委託,在內部將cool封裝成bar的方法
this.cool();
};
bar.doCool();
這是使用了內部委託的形式,在bar中創建一個方法,調用原型鏈上的cool方法,將此方法進行封裝。
總結:
原型實際上就是對於一個對象的引用,在面向對象的編程語言中把這個叫做繼承,但是繼承不能表達JavaScript中的這種行爲,叫委託或許更好一點,因爲在面向對象中,繼承是父類中的屬性和方法複製到子類中,而委託確是保持對屬性和方法的引用。
我們在訪問對象的屬性時,會觸發[[Get]]操作,此操作一開始會在對象中查找屬性,查找不到就會開始遍歷對象的原型鏈(從此對象開始),實在查找不到的話,返回"undefined"。
所有原型鏈的最後一個環節是"Object.prototype",因此在"Object"中定義的帶有"Object.prototype"前綴的屬性和方法,所有對象都能使用,比如說"_proto_" "constructor"以及"toString(...)"
修改原型對象的方法有很多,最常用的是"new"、"Object.create(...)"以及"Object.setPrototypeOf(...)",
-
new:var bar=new foo(); bar是普通對象,foo是函數對象,bar的原型對象是foo.prototype
-
Object.create(...): var bar=Object.create(foo)。bar和foo都是普通對象,bar的原型對象是foo。此方法略微減少性能,因爲它會捨棄原有的bar原型對象
-
Object.setPrototypeOf(...):var bar=Object.setPrototypeOf(foo)。與上相同,唯一不同的是它直接修改bar的原型對象,性能不會減少。
要想得到屬性的原型對象,推薦使用"Object.getprototypeOf(...)"。
原型鏈真正的作用是“備用”,當作對象屬性和方法的備用庫,爲了讓代碼看上去更加簡潔清晰,所以我們採用內部委託將從原型鏈繼承過來的屬性和方法封裝成自己的屬性。