什么是原型?


原型是順應人類自然思維的產物。中文中有個成語叫做“照貓畫虎”,這里的貓看起來就是虎的原型,所 以,由此我們可以看出,用原型來描述對象的方法可以說是古已有之。

在不同的編程語言中,設計者也利用各種不同的語言特性來抽象 描述對象。

 

最為成功的流派是使用“類”的方式來描述對象,這誕生了諸如 C++、Java等流行的編程語言。這個流派叫 做基於類的編程語言。

還有一種就是基於原型的編程語言,它們利用原型來描述對象。我們的JavaScript就是其中代表。

 

“基於類”的編程提倡使用一個關注分類和類之間關系開發模型。在這類語言中,總是先有類,再從類去實 例化一個對象。

類與類之間又可能會形成繼承、組合等關系。類又往往與語言的類型系統整合,形成一定編譯時的能力。

 

與此相對,“基於原型”的編程看起來更為提倡程序員去關注一系列對象實例的行為,

而后才去關心如何將 這些對象,划分到最近的使用方式相似的原型對象,而不是將它們分成類。

 

基於原型的面向對象系統通過“復制”的方式來創建新對象。一些語言的實現中,還允許復制一個空對象。 這實際上就是創建一個全新的對象。

基於原型和基於類都能夠滿足基本的復用和抽象需求,但是適用的場景不太相同。

這就像專業人士可能喜歡在看到老虎的時候,喜歡用貓科豹屬豹亞種來描述它,但是對一些不那么正式的場 合,“大貓”可能更為接近直觀的感受一些(插播一個冷知識:比起老虎來,美洲獅在歷史上相當長時間都 被划分為貓科貓屬,所以性格也跟貓更相似,比較親人)。

我們的JavaScript 並非第一個使用原型的語言,在它之前,self、kevo等語言已經開始使用原型來描述對象 了。

事實上,Brendan更是曾透露過,他最初的構想是一個擁有基於原型的面向對象能力的scheme語言(但是函數式的部分是另外的故事,這篇文章里,我暫時不做詳細講述)

在JavaScript之前,原型系統就更多與高動態性語言配合,並且多數基於原型的語言提倡運行時的原型修改,我想,這應該是Brendan選擇原型系統很重要的理由。

原型系統的“復制操作”有兩種實現思路:

一個是並不真的去復制一個原型對象,而是使得新對象持有一個原型的引用;

另一個是切實地復制對象,從此兩個對象再無關聯。

歷史上的基於原型語言因此產生了兩個流派,顯然,JavaScript顯然選擇了前一種方式。

JavaScript的原型

如果我們拋開JavaScript用於模擬Java類的復雜語法設施(如new、Function Object、函數的prototype屬性 等),

原型系統可以說相當簡單,我可以用兩條概括:

1.如果所有對象都有私有字段[[prototype]],就是對象的原型;

2.讀一個屬性,如果對象本身沒有,則會繼續訪問對象的原型,直到原型為空或者找到為止。

這個模型在ES的各個歷史版本中並沒有很大改變,但從 ES6 以來,JavaScript提供了一系列內置函數,以便更為直接地訪問操縱原型。

三個方法分別為:

Object.create 根據指定的原型創建新對象,原型可以是null;

Object.getPrototypeOf 獲得一個對象的原型;

Object.setPrototypeOf 設置一個對象的原型

利用這三個方法,我們可以完全拋開類的思維,利用原型來實現抽象和復用。

我用下面的代碼展示了用原型 來抽象貓和虎的例子。

var cat = {

say(){

console.log("meow~");

},

jump(){

console.log("jump");

}

}

var tiger = Object.create(

cat,

{

say:{

writable:true, configurable:true, enumerable:true,

value:function(){

console.log("roar!");

} } })

var anotherCat = Object.create(cat);

anotherCat.say();

var anotherTiger = Object.create(tiger);

anotherTiger.say();

這段代碼創建了一個“貓”對象,又根據貓做了一些修改創建了虎,

之后我們完全可以用Object.create來創 建另外的貓和虎對象,

我們可以通過“原始貓對象”和“原始虎對象”來控制所有貓和虎的行為。

但是,在更早的版本中,程序員只能通過Java風格的類接口來操縱原型運行時,可以說非常別扭。

 

考慮到new和prototype屬性等基礎設施今天仍然有效

,而且被很多代碼使用,學習這些知識也有助於我們 理解運行時的原型工作原理,

下面我們試着回到過去,追溯一下早年的JavaScript中的原型和類。

 

早期版本中的類與原型

在早期版本的JavaScript中,“類”的定義是一個私有屬性 [[class]],

語言標准為內置類型諸如Number、String、Date等指定了[[class]]屬性,以表示它們的類。

語言使用者唯一可以訪問[[class]]屬性的方式是 Object.prototype.toString。

以下代碼展示了所有具有內置class屬性的對象:

var o = new Object;

var n = new Number;

var s = new String;

var b = new Boolean;

var d = new Date;

var arg = function(){ return arguments }();

var r = new RegExp;

var f = new Function;

var arr = new Array;

var e = new Error;

