ilyachalov (ilyachalov) wrote,
ilyachalov
ilyachalov

Categories:

Учебник по JavaScript: ч.1, параметры и замыкания

Продолжаю читать шестой раздел («Продвинутая работа с функциями») первой части («Язык программирования JavaScript») учебника по JavaScript.

https://learn.javascript.ru

Часть 1. Язык программирования JavaScript (в т.ч. 93 подраздела)

Разделы:

6. Продвинутая работа с функциями (11 подразделов)

6.2 Остаточные параметры и оператор расширения
6.3 Замыкание

Как оказалось, в языке JavaScript в любую функцию можно при вызове передать любое количество параметров. Причем из переданных в функцию при ее вызове параметров идущие первыми функция может поместить в переменные, а оставшиеся («остаточные») — поместить в массив. К массиву с остаточными параметрами функция имеет доступ:

function sum(par1, par2, ...others) {
    let res = par1 + par2;

    for(let par of others) {
        res += par;
    }

    return res;
}

alert( sum(5, 4) );                 // 9
alert( sum(5, 4, 16) );             // 25
alert( sum(5, 4, 16, 3, 1, 4, 1) ); // 34

Получилась функция с переменным количеством параметров. При изучении языка C++ я о таком мечтал. Понятно, что без этого можно и обойтись, но всё же это очень удобная штука, которая дает большее чувство свободы при написании программ.

В языке C++ в таких случаях обычно используют передачу в функцию массива.

Предположим, что в программе на JavaScript в вышеприведенную функцию sum нужно передать числа, но они хранятся в массиве. Если просто передать массив в эту функцию, формально (синтаксически) ошибки не будет, но функция возвратит неправильный результат (требуется сумма чисел, хранящихся в массиве):

let arr = [11, 4, 5, 73, 1, 6];

alert( sum(arr) ); // будет возвращена строка "11,4,5,73,1,6undefined"

Для такого случая в языке JavaScript предусмотрено «расширение» массива в параметры функции (то есть элементы массива передаются в функцию как отдельные значения) с помощью оператора расширения ... (три точки). Пример:

let arr = [11, 4, 5, 73, 1, 6];

alert( sum(...arr) ); // 100

Замыкания

Довольно большая и довольно сложная для понимания начинающими тема. У авторов учебника не получилось изложить ее понятно. Думаю, этот подраздел требует доработки. Но разобраться можно. Пришлось прочитать текст трижды, еще я прочел и англоязычный вариант подраздела (между русскоязычным и англоязычным вариантами этого подраздела есть довольно существенная разница: тема изложена в разном порядке, с несколько разными примерами, а практических задач к подразделу почему-то в этих вариантах разное количество).

В принципе, речь идет об области видимости переменных (по-английски «variable scope»):

https://ru.wikipedia.org/wiki/Область_видимости

В этом плане язык JavaScript очень отличается от C++. В языке JavaScript функции являются «функциями первого класса», а в C++ — нет:

https://ru.wikipedia.org/wiki/Объект_первого_класса
https://ru.wikipedia.org/wiki/Функции_первого_класса
https://ru.wikipedia.org/wiki/Замыкание_(программирование)

Все эти три понятия («объект первого класса», «функция первого класса» и «замыкание» (по-английски «closure»)) для JavaScript являются родственными понятиями (одним и тем же). В языке JavaScript функции — это тоже объекты. Не все объекты — функции, но все функции — это объекты. Поэтому с функциями можно обращаться так же, как и с другими объектами: они могут быть переданы в другие функции в качестве параметра, возвращены из другой функции в качестве результата работы этой другой функции, присвоены переменной. В языке C++ такое обычно не делается.

В языке JavaScript практически все функции являются замыканиями. Что такое «замыкание»? Это такая функция, которая автоматически запоминает, где была создана, с помощью специального скрытого свойства Environment, которое недоступно программисту (его использует только движок языка).

