忘記在哪裏看到過,有人說鑑別一我的是否 js 入門的標準就是看他有沒有理解 js 原型,因此第一篇總結就從這裏出發。數據庫
對象編程
JavaScript 是一種基於對象的編程語言,但它與通常面向對象的編程語言不一樣,由於他沒有類(class)的概念。設計模式
對象是什麼?ECMA-262 把對象定義爲:「無序屬性的集合,其屬性能夠包含基本值、對象或者函數。」簡單來講,對象就是一系列的鍵值對(key-value),我習慣把鍵值對分爲兩種,屬性(property)和方法(method)。安全
面向對象編程,在個人理解裏是一種編程思想。這種思想的核心就是把萬物都抽象成一個個對象,它並不在意數據的類型以及內容,它在意的是某個或者某種數據可以作什麼,而且把數據和數據的行爲封裝在一塊兒,構建出一個對象,而程序世界就是由這樣的一個個對象構成。而類是一種設計模式,用來更好地建立對象。app
舉個例子,把我本身封裝成一個簡單的對象,這個對象擁有個人一些屬性和方法。編程語言
//構造函數建立
var klaus = new Object(); klaus.name = 'Klaus'; klaus.age = 22; klaus.job = 'developer'; klaus.introduce = function(){ console.log('My name is ' + this.name + ', I\'m ' + this.age + ' years old.'); };
//字面量語法建立,與上面效果相同
var klaus = { name: 'Klaus', age: 22, job: 'developer', introduce: function(){ console.log('My name is ' + this.name + ', I\'m ' + this.age + ' years old.'); } };
這個對象中,name、age 和 job 是數據部分,introduce 是數據行爲部分,把這些東西都封裝在一塊兒就構成了一個完整的對象。這種思想不在意數據(name、age 和 job)是什麼,它只在意這些數據能作什麼(introduce),而且把它們封裝在了一塊兒(klaus 對象)。函數式編程
跑一下題,與面向對象編程相對應的編程思想是面向過程編程,它把數據和數據行爲分離,分別封裝成數據庫和方法庫。方法用來操做數據,根據輸入的不一樣返回不一樣的結果,而且不會對輸入數據以外的內容產生影響。與之相對應的設計模式就是函數式編程。函數
工廠模式建立對象this
若是建立一個簡單的對象,像上面用到的兩種方法就已經夠了。可是若是想要建立一系列類似的對象,這種方法就太過麻煩了。因此,就順勢產生了工廠模式。spa
function createPerson(name, age, job){ var o = new Object(); o.name = name; o.age = age; o.job = job; o.introduce = function(){ console.log('My name is ' + this.name + ', I\'m ' + this.age + ' years old.'); }; return o; } var klaus = createPerson('Klaus', 22, 'developer');
隨着 JavaScript 的發展,這種模式漸漸被更簡潔的構造函數模式取代了。(高程三中提到工廠模式沒法解決對象識別問題,我以爲徹底能夠加一個_type 屬性來標記對象類型)
構造函數模式建立對象
咱們能夠經過建立自定義的構造函數,而後利用構造函數來建立類似的對象。
function Person(name, age, job){ this.name = name; this.age = age; this.job = job; this.introduce = function(){ console.log('My name is ' + this.name + ', I\'m ' + this.age + ' years old.'); }; } var klaus = new Person('Klaus', 22, 'developer'); console.log(klaus instanceof Person); //true console.log(klaus instanceof Object); //true
如今咱們來看一下構造函數模式與工廠模式對比有什麼不一樣:
函數名首字母大寫:這只是一種約定,寫小寫也徹底沒問題,可是爲了區別構造函數和通常函數,默認構造函數首字母都是大寫。
不須要建立對象,函數最後也不須要返回建立的對象:new 操做符幫你建立對象並返回。
添加屬性和方法的時候用 this:new 操做符幫你把 this 指向建立的對象。
建立的時候須要用 new 操做符來調用構造函數。
能夠獲取原型上的屬性和方法。(下面會說)
能夠用 instanceof 判斷建立出的對象的類型。
new
這麼看來,構造函數模式的精髓就在於這個 new 操做符上,因此這個 new 到底作了些什麼呢?
建立一個空對象。
在這個空對象上調用構造函數。(因此 this 指向這個空對象)
將建立對象的內部屬性__proto__指向構造函數的原型(原型,後面講到原型會解釋)。
檢測調用構造函數後的返回值,若是返回值爲對象(不包括 null)則 new 返回該對象,不然返回這個新建立的對象。
用代碼來模仿大概是這樣的:
function _new(fn){ return function(){ var o = new Object(); var result = fn.apply(o, arguments); o.__proto__ = fn.prototype; if(result && (typeof result === 'object' || typeof result === 'function')){ return result; }else{ return o; } } } var klaus = _new(Person)('Klaus', 22, 'developer');
組合使用構造函數模式和原型模式
構造函數雖然很好,可是他有一個問題,那就是建立出的每一個實例對象裏的方法都是一個獨立的函數,哪怕他們的內容徹底相同,這就違背了函數的複用原則,並且不能統一修改已建立實例對象裏的方法,因此,原型模式應運而生。
function Person(name, age, job){ this.name = name; this.age = age; this.job = job; this.introduce = function(){ console.log('My name is ' + this.name + ', I\'m ' + this.age + ' years old.'); }; } var klaus1 = new Person('Klaus', 22, 'developer'); var klaus2 = new Person('Klaus', 22, 'developer'); console.log(klaus1.introduce === klaus2.introduce); //false
什麼是原型?咱們每建立一個函數,他就會自帶一個原型對象,這個原型對象你能夠理解爲函數的一個屬性(函數也是對象),這個屬性的 key 爲 prototype,因此你能夠經過 fn.prototype 來訪問它。這個原型對象除了自帶一個不可枚舉的指向函數自己的 constructor 屬性外,和其餘空對象並沒有不一樣。
那這個原型對象到底有什麼用呢?咱們知道構造函數也是一個函數,既然是函數那它也就有本身的原型對象,既然是對象你也就能夠給它添加一些屬性和方法,而這個原型對象是被該構造函數全部實例所共享的,因此你就能夠把這個原型對象當作一個共享倉庫。下面來講說他具體是如何共享的。
上面講 new 操做符的時候講過有一步,將建立對象的內部屬性__proto__指向構造函數的原型,這一步纔是原型共享的關鍵。這樣你就能夠在新建的實例對象裏訪問構造函數原型對象裏的數據。
function Person(name, age, job){ this.name = name; this.age = age; this.job = job; this.introduce = this.__proto__.introduce; //這句能夠省略,後面會介紹 } Person.prototype.introduce = function(){ console.log('My name is ' + this.name + ', I\'m ' + this.age + ' years old.'); }; var klaus1 = new Person('Klaus', 22, 'developer'); var klaus2 = new Person('Klaus', 22, 'developer'); console.log(klaus1.introduce === klaus2.introduce); //true
這樣,咱們就達到了函數複用的目的,並且若是你修改了原型對象裏的 introduce 函數後,全部實例的 introduce 方法都會同時更新,是否是很方便呢?可是原型絕對不止是爲了這麼簡單的目的所建立的。
咱們首先明確一點,當建立一個最簡單的對象的時候,其實默認用 new 調用了 JavaScript 內置的 Objcet 構造函數,因此每一個對象都是 Object 的一個實例(用 Object.create(null) 等特殊方法建立的暫不討論)。因此根據上面的介紹,每一個對象都有一個__proto__的屬性指向 Object.prototype。這是理解下面屬性查找機制的前提。
var klaus = { name: 'Klaus', age: 22, job: 'developer', introduce: function(){ console.log('My name is ' + this.name + ', I\'m ' + this.age + ' years old.'); } }; console.log(klaus.friend); //undefined console.log(klaus.toString); //ƒ toString() { [native code] }
上面代碼能夠看出,若是咱們訪問 klaus 對象上沒有定義的屬性 friend,結果返回 undefined,這個能夠理解。可是一樣訪問沒定義的 toString 方法卻返回了一個函數,這是否是很奇怪呢?其實一點不奇怪,這就是 JavaScript 對象的屬性查找機制。
屬性查找機制:當訪問某對象的某個屬性的時候,若是存在該屬性,則返回該屬性的值,若是該對象不存在該屬性,則自動查找該對象的__proto__指向的對象的此屬性。若是在這個對象上找到此屬性,則返回此屬性的值,若是__proto__指向的對象也不存在此屬性,則繼續尋找__proto__指向的對象的__proto__指向的對象的此屬性。這樣一直查下去,直到找到 Object.prototype 對象,若是還沒找到此屬性,則返回 undefined。(原型鏈查找,講繼承時會詳細講)
理解了上面的查找機制之後,也就不難理解 klaus.toString 其實也就是 klaus.__proto__.toString,也就是 Object.prototype.toString,因此就算你沒有定義依然也能夠拿到一個函數。
理解了這一點之後,也就理解了上面 Person 構造函數裏的那一句我爲何註釋了能夠省略,由於訪問實例的 introduce 找不到時會自動找到實例__proto__指向的對象的 introduce,也就是 Person.prototype.introduce。
這也就是原型模式的強大之處,由於你能夠在每一個實例上訪問到構造函數的原型對象上的屬性和方法,並且能夠實時修改,是否是很方便呢。
除了給原型對象添加屬性和方法以外,也能夠直接重寫原型對象(由於原型對象本質也是一個對象),只是別忘記添加 constructor 屬性。
還須要注意一點,若是原型對象共享的某屬性是個引用類型值,一個實例修改該屬性後,其餘實例也會所以受到影響。
以及,若是用 for-in 循環來遍歷屬性的 key 的時候,會遍歷到原型對象裏的可枚舉屬性。
function Person(name, age, job){ this.name = name; this.age = age; this.job = job; } Person.prototype = { introduce: function(){ console.log('My name is ' + this.name + ', I\'m ' + this.age + ' years old.'); }, friends: ['person0', 'person1', 'person2'] }; Object.defineProperty(Person.prototype, 'constructor', { enumerable: false, value: Person }); var klaus1 = new Person('Klaus', 22, 'developer'); var klaus2 = new Person('Klaus', 22, 'developer'); console.log(klaus1.friends); //['person0', 'person1', 'person2'] klaus1.friends.push('person3'); console.log(klaus1.friends); //['person0', 'person1', 'person2', 'person3'] console.log(klaus2.friends); //['person0', 'person1', 'person2', 'person3'] for(var key in klaus1){ console.log(key); //name, age, job, introduce, friends }
ES6 class
若是你有關注最新的 ES6 的話,你會發現裏面提出了一個關鍵字 class 的用法,難道 JavaScript 要有本身類的概念了嗎?
tan90°,不存在的,這只是一個語法糖而已,上面定義的 Person 構造函數能夠用 class 來改寫。
class Person{ constructor(name, age, job){ this.name = name; this.age = age; this.job = job; } introduce(){ console.log('My name is ' + this.name + ', I\'m ' + this.age + ' years old.'); } } Person.prototype.friends = ['person0', 'person1', 'person2']; var klaus = new Person('Klaus', 22, 'developer');
很遺憾,ES6 明確規定 class 裏只能有方法而不能有屬性,因此像 friends 這樣的屬性可能只能在外面單獨定義了。
下面簡單舉幾個差別點,若是想詳細瞭解能夠去看阮一峯的《ECMAScript 6 入門》或者 Nicholas C. Zakas 的《Understanding ECMAScript 6》。
class 裏的靜態方法(相似於 introduce)是不可枚舉的,而用 prototype 定義的是可枚舉的。
class 裏面默認使用嚴格模式。
class 已經不屬於普通的函數了,因此不使用 new 調用會報錯。
class 不存在變量提高。
class 裏的方法能夠加 static 關鍵字定義靜態方法,這種靜態方法就不是定義在 Person.prototype 上而是直接定義在 Person 上了,只能經過 Person.method() 調用而不會被實例共享。
做用域安全的構造函數
不論是高程仍是其餘的一些資料都提到過做用域安全的構造函數這個概念,由於構造函數若是不用 new 來調用就只是一個普通的函數而已,這樣在函數調用的時候 this 會指向全局(嚴格模式爲 undefined),這樣若是錯誤調用構造函數就會把屬性和方法定義在 window 上。爲了不這種狀況,能夠將構造函數稍加改造,先用 instanceof 檢測 this 而後決定調用方法。
function Person(name, age, job){ if(this instanceof Person){ this.name = name; this.age = age; this.job = job; }else{ return new Person(name, age, job); } } var klaus1 = Person('Klaus', 22, 'developer'); var klaus2 = new Person('Klaus', 22, 'developer'); //兩種方法結果同樣
不過我的認爲這種沒什麼必要,構造函數已經首字母大寫來加以區分了,若是還錯誤調用的話那也沒啥好說的了。。。