- ES2015 (ES6)
- Function Pattern
- 不 return、使用 call (or apply) + new 的方法實作 (all in one)
- Pseudo-Classical pattern
一些基礎知識 (須看懂這裡,下面才看的懂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。
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。
- 建立 Object。
- 繼承 Car。
- 呼叫 Constructor (Car function)。
- 需要注意,如果 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 所以洗掉了)
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