company_banner

5 интересных JavaScript-находок, сделанных в исходном коде Vue

Автор оригинала: bitfish
  • Перевод
Чтение исходного кода известных фреймворков может хорошо помочь программисту в улучшении его профессиональных навыков. Автор статьи, перевод которой мы сегодня публикуем, недавно анализировал код vue2.x. Он нашёл в этом коде некоторые интересные JavaScript-идеи, которыми решил поделиться со всеми желающими.



1. Определение точного типа любого объекта


Как все мы знаем, в JavaScript существует шесть примитивных типов данных (Boolean, Number, String, Null, Undefined, Symbol) и один объектный тип — Object. А вам известно о том, как различать типы различных объектных значений? Объект может быть массивом или функцией, он может представлять собой коллекцию значений Map или что-то ещё. Что нужно сделать для того чтобы узнать точный тип объекта?

Прежде чем искать ответ на этот вопрос — подумаем о разнице между Object.prototype.toString.call(arg) и String(arg).

Использование этих выражений направлено на преобразование переданного им параметра в строку. Но работают они по-разному.

При вызове String(arg) система попытается вызвать arg.toString() или arg.valueOf(). В результате, если в arg или в прототипе arg будут перезаписаны эти методы, вызовы Object.prototype.toString.call(arg) и String(arg) дадут разные результаты.

Рассмотрим пример.

const _toString = Object.prototype.toString
var obj = {}
obj.toString()  // [object Object]
_toString.call(obj) // [object Object]

Выполним этот код в консоли инструментов разработчика браузера.


В данном случае вызовы obj.toString() и Object.prototype.toString.call(obj) приводят к одним и тем же результатам.

Вот ещё пример.

const _toString = Object.prototype.toString
var obj = {}
obj.toString = () => '111'
obj.toString()  // 111
_toString.call(obj) // [object Object]
/hello/.toString() // /hello/
_toString.call(/hello/) // [object RegExp]

Выполним код в консоли.


А теперь вызов метода объекта .toString() и использование конструкции Object.prototype.toString.call(obj) дают разные результаты.

Вот какие правила описывают в стандарте ECMAScript поведение метода Object.prototype.toString().


Описание метода Object.prototype.toString() в стандарте ECMAScript

Взглянув на документацию, можно сделать вывод о том, что при вызове Object.prototype.toString() для разных объектов будут возвращаться различные результаты.

Исследуем эту идею в консоли.


Кроме того, значение, возвращаемое Object.prototype.toString(), всегда представлено в следующем формате:

‘[object ’ + ‘tag’ +‘] ’

Если нам нужно извлечь из этой конструкции только часть tag, добраться до этой части можно, удалив ненужные символы из начала и конца строки с помощью регулярного выражения или метода String.prototype.slice().

function toRawType (value) {
    const _toString = Object.prototype.toString
    return _toString.call(value).slice(8, -1)
}
toRawType(null) // "Null"
toRawType(/sdfsd/) //"RegExp"

Исследуем эту функцию в консоли.


Как видите, с помощью вышеприведённой функции можно узнать точный тип объектной переменной.

Здесь, в репозитории Vue, можно найти код подобной функции.

2. Кеширование результатов работы функции


Предположим, имеется функция, подобная следующей, выполняющая длительные вычисления:

function computed(str) {    
    console.log('2000s have passed')
    return 'a result'
}

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

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

function cached(fn){
  // Создаём объект для хранения результатов, возвращаемых целевой функцией после её выполнения
  const cache = Object.create(null);

  // Возвращаем целевую функцию в соответствующей обёртке
  return function cachedFn (str) {

    // Если в кеше нет подходящих результатов - функция будет выполнена
    if ( !cache[str] ) {
        let result = fn(str);

        // Сохраняем результат выполнения функции в кеше
        cache[str] = result;
    }

    return cache[str]
  }
}

Вот пример использования вышеописанной функции.


