Name: Password: Sign in
LiKy - 一个 Javascript MVVM 框架的实现 中的文章

本文在 署名-非商业性使用-相同方式共享 3.0 版权协议下发布, 转载请注明出自 kyleslight.net

Javascript 是可以实现类的,就像我在 Javascript 的原型继承 中介绍过的一样;当然,现在通过 Typescript 可以非常轻易地像其他语言一样实现类和类的特性。

在这一篇我简单介绍一下使用原生的方法实现类。

构造函数

我们知道 Javascript 中一切都是对象,我们可以直接字面创建对象。如果我们需要一个工厂来批量创建对象我们可以创造一个构造函数,就像这样:

var Book = function (title) {
    this._title = title;
    this.getTitle = function () {
        return this._title;
    }
}

var SICP = new Book('Structure and Interpretation of Computer Programs');
SICP.getTitle(); // 'Structure and Interpretation of Computer Programs'

这样一个模式在简单创建简单对象的时候是有效的。然而,很明显这样的代码复用并不强。我们无法实现简单的继承以及扩充共用的代码模块。

一个更好的想法是提供一个实现类机制的函数( 不妨我们叫它 类模板 ),它帮助实现了所有我们需要的类特性并返回一个构造函数,我们构建类就在这个构造函数之上。

类模板

初始化

一开始我们的类模板是这样的:

var Class = function () {
    var _class = function () {
        this.init.apply(this, arguments);
    };

    _class.fn = _class.prototype;
    _class.fn.init = function () {};
    return _class;
}

它创建了一个 _classclass 是关键字所以我们避免使用它 ),_class 可以在实例一个对象的时候调用这个对象上下文中的 init 方法初始化这个对象;之后我们清空了 init 保证它在对象创建完之后不再起作用( 其间我们给 prototype 起了一个新名字,当然这不是必须的 )。这个 _class 被赋予到新类上。

在外部使用它类似这样:

var Book = new Class;

Book.prototype = {
    _title : '',
    init : function (title) {
        this._title = title;
    },
    getTitle : function () {
        return this._title;
    }
}

var SICP = new Book('Structure and Interpretation of Computer Programs');
SICP.getTitle(); // 'Structure and Interpretation of Computer Programs'

这一段几乎和之前相同,注意到 new 关键字后面的函数如果返回一个对象那么 new 操作就会返回这个对象。

实现类属性与实例属性的添加

像上面这样直接在 prototype 上创建实例属性是可行的,不过我们可以用更优雅的方式来添加类属性与实例属性。

首先是实现添加实例属性:

_class.include = function (obj) {
    var included = obj.included;
    for(var key in obj) {
        _class.fn[key] = obj[key];
    }
    if (included) included(_class);
}

include 方法做了一点微小的工作,_class.fn 直接接收了来自 obj 的属性;另外,objincluded 方法作为回调函数在类属性添加完时自动被触发。

使用类似于这样:

var Point = new Class;

Point.include({
    _x: 0,
    _y: 0,
    init: function(x, y) {
        this._x = x;
        this._y = y;
    },
    rotation: function (theta) {
        var x_prime = this._x * Math.cos(theta) - this._y * Math.sin(theta);
        var y_prime = this._x * Math.sin(theta) + this._y * Math.cos(theta);
        this._x = x_prime;
        this._y = y_prime;
    },
    included: function (_class) {
        console.log('Include Completed =_=');
    },
    printSelf: function () {
        console.log('{ Point:  \tx:%s  \ty:%s }', this._x.toPrecision(5), this._y.toPrecision(5));
    }
}); // 'Include Completed =_='

var p = new Point(3.0, 4.0);
p.printSelf(); // '{ Point:      x:3.0000      y:4.0000 }'
p.rotation(Math.PI / 2);
p.printSelf(); // '{ Point:      x:-4.0000      y:3.0000 }'

然后是类属性的实现,方法几乎和实例属性的实现相同:

_class.extend = function(obj) {
    var extended = obj.extended;
    for(var key in obj) {
        _class[key] = obj[key];
    }
    if (extended) extended(_class);
}

真正有区别的地方只有给属性赋值的地方,一个是在 _class.fn 上,另一个在 _class 上。

接着上面的例子:

Point.extend({
    createRandomPoint: function (xRange, yRange) {
        var x = Math.random() * xRange - xRange / 2.0;
        var y = Math.random() * yRange - yRange / 2.0;
        return new Point(x, y);
    },
    extended: function () {
        console.log('Extend Completed =_=');
    }
}); // 'Extend Completed =_='

var p = Point.createRandomPoint(4.0, 4.0);
p.printSelf(); // '{ Point:      x:0.18837      y:1.1747 }'

这样安排有一个好处,我们可以封装一些通用的方法作为独立的模块,例如

var PointTools = {
    distFromOrigin: function () {
        var x = this.getX();
        var y = this.getY();
        return Math.sqrt(x * x + y * y);
    },
    getX: function() {
        return this._x;
    },
    getY: function() {
        return this._y;
    }
}

Point.include(PointTools);

var p = new Point(3, 4);
p.distFromOrigin(); // 5

继承的实现

很自然的,选择复用就会想到继承,继承的实现反倒是相对简单的了:

