详解Javascript函数声明与递归调用
本文深入解析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构造函数创建的函数在每次调用时都会重新编译,递归调用会引发性能问题,导致内存迅速耗尽。
以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持本站。
D3.js 比例尺与坐标轴的配置详解
2026-11-05 16:54:46
JavaScript怎么遍历对象的Symbol键和不可枚举属性
2026-11-05 13:13:39
封装一个健壮的JS字符串判空工具函数
2026-11-05 12:16:14
JavaScript合并数组的三种常用方法:concat、扩展运算符与push
2026-11-05 11:22:29
JavaScript立即执行函数(IIFE)是什么及基本语法
2026-11-05 10:51:19
