javascript
JavaScript 面向对象详解
首先,我肯定是需要你告訴我,什么是面向?qū)ο?#xff0c;面向?qū)ο笥心男┨攸c,以及這些特點的解釋。
JavaScript 如何實現(xiàn)這些特點,比如封裝、繼承、多態(tài)。如果關(guān)于上述三點,你能夠解釋到有多少種實現(xiàn)方式、優(yōu)缺點是什么。以及近幾年流行的解決方案是什么。這就是「加分」 ,比如對于繼承吧。類式繼承、構(gòu)造函數(shù)繼承、組合繼承、原型繼承、寄生組合繼承等等,說出大概的實現(xiàn)思路和優(yōu)缺點,再介紹下 extends 或者 mixin 的實現(xiàn)甚至你可以衍生到 JavaScript 的模塊化發(fā)展甚至到為什么現(xiàn)在 TS 如此流行。那么可以說到這一環(huán)節(jié)解答的就非常棒了。
回答完 JavaScript 的面向?qū)ο?#xff0c;是不是可以從此衍生下為什么需要面向?qū)ο蟆R约爱?dāng)先對于軟件設(shè)計的高內(nèi)聚、低耦合的思考?來個對此題一個提綱挈領(lǐng)的總結(jié)?
綜上所述,其實不難看出,越是這種基礎(chǔ)且開放的題目,可以是一個陷阱,更可以是一個機會。因為一道題真的可以全方面的感受到應(yīng)聘的基礎(chǔ)是否扎實。
JavaScript 面向?qū)ο笤斀?一)
ES6 之前的 JavaScript 面向?qū)ο蟊容^不好理解,涉及到很多知識和思想。
ES6 增加了 class 和 extends 來實現(xiàn)類的封裝和繼承,但是通過 babel 轉(zhuǎn)換成 ES5 之后還是之前的一套邏輯。
這里,我打算用四篇文章,來講解一下關(guān)于 ES5 中面向?qū)ο蟮闹R體系,一起學(xué)習(xí)一下吧!
一. JavaScript 的對象
1.1. 傳統(tǒng)對象 vs JavaScript 對象
傳統(tǒng)的面向?qū)ο?/p>
- 面向?qū)ο笳Z言的一個標(biāo)志就是類
- 類是所有對象的統(tǒng)稱, 是更高意義上的一種抽象. 對象是類的實例.
- 通過類我們可以創(chuàng)建任意多個具體的對象.
- 在學(xué)習(xí) C++/OC/Java/Python 等編程語言的時候, 都可以按照這種方式去創(chuàng)建類和對象.
JavaScript 的面向?qū)ο?/p>
- JavaScript 中沒有類的概念(ES6 之前),因此我們通常稱為基于對象,而不是面向?qū)ο?
- 雖然 JavaScript 中的基于對象也可以實現(xiàn)類似于類的封裝、繼承、甚至是多態(tài)。但是和傳統(tǒng)意義的面向?qū)ο筮€是稍微有一些差異(后面我們會講解它是如何實現(xiàn)的)
- ECMA 中定義對象是這樣: 無序?qū)傩缘募? 屬性可以包含基本值, 對象或者函數(shù).
- 也就是對象是一組沒有順序的值組成的集合而已.
- 對象的每個屬性或者方法都有一個名字, 而名字對應(yīng)一個值. 有沒有覺得非常熟悉?
- 沒錯, 其實就是我們經(jīng)常看到和使用的映射(或者有些語言稱為字典, 通常會使用哈希表來實現(xiàn)).
1.2. 簡單的方式創(chuàng)建對象
創(chuàng)建自定義對象最簡單的方式就是創(chuàng)建一個 Object 實例, 然后添加屬性和方法
// 1.創(chuàng)建person的對象 var person = new Object();// 2.給person對象賦值了一些動態(tài)的屬性和方法 person.name = 'LBJ輝'; person.age = 18; person.height = 1.88;person.sayHello = function () {alert('Hello, My name is ' + this.name); };// 3.調(diào)用方法, 查看結(jié)果 person.sayHello();代碼解析:
- 步驟一: 創(chuàng)建一個名為 person 的對象.
- 步驟二: 給對象動態(tài)的賦值了一些屬性包括一個方法
- 步驟三: 調(diào)用 sayHello()方法, 主要看一下 this.name 會獲取到誰呢? LBJ 輝
插播一個信息: 函數(shù)和方法的關(guān)系
-
很多人在學(xué)習(xí)編程的時候, 會分不清楚什么是函數(shù), 什么又是方法. 或者在什么情景下稱為函數(shù), 什么情景下稱為方法.
-
首先, 如果你看的是英文文檔, 會有明顯的區(qū)分: Function 被稱為函數(shù), Method 被稱為方法.
-
但是英文中, 為什么需要有這兩個稱呼呢?
-
- 在早期的編程語言中, 只有函數(shù)(類似于數(shù)學(xué)中函數(shù)的稱呼)
- 后來有了面向?qū)ο笳Z言, 面向?qū)ο笳Z言中, 類中也可以定義函數(shù). 但是人們?yōu)榱藚^(qū)分在類中定義的函數(shù), 通常稱類中的函數(shù)為方法.
- 還有一個非常重要的原因是, 通常方法中會攜帶一個調(diào)用者的當(dāng)前對象(會將調(diào)用者作為參數(shù)一起傳遞進(jìn)去), 也就是說 this(有些語言中是 self. 比如 OC/Swift/Python 等)
- 當(dāng)然, 你從這個角度來說, JavaScript 中就沒有函數(shù)了, 因為函數(shù)中都有 this 這樣的參數(shù). 但是通常來說, 我們還是會將封裝到類中的函數(shù)稱為方法, 而全局定義的函數(shù)稱為函數(shù).
-
如果接觸過 Java 的同學(xué)可能會知道 Java 中只有方法的程序, 沒有函數(shù)的稱呼. 學(xué)習(xí)過 C 語言的同學(xué)可能知道, C 語言中只有函數(shù)的稱呼, 沒有方法的稱呼.
-
這就是因為 Java 中通常不定義全局函數(shù), 但是在類中定義的. 而 C 語言不支持面向?qū)ο蟮木幊?
OK, 我們繼續(xù) JavaScript 面向?qū)ο笾?
- 前面創(chuàng)建對象的方式, 被早期的 JavaScript 程序員經(jīng)常使用
后來, 對象字面量稱為創(chuàng)建這種對象的首選方式
// 1.創(chuàng)建對象的字面量 var person = {name: 'Coderwhy',age: 18,height: 1.88,sayHello: function () {alert('My name is ' + this.name);}, };// 2.調(diào)用對象的方法 person.sayHello();1.3. JavaScript 中屬性的特性
JavaScript 中關(guān)于屬性有一個比較重要的概念: 屬性描述符
- 雖然我們開發(fā)中, 大多數(shù)情況不去可以的使用這些屬性描述符
- 但是某些情況下, 也確實會用到.
- 建議大家先了解一下這些屬性描述符, 以及它們的作用, 在以后用到時會非常有幫助.
JavaScript 中開始擁有了一種描述屬性特征的特性(即屬性描述符)。
- 根據(jù)特性的不同,可以把屬性分成兩種類型:數(shù)據(jù)屬性和訪問器屬性。
常見的屬性特性有哪些呢?
-
[[Configurable]] // true or false
-
- 表示能否通過 delete 刪除屬性從而重新定義屬性,能否修改屬性的特性,或者能否把屬性修改為訪問器屬性。像前面例子中那樣直接在對象上定義的屬性,它們的這個特性默認(rèn)值為 true。
-
[[Writable]] // true or false
-
- 表示能否修改屬性的值。像前面例子中那樣直接在對象上定義的屬性,它們的這個特性默認(rèn)值為 true。
-
[[Enumerable]] // true or false
-
- 表示能否通過 for-in 循環(huán)返回屬性。像前面例子中那樣直接在對象上定義的屬性,它們的這個特性默認(rèn)值為 true。
-
[[Value]] // everty thing
-
- 包含這個屬性的數(shù)據(jù)值。讀取屬性值的時候,從這個位置讀;寫入屬性值的時候,把新值保存在這個位置。這個特性的默認(rèn)值為 undefined。
-
[[set]] // function or undefined
-
- 在寫入屬性時調(diào)用的函數(shù)。默認(rèn)值為 undefined。
-
[[get]] // function or undefined
-
- 在讀取屬性時調(diào)用的函數(shù)。默認(rèn)值為 undefined。
這些屬性特性是什么東西呢?
-
從上面, 我們對這些特定的解釋, 你會發(fā)現(xiàn), 每個特定都會有自己特定的用途.
-
比如 Configurable 當(dāng)我們配置為 false 時, 就無法使用 delete 來刪除該屬性.
-
設(shè)置屬性特定
-
- obj: 將要被添加屬性或修改屬性的對象
- prop: 對象的屬性
- descriptor: 對象屬性的特性
- 要想修改屬性的特性,必須通過兩個 Object 方法,即 Object.defineProperty 和 Object.defineProperties
- 正如其字面意思,這兩個方法都是用來定義(修改)屬性的,前者一次只能定義一個屬性,后者則可以多個。
- defineProperty(obj, prop, descriptor)
案例練習(xí):
var person = {}; Object.defineProperty(person, 'birth', {writable: false,value: 2000, });alert(person.birth); // 2000 person.birth = 1999; alert(person.birth); // 2000注意:在使用 defineProperty 方法定義新屬性時(非修改舊屬性),如果不指定,configurable, enumerable 和 writable 特性的默認(rèn)值都是 false。
也就是上面的代碼等同于:
var person = {}; Object.defineProperty(person, 'birth', {configurable: false,enumerable: false,writable: false,value: 2000, });數(shù)據(jù)屬性:
- 數(shù)據(jù)屬性包含一個數(shù)值的位置,在這個位置可以讀取和寫入值。
- 數(shù)據(jù)屬性擁有 4 個特性: [[Configurable]]/[[Enumerable]]/[[Writable]]/[[Value]]
- 按照上面的方式, 我們定義的屬性就是數(shù)據(jù)屬性
訪問器屬性:
- 訪問器屬性不包含數(shù)據(jù)值,它們包含一對 getter 和 setter 函數(shù)。
- 訪問器屬性不能直接定義,需要使用后面提到的 Object.defineProperty 函數(shù)定義。
- 訪問器屬性也擁有 4 個特性: [[Configurable]]/[[Enumerable]]/[[Get]]/[[Set]]
定義一個訪問器屬性:
var person = {birth: 2000,age: 17, }; Object.defineProperty(person, 'year', {get: function () {return this.birth + this.age;},set: function (newValue) {this.age = newValue - this.birth;}, });person.year = 2088; alert(person.age); // 88 person.age = 30; alert(person.year); // 2030注意: getter 和 setter 都是可選的,在非嚴(yán)格模式下,只指定了 getter 卻進(jìn)行了寫入操作,寫入的值會被忽略; 只指定了 setter 卻進(jìn)行了讀取操作,讀取到的屬性值為 undefined。在嚴(yán)格模式下,則都會報錯。
二. JavaScript 創(chuàng)建對象
雖然 Object 構(gòu)造函數(shù)或?qū)ο笞置媪靠梢杂脕韯?chuàng)建單個對象
但是這些方式有個明顯的缺點: 使用同一個接口創(chuàng)建很多對象, 會產(chǎn)生大量的重復(fù)代碼.
我們會有一些列的方式來解決這個問題, 最終得到我們最佳理想的方式來創(chuàng)建對象.
2.1. 使用工廠模式
工廠模式是一種非常常見的設(shè)計模式, 這種模式抽象了創(chuàng)建具體對象的過程.
因為 JavaScript 中沒法創(chuàng)建類, 開發(fā)人員就發(fā)明了一種函數(shù), 用函數(shù)來封裝以特定接口創(chuàng)建對象的細(xì)節(jié).
工廠模式創(chuàng)建對象:
// 創(chuàng)建工廠函數(shù) function createPerson(name, age, height) {var o = new Object();o.name = name;o.age = age;o.height = height;o.sayHello = function () {alert('Hello, My name is ' + this.name);};return o; }// 創(chuàng)建兩個對象 var person1 = createPerson('LBJ輝', 18, 1.88); var person2 = createPerson('LBJ', 38, 2.03); person1.sayHello(); // Hello, My name is LBJ輝 person2.sayHello(); // Hello, My name is LBJ代碼解析:
- 函數(shù) createPerson()能夠根據(jù)接受的參數(shù)來構(gòu)建一個包含所有必要信息的 Person 對象
- 可以無數(shù)次地調(diào)用這個函數(shù),而每次它都會返回一個包含三個屬性一個方法的對象。
- 工廠模式雖然解決了創(chuàng)建多個相似對象的問題,但卻沒有解決對象識別的問題(即怎樣知道一個對象的類型)。
- 隨著 JavaScript 的發(fā)展,又一個新模式出現(xiàn)了。
2.2. 構(gòu)造函數(shù)模式
JavaScript 中的構(gòu)造函數(shù)可用來創(chuàng)建特定類型的對象。
- 像 Object 和 Array 這樣的原生構(gòu)造函數(shù),在運行時會自動出現(xiàn)在執(zhí)行環(huán)境中。
- 此外,也可以創(chuàng)建自定義的構(gòu)造函數(shù),從而定義自定義對象類型的屬性和方法。
使用構(gòu)造函數(shù)模式創(chuàng)建對象:
// 構(gòu)造函數(shù) function Person(name, age, height) {this.name = name;this.age = age;this.height = height;this.sayHello = function () {alert(this.name);}; }// 使用構(gòu)造函數(shù)創(chuàng)建對象 var person1 = new Person('LBJ輝', 18, 1.88); var person2 = new Person('LBJ', 38, 2.03); person1.sayHello(); // LBJ輝 person2.sayHello(); // LBJ代碼解析:
-
在這個例子中,Person()函數(shù)取代了 createPerson()函數(shù)。
-
我們會發(fā)現(xiàn)這個函數(shù)有一些不太一樣的地方:
-
- 沒有顯式地創(chuàng)建對象;(比如創(chuàng)建一個 Object 對象)
- 直接將屬性和方法賦給了 this 對象;
- 沒有 return 語句
-
另外, 我們還注意到函數(shù)名 Person 使用的是大寫字母 P。
-
- 按照慣例,構(gòu)造函數(shù)始終都應(yīng)該以一個大寫字母開頭,而非構(gòu)造函數(shù)則應(yīng)該以一個小寫字母開頭;
- 這個做法借鑒自其他面向?qū)ο笳Z言,主要是為了區(qū)別于 ECMAScript 中的其他函數(shù);
- 因為構(gòu)造函數(shù)本身也是函數(shù),只不過可以用來創(chuàng)建對象而已;
-
還有, 我們在調(diào)用函數(shù)時, 不再只是簡單的函數(shù)+(), 而是使用了 new 關(guān)鍵字
-
這種方式調(diào)用構(gòu)造函數(shù)實際上會經(jīng)歷以下 4 個步驟:
-
- 創(chuàng)建一個新對象, 這個新的對象類型其實就是 Person 類型.
- 將構(gòu)造函數(shù)的作用域賦給新對象(因此 this 就指向了這個新對象,也就是 this 綁定);
- 執(zhí)行構(gòu)造函數(shù)中的代碼(為這個新對象添加屬性和方法);
- 返回新對象, 但是是默認(rèn)返回的, 不需要使用 return 語句;
在前面例子的最后,person1 和 person2 分別保存著 Person 的一個不同的實例。
- 這兩個對象都有一個 constructor(構(gòu)造函數(shù))屬性,該屬性指向 Person.
- 后面我們會詳細(xì)說道 constructor 到底從何而來, 所以你需要特別知道一下這里有這個屬性.
我們也可以通過 instanceof 來查看它的類型
- 注意: 我們會發(fā)現(xiàn) person1 和 person2 既是 Person 類型, 也是 Object 類型.
- 這是因為默認(rèn)所有的對象都繼承自 Object.(關(guān)于繼承, 后續(xù)詳細(xì)討論)
2.3. 關(guān)于構(gòu)造函數(shù)
關(guān)于構(gòu)造函數(shù)
- 我們知道, 構(gòu)造函數(shù)也是一個函數(shù), 只是使用的方式和別的函數(shù)不太一樣.(使用 new)
- 但是, 構(gòu)造函數(shù)畢竟也是函數(shù), 因此也可以像普通的函數(shù)一樣去使用.
- 而且, 其他任何的函數(shù), 也可以通過 new 關(guān)鍵字來調(diào)用, 這個時候這個函數(shù)也可以被稱為構(gòu)造函數(shù).
把構(gòu)造函數(shù)當(dāng)做普通的函數(shù)去調(diào)用
// 當(dāng)做構(gòu)造函數(shù)使用 var person = new Person('LBJ輝', 18, 1.88); // person對象 person.sayHello();// 作為普通的函數(shù)調(diào)用 Person('LBJ', 38, 2.03); // window對象 window.sayHello();// 在另外一個對象的作用域調(diào)用 var o = new Object(); Person.call(o, 'Wade', 39, 1.93); // o對象 o.sayHello();構(gòu)造函數(shù)來創(chuàng)建對象的缺陷:
- 構(gòu)造函數(shù)模式雖然好用,但也并非沒有缺點。
- 使用構(gòu)造函數(shù)的主要問題,就是每個方法都要在每個實例上重新創(chuàng)建一遍。
- 在前面的例子中,personl 和 person2 都有一個名為 sayName()的方法,但那兩個方法不是同一個 Function 的實例。
- JavaScript 中的函數(shù)也是對象,因此每定義一個函數(shù),也就是實例化了一個對象
構(gòu)造函數(shù)的換一種形式:
- 也就是上面的代碼類似于下面的寫法
有什么問題呢?
- 從這個角度上來看構(gòu)造函數(shù),更容易明白每個 Person 實例都包含一個不同的 Function 實例.
- 但是, 有必要創(chuàng)建多個 Function 實例嗎? 它們執(zhí)行的代碼完全相同.
- 你也許會考慮, 它們需要區(qū)分不同的對象, 不過, 在調(diào)用函數(shù)時, 我們傳入的 this 就可以區(qū)分了. 沒有必要創(chuàng)建出多個 Function 的實例.
我們可以驗證一下這是兩個不同的函數(shù):
alert(person1.sayHello === person2.sayHello); // false有沒有辦法讓它們是同一個函數(shù)呢? 使用全局函數(shù)即可
// 定義全局和函數(shù) function sayHello() {alert(this.name); }// 構(gòu)造函數(shù) function Person(name, age, height) {this.name = name;this.age = age;this.height = height;this.sayHello = sayHello; }// 使用構(gòu)造函數(shù)創(chuàng)建對象 var person1 = new Person('LBJ輝', 18, 1.88); var person2 = new Person('LBJ', 38, 2.03);alert(person1.sayHello === person2.sayHello); // true新的問題:
- 這樣做確實解決了兩個函數(shù)做同一件事的問題,可是新問題又來了: 在全局作用域中定義的函數(shù)我們的目的卻是只能被某個對象調(diào)用,這讓全局作用域有點名不副實。
- 而且我們進(jìn)一步思考: 如果對象需要定義很多方法,那么就要定義很多個全局函數(shù),于是我們這個自定義的引用類型就絲毫沒有封裝性可言了。
有沒有新的解決方案呢?使用原型模式.
JavaScript 面向?qū)ο笤斀?二)
前面, 我們討論了很多種場景對象的方式: 從 Object 到字面量, 再到工廠模式, 再到構(gòu)造函數(shù).
最終我們發(fā)現(xiàn), 構(gòu)造函數(shù)是比較理想的一種方式, 但是它也存在問題.
為了最終解決這個問題, 我們需要學(xué)習(xí)一個新的知識: 原型(prototype).
一. 理解原型模式
1.1. 什么是原型呢?
你需要先知道一個事實:
- 我們創(chuàng)建的每個函數(shù)都有一個 prototype(原型)屬性
- 這個屬性是一個指針,指向一個對象
- 而這個對象的作用是存放這個類型創(chuàng)建的所有實例共享的屬性和方法。
- 指向的這個對象, 就是我們的所謂的原型對象.
原型對象的作用:
- 使用原型對象的好處是可以讓所有對象實例共享它所包含的屬性和方法。
- 換句話說,不必在構(gòu)造函數(shù)中定義對象實例的信息,而是可以將這些信息直接添加到原型對象中。
我們來看看原型對象的使用:
// 創(chuàng)建對象的構(gòu)造函數(shù) function Person() {}// 通過原型對象來設(shè)置一些屬性和值 Person.prototype.name = 'LBJ輝'; Person.prototype.age = 18; Person.prototype.height = 1.88; Person.prototype.sayHello = function () {alert(this.name); };// 創(chuàng)建兩個對象, 并且調(diào)用方法 var person1 = new Person(); var person2 = new Person();person1.sayHello(); // LBJ輝 person2.sayHello(); // LBJ輝代碼解析:
- 在上面的代碼中, 我們沒有給實例對象單獨設(shè)置屬性和方法, 而是直接設(shè)置給了原型對象.
- 而原型對象的作用是可以讓所有的對象來共享這些屬性和方法.
- 因此, 我們調(diào)用 sayHello()方法時, 它們打印的結(jié)果是一樣的, 它們是共享的.
1.2. 深入原型對象
原型對象的創(chuàng)建:
- 無論什么時候,只要創(chuàng)建了一個新函數(shù),就會根據(jù)一組特定的規(guī)則為該函數(shù)創(chuàng)建一個 prototype 屬性,這個屬性指向函數(shù)的原型對象。
原型上的 constructor 屬性:
- 默認(rèn)情況下,所有原型對象都會自動獲得一個 constructor(構(gòu)造函數(shù))屬性,這個屬性包含一個指向 prototype 屬性所在函數(shù)的指針。
- 用我們上面的例子來說, Person.prototype.constructor 指向 Person。
- 也就是原型對象自身來說, 只有一個 constructor 屬性, 而其他屬性可以由我們添加或者從 Object 中繼承.
新的實例創(chuàng)建時, 原型對象在哪里呢?
- 當(dāng)調(diào)用構(gòu)造函數(shù)創(chuàng)建一個新實例后,該實例的內(nèi)部將包含一個內(nèi)部屬性,該屬性的指針, 指向構(gòu)造函數(shù)的原型對象。
- 這個屬性是 proto
- 簡單說, 每個實例中, 其實也會有一個屬性, 該屬性是指向原型對象的.
我們通過一個圖來解釋上面的概念:
解析:
- 上面的圖解析了 Person 構(gòu)造函數(shù)、Person 的原型屬性以及 Person 現(xiàn)有的兩個實例之間的關(guān)系
- Person.prototype 指向原型對象, 而 Person.prototype.constructor 又指回了 Person.
- 原型對象中除了包含 constructor 屬性之外,還包括后來添加的其他屬性。
- Person 的每個實例——personl 和 person2 都包含一個內(nèi)部屬性* proto *,該屬性也指向原型對象;
對象搜索屬性和方法的過程:
-
每當(dāng)代碼讀取某個對象的某個屬性時,都會執(zhí)行一次搜索,也就是要找到給定名稱的屬性。
-
搜索首先從 對象實例本身 開始
- 如果在實例中找到了具有給定名字的屬性,則返回該屬性的值;
-
如果沒有找到,則繼續(xù)搜索指針指向的原型對象,在原型對象中查找具有給定名字的屬性
- 如果在原型對象中找到了這個屬性,則返回該屬性的值。
-
也就是說,在我們調(diào)用 personl.sayHello()的時候,會先后執(zhí)行兩次搜索。
-
現(xiàn)在我們也能理解, 為什么所有的實例中都包含一個 constructor 屬性, 這是因為默認(rèn)所有的原型對象中都包含了該屬性.
可以通過proto來修改原型的值(通常不會這樣修改, 知道即可)
- 你可以理解為什么 person1 修改了 name 后, person2 也會修改嗎?
- 通過上面的圖, 自己再來理解一下吧.
但是要注意下面的情況:
- 當(dāng)我們給 person1.name 進(jìn)行賦值時, 其實在給 person1 實例添加一個 name 屬性.
- 這個時候再次訪問時, 就不會訪問原型中的 name 屬性了.
通過 hasOwnProperty 判斷屬性屬于實例還是原型.
// 判斷屬性屬于誰 alert(person1.hasOwnProperty('name')); // true alert(person2.hasOwnProperty('name')); // false1.3. 簡潔的原型語法
簡潔語法概述:
- 如果按照前面的做法, 每添加一個原型屬性和方法, 都要敲一遍 Person.prototype.
- 為了減少不必要的輸入, 另外也為了更好的封裝性, 更常用的做法是用一個包含所有屬性和方法的對象字面量來重寫整個原型對象.
字面量重寫原型對象:
// 定義Person構(gòu)造函數(shù) function Person() {}// 重寫Person的原型屬性 Person.prototype = {name: 'LBJ輝',age: 18,height: 1.88,sayHello: function () {alert(this.name);}, };注意:
- 我們將 Person.prototype 賦值了一個新的對象字面量, 最終結(jié)果和原來是一樣的;
- 但是: constructor 屬性不再指向 Person 了;
- 前面我們說過, 每創(chuàng)建一個函數(shù), 就會同時創(chuàng)建它的 prototype 對象, 這個對象也會自動獲取 constructor 屬性;
- 而我們這里相當(dāng)于給 prototype 重新賦值了一個對象, 那么這個新對象的 constructor 屬性, 會指向 Object 構(gòu)造函數(shù), 而不是 Person 構(gòu)造函數(shù)了;
如果在某些情況下, 我們確實需要用到 constructor 的值, 可以手動的給 constructor 賦值即可
// 定義Person構(gòu)造函數(shù) function Person() {}// 重寫Person的原型屬性 Person.prototype = {constructor: Person,name: 'LBJ輝',age: 18,height: 1.88,sayHello: function () {alert(this.name);}, };// 創(chuàng)建Person對象 var person = new Person();alert(person.constructor === Object); // false alert(person.constructor === Person); // truealert(person instanceof Person); // true上面的方式雖然可以, 但是也會造成 constructor 的[[Enumerable]]特性被設(shè)置了 true.
- 默認(rèn)情況下, 原生的 constructor 屬性是不可枚舉的.
- 如果希望解決這個問題, 就可以使用我們前面介紹的 Object.defineProperty()函數(shù)了.
1.4. 修改原型屬性
考慮下面的代碼執(zhí)行是否會有問題:
// 定義Person構(gòu)造函數(shù) function Person() {}// 創(chuàng)建Person的對象 var person = new Person();// 給Person的原型添加方法 Person.prototype.sayHello = function () {alert('Hello JavaScript'); };// 調(diào)用方法 person.sayHello();代碼解析:
- 我們發(fā)現(xiàn)代碼的執(zhí)行沒有任何問題.
- 因為在創(chuàng)建 person 的時候, person 的proto也是指向的 Person.prototype.
- 所以, 當(dāng)動態(tài)的修改了 Person.prototype 中的 sayHello 屬性時, person 中也可以獲取到該屬性
我們再來看下面的代碼會不會有問題:
// 定義Person構(gòu)造函數(shù) function Person() {}// 創(chuàng)建Person的對象 var person = new Person();// 給Person的原型添加方法 Person.prototype = {constructor: Person,sayHello: function () {alert('Hello JavaScript');}, }; // 調(diào)用方法 person.sayHello();代碼解析:
- 代碼是不能正常運行的. 因為 Person 的 prototype 指向了一個新的對象.
- 而最初我們創(chuàng)建的 person 依然指向原來的原型對象, 原來的原型對象沒有 sayHello()函數(shù).
- 當(dāng)然, 如果再次之后, 再創(chuàng)建的 Person 對象, 是可以調(diào)用 sayHello()的, 但是再次之前創(chuàng)建的, 沒有該方法.
1.5. 原型對象問題
原型對象也有一些缺點:
- 首先, 它不再有為構(gòu)造函數(shù)傳遞參數(shù)的環(huán)節(jié), 所有實例在默認(rèn)情況下都將有相同的屬性值.
- 另外, 原型中所有的屬性是被很多實例共享的, 這種共享對于函數(shù)來說非常適合, 對于基本屬性通常情況下也不會有問題. (因為通過 person.name 直接修改時, 會在實例上重新創(chuàng)建該屬性名, 不會在原型上修改. 除非使用 person.proto.name 修改).
- 但是, 對于引用類型的實例, 就必然會存在問題.
考慮下面代碼的問題:
// 定義Person構(gòu)造函數(shù) function Person() {}// 設(shè)置Person原型 Person.prototype = {constructor: Person,name: 'LBJ輝',age: 18,height: 1.88,hobby: ['Basketball', 'Football'],sayHello: function () {alert('Hello JavaScript');}, };// 創(chuàng)建兩個person對象 var person1 = new Person(); var person2 = new Person();alert(person1.hobby); // Basketball,Football alert(person2.hobby); // Basketball,Footballperson1.hobby.push('tennis');alert(person1.hobby); // Basketball,Football,tennis alert(person2.hobby); // Basketball,Football,tennisOK, 我們會發(fā)現(xiàn), 我們明明給 person1 添加了一個愛好, 但是 person2 也被添加到一個愛好.
- 因為它們是共享的同一個數(shù)組.
- 但是, 我們希望每個人有屬于自己的愛好, 而不是所有的 Person 愛好都相同.
二. 組合構(gòu)造函數(shù)和原型模式
創(chuàng)建自定義類型的最常見方式,就是組合使用構(gòu)造函數(shù)模式與原型模式。
構(gòu)造函數(shù)模式用于定義實例屬性,而原型模式用于定義方法和共享的屬性。
結(jié)果,每個實例都會有自己的一份實例屬性的副本,但同時又共享著對方法的引用,最大限度地節(jié)省了內(nèi)存。
另外,這種混成模式還支持向構(gòu)造函數(shù)傳遞參數(shù);可謂是集兩種模式之長。
組合構(gòu)造函數(shù)和原型模式的代碼
// 創(chuàng)建Person構(gòu)造函數(shù) function Person(name, age, height) {this.name = name;this.age = age;this.height = height;this.hobby = ['Basketball', 'Football']; }// 重新Peron的原型對象 Person.prototype = {constructor: Person,sayHello: function () {alert('Hello JavaScript');}, };// 創(chuàng)建對象 var person1 = new Person('LBJ輝', 18, 1.88); var person2 = new Person('LBJ', 38, 2.03);// 測試是否共享了函數(shù) alert(person1.sayHello == person2.sayHello); // true// 測試引用類型是否存在問題 person1.hobby.push('tennis'); alert(person1.hobby); alert(person2.hobby);如果理解了原型, 上面的代碼非常好理解.
- person1 和 person2 各有一份自己的屬性, 但是方法是共享的.
事實上, 還有一些其他的變種模式來實現(xiàn)基于對象的封裝. 但是這種方式是最常用的, 因此我們這里不再展開討論其他的模式. 后續(xù)需要我們再深入討論。
JavaScript 面向?qū)ο笤斀?#xff08;三)
繼承是面向?qū)ο笾蟹浅V匾奶匦?
ES5 中和類的實現(xiàn)一樣, 不能直接實現(xiàn)繼承. 實現(xiàn)繼承主要是依靠原型鏈來實現(xiàn)的。
一. 原型鏈
原型鏈?zhǔn)?ES5 中實現(xiàn)繼承的主要手段, 因此相對比較重要, 我們需要深入理解原型鏈.
1.1. 深入理解原型鏈
先來回顧一下構(gòu)造函數(shù)、原型和實例的關(guān)系:
- 每個構(gòu)造函數(shù)都有一個原型對象, 通過 prototype 指針指向該原型對象.
- 原型對象都包含一個指向構(gòu)造函數(shù)的指針, 通過 constructor 指針, 指向構(gòu)造函數(shù)
- 而實例都包含一個指向原型對象的內(nèi)部指針, 該內(nèi)部指針我們通常使用proto來描述.
思考如下情況:
- 我們知道, 可以通過 Person.prototype = {}的方式來重寫原型對象.
- 假如, 我們后面賦值的不是一個{}, 而是另外一個類型的實例, 結(jié)果會是怎么樣呢?
- 顯然,此時的原型對象將包含一個指向另一個原型的指針,相應(yīng)地,另一個原型中也包含著一個指向另一個構(gòu)造函數(shù)的指針。
- 假如另一個原型又是另一個類型的實例,那么上述關(guān)系依然成立,如此層層遞進(jìn),就構(gòu)成了實例與原型的鏈條。這就是所謂原型鏈的基本概念。
有些抽象, 我們通過代碼來理解:
// 創(chuàng)建Person構(gòu)造函數(shù) function Person() {}// 設(shè)置Animal的原型 Person.prototype = {};我們將代碼修改成原型鏈的形式:
// 1.創(chuàng)建Animal的構(gòu)造函數(shù) function Animal() {this.animalProperty = 'Animal'; }// 2.給Animal的原型中添加一個方法 Animal.prototype.animalFunction = function () {alert(this.animalProperty); };// 3.創(chuàng)建Person的構(gòu)造函數(shù) function Person() {this.personProperty = 'Person'; }// 4.給Person的原型對象重新賦值 Person.prototype = new Animal();// 5.給Person添加屬于自己的方法 Person.prototype.personFunction = function () {alert(this.personProperty); };// 6.創(chuàng)建Person的實例 var person = new Person(); person.animalFunction(); //Animal person.personFunction(); //Person代碼解析:
-
代碼有一些復(fù)雜, 但是如果你希望學(xué)習(xí)好原型鏈, 必須耐心去看一看上面的代碼, 你會發(fā)現(xiàn)其實都是我們學(xué)習(xí)過的.
-
重點我們來看第 4 步代碼: 給 Person.prototype 賦值了一個 Animal 的實例. 也就是 Person 的原型變成了 Animal 的實例.
-
Animal 實例本身有一個proto可以指向 Animal 的原型.
-
那么, 我們來思考一個問題: 如果現(xiàn)在搜索一個屬性或者方法, 這個時候會按照什么順序搜索呢?
- 第一步, 在 person 實例中搜索, 搜索到直接返回或者調(diào)用函數(shù). 如果沒有執(zhí)行第二步.
- 第二步, 在 Person 的原型中搜索, Person 的原型是誰? Animal 的實例. 所以會在 Animal 的實例中搜索, 無論是屬性還是方法, 如果搜索到則直接返回或者執(zhí)行. 如果沒有, 執(zhí)行第三步.
- 第三步, 在 Animal 的原型中搜索, 搜索到返回或者執(zhí)行, 如果沒有, 搜索結(jié)束. (當(dāng)然其實還有 Object, 但是先不考慮)
畫圖解析可能更加清晰:
當(dāng)代碼執(zhí)行到第 3 步(上面代碼的序號)的時候, 如圖所示:
當(dāng)代碼執(zhí)行第 4 步(上面代碼的序號)時, 發(fā)生了如圖所示的變化
- 注意圖片中的紅色線, 原來指向的是誰, 現(xiàn)在指向的是誰.
代碼繼續(xù)執(zhí)行
- Person.prototype.personFunction = function (){}
- 當(dāng)執(zhí)行第 5 步, 也就是給 Person 的原型賦值了一個函數(shù)時, 事實上在給 new Animal(Animal 的實例)賦值了一個新的方法.
代碼繼續(xù)執(zhí)行, 我們創(chuàng)建了一個 Person 對象
- 創(chuàng)建 Person 對象, person 對象會有自己的屬性, personProperty.
- 另外, person 對象有一個prototype指向 Person 的原型.
- Person 的原型是誰呢? 就是我們之前的 new Animal(Animal 的一個實例), 所以會指向它.
原型鏈簡單總結(jié):
- 通過實現(xiàn)原型鏈,本質(zhì)上擴(kuò)展了本章前面介紹的原型搜索機制。
- 當(dāng)以讀取模式訪問一個實例屬性時,首先會在實例中搜索該屬性。如果沒有找到該屬性,則會繼續(xù)搜索實例的原型。在通過原型鏈實現(xiàn)繼承的情況下,搜索過程就得以沿著原型鏈繼續(xù)向上。
- 在找不到屬性或方法的情況下,搜索過程總是要一環(huán)一環(huán)地前行到原型鏈末端才會停下來。
1.2. 原型和實例的關(guān)系
如果我們希望確定原型和實例之間的關(guān)系, 有兩種方式:
- 第一種方式是使用 instanceof 操作符,只要用這個操作符來測試實例與原型鏈中出現(xiàn)過的構(gòu)造函數(shù),結(jié)果就會返回 true。
- 第二種方式是使用 isPrototypeOf()方法。同樣,只要是原型鏈中出現(xiàn)過的原型,都可以說是該原型鏈所派生的實例的原型,因此 isPrototypeOf()方法也會返回 true
instanceof 操作符
// instanceof alert(person instanceof Object); // true alert(person instanceof Animal); // true alert(person instanceof Person); // trueisPrototypeOf()函數(shù)
// isPrototypeOf函數(shù) alert('isPrototypeOf函數(shù)函數(shù)'); alert(Object.prototype.isPrototypeOf(person)); // true alert(Animal.prototype.isPrototypeOf(person)); // true alert(Person.prototype.isPrototypeOf(person)); // true1.3. 添加新的方法
添加新的方法
- 在第 5 步操作中, 我們?yōu)樽宇愋吞砑恿艘粋€新的方法. 但是這里有一個注意點.
- 無論是子類中添加新的方法, 還是對父類中方法進(jìn)行重寫. 都一定要將添加方法的代碼, 放在替換原型語句之后.
- 否則, 我們添加的方法將會無效.
錯誤代碼引起的代碼:
// 1.定義Animal的構(gòu)造函數(shù) function Animal() {this.animalProperty = 'Animal'; }// 2.給Animal添加方法 Animal.prototype.animalFunction = function () {alert(this.animalProperty); };// 3.定義Person的構(gòu)造函數(shù) function Person() {this.personProperty = 'Person'; }// 4.給Person添加方法 Person.prototype.personFunction = function () {alert(this.personProperty); };// 5.給Person賦值新的原型對象 Person.prototype = new Animal();// 6.創(chuàng)建Person對象, 并且調(diào)用方法 var person = new Person(); person.personFunction(); // 不會有任何彈窗, 因為找不到該方法代碼解析:
- 執(zhí)行上面的代碼不會出現(xiàn)任何的彈窗, 因為我們添加的方法是無效的, 被賦值的新的原型覆蓋了.
- 正確的辦法是將第 4 步和第 5 步操作換一下位置即可.
總結(jié)
- 其實這個問題沒什么好說的, 只要你理解了原型鏈(好好看看我上面畫的圖, 或者自己畫一下圖)
- 但是, 切記在看圖的過程中一樣掃過, 因為這會讓你錯過很多細(xì)節(jié), 對原型鏈的理解就會出現(xiàn)問題.
1.4. 原型鏈的問題
原型鏈對于繼承來說:
- 原型鏈似乎對初學(xué) JavaScript 原型的人來說, 已經(jīng)算是比較高明的設(shè)計技巧了, 有些人理解起來都稍微有些麻煩.
- 但是, 這種設(shè)計還存在一些缺陷, 不是最理性的解決方案. (但是后續(xù)的解決方案也是依賴原型鏈, 無論如何都需要先理解它)
原型鏈存在的問題:
- 原型鏈存在最大的問題是關(guān)于引用類型的屬性.
- 通過上面的原型實現(xiàn)了繼承后, 子類的 person 對象繼承了(可以訪問)Animal 實例中的屬性(animalProperty).
- 但是如果這個屬性是一個引用類型(比如數(shù)組或者其他引用類型), 就會出現(xiàn)問題.
引用類型的問題代碼:
// 1.定義Animal的構(gòu)造函數(shù) function Animal() {this.colors = ['red', 'green']; }// 2.給Animal添加方法 Animal.prototype.animalFunction = function () {alert(this.colors); };// 3.定義Person的構(gòu)造函數(shù) function Person() {this.personProperty = 'Person'; }// 4.給Person賦值新的原型對象 Person.prototype = new Animal();// 5.給Person添加方法 Person.prototype.personFunction = function () {alert(this.personProperty); };// 6.創(chuàng)建Person對象, 并且調(diào)用方法 var person1 = new Person(); var person2 = new Person();alert(person1.colors); // red,green alert(person2.colors); // red,greenperson1.colors.push('blue');alert(person1.colors); // red,green,blue alert(person2.colors); // red,green,blue代碼解析:
- 我們查看第 6 步的操作
- 創(chuàng)建了兩個對象, 并且查看了它們的 colors 屬性
- 修改了 person1 中的 colors 屬性, 添加了一個新的顏色 blue
- 再次查看兩個對象的 colors 屬性, 會發(fā)現(xiàn) person2 的 colors 屬性也發(fā)生了變化
- 兩個實例應(yīng)該是相互獨立的, 這樣的變化如果我們不制止將會在代碼中引發(fā)一些列問題.
原型鏈的其他問題:
- 在創(chuàng)建子類型的實例時,不能向父類型的構(gòu)造函數(shù)中傳遞參數(shù)。
- 實際上,應(yīng)該說是沒有辦法在不影響所有對象實例的情況下,給父類型的構(gòu)造函數(shù)傳遞參數(shù)。
- 從而可以修改父類型中屬性的值, 在創(chuàng)建構(gòu)造函數(shù)的時候就確定一個值.
二. 經(jīng)典繼承
為了解決原型鏈繼承中存在的問題, 開發(fā)人員提供了一種新的技術(shù): constructor stealing(有很多名稱: 借用構(gòu)造函數(shù)或經(jīng)典繼承或偽造對象), steal 是偷竊的意思, 但是這里可以翻譯成借用.
2.1. 經(jīng)典繼承的思想
經(jīng)典繼承的做法非常簡單: 在子類型構(gòu)造函數(shù)的內(nèi)部調(diào)用父類型構(gòu)造函數(shù).
- 因為函數(shù)可以在任意的時刻被調(diào)用
- 因此通過 apply()和 call()方法也可以在新創(chuàng)建的對象上執(zhí)行構(gòu)造函數(shù).
經(jīng)典繼承代碼如下:
// 創(chuàng)建Animal的構(gòu)造函數(shù) function Animal() {this.colors = ['red', 'green']; }// 創(chuàng)建Person的構(gòu)造函數(shù) function Person() {// 繼承Animal的屬性Animal.call(this);// 給自己的屬性賦值this.name = 'LBJ輝'; }// 創(chuàng)建Person對象 var person1 = new Person(); var person2 = new Person();alert(person1.colors); // red,green alert(person2.colors); // red,green person1.colors.push('blue'); alert(person1.colors); // red,green,blue alert(person2.colors); // red,green代碼解析:
- 我們通過在 Person 構(gòu)造函數(shù)中, 使用 call 函數(shù), 將 this 傳遞進(jìn)去.
- 這個時候, 當(dāng) Animal 中有相關(guān)屬性初始化時, 就會在 this 對象上進(jìn)行初始化操作.
- 這樣就實現(xiàn)了類似于繼承 Animal 屬性的效果.
這個時候, 我們也可以傳遞參數(shù), 修改上面的代碼:
// 創(chuàng)建Animal構(gòu)造函數(shù) function Animal(age) {this.age = age; }// 創(chuàng)建Person構(gòu)造函數(shù) function Person(name, age) {Animal.call(this, age);this.name = name; }// 創(chuàng)建Person對象 var person = new Person('LBJ輝', 18); alert(person.name); alert(person.age);2.2. 經(jīng)典繼承的問題
經(jīng)典繼承的問題:
- 對于經(jīng)典繼承理解比較深入, 你已經(jīng)能發(fā)現(xiàn): 經(jīng)典繼承只有屬性的繼承, 無法實現(xiàn)方法的繼承.
- 因為調(diào)用 call 函數(shù), 將 this 傳遞進(jìn)去, 只能將父構(gòu)造函數(shù)中的屬性初始化到 this 中.
- 但是如果函數(shù)存在于父構(gòu)造函數(shù)的原型對象中, this 中是不會有對應(yīng)的方法的.
回顧原型鏈和經(jīng)典繼承:
- 原型鏈存在的問題是引用類型問題和無法傳遞參數(shù), 但是方法可以被繼承
- 經(jīng)典繼承是引用類型沒有問題, 也可以傳遞參數(shù), 但是方法無法被繼承.
- 怎么辦呢? 將兩者結(jié)合起來怎么樣?
三. 組合繼承
如果你認(rèn)識清楚了上面兩種實現(xiàn)繼承的方式存在的問題, 就可以很好的理解組合繼承了.
組合繼承(combination inheritance, 有時候也稱為偽經(jīng)典繼承), 就是將原型鏈和經(jīng)典繼承組合在一起, 從而發(fā)揮各自的優(yōu)點.
3.1. 組合繼承的思想
組合繼承:
- 組合繼承就是發(fā)揮原型鏈和經(jīng)典繼承各自的優(yōu)點來完成繼承的實現(xiàn).
- 使用原型鏈實現(xiàn)對原型屬性和方法的繼承.
- 通過經(jīng)典繼承實現(xiàn)對實例屬性的繼承, 以及可以在構(gòu)造函數(shù)中傳遞參數(shù).
組合繼承的代碼:
// 1.創(chuàng)建構(gòu)造函數(shù)的階段 // 1.1.創(chuàng)建Animal的構(gòu)造函數(shù) function Animal(age) {this.age = age;this.colors = ['red', 'green']; }// 1.2.給Animal添加方法 Animal.prototype.animalFunction = function () {alert('Hello Animal'); };// 1.3.創(chuàng)建Person的構(gòu)造函數(shù) function Person(name, age) {Animal.call(this, age);this.name = name; }// 1.4.給Person的原型對象重新賦值 Person.prototype = new Animal(0);// 1.5.給Person添加方法 Person.prototype.personFunction = function () {alert('Hello Person'); };// 2.驗證和使用的代碼 // 2.1.創(chuàng)建Person對象 var person1 = new Person('LBJ輝', 18); var person2 = new Person('LBJ', 38);// 2.2.驗證屬性 alert(person1.name + '-' + person1.age); // LBJ輝,18 alert(person2.name + '-' + person2.age); // LBJ,38// 2.3.驗證方法的調(diào)用 person1.animalFunction(); // Hello Animal person1.personFunction(); // Hello Person// 2.4.驗證引用屬性的問題 person1.colors.push('blue'); alert(person1.colors); // red,green,blue alert(person2.colors); // red,green代碼解析:
- 根據(jù)前面學(xué)習(xí)的知識, 結(jié)合當(dāng)前的代碼, 大家應(yīng)該可以理解上述代碼的含義.
- 但是我還是建議大家一定要多手動自己來敲代碼, 來理解其中每一個步驟.
- 記住: 看懂, 聽懂不一定真的懂, 自己可以寫出來, 才是真的懂了.
3.2. 組合繼承的分析
組合繼承是 JavaScript 最常用的繼承模式之一.
- 如果你理解到這里, 點到為止, 那么組合來實現(xiàn)繼承只能說問題不大.
- 但是它依然不是很完美, 存在一些問題不大的問題.(不成問題的問題, 基本一詞基本可用, 但基本不用)
組合繼承存在什么問題呢?
- 組合繼承最大的問題就是無論在什么情況下, 都會調(diào)用兩次父類構(gòu)造函數(shù).
- 一次在創(chuàng)建子類原型的時候
- 另一次在子類構(gòu)造函數(shù)內(nèi)部(也就是每次創(chuàng)建子類實例的時候).
- 另外, 如果你仔細(xì)按照我的流程走了上面的每一個步驟, 你會發(fā)現(xiàn): 所有的子類實例事實上會擁有兩份父類的屬性
- 一份在當(dāng)前的實例自己里面(也就是 person 本身的), 另一份在子類對應(yīng)的原型對象中(也就是 person.proto里面)
- 當(dāng)然, 這兩份屬性我們無需擔(dān)心訪問出現(xiàn)問題, 因為默認(rèn)一定是訪問實例本身這一部分的.
怎么解決呢?
- 看起來組合繼承也不是非常完美的解決方案, 雖然也可以應(yīng)用.
- 有沒有終極的解決方案呢? 預(yù)知后事如何, 且聽下回分解.
JavaScript 面向?qū)ο笤斀?#xff08;四)
在上一篇中, 我們討論了 ES5 中, 實現(xiàn)繼承的一些方式.
在最后, 我們說了組合繼承是相對完美的解決方案, 但是它也存在一些問題.
這篇文章, 我們就通過某種新的模式, 給出一種目前使用最多, 也是我們最終的解決方案.
一. 原型式繼承
1.1. 原型式繼承的思想
原型式繼承的淵源
- 這種模式要從道格拉斯·克羅克福德(Douglas Crockford, 著名的前端大師, JSON 的創(chuàng)立者)在 2006 年寫的一篇文章說起: Prototypal Inheritance in JavaScript(在 JS 中使用原型式繼承)
- 在這篇文章中, 它介紹了一種繼承方法, 而且這種繼承方法不是通過構(gòu)造函數(shù)來實現(xiàn)的.
- 為了理解這種方式, 我們先再次回顧一下 JavaScript 想實現(xiàn)繼承的目的: 重復(fù)利用另外一個對象的屬性和方法.
原型式繼承的核心函數(shù):
// 封裝object()函數(shù) function object(o) {function F() {}F.prototype = o;return new F(); }代碼解析:
- 在 object()函數(shù)內(nèi)部, 先創(chuàng)建一個臨時的構(gòu)造函數(shù).
- 然后將傳遞的對象作為這個構(gòu)造函數(shù)的原型
- 最后返回了這個臨時類型的一個新的實例.
- 事實上, object()對傳入的對象執(zhí)行了一次淺復(fù)制.
1.2. 原型式繼承的使用
使用原型式繼承:
// 使用原生式繼承 var person = {name: 'LBJ輝',colors: ['red', 'green'], };// 通過person去創(chuàng)建另外一個對象 var person1 = object(person); person1.name = 'LBJ'; person1.colors.push('blue');alert(person1.name); // LBJ alert(person1.colors); // red,green,bluealert(person.name); // LBJ輝 alert(person.colors); // red,green,blue代碼解析:
- 這種方式和我們傳統(tǒng)意義上理解的繼承有些不同. 它做的事情是通過一個對象去創(chuàng)建另外一個對象.(利用 person 去創(chuàng)建 person1)
- 當(dāng)然, person1 中繼承過來的屬性是放在了自己的原型對象中的.
- 也可以給 person1 自己再次添加 name 屬性, 這個時候 name 才是在實例本身中.
- 但是如果是修改或者添加引用類型的內(nèi)容, 還是會引起連鎖反應(yīng).
- 可能暫時你看不到這些代碼的意義, 但是這些代碼是我們后續(xù)終極方案的前提思想, 所以先看看和練習(xí)一下這些代碼.
針對這種思想, ES5 中新增了 Object.create()方法來規(guī)范化了原型式繼承.
- 也就是上面的代碼可以修改成這樣.(只是將 object 函數(shù)修改成了 Object.create)
Object.create()還可以傳入第二個參數(shù):
- 第二個參數(shù)用于每個屬性的自定義描述.
- 比如 person1 的 name 我們希望修改成"LBJ", 就可以這樣來做
1.3. 原型式繼承的問題
-
原型式繼承的的優(yōu)點和缺點:
-
- 如果我們只是希望一個對象和另一個對象保持類似的情況下, 原型式繼承完全可以勝任, 這是它的優(yōu)勢.
- 但是, 原型式繼承依然存在屬性共享的問題, 就像使用原型鏈一樣.
二. 寄生式繼承
2.1. 寄生式繼承的思想
寄生式(Parasitic)繼承
- 寄生式(Parasitic)繼承是與原型式繼承緊密相關(guān)的一種思想, 并且同樣由道格拉斯·克羅克福德(Douglas Crockford)提出和推廣的
- 寄生式繼承的思路是結(jié)合原型類繼承和工廠模式的一種方式.
- 即創(chuàng)建一個封裝繼承過程的函數(shù), 該函數(shù)在內(nèi)部以某種方式來增強對象, 最后再將這個對象返回.
寄生式函數(shù)多增加了一個核心函數(shù):
// 封裝object函數(shù) function object(o) {function F() {}F.prototype = o;return new F(); }// 封裝創(chuàng)建新對象的函數(shù) function createAnother(original) {var clone = object(original);clone.sayHello = function () {alert('Hello JavaScript');};return clone; }2.2. 寄生式繼承的應(yīng)用
我們來使用一下寄生式繼承
// person對象 var person = {name: 'LBJ輝',colors: ['red', 'green'], };// 新的對象 var person1 = createAnother(person); person1.sayHello();代碼解讀:
- 我們基于 person 對象, 創(chuàng)建了另外一個對象 person1.
- 在最新的 person1 對象中, 不僅會擁有 person 的屬性和方法, 而且還有自己定義的方法.
2.3. 寄生式繼承的問題
寄生式繼承存在的問題:
- 寄生式繼承和原型式繼承存在一樣的問題, 引用類型會共享. (因為是在原型式繼承基礎(chǔ)上的一種封裝)
- 另外寄生式繼承還存在函數(shù)無法復(fù)用的問題, 因為每次 createAnother 一個新的對象, 都需要重新定義新的函數(shù).
三. 寄生組合式繼承
3.1. 寄生組合式繼承的思想
寄生組合式繼承
-
現(xiàn)在我們來回顧一下之前提出的比較理想的組合繼承
-
- 組合繼承是比較理想的繼承方式, 但是存在兩個問題:
- 問題一: 構(gòu)造函數(shù)會被調(diào)用兩次: 一次在創(chuàng)建子類型原型對象的時候, 一次在創(chuàng)建子類型實例的時候.
- 問題二: 父類型中的屬性會有兩份: 一份在原型對象中, 一份在子類型實例中.
-
事實上, 我們現(xiàn)在可以利用寄生式繼承將這兩個問題給解決掉.
-
- 你需要先明確一點: 當(dāng)我們在子類型的構(gòu)造函數(shù)中調(diào)用父類型.call(this, 參數(shù))這個函數(shù)的時候, 就會將父類型中的屬性和方法復(fù)制一份到了子類型中. 所以父類型本身里面的內(nèi)容, 我們不再需要.
- 這個時候, 我們還需要獲取到一份父類型的原型對象中的屬性和方法.
- 能不能直接讓子類型的原型對象 = 父類型的原型對象呢?
- 不要這么做, 因為這么做意味著以后修改了子類型原型對象的某個引用類型的時候, 父類型原生對象的引用類型也會被修改.
- 我們使用前面的寄生式思想就可以了.
寄生組合式的核心代碼:
// 定義object函數(shù) function object(o) {function F() {}F.prototype = o;return new F(); }// 定義寄生式核心函數(shù) function inhreitPrototype(subType, superType) {var prototype = object(superType.prototype);prototype.constructor = subType;subType.prototype = prototype; }3.2. 寄生組合式繼承的應(yīng)用
直接給出使用的代碼, 也是我們以后使用繼承的終極方式
// 定義Animal構(gòu)造函數(shù) function Animal(age) {this.age = age;this.colors = ['red', 'green']; }// 給Animal添加方法 Animal.prototype.animalFunction = function () {alert('Hello Animal'); };// 定義Person構(gòu)造函數(shù) function Person(name, age) {Animal.call(this, age);this.name = name; }// 使用寄生組合式核心函數(shù) inhreitPrototype(Person, Animal);// 給Person添加方法 Person.prototype.personFunction = function () {alert('Hello Person'); };代碼的優(yōu)點:
- 這種方式的高效體現(xiàn)在現(xiàn)在它只調(diào)用了一次 Animal 的構(gòu)造函數(shù).
- 并且也避免了在原型上面多出的多余屬性, 而且原型之間不會產(chǎn)生任何的干擾(子類型原型和父類型原型之間).
- 在 ES5 中, 普遍認(rèn)為寄生組合式繼承是最理想的繼承范式.
總結(jié)
以上是生活随笔為你收集整理的JavaScript 面向对象详解的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 《Mysql》必知必会读书笔记
- 下一篇: SpringBoot 二维码生成base