[Effective JavaScript 笔记]第27条:使用闭包而不是字符串来封装代码

Posted 脚后跟着猫

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了[Effective JavaScript 笔记]第27条:使用闭包而不是字符串来封装代码相关的知识,希望对你有一定的参考价值。

函数是一种将代码作为数据结构存储的便利方式,代码之后可以被执行。这使得富有表现力的高阶函数抽象如map和forEach成为可能。它也是js异步I/O方法的核心。与此同时,也可以将代码表示为字符串的形式传递给eval函数以达到同样的功能。
程序员面临一个选择:应该将代码表示为函数还是字符串?
毫无疑问,应该将代码表示为函数。字符串表示代码不够灵活的一个重要原因是:它们不是闭包。

闭包回顾

看下面这个图
1465186516975

js的函数值包含了比调用它们时执行所需要的代码还要多的信息。而且js函数值还在内部存储它们可能会引用的定义在其封闭作用域的变量。那些在其所涵盖的作用域内跟踪变量的函数被称为闭包。
详细的信息到之前《[Effective JavaScript 笔记] 第11条:熟练掌握闭包》查看

字符串封装代码

假设有一个简单的多次重复用户提供的动作的函数。

function repeat(n,action){
   for(var i=0;i<n;i++){
       eval(action);
   }
}

该函数在全局作用域会不作得很好,因为eval函数会将出现的字符串中的所有变量引用作为全局变量来解释。例如,一个测试函数基准执行速度的脚本可能恰好使用全局的start和end变量来存储时间。

var start=[],end=[],timings=[];
repeat(1000,"start.push(Date.now());f();end.push(Date.now())");
for(var i=0,n=start.length;i<n;i++){
   timings[i]=end[i]-start[i];
}

但脚本很脆弱。如果我们简单地将代码移动到一个函数中,那么start和end变量将不再是全局变量。

function benchmark(){
    var start=[],end=[],timings=[];
    repeat(1000,"start.push(Date.now());f();end.push(Date.now())");
    for(var i=0,n=start.length;i<n;i++){
       timings[i]=end[i]-start[i];
    }
    return timings;
}

这个时候repeat函数并不能访问benchmark函数的内部变量start,end。还是在全局空间查找start,end变量,如果没有还是较好的情况,可以根据错误提示,完成错误定位。如果这个时候全局中恰好有start,end变量,这个时候就会对全局变量进行修改,产生的行为无法进行预测。

闭包封装代码

还是使用上面的例子,但这一些我们使用闭包来对代码进行处理。
改写repeat函数,参数action是一个函数,而不是字符串

function repeat(n,action){
   for(var i=0;i<n;i++){
       action();
   }
}

改写benchmark函数,脚本能安全地引用闭包中的局部变量start,end,该闭包以repeat函数的回调函数传递进来。

function benchmark(){
    var start=[],end=[],timings=[];
    repeat(1000,function(){
        start.push(Date.now());
        f();
        end.push(Date.now());
    });
    for(var i=0,n=start.length;i<n;i++){
       timings[i]=end[i]-start[i];
    }
    return timings;
}

eval函数的另一个问题是,通常一些高性能的引擎难优化字符串中的代码,因为编译器能不能尽可能早地获得源代码来及时 优化代码。函数表达式在其代码出现的同时就能被编译,这使得它更适合标准化编译。
其它eval相关内容可查看:
[Effective JavaScript 笔记]第16条:避免使用eval创建局部变量
[Effective JavaScript 笔记]第17条:间接调用eval函数优于直接调用

提示

  • 当将字符串传递给eval函数以执行它们的API时,绝不要在字符串中包含局部变量引用

  • 接受函数调用的API优于使用eval函数执行字符串的API

附录:代码完整版

把上面的代码整理一下,生成一个可以测试任何函数执行时间的代码

function repeat(n,action){
   for(var i=0;i<n;i++){
       action();
   }
}
function benchmark(n,fn){
    var start=[],end=[],timings=[];
    repeat(n,function(){
        start.push(Date.now());
        fn();
        end.push(Date.now());
    });
    for(var i=0,n=start.length;i<n;i++){
       timings[i]=end[i]-start[i];
    }
    return timings;
}
//测试代码function concatString(a,b){
    return a+b;
}
benchmark(10000,function(){concatString(\'1\',\'2\');});//常规调用
benchmark(10000,concatString.bind(null,\'1\',\'2\'));//利于bind方法来产生新函数

以上是关于[Effective JavaScript 笔记]第27条:使用闭包而不是字符串来封装代码的主要内容,如果未能解决你的问题,请参考以下文章

[Effective JavaScript 笔记] 第12条:理解变量声明提升

[Effective JavaScript 笔记]第15条:当心局部块函数声明笨拙的作用域

[Effective JavaScript 笔记]第48条:避免在枚举期间修改对象

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

[Effective JavaScript 笔记]第23条:永远不要修改arguments对象

[Effective JavaScript 笔记]第17条:间接调用eval函数优于直接调用