Рекурсивный вызов функции javascript

90

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

/* Count down to 0 recursively.
 */
var functionHolder = function (counter) {
    output(counter);
    if (counter > 0) {
        functionHolder(counter-1);
    }
}

При этом functionHolder(3);будет вывод 3 2 1 0. Допустим, я сделал следующее:

var copyFunction = functionHolder;

copyFunction(3);выведет, 3 2 1 0как указано выше. Если я затем изменил functionHolderследующим образом:

functionHolder = function(whatever) {
    output("Stop counting!");

Потом functionHolder(3);дадут Stop counting!, как положено.

copyFunction(3);теперь дает то, 3 Stop counting!на что ссылается functionHolder, а не функцию (на которую указывает сам). В некоторых случаях это может быть желательно, но есть ли способ написать функцию, чтобы она вызывала себя, а не переменную, которая ее хранит?

То есть, можно ли изменить только строку, functionHolder(counter-1);чтобы выполнение всех этих шагов по-прежнему давало 3 2 1 0при вызове copyFunction(3);? Я пробовал, this(counter-1);но это дает мне ошибку this is not a function.

Samthere
источник
1
NB. Внутри функции это относится к контексту выполнения функции, а не к самой функции. В вашем случае это, вероятно, указывало на глобальный объект окна.
антуан

Ответы:

146

Использование выражений именованных функций:

Вы можете дать функциональному выражению имя, которое на самом деле является частным и видно только изнутри функции ifself:

var factorial = function myself (n) {
    if (n <= 1) {
        return 1;
    }
    return n * myself(n-1);
}
typeof myself === 'undefined'

Вот myselfэто видно только внутри функции самого.

Вы можете использовать это частное имя для рекурсивного вызова функции.

См. 13. Function DefinitionСпецификацию ECMAScript 5:

На идентификатор в FunctionExpression можно ссылаться изнутри FunctionBody FunctionExpression, чтобы функция могла вызывать себя рекурсивно. Однако, в отличие от FunctionDeclaration, на Идентификатор в FunctionExpression нельзя ссылаться, и это не влияет на область, содержащую FunctionExpression.

Обратите внимание, что Internet Explorer до версии 8 ведет себя некорректно, так как имя фактически отображается в окружении переменных, включающих в себя, и ссылается на дубликат фактической функции (см. Комментарий Патрика DW ниже).

Используя arguments.callee:

В качестве альтернативы вы можете использовать arguments.calleeдля ссылки на текущую функцию:

var factorial = function (n) {
    if (n <= 1) {
        return 1;
    }
    return n * arguments.callee(n-1);
}

Пятая редакция ECMAScript запрещает использование arguments.callee () в строгом режиме , однако:

(Из MDN ): в обычном коде arguments.callee относится к включающей функции. Этот вариант использования слабый: просто назовите включающую функцию! Более того, arguments.callee существенно затрудняет оптимизацию, такую ​​как встраивание функций, потому что должна быть предусмотрена возможность предоставления ссылки на невстроенную функцию при доступе к arguments.callee. arguments.callee для функций строгого режима - это свойство, не подлежащее удалению, которое генерируется при установке или извлечении.

Арно Ле Блан
источник
4
+1 Хотя в IE8 и ниже это немного глючит, myselfно на самом деле это видно в окружении включающих переменных и ссылается на дубликат фактической myselfфункции. Вы должны иметь возможность установить внешнюю ссылку на null.
user113716
Спасибо за ответ! Оба были полезны и решили проблему двумя разными способами. В конце концов, я случайным образом решил, что принять: P
Samthere
просто для меня, чтобы понять. В чем причина умножения функции при каждом возврате? return n * myself(n-1);?
chitzui
почему функция работает так? jsfiddle.net/jvL5euho/18 после 4-кратного цикла.
Prashant Tapase
Согласно некоторым ссылкам arguments.callee не будет работать в строгом режиме.
Крунал Лимбад
10

Вы можете получить доступ к самой функции, используя arguments.callee [MDN] :

if (counter>0) {
    arguments.callee(counter-1);
}

Однако в строгом режиме это не работает.

Феликс Клинг
источник
6
Я считаю, что это устарело (и не разрешено в строгом режиме)
Арно Ле Блан
@Felix: Да, «строгий режим» даст TypeError, но я не нашел ничего, что официально заявляло бы, что arguments.callee (или любое нарушение строгого режима) устарело за пределами «строгого режима».
user113716
Спасибо за ответ! Оба были полезны и решили проблему двумя разными способами. В конце концов, я случайным образом решил, что принять: P
Samthere
6

Вы можете использовать Y-комбинатор: ( Википедия )

// ES5 syntax
var Y = function Y(a) {
  return (function (a) {
    return a(a);
  })(function (b) {
    return a(function (a) {
      return b(b)(a);
    });
  });
};

// ES6 syntax
const Y = a=>(a=>a(a))(b=>a(a=>b(b)(a)));

// If the function accepts more than one parameter:
const Y = a=>(a=>a(a))(b=>a((...a)=>b(b)(...a)));

И вы можете использовать это так:

// ES5
var fn = Y(function(fn) {
  return function(counter) {
    console.log(counter);
    if (counter > 0) {
      fn(counter - 1);
    }
  }
});

// ES6
const fn = Y(fn => counter => {
  console.log(counter);
  if (counter > 0) {
    fn(counter - 1);
  }
});
Николо
источник
5

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

  var fn = (function() {
    var innerFn = function(counter) {
      console.log(counter);

      if(counter > 0) {
        innerFn(counter-1);
      }
    };

    return innerFn;
  })();

  console.log("running fn");
  fn(3);

  var copyFn = fn;

  console.log("running copyFn");
  copyFn(3);

  fn = function() { console.log("done"); };

  console.log("fn after reassignment");
  fn(3);

  console.log("copyFn after reassignment of fn");
  copyFn(3);
Сандро
источник
3

Вот один очень простой пример:

var counter = 0;

function getSlug(tokens) {
    var slug = '';

    if (!!tokens.length) {
        slug = tokens.shift();
        slug = slug.toLowerCase();
        slug += getSlug(tokens);

        counter += 1;
        console.log('THE SLUG ELEMENT IS: %s, counter is: %s', slug, counter);
    }

    return slug;
}

var mySlug = getSlug(['This', 'Is', 'My', 'Slug']);
console.log('THE SLUG IS: %s', mySlug);

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

После того , как рекурсия соответствует конечному пункту вызова стека, это батуты «из» вызовов функций, в то время как первое приращение counterпроисходит внутри последний вложенного вызов.

Я знаю , что это не «исправление» на коде спрашивающего, но получил титул я думал , что я в общем служить примером рекурсии для лучшего понимания рекурсии, вчистую.

Коди
источник