JavaScript OO不XX 學習總結


一、廢話

    總覺得面向對象這東西,如果做的東西不是十分復雜的話,其實不太有場景能用上。最近重新學習了《JavaScript高級程序設計》中面向對象程序部分的知識,有一些收獲,特此記錄。

 

二、JavaScript創建對象最佳實踐

    2.1 理論

    JavaScript是基於原型的語言,創建對象比較常用的方法是采用“構造函數+掛載原型”的方式。

    舉個例子:

  var Engineer = function (name) {
    this.name = name;
  };
  Engineer.prototype.codeWith = function (tools) {
    return this.name + ' is coding with ' + tools.join(','); 
  };
  Engineer.prototype.solve = function (problem) {
    return this.name + ' is solving ' + problem;
  };

  var a = new Engineer('kohpoll');
  var b = new Engineer('xp');

  console.log(a, b);
  console.log(a.codeWith(['vim']));
  console.log(a.solve('oo'));

    這段代碼執行后,事實上的結構是這樣的:

    這里總結2點:1)每創建一個函數,該函數默認都會擁有一個prototype屬性,這個prototype是一個對象,默認會擁有一個constructor屬性反過來指向該函數(比如:例子里的函數Engineer擁有的原型屬性prototype,prototype里擁有constructor指向Engineer);2)每創建一個對象,該對象都會擁有一個內置屬性__proto__,該屬性指向構造了該對象的構造函數的prototype屬性(比如:例子里的a對象的__proto__指向構造了它的構造函數Engineer的prototype)。

    那為什么Engineer.prototype擁有__proto__屬性,且指向Object.prototype呢?這是因為原型對象prototype也是一個對象,那是誰構造了這個對象呢?當然是Object構造函數,所以Engineer.prototype的__proto__指向Object的原型(即:Object.prototype)。Object.prototype的__proto__已經到達頂端了,直接指向空。這就是所謂的原型鏈。

    當我們訪問某個成員時,如:a.codeWith(['vim'])。會先從對象自身搜尋(上圖的第一個方塊),沒有找到的話,就順着__proto__來到Engineer.prototye,發現找到了這個方法,於是進行調用,若這里還沒有找到,那就繼續順着Engineer.prototype的__proto__來到Object.prototype,如果這里還是找不到,若是訪問屬性就返回undefined,若是訪問方法就報“Uncaught TypeError: Object [object Object] has no method"錯誤。

    所以,JavaScript中所有對象都繼承自Object其實是說訪問成員時,最終會在Object.prototype上結束。我們創建出一個對象后,toString、valueOf方法自動就可用,正是因為它們是掛載在Object.prototype上的。

    通過構造函數初始化屬性,將方法掛載在原型上,我們實現了多個對象復用原型上的共有方法(不必在每個對象中都得定義一次),每個對象又分別擁有自己的屬性。

    2.2 實踐

    反過來,看上面的代碼,總覺得每次都要這樣編寫(尤其是寫一堆prototype的部分),是件比較煩人的事。我們可以將生成構造函數這個過程進行一個封裝:

var construct = function () {// 構造器
  var Klass = function () {
    this.initialize.apply(this, arguments);
  };

// 添加實列成員(屬性,方法) Klass.include = function (obj) { for (var name in obj) { this.prototype[name] = obj[name]; } return this; }; return Klass; };

    那我們的示例代碼可以這樣來寫:

var Engineer = construct().include({
  initialize: function (name) {
    this.name = name;
  },
  codeWith: function (tools) {
    return this.name + ' is coding with ' + tools.join(','); 
  },
  solve: function (problem) {
    return this.name + ' is solving ' + problem;
  }
});

var a = new Engineer('kohpoll');
var b = new Engineer('xp');

console.log(a, b);
console.log(a.codeWith(['vim']));
console.log(a.solve('oo'));

 

三、JavaScript繼承最佳實踐

    3.1 理論

    前面說過,JavaScript是基於原型的語言,其實從對原型鏈的說明中,我們已經大概能看出JavaScript實現繼承的方法了,那就是構造原型鏈。如果,現在我們添加一個FrontEndEngineer子類繼承Engineer,那我們想的效果應該是這樣的結構:

    也就是說,如果我們能夠打斷默認情況下FronEndEngineer.prototype.__proto__的指向,讓FrontEnginner.prototype.__proto__屬性指向父類Engineer的原型prototype,根據前面說明過的原型鏈搜尋過程,我們就實現了FrontEnginner繼承Engineer。即:FrontEndEngineer.prototype.__proto__=Engineer.prototype。

    可惜的是,__proto__是一個內部屬性,各個瀏覽器的內部實現不一樣,也許並不都叫__proto__,而且我們也不能直接修改。回想我們前面總結的2點中的第二點:每創建一個對象,該對象都會擁有一個內置屬性__proto__,該屬性指向構造了該對象的構造函數的prototype屬性。對照我們的目的,讓FrontEndEngineer.prototype.__proto__指向Enginner.prototype。於是,我們的方法就出現了:FroneEndEngineer.prototype = new Engineer()。

    說明如下:FrontEndEngineer.prototype是一個對象,擁有__proto__屬性,默認情況下是指向Object.prototype(因為是Object構造函數構造了FrontEndEngineer.prototype),現在我們讓FrontEndEngineer.prototype等於new Engineer(),等價於說FrontEndEngineer.prototype現在是由Engineer函數構造出的,根據上面提到的“每創建一個對象,該對象都會擁有一個內置屬性__proto__,該屬性指向構造了該對象的構造函數的prototype屬性”,那此時FrontEndEngineer.prototype這個對象的__proto__應該指向構造了FrontEndEngineer.prototype這個對象的構造函數的prototype,即:Engineer.prototype。

    代碼如下:

var Engineer = construct().include({
  initialize: function (name) {
    this.name = name;
  },
  codeWith: function (tools) {
    return this.name + ' is coding with ' + tools.join(','); 
  },
  solve: function (problem) {
    return this.name + ' is solving ' + problem;
  }
});

var FrontEndEngineer = function (name) {
  this.name = name;
};
FrontEndEngineer.prototype = new Engineer();
FrontEndEngineer.prototype.fuckIE6 = function () {
  return this.name + 'fuck ie6';
};
FrontEndEngineer.prototype.constructor = FrontEndEngineer;
var a = new FrontEndEngineer('kohpoll'); var b = new FrontEndEngineer('xp'); console.log(a, b); console.log(a.codeWith(['vim'])); console.log(a.solve('oo')); console.log(a.fuckIE6());
console.log(a instanceof Engineer, a instanceof FrontEndEngineer);

    於是,通過重寫原型,我們實現了繼承。通過這種方法需要注意的問題是:1)為子類FrontEndEngineer新添加的方法要在重寫原型(即:FrontEndEngineer.prototype=new Engineer)后進行添加,否則,會被直接覆蓋掉;2)由於我們重寫了原型,原型的constructor屬性也會改變,如果很在意,可以重新進行賦值;3)每次重寫原型都調用了父類的構造函數,其實完全可以避免(下面說)。

    3.2 實踐

    上面實現了繼承,但是寫起來也是有很多要注意的地方,比較麻煩。我們可以進行一個封裝,代碼如下:

var inherits = function (klass, supr, protoProps) {
  // 用於共享原型的空函數
  var F = function () {};

  // 重寫原型實現繼承
  F.prototype = supr.prototype;
  klass.prototype = new F();

  // 添加實列成員
  klass.include(protoProps);

  // 設置構造器的constructor(因為重寫了原型)
  klass.prototype.constructor = klass;

  return klass;
};

    上面提到重寫原型時會調用父類的構造函數,其實我們的目的僅僅是要讓FrontEndEngineer.prototype的__proto__指向父類的prototype就好,根本不關心是不是真的是父類Engineer構造了FrontEndEngineer.prototype對象。於是,我們使用一個空函數F,將父類Engineer的prototype原型賦值給空函數F的prototype原型,然后讓這個F來構造子類的prototype原型,就完成了原型的重寫。

    於是,我們的示例代碼可以這樣來編寫:

var Engineer = construct().include({
  initialize: function (name) {
    this.name = name;
  },
  codeWith: function (tools) {
    return this.name + ' is coding with ' + tools.join(','); 
  },
  solve: function (problem) {
    return this.name + ' is solving ' + problem;
  }
});

var FrontEndEngineer = construct();
inherits(FrontEndEngineer, Engineer, {
  fuckIE6: function () {
    return this.name + 'fuck ie6';
  }
});

var a = new FrontEndEngineer('kohpoll');
var b = new FrontEndEngineer('xp');

console.log(a, b);
console.log(a.codeWith(['vim']));
console.log(a.solve('oo'));
console.log(a.fuckIE6());
console.log(a instanceof Engineer, a instanceof FrontEndEngineer);

 