var Class = function (parent) {
    var _class = function () {
        if (parent && parent.prototype.init) this.superInit = parent.prototype.init;
        this.init.apply(this, arguments);
    };
    if (parent) {
        var subclass = function () {};
        subclass.prototype = parent.prototype;
        _class.prototype = new subclass;
    }
    ...
}

this.superInit 实现了对父类构造函数的复用,之后我们利用 subclass.prototype 作为接收父类 prototype 的容器,对它进行 new 操作会返回父类原型链上的对象,我们将这个对象赋给 _class.prototype 就继承了原型链上的所有属性。

下面是我们对继承的测试:

var Song = new Class;
Song.include({
    _title: 'No title',
    _band: 'No band',
    _price: 0,
    init: function (title, band, price) {
        this._title = title;
        this._band = band;
        this._price = price;
    },
    printSelf: function () {
        console.log('{ Song: %s by %s, $%d }', this._title, this._band, this._price);
    }
});

var SongInChineseStore = new Class(Song);
SongInChineseStore.include({
    _EXCHANGERATEUSDOLLAR: 6.46,
    init: function (title, band, price) {
        this.superInit(title, band, price);
    },
    printSelf: function () {
        console.log('{ Song: %s by %s, ¥%f }', this._title, this._band, this.getChinesePrice());
    },
    getChinesePrice: function () {
        return this._EXCHANGERATEUSDOLLAR * this._price;
    }
});

(new Song('Apples', 'The Seasons', 10)).printSelf(); // '{ Song: Apples by The Seasons, $10 }'

(new SongInChineseStore('印第安老斑鸠', 'Jay Chou', 10)).printSelf(); // { Song: 印第安老斑鸠 by Jay Chou, ¥64.6 }

Everything works fine :)

上下文与代理

Javascript 与其他语言相比有一个非常奇特的不同之处在于它可以动态修改上下文:

var getTitle = function () {
    return this._title;
};

var ModuleA = {
    _title: 'ModuleA',
    printSelf: function () {
        console.log('{ ModuleA title: %s }', this.getTitle());
    },
    getTitle: getTitle
};

var ModuleB = {
    _title: 'ModuleB',
    printSelf: function () {
        console.log('{ ModuleB title: %s }', this.getTitle());
    },
    getTitle: getTitle
}

ModuleA.printSelf(); // '{ ModuleA title: ModuleA }'
ModuleB.printSelf(); // '{ ModuleB title: ModuleB }'

当然如果你正好有这样(需要切换上下文,例如执行回调)的需求也不错,不过其他的时候我们想让它保持和其他语言一样的语义,即 this 只指代当前构造函数创建的对象。

Javascript 可以使用 applycall 方法来指定函数的上下文,例如:

var getTitle = function () {
    return this._title;
};

var ModuleTest = {
    _title: 'test'
}

getTitle.apply(ModuleTest); // 'test'

call 的用法与 apply 类似( apply 的第二个参数是数组,call 像一般的函数接受单独的参数 ),这种表现就像是代理(proxy),把要处理的内容(上下文)分派给其他的函数,而这些函数实现本身不需要知道具体的上下文是什么。

知道了这一点之后我们可以在类模板中加入代理:

...
_class.proxy = function (func) {
    var self = this;
    return (function() {
        return func.apply(self, arguments);
    })
}
_class.fn.proxy = _class.proxy;
...

我们将当前对象的方法做了一层封装保证内部的函数的上下文是该对象。这样我们可以放心的将我们当前对象的方法交付给一个其他的对象调用而不用在乎中途上下文被切换,例如下面这个例子(在 DOM 中事先添加一个 idtest-proxy 的元素):

var Button = new Class;

Button.include({
    _title: '',
    init: function (title, id) {
        this._title = title;
        this.domElement = document.getElementById(id);
        this.domElement.addEventListener('click', this.proxy(this.printSelf), false);
    },
    printSelf: function () {
        console.log('{ Button title: %s }', this._title);
    }
});

var button = new Button('TestProxy', 'test-proxy');

// click the button we will get '{ Button title: TestProxy }'

这个例子中,this.domElement 调用 addEventListener,此时原本的上下文是 this.domElement,由于经过 proxy 包装,内部函数( this.printSelf )所看到的上下文是 button,与我们的需求相符。

整体回顾

Class 的不同部分串连起来如下所示:

var Class = function (parent) {
    var _class = function () {
        if (parent && parent.prototype.init) this.superInit = parent.prototype.init;
        this.init.apply(this, arguments);
    };

    if (parent) {
        var subclass = function () {};
        subclass.prototype = parent.prototype;
        _class.prototype = new subclass;
    }

    _class.fn = _class.prototype;
    _class.fn.init = function () {};
    _class.fn.parent = _class;
    _class.fn._super = _class.__proto__;

    _class.proxy = function (func) {
        var self = this;
        return (function() {
            return func.apply(self, arguments);
        })
    }
    _class.fn.proxy = _class.proxy;

    _class.include = function (obj) {
        var included = obj.included;
        for(var key in obj) {
            _class.fn[key] = obj[key];
        }
        if (included) included(_class);
    }

    _class.extend = function (obj) {
        var extended = obj.extended;
        for(var key in obj) {
            _class[key] = obj[key];
        }
        if (extended) extended(_class);
    }
    return _class;
}
Copyright (c) 2014-2016 Kyles Light.
Powered by Tornado.
鄂 ICP 备 15003296 号