И я решил сделать свой микровклад в эту тему.
Для одной из типичных задач, хранения данных в виде «ключ — значение», почти всегда разработчики на Javascript используют объект. Просто потому что объект сам по себе именно так и устроен, представляет из себя хэш-таблицу, где имя поля это и есть ключ. Но у этого есть недостаток, о котором я узнал, обжегшись на нем. Проиллюстрирую его следующим тестом:
let a = {
'myKey': 'myValue'
}
let key = 'constructor'; // comes from outside source
let b = a[key] || 'defaultValue';
expect(b).to.be.equal('defaultValue'); // fails
И весь код, с которым я когда либо работал, говорил о том, что все те разработчики, которые его писали, на эту проблему как и я не натыкались, и соответсвенно никак не пытались с ней бороться.
Рассказывать подробности того, по какой причине это сломалось, смысла не имеет, то что произошло — совершенно очевидно. Вероятность наткнуться на такое поведение невысока, но тем не менее, как выяснилось, ненулевая. Первое, что я хотел сделать, это найти библиотеку, которая это решает, предоставляет интерфейс хэш таблицы. Но, поискав, я нашел лишь библиотеки, которые полностью изолируются от использования объекта, и честно вычисляют хэш. Они, конечно, применимы и даже необходимы, если в качестве ключа нужно использовать объект, а не строку или число. Но у них есть и вытекающий недостаток — они накладывают некоторый «оверхед», на который не все могут согласиться. После недолгих мытарств я пришел к тому, что нужно таки написать еще один велосипед.
Библиотека hash-map решает эту задачу методом сокрытия всех нижележащих полей пустыми значениями:
const result = {};
for (var prop of Object.getOwnPropertyNames(Object.prototype)) {
result[prop] = undefined;
}
return result;
Способы использования можно посмотреть в readme.
Yet another JS library
Статья получилась короткая, да и нечего особо рассказать об этой микропроблеме. Поэтому я до кучи решил упомянуть об еще одной библиотечке — typescript-reexport-generator. В процессе разработки на typescript я прибегал к разным способам экспортировать-импортировать код между файлами, пришел к тому, что наиболее удобным является следующее. Все .ts файлы в папке экспортируют код следующим образом:
// file1
export function myFunction(){}
// file2
export class myClass{}
Далее в папку кладется файл index.ts со следующим содержимым:
export * from './file1';
export * from './file2';
Теперь можно импортировать можно вот так:
//было
import { myFunction } from './folder/file1';
import { myClass } from './folder/file2';
//стало
import { myFunction, myClass } from './folder';
кроме этого, между файлами одной папки можно импортироваться вот так:
import { myClass } from '.';
export function myFunction(){ // doSmth with class }
Есть еще один мини-выигрыш: навигация в VSCode (ctrl + mouse click) наилучшим образом работает с таким экспортированием. Навигация от использования до имплементации в 1 клик. С default экспортом навигация осуществлялась в два клика, что несколько удручало, поэтому я от такого довольно быстро полностью отказался.
И для того, чтобы не писать эти реэкспорты руками, я написал простенький генератор, который создает эти файлы index.ts из таски с gulp.watch. Если вы используете такой же способ импортов-экспортов, библиотека может оказаться полезной.
Недостаток библиотеки, а куда же без них, это то, что VSCode не следит за изменениями файлов, поэтому только что созданный файл с экспортами не сразу позволяет импортироваться снаружи. Приходится руками зайти в index, чтобы студия «увидела», что там появилась новая строчка. Другой недостаток, который уже зависит от меня — gulp.watch не сообщает что именно изменилось, соответственно генератору приходится просматривать (и парсить) все файлы в проекте. В будущем возможно создам следующую версию библиотеки где это будет решено. Полным будет только первый проход, а далее будут парситься только те файлы, которые были изменены.
Комментарии (26)
kaljan
24.09.2017 23:53gulp.watch знает, какие файлы были изменены
gulp.watch(jsFiles, ['minjs']).on("change", callEslintAfterJsChanged); var callEslintAfterJsChanged = function(file) { gulp.src(file.path) .pipe(eslint()) .pipe(eslint.format()); }
inook
24.09.2017 23:53Вместо
нужно использоватьconst a = {};
, если очень хочется реализовать структуру Mapconst a = Object.create(null);
dem0n3d
25.09.2017 07:55Они, конечно, применимы и даже необходимы, если в качестве ключа нужно использовать объект, а не строку или число.
Не совсем так: ключами объекта могут быть ТОЛЬКО строки. Если использовать число в качестве ключа объекта, оно будет преобразовано в строку.
trueshura
25.09.2017 08:55+3ну т.е. смысл статьи в том, что если у вас в ключах могут оказаться «зарезервированные слова» (присутствующие в Object.prototype), то подумайте о том, чтобы не использовать Object?
0shn1x
25.09.2017 11:30Если вам, как вы указали выше, важно чтобы Object.Prototype объекта был не null (для redux), то почему бы не написать собственный getter, который бы выглядел как-то так:
function get(object, key){ return object.hasOwnProperty(key) ? object[key] : undefined }
Таким образом вы оставите только те свойства объекта, которые принадлежат именно ему, т.е. для вашего map — только добавленные свойстваVladVR Автор
25.09.2017 12:15Меня не прототип null не устраивает, а то что это свойство теряется после некоторых операций. Но честно скажу не знал о возможности создавать объекты без прототипа, может бы и не стал заморачиваться с велосипедостроением.
Геттер с этой проверкой тоже один из вариантов решения, почему нет. Одиночный оверхед во время создания против постоянного оверхеда при работе с объектом.superconductor
25.09.2017 21:39Afaik, hasOwnProperty — это способ сказать движку, что не надо лезть в прототип и это прекрасно оптимизируется.
aamonster
Лень проверять, но всё же: стандартный Map (ECMAScript 2016, но вроде уже давно есть во всех браузерах) проблему не решает?
Aingis
Решает. Более того, даже
Object.create(null)
из ES5 её решает (IE9+). Шёл 2017 год…VladVR Автор
Действительно, отсутствие прототипа помогает. Но нельзя делать вот так, например.
newMap = {...oldMap1, 'newKey': 'newValue' }
Кстати в lodash метод _.groupBy тоже этому подвержен.
Насчет Map — честно говоря, сейчас не помню почему ещё мне не захотелось вызывать методы get и set.
VladVR Автор
Вспомнил. Cейчас же неделя react vs vue vs angular. В redux есть одно неочевидное требование. Объекты, которые возвращает редюсер должны быть сериализуемы.
Если мы захотим сохранить состояние для последующего восстановления и использования, то в случе Map, после сериализации-десериализации будем иметь не тоже самое, что было до. Это же касается и объекта без прототипа.
VladVR Автор
Немного подумал, написал вот такой тест. Оказалось, что падает
починил, не смог определить, это breaking change или нет. У меня, по крайней мере ничего не сломалось
foxmuldercp
А для новичков — что за тестовый Фреймворк и где почитать*
JokerNN
Jasmine — jasmine.github.io/2.0/introduction.html
JokerNN
Был не прав!
VladVR Автор
Я использовал в качестве тест-раннера Mocha. И еще chai (assertion library)
8bitjoey
У редакса вообще много проблем с (де)сериализацией. Даже не надо такой экзотики как Map, достаточно вспомнить Date. При сериализации он становится строкой и строкой же десериализуется. И есть вполне рабочие методы избежать этого, которые, думается, можно применить и в вашем случае.