如何编写高效的js代码(3)

小心使用命名的函数表达式

一般函数声明为

function double(x) {  
    returnx * 2; 
}

此处声明了一个函数double,并将其绑定为当前作用域的一个变量,假设此处为全局的地方,则我们在window下定义了一个函数

另外一种定义函数的方式叫命名函数表达式

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

该方式将函数赋值给了f,而不是double,当然一般情况下我们不需要给函数表达式一个名字,可以使用匿名函数的形式

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

这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, key);  
};

这里find只能在函数内部使用,如果你在外面使用,则会报错

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

当然如果是为了写递归使用命名函数表达式好像没太大意义,因为你也可以直接使用f

var f = function(tree, key) {  
    if(!tree) {
    return null;
}
if(tree.key === key) {  
    return tree.value;
}
return f(tree.left, key) || f(tree.right, key);  
};

命名函数表达式真正比较有用的一点是在调试过程中,Error对象打印出来的错误堆栈是可以显示到这个具体的名字的,方便我们查找错误原因

然而遗憾的是,由于ECMAScript规范的一些历史原因和一些浏览器引擎的bug,命名函数表达式的作用域被插入一个object对象,就像with语法一样,这个对象的一个属性就是这个命名函数表达式的名字,另外它继承了Object.prototype的属性,如

var constructor = function() { return null; };  
var f = function f() {  
return constructor();  
};
f();// {} (in ES3 environments)  

命名函数表达式继承了Object.prototype.constructor,因此f()产生了一个新的空对象,因此对Object.prototype的任何修改都会影响到命名函数表达式内的使用,幸运的是,ES5修正了此错误,但是仍然有很多旧的js引擎会有这个bug

要避免出现这个bug,需要尽量避免在Object.prototype上进行新增、修改等操作,同时尽量避免使用与Object.prototype上同名的方法构造的本地变量,如constructor等

另外一个关于命名函数表达式的bug是js引擎会把函数命名表达式提权(hoisting)

var f = function g() { return 17; };  
g();// 17 (in nonconformant environments)  

更扯的是,有的js环境会认为f和g是不同的对象而导致不必要的内存分配,一个解决这个bug的方法是重新用var声明命名函数表达式名字,然后设置为null

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

用var定义g使其不会被提权(hosting),设置为null使其后面会被垃圾回收,最好的办法还是尽量避免给函数表达式命名

小心使用本地块级函数声明

js没有块级作用域的概念,只有在函数中定义的变量才是在函数中生效的,函数内部声明的函数也是在函数内部生效,如

function f() { return"global"; }  
function test(x) {  
function f() { return"local"; }  
var result = [];  
if(x) {  
result.push(f());  
}

result.push(f());  
return result;  
}
test(true); // ["local", "local"]  
test(false);// ["local"]  

但是如果稍微修改下

function f() { return"global"; }  
function test(x) {  
var result = [];  
if(x) {  
function f() { return"local"; } // block-local  
result.push(f());  
}
result.push(f());  
return result;  
}
test(true); // ?  
test(false);// ?  

因为js没有{}级作用域,只有函数级,因此if块里面的函数声明其实应该还是应该属于test函数下的,因此结果应该和前面的一样,但是事实是部分浏览器环境是这样,有些浏览器环境是根据条件在运行时来绑定f,那根据什么呢?根据if里面的代码块是否执行,这种情况使得代码不容易理解,而且性能也会受影响,但是这个又不同于with表达式

ES5规范对这种情况也木有说明,ES5都不承认有块级局部函数声明这种东西的存在,函数声明只能在某个函数内部的最外层,在strict模式下,定义这种块级局部函数声明会报错

因此如果你需要根据条件来选择使用哪个函数,你应当用var声明,同时使用函数赋值表达式

function f() { return"global"; }  
function test(x) {  
var g = f, result = [];  
if(x) {  
g = function() { return"local"; }  
result.push(g());  
}
result.push(g());  
return result;  
}

也就是说使用条件赋值函数来代替条件声明函数

作者:shaynegui
喜欢打德州,玩dota,听电音,web前端脑残粉
我的专栏 GitHub