函数递归是个经典的问题,平常用的时候,小练习可以通过函数名来反复调用,比如:
function factorial(num) { if (num <= 1) { return 1; } else { return num * factorial(num - 1); } }
js高程认为:
这个函数的执行与函数名factorial 紧紧耦合在了一起。为了消除这种紧密耦合的现象,可以像下面这样使用 arguments.callee (我理解就是,如果想改个函数名 factorial ,那我要改两次或者更多次,麻烦且容易漏掉)
function factorial(num) { if (num <= 1) { return 1; } else { return num * arguments.callee(num - 1); } }
但是MDN认为:
这实际上是一个非常糟糕的解决方案,因为这 (以及其它的 arguments
, callee
, 和 caller
问题) 使得在通常的情况(你可以通过调试一些个别例子去实现它,但即使最好的代码是最理想的,你也没必要去检查调试它)不可能实现内联和尾递归。另外一个主要原因是递归调用会获取到一个不同的 this
值,例如:
var global = this; var sillyFunction = function (recursed) { if (!recursed) { return arguments.callee(true); } if (this !== global) { alert("This is: " + this); } else { alert("This is the global"); } } sillyFunction();
例子说明了问题,第二次递归的时候,this 变成了 arguments 对象,MDN 讲的有道理,因为 arguments.callee 是作为 arguments 对象的方法调用的,任何函数只要作为方法调用实际上都会传入一个隐式的实参-方法调用的母体对象,所以默认的 this 就是 arguments 对象。
但是如果想保存 this 值,我觉得也是可以实现的,比如用 .call() 和 .apply()
var global = this; var sillyFunction = function (recursed) { if (!recursed) { return arguments.callee.call(global,true); } if (this !== global) { alert("This is: " + this); } else { alert("This is the global"); } }; sillyFunction();
但是还有个缺点就是,
因为这 (以及其它的 arguments
, callee
, 和 caller
问题) 使得在通常的情况(你可以通过调试一些个别例子去实现它,但即使最好的代码是最理想的,你也没必要去检查调试它)不可能实现内联和尾递归,
内联和尾递归是什么我不知道了,等遇到了我再看看能不能优化吧,不过 MDN 倒是赞同通过命名函数表达式解决这些问题;
什么叫命名函数表达式呢?
通常我们定义函数有两种方式,一般都是:
//函数声明语句
function factorial(num) { if (num <= 1) { return 1; } else { return num * /*怎么填???*/(num - 1); } } //函数定义表达式 var factorial=function (num) { if (num <= 1) { return 1; } else { return num * /*怎么填???*/(num - 1); } };
但是这样就面临递归的问题,除了上面两种情况,我们也可以这样定义:
var factorial=function f(num) { if (num <= 1) { return 1; } else { return num * f(num - 1); } };
js 权威指南里指出:
函数名称标识符,对于函数定义表达式来说,这个名称是可选的;如果存在,该名字只存在于函数体中,并指代该函数对象本身。
后面半句话很关键哦,函数名称标识符,如果存在,该名字只存在于函数体中,并指代该函数对象本身。这意思是,函数名称标识符作为函数体中的一个局部变量存在,指代函数对象本身,它和被函数赋值的变量名并不在同一个执行环境,被函数赋值的变量名在上一级环境;例如:
这意味着,你要改函数名,就不用改 f 了,只要改变量名就行了,解决了 js高程 的代码耦合的问题,而且避免了可能出现的 MDN 提出的问题;
除了在函数定义的时候可以用,平时也可以用,把 匿名函数 换成 命名函数表达式 就可以了,如:
[1, 2, 3, 4, 5].map(function(n) { return !(n > 1) ? 1 : arguments.callee(n - 1) * n; }); //优化的代码 [1, 2, 3, 4, 5].map(function f(n) { return !(n > 1) ? 1 : f(n - 1) * n; });
参考资料: