一個專注於web技術的80後
你不用拼過聰明人,你只需要拼過那些懶人 你就一定會超越大部分人!
首先我們瞭解一下普通物件 與 函式物件我們在學習原型之前首先了解一下javascript當中的 普通物件 和 函式物件,
如圖1
普通物件:最普通的物件:具有_ _ proto_ _這個屬性它(指向其原型鏈),注意: 普通的物件是沒有prototype這個屬性的! 如果你呼叫必然返回undefined原型物件: 我們會在下面繼續說明!javascript中哪些情況屬於普通物件 如下程式碼:
//普通物件 function Test() { } var obj1 = new Test(); var obj2 = new Object(); var obj3 = {}; console.log(typeof obj1); //Object console.log(typeof obj2); //Object console.log(typeof obj3); //Object
函式物件:凡是用Function()建立的都是函式物件。 比如:自定義函式, 事件函式 系統的Object、Array、Date、String、RegEx 以上都屬於 函式物件
==小提示:== 以上的都是Function的例項物件, 那麼也只有例項才會有_ _ proto_ _ 這個屬性
注意: 這裡的Function是比較特殊的函式物件, 因為Function.prototype本身它應該指向是原型物件, 但Function的prototype卻是函式物件 上圖已經有說明!
javascript中哪些情況屬於函式物件 如下程式碼:
//函式物件 function F1(){ } var F2 = function(){ } var F3 = function(a,b){ } window.onload=function () { var div1=document.getElementById('div1'); div1.onclick=function () { alert(1); } console.log(typeof div1.onclick); //function } console.log(typeof F1); //function console.log(typeof F2); //function console.log(typeof F3); //function console.log(typeof Object); //function console.log(typeof Array); //function console.log(typeof String); //function console.log(typeof Date); //function console.log(typeof RegEx); //function console.log(typeof Function); //function
函式物件都是Function的例項物件
就如同Array是透過Function創建出來的。
因為Array是Function的例項,是例項就會有._ _ proto_ _ 這個屬性, 從上面的流程圖上看 一個函式物件的_ _ proto_ _屬性值是: ƒ () { [native code] }
而特殊的Function.prototype的值也是一個: ƒ () { [native code] }
所以我們可以推斷出一個條件: 函式物件._ _ proto_ _ === Function.prototype 是成立的! 返回true
由此引出下面判斷條件:
Array._ _proto_ _ == Function.prototype //true String._ _proto_ _ == Function.prototype //true RegExp._ _proto_ _== Function.prototype //true Date._ _proto_ _ == Function.prototype //true
ƒ () { [native code] } 是什麼?native code 的意思是它是程式自帶的,是二進位制編譯得無法顯示出來程式碼, native code是原生代碼, 這裡我們就簡單地解釋一下即可!
以上內容作為學習原型之前的鋪墊了解即可!! 接下來我們就慢慢地拆解圖1的細節內容!!
引出兩個問題問題1 效能方面
如果建立一個物件 在記憶體的堆區中就會開闢一個空間來儲存物件,如果每個物件裡面有相同的方法也會被創建出來
這樣就存在一個問題,就是公共的方法或者屬性會存在記憶體當中n份.. , 大量的佔用了記憶體開銷!
如圖2
每一個物件都生成了同樣的say()方法, 這種程式碼中如果每個物件都有公共一樣的方法 就顯得很佔據記憶體空間!
上圖的程式碼如下
function Person(name,age){ this.name=name; this,age=age; this.say=function () { console('輸出結果'); } } var obj1=new Person(); var obj2=new Person(); var obj3=new Person(); var obj4=new Person(); var obj5=new Person();
問題2 屬性 方法 不能共享!
有時候我們希望一個方法能夠被多個相同物件型別都可以公共的進行使用!
例如: 定義一個數組私有方法, 而另外一個數組物件是不能訪問這個私有方法的
程式碼案例:
var arr=[5,5,10]; //arr陣列物件的sum方法 arr.sum=function () { var result=0; for(var i=0;i<this.length;i++){ result+=this[i]; } return result; } console.log(arr.sum()); //結果: 20 var arr2=[10,10,10]; console.log(arr2.sum()); //結果: Uncaught TypeError: arr2.sum is not a function
解答: 這裡報錯是因為 arr2 這個陣列物件 根本就不存在sum() 這個方法, 它是屬於arr陣列物件私有的一個方法!
所以有時候我們希望一個方法能夠被多個相同物件型別都可以公共的來使用!
以上兩個問題 , 問題1是效能最佳化不足,問題2是私有方法不能被相同型別的物件呼叫 , 所以解決上述問題方法或屬性不能共享的辦法 就要用到: 原型 [提高效能] 也就是通常說的: 原型模式
接下來我們就來探討一下原型是什麼!
原型物件是什麼!根據圖1 函式物件都有一個prototype屬性指向的是一個原型物件, 那麼我們可以推出以下概念:
每建立一個函式都會有一個prototype屬性,這個屬性是一個指標,指向一個物件(原型物件)
原型物件,也就是(建構函式.prototype), 當中含有一個constructor 屬性 這個屬性(指向的就是當前原型物件的建構函式)
如下圖:
原型物件作用:是包含特定型別的所有例項共享的屬性和方法, 就是說你把屬性和方法定義在原型物件裡面之後,那麼這個型別的例項就都會共享這些屬性和方法!
原型物件優點: 就是可以讓所有例項物件共享它所包含的屬性和方法。
原型物件的語法基礎要特定型別的所有例項都共享的屬性和方法, 就要把它們定義在原型物件的下面!
原型: 要使用prototype這個關鍵字, 要寫在建構函式的下面:
//語法如下 建構函式名.prototype.屬性=值; 建構函式名.prototype.方法=function(){ ..程式碼段.. }
如下圖: 所以用: 建構函式名.prototype 你就可以把屬性和方法定義在原型物件當中
案例程式碼:
function createPerson(name,age) { this.name=name; this,age=age; } createPerson.prototype.say=function () { console.log('我的名字叫'+this.name); } var a=new createPerson('張三','33'); var b=new createPerson('李四','55'); var c=new createPerson('王武','66'); a.say(); b.say(); c.say();
為了方便理解 我畫了一張圖, 以上程式碼的流程圖分析如下圖:
特殊的Function.prototypeFunction.prototype是個例外,為什麼說它是一個例外呢? 按道理來說它這裡獲取出來的應該是一個原型物件,但卻是一個函式物件,
作為一個函式物件,它又沒有prototype屬性。 從圖1中就可以看出來這個道理!
你用Function.prototype 獲取出來是一個: ƒ () { [native code] } 這個東西是什麼上面已經解釋過了!
如下圖:
原型知識點為了節省記憶體,我們把物件的方法都放在原型裡面。為什麼呢?
因為改寫物件下面公用的方法或者屬性、讓公用的方法或者屬性在記憶體中只存在一份
在我們透過new例項化物件的時候,在記憶體中,會自動複製建構函式中的所有屬性和方法,用來給例項物件賦值,而不管我們例項化多少次,原型裡面的屬性和方法只生成一次,所以會節省記憶體。
普通定義方式與原型定義的優先順序高低如下程式碼:
function createPerson(name,age) { this.name=name; this,age=age; } createPerson.prototype.say=function () { console.log('我的名字叫'+this.name); } var a=new createPerson('張三','33'); var b=new createPerson('李四','55'); var c=new createPerson('王武','66'); a.say(); b.say(); c.say(); //普通定義的優先順序高於原型prototype c.say=function(){ console.log('輸出ok'); } c.say();
所以以上的普通定義的方式要比原型定義的方式的優先順序高!,但這並不是把原型覆蓋了 只是優先呼叫普通定義的方法
如圖:
原型的中的 _ _ proto_ _首先回顧一下, 例項化new的時候,系統會在記憶體中建立了一個空的物件,就像是這樣 var p = {} , 複製建構函式中的屬性和方法到空物件中。
重點的是: 每個例項化物件都會有一個 _ _ proto_ _ 屬性, 這個屬性是自動生成的, _ _ proto_ _ 屬性指向類的原型物件。
建構函式、例項化物件、原型物件的之間的關係
先來看一段程式碼案例
function createPerson(name,age) { this.name=name; this,age=age; } createPerson.prototype.say=function () { console.log('我的名字叫'+this.name); } var obj=new createPerson('張三','33'); console.log(obj.__proto__); //例項化obj的__proto__屬性 可以獲取到原型物件 console.log(createPerson.prototype); //建構函式的prototype屬性 也可以獲取到原型物件 console.log(obj.__proto__.constructor); //原型物件中的constructor又可以獲取到建構函式
建構函式、例項化物件、原型物件的基本關係圖分析當然你也可以透過 例項化物件的_ _ proto_ _屬性 和 建構函式的prototype屬性進行比較可以驗證結果, 我們可以透過列印來驗證
console.log(例項化物件._ _proto_ _ === 建構函式.prototype); //true
定義在例項和定義在原型下的區別總結:
先看一段程式碼案例:
function Test(){ } //定義屬性 Test.prototype.name = "張三"; Test.prototype.age = 33; //定義方法 Test.prototype.getAge = function(){ return this.age; } var t1 = new Test(); var t2 = new Test(); var t3 = new Test(); t3.name = "李四"; console.log(t1.name); // 張三 來自原型 console.log(t2.name); // 張三 來自原型 console.log(t3.name); // 李四 來自例項 //列印例項看下圖結果 console.log(t1); console.log(t2); console.log(t3);
以上圖就解釋了為什麼定義在原型中 屬性和方法是公用的, 而單獨定義在例項中是屬於獨立的屬性和方法不共有!
所以 我們也推斷出 在例項中定義屬性和方法 會覆蓋 或者說 會實現呼叫例項中定義的屬性和方法 如果沒有才會到原型中去尋找! 這裡其實就是我們一會要講到的原型鏈!
_ _ proto_ _與 prototype的詳細認識1.所有的引用型別,比如陣列、物件、都有一個_ _ proto_ _屬性(也叫隱式原型,它來指向自己的原型物件)
重點再次提醒: 所有的物件引用 都有_ _ proto_ _ 這個屬性! 記住了!
透過下面的測試我們不難發現,其中它們賦值的引用物件中 打印出來看到都有一個 _ _ proto_ _的屬性 都是指向自己的原型物件
程式碼如下:
function createPerson(name,age) { this.name=name; this,age=age; } createPerson.prototype.say=function () { console.log('我的名字叫'+this.name); } var obj=new createPerson('測試','33'); //物件引用列印 console.log(obj); var arr=[1,2,3]; //陣列引用的列印 console.log(arr); var arr2=new Array(2,2,2); //陣列引用的列印 console.log(arr2);
2.再一次重點注意: 所有引用型別,它的_ _ proto_ _屬性指向這個引用本身的原型物件 而建構函式的prototype屬性的值也就是指向的原型物件
所以在各自相應引用型別的_ _ proto_ _屬性 和 建構函式的 prototype屬性 彼此它們兩個是相等的! 上面的圖中也可以表明這一點!
_ _ proto_ _屬性 和 建構函式的 prototype屬性比較, 案例程式碼如下:
//案例1 function createPerson(name,age) { this.name=name; this,age=age; } createPerson.prototype.say=function () { console.log('我的名字叫'+this.name); } var obj=new createPerson('張三','33'); console.log(obj.__proto__); //打印出obj物件引用的原型物件 console.log(createPerson.prototype); //打印出createPerson建構函式的原型物件 console.log(obj.__proto__===createPerson.prototype); //而且它們是相等的,指向同一個原型物件 //案例2 var arr=new Array(2,2,2); console.log(arr.__proto__); console.log(Array.prototype); console.log(arr.__proto__ === Array.prototype);
3.所有的建構函式 或者 普通函式都有一個prototype屬性 (這也叫顯式原型,它也指向自己的原型物件)。
案例程式碼:
//普通函式 function Test() { } //列印普通函式的prototype屬性 console.log(Test.prototype); //建構函式 function createPerson(name,age) { this.name=name; this,age=age; } createPerson.prototype.say=function () { console.log('我的名字叫'+this.name); } console.log(createPerson.prototype);
圖解如下:
_ _proto _ _和 prototype區別prototype是每個函式都會具備的一個屬性,它是一個指標,指向原型物件,只有普通函式或 建構函式才有。
_ _ proto_ _屬性 是主流瀏覽器上在除null物件以外的每個引用物件上都支援存在的一個屬性,它能夠指向當前該引用物件的:原型物件 其實_ _ proto_ _ 就是用來將引用物件與原型相連的屬性
小結: 一個只有函式才有的屬性(prototype),一個是引用物件才有的屬性(_ _ proto_ _ ),
注意: 你用一個函式去呼叫屬性(_ _ proto_ _ ), 會得到一個: ƒ () { [native code] }
原型中批次新增屬性與方法使用prototype這個關鍵字, 要批次的把屬性和方法寫入原型 就在建構函式的下面寫一個JSON格式 如下程式碼, 這樣比單一的一個個寫方便!
//語法 建構函式.prototype={ 屬性名: 值, 方法名:function(){ //方法體... 這裡的this是什麼要看執行的時候誰呼叫了這個函式 }, 方法名:function(){ //方法體... 這裡的this是什麼要看執行的時候誰呼叫了這個函式 } }
案例程式碼:
createPerson.prototype={ aaa:123, // prototype物件裡面又有其他的屬性 showName:function(){ //this是什麼要看執行的時候誰呼叫了這個函式 console.log("我的名字叫:"+this.name); }, showAge:function(){ //this是什麼要看執行的時候誰呼叫了這個函式 console.log("我的年齡是:"+this.age); } } function createPerson(name,age) { this.name=name; this.age=age; } var obj= new createPerson('張三','33'); console.log(obj); obj.showName(); obj.showAge(); console.log(obj.aaa);
以上原型程式碼 圖解
原型注意事項
重點注意
如果是自定義建構函式,並且使用{ }這種方式批次的在prototype中定義屬性和方法, 會改變原型中constructor對建構函式的指向!
也就是說使用{ }這種方式批次在prototype中定義屬性和方法, 那麼constructor的指向就是一個函式物件
測試程式碼如下
function createPerson(name,age) { this.name=name; this,age=age; } /* createPerson.prototype.say=function(){ console.log('我的名字叫'+this.name); }*/ createPerson.prototype={ say:function () { console.log('我的名字叫'+this.name); } } var obj=new createPerson('張三','33'); console.log(obj.__proto__);
在控制檯輸出會看到 _ _proto _ _的值, constructor這個屬性就沒有了!
console.log(createPerson.prototype.constructor); //返回 ƒ Object() { [native code] }
原型基本小結
讓相同方法在記憶體中存在一份
原型定義方式要比普通定義方式的優先順序要低
在專案當中公共相同的屬性和方法可以載入在原型上
原型鏈 核心原理首先這裡要提出一點的是 在JS中實現繼承主要是依靠原型鏈來實現! ,所以我們才需要學習原型鏈的原理!
原型鏈核心概念
原型鏈: 當試圖呼叫或想得到一個物件例項中的屬性 或 方法時,如果這個物件本身不存在這個屬性 或 方法 也就是說建構函式中沒有定義你想要的屬性或方法,那麼就會透過建構函式的prototype屬性到原型中去 尋找這個屬性 或者 方法 (也就是它的建構函式的’prototype’屬性會到原型物件中去尋找) , 如果有就返回,如果沒有就會到頂層的Object去找 , 如果有就返回, 如果還是找不到就返回undefined!
原型鏈流程圖
上圖可以用以下例子來說明:當建構函式 Test 存在 getName 這個方法時,就不用到建構函式的原型當中去找 getName這個方法;反之,就到建構函式的原型當中去找 getName這個方法;如果建構函式的原型中也不存在 getName這個方法時,就要到頂層物件的原型中去找 getName這個方法。
上圖測試案例程式碼如下:
function Test(name){ this.name=name; this.getName=function(){ return this.name+"我在建構函式中"; } } Test.prototype.getName=function(){ return this.name+"我在原型物件中"; } Object.prototype.getName=function(){ return this.name+"我在頂層物件中"; } var t1=new Test('小紅'); console.log(t1.getName());
原型鏈案例程式碼2 如下:
Array.prototype.aaa=123; //把這個自定義屬性定義到原型物件下 var arr=new Array(1,2,3); console.log(arr.aaa); console.log(Array.prototype); var arr2=['重慶','上海','北京']; console.log(arr2.aaa);
如下圖:
小結: arr 和 arr2都能夠找到aaa這個屬性, 並且這個屬性是陣列原型物件下的屬性,是公共的
只要是陣列就可以呼叫這個屬性, 同理方法也是一樣,
所以建立很多很多個相同型別物件的時候, 創建出來的每一個物件,如果裡面都有一些公共的方法,這樣就會佔用很多的資源,
而透過原型來實現的話,只需要在構造函數里面給屬性賦值,而把方法寫在prototype屬性,當然屬性也是可以寫在原型當中的,
這樣每個物件引用都可以使用prototype屬性裡面的方法 或 屬性,並且節省了不少的資源
這就是我們為什麼要使用原型的原因!
原型鏈總體結構圖解小結當試圖呼叫或想得到一個物件例項中的屬性 或 方法時,如果這個物件本身不存在這個屬性 或 方法 那麼就會透過建構函式的prototype屬性到原型中去 尋找這個屬性 或者 方法 如果有就返回,如果沒有就會到頂層的Object去找 , 如果有就返回, 如果還是找不到就返回undefined!
但又因為建構函式中的prototype屬性值本身又是一個物件(原型物件), 所以這裡它也有一個_ _ proto_ _ 屬性 , 就可以向上繼續獲取_ _ _ proto_ _屬性 , 那麼這裡獲取出來的就是頂層的Object物件
如下圖:
小結:
當obj呼叫test()方法,JS發現Fn中沒有這個方法,於是它就去Fn.prototype中去找,發現還是沒有這個方法,然後就去Object.prototype中去找,找到了,就呼叫Object.prototype中的test()方法。
這就是原型鏈查詢,而則一層一層的連結的關係就是:原型鏈。
obj能夠呼叫Object.prototype中的方法正是因為存在原型鏈關係的機制!
另外,在使用原型的時候,一般推薦將需要擴充套件的方法寫在 建構函式.prototype屬性中,
而不要寫在: 建構函式.prototype._ _ proto _ _中, 因為這裡也就是Object頂層物件,定義到這裡的屬性和方法 所有JS物件都可以呼叫,在這裡面定義多了就會影響整體效能,所以不建議定義到這裡!
大家的支援就是我堅持下去的動力!