console.log([o, n, s, b, d, arg, r, f, arr, e].map(v => Object.prototype.toString.call(v)));

因此,在ES3和之前的版本,JS中類的概念是相當弱的,它僅僅是運行時的一個字符串屬性。

在ES5開始,[[class]] 私有屬性被 Symbol.toStringTag 代替,

Object.prototype.toString 的意義從命名上不 再跟 class 相關。

我們甚至可以自定義 Object.prototype.toString 的行為,

以下代碼展示了使用 Symbol.toStringTag來自定義 Object.prototype.toString 的行為:

var o = { [Symbol.toStringTag]: "MyObject" } console.log(o + "");

這里創建了一個新對象,並且給它唯一的一個屬性 Symbol.toStringTag,

我們用字符串加法觸發了 Object.prototype.toString的調用,

發現這個屬性最終對Object.prototype.toString 的結果產生了影響。

考慮到JavaScript語法中跟Java相似的部分,我們對類的討論不能用“new運算是針對構造器對象,而 不是類”來試圖回避。

我們仍然要把new理解成JavaScript面向對象的一部分,下面我就來講一下new操作具體做了哪些事 情。

1.new 運算接受一個構造器和一組調用參數,實際上做了幾件事:

2.以構造器的 prototype 屬性(注意與私有字段[[prototype]]的區分)為原型,創建新對象;

3.將 this 和調用參數傳給構造器,執行;

 

如果構造器返回的是對象,則返回,否則返回第一步創建的對象。

new 這樣的行為,試圖讓函數對象在語法上跟類變得相似,

但是,它客觀上提供了兩種方式,一是在構造 器中添加屬性,二是在構造器的 prototype 屬性上添加屬性。

下面代碼展示了用構造器模擬類的兩種方法:

 

function c1(){

this.p1 = 1;

this.p2 = function(){

console.log(this.p1);

}}

var o1 = new c1;

o1.p2();

第一種方法是直接在構造器中修改this,給this添加屬性

 

function c2(){ }

c2.prototype.p1 = 1;

c2.prototype.p2 = function(){ console.log(this.p1); }

var o2 = new c2; o2.p2();

第二種方法是修改構造器的prototype屬性指向的對象,它是從這個構造器構造出來的所有對象的原型。

 

沒有Object.create、Object.setPrototypeOf 的早期版本中,

new 運算是唯一一個可以指定[[prototype]]的方 法(當時的mozilla提供了私有屬性__proto__,但是多數環境並不支持),

所以,當時已經有人試圖用它來 代替后來的 Object.create,

我們甚至可以用它來實現一個Object.create的不完整的polyfill,見以下代碼:

 

Object.create = function(prototype){

var cls = function(){}

cls.prototype = prototype; return new cls;

}

這段代碼創建了一個空函數作為類,並把傳入的原型掛在了它的prototype,最后創建了一個它的實例,根 據new的行為,這將產生一個以傳入的第一個參數為原型的對象。

這個函數無法做到與原生的Object.create一致,一個是不支持第二個參數,另一個是不支持null作為原型, 所以放到今天意義已經不大了。

 

ES6 中的類

在ES6中加入了新特性class,在任何場景,我都推薦使用ES6的語法來定義類,而令function回歸原本的函數語義。下面我們就來看一下 ES6中的類。

ES6中引入了class關鍵字,並且在標准中刪除了所有[[class]]相關的私有屬性描述,

類的概念正式從屬性升級成語言的基礎設施,從此,基於類的編程方式成為了JavaScript的官方編程范式。

 

class Rectangle {

constructor(height, width) {

this.height = height; t

his.width = width;

}

// Getter

Getter get area() {

return this.calcArea();

}

// Method

calcArea() {

return this.height * this.width;

} }

在現有的類語法中,getter/setter和method是兼容性最好的。

我們通過get/set關鍵字來創建getter,通過括號和大括號來創建方法,數據型成員最好寫在構造器里面。

類的寫法實際上也是由原型運行時來承載的,邏輯上JavaScript認為每個類是有共同原型的一組對象,類中定義的方法和屬性則會被寫在原型對象之上。

此外,最重要的是,類提供了繼承能力。我們來看一下下面的代碼。

 

class Animal {

constructor(name) {

this.name = name;

}

speak() {

console.log(this.name + ' makes a noise.');

} }

class Dog extends Animal {

constructor(name) {

super(name); // call the super class constructor and pass in the name parameter

}

speak() {

console.log(this.name + ' barks.');

} }

 

let d = new Dog('Mitzie');

d.speak(); // Mitzie barks.

以上代碼創造了Animal類,並且通過extends關鍵字讓Dog繼承了它,展示了最終調用子類的speak方法獲取了父類的name。

比起早期的原型模擬方式,使用extends關鍵字自動設置了constructor,並且會自動調用父類的構造函數, 這是一種更少坑的設計。

所以當我們使用類的思想來設計代碼時,應該盡量使用class來聲明類,而不是用舊語法,拿函數來模擬對象。

一些激進的觀點認為,class關鍵字和箭頭運算符可以完全替代舊的function關鍵字,它更明確地區分了定義函數和定義類兩種意圖,我認為這是有一定道理的。

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM