JavaScript: класс VS функция-конструктор (визуализация)

Начало в прошлом посте: JavaScript: класс VS функция-конструктор.

Итак, мы создали объект clock класса Clock двумя способами: 1) с помощью функции-конструктора Clock и 2) с помощью синтаксиса с ключевым словом class. Тексты обоих скриптов приведены в прошлом посте.

Я решил пойти несколько дальше, чем требуется по условиям задачи, и разобраться, как устроены внутри объекты, полученные первым способом и вторым способом, и чем они отличаются. Для этого я использовал консоль разработчика. По результатам этого разбора я нарисовал две схемы, которые можно увидеть ниже. На схемах отражена не полная картина хранения данных движком JavaScript, а только те элементы этой картины, которые важны в контексте нашей задачи. А именно я хотел знать, где и в каком качестве хранятся переменные timer и template, а также функции render, start и stop.

1. Схема объекта clock, полученного с помощью функции-конструктора Clock:



В данном случае функции start и stop были добавлены в сам конструируемый объект clock в качестве методов. У каждого из этих методов есть скрытое свойство [[Scopes]], указывающее на некую структуру данных Scopes, похожую на массив (английское слово «Scopes» по-русски означает «Области видимости»). Ссылки [[Scopes]] обоих методов, очевидно, указывают на одну и ту же структуру данных, потому что оба метода находятся в одной и той же области видимости и так было бы правильно из соображений экономии памяти.

В массиве Scopes есть три элемента, каждый из которых содержит отдельный объект, представляющий свою область видимости. В первом элементе (с кодом 0) массива содержится объект «Closure (Clock)», представляющий область видимости внутри функции-конструктора Clock. Английское слово «Closure» по-русски означает «Замыкание». Очевидно, что здесь речь идет про то замыкание, о котором рассказывалось в подразделе 6.3 «Замыкание» учебника. В этом самом объекте «Closure (Clock)» и хранятся вспомогательная функция render, а также переменные template и timer.

2. Схема объекта clock, полученного с помощью синтаксиса с ключевым словом class:



Тут всё выглядит гораздо проще. При создании объекта clock функции render, start и stop помещаются в качестве методов в прототип (объект-родитель) объекта clock. Оттуда они наследуются объектом clock, то есть становятся доступны программисту через объект clock.

Значение переменной-параметра template в теле метода constructor класса Clock помещается в создаваемое для этого свойство template конструируемого объекта clock. Поэтому это свойство доступно сразу же после создания объекта clock.

Свойство же timer создается в объекте clock только при вызове метода clock.start, потому что оно создается в теле этого метода.

3. Различие в работе этих объектов. При создании объекта clock вторым способом программисту доступны три метода clock.start, clock.stop и clock.render, а также два свойства clock.template и clock.timer.

При создании этого объекта первым способом программисту доступны только два метода clock.start и clock.stop, а функция render, переменная-параметр template и переменная timer — недоступны, потому что они объявлены локально внутри функции-конструктора Clock и недоступны извне.

JavaScript: класс VS функция-конструктор

Прочел подраздел 9.1 «Класс: базовый синтаксис» учебника по JavaScript:
https://learn.javascript.ru/class

Для создания множества объектов одного вида в учебнике ранее предлагалось использовать функцию-конструктор, о которой рассказывалось в подразделе 4.5 «Конструкторы, создание объектов через "new"» учебника:
https://learn.javascript.ru/constructor-new

Классы, в принципе, используются для того же.

К подразделу 9.1 имеется только одна задача «Перепишите класс», в которой задан класс Clock, написанный с помощью функции-конструктора. Требуется переписать этот класс с помощью синтаксиса, использующего ключевое слово class.

Сама постановка задачи мне нравится. Если в языке есть два способа сделать одно и то же, то неплохо было бы знать, чем эти способы отличаются друг от друга и чем отличаются друг от друга полученные с помощью этих способов результаты. Однако, как оказалось, изложенного в подразделе недостаточно (по крайней мере, мне) для решения этой задачи с налёта. Думаю, подраздел 9.1 учебника требует дальнейшей доработки.

