Краткий конспект по языку JavaScript
Я —.NET разработчик. Но в последнее время всё чаще сталкиваюсь с JavaScript. Причём, процентах в 50 случаев я что-то на нём пишу, в остальных 50 — разбираюсь с чужим кодом, да ещё и прошедшим через минификацию, а иногда и обфускацию. В этой статье захотелось поделиться теми моментами, которые мне показались важными для понимания языка и эффективной работы с ним. Тут не будет ничего нового или неизвестного для людей, уже имевших дело с языком, и не будет чего-то такого, чего нельзя найти в других источниках. Для меня статья будет полезна как способ лучше разобраться в предмете, для читателей, я надеюсь, — как повод освежить знания.
Брендан Айк упоминал, что JavaScript был создан за 10 дней. Думаю, идея вынашивалась дольше. Как бы то ни было, язык получился и с тех пор только набирает популярность. Особенно после появления AJAX.
JavaScript — язык со слабой динамической неявной типизацией, автоматическим управлением памятью и прототипным наследованием.
JavaScript состоит из трёх обособленных частей:
- ядро (ECMAScript),
- объектная модель браузера (Browser Object Model или BOM),
- объектная модель документа (Document Object Model или DOM).
В статье, в основном, пойдёт речь о ядре. Конечно, в примерах кода будут использоваться элементы DOM и BOM, но заострять на них внимание не буду.
Система типов
Диаграмма типов JavaScript выглядит примерно так:
- Number
- String
- Boolean
- Object
- Function
- Array
- Date
- RegExp
- Null
- Undefined
Примерно — потому что ещё есть типы для ошибок, которые в эту диаграмму не вошли.
Из них 5 типов — примитивы:
- Number
- String
- Boolean
- Null
- Undefined
Всё остальное — объекты. Примитивы Boolean, String и Number могут быть обёрнуты в соответствующие объекты. В таком случае объекты будут экземплярами конструкторов Boolean, String и Number соответственно.
console.log('Holly Golightly' == new String('Holly Golightly')); // true
console.log('Holly Golightly' === new String('Holly Golightly')); // false
console.log('Holly Golightly' === String('Holly Golightly')); // true
Примитивы не имеют свойств. Если мы, например, попытаемся получить значение свойства length у примитива String, примитив будет преобразован к объекту, у объекта будет получено значение свойства, после чего тот отправится куда-нибудь в сборщик мусора.
Примитивам нельзя добавить свойство.
var person = 'Paul Varjak';
person.profession = 'writer';
console.log(person.profession); // undefined
Что произошло? Примитив person был преобразован к объекту, у объекта добавилось свойство, после чего тот отправился в небытие.
Числа
Числа в JavaScript представлены типом Number, в языке нет разделения на целые, числа с фиксированной точкой, числа с плавающей точкой. Не стоит забывать, что операции над дробными числами не всегда точны. Например,
console.log(0.1 + 0.2); // 0.30000000000000004
console.log(0.2 + 0.3); // 0.5
В языке есть несколько специальных значений для типа Number: +Infinity, -Infinity и NaN.
console.log(Infinity === Infinity) // true
А теперь, внимание
console.log(NaN === NaN); // false
NaN вообще ничему не равен. На случай проверки на NaN в язык встроена функция isNaN. Ах да, функция isFinite тоже есть. NaN заразен — результат любых любых арифметических операций или функций из Math с NaN тоже равен NaN. А вот <<, >>, >>>, ||, |, ^, &,! (их даже больше, чем арифметических) могут уничтожить NaN.
console.log(NaN || 2); // 2
console.log(0 || NaN); // NaN
console.log(!NaN); // true
Нужно быть осторожным с функцией parseInt, в старых и новых браузерах она ведет себя по-разному при обработке строк, начинающихся с нуля. А лучше всегда указывать систему счисления. Старые браузеры воспринимают начальный ноль как признак восьмеричного числа.
Строки
Строки в JavaScript — не что иное, как последовательности Unicode-символов. Отдельного типа для одиночных символов не существует, вместо него используется строка длины 1.
Строки в JavaScript неизменяемы. То есть, строку нельзя изменить после создания, все операции над строками создают новые объекты. Строки в качестве аргументов функции передаются по ссылке, а не по значению. Но даже если одна и та же строка обрабатывается разными методами, благодаря неизменяемости строк, код ведёт себя предсказуемо.
Не стоит забывать, что функция replace заменяет только первое вхождение подстроки в строку, если первым параметром получает строку, а не регулярное выражение. Причем регулярное выражение должно быть жадным (должно иметь модификатор g).
var str = "This is the house that Jack built. This is the malt That lay in the house that Jack built. This is the rat, That ate the malt That lay in the house that Jack built".replace("Jack", "Captain Jack Sparrow");
console.log(str);
// This is the house that Captain Jack Sparrow built. This is the malt That lay in the house that Jack built. This is the rat, That ate the malt That lay in the house that Jack built
var str = "This is the house that Jack built. This is the malt That lay in the house that Jack built. This is the rat, That ate the malt That lay in the house that Jack built".replace(/Jack/, "Captain Jack Sparrow");
console.log(str);
// This is the house that Captain Jack Sparrow built. This is the malt That lay in the house that Jack built. This is the rat, That ate the malt That lay in the house that Jack built
var str = "This is the house that Jack built. This is the malt That lay in the house that Jack built. This is the rat, That ate the malt That lay in the house that Jack built".replace(/Jack/g, "Captain Jack Sparrow");
console.log(str);
// This is the house that Captain Jack Sparrow built. This is the malt That lay in the house that Captain Jack Sparrow built. This is the rat, That ate the malt That lay in the house that Captain Jack Sparrow built
Кстати, callback-функцию тоже можно передать. Пример ниже может уберечь от разработки велосипедов.
var callback = (function(i){
return function(a){
i++;
return a + i;
};
})(0);
var str = "This is the house that Jack built. This is the malt That lay in the house that Jack built. This is the rat, That ate the malt That lay in the house that Jack built".replace(/Jack/g, callback);
console.log(str);
// This is the house that Jack1 built. This is the malt That lay in the house that Jack2 built. This is the rat, That ate the malt That lay in the house that Jack3 built
Важно помнить, что регулярные выражения хранят состояние, и результат работы методов test и exec зависит и от аргументов, и от состояния. Вот парочка примеров (спасибо, sekrasoft ):
/a/g.test('aa') // true
/a/g.test('ab') // true
var re = /a/;
re.test('aa') // true
re.test('ab') // true
// но
var re = /a/g;
re.test('aa') // true
re.lastIndex // 1, в 'aa' больше одного вхождения a, конца строки не достигли
re.test('ab') // false
re.lastIndex // 0, т.к. достигли конца строки
re.test('ab') // true
re.lastIndex // 1
Кириллические строки лучше сравнивать функцией localeCompare.
"Ёлка" > "Арбуз" // false
"Елка" > "Арбуз" // true
"Ёлка".localeCompare("Арбуз") // 1
"Елка".localeCompare("Арбуз") // 1
Преждевременная оптимизация — зло. Вот парочка примеров:
jsperf.com/array-join-vs-connect
// Вариант 1
var arr = [];
for (var i = 0; i < 10000; i++) {
arr.push(i);
}
var result = arr.join(', ')
// Вариант 2
var result = '';
for (var i = 0; i < 9999; i++) {
result += i + ', ';
}
result += i;
По производительности выигрывает второй вариант.
jsperf.com/heera-string-literal-vs-object
// Вариант 1
var s = '0123456789';
for (var i = 0; i < s.length; i++) {
s.charAt(i);
}
// Вариант 2
var s = new String('0123456789');
for (var i = 0; i < s.length; i++) {
s.charAt(i);
}
В этом случае с громадным отрывом побеждает первый вариант. Дело в том, что в движки браузеров уже встроены оптимизации таких шаблонов.
Для обрамления строк могут использоваться и двойные, и одинарные кавычки. JSON валиден только с двойными кавычками. В остальном же стоит придерживаться стиля, принятого на проекте.
null и undefined
null и undefined — примитивы, которые не имеют соответствующих им объектов. Поэтому попытка добавить свойство одному из этих примитивов или получить значение свойства, в отличие от строк, чисел и булевых значений, приведёт к TypeError.
Семантически null и undefined похожи, но и различия есть. null означает отсутствие объекта, undefined — отсутствие значения как такового. null — ключевое слово, undefined — свойство глобального контекста. Правда, в современных браузерах присвоить ему другое значение не получится. Любая не присвоенная переменная по-умолчанию имеет значение undefined.
Объекты
Объекты в JavaScript — ассоциативные массивы.
Пустой объект можно создать несколькими способами.
var obj = {};
var obj1 = new Object();
var obj2 = Object.create(null);
Первый способ называется литеральным и рекомендуется к использованию.
Создать объект можно и через функцию-конструктор.
function Company(name, address){
this.name = name;
this.address = address;
}
var company = new Company('Sing-Sing', 'Ossining, Westchester County, New York, United States');
Обратиться к свойствам созданного объекта можно двумя основными способами.
obj.name = 'Tiffany'
var name = obj.name;
и
obj['name'] = 'Tiffany';
var name = obj['name'];
Ключом в JavaScript объектах всегда служит строка, поэтому их не получается использовать как словари с произвольными ключами. Если попытаться в качестве ключа использовать не строку, то используемое значение будет приведено к строке.
var obj = {};
obj[1] = 1;
obj[null] = 2;
obj[{}] = 3;
obj[{a: 1}] = 4;
var val = obj[{}] // 4
Object.getOwnPropertyNames(obj); // ["1", "null", "[object Object]"]
Можно видеть, что {} и {a: 1} были приведены к одному и тому же значению — "[object Object]", а число и null были конвертированы в соответствующие строки.
В языке есть возможность создать свойства с геттерами и сеттерами. Есть несколько способов сделать это.
Литеральный:
var consts = {
get pi(){
return 3.141592;
}, set pi(){
throw new Error('Property is read only');
}
};
С помощью функции Object.defineProperty:
var consts = {};
Object.defineProperty(consts, ‘pi’, {
get : function () {
return 3.14159265359;
}, set: function(){
throw new Error('Property is read only');
}
});
С помощью функции Object.defineProperties:
var consts = {};
Object.defineProperties(consts, {'pi': {
get : function () {
return 3.14159265359;
}, set: function(){
throw new Error('Property is read only');
}}, 'e': {
get : function () {
return 2.71828182846;
}, set: function(){
throw new Error('Property is read only');
}
}});
Функции
В javascript функции являются объектами встроенного класса Function. Их можно присваивать переменным, передавать в качестве параметров в функции, возвращать как результат функции, обращаться к их свойствам.
Функции бывают именованные:
function getAnswer(){
return 42;
}
и анонимные:
var getAnswer = function(){
return 42;
}
В функцию можно передать сколько угодно параметров. Все они будут доступны через объект arguments. Кроме того, в этом объекте будут свойства length — количество аргументов и callee — ссылка на саму функцию.
function toArray(){
return Array.prototype.slice.call(arguments);
}
toArray(1, 2, 3, 6, 'Tiffany'); // [1, 2, 3, 6, "Tiffany"]
А ссылка на саму функцию позволяет создавать рекурсивные анонимные функции.
var fibonacci = function(n){
if (n <= 1){
return n;
} else {
return arguments.callee(n - 2) + arguments.callee(n - 1);
}
}
console.log(fibonacci(22)); // 17711
В последней редакции стандарта это свойство было удалено. Зато можно написать так:
var fibonacci = function f(n){
if (n <= 1){
return n;
} else {
return f(n - 2) + f(n - 1);
}
}
console.log(fibonacci(22)); // 17711
Область видимости переменных, объявленных через var, в JavaScript ограничена функцией. Ключевое слово let на подходе, оно будет задавать блочную область видимости, но пока браузеры поддерживают его неохотно.
Часто делают так. Это называется самовызывающейся функцией (self executed function).
(function(){
var person = new Person();
// do something
})();
Думаю, это хорошая практика. Таким образом получается не засорять глобальную область видимости ненужными переменными.
Ключевое слово this
Значение this в JavaScript не зависит от объекта, в котором создана функция. Оно определяется во время вызова.
В глобальном контексте:
console.log(this); // Window
Как свойство объекта:
var obj= {
data: 'Lula Mae'
};
function myFun() {
console.log(this);
}
obj.myMethod = myFun;
obj.myMethod(); // Object {data: "Lula Mae", myMethod: function}
Как обычная функция:
var obj = {
myMethod : function () {
console.log(this);
}
};
var myFun = obj.myMethod;
myFun(); // Window
Выполнение через eval (не используйте eval):
function myFun() {
console.log(this);
}
var obj = {
myMethod : function () {
eval("myFun()");
}
};
obj.myMethod(); // Window
С использованием методов call или apply:
function myFunc() {
console.log(this);
}
var obj = {
someData: "a string"
};
myFunc.call(obj); // Object {someData: "a string"}
В конструкторе:
var Person = function(name){
this.name = name;
console.log(this);
}
var person = new Person('Lula Mae'); // Person {name: "Lula Mae"}
Кстати, функции-конструкторы принято называть с заглавной буквы.
А ещё недавно появился метод bind, который привязывает функцию к контексту.
var myFunc = function() {
console.log(this);
}.bind(999);
myFunc(); // Number {[[PrimitiveValue]]: 999}
Замыкания
JavaScript устроен так, что вложенные функции имеют доступ к переменным внешних функций. Это и есть замыкание.
Вернёмся к примеру с Джеком.
var callback = (function(i){
return function(a){
i++;
return a + i;
};
})(0);
var str = 'Jack Jack Jack'.replace(/Jack/g, callback);
console.log(str); // Jack1 Jack2 Jack3
Получилось так, что функция, которая принимает аргумент a, имеет доступ к переменным внешней функции. И каждый раз при вызове внутренней функции мы увеличиваем на 1 переменную-счётчик i.
Пример попроще:
function add(a) {
var f = function(b) {
return a+b;
};
return f;
}
console.log(add(5)(7)); // 12
Давайте разберёмся, что произошло.
При вызове функции создаётся её контекст. Удобно считать его просто объектом, в котором каждой переменной функции соответствует свойство с её именем. А при вызове вложенных функций они получают ссылку на контекст внешней функции, При обращении к переменной происходит поиск в контексте функции, потом — в контексте внешней функции и так далее.
Вызов add(5)
- Создаётся [[scope]] = { a: 5 }
- Создаётся функция f = function(b) { return a+b; }
- Функция f получает ссылку на [[scope]]
- Ссылка на функцию f добавляется в [[scope]]
- Возвращается ссылка на функцию f
Вызов add(5)(7)
- Создаётся [[scope2]] = { b: 7 }
- Производится поиск свойства a в объекте [[scope2]]. Не найдено.
- Производится поиск свойства a в объекте [[scope]]. Найдено, значение равно 5.
- Производится поиск свойства b в объекте [[scope2]]. Найдено, значение равно 7.
- Складываются 5 и 7.
- Возвращается результат сложения — число 12.
Передача параметров
function myFunc(a, b){
console.log('myFunc begins');
console.log('myFunc ' + a);
console.log('myFunc ' + b);
}
function getArgument(arg){
console.log('getArgument ' + arg);
return arg;
}
myFunc(getArgument(5), getArgument(7));
// getArgument 5
// getArgument 7
// myFunc begins
// myFunc 5
// myFunc 7
И что с того? Во-первых, видно, что аргументы вычисляются до их передачи в функцию, это так называемая строгая стратегия обработки параметров… Во-вторых, вычисляются они слева направо. Такое поведение определено стандартом и не зависит от реализации.
А по ссылке или по значению передаются значения? Примитивы, кроме строк, передаются по значению. Строки передаются по ссылке, а сравниваются по значению. Так как строки неизменяемы, это позволяет экономить память и не имеет каких-то побочных эффектов. Объекты передаются по ссылке. Тут следует оговориться, что перед передачей аргумента по ссылке в функцию создаётся копия ccылки, которая существует только внутри вызываемой функции. То есть, другими словами, объекты передаются по значению ссылки. Посмотрим на примере:
var a = { data: 'foo' };
var b = { data: 'bar' };
function change(arg){
arg.data = 'data';
}
function swap(x, y){
var tmp = x;
x = y;
y = tmp;
}
change(a);
swap(a, b);
console.log(a); // Object {data: "data"}
console.log(b); // Object {data: "bar"}
Получается, свойство объекта изменить можно — копия ссылки ссылается на тот же объект. А при присвоении нового значения ссылке оно действует только на копию, которая была передана в функцию, а не на исходную ссылку.
Всплытие переменных и разрешение имен
Давайте посмотрим на такой код.
var a = 1;
function b(){
console.log(a);
if (false){
var a = 2;
}
}
b(); // undefined
Почему не 1? Дело в том что у переменных, объявленных через var, область видимости ограничивается функцией, а ещё в том, что существует механизм всплытия переменных. Интерпретатор языка всегда переносит объявление всех переменных в начало области видимости. При этом переносится только объявление, а присвоение значения не переносится. Код выше эквивалентен следующему:
var a = 1;
function b(){
var a;
console.log(a);
if (false){
a = 2;
}
}
b();
Алгоритм поиска объекта по имени такой:
- Искать среди предопределённых языком переменных. Если найден — использовать его.
- Искать среди формальных параметров функции. Если найден — использовать его.
- Искать среди объявленных функций текущей области видимости. Если найден — использовать его.
- Искать среди объявленных переменных текущей области видимости. Если найден — использовать его.
- Перейти на область видимости выше и начать сначала.
Исключение подтверждает существование общего правила, где эти исключения не оговорены. Переменная arguments — как раз такое исключение. Хоть это и предопределённая языком переменная, формальный параметр arguments имеет приоритет при поиске значения.
function a(){
console.log(arguments);
}
function b(arguments){
console.log(arguments);
}
a(); // []
b(); // undefined
Наследование
В отличие от таких языков, как Java, C#, C++, в JavaScript наследуются не классы, а объекты. Тем не менее, class — зарезервированное слово, назвать так переменную нельзя.
Каждый объект содержит ссылку на другой объект, который называется прототипом. Прототип содержит ссылку на свой прототип и так далее. В какой-то момент находится объект с прототипом null, и цепочка заканчивается.
Есть мнение, что прототипная модель наследования более мощная, чем наследование, основанное на классах. В пользу такого суждения есть следующий аргумент: наследование на классах реализуется поверх прототипного достаточно легко, а наоборот — нет.
Уже упоминалось, что объекты в JavaScript — просто ассоциативные словари свойств. Теперь выясняется, что есть ещё скрытое свойство, будем обозначать его [[Prototype]], которое нельзя использовать в коде, и которое служит “запасным” источником свойств. Давайте разберёмся, как происходит поиск свойства в таком случае.
Пусть у нас есть такая цепочка прототипов
{a:1, b:2} ---> {b:3, c:4} ---> null
console.log(o.a); // 1
В объекте есть собственное свойство a, поэтому просто берём его значение.
console.log(o.b); // 2
В объекте есть собственное свойство b, поэтому просто берём его значение. В прототипе такое свойство тоже есть, но мы его не проверяем. Это называется перекрытием свойств.
console.log(o.c); // 4
В объекте нет свойства с. Зато оно есть в прототипе, используем свойство прототипа.
console.log(o.d); // undefined
В объекте нет свойства d. Ищем в прототипе, там его тоже нет. Продолжаем поиск в прототипе прототипа, а он равен null. Прекращаем поиск, свойство не найдено, возвращаем null.
С методами всё происходит точно так же. Ещё бы, методы — это тоже свойства объекта. Один нюанс — ключевое слово this в функции будет указывать на объект, а не на прототип, даже если функция найдена в прототипе. В принципе, это логично.
Как же назначить прототип объекту? Есть несколько способов.
Во-первых, прототипы назначаются автоматически при создании объектов, массивов, функций.
var o = {a: 1}; // o ---> Object.prototype ---> null
var a = ["horse", "table"]; // a ---> Array.prototype ---> Object.prototype ---> null
function getRandomNumber(){
return 4;
}
// getRandomNumber ---> Function.prototype ---> Object.prototype ---> null
Во-вторых, прототипы можно назначать при создании объектов через конструктор. В таком случае прототипом объекта становится прототип конструктора.
function Animal(){
this.eat = function(){
console.log('eat called');
};
}
function Cat(name){
this.name = name;
};
Cat.prototype = new Animal();
var cat = new Cat('No name cat');
cat.eat(); // eat called
console.log(cat.name); // No name cat
console.log(cat.constructor); // Animal
Свойству [[Prototype]] присваивается значение Cat.prototype при выполнении new Cat(). Вместе с тем свойству constructor объекта cat присвоилось значение Animal. Можно исправить код, чтобы конструктор оставался правильным. Добавим строчку Cat.prototype.constructor = Cat;
function Animal(){
this.eat = function(){
console.log('eat called');
};
}
function Cat(name){
this.name = name;
};
Cat.prototype = new Animal();
Cat.prototype.constructor = Cat;
var cat = new Cat('No name cat');
cat.eat(); // eat called
console.log(cat.name); // No name cat
console.log(cat.constructor); // Cat
В-третьих, прототип можно назначить при создании объекта при помощи метода Object.create. Прототип указывается в первом аргументе этого метода.
var a = {a: 1}; // a ---> Object.prototype ---> null
var b = Object.create(a); // b ---> a ---> Object.prototype ---> null
console.log(b.a); // 1
Просто так присвоить прототип нельзя. prototype — свойство конструктора, а не объекта.
var o = { a: 1 };
o.prototype = { b: 2 };
console.log(o.b); // undefined
Зато можно изменить прототипы встроенных типов, например, Object. Это плохая практика. Единственным приемлемым случаем для изменения встроенных прототипов может быть только эмуляция возможностей из более новых версий языка.
Strict Mode
Этот режим включается директивой
'use strict';
Эта директива означает, что код будет выполняться в соответствии со стандартом ECMAScript 5. То есть, некоторые вещи будут работать по-другому. Возможно, более логично и более правильно, но не так, как раньше. Директива может применяться к скрипту целиком или к отдельной функции, включая вложенные функции. Под вложенными понимаются функции, объявленные внутри функции. Если же функция объявлена в другом месте, а внутри “строгой” функции только выполняется, то директива на неё не действует. На примере хорошо видно это:
function a(){
console.log(arguments.callee);
}
(function() {
"use strict";
function b(){
console.log(arguments.callee);
}
a(); // function a(){...}
b(); // TypeError
})();
А самовызывающаяся функция тут для того, чтобы пример работал в консоли браузера. “use strict” в консоли не работает вне функции.
Что изменится? Во-первых, больше не будет доступно свойство arguments.callee, об этом уже упоминалось.
Во-вторых, this не будет подменяться на глобальный объект в случае null или undefined или оборачиваться в экземпляр конструктора в случае примитива.
(function() {
"use strict";
var a = function(){
console.log(this);
}.bind(null)
a(); // null
})();
(function() {
var a = function(){
console.log(this);
}.bind(null)
a(); // Window
})();
В-третьих, будет нельзя создавать глобальные переменные без их явного объявления.
(function() {
"use strict";
a = 0; // ReferenceError: a is not defined
})();
В-четвертых, больше не будет поддерживаться конструкция with(obj){}
В-пятых, нельзя будет создать объект с одинаковыми ключами.
(function() {
"use strict";
var o = { p: 1, p: 2 }; // SyntaxError: Duplicate data property in object literal not allowed in strict mode
})();
Это не всё, но перечислять всё не буду.
Приватное и публичное
В языке нет ключевых слов private и public, но разделять приватные и публичные данные можно. Дня этого есть несколько способов, например Module Pattern:
blackBox = (function(){
var items = ['table', 'horse', 'pen', 48];
return {
pop: function(){
return items[~~(Math.random() * items.length)];
}
};
})();
console.log(blackBox.pop()); // 48
console.log(blackBox.items); // undefined
Или можно сделать так:
function BlackBox(){
var items = ['table', 'horse', 'pen', 48];
this.pop = function(){
return items[~~(Math.random() * items.length)];
};
}
var blackBox = new BlackBox();
console.log(blackBox.pop()); // "pen"
console.log(blackBox.items); // undefined
Отладка
Инструкция debugger вызывает отладчик, если он доступен. При этом выполнение скрипта останавливается на строчке с этой инструкцией.
Например, если приходится разбираться, откуда появилось всплывающее сообщение, можно выполнить в консоли:
window.alert = function(){
debugger;
};
Теперь вместо сообщения запустится отладчик, а выполнение скрипта остановится в месте вызова alert.
Иногда бывает полезно логировать стек вызовов какой-нибудь функции. Сделать это можно так:
function a(){
console.log(new Error().stack);
}
function b(){
a();
}
function c(){
b();
}
c();