2015年12月25日 星期五

[JavaScript] JavaScript inheritance patterns

JavaScript 要怎麼做繼承,整理了一些寫法:
  • ES2015 (ES6)
  • Function Pattern
  • 不 return、使用 call (or apply) + new 的方法實作 (all in one)
  • Pseudo-Classical pattern 
環境:Chrome v47.0.2526.106

一些基礎知識 (須看懂這裡,下面才看的懂QQ)


__proto__:用來指向(pointing) prototype 的 property。

prototype:每個 object 都有 prototype,prototype 自己也是 object,Object 會繼承他們的 prototype 的屬性和方法。

Object.prototype是最上層的 prototype chain。

Object.create(prototype):建立一個 new object 並把其 __proto__ 設定成指定 object 的 properties。

Object.setPrototypeOf(obj, proto):設定 obj 的 prototype 為指定的 prototype,原型為 object.__proto__ = prototype。

prototype chain (__proto__ chain 或原型鏈):JavaScript 會先找 Instance 自己有沒有 property,沒有會經由 __proto__ 去找上一層的 prototype,沒有再找上一層,直到 Object.prototype。

下述範例 Car 的 prototype 就包含了 name、driver、getName()、run()。
var Car = function(name) {
    this.name = name || "car";
};
Car.prototype = {
    name    : "",
    driver  : "",
    getName : function() {
        return this.name;
    },
    run     : function () {
        console.log(this.name + " is running");
    },
};
把 Car 印出如下圖,他的上一層是 Function.prototyp,top 是 Object.prorotype


要呼叫 像 getName 需要使用 Car.prototype.getName() 才能呼叫,
直接 Car.getName() 會有錯誤 Uncaught TypeError: Car.getName is not a function。
不過要呼叫 Function.prorotype.call() 就可以直接使用 Car.call() 去呼叫 。
要呼叫 Object.prorotype.isPrototypeOf() 可以直接使用 Car.isPrototypeOf() 去呼叫 。
表示經由 __proto__往上尋找。

function: 把一個變數宣告為 function 時,如下
var Car = function(name) {
    this.name = name;
};
如圖,會繼承 Function.prototype,並且會把 Car 的 prototype.constructor 指向 Car 自己這個 function 。

prototype.constructor:指向 function 本身。本來以為跟 new constructor 會優先找這個並由這裡開始做 constructor 進入點,但實驗在 Chrome 時 new 不會看這個。

new constructor:
用實例來說明
var car = new Car(); 

console.log(car instanceof Car); //true


會先檢查 Car 是不是 function,需要是 function ,因為會拿來做 constructor。
  1. 建立 Object。
  2. 繼承 Car。
  3. 呼叫 Constructor (Car function)。
  4. 需要注意,如果 Constructor (Car function)有 return value 時 car 會被這個 return value 取代掉。
很像下列程式
var car = {};
car.__proto__ = Car.prototype; // 等同 Object.setPrototypeOf(car, Car.prototype);
Car.call(car, "CAR");

console.log(car instanceof Car); // true

或著

var car = Object.create(Car.prototype);
Car.call(car, "CAR");

console.log(car instanceof Car); // true
很接近不等於,差別是用 new 下圖是 Car,但用上列程式結果是 Object。
結果如下圖 (這裡 prototype.constructor 不見,是因為程式是直接設定 prototype 所以洗掉了)
用 new  constructor                          用 Object.create or __proto__ or Object.setPrototypeOf









ES 2015 (ES6)


ES2015 新增和支援 class、extends、constructor、super、set、get、static 等保留字,可以像其他語言寫出一個簡單的 class 並且使用 extend 來繼承母類別。最大的好處是類似 c++、java、php 等直覺寫法。不過目前尚未支援像是 public、protected、private 等 access modifiers,所以無法設定 private method。

