В этой статье пойдет речь об общих и не столь общих методах и шаблонах JavaScript, которые помогут улучшить эстетический вид кода, а также его размер. Чтобы разобраться в этом уроке, у вас должно быть хорошее понимание функционального программирования в JS и обычных выражениях.
JavaScript методы и шаблоны
Предположим, что вы уже знакомы с this и that, call, apply, bind и прототипами…
Длинные условия
Этот первый совет может показаться не совсем очевидным, но он поможет вам сократить длинные if заявления. Идея состоит в том, чтобы заменить логические операторы и сравнения на простой regex(регулярное выражение) тест.
Если вы делаете это…
if (val == 'a' || val == 'b' || val == 'c')
…то с regex оно будет выглядеть проще:
if (/a|b|c/.test(val))
Еще, возможно, вам придется иметь дело с несколькими переменными и AND логическим оператором, например:
if (a == 1 && b == 2 && c == 3 && d == 4 && e == 5)
Если у вас есть очень длинный список переменных, где вам нужно сравнить их значения, тогда вы можете создать таблицу с ожидаемыми значениями, а другую таблицу с заданными переменными и использовать every метод для сравнивания. every возвращает true, только если все условия были выполнены.
var vars = [a,b,c,d,e], vals = [1,2,3,4,5]; if (vals.every(function(val,i){ return vars[i] == val }))
Это может быть суммировано для повторного использования:
function isTrue(vars, vals) { return vals.every(function(val,i){ return vars[i] == val }); } if (isTrue([a,b,c,d,e], [1,2,3,4,5]))
В случае логического оператора OR нужно использовать some метод, который возвращает true, если хотя бы одно условие было выполнено. Но это мало вероятно, найти такие длинные OR модели; как правило они смешиваются с AND’s:
if (a == 1 && b == 2 || c == 3 && d == 4 || e == 5 && f == 6)
Манипуляция текстом
JavaScript инструменты для строк весьма ограничены по сравнению с другими языками. Рассмотрим случай, когда у нас есть несколько пунктов, разделенных точками, и мы хотим, чтобы первая буква каждого из них была заглавной. Использование циклов (loops) и методов работы со строками, мы сделаем что-то вроде этого:
var str = 'Небо голубое. Яблоки зеленые.'; var para = str.split('.'), // получите массив параграфов i = 0, len = para.length, p; for (; i < len; i++) { p = para[i].trim(); // убедитесь, что нет никаких дополнительных пробелов para[i] = p.substr(0,1).toUpperCase() + p.substr(1, p.length); } para = para.join('. ').trim(); // Сложите все обратно console.log(para); //^ "Небо голубое. Яблоки зеленые."
Несомненно, этот способ работает отлично, но очевидным умным подходом здесь будет использование обычных выражений, и все станет невероятно красивым и лаконичным:
str = str.replace(/([^.]+\.\s?)/g, function(_, para) { return para.replace(/^\w/, function(first) { return first.toUpperCase(); }); }); console.log(str); // ^ "Небо голубое. Яблоки зеленые."
Рассмотрим еще один обычный случай, где вам нужно извлечь все телефонные номера из текста. Давайте предположим, что в нашей строке все телефонные номера имеют префикс tel:, таким образом мы cможем захватить только телефонные номера, а не какие либо другие цифровые значения, которые могут содержаться в строке. Ясное дело, что обычное выражение станет лучшим инструментом для решения этой задачи, и так как в тексте может быть несколько телефонных номеров, мы будем использовать глобальный regex (обычное выражение). Мы могли бы попробовать использовать match:
var str = 'Павел, 26 лет, tel: 555-000-0000. Юлия, 19 лет, tel:777-000-0000.'; var numbers = str.match(/tel:\s?([\d-]+)/g); console.log(numbers); //=> ["tel: 555-000-0000", "tel:777-000-0000"]
Но, как вы видите, output все еще содержит tel:, при всем том, что мы используем группы захвата, чтобы только получить номера. Проблема в том, что match не захватывает группы в глобальном regex, по этой причине вам нужно использовать exec и while петли шаблона, который вы видели раньше:
var re = /tel:\s?([\d-]+)/g, numbers = [], match; while (match = re.exec(str)) { numbers.push(match[1]); } console.log(numbers); //=> ["555-000-0000", "777-000-0000"]
Это код необходимый для извлечения глобальных match, но это не единственный вариант. Mетод replace берет функцию, в котором первым аргументом является сама строка и следующий аргумент указывает на различные группы захвата. Последние два аргумента, в этом случае, тривиальны. Зная это, мы можем улучшить наш код:
var numbers = []; str.replace(/tel:\s?([\d\-]+)/g, function(_, number) { numbers.push(number); }); console.log(numbers); //=> ["555-000-0000", "777-000-0000"]
Мы даже могли бы сделать этот код доступным для всех строк, как новый метод gmatch:
String.prototype.gmatch = function(regex) { var result = []; this.replace(regex, function() { // извлеките матчи(matches), удалив аргументы, которые нам не нужны var matches = [].slice.call(arguments, 1).slice(0,-2); result.push.apply(result, matches); }); return result; }; console.log(str.gmatch(/tel:\s?([\d\-]+)/g)) //=> ["555-000-0000", "777-000-0000"]
Генерация HTML разметки
Очень распространенным случаем при работе со строками HTML бывает создание списков таких как li, td, option и другие. Обычно вы бы сделали это:
var values = ['one', 'two', 'three', 'four', 'five']; var html = ''; for (var i = 0; i < values.length; i++) { html += '<td>'+ values[i] +'</td>'; }
Этот код кажется достаточно эффективным, но for петли довольно уродливы. Мы можем переделать этот код с помощью join метода:
var html = '<td>'+ values.join('</td><td>') +'</td>';
Это выглядит получше, но что если нам нужен индекс? В этом случае, map метод может быть использован в сочетании с join:
var html = values.map(function(text, i) { return '<td>'+ text +': '+ i +'</td>' }).join('');
Когда все это становится все более сложным, нам в конечном итоге приходится писать много петель с несвязными HTML кусками, так что все становится небольшим беспорядком. Решение этой проблемы заключается в использовании шаблонов:
function template(arr, html) { return arr.map(function(obj) { return html.join('').replace( /#\{(\w+)\}/g, function(_, match) { return obj[match]; } ); }).join(''); }
Это может быть использовано следующим образом:
var people = [ { name: 'John', age: 25, status: 'Single' }, { name: 'Bill', age: 23, status: 'Married' }, { name: 'Mika', age: 17, status: 'Single' } ]; var html = template(people, [ '<div>', '<h1>Name: #{name}</h1>', '<h2>Age: #{age}, Status: #{status}</h2>', '</div>' ]); console.log(html); // ^ // "<div><h1>Name: John</h1><h2>Age: 25, Status: Single</h2></div>\ // <div><h1>Name: Bill</h1><h2>Age: 23, Status: Married</h2></div>\ // <div><h1>Name: Mika</h1><h2>Age: 17, Status: Single</h2></div>"
Полная функциональность
JavaScript является языком многопрофильной парадигмы, с впечатляющим функциональным программированием, которое зачастую делает ваш код более кратким и компактным, как вы уже видели в предыдущих примерах.
Все последние браузеров, включая IE9, имеют поддержку ECMAScript5 массива и методов объекта. Всякий раз, когда есть for или for..in циклы(loops), также есть альтернативное решение с одним из этих методов.
Одно из преимуществ функциональных циклов, которое может показаться не столь очевидным это то, что он создают новые возможности (границы). Это особенно полезно при работе с асинхронным кодом внутри цикла. Представьте себе такой случай:
var arr = ['a','b','c']; for (var i = 0; i < arr.length; i++) { setTimeout(function() { console.log(arr[i] +':'+ i); }, i * 1000); }
Целью здесь является вход a:0..b:1..c:2 с промежутком в одну секунду, но результат заканчивается этим undefined:3..undefined:3..undefined:3. (неопределенная). Типичная проблема решается путем создания новой области:
for (var i = 1; i <= 3; i++) { setTimeout((function(i) { console.log(arr[i] +':'+ i); }(i)), i * 1000); }
Теперь результат выглядет как ожидалось, но немного запутанный. Так как forEach цикл создает новую область и вещи становятся проще:
arr.forEach(function(v, i) { setTimeout(function() { console.log(v +':'+ i); }, i * 1000); });
При работе с большими объемами данных, использование функционального подхода является отличным способом для улучшения читабельности. Возьмите данные вроде следующих:
var users = [ { Name: 'Саша', Age: 25, Job: 'Разработчик', Subscription: 'Золотая' }, { Name: 'Владимир', Age: 28, Job: 'Повар', Subscription: 'Платиновая' }, { Name: 'Зоя', Age: 32, Job: 'Визажист', Subscription: 'Серебряная' }, { Name: 'Елена', Age: 35, Job: 'Повар', Subscription: 'Серебряная' } ];
С array методами можно фильтровать пользователей по любым критериям:
var over30 = users.filter(function(user) { return user.age >= 30; }); var cook = users.filter(function(user) { return user.job == 'Повар'; });
Этот код работает и выглядит достаточно хорошо, но похоже, что нам придется повторять эту модель снова и снова, чтобы извлечь информацию из наших объектов. Для того, чтобы сделать наш код умнее, на этот раз мы будем использовать прототипы и функциональные шаблоны, которые мы рассматривали до этого. Мы создадим очень маленькие DSL для обработки наших данных.
Мы хотим иметь возможность сделать следующие:
var over30 = users.where('age').is('>=30'); var cooks = users.where('job').is('Повар');
Прежде всего, давайте посмотрим на код, а затем решим, что возможно сделать с этой маленькой библиотекой. Вам может быть сложно следовать за кодом, если у вас нет глубокого понимания функционального программирования в JavaScript, но мы надеемся, что наши комментарии сделают его немного понятней.
// An Immediately Invoked Function Expression (IIFE) // это помешать утечки переменных в глобальную область (function(win) { // Главный конструктор для MyStorage объекта function MyStorage(data) { this.data = data; // матрица содержащая наши данные this.length = this.data.length; // просто ярлык } // Ярлык, чтобы создать новые экземпляры MyStorage function stored(data) { return new MyStorage(data); } // простой наполнитель объекта function _extend(obj, target) { for (var o in obj) target[o] = obj[o]; return target; } MyStorage.prototype = { // Приватно: // Обновите данные и верните новый экземпляр. // Нам нужно клонировать объекты, так как в противном случае // они будут переданы как рефералы _new: function(result) { result = result.map(function(o) { return _extend(o, {}); }); this.length = this.data.length; return stored(result); }, // Профильтруйте текущие свойства данных с функцией. // Свойство передается в качестве параметра в 'fn' callback _filter: function(fn) { return this._new(this.data.filter(function(user, i) { return fn.call(this, user[this.prop], i); }.bind(this))); }, // Публично: // Получите массив с определенной опорой (prop) из каждого объекта // или вернуть сбор данных в противном случае get: function(prop) { if (prop) return this.map(function() { return this[prop]; }); return this.data; }, // Set the property to filter or compare to where: function(prop) { return this.prop = prop, this; }, //Ярлык для семантики and: function(prop) { return this.where(prop); }, // Сравнить текущее свойство с данным условием is: function(condition) { // Filter by regular expression or string/number var regex = condition instanceof RegExp ? condition : new RegExp('^'+ condition +'$'); // Извлекайте символ в таких случаях как '>50' и '<=10'. // 'null' и '0' данны как значения по умолчанию // строка не содержит символ var symbol = (/^([<>=%]+)([\d.]+)/.exec(condition) || [0,null,0]); // Запустите символ, если он присутствует if (symbol[1]) return this[symbol[1]](+symbol[2]); return this._filter(function(prop) { return regex.test(prop); }); }, '>': function(v) { return this._filter(function(p) { return p > v; }); }, '>=': function(v) { return this._filter(function(p) { return p >= v; }); }, '<': function(v) { return this._filter(function(p) { return p < v; }); }, '<=': function(v) { return this._filter(function(p) { return p <= v; }); }, '%': function(v) { return this._filter(function(p) { return p % v === 0; }); }, // Сортируйте коллекция по данной функции // и верните новый экземпляр Templee sort: function(fn) { return this._new(this.data.sort(function() { return fn.apply(this, [].slice.call(arguments)); }.bind(this))); }, // Фильтруйте коллекция по индексу eq: function(index) { return this._new(this.data[index]); } }; 'forEach map slice every some reduce'.split(' ').forEach(function(method) { MyStorage.prototype[method] = function(fn) { return this.get()[method](function() { return fn.apply(arguments[0], [].slice.call(arguments)); }.bind(this)); }; }); win.stored = stored; // покажите конструктор пользователям }(window));
Использование Storage подобно тому, как вы бы использовали jQuery, очень интуитивно:
stored(users) .where('Age').is('>30') .and('Job').is('Повар').forEach(function(user) { console.log(user.name); //Елена });
Представьте себе возможность сочетания этого с системой HTML шаблонов, которую мы создали ранее:
var movies = [ { title: 'Spiderman', score: 7, gross: 90e6 }, { title: 'Aliens vs Predators', score: 5 , gross: 50e6 }, { title: 'American Beauty', score: 9.5 , gross: 140e6 }, { title: '500 Days of Summer', score: 8.5 , gross: 75e6 }, { title: 'Drive', score: 7.5 , gross: 120e6 }, { title: '127 Hours', score: 9 , gross: 78e6 } ]; var myMovies = stored(movies); var featured = template(myMovies.where('score').is('>=8').get(), [ '<div class="featured-movie">', '<h2>#{title}</h2>', '<h3>Score: #{score}, Gross: #{gross}</h3>', '</div>' ]); var bigGross = template(myMovies.where('gross').is('>80000000').get(), [ '<ul class="big-gross-movies">', '<li>#{title} <span class="gross">#{gross}</span></li>', '</ul>' ]); $('body').append([ '<h1>Featured Movies:</h1>'+ featured, '<h1>Big Gross Movies:</h1>'+ bigGross, ].join(''));
Другие более сложные примеры также возможны:
// Собрать все фильмы, которые начинаются с цифры myMovies.where('title').is(/^\d/).get('title'); //=> ["127 Hours", "500 Days of Summer"] // Превязывание myMovies.where('gross').is('>100000000') .and('score').is('>9').get('title') //=> ["American Beauty"]
И конечно же, вы можете присоединить все те полезные array методы: forEach, map, slice, every, some, reduce
myMovies.forEach(function() { setTimeout(function() { console.log(this.title) }.bind(this), 1000); });
С помощью функциональных методов мы можем получить очень чистый, абстрагируемый, модульный код. Такие библиотеки как jQuery и Zepto, используют некоторые из этих моделей, чтобы сделать свой код лучше.
Надеемся, вы подпитались свежими идеями, чтобы попробовать сделать код более разумным и эффективным.
Высоких конверсий!