四、改進

    4.1 接口使用上改進

    上面實現的封裝使用起來還是不太爽,我們參考下prototype(http://prototypejs.org/learn/class-inheritance),改進后得到如下代碼:

var Class = {
  create: function () {
    var supr = Object;
    var protoProps = arguments[0] || {};
    var klass;

    if (typeof arguments[0] == 'function') {
      supr = arguments[0];
      protoProps = arguments[1] || {};
    }

    if (typeof protoProps.initialize != 'function') {
      protoProps.initialize = function () {};
    } 

    klass = this._construct();
    this._inherits(klass, supr, protoProps);

    return klass;
  },
  _construct: function () {//{{{// 構造器
    var Klass = function () {
      this.initialize.apply(this, arguments);
    };

// 添加實列成員(屬性,方法) Klass.include = function (obj) { for (var name in obj) { this.prototype[name] = obj[name]; } return this; }; return Klass; },//}}} _inherits: function (klass, supr, protoProps) {//{{{ // 用於共享原型的空函數 var F = function () {}; // 重寫原型實現繼承 F.prototype = supr.prototype; klass.prototype = new F(); // 添加實列成員 klass.include(protoProps); // 設置構造器的constructor(因為重寫了原型) klass.prototype.constructor = klass; return klass; }//}}} };

    於是,現在我們的示例代碼可以這樣來寫了:

var Engineer = Class.create({
  initialize: function (name) {
    this.name = name;
  },
  codeWith: function (tools) {
    return this.name + ' is coding with ' + tools.join(','); 
  },
  solve: function (problem) {
    return this.name + ' is solving ' + problem;
  }
});

var FrontEndEngineer = Class.create(Engineer, {
  initialize: function (name) {
    this.name = name;
  },
  fuckIE6: function () {
    return this.name + 'fuck ie6';
  }
});

var a = new FrontEndEngineer('kohpoll');
var b = new FrontEndEngineer('xp');

console.log(a, b);
console.log(a.codeWith(['vim']));
console.log(a.solve('oo'));
console.log(a.fuckIE6());
console.log(a instanceof Engineer, a instanceof FrontEndEngineer);

    4.2 繼承使用上改進

    經過這樣改進,代碼看起來比較清晰了。但是,繼承有一個很關鍵的問題沒有解決,就是子類怎么調用父類的方法,從而實現代碼復用?下面我們就來解決這個問題。

    step1  事實上,我們可以直接通過父類的prototype屬性來訪問父類的方法,為了方便,我們在生成構造器時添加一個$super屬性。於是,代碼變成如下(標紅的是新增代碼):

var Class = {
  create: function () {
    var supr = Object;
    var protoProps = arguments[0] || {};
    var klass;

    if (typeof arguments[0] == 'function') {
      supr = arguments[0];
      protoProps = arguments[1] || {};
    }

    if (typeof protoProps.initialize != 'function') {
      protoProps.initialize = function () {};
    } 

    klass = this._construct();
    this._inherits(klass, supr, protoProps);

    return klass;
  },
  _construct: function () {//{{{
    // 構造器
    var Klass = function () {
      // 訪問父類成員快捷方式
      this.$super = Klass.$super;

      this.initialize.apply(this, arguments);
    };

    // 添加實列成員(屬性,方法)
    Klass.include = function (obj) {
      for (var name in obj) {
        this.prototype[name] = obj[name];
      }

      return this;
    };

    return Klass;
  },//}}}
  _inherits: function (klass, supr, protoProps) {//{{{
    // 用於共享原型的空函數
    var F = function () {};

    // 重寫原型實現繼承
    F.prototype = supr.prototype;
    klass.prototype = new F();

    // 保存父類原型
    klass.$super = supr.prototype;

    // 添加實列成員、類成員
    klass.include(protoProps);

    // 設置構造器的constructor(因為重寫了原型)
    klass.prototype.constructor = klass;

    return klass;
  }//}}}
};

    於是,示例代碼可以這樣使用:

var Engineer = Class.create({
  initialize: function (name) {
    this.name = name;
  },
  codeWith: function (tools) {
    return this.name + ' is coding with ' + tools.join(','); 
  },
  solve: function (problem) {
    return this.name + ' is solving ' + problem;
  }
});

var FrontEndEngineer = Class.create(Engineer, {
  initialize: function (name) {
    this.$super.initialize.call(this, name);
    // this.name = name;
  },
  codeWith: function (tools) {
    return 'fron end ' + this.$super.codeWith.call(this, tools);
  },
  fuckIE6: function () {
    return this.name + 'fuck ie6';
  }
});

var a = new FrontEndEngineer('kohpoll');
var b = new FrontEndEngineer('xp');

console.log(a, b);
console.log(a.codeWith(['vim']));

    step2  其實經過這樣改進,已經比較不錯了,只是每次調用父類方法時都得使用call方法來確保this正確的指向子類實例。如果不用call,那this會指向什么呢?答案是父類的prototye,在本例中,$super存的是Engineer.prototype,那當我們使用$super.codeWith()時,實際上,是指Engineer.prototype.codeWith(),this指向Engineer.prototype。

    我們想辦法來改進這一點,得到代碼如下(標紅為新增):

var Class = {
  create: function () {
    var supr = Object;
    var protoProps = arguments[0] || {};
    var klass;

    if (typeof arguments[0] == 'function') {
      supr = arguments[0];
      protoProps = arguments[1] || {};
    }

    if (typeof protoProps.initialize != 'function') {
      protoProps.initialize = function () {};
    } 

    klass = this._construct();
    this._inherits(klass, supr, protoProps);

    return klass;
  },
  _construct: function () {//{{{
    var slice = Array.prototype.slice;

    // 構造器
    var Klass = function () {
      // 訪問父類成員快捷方式
      this.$super = function (name) {
        var args = slice.call(arguments, 1) || [];
        var fn = Klass.$super[name];
        return typeof fn == 'function' ? fn.apply(this, args) : fn;
      };

      this.initialize.apply(this, arguments);
    };

    // 添加實列成員(屬性,方法)
    Klass.include = function (obj) {
      for (var name in obj) {
        this.prototype[name] = obj[name];
      }

      return this;
    };

    return Klass;
  },//}}}
  _inherits: function (klass, supr, protoProps) {//{{{
    // 用於共享原型的空函數
    var F = function () {};

    // 重寫原型實現繼承
    F.prototype = supr.prototype;
    klass.prototype = new F();

    // 保存父類原型
    klass.$super = supr.prototype;

    // 添加實列成員、類成員
    klass.include(protoProps);

    // 設置構造器的constructor(因為重寫了原型)
    klass.prototype.constructor = klass;

    return klass;
  }//}}}
};

    現在,我們將$super重寫成函數,通過傳入的函數名在父類的prototype里查找對應方法,若找到了就通過apply來調用,此時傳入的this就指向了子類實例(因為$super現在是被子類的實例調用,$super函數內部this就指向子類實例)。

    於是,使用方法如下:

var Engineer = Class.create({
  initialize: function (name) {
    this.name = name;
  },
  codeWith: function (tools) {
    return this.name + ' is coding with ' + tools.join(','); 
  },
  solve: function (problem) {
    return this.name + ' is solving ' + problem;
  }
});

var FrontEndEngineer = Class.create(Engineer, {
  initialize: function (name) {
    this.$super('initialize', name);
    // this.name = name;
  },
  codeWith: function (tools) {
    return 'front end ' + this.$super('codeWith', tools);
  },
  solve: function (problem) {
    return 'front end ' + this.$super('codeWith', ['html', 'js', 'css']) + this.fuckIE6();
  },
  fuckIE6: function () {
    return ' and fuck ie6';
  }
});

var a = new FrontEndEngineer('kohpoll');
console.log(a.solve('work'));

    4.3 添加靜態成員

    最后一點改進,是添加類似static的所有類共享的方法和屬性。實現方法就是,直接將這些成員掛載到構造函數上面。這個就不多說了。於是得到最終代碼如下:

  var Class = {
    create: function () {
      var supr = Object;
      var protoProps = arguments[0] || {}, staticProps = arguments[1] || {};
      var klass;

      if (typeof arguments[0] == 'function') {
        supr = arguments[0];
        protoProps = arguments[1] || {};
        staticProps = arguments[2] || {};
      }

      if (typeof protoProps.initialize != 'function') {
        protoProps.initialize = function () {};
      } 

      klass = this._construct();
      this._inherits(klass, supr, protoProps, staticProps);

      return klass;
    },
    _construct: function () {//{{{
      var slice = Array.prototype.slice;

      // 構造器
      var Klass = function () {
        // 訪問類成員快捷方式 
        this.$self = Klass.$self;

        // 訪問父類成員快捷方式
        this.$super = function (name) {
          var args = slice.call(arguments, 1) || [];
          var fn = Klass.$super[name];
          return typeof fn == 'function' ? fn.apply(this, args) : fn;
        };

        this.initialize.apply(this, arguments);
      };

      // 用於添加類成員(屬性,方法)
      Klass.extend = function (obj) {
        for (var name in obj) {
          this[name] = obj[name];
        }

        return this;
      };

      // 添加實列成員(屬性,方法)
      Klass.include = function (obj) {
        for (var name in obj) {
          this.prototype[name] = obj[name];
        }

        return this;
      };

      return Klass;
    },//}}}
    _inherits: function (klass, supr, protoProps, staticProps) {//{{{
      // 用於共享原型的空函數
      var F = function () {};

      // 重寫原型實現繼承
      F.prototype = supr.prototype;
      klass.prototype = new F();

      // 保存父類原型
      klass.$super = supr.prototype;
      // 保存類自身
      klass.$self = klass;

      // 添加實列成員、類成員
      klass.include(protoProps).extend(staticProps);

      // 設置構造器的constructor(因為重寫了原型)
      klass.prototype.constructor = klass;

      return klass;
    }//}}}
  };

    

    最后的最后,可以在這里獲取所有源碼:https://github.com/KohPoll/zuki


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM