Заметки в категории «JavaScript» (страница 7)

Экранирование русских символов

В js-файлах русские символы, да и любые другие символы с кодом больше 127, внутри строк нужно обязательно экранировать, если кодировка файла не совпадает с кодировкой страницы. Конечно, в таких случаях можно явно указывать требуемую кодировку

<script src="my-cool-app.js" charset="windows-1251">

Если заранее экранировать такие символы, то в дальнейшем уже будет неважно в какой кодировке «сервируется» ваш файл.

Mathias Bynens описал различные варианты экранирования и даже сделал утилиту, которая может экранировать так, чтобы результирующая строка была наименьшей длинны.

Я расскажу очень быстрый способ получить экранированную строку прямо в консоли браузера.

escape("Привет").replace(/%(u[0-9a-f]{4})/ig, "\\$1")

В результате получим экранированную строку:

"\u041F\u0440\u0438\u0432\u0435\u0442"

У этого метода есть один недостаток: символы с кодом меньше 128 нужно обрабатывать отдельным регэкспом.


escape("Hello!").replace(/%([0-9a-f]{2})/ig, "\\x$1")
Комментарии к заметке: 2

Подключение Яндекс.Карт после загрузки страницы

В API Яндекс.Карт есть метод ready , который выполняет указанную функцию после полной загрузки всех требуемых модулей. Однако он стабильно работает только когда карты подключаются на страницу в теге head или по крайней мере до события DOMContentLoaded. Если карту попытаться загрузить гораздо позже, то метод может и не выполниться (обсуждение этого дефекта в клубе и ещё другое обсуждение этого же вопроса). Гарантировано получить уведомление о готовности API для работы можно другим способом.

У загрузчика карт есть недокументированный параметр onload, в котором можно указать имя функции. Эта функция как раз будет вызвана, когда будут загружены все требуемые модули карты.


function loadYandexMaps(callback) {
    var callbackName = "_myapp_map_init",
        loaderUrl = "http://api-maps.yandex.ru/2.0/?" +
            "load=package.standard&" +
            "lang=ru-RU&" +
            "onload=" + encodeURIComponent(callbackName); 
    if (!window['ymaps']) {
        window[callbackName] = function () {
            callback();
            window[callbackName] = null;
        };
        yepnope.injectJs(loaderUrl);
    } else {
        callback();
    }
}

Загрузчик карт добавляется на страницу с помощью yepnope.js только тогда, когда он реально потребуется на странице.

Функции, которая добавляется в параметры загрузчика, должна быть доступна глобально. Т.е. использовать что-то вроде App.Maps.init уже не получится. С другой стороны, название этой функции не должно меняться, чтобы запросы могли быть закешированы браузером. Поэтому к выбору этого названия стоит подойти с особой тщательностью.

Комментарии к заметке: 1

Особенности @param в JsDoc

В комментариях я использую разметку JsDoc. Есть утилиты, которые автоматически строят документацию, основываясь на этой разметке. Ещё она польза заключается в том, что статические анализаторы в IDE умею проверять использование прокомментированных функций.

С перечнем аннотаций , их параметрами и примерами использования можно ознакомиться, например, в документации к генератору jsdoc-toolkit.

Самой популярной, пожалуй, аннотацией является @param.


@param {paramType} paramName paramDescription
  • paramType — опциональный: ожидаемый тип параметра;
  • paramName — обязательный: название параметра;
  • paramDescription — опциональный: описание параметра.

В JS нет строгой типизации параметров функции. В одном случае, например, тип параметра может быть числом, а в другом — строка. Все ожидаемые типы перечисляются с разделителем | .

/**
 * @param {Number|String} value
 */
function showValue(value) {
    alert(value);
}

А ещё параметры функции могут быть опциональными. В этом случае название параметра заключаются в квадратные скобки.


var uidx = 1;
/**
 * Generate an id that is unique among the application
 * @param {String} [prefix] optional guid prefix
 */
