詳解JavaScript中的原型和繼承

2018-06-17 11:08 更新

最近組內(nèi)的童鞋和我說(shuō)的自己一直搞不太清楚js原型這一塊的東西,我想了想覺(jué)得這東西也不是一兩句話就能解釋清楚的,所以我決定來(lái)解釋解釋js中的原型機(jī)制,希望也能幫到你。

本文將會(huì)介紹面向?qū)ο?,繼承,原型等相關(guān)知識(shí),涉及的知識(shí)點(diǎn)如下:

  • 面向?qū)ο笈c繼承
  • CEOC
  • OLOO
  • 臃腫的對(duì)象
  • 原型與原型鏈
  • 修改原型的方式

面向?qū)ο笈c繼承

最近學(xué)習(xí)了下python,還寫了篇博文《重拾編程樂(lè)趣——我的Python筆記》,加深了我對(duì)面向?qū)ο蟮囊恍├斫狻?/p>

我們會(huì)對(duì)我們寫的程序進(jìn)行抽象,而不同的語(yǔ)言都提供了不同的抽象工具,比如各種語(yǔ)言里面的數(shù)組,集合(鍵值數(shù)組,哈希表,字典等)等提供了對(duì)數(shù)據(jù)的抽象;而VB里面的子程序,類C語(yǔ)言里面的函數(shù),提供了抽象代碼段的能力。

有時(shí)我們希望將數(shù)據(jù)和對(duì)數(shù)據(jù)的操作封裝到一起,這被稱作對(duì)象,是一種更高唯獨(dú)的抽象工具,而這種抽象工具——對(duì)象可以對(duì)現(xiàn)實(shí)世界進(jìn)行建模。

現(xiàn)實(shí)世界中的事物都有一些聯(lián)系,比如我們可以抽象出來(lái)貓,然后又公貓,母貓,顯然公貓應(yīng)該擁有貓的特性,這也就是繼承,細(xì)分的事物應(yīng)該有高緯度事物的特點(diǎn)。

想要實(shí)現(xiàn)對(duì)象和繼承這套思維,目前有兩種實(shí)現(xiàn)方法,分別是CEOC和OLOO。

CEOC

CEOC(class extend other class)是一套基于類和實(shí)例的實(shí)現(xiàn)方式,這種方式實(shí)用的比較廣泛,大部分流星的面向?qū)ο笳Z(yǔ)言都在使用,比如java,python等。

類作為對(duì)象的抽象描述,對(duì)象是類的實(shí)例,也稱作類的泛化,泛化和抽象對(duì)應(yīng)。最恰當(dāng)?shù)念惐染褪巧w房子了,類就相當(dāng)于房子的圖紙,圖紙是對(duì)房子屬性和功能的描述,根據(jù)圖紙可以蓋很多個(gè)類似的房子;另一種類比就是磨具和零件,磨具相當(dāng)于類,零件相當(dāng)于對(duì)象,通過(guò)磨具我們可以造出來(lái)很多零件。

其實(shí)與其說(shuō)是面向?qū)ο筮€不如說(shuō)是面向類編程,在這種機(jī)制中解決上面提到的繼承邏輯時(shí),是在類上實(shí)現(xiàn)的,子類可以繼承父類。

在類的機(jī)制中更像是對(duì)工廠里通過(guò)磨具造零件機(jī)制的模擬,而非動(dòng)物世界這種繁衍繼承機(jī)制的模擬,下面介紹的OLOO則是對(duì)世界更好的模擬。

OLOO

OLOO(object link other object)是一套基于對(duì)象和原型的實(shí)現(xiàn)方式,這種方式唯一實(shí)現(xiàn)廣泛語(yǔ)言就是js了,熟悉類機(jī)制的同學(xué),初次接觸這個(gè),可能會(huì)覺(jué)得不是很好理解,而很多前端同學(xué)理解的也不是很明白,之前寫過(guò)一篇原型的文章,推薦大家閱讀《JavaScript原型之路》。

