Ads
Comments 42
Это что, целая книга про разные способы передачи параметров в функцию: передача по ссылке (функция получает ссылку на значение аргумента и может его менять) и передача по значению (функция получает копию аргумента и не может его изменить)?
И про то, в каких случаях какой способ передачи надо использовать?
Именно. Дожили называется, Хипстеры вдруг решили, что использование ссылочных типов зло, а передача по значению единственный верный вариант… :D И назвали то как пафосно «Иммутабельность» :D
Но… Бывает же передача иммутабельного аргумента по ссылке? Не скажу за JS, но в том же C++ вполне (а думаю, что еще в C#/Java/Scala)
Далеко не специалист в JS, однако мне не понятен смысл примеров с объектами и массивами.
const person = {
  name: 'John',
  age: 28
}
const newPerson = person
newPerson.age = 30

Достаточно просто
person.age = 30;

Также и с массивом
const characters = [ 'Obi-Wan', 'Vader' ]
const newCharacters = characters
newCharacters.push('Luke')

с таким же успехом можно просто
characters.push('Luke');
Так суть в обратном. В следующем коде

const person = {
  name: 'John',
  age: 28
}
const newPerson = person
newPerson.age = 30


нашей задачей было получить новый объект newPerson, такой же самый как person только с age 30. А проблема возникает в том что возраст изменялся и у первого объекта. В этом то и суть проблемы, о которой весь пост.
Ну так получайте новый объект, а не приравнивайте указатель к старому :)
В js переменные это не значения, только указатели. И даже на числа и строки. Просто так уж вышло, что как и в Lua, в js просто нет функций для изменения строк и чисел по указателю — все функции чистые и возвращают новую строку\число. Но поверьте, переменная со строкой все равно есть указатель на нее.
Я конечно капитан, но статья капитан не меньше.
Еще и статью не читал. Автор жалуется на то, что при присвоении происходит копирование ссылки, а не самого объекта.
Я не автор, но уверен что const — просто так, и с таким же успехом его можно заменить на var. Просто в es6 принято всё что не будет изменяемо — объявлять с const.

const не позволит перезаписать person. (+ мелкие бонусы в виде отсутствия hoisting и нормальной работы с block scopes)


const person = {age: 26}
person = {age: 30} // ошибка


В примере используется потому что просто привычка, после const/let var кажется отвратительным (ну он такой и есть)

уверен что const — просто так
В примере используется потому что просто привычка

vlreshet, Fen1kz, ок, теперь ясно.
Костыли для тех, кто не понимает разницы между ссылкой и значением? Да, бывают условия, при которых нужно копирование объектов, но использовать это всегда и везде — это как-то странно. И да, ваш «object spread» сломается, как только внутри объекта появится ссылка на другой объект, поэтому без отдельной функции для «deep clone» все равно не обойтись.
В статье описывается функциональный подход к программированию JS. Можно использовать для Redux, когда требуется обеспечить иммутабельность данных в хранилище данных store, именно такой код используется для редюсеров (чистых функции которые принимают хранилище данных store и возвращают новое измененное хранилище).

И да, если внутри объекта есть ссылка на другой объект, то она копируется поверхностно, надо это учитывать и использовать deep clone или деструктуризацию и последующую структуризацию объектов, что кстати описано в указанной книге Кайла Симпсона на стр.62. Так что книга дельная

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

В реальных скриптах объекты более сложные и вложенные.
Никакие 'object assign', 'object spread', 'object freeze' итп не помогут в таких условиях.
Только рекурсивное клонирование, вот пример

https://github.com/timCF/jsfunky/blob/8d73af422c4e3c30f78a8e1689d2f09b7d5ccbb0/jsfunky.iced#L10-L21

Написал для упрощения жизни какое-то время назад этот модуль, пользуюсь часто в реальных задачах.
Хотя это перевод, все равно скажу.

Одна из наиболее сложных операций в JavaScript – это отслеживание изменений объекта. Решения вроде Object.observe(object, callback) довольно тяжеловесны. Однако, если держать состояние неизменяемым, то можно обойтись oldObject === newObject и таким образом проверять, не изменился ли объект. Такая операция не так сильно нагружает CPU.

Вообще, даже для равных objObject и newObject они не равны, ведь сравниваются по ссылке:
var a = {value: 1};
var b = a;
console.log(a === b); // true

var a = {value: 1};
var b = {value: 2};
console.log(a === b); // false :-(


PS. Отсутствие запятых — зло.

В тексте немного странно написано.
В редуксе смысл в том, что проверка наличия изменений сводится к newState !== oldState.
Поэтому так важно возвращать новое состояние при изменениях, а не изменять текущее

Ну вообще в Immutable.js реализованы persistence hash trees, так же как и в clojure. Так что в целях производительности лучше использовать библиотеку все таки

Мне кажется иммутабельные объекты делать лучше как-то так:


class Copiable {
  copy(key,value){
    let result = Object.assign({},this);
    result[key]=value;
    result.__proto__ = this.__proto__
    Object.freeze(result);
    return result;
  }
}

class Person extends Copiable {
  constructor(name='Adam', age=31){
    super();
    this.name=name;
    this.age=age;
    Object.freeze(this);
  }
}