Формулировка самой задачи тоже вызывает вопросы. В тексте задачи код класса Clock не задан и сразу непонятно, где его искать (в англоязычной версии этой задачи, кстати, сказано, что этот код находится в песочнице). В итоге извлекаем код из песочницы:

0. Исходный код
//function Clock({ template }) {
function Clock( template ) {

    let timer;

    function render() {
        let date = new Date();

        let hours = date.getHours();
        if (hours < 10) hours = '0' + hours;

        let mins = date.getMinutes();
        if (mins < 10) mins = '0' + mins;

        let secs = date.getSeconds();
        if (secs < 10) secs = '0' + secs;

        let output = template
            .replace('h', hours)
            .replace('m', mins)
            .replace('s', secs);

        console.log(output);
    }

    this.stop = function() {
        clearInterval(timer);
    };

    this.start = function() {
        render();
        timer = setInterval(render, 1000);
    };

}
  
//let clock = new Clock({template: 'h:m:s'});
let clock = new Clock('h:m:s');
clock.start();

В этом коде объявляется функция-конструктор Clock, в теле которой объявлены переменная timer и три функции render, this.stop и this.start. Далее с помощью этой функции-конструктора создается объект clock, после чего запускается метод этого объекта clock.start. При запуске этого скрипта в браузерном окружении работы этого скрипта не видно, так как на экран ничего не выводится. Однако, если после запуска скрипта открыть консоль разработчика, то в ней можно увидеть как тикают созданные нами часы: это реализовано бесконечным выводом одной за другой строк с текущим временем. Новые строки в консоль выводятся каждую секунду.

Разобраться в работе этого кода несложно, если вы читали предыдущие подразделы учебника.

