Рекурсивний виклик функції 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.


1
Примітка. Усередині функції це стосується контексту виконання функції, а не самої функції. У вашому випадку це, мабуть, вказувало на глобальний об'єкт вікна.
antoine

Відповіді:


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 може посилатися зсередини FunctionBox FunctionExpression, щоб функція могла викликати себе рекурсивно. Однак, на відміну від FunctionDeclaration, на Ідентифікатор у FunctionExpression не можна посилатися та не впливає на область, що охоплює FunctionExpression.

Зверніть увагу, що Internet Explorer до версії 8 не поводиться належним чином, оскільки ім’я фактично видно в оточуючому змінному середовищі, і воно посилається на дублікат фактичної функції (див. Коментар patrick dw нижче).

Використовуючи аргументи.callee:

Крім того, ви можете використати arguments.calleeпосилання на поточну функцію:

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

5-е видання ECMAScript забороняє використовувати аргументи.callee () у суворому режимі , однак:

MDN ): У звичайних аргументах коду. Callee посилається на функцію, що включає. Цей варіант використання слабкий: просто назвіть функцію, що включає! Більше того, argument.callee суттєво заважає оптимізації, як вбудовування функцій, оскільки має бути можливим надання посилання на невбудовану функцію, якщо доступ до argument.callee. arguments.callee для функцій суворого режиму - це властивість, що не підлягає видаленню і яка видає, коли встановлено або отримано.


4
+1 Хоча це трохи глючить в IE8 і нижче, що myselfнасправді видно в оточуючому змінному середовищі, і він посилається на дублікат фактичної myselfфункції. Ви повинні мати можливість встановити зовнішнє посилання на null.
user113716

Дякую за відповідь! Обидва були корисними та вирішили проблему двома різними способами. Врешті-решт я випадково вирішив, що прийняти: P
Samthere

просто щоб я зрозумів. У чому причина множення функції при кожному поверненні? return n * myself(n-1);?
chitzui

чому функція працює так? jsfiddle.net/jvL5euho/18 після, якщо цикл повторюється 4 рази.
Прашант Тапасе

Згідно з деякими посилальними аргументами. Callee не працюватиме в суворому режимі.
Крунал Лімбад,

10

Ви можете отримати доступ до самої функції за допомогою arguments.callee [MDN] :

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

Однак це призведе до поломки в суворому режимі.


6
Я вважаю, що це застаріло (і не дозволяється в суворому режимі)
Арно Ле Блан

@Felix: Так, "суворий режим" дасть "a" 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відбувається всередині останнього вкладеного виклику.

Я знаю, що це не "виправлення" коду запитувача, але, враховуючи заголовок, я думав, що в цілому буду прикладом Рекурсії для кращого розуміння рекурсії, прямо.

Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.