February 10, 2016 · JavaScript

Exemplar Pattern in JavaScript

在先前的文章中,曾經討論過為什麼不該採用 constructor pattern 來實作繼承。本文將延續這個議題,接著討論如何以 prototypal inheritance 避免使用 new operator 與 constructor 來建立(繼承)objects。

Basic Idea: Exemplar Object

從前文我們知道,JavaScript 的繼承是基於 prototype chain,也就是 object 與其 prototype object 之間的關聯:objects 以其 prototype 作為範本(exemplar)。prototype 定義了這些 objects 共有的 properties(通常是 methods),而根據其建立出來的 object 則在此之上擴充(extend)自身的 properties。

最簡易的 prototypal inheritance 大概會像這樣:

var Shape = { // exemplar object  
        move: function(x, y) {
            this.x += x;
            this.y += y;
        }
    },
    shape = Object.create(Shape);

// extend created object
shape.x = -6;  
shape.y = 0;

console.log('(' + shape.x + ', ' + shape.y + ')'); // (-6, 0)  
shape.move(2, 4);  
console.log('(' + shape.x + ', ' + shape.y + ')'); // (-4, 4)  

Initialization Method for Created Object

由於每個生成出來的 object 都需要做一些初始化(initialize)以擴充自身的 properties。如上例的 shape object 都需要在建立後新增 property xy。因此,我們可以在 Shape 上定義一個 initialization method 來做這件事:

var Shape = { // exemplar object  
        __init__: function(x, y) {
            this.x = x;
            this.y = y;
        },

        move: function(x, y) {
            // as before
        }
    },
    shape = Object.create(Shape).__init__(-6, 0);

console.log('(' + shape.x + ', ' + shape.y + ')'); // (-6, 0)  
shape.move(2, 4);  
console.log('(' + shape.x + ', ' + shape.y + ')'); // (-4, 4)  

Creation Method of Exemplar Object

此外,由於 object 都會在其被建立後立即呼叫其 initialization method,那麼乾脆就讓 exemplar object 自己負責建立 object 以及呼叫 initialization method 的工作,以讓這兩個步驟一併進行:

var Shape = { // exemplar object  
        __init__: function(x, y) {
            // as before
        },

        create: function() {
            var obj = Object.create(this);
            obj.__init__.apply(obj, arguments);
            return obj;
        },

        move: function(x, y) {
            // as before
        }
    },
    shape = Shape.create(-6, 0);

console.log('(' + shape.x + ', ' + shape.y + ')'); // (-6, 0)  
shape.move(2, 4);  
console.log('(' + shape.x + ', ' + shape.y + ')'); // (-4, 4)  

Extending the Exemplar Object

接下來,若是我們想要仿照前文的例子,基於 Shape 建立一個新的 exemplar object Rect,使得基於 Rect 建立的 rect object 不僅能存取 Rect 的 properties,也能存取 Shape 的 properties 呢?

我們可以在 Shape 上新增一個 method extend(),以此建立基於它的 exemplar object:

var Shape = { // exemplar object  
        __init__: function(x, y) {
            // as before
        },

        create: function() {
            // as before
        },

        extend: function(source) {
            var obj = Object.create(this),
                propNames = Object.getOwnPropertyNames(source),
                numProps = propNames.length,
                prop,
                descriptor,
                i;

            for (i = 0; i < numProps; i++) {
                prop = propNames[i];
                descriptor = Object.getOwnPropertyDescriptor(source, prop);
                Object.defineProperty(obj, prop, descriptor);
            }

            return obj;
        },

        move: function(x, y) {
            // as before
        }
    };

extend() 會以自身作為 prototype 建立新的 object,並把 argument source 的每一個 property 複製到新建立的 object 上。

接著就可以像這樣定義並使用 Rect

var Rect = Shape.extend({  
        __init__: function(x, y, length, width) {
            Shape.__init__.call(this, x, y);
            this.length = length;
            this.width = width;
        },

        area: function() {
            return this.length * this.width;
        }
    }),
    rect = Rect.create(-6, 0, 3, 5);

console.log(rect.length + ' * ' + rect.width + ' = ' + rect.area()); // 3 * 5 = 15

console.log('(' + rect.x + ', ' + rect.y + ')'); // (-6, 0)  
rect.move(2, 4);  
console.log('(' + rect.x + ', ' + rect.y + ')'); // (-4, 4)  

Abstracting Exemplar Object

最後,我們要將上例的 create()extend() 提取出來,並抽象化出一個 object Exemplar

var Exemplar = {  
        __init__: function() {
            // do nothing
        },

        create: function() {
            // as before
        },

        extend: function() {
            // as before
        }
    });

Shape 便可以改寫成這樣:

var Shape = Exemplar.extend({  
        __init__: function(x, y) {
            this.x = x;
            this.y = y;
        },

        move: function(x, y) {
            this.x += x;
            this.y += y;
        }
    });

Discussion

到這裡,我們定義了一個 object Exemplar,以及它的兩個 methods:create()extend()create() 讓你能夠建立以 exemplar object 作為 prototype 的 object,並呼叫其 initialization method。extend() 則是以現有的 exemplar object 作為 prototype,並基於它建立新的 exemplar object。

但等等,現在我們是使用 exemplar object 的 create() method 來建立 object,並透過 __init__() method 來初始化:

var rect = Rect.create(-6, 0, 3, 5);  

這跟下面這種透過 new operator 與 constructor 建立並初始化 object 的方式不是差不多嗎?

var rect = new Rect(-6, 0, 3, 5);  

這之中主要的不同,在於前者的 Rect 是 prototype 本身,負責初始化的 initialization method 則是其 property(Rect.__init__());後者的 Rect 則是負責初始化生成 object 的 constructor,而其所生成 object 的 prototype 則是其 property(Rect.prototype)。

ShapeRect 的例子來說,若是採用 pseudo-classical inheritance(constructor pattern),實際的繼承並非發生在兩個 constructors(RectShape),而是在兩者的 prototype properties(Rect.prototypeShape.prototype)之間。如同前文所述,這是 constructor pattern 之所以難以理解之處。

另一方面,若是採用 exemplar pattern,實際的繼承就真的是發生在 RectShape 之間。因為這兩者這時都不是 constructor,而是 prototype object 本身,從而避免了 constructor pattern 的不直覺之處。

換句話說,exemplar pattern 反轉了 initialization function 與 prototype object 在 constructor pattern 中的地位:其令 initialization function 為 prototype object 的一部分,而非令 prototype object 作為 initialization function(constructor)的 property。

透過這樣的反轉,才能正確反映出 object 才是主角,而不是被「想偽裝成 class」的 constructor 喧賓奪主搶走了焦點。也因此,我想相比於 constructor pattern,exemplar pattern 才是更貼近 JavaScript 本質的繼承方式。

Further Reading

  • LinkedIn
  • Tumblr
  • Reddit
  • Google+
  • Pinterest
  • Pocket
Comments powered by Disqus