
В этой статье пойдет речь об общих и не столь общих методах и шаблонах 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, используют некоторые из этих моделей, чтобы сделать свой код лучше.
Надеемся, вы подпитались свежими идеями, чтобы попробовать сделать код более разумным и эффективным.
Высоких конверсий!