То есть два простых правила — наследоваться от Copiable и в конце конструктора фризить объект.

Лучше всего клонирование делать через конструкторы. То есть в идеале конструктор объекта должен принимать объект такого же типа и возвращать копию.
const person = {
  name: 'John',
  password: '123',
  age: 28
}
const newPerson = Object.keys(person).reduce((obj, key) => {
  if (key !== property) {
    return { ...obj, [key]: person[key] }
  }
  return obj
}, {})

А это вообще нормально N-1 раз копировать объект, добавляя по одному свойству за итерацию?

Не вижу ничего фатального. Прошлые версии будут почти моментально собраны GC. Но если что — можно добавить метод для добавления пачки полей.

Вы, верно, троллите, если пишете, что не видите проблемы насилования устройства аллокациями в цикле


const newPerson1 = {...person};
delete newPerson1[property];

// vs

const newPerson2 = Object.keys(person).reduce((obj, key) => {
  if (key !== property) {
    return { ...obj, [key]: person[key] }
  }
  return obj
}, {})

Ну весь хаскель так работает, но ещё раз — можно оптимизировать достаточно сильно. Главное н езаьывать фриз сделать.

Сложно сказать. Попробовал вот на коленке пример собрать:


const keys = []; for(let i = 0; i < 100000; i += 10) keys.push(i); 

console.time('mutable');
const obj1 = {};
for(let key of keys) obj1[key] = key;
console.timeEnd('mutable');

console.time('reduce');
const obj2 = keys.reduce((o, key) => Object.assign({}, o, { [key]: key }), {});
console.timeEnd('reduce');

Результат: 7.7ms vs 18'781ms. Таки JS не Haskell.
Касательно "не забывать freeze вызывать", не знаю куда именно его воткнуть. Покажите на примере выше, плз. Может быть это улучшит ситуацию.

А вот промежуточный вариант с delete срабатывает за тоже самое время, что и чисто mutable-вариант. Т.е. в 2600 раз быстрее.

Конечно, с чего бы это оптимизатору js понимать что за ужс натворил писатель этого творения.


Object.freeze тоже совсем не обязательная вещь — глубокая заморозка очень дорогая вещь, она полезна на стадии разработки, но в продакшене мы даже не должны пытаться ошибочно изменять иммутабельный объект, или же мы совершим ошибку, за которую нас справедливо покарают исключением TypeError (в строгом режиме).


Т.е. моё мнение, что с Object.freeze хорошо тестировать код и находить ошибочные попытки изменить объект, но плохо костылить "иммутабельность" в продакшене

Мне кажется, что пока внятной поддержки имутабельных структур в js не появится ― не нужно уродоваться со всякими freeze, и уж тем более с такой дичью, как этот reduce + {..., [key]: }. Это уже ни в какие ворота не лезет :) Нужно искать компромиссы, вроде вышеописанного delete.

Вот тут согласен. Как решили что функциональность работает — можно выпилить freeze.

Не-не, оно гарантированно ничего не ускорит. Более того, копирование объектов очевидно не может быть быстрее. Оно может быть только надёжнее в плане иммутабельности.

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


Про что-то подобное в JS я ещё нигде не натыкался.

Ну в спеке на хаскел открытым текстом сказано что новые инстансы создаются постоянно и на месте же собираются GC если на старые версии больше никто не ссылается. Но за счёт этого immutability gc работает очень эффективно.

Полагаю, что там всё не так просто, как вы пишете. Выделять память и тут же её освобождать копируя большие куски данных на каждый чих ― это даже в теории не может быть сопоставимым, с примитивной мутацией. Разница на порядки. Полагаю, что в Haskell-е за счёт его нативной немутабельности просто много хитростей. А в JS про них пока ни намёка. Оттого и разница в 2600 раз.

А я уже и забыл про ту статью. Спасибо, что напомнили. Правда это такой "хак", что всем хакам хак. Не думаю, что стоит полагаться на такое в production-е.

Согласен. Насчет примера выше, имеет ли смысл каждый раз создавать объект? Mожет лучше сразу:


const obj2 = keys.reduce((o, key) => 
  Object.assign(o, { [key]: key }), {});

Присоединяюсь к вопросу. Может кто проводил полноценные замеры? В том числе и по .concat в []. На первый взгляд такие вещи использовать при итерации просто дико. Но может быть там есть под капотом оптимизации под это?

Однако, на самом деле в EcmaScript есть специальный синтаксис, еще сильнее упрощающий такие задачи. Он называется object spread, использовать его можно при помощи транспилятора Babel.

А можно ссылку в спецификации про object spread? Потому что ни в последнем Chrome, ни в FF, ни в node.js 6/7 "из коробки" эта возможность не работает. Однако по таблице поддержки из статьи spread везде поддерживается полностью. Полагаю, что это какая-то драфтовая возможность

let person = {};
Object.defineProperty(person, 'name', {
  value: 'John',
  writable: false
});

Думаю как-то так можно сделать «иммутабельность». Так же можно попробовать freeze
Only those users with full accounts are able to leave comments. Log in, please.