奇怪的局部块函数声明作用域

382 阅读3分钟

小知识,大挑战!本文正在参与“程序员必备小知识”创作活动

局部块函数声明笨拙的作用域

先来看一段案例

 function f() {
   return "global fff";
 }
 
 function test(x) {
   var result = [];
   function f() {
     return "local fff";
   }
   if (x) {
     result.push(f());
   }
   result.push(f());
   return result;
 }
 ​
 console.log(test(true));  // [ 'local fff', 'local fff' ]
 console.log(test(false)); // [ 'local fff' ]

现在看起来丝毫没有问题,但是如果我们把 test 函数内的 f 函数移动到 if 语句块里面

 function f() {
   return "global fff";
 }
 ​
 function test(x) {
   var result = [];
   if (x) {
     function f() {
       return "local fff";
     }
     result.push(f());
   }
   result.push(f());  // Error : f is not a function
   return result;
 }
 ​
 console.log(test(true));  // ???
 console.log(test(false)); // ???

你或许你产生疑问为什么 13 行的代码会报错函数 f 不是在外部还定义了一个吗?为什么访问不到外部的全局 f 函数。

由于内部的函数 f 出现在 if 语句块中,因此你可能认为第一次调用 test 产生数组 ["local fff","globa fff"],第二次调用产生数组["global"]。但是要记住 JavaScript 没有块级作用域,所以内部函数f的作用域应该是整个 test 函数。

第二个例子的合理猜测是 ["local fff","local fff"]和["local fff"]。而事实上,一些 JavaScript 环境的确如此行事。但并不是所有的JavaScript 环境都这样。其他一些环境在运行时根据包含函数f的块是否被执行来有条件地绑定函数f。(不仅使代码更难理解,而且还致使性能降低。这与 with 语句没什么不同。)

关于这一点 ECMAScript 标准说了什么呢?令人惊讶的是,几乎没有。直到 ES5,JavaScript标准才承认局部块函数声明的存在。官方指定函数声明只能出现在其他函数或者程序的最外层。ES5 甚至建议将在非标准环境的函数声明转变成警告或错误。一些流行的JavaScript 实现在严格模式下将这类函数报告为错误(具有局部块函数声明的处于严格模式下的程序将报告一个语法错误)。这有助于检测出不可移植的代码,并为未来的标准版本在给局部块函数声明指定更明智和可移植的语义开辟了一条路。

在此期间,编写可移植的函数的最好方式是始终避免将函数声明置于局部块或子语句中。如果你想编写嵌套函数声明,应该将它置于其父函数的最外层,正如最开始的示例所示。另外,如果你需要有条件地选择函数,最好的办法是使用 var 声明和函数表达式来实现。

 function f() {
   return "global";
 }
 ​
 function test(x) {
   var result = [],
     g = f;
   if (x) {
     g = function() {
       return "local";
     };
     result.push(g());
   }
   result.push(f());
   return result;
 }
 ​
 console.log(test(true));  // [ 'local', 'global' ]
 console.log(test(false)); // [ 'global' ]

这消除了内部变量(重命名为g)作用域的神秘性。它无条件地作为局部变量被绑定,而仅仅只有赋值语句是有条件的。结果很明确,该函数完全可移植。

总结

  • 始终将函数声明置于程序或被包含的函数的最外层以避免不可移植的行为。
  • 使用 var 声明和有条件的赋值语句替代有条件的函数声明。