→ Вот код подобной функции, который имеется в кодовой базе Vue.

3. Преобразование строки вида hello-world к строке вида helloWorld


Когда над одним и тем же проектом совместно работает несколько программистов, им очень важно заботиться о единообразии стиля кода. Кто-то, например, может записывать некие составные идентификаторы в формате helloWorld, а кто-то — в формате hello-world. Для того чтобы навести в этой области порядок, можно создать функцию, которая преобразует строки вида hello-world к строкам вида helloWorld.

const camelizeRE = /-(\w)/g
const camelize = cached((str) => {
  return str.replace(camelizeRE, (_, c) => c ? c.toUpperCase() : '')
})
camelize('hello-world')
// "helloWorld"

Вот то место кода Vue, откуда взят этот пример.

4. Определение того, в каком именно окружении выполняется JavaScript-код


В наши дни, учитывая быстрое развитие браузеров, JavaScript-код может выполняться в различных окружениях. Для того чтобы лучше адаптировать проекты к различным средам, нужно уметь определять то, где именно выполняются программы:

const inBrowser = typeof window !== 'undefined'
const inWeex = typeof WXEnvironment !== 'undefined' && !!WXEnvironment.platform
const weexPlatform = inWeex && WXEnvironment.platform.toLowerCase()
const UA = inBrowser && window.navigator.userAgent.toLowerCase()
const isIE = UA && /msie|trident/.test(UA)
const isIE9 = UA && UA.indexOf('msie 9.0') > 0
const isEdge = UA && UA.indexOf('edge/') > 0
const isAndroid = (UA && UA.indexOf('android') > 0) || (weexPlatform === 'android')
const isIOS = (UA && /iphone|ipad|ipod|ios/.test(UA)) || (weexPlatform === 'ios')
const isChrome = UA && /chrome\/\d+/.test(UA) && !isEdge
const isPhantomJS = UA && /phantomjs/.test(UA)
const isFF = UA && UA.match(/firefox\/(\d+)/)

Вот где я нашёл этот код.

5. Различение встроенных и пользовательских функций


Известно, что в JavaScript существует два вида функций. Первый вид — это встроенные, или, как их ещё называют, «нативные» функции. Такие функции даёт нам среда, в которой выполняется код. Второй вид — это так называемые «пользовательские функции», то есть те, которые программисты пишут сами. Различить эти функции можно, учтя тот факт, что, при преобразовании их в строки, возвращаются различные результаты.

Array.isArray.toString() // "function isArray() { [native code] }"
function fn(){} 
fn.toString() // "function fn(){}"

Поэкспериментируем с этим кодом в консоли.


Метод toString() нативной функции всегда возвращает конструкцию следующего вида:

function fnName() { [native code] }

Зная это, можно написать функцию, позволяющую различать нативные и пользовательские функции:

function isNative (Ctor){
  return typeof Ctor === 'function' && /native code/.test(Ctor.toString())
}

→ Вот то место в кодовой базе Vue, где есть такая функция.

А вам удавалось находить что-то интересное, исследуя код в репозиториях известных JavaScript-проектов?

RUVDS.com
RUVDS – хостинг VDS/VPS серверов

