Страницы

Поиск по вопросам

Показаны сообщения с ярлыком ecmascript-2015. Показать все сообщения
Показаны сообщения с ярлыком ecmascript-2015. Показать все сообщения

четверг, 23 января 2020 г.

Наследование от функции

#javascript #наследование #ecmascript_2015


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

class Smth extends Function {
  // Что здесь сделать
}

(new Smth)() // чтобы тут вернулось не undefined?


PS: Этот вопрос на английском.
    


Ответы

Ответ 1



Взяв в качестве базового вот этот ответ, я немного его доработал и вот что получилось. Дорабатывал я две вещи. Во-первых, передавать функцию в конструктор базового класса - не самое красивое решение; потому я переадресую реальный вызов методу call. Обычно этот метод вызывает основную функцию - но у меня получилось наоборот (сигнатура удачно совпала). Во-вторых, надо обеспечить адекватное значение для this (желательно чтобы это был сам объект-функция, потому что IDE будет думать к контекстных подсказках именно так). При этом надо не потерять реальный this (я его передаю первым аргументом). Ну и плюс надо убрать не имеющую смысла информацию из вывода toString() - никому не интересно видеть там замыкание вида "взять аргументы и передать дальше". Получилось вот так: class ExtendableFunction extends Function { constructor() { super() function f(...args) { return f.call(this, ...args); } Object.setPrototypeOf(f, this.constructor.prototype); return f } call(context, ...args) { throw "Not implemented" } toString() { return this.constructor.name + "(...)"; } }; class HelloWorld extends ExtendableFunction { call() { console.log("Hello, world!", this, arguments) } }; Но мое чувство прекрасного все еще не радо. Вспомните, как часто вы делали обертки над функциями, которые вызывают вложенную функцию через apply. Или как часто вы bindили функцию к объекту... Теперь во всех этих цепочках будет на 1 обертку больше - ведь эти методы будут применяться сначала к f - а потом уже к call. Наконец, если мы уже пишем функтор-обертку, то, вероятно, нам будет проще определять для него не метод call - а метод apply. Поэтому в финальной версии я "спрямил" метод bind, а методы call и apply сделал по умолчанию вызывающими друг друга - чтобы наследник мог переопределить любого. class ExtendableFunction extends Function { constructor() { super() function f(...args) { return f.call(this, ...args); } Object.setPrototypeOf(f, this.constructor.prototype); return f } call(context, ...args) { return this.apply(context, args) } apply(context, args) { return this.call(context, ...args) } bind(context, ...args) { return this.call.bind(this, context, ...args) } toString() { return this.constructor.name + "(...)"; } }; class HelloWorld extends ExtendableFunction { call(context, ..args) { console.log("Hello, world!", this, context, args) } }; class HelloWorld2 extends ExtendableFunction { apply(context, args) { console.log("Hello, world!", this, context, args) } };

понедельник, 30 декабря 2019 г.

Регистрация обработчиков событий в JavaScript при переборе элементов документа

#javascript #события #ecmascript_2015 #замыкания


Этот вопрос не дубликат, так как он затрагивает не только замыкания, но и иные распространённые
ошибки в JS, а также их устранения используя новые возможности спецификации ES 2015.

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

По задумке.
1 При наведении на fieldset сообщения подсвечивается иконка удаления каждого сообщения,
при отведении мышки иконка удаления, соответствующего сообщения, снова угасает.
2 При наведении на иконку удаления всплывает подсказка со временем соответствующего
сообщения.
3 При клике на иконку соответствующего сообщения удаления всплывает алерт со временем
соответствующего сообщения.

Имею.
1 При наведении на любой fieldset сообщения подсвечивается иконка удаления последнего
сообщения, при отведении мышки иконка удаления, последнего сообщения, снова угасает.
2 Это единственное, что работает правильно.
3 При клике на иконку удаления любого сообщения всплывает алерт со временем последнего
сообщения.

Что я делаю не так, что я тут не понимаю и как мне сделать правильно?



