通过从构造函数返回现有实例来实现javascript实例存储

时间:2012-06-21 19:13:27

标签: javascript backbone.js

我正在尝试在Backbone.js中实现我的版本“Instance Store”,正如Soundcloud在他们最近的博客文章中描述的那样:

http://backstage.soundcloud.com/2012/06/building-the-next-soundcloud/

相关摘录:

要解决这个问题,我们使用一个我们称之为实例存储的构造。此存储是一个对象,每次调用模型的构造函数时都会隐式访问和修改该对象。当第一次构建模型时,它将自身注入到商店中,使用其id作为唯一键。如果使用相同的id调用相同的模型构造函数,则返回原始实例。

var s1 = new Sound({id: 123}),
    s2 = new Sound({id: 123});

s1 === s2; // true, these are the exact same object.

这是因为Javascript的一个鲜为人知的鲜为人知的功能。如果构造函数返回一个对象,那么这就是赋值。因此,如果我们返回对先前创建的实例的引用,我们将获得所需的行为。在幕后,构造函数基本上是这样做的:

var store = {};

function Sound(attributes) {
    var id = attributes.id;

    // check if this model has already been created
    if (store[id]) {
        // if yes, return that
        return store[id];
    }
    // otherwise, store this instance
    store[id] = this;
}

我通过覆盖Backbone.Model类来实现我的版本,以创建我自己的构造函数。

var MyModel = Backbone.Model.extend({
    constructor: function (attributes, options) {
        var id = attributes ? attributes.id : undefined;

        if (this.store[id]) {
            return this.store[id];
        }

        Backbone.Model.prototype.constructor.apply(this, arguments);

        if (id) {
            this.store[id] = this;
        }
    }
});

var MyOtherModel = MyModel.extend({
    store: {},

    //other model stuff
});

这工作得很好,但有些事情必须改变,现在它已停止工作了,我不确定为什么。新创建的实例存储在store对象中没有问题 - 扩展MyModel类的每个类都有自己的空存储,以避免具有相同id的不同类型的实例发生冲突。当使用现有id调用构造函数时,也会检索正确的实例,但是当从构造​​函数返回它们时,将忽略返回值。我对规范的理解是构造函数可以返回一个对象 - 但不是一个基元 - 当使用new运算符调用构造函数时,返回的对象将被赋值给赋值语句的左侧。这种情况不会发生,即使构造函数返回一个对象,也会使用new运算符创建的空对象。

一些调试信息。不确定这些信息会有多大帮助。这是MyModel构造函数中的“this”,用于第一次实例化的对象。

