详解Javascript函数声明与递归调用

作者:异步漫步

时间:2026-10-30

浏览:551

本文深入解析JavaScript中函数的四种声明方式及递归调用的实现机制。重点探讨变量式与函数式声明在递归时的差异,分析对象方法递归中this指向带来的问题,并对比内联函数与arguments.callee的优劣。文章指出Function构造函数的性能隐患及ECMAScript 5严格模式下对arguments.callee的限制,为开发者提供安全高效的递归解决方案。

文章目录

JavaScript函数概述

关于JavaScript函数的声明与调用,虽然已是老生常谈,但仍有必要反复梳理。每当看到资料中列举JavaScript函数的四种调用方式时,不禁让人联想到孔乙己所言“茴字有四种写法”的典故。

尽管JavaScript存在诸多缺陷,但其作为顶级对象(first-class objects)的函数特性仍极具魅力。函数可以像普通对象一样被创建、赋值给变量、作为参数传递、作为返回值返回,甚至持有属性和方法。这种顶级对象的特性赋予了JavaScript强大的函数式编程能力,同时也带来了灵活性过高、难以控制的问题。

函数声明方式

1、函数声明

变量式声明首先创建一个匿名函数,随后将其赋值给指定的变量:


var f = function () { // function body }; 

通常无需关注等号右侧表达式的作用域是全局还是闭包内,因为只能通过左侧变量f进行引用,重点应关注变量f自身的作用域。若f指向函数的引用被销毁(如执行f = null),且该函数未赋值给其他变量或对象属性,匿名函数将因失去所有引用而被垃圾回收机制销毁。

也可以采用函数表达式来创建函数:


function f() { // function body } 

与变量式声明不同,这种方式会为函数的内置属性name赋值,并将函数赋值给当前作用域中一个同名的变量(该name属性的configurable、enumerable和writable属性均为false)。


function f() { // function body } 
console.log(f.name); // "f" 
console.log(f); // f() 

JavaScript变量具有声明提前的特性,表达式式的函数声明也会将整个函数定义前置,因此可以在函数定义之前调用它:


console.log(f.name); // "f" 
console.log(f); // f() 
function f() { // function body } 

函数表达式的声明会被提升至作用域顶层,以下代码展示了这一现象,但这并非本文重点:


var a = 0; 
console.log(a); // 0 or a()? 
function a () {} 

Crockford建议始终使用第一种方式声明函数,他认为第二种方式放宽了“先声明后使用”的要求,容易导致代码混乱。(Crockford是一位类似罗素形容维特根斯坦的“有良心的艺术家”那样的“有良心的程序员”,此比喻颇为拗口)。

函数式声明


function f() {} 

看起来是


var f = function f(){}; 

的简写形式。而


var a = function b(){}; 

的表达式会创建一个函数,将其内置name属性赋值为“b”,然后将该函数赋值给变量a。你可以在外部通过a()调用它,但不能直接使用b(),因为函数已赋值给a,不会自动创建变量b,除非显式使用var b = a声明一个变量b。需注意,该函数的name属性值为“b”而非“a”。

使用Function构造函数同样可以创建函数:


var f = new Function("a,b,c","return a+b+c;"); 

这种方式实际上是在全局作用域内生成一个匿名函数,并将其赋值给变量f。

递归调用与陷阱

2、递归调用

递归常用于简化问题,其核心是在函数体内调用自身:


// 一个简单的阶乘函数 
var f = function (x) { 
  if (x === 1) { 
    return 1; 
  } else { 
    return x * f(x - 1); 
  } 
}; 

JavaScript中函数的巨大灵活性导致在递归时使用函数名变得困难。对于上述变量式声明,f只是一个变量,其值容易被替换:


var fn = f; 
f = function () {}; 

函数作为一个值被赋给fn,我们期望通过fn(5)计算数值,但由于函数内部依然引用变量f,导致无法正常工作。

函数式声明看似更好,但实际情况令人遗憾:


function f(x) { 
  if (x === 1) { 
    return 1; 
  } else { 
    return x * f(x - 1); 
  } 
} 
var fn = f; 
f = function () {}; // may been warning by browser 
fn(5); // NaN 

由此可见,一旦定义了递归函数,便需谨慎避免随意更改变量名。

上述讨论均针对函数式调用,函数还有其他调用方式,例如作为对象方法调用。

我们通常这样声明对象:


var obj1 = { 
  num : 5, 
  fac : function (x) { 
    // function body 
  } 
}; 

声明一个匿名函数并将其赋值给对象的属性(fac)。

若在此处编写递归逻辑,需引用属性本身:


var obj1 = { 
  num : 5, 
  fac : function (x) { 
    if (x === 1) { 
      return 1; 
    } else { 
      return x * obj1.fac(x - 1); 
    } 
  } 
}; 

当然,它也会遭遇与函数调用方式相同的问题:


var obj2 = {fac: obj1.fac}; 
obj1 = {}; 
obj2.fac(5); // Sadness 

方法被赋值给obj2的fac属性后,内部仍需引用obj1.fac,因此执行失败。

换一种方式或许能有所改进:


var obj1 = { 
   num : 5, 
   fac : function (x) { 
    if (x === 1) { 
      return 1; 
    } else { 
      return x * this.fac(x - 1); 
    } 
  } 
}; 
var obj2 = {fac: obj1.fac}; 
obj1 = {}; 
obj2.fac(5); // ok 

通过this关键字获取函数执行时上下文中的属性,这样在执行obj2.fac时,函数内部便会引用obj2的fac属性。

然而,函数还可以被任意修改context来调用,即使用call和apply方法:


obj3 = {}; 
obj1.fac.call(obj3, 5); // dead again 

于是,递归函数再次无法正常工作。

递归解决方案与建议

我们需要尝试解决这一问题,还记得前文提到的一种函数声明方式吗?


var a = function b(){}; 

这种声明方式称为内联函数(inline function)。虽然在函数外部未声明变量b,但在函数内部可以使用b()来调用自身,因此:


var fn = function f(x) { 
  // try if you write "var f = 0;" here 
  if (x === 1) { 
    return 1; 
  } else { 
    return x * f(x - 1); 
  } 
}; 
var fn2 = fn; 
fn = null; 
fn2(5); // OK 

 // here show the difference between "var f = function f() {}" and "function f() {}" 
var f = function f(x) { 
  if (x === 1) { 
    return 1; 
  } else { 
    return x * f(x - 1); 
  } 
}; 
var fn2 = f; 
f = null; 
fn2(5); // OK 

var obj1 = { 
  num : 5, 
  fac : function f(x) { 
    if (x === 1) { 
      return 1; 
    } else { 
      return x * f(x - 1); 
    } 
  } 
}; 
var obj2 = {fac: obj1.fac}; 
obj1 = {}; 
obj2.fac(5); // ok 
 
var obj3 = {}; 
obj1.fac.call(obj3, 5); // ok 

这样,我们获得了一个在内部可用的名字,无需担心递归函数被赋值给谁或以何种方式被调用。

JavaScript函数内部的arguments对象包含一个callee属性,指向函数本身。因此,也可以使用arguments.callee在内部调用函数:


function f(x) { 
  if (x === 1) { 
    return 1; 
  } else { 
    return x * arguments.callee(x - 1); 
  } 
} 

但arguments.callee是一个即将被弃用的属性,未来ECMAscript版本中可能会消失。在ECMAscript 5的“use strict”模式下,禁止使用arguments.callee。

最后建议:若需声明递归函数,请慎用new Function这种方式。Function构造函数创建的函数在每次调用时都会重新编译,递归调用会引发性能问题,导致内存迅速耗尽。

以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持本站。

实用工具推荐

电子包浆

绿化、压缩、噪点和虚拟水印,自由调节做旧程度。

使用 ›
图片压缩

选择画质和输出格式,预览压缩前后的大小。

使用 ›
相关文章