Комментарии 32

    +15
    Зная это, можно написать функцию, позволяющую различать нативные и пользовательские функции

    isNative(isNative) => true

    Люблю javascript ^^

      0
      Точно. А почему так?
        +2

        Потому что в исходнике isNative присутствует постройка, по которой определяется нативность.

          +1
          Потому что в тексте функции содержится 'native code'
            +5

            Потому что строчка native code внезапно есть в тексте функции isNative :)
            Корректная реализация видимо должна как-то так проверять:


            function isNative (Ctor){
              return typeof Ctor === 'function' && /\{\s*\[native code\]\s*\}$/.test(Ctor.toString())
            }
            
              +1
              isNative.toString = function() { 
                  return parseInt.toString().replace(parseInt.name, this.name ); 
              }
              isNative(isNative) => true
              
                0

                Ну это всё-таки намеренное стреляние в ногу. Фишка с bind от B_bird более прикольная, показывает что определить точно, будет ли вызываться пользовательский код невозможно.

            +1
            Это скорее ошибка логики в написании функции
              +1
              // Used to resolve the internal `[[Class]]` of values
              const toString = Object.prototype.toString;
              
              // Used to resolve the decompiled source of functions
              const fnToString = Function.prototype.toString;
              
              // Used to detect host constructors (Safari > 4; really typed array specific)
              const reHostCtor = /^\[object .+?Constructor]$/;
              
              // Compile a regexp using a common native method as a template.
              // We chose `Object#toString` because there's a good chance it is not being mucked with.
              const reNative = RegExp('^' +
                // Coerce `Object#toString` to a string
                String(toString)
                  // Escape any special regexp characters
                  .replace(/[.*+?^${}()|[\]/\\]/g, '\\$&')
                  // Replace mentions of `toString` with `.*?` to keep the template generic.
                  // Replace thing like `for ...` to support environments like Rhino which add extra info
                  // such as method arity.
                  .replace(/toString|(function).*?(?=\\\()| for .+?(?=\\])/g, '$1.*?') + '$'
              );
              
              const isNative = value => {
                const type = typeof value;
                return type === 'function'
                  // Use `Function#toString` to bypass the value's own `toString` method
                  // and avoid being faked out.
                  ? reNative.test(fnToString.call(value))
                  // Fallback to a host object check because some environments will represent
                  // things like typed arrays as DOM methods which may not conform to the
                  // normal native pattern.
                  : (value && type === 'object' && reHostCtor.test(toString.call(value))) || false;
              };
              
              isNative(isNative); // Покажет false
              isNative(Symbol); // Покажет true
                0

                Всё равно


                isNative(function(){}.bind(this)) // true
                  0

                  Беда, беда.

              +3
              Различение встроенных и пользовательских функций

              Только не забывать про использование bind:
              const a = function() { console.log(1); }
              a.toString()
              "function() { console.log(1); }"
              a.bind(this).toString()
              "function () { [native code] }"

                +1

                Реализация cached неудачная: нули, пустые строки, false, null и undefined функция кэшировать не будет, а будет каждый раз вычислять

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

                  fn.chache = [ {} ];
                  
                  function fn( arg )  {
                    let chachedResult= fn.chache.find( ( x ) => { return x.arg === arg } );
                    if ( chachedResult )
                        return chachedResult.result;
                  
                    let result;
                    ... //  вычисление result
                  
                    fn.chache.push( { arg: arg, result: result } );
                    return result;
                  }

                  Тем более тут нет проблемы с null, undefined.
                    0

                    Тяжелые функции могут быть библиотечными, автор которых не считает нужным кэшировать. Да и вообще, код хуже читается, дублирование кода в разных местах весьма вероятно и т. п.

                      0
                      let chachedResult= fn.chache.find( ( x ) => { return x.arg === arg } );

                      Зачем вам тут O(n)? Сделайте кеш с использованием new Map или new WeakMap.


                      кэшировании своих результатов без обёртки

                      Ну вы собственно сделали обёртку, просто не в виде метода, а в виде кода выше и кода ниже самих вычислений. Суть та же самая. От перестановки мест слагаемых...

                        0
                        Потому что инвалидация кэша — одна из основных проблем программирования. Предположения автора функции сделали бы ее менее гибкой.
                        +2

                        А еще неудачное имя, так как это принято называть мемоизацией или memoization. А если правильно назвать, то смотри, можно и кучу готовых и проверенных годами решений найти, lodash например.

                          0

                          Осторожнее с утечками памяти. Кажется по-дефолту там используется {} в качестве хранилища. Код какой-то сильно путанный, бегло пробежался. Предлагают использовать weakMap в качестве альтернативы, но тут имейте ввиду что ничего кроме object-ов в качестве ключей тогда использовать будет нельзя.

                            0

                            Ну не то чтобы прям утечка памяти, просто по-умолчанию он не чистится. Жаль в JS нет встроенной возможности сделать кеш из серии "ну можно чистить, когда памяти не хватает". В любом случае, нужно использовать с умом: если вы знаете что функция хоть и тяжелая, но будет выпоняться для ограниченного набора элементов — почему бы и нет. Вот, скажем, у меня есть инструмент, где учитель может видеть результаты работы учеников. Максимальное количество закешированных элементов равняется количеству учеников. Не то чтобы стоит переживать.


                            А вот если речь идет об сложной обработке сложных строк, которые идут из пользовательского ввода, т.е. набор элементов может быть неограничен — стоит позаботиться о том, чтобы хранить только немного элементов (иногда вообще только последний результат).

                              +1
                              В любом случае, нужно использовать с умом

                              Имхо проще написать свои велоспеды чем использовать такой _.memoize. А ещё есть много вменяемых альтернатив на любой вкус и цвет (даже с использованием proxy dependency detection).

                        0
                        Серьезно, открытие в 2020 году кэша функции ???? А вот про типы было интересно!
                          0
                          Функция с кешем была бы лучше, если предыдущий falsey результат не вызывал рассчет заново.
                          export function cached<F: Function> (fn: F): F {
                          const cache = Object.create(null)
                          return (function cachedFn (str: string) {
                          if (!cache.hasOwnProperty(str)) cache[str] = fn(str);
                          return cache[str];
                          }: any)
                          }
                            +1
                            if ( !cache[str] )

                            Что здесь, что во Vue одна и та же грубая ошибка: если исходная функция вернет любое «false» значение — кэш не сработает.
                              0
                              Как все мы знаем, в JavaScript существует шесть примитивных типов данных (Boolean, Number, String, Null, Undefined, Symbol) и один объектный тип — Object.
                              С каких это пор Null является типом данных? Почему нету тогда типа NaN?
                                +1

                                Вроде с самого начала, но сейчас 100% — http://www.ecma-international.org/ecma-262/6.0/#sec-ecmascript-language-types-null-type так же как undefined — отдельный тип. А NaN — число по стандарту IEEE 754

                                  0
                                  Интересно, не знал что в стандарте он описуется именно как отдельный тип, хотя на практике, значение null является типом Object, так сказать без ссылки на сам объект.
                                    0
                                    Результат typeof null == "object" – это официально признанная ошибка в языке, которая сохраняется для совместимости.

                                    Ну и на практике сплошь и рядом реальные типы в программе number|null, string|null и иногда даже boolean|null, а не только object|null

                                +1
                                По сути все что вы перечислили — это хорошо известные паттерны разработки или полезные функции.

                                Их существует очень много:
                                JavaScript паттерны проектирования 1
                                JavaScript паттерны проектирования 2
                                JavaScript — полезные функции

                                и т.д.
                                  +1
                                  1. Определение точного типа любого объекта

                                  И ещё находятся люди, которые утверждают, что на языках с динамической типизацией прлще писать программы.

                                    +1
                                    Думаю, что первые 4 позиции тут-там периодически всем нам от проекта к проекту приходится юзать/реализовывать, и, в принципе, ничего особо интересного в этих находках нет, более того, как подметили некоторые комментаторы — есть более грамотные решения.

                                    Зная это, можно написать функцию, позволяющую различать нативные и пользовательские функции

                                    Ни разу не требовалось подобного, но очень хотелось бы увидеть реальный пример из практики. Заранее благодарю )
                                      0
                                      Ну едва ли это можно называть интересным- типичные подходы, коим более 10 лет… toString еще и весьма сомнительный из-за производительности.

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

                                      Самое читаемое