如以下範例 (這裡是使用 Babel 來編輯)。
class Car {
    constructor(name){
        this.name = name || "car";
    }
    getName() {
        return this.name;
    }
    run () {
        console.log("car is running");
    }
    set driver(name) {
        this.driver = name;
    }
    get driver() {
        return this.driver;
    }
}
class Formula extends Car {
    constructor(name, color){
        super(name || "F1");
        this.color = color;
    }
    getColor() {
        return this.color;
    }
    run () {
        console.log("F1 is running");
    }
}
實驗結果,符合預期。
var car = new Car("i am a car");
console.log(car.getName());  // CAR
car.run();                   // CAR is running
car.driver = "toolman";
console.log(car.driver);     // toolman
console.log(car.getColor()); // Uncaught TypeError: car.getColor is not a function
console.log(car instanceof Car);     //true
console.log(car instanceof Formula); //false

var f1 = new Formula("i am a F1", "red");
console.log(f1.getName());   // F91
f1.run();                    // red F91 is running
console.log(f1.getColor());  // red
f1.driver = "prettygirl";
console.log(f1.driver);      // prettygirl
console.log(f1 instanceof Car);     //true
console.log(f1 instanceof Formula); //true

Function pattern(或稱 Factory constructor patter)


使用 Closure 的技巧直接在 function return 實做好的 object,子類別也是直接呼叫父類別 function 取得的 object 後,擴增完成,再 return。這個方法好處在於能實作出 private variable(name、color) 和 method (_run) 的效果。但缺點會失去繼承的結構,所以無法使用 instanceof 來判斷 instance 是屬於那個 class 產生的,或是那個 class 的子類別。(使用此 pattern 可以不用 new ,因為有沒有用結果都一樣)。
另外其實也可以自己連接 __proto__ 到父的 prototype 讓其有繼承關係。
var Car = function(setname) {
    var that = {};
    var name = setname || "car";
    that.getName = function() {
        return name;
    };
    var _run = function () {
        return name + " is running";
    };
    that.run = function () {
        console.log(_run());
    };
    that.driver = "";
    return that;
};

var Formula = function(name, color) {
    var that = Car(name);
    that.getColor = function() {
        return color;
    };
    that.run = function() {
        console.log(color + " " + that.getName() +" is running");
    };
    return that;
}
實驗結果 (其他結果跟 ES2015 一樣就不重複了,看 instanceof 的結果)
發現不屬於任何 class
var car = Car("i am a car");
console.log(car instanceof Car);     //false
console.log(car instanceof Formula); //false

var f1 = Formula("i am a F1", "red");
console.log(f1 instanceof Car);     //false
console.log(f1 instanceof Formula); //false
左邊是 car 的 instance                                       右邊是 f1 的 instance

可以由圖看出上一層直接是 Object 了,所以無法用 instanceof 判斷是不是從 Car or Formula 繼承的 instance。

不 return,使用 call (or apply) + new 的方法實作 (or all in one)


這個方法跟上述很接近,利用 call (or apply )+ new(需要用 new constructor 上述有寫 new 的行為) 所以直接使用 this 並且不 return 。也可以實作出有 private 效果的 method、variable,所以關鍵在子類別的 Car.call(this, arg1)把 this extend。優點在於能實作出 private 效果,有記住自己是由那個 class 所產生,無法判斷父類別是誰,所以像 design pattern 常用的 abscract class 就比較不適合。
一樣也可以自己連接 __proto__ 到父的 prototype 讓其有繼承關係。
var Car = function(setname) {
    var name = setname || "car";
    this.getName = function() {
        return name;
    };
    var _run = function () {
        return name + " is running";
    };
    this.run = function () {
        console.log(_run());
    };
    this.driver = "";
};

var Formula = function(name, color) {
    Car.call(this, name || "F1");
    this.getColor = function() {
        return color;
    };
    this.run = function() {
        console.log(color + " " + this.getName() +" is running");
    };
}
實驗結果,記得自己是由那個 class 衍生出來的,但無法判斷父類別是什麼。
var car = new Car("i am a car");
console.log(car instanceof Car);     //true
console.log(car instanceof Formula); //false

var f1 = new Formula("i am a F1", "red");
console.log(f1 instanceof Car);     //false
console.log(f1 instanceof Formula); //true
左邊是 car 的 instance                                                        右邊是 f1 的 instance

 

由圖可以看出
car -> Car -> Object
f1 -> Formula -> Object
的關係,符合 instanceof 測試結果。

Pseudo-Classical pattern


