Подтвердить что ты не робот

Почему не возвращает доход из обратного вызова `.map`?

Узнать Генераторы - 4" ОШИБКА СЕТЬ! Решение использует for loop, но я просто ничего не нашел в MDN - Iteration Protocols, который ссылается на выход в обратных вызовах.

Я собираюсь угадать, что ответ просто don't do that, но спасибо заранее, если у кого есть время или желание дать объяснение!

Код:

function *upper (items) {
  items.map(function (item) {
    try {
      yield item.toUpperCase()
    } catch (e) {
      yield 'null'
    }
  }
}

var badItems = ['a', 'B', 1, 'c']

for (var item of upper(badItems)) {
  console.log(item)
}
// want to log: A, B, null, C

Ошибка:

⇒  learn-generators run catch-error-map.js
/Users/gyaresu/programming/projects/nodeschool/learn-generators/catch-error-map.js:4
      yield item.toUpperCase() // error below
            ^^^^
SyntaxError: Unexpected identifier
    at exports.runInThisContext (vm.js:73:16)
    at Module._compile (module.js:443:25)
    at Object.Module._extensions..js (module.js:478:10)
    at Module.load (module.js:355:32)
    at Function.Module._load (module.js:310:12)
    at Function.Module.runMain (module.js:501:10)
    at startup (node.js:129:16)
    at node.js:814:3

Даже мой редактор знает, что это ужасная идея...

yield within callback

4b9b3361

Ответ 1

Отказ от ответственности: я являюсь автором Learn generators workshopper.

Ответа на этот вопрос @slebetman является правильным, и я также могу добавить еще:

Да, MDN - Протокол итерации не относится непосредственно к yield в обратных вызовах. Но, это говорит нам о важности, откуда вы yield, потому что вы можете использовать только yield внутри генераторов. Подробнее см. MDN - Iterables.

@marocchino предлагать просто отличное решение итерации по массиву который был изменен после карты:

function *upper (items) {
  yield* items.map(function (item) {
    try {
      return item.toUpperCase();
    } catch (e) {
      return null;
    }
  });
}

Мы можем это сделать, потому что у массива есть механизм итерации, см. Array.prototype [@@iterator]().

var bad_items = ['a', 'B', 1, 'c'];

for (let item of bad_items) {
  console.log(item); // a B 1 c
}

Array.prototype.map не имеет поведения по умолчанию по умолчанию, поэтому мы не могли его перебирать.

Но генераторы - это не просто итераторы. Каждый генератор является итератором, но не наоборот. Генераторы позволяют настраивать процесс итерации (и не только), вызывая ключевое слово yield. Здесь вы можете играть и видеть разницу между генераторами/итераторами:

Демо: babel/repl.

Ответ 2

Одна проблема: yield дает только один уровень для вызывающего функции. Поэтому, когда вы yield в обратном вызове, он может не делать то, что вы думаете:

// The following yield:
function *upper (items) { // <---- does not yield here
  items.map(function (item) { // <----- instead it yields here
    try {
      yield item.toUpperCase()
    } catch (e) {
      yield 'null'
    }
  }
}

Итак, в приведенном выше коде у вас нет абсолютно никакого доступа к полученному значению. Array.prototype.map имеет доступ к полученному значению. И если бы вы были тем, кто написал код для .map(), вы можете получить это значение. Но поскольку вы не тот, кто написал Array.prototype.map, а так как человек, который написал Array.prototype.map, не возвращает возвращаемое значение, вы вообще не получаете доступ к полученным значениям (и) мы надеемся, что все они будут собирать мусор).

Можем ли мы заставить его работать?

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

// WARNING: UNTESTED!
function *mapGen (arr,callback) {
    for (var i=0; i<arr.length; i++) {
        yield callback(arr[i])
    }
}

Затем вы можете использовать его следующим образом:

mapGen(items,function (item) {
    yield item.toUpperCase();
});

Или, если вы храбры, вы можете расширить Array.prototype:

// WARNING: UNTESTED!
Array.prototype.mapGen = function *mapGen (callback) {
    for (var i=0; i<this.length; i++) {
        yield callback(this[i])
    }
};

Мы можем назвать это следующим образом:

function *upper (items) {
  yield* items.mapGen(function * (item) {
    try {
      yield item.toUpperCase()
    } catch (e) {
      yield 'null'
    }
  })
}

Обратите внимание, что вам нужно получить дважды. Это потому, что внутренний доход возвращается к mapGen, тогда mapGen даст это значение, тогда вам нужно будет его вернуть, чтобы вернуть это значение из upper.

OK. Такие работы, но не совсем:

var u = upper(['aaa','bbb','ccc']);
console.log(u.next().value); // returns generator object

Не то, что мы хотим. Но это имеет смысл, поскольку первый доход дает доходность. Итак, мы обрабатываем каждый доход как объект-генератор? Давайте посмотрим:

var u = upper(['aaa','bbb','ccc']);
console.log(u.next().value.next().value.next().value); // works
console.log(u.next().value.next().value.next().value); // doesn't work

OK. Дайте понять, почему второй вызов не работает.

Верхняя функция:

function *upper (items) {
  yield* items.mapGen(/*...*/);
}

возвращает возвращаемое значение mapGen(). Пока же пусть игнорирует то, что mapGen делает и просто думает о том, что на самом деле означает yield.

Итак, в первый раз, когда мы вызываем .next(), функция здесь приостановлена:

function *upper (items) {
  yield* items.mapGen(/*...*/); // <----- yields value and paused
}

который является первым console.log(). Во второй раз, когда мы вызываем .next(), вызов функции продолжается в строке после yield:

function *upper (items) {
  yield* items.mapGen(/*...*/);
  // <----- function call resumes here
}

который возвращает (не выход из-за отсутствия ключевого слова yield на этой строке) ничего (undefined).

Вот почему второй console.log() терпит неудачу: у функции *upper() закончились объекты. В самом деле, он только когда-либо дает один раз, поэтому у него есть только один объект - он является генератором, который генерирует только одно значение.

OK. Поэтому мы можем сделать это следующим образом:

var u = upper(['aaa','bbb','ccc']);
var uu = u.next().value; // the only value that upper will ever return
console.log(uu.next().value.next().value); // works
console.log(uu.next().value.next().value); // works
console.log(uu.next().value.next().value); // works

Ура! Но, если это так, как работает внутренняя yield в обратном вызове?

Хорошо, если вы тщательно подумаете, вы поймете, что самый внутренний yield в обратном вызове также ведет себя как yield в *upper() - он только вернет одно значение. Но мы никогда не используем его более одного раза. Это потому что во второй раз, когда мы вызываем uu.next(), мы не возвращаем один и тот же обратный вызов, а другой обратный вызов, который, в свою очередь, также будет возвращать только одно значение.

Так оно и работает. Или это можно заставить работать. Но это как-то глупо.

Вывод:

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

Второй момент для понимания заключается в том, что обратные вызовы и все эти методы Array (.map, .forEach и т.д.) не являются волшебными. Это просто функции javascript. По сути, это немного ошибочно думать о них как о контрольных структурах вроде for или while.

Эпилог

Существует способ сделать работу mapGen чистой:

function upper (items) {
  return items.mapGen(function (item) {
    try {
      return item.toUpperCase()
    } catch (e) {
      return 'null'
    }
  })
}
var u = upper(['aaa','bbb','ccc']);
console.log(u.next().value);
console.log(u.next().value);
console.log(u.next().value);

Но вы заметите, что в этом случае мы возвращаем форму обратного вызова (not yield), а также возвращаем форму upper. Таким образом, этот случай возвращается в yield внутри цикла for, что не является тем, что мы обсуждаем.