Как правильно получить объект с частными переменными, используя наследование javascript (прототип)

Я новичок в наследовании (прототипа) JavaScript и пытаюсь узнать об этом больше. В качестве примера я использую простой шаблон наблюдателя, в котором я хочу, чтобы наблюдаемые объекты были получены из объекта «субъект». Вот что я ХОЧУ сделать:

function subject()
{
    var callbacks = {}

    this.register = function(name, callback)
    {
        callbacks[name] = callback;
    }

    this.unregister = function(name)
    {
        delete callbacks[name];
    }

    var trigger = function()
    {
        var a = arguments;
        var t = this;

        $.each(callbacks, function(name, callback)
        {
            callback.apply(t, a);
        });
    }
}


list.prototype = new subject()

function list()
{
    var items = {}

    this.add = function(name, item)
    {
        items[name] = item;
        trigger('add', name);
    }

    this.remove = function(name)
    {
        delete items[name];
        trigger('remove', name);
    }
}

Теперь, используя приведенный выше код, как показано ниже, я сталкиваюсь со своей первой проблемой:

var l = new list()
l.register('observer1', function() { console.log(this, arguments) });
l.add('item1', 'value1'); // <-- ReferenceError: trigger is not defined, trigger('add', name);

Чтобы продолжить тестирование, я сделал функцию триггера общедоступной, используя вместо этого this.trigger. Запустив мой пример снова, я столкнулся со следующей проблемой:

var l = new list()
l.register('observer1', function() { console.log(this, arguments) });
l.add('item1', 'value1'); // <-- output: subject, ["add", "item1"]

Объект this — это subject, я хочу, чтобы он был list. Моя третья проблема возникает при создании другого списка:

var l2 = new list();
//Don;t register any observers
l2.add('item1', 'value1'); // <-- output: subject, ["add", "item1"]

Список callbacks является общим для двух списков.

Я пробовал подобные вещи с Object.create(new subject()) и сталкивался с похожими проблемами.

Мои 3 вопроса в этом:

  1. Могу ли я иметь частные методы, которые можно использовать в производных объектах (и должен ли я вообще заботиться о том, чтобы они были частными или общедоступными)?
  2. Как мне получить объект this, который я хочу (без использования function.call в производном объекте, если это возможно)?
  3. Как я могу сохранить список callbacks в базовом объекте без совместного использования?

person Mattie    schedule 09.12.2013    source источник


Ответы (3)


Интересный вопрос. Что касается № 1 и № 2: допустим, у вас есть функция foo:

function foo() {
 var _private = 'private var!';
 this.access = function () {
    return _private;
 }
}

access — это так называемый привилегированный метод, это замыкание, которое может получить доступ к приватной переменной private.

вы можете унаследовать все это, используя call, например:

function bar() {
  foo.call(this);
}

var b = new bar();
console.log(b.output()); // prints 'private var!'

С помощью методов apply, call и bind вы можете установить контекст функции, эффективно подделывая объект this. (ваш вопрос №2, прочитайте здесь )

Естественно, вы не можете использовать полностью закрытый метод в производном объекте. Вам понадобится метод доступа, который противоречил бы цели исходного метода, являющегося закрытым. Сказав это, так это работает и на строго типизированных языках (в java, если вы пометите метод как частный, даже подклассы не смогут получить к нему доступ, он должен быть защищен).

Что касается № 3, я не могу придумать, как сделать обратные вызовы общими и приватными. Но вы можете сделать его статическим свойством для всех экземпляров функции (так же, как статическое свойство в таком языке, как java), просто объявив функцию, например:

function foo() {
}

добавьте свои прототипы, которые будут назначены каждому экземпляру

foo.prototype.bar = // ...

и статическое свойство

foo.callbacks = [];

Все экземпляры foo будут совместно использовать свойство callbacks.

person Joe Minichino    schedule 09.12.2013
comment
Я попробовал ваше предложение, используя function bar() { foo.call(this); }, и это, похоже, также решает # 3. Триггер доступен в списке, а обратные вызовы устанавливаются для каждого списка. Единственная проблема в том, что this в list теперь subject. - person Mattie; 09.12.2013
comment
Мне удалось решить и это, добавив list.constructor = subject; list.prototype.constructor = list; из примера, но я не уверен, что он на самом деле делает. - person Mattie; 09.12.2013
comment
Я предлагаю вам взглянуть на это. ваши сомнения по поводу this :) - person Joe Minichino; 09.12.2013

У вас не может быть приватных методов, вот и всё. Он никогда не будет работать правильно и красиво одновременно, поэтому не пытайтесь эмулировать их в JavaScript.