window.addEventListener('load',function()
{
 var imgs=document.getElementsByClassName('right')[0].getElementsByClassName('del');
 for(var i=0,l=imgs.length;i0&&!isNaN(img.dataset.fid)&&img.dataset.fid>0&&img.dataset.title)
   alert('Are you sure you want to delete '+img.dataset.title+' message?');
  });
 }
});
*
{
  margin:0;
  padding:0;
  border:0;
  border-radius:5px;
  transition:all 0.2s linear;
}
html,body
{
  height:100%;
  min-height:100%;
}
body
{
  background-color:#fff;
  color:#000;
  font:12px/18px Arial,Helvetica,sans-serif;
  margin:0 auto;
}
fieldset
{
  border:1px solid #ccc;
}
legend
{
  font-weight:bold;
}
div.message
{
  min-width:400px;
  width:400px;
  max-height:400px;
  margin:0 auto;
  padding:10px;
  text-align:center;
}
div.message div.left, div.message div.right
{
  float:left;
  height:100%;
  overflow:auto;
}
div.message div.right
{
  width:250px;
  text-align:center;
}
div.message div.right hr
{
  border-top:1px solid #ccc;
  margin-top:10px;
}
div.message div.right hr+span
{
  background-color:#fff;
  font-weight:bold;
  padding:0 10px;
  position:relative;
  bottom:10px;
}
div.message div.right fieldset
{
  margin:0 0 10px;
  padding:10px;
  text-align:left;
  position:relative;
}
div.message div.right fieldset.from
{
  margin-right:50px;
  border-color:#6f6;
  background-color:#dfd;
  color:#040;
}
div.message div.right fieldset.to
{
  margin-left:50px;
  border-color:#66f;
  background-color:#ddf;
  color:#004;
}
div.message div.right fieldset img.del
{
  width:16px;
  top:0;
  right:8px;
}
div.message img.del
{
  position:absolute;
  opacity:0.3;
}
div.message img.del:hover
{
  cursor:pointer;
  opacity:1;
}


	
		
		Test
	
		
		

08.10.2016
Ilya Indigo 03:59 del

Тестовое сообщение
новая строка
ещё одна строка
и ещё одна строка.

Ilya Indigo 04:03 del

Ещё одно.

Ilya Indigo 2 04:17 del

Отвечаю.

Ilya Indigo 05:08 del

Ещё одно.

Ответ: Самый простой и быстрый способ объявить все переменные через let вместо var.


Ответы

Ответ 1



