Javascript-форум (https://javascript.ru/forum/)
-   ExtJS (https://javascript.ru/forum/extjs/)
-   -   Проблема с созданием класса - Значения свойств передаются другому экземпляру (https://javascript.ru/forum/extjs/57023-problema-s-sozdaniem-klassa-znacheniya-svojjstv-peredayutsya-drugomu-ehkzemplyaru.html)

khusamov 15.07.2015 17:50

Проблема с созданием класса - Значения свойств передаются другому экземпляру
 
Что я сделал не так?

В консоли должно быть пусто, а вместо этого [1, 2, 3, 4]
То есть в экземпляре s2 оказались члены массива из экземпляра s1...
Как такое могло получиться???

Код в песочнице https://fiddle.sencha.com/#fiddle/qgo

Ext.define("Subpath", {
	
	segments: [],
	
	add: function(segment) {
		this.segments.push(segment);
		return segment;
	}
    
});

Ext.application({
    name : 'Fiddle',
    launch : function() {
        
        var s1 = Ext.create("Subpath");
        s1.add(1);
        s1.add(2);
        s1.add(3);
        s1.add(4);
        
        var s2 = Ext.create("Subpath");
        
        console.log(s2.segments);
        
    }
});


Вот тут
http://docs.sencha.com/extjs/5.1/cor...s/classes.html
явно видно, что можно задавать значения по умолчанию для свойств класса.

novikov 15.07.2015 18:28

В документации видно, что такие свойства задаются через конструктор:

Ext.define('My.sample.Person', {
    name: 'Unknown',

    constructor: function(name) {
        if (name) {
            this.name = name;
        }
    },

    eat: function(foodType) {
        alert(this.name + " is eating: " + foodType);
    }
});

var bob = Ext.create('My.sample.Person', 'Bob');

bob.eat("Bob"); // alert("Bob is eating: Salad");


А не через самодельный сеттер. Интуитивно непонятно, конечно.

khusamov 15.07.2015 18:37

Строка 05 содержит условие if (name) {...},. То есть предполагается, что класс можно использовать так:

var noname = Ext.create('My.sample.Person');

В этом случае name будет содержать 'Unknown'.

Отсюда я делаю вывод, что строка 02 содержит значение по умолчанию.

Иначе бы они написали так:

name: undefined,

constructor: function(name) {
        if (name) {
            this.name = name;
        } else {
            this.name = 'Unknown';
        }
},

В этом случае видно, что значение по умолчанию задается только в конструкторе.

nohuhu 15.07.2015 21:14

Вы путаете конфигурационные свойства, которые задаются в блоке config: {} и могут наследоваться/объединяться, с обычными свойствами, которые никаким специальным образом не обрабатываются.

Класс Ext JS - это функция-конструктор; всё, что вы задаёте в блоке Ext.define(, {…}) попадает на прототип этого конструктора. Экземпляр этого класса, соответственно, это просто объект JavaScript, который через __proto__ показывает на соответствующий класс.

Ну и как отсюда очевидно, если у вас в *экземпляре* класса (т.е. в объекте) нет своего свойства segments, то будет использоваться свойство segments с *прототипа*. Т.е. каждый экземпляр класса Subpath будет обращаться к одному и тому же массиву, наступая друг другу на ноги. Обычный JavaScript, ничего сложного. ;)

Решение простое: в constructor присваивайте экземпляру все свойства, которые должны быть уникальными для каждого экземпляра.

Ext.define('Subpath', {
    /**
     * @property {Number[]} segments Тут живут сегменты
     */

    constructor: function() {
        this.segments = [];
    }
});


Что касается примера г-на novikov, то это как раз и есть случай конфигурационного свойства. В таких случаях велосипед изобретать не надо, а лучше использовать блок config:

Ext.define('My.sample.Person', {
    config: {
        name: 'Unknown' // значение по умолчанию
    }
});

// Значение не передано, будет использовано по умолчанию
var unknown = new My.sample.Person();
console.log(unknown.getName()); // Unknown

// А тут мы передаём значение явно
var foo = new My.sample.Person({ name: 'foo' });
console.log(foo.getName()); // foo


Особо отмечу, что первый и второй случаи друг другу совершенно не противоречат; segments это *внутреннее* свойство объекта, а name это конфигурационное свойство. Использовать и те и другие в одном объекте это совершенно обычное дело, надо просто понимать разницу.

khusamov 15.07.2015 21:26

Цитата:

Ну и как отсюда очевидно, если у вас в *экземпляре* класса (т.е. в объекте) нет своего свойства segments, то будет использоваться свойство segments с *прототипа*. Т.е. каждый экземпляр класса Subpath будет обращаться к одному и тому же массиву, наступая друг другу на ноги. Обычный JavaScript, ничего сложного. ;)
Не понятно как система классов Ext JS допустила эту странную ситуацию. Зачем такое может вообще понадобиться? Чтобы иметь доступ к общему массиву (или что там будет вложено в свойство).

На мой взгляд не совсем логично, что свойства надо создавать в конструкторе (точнее с точки зрения JS нормально, но с точки зрения обычного ООП не привычно, а ведь Ext JS пытается имитировать обычный ООП). И если забудешь создать, то будет ссылка на общее значение.

Кстати, я до сих пор не сталкивался с этой проблемой, а ведь уже сколько подобных свойств я насоздавал и пока не напарывался... Придется всю толпу классов перерыть и всюду сделать инициализацию в конструкторе... неслабо однако...

Кстати, а где об этом написано в документации?

khusamov 15.07.2015 21:32

Кстати, в свете новых сведений о создании свойств не ясно как создавать приватные свойства.

Ext.define(..., {

privates: {...}

});


Ведь в блоке privates их объявлять нельзя получается. Только разве комментарий можно. А в конструкторе похоже можно создавать публичные свойства. Бардачок намечается чтоли?

nohuhu 15.07.2015 23:57

Цитата:

Сообщение от khusamov (Сообщение 379852)
Не понятно как система классов Ext JS допустила эту странную ситуацию. Зачем такое может вообще понадобиться? Чтобы иметь доступ к общему массиву (или что там будет вложено в свойство).

Подождите, а как вы предполагаете обойти базовые свойства языка в библиотеке? :))

