使用物件實字 (Object Literal)

    前言

    在 JavaScript 中,物件實字 {} 是以鍵值對來儲存資料的物件,有以下用途:

    • 模擬映射 (map)
    • 模擬命名空間 (namespace)
    • 撰寫基於物件 (object-based) 的程式

    在 ES6 加入新的語法特性後,除了模擬命名空間這個任務外,其他的用途都有更好的方式可以替代,不再建議使用物件實字。但現有的 ES5 代碼大量使用物件實字,JSON 也會用到物件實字,故仍要熟悉這項特性。

    把物件實字當成映射

    以下的範例把物件實字當成映射使用:

    // Home-made `assert`.
    function assert(cond, msg) {
        if (!(cond)) {
            throw (msg ? msg : 'Assertion failed');
        }
    }
    
    // Create an object literal as a map.
    let map = {};
    
    // Add some key-value pairs.
    map.one = "eins";
    map.two = "zwei";
    map.three = "drei";
    
    // Check whether the pair exists.
    assert(map.two === "zwei", "map.two should be zwei");
    
    // Delete a key-value pair.
    delete map.two;
    
    // Recheck its existence.
    assert(typeof map.two === "undefined", "map.two should not exist");
    

    在這個簡短的例子中,可以看到映射的各種情境,包括建立物件、新增鍵值對、確認鍵值對的值、移除鍵值對等。

    用物件實字模擬命名空間

    以下是一個假想的例子:

    // Create the namespace `come.example`.
    var com = com || {};
    com.example = com.example || {};
    
    // Create the variable `foo` in `com.example` namespace.
    com.example.foo = "Some string";
    
    // Create the function `bar` in `com.example` namespace.
    com.example.bar = function () {
        // Implement your code here.
    };
    

    在這個例字中,我們用兩層物件實字建立新的命名空間 com.example,就可以在該命名空間下放入資料、函式等。

    在網頁前端程式中,模組是後來才加入的概念。在預設情形下,所有的函式庫都會自動進入全域命名空間中。所以,利用物件實字模擬命名空間是重要的手法。

    用物件實字寫物件

    以下是一個略長的例子,請讀者先試著讀一下,我們會講解。

    /* 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 ? msg : 'Assertion failed');
        }
    }
    
    // Create the object `Point`.
    let Point = {};
    
    // Fields per object.
    Point.x = 0;
    Point.y = 0;
    
    // Function shared among objects.
    Object.setPrototypeOf(Point, {
        distance: function (p, q) {
            let dx = p.x - q.x;
            let dy = p.y - q.y;
            return Math.sqrt(dx * dx + dy * dy);
        }
    });
    
    let p = copy(Point);
    
    let q = copy(Point);
    q.x = 3;
    q.y = 4;
    
    assert(Point.distance(p, q) === 5, "The distance should be 5");
    

    我們一開始建立物件 Point,之後建立兩個屬性 xy

    當我們建立物件的方法 (method) distance 時,我們沒有將 distance 函式直接建立在物件 Point 上,而是建立在 Point 的原型鏈 (prototype) 上。這是為了簡約記憶體。有些教材會這樣寫:

    Point.distance = function (p, q) {
        let dx = p.x - q.x;
        let dy = p.y - q.y;
        return Math.sqrt(dx * dx + dy * dy);
    };
    

    這樣寫的話,函式 distance 視為物件 Point 的屬性。每次拷貝 Point 時,都會拷貝一份函式 distance。將共用的函式寫到原型鏈上,可以節約記憶體。我們會在後文講到原型鏈。

    JavaScript 沒有類別 (class) 的概念,想要做新物件時,從原物件拷貝一個即可。這是因為 JavaScript 的物件是基於原型 (self-based)。

    內建的物件拷貝函式是 Object.assign()。但 Object.assign() 是淺拷貝 (shallow copy),故我們自己寫了一個深拷貝 (deep copy) 的工具函式 copycopy 函式的原理相當簡單,就是建立一個新的物件實字 dest,將所有的屬性從 src 逐一拷貝過去即可。

    相對來說,xy 在每個物件各自有一份,不會改某個物件的屬性而影響到另一個物件的屬性。

    【分享本文】
    Facebook Twitter LinkedIn LINE Skype EverNote GMail Yahoo Yahoo
    【追蹤本站】
    Facebook Facebook Twitter Plurk