Единственное, что меня сначала ввело в ступор, это передача параметра template в функцию-конструктор в первой строке кода:
function Clock({ template }) {

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

Итак, начнем переписывать класс. Понятно, что нужно применить ключевое слово class, создать метод constructor, в который нужно передать параметр template, а методы this.stop и this.start легко переделать в методы класса:

1.
class Clock {

    constructor(template) {}

    stop() {
        clearInterval(timer);
    }

    start() {
        render();
        timer = setInterval(render, 1000);
    }

}

Но что делать с переменной timer и вспомогательной функцией render? Почему бы не оставить их, как есть, ведь класс — это разновидность функции в языке JavaScript (так сказано в подразделе 9.1 учебника), а в теле функции можно объявлять локальные переменные и локальные функции. Однако, как выяснилось, в теле класса такие объявления запрещены. Например, попробуем запустить в браузере следующие куски кода:

class Clock {

    let timer;

}
или
class Clock {

    function render() {}

}

Мой браузер даже не может отобразить такой скрипт в консоли разработчика и выдает ошибку «Uncaught SyntaxError: Unexpected identifier».

После этого я решил, что вспомогательную функцию render тоже можно сделать методом класса. А переменную timer можно сделать свойством класса. Тогда получается следующее:

2.
class Clock {

    timer;

    constructor(template) {}

    render() {
        let date = new Date();

        let hours = date.getHours();
        if (hours < 10) hours = '0' + hours;

        let mins = date.getMinutes();
        if (mins < 10) mins = '0' + mins;

        let secs = date.getSeconds();
        if (secs < 10) secs = '0' + secs;

        let output = template
            .replace('h', hours)
            .replace('m', mins)
            .replace('s', secs);

        console.log(output);
    }

    stop() {
        clearInterval(timer);
    }

    start() {
        render();
        timer = setInterval(render, 1000);
    }

}

let clock = new Clock('h:m:s');
clock.start();

Синтаксически данный вариант кода класса уже не вызовет ошибки и объект clock будет успешно создан. Но при запуске метода clock.start выясняется, что внутри этого метода не видно метода render, что приводит к ошибке.

На самом деле, как я понял, обращение к методам и свойствам класса из других методов этого же класса должно происходить через переменную this. В подразделе 9.1 об этом прямо не сказано, но показано в примерах. В языке C++, который я изучал ранее, в методах класса можно спокойно обращаться к другим методам и свойствам класса напрямую. Ладно, переписываем класс с учетом этого момента (объявление свойства timer класса теперь можно убрать, так как оно должно автоматически добавиться к объекту при первом к нему обращении из метода clock.start):

3.
class Clock {

    // timer;

    constructor(template) {}

    render() {
        let date = new Date();

        let hours = date.getHours();
        if (hours < 10) hours = '0' + hours;

        let mins = date.getMinutes();
        if (mins < 10) mins = '0' + mins;

        let secs = date.getSeconds();
        if (secs < 10) secs = '0' + secs;

        let output = template
            .replace('h', hours)
            .replace('m', mins)
            .replace('s', secs);

        console.log(output);
    }

    stop() {
        clearInterval(this.timer);
    }

    start() {
        this.render();
        this.timer = setInterval(this.render, 1000);
    }

}

let clock = new Clock('h:m:s');
clock.start();

Теперь метод this.render класса запустился из первой строки метода clock.start, но внутри метода this.render не видно параметра template. Очевидно, что значение параметра template должно быть доступно всем методам класса, а методы класса, как я понимаю, видят только свойства класса. Следовательно, для хранения значения параметра template нужно создать свойство класса, назовем его this.template. Так же, как и свойство timer класса, объявлять свойство this.template класса отдельно не будем, а создадим его в объекте при первом к нему обращении. Где же сделать это первое обращение? Очевидно, что в методе constructor. Переписываем код:

4.
class Clock {

    constructor(template) {
        this.template = template;
    }

    render() {
        let date = new Date();

        let hours = date.getHours();
        if (hours < 10) hours = '0' + hours;

        let mins = date.getMinutes();
        if (mins < 10) mins = '0' + mins;

        let secs = date.getSeconds();
        if (secs < 10) secs = '0' + secs;

        let output = this.template
            .replace('h', hours)
            .replace('m', mins)
            .replace('s', secs);

        console.log(output);
    }

    stop() {
        clearInterval(this.timer);
    }

    start() {
        this.render();
        this.timer = setInterval(this.render, 1000);
    }

}

let clock = new Clock('h:m:s');
clock.start();

Код всё еще не работает. Почему? Первая строка метода clock.start теперь отрабатывает успешно. Однако, при выполнении второй строки этого метода происходит ошибка (в консоли разработчика показаны уже последствия этой ошибки):
        this.timer = setInterval(this.render, 1000);

При обращении к this.render происходит потеря this. Об этом подробно рассказывалось в подразделе 6.10 «Привязка контекста к функции». Один из способов избавиться от этой ошибки — использование стрелочной функции в качестве обертки. Переписываем код:

5. Окончательное решение
class Clock {

    constructor(template) {
        this.template = template;
    }

    render() {
        let date = new Date();

        let hours = date.getHours();
        if (hours < 10) hours = '0' + hours;

        let mins = date.getMinutes();
        if (mins < 10) mins = '0' + mins;

        let secs = date.getSeconds();
        if (secs < 10) secs = '0' + secs;

        let output = this.template
            .replace('h', hours)
            .replace('m', mins)
            .replace('s', secs);

        console.log(output);
    }

    stop() {
        clearInterval(this.timer);
    }

    start() {
        this.render();
        this.timer = setInterval(() => this.render(), 1000);
    }

}

let clock = new Clock('h:m:s');
clock.start();

Теперь всё работает. Задание выполнено.

Что интересно, использование класса, объявленного с помощью функции-конструктора, и использование класса, объявленного с помощью ключевого слова class, в итоге ничем друг от друга не отличаются. Сравните последние две строки исходного кода и последние две строки окончательного решения. Они идентичны:

let clock = new Clock('h:m:s');
clock.start();

Учебник по JavaScript: ч.1: прототипы, наследование

Мои посты, связанные с этим постом:
1. JavaScipt: пример создания полифила
2. JavaScript: решение задачи добавления toString в словарь

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

https://learn.javascript.ru

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

Разделы:

8. Прототипы, наследование (4 подраздела)

8.1 Прототипное наследование
8.2 F.prototype
8.3 Встроенные прототипы
8.4 Методы прототипов, объекты без свойства __proto__

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

В языке JavaScript объект-родитель называется прототипом. У каждого объекта есть скрытое свойство [[Prototype]], которое либо указывает на объект-родитель, либо равно специальному значению null (это означает, что у объекта нет объекта-родителя). Множественное наследование (когда у объекта больше одного объекта-родителя), как я понимаю, в JavaScript невозможно (в отличие, к примеру, от языка C++). Одним из способов изменить значение скрытого свойства [[Prototype]] является свойство-аксессор __proto__ (про свойства-аксессоры рассказывалось в подразделе 7.2 «Свойства - геттеры и сеттеры» учебника).

В подразделе 8.2 «F.prototype» рассказано про еще один способ изменить скрытое свойство [[Prototype]] объекта. Этот способ работает при создании нового объекта с помощью функции-конструктора (про функции-конструкторы рассказывалось в подразделе 4.5 «Конструкторы, создание объектов через "new"» учебника). Синтаксически функция-конструктор ничем не отличается от любой другой функции. Поэтому свойство prototype, нужное для конструирования нового объекта, есть у каждой функции в языке JavaScript. Это свойство по умолчанию содержит ссылку на объект с единственным свойством constructor, указывающим обратно на функцию-конструктор (это сделано для того, чтобы можно было определить, с помощью какой функции-конструктора был создан данный объект). При создании нового объекта с помощью функции-конструктора объектом-родителем нового объекта становится объект, указанный в свойстве prototype функции-конструктора.

В подразделе 8.3 «Встроенные прототипы» рассказано про то, как устроены встроенные в движок функции-конструкторы Object, Array, Function, Number и так далее. Все эти встроенные функции-конструкторы и встроенные объекты состоят в одной иерархии, наследуя друг от друга. На вершине этой иерархии находится объект, на который указывает свойство Object.prototype. Из этого подраздела можно понять, как устроен язык JavaScript.

В подразделе 8.4 «Методы прототипов, объекты без свойства __proto__» рассказано про современные способы изменения скрытого свойства [[Prototype]] объекта (перечисленные выше способы с помощью свойства-аксессора __proto__ объекта и с помощью свойства prototype функции-конструктора при создании нового объекта считаются устаревающими). Эти современные способы заключаются в использовании методов Object.create, Object.getPrototypeOf и Object.setPrototypeOf.

Вчера открыл беговой сезон

Обычно начинаю с мая, но в этом году весна выдалась ранняя.

Пробежал свою любимую десяточку неожиданно с первого раза. Обычно начинаю с 3-4 километров, дыхалка и физика после зимы сразу не позволяют бежать более длинные дистанции. Но в этот раз получилось.

Выбежал при температуре +12 °C, закончил при +9 °C. Солнечно, ветра почти не было. Погода замечательная. Бежал в в футболке, спортивных штанах и вязаной шапке.

Вчера ныли мышцы бедра и поясничные. Сегодня — мышцы спины и икроножные.

JavaScript: решение задачи добавления toString в словарь

Задача называется «Добавьте toString в словарь»:
https://learn.javascript.ru/task/dictionary-tostring

Это одна из двух задач к подразделу 8.4 «Методы прототипов, объекты без свойства __proto__» учебника по JavaScript.

По условиям задачи ученику дан следующий код:
let dictionary = Object.create(null);

// здесь напишите ваш код, который добавляет метод dictionary.toString

// добавляем немного данных
dictionary.apple = "Apple";
dictionary.__proto__ = "test"; // здесь __proto__ -- это обычный ключ

// только apple и __proto__ выведены в цикле
for(let key in dictionary) {
    alert(key);                // "apple", затем "__proto__"
}

// ваш метод toString в действии
alert(dictionary);             // "apple,__proto__"

Здесь в первой строке создается объект, ссылка на который записывается в переменную dictionary (по-русски это слово означает «словарь»). Далее в словарь записываются две пары «ключ/значение», это реализовано добавлением к объекту двух свойств. При этом имя свойства является ключом, а значение свойства — значением. После этого в цикле for..in последовательно выводятся на экран имена всех свойств (ключи) объекта-словаря. В конце делается попытка вывести на экран объект-словарь.

По замыслу автора этого кода сначала на экран последовательно должны быть выведены ключи, в данном случае apple и затем __proto__. При выводе на экран объекта-словаря должна быть выведена строка, содержащая ключи (имена свойств объекта) через запятую, то есть в данном случае apple,__proto__.

Если запустить этот код в браузере в заданном виде, то имена свойств (ключи) в цикле for..in будут выведены успешно. Однако, при попытке вывести на экран объект ничего не произойдет из-за ошибки (ее можно увидеть в консоли разработчика): «Uncaught TypeError: Cannot convert object to primitive value» (по крайней мере, так она показана в моем браузере «Microsoft Edge», который сейчас делают на базе движка «Chromium»).

Эта ошибка происходит из-за того, что в данном случае объект-словарь создан специальным образом, без прототипа. В его скрытое свойство [[Prototype]] записано специальное значение null с помощью метода Object.create. Если бы мы создали объект обычным способом (с помощью «литерального синтаксиса»), то ошибки при выводе объекта на экран не произошло бы. Например:

let dictionary = {}; // синтаксис «литерал объекта»
alert( dictionary );  // [object Object]

При создании объекта с помощью «литерального синтаксиса» в его скрытое свойство [[Prototype]] записывается ссылка на объект-прототип всех объектов в языке JavaScript, на который ссылается свойство Object.prototype встроенной функции-конструктора Object. В этом прототипе есть метод toString, который и возвращает строку [object Object] в приведенном примере, которую функция alert выводит на экран.

Вернемся к задаче. От нас требуется написать метод toString, который будет использоваться для преобразования объекта-словаря, ссылка на который хранится в переменной dictionary, в строку, которую функция alert выведет на экран.

Мой первый вариант решения задачи был такой:
dictionary.toString = function() {
    return Object.keys(dictionary);
};

Тут два момента. Во-первых, после добавления метода toString в объект-словарь, на который указывает переменная dictionary, цикл for..in начал выдавать на экран не только имена свойств (ключи) объекта-словаря, но и имя метода toString, потому что методы тоже считаются своеобразными свойствами объекта. Но по условиям задачи цикл for..in должен игнорировать метод toString.

Во-вторых, вывода объекта на экран не произошло, а в консоли разработчика была выдана та же ошибка: «Uncaught TypeError: Cannot convert object to primitive value». Однако, на самом деле, здесь речь идет уже о другом объекте. Выражение Object.keys(dictionary) возвращает массив ключей объекта-словаря, а массив в языке JavaScript тоже является объектом. Я надеялся, что в данном случае сработает метод toString массива, но этого не произошло. Получается, что наш метод dictionary.toString обязательно должен возвратить строку, а не объект.

Учитывая всё вышеизложенное, я написал второй вариант решения задачи:
dictionary.toString = function() {
    return Object.keys(dictionary).join();
};

Object.defineProperty(dictionary, "toString", {
    enumerable: false
});

Выражение Object.keys(dictionary) возвращает массив с ключами объекта-словаря, после чего к этому массиву применяется метод join, который преобразует массив ключей в строку со значениями элементов массива (ключами), разделенными запятой (по умолчанию этот метод в качестве разделителя использует запятую, если разделитель не задан).

С помощью метода Object.defineProperty свойство-метод toString сделано неперечислимым и теперь будет проигнорировано циклом for..in. Об этом рассказывалось в подразделе 7.1 «Флаги и дескрипторы свойств» учебника.

В принципе, задача решена и всё работает так, как требуется.

Однако, в выражении Object.keys(dictionary) из тела нашего метода переменную dictionary по-хорошему следует заменить на переменную this. Для чего это нужно?

Дело в том, что переменная dictionary содержит лишь указатель на наш объект-словарь, а не является объектом-словарем. Если в промежутке между объявлением нашего метода toString и его применением указатель на объект-словарь будет переписан в другую переменную, а в переменную dictionary будет записано что-то другое, это приведет к ошибке при применении нашего метода toString. Например:

let dictionary = Object.create(null);

dictionary.toString = function() {
    return Object.keys(dictionary).join(); // первопричина ошибки
};

Object.defineProperty(dictionary, "toString", {
    enumerable: false
});

dictionary.apple = "Apple";
dictionary.__proto__ = "test";

let dictionary2 = dictionary; // указатель на объект-словарь сохранен в другой переменной
dictionary = {};              // в переменную dictionary сохранен указатель на пустой объект

// обе эти попытки вывода ключей объекта-словаря не приведут к успеху
alert(dictionary);            // [object Object]
alert(dictionary2);           // пустая строка

Переменная this же всегда будет равна той переменной, от которой метод toString запускается в данный момент. Таким образом, окончательное решение задачи у меня выглядит так:

dictionary.toString = function() {
    return Object.keys(this).join();
};

Object.defineProperty(dictionary, "toString", {
    enumerable: false
});

JavaScipt: пример создания полифила

Продолжаю читать учебник по JavaScript:
https://learn.javascript.ru

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

Итак, вот этот пример:
if (!String.prototype.repeat) {             // если такого метода нет,
                                            // добавляем его в прототип
    String.prototype.repeat = function(n) {
        return new Array(n + 1).join(this); // повторить строку n раз
    };
}

// проверка работы метода
alert( "La".repeat(3) ); // LaLaLa

Здесь String — это встроенная функция-конструктор для строк. Про функции-конструкторы рассказывалось в подразделе 4.5 «Конструкторы, создание объектов через "new"» учебника. Свойство prototype функции-конструктора String указывает на объект-прототип, который станет прототипом нового объекта, создаваемого функцией-конструктором String. Это один из методов реализации наследования в языке JavaScript. О наследовании рассказывается в разделе 8 «Прототипы, наследование» учебника.

Встроенные методы, которые можно использовать для работы со строками в языке JavaScript, содержатся в объекте-прототипе всех строк, на который указывает свойство String.prototype.

Метод repeat для строк, конечно, описан в спецификации языка JavaScript:

https://developer.mozilla.org/ru/docs/Web/JavaScript/Reference/Global_Objects/String/repeat

Но мы предполагаем, что в каких-то старых версиях браузеров этот метод может быть не реализован, а в текстах наших программ этот метод, предположим, активно используется. Конечно, можно найти в программах все использования метода repeat и как-то заменить его. Но мы идем другим путем и сами вручную пишем этот метод и добавляем его в объект-прототип всех строк, если его там нет. Это и происходит в вышеприведенном коде примера.

На этом часть статьи по поводу реализации полифила заканчиваю, а далее хотел бы разобрать, что это за функция repeat и каким образом она здесь реализована.

Вообще, функция repeat должна возвратить повторение заданной строки n раз (то есть возвращается тоже одна строка). Например, задана строка «La» и ее нужно повторить 3 раза, тогда возвращена должна быть строка «LaLaLa». Выделим реализацию метода repeat из вышеприведенного кода:
String.prototype.repeat = function(n) {
    return new Array(n + 1).join(this); // повторить строку n раз
};

Я сразу не смог понять, как работает этот код, поэтому стал разбирать по частям.

Я бы реализовал требуемое циклом, но в вышеприведенном коде нет цикла или рекурсии. Функция возвращает (с помощью служебного слова return) результат выражения new Array(n + 1).join(this).

Здесь Array — встроенная функция-конструктор для создания массивов. С помощью служебного слова new, примененного с функцией-конструктором Array, создается новый массив. В этом массиве не создается элементов, но длина массива становится равной n + 1. То есть обращение к значениям элементов такого массива возвратит специальное значение undefined (что по-русски означает «значение не определено» или «значение отсутствует»). Об этом рассказывалось в подразделе 5.4 «Массивы» учебника:

https://learn.javascript.ru/array#new-array
https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/Array

Далее к созданному массиву применяется метод join, который соединяет элементы массива в одну строку и возвращает эту строку. Про этот метод было рассказано в подразделе 5.5 «Методы массивов»:

https://learn.javascript.ru/array-methods#split-i-join

В качестве «соединителя» по умолчанию используется символ запятой, но в нашем случае «соединитель» задан и им является встроенная переменная this, которая содержит указатель на объект, указанный программистом в коде перед точкой, после которой прописан вызов метода repeat. Например, проверку работы метода repeat мы выполняем так:

alert( "La".repeat(3) ); // LaLaLa

Внутри метода repeat встроенная переменная this становится равной строке «La».

Учитывая всё вышеизложенное, в итоге, как я думал, метод repeat должен был возвратить такую строку:

undefinedLaundefinedLaundefinedLaundefined

То есть, как я думал, должны были быть возвращены четыре (n + 1) значения элементов нового массива, каждый из которых равен специальному значению undefined, соединенные строкой «La». Но возвращается строка «LaLaLa». Почему всё работает правильно?

А дело в том, что метод join преобразует элементы массива со значением undefined или null в пустую строку. Об этом не было рассказано в учебнике, но об этом сказано в спецификации языка JavaScript:

https://developer.mozilla.org/ru/docs/Web/JavaScript/Reference/Global_Objects/Array/join

Учебник по JavaScript: ч.1: свойства объекта, их конфигурация

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

https://learn.javascript.ru

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

Разделы:

7. Свойства объекта, их конфигурация (2 подраздела)

7.1 Флаги и дескрипторы свойств
7.2 Свойства - геттеры и сеттеры

У каждого из свойств объекта кроме ключа (идентификатора свойства, по-английски «key») и значения (по-английски «value») еще есть три флага, которые могут принимать значения true или false. Эти флаги называются writable (по-русски «доступное для записи»), enumerable (по-русски «видимое при перечислениях», например, в цикле for..in) и configurable (по-русски «доступное для переконфигурирования»).

Когда мы создаем свойство объекта «обычным способом», эти три флага по умолчанию устанавливаются в значение true, то есть свойство объекта по умолчанию является доступным для записи, видимым при перечислениях и доступным для переконфигурирования (под «переконфигурированием» подразумевается изменение значений флагов).

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

Object.defineProperty
Object.getOwnPropertyDescriptor

Эти методы оперируют так называемыми дескрипторами (по-английски «descriptor») свойств объектов (эту кальку с английского на русский язык можно перевести словом «описатель», то есть дескриптор описывает конфигурацию свойства объекта). Пример дескриптора:

{
    value: "Иван",
    writable: false,
    enumerable: false,
    configurable: false
}

Пример создания нового свойства name объекта user с использованием дескриптора:
let user = { };

Object.defineProperty(user, "name", {
    value: "Иван",
    writable: false,
    enumerable: true,
    configurable: true
});
Если при определении нового свойства объекта таким образом какой-то флаг в дескрипторе не указать, значением этого флага по умолчанию будет являться false.

В подразделе 7.2 учебника рассказано, что свойства объекта делятся на свойства-данные (по-английски «data properties»), которые обсуждались в учебнике ранее, и свойства-аксессоры (по-английски «accessor properties», что по-русски может быть переведено как «свойства доступа» или «свойства средств доступа»), о которых рассказывается в этом подразделе.

Кальку «аксессор» на русский язык можно перевести как «средство доступа». Свойства-аксессоры можно использовать вне объекта так же, как и свойства-данные. Однако, на самом деле свойств-аксессоров внутри объекта не существует, вместо них должны быть прописаны специальные методы с ключевыми словами get и set, отдельные для каждого свойства-аксессора. Эти методы называют геттером и сеттером, что по-русски означает «получатель» и «установщик» (имеется в виду получение (чтение) значения свойства-аксессора и установка (присвоение, запись, перезапись) значения свойства-аксессора). На самом деле геттеры и сеттеры обычно получают значения из обычных свойств-данных и записывают значения в те же обычные свойства-данные.

Вот как это работает:
let user = {
    name: "John",                                     // свойство-данные
    surname: "Smith",                                 // свойство-данные

    get fullName() {                                  // геттер свойства-аксессора fullName
        return `${this.name} ${this.surname}`;
    },

    set fullName(value) {                             // сеттер свойства-аксессора fullName
        [this.name, this.surname] = value.split(" ");
    }
};
Снаружи объекта доступ к свойству-аксессору user.fullName синтаксически ничем не отличается от доступа к свойствам-данным user.name и user.surname.

Учебник по JavaScript: ч.1: привязка контекста, повтор стрелочных функций

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

https://learn.javascript.ru

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

Разделы:

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

6.10 Привязка контекста к функции
6.11 Повторяем стрелочные функции

Рассказано про проблему потери контекста this в некоторых случаях (например, при передаче методов объекта в функции, в которых в качестве параметра требуется функциональный объект).

Эта проблема проистекает из того, что в языке JavaScript переменная this может менять свое значение в зависимости от окружения. Об этом уже ранее рассказывалось в подразделе 4.4 «Методы объекта, "this"» учебника. Но здесь это явление рассматривается подробнее.

Для решения проблемы потери контекста предлагается применять встроенный метод bind, который фиксирует значение this в указанное. После этого this метода остается фиксированным и изменить его нельзя.

Также с помощью встроенного метода bind можно уменьшать количество параметров функции. В википедии про это есть статья:

https://ru.wikipedia.org/wiki/Частичное_применение

В подразделе 6.11 более подробно разбираются свойства стрелочных функций. Ранее про стрелочные функции было рассказано в подразделе 2.17 «Функции-стрелки, основы» учебника, а также они активно применялись в задачах предыдущих подразделов.

Стрелочные функции не имеют собстенного контекста this и к их аргументам нельзя обратиться через внутренний псевдомассив arguments (про этот псевдомассив было рассказано в подразделе 6.2 «Остаточные параметры и оператор расширения» учебника). Также стрелочные функции не могут быть вызваны с new (не могут быть использованы как конструкторы объектов, подробнее об этом было в подразделе 4.5 «Конструкторы, создание объектов через "new"» учебника). Эти ограничения (хотя, можно считать их не ограничениями, а, наоборот, полезными свойствами, с какой стороны посмотреть) введены потому, что стрелочные функции предназначены для небольшого по объему кода, который не имеет своего контекста, выполняясь в текущем контексте (контексте своего «родителя»).

JavaScript, Sinon.JS, поддельные таймеры

Разобрался с вопросом в комментариях к подразделу 6.9 «Декораторы и переадресация вызова, call/apply» учебника по JavaScript:

https://learn.javascript.ru/call-apply-decorators#comment-5334632652

Автор комментария (с забавным ником Defeated Sanity) предложил следующее решение задачи «Декоратор debounce», о которой я писал в прошлом посте:

function debounce(f, ms) {
    let t = ms;
    function call () {
        if (Date.now() - call.time > t) {
            call.time = Date.now();
            f.apply(this, arguments);
        }
    }
    call.time = 0;
    return call;
}

Отмечу, что переменная t здесь лишняя, а присвоение call.time = Date.now(); правильнее поставить после вызова f.apply(this, arguments); (я разбирал этот момент в прошлом посте), но речь сейчас не об этом. Моё решение этой задачи из прошлого поста принципиально похоже на это решение, однако моё решение проходит песочницу с тестами авторов учебника, а вышеприведенное решение — не проходит первый тест из двух.

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

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

Я полез в код скрипта с тестами песочницы и увидел там следующий фрагмент в начале скрипта:

before(function() {
    this.clock = sinon.useFakeTimers();
});

after(function() {
    this.clock.restore();
});

То есть перед каждым тестом происходит подмена глобальных таймеров, а после каждого теста — обратная подмена (восстановление). Эта функциональность предоставляется mock-библиотекой Sinon.JS:

https://sinonjs.org/releases/latest/fake-timers/
https://ru.wikipedia.org/wiki/Mock-объект

В результате этих манипуляций при первом вызове функции в тестовом коде метод Date.now() возвращает не текущую метку времени (количество миллисекунд с 1 января 1970 года), как мы ожидаем, исходя из спецификации JavaScript, а число 0 (то есть таймер начинает отсчет миллисекунд от начала теста, а не с 1 января 1970 года). В результате условие Date.now() - call.time > t, прописанное в коде функции-декоратора, не выполняется для первого вызова функции из тестового кода и этот вызов не происходит. После чего тест оказывается не пройден.

В моем варианте решения первый вызов функции через декоратор обрабатывается отдельно, поэтому мой вариант решения работает. Я внес следующие исправления в код обсуждаемой функции-декоратора:

function debounce(f, ms) {
    let t = ms;
    function call () {
        if (Date.now() - call.time > t) {
            call.time = Date.now();
            f.apply(this, arguments);
        } else if (call.time === undefined) { // первый вызов обрабатываем отдельно
            call.time = Date.now();
            f.apply(this, arguments);
        }
    }
    // call.time = 0;
    return call;
}

Этот код проходит тесты из песочницы авторов учебника.