對象(Object)應該算是js中最爲重要的部分,也是js中很是難懂晦澀的一部分。更是面試以及框架設計中各出沒。寫這篇文章,主要參考與JavaScript紅寶書(JavaScript高級程序設計 第六章章節)以及各大博主博客。
原文地址:https://github.com/Nealyang/YOU-SHOULD-KNOW-JSjavascript
畢竟是面向對象編程,咱們在討論如何面向對象以前先討論討論對象具備哪些屬性和特性。html
簡單的說,對象擁有四個屬性:前端
若是要修改屬性默認的特性,必須經過Object.defineProperty()方法。大體以下:java
var animal = {};
Object.defineProperty(animal,"name",{
writable:false,
value: 'dog';
});
console.log(animal.name);//dog
animal.name = 'cat';
console.log(animal.name);//dog複製代碼
從上面的實例你們也能看出,在調用Object.defineProperty()方法後,若是不指定 configurable、enumerable、writable 特性的值時,默認爲FALSE。git
訪問器屬性不包含數據值,可是包含getter和setter函數。在讀取訪問器屬性時,會調用getter函數,這個函數負責返回有效值。在寫入訪問器屬性時,回到用setter函數並傳入新值。github
這裏不作過多解釋,直接看例子吧(來自js紅寶書)web
var book = {
_year:2012,
edition:1
};
Object.defineProperty(book, 'year',{
get:function(){
return this._year
},
set:function(value){
if(value>2012){
this._year = value,
this.edition++
}
}
});
book.year = 2013;
console.log(book.edition);//2複製代碼
其實對於多個屬性的定義,咱們可使用Object.defineProperties方法。而後對於讀取屬性的特性咱們可使用Object.getOwnPropertyDescriptor()方法。你們自行查看哈。面試
建立對象,咱們不是直接能夠經過Object的構造函數或者對象字面量的方法來實現對象的建立嘛?固然,這些方法是能夠的,可是有一個明顯的缺點:使用同一個接口建立不少對象,產生大量重複的代碼。因此這裏,咱們使用以下的一些騷操做編程
一種很基礎的設計模式,簡而言之就是用函數來封裝以特定接口建立對象的細節。設計模式
function createAnimal(name,type){
var o = new Object();
o.name = name;
o.type = type;
o.sayName = function(){
alert(this.name)
}
return o;
}
var cat = createAnimal('小貓','cat');
var dog = createAnimal('小🐽','dog');複製代碼
優勢:能夠無數次的調用這個函數,來建立類似對象。
缺點:不能解決對象識別的問題。也就是說,我不知道你是誰家的b孩子
ECMAScript中的構造函數能夠用來建立特定類型的對象。在運行時會自動出如今執行環境中(這句話後面講解this的時候仍是會說到)。
function Animal(name,type){
this.name = name;
this.type = type;
this.say = function(){
alert(this.name);
}
}
var cat = new Animal('小貓','cat');
var dog = new Animal('小🐽','dog');複製代碼
注意上面咱們沒有顯示的return過一個對象出來,爲何?由於this(後面會講this的)。
關於構造函數慣例首字母大寫就不囉嗦了。強調構造函數必定要使用關鍵字new來調用。爲何使用new呢?由於你使用了new,他會
那麼解決了工廠模式的詬病了麼?固然~
在實例對象中,都有一個constructor屬性。
cat.constructor == Animal //true
dog.constructor == Animal //true
cat instanceof Animal //true
dog instanceof Animal //true複製代碼
構造函數模式的優勢如上所說,可是缺點仍是有的,好比說
cat.sayName == dog.sayName //false複製代碼
也就是說,他建立了兩個功能同樣的函數,這樣是很沒有必要的,固然,咱們能夠把sayName放到構造函數外面,而後經過this.sayName=sayName來操做,可是這樣的話,又會致使全局變量的污染。腫麼辦???
咱們在建立每個函數的時候都有一個prototype(原型)屬性,這個屬性是一個指針,指向一個對象。而這個對象的用途就是包含由特定類型的全部實例共享的屬性和方法。
function Animal() {}
Animal.prototype.name = '毛毛';
Animal.prototype.type = 'dog';
Animal.prototype.sayName = function() {
alert(this.name);
}
var cat = new Animal();
var dog = new Animal();
alert(cat.sayName == dog.sayName)//true複製代碼
原型模式的好處就是可讓全部的對象實例共享他的屬性和方法。沒必要在構造函數中定義對象實例的信息。
function Person() {}
Person.prototype.name = 'Nealyang';
Person.prototype.age = 24;
Person.prototype.sayName = function(){
alert(this.name);
}
var neal = new Person();
console.log(neal.name)//'Nealyang' -> 來自原型
neal.name = 'Neal';
console.log(neal.name)// Neal -> 來自實例
delete neal.name;
console.log(neal.name)//'Nealyang' -> 來自原型複製代碼
上面的例子說明兩點
咱們能夠經過hasOwnProperty()方法來肯定一個屬性是在原型上仍是在實例上。person1.hasOwnProperty('name'),若是name爲實例屬性,則返回true。
咱們也能夠經過 'name' in person1 來肯定,person1上是否有name這個屬性。
上面你們可能已將發現,這種原型模式的寫法很是的繁瑣,有了大量的XXX.prototype. 這裏有一種簡寫的形式。
參照具體說明參照阮神的博客 面向對象第二篇
function Person(){}
Person.prototype = {
constructor:Person,
name:"Neal",
age:24,
job:'Software Engineer',
sayName:function(){
alert(this.name);
}
}複製代碼
上面代碼特地添加了一個constructor屬性,由於每建立一個函數,就會自動建立他的prototype對象,這個對象會自動獲取contractor屬性。而咱們這中寫法,本質上重寫了默認的prototype對象,所以,constructor屬性也就變成新的對象的constructor屬性了(指向Object構造函數),因此這裏的簡寫方式,必定要加上constructor。
下面咱們再談一談原型模式的優缺點。
優勢,正如上面咱們說到的,能夠省略爲構造函數傳遞出實話參數這個環節,而且不少實例能夠共享屬性和方法。正是由於原型中全部的屬性是被全部的實例所共享的,這個特性在方法中很是實用,可是對於包含引用類型的屬性來講問題就比較突出了。
function Person(){};
Person.prototype = {
constructor:Person,
name:"neal",
friends:['xiaohong','xiaoming'],
sayName:function(){
alert(this.name);
}
}
var person1 = new Person();
var person2 = new Person();
person1.friends.push('xiaohua');
alert(person1.friends);//'xiaohong','xiaoming','xiaohua'
alert(person2.friends);//'xiaohong','xiaoming','xiaohua'
alert(person1.friends == person2.friends)//true複製代碼
因爲friends數組存在於Person.prototype上,並非person1上面,因此當咱們修改的時候,其實修改的是全部實例所共享的那個值。
這是建立自定義類型最多見的一種方式。就是組合使用構造函數和原型模式.構造函數模式用於定義實力屬性,原型模式用於定義方法和共享的屬性。
function Person(name,age){
this.name = name,
this.age = age
}
Person.prototype = {
constructor:Person,
sayName:function(){
alert(this.name);
}
}
var person1 = new Person('Neal',24);
var person2 = new Person('Yang',23);
...複製代碼
上面的例子中,實例全部的屬性都是在構造函數中定義,而實例全部共享的屬性和方法都是在原型中定義。這種構造函數和原型模式混合的模式,是目前ECMAScript中使用最爲普遍的一種方法。
固然,有些人會以爲獨立的構造函數和原型很是的難受,因此也有推出所謂的動態原型構造模式的這麼一說。
function Person(name,age){
this.name = name,
this.age = age,
if(typeof this.sayName != 'function'){
Person.prototype.sayName = function(){
console.log(this.name)
}
}
}
...複製代碼
注意上面的代碼,以後在sayName不存在的時候,纔會在原型上給他添加相應的方法。由於對原型的修改,可以當即在全部的實例中獲得反應。因此這中作法確實也是很是的完美。
關於javaScript高程中說到的別的寄生構造函數模式和穩妥構造函數模式你們能夠自行查看哈~這裏就不作過多介紹了。
說到面向對象,固然得說到繼承。說到繼承固然得說到原型。說到原型,這裏咱們摘自網上一篇博客裏的段落
爲了說明javascript是一門面向對象的語言,首先有必要從面相對象的概念入手一、一切事物皆對象。二、對象具備封裝和繼承特性。三、對象與對象之間使用消息通訊,各自存在信息隱祕 。
javascript語言是經過一種叫作原型(prototype) 的方式來實現面向對象編程的。固然,還有好比java就是基於類來實現面向對象編程的。
對於基於類的面向對象的方式中,對象依靠class類來產生。而在基於原型的面向對象方式中,對象則是依靠構造器(constructor)利用原型(prototype)構造出來的。舉個客觀世界的例子來講,例如工廠造一輛汽車一方面,工人必須參照一張工程圖紙,設計規定這輛車如何製造,這裏的工程圖紙就比如語言中的類class。而車就是按照這個類製造出來的。另外一方面,工人和機器至關於contractor,利用各類零部件(prototype)將汽車造出來。
固然,對於上面的例子兩種思惟各類說法。固然,筆者更加傾向於基於原型的面向對象編程,畢竟我是前端出生(咳咳,真相了),正當理由以下:
首先,客觀世界中的對象的產生都是其餘實物對象構造的世界,而抽象的圖紙是不能產生出汽車的。也就是說,類,是一個抽象概念的而非實體,而對象的產生是一個實體的產生。其次,按照一切事物皆對象的這餓極本的面向對象的法則來講,類自己並非一個對象,然而原型方式的構造函數和原型自己也是個對象。再次,在類的面嚮對象語言中,對象的狀態又對象的實例所持有,對象的行爲方法則由申明該對象的類所持有,而且只有對象的構造和方法可以被繼承。而在原型的面嚮對象語言中,對象的行爲、狀態都屬於對象自己,而且可以一塊兒被繼承。
ECMAScript描述了原型鏈的概念,並將原型鏈做爲實現繼承的主要方法。基本思想就是利用原型讓一個引用類型繼承另外一個引用類型的屬性和方法。
實現原型鏈有一種基本模式:
function SuperType(){
this.property = true;
}
SuperType.prototype.getSuperValue = function(){
return this.property;
}
function SubType (){
this.subproperty = false;
}
SubType.prototype = new SuperType();
SubType.prototype.getSubValue = function(){
return this.subproperty;
}
var instance = new SubType();
alert(instance.getSuperValue());複製代碼
在上面的代碼中,咱們沒有使用SubType默認提供的原型,而是給它換了一個新的原型,這個新原型就是SuperType的實例。因而,新原型不只具備所謂一個SuperType的實例所擁有的所有屬性和方法,並且其內部還有一個指針,指向SuperType的原型。最終結果是這樣的:instance指向subtype的原型,subtype的原型又指向SuperType的原型。
經過實現原型鏈,本質上是擴展了原型搜索機制。
雖然如上,咱們已經實現了javascript中的繼承。可是依舊存在一些問題:最主要的問題來自包含引用類型的原型。第二個問題就是在建立子類型的實例時,不能向超類型的構造函數中傳遞參數。這兩個問題上面也都有說到,這裏就不作過多介紹,直接看解決辦法!
在解決原型中包含引用類型的數據時,咱們能夠在子類型構造函數內部調用超類型的構造函數。直接看代碼:
function SuperType(name){
this.colors = ['red','yellow'];
this.name = name;
}
function SubType(name){
//繼承了Super
SuperType.call(this,name)
}
var instance1 = new SubType('Neal');
alert(instance1.name)
instance1.colors.push('black');
alert(instance1.colors);//'red','yellow','black'
var instance2 = new SubType('yang');
alert(instance2.colors);//'red','yellow'複製代碼
畢竟函數只不過是在特定環境中執行代碼的對象,所以能夠經過call活着apply方法在新建立的對象上執行構造函數。並且如上代碼也解決了子類構造函數中向超類構造函數傳遞參數的問題
可是,這樣問題就來了,相似咱們以前討論建立的對象那種構造函數的問題:若是都是使用構造函數,那麼,也就避免不了方法都在構造函數中定義,而後就會產生大量重複的代碼了。
由於考慮到上述的缺點,因此這裏又使用了組合繼承的方式,歷史老是驚人的類似。直接看代碼:
function SuperType(name){
this.name = name;
this.colors = ['red','yellow'];
}
SuperType.prototype.sayName = function(){
alert(this.name);
}
function SubType(name,age){
//繼承屬性
SuperType.call(this,name);
this.age = age;
}
//繼承方法
SubType.prototype = new SuperType();
SubType.prototype.constructor = SubType;
SubType.prototype.sayAge = function(){
alert(this.age);
}
var instance1 = new SubType('Nealyang',24);
instance1.colors.push('white');
instance1.sayName();//Nealyang
instance1.sayAge();// 24
var instance2 = new SubType('Neal',21);
alert(instance2.colors);//'red','yellow'
instance2.sayName();//Neal
instance2.sayAge();//21複製代碼
在上面的例子中,SuperType構造函數定義了兩個屬性,name和colors,SuperType的原型中定義了一個方法sayName,subtype的構造函數中調用SuperType構造函數而且傳入name,而後將SuperType的實例賦值給subtype的原型。而後又在新的原型中定義了sayAge的方法。這樣一來,就可讓兩個不一樣的SubType實例既分別擁有本身的屬性,包括colors,又可使用相同的方法了。
組合繼承避免了原型鏈和借用構造函數的缺陷,融合了他們的優勢。成爲javascript中最爲常見的繼承模式。並且instanceof和isPrototypeOf方法也能用於識別組合模式建立的對象。
繼承模式是有不少,上面只是說到咱們常用到的繼承模式。包括還有原型式繼承、寄生式繼承、寄生組合式繼承等,其實,只要理解了原型、原型鏈、構造函數等對象的基本概念,理解起來這中模式都是很是容易的。後續若是有時間,再給你們總結吧~
掃碼關注個人我的微信公衆號,分享更多原創文章。點擊交流學習加我微信、qq羣。一塊兒學習,一塊兒進步
歡迎兄弟們加入:
Node.js技術交流羣:209530601
React技術棧:398240621
前端技術雜談:604953717 (新建)