過去最標準的作法,使用 prototype 來設定 variable 和 method 並利用 Object.setPrototypeOf() 或 __proto__ 指向父類別來實現繼承。
使用 prototype 好處是在 new 時 prototyp 是參考型別( __proto__ 指向 prototype),不會重複建立 method 和 variable,較 function pattern 有效率和節省 memory。
這個方法優點是有完整的繼承關係,而且跟前述幾種方法 (除 ES2015,用 babel 模擬,非瀏覽器內建實作) 在 instance 結構上也是最省的。缺點要記得 extend 。

function extends(child, parent) {
    Object.setPrototypeOf(child.prototype, parent.prototype);
    // 等同 child.prototype.__proto__ = parent.prototype;
}

var Car = function(name) {
    this.name = name || "car";
};
Car.prototype = {
    constructor : Car,
    name    : "",
    driver  : "",
    getName : function() {
        return this.name;
    },
    run     : function () {
        console.log(this.name + " is running");
    },
};

var Formula = function(name, color) {
    this.name = name;
    this.color = color;
};
Formula.prototype = {
    constructor : Formula,
    getColor : function() {
        return this.color;
    },
    run      : function() {
        console.log(this.color + " " + this.name +" is running");
    }
};
extends(Formula, Car);
實驗結果
var car = new Car("i am a car");
console.log(car instanceof Car);     //true
console.log(car instanceof Formula); //false

var f1 = new Formula("i am a F1", "red");
console.log(f1 instanceof Car);     //true
console.log(f1 instanceof Formula); //true
左邊是 car 的 instance                                                        右邊是 f1 的 instance

由圖可以看出
car -> Car -> Object
f1 -> Formula -> Car -> Object
的關係,符合 instanceof 測試結果。


再來依據前述的一些觀念,可能會有人問為什麼不把 extend 放在子類別人?如下程式。
這樣好處是好讀比較會記得。但缺點是設定 __proto__是 cost 很大的動作(),放進去後,每次 new 都會重設一次,所以一般還是放在外面居多。不過這點我也是抱著懷疑,因為 javascript 在 set object 時,不是都是 call by sharing? reference to the object
function extend(child, parent) {
    Object.setPrototypeOf(child.__proto__, parent.prototype);
    // 等同 child.__proto__.__proto__ = parent.prototype;
}

var Car = function(name) {
    this.name = name || "car";
};
Car.prototype = {
    constructor : Car,
    name    : "",
    driver  : "",
    getName : function() {
        return this.name;
    },
    run     : function () {
        console.log(this.name + " is running");
    },
};

var Formula = function(name, color) {
    extend(this, Car);
    this.name = name;
    this.color = color;

};
Formula.prototype = {
    constructor : Formula,
    getColor : function() {
        return this.color;
    },
    run      : function() {
        console.log(this.color + " " + this.name +" is running");
    }
};

結論

方法 2,3 都可以用 pseudo classical 的 extend 類似的方法去完成繼承關係。這幾個差別就看實際上的要不要 private 應用、怎麼寫法自己喜歡、或是在不在意使不使用 prototype 的效能上的差異。好壞差異不大。未來當然都是使用 ES2015 最好的。


Reference:


http://www.w3schools.com/js/js_object_prototypes.asp
http://stackoverflow.com/questions/650764/how-does-proto-differ-from-constructor-prototype
http://dmitrysoshnikov.com/ecmascript/javascript-the-core/
http://davidshariff.com/blog/javascript-inheritance-patterns/
https://medium.com/@PitaJ/javascript-inheritance-patterns-179d8f6c143c#.qbayiakzb
http://es6.ruanyifeng.com/#docs/class
http://www.codedata.com.tw/javascript/essential-javascript-14-constructor/
http://www.codedata.com.tw/javascript/essential-javascript-15-prototype/
http://www.codedata.com.tw/javascript/essential-javascript-16-introspection/
http://www.codedata.com.tw/javascript/essential-javascript-18-class-based-oo-simulation/
https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/new
https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/create
https://developer.mozilla.org/zh-TW/docs/Web/JavaScript/Reference/Global_Objects/Object/setPrototypeOf

沒有留言:

張貼留言