child
    _callbacks: Object
    _escapedAttributes: Object
    _previousAttributes: Object
    _setting: false
    attributes: Object
        id: "4fd6140032a6e522f10009ac"
        manufacturer_id: "4f4135ae32a6e52a53000001"
        name: "Tide"
        uniqueName: "tide"
    __proto__: Object
    cid: "c50"
    collection: child
    id: "4fd6140032a6e522f10009ac"
    __proto__: ctor
        constructor: function (){ parent.apply(this, arguments); }
        defaults: Object
        store: Object
        url: function () {
        urlRoot: function () {
        __proto__: ctor

当MyModel构造函数是从实例存储返回的对象时,这是“this”:

child
    _callbacks: Object
    _escapedAttributes: Object
    _previousAttributes: Object
    _setting: false
    attributes: Object
        _validate: function (attrs, options) {
        bind: function (events, callback, context) {
        change: function (options) {
        changedAttributes: function (diff) {
        clear: function (options) {
        clone: function () {
        constructor: function (){ parent.apply(this, arguments); }
        defaults: Object
        destroy: function (options) {
        escape: function (attr) {
        fetch: function (options) {
        get: function (attr) {
        has: function (attr) {
        hasChanged: function (attr) {
        idAttribute: "id"
        initialize: function (){}
        isNew: function () {
        isValid: function () {
        manufacturer_id: 0
        name: ""
        off: function (events, callback, context) {
        on: function (events, callback, context) {
        parse: function (resp, xhr) {
        previous: function (attr) {
        previousAttributes: function () {
        save: function (key, value, options) {
        set: function (key, value, options) {
        store: Object
        toJSON: function () {
        trigger: function (events) {
        unbind: function (events, callback, context) {
        unset: function (attr, options) {
        url: function () {
        urlRoot: function () {
        __proto__: Object
        cid: "c141"
     __proto__: ctor
        constructor: function (){ parent.apply(this, arguments); }
        defaults: Object
        store: Object
        url: function () {
        urlRoot: function () {
        __proto__: ctor

我注意到第二个中的属性对象具有包含在其中的骨干对象的所有方法,它们不应该是。它也没有id,我不知道为什么。希望这提供了一些见解。感谢。

3 个答案:

答案 0 :(得分:3)

我不会使用extend,我认为拥有一个单独的“工厂”是正确的想法。它可以让你扩展你的模型而不用担心副作用。

annotated source骨干做了一些奇怪的东西延伸,我还没有完全围绕它。 (另请查看inherits)所以我们暂时跳过它并坚持使用您的工作解决方案。

我已经修改了你的方法以生成工厂模型,你应该能够像普通模型一样使用它们(例如,将它们设置在集合上),除非扩展它们不起作用。他们还会使用像soundcloud示例那样的新数据来处理模型更新。

var makeStoreable = function(model){
  var StoreModel = function(attr, opt){
    if(!attr || !attr.id){
      // The behavior you exhibit here is up to you
      throw new Error('Cool Models always have IDs!');
    }
    if(this.store[attr.id]){
      this.store[attr.id].set(attr, opt);
    }else{
      var newModel = new model(attr, opt);
      this.store[attr.id] = newModel;
    }
    return this.store[attr.id];
  };
  StoreModel.prototype.store = {};
  return StoreModel;
};

var CoolModel = Backbone.Model.extend({});

CoolModel = makeStoreable(CoolModel);

var a = new CoolModel({
    id: 4,
    coolFactor: 'LOW'
});

var b = new CoolModel({
    id:4,
    coolFactor: 'HIGH'
});

console.log(a===b); //true!
console.log(a.get('coolFactor') === 'HIGH'); //true!

here's a fiddle一起玩。

此外,我欢迎有人提出一个模型解决方案,将“商店”保留在模型实例的原型中。另外,为了防止内存泄漏,我们应该在工厂或模型本身创建一个引用计数destroy方法。

答案 1 :(得分:2)

@ wizard的方法看起来非常干净整洁。 +1。那个。

它在SoundCloud中的实现方式是重写Backbone.Model.extend方法,用我们修改过的构造函数和闭包中的商店创建一个类。该商店最初是在一个闭包中创建的,以保持类的界面干净,但过了一段时间后,它被发现对调试有用,可以引用每个类的商店,所以它也附在那里

我们确实有一个引用计数,以便内存使用不会爆炸,并且还为类提供了定义自定义函数的能力,该函数提供了识别它的唯一值。大多数时候id已经足够了,但是在某些极端情况下它并没有完全解决。

  

我欢迎有人提出一个模型解决方案来保持"存储"在模型实例的原型

你可以myInstance.constructor.store

答案 2 :(得分:0)

使用@reconbot解决方案后,我发现它破坏了instanceof运算符:

(new CoolModel) instanceof CoolModel // FALSE!!!

var MyModel = Backbone.Model.extend({
    idAttribute: 'myId'
});
new MyModel({ myId: 1 }) === new MyModel({ myId: 1 }) // FALSE!

我开发了一个新版本,它使用模型自己的id属性(通过idAttribute)并使用instanceof并允许你扩展工厂:

FIDDLE

function makeStoreable(model) {
    var store = {};
    var idField = model.prototype.idAttribute;

    function ModelFactory(attr, opt) {
        if (!attr || !(idField in attr)) {
            throw new Error('Cool Models always have IDs!');
        }

        var id = attr[idField];

        if (store.hasOwnProperty(id)) {
            store[id].set(attr, opt);
        } else {
            model.call(this, attr, opt);
            store[id] = this;
        }

        return store[id];
    }

    function intermediate() {}
    intermediate.prototype = model.prototype;
    ModelFactory.prototype = new intermediate;

    // Only EcmaScript5!
    // ModelFactory.extend = model.extend.bind(model);
    ModelFactory.extend = function() {
        return model.extend.apply(model, arguments);
    };

    return ModelFactory;
}

测试:

var RareID = Backbone.Model.extend({
    idAttribute: '_myOwnServerId'
});
RareID = makeStoreable(RareID);

var a = new RareID({
    _myOwnServerId: 4,
    coolFactor: 'LOW'
});

var b = new RareID({
    _myOwnServerId: 4,
    coolFactor: 'HIGH'
});

console.log(a===b); //true!
console.log(a instanceof RareID); //true!
console.log(a.get('coolFactor') === 'HIGH'); //true!

:)