2016年4月26日 星期二

JavaScript Note: constructor 徹底研究

JavaScript 混亂的程度,就算是拿大師們的著作來比對都會發現不一致之處。不信嗎?讓我們看下去......

JavaScript 壞掉優良部份

這本書當初是某 ptt 鄉民跟小弟推薦的,作者是 Yahoo! JavaScript 架構師,那裡面應該有黃金,讓我們看看 Page.49:

var Mammal = function(name){
    console.log('Mammal');
    this.name = name;
};

Mammal.prototype.get_name = function(){
    return this.name;
}

Mammal.prototype.says = function(){
    return this.saying || '';
}

var Cat = function(name){
    this.name = name;
    this.saying = 'meow';
}

Cat.prototype = new Mammal();

我在 Mammal constructor 加了一行 console.log('Mammal'),這樣當 constructor 被呼叫時就可以在 Browser console (Chrome -> key F12)觀察到。問題出在最後一行:

  • Mammal() 需要參數,但作者忽略了他
  • 建立 sub class prototype 時呼叫 super class constructor 是正確行為嗎?
這本書的出版日期是 2008 年,或許不該再拿明朝的劍來斬清朝的官了,請繼續往下看。

Secrets of JavaScript Ninja


作者之一是大名鼎鼎 jQuery 之父 John Resig,翻開 Page.150,你會得到同樣的例子:

function Person(){}
Person.prototype.dance = function(){}

function Ninja(){}

Ninja.prototype = new Person();

搞什麼!?難道真的這樣寫才對嗎?先不用跟小弟一樣開始懷疑自己的智力,其實後面作者有附一個真正的解法,不過複雜到你覺得我還是放棄當忍者,回頭當足輕好了。

Effective JavaScript

Effective 系列一向有口皆碑,據說始祖 Scott Meyers 靠此就建立了他的事業。身為 Effective 一員,當然對此也要發表一下意見,翻開 Page.110:

「super class constructor 應該只在 sub class constructor 中被調用,而非在建立 sub class prototype 時被調用」

作者認為你在建立 sub class prototype 時呼叫 super class constructor 根本就給不出有意義的參數,而且 super class constructor 應該是在建立 sub class instance 被呼叫(同上述)。來看一下作者建議的正確使用方式:

function Actor(scene, x, y){
    this.scene = scene;
    this.x = x;
    this.y = y;
    scene.register(this);
}

//......

function SpaceShip(scene, x, y){
    Actor.call(this, scene, x, y);
    this.point = 0;
}

SpaceShip.prototype = Object.create(Actor.prototype);

看起來可以打完收工了,真的嗎?

function Actor(scene, x, y){
    this.scene = scene;
    this.x = x;
    this.y = y;
    //scene.register(this);
}

Actor.prototype.moveTo = function(){
    this.x = x;
    this.y = y;
    this.scene.draw();
}

function SpaceShip(scene, x, y){
    Actor.call(this, scene, x, y);
    this.point = 0;
}

SpaceShip.prototype = Object.create(Actor.prototype);

var ss = new SpaceShip(null, 1, 2);
console.log(ss.constructor == SpaceShip); //false
console.log(ss instanceof SpaceShip);     //true

慢著,ss 是 SpaceShip 的實體,但 constructor 是 Actor?WTF?好像還差一味耶?

JavaScript 深入精要


Page.92: 「以新物件來取代  prototype 屬性時,將會移除預設的建構式屬性」。所以作者建議你這麼做:

var Foo = function Foo(){
}

Foo.prototype = {constructor:Foo};
var FooInstance = new Foo();
console.log(FooInstance.constructor === Foo); //true
console.log(FooInstance.constructor); //function Foo()

不過離我們的目標還有一小段距離,作者沒解釋是否在 prototype 繼承時也該這麼做。

JavaScript Cookbook 2nd


最後,付了一堆錢給 O'Reilly 之後我們終於有了答案:

function origObject() {
    this.val1 = 'a';
    this.val2 = 'b';
}

origObject.prototype.returnVal1 = function() {
    return this.val1;
};

origObject.prototype.returnVal2 = function() {
    return this.val2;
};

function newObject() {
    this.val3 = 'c';
    origObject.call(this);
}

newObject.prototype = Object.create(origObject.prototype);
newObject.prototype.constructor = newObject;

newObject.prototype.getValues = function() {
    return this.val1 + " " + this.val2 + " "+ this.val3;
};

var obj = new newObject();

程式設計的大數法則

「訴諸權威」是種邏輯繆誤,大師也會犯錯,當書上的東西模稜兩可不可信時,小弟的作法是去找一個最多人用的 open source project 來研究,如果大家都會錯,那也比較多人跟著一起陪葬

我們下載一個知名 library - backbone.js,試著 grep 看看能不能找到我們要的答案:

//--- underscore.js

   nativeCreate       = Object.create; //(1)

  // An internal function for creating a new object that inherits from another.
  var baseCreate = function(prototype) {
    if (!_.isObject(prototype)) return {};
    if (nativeCreate) return nativeCreate(prototype);
    Ctor.prototype = prototype;
    var result = new Ctor;
    Ctor.prototype = null;
    return result;
  };


  // Creates an object that inherits from the given prototype object.
  // If additional properties are provided then they will be added to the
  // created object.
  _.create = function(prototype, props) {
    var result = baseCreate(prototype);
    if (props) _.extendOwn(result, props);
    return result;
  };

//--- backbone.js
    // Set the prototype chain to inherit from `parent`, without calling
    // `parent`'s constructor function and add the prototype properties.
    child.prototype = _.create(parent.prototype, protoProps);
    child.prototype.constructor = child;

由(1)可以發現,他的作法的確是跟 JavaScript Cookbook 一樣的,會弄得這麼複雜是為了相容不支援 ES5 的瀏覽器。

END

沒有留言:

張貼留言