JavaScript原型詳解

gold.xitu.io發表於2016-10-10

1,前言

下面是2008年Github建立以來,各種程式語言的排名情況

JavaScript原型詳解
排名

其中JavaScript自2013年之後就盤踞第一名,成為github上被使用最多的語言,早期,JS的使用還主要集中於瀏覽器中,但是隨著node.js進軍伺服器開發和React Native逐漸向移動端滲透,一個屬於JS的全棧時代就要來臨了。而且JS界還流傳一句名言:“所有能用JS開發的應用程式,最終都會用JS來開發”。我就問你怕不怕?

好了,說了這麼多,我並不是想說JS為世界上最好的語言(顯然PHP才是,對吧?←_←),也不是覺得JS會替代誰,我只是覺得,JavaScript將會是一個大家(不止web端)都應該瞭解和學習的語言工具。

2,面對物件(OOP)

2.1 實現思路

面對物件是大家都很熟悉的程式設計思想,是對真實世界的抽象,目前主要OOP語言用來實現面對物件的基礎是類,通過類的封裝,繼承來對映真實世界。包括Java,C#,甚至是python等都通過類的設計來實現面對物件。但是細想起來也會覺得有問題,因為真實世界其實沒有類這種概念,只有一個個不同的物件,真實世界中,繼承關係發生在物件和物件之間,而不是類。就比如孩子是物件,父母也是物件,孩子(物件)繼承自父母(物件)

JS也是面對物件的程式語言,只不過它實現面對物件的思路是基於原型(prototype),而不是類。這種思路也叫物件關聯(Object Link Other Object),即在物件上直接對映那種真實世界的關係(如繼承)。

2.2 原型概念

相關的概念其實我研究了好幾天,除開原型概念本身,與之聯絡的物件的產生,建構函式,proto,prototype的區別,為什麼物件沒有prototype這個指向原型的屬性,而是使用proto來指向原型?

好,我們先來談談原型這個概念。JS中一切皆物件,而每個物件都有一個原型(Object除外),這個原型,大概就像Java中的父類,所以,基本上你可以認為原型就是這個物件的父物件,即每一個物件(Object除外)內部都儲存了它自己的父物件,這個父物件就是原型。一般建立的物件如果沒有特別指定原型,那麼它的原型就是Object(這就很類似Java中所有的類預設繼承自Object類)。

2.3 物件建立

在JS中,物件建立的方法有很多種,最常見的如下:

//第一種,手動建立
var a={'name':'lala'};   

//第二種,建構函式
function A(){
    this.name='lala';
}
var a=new A();

//第三種,class (ES6標準寫法)

class A{
    constructor(){
        //super();此處沒有使用extends顯式繼承,不可使用super()
        this.name='lala';
    }
}
var a=new A()
//其實後面兩種方法本質上是一種寫法複製程式碼

這三種寫法建立的物件的原型(父物件)都是Object,需要提到的是,ES6通過引入class ,extends等關鍵字,以一種語法糖的形式把建構函式包裝成類的概念,更便於大家理解。是希望開發者不再花精力去關注原型以及原型鏈,也充分說明原型的設計意圖和類是一樣的。

2.3 檢視物件原型

當物件被建立之後,檢視它們的原型的方法不止一種,以前一般使用物件的proto屬性,ES6推出後,推薦用Object.getPrototypeOf()方法來獲取物件的原型

function A(){
    this.name='lala';
}
var a=new A();
console.log(a.__proto__)  
//輸出:Object {}

//推薦使用這種方式獲取物件的原型
console.log(Object.getPrototypeOf(a))  
//輸出:Object {}複製程式碼

無論物件是如何建立的,預設原型都是Object,在這裡需要提及的比較特殊的一點就是,通過建構函式來建立物件,函式A本身也是一個物件,而A有兩個指向表示原型的屬性,分別是proto和prototype,而且兩個屬性並不相同

function A(){
    this.name='lala';
}
var a=new A();
console.log(A.prototype)  
//輸出:Object {}

console.log(A.__proto__)  
//輸出:function () {}
console.log(Object.getPrototypeOf(A))
//輸出:function () {}複製程式碼

函式的的prototype屬性只有在當作建構函式建立的時候,把自身的prototype屬性值賦給物件的原型。而實際上,作為函式本身,它的原型應該是function物件,然後function物件的原型才是Object。

總之,建議使用ES6推薦的檢視原型和設定原型的方法。

2.4 原型的用法

其實原型和類的繼承的用法是一致的:當你想用某個物件的屬性時,將當前物件的原型指向該物件,你就擁有了該物件的使用權了。

function A(){
    this.name='world ';
}
function B(){
    this.bb="hello"
    }
var a=new A();
var b=new B();

Object.setPrototypeOf(a,b);
//將b設定為a的原型,此處有一個問題,即a的constructor也指向了B建構函式,可能需要糾正
a.constructor=A;
console.log(a.bb)
//輸出 hello複製程式碼

(增補)

如果使用ES6來做的話則簡單許多,甚至不涉及到prototype這個屬性

class B{
     constructor(){

        this.bb='hello'
     }
}
class A  extends B{
     constructor(){
        super()
        this.name='world'
     }
}

var a=new A();
console.log(a.bb+" "+a.name);
//輸出hello world


console.log(typeof(A))
//輸出  "function"複製程式碼

怎麼樣?是不是已經完全看不到原型的影子了?活脫脫就是類繼承,但是你也看得到實際上類A 的型別是function,所以說,本質上class在JS中是一種語法糖,JS繼承的本質依然是原型,不過,ES6引入class,extends 來掩蓋原型的概念也是一個很友好的舉動,對於長期學習那些類繼承為基礎的面對物件程式語言的程式設計師而言。

我的建議是,儘可能理解原型,儘可能用class這種語法糖。


2.5 原型鏈

這個概念其實也變得比較簡單,可以類比類的繼承鏈條,即每個物件的原型往上追溯,一直到Object為止,這組成了一個鏈條,將其中的物件串聯起來,當查詢當前物件的屬性時,如果沒找到,就會沿著這個鏈條去查詢,一直到Object,如果還沒發現,就會報undefined。那麼也就意味著你的原型鏈不能太長,否則會出現效率問題。

3,總結

  • 對於原型概念的理解
    • 類比類的繼承,物件的原型可以理解為物件的父物件
  • 原型的運用
    • 儘可能使用ES6的標準,使用class,extends,Object.getPrototype(),Object.setPrototype()等等
  • 需要注意的點
    • 原型繼承鏈條不要太長
    • 指定原型時,注意constructor也會改變。

4,後記(增補)

還有人覺得我的分析很抽象,所以,我再總結一下,如果要一句話理解JS中的原型是什麼,那就是,物件的原型就指的物件的父物件。每個物件都有父物件,父物件本身也有父物件(爺物件?)。而原型鏈呢,很像過去家譜的概念,可以從你往上追溯你父親,到爺爺,到太爺爺一直到頭,這就形成了一個鏈條,如果其中每個人都比作一個物件,那麼這個鏈條就是原型鏈。

相關文章