Цитата:

На мой взгляд не совсем логично, что свойства надо создавать в конструкторе (точнее с точки зрения JS нормально, но с точки зрения обычного ООП не привычно, а ведь Ext JS пытается имитировать обычный ООП). И если забудешь создать, то будет ссылка на общее значение.
Ну, вот такой вот язык этот JavaScript. Выбора какбэ нет, скорбь и пичалька.

Цитата:

Кстати, я до сих пор не сталкивался с этой проблемой, а ведь уже сколько подобных свойств я насоздавал и пока не напарывался... Придется всю толпу классов перерыть и всюду сделать инициализацию в конструкторе... неслабо однако…
Большая часть таких конструкций может работать просто по случайности, как оно обычно и бывает. Если не создаёте более одного экземпляра класса одновременно, то и проблем как будто бы нет. У вас ведь юнит-тесты последовательно исполняются, правильно? Обычно все так и делают, поэтому проблема и не вылазит. Точнее, не вылазит в разработке, а вот внедрение уже бывает бодрым и весёлым. :))

Цитата:

Кстати, а где об этом написано в документации?
В документации не написано просто потому, что не имеет смысла повторять учебник JavaScript. А вот в каких-нибудь вводных статьях может и упомянуто, но я их никогда не читал. :)

Цитата:

Ведь в блоке privates их объявлять нельзя получается. Только разве комментарий можно. А в конструкторе похоже можно создавать публичные свойства. Бардачок намечается чтоли?
Я вам страшную тайну открою: у объектов JavaScript вообще не бывает приватных свойств. То есть совсем. Ну, вот язык такой. Есть closures, но это не совсем то (точнее, совсем не то). А то, что в Ext JS классовая система поддерживает приватные методы, так это просто хак для облегчения нашей с вами жизни. И то больше нашей, чем вашей. :)

Практически единственный смысл блока privates - это дать вам, как пользователю, возможность узнать о том, что в вашем наследованном классе есть метод с таким же названием, как и приватный метод в родительском классе. В отличие от публичного API, приватные методы не гарантированы по прямой и обратной совместимости между версиями; т.е. мы оставляем себе право их менять как нам заблагорассудится. Мы слишком часто натыкались на ситуации, когда пользователи в своём коде называли методы так же, как и мы в своём. Результаты бывают феерически прекрасны, но очень трудоёмки к поимке.

Из этой проблемы и растут ноги у блока privates. Это наш способ дать вам знать: вот этот метод - он приватный, не трогайте его. А если потрогаете и ему не понравится, то мы не виноваты. :)

Makarov 16.07.2015 12:09

nohuhu,
я каждый раз когда ваши сообщения в разделе читаю, жалею что движок форума не разрешает одному человеку много плюсов ставить =)

khusamov 16.07.2015 13:06

Цитата:

Я вам страшную тайну открою: у объектов JavaScript вообще не бывает приватных свойств. То есть совсем.
Я наверное плохо донес... это свойство JS я в общем-то знаю. Просто я считал, что классовая система Ext исправила ситуацию.

Спасибо! Теперь есть чем заняться. Буду все написанные классы исправлять, там много у меня подобных свойств.

Цитата:

Подождите, а как вы предполагаете обойти базовые свойства языка в библиотеке? :))
Мне сейчас придется начальные значения этих свойств описывать в конструкторе. По идее, классовая система Ext может это делать за меня, если в конфиге класса найдет такие свойства. Хотя бы так.

Но это не принципиально, просто заранее надо знать, что этот момент Ext не решает и надо самому не попасться.

khusamov 16.07.2015 13:09

Цитата:

Сообщение от Makarov (Сообщение 379965)
nohuhu,
я каждый раз когда ваши сообщения в разделе читаю, жалею что движок форума не разрешает одному человеку много плюсов ставить =)

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

Makarov 16.07.2015 13:57

khusamov,
Там надо много людей плюсануть чтобы повторно можно было, человек 10 наверное, точно не считал

nohuhu 16.07.2015 21:37

Цитата:

Сообщение от khusamov (Сообщение 379976)
Я наверное плохо донес... это свойство JS я в общем-то знаю. Просто я считал, что классовая система Ext исправила ситуацию.

Честно сказать, мы бы исправили, если бы это было возможно. Но это не единственный косяк JavaScript, с которым приходится жить, поэтому проще сжать булки и иметь в виду.

Цитата:

Спасибо! Теперь есть чем заняться. Буду все написанные классы исправлять, там много у меня подобных свойств.
Лучше поздно в цикле разработки, чем после внедрения. Поверьте моему больному опыту. ;)

Цитата:

Мне сейчас придется начальные значения этих свойств описывать в конструкторе. По идее, классовая система Ext может это делать за меня, если в конфиге класса найдет такие свойства. Хотя бы так.
Неа-неа. Это вам кажется, что всё просто и классовая система может всё за вас сделать. В отдельно взятом простом случае, оно может быть и так. Но на всю библиотеку этот подход не масштабируется никак.

Цитата:

Но это не принципиально, просто заранее надо знать, что этот момент Ext не решает и надо самому не попасться.
Вы всё же путаете конфиги и обычные свойства. Для конфигов наследование как раз делается классовой системой:

Ext.define('Foo', {
    config: {
        blerg: {
            frobbe: 'throbbe'
        }
    }
});

Ext.define('Bar', {
    extend: 'Foo',

    config: {
        blerg: {
            qux: 'xyzzy'
        }
});

var beer = new Bar({
    blerg: {
        foo: 42
    }
});

console.log(beer.getBlerg());
// {
//     foo: 42,
//     frobbe: 'throbbe',
//     qux: 'xyzzy'
// }

var gurgle = new Bar({
    blerg: {
        frobbe: 'zingbong'
    }
});

console.log(gurgle.getBlerg());
// {
//     frobbe: 'zingbong',
//     qux: 'xyzzy'
// }


Ну, я малость разошёлся с именами метапеременных, но надеюсь, что понятно всё же будет. ;)

