JavaScript面向對象與原型html
學習要點:程序員
1.學習條件數組
2.建立對象瀏覽器
3.原型安全
4.繼承函數
ECMAScript有兩種開發模式:1.函數式(過程化),2.面向對象(OOP)。面向對象的語言有一個標誌,那就是類的概念,而經過類能夠建立任意多個具備相同屬性和方法的對象。可是,ECMAScript沒有類的概念,所以它的對象也與基於類的語言中的對象有所不一樣。學習
一.學習條件測試
在JavaScript視頻課程第一節課,就已經聲明過,JavaScript課程須要大量的基礎。這裏,咱們再詳細探討一下:this
1.xhtml基礎:JavaScript方方面面須要用到。spa
2.扣代碼基礎:好比XHTML,ASP,PHP課程中的項目都有JS扣代碼的過程。
3.面向對象基礎:JS的面向對象是非正統且怪異的,必須有正統面向對象基礎。
4.以上三大基礎,必須是基於項目中掌握的基礎,只是學習基礎知識不夠牢固,必須在項目中掌握上面的基礎便可。
二.建立對象
建立一個對象,而後給這個對象新建屬性和方法。
var box = new Object(); //建立一個Object對象 box.name = 'Lee'; //建立一個name屬性並賦值 box.age = 100; //建立一個age屬性並賦值 box.run = function () { //建立一個run()方法並返回值 return this.name + this.age + '運行中...'; }; alert(box.run()); //輸出方法的值 alert(box.name); //輸出屬性
上面建立了一個對象,而且建立屬性和方法,在run()方法裏的this,就是表明box對象自己。這種是JavaScript建立對象最基本的方法,但有個缺點,想建立一個相似的對象,就會產生大量的代碼。
var box = new Object(); //建立一個Object對象 box.name = 'Lee'; //建立一個name屬性並賦值 box.age = 100; //建立一個age屬性並賦值 box.run = function () { //建立一個run()方法並返回值 return this.name + this.age + '運行中...'; }; alert(box.run()); //輸出方法的值 alert(box.name); //輸出屬性 var box2 = box; //獲得box的引用 box2.name = 'Jack'; //直接改變了name屬性 alert(box2.run()); //用box.run()發現name也改變了 var box2 = new Object(); box2.name = 'Jack'; box2.age = 200; box2.run = function () { return this.name + this.age + '運行中...'; }; alert(box2.run()); //這樣才避免和box混淆,從而保持獨立
工廠模式建立對象【推薦】
爲了解決多個相似對象聲明的問題,咱們可使用一種叫作工廠模式的方法,這種方法就是爲了解決實例化對象產生大量重複的問題。
function createObject(name, age) { //集中實例化的函數 var obj = new Object(); //建立一個對象 obj.name = name; //追加一個屬性 obj.age = age; //追加一個屬性 obj.run = function () { //追加一個方法 return this.name + this.age + '運行中...'; //在對象裏面的this表明對象自己,對象裏的name屬性加上age屬性 }; return obj; //返回對象 } var box1 = createObject('Lee', 100); //第一個實例 var box2 = createObject('Jack', 200); //第二個實例 alert(box1.run()); alert(box2.run()); //保持獨立 //之後要構造不一樣數據的實例,只須要構造一個實例傳入不一樣參數便可,這樣減小了大量重複代碼
工廠模式解決了重複實例化的問題,但還有一個問題,那就是識別問題,由於根本沒法搞清楚他們究竟是哪一個對象的實例。因此就有了構造函數
構造函數(構造方法)建立對象【重點推薦】
ECMAScript中能夠採用構造函數(構造方法)可用來建立特定的對象。類型於Object對象。
function Box(name, age) { //構造函數模式建立對象 this.name = name; //構造函數後臺會自動new Object()建立對象;數體內的this就表明new Object()出來的對象。 this.age = age; //向對象追加屬性 this.run = function () { //向對象追加方法 return this.name + this.age + '運行中...'; }; } var box1 = new Box('Lee', 100); //注意:構造函數對象實列化必須使用new 運算符,構造函數傳參初始化對象數據 var box2 = new Box('Jack', 200); //構造函數傳參初始化對象數據 alert(box1.run()); //打印box1對象實列化下的run()方法, alert(box1 instanceof Box); //很清晰的識別他從屬於Box
使用構造函數的方法,即解決了重複實例化的問題,又解決了對象識別的問題,但問題是,這裏並無new Object(),爲何能夠實例化Box(),這個是哪裏來的呢?
使用了構造函數的方法,和使用工廠模式的方法他們不一樣之處以下:
1.構造函數方法沒有顯示的建立對象(new Object());
2.直接將屬性和方法賦值給this對象;
3.沒有renturn語句。
構造函數的方法有一些規範:
1.函數名和實例化構造名相同且大寫,(PS:非強制,但這麼寫有助於區分構造函數和普通函數);
2.經過構造函數建立對象,必須使用new運算符。
既然經過構造函數能夠建立對象,那麼這個對象是哪裏來的,new Object()在什麼地方執行了?執行的過程以下:
1.當使用了構造函數,而且new 構造函數(),那麼就後臺執行了new Object();
2.將構造函數的做用域給新對象,(即new Object()建立出的對象),而函數體內的this就表明new Object()出來的對象。
3.執行構造函數內的代碼;
4.返回新對象(後臺直接返回)。
this關於this的使用
關於this的使用,this其實就是表明當前做用域對象的引用。若是在全局範圍this就表明window對象,若是在構造函數體內,就表明當前的構造函數所聲明的對象。若是是在對象內,就表明當前對象
var box = 2; alert(this.box); //全局,表明window
構造函數和普通函數的惟一區別,就是他們調用的方式不一樣。只不過,構造函數也是函數,必須用new運算符來調用,不然就是普通函數。
function Box(name, age) { //構造函數模式建立對象 this.name = name; //構造函數後臺會自動new Object()建立對象;數體內的this就表明new Object()出來的對象。 this.age = age; //向對象追加屬性 this.run = function () { //向對象追加方法 return this.name + this.age + '運行中...'; }; } var box1 = new Box('Lee', 100); //注意:構造函數對象實列化必須使用new 運算符,構造函數傳參初始化對象數據 var box2 = new Box('Jack', 200); //構造函數傳參初始化對象數據 alert(box1.run()); //打印box1對象實列化下的run()方法, var box3 = new Box('Lee', 100); //構造模式調用 alert(box3.run()); //打印box3對象實列化下的run()方法, Box('Lee', 20); //普通模式調用,無效 var o = new Object(); //建立一個對象 Box.call(o, '林貴秀', 200); //o對象冒充Box構造函數調用 alert(o.run()); //打印Box構造函數裏的run()方法
探討構造函數內部的方法(或函數)的問題,首先看下兩個實例化後的屬性或方法是否相等。
function Box(name, age) { //構造函數模式建立對象 this.name = name; //構造函數後臺會自動new Object()建立對象;數體內的this就表明new Object()出來的對象。 this.age = age; //向對象追加屬性 this.run = function () { //向對象追加方法 return this.name + this.age + '運行中...'; }; } var box1 = new Box('Lee', 100); //注意:構造函數對象實列化必須使用new 運算符,構造函數傳參初始化對象數據 var box2 = new Box('Lee', 100); //構造函數傳參初始化對象數據 alert(box1.name == box2.name); //true,屬性的值相等 alert(box1.run == box2.run); //false,方法其實也是一種引用地址 alert(box1.run() == box2.run()); //true,方法的值相等,由於傳參一致
能夠把構造函數裏的方法(或函數)用new Function()方法來代替,獲得同樣的效果,更加證實,他們最終判斷的是引用地址,惟一性。
function Box(name, age) { //new Function()惟一性 this.name = name; this.age = age; this.run = new Function("return this.name + this.age + '運行中...'"); }
咱們能夠經過構造函數外面綁定同一個函數的方法來保證引用地址的一致性,但這種作法沒什麼必要,只是加深學習瞭解:
function Box(name, age) { this.name = name; this.age = age; this.run = run; } function run() { //經過外面調用,保證引用地址一致 return this.name + this.age + '運行中...'; }
雖然使用了全局的函數run()來解決了保證引用地址一致的問題,但這種方式又帶來了一個新的問題,全局中的this在對象調用的時候是Box自己,而看成普通函數調用的時候,this又表明window。
三.prototype原型
咱們建立的每一個函數都有一個prototype(原型)屬性,這個屬性是一個對象,它的用途是包含能夠由特定類型的全部實例共享的屬性和方法。邏輯上能夠這麼理解:prototype經過調用構造函數而建立的那個對象的原型對象。使用原型的好處可讓全部對象實例共享它所包含的屬性和方法。也就是說,沒必要在構造函數中定義對象信息,而是能夠直接將這些信息添加到原型中
prototype原型建立對象
function Box() {} //聲明一個構造對象函數,沒有初始化數據 //每個函數都有一個原型對象prototype,向原型對象添加數據時,構造對象函數會調用原型對象數據 Box.prototype.name = 'Lee'; //在原型裏添加屬性 Box.prototype.age = 100; //在原型裏添加屬性 Box.prototype.run = function () { //在原型裏添加方法 return this.name + this.age + '運行中...'; }; var box1 = new Box(); //實列化對象 alert(box1.run()); //打印執行對象裏的run方法
比較一下原型內的方法地址是否一致:
function Box() {} //聲明一個構造對象函數,沒有初始化數據 //每個函數都有一個原型對象prototype,向原型對象添加數據時,構造對象函數會調用原型對象數據 Box.prototype.name = 'Lee'; //在原型裏添加屬性 Box.prototype.age = 100; //在原型裏添加屬性 Box.prototype.run = function () { //在原型裏添加方法 return this.name + this.age + '運行中...'; }; var box1 = new Box(); //實列化對象1 var box2 = new Box(); //實列化對象2 alert(box1.run == box2.run); //true,方法的引用地址保持一致
ps:由此能夠看出,構造對象函數的實列的方法地址是不同的,而原型函數構造對象的實列的方法地址是同樣的
爲了更進一步瞭解構造函數的聲明方式和原型模式的聲明方式,咱們經過圖示來了解一下:
構造函數方式:原理圖
原型模式方式:原理圖
原型模式的執行流程:
1.先查找構造函數實例裏的屬性或方法,若是有,馬上返回;
2.若是構造函數實例裏沒有,則去它的原型對象裏找,若是有,就返回;
在原型模式聲明中,多了兩個屬性,這兩個屬性都是建立對象時自動生成的。__proto__屬性是實例指向原型對象的一個指針,它的做用就是指向構造函數的原型屬性constructor。經過這兩個屬性,就能夠訪問到原型裏的屬性和方法了。
__proto__構造對象函數裏內部指向原型的指針
PS:IE瀏覽器在腳本訪問__proto__會不能識別,火狐和谷歌瀏覽器及其餘某些瀏覽器均能識別。雖然能夠輸出,但沒法獲取內部信息。
function Box() {} //聲明一個構造對象函數,沒有初始化數據 //每個函數都有一個原型對象prototype,向原型對象添加數據時,構造對象函數會調用原型對象數據 Box.prototype.name = 'Lee'; //在原型裏添加屬性 Box.prototype.age = 100; //在原型裏添加屬性 Box.prototype.run = function () { //在原型裏添加方法 return this.name + this.age + '運行中...'; }; var box1 = new Box(); //實列化對象1 alert(box1.__proto__); //[object Object],__proto__構造對象函數裏內部指向原型的指針
constructor原型屬性,指向的構造函數自己
function Box() {} //聲明一個構造對象函數,沒有初始化數據 //每個函數都有一個原型對象prototype,向原型對象添加數據時,構造對象函數會調用原型對象數據 Box.prototype.name = 'Lee'; //在原型裏添加屬性 Box.prototype.age = 100; //在原型裏添加屬性 Box.prototype.run = function () { //在原型裏添加方法 return this.name + this.age + '運行中...'; }; var box1 = new Box(); //實列化對象1 alert(box1.constructor); //constructor原型屬性,指向的構造函數自己 //返回構造函數自己
isPrototypeOf()方法測試判斷一個對象的實列,是否指向了對象的原型【有參實列名稱】返回布爾值
使用方法:
對象名稱(對象構造函數).prototype.isPrototypeOf(實列名稱)
通常只要實列化了會自動指向對象的原型
function Box() {} //聲明一個構造對象函數,沒有初始化數據 //每個函數都有一個原型對象prototype,向原型對象添加數據時,構造對象函數會調用原型對象數據 Box.prototype.name = 'Lee'; //在原型裏添加屬性 Box.prototype.age = 100; //在原型裏添加屬性 Box.prototype.run = function () { //在原型裏添加方法 return this.name + this.age + '運行中...'; }; var box1 = new Box(); //實列化對象1 alert(Box.prototype.isPrototypeOf(box1));//isPrototypeOf()方法測試判斷一個對象的實列,是否指向了對象的原型
雖然咱們能夠經過對象實例訪問保存在原型中的值,但卻不能訪問經過對象實例重寫原型中的值。
function Box() {} //聲明一個構造對象函數,沒有初始化數據 //每個函數都有一個原型對象prototype,向原型對象添加數據時,構造對象函數會調用原型對象數據 Box.prototype.name = 'Lee'; //在原型裏添加屬性 Box.prototype.age = 100; //在原型裏添加屬性 Box.prototype.run = function () { //在原型裏添加方法 return this.name + this.age + '運行中...'; }; var box1 = new Box(); //實列化對象1 alert(box1.name); //Lee,原型裏的值 box1.name = 'Jack'; //在對象裏追加一個相同的屬性 alert(box1.name); //Jack,就近原則, var box2 = new Box(); //實列化對象2 alert(box2.name); //Lee,原型裏的值,沒有被box1修改
構造函數實例屬性和原型屬性示意圖
delete刪除對象裏的屬性
若是想要box1也能在後面繼續訪問到原型裏的值,能夠把構造函數裏的屬性刪除便可,具體以下:
function Box() {} //聲明一個構造對象函數,沒有初始化數據 //每個函數都有一個原型對象prototype,向原型對象添加數據時,構造對象函數會調用原型對象數據 Box.prototype.name = 'Lee'; //在原型裏添加屬性 Box.prototype.age = 100; //在原型裏添加屬性 Box.prototype.run = function () { //在原型裏添加方法 return this.name + this.age + '運行中...'; }; var box1 = new Box(); //實列化對象1 alert(box1.name); //Lee,原型裏的值 box1.name = 'Jack'; //在對象裏追加一個相同的屬性 delete box1.name; //刪除對象裏添加的name屬性 alert(box1.name); //仍是返回Lee,說明對象裏新添加的屬性被刪除了
hasOwnProperty()函數,判斷對象的屬性或者方法,是在構造函數的實列裏,仍是在原型裏,返回布爾值,實列裏返回true,不然返回false
使用方法:
對象名稱(構造對象函數名稱).hasOwnProperty('對象屬性');
如何判斷屬性是在構造函數的實例裏,仍是在原型裏?可使用hasOwnProperty()函數來驗證:
function Box() {} //聲明一個構造對象函數,沒有初始化數據 //每個函數都有一個原型對象prototype,向原型對象添加數據時,構造對象函數會調用原型對象數據 Box.prototype.name = 'Lee'; //在原型裏添加屬性 Box.prototype.age = 100; //在原型裏添加屬性 Box.prototype.run = function () { //在原型裏添加方法 return this.name + this.age + '運行中...'; }; var box1 = new Box(); //實列化對象1 alert(box1.name); //Lee,原型裏的值 alert(box1.hasOwnProperty('name')); //返回false,說明name屬性在原型裏 alert(box1.hasOwnProperty('run')); //返回false,說明run方法在原型裏 box1.name = 'Jack'; //在對象裏追加一個相同的屬性 alert(box1.hasOwnProperty('name')); //返回true,在對象裏新追加了name屬性,按照就近原則,此時判斷的屬性在構造函數的實列裏
in判斷對象的屬性是否存在,只要存在無論是在實列中仍是在原型中都返回true,實列和原型中都沒有就返回false
使用方法:
'對象屬性名稱' in Box
function Box() {} //聲明一個構造對象函數,沒有初始化數據 //每個函數都有一個原型對象prototype,向原型對象添加數據時,構造對象函數會調用原型對象數據 Box.prototype.name = 'Lee'; //在原型裏添加屬性 Box.prototype.age = 100; //在原型裏添加屬性 Box.prototype.run = function () { //在原型裏添加方法 return this.name + this.age + '運行中...'; }; alert('name' in Box); //返回true,說明對象存在這個屬性
自定義函數判斷對象屬性是否只存在原型中
咱們能夠經過hasOwnProperty()方法檢測屬性是否存在實例中,也能夠經過in來判斷實例或原型中是否存在屬性。那麼結合這兩種方法,能夠判斷原型中是否存在屬性。
function Box() {} //聲明一個構造對象函數,沒有初始化數據 //每個函數都有一個原型對象prototype,向原型對象添加數據時,構造對象函數會調用原型對象數據 Box.prototype.name = 'Lee'; //在原型裏添加屬性 Box.prototype.age = 100; //在原型裏添加屬性 Box.prototype.run = function () { //在原型裏添加方法 return this.name + this.age + '運行中...'; }; //自定義函數判斷對象屬性是否只存在原型中 function panduan(shlh,mch){ //接收兩個參數,第一個實列化對象名稱,第二個要判斷的對象屬性 return !shlh.hasOwnProperty(mch) && (mch in shlh); //當實列對象裏不存在這個屬性,而且原型中存在時返回 } var box1 = new Box(); alert(panduan(box1,'name')); //執行判斷函數,返回true,說明屬性在原型中
字面量方式建立原型
爲了讓屬性和方法更好的體現封裝的效果,而且減小沒必要要的輸入,原型的建立可使用字面量的方式:
function Box() {} //聲明一個構造對象函數,沒有初始化數據 //每個函數都有一個原型對象prototype,向原型對象添加數據時,構造對象函數會調用原型對象數據 Box.prototype = { //使用字面量的方式建立原型 name : 'Lee', age : 100, run : function () { return this.name + this.age + '運行中...'; } }; var box1 = new Box(); //實列化對象 alert(box1.run()); //打印對象下面的run方法
字面量方式建立原型與追加原型的區別
使用構造函數建立原型對象和使用字面量建立對象在使用上基本相同,但仍是有一些區別,字面量建立的方式使用constructor屬性不會指向實例,而會指向Object,構造函數建立的方式則相反。
//字面量方式建立原型 function Box() {} //聲明一個構造對象函數,沒有初始化數據 //每個函數都有一個原型對象prototype,向原型對象添加數據時,構造對象函數會調用原型對象數據 Box.prototype = { //使用字面量的方式建立原型,至關於新建立了一個Object對象 name : 'Lee', age : 100, run : function () { return this.name + this.age + '運行中...'; } }; var box = new Box(); //實列化對象 alert(box instanceof Box); //判斷box實列化是不是Box對象類型 //返回true alert(box instanceof Object); //判斷box實列化是不是Object對象類型 //返回true alert(box.constructor == Box); //判斷box實列化的原型constructor屬性是否等於Box對象,也就是原型constructor屬性是否指向Box對象 //返回false alert(box.constructor == Object); //判斷box實列化的原型constructor屬性是否等於Object對象,也就是原型constructor屬性是否指向Object對象 //返回true //由此能夠說明:字面量方式建立原型,原型的constructor屬性不是指向構造函數的,是指向構造函數裏面,字面量新建的Object對象
PS:字面量方式爲何constructor會指向Object?由於Box.prototype={};這種寫法其實就是建立了一個新對象。而每建立一個函數,就會同時建立它prototype,這個對象也會自動獲取constructor屬性。因此,新對象的constructor重寫了Box原來的constructor,所以會指向新對象,那個新對象沒有指定構造函數,那麼就默認爲Object。
字面量方式建立原型,將原型的constructor屬性強制指向實列對象(構建對象函數)
若是想讓字面量方式的constructor指向實例對象,那麼能夠這麼作:
//字面量方式建立原型 function Box() {} //聲明一個構造對象函數,沒有初始化數據 //每個函數都有一個原型對象prototype,向原型對象添加數據時,構造對象函數會調用原型對象數據 Box.prototype = { //使用字面量的方式建立原型,至關於新建立了一個Object對象 constructor:Box, //字面量方式建立原型,將原型的constructor屬性強制指向實列對象(構建對象函數) name : 'Lee', age : 100, run : function () { return this.name + this.age + '運行中...'; } }; var box = new Box(); //實列化對象 alert(box instanceof Box); //判斷box實列化是不是Box對象類型 //返回true alert(box instanceof Object); //判斷box實列化是不是Object對象類型 //返回true alert(box.constructor == Box); //判斷box實列化的原型constructor屬性是否等於Box對象,也就是原型constructor屬性是否指向Box對象 //返回true alert(box.constructor == Object); //判斷box實列化的原型constructor屬性是否等於Object對象,也就是原型constructor屬性是否指向Object對象 //返回false //由此能夠說明:字面量方式建立原型,將原型的constructor屬性強制指向實列對象(構建對象函數),也就是原型constructor屬性不在指向Object對象
原型的聲明是有前後順序的,因此,重寫的原型會切斷以前的原型。
function Box() {}; //構造對象函數 Box.prototype = { //構造原型 constructor : Box, //將原型強制指向構造函數 name : 'Lee', age : 100, run : function () { return this.name + this.age + '運行中...'; } }; //重寫構造原型 Box.prototype = { age : 200 }; //注意:只要重寫了構造原型,就會截斷原來的構造原型,下面的實列化就訪問不了原來的原型了 var box = new Box(); //實列化對象 alert(box.run()); //打印實列化對象裏的run方法,由於原型被重寫了,重寫的原型裏沒有run方法,報錯
內置的引用類型均可以使用原型,也就是說js的各類數據類型就是一個對象,因此數據類型對象也是可使用原型的,好比:String字符串類型,Array數組類型等,這些類型的方法就是它自己對象的原型方法
原型對象不只僅能夠在自定義對象的狀況下使用,而ECMAScript內置的引用類型均可以使用這種方式,而且內置的引用類型自己也使用了原型。
prototype查看一個方法是不是一個類型對象的原型,
使用方式:
類型對象名稱.prototype.要查詢的方法
alert(Array.prototype.sort); //sort就是Array類型的原型方法 alert(String.prototype.substring); //substring就是String類型的原型方法
使用對象原型給內置數據類型拓展方法,好比字符串類型對象
//給字符串對象添加一個原型方法 String.prototype.addstring = function () { //給String字符串類型添加一個addstring方法 return this + ',被添加了!'; //this表明調用的字符串 }; alert('Lee'.addstring()); //使用這個方法
PS:儘管給原生的內置引用類型添加方法使用起來特別方便,但咱們不推薦使用這種方法。由於它可能會致使命名衝突,不利於代碼維護。
原型的缺點和優勢
原型模式建立對象也有本身的缺點,它省略了構造函數傳參初始化這一過程,帶來的缺點就是初始化的值都是一致的。而原型最大的缺點就是它最大的優勢,那就是共享。
原型中全部屬性是被不少實例共享的,共享對於函數很是合適,對於包含基本值的屬性也還能夠。但若是屬性包含引用類型,就存在必定的問題:
原型的優勢
原型的優勢就是數據共享,只要在原型裏設置一次數據,其餘的實列化都共享原型裏的數據
//字面量方式建立原型 function Box() {} //聲明一個構造對象函數,沒有初始化數據 //每個函數都有一個原型對象prototype,向原型對象添加數據時,構造對象函數會調用原型對象數據 Box.prototype = { //使用字面量的方式建立原型,至關於新建立了一個Object對象 constructor:Box, //字面量方式建立原型,將原型的constructor屬性強制指向實列對象(構建對象函數) name : 'Lee', age : 100, asdc : ['哥哥','姐姐','妹妹'] //原型裏添加一個數組 }; var a1 = new Box(); //實列化對象1 alert(a1.asdc); //打印實列化1對裏的數字 //返回:哥哥,姐姐,妹妹 var a2 = new Box();//實列化對象2 alert(a2.asdc);//打印實列化2對裏的數字 //因而可知,原型的優勢就是數據共享,只要在原型裏設置一次數據,其餘的實列化都共享原型裏的數據
原型的缺點
原型數據是共享的沒法保證每一個實列化的數據獨立,由於原型是共享的,有一個實列改變了原型數據,後面的實列也隨之共享了改變後的數據
//字面量方式建立原型 function Box() {} //聲明一個構造對象函數,沒有初始化數據 //每個函數都有一個原型對象prototype,向原型對象添加數據時,構造對象函數會調用原型對象數據 Box.prototype = { //使用字面量的方式建立原型,至關於新建立了一個Object對象 constructor:Box, //字面量方式建立原型,將原型的constructor屬性強制指向實列對象(構建對象函數) name : 'Lee', age : 100, asdc : ['哥哥','姐姐','妹妹'] //原型裏添加一個數組 }; var a1 = new Box(); //實列化對象1 alert(a1.asdc); //打印實列化1對裏的數字 //返回:哥哥,姐姐,妹妹 var a2 = new Box();//實列化對象2 a2.asdc.push('弟弟');//向原型裏的數組添加一個元素 alert(a2.asdc);//打印實列化2對裏的數字 //返回:哥哥,姐姐,妹妹,弟弟 alert(a1.asdc);//此時打印實列化1也添加了數據了,由於原型是共享的,有一個實列改變了原型數據,後面的實列也隨之共享了改變後的數據 //因而可知,實列化1也添加了數據了,由於原型是共享的,有一個實列改變了原型數據,後面的實列也隨之共享了改變後的數據
PS:數據共享的緣故,致使不少開發者放棄使用原型,由於每次實例化出的數據須要保留本身的特性,而不能共享。
組合構造函數+原型模式:也就是不共享的數據使用構造函數,共享的數據使用原型
爲了解決構造傳參和共享問題,能夠組合構造函數+原型模式:
function Box(name, age) { //不共享的使用構造函數 this.name = name; //將傳參添加到對象屬性 this.age = age; //將傳參添加到對象屬性 this. family = ['父親', '母親', '妹妹']; } //字面量方式建立原型 Box.prototype = { //共享的使用原型模式 constructor : Box, run : function () { return this.name + ',' +this.age + ',' + '家庭成員' + ',' + this.family; } }; var a1 = new Box('小明',20); //傳參實列化對象1 alert(a1.run()); //打印實列對象1裏的原型方法 //返回:小明,20,家庭成員,父親,母親,妹妹 var a2 = new Box('小張',30);//傳參實列化對象2 alert(a2.run());//打印實列對象2裏的原型方法 //返回:小張,30,家庭成員,父親,母親,妹妹
PS:這種混合模式很好的解決了傳參和引用共享的大難題。是建立對象比較好的方法。
動態原型模式。也就是將原型封裝到構造函數裏【重點推薦】
構造函數+原型部分讓人感受又很怪異,最好就是把構造函數和原型封裝到一塊兒。爲了解決這個問題,咱們可使用動態原型模式。
function Box(name ,age) { //將全部信息封裝到函數體內 this.name = name; //添加對象屬性 this.age = age; //添加對象屬性 //將原型封裝到構造函數裏 if (typeof this.run != 'function') { //僅在第一次調用的初始化,判斷run不等於方法的時候,才建立原型方法,避免每次實列都建立方法 Box.prototype.run = function () { return this.name + this.age + '運行中...'; }; } } var box = new Box('Lee', 100); //實列化對象 alert(box.run()); //打印對象裏的原型方法 //返回:Lee100運行中...
當第一次調用構造函數時,run()方法發現不存在,而後初始化原型。當第二次調用,就不會初始化,而且第二次建立新對象,原型也不會再初始化了。這樣及獲得了封裝,又實現了原型方法共享,而且屬性都保持獨立。
PS:使用動態原型模式,要注意一點,不能夠再使用字面量的方式重寫原型,由於會切斷實例和新原型之間的聯繫。
寄生構造函數
以上講解了各類方式對象建立的方法,若是這幾種方式都不能知足需求,可使用一開始那種模式:寄生構造函數。
function Box(name, age) { //建立構建對象函數 var obj = new Object(); //建立對象 obj.name = name; //給對象添加屬性 obj.age = age; //給對象添加屬性 obj.run = function () { //給對象添加方法 return this.name + this.age + '運行中...'; }; return obj; //返回對象 } var asdf = Box('NIH',200); alert(asdf.run());
寄生構造函數,其實就是工廠模式+構造函數模式。這種模式比較通用,但不能肯定對象關係,因此,在可使用以前所說的模式時,不建議使用此模式。
寄生構造數據類型對象的額外方法
在什麼狀況下使用寄生構造函數比較合適呢?假設要建立一個具備額外方法的引用類型。因爲以前說明不建議直接String.prototype.addstring,能夠經過寄生構造的方式添加。
function myString(string) { //構造對象拓展函數 var str = new String(string); //實列化一個字符串對象 str.addstring = function () { //向字符串對象裏添加一個方法 return this + ',被添加了!'; }; return str; //返回實列化的字符串 } var box = new myString('Lee'); //實列化拓展函數傳參 alert(box.addstring()); //打印實列化拓展對象裏的addstring()方法 //返回:Lee,被添加了!
穩妥構造函數
在一些安全的環境中,好比禁止使用this和new,這裏的this是構造函數裏不使用this,這裏的new是在外部實例化構造函數時不使用new。這種建立方式叫作穩妥構造函數。
function Box(name , age) { //構造函數 var obj = new Object(); //建立一個對象 obj.run = function () { //給對象添加一個方法 return name + age + '運行中...'; //直接接受函數參數name和age }; return obj; //返回對象 } var box = Box('Lee', 100); //直接調用函數 alert(box.run()); //打印構造函數裏的run方法
PS:穩妥構造函數和寄生相似。
四.繼承
prototype繼承的方式依靠原型鏈完成
繼承是面向對象中一個比較核心的概念。其餘正統面嚮對象語言都會用兩種方式實現繼承:一個是接口實現,一個是繼承。而ECMAScript只支持繼承,不支持接口實現,而實現繼承的方式依靠原型鏈完成。
function Box() { //Box構造對象函數(父類) this.name = 'Lee'; //向對象裏添加一個name屬性 } function Desk() { //Desk構造對象函數(子類) this.age = 100; //向對象裏添加一個age屬性 } Desk.prototype = new Box(); //Desc繼承了Box,經過原型,造成鏈條,子類的原型等於實列化父類 var desk = new Desk(); alert(desk.age); alert(desk.name); //獲得被繼承的name屬 //以此讓子類繼承父類 function Table() { //Table構造對象函數(孫類) this.level = 'AAAAA'; //向對象裏添加一個age屬性 } Table.prototype = new Desk(); //Table繼承了Desk,經過原型,造成鏈條,孫類的原型等於實列化子類 var table = new Table(); alert(table.name); //繼承了Box的name屬性 alert(table.age); //繼承了Desk的age屬性
ps若是一個對象的實列和原型裏都有相同的一個屬性,按照就近原則,先到實列中查找,實列中沒有在到原型中
以上原型鏈繼承還缺乏一環,那就是Obejct,全部的構造函數都繼承自Obejct。而繼承Object是自動完成的,並不須要程序員手動繼承。
通過繼承後的實例,他們的從屬關係會怎樣呢?
function Box() { //Box構造對象函數(父類) this.name = 'Lee'; //向對象裏添加一個name屬性 } function Desk() { //Desk構造對象函數(子類) this.age = 100; //向對象裏添加一個age屬性 } Desk.prototype = new Box(); //Desc繼承了Box,經過原型,造成鏈條,子類的原型等於實列化父類 function Table() { //Table構造對象函數(孫類) this.level = 'AAAAA'; //向對象裏添加一個age屬性 } Table.prototype = new Desk(); //Table繼承了Desk,經過原型,造成鏈條,孫類的原型等於實列化子類 var table = new Table(); //實列化Table對象 var desk = new Desk(); //實列化Desk對象 alert(table instanceof Object); //true,判斷table實列化是否從屬於Object對象 alert(desk instanceof Table); //false,判斷desk實列化是否從屬於Table對象,由於desk是Table的父類不從屬 alert(table instanceof Desk); //true,判斷table實列化是否從屬於Desk對象 alert(table instanceof Box); //true,判斷table實列化是否從屬於Box對象
在JavaScript裏,被繼承的函數稱爲超類型(父類,基類也行,其餘語言叫法),繼承的函數稱爲子類型(子類,派生類)。繼承也有以前問題,好比字面量重寫原型會中斷關係,使用引用類型的原型,而且子類型還沒法給超類型傳遞參數。
借用構造函數(對象冒充)繼承
爲了解決引用共享和超類型沒法傳參的問題,咱們採用一種叫借用構造函數的技術,或者稱爲對象冒充(僞造對象、經典繼承)的技術來解決這兩種問題。
function Box(age) { //構造對象函數 this.name = ['Lee', 'Jack', 'Hello']; //向對象添加一個數組 this.age = age; //向對象添加一個屬性 } function Desk(age) { //構造對象函數 Box.call(this, age); //對象冒充,給超類型傳參 } var desk = new Desk(200); //實列化冒充Box對象傳參 alert(desk.age); alert(desk.name); desk.name.push('AAA'); //添加的新數據,只給desk alert(desk.name);
原型鏈+借用構造函數(對象冒充)組合繼承【重點推薦】
借用構造函數雖然解決了剛纔兩種問題,但沒有原型,複用則無從談起。因此,咱們須要原型鏈+借用構造函數的模式,這種模式成爲組合繼承。
function Box(age) { //構造對象函數 this.name = ['Lee', 'Jack', 'Hello']; //給對象添加一個數組 this.age = age; //給對象添加一個方法 } Box.prototype.run = function () { //給Box對象添加一個原型方法 return this.name + this.age; }; function Desk(age) { //對象冒充函數傳參 Box.call(this, age); //將Desk對象冒充Box對象 } Desk.prototype = new Box(); //原型鏈繼承,Desk對象繼承Box對象的數據 var desk = new Desk(100); //實列化冒充對象傳參100 alert(desk.run()); //打印冒充對象裏的原型run()方法,也就是Box的原型方法,由於Desk對象冒充的Box對象
原型式繼承
還有一種繼承模式叫作:原型式繼承;這種繼承藉助原型並基於已有的對象建立新對象,同時還沒必要所以建立自定義類型。
function obj(o) { //建立一個普通函數,接收一個參數 function F() {} //建立一個構造函數 F.prototype = o; //把普通函數的傳參,賦值給構造對象的原型 return new F(); //最終返回出實例化的構造函數 } var box = { //字面量建立對象 name : 'Lee', //建立對象屬性 arr : ['哥哥','妹妹','姐姐'] //建立對象數組 }; var box1 = obj(box); //將box對象當作參數傳給obj函數 alert(box1.name); //打印對象原型的name屬性 //返回:Lee box1.name = 'Jack'; //向構造對象實列裏添加一個屬性 alert(box1.name); //打印構造對象實列裏的name //返回:Jack alert(box1.arr); //打印對象原型的arr屬性 //返回:哥哥,妹妹,姐姐 box1.arr.push('父母'); //向對象原型裏的數字追加一個元素 alert(box1.arr); //打印對象原型裏追加後的數字 //返回:哥哥,妹妹,姐姐,父母 var box2 = obj(box); //將box對象當作參數傳給obj函數 alert(box2.name); //打印構造對象原型裏的name屬性 //返回:Lee alert(box2.arr); //引用類型共享了 //返回:哥哥,妹妹,姐姐,父母
寄生式繼承,把原型式+工廠模式結合而來,目的是爲了封裝建立對象的過程。
//臨時函數 function obj(o) { //建立一個普通函數,接收一個參數 function F() {} //建立一個構造函數 F.prototype = o; //把普通函數的傳參,賦值給構造對象的原型 return new F(); //最終返回出實例化的構造函數 } //建立對象 var box = { //字面量建立對象 name : 'Lee', //建立對象屬性 arr : ['哥哥','妹妹','姐姐'] //建立對象數組 }; //寄生函數 function create(o) { //建立寄生函數,將對象當作參數傳進來 var f= obj(o); //又將對象當作參數傳給obj函數執行,獲得的是構造對象原型實列化後的對象 f.run = function () { //向構造對象裏添加一個實列方法 return this.arr; //一樣,會共享引用,由於仍是調用的對象原型 }; return f; //返回最後實列後的對象 } var box1 = create(box); //實列化對象,將box對象傳到寄生函數,在由寄生函數傳到臨時函數添加成構造對象的原型 alert(box1.run());//打印對象實列裏的run方法
組合式繼承
組合式繼承是JavaScript最經常使用的繼承模式;但,組合式繼承也有一點小問題,就是超類型在使用過程當中會被調用兩次:一次是建立子類型的時候,另外一次是在子類型構造函數的內部。
function Box(name) { //構造對象函數 this.name = name; //添加對象實列屬性 this.arr = ['哥哥','妹妹','父母']; //添加對象實列數組 } Box.prototype.run = function () { //添加對象原型方法 return this.name; }; function Desk(name, age) { //建立冒充對象,將Desk冒充Box對象 Box.call(this, name); //第二次調用Box this.age = age; } Desk.prototype = new Box(); // Desk的原型等於new Box(),也就是Desk繼承 Box對象 //第一次調用Box var asdf = new Desk('LII',200); //實列化對象 alert(asdf.run()); //打印對象裏原型方法 //超類型在使用過程當中會被調用兩次:一次是建立子類型的時候,另外一次是在子類型構造函數的內部。
寄生組合繼承【重點推薦】
以上代碼是以前的組合繼承,那麼寄生組合繼承,解決了兩次調用的問題。
function obj(o) { //建立一箇中轉函數 function F() {} //建立一個對象 F.prototype = o; //將函數接收到的參數添加到對象原型 return new F(); //返回實列對象 } function create(box, desk) { //建立寄生函數,接收box, desk對象 var f = obj(box.prototype); //將box對象的原型傳入obj函數,獲得obj實列對象 f.constructor = desk; //將obj實列對象的原型指向desk desk.prototype = f; //將desk的原型指向f對象 } function Box(name) { //建立Box對象 this.name = name; this.arr = ['哥哥','妹妹','父母']; } Box.prototype.run = function () { //添加Box原型方法 return this.name; }; function Desk(name, age) { //定義冒充對象 Box.call(this, name); this.age = age; } create(Box, Desk); //經過這裏實現繼承 var desk = new Desk('Lee',100); desk.arr.push('姐姐'); alert(desk.arr); alert(desk.run()); //只共享了方法 var desk2 = new Desk('Jack', 200); alert(desk2.arr); //引用問題解決