Еще один нюанс JavaScript, о котором все знают, но не все задумываются

    В последнее время вышло много статей о Javascript. Как холиварных, рассказывающих о том, какой он плохой, или какой он хороший, так и полезных рассказывющих о некоторых странных особенностях и разжевывающих «почему так», как например вот эта.
    И я решил сделать свой микровклад в эту тему.

    Для одной из типичных задач, хранения данных в виде «ключ — значение», почти всегда разработчики на 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
    • +1

      Лень проверять, но всё же: стандартный Map (ECMAScript 2016, но вроде уже давно есть во всех браузерах) проблему не решает?

      • +16

        Решает. Более того, даже Object.create(null) из ES5 её решает (IE9+). Шёл 2017 год…

        • 0
          Действительно, отсутствие прототипа помогает. Но нельзя делать вот так, например.
          newMap = {...oldMap1, 'newKey': 'newValue' }

          Кстати в lodash метод _.groupBy тоже этому подвержен.

          Насчет Map — честно говоря, сейчас не помню почему ещё мне не захотелось вызывать методы get и set.
          • 0
            Вспомнил. Cейчас же неделя react vs vue vs angular. В redux есть одно неочевидное требование. Объекты, которые возвращает редюсер должны быть сериализуемы.
            Если мы захотим сохранить состояние для последующего восстановления и использования, то в случе Map, после сериализации-десериализации будем иметь не тоже самое, что было до. Это же касается и объекта без прототипа.
            • 0
              Немного подумал, написал вот такой тест. Оказалось, что падает
                it('should return no value for prototype property after serialize/deserialize action', async function (): Promise<void> {
                  // arrange
                  const map = stringMap();
              
                  // act
                  const deserialized = JSON.parse(JSON.stringify(map));
                  const ctor = deserialized['constructor'];
              
                  // assert
                  expect(noValue(ctor)).to.be.equal(true);
                });
              

              починил, не смог определить, это breaking change или нет. У меня, по крайней мере ничего не сломалось
              • 0

                А для новичков — что за тестовый Фреймворк и где почитать*

              • 0
                У редакса вообще много проблем с (де)сериализацией. Даже не надо такой экзотики как Map, достаточно вспомнить Date. При сериализации он становится строкой и строкой же десериализуется. И есть вполне рабочие методы избежать этого, которые, думается, можно применить и в вашем случае.
      • +2
        Хм, а тупо препендить что-то к ключу, чтобы не натыкаться на зарезервированные слова, это типа костыль и криво выглядит?
        • –1

          Да, я тоже недоумении, мне эта мысль пришла сразу же в голову...

        • 0
          gulp.watch знает, какие файлы были изменены
          gulp.watch(jsFiles, ['minjs']).on("change", callEslintAfterJsChanged);
          
          var callEslintAfterJsChanged = function(file) {
                      gulp.src(file.path)
                          .pipe(eslint())
                          .pipe(eslint.format());
                  }
          
          
          • 0
            Вместо
            const a = {};
            нужно использовать
            const a = Object.create(null);
            , если очень хочется реализовать структуру Map
            • 0
              Они, конечно, применимы и даже необходимы, если в качестве ключа нужно использовать объект, а не строку или число.

              Не совсем так: ключами объекта могут быть ТОЛЬКО строки. Если использовать число в качестве ключа объекта, оно будет преобразовано в строку.

              • +1
                Ключом объекта может быть Symbol. 2017-ый.
                • +1
                  А те же Map/Set могут в качестве ключей иметь объекты.
                • +3
                  ну т.е. смысл статьи в том, что если у вас в ключах могут оказаться «зарезервированные слова» (присутствующие в Object.prototype), то подумайте о том, чтобы не использовать Object?
                  • –1
                    hasOwnProperty! не?

                    let b = a.hasOwnProperty(key)? a[key] || 'defaultValue': 'defaultValue';
                    • 0
                      Или немного покороче:
                      a.hasOwnProperty(key) && a[key] || 'defaultValue';

                      Правда читаемость кода…
                  • 0
                    Если вам, как вы указали выше, важно чтобы Object.Prototype объекта был не null (для redux), то почему бы не написать собственный getter, который бы выглядел как-то так:
                    function get(object, key){
                      return object.hasOwnProperty(key) ? object[key] : undefined
                    }
                    


                    Таким образом вы оставите только те свойства объекта, которые принадлежат именно ему, т.е. для вашего map — только добавленные свойства
                    • 0
                      Меня не прототип null не устраивает, а то что это свойство теряется после некоторых операций. Но честно скажу не знал о возможности создавать объекты без прототипа, может бы и не стал заморачиваться с велосипедостроением.

                      Геттер с этой проверкой тоже один из вариантов решения, почему нет. Одиночный оверхед во время создания против постоянного оверхеда при работе с объектом.
                      • 0

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

                  • +1
                    Так есть же Map и WeakMap в ES6

                    Только полноправные пользователи могут оставлять комментарии. Войдите, пожалуйста.