Затем все, что вам нужно сделать, это вызвать родительский конструктор в производном конструкторе.

function subject()
{
    var callbacks = {};

    this.register = function(name, callback)
    {
        callbacks[name] = callback;
    };

    this.unregister = function(name)
    {
        delete callbacks[name];
    };

    this.trigger = function()
    {
        var a = arguments;
        var t = this;

        $.each(callbacks, function(name, callback)
        {
            callback.apply(t, a);
        });
    };
}


list.prototype = Object.create(subject);
list.prototype.constructor = list;

function list()
{
    subject.call(this);

    var items = {};

    this.add = function(name, item)
    {
        items[name] = item;
        this.trigger('add', name);
    };

    this.remove = function(name)
    {
        delete items[name];
        this.trigger('remove', name);
    };
}
person Ry-♦    schedule 10.12.2013
comment
Однако IE9 и более ранние версии не поддерживают Object.create. Кроме того, вы используете функцию субъекта для создания прототипа списка, это предназначено? list больше не является экземпляром subject. Функция триггера работает, если я делаю ее глобальной областью действия (триггер = вместо var trigger =). Проблема связана со списком обратных вызовов. Когда я делаю список обратных вызовов общедоступным и делаю Object.create(new subject()), все работает как надо. Не рад раскрытию списка обратных вызовов, но это придется сделать. - person Mattie; 10.12.2013
comment
@Mattie: Все это сделано намеренно, и Object.create(subject) достаточно близко к new subject(), поэтому вы можете использовать его вместо этого, если хотите совместимость с ≤IE9. Кроме того, list определенно instanceof subject, и все это сделано намеренно, да. - person Ry-♦; 11.12.2013

Включив предложение Джо, вот что я в конечном итоге получил:

function subject()
{
    var callbacks = {}

    this.register = function(name, callback)
    {
        callbacks[name] = callback;
    }

    this.unregister = function(name)
    {
        delete callbacks[name];
    }

    trigger = function()
    {
        var a = arguments;
        var t = this;
        $.each(callbacks, function(name, callback)
        {
            callback.apply(t, a);
        });
    }
}

//without the following line, 'this' in firefox is 'subject' instead of 'list' (in chrome it is)
list.prototype = new subject()
//without these, 'list' is not an instanceof 'subject'
list.constructor = subject;
list.prototype.constructor = list;

function list(n)
{
    this.name = n;
    subject.call(this); //as suggested by Joe
    var items = {}

    this.add = function(name, item)
    {
        items[name] = item;
        trigger.call(this, 'add', name); //no way to do this without using call/apply
    }

    this.remove = function(name)
    {
        delete items[name];
        trigger.call(this, 'remove', name); //no way to do this without using call/apply
    }

    this.getitems = function() { return items }
}

//without the following line, 'this' in firefox is 'subject' instead of 'queue'
queue.prototype = new subject()
//without these, 'queue' is not an instanceof 'subject'
queue.constructor = subject;
queue.prototype.constructor = queue;

function queue(n)
{
    this.name = n;
    subject.call(this); //as suggested by Joe
    var items = [];

    this.enqueue = function(item)
    {
        items.push(item);
        trigger.call(this, 'enqueue', item); //no way to do this without using call/apply
    }

    this.dequeue = function()
    {
        var d = items.shift();
        trigger.call(this, 'dequeue', d); //no way to do this without using call/apply
        return d;
    }

    this.getitems = function() { return items }
}

var l1 = new list('l1')

l1.register('observer1', function() { console.log('l1', this, arguments) });
l1.add('item1', 'value1');
// ^ 'l1', list { name = 'l1' ... }, ['add', 'item1']

var l2 = new list('l2')
l2.register('observer2', function() { console.log('l2', this, arguments) });
l2.add('item2', 'value2');
// ^ 'l2', list { name = 'l2' ... }, ['add', 'item2']

var q1 = new queue('q1')
q1.register('observer3', function() { console.log('q1', this, arguments) });
q1.enqueue('item3');
// ^ 'q1', queue { name = 'q1' ... }, ['enqueue', 'item3']

console.log(l1 instanceof list, l1 instanceof subject, l1 instanceof queue);
// ^ true, true, false
console.log(q1 instanceof list, q1 instanceof subject, q1 instanceof queue);
// ^ false, true, true

Это отвечает всем моим требованиям (кроме использования вызова, но я могу с этим смириться).

Спасибо за помощь,

Мэтти

EDIT: по-видимому, это не работает должным образом. создание нового объекта перезаписывает обратные вызовы других объектов

person Mattie    schedule 09.12.2013