使用包装函数来隐藏变量和函数的方法会导致一些额外的问题:
- 必须声明一个明明函数如foo(), 这样foo本身污染了所在作用域
- 必须显式地通过函数名调用才能执行函数
如何避免函数名污染作用域并立即执行?
var a = 2;
(function foo(){
var a = 3;
conosole.log(a);
})();
console.log(a);
函数声明以(function... 开始 而不是function开始,这样使得函数声明变成函数表达式。这样一来(function foo(){ .. })作为函数表达式使得foo只能在..所代表的位置被访问,外部访问不了。就不会污染所在作用域了。
如何区分函数声明和函数表达式? 如果function关键字是声明中的第一个词是函数声明,否则就是函数表达式。
3.3.1 匿名和具名
setTimeout(function(){
console.log('1 second!')
},1000);
匿名函数表达式,最常用在回调函数中,省略了名称标识符。函数表达式可以匿名,但是函数声明不可以(非法)。
👎缺点:
- 匿名函数在追踪栈中不会显示出有意义的函数名,使得调试困难
- 当函数需要引用自身时只能使用过期的arguments.callee引用,比如在递归中,或者事件监听器触发后需要解绑自身。
- 函数名很大程度上提高了代码的可读性,匿名函数损失了一部分可读性。
👍最佳实践:始终给函数表达式命名
setTimeout(function timeoutHandler{
console.log('1 seconds');
},1000)
🌈拓展:arguments.callee
Callee 是 arguments对象的一个属性,可以用于引用函数体内部当前正在执行的函数,常在函数名未知的情况下引用。
⚠️注意: 在严格模式下,ES5禁止使用arguments.callee(),如果要调用自身,要么给函数体一个名字,要么使用函数声明
为什么使用arguments.callee?
早期的js不允许使用命名函数表达式,因此无法创建递归函数表达式。
1⃣️使用函数声明进行递归
function factorial(n){
return !(n>1)?1:factorial(n-1)*n;
}
[1,2,3,4,5].map(factorial);
2⃣️使用匿名函数表达式(👎没法整)
[1,2,3,4,5].map(function(n){
return !(n>1)?1:/* 啥啥啥??? */(n-1)*n;
});
// 看吧,这样是行不通的,于是为了解决这个问题,就添加了arguments.callee
3⃣️使用arguments.callee
[1,2,3,4,5].map(function(n){
return !(n>1)?1:arguments.callee(n-1)*n
})
👎但是,这个解决方案并不好,原因如下:
1⃣️这种方案使得在通常的情况下,不可能实现内联和尾递归(也包括其他的arguments、callee、caller问题)
2⃣️递归调用会获取到一个不同的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 is: [object Arguments]
到了ES3之后,就可以通过命名函数表达式来解决上述问题了,参考上述3.3.1。
🌈拓展:Function.caller
⚠️👎 注意:非标准特性
Function.caller 不是标准特性,尽量避免在生产环境中使用。
返回调用指定函数的函数
如果一个函数是在全局作用域中被调用的,那么f.caller为 null;如果一个函数是在另一个函数的作用域内被调用的,那么f.caller指向调用它的函数。
该属性的常用形式arguments.callee.caller
代替了被废弃的 arguments.caller
.
⚠️注意: 在使用递归调用时,不能通过该属性来重现出调用栈,如下
function f(n){g(n-1);}
function g(n){if(n>0)f(n);else stop();}
f(2); // f(2) --> g(1) --> f(1) --> g(0) --> stop()
由于下面的表达式为 true(只保留函数最后一次被调用时的caller)
stop.caller === g && f.caller === g && g.caller === f
所以如果你尝试在stop函数中获取调用栈的话:
var f = stop;
var stack = “调用栈“;
while(f){
stack+="\n" + f.name;
f = f.caller;
}
则上面的代码会进入一个死循环。
👀有一个特殊属性_caller_,可以返回调用当前函数的函数的活动对象(可以用来重现整个调用栈),但是处于安全考虑,该属性已经被删除。
🌰例子:检测一个函数的caller属性的值
function myFunc(){
if(myFunction.caller == null){
return '我实在全局作用域被调用的';
} else
return ("调用我的函数是"+ myFunc.caller);
}
🌈拓展:内联
作用: 不实在调用时发生控制转移,而是在编译时将函数体嵌入在每一个调用处,适用于功能简单,规模较小又使用频繁的函数。
主要体现在inline关键字(❓可能是指的其他语言,js没有见过这个关键字)。
内联是以代码膨胀为代价,仅仅省去了函数调用的开销,从而提高了代码执行效率。如果执行函数体内部代码的时间比函数调用的开销还要大的话,那样效率的收获就会很少。另一方面,每一处内联函数的调用都要复制代码,将使程序的总代码量增大,消耗更多的内存空间。
⚠️注意事项:
- 内联函数不能包含复杂的控制语句,如循环语句和switch语句;
- 递归函数无法内联处理;
- 不能进行异常接口声明。
- 只将规模很小(一般五条语句以下),使用频繁的函数声明为内联函数。在函数规模很小的情况下,函数调用的时间开销可能相当于甚至超过执行函数本身的时间,把它定义为内联函数,可以大大节省程序运行时间。
⭐️内联命名函数: var function = function(){...}
❓匿名函数是内联函数吗?如果是的话,都是吗?
🌈拓展:尾递归
参考知乎回答:https://www.zhihu.com/question/20761771/answer/23254340
function story(){
从前有座山,山里有座庙,庙里有个老和尚给小和尚讲故事:story(); // 尾递归,进入下个函数不再需要上一个函数的环境了,得了结果直接返回
}
function story(){
从前有座山,山里有座庙,庙里有个老和尚给小和尚讲故事:story();
小和尚听了,找了块豆腐撞死了。 // 非尾递归,下个函数结束之后此函数还有后续,所以需要保存本身的环境以处理返回值
}
精辟啊❗️