На первый взгляд может показаться, что здесь классическая проблема с замыканием, и это она действительно и есть. Но! Решение здесь намного проще: так как в обработчике подразумевается работа с элементом по которому нажали, проблема замыкания решается использованием this, вместо переменной img. Так как, в данном случае this - будет, как раз тем элементом, по которому кликнули. window.addEventListener('load', function() { var imgs = document.getElementsByClassName('right')[0].getElementsByClassName('del'); for (var i = 0, l = imgs.length; i < l; i++) { var img = imgs[i]; var doc = document.getElementById('mes-' + img.dataset.date + '-' + img.dataset.fid); doc.addEventListener('mouseover', function() { // здесь проблема остается, так как обработчик вешается на fieldset img.style.opacity = 1; }); doc.addEventListener('mouseout', function() { // здесь проблема остается, так как обработчик вешается на fieldset img.removeAttribute('style'); }); img.setAttribute('title', 'Delete ' + img.dataset.title + ' message'); img.addEventListener('click', function() { if (!isNaN(this.dataset.date) && this.dataset.date > 0 && !isNaN(this.dataset.fid) && this.dataset.fid > 0 && this.dataset.title) alert('Are you sure you want to delete ' + this.dataset.title + ' message?'); }); } }); legend { font-weight: bold; } div.message { width: 400px; } div.message div.right fieldset { text-align: left; position: relative; border-color: #6f6; background-color: #dfd; color: #040; } div.message div.right fieldset.to { background-color: #ddf; } div.message div.right fieldset img.del { width: 16px; top: 0; right: 8px; position: absolute; opacity: 0.3; }

08.10.2016
Ilya Indigo 03:59 del

Тестовое сообщение
новая строка
ещё одна строка
и ещё одна строка.

Ilya Indigo 04:03 del

Ещё одно.

Ilya Indigo 2 04:17 del

Отвечаю.

Ilya Indigo 05:08 del

Ещё одно.

Причины ошибки: Переменные объявленные с помощью ключевого слова var имеют функциональную область видимости. Это значит, что ни доступны внутри всей функции в которой объявлены и внутри объявленных вложенных функций. Несмотря на то, что объявление переменной находится внутри for будет объявлена всего одна переменная, значение которой будет меняться. Так как обработчики выполняются не сразу внутри цикла, а когда наступает определенное событие, они берут значение указанной переменной в момент выполнения, а это, в данном случае, значение присвоенное на последней итерации цикла. Возможные решения: Создание функции, создающей конкретный обработчик: в данном случае проблема решается за счет создание функции-фабрики, которая будет создавать функцию обработчика на основе параметра. Частным случаем этого подхода является использование IIFE, когда функция-фабрика вызывается сразу при объявлении Пример с именованной фабрикой: function createClickHandler(img) { return function() { if (!isNaN(img.dataset.date) && img.dataset.date > 0 && !isNaN(img.dataset.fid) && img.dataset.fid > 0 && img.dataset.title) alert('Are you sure you want to delete ' + img.dataset.title + ' message?'); }; } function createMouseoverHandler(img) { return function() { img.style.opacity = 1; }; } function createMouseoutHandler(img) { return function() { img.removeAttribute('style'); }; } window.addEventListener('load', function() { var imgs = document.getElementsByClassName('right')[0].getElementsByClassName('del'); for (var i = 0, l = imgs.length; i < l; i++) { var img = imgs[i]; var doc = document.getElementById('mes-' + img.dataset.date + '-' + img.dataset.fid); doc.addEventListener('mouseover', createMouseoverHandler(img)); doc.addEventListener('mouseout', createMouseoutHandler(img)); img.setAttribute('title', 'Delete ' + img.dataset.title + ' message'); img.addEventListener('click', createClickHandler(img)); } }); legend { font-weight: bold; } div.message { width: 400px; } div.message div.right fieldset { text-align: left; position: relative; border-color: #6f6; background-color: #dfd; color: #040; } div.message div.right fieldset.to { background-color: #ddf; } div.message div.right fieldset img.del { width: 16px; top: 0; right: 8px; position: absolute; opacity: 0.3; }

08.10.2016
Ilya Indigo 03:59 del

Тестовое сообщение
новая строка
ещё одна строка
и ещё одна строка.

Ilya Indigo 04:03 del

Ещё одно.

Ilya Indigo 2 04:17 del

Отвечаю.

Ilya Indigo 05:08 del

Ещё одно.

Пример с IIFE: window.addEventListener('load', function() { var imgs = document.getElementsByClassName('right')[0].getElementsByClassName('del'); for (var i = 0, l = imgs.length; i < l; i++) { var img = imgs[i]; var doc = document.getElementById('mes-' + img.dataset.date + '-' + img.dataset.fid); doc.addEventListener('mouseover', function createMouseoverHandler(imgParam) { return function() { imgParam.style.opacity = 1; }; }(img)); doc.addEventListener('mouseout', function createMouseoutHandler(imgParam) { return function() { imgParam.removeAttribute('style'); }; }(img)); img.setAttribute('title', 'Delete ' + img.dataset.title + ' message'); img.addEventListener('click', function createClickHandler(imgParam) { return function() { if (!isNaN(imgParam.dataset.date) && imgParam.dataset.date > 0 && !isNaN(imgParam.dataset.fid) && imgParam.dataset.fid > 0 && imgParam.dataset.title) alert('Are you sure you want to delete ' + imgParam.dataset.title + ' message?'); }; }(img)); } }); legend { font-weight: bold; } div.message { width: 400px; } div.message div.right fieldset { text-align: left; position: relative; border-color: #6f6; background-color: #dfd; color: #040; } div.message div.right fieldset.to { background-color: #ddf; } div.message div.right fieldset img.del { width: 16px; top: 0; right: 8px; position: absolute; opacity: 0.3; }

08.10.2016
Ilya Indigo 03:59 del

Тестовое сообщение
новая строка
ещё одна строка
и ещё одна строка.

Ilya Indigo 04:03 del

Ещё одно.

Ilya Indigo 2 04:17 del

Отвечаю.

Ilya Indigo 05:08 del

Ещё одно.

Использование let/const. Использование данных ключевых слов, которые появились в ES2015, позволяет объявлять переменные область видимости которых текущий блок. Это означает, что на каждой итерации цикла будет создаваться новая переменная, поэтому во время выполнения обработчика будет использоваться корректное значение window.addEventListener('load', function() { var imgs = document.getElementsByClassName('right')[0].getElementsByClassName('del'); for (var i = 0, l = imgs.length; i < l; i++) { let img = imgs[i]; var doc = document.getElementById('mes-' + img.dataset.date + '-' + img.dataset.fid); doc.addEventListener('mouseover', function() { img.style.opacity = 1; }); doc.addEventListener('mouseout', function() { img.removeAttribute('style'); }); img.setAttribute('title', 'Delete ' + img.dataset.title + ' message'); img.addEventListener('click', function() { if (!isNaN(img.dataset.date) && img.dataset.date > 0 && !isNaN(img.dataset.fid) && img.dataset.fid > 0 && img.dataset.title) alert('Are you sure you want to delete ' + img.dataset.title + ' message?'); }); } }); legend { font-weight: bold; } div.message { width: 400px; } div.message div.right fieldset { text-align: left; position: relative; border-color: #6f6; background-color: #dfd; color: #040; } div.message div.right fieldset.to { background-color: #ddf; } div.message div.right fieldset img.del { width: 16px; top: 0; right: 8px; position: absolute; opacity: 0.3; }

08.10.2016
Ilya Indigo 03:59 del

Тестовое сообщение
новая строка
ещё одна строка
и ещё одна строка.

Ilya Indigo 04:03 del

Ещё одно.

Ilya Indigo 2 04:17 del

Отвечаю.

Ilya Indigo 05:08 del

Ещё одно.

Использование вместо for, функции forEach. Так как в данном случае в замыкании будет использован параметр функции, а не переменная, значение в момент выполнения обработчика события будет корректным. Из-за того, что функция getElementsByClassName возвращает не массив, а коллекцию, использовать у нее функцию forEach нельзя. Но можно преобразовать эту коллекцию в массив, либо использовать call. Пример: window.addEventListener('load', function() { var imgs = document.getElementsByClassName('right')[0].getElementsByClassName('del'); [...imgs].forEach(function(img) { var doc = document.getElementById('mes-' + img.dataset.date + '-' + img.dataset.fid); doc.addEventListener('mouseover', function() { img.style.opacity = 1; }); doc.addEventListener('mouseout', function() { img.removeAttribute('style'); }); img.setAttribute('title', 'Delete ' + img.dataset.title + ' message'); img.addEventListener('click', function() { if (!isNaN(img.dataset.date) && img.dataset.date > 0 && !isNaN(img.dataset.fid) && img.dataset.fid > 0 && img.dataset.title) alert('Are you sure you want to delete ' + img.dataset.title + ' message?'); }); }); }); legend { font-weight: bold; } div.message { width: 400px; } div.message div.right fieldset { text-align: left; position: relative; border-color: #6f6; background-color: #dfd; color: #040; } div.message div.right fieldset.to { background-color: #ddf; } div.message div.right fieldset img.del { width: 16px; top: 0; right: 8px; position: absolute; opacity: 0.3; }

08.10.2016
Ilya Indigo 03:59 del

Тестовое сообщение
новая строка
ещё одна строка
и ещё одна строка.

Ilya Indigo 04:03 del

Ещё одно.

Ilya Indigo 2 04:17 del

Отвечаю.

Ilya Indigo 05:08 del

Ещё одно.

Использовать в обработчиках this, и искать элементы относительно элемента this. Так как, при добавлении обработчика с помощью addEventListener, внутри обработчика this будет указывать на элемент, к которому добавляли обработчик, то при клике на картинку, вместо замыкания можно использовать this. В случае, когда обработчик добавляется на fieldset можно найти нужную картинку внутри this, например с помощью querySelector Пример window.addEventListener('load', function() { var imgs = document.getElementsByClassName('right')[0].getElementsByClassName('del'); for (var i = 0; i < imgs.length; i++) { var img = imgs[i]; var doc = document.getElementById('mes-' + img.dataset.date + '-' + img.dataset.fid); doc.addEventListener('mouseover', function() { this.querySelector('img').style.opacity = 1; }); doc.addEventListener('mouseout', function() { this.querySelector('img').removeAttribute('style'); }); img.setAttribute('title', 'Delete ' + img.dataset.title + ' message'); img.addEventListener('click', function() { if (!isNaN(this.dataset.date) && this.dataset.date > 0 && !isNaN(this.dataset.fid) && this.dataset.fid > 0 && this.dataset.title) alert('Are you sure you want to delete ' + this.dataset.title + ' message?'); }); } }); legend { font-weight: bold; } div.message { width: 400px; } div.message div.right fieldset { text-align: left; position: relative; border-color: #6f6; background-color: #dfd; color: #040; } div.message div.right fieldset.to { background-color: #ddf; } div.message div.right fieldset img.del { width: 16px; top: 0; right: 8px; position: absolute; opacity: 0.3; }
Ilya Indigo 03:59 del

Тестовое сообщение
новая строка
ещё одна строка
и ещё одна строка.

Ilya Indigo 04:03 del

Ещё одно.

Ilya Indigo 2 04:17 del

Отвечаю.

Ilya Indigo 05:08 del

Ещё одно.

Делегация событий вместо цикла. В данном подходе обработчик вешается не на конкретную картинку, а на контейнер, где лежат все картинки. Внутри обработчика уже определяется по какому элементу щелкнули и выполняются нужные функции. Пример window.addEventListener('load', function() { var right = document.getElementsByClassName('right')[0]; right.addEventListener('click', function(e) { if (e.target.className === 'del') { if (!isNaN(e.target.dataset.date) && e.target.dataset.date > 0 && !isNaN(e.target.dataset.fid) && e.target.dataset.fid > 0 && e.target.dataset.title) alert('Are you sure you want to delete ' + e.target.dataset.title + ' message?'); } }); [...right.querySelectorAll('img')].forEach(img => img.setAttribute('title', 'Delete ' + img.dataset.title + ' message')); var fieldSets = right.querySelectorAll('fieldset'); for (var i = 0; i < fieldSets.length; i++) { fieldSets[i].addEventListener('mouseover', function() { this.querySelector('img').style.opacity = 1; }); fieldSets[i].addEventListener('mouseout', function() { this.querySelector('img').removeAttribute('style'); }); } }); legend { font-weight: bold; } div.message { width: 400px; } div.message div.right fieldset { text-align: left; position: relative; border-color: #6f6; background-color: #dfd; color: #040; } div.message div.right fieldset.to { background-color: #ddf; } div.message div.right fieldset img.del { width: 16px; top: 0; right: 8px; position: absolute; opacity: 0.3; }
Ilya Indigo 03:59 del

Тестовое сообщение
новая строка
ещё одна строка
и ещё одна строка.

Ilya Indigo 04:03 del

Ещё одно.

Ilya Indigo 2 04:17 del

Отвечаю.

Ilya Indigo 05:08 del

Ещё одно.



Ответ 2



Ответ находятся тут. https://javascript.ru/basic/closure#primer-oshibochnogo-ispolzovaniya Причём вступление автора "С вопроса "Почему это не работает?" люди обычно начинают изучение замыканий." Просто, как никогда, попадает в точку и бьёт напролом! Рабочий пример будет выгладить так. window.addEventListener('load',function() { var imgs=document.getElementsByClassName('right')[0].getElementsByClassName('del'); for(var i=0,l=imgs.length;i0&&!isNaN(x.dataset.fid)&&x.dataset.fid>0&&x.dataset.title) alert('Are you sure you want to delete '+x.dataset.title+' message?'); } }(img)); } }); Но всё же, стандарт ECMA Script 2015 позволяет воспользоваться более простым и изящным методом, объявляя переменные не через var, а через let. window.addEventListener('load',function() { let imgs=document.getElementsByClassName('right')[0].getElementsByClassName('del'); for(let i=0,l=imgs.length;i0&&!isNaN(img.dataset.fid)&&img.dataset.fid>0&&img.dataset.title) alert('Are you sure you want to delete '+img.dataset.title+' message?'); }); } });

Ответ 3



В качестве примера могу привести следующий код: //функция создает объекты в циле, из интерисующих нас параметров box- контейнер в котором находятся DOM эелементы и plate - сами элементы function createPlates(box, plate, step, speed, bool) { var box = document.querySelector(box); if(!box) { return false; } else { var plates = Array.prototype.slice.apply(box.querySelectorAll(plate)); //в цикле создаем объекты с помощью конструктора (будет описан ниже) for(var i = 0; i < plates.length; i++) { plates[i] = new ServisePlate (plates[i], step, speed, bool); } // на контейнер вешаем обработчик на клик box.addEventListener('click', function(event) { // И перебираем массив plates с созданными объектами plates.forEach(function(element, i) { // если объект события совпадает со ссылкой на DOM элемент, сохраненной в объекте, запускаем его метод. if(event.target == plates[i].elem) { plates[i].disclose(); // Здесь проверка на клик по дочернему элементу необходимого нам элемента }else if(plates[i].elem.contains(event.target)){ for(var j = 0; j < plates.length; j++) { if(plates[j].elem.style.height) { plates[j].disclose(); } } plates[i].disclose(); }else { return false; } }); }); } } createPlates('.servises', '.servises_content', 5, 20, true); Собственно сам конструктор: function ServisePlate(plate, step, speed, headSet) { this.elem = plate; this.header = plate.querySelector('h3'); this.body = plate.querySelector('div'); this.link = plate.querySelector('a'); if(this.link) { this.bodySize = this.body.offsetHeight + this.link.offsetHeight; }else{ this.bodySize = this.body.offsetHeight; } this.settings = { step: step, speed: speed } this.initialSize = this.elem.offsetHeight; var self = this; function setHead() { if(29 < self.header.innerHTML.length) { self.header.classList.add('servises_content_header--p18'); } if(59 < self.header.innerHTML.length) { self.header.classList.remove('servises_content_header--p18'); self.header.classList.add('servises_content_header--p1'); } } if(headSet === true) { setHead(); } } И его метод disclose, сохраненный в прототипе: ServisePlate.prototype.disclose = function() { function showBody() { if(this.elem.offsetHeight < (this.bodySize + this.initialSize)) { this.elem.style.height = this.elem.offsetHeight + this.settings.step + 'px'; setTimeout(showBody.bind(this), this.settings.speed); } } function hideBody() { if(this.elem.offsetHeight > this.initialSize) { this.elem.style.height = this.elem.offsetHeight - this.settings.step + 'px'; setTimeout(hideBody.bind(this), this.settings.speed); }else { this.elem.style.height = ''; } } if(this.elem.offsetHeight == this.initialSize || this.elem.offsetHeight >= (this.bodySize + this.initialSize)) { if(this.elem.offsetHeight == this.initialSize) { showBody.call(this); } else { hideBody.call(this); } } } Правда некоторые участки кода у меня самого вызывают подозрения на правильность, так как я уже говорил что мой js тянет на 3 с минусом. Но суть не в правильности тех или иных участков, а в подходе в целом.

среда, 4 декабря 2019 г.

Что скрывает yield *?

#javascript #ecmascript_2015


В чём разница между этими фрагментами кода?

yield *smth;




for (let x of smth) {
  yield x;
}


@torazaburo в комментарии сказал


  This is an OK simplification, but it fails to address what the yield *generator()
evaluates to, which as it turns out is the end-of-iteration value return'ed by the
generator object. It also does not correctly represent what happens when the consumer
of the generator throws into the generator, etc.


Хотелось бы узнать, в чём именно заключается разница и в каких случаях она проявит себя.
    


Ответы

Ответ 1



На самом деле, синтаксис yield * gen используется для передачи управления внутрь другого генератора. Вот что об этом говорит MDN: The yield* expression iterates over the operand and yields each value returned by it. А вот и пример того, как это работает: let seqPos = function * (max) { for (let i = 1; i <= max; i++) { yield i; } } let seqNeg = function * (min) { let start = min > 0 ? -1*min : min; for (let i = start; i < 0; i++) { yield i; } } let seq = function * (val) { yield * seqNeg(-1*val); yield 0; yield * seqPos(val); } for (let i of seq(2)) { console.log(i); } Пример выше выведет: -2 -1 0 1 2 А вот и JSFiddle с примером. Что касается основного вопроса, то разница между конструкциями: for (let val of gen) { yield val; } и yield * gen; заключается в том, что в первом случае всего лишь возвращаются значения дочернего генератора, а во втором управление полностью передается дочернему генератору. Это значит, что вызов методов Generator.prototype.next, Generator.prototype.throw и Generator.prototype.return клиентским кодом на родительском генераторе будет адресован дочернему генератору: let innerGen = function * () { try { for (let i = 0; i < 3; i++) { console.log(yield i); } } catch (e) { console.log('Got you!'); } } let outerGen = function * () { let child = innerGen(); yield * child; } let g = outerGen(); g.next(); g.next("foo"); // выведет в консоль "foo" g.throw(new Error('Oops!')); // выведет "Got you!" Ситуация, с пробросом исключений в дочерний итератор, описанная участником @Роман Парадеев в соседнем ответе, является всего лишь частным случаем передачи управления дочернему генератору.

Ответ 2



Вот что говорит на этот счёт говорит ECMAScript Wiki: This is similar to a for-in loop over the generator, except that it propagates exceptions thrown via the outer generator’s throw method into the delegated generator. Так что разница, похоже, только в пробросе исключений. Я набросал небольшой пример, который иллюстрирует различие: function * delegate() { try { yield; } catch (error) { console.log('Caught by delegate'); } } function * gen1() { console.log('Generator 1'); try { yield * delegate(); } catch (error) { console.log('Caught by generator'); } } function * gen2() { console.log('Generator 2'); try { for (let x of delegate()) { yield x; } } catch (error) { console.log('Caught by generator'); } } [gen1, gen2].forEach(gen => { const genObj = gen(); genObj.next(); genObj.throw('Uh oh...'); }); Как и ожидалось, вывод будет следующий Generator 1 Caught by delegate Generator 2 Caught by generator

понедельник, 8 октября 2018 г.

Что скрывает yield *?

В чём разница между этими фрагментами кода?
yield *smth;

for (let x of smth) { yield x; }
@torazaburo в комментарии сказал
This is an OK simplification, but it fails to address what the yield *generator() evaluates to, which as it turns out is the end-of-iteration value return'ed by the generator object. It also does not correctly represent what happens when the consumer of the generator throws into the generator, etc.
Хотелось бы узнать, в чём именно заключается разница и в каких случаях она проявит себя.


Ответ

На самом деле, синтаксис yield * gen используется для передачи управления внутрь другого генератора.
Вот что об этом говорит MDN
The yield* expression iterates over the operand and yields each value returned by it.
А вот и пример того, как это работает:
let seqPos = function * (max) { for (let i = 1; i <= max; i++) { yield i; } }
let seqNeg = function * (min) { let start = min > 0 ? -1*min : min; for (let i = start; i < 0; i++) { yield i; } }
let seq = function * (val) { yield * seqNeg(-1*val); yield 0; yield * seqPos(val); }
for (let i of seq(2)) { console.log(i); }
Пример выше выведет:
-2 -1 0 1 2
А вот и JSFiddle с примером.

Что касается основного вопроса, то разница между конструкциями:
for (let val of gen) { yield val; }
и
yield * gen;
заключается в том, что в первом случае всего лишь возвращаются значения дочернего генератора, а во втором управление полностью передается дочернему генератору.
Это значит, что вызов методов Generator.prototype.next, Generator.prototype.throw и Generator.prototype.return клиентским кодом на родительском генераторе будет адресован дочернему генератору:
let innerGen = function * () { try { for (let i = 0; i < 3; i++) { console.log(yield i); } } catch (e) { console.log('Got you!'); } }
let outerGen = function * () { let child = innerGen(); yield * child; }
let g = outerGen(); g.next(); g.next("foo"); // выведет в консоль "foo" g.throw(new Error('Oops!')); // выведет "Got you!"
Ситуация, с пробросом исключений в дочерний итератор, описанная участником @Роман Парадеев в соседнем ответе, является всего лишь частным случаем передачи управления дочернему генератору.