美思 現代 [JavaScript] 程式設計教學:建立物件 (object)

Facebook Twitter LinkedIn LINE Skype EverNote GMail Yahoo Email

前言

JavaScript 的物件系統是以原型 (prototype) 為基礎,相較起來,大部分主流的語言的物件是以類別 (class) 為基礎,兩者有一些差異。JavaScript 建立物件的方法較為靈活,會依需求而有不同的建立方式。較常見的有以下三種:

  • 物件實字 (object literal)
  • 以函式 (function) 為建構子 (constructor)
  • (新) 使用 class 建立類別 (class)

前兩種在 ES5 時代就出現了,class 則是 ES6 後才出現的語法糖。如果不是要維護舊有程式碼,應優先使用 class;但許多現存的程式仍未轉換到新的語法,所以也要能閱讀用原有的手法撰寫的程式碼。

在本文中,我們以二維坐標點為例,展示不同的做法,供讀者參考。

使用物件實字 (Object Literal) 建立物件

物件實字 {} 就像是一個雜湊表 (hash table),可以增加純量 (value) 或是函式物件 (function object) 或內嵌的物件實字等。透過物件實字來建立物件是最簡單的方式,可參考以下程式碼建立 Point 物件:

// Object with fields per object.
let Point = {
    x: 0,
    y: 0
};

// Shared function among objects.
Object.setPrototypeOf(Point, {
    distanceTo: function(p) {
        let dx = this.x - p.x;
        let dy = this.y - p.y;

        return Math.sqrt(dx * dx + dy * dy);
    }
});

雖然我們會在心中把 Point 當成類別來用,嚴格上來說,Point 本身也是一個物件,該物件有兩個公開屬性 xy 以及一個公開方法 distanceTo。此處的 this 是一個特殊的變數,會指向物件實字本身。

使用 Point 物件的方式如下:

let p = copy(Point);

let q = copy(Point);
q.x = 3;
q.y = 4;

assert(p.distanceTo(q) === 5, "The distance should be 5");

// Deep copy.
function copy (src) {
    let dest = {};

    // Inherit prototype from `src`.
    Object.setPrototypeOf(dest, Object.getPrototypeOf(src));

    // Copy properties from `src`.
    for (let prop in src) {
        if (src.hasOwnProperty(prop)) {
            dest[prop] = src[prop];
        }
    }

    return dest;
}

// Home-made `assert`.
function assert(cond, msg) {
    if (!(cond)) {
        throw msg;
    }
}

物件實字所有的屬性和方法都是公開的,而直接使用公開屬性在軟體工程上是不好的習慣,所以我們較少直接從物件實字來建立物件,而會用下一節的方法。

以函式 (Function) 為建構子 (Constructor)

在 ES5 的時代,使用函式做為物件的建構子是主流的手法。延續我們的例子,以函式建構 Point 的範例如下:

let Point = function (x, y) {
    // The private fields
    var _x;
    var _y;

    // Property `x` of the object.
    Object.defineProperty(this, "x", {
        get: function () {
            return _x;
        },
        set: function (value) {
            _x = value;
        }
    });

    // Property `y` of the object.
    Object.defineProperty(this, "y", {
        get: function () {
            return _y;
        },
        set: function (value) {
            _y = value;
        }
    });

    // Init the fields of the object.
    this.x = x;
    this.y = y;

    return this;
};

// The public method `distanceTo`.
Point.prototype.distanceTo = function (p) {
    return Math.sqrt(Math.pow(this.x - p.x, 2) + Math.pow(this.y - p.y, 2));
};

Point 物件中,_x_y 是私有的,利用閉包 (closure) 包在 Point 物件中。我們另外定義 xy 兩個公開屬性去存取相對應的私有屬性,最後回傳 this 本身。在 JavaScript 中,函式會用來控制可視域 (scope)。

Point 中,不論將 xy 設為 field 或 property 都沒有差別,但在工程的觀察上,使用 property 會優於直接使用 field,因 property 可控制包覆的私有 field。

我們將 distanceTo 拉到 Point 的原型鍵而不直接寫在建構函式內,這是為了節約記憶體。許多教材會將 distanceTo 寫在建構函式內,這在語法上比較好懂,但每建立一個物件時就會重覆建立一個函式,記憶體耗費較多。

使用方式如下:

let p = new Point(0, 0);
console.log(`(${p.x}, ${p.y})`);

p.x = 3;
p.y = 4;
console.log(`(${p.x}, ${p.y})`);

console.log(p.distanceTo(new Point(0, 0)));

對於建構函式,我們會使用 new 來製作新的物件。

(新) 使用 class 保留字建立類別 (Class)

class 是 ES6 後引入的新語法,由於 JavaScript 在本質上仍採用以原型 (prototype) 為基礎的物件,所以 class 算是一種語法糖。

我們用 class 建立 Point 物件:

let Point = (function () {
    let fields = new WeakMap();

    return class Point {
        constructor (x, y) {
            fields.set(this, {});

            this.x = x;
            this.y = y;
        }

        // The getter of `x`
        get x() {
            return fields.get(this).x;
        }

        // The setter of `x`
        set x(value) {
            fields.get(this).x = value; 
        }

        // The getter of `y`
        get y() {
            return fields.get(this).y;
        }

        // The setter of `y`
        set y(value) {
            fields.get(this).y = value; 
        }

        // The method `distanceTo`
        distanceTo(p) {
            return Math.sqrt(Math.pow(this.x - p.x, 2) + Math.pow(this.y - p.y, 2));
        }
    };
})();

這裡的寫法和一般線上教材略有不同,這是為了處理私有屬性的議題。原本的 class 沒有私有屬性的概念,我們用 IIFE 的手法將 fields 包在閉包中,做為儲存私有屬性的物件。fields 本身用 WeakMap 而非 Map 是預防記憶體洩露 (memory leak)。

部分讀者可能會對 Point 物件可回傳感到疑惑,此處的 Point 物件是 class expression,類似於 function expression,所以可以當成回傳值。

該物件使用的方式和先前雷同:

let p = new Point(0, 0);
console.log(`(${p.x}, ${p.y})`);

p.x = 3;
p.y = 4;
console.log(`(${p.x}, ${p.y})`);

console.log(p.distanceTo(new Point(0, 0)));

結語

在本文中,我們見到三種建立物件的方式。除非要刻意使用 ES5 版本的 JavaScript,使用 class 保留字來建立物件應該是比較簡單的方式。但另外兩種方式也要能讀懂,因為有許多代碼仍用這些手法來建立物件。

關於作者

身為資訊領域碩士,美思認為開發應用程式的目的是為社會帶來價值。如果在這個過程中該軟體能成為永續經營的項目,那就是開發者和使用者雙贏的局面。

美思喜歡用開源技術來解決各式各樣的問題,但必要時對專有技術也不排斥。閒暇之餘,美思將所學寫成文章,放在這個網站上和大家分享。