原文地址在我的博客, 轉(zhuǎn)載請注明出處姨夹,謝謝纤垂!
標(biāo)簽: [es5對象、原型, 原型鏈, 繼承]
注意
(這篇文章特別長)
這篇文章僅僅是我個人對于JavaScript對象的理解磷账,并不是教程峭沦。這篇文章寫于我剛了解js對象之后。文章肯定有錯誤之處逃糟,還望讀者費(fèi)心指出吼鱼,在下方評論即可-
什么是JavaScript對象
var person = { //person就是對象,對象都有各種屬性绰咽,每個屬性又都對應(yīng)著自己的值
//鍵值對形式
name: "Mofan",//可以包含字符串
age: 20,//數(shù)字
parents: [ //數(shù)組
"Daddy",
"Mami",
]
sayName: function(){ //函數(shù)
console.log(this.name);
},
features: { //甚至是對象(很少用菇肃,我是沒見過)
height: "178cm",
weight: "60kg",
}
}
js里除了基本類型外所有事物都是對象:
-
函數(shù)是對象:
function sayName(){}
——sayName是函數(shù)對象
-
數(shù)組是對象:
var arr = new Array()
——arr是數(shù)組對象
為什么JavaScript要這么設(shè)計呢?我覺得首先這樣一來取募,統(tǒng)一了數(shù)據(jù)結(jié)構(gòu)琐谤,使JavaScript成為一門編程風(fēng)格非常自由化的腳本語言:無論定義什么變量,統(tǒng)統(tǒng)var
玩敏;其次斗忌,JavaScript對象都有屬性和方法,函數(shù)數(shù)組都是對象旺聚,調(diào)用引用就會非常靈活方便飞蹂;再者,為了構(gòu)建原型鏈翻屈?
創(chuàng)建對象的幾種方式
- Object()模式
使用對象字面量:var obj={...}
就像上面那樣
或者使用原生構(gòu)造函數(shù)Object():
var person = new Object();
person.name = "Mofan";
person.sayName = function(){
console.log(this.name);
};
console.log(person.name);//Mofan
obj.sayName();//Mofan
- 利用函數(shù)作用域使用自定義構(gòu)造函數(shù)模式模仿類(構(gòu)造器模式):
function Person(name,age){
this.name = name;
this.age = age;
this.print = function(){
console.log(this.name + this.age)
};
}
var person = new Person("Mofan",19);
console.log(person.name+person.age);//Mofan19
person.print();//Mofan19
- 原型模式:
function Person(){}
//可以這樣寫
/*Person.prototype.name = "Mofan";
Person.prototype.age = 19;
Person.prototype.print = function(){
console.log(this.name+this.age);
}*/
//推薦下面這樣寫陈哑,但兩種方式不能混用!因為下面這種方式實(shí)際上重寫了
//Person原型對象,如果兩者混用惊窖,后面賦值方式會覆蓋前面賦值方式
Person.prototype = {
name:"Mofan",
age:19,
print:function(){
console.log(this.name+this.age);
}
}
var person = new Person();
console.log(person.name+person.age);//Mofan19
person.print();//Mofan19
- 組合構(gòu)造函數(shù)模式和原型模式:
function Person(name,age){
//這里面初始化屬性
this.name = name;
this.age = age;
...
}
Person.prototype = {
//這里面定義公有方法
print:function(){
console.log(this.name+this.age);
},
...
}
var person = new Person("Mofan",19);
console.log(person.name+person.age);//Mofan19
person.print();//Mofan19
- 動態(tài)創(chuàng)建原型模式:
function Person(name,age){
//初始化屬性
this.name = name;
this.age = age;
//在創(chuàng)建第一個對象(第一次被調(diào)用)時定義所有公有方法,以后不再調(diào)用
if(typeof this.print !="function"){
Person.prototype.print =function(){
console.log(this.name+this.age);
};
Person.prototype.introduction=function(){
console.log("Hi!I'm "+this.name+",I'm "+this.age);
};
//如果采用對象字面量對原型添加方法的話刽宪,第一次創(chuàng)建的對象將不會有這些方法
};
}
var person = new Person("Mofan",19);
person.print();//Mofan19
person.introduction();//Hi!I'm Mofan,I'm 19
還有一些模式用的場景比較少
這些模式的應(yīng)用場景
怎么會有這么多的創(chuàng)建模式?其實(shí)是因為js語言太靈活了界酒,因此前輩們總結(jié)出這幾種創(chuàng)建方式以應(yīng)對不同的場景圣拄,它們各有利弊。
- 第一種方式毁欣,使用字面量或者使用構(gòu)造函數(shù)Object()常用于創(chuàng)建普通對象存儲數(shù)據(jù)等庇谆。它們的原型都是Object,彼此之間沒有什么關(guān)聯(lián)凭疮。事實(shí)上饭耳,下面創(chuàng)建方式都是一樣的:
var o1 = {};//字面量的表現(xiàn)形式
var o2 = new Object;
var o3 = new Object();
var o4 = new Object(null);
var o5 = new Object(undefined);
var o6 = Object.create(Object.prototype);//等價于 var o = {};//即以 Object.prototype 對象為一個原型模板,新建一個以這個原型模板為原型的對象
![](http://i4.buimg.com/567571/d9e639e790ea6112.png)
- 第二種方式,利用函數(shù)作用域模仿類执解,這樣就可以在創(chuàng)建對象時傳參了寞肖,可以創(chuàng)建不同屬性值得對象,實(shí)現(xiàn)對象定制衰腌。不過print方法也定義在了構(gòu)造函數(shù)里面新蟆,如果要把它當(dāng)做公有方法的話,這樣每new一個對象右蕊,都會有這個方法琼稻,太浪費(fèi)內(nèi)存了∪那簦可以這樣修改一下構(gòu)造器模式:
//構(gòu)造器方法2
function print(){ //定義一個全局的 Function 對象,把要公有的方法拿出來
console.log(this.name + this.age);
}
function Person(name,age){
this.name = name;
this.age = age;
this.print = print.bind(this);//每個 Person 對象共享同一個print 方法版本(方法有自己的作用域帕翻,不用擔(dān)心變量被共享)
}
var person = new Person("Mofan",19);
console.log(person.name+person.age);//Mofan19
person.print();//Mofan19
然而這樣看起來很亂乡革,也談不上類的封裝性。還是使用原型吧
第三種方式蝌蹂,純原型模式熄赡,不管是屬性還是方法都添加到原型里面去了,這樣做好處是很省內(nèi)存对蒲,但是應(yīng)用范圍就少了,更多的對象 內(nèi)部的屬性是需要定制的,而且一旦更改原型卿拴,所有這個原型實(shí)例都會跟著改變。因此可以結(jié)合構(gòu)造函數(shù)方式來實(shí)現(xiàn)對對象的定制梨与,于是就有了第四種方式——組合構(gòu)造函數(shù)模式與原型模式堕花,可以定制的放在構(gòu)造器里,共有的放在原型里粥鞋,這也符合構(gòu)造器和原型的特性缘挽。
“這是es5中使用最廣泛、認(rèn)同度最高的創(chuàng)建自定義類型的方法”
---《JavaScript高級程序設(shè)計》第三版第五種方式,動態(tài)原型模式壕曼,出現(xiàn)這種方式是因為有些面向?qū)ο箝_發(fā)人員習(xí)慣了類構(gòu)造函數(shù)苏研,于是對這種獨(dú)立出來的構(gòu)造函數(shù)和原型感到困惑和不習(xí)慣。于是腮郊,就出現(xiàn)了把定義原型也寫進(jìn)構(gòu)造函數(shù)里的動態(tài)原型模式摹蘑。
上面在動態(tài)原型模式程序里面講“如果采用對象字面量對原型添加方法的話,第一次創(chuàng)建的對象將不會有這些方法
”這是因為在if語句執(zhí)行以前轧飞,第一個對象已經(jīng)被創(chuàng)建了衅鹿,然后執(zhí)行if里面的語句,如果采用對象字面量給原型賦值过咬,就會導(dǎo)致原型在實(shí)例創(chuàng)建之后被重寫大渤,創(chuàng)建的第一個實(shí)例就會失去與原型的鏈接,也就沒有原型里的方法了援奢。不過以后創(chuàng)建的對象就可以使用原型里的方法了兼犯,因為它們都是原型被修改后創(chuàng)建的。
原型是什么
在JavaScript中集漾,原型就是一個對象切黔,沒必要把原型和其他對象區(qū)別對待,只是通過它可以實(shí)現(xiàn)對象之間屬性的繼承具篇。任何一個對象也可以成為原型纬霞。之所以經(jīng)常說對象的原型,實(shí)際上就是想找對象繼承的上一級對象驱显。對象與原型的稱呼是相對的诗芜,也就是說,一個對象埃疫,它稱呼繼承的上一級對象為原型伏恐,它自己也可以稱作原型鏈下一級對象的原型。
一個對象內(nèi)部的[[Prototype]]屬性生來就被創(chuàng)建栓霜,它指向繼承的上一級對象翠桦,稱為原型。函數(shù)對象內(nèi)部的prototype屬性也是生來就被創(chuàng)建(只有函數(shù)對象有prototype屬性)胳蛮,它指向函數(shù)的原型對象(不是函數(shù)的原型O铡)。
當(dāng)使用var instance = new Class();
這樣每new一個函數(shù)(函數(shù)被當(dāng)做構(gòu)造函數(shù)來使用)創(chuàng)建實(shí)例時仅炊,JavaScript就會把這個原型的引用賦值給實(shí)例的原型屬性斗幼,于是實(shí)例內(nèi)部的[[Prototype]]屬性就指向了函數(shù)的原型對象,也就是prototype屬性抚垄。
原型真正意義上指的是一個對象內(nèi)部的[[Prototype]]屬性蜕窿,而不是函數(shù)對象內(nèi)部的prototype屬性谋逻,這兩者之間沒有關(guān)系!對于一個對象內(nèi)部的[[Prototype]]屬性渠羞,不同瀏覽器有不同的實(shí)現(xiàn):
var a = {};
//Firefox 3.6+ and Chrome 5+
Object.getPrototypeOf(a); //[object Object]
//Firefox 3.6+, Chrome 5+ and Safari 4+
a.__proto__; //[object Object]
//all browsers
a.constructor.prototype; //[object Object]
之所以函數(shù)對象內(nèi)部存在prototype屬性斤贰,并且可以用這個屬性創(chuàng)建一個原型,是因為這樣以來次询,每new一個這樣的函數(shù)(函數(shù)被當(dāng)做構(gòu)造函數(shù)來使用)創(chuàng)建實(shí)例荧恍,JavaScript就會把這個原型的引用賦值給實(shí)例的原型屬性,這樣以來屯吊,在原型中定義的方法等都會被所有實(shí)例共用送巡,而且,一旦原型中的某個屬性被定義盒卸,就會被所有實(shí)例所繼承(就像上面的例子)骗爆。這種操作在性能和維護(hù)方面其意義是不言自明的。這也正是構(gòu)造函數(shù)存在的意義(JavaScript并沒有定義構(gòu)造函數(shù)蔽介,更沒有區(qū)分構(gòu)造函數(shù)和普通函數(shù)摘投,是開發(fā)人員約定俗成)。下面是一些例子:
var a = {} //一個普通的對象
function fun(){} //一個普通的函數(shù)
//普通對象沒有prototype屬性
console.log(a.prototype);//undefined
console.log(a.__proto__===Object.prototype);//true
//只有函數(shù)對象有prototype屬性
console.log(fun.prototype);//Object
console.log(fun.__proto__===Function.prototype);//true
console.log(fun.prototype.__proto__===Object.prototype);//true
console.log(fun.__proto__.__proto__===Object.prototype);//true
console.log(Function.prototype.__proto__===Object.prototype);//true
console.log(Object.prototype.__proto__);//null
當(dāng)執(zhí)行console.log(fun.prototype);
輸出為
![](http://i4.buimg.com/567571/b704619ba92d5cc5.png)
可以看到虹蓄,每創(chuàng)建一個函數(shù)犀呼,就會創(chuàng)建prototype屬性,這個屬性指向函數(shù)的原型對象(不是函數(shù)的原型)薇组,并且這個原型對象會自動獲得constructor屬性外臂,這個屬性是指向prototype屬性所在函數(shù)的指針。而
__proto__
屬性是每個對象都有的律胀。
接著上面再看:
function Person(){}//構(gòu)造函數(shù),約定首字母大寫
var person1 = new Person();//person1為Person的實(shí)例
console.log(person1.prototype);//undefined
console.log(person1.__proto__===Person.prototype);//true
console.log(person1.__proto__.__proto__===Object.prototype);//true
console.log(person1.constructor);//function Person(){}
//函數(shù)Person是Function構(gòu)造函數(shù)的實(shí)例
console.log(Person.__proto__===Function.prototype);//true
//Person的原型對象是構(gòu)造函數(shù)Object的實(shí)例
console.log(Person.prototype.__proto__===Object.prototype);//true
person1和上面那個普通的對象a有區(qū)別宋光,它是構(gòu)造函數(shù)Person的實(shí)例。前面講過:
當(dāng)使用
var instance = new Class();
這樣每new一個函數(shù)(函數(shù)被當(dāng)做構(gòu)造函數(shù)來使用)創(chuàng)建實(shí)例時炭菌,JavaScript就會把這個原型的引用賦值給實(shí)例的原型屬性罪佳,于是實(shí)例內(nèi)部的[[Prototype]]屬性就指向了函數(shù)的原型對象,也就是prototype屬性黑低。
因此person1內(nèi)部的[[Prototype]]屬性就指向了Person的原型對象赘艳,然后Person的原型對象內(nèi)部的[[Prototype]]屬性再指向Object.prototype,相當(dāng)于在原型鏈中加了一個對象投储。通過這種操作第练,person1就有了構(gòu)造函數(shù)的原型對象里的方法阔馋。
另外玛荞,上面代碼console.log(person1.constructor);//function Person(){}
中,person1內(nèi)部并沒有constructor屬性呕寝,它只是順著原型鏈往上找勋眯,在person1.__proto__
里面找到的。
可以用下面這張圖理清原型、構(gòu)造函數(shù)客蹋、實(shí)例之間的關(guān)系:
![](http://i1.piimg.com/567571/b46336f2cd673caf.png)
繼承
JavaScript并沒有繼承這一現(xiàn)有的機(jī)制塞蹭,但可以利用函數(shù)、原型讶坯、原型鏈模仿番电。
下面是三種繼承方式:
類式繼承
//父類
function SuperClass(){
this.superValue = "super";
}
SuperClass.prototype.getSuperValue = function(){
return this.superValue;
};
//子類
function SubClass(){
this.subValue = "sub";
}
//類式繼承,將父類實(shí)例賦值給子類原型,子類原型和子類實(shí)例可以訪問到父類原型上以及從父類構(gòu)造函數(shù)中復(fù)制的屬性和方法
SubClass.prototype = new SuperClass();
//為子類添加方法
SubClass.prototype.getSubValue = function(){
return this.subValue;
}
//使用
var instance = new SubClass();
console.log(instance.getSuperValue);//super
console.log(instance.getSubValue);//sub
這種繼承方式有很明顯的兩個缺點(diǎn):
- 實(shí)例化子類時無法向父類構(gòu)造函數(shù)傳參
- 如果父類中的共有屬性有引用類型辆琅,就會在子類中被所有實(shí)例所共用漱办,那么任何一個子類的實(shí)例更改這個引用類型就會影響其他子類實(shí)例,可以使用構(gòu)造函數(shù)繼承方式解決這一問題
構(gòu)造函數(shù)繼承
//父類
function SuperClass(id){
this.superValue = ["big","large"];//引用類型
this.id = id;
}
SuperClass.prototype.getSuperValue = function(){
return this.superValue;
};
//子類
function SubClass(id){
SuperClass.call(this,id);//調(diào)用父類構(gòu)造函數(shù)并傳參
this.subValue = "sub";
}
var instance1 = new SubClass(10);//可以向父類傳參
var instance2 = new SubClass(11);
instance1.superValue.push("super");
console.log(instance1.superValue);//["big", "large", "super"]
console.log(instance1.id);//10
console.log(instance2.superValue);["big", "large"]
console.log(instance2.id);//11
console.log(instance1.getSuperValue());//error
這種方式是解決了類式繼承的缺點(diǎn),不過在代碼的最后一行你也看到了婉烟,沒有涉及父類原型娩井,因此違背了代碼復(fù)用的原則。所以組合它們:
組合繼承
function SuperClass(id){
this.superValue = ["big","large"];//引用類型
this.id = id;
}
SuperClass.prototype.getSuperValue = function(){
return this.superValue;
};
//子類
function SubClass(id,subValue){
SuperClass.call(this,id);//調(diào)用父類構(gòu)造函數(shù)并傳參
this.subValue = subValue;
}
SubClass.prototype = new SuperClass();
SubClass.prototype.getSubValue = function(){
return this.subValue;
}
var instance1 = new SubClass(10,"sub");//可以向父類傳參
var instance2 = new SubClass(11,"sub-sub");
instance1.superValue.push("super");
console.log(instance1.superValue);//["big", "large", "super"]
console.log(instance1.id);//10
console.log(instance2.superValue);["big", "large"]
console.log(instance2.id);//11
console.log(instance1.getSuperValue());["big", "large", "super"]
console.log(instance1.getSubValue());//sub
console.log(instance2.getSuperValue());//["big", "large"]
console.log(instance2.getSubValue());//sub-sub
嗯似袁,比較完美了洞辣,但是有一點(diǎn),父類構(gòu)造函數(shù)被調(diào)用了兩次昙衅,這就導(dǎo)致第二次調(diào)用也就是創(chuàng)建實(shí)例時重寫了原型屬性扬霜,原型和實(shí)例都有這些屬性,顯然性能并不好绒尊。先來看看克羅克福德的寄生式繼承:
function object(o){
function F(){};
F.prototype = o;
return new F();
}
function createAnnther(original){
var clone = object(original);
clone.sayName = function(){
console.log(this.name);
}
return clone;
}
var person = {
name:"Mofan",
friends:["xiaoM","Alice","Neo"],
};
var anotherPerson = createAnnther(person);
anotherPerson.sayName();//"Mofan"
}
就是讓一個已有對象變成新對象的原型畜挥,然后再在createAnother函數(shù)里加強(qiáng)。你也看到了婴谱,person就是一個普通對象蟹但,所以這種寄生式繼承適合于根據(jù)已有對象創(chuàng)建一個加強(qiáng)版的對象,在主要考慮通過已有對象來繼承而不是構(gòu)造函數(shù)的情況下谭羔,這種方式的確很方便华糖。但缺點(diǎn)也是明顯的,createAnother函數(shù)不能復(fù)用瘟裸,我如果想給另外一個新創(chuàng)建的對象定義其他方法客叉,還得再寫一個函數(shù)。仔細(xì)觀察一下话告,其實(shí)寄生模式就是把原型給了新對象兼搏,對象再加強(qiáng)。
等等沙郭,寫到這個地方佛呻,我腦子有點(diǎn)亂,讓我們回到原點(diǎn):繼承的目的是什么病线?應(yīng)該繼承父類哪些東西吓著?我覺得取決于我們想要父類的什么鲤嫡,我想要父類全部的共有屬性(原型里)并且可以自定義繼承的父類私有屬性(構(gòu)造函數(shù)里)!前面那么多模式它們的缺點(diǎn)主要是因為這個:
SubClass.prototype = new SuperClass();
那為什么要寫這一句呢绑莺?是只想要繼承父類的原型嗎暖眼?如果是為什么不這么寫:
SubClass.prototype = SuperClass.prototype;
這樣寫是可以繼承父類原型,但是風(fēng)險極大:SuperClass.prototype
屬性它是一個指針纺裁,指向SuperClass的原型诫肠,如果把這個指針賦給子類prototype屬性,那么子類prototype也會指向父類原型欺缘。對SubClass.prototype
任何更改区赵,就是對父類原型的更改,這顯然是不行的浪南。
寄生組合式繼承
但出發(fā)點(diǎn)沒錯笼才,可以換種繼承方式,看看上面的寄生式繼承里的object()
函數(shù)络凿,如果把父類原型作為參數(shù)骡送,它返回的對象實(shí)現(xiàn)了對父類原型的繼承,沒有調(diào)用父類構(gòu)造函數(shù)絮记,也不會對父類原型產(chǎn)生影響摔踱,堪稱完美。
function object(o){
function F(){};
F.prototype = o;
return new F();
}
function inheritPrototype(subType,superType){
var proto = object(superType.prototype);
proto.constructor = subType;//矯正一下construcor屬性
subType.prototype = proto;
}
function SuperClass(id){
this.superValue = ["big","large"];//引用類型
this.id = id;
}
SuperClass.prototype.getSuperValue = function(){
return this.superValue;
};
//子類
function SubClass(id,subValue){
SuperClass.call(this,id);//調(diào)用父類構(gòu)造函數(shù)并傳參
this.subValue = subValue;
}
inheritPrototype(SubClass,SuperClass);//繼承父類原型
SubClass.prototype.getSubValue = function(){
return this.subValue;
}
var instance1 = new SubClass(10,"sub");//可以向父類傳參
var instance2 = new SubClass(11,"sub-sub");
instance1.superValue.push("super");
console.log(instance1.superValue);//["big", "large", "super"]
console.log(instance1.id);//10
console.log(instance2.superValue);//["big", "large"]
console.log(instance2.id);//11
console.log(instance1.getSuperValue());//["big", "large", "super"]
console.log(instance1.getSubValue());//sub
console.log(instance2.getSuperValue());//["big", "large"]
console.log(instance2.getSubValue());//sub-sub
解決了組合繼承的問題怨愤,只調(diào)用了一次父類構(gòu)造函數(shù)派敷,而且還能保持原型鏈不變,為什么這么說撰洗,看對寄生組合的測試:
console.log(SubClass.prototype.__proto__===SuperClass.prototype);//ture
console.log(SubClass.prototype.hasOwnProperty("getSuperValue"));//false
因此篮愉,這是引用類型最理想的繼承方式。
總結(jié)
創(chuàng)建用于繼承的對象最理想的方式是組合構(gòu)造函數(shù)模式和原型模式(或者動態(tài)原型模式)差导,就是讓可定義的私有屬性放在構(gòu)造函數(shù)里试躏,共有的放在原型里;繼承最理想的方式是寄生式組合设褐,就是讓子類的原型的[[prototype]]屬性指向父類原型颠蕴,然后在子類構(gòu)造函數(shù)里調(diào)用父類構(gòu)造函數(shù)實(shí)現(xiàn)自定義繼承的父類屬性。
JavaScript對象總有一些讓我困惑的地方助析,不過我還會繼續(xù)探索犀被。我在此先把我了解的記錄下來,與各位共勉外冀。錯誤的地方請費(fèi)心指出寡键,我將感謝您的批評指正。
本文為作者原創(chuàng)锥惋,轉(zhuǎn)載請注明本文鏈接昌腰,作者保留權(quán)利。
參考文獻(xiàn):
[1] http://www.cnblogs.com/chuaWeb/p/5039232.html
[2] http://www.cnblogs.com/xjser/p/4962821.html
[3] https://javascriptweblog.wordpress.com/2010/06/07/understanding-javascript-prototypes/