談談 javascript 面向對象的一些細節問題

綜述

在 ES6 以前,ES5 實現面向對象是你們常常討論的問題,趁着 ES6 還沒進入瀏覽器,借我本身的一段腳本,跟你們討論一下 js 面向對象的一些細節問題,歡迎留言指教。java

例子代碼

基本的概念好比「基類」,「子類」等就不解釋了。下面是我寫的一段實現類繼承的 js 腳本:瀏覽器

/**
 * @file Inheritable.js
 * @author Y3G
 * @fileoverview
 * 可繼承對象
 */

var _ = require('lodash');
var assert = require('./assert');
var seq = require('./seq');

/**
 * 並聯函數
 * @return 生成的函數
 */
var parallel = function () {
    var fns = _.filter(arguments, _.isFunction);

    if (fns.length === 0) return;

    return function () {
        _.forEach(fns, function (fn) {
            fn.apply(this, _.slice(arguments));
        });
    };
}

/**
 * 建立類
 * @param base 基類函數或原型對象
 * @param ex 實例擴展
 * @return 生成的類函數
 *         默認方法:
 *           __init__: 構造函數
 *           __initProto__: 原型構造函數
 *         默認類方法:
 *           makeClass: 建立類
 *           makeSubClass: 建立子類
 */
function makeClass(base, ex) {
    base = base || {};
    ex = ex || {};

    var proto = _.isFunction(base) ? new base(true) : base;

    ex.__init__ = parallel(proto.__init__, ex.__init__); // 實例初始化函數
    ex.__initProto__ = parallel(proto.__initProto__, ex.__initProto__); // 原型初始化函數
    proto = _.mixin(proto, ex); // 合併原型和實例擴展

    function SubClass(isProto) {
        var initFunc = isProto ? this.__initProto__ : this.__init__;
        if (_.isFunction(initFunc)) {
            initFunc.apply(this, _.slice(arguments));
        }
        
        this.$id = seq();
    }

    if (_.isFunction(base)) {
        // 複製靜態函數等屬性到子類
        _.forOwn(base, function (val, key) {
            SubClass[key] = val;
        });
    }

    SubClass.prototype = proto;
    SubClass.prototype.constructor = SubClass;
    SubClass.makeSubClass = _.curry(makeClass, SubClass);
    SubClass.makeClass = makeClass;

    return SubClass;
}

/**
 * 根基類
 * @class Root
 */
var Root = makeClass();

module.exports = Root;

這段代碼導出了一個根基類 Root,它有一個靜態方法叫作 makeSubClass,調用可生成一個 Root 的子類,建立出的子類一樣帶有 makeSubClass 這個靜態方法。同時,建立的子類有幾個固定字段,分別是:app

  • __init__ 初始化函數函數

  • __initProto__ 原型初始化函數ui

  • $id 對象 idthis

經過 parallel 這個函數,makeClass 把基類和子類的 __init__ 函數合併執行,這樣解決了基類構造函數沒法執行的問題。prototype

下面說說我對幾個細節問題的思考。code

幾個問題和個人見解

:構造函數 __init__ 的執行順序是基類 -> 子類比較好仍是子類 -> 基類比較好?
對象

按照我貼的代碼,是基類的構造函數先執行,當時我是想模仿 C++。可是我如今認爲應該子類構造函數先執行。繼承

緣由很簡單,就是 ES6 使用的是類 java 方式, constructor 函數是子類先執行的,而且基類 constructor 是靠
super() 手工調用的。基於一點 java 的使用經驗,我也認爲這樣的構造順序,比基類 -> 子類靈活很多。另外,採用和 ES6 同樣的構造順序,更有利於移植。

__initProto__ 是什麼鬼?

這是我一直以來堅持的見解——用 js 模擬類,若是一個類的實例有可能做爲 prototype 存在,就必須把實例構造和 prototype
構造分開,而 __initProto__ 就是專門用來初始化 prototype 的。

緣由有兩方面:

  • 一是對象可能會很昂貴,佔不少資源。

  • 二是構造函數 __init__ 可能不止會操做 this,還可能會修改全局的某些狀態(好比計數器)。這種時候多建立一個和少建立一個實例,顯然是不一樣的。

:爲何要把基類函數上的靜態內容都拷貝到子類函數上?

由於原型鏈查找對靜態內容無效。

好比這樣:

var Foo = Root.makeSubClass({});

Foo.SOME_STATIC_THING = 0;

var Bar = Foo.makeSubClass({});

alert(Bar.SOME_STATIC_THING);

這時候假設不拷貝 SOME_STATIC_THING 到子類上去,就不能經過 Bar.SOME_STATIC_THING 訪問到該屬性。這是很是違反常識的,沒有語言是這樣子的。同時,如今又有了另外一個這麼作的理由,就是 ES6 有 static
關鍵字,若是你使用其餘方式實現靜態屬性,將不利於之後移植到 ES6。

不過這裏有個小坑,就若是靜態變量是基本類型(好比字符串),那麼顯然在子類上修改對基類無效。因而基本類型的靜態變量只能是常量,若是須要很是量的靜態變量,必須使用對象。

相關文章
相關標籤/搜索