nohuhu 16.07.2015 21:40

Цитата:

Сообщение от Makarov (Сообщение 379965)
nohuhu,
я каждый раз когда ваши сообщения в разделе читаю, жалею что движок форума не разрешает одному человеку много плюсов ставить =)

Спасибо, но вы не переживайте. Я тут не кармы ради, а помощи для. :)

khusamov 17.07.2015 00:34

о как, конфиг теперь рекурсивно обрабатывается???
раньше такого не было.

nohuhu 17.07.2015 02:11

Ээээ, ну, не совсем рекурсивно… Там всё чуть сложнее. :) Эта система притащена в Ext JS 5.0 из Sencha Touch и (гхм) слегка допилена по месту. Делать с ней можно много весёлых штук для фана и профита, но лучше ограничиться документированным применением, примерно как я показал выше.

Ещё ньюанс: с массивами в конфигах надо быть осторожнее. Они, конечно, формально тоже объекты, но с массивами связка по прототипу не работает, и результат может быть непредсказуем. Это можно обойти, но я не уверен, что эти инструменты входят в публичное API.

khusamov 17.07.2015 14:23

Цитата:

Ещё ньюанс: с массивами в конфигах надо быть осторожнее
Вообще-то у меня много массивов в конфигах... тоже ожидать сюрпризов как и с обычными свойствами???

nohuhu 17.07.2015 21:17

Если вы не помещаете массивы в свойства блока config, то никакой специальной магии к ним применяться не будет. Это будут простые свойства объекта, которые совершенно одинаковым образом работают на чтение - при этом неважно, где именно свойство находится, на экземпляре объекта или на прототипе. Сюрпризы могут быть, как вы уже выяснили, если пытаться в такие свойства записывать; но мы же о конфигах говорим, правильно? Конфиги по определению должны только читаться.

Если же вам надо помещать массивы в свойства блока config, то специальную магию надо будет слегка подправить напильничком, чтобы она работала как надо. Если не подправить, то из коробки может и не заработать. Со свойствами-объектами всё достаточно просто, их связывают по прототипной цепочке и они "просто работают". С массивами такого очевидного варианта нет.

khusamov 20.07.2015 19:48

Цитата:

Конфиги по определению должны только читаться.
Как так? А для чего тогда создаются методы setParam() для каждого param из раздела config? Для чего создаются методы applyParam, updateParam?

Я все этим методы активню пользуюсь, а теперь выясняется что нельзя? Они только для чтения???

khusamov 20.07.2015 19:51

Цитата:

Если же вам надо помещать массивы в свойства блока config, то специальную магию надо будет слегка подправить напильничком, чтобы она работала как надо.
Конечно надо. У меня там целая прорва свойств с массивами (в config, не в обычных свойствах). К счастью пока проблем с ними не было, но то что нужно оказывается что-то допиливать напильником - настораживает. И что конкретно нужно менять-доделывать, чтобы это все не развалилось???

nohuhu 21.07.2015 21:21

Цитата:

Сообщение от khusamov (Сообщение 380532)
Как так? А для чего тогда создаются методы setParam() для каждого param из раздела config? Для чего создаются методы applyParam, updateParam?

Я все этим методы активню пользуюсь, а теперь выясняется что нельзя? Они только для чтения???

Перечитал своё сообщение, понял что мысль выразил не доходчиво. Попытка №2:

Система конфигурации в Ext JS 5 имеет основной целью конфигурирование экземпляров объектов. Конфигурация каждого экземпляра может динамически меняться, при этом конфигурация других объектов того же класса изменяться не должна. Отсюда логически следует, что конфигурационные свойства классов должны быть доступны только для чтения, в отличие от объектов.