其實(shí)面向類的機(jī)制有些多此一舉了,因?yàn)樽詈笫褂玫氖菍?duì)象,而不是類,那么我們直接讓對(duì)象可以集成對(duì)象不就行了,在這種機(jī)制中可以通過(guò)某種操作讓對(duì)象和對(duì)象之間可以建立繼承的關(guān)系,當(dāng)繼承的對(duì)象(子對(duì)象)中沒(méi)有某些屬性時(shí)可以去被繼承的對(duì)象(父對(duì)象)中去查找。

在OLOO中,父對(duì)象也可以成為子對(duì)象的原型,這有些類似我們的人類的繁衍,每一個(gè)人都是一個(gè)對(duì)象,都是父母所生,而不是用模版建造出來(lái)的,每一個(gè)人都有一個(gè)父親,孩子和父親之間有一種特殊的關(guān)系,成為父子。

臃腫的對(duì)象

來(lái)說(shuō)說(shuō)JS中的對(duì)象機(jī)制,JS中的對(duì)象顯得有些臃腫,JS中的對(duì)象承接了兩個(gè)功能,一是面向?qū)ο髾C(jī)制中的對(duì)象,另一個(gè)是數(shù)據(jù)抽象中的集合——其他語(yǔ)言中稱為鍵值數(shù)組,哈希表或字典。

其實(shí)在其它語(yǔ)言中的對(duì)象都不耦合集合的功能,比如python中有字典,php中有鍵值數(shù)組,好在es2015中引入新的map類型,來(lái)把這個(gè)功能解耦出去,但我相信很多人都習(xí)慣這么使用了o(╯□╰)o

而本文所說(shuō)的對(duì)象不包括后面的這一部分功能,特指js中面向?qū)ο髾C(jī)制中的對(duì)象。

原型與原型鏈

js語(yǔ)言是基于原型的語(yǔ)言,在js中幾乎一切都是對(duì)象,每個(gè)對(duì)象都有原型,而原型也是一個(gè)對(duì)象,也有自己的原型,從而形成原型鏈。

當(dāng)試圖訪問(wèn)一個(gè)對(duì)象的屬性時(shí),它不僅僅在該對(duì)象上搜尋,還會(huì)搜尋該對(duì)象的原型,以及該對(duì)象的原型的原型,依此層層向上搜索,直到找到一個(gè)名字匹配的屬性或到達(dá)原型鏈的末尾。

js中提到原型可能有不同的意思,我們得先區(qū)分清楚,需要清除到底是哪個(gè)原型:

  1. js是基于原型的語(yǔ)言中的prototype
  2. 每個(gè)對(duì)象都有自己的原型中的prototype
  3. 每個(gè)函數(shù)都有一個(gè)原型屬性中的prototype

第一點(diǎn)中的prototype是一個(gè)概念,一種機(jī)制,而不是具體的什么東西。

第二點(diǎn)中的prototype是說(shuō)每個(gè)對(duì)象都有自己的原型對(duì)象(父對(duì)象),下面我們用[[Prototype]]來(lái)指代這個(gè)原型對(duì)象。

第三點(diǎn)和第二點(diǎn)很容易混淆,每個(gè)函數(shù)都有一個(gè)原型屬性,我們用prototype來(lái)指代。

es5帶來(lái)了查看對(duì)象原型的方法——Object.getPrototypeOf,該方法返回指定對(duì)象的原型(也就是該對(duì)象內(nèi)部屬性[[Prototype]]的值)。

console.log(Object.getPrototypeOf({}))
>>> Object.prototype

es6帶來(lái)了另一種查看對(duì)象原型的方法——Object.prototype.__proto__,一個(gè)對(duì)象的__proto__ 屬性和自己的內(nèi)部屬性[[Prototype]]指向一個(gè)相同的值 (通常稱這個(gè)值為原型),原型的值可以是一個(gè)對(duì)象值也可以是null(比如說(shuō)Object.prototype.__proto__的值就是null)。

