[Effective JavaScript 笔记] 第14条:当心命名函数表达式笨拙的作用域

时间:2021-03-05 21:47:43

js函数会根据上下文改变其含义。

function double(x){return x*2;}

这是一个函数声明,也可以是一个命名函数表达式(named function expression),取决于它出现的地方。

声明一个函数,并绑定一个当前作用域的变量。

同一段函数代码也可以作为一个表达式。

var f=function double(x){return x*2;}

根据ECMAScript规范,该函数绑定到变量f,而不是变量double。这里给函数表达式命名并不是必要的的,可以直接使用匿名的函数表达式。

var f=function(x){return x*2;}

匿名和命名函数表达式的官方区别在于后者会绑定到与其函数名相同的变量上,该变量将作为函数内的一个局部变量。

可以用来写递归函数表达式

var f=function find(tree,key){

if(!tree){

return null;

}

if(tree.key === key){

return tree.value;

}

return find(tree.left,key)||find(tree.right,value);

}

注意:变量find的作用域只在其自身函数中。不像函数声明,命名函数表达式不能通过其内部的函数名在外部被引用。

find(myTree,”foo”);//error:find is not defined

使用命名函数表达式来进行递归似乎没有必要,因为使用外部作用域的函数名也可以达到同样的效果

var f=function(tree,key){

if(!tree){

return null;

}

if(tree.key === key){

return tree.value;

}

return f(tree.left,key)||f(tree.right,value);

}

function find(tree,key){

if(!tree){

return null;

}

if(tree.key === key){

return tree.value;

}

return find(tree.left,key)||find(tree.right,value);

}

var f=find;

命名函数的真正的用处是进行调试

大多数现代的JS环境都提供对Error对旬的栈跟踪功能。在栈跟踪中,函数表达式的名称通常作为其入口作用。用于检查栈的设备调试器对命名函数表达式有类似的使用。

命名函数表达式是作用域和兼容性问题的来源。ES规范的错误,在ES3中就已经存在,JS引擎被要求将命名函数表达式的作用域表示为一个对象,像有问题的with结构。该作用域对象只含有单个属性,该属性将函数名和函数自身绑定起来。该作用域对象也继承了Object.prototype的属性。这意味着仅仅是给函数表达式命名也会将Object.prototype中的所有属性引用到作用域中。结果出问题如:

var constructor=function(){return null;};

var f=function f(){

return constructor();

};

f();//{}(in ES3 environments)

ES5修正了这个问题。运行如下:

[Effective JavaScript 笔记] 第14条:当心命名函数表达式笨拙的作用域

但有些JS环境仍然使用过时的对象作用域,有些环境更不符合标准,对匿名函数表达式使用对象作为作用域。

在系统中避免对象污染函数表达式作用域的最好方式是避免任何时候在Object.prototype中添加属性,以及避免使用任何与标准Object.prototype属性同名的局部变量

在流行的JS引擎中的另一个缺陷是对命名函数表达式的声明进行提升。

var f=function g(){return 17;}

g();//17(在不标准的环境中)

注意这是不符合标准的行为。

有一些JS环境甚至把f和g这两个函数作为不同的对象,从而导致不必要的内存分配。这种行为的一个合理的解决办法是创建一个与函数表达式同名的局部变量并赋值为null。

var f=function g(){return 17;}

var g=null;

即使在没有错误地提升函数表达式声明的环境中,使用var重声明变量能确保仍然会绑定变量g。设置变量g为null能确保重复的函数被垃圾回收。

合理的结论

  • 命名函数表达式由于会导致很多问题,所以并不值得使用。
  • 一个不太严肃的回应是开发阶段使用命名函数表达式用作调试,在发布前通过构建工具去把所有函数表达式转化为匿名的。
  • 要明确自己要发布的平台的JS环境。

提示

  • 在Error对象和调试器中使用命名函数表达式改进栈跟踪
  • 在ES3和有问题的JS环境中,函数表达式作用域会被Object.prototype污染
  • 错误百出的JS环境中会提升命名函数表达式声明,并导致命名函数表达式的重复存储
  • 考虑避免使用命名函数表达式或在发布前删除函数命名
  • 确保JS代码到正确实现ES5环境中,就不用再担心这些问题了