В теории это всё звучит хорошо, но на практике у нас есть JavaScript и только одна схема наследования, прототипная. Это означает, что наследование конфигов можно сделать двумя способами: либо связывая конфиги-объекты по прототипной цепочке с соответствующими конфигами классов, либо тупо копируя все свойства для каждого нового экземпляра. Второй способ отпал по причине низкой производительности, поэтому остаётся первый.

Если конфигурационные объекты по прототипу не связывать, то для каждого экземпляра объекта будут доступны только те свойства, которые содержатся в конфиге самого экземпляра - наследованные свойства доступны не будут. Именно так это работало в Ext JS 4:

Ext.define('Bar', {
    blerg: {
        frobbe: 'throbbe'
    }
});

var beer = new Bar({
    blerg: {
        qux: 'dux'
    }
});

console.log(beer.blerg);
// {
//     qux: 'dux'
// }


Как видите, никакого наследования быть не может, т.к содержимое переменной beer это объект, прототипом которого является объект Bar. У каждого из этих объектов есть свойство blerg, и использоваться будет "верхнее" значение в прототипной цепочке.

Блок config в Ext JS 5 работает по-другому и свойства-объекты связываются по прототипу, чтобы обеспечить наследование конфигов. Заметьте, что все эти пространные рассуждения никак не относятся к тривиальным значениям конфигов: строкам, числам, etc. С ними всё просто. Но объекты наследуются:

Ext.define('Foo', {
    config: {
        blerg: {
            zilch: 42
        }
    },

    // Это нужно только в данном примере;
    // в обычном случае initConfig уже вызывается
    // родительским классом (Ext.Component, etc)
    constructor: function(config) {
        this.initConfig(config);
    }
});

var foo = new Foo({
    blerg: {
        bar: 'qux',
        zilch: 43
    }
});

console.log(foo.getBlerg());
// {
//     bar: 'qux',
//     zilch: 43
// }


В этом примере объект в переменной foo имеет прототипом объект Foo, оба имеют конфиг blerg. Этот конфиг, в свою очередь, это тоже объект, прототипом которого является объект конфига blerg в объекте Foo. Свойство zilch имеется в обоих конфигах, но "верхнее" в цепочке принадлежит конфигу foo, поэтому оно и используется; свойство bar в конфиге Foo отсутствует и есть только в конфиге foo.

Поскольку конфиги динамические, то их можно менять, но только для экземпляра объекта:

foo.setBlerg({ zilch: 44 });

foo.getBlerg();
// {
//     zilch: 44
// }


Если вы проинспектируете конфиг объекта Foo, то увидите, что он не изменился. Конфигурация происходит только на уровне экземпляра, но не класса.

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

Ext.define('Baz', {
    config: {
        qux: [42]
    },

    constructor: function(cfg) {
        this.initConfig(cfg);
    }
});

var bazz = new Baz();

bazz.getQux();
// [42]

var brazz = new Baz({ qux: [-1] });

brazz.getQux();
// [-1]


Специальная магия нужна в случае, если вы хотите, чтобы массив был объединением элементов из наследованных конфигов с элементами из массива на экземпляре:

Ext.define('MagicParent', {
    config: {
        qux: {
            $value: [42],
            merge: function(newValue, oldValue) {
                // Пристёгиваем новые элементы к старым
                return (oldValue || []).concat(newValue);
        }
    },

    constructor: function(cfg) {
        this.initConfig(cfg);
    }
});

Ext.define('MagicChild', {
    extend: 'MagicParent',

    config: {
        qux: [43]
    }
});

var magic = new MagicChild({ qux: [44] });

magic.getQux();
// [42, 43, 44]

// Значения, переданные в setter, считаются полными
// и через цепочку связываний не проходят:
magic.setQux([-1]);

magic.getQux();
// [-1]


Надеюсь, теперь будет понятнее. В целом система конфигов не безобразно сложна к пониманию, но имеет некоторые особенности, поэтому и остаётся большей частью приватной. Если разобраться, можно творить чудесатые чудеса. :)


Часовой пояс GMT +3, время: 02:05.