JavaScript中的面向對象(object-oriented)編程

本文原發於個人我的博客,經屢次修改後發到sf上。本文仍在不斷修改中,最新版請訪問我的博客。javascript


最近工做一直在用nodejs作開發,有了nodejs,前端、後端、腳本全均可以用javascript搞定,非常方便。可是javascript的不少語法,好比對象,就和咱們經常使用的面向對象的編程語言不一樣;看某個javascript開源項目,也常常會看到使用this關鍵字,而這個this關鍵字在javascript中因上下文不一樣而意義不一樣;還有讓人奇怪的原型鏈。這些零零碎碎的東西加起來就很容易讓人不知所措,因此,有必要對javascript這門語言進行一下深刻了解。前端

我這篇文章主要想說說如何在javascript中進行面向對象的編程,同時會講一些javascript這門語言在設計之初的理念。下面讓咱們開始吧。java

首先強調一下,咱們如今普遍使用的javascript都是遵循了ECMAScript 5.1標準的,正在制定中的版本爲6.0,這個版本變化很大,增長了不少新的語法與函數,你們能夠去Mozilla Developer Network上查看。node

設計理念

javascript1.0 最初是由網景公司的Brendan Eich在1995年5月花了十天搞出來的,Eich的目標是設計出一種即輕量又強大的語言,因此Eich充分借鑑了其餘編程語言的特性,好比Java的語法(syntax)、Scheme的函數(function)、Self的原型繼承(prototypal inheritance)、Perl的正則表達式等。react

其中值得一提的是,爲何繼承借鑑了Self語言的原型機制而不是Java的類機制?首先咱們要知道:git

  • Self的原型機制是靠運行時的語義
  • Java的類機制是靠編譯時的類語法

Javascript1.0的功能相對簡單,爲了在從此不斷豐富javascript自己功能的同時保持舊代碼的兼容性,javascript經過改變運行時的支持來增長新功能,而不是經過修改javascript的語法,這就保證了舊代碼的兼容性。這也就是javascript選擇基於運行時的原型機制的緣由。github

wikipedia這樣描述到:JavaScript is classified as a prototype-based scripting language with dynamic typing and first-class functions。這些特性使得javascript是一種多範式解釋性編程語言,支持面向對象命令式(imperative)函數式(functional)編程風格。正則表達式

對象

在javascript中,除了數字、字符串、布爾值(true/false)、undefined這幾個簡單類型外,其餘的都是對象。編程

數字、字符串、布爾值這些簡單類型都是不可變量,對象是可變的鍵值對的集合(mutable keyed conllections),對象包括數組Array、正則表達式RegExp、函數Function,固然對象Object也是對象。後端

對象在javascript中說白了就是一系列的鍵值對。鍵能夠是任何字符串,包括空串;值能夠是除了undefined之外的任何值。在javascript中是沒有類的概念(class-free)的,可是它有一個原型鏈(prototype linkage)。javascript對象經過這個鏈來實現繼承關係。

javascript中有一些預約義對象,像是ObjectFunctionDateNumberStringArray等。

字面量(literal)

javascript中的每種類型的對象均可以採用字面量(literal)的方式建立。

對於Object對象,可使用對象字面量(Object literal)來建立,例如:

var empty_object = {};//建立了一個空對象
//建立了一個有兩個屬性的對象
var stooge = {
    "first-name": "Jerome",
    "last-name": "Howard"
};

固然,也能夠用new Object()Object.create()的方式來建立對象。

對於FunctionArray對象都有其相應的字面量形式,後面會講到,這裏再也不贅述。

原型鏈(prototype linkage)

javascript中的每一個對象都隱式含有一個[[prototype]]屬性,這是ECMAScript中的記法,目前各大瀏覽器廠商在實現本身的javascript解釋器時,採用的記法是__proto__,也就是說每一個對象都隱式包含一個__proto__屬性。舉個例子:

var foo = {
    x: 10,
    y: 20
};

foo這個對象在內存中的存儲結構大體是這樣的:

basic-object

當有多個對象時,經過__proto__屬性就可以造成一條原型鏈。看下面的例子:

var a = {
    x: 10,
    calculate: function (z) {
        return this.x + this.y + z;
    }
};
var b = {
    y: 20,
    __proto__: a
};
var c = {
    y: 30,
    __proto__: a
};
// call the inherited method
b.calculate(30); // 60
c.calculate(40); // 80

上面的代碼在聲明對象b、c時,指明瞭它們的原型爲對象a(a的原型默認指向Object.prototye,Object.prototype這個對象的原型指向null),這幾個對象在內存中的結構大體是這樣的:

prototype-chain

這裏須要說明一點,咱們若是想在聲明對象時指定它的原型,通常採用Object.create()方法,這樣效率更高。
除了咱們這裏說的__proto__屬性,相信你們日常更常見的是prototype屬性。好比,Date對象中沒有加幾天的函數,那麼咱們能夠這麼作:

Date.prototype.addDays = function(n) {
    this.setDate(this.getDate() + n);
}

那麼之後全部的Date對象都擁有addDays方法了(後面講解繼承是會解釋爲何)。那麼__proto__屬性與prototype屬性有什麼區別呢?

javascript的每一個對象都有__proto__屬性,可是隻有函數對象prototype屬性。

那麼在函數對象中, 這兩個屬性的有什麼區別呢?

  1. __proto__表示該函數對象的原型
  2. prototype表示使用new來執行該函數時(這種函數通常成爲構造函數,後面會講解),新建立的對象的原型。例如:

    var d = new Date();
    d.__proto__ === Date.prototype; //這裏爲true

看到這裏,但願你們可以理解這兩個屬性的區別了。

在javascript,原型和函數是最重要的兩個概念,上面說完了原型,下面說說函數對象。

函數對象Function

首先,函數在javascript中無非也是個對象,能夠做爲value賦值給某個變量,惟一不一樣的是函數可以被執行。

使用對象字面量方式建立的對象的__proto__屬性指向Object.prototype(Object.prototype__proto__屬性指向null);使用函數字面量建立的對象的__proto__屬性指向Function.prototype(Function.prototype對象的__proto__屬性指向Object.prototype)。

函數對象除了__proto__這個隱式屬性外,還有兩個隱式的屬性:

  1. 函數的上下文(function’s context)
  2. 實現函數的代碼(the code that implements the function’s behavior)

和對象字面量同樣,咱們可使用函數字面量(function literal)來建立函數。相似於下面的方式:

//使用字面量方式建立一個函數,並賦值給add變量
var add = function (a, b) { 
    return a + b;
};

一個函數字面量有四個部分:

  1. function關鍵字,必選項。
  2. 函數名,可選項。上面的示例中就省略了函數名。
  3. 由圓括號括起來的一系列參數,必選項。
  4. 由花括號括起來的一系列語句,必選項。該函數執行時將會執行這些語句。

函數調用與this

一個函數在被調用時,除了聲明的參數外,還會隱式傳遞兩個額外的參數:thisarguments

this在OOP中很重要,this的值隨着調用方式的不一樣而不一樣。javascript中共有四種調用方式:

  1. method invocation pattern。當函數做爲某對象一個屬性調用時,this指向這個對象。this賦值過程發生在函數調用時(也就是運行時),這叫作late binding
  2. function invocation pattern。當函數不做爲屬性調用時,this指向全局對象,這是個設計上的錯誤,正確的話,內部函數的this應該指向外部函數。能夠經過在函數中定義一個變量來解決這個問題。

    var add = function(a, b) {return a+b;}
    var obj = {
        value: 3,
        double: function() {
            var self = this;//把this賦值給了self
            this.value = add(self.value, self.value);
        }
    }
    obj.double(); //obj.value如今爲6
  3. construct invocation pattern。javascript是一門原型繼承語言,這也就意味着對象能夠直接從其餘對象中繼承屬性,沒有類的概念。這和java中的繼承不同。可是javascript提供了一種相似與java建立對象的語法。當一個函數用new來調用時,this指向新建立的對象。這時的函數一般稱爲構造函數。
  4. apply invocation pattern。使用函數對象的apply方法來執行時,this指向apply的第一個參數。

除了this外,函數在調用是額外傳入的另外一個參數是arguments。它是函數內部的一個變量,包含函數調用處的全部參數,甚至包含函數定義時沒有的參數。

var sum = function () { 
    var i, sum = 0;
    for (i = 0; i < arguments.length; i += 1) {
        sum += arguments[i];
    }
    return sum;
};
sum(4, 8, 15, 16, 23, 42); // 108

須要注意的是,這裏的arguments不是一個數組,它只是一個有length屬性的類數組對象(Array-like),它並不擁有數組的其餘方法。

關於對象,最後說一下數組,javascript中的數組和日常編程中的數組不大同樣。

數組對象Array

數組是一種在內存中線性分配的數據結構,經過下標計算出元素偏移量,從而取出元素。數組應該是一個快速存取的數據結構,可是在javascript中,數組不具有這種特性。

數組在javascript中一個具備傳統數組特性的對象,這種對象可以把數組下標轉爲字符串,而後把這個字符串做爲對象的key,最後對取出對應該key的value(這又一次說明了對象在javascript中就是一系列鍵值對)。

雖然javascript中的數組沒有傳統語言中的數組那麼快,可是因爲javascript是弱類型的語言,因此javascript中的數組能夠存聽任何值。此外Array有不少實用的方法,你們能夠去MDN Array查看。

javascript也爲數組提供了很方便的字面量(Array Literal)定義方式:

var arr = [1,2,3]

經過數組字面量建立的數組對象的__proto__指向Array.prototype。

繼承Inheritance

在Java中,對象是某個類的實例,一個類能夠從另外一個類中繼承。可是在基於原型鏈的javascript中,對象能夠直接從另外一個對象建立。

在上面講解對象時,咱們知道了在建立一個對象時,該對象會自動賦予一個__proto__屬性,使用各類類型的字面量(Literal)時,javascript解釋器自動爲__proto__進行了賦值。當咱們在javascript執行使用new操做符建立對象時,javascript解釋器在構造函數時,同時會執行相似於下面的語句

this.__proto__ = {constructor: this};

新建立的對象都會有一個__proto__屬性,這個屬性有一個constructor屬性,而且這個屬性指向這個新對象。舉個例子:

var d = new Date()
d.__proto__.constructor === Date //這裏爲true

若是new不是一個操做符,而是一個函數的話,它的實現相似於下面的代碼:

Function.prototype.new =  function () {
    // Create a new object that inherits from the constructor's prototype.
    var that = Object.create(this.prototype);
    // Invoke the constructor, binding –this- to the new object.
    var other = this.apply(that, arguments);
    // If its return value isn't an object, substitute the new object.
    return (typeof other === 'object' && other) || that;
};

以前也說了,基於原型的繼承機制是根據運行時的語義決定的,這就給咱們提供了很大的便利。好比,咱們想爲全部的Array添加一個map函數,那麼咱們能夠這麼作:

Array.prototype.map = function(f) {
    var newArr = [];
    for(i=0; i<this.length; i++) {
        newArr.push(f(this[i]));
    }
    return newArr;
}

由於全部的數組對象的__proto__都指向Array.prototype對象,因此咱們爲這個對象增長方法,那麼全部的數組對象就都擁有了這個方法。

javascript解釋器會順着原型鏈查看某個方法或屬性。若是想查看某對象的是否有某個屬性,可使用Object.prototype.hasOwnProperty方法。

總結

經過上面屢次講解,但願你們對對象在javascript中就是一系列的鍵值對原型函數這三個概念有更加深入的認識,使用javascript來寫前端、後端與腳本。在React.js 2015大會上,Facebook公佈了即將開源的React Native,這意味着從此咱們能夠用javascript來寫IOS、Android的原生應用了,這可真是learn-once, write-anywhere。相信隨着ECMAScript 6的發佈,javascript這門語言還會有一系列翻天覆地的變化,Stay Tuned。:-)

參考

相關文章
相關標籤/搜索