Comments 57
var obj = {
get youcantmodifyme() {
return 1;
}
}
obj.youcantmodifyme=5;
console.log(obj.youcantmodifyme); //1
obj={};
console.log(obj.youcantmodifyme); //undefined
Для массивов можно провернуть некислой хаковатости трюк(так делать не надо!):
var q=[1,2,3];
var settings={};
for(var i in q){
settings[i]={
value: q[i],
writeable: false
}
}
var p=Object.defineProperties({}, settings); //а затем из Array тырим нужные методы по типу push. P, кстати говоря, Object, а не Array, зато поля немодифицируемые(ну как немодифицируемые - те, что есть, немодифицируемые, новые добавлять можно спокойно).
А самый красивый, пожалуй, способ для ES6(причем и для массивов, хотя и возвращает объект) — Proxy(спасибо stackoverflow за примеры):
var s=[1,2,3];
var arr = new Proxy(s, {
get: function(target, name) {
return target[name];
},
set: function(target, name){
return 1;
}
});
arr.push(1); //[1,2,3]
arr[1]=100; //[1,2,3]
arr[10000]=100000000; //а вот хрен, все еще [1,2,3]
(Оригинал — http://stackoverflow.com/questions/2449182/getter-setter-on-javascript-array).
А вообще, спасибо за статью, познавательно. Про Object.freeze вообще не знал.
Спасибо за предложение — про иммутабельные поля на getter/setter обязательно добавлю. Про Proxy — не знаю, стоит ли. Поддержка в браузерах пока, что оставляет желать лучшего http://caniuse.com/#feat=proxy
Да — для node.js использовать Proxy — не проблема. А вот насчет шимов и полифилов — для Proxy они невозможны
Простое и быстрое отслеживание изменений
Тоже ложь:
function threshold (before) {
return {
r : before.r > 100 ? 255 : 0,
g : before.g > 100 ? 255 : 0,
b : before.b > 100 ? 255 : 0,
};
}
const idealBefore = { r: 105, g: 0, b: 0 };
const idealAfter = threshold(idealBefore);
idealBefore === idealAfter //false
idealBefore.r === idealAfter.r //false
idealBefore.g === idealAfter.g //true
idealBefore.b === idealAfter.b //true
// epic fail:
const before = { r: 0, g: 0, b: 0 };
const after = threshold(before);
before === after //false
before.r === after.r //true
before.g === after.g //true
before.b === after.b //true
Так что процедуру `
areColorsEqual
` писать придется, как и в любом другом подходе.В нормальной реализации объект {r: 0, g: 0, b: 0} будет зарезирвирован в памяти
А есть такие реализации? О.о
А если это коодината в игре с флоат-значениями
{ x: 0.1, y: 0.2 }
— тоже все кешироваться будут?А профит от кеша — быстрое сравнение естественно.
Кстати, в $mol мы решаем эту проблему довольно элегантно. Например, можно ввести глобальный реестр точек:
class $mol_point extends $mol_object {
@ $mol_mem_key()
item( point : { x : number , y : number } ) {
return point
}
}
Обращение к реестру с одними и теми же параметрами будет возвращать один и тот же объект:
$mol_point.item({ x : 1 , y : 2 }) === $mol_point.item({ x : 1 , y : 2 }) // true
Под капотом $mol_mem_key создаёт $mol_atom, который отслеживает зависимости от этой точки и если зависимостей не останется — соответствующий кеш будет подчищен.
$mol_atom
может знать, что больше никто не запросит $mol_point.item({ x : 1 , y : 2 })
Что никто не запросит — не знает. А вот, что нигде сейчас не используется (при учёте, что все, кому надо обеспечить кешируемость, дёрнули $mol_point.item
) — вполне.
Например, рендерим в компонент координаты:
class $my_hint extends $mol_view {
@ $mol_mem()
point( next : { x : number , y : number } ) {
return $mol_point.item( next || { 0 : 0 , y : 0 } )
}
sub() {
console.log( this.point() )
return [ JSON.stringify( this.point() ) ]
}
}
Теперь, если мы при передадим эквивалентный объект, то ничего не произойдёт:
hint.point({ x : 0 , y : 0 })
А если неэквивалентный, то будет ререндеринг:
hint.point({ x : 1 , y : 2 })
//{ x : 1 , y : 2 }
Соответственно, $mol_view рендерится в рамках вычисления другого атома, который "держит" свои зависимости от самоуничтожения, поэтому при следующем обновлении соответствующий объект будет найден в кеше и возвращён вместо того, что мы передали в $mol_point.item и никакого ререндеринга произведено не будет:
hint.point({ x : 1 , y : 2 })
Соответственно, в этот момент в кеше будет только { x: 1, y: 2 }, а { x: 0, y: 0 } — нет, так как в рантайме нигде сейчас не используется.
Всегда придерживался концепции использования immutable-структур только там, где без них никак, и эта статья лишний раз убедила меня в правильном выборе концепции.
В принципе, за исключением особых случаев, скажу так: иммутабельность в JavaScript — еще один ненужный велосипед, коих в последнее время ну очень много.
Безопаснее использовать и легче тестировать
В описании есть аргумент только для «Безопаснее». А чем легче тестировать?
Безопаснее использовать, и поэтому легче тестировать.
Предположим, первый тест вам массив случайно испортил, отсортировал его, а второй тест ожидает элементы в исходном порядке, а на отсортированном массиве он сломается. Т.е. ситуация такая — обвалился второй тест, а виноват первый. Причину поломки будет найти не так просто.
А в пределах одного теста слишком мало изменений, чтобы сложно было найти поломку.
И аргумент вообще выглядит как «ну потому что мутабельность это плохо в целом». А тесты сюда притянуты за компанию.
Я не фанат иммутабельности. Просто мне приходится использовать React + Redux. А там без иммутабельности никуда, на ней все держится. Я прекрасно понимаю, что мутабельность в большинстве случаев быстрее и лучше. Но не в React + Redux, а это нынче модное направление, и многим людям придется столкнуться иммутабельностью из-за этого.
Ситуация с тестами все таки возможна, и даже если найти поломку получится быстро — все равно это не очень приятный момент
Для меня есть только один весомый аргумент в пользу иммутабельности: потокобезопасность. Да, в JS потоков нет, и отсюда вывод: в JS иммутабельность не нужна.
Конечно, понятность кода и защита от дурака тоже важны, но не ценой тех накладных расходов, которые иммутабельность порождает.
Старшему разработчику достаточно написать коммент при объявлении переменной: // immutable, — вот и вся реализация. А младший разработчик, использующий иммутабельные структуры данных, будет писать работающий код, но очень неэффективный (то есть всего лишь одной строкой кода он может совершенно ненарочно породить жуткие тормоза). Получается, ни тому, ни другому строгая реализация иммутабельности не поможет.
Все равно, обзор очень интересный, спасибо!
Биндинг через события порождает много хрупкого кода, который сложно поддерживать. Подробнее в статье про атомы.
Я не говорю, что Angular плох, просто он не для каждого приложения подходит.
- Ангуляр не отслеживает зависимости.
- У Ангуляра нет ограничений на число вотчеров.
- События отлаживать не менее сложно.
- События триггериться тоже могут в неочевидном порядке.
2. Есть. Попробуйте завести 100000+ вотчеров на странице и запустить дайджест
3. Менее. Стек вызовов для обработчика события покажет, где оно возникло, если только вы не используете отложенную обработку
4. Приведите пример
- "у метода отслеживания изменений недостатков больше. Взять тот же Angular"
- Так и что произойдёт? В любом случае 100к — не похоже на "жёсткое" ограничение.
- И конечно же он покажет, почему обработчик события вызывается в контексте уничтоженного объекта. :-)
- Например, в порядке создания/активации вьюшек пользователем, а не в порядке их следования в коде.
2. Тормоза будут все сильнее с каждым новым вотчем. С событиями же никаких тормозов не будет, пока память не закончится. Тормоза не накапливаются
3. В каком смысле «уничтоженного»? Что такое «уничтоженный объект»?
4. Если речь о синхронном создании вьюшек, то они создаются/активируются в том порядке, в котором это написано в коде. Если об асинхронном — другое дело, согласен, придется повозиться, чтобы разобраться, что к чему. Но отслеживание изменений в качестве альтернативы событиям здесь не поможет — скорее, наоборот, усугубит проблему
- А, тогда сори. :-)
- Ну, то есть никакого ограничения всё же нет. :-) То, что там алгоритмическая сложность выше — другой вопрос. Это плата за простоту отслеживания изменений.
- Например, это вьюшка удалённая из дерева.
- Используя функциональную парадигму совершенно не важно в каком порядке вызываются обработчики. А вот с императивной это да, проблема.
4. А, я наконец-то понял, о чем вы. Ну, да, если вы математически докажете чистоту функций всех своих зависимостей, то порядок обработки будет неважен. О чем-то подобном, вроде бы, идет речь во втором ангуляре, но я с этим пока не ознакомился. В первом же это была головная боль.
Используя функциональную парадигму совершенно не важно в каком порядке вызываются обработчики. А вот с императивной это да, проблема.
Простите, а почему не важно? Вот у меня есть пошаговая игра (я делал что-то похожее).
Абилка А увеличивает урон на 2, абилка Б уменьшает урон в два раза.
Был урон 4. Сработало событие НаноситсяУрон.
Вариант 1.
Срабатывает абилка А потом Б. Урон = (4+2)/2 = 3
Вариант 2.
Срабатывает абилка Б потом А. Урон = 4/2+2 = 4
Тут хоть ФП, хоть ООП. Результат зависим от порядка.
Или, скажем (исскуственный пример) событие «Добавить текст». Один обработчик очищает от html-тегов, второй заменяет markdown-code на html-теги. Функции вполне будут чистыми, но порядок важен. Объясните, почему вы говорите, что в ФП порядок не важен?
У вас оба варианта императивны и как раз показывают проблему императивного кода — порядок важен :-)
С ФП будет что-то типа:
Срабатывают абилка А, потом Б. Урон = вычислить_урон( список абилок, состояние ) = 4 +2 +4/2 = 8
Срабатывают абилка Б, потом А. Урон = вычислить_урон( список абилок, состояние ) = 4 +2 +4/2 = 8
Либо нам нужно явно задать порядок применения абилок.
Срабатывают сначала аддитивные абилки, потом мультипликативные. Урон = (4+2)/2 = 3
По второму примеру та же ситуация — вы императивно меняете состояния. В функциональном у вас будет 3 отдельных свойства:
this.sourceText() { '<b>hello'</b>' }
— исходный текст
this.sanitizedtext() { return this.sourceText().replace( /<.*?>/g, '' ) ) }
— очищенный текст
this.sanitizedAndMarkdownedText() { return Markdown.makeHTML( this.sanitizedText() ) ) }
— готовый html.
Разумеется в данном случае никакие перестановки не возможны. Переставлять можно лишь функционально независимые вычисления.
У вас оба варианта императивны и как раз показывают проблему императивного кода — порядок важен :-)
Подождите, там вообще кода нету. Пока все на уровне дизайна, который говорит, что порядок срабатывания абилок важен.
Я, к сожалению, не понял, как у вас получилась восьмерка.
У нас есть, допустим, бизнес-требование — абилки срабатывают в порядке, как техника выводится на поле. При событийной модели они как раз будут подписываться в таком порядке, при функциональном подходе — в новом списке новый танк со своей абилкой будет добавлен в конец. То есть и там и там порядок важен и соблюдается приблизительно одинаково.
Покажите тогда пример, когда в функциональном коде не важен порядок. (в виде «бизнес требование => псевдокод»), я, к сожалению, не могу вас понять.
Поскольку state надо в сотни раз чаще читать, а не писать, вес победы Mori в тестах на запись несколько блекнет по сравнению с полным своим провалом в тестах на чтение (и безусловной победой там seamless).
Поэтому правильнее говорить, что самая быстрая, удобная и простая — это seamless. Самая стабильная и распространенная Immutable.js. А Mori остается быть только самой необычной.
Отличия от Immutable.js:
…
Функциональные плюшки (ленивые коллекции и т.д.)
Секундочку, в
Immutable.js
тоже есть ленивые коллекции, например Seq
, вот что говорит документация:Seq is lazy — Seq does as little work as necessary to respond to any method call. Values are often created during iteration, including implicit iteration when reducing or converting to a concrete data structure such as a List or JavaScript Array.
Спасибо за статью. Автор отчетливо говорит, что immutability не должна являться правилом. Єто скорее инструмент, которьій нужно использовать в специальньіх ситуациях. Их не так уж и много, практически всего две:
Отслеживание изменений состояния.
Использование многопоточности.
Кроме того, бьівають различньіе случаи требований бизнес-логики. Скорее исключения из общего правила. Можно упомянуть принцип pure functions. Однако, несмотря на явную схожесть pure functions and immutability -- ето два совершенно разньіх принципа. Pure functions не должньі менять ничего и нигде, независимо от того, нужна ли хоть где-то immutability или нет. Автор правильно и неоднократно упомянул, что immutability -- єто излишняя нагрузка на производительность и память. Его нужно применять осознанно. Но слово красивое и джуньі иногда начинают использовать єтот принцип буквально везде:
function getData() {
const data = { a: 1 };
// есть еще одно свойство?
const data2 = { ...data, b: 2 };
// еще свойство?
const data3 = { ...data2, c: 3 };
// immutability -- ето важно!
return {
...data3,
d: 4,
};
}
Увлеченность иммутабельностью часто ходит рядом с приверженностью использовать Array.map(...) при любьіх изменениях данньіх в массиве. Immutability -- хороший принцип, когда он важен, и наоборот. Впрочем как и все в программировании.
Иммутабельность в JavaScript