function guid(prefix) {
    return (prefix || 'app') + '-' + uidx++;
}

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

Комментарии к заметке: 3

Конкурирующие асинхронные запросы

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


var concurrent = (function () {
    var concurrentRequests = {};
    return function (opts, cache) {
        // вместо jqXHR функция будет возвращать promise-объект
        var deferred = $.Deferred(), promise = deferred.promise();

        // сохраняем колбеки, если они есть
        promise.done(opts.success || $.noop);
        promise.fail(opts.error || $.noop);
        promise.always(opts.complete || $.noop);

        // удаляем колбеки из параметров,
        // так как cacheable не обрабатывает их
        delete opts.success;
        delete opts.error;
        delete opts.complete;

        // таймстемп запроса.
        // ответы с таймстемпом меньше текущего игнорируются
        var requestId = concurrentRequests[opts.url] = $.now();

        // кеширующий или обычный запрос
        (cache ? cacheable : $.ajax)(opts)
            .done(function () {
                if (concurrentRequests[opts.url] <= requestId) {
                    deferred.resolveWith(promise,
                        Array.prototype.slice.apply(arguments));
                }
            })
            .fail(function () {
                if (concurrentRequests[opts.url] <= requestId) {
                    deferred.rejectWith(promise,
                        Array.prototype.slice.apply(arguments));
                }
            });

        return promise;
    }
}());

В паре с функций cacheable получилась достаточно универсальная замена традиционной функции $.ajax.


$(".list").each(function () {
    var list = $(this);
    list.on("click", ".list__page", function () {
        concurent({
            url: "/list/",
            data: {page: $.tim($(this).text())},
            dataType: "html"
        }, true).done(function (markup) {
            list.html(markup);
        });
    });
});

Оставте свой комментарий

Нелинейная шкала слайдера

Когда слайдере нужно использовать для ввода несоизмеримых по величине данных (например, от 1 тысячи до 10 миллионов рублей), то разумнее использовать какую-то нелинейную шкалу. В частности, для ввода сумм подходящей шкалой будет логарифмическая (по основанию 10).

Для сравнения приведу два слайдера с линейной и нелинейной шкалой.

Нелинейная система координат

Чтобы обычный слайдер превратить такой нелинейный нужно определить две функции, которые будут переводить числа из одной системы отсчета в другую.


function fromSlider(value) {
    return Math.round(Math.pow(10, value));
}

function toSlider(value) {
    return Math.log(value) / Math.log(10);
}

Теперь там где нужно установить значение в слайдер используем toSlider, а там где получаем значение из слайдера — fromSlider.


$(".logarithmic__slider").slider({
    min: toSlider(1000),
    max: toSlider(10000000),
    step: 0.01,
    value: toSlider(initialValue),
    slide: function (e, ui) {
        $(".logarithmic__value").html(fromSlider(ui.value));
    }
});

Округление и форматирование

Чтобы данные выглядели презентабельно их хорошо бы ещё округлить и отформатировать.


function round(n, threshold) {
    var digits, out;

    n = n.toFixed(0);
    digits = n.length;
    out = n.substr(0, Math.min(digits, threshold));

    if (digits > threshold) {
        out = out + "000000000000".substr(0, digits - threshold);
    }

    return parseInt(out);
}

Функция оставляет только указанное количество значимых разрядов в числе. Остальные будут обнулены.


function splitGroups(n, delim) {
    var digits;

    delim = delim || '<span class="tsp"> </span>';

    n = n.toFixed(0);
    digits = n.length > 3 ? n.length % 3 : 0;

    return (digits ? n.substr(0, digits) + delim : '') +
        n.substr(digits).replace(/(\d{3})(?=\d)/g, "$1" + delim);
}

А эта функция разделяет группы разрядов числа. В качестве разделителя по умолчанию я использую пробел половинной ширины.


.tsp {
    font-size: 50%;
    line-height: 1;
}

Или можно указать требуемый разделитель вторым параметром функции.

Комментарии к заметке: 1