Это скрытое свойство ссылается на специальный объект, тоже недоступный программисту, а доступный только движку языка. Этот объект называется «лексическим окружением» (по-английски «Lexical Environment»). Каждый объект LexicalEnvironment состоит из хранилища значений переменных в локальной области видимости (по-английски «Environment Record») и ссылки на внешний для данного замыкания (функции) объект LexicalEnvironment. Тот, в свою очередь, тоже состоит из тех же двух частей, хранилища и ссылки. То есть эта цепочка объектов LexicalEnvironment может быть длиной от одного объекта до нескольких, смотря на какой глубине вложенности создана данная функция (замыкание).

Важно запомнить следующее. При создании функции она запоминает внешнюю цепочку объектов-«лексических окружений». А при вызове функции на исполнение она создает локальный объект-«лексическое окружение». Поиск значения нужной для работы функции переменной происходит, начиная от локального (самого внутреннего) объекта-«лексического окружения», а если значение перменной не найдено, поиск продолжается во внешней цепочке объектов-«лексических окружений», от внутреннего к внешнему, глобальному объекту-«лексическому окружению», содержащему значения глобальных переменных.

Мне понравился пример со счетчиками из учебника, в котором используется эта система:

function makeCounter() { // создание функции
    let count = 0;

    return function() {  // создание функции
        return count++;
    };
}

let counter1 = makeCounter(); // вызов функции
let counter2 = makeCounter(); // вызов функции

alert( counter1() ); // вызов функции, результат: 0
alert( counter1() ); // вызов функции, результат: 1
alert( counter1() ); // вызов функции, результат: 2

alert( counter2() ); // вызов функции, результат: 0
alert( counter2() ); // вызов функции, результат: 1

Во-первых, тут можно видеть, что такое «функция первого класса» в языках программирования. Функция, производящая инкремент (увеличение на единицу), создается внутри другой функции, которая называется makeCounter. (Кстати, функции, определенные внутри других функций, называют «вложенными функциями», по-английски «nested function».) Функция инкремента возвращается функцией makeCounter в качестве результата своей работы, позже возвращенная функция присваивается переменным counter1 и counter2. То есть с функцией обращаются, как с объектом, а не как с простым перечнем инструкций, поэтому такие функции и называют «функциями первого класса».

Рассмотрим цепочки «лексических окружений» для создаваемых и вызываемых функций.

Функция makeCounter создается один раз в начале кода. В ее скрытое свойство Environment записывается ссылка на глобальное «лексическое окружение» (в нашем примере до создания функции в коде не определено глобальных переменных, поэтому хранилище переменных глобального «лексического окружения» пока пустое, ссылка на внешнее «лексическое окружение» равна null, так как у глобального «лексического окружения» нет внешнего «лексического окружения»):


Функция инкремента создается внутри функции makeCounter, поэтому у нее цепочка внешних «лексических окружений» состоит из двух звеньев:


Теперь рассмотрим, что происходит при двух вызовах функции makeCounter:


При каждом вызове функции makeCounter создается внутреннее для нее «лексическое окружение». Для разных вызовов одной и той же функции создаются отдельные объекты-«внутренние лексические окружения». Но каждый из этих отдельных объектов одновременно является внутренним для функции makeCounter и внешним для функций counter1 и counter2.

Из-за этого функции counter1 и counter2 имеют отдельные друг от друга внешние «лексические окружения» и отдельные друг от друга переменные count с начальным значением 0. При этом глобальное «лексическое окружение» для них — одно и то же.

Всё вышеописанное позволяет функциям counter1 и counter2 работать в качестве счетчиков независимо друг от друга. Это разные счетчики.

В хранилищах «лексических окружений» кроме переменных хранятся функции, но я не стал их отмечать на рисунках, чтобы не усложнять объяснения.

Следует отметить, что объекты-«лексические окружения» создаются не только для функций, но и для блоков кода, ветвлений и циклов.

Еще в этом подразделе затронуты вопросы «сборки мусора» (очистка памяти для объектов-«лексических окружений» выполняется так же, как и для обычных объектов: при утрате «достижимости», об этом ранее было в учебнике, в подразделе 4.3 «Сборка мусора») и оптимизации, выполняемые движком языка, которые нужно учитывать, рассматривая области видимости переменных при отладке программ.
Tags: Образование, Программирование
Subscribe

  • Post a new comment

    Error

    Anonymous comments are disabled in this journal

    default userpic

    Your IP address will be recorded 

  • 0 comments