js 函数递归优化,arguments.callee 优化

函数递归是个经典的问题,平常用的时候,小练习可以通过函数名来反复调用,比如:

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 权威指南里指出:

函数名称标识符,对于函数定义表达式来说,这个名称是可选的;如果存在,该名字只存在于函数体中,并指代该函数对象本身。

后面半句话很关键哦,函数名称标识符,如果存在,该名字只存在于函数体中,并指代该函数对象本身。这意思是,函数名称标识符作为函数体中的一个局部变量存在,指代函数对象本身,它和被函数赋值的变量名并不在同一个执行环境,被函数赋值的变量名在上一级环境;例如:

image

这意味着,你要改函数名,就不用改 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;
});

参考资料:

1、MDN: arguments.callee 属性包含当前正在执行的函数。

2、JavaScript高级程序设计-第3版

3、JavaScript权威指南-第6版

原文地址:https://www.cnblogs.com/xianshenglu/p/8086042.html