JavaScript
,或者說ECMAScript
具備面嚮對象語言的一些特色,但它不是一門純粹的面嚮對象語言,由於它也包含着函數式編程的一些東西。事實上,如今不少的面向對象的語言,好比Java
,也開始實現一些函數式的新特性。總之,全部的編程語言都在隨着應用場景的變化而不斷進化。編程
這篇文章儘量的將ECMAScript
這門語言中關於面向對象的實現表述徹底。好了,咱們先從對象開始吧!數組
對象的定義:無序屬性的集合,其屬性能夠包含基本值、對象、或者函數。能夠看作一個散列。瀏覽器
對象的建立:每一個對象都是基於一個引用類型建立的,這個引用類型能夠是原生類型,也能夠是自定義的類型。安全
對象有屬性(property
),屬性有特性(attribute
),特性通常表示形式爲雙方括號(如[[attribute]]
)。編程語言
對象有兩種屬性(property
):數據屬性和訪問器屬性。函數式編程
數據屬性包含一個數據值的位置。在這個位置能夠讀/寫值。數據屬性有四個特性([[attribute]]
):函數
[[Configurable]]
表示可否經過delete
刪除屬性,可否修改屬性的特性值,可否把屬性修改成訪問器屬性。直接在對象上定義的屬性的默認值爲true
[[Enumerable]]
表示可否經過for-in
循環返回屬性。直接在對象上定義的屬性的默認值爲true
[[Writable]]
表示可否修改屬性的值。直接在對象上定義的屬性的默認值爲true
[[Value]]
屬性的數據值。此處用來存儲。默認值爲undefined
defineProperty
方法能夠配置屬性的特性。(IE9+)post
var obj = {}
Object.defineProperty(obj, 'x', {
configurable: true,
enumerable: true,
writable: true,
value: 123
})
obj; // {x: 123}
複製代碼
訪問器屬性沒有數據值([[Value]]
),因此也沒有([[Writable]]
),可是多了[[Get]]
、[[Set]]
。也叫getter
、setter
。用於讀、寫對應屬性的值。測試
[[Configurable]]
表示可否經過delete
刪除屬性,可否修改屬性的特性值,可否把屬性修改成訪問器屬性。直接在對象上定義的屬性的默認值爲true
[[Enumerable]]
表示可否經過for-in
循環返回屬性。直接在對象上定義的屬性的默認值爲true
[[Get]]
讀取屬性時調用的函數。默認值是undefined
[[Set]]
寫入屬性時調用的函數,參數爲寫入值,默認值是undefined
訪問器屬性不能直接定義,必須使用defineProperty
來定義。ui
var book = {
_page: 2
}
Object.defineProperty(book, 'page', {
get: function () {
console.log('你調用了get方法')
return this._page
},
set: function (val) {
console.log('你調用了set方法')
this._page = val
}
})
book.page; // 你調用了get方法
book.page = 3 // 你調用了set方法
複製代碼
defineProperty
是ES5新加的方法,在此以前,對於getter
、setter
,瀏覽器內部有本身的實現。
var book = {
_page: 2
}
book.__defineGetter__('page', function () {
return this._page
})
book.__defineSetter__('page', function (val) {
this._page = val
})
複製代碼
defineProperties()
(ES5,IE9+)能夠同時定義多個屬性。
var book = {}
Object.defineProperties(book, {
_page: {
value: 2
},
author: {
value: 'JiaHeSheng'
},
page: {
get: function () {
return this._page
},
set: function (val) {
this._page = val
}
}
})
book;
/* { author: "JiaHeSheng" page: 2 _page: 2 get page: ƒ () set page: ƒ (val) } */
複製代碼
如何查看對象某個屬性的特性呢?ES5提供了getOwnPropertyDescriptor
方法, 它返回一個屬性所擁有的特性組成的對象。
var obj = { x: 456 }
Object.getOwnPropertyDescriptor(obj, 'x')
/* { configurable: true enumerable: true value: 456 writable: true } */
複製代碼
建立對象最簡單的模式就是經過對象字面量進行建立,如var obj = {}
。也能夠經過Object
構造函數,配合new
命令進行建立,如var obj = new Object()
。但這些都適用於建立單個對象,若是我要批量建立一些「具備某些相同屬性」的對象呢?
經過參數傳入工廠函數,每次均可已生成一個包含特有信息和相同信息的新對象。但缺點是,咱們並不能經過其生成的實例找到與對應的工廠函數的關聯。
function factoryFoo (name, age) {
var o = {}
o.name = name;
o.age = age;
o.say = function () {
console.log(this.name)
}
return o
}
var p1 = factoryFoo('Tom', 23)
var p2 = factoryFoo('Jack', 24)
p1.constructor // Object
p1 instanceof factoryFoo // false
複製代碼
咱們能夠看到,實例的constructor
指向了Object
,而且也不能證實p1
是工廠函數的實例。
爲了解決工廠函數帶來了問題,咱們試着使用構造函數+new
來生成實例。
function Person (name, age) {
this.name = name;
this.age = age;
this.say = function () {
console.log(this.name)
}
}
var p1 = new Person('Tom', 23)
var p2 = new Person('Jack', 24)
p1.constructor // Person
p2.constructor // Person
p1 instanceof Person // true
p1 instanceof Object // true
複製代碼
咱們能夠看到,使用構造函數模式構造出來的對象實例,能夠經過其constructor
屬性找到它的構造函數。(解決了工廠函數的問題)
另外,與工廠函數相比,少了顯式地建立對象,少了return
語句。這是由於使用new
操做符,隱式地作了這些事情。
構造函數與普通函數的區別就是,它被new
命令用來建立了實例。換言之,沒有被new
操做的構造函數就是普通函數。
構造函數也有它的缺點:每一個方法都會在實例化的時候被從新創造一遍,即便它們如出一轍。 上例中的say
方法就被創造了兩次。
// 建立實例時
this.say = new Function('console.log(this.name)')
// 建立後
p1.say === p2.say // false
複製代碼
爲了解決這個問題,咱們能夠這樣:
function say () {
console.log(this.name)
}
function Person (name, age) {
this.name = name;
this.age = age;
this.say = say
}
複製代碼
可是這又引出了一個新問題:總不能每一個方法都這樣全局定義吧?
new
運算符用來建立一個用戶定義的對象類型的實例或具備構造函數的內置對象的實例。
當代碼 new Foo(...) 執行時,會發生如下事情:
Foo.prototype
的新對象被建立。Foo
,並將 this
綁定到新建立的對象。new Foo
等同於 new Foo()
,也就是沒有指定參數列表,Foo
不帶任何參數調用的狀況。new
表達式的結果。若是構造函數沒有顯式返回一個對象,則使用步驟1建立的對象。(通常狀況下,構造函數不返回值,可是用戶能夠選擇主動返回對象,來覆蓋正常的對象建立步驟)爲了解決上面的問題,ECMAScript
語言中有了原型(prototype
)和原型鏈的概念。
每個函數上都有一個prototype
屬性,這個屬性是一個指針,指向一個對象,這個對象包含了一些屬性和方法,這些屬性和方法能夠被全部由這個函數建立的實例所共享。
舉例來講,任意一個函數Person
的prototype
屬性指向對象prototypeObject
對象,全部由new Person()
建立的實例(p1
、p2
...pn
),都會共享prototypeObject
的屬性和方法。
function Person (){
}
Person.prototype.age = 34;
Person.prototype.getAge = function () {
return this.age
}
var p1 = new Person()
var p2 = new Person()
p1.age === p2.age // true
p1.age // 34
p1.getAge === p2.getAge // true
p2.getAge() // 34
複製代碼
不管何時,建立一個新函數,新函數就會有prototype
屬性,它指向該函數的原型對象。 默認狀況下,每一個原型對象都有一個屬性constructor
,它指向原型所在的函數。 當調用這個函數生成出一個實例以後,生成的實例有個隱藏的屬性(不可見,也沒法訪問)[[prototype]]
,它指向原型對象。 幸虧,瀏覽器實現了這個屬性:__proto__
,經過這個屬性能夠訪問原型對象。不過這不是標準實現,不建議在生產環境中使用。
經過上面的示例和描述,我製做了一張圖片,說明「構造函數、實例、原型對象」的關係:
知道他們的關係以後,咱們看下經過哪些方法能夠查看他們關係。
// 證實 p1是 Person 的實例
p1.constructor === Person // true
p1 instanceof Person // true
// 證實 Person.prototype 是 p1 的原型對象
Person.prototype === p1.__proto__ // true
Person.prototype.isPrototypeOf(p1) // true
Object.getPrototypeOf(p1) === Person.prototype // true
複製代碼
點擊查看Object.prototype.isPrototypeOf()和Object.getPrototypeOf的詳細用法。
若是要讀取實例上的屬性或者方法,就會如今實例對象上搜索,若是有就返回搜到的值;若是沒有,繼續在實例的原型對象上搜索,若是搜到,就返回搜到的值,若是沒搜到,就返回undefined
。 下面是示例:
function Person () {}
var p1 = new Person();
p1.x // undefined
Person.prototype.x = 'hello'
p1.x // hello 來自原型對象
p1.x = 'world'
p1.x // world 來自實例
Person.prototype.x // hello
複製代碼
這是抽象出來的搜索流程圖:
咱們在代碼示例中看到,給示例的屬性賦值,並無覆蓋原型上對應的屬性值,只是在搜索時,屏蔽掉了而已。 而這就是使用原型對象的好處:生成的實例,能夠共享原型對象的屬性和方法,也能夠在自身自定義屬性和方法,即便同名也互不影響,而且優先使用實例上的定義。
繼續看下面的代碼:
p1.x = null
p1.x // null 來自實例
delete p1.x
p1.x // hello 來自原型對象
複製代碼
設置屬性值爲null
,獲取屬性的時候,並不會跳過實例,若是要從新創建與原型對象的連接,可使用delete
刪除實例上的屬性。
那麼如何知道當前獲取的屬性值是在實例仍是在原型對象上面定義的呢?ECMAScript
提供了hasOwnProperty方法,該方法會忽略掉那些從原型鏈上繼承到的屬性。
p1.x = 'world'
p1.hasOwnProperty('x') // true
delete p1.x
p1.hasOwnProperty('x') // false
Person.prototype.x = 'hello'
p1.hasOwnProperty('x') // false
Person.prototype.hasOwnProperty('x') // true
複製代碼
in
操做符in
操做符會在經過對象可以訪問給定屬性時返回true
,不管該屬性存在於實例仍是原型中。
Person.prototype.x = 'hello'
'x' in Person.prototype // true
'x' in p1 // true
p1.x = 'world'
'x' in p1 // true
複製代碼
組合使用in
操做符和hasOwnProperty
便可判斷取到的屬性值,是否是存於原型中的。
function hasPrototypeProperty (obj, name) {
return !obj.hasOwnProperty(name) && (name in obj)
}
Person.prototype.x = 'hello'
hasPrototypeProperty(p1, 'x') // true
p1.x = 'world'
hasPrototypeProperty(p1, 'x') // false
複製代碼
那麼,如何獲取對象上全部自身的屬性和方法呢?
Object.keys。能夠獲取對象上全部自身的可枚舉屬性和方法名,返回一個名稱列表。
Object.getOwnPropertyNames。能夠獲取對象上自身的全部屬性和方法名,包括不可枚舉的,也返回一個名稱列表。
上面示例中,咱們添加原型屬性,是一個一個在Person.prototype
上添加。爲了減小沒必要要的輸入,視覺上也更易讀,咱們能夠把要添加的屬性和方法,直接封裝成對象,而後改變Person.prototype
指向的位置。
function Person () {}
Person.prototype = {
age: 34,
getAge: function () {
return this.age
}
}
複製代碼
可是,若是這樣作,Person.prototype.constructor
也被重寫,指向了封裝對象的構造函數,也就是Object
。
Person.prototype.constructor === Object // true
複製代碼
這時,咱們已經沒法經過constructor
知道原型對象的構造類型了。若是你還記得,工廠模式也存在這個問題。咱們能夠這樣作:
function Person () {}
Person.prototype = {
constructor: Person,
age: 34,
getAge: function () {
return this.age
}
}
複製代碼
可是,這樣也會有問題。默認的constructor
是不可枚舉的,這樣顯式的賦值以後,就會變成可枚舉的了。
Person.hasOwnProperty('constructor') // true
複製代碼
若是你很在乎這個,可使用defineProperty
,修改constructor
屬性爲不可枚舉。
Object.defineProperty(Person.prototype, 'constructor', {
enumerable: false,
value: Person
})
複製代碼
由於聯繫原型對象和實例的只是一個指針,而不是一個原型對象的副本,因此原型對象上屬性的任何修改都會在實例上反應出來,不管實例建立是在改動以前或者以後。
function Person () {}
Person.prototype.age = 12
var p1 = new Person()
p1.age // 12
Person.prototype.age = 24
p1.age // 24
var p2 = new Person()
p2.age // 24
複製代碼
但若是修改了整個原型對象,那狀況不同了。由於重寫原型對象會切斷構造函數與原先原型對象的聯繫,而實例的指針指向的倒是原來的原型對象。
function Person () {}
Person.prototype = {
age: 12
}
var p1 = new Person()
p1.age // 12
p1.__proto__ === Person.prototype // true
Person.prototype = {
age: 24
}
p1.age // 12
Person.prototype.age // 24
p1.__proto__ === Person.prototype // false
複製代碼
因此,修改原型對象是把雙刃劍,用得好能夠解決問題,用很差就會帶來問題。
原生對象(Object
、Array
、String
等)其實也是構造函數
typeof Object // function
typeof Array // function
typeof String // function
複製代碼
它們自身擁有一些屬性和方法,它們的原型對象也擁有一些,而原型對象上面的屬性和方法,都會被它們構造的實例所共享。
Object.getOwnPropertyNames(Object).join(',') // "length,name,prototype,assign,getOwnPropertyDescriptor,getOwnPropertyDescriptors,getOwnPropertyNames,getOwnPropertySymbols,is,preventExtensions,seal,create,defineProperties,defineProperty,freeze,getPrototypeOf,setPrototypeOf,isExtensible,isFrozen,isSealed,keys,entries,values"
Object.getOwnPropertyNames(Object.prototype).join(',') // "constructor,__defineGetter__,__defineSetter__,hasOwnProperty,__lookupGetter__,__lookupSetter__,isPrototypeOf,propertyIsEnumerable,toString,valueOf,__proto__,toLocaleString"
Object.getOwnPropertyNames(Object.getPrototypeOf({})).join(',') // "constructor,__defineGetter__,__defineSetter__,hasOwnProperty,__lookupGetter__,__lookupSetter__,isPrototypeOf,propertyIsEnumerable,toString,valueOf,__proto__,toLocaleString"
複製代碼
既然能夠共享,固然也能夠修改和添加。
Object.prototype.toString = function () {
return 'hello world'
}
var a = {}
a.toString() // hello world
複製代碼
雖然這樣很方便,可是,咱們並不推薦這麼作。由於每一個原生對象的屬性和方法,都是有規範可尋的,而且這個規範是全部開發人員都承認的。那麼,若是「自定義」了這些屬性和方法,可能在多人協做的項目中引發沒必要要衝突。而且若是規範更新,也會帶來問題。
上面說了使用原型對象的諸多優勢,可是原型模式也是有問題的。原型模特的優勢是由於它的共享特性,缺點也是。好比,咱們在原型對象上定義了一個引用類型的屬性。
function Person () {}
Person.prototype.family = ['father','mother']
var p1 = new Person()
var p2 = new Person()
p1.family.push('girlFriend')
p1.family // ["father", "mother", "girlFriend"]
p2.family // ["father", "mother", "girlFriend"]
複製代碼
咱們在p1
的family
屬性中添加了girlFriend
,可是p2.family
也添加了,由於他們指向的是同一個數組。而這,是咱們不但願看到的。實例之間須要共享的屬性和方法,天然,也須要自有的屬性和方法。
實例屬性(OwnProperty) 該屬性在實例上,而不是原型上。能夠在構造函數內部或者原型方法內部建立。 建議只在構造函數中建立全部的實例屬性,保證變量聲明在一個地方完成。
function Person () {
this.family = ['father','mother']
}
var p1 = new Person()
var p2 = new Person()
p1.family.push('girlFriend')
p1.family // ["father", "mother", "girlFriend"]
p2.family // ["father", "mother"]
複製代碼
若是你還有印象,以前的構造函數模式不就是建立的實例屬性和方法嗎?因此,結合使用這兩種方式,是目前ECMAScript
中使用最普遍、認同度最高的建立自定義類型的方法。
function Person () {
this.family = ['father','mother']
}
Person.prototype.age = 24
var p1 = new Person()
var p2 = new Person()
p1.family.push('girlFriend')
p1.family // ["father", "mother", "girlFriend"]
p2.family // ["father", "mother"]
p1.age = 25
p2.age // 25
複製代碼
以上示例,既有原型對象的共享屬性,也有實例自身的屬性,各得其所。
可是,上面示例在混合使用兩種模式時,依然是割裂開的,兩種模式並無在一個方法中完成。而動態原型模式,正是來解決這個問題。
function Person () {
this.age = 24
if(typeof this.getAge !== 'function'){
Person.prototype.getAge = function () {
return this.age
}
}
}
複製代碼
能夠看到,咱們把原型對象模式的定義語句移動到了構建函數中,顯式的將兩種模式統一在了一塊兒。
以前咱們說過,儘可能不要改動原生對象,可是若是想在原生對象上增長方法怎麼辦?咱們能夠在原生對象的基礎上,增長方法,而後生成一個新的對象。這就是寄生構造函數模式。
function ArrayPlus () {
var plus = []
plus.pipeStr = function () {
return this.join('|')
}
return plus
}
var plus1 = new ArrayPlus()
plus1.push('red')
plus1.push('black')
plus1.pipeStr() // red|black
plus1.constructor // Array
Object.getPrototypeOf(Object.getPrototypeOf(plus1)) === Array.prototype // true
plus1 instanceof ArrayPlus // false
plus1 instanceof Aarray // true
複製代碼
可是,生成的實例跟構造函數和原型對象是徹底沒有聯繫的,而且也沒法經過instanceof
肯定其類型。因此,在其餘模式可用的狀況下,不推薦使用這個模式。
穩妥對象是指沒有公共屬性,而且其方法也不引用this的對象。適合一些安全的環境。下面的示例中,除了對象提供的方法,是沒有其餘途徑得到對象內部的原始數據的。 當前與寄生構造函數模式同樣,生成的實例跟構造函數和原型對象是徹底沒有聯繫的,而且也沒法經過instanceof
肯定其類型。
function Person (age) {
return {
getAge: function () {
return age
}
}
}
var p1 = Person(12)
p1.getAge() // 12
Object.getPrototypeOf(p1) === Object.prototype // true
p1 instanceof Person // false
複製代碼
經過原型對象和構造函數相結合的模式,咱們能夠批量的生成對象,這種模式能夠稱之爲ECMAScript
中的「類」;
那若是要批量生成「類」呢?這就要用到「繼承」了。ECMAScript
中的繼承主要是依賴原型鏈來實現。
原型鏈的基本思想是利用原型讓一個引用類型繼承另外一個引用類型的屬性和方法。實際作法就是:
A
的原型對象A.prototype
指向另外一個構造函數B
的實例b1
,此時A.prototype === b1
,那麼構造函數A
的實例a1
會擁有構造函數B
的原型對象B.prototype
的全部屬性和方法。B
的原型對象B.prototype
剛好又指向另外一個構造函數C
的實例c1
,即B.prototype === c1
。那麼構造函數B
的實例b1
會擁有構造函數C
的原型對象C.prototype
的全部屬性和方法。A
的實例a1
會同時擁有構造函數B
的原型對象B.prototype
和構造函數C
的原型對象C.prototype
的全部屬性和方法。這就是原型鏈的基本概念。代碼實例以下:
function Grandpa () {}
Grandpa.prototype.sayHello = function () {
return 'hello'
}
function Father () {}
Father.prototype = new Grandpa()
Father.prototype.sayWorld = function () {
return 'world'
}
function Son () {}
Son.prototype = new Father()
var son1 = new Son()
son1.sayHello() // hello
son1.sayWorld() // world
複製代碼
若是你還記得以前的原型搜索機制(仍是下面這張圖),那麼原型鏈其實就是對這種機制的向下拓展。
// 調用son1實例上的sayWorld方法
son1.sayWorld()
// 先在實例上尋找,沒有
Object.getOwnPropertyNames(son1) // []
// 繼續在實例的原型上尋找,也沒有
Object.getOwnPropertyNames(Son.prototype) // []
// 繼續在實例的原型的原型上尋找,找到了
Object.getOwnPropertyNames(Father.prototype) // ["sayWorld"]
// 一樣的,調用son1實例上的sayWorld方法
son1.sayHello()
// 先在實例上尋找,沒有
Object.getOwnPropertyNames(son1) // []
// 繼續在實例的原型上尋找,也沒有
Object.getOwnPropertyNames(Son.prototype) // []
// 繼續在實例的原型的原型上尋找,沒有
Object.getOwnPropertyNames(Father.prototype) // ["sayWorld"]
// 繼續在實例的原型的原型的原型上尋找,找到了
Object.keys(Grandpa.prototype) // ["constructor", "sayHello"]
複製代碼
那原型鏈的盡頭————實例的原型的原型的原型...的原型是誰呢? 全部函數的默認原型都是Object
的實例,因此全部自定義類型都繼承了Object.prototype
上的屬性和方法。
Object.getPrototypeOf(Son.prototype) === Father.prototype // true
Object.getPrototypeOf(Father.prototype) === Grandpa.prototype // true
Object.getPrototypeOf(Grandpa.prototype) === Object.prototype // true
複製代碼
那Object
的原型指向誰呢?
Object.getPrototypeOf(Object.prototype) // null
複製代碼
Object.getPrototypeOf
的返回值是傳入對象繼承的原型對象,因此,若是傳入對象沒有繼承值,那麼就返回null
。
instanceof
操做符。測試實例與原型鏈中的構造函數。
son1 instanceof Son // true
son1 instanceof Father // true
son1 instanceof Grandpa // true
son1 instanceof Object // true
複製代碼
isPrototypeOf()
方法。只要是原型鏈中出現過的原型,均可以說是該原型鏈所派生的實例的原型。
Son.prototype.isPrototypeOf(son1) // true
Father.prototype.isPrototypeOf(son1) // true
Grandpa.prototype.isPrototypeOf(son1) // true
Object.prototype.isPrototypeOf(son1) // true
複製代碼
這一塊在講原型的時候也有說起,主要有兩點:在原型鏈末端定義的重名屬性或方法,會屏蔽掉在原型鏈頂端的定義;使用原型覆蓋默認原型對象,要在添加原型的方法以前進行。
function Grandpa() {}
Grandpa.prototype.say = function () {
return 'grandpa'
}
function Father() {}
Father.prototype = new Grandpa()
Father.prototype.say = function () {
return 'father'
}
function Son () {}
Son.prototype.age = 12
Son.prototype = new Father()
var son1 = new Son()
son1.say() // father
son1.age // undefined
複製代碼
另外,使用對象字面量的方式爲原型添加方法,也會覆蓋以前的原型對象。
第一個問題以前在講原型的時候也說過,就是若是在原型對象上定義一個引用類型的屬性,可能出現問題。
第二個問題是在建立子類型的實例(son1
)時,不能向超類型的構造函數(Grandpa
)傳遞參數。
有鑑於此,通常不單獨使用原型鏈。
使用構造函數,能夠解決上面提到的問題一。
function Grandpa () {
this.family = ['house', 'car']
}
function Father () {
// 使用call,完成實例屬性繼承
// 其實就是以當前函數的做用域,替換目標函數做用域,並執行目標函數
Grandpa.call(this)
this.age = 26
}
// 工廠模式寫法
// function Father () {
// var that = new Grandpa()
// that.age = 26
// return that
// }
var f1 = new Father()
var f2 = new Father()
f1.family.push('money')
f2.family // ['house', 'car']
複製代碼
能夠傳遞參數,解決了問題2
function Grandpa (name) {
this.name = name
}
function Father (name) {
Grandpa.call(this, name)
this.age = 26
}
// 工廠模式寫法
// function Father (name) {
// var that = new Grandpa(name)
// that.age = 26
// return that
// }
var f1 = new Father('jiahesheng')
f1.name // jiahesheng
f1.age // 26
複製代碼
可是,借用構造函數也有本身的問題。也就是不能複用共享屬性和方法了。
其實就是結合了原型鏈和借用構造函數兩種技術。
function Father (name) {
this.name = name
}
Father.prototype.sayWorld = function () {
return 'world'
}
function Son (name) {
Father.call(this, name)
}
Son.prototype = new Father()
var s1 = new Son('zhu')
var s2 = new Son('sang')
s1.name // 'zhu'
s2.name // sang
s1.sayWorld() // 'world'
s2.sayWorld() // 'world'
複製代碼
所謂的共享和自有,能夠這麼理解: 使用了原型鏈共享了屬性和方法的實例,其實,就是包含了一堆指針,這些指針指向原型對象; 使用了借用構造函數技術擁有了自有的屬性和方法的實例,其實,就是擁有了構造函數屬性和方法的副本。
DC 最先提出,ECMAScript
添加了Object.create
方法規範化了這種模式。原型式繼承的主要應用場景,就是返回一個對象,對象的原型指向傳入的對象。
var person = {
age: 24
}
// from DC
function object (o) {
function F() {}
F.prototype = o
return new F()
}
var p1 = object(person)
Object.getPrototypeOf(p1) === person // true
// from ECMAScript
var p2 = Object.create(person)
Object.getPrototypeOf(p2) === person // true
複製代碼
Object.create
還支持第二個參數,格式與Object.defineProperties
相同
var person = {
age: 24
}
var p1 = Object.create(person, {
age: {
value: 12
}
})
p1.age // 12
複製代碼
Object.create
最經常使用的方法仍是建立一個純淨的數據字典(沒有原型對象的對象實例,即實例的原型指向null
): Object.create(null)
var p3 = Object.create(null)
p3.__proto__ // undefined
Object.getPrototypeOf(p3) // null
複製代碼
使用Object.setPrototypeOf
也能夠實現:
var p4 = {}
Object.setPrototypeOf(p4, null)
p4.__proto__ // undefined
Object.getPrototypeOf(p4) // null
複製代碼
寄生式繼承就是建立一個僅用於封裝繼承過程的函數,該函數在內部加強對象以後,會返回新的對象。寄生式繼承的實際用途在下一節能更好的表示。
咱們先來回顧一下,組合式繼承:
function Father (name) {
this.name = name || 'default'
}
Father.prototype.sayWorld = function () {
return 'world'
}
function Son (name) {
Father.call(this, name)
}
Son.prototype = new Father()
var s1 = new Son('zhu')
var s2 = new Son('sang')
s1.name // 'zhu'
s2.name // 'sang'
s1.sayWorld() // 'world'
s2.sayWorld() // 'world'
Son.prototype.name // 'default'
複製代碼
它實現了屬性和方法的自有和共享。可是,也帶來了一些問題。
Father
被調用執行了兩次。一次在new Father()
,一次在Father.call(this, name)
。Son.prototype = new Father()
這個語句後,其實Son.prototype
也擁有了name
屬性。只是咱們在使用name
屬性的時候,被實例上的name
屬性屏蔽了。怎麼解決這個問題呢?咱們將原型鏈繼承這一步(Son.prototype = new Father()
)重寫便可!避免調用new Father()
,避免繼承Father
的實例屬性和方法。 咱們能夠組合使用寄生式繼承和原型式繼承,定義這樣一個函數:
function inheritPrototype (prototypeObj, inheritor) {
var prototype = Object.create(prototypeObj)
prototype.constructor = inheritor
inheritor.prototype = prototype
}
複製代碼
inheritPrototype
方法作了兩件事:恢復了原型對象對構造函數的指針屬性,「淺複製」了原型對象。以前咱們也說過,其實原型鏈的共享只是一堆指針的公用,指向的其實仍是一個原型對象。因此,「淺複製」恰好用上。
如今咱們把這個方法用起來!
function Father (name) {
this.name = name
}
Father.prototype.sayWorld = function () {
return 'world'
}
function Son (name) {
Father.call(this, name)
}
inheritPrototype(Father.prototype, Son)
var s1 = new Son('zhu')
複製代碼
到此咱們實現了最完美的繼承!
歷盡艱險,咱們終於使用ES5
實現了「類」和「繼承」,可是這相較於其餘的面向對象的語言,看起來很不「規範」,而且實現起來也太麻煩。因此,在ECMAScript 2015
版本中,使用class
和 extend
關鍵字,更加「規範」的實現了「類」和「繼承」。
咱們下篇文章繼續探討新的規範中的「面向對象」。
《JavaScript高級程序設計第三版》