Основы программирования на JavaScript. Учебное пособие
.pdfconsole.log (userName) выводит внешнюю переменную. На уровне кода это выглядит как поиск во внешней области видимости, вне функции.
Если обобщить:
–каждая функция при создании получает ссылку [[Scope]] на объект с переменными, в контексте которого была создана;
–при запуске функции создается новый объект с переменными LexicalEnvironment. Он получает ссылку на внешний объект перемен-
ных из [[Scope]];
–поиск переменных осуществляется сначала в текущем объекте переменных, а потом – по этой ссылке.
Текущее значение
Значение переменной из внешней области берется всегда текущее. Оно может быть уже не то, что было на момент создания функции.
Например, в коде ниже функция sayHi берет phrase из внешней области:
var phrase = 'Привет'; function sayHi(name) {
console.log (phrase + ', ' + name);
}
sayHi('Иван'); // Привет, Иван (*) phrase = 'Пока';
sayHi('Иван'); // Пока, Иван (**)
На момент первого запуска (*) переменная phrase имела значение 'Привет', а ко второму (**) изменила его на 'Пока'.
Это естественно, ведь для доступа к внешней переменной функция по ссылке [[Scope]] обращается во внешний объект переменных и берет то значение, которое там есть на момент обращения.
Вложенные функции
Внутри функции можно объявлять не только локальные переменные, но и другие функции. К примеру, вложенная функция может помочь лучше организовать код:
function sayHiBye(firstName, lastName) { console.log ( "Привет, " + getFullName() ); console.log ( "Пока, " + getFullName() ); function getFullName() {
return firstName + " " + lastName;
}
}
sayHiBye('Иван', 'Иванов'); // Привет, Иван Иванов ; Пока, Иван Иванов
Здесь для удобства создана вспомогательная функция getFullName().
41
Вложенные функции получают [[Scope]] так же, как и глобальные. В нашем случае:
getFullName.[[Scope]] = объект переменных текущего запуска sayHiBye
Благодаря этому getFullName() получает снаружи firstName и lastName.
Если переменная не найдена во внешнем объекте переменных, то она ищется в еще более внешнем (через [[Scope]] внешней функции), то есть такой пример тоже будет работать:
var phrase = 'Привет'; function say() {
function go() {
console.log ( phrase ); // найдет переменную
снаружи
}
go();
}
say();
Возврат функции
В разработке интерфейсов существует стандартный прием, при котором внутри одной функции создается другая и возвращается в качестве результата. Затем функция может назначаться как обработчик действий посетителя.
Создадим функцию-счетчик, которая считает свои вызовы и возвращает их текущее число:
function makeCounter() { var currentCount = 1;
return function() { // (**) return currentCount++;
};
}
var counter = makeCounter(); // (*)
//каждый вызов увеличивает счетчик и возвращает результат
console.log ( counter() ); // 1 console.log ( counter() ); // 2 console.log ( counter() ); // 3
//создать другой счетчик, он будет независим от
первого
var counter2 = makeCounter(); console.log ( counter2() ); // 1
Получаем два независимых счетчика counter и counter2, каждый из которых незаметным снаружи образом сохраняет текущее количе-
42
ство вызовов во внешней переменной currentCount, которая у каждого счетчика своя.
Если подробнее описать происходящее:
1. В строке (*) запускается makeCounter(). При этом создается LexicalEnvironment для переменных текущего вызова. В функции есть одна переменная var currentCount, которая станет свойством этого объекта. Она изначально инициализуется в undefined, затем в процессе выполнения получит значение 1:
function makeCounter() {
// LexicalEnvironment = { currentCount: undefined
}
var currentCount = 1;
// LexicalEnvironment = { currentCount: 1 } return function() { // [[Scope]] -> LexicalEnvi-
ronment (**)
return currentCount++;
};
}
var counter = makeCounter(); // (*)
2.В процессе выполнения makeCounter() создает функцию в строке (**). При создании эта функция получает внутреннее свойство
[[Scope]] со ссылкой на текущий LexicalEnvironment.
3.Далее вызов makeCounter() завершается и функция (**) возвращается и сохраняется во внешней переменной counter (*).
На этом создание «счетчика» завершено. Итоговым значением, записанным в переменную counter, является функция:
function() { // [[Scope]] -> {currentCount: 1} return currentCount++;
};
Возвращенная из makeCounter() функция counter помнит (через [[Scope]]) о том, в каком окружении была создана. Это и используется для хранения текущего значения счетчика.
Далее, когда-нибудь, функция counter будет вызвана. Эта функция состоит из одной строки: return currentCount++, ни переменных, ни параметров в ней нет, поэтому ее собственный объект переменных, назовем его LE, будет пуст.
Однако у нее есть свойство [[Scope]], которое указывает на внешнее окружение. Чтобы увеличить и вернуть currentCount, интерпретатор ищет в текущем объекте переменных LE, не находит, затем идет во внешний объект, там находит, изменяет и возвращает новое значение:
function makeCounter() { var currentCount = 1;
43
return function() { return currentCount++;
};
}
var counter = makeCounter(); // [[Scope]] -> {currentCount: 1}
console.log (counter()); // 1, [[Scope]] -> {currentCount: 1}
console.log (counter()); // 2, [[Scope]] -> {currentCount: 2}
console.log (counter()); // 3, [[Scope]] -> {currentCount: 3}
Переменную во внешней области видимости можно не только читать, но и изменять.
В примере выше было создано несколько взаимно независимых счетчиков. Они независимы, потому что при каждом запуске makeCounter создается свой объект переменных LexicalEnvironment со своим свойством currentCount, на который новый счетчик получит ссылку [[Scope]]:
var counter = makeCounter(); var counter2 = makeCounter(); console.log ( counter() ); // 1 console.log ( counter() ); // 2 console.log ( counter() ); // 3
console.log ( counter2() ); // 1, счетчики независимы
Свойства функции
Функция в JavaScript является объектом, поэтому можно присваивать ей свойства:
function f() {} f.test = 5;
console.log ( f.test );
Свойства функции не стоит путать с переменными и параметрами. Переменные доступны только внутри функции и создаются в процессе ее выполнения. Свойства функции доступны везде и всегда.
Если хочется привязать значение к функции, то можно им воспользоваться вместо внешних переменных. Перепишем пример со счетчиком:
function makeCounter() { function counter() {
return counter.currentCount++;
};
counter.currentCount = 1; return counter;
44
}
var counter = makeCounter(); console.log ( counter() ); // 1 console.log ( counter() ); // 2
При запуске пример работает так же. Принципиальная разница – во внутренней механике и в том, что свойство функции, в отличие от переменной из замыкания, общедоступно, к нему имеет доступ любой, у кого есть объект функции.
Например, можно взять и поменять счетчик из внешнего кода: var counter = makeCounter();
console.log ( counter() ); // 1 counter.currentCount = 5; console.log ( counter() ); // 5
Иногда свойства, привязанные к функции, называют «статическими переменными». В некоторых языках программирования можно объявлять переменную, которая сохраняет значение между вызовами функции. В JavaScript это ближайший аналог.
Практические задания
Выполните задания по варианту из табл. 9.
Таблица 9. Варианты заданий
№ |
Задания |
№ |
Задания |
№ |
Задания |
№ |
Задания |
|
вар. |
вар. |
вар. |
вар. |
|||||
|
|
|
|
|||||
1 |
1, 3, 5 |
6 |
2, 3, 7 |
11 |
2, 3, 5 |
16 |
1, 3, 7 |
|
2 |
1, 4, 6 |
7 |
2, 4, 5 |
12 |
2, 4, 6 |
17 |
1, 4, 7 |
|
3 |
1, 5, 7 |
8 |
2, 5, 6 |
13 |
2, 5, 7 |
18 |
1, 5, 6 |
|
4 |
1, 4, 5 |
9 |
2, 3, 6 |
14 |
2, 4, 7 |
19 |
1, 3, 6 |
|
5 |
1, 6, 7 |
10 |
2, 5, 7 |
15 |
2, 6, 7 |
20 |
1, 5, 7 |
1. Реализовать функцию sum, которая работает следующим образом: sum(a)(b) = a + b.
Пример работы функции: sum(1)(2) = 3 sum(5)(-1) = 4
2.Реализовать функцию pow, которая работает следующим образом: pow(a)(b) = ab.
Пример работы функции: pow(2)(3) = 8 pow(5)(-1) = 0.2
3.Реализовать функцию CreateModule, которая возвращает объект с методами: getValue и setValue.
Пример работы функции:
const module = CreateModule();
45
module.setValue(2) module.getValue() // 2
4.Реализовать функцию multiply(num), которая запоминает параметр num1 и возвращает функцию, которая умножает принимаемый аргумент на num.
Пример работы функции: const mul2 = multiply(2); mul2(5) // 10
const mul5 = multiply(5); mul5(10) // 50
5.Создать калькулятор, который может выполнять операции сложения, вычитания, деления, умножения. Калькулятор должен сохранять текущее состояние (результат предыдущей операции) и иметь метод getResult для его получения. Начальное значение равно 0.
Пример работы функции:
const calc = calculator(); calc.add(1) // 1 calc.multiply(5) // 5 calc.getResult() // 5 calc.substract(1) // 4 calc.divide(2) // 2 calc.getResult() // 2
6.Напишите функцию throttle(delay), которая принимает параметр delay (задержка в миллисекундах), сохраняет его в замыкании и возвращает новую функцию, которая выводит сообщение «executed» не чаще, чем через заданное время delay, иначе выводит сообщение
“not executed”.
Пример работы функции:
const func = throttle(500) func() // not executed func() // not executed
setTimeout(func, 100) // not executed setTimeout(func, 1500) // executed setTimeout(func, 1700) // not executed setTimeout(func, 2500) // executed
7.Написать функцию add, которая первым аргументом принимает начальное значение суммы, все последующие значения должны сохраняться и суммироваться с накопленным результатом. При запуске без аргументов функция должна вернуть накопленный результат, при этом количество вызовов с аргументом может быть любым.
Пример работы функции: add(10)(1)(2)(10)() // 23 add(10)(1)(2)(3)(4)...(10)(11)(12)()
46
2.5. Лабораторная работа и практическое занятие № 5. Объекты и прототипы
Объекты
Объекты используются для хранения коллекций различных значений и более сложных сущностей. Объект может быть создан с помощью фигурных скобок {…} с необязательным списком свойств. Свойство – это пара «ключ: значение», где ключ – это строка («имя свойства»), а значение может быть любого типа.
Объект можно представить в виде ящика с папками. Каждый элемент данных хранится в своей папке, на которой написан ключ. По ключу папку легко найти, удалить или добавить в нее что-либо.
Пустой объект («пустой ящик») можно создать, используя один из двух вариантов синтаксиса:
let user = new Object(); // "конструктор объекта" let user = {}; // "литерал объекта"
Литералы и свойства
При использовании литерального синтаксиса {...} можно сразу поместить в объект несколько свойств в виде пар «ключ: значение». Если в объекте несколько свойств, они перечисляются через запятую. Имя свойства может состоять из нескольких слов, но тогда оно должно быть заключено в кавычки:
let user = { name: "John", age: 30,
"likes birds": true // имя свойства в кавычках
};
Последнее свойство объекта может заканчиваться запятой. Это называется «висячая запятая». Такой подход упрощает добавление, удаление и перемещение свойств, так как все строки объекта становятся одинаковыми.
Для обращения к свойствам используется запись «через точку»:
// получаем свойства объекта: console.log ( user.name ); // John console.log ( user.age ); // 30
Для свойств, имена которых состоят из нескольких слов, доступ к значению «через точку» не работает:
user.likes birds = true // синтаксическая ошибка
Точка требует, чтобы ключ не имел пробелов, не начинался с цифры и не содержал специальные символы, кроме $ и _. Существует альтернативный способ доступа к свойствам.
47
Квадратные скобки также позволяют обратиться к свойству, имя которого может быть результатом выражения. Например, имя свойства может храниться в переменной:
let key = "likes birds";
// то же самое, что и user["likes birds"] = true; user[key] = true;
Переменная key может быть вычислена во время выполнения кода или зависеть от пользовательского ввода. Например:
let user = { name: "John", age: 30
};
let key = prompt("Что вы хотите узнать о пользова-
теле?", "name");
// доступ к свойству через переменную console.log ( user[key] ); // John (если ввели
"name")
Для удаления свойства используется оператор delete: delete user.age;
Вычисляемые свойства
В литеральной нотации можно использовать квадратные скобки для создания вычисляемого свойства. Например:
let fruit = prompt("Какой фрукт купить?", "apple"); let bag = {
[fruit]: 5, // имя свойства будет взято из fruit
};
console.log ( bag.apple ); // 5, если fruit="apple"
Смысл вычисляемого свойства прост: запись [fruit] означает, что имя свойства необходимо взять из переменной fruit. Если посетитель введет слово "apple", то в объекте bag будет свойство {apple: 5}.
Квадратные скобки позволяют использовать любые имена свойств и переменные, хотя и требуют более громоздких конструкций кода. Например:
let fruit = 'apple'; let bag = {
[fruit + 'Computers']: 5 // bag.appleComputers=5
};
Свойство из переменной
В реальном коде часто необходимо использовать существующие переменные как значения для свойств с тем же именем. Например:
function makeUser(name, age) { return {
name: name,
48
age: age
// ...другие свойства
};
}
let user = makeUser("John", 30); console.log (user.name); // John
Названия свойств name и age совпадают с названиями переменных, которые будут значениями этих свойств. Существуют короткие свойства для упрощения этой записи. В одном объекте можно использовать как обычные, так и короткие свойства:
let user = {
name, // короткое свойство, равно name:name age: 30
};
Ограничения на имена свойств
Имя переменной не может совпадать с зарезервированными словами, такими как «for», «let», «return» и т.д. Но для свойств объекта такого ограничения нет. Они могут быть в виде строк или символов. Все другие типы данных будут автоматически преобразованы к строке.
Например, если использовать число 0 в качестве ключа: let obj = {
0: "Тест" // равно "0": "Тест"
};
// обе функции console.log выведут свойство: console.log ( obj["0"] ); // Тест console.log ( obj[0] ); // Тест
Есть небольшой «подводный камень», связанный со специальным свойством __proto__. Нельзя установить его в необъектное значение:
let obj = {};
obj.__proto__ = 5; // присвоение числа console.log (obj.__proto__); // [object Object],
значение - это объект, значение 5 игнорируется
Проверка существования свойства, оператор «in»
В отличие от многих других языков, особенность JavaScriptобъектов в том, что можно получить доступ к любому свойству. Если свойства не существует, возвращается undefined, т.е. ошибки не будет.
Также существует специальный оператор in для проверки существования свойства в объекте. Синтаксис оператора: "key" in object.
Пример:
let user = { name: "John", age: 30 }; console.log ( "age" in user ); // true console.log ( "bar" in user ); // false
49
Слева от оператора in может быть не имя свойства (в кавычках), а имя переменной, в которой находится имя свойства (без кавычек):
let user = { age: 30 }; let key = "age";
console.log ( key in user ); // true
Цикл «for..in»
Для перебора всех свойств объекта используется цикл for..in. Синтаксис:
for (key in object) {
// тело цикла выполняется для свойств объекта
}
Например, выведем все свойства объекта user: let user = {
name: "John", age: 30, isAdmin: true
};
for (let key in user) {
// ключи
console.log ( key ); // name, age, isAdmin
// значения ключей
console.log ( user[key] ); // John, 30, true
}
Упорядочение свойств объекта
Свойства упорядочены особым образом: свойства с целочисленными ключами сортируются по возрастанию, остальные располагаются в порядке создания.
Например, объект с телефонными кодами: let codes = {
"44": "Великобритания", "49": "Германия", "41": "Швейцария",
// ..,
"1": "США"
};
for (let code in codes) {
console.log (code); // 1, 41, 44, 49
}
Чтобы коды выводились по важности для заказчика (в порядке создания), например для британской аудитории, можно сделать коды нецелочисленными, добавив знак "+" перед каждым кодом.
Копирование объектов и ссылки
Примитивные типы: строки, числа, логические значения – присваиваются и копируются «по значению». Объекты хранятся и копи-
50
