JavaScript. Одновременно пугающий и притягательный. Я уверен, если бы
Пабло Пикассо был программистом, то он создал именно этот язык. Null
здесь является объектом, пустым массивом, он равен false, а функции
летают рядом с ним как теннисные мячики.
Целевая аудитория статьи — продвинутые разработчики, которые хотят
узнать больше о JavaScript. Это коллекция странностей и хорошо хранимых
секретов. Секреты некоторых глав могут пригодиться вам при разработке
собственных программных продуктов, другие просто являются предметами для
смеха и осуждения. В любом случае, мы начинаем!
Типы данных и объявления
1. Null — это объект
Давайте начнем с известной всем странности. Null — объект, причем самый
высший из них. Null? Объект? «Этого не может быть, так как null — это
полное отсутствие какого-либо значения», — скажете вы. И вы, несомненно, правы. Однако, как бы то ни было, у меня есть доказательство. Вот оно:
alert (typeof null);
Несмотря на это, null не является экземпляром object. (Если вы не знали, значения в JavaScript являются
экземплярами базового
объекта. Каждое число является экземпляром объекта Number, каждый
объект является экземпляром объекта object и так далее.) Это возвращает
нам разум, ведь null —
отсутствие значения, значит он не может быть экземпляром чего-либо. Так следующая строка выводит false:
alert (null instanceof Object);
2. NaN — это число
Вы удивлены, что null — это объект? Тогда подумайте вот о чем — NaN
(«Not a Number» — не число) является числом! Чем дальше, тем страшнее!
NaN даже не равен самому себе. (Ваша голова еще не болит?)
alert (typeof NaN);
alert (NaN === NaN);
По факту, NaN не равен вообще ничему. Единственный способ сравнить что-нибудь с NaN — функция isNaN().
3. Массив без ключей == ложь (или о правдочке и кривдочке)
Вот и другая странность JavaScript:
alert (new Array() == false);
Чтобы понять, что здесь происходит, вы должны принять концепцию
правдочки и кривдочки (truthy and falsy). Есть «облегченные» виды true и
false, которые могут вас подвести, если вы углубитесь в логику и
философию.
Я прочитал много определений, что такое правдочка и кривдочка, и
простейшее, по моему мнению, звучит так: в JavaScript каждый объект
имеет встроенное булевское значение, которое вызывается, когда объект
должен вести себя как булевский; например, когда вы сравниваете его с
false.
Так как яблоки нельзя сравнивать с жемчугом, когда JavaScript вызывает
сравнение объектов разных типов, он выполняет их приведение к общему
типу.
False, zero, null, undefined, пустые строки и NaN — все приводится к false — не постоянно, а только в контексте данного выражения. Вот пример:
var someVar = 0;
alert (someVar == false);
Здесь мы запрашиваем сравнение числа 0 и значения false. Так как данные
не являются однотипными, JavaScript неявно преобразует их значения к
значениям встроенных булевских флажков, который (в случае с нулем) равен
кривдочке.
Вы могли заметить, что я не включал пустые массивы в список объектов,
приводящихся к false. Они ведут себя очень интересно — они вычисляются
как правдочка, но, при сравнении с булевским значением, ведут себя как
кривдочка. Вы еще живы? Хорошо, потому что я подготовил для вас еще один
пример:
var someVar = [];
alert (someVar == false);
if (someVar) alert ('hello');
Чтобы избежать приведения, вы можете использовать оператор сравнения
типа и значения === (вместо ==, который сравнивает только значения.)
var someVar = 0;
alert (someVar == 0);
alert (someVar === false);
Фух. Как вы уже поняли, приведение типов в JavaScript — довольно сложная
тема, так что вам стоит посвятить еще пару вечеров на изучение данной
темы.
Вы можете пообсуждать этот вопрос со мной
здесь(eng.) или, если вы уже достаточно готовы, то можете прочитать точный процесс приведения, он описан в
документации.
Регулярные выражения
4. Метод replace() может принимать callback функцию
Это один из самых секретных секретов JavaScript, он пришел к нам в
версии 1.3. Большинство использований replace выглядит как-то так:
alert('10 13 21 48 52'.replace(/d+/g, '*'));
Это простая замена, число — звездочка. Но что, если мы хотим получить
возможность самому выбрать место для замены? Что если мы хотим заменить
только те числа, которые меньше 30? Это может быть достигнуто с помощью
того же метода replace. Мы просто должны вызвать
callback функцию при каждом совпадении:
alert('10 13 21 48 52'.replace(/d+/g, function(match) {
return parseInt(match) < 30 ? '*' : match;
}));
Для каждого совпадения JavaScript будет вызывать нашу функцию, передавая
наше совпадение в качестве параметра. Потом, мы вернем звездочку (если
число меньше 30) или параметр без изменений.
5. Регулярные выражения — больше чем найти и заменить
Многие продвинутые JavaScript разработчики используют только
match и replace при работе с регулярными выражениями. Но JavaScript дает нам больше возможностей, чем просто 2 метода.
Наибольший интерес представляет test(), которые работает как match, но
не возвращает совпадение, он просто проверяет вхождение паттерна в
строку.
alert (/w{3,}/.test('Hello'));
Код сверху ищет в строке 3 и более буквенно-числовых символа и, так как
строка Hello соответствует этим требованиям, мы получаем true. Нам не
нужно само совпадение, только результат.
Также, объект RegExp, с помощью которого вы можете создавать
динамические регулярные выражения, противоположен статичному. Это дает
преимущество в виде короткой записи (мы просто окружили паттерн
слэшами). Но, в данном случае, вы не можете указывать на переменные, так
что создание динамических паттернов невозможно. С RegExp() вы можете
это сделать
function findWord(word, string) {
var instancesOfWord = string.match(new RegExp('\b'+word+'\b', 'ig'));
alert(instancesOfWord);
}
findWord('car', 'Carl went to buy a car but had forgotten his credit card.');
Здесь мы создаем динамический паттерн, который основан на значении
аргумента word. Функция возвращает число нахождения слова word в строке
целиком (не частью другого слова). Так, наш пример вернет car однажды,
игнорируя это слово, входящее в Carl и card. Мы делаем это с помощью
флага b.
Так как RegExp определен как строка, а не с помощью короткого
синтаксиса, мы можем использовать переменные при построении паттерна.
Это означает, что мы должны дважды экранировать любые специальные
символы, так как мы сделали с символом границы слова.
Функции и контексты
6. Вы можете подделать контекст
Контекст, в котором что-то выполняется, определяет, какие
переменные доступны. Свободный JavaScript (тот, который запускается не
из функции) оперирует с глобальным контекстом объекта window, к которому
всё имеет доступ, а локальные переменные, которые объявлены внутри
функции доступны только внутри этой функции, не снаружи.
var animal = 'dog';
function getAnimal(adjective) { alert(adjective+' '+this.animal); }
getAnimal('lovely');
В этом примере наша переменная и функция объявлены в глобальном
контексте. Так как он всегда указывает на текущий контекст, в этом
пример он указывает на window. Поэтому, когда функция ищет
window.animal, она находит его. Пока все нормально. Но, мы можем
заставить нашу функцию думать, что она запускается в другом контексте,
не смотря на родной контекст. Мы можем провернуть это с помощью метода
call(), а не с помощью функции.
var animal = 'dog';
function getAnimal(adjective) { alert(adjective+' '+this.animal); };
var myObj = {animal: 'camel'};
getAnimal.call(myObj, 'lovely');
Здесь наша функция вызывается не в контексте window, а в контексте myObj
как аргумент метода call(). Call() делает вид, что функция является
методом myObj. Заметьте, что любые аргументы, которые мы передаем в
call() после первого будут переданы в функцию.
Я слышал разработчиков, которые говорили, что они программируют многие
годы и никогда не использовали это, потому что это ненормальное
поведение кода. Хотя, это довольно интересно.
Кстати, apply() выполняет ту же работу, что и call(), кроме того, что аргументы передаются как массив. Вот пример:
getAnimal.apply(myObj, ['lovely']);
7. Функции могут вызывать сами себя
Ничего не запрещает этого:
(function() { alert('hello'); })();
Синтаксис очень прост: мы объявляем функцию и сразу же вызываем ее как и
другие функции, с помощью скобок. Вы можете спросить: «Зачем нам
использовать это?» Это может выглядеть как несоответствие терминов:
функция обычно содержит код, который мы хотим выполнить позже, не
сейчас, иначе, мы не помещаем его в функцию.
Одно из преимуществ
само-исполняющихся инструкций (СИИ) — использование текущих значений переменных внутри отложенного кода. Вот проблема:
var someVar = 'hello';
setTimeout(function() { alert(someVar); }, 1000);
var someVar = 'goodbye';
Новички на форумах спрашивают, почему alert выводит goodbye, а не hello.
Ответ в том, что значение переменной someVar не вычисляется до запуска
кода. А к тому времени, переменная уже перезаписана значением goodbye.
СИИ дают возможность решить эту проблему. Вместо использования
timeout callback, мы возвращаем его из
СИИ,
в который мы передаем текущее значение переменной в качестве аргумента.
Это означает, что мы передаем и изолируем текущее значение переменной,
защищая его от того, что происходит с настоящей переменной someVar. Это
как сделать фотографию автомобиля перед перекраской, фото не обновится,
оно всегда будет показывать цвет машины на момент взятия фотографии.
var someVar = 'hello';
setTimeout((function(someVar) {
return function() { alert(someVar); }
})(someVar), 1000);
var someVar = 'goodbye';
Теперь, мы видим hello, как мы и хотели, потому что функция показывает изолированную версию переменной.
Браузер
8. Firefox читает и возвращает цвета в RGB, не в HEX
Я никогда не мог понять, почему Mozilla делает это. Конечно, я должен показать пример:
Hello, world!
<script>
var ie = navigator.appVersion.indexOf('MSIE') != -1;
var p = document.getElementById('somePara');
alert(ie ? p.currentStyle.color : getComputedStyle(p, null).color);
</script>
Когда большинство браузеров возвращает ff9900, Firefox вернет rgb(255,
153, 0) эквивалент. Есть функции, которые конвертируют RGB в HEX.
Как вы могли заметить, у IE
(ненавижу тебя — прим. пер.) есть иные методы определения вычисленных стилей.
Дополнительно
9. 0.1 + 0.2 !== 0.3
Эта странность встречается не только в JavaScript, это проблема всей
компьютерной науки. Вывод будет такой — 0.30000000000000004.
Эта проблема называется компьютерной неточностью. Когда JavaScript
выполняет строку, он переводит значения в их двоичный эквивалент.
Вот где проблема начинается — 0.1 не 0.1 в двоичном эквиваленте, но
близкое к этому значение. Это как перевести текст с русского на
белорусский. Похоже, но не одинаково.
Остальное выходит за рамки этой статьи (у нас тут не математический кружок)
Данная тема широко рассматривается на форумах о компьютерной науке и
разработке. Вы можете предложить свои пути решения этой проблемы.
10. Неопределенное может быть определено
Хорошо, давайте закончим глупостью. Это странно звучит, но undefined —
незарезервированное слово в JavaScript, хотя и имеет особое значение —
показать, что переменная не определена.
var someVar;
alert(someVar == undefined);
Пока все в порядке. Но:
undefined = "I'm not undefined!";
var someVar;
alert(someVar == undefined);
Вы можете обратиться к
списку всех зарезервированных слов JavaScript.