練習中使用的是IE10,如果有錯誤之處,還請各位朋友多多指教。本文關於原型難以描述,故多用代碼展示 原型是JS中一個很重要的概念,也是JS中一個痛點,語言上難以描述,原型對象的屬性和方法叫做原型屬性和原型方法,建構函式中的屬性和方法叫做執行個體屬性和執行個體方法,它們的區別就是:對於一個對象的多個執行個體之間,它們的執行個體屬性和執行個體方法是各不一樣的,前面的物件導向中已經證明,而他們的原型屬性和原型方法是一模一樣的,完全相等的 建構函式方式聲明原型對象: 複製代碼 1 //建構函式定義的成員變數時執行個體成員 2 function Box(user,age){ 3 this.user=user; //執行個體屬性 4 this.age=age; 5 this.run=function(){ //執行個體方法 6 return this.user+" "+this.age+" "+"運行中..."; 7 } 8 } 9 10 //原型聲明,建構函式中什麼也不寫,然後通過prototype對象來添加屬性和方法,調用都是一樣的11 function Box1(){};12 Box1.prototype.user='abc'; //通過prototype定義的叫做 原型屬性13 Box1.prototype.age=22; //下面這個方法叫做原型方法14 Box1.prototype.run=function(){ return this.user+" "+this.age+" "+"運行中..."; };15 16 var box=new Box1();17 alert(box.user); // abc18 alert(box.age); // 2219 alert(box.run()); // abc 22 運行中...20 21 //聲明了兩個對象 從下面的結果可以知道,這兩個對象的引用不相等,但是他們中間的方法和屬性是完全相等的 包括引用22 var box1=new Box();23 var box2=new Box();24 alert(box1.user); //abc25 alert(box2.user); //abc26 alert(box1.age); //2227 alert(box2.age); //2228 29 //如果是執行個體方法,不同的執行個體化,他們的方法的引用地址是不一樣的,是唯一的30 //但是原型對象,不同的執行個體化,他們的方法的引用地址也是一樣的,共用的,大家都一樣31 alert(box1.run==box2.run); // true32 alert(box1==box2); //false複製代碼 1、 原型的作用主要作用 用來共用一些屬性和方法,我們每建立一個函數,都會自動的建立一個原型對象,原型對象是由函數下面的一個屬性[__proto__]來指向的,這個屬性是一個指標,它指向了原型對象的constructor屬性,我們可以通過這兩個屬性就可以訪問原型對象中的屬性和方法了。 constructor是一個構造屬性,是可以擷取建構函式的本身的,它的作用其實就是被原型指標[__proto__]定位,然後擷取到建構函式的本身 1 alert(Box.prototype); //存取方法的屬性prototype2 alert(box1.prototype); //undefined 這個屬性是一個對象,是訪問不到的 3 alert(box1.__proto__); //object Object 低版本的IE可能列印不出來4 alert(box1.constructor); //function Box(){}; //構造屬性5 alert(Box.constructor); // function Function(){...}; Box 本身就是一個Function類型6 //alert(box1.prototype.constructor) //error 2、isPrototypeOf() 方法 判斷一個執行個體對象是否是指向了該建構函式的原型對象,可以用 isPrototypeOf() 方法來判斷,如果指向了返回為true,沒有則返回為false,一切對象都是繼承自Object對象 1 alert(Box.prototype.isPrototypeOf(box1)); //true 只要是執行個體化的,都指向了原型對象2 alert(Box.prototype.isPrototypeOf(box2)); //true3 alert(Object.prototype.isPrototypeOf(box1));//true 因為一切對象都是 Object 類型的,故指向了4 var box=new Object();5 alert(Box.prototype.isPrototypeOf(box)); // false box對象是Object類型的對象,Box其實類似於是繼承自Object類型 3、原型模型的執行流程:遵循JS中的就近原則 先尋找建構函式執行個體裡面的方法和屬性,即先尋找執行個體屬性,如果有,立即傳回值或者執行對應的方法 如果執行個體屬性或者執行個體方法中沒有,則去原型對象中尋找相應的屬性和方法,有就返回或執行方法,如果沒有就返回undefined或者報錯 複製代碼1 var box1=new Box();2 var box2=new Box(); 3 box1.name='kkk'; //給對象box1添加一個執行個體屬性,4 alert(box1.name); //訪問的是box1的執行個體屬性,box2是訪問不到的,因為原型屬性中也沒有5 alert(box2.name);6 box1.user='jjj'; //其實是執行個體屬性,並沒有重寫原型屬性的值7 alert(box1.__proto__.user);//abc 訪問原型屬性中的值8 alert(box1.user);//jjj 訪問的是執行個體屬性中的值9 alert(box2.user);//abc box2 中不存在執行個體屬性 user 就返回的是原型屬性,它訪問不到box1中的執行個體屬性,因為他們之間共用的只是原型屬性和方法複製代碼 4、屬性的刪除和修改 可以通過 delete 關鍵字來刪除執行個體屬性和原型屬性,刪除和修改原型屬性可以通過兩種方式:(1) 通過執行個體對象的指標修改:box1.__proto__.age; (2) 定義原型屬性一樣用建構函式修改 prototype: Box.prototype.age; 建構函式中是改了就生效,不管何時聲明的對象,而在後面的字面量形式中只有在聲明執行個體對象之前該才有效(詳細的見後面字面量形式建立中) 複製代碼1 delete box1.user; //刪除對象box1中的執行個體屬性2 alert(box1.user); //abc 因為前面刪除了執行個體屬性中的user屬性,返回的就是原型屬性中的user屬性3 //delete box1.__proto__.age; //通過這種方式可以刪除原型對象中的屬性,但是別這樣弄,牽一髮而動全身4 //delete Box.prototype.age; //也是刪除了原型對象中的屬性 age5 alert(box2.age); //undefined 因為前面刪除了原型屬性中的age屬性6 box1.__proto__.age=33; //修改了原型屬性中的值7 alert(box2.age); //338 Box.prototype.age=44; //修改了原型屬性中的值9 alert(box2.age); //44複製代碼 5、hasProperty() 方法和 in 操作符 判斷某個對象是否擁有某個執行個體屬性,可以通過 hasOwnProperty() 方法來測試,有就返回true,否則返回false in 操作符可以判斷對象中是否包含某個屬性,不管這個屬性是原型屬性還是執行個體屬性,包含則返回true 可以通過 hasOwnProperty() 方法和 in 操作符 共同來判斷某個對象是否包含某個原型屬性 複製代碼 var box1 = new Box(); var box2 = new Box(); 1 box1.name='jjj'; 2 alert(box1.hasOwnProperty('name')); //true 3 alert(box2.hasOwnProperty('name')); //false box2 並沒有執行個體屬性name 4 alert(box1.hasOwnProperty('user')); //false user 屬性是原型屬性,不是執行個體屬性 5 6 alert('name' in box1); //false 7 alert('user' in box1); //true 原型屬性 8 box1.name='jjj'; 9 alert('name' in box1); //true 執行個體屬性10 11 //通過 hasOwnProperty() 方法和 in 操作符 共同來判斷某個對象是否包含某個原型屬性12 function checkPropo(object,element){ //判斷的是在某個對象中的屬性,故要將對象和屬性傳遞過來13 if(!object.hasOwnProperty(element)){ //先判斷是否存在執行個體屬性,如果存在就返回false14 if(element in object){ //如果存在就返回true15 return true; 16 }else{17 return false;18 }19 }else{20 return false;21 }22 //上面的代碼可以用一個運算式來表示:return !object.hasOwnProperty(element)&& (element in object);23 }24 25 alert(checkPropo(box1,'name')); //false26 box1.name='name';27 alert(checkPropo(box1,'name')); //false 雖然有屬性 name,但是這是一個執行個體屬性28 alert(checkPropo(box1,'user')); //true //原型屬性 user 是存在的複製代碼 字面量形式建立原型對象: 複製代碼 1 function Box(){}; 2 Box.prototype={ // 通過字面方法來建立原型屬性和方法,這樣有點封裝的感覺 3 user:'abc', 4 age:123, 5 run:function(){return this.user+" "+this.age+" 運行中...";} 6 } 7 8 //建立兩個對象 9 var box1=new Box();10 var box2=new Box();11 //運行結果是一樣的12 alert(box1.run()); //abc 123 運行中...13 alert(box2.run()); //abc 123 運行中...14 alert(box1.run==box2.run); //true複製代碼 6、字面量方式建立原型對象注意的問題一:構造屬性的指向 字面量建立的方式 用constructor 屬性指向的是Object對象,而不是執行個體本身,但是建構函式方式建立的這相反; 如果想讓constructor指向執行個體[Box],可以採用強制指向的方式 複製代碼 1 var box1 = new Box(); 2 3 alert(box1.constructor); //function Object() { [native code] } 4 alert(box1.constructor == Box); //false 5 alert(box1.constructor == Object); //true 6 alert(box1 instanceof Box); //true 7 alert(box1 instanceof Object); //true 8 alert(Box.constructor); //function Function... 9 alert(Box.prototype); //object Object //使用建構函式名(對象名)訪問prototype10 alert(box1.__proto__); //object Object //使用對象執行個體訪問prototype的指標11 12 // 如果想讓constructor指向執行個體[Box],可以採用強制指向的方式13 function Box(){};14 Box.prototype={ 15 constructor:Box, //強制原型對象來指向Box16 user:'abc',17 age:123,18 run:function(){return this.user+" "+this.age+" 運行中...";}19 }20 21 //建立兩個對象22 var box1=new Box();23 var box2=new Box();24 25 //強制constructor指向Box的時候,返回結果如下26 alert(box1.constructor == Box); //true27 alert(box1.constructor == Object); //false複製代碼 之所以出現上面的原因是因為:通過Box.prototype={..};方式建立的時候,都建立一個新的對象,而每次建立一個函數,都會同時建立它自己的prototype,那麼這個新的對象也就會自動擷取它自己的constructor屬性,這樣新對象的constructor屬性重寫了Box執行個體的constructor屬性,因此會執行新的對象,而這個新的對象又沒有指定建構函式,故預設的就是Object 7、字面量形式建立原型對象的問題二:原型屬性的重寫 原型的聲明是有先後順序的,所以,重寫的原型會切斷之前的原型,故應該注意避免此種情況的發生 複製代碼 1 var box2=new Box(); 2 Box.prototype={ //字面量方式不存在修改原型屬性,直接是覆蓋掉原來的,因為每次都是建立一個新的對象 3 user:444 //這裡不會保留之前原型的任何資訊 4 //把原來的原型對象和建構函式對象之間的關係給切斷了 5 } 6 //字面量方式建立,只要聲明了,以後隨便怎麼重寫原型對象,已經建立的執行個體對象的值不會改變 7 var box1=new Box(); 8 9 alert(box1.user); // 444 重寫中賦值為44410 alert(box1.age); //undefined 因為第二次重寫中沒有這個屬性11 12 alert(box2.age);//123 因為在對象的定義是發生在用字面量形式重寫原型屬性之前,故以後的原型屬性的修改和box2無關複製代碼 9、通過原型模式擴充類型的方法 原型對象不僅僅可以在自訂對象的情況下使用, 而 ECMAScript 內建的參考型別都可以使用這種方式,並且內建的參考型別本身也使用了原型。 通過prototype來添加方法,訪問的時候要用這種類型的變數點這個方法,和訪問系統內建的方法是一樣的,但是最好不要用這種方式來添加方法,因為可能會存在命名衝突的問題,特別是在代碼量大的時候,容易照成命名衝突問題。 複製代碼 1 String.prototype.addString=function(s){ //傳遞一個參數過來, 2 return '【' + s + '】'; 3 } 4 5 alert('abc'.addString('111'));// 【111】 6 7 String.prototype.addString=function(){ //可以不進行傳參,通過this來代表當前調用這個方法 的字串 8 return this+"被添加了!"; 9 }10 11 alert("abcd".addString()); // abcd被添加了!複製代碼 原型模式建立對象的缺點以及採用的方式: 原型模式建立對象也有自己的缺點, 它省略了建構函式傳參初始化這一過程, 帶來的缺點就是初始化的值都是一致的。而原型最大的缺點就是它最大的優點,那就是共用。 原型中所有的屬性都能被很多執行個體共用,共用對於函數非常適合,對於包含基本值的屬性頁面還可以,但是如果屬性包含參考型別就一定存在問題,見代碼中 複製代碼 1 function Box(){}; 2 Box.prototype={ 3 constructor:Box, 4 user:'abc', 5 age:22, 6 family:['哥哥','姐姐','妹妹'], 7 run:function(){ 8 return this.user+" "+this.age+" 運行中..."; 9 }10 }11 12 //var box=new Box(123); //缺點之一就是不能夠傳遞參數13 14 var box1=new Box(); //缺點二就是原型的共用性,這也是它最大的優點15 alert(box1.family); //哥哥,姐姐,妹妹16 box1.family.push('弟弟'); //在第一個執行個體後修改了參考型別,保持了共用,但本質上不希望它共用17 alert(box1.family); //哥哥,姐姐,妹妹,弟弟18 19 var box2=new Box();20 alert(box2.family); //哥哥,姐姐,妹妹,弟弟 共用了box1參考型別添加後的原型複製代碼 11、組合建構函式 + 原型模式 這種方式能夠很好的解決傳參和引用共用的問題,是建立對象比較好的方法 複製代碼 1 function Box(user,age){ //建構函式,這裡面聲明一些會變的屬性和參考型別的屬性 2 this.user=user; 3 this.age=age; 4 this.family=['哥哥','姐姐','妹妹']; 5 } 6 7 Box.prototype={ //原型模式 8 constructor:Box, 9 run:function(){10 return this.user+" "+this.age+" 運行中...";11 }12 }13 14 //下面可以看出通過建構函式總寫一些自己會變的屬性等,即使值改變了也不會被共用出去15 var box1=new Box('abc',22);16 alert(box1.run()); // abc 22 運行中...17 alert(box1.family); //哥哥,姐姐,妹妹 18 box1.family.push("弟弟");19 alert(box1.family); //哥哥,姐姐,妹妹,弟弟20 21 var box2=new Box('jack',33);22 alert(box2.family); //哥哥,姐姐,妹妹 並沒有共用對象box1中的參考型別23 alert(box2.run()); //jack 33 運行中... 和box1 中的輸出結果是不一樣的複製代碼 11、動態原型模型 將建構函式和原型模型封裝在一起,也能夠解決共用的問題,但是要注意兩個問題,一是資源的浪費,還有就是要注意,不可以再使用字面量的方式重寫原型,因為會切斷執行個體和新原型之間的聯絡 在下面的代碼中,當第一次調用建構函式時,run()方法發現不存在,然後初始化原型。當第二次調用, 就不會初始化, 並且第二次建立新對象, 原型也不會再初始化了。 這樣及得到了封裝, 又實現了原型方法共用,並且屬性都保持獨立 複製代碼function Box(user,age){ this.user=user; this.age=age; this.family=['哥哥','姐姐','妹妹']; if(typeof this.run != 'function'){//判斷this.run是否存在,因為執行一次後類型傳回值就為為function alert("原型初始化開始");//沒有判斷之前,每new一次Box都會執行一次, Box.prototype.clss='person'; Box.prototype.run=function(){ return this.user+" "+this.age+" 運行中..."; }; alert("原型初始化結束"); } } var box1=new Box('abc',22); var box2=new Box('jack',33); //也可以判斷原型中任意一個屬性的類型傳回值,是否等於undefined,若等於原型就還沒有建立 //因為原型中的屬性一般都是一開始就有特定的值的,除非故意賦值為undefined[這樣沒意義了] function Box(user,age){ this.user=user; this.age=age; this.family=["哥哥","姐姐","妹妹"]; if(typeof this.clss == "undefined"){ alert("原型初始化開始");//沒有判斷之前,每new一次Box都會執行一次, Box.prototype.clss="person"; Box.prototype.run=function(){ return this.user+" "+this.age+" 運行中..."; }; alert("原型初始化結束"); } } var box1=new Box('abc',22); var box2=new Box('jack',33);複製代碼 12、寄生建構函式 寄生建構函式,其實就是原廠模式+建構函式模式。這種模式比較通用,但不能確定對象關係,所以,在可以使用之前所說的模式時,不建議使用此模式;在什麼情況下使用寄生建構函式比較合適呢?假設要建立一個具有額外方法的參考型別。由於之前說明不建議直接 String.prototype.addstring,可以通過寄生構造的方式添加 複製代碼 function myString(string){ var str = new String(string); str.addString = function(){ return this + ",被添加了!";//this 指的是對象str下的值string } return str; } var box = new myString("abcd"); alert(box.addString());複製代碼 13、穩妥建構函式 在一些安全的環境中, 比如禁止使用 this 和 new, 這裡的 this 是建構函式裡不使用 this,這裡的 new 是在外部執行個體化建構函式時不使用 new。這種建立方式叫做穩妥建構函式。 複製代碼 1 function Box(name, age) { 2 var obj = new Object(); 3 obj.name = name; 4 obj.age = age; 5 obj.run = function () { 6 return name + age + '運行中...'; 7 }; 8 return obj; 9 }10 11 var box1 = Box('Lee', 100);12 alert(box1.run());13 14 var box2 = Box('Jack', 200);15 alert(box2.run());複製代碼