[前端漫談_6] 從原型聊到原型繼承,深刻理解 JavaScript 面向對象精髓

前言

開頭說點題外話,不知道何時開始,我發如今 JavaScript 中,你們都喜歡用 foo 和 bar 來用做示例變量名,爲此專門查了一下這傢伙的 來源javascript

「The etymology of foo is obscure. Its use in connection with bar is generally traced to the World War II military slang FUBAR, later bowdlerised to foobar. ... The use of foo in a programming context is generally credited to the Tech Model Railroad Club (TMRC) of MIT from circa 1960.」html

foo的詞源是模糊的。 它與bar的關係能夠追溯到第二次世界大戰的軍事俚語 FUBAR,後簡化爲foobar。 而在編程環境中使用 foo 一般認爲起源於約 1960 年時麻省理工學院的技術模型鐵路俱樂部(TMRC)。前端

okay ,那麼今天,咱們也看看這段 Foo 的代碼來聊聊原型。java

function Foo(name){
    this.name = name
}

let foo = new Foo('demoFoo')

console.log(foo.name) // demoFoo
複製代碼

1. 爲何 JavaScript 被設計成基於原型的模式?

你們都知道 Java 做爲面向對象的語言的三個要素:封裝繼承多態,我以前也寫過 Java ,因此在一開始學習 JavaScript 的時候,我老是會去經過類比熟悉的 Java 來理解 JavaScript 中關於繼承的概念,可是不管怎麼去類比都以爲不是那麼回事,由於這自己就是兩種徹底不一樣的方式。編程

JavaScript 是如何設計出來的呢,wiki 是這樣說的函數

「網景決定發明一種與 Java 搭配使用的輔助腳本語言,而且語法上有些相似」學習

無論怎麼說,JavaScript 在設計之初都受到了 Java 的影響,因此在 Javascript 中也有了對象的概念,可是做爲一種 輔助腳本語言 ,類的概念有些過於笨重,不夠簡單。可是對象之間須要一種讓彼此都產生聯繫的機制,怎麼辦呢?ui

依舊是參考了 Java 的設計,Java 中生成一個對象的語法是這樣:this

Foo foo = new Foo() // 請注意這裏的Foo 指的是類名,而不是構造函數。
複製代碼

因而 Brendan Eich 也模仿了這樣的方式使用了 new 來生成對象,可是 JavaScript 中的 new 後面跟的不是 Class 而是 Constructorspa

okay 解決了實例化的問題,可是僅僅只靠一個構造函數,當前對象沒法與其餘的對象產生聯繫,例若有的時候咱們指望共享一些屬性:

function People(age) {
    this.age = age
    this.nation = 'China'
}

// 父子大小明
let juniorMing = new People(12)
let seniorMing = new People(38)

// 有天他們一塊兒移民了,此時我想改變他們的國籍爲 America
juniorMing.nation = 'America'
// 可是改變小明一人的國籍並不能影響大明的國籍
console.log(seniorMing.nation) // China
複製代碼

(nation)國籍 在這個例子中成爲了咱們想在兩個對象之間共享的屬性,可是因爲沒有類的概念,必須得有一個新的機制來處理這部分 須要被共享的屬性。這就是 prototype 的由來。

因此咱們上面的例子變成了什麼呢?

function People(age) {
    this.age = age
}

People.prototype.nation = 'China'

// 父子大小明
let juniorMing = new People(12)
let seniorMing = new People(38)

// 有天他們一塊兒移民了,此時我想改變他們的國籍爲 America
People.prototype.nation = 'America'

console.log(seniorMing.nation) // America
console.log(juniorMing.nation) // America
複製代碼

2. 最簡單的原型

結合前言部分中的代碼:

function Foo(name){
    this.name = name
}

let foo = new Foo('demoFoo')

console.log(foo.name) // demoFoo
複製代碼

