前面的系列文章,基本把JavaScript的核心知識點的基本語法、標準庫等章節講解完;
本章開始進入JavaScript核心知識點的高級部分——面向對象的程序設計,這一部分的內容將會對對象這一數據類型作進一步的深化理解,而且講述幾種建立對象的設計模式以及JavaScript獨特的繼承機制;javascript
"面向對象編程"(Object Oriented Programming,縮寫爲OOP)自己是一種編程的思惟模式,它把世界的一切看做是對象的集合,世界的運轉就是靠一個個對象分工、合做的結果,體現一切皆「對象」思想;
而在程序設計當中,面向對象編程就能夠看作編寫各個具備特定功能的對象(模塊)並將它們進行有機的分工合做,即目前的模塊化編程就是面向對象的程序設計的實際應用;html
對象在前面的系列文章中曾經提到,從數據特徵上看,對象是無序屬性(鍵值對)的集合;
咱們可使用字面量和構造函數的方式去建立一個最爲簡單的對象:java
var person = new Object(); person.name = "teren"; person.age = 18; person.greet = function(){ console.log("hello "+this.name); } var teren = { name:"teren", age:18, greet:function(){ console.log("hello "+this.name); } }
一般建立一個簡單的對象,都是採用字面量的方式;
上面的對象就是對現實對象的一種抽象表達;編程
前面章節中咱們使用delete命令能夠刪除一些對象的屬性,有一些又不能夠,使用Object.keys()方法只能遍歷可枚舉屬性,那麼對象的屬性是否有一些特性是咱們還沒有了解的呢?
ES5提供了一種只有內部才用的特性(attribute)去描述對象的屬性(property)的各類特性,使用[[attribute]]
表示,在JavaScript中不能直接訪問它們;
一個咱們很是熟悉的栗子就是Number構造函數構造出來的實例對象;設計模式
咱們沒法直接訪問num.[[PrimitiveValue]]
,這一屬性,只能經過num.valueOf()
訪問該值;數組
ES5中定義對象屬性的兩種特性,數據特性和訪問器特性,對象屬性能夠兼備這兩種特性;閉包
數據特性定義對象的屬性值的特性,一個屬性值能夠包括如下四個數據特性:模塊化
[[Value]]:存放屬性值; [[Writable]]:是否可寫屬性; [[Enumerable]]:是否爲可枚舉屬性; [[Configurable]]:是否可用delete命令刪除;
訪問器特性定義對象的屬性在訪問屬性和設置屬性時調用的兩個函數getter和setter;函數
[[Get]]:訪問屬性時調用的函數; [[Set]]:設置屬性時調用的函數;
下面以一個實例對象直接講解這兩個特性:this
//數據特性; var teren = {}; Object.defineProperty(teren,{ value:"teren", writable:false, enumerable:true, configurable:true }) //訪問器特性; //html <div id="name"></div> //js var obj = Object.defineProperty({},"name",{ set:function(name){ document.getElementById('name').innerHTML=name }, get:function(){ console.log( document.getElementById('name').innerHTML ) }, }) obj.name = "hello world" obj.name
【demo】
Object.defineProperties能夠一次性配置對象的多個屬性;
上一節咱們對面向對象的程序設計思想和對象有了初步理解,這一節咱們深刻探討一下對象的建立方式及其優缺點;
建立對象的不一樣方式也能夠簡單的稱做設計模式,不一樣的設計模式在實際編程應用中起到不一樣的做用;
單例模式就是產生一個類的惟一實例對象,它可以確保您只有一個對象實例可以實際派上用場;
單例模式下,建立對象方式以下:
var singleton = { attr:1, method:function(){ return this.attr } } var ex1 = singleton; var ex2 = singleton; ex1 === ex2//true
上述建立單例的方式:
優勢:使用很是簡捷;
缺點:缺少封裝,成員暴露,初始化時佔用資源;
可使用閉包方式解決這一問題:
var substance = (function(){ var unique; function init(){ var type; return { setType:function(t){ return type = t; } } } return { getInstance:function(){ if(!unique){ unique = init(); } return unique; } } })(); var Adam = substance.getInstance(); var Eve = substance.getInstance(); Adam === Eve Adam.setType('Man')//Man
單例模式只能創做單個實例對象,也就是說若是將該實例對象賦予多個變量時,會存在對象的引用問題,即修改其中一個變量會影響到另外一個變量;
有時咱們須要創造多個結構類似的對象,只有部分屬性有所區別,這時候工廠模式派上用場;
工廠模式的設計思想就是可以像工廠同樣批量生產出類似屬性和方法的對象,使用工廠模式能解決多個類似的問題,例如創造多個彈窗(只是標題不一樣);
function person(name,age){ var obj = new Object(); obj.name = name; obj.age = age; obj.greet = function(){ return "hello "+this.name; }; return obj } var Adam = person("Adam",18); var Eve = person("Eve",20);
上述工廠模式:
優勢:能批量生產結構相似的對象;封裝建立對象的細節;
缺點:未能解決對象的類型,即由哪一個構造函數建立的;
構造函數能夠建立特定類型的對象,相似以前的Array、RegExp等原生對象都能創造特定類型的實例對象;
function Person(name,age){ this.name = name; this.age = age; this.greet = function(){ return "hello "+this.name; } } var p1 = new Person('Adam',18); var p2 = new Person('Eve',20);
使用構造函數模式就可以解決實例對象由誰建立的問題;
上述代碼和工廠模式的區別在於:
1.沒有顯示建立新對象;
2.直接將屬性和方法賦給this對象;
3.沒有return語句;
4.函數名開頭大寫以區別普通函數;
5.使用new操做符去建立對象實例;
new操做符的原理
使用new操做符去調用函數和直接調用函數不一樣,其new操做符的運行函數的過程爲:
建立一個新對象;
將構造函數的做用域賦給新對象並執行構造函
內的代碼;
返回新對象;
使用代碼表示以下:
function Person(name,age){ this.name = name; this.age = age; this.greet = function(){ return "hello "+this.name; } } function createPerson(name,age){ var o = new Object(); Person.call(o,name,age); return o; } var p1 = createPerson('Adam',18); var p2 = createPerson('Eve',20);
使用構造函數模式建立對象的優缺點在於:
優勢:可以識別對象屬於的構造函數;
缺點:若是存在不一樣實例對象共享的屬性和方法,使用構造函數模式則會浪費內存;
【注】
關於this關鍵字的更多知識點能夠參見【what's this】;
構造函數若是不用new操做符調用和普通函數是同樣的;
每一個函數都有一個prototype原型屬性,這個原型屬性能夠部署特定類型的實例共享的屬性和方法;
function Person(){} Person.prototype.greet = function(){ return "hello "+this.name; }
將原來的greet函數部署在Person函數的prototype原型屬性上,這樣p1和p2能夠共享該方法,而不像構造函數模式每建立一個實例就增長一個greet方法浪費內存;
【注】
關於原型對象的更多理解詳見下一節——JavaScript的繼承機制;
使用原型模式建立對象的優缺點在於:
優勢:對於每一個實例的共享屬性和方法可以較好實現;
缺點:單獨採用原型模式將沒法區分不一樣實例的私有屬性;
混合模式,就是綜合構造函數模式和原型模式的優缺點,構造函數模式部署實例的私有屬性,原型模式部署實例的公有屬性;
function Person(name,age){ this.name = name; this.age = age; } Person.prototype.greet = function(){ return "hello "+this.name; } var p1 = new Person("Adam",18); var p2 = new Person("Eve",20);
混合模式是目前使用最普遍、認同度最高的一種建立自定義類型(類)的設計模式;
【注】
固然,設計模式不只僅上述所提到,還有更加精深能夠參考《設計模式》一書以及以前小羊寫的一篇文章《設計模式梗概》;
上一節咱們經過建立對象的不一樣模式,隱式引出了原型對象的概念,這一節中咱們將詳細瞭解一下原型對象、原型鏈及其實現的繼承機制;
前面,咱們從數據特徵上看,知道對象是無序屬性(鍵值對)的集合;
如今,咱們能夠從面向對象的角度看,任何對象都是更爲抽象的對象的實例,能夠理解爲類的概念;
從這個角度理解,咱們如今能夠從新定義一下對象和類的含義:
對象能夠說是對現實事物的抽象,對象封裝了屬性和方法,屬性值指的是對象的狀態,方法指的是對象的行爲;
類是提供一種模板的‘對象’,它是對象的抽象;
舉個簡單的栗子:
function Person(name,age){ this.name = name; this.age = age; } Person.prototype.greet = function(){ return "hello "+this.name; } var p1 = new Person("Adam",18); var p2 = new Person("Eve",20);
上述代碼代表,p1和p2兩個實例是對現實Adam和Eve的抽象,而「類」Person又是對2個實例的抽象,爲建立類似結構的人提供標準的模板;
[注]ES6以前JavaScript中沒有類,在ES6中定義了類;
在上一節的原型模式中,咱們提到每一個函數都有一個prototype屬性,這個屬性指向函數的原型對象,能夠部署特定類型的實例共享的屬性和方法;
更爲深刻理解prototype原型對象,prototype原型對象不只能夠部署特定類型的實例共享的屬性和方法,並且仍是實現JavaScript繼承的關鍵;
只要建立一個新函數就會爲該函數建立一個prototype屬性,每一個prototype屬性自動得到一個constructor屬性,該屬性指向prototype屬性所在的函數指針;
當使用構造函數建立一個實例時,該實例內部包含一個內部屬性__proto__
指向構造函數的原型對象;
由此,一個簡單的繼承便產生了;
如下面代碼爲例:
function Person(name,age){ this.name = name; this.age = age; } Person.prototype.greet = function(){ return "hello "+this.name; } var p1 = new Person("Adam",18); var p2 = new Person("Eve",20);
構造函數建立以後,自動建立一個prototype屬性,prototype原型對象下有一個默認的constructor屬性指向prototype屬性所在的函數Person中;
在prototype原型對象上部署greet方法,實例p1的內部屬性__proto__
指向構造函數Person.prototype,由此繼承了構造函數的原型對象上的greet方法;
【注意】
實例的__proto__
指向構造函數的prototype原型對象實現繼承,這種聯繫存在於實例與構造函數的原型對象之間而不是構造函數之間;
當js引擎解析對象的屬性時,先會搜索對象自己的屬性,若是沒有則會去搜索__proto__
指向的原型對象上的屬性,直到找到爲止,若是在對象自己定義的屬性和原型對象上的具備相同屬性名,則在讀取該屬性時,自身屬性會屏蔽原型對象上的屬性;
function Person(name,age){ this.name = name; this.age = age; } Person.prototype.greet = function(){ return "hello "+this.name; } var p1 = new Person("Adam",18); p1.greet()//hello Adam; p1.greet = function(){ return "hello world" }
修改構造函數的原型對象能夠直接使用點操做,效果是直接在原來的原型對象上增長屬性,有時須要增長的屬性太可能是,點操做就顯得太麻煩,能夠採用重置原型對象的方法:
function Person(name,age){ this.name = name; this.age = age; } Person.prototype = { constructor:Person, greet1:function(){}, greet2:function(){}, greet3:function(){} }; var p1 = new Person("Adam",18); Person.prototype.constructor.name//"Object"
須要注意的是,重置原型對象後,要從新爲原型對象的constructor的屬性指回Person構造函數;
若是不重置constructor的話,那麼此時的Person.prototype是由字面量建立的對象,字面量建立的對象默認的構造函數是Object;
上面咱們只定義一個構造函數,實現一次繼承;若是存在多個構造函數,它們之間也存在繼承關係,那麼就會造成一條關於繼承的原型鏈;
function SuperType(name,age){ this.name = name; this.age = age } SuperType.prototype.greet = function(){ return "hello "+this.name } function SubType(name,age,height){ SuperType.call(this,name,age); this.height = height; } SubType.prototype = Object.create(SuperType.prototype); SubType.prototype.constructor = SubType; SubType.prototype.method = function(){return 1;} var instance = new SubType('teren',18,180)
上面就是一個最爲經常使用的實現多個類間繼承的設計模式;
使用Object.create(SuperType.prototype)的優缺點在於:
優勢:可以建立一個新的SuperType.prototype對象賦給SubType.prototype,修改SubType.prototype這個而不影響原來構造函數SuperType.prototype;
缺點:雖然擁有子類的prototype和父類的prototype值是相同的,但內存不一樣,從而切斷子類和父類之間的類型;
還可使用SubType.prototype =new SuperType()實現相同效果,其優缺點在於:
優勢:可以體現子類和父類的繼承關係;
缺點:子類具備父類的私有屬性;
因此,通常在實際實現原型鏈時使用Object.create()方法,而理解原型鏈時使用new SuperType()方法;
遍歷對象屬性方法Object.keys()
和Object.getOwnPropertyNames()
用於遍歷對象自身而不是繼承的屬性名,返回一個數組,其中Object.keys()只返回可枚舉屬性;
in
用於檢查一個對象是否具備某個屬性。它不區分該屬性是對象自身的屬性,仍是繼承的屬性;for...in
用於遍歷對象的全部可枚舉屬性(不論是自身的仍是繼承的)
若是隻遍歷自身的屬性,可使用以下代碼:
for (var key in instance){ if(instance.hasOwnProperty(key)){ console.log(key) } }
判斷屬性是否爲自身的方法Object.prototype.hasOwnProperty()
返回一個布爾值,用於判斷某個屬性定義在對象自身,仍是定義在原型鏈上;
設置和獲取實例對象的原型對象的方法Object.getPropertyOf()
返回一個實例對象的原型對象;Object.setPropertyOf(obj,prototype)
可傳兩個參數,第1個爲現有參數,第2個爲原型對象;
判斷一個對象是否爲另外一個對象的原型對象Object.prototype.isPrototypeOf()
用於判斷一個對象是不是另外一個對象的原型;
通讀本文,咱們能夠知道:
面向對象編程時一種思惟模式,它把世界的一切看作是對象的集合,世界的運做是一個個對象分工合做的結果;映射到程序設計中,就是編寫各個具備特定功能的對象(模塊),並將它們有機整合使程序得以運做;
對象從數據特徵角度看,是無序鍵值對的集合,對象的屬性具備兩種特性——數據特性和訪問器特性;
建立對象的不一樣方式能夠稱爲設計模式,本文簡單講解了單例模式、工廠模式、構造函數模式、原型模式、混合模式等;
從面向對象角度看,對象能夠說是對現實事物的抽象,類是對對象的抽象;
每一個函數都有一個原型對象prototype,既能夠部署特定類型實例的共享屬性和方法,也是JavaScript實現繼承的關鍵;
prototype原型對象有一個constructor屬性,該屬性指向prototype所在的函數指針;
每當使用構造函數建立一個實例時,該實例內部包含一個內部屬性__proto__
指向構造函數的原型對象,由此實現簡單的繼承;
當A構造函數是B構造函數的實例時,由此就會造成一條原型鏈,即
A構造函數的實例對象C的__proto__
指向A構造函數的原型對象prototype,A構造函數prototype的__proto__
指向B構造函數的原型對象prototype,B構造函數prototype的__proto__
指向Function構造函數的prototype,Function的prototype的__proto__
指向Object的prototype;
與原型對象的相關方法包括:Object.keys()和Object.getPropertyNames()、for...in方法,Object.getPrototypeOf()和Object.setPrototypeOf()方法,Object.prototype.hasOwnProperty()和Object.prototype.isPrototypeOf()方法;
《JavaScript高級程序設計(第3版)》