({}).__proto__
>>> Object.prototype

下面舉個(gè)例子來(lái)說(shuō)說(shuō)原型與原型鏈,以及訪問(wèn)對(duì)象屬性的時(shí)候會(huì)發(fā)生的行為:

// a ---> b 代表b是a的原型

修改原型的方式

在js中創(chuàng)建和修改原型的方法有很多,下面一一列舉出來(lái)。

在下面的例子中我們將對(duì)象a的[[Prototype]]指向b。

// a ---> b

使用普通語(yǔ)法創(chuàng)建對(duì)象

這是最容易被大家忽略的方法,在js中你是繞不過(guò)原型的,不經(jīng)意間就創(chuàng)建了原型

var o = {a: 1};
// o ---> Object.prototype ---> null

var a = [];
// a ---> Array.prototype ---> Object.prototype ---> null

function f(){}
// f ---> Function.prototype ---> Object.prototype ---> null

這種方法無(wú)法讓a的[[Prototype]]指向b。

使用構(gòu)造器創(chuàng)建對(duì)象

構(gòu)造函數(shù)就是一個(gè)普通的函數(shù),只不過(guò)這次不是直接調(diào)用函數(shù),而是在函數(shù)前加上new關(guān)鍵字。

每個(gè)函數(shù)都有一個(gè)prototype屬性,通過(guò)new關(guān)鍵字新建的對(duì)象的原型會(huì)指向構(gòu)造函數(shù)的prototype屬性,所以我們可以修改構(gòu)造函數(shù)的prototype屬性從而達(dá)到操作對(duì)象原型的目的。

為了讓b繼承a,需要有一個(gè)構(gòu)造函數(shù)A

var b = {};
function A() {};

A.prototype = b;

var a = new A();

Object.getPrototypeOf(a) === b;
// true
// a ---> A.prototype === b

使用 Object.create 創(chuàng)建對(duì)象

ES5帶來(lái)了Object.create接口,可以讓我們直接設(shè)置一個(gè)對(duì)象原型

var b = {};
var a = Object.create(b);

Object.getPrototypeOf(a) === b;
// true
// a ---> b

Object.setPrototypeOf

ES6帶來(lái)了另一個(gè)接口,可以繞過(guò)創(chuàng)建對(duì)象的過(guò)程,直接操作原型

var a = {};
var b = {};

Object.setPrototypeOf(a, b);
Object.getPrototypeOf(a) === b;
// true
// a ---> b

proto

ES6還帶來(lái)了一個(gè)屬性,通過(guò)這個(gè)屬性也可以直接操作原型

var a = {};
var b = {};

a.__proto__ = b;
Object.getPrototypeOf(a) === b;
// true
// a ---> b

注意這個(gè)屬性在ES6規(guī)范的附錄中,也就意味著不是所有的環(huán)境都會(huì)有這個(gè)屬性。

使用 class 關(guān)鍵字

ES6引入了以class語(yǔ)法糖,通過(guò)extends關(guān)鍵字我們也可以實(shí)現(xiàn)繼承,但是無(wú)法直接操作對(duì)象的原型,而是要借助“類”,其實(shí)就是構(gòu)造函數(shù)和函數(shù)的prototype屬性。

class B {}

class A extends B {}

var a = new A();

Object.getPrototypeOf(a) === A.prototype;
// true
// a ---> A.prototype === B的實(shí)例

總結(jié)

不知道看完文章你理解原型了嗎?如果還有疑惑建議你閱讀下面的文章。

參考文章

以上內(nèi)容是否對(duì)您有幫助:
在線筆記
App下載
App下載

掃描二維碼

下載編程獅App

公眾號(hào)
微信公眾號(hào)

編程獅公眾號(hào)