先提取一下關鍵信息:

  • foo 是被構造出來的實例對象。
  • foo 的構造方法是 Foo()

因此最基礎的原型鏈就是這樣:

在這個例子中 Constructor.prototype 等價於 Foo.prototype

構造函數 Foo 能夠經過 Foo.prototype 來訪問原型,同時被構造出來的對象 foo 也能夠經過 foo.__proto__ 來訪問原型:

Foo.prototype === foo.__proto__ // true
foo.__proto__.constructor === Foo // true
複製代碼

簡單的來講,Foo 函數,參照了 Foo.prototype 生產出來了一個 foo 對象。

爲了更好的理解這一過程,我得從一個故事開始提及:

  • 在好久好久好久之前,有一個工匠偶然間看到了一個很美的古蹟雕像(原型 Foo.prototype
  • 他想經過批量的生產復刻的版原本發家致富,因而他先分析雕像,還原了製造的過程,而且設計出一條生產線(構造器 Foo
  • 而後經過這個構造器,能夠源源不斷的造出許多的復刻雕像(實例 foo)。

3.原型鏈

剛剛的故事尚未結束,後來一天這個工匠開始思考,以前看到的那個雕塑是哪裏來的呢?又是怎麼作出來的呢?就算是自然造成的,那又是什麼條件造成了這樣的雕塑呢?

帶着這些問題,他開啓了 996 模式,尋師訪友,查閱典籍,經歷了多年苦心研究,終於有了新的發現:

① 原來他做爲參照物(原型)的雕像 ( Foo.prototype / foo.__proto__ ) 是 n 年前一位雕刻大師參照天然的現象( Object.prototype )而後設計了鑄造方式( Object() )造出來的。

從代碼來看:

foo.__proto__.__proto__ === Object.prototype //true

Foo.prototype.__proto === Object.prototype //true

Object === Object.prototype.constructor
複製代碼

用圖來描述這一過程就是:

② 除此以外,他發現,原來這位雕刻大師設計的鑄造方式( Object() ),是根據天然現象的造成規律( Function.prototype )來設計的。因此,本質上來講,他所設計出的生產線( Foo() ),也間接的參考了天然現象的造成規律( Function.prototype )

console.log(Object.__proto__=== Function.__proto__)
console.log(Foo.__proto__ === Function.prototype)
複製代碼

而後咱們來看看圖會更加清晰一些:

③ 故事到這裏尚未結束,這位工匠發現,原來,對於天然現象造成規律的描述(Function.prototype)是先輩們從這天然現象( Object.prototype )中總結出來的。

因此咱們從代碼中看到:

Function.prototype.__proto__ === Object.prototype
複製代碼

因此 人法地、地法天、天法道、道法天然,如今咱們能夠看到完整的原型鏈:

④ 故事並無像咱們想象中那樣結束,這位工匠最後改良了生產鏈,結合了先人的方式和關於這一天然現象的規律,從新定義了關於這一規律的描述。

因此代碼被改寫爲:

let foo = new Object()
console.log(foo.__proto__ === Object.prototype) // true

複製代碼

而這一故事也一直流傳到今天:

4.原型繼承

看到這裏,相信你對 JavaScript 中的原型和原型鏈,都有了新的認識,那麼咱們再來聊聊原型繼承,在聊原型繼承以前,咱們想一想什麼叫作繼承呢?

拋開計算機中的理論,咱們就說一個最簡單的例子,小王經過繼承了他老爸的遺產走上了人生巔峯。這裏面其實有一個關鍵信息:他老爸的遺產幫助他更快的走上了人生巔峯,換言之,小王不須要本身努力也能經過他老爸留下的財產走上人生巔峯。

聽起來很像是廢話,可是本質也在這裏:

  • 咱們定義了一個新的對象 child
  • 可是咱們並不想再幫他定義其餘複雜的屬性
  • 因此咱們選擇了一個以前定義過的構造函數 parents 而後讓 child 直接從 parents 那裏把全部東西都繼承過來。
  • parents 的基礎上,我可能爲 child 定製了一些內容,因此之後有可能直接從 child 來繼承這一切。

聽起來繼承就像是 讓一個普通的對象快速的得到本來不屬於它的超能力。

蝙蝠俠的故事告訴咱們:rich 也是一種超能力。

okay 讓咱們看看怎麼樣讓 JavaScript 中的對象快速得到超能力呢?結合咱們上面瞭解的內容,咱們會發現幾個很關鍵的點,若是要讓一個對象得到超能力,只有下面的三種途徑:

  1. 由於對象是由 constructor 生產的,因此咱們能夠經過改變 constructor 來實現。
  2. constructor 有一個關鍵的屬性: constructor.prototype 因此改變原型也能達到目的。
  3. 直接改變對象的屬性,將要添加的內容複製過來。

不管你怎麼去找繼承的方法,繼承的本質就在這裏,領悟本質會讓問題變得簡單,因此咱們一塊兒來看看具體的實現。

改寫構造函數

咱們從最簡單的開始,這種方式的核心就在於直接在當前構造函數中,調用你想繼承的構造函數。

function Student(){
    this.title = 'Student'
}

function Girl(){
    Student.call(this)
    this.sex = 'female'
}

let femaleStudent = new Girl()

console.log(femaleStudent)
複製代碼

這種方式並無影響到你的原型鏈,由於本質上來講,你仍是經過 Girl() 來生成了一個對象,而且 Girl.prototype 也並未受到影響,因此原型鏈不會產生變化。

改變 constructor.prototype

改變 constructor.prototype 這一方式有不一樣的狀況,咱們能夠分開來看

① chidConstructor.prototype = new Parents()

這種方式的核心已經寫在了標題上,因此咱們來看看代碼吧:

function Parent() {
    this.title = "parent"
}
function Child() {
    this.age = 13
}
const parent = new Parent()
Child.prototype = parent
const child = new Child()
console.log(child);
console.log('child.__proto__.constructor: ', child.__proto__.constructor);
console.log('parent.constructor: ', parent.constructor);//每個實例也有一個constructor屬性,默認調用prototype對象的constructor屬性
複製代碼

打印出來是什麼呢?

把他和本來沒有繼承以前的 child 對比一下:

結論是:

  • 當前對象的原型已經被重置爲一個 parent 對象
  • 當前對象的構造方法由 Child() 變成了 Parent()
  • 本來的 child.prototype 被替換爲 parent 對象後與構造器之間的聯繫成爲了單向:
console.log(parent.constructor.prototype === parent) //false
複製代碼

咱們用圖來描述一下:

爲了解決上面存在的問題,咱們改寫了代碼,添加了一行

...
const parent = new Parent()
Child.prototype = parent
Child.prototype.constructor = Child; //添加了這行
const child = new Child()
...
複製代碼

因此原型鏈成爲下面這樣:

有的同窗可能會不理解爲何 Child.prototype === parent 以及 parent.constructor === Child()

Child.prototype === parent 是由於咱們在代碼中中強行設置了,parent.constructor === Child() 是由於 parent 對象自己也有一個 constructor 屬性,這個屬性默認返回 parent.__proto__.constructor 因此以前是 Parent() 可是如今也被代碼強制設置爲了 Child()

Child.prototype = parent
Child.prototype.constructor = Child; // parent.constructor === Child
複製代碼

② 方法 ① 和 改變構造函數的組合

咱們把前面兩種方式組合起來:

function Parent() {
    this.title = "parent"
}
function Child() {
    Parent.call(this)
    this.age = 13
}
const parent = new Parent()
Child.prototype = parent
Child.prototype.constructor = Child;
const child = new Child()
console.log(child);
複製代碼

打印的結果是:

這樣咱們生成的 child 對象自己包含了他從 Parent 中繼承來的 title 屬性,可是同時 Child.prototype 同時也包含了全部 Parent 上的全部屬性,形成內存的浪費:

③ 方法 ② 組合改進

因此咱們把方法 ② 改變一下,避免內存的浪費,既然緣由是由於咱們將 Child.prototype設置爲 new Parent() 的過程當中,使用 Parent() 進行實例化因此將屬性都繼承到了 Child 原型上,那麼爲何不能夠直接使用原型對原型進行賦值呢?

也就是 chidConstructor.prototype = Parents.prototype

function Parent() {
    this.title = "parent"
}
function Child() {
    Parent.call(this)
    this.age = 13
}
Child.prototype = Parent.prototype
Child.prototype.constructor = Child;
const child = new Child()
console.log(child);
複製代碼

咱們看下打印信息:

okay,關鍵的信息看起來都很完美,再分析下原型鏈:

嗯,單純站在 child 的角度來看好像沒有什麼問題,可是若是咱們打印下面這幾行會發現問題:

console.log('parent: ', new Parent());
console.log(Child.prototype === Parent.prototype) //true
複製代碼

Parent.prototype 的構造器變成了 Child 是不合理的,並且此時 Child.prototype === Parent.prototype 兩個屬性指向同一個對象,當咱們改變 Child.prototype 的時候,咱們並不但願影響到 Parent.prototype 可是在這裏成爲了避免可避免的問題。

那有什麼辦法能夠解決這個問題呢?

④ 拷貝 prototype

若是咱們並不直接將 Parent.prototype 賦值給 Child.prototype 而是複製一個如出一轍的新對象出來替代呢?

function Parent() {
    this.title = "parent"
}
function Child() {
    Parent.call(this)
    this.age = 13
}
Child.prototype = Object.create(Parent.prototype)
Child.prototype.constructor = Child;//咱們在這裏修改了構造器的指向,你一樣能夠在Object.create 方法中作這件事。
const child = new Child()
console.log('parent: ', new Parent());
console.log(child);
複製代碼

最後打印出來的結果如何呢?

沒有任何問題,避免了父子原型的直接賦值致使的各類問題~

⑤ 空對象法

除了上面的解決方案,還有沒有別的辦法呢?答案是有的,除了經過複製創造一個新的原型對象,咱們還能夠用一箇中間函數來實現這件事:

...
function extend(Child, Parent) {
    var X = function () { };
    X.prototype = Parent.prototype;
    Child.prototype = new X();
    Child.prototype.constructor = Child;
}
...
複製代碼

一樣很完美。這也是 YUI 庫實現繼承的方式。

直接改變對象的屬性

咱們再也不基於原型去玩什麼花樣,而是直接把整個父對象的屬性都拷貝給子對象。若是僅僅是值類型的話,是沒有問題的,但若是這時候,父對象的屬性中本來包含的是引用類型的值呢?

咱們就要考慮把整個引用類型的屬性拷貝一份到子對象,這裏就設計到淺拷貝和深拷貝的內容啦~

5.最後

若是你仔細的讀完本文,相信你對 JavaScript 中的原型,原型鏈,原型繼承,會有新的認識。

若是以爲對你有幫助,記得 點贊 哦,

畢竟做者也是要恰飯的。

很是感謝 阮一峯 老師關於繼承整理。

本來想寫寫靜態分析,可是寫起來發現本身還有不少的不足,因此這裏也不說下次會寫啥了,由於太渣也不必定寫得出來...

這裏是 Dendoink ,奇舞週刊原創做者,掘金 [聯合編輯 / 小冊做者] 。

對於技術人而言: 是單兵做戰能力, 則是運用能力的方法。駕輕就熟,出神入化就是 。在前端娛樂圈,我想成爲一名出色的人民藝術家。

掃碼關注公衆號 前端惡霸 我在這裏等你:

相關文章
相關標籤/搜索