Board logo

标题: JavaScript 中的函数式编程实践(2)JavaScript 中的函数式编程 [打印本页]

作者: look_w    时间: 2018-9-14 17:10     标题: JavaScript 中的函数式编程实践(2)JavaScript 中的函数式编程

JavaScript 中的函数式编程JavaScript 是一门被误解甚深的语言,由于早期的 Web 开发中,充满了大量的 copy-paste 代码,因此平时可以见到的 JavaScript 代码质量多半不高,而且 JavaScript 代码总是很飞动的不断闪烁的 gif 广告,限制网页内容的复制等联系在一起的,因此包括 Web 开发者在内的很多人根本不愿意去学习 JavaScript。
这种情形在 Ajax 复兴时得到了彻底的扭转,Google Map,Gmail 等 Ajax 应用的出现使人们惊叹:原来 JavaScript 还可以做这样的事!很快,大量优秀的 JavaScript/Ajax 框架不断出现,比如 Dojo,Prototype,jQuery,ExtJS 等等。这些代码在给页面带来绚丽的效果的同时,也让开发者看到函数式语言代码的优雅。
函数式编程风格在 JavaScript 中,函数本身为一种特殊对象,属于顶层对象,不依赖于任何其他的对象而存在,因此可以将函数作为传出 / 传入参数,可以存储在变量中,以及一切其他对象可以做的事情 ( 因为函数就是对象 )。
JavaScript 被称为有着 C 语法的 LISP,LISP 代码的一个显著的特点是大量的括号以及前置的函数名,比如:
清单 4. LISP 中的加法
1
(+ 1 3 4 5 6 7)




加号在 LISP 中为一个函数,这条表达式的意思为将加号后边的所有数字加起来,并将值返回,JavaScript 可以定义同样的求和函数:
清单 5. JavaScript 中的求和
1
2
3
4
5
6
7
8
9
10
function sum(){
  var res = 0;
  for ( var i = 0, len = arguments.length; i < len; i++){
res += parseInt(arguments);
     }
  return res;
}

print(sum(1,2,3));
print(sum(1,2,3,4,6,7,8));




运行此段代码,得到如下结果:
1
2
6
31




如果要完全模拟函数式编码的风格,我们可以定义一些诸如:
清单 6. 一些简单的函数抽象
1
2
3
4
5
6
7
8
9
10
function add(a, b){  return a+b; }
function sub(a, b){  return a-b; }
function mul(a, b){  return a*b; }
function div(a, b){  return a/b; }
function rem(a, b){  return a%b; }
function inc(x){  return x + 1; }
function dec(x){  return x - 1; }
function equal(a, b){  return a==b; }
function great(a, b){  return a>b; }
function less(a, b){  return a<b; }




这样的小函数以及谓词,那样我们写出的代码就更容易被有函数式编程经验的人所接受:
清单 7. 函数式编程风格
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// 修改之前的代码
function factorial(n){
if (n == 1){
return 1;
} else {
return factorial(n - 1) * n;
    }
}

// 更接近“函数式”编程风格的代码
function factorial(n){
    if (equal(n, 1)){
        return 1;
   } else {
        return mul(n, factorial(dec(n)));
   }
}




闭包及其使用闭包是一个很有趣的主题,当在一个函数 outter 内部定义另一个函数 inner,而 inner 又引用了 outter 作用域内的变量,在 outter 之外使用 inner 函数,则形成了闭包。描述起来虽然比较复杂,在实际编程中却经常无意的使用了闭包特性。
清单 8. 一个闭包的例子
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
function outter(){
  var n = 0;
  return
                 function (){
  return n++;
}
}

  var o1 = outter();
o1();//n == 0
o1();//n == 1
o1();//n == 2
  var o2 = outter();
o2();//n == 0
o2();//n == 1




匿名函数 function(){return n++;} 中包含对 outter 的局部变量 n 的引用,因此当 outter 返回时,n 的值被保留 ( 不会被垃圾回收机制回收 ),持续调用 o1(),将会改变 n 的值。而 o2 的值并不会随着 o1() 被调用而改变,第一次调用 o2 会得到 n==0 的结果,用面向对象的术语来说,就是 o1 和 o2 为不同的 实例,互不干涉。
总的来说,闭包很简单,不是吗?但是,闭包可以带来很多好处,比如我们在 Web 开发中经常用到的:
清单 9. jQuery 中的闭包
1
2
3
4
var con = $("div#con");
setTimeout( function (){
con.css({background:"gray"});
}, 2000);




上边的代码使用了 jQuery 的选择器,找到 id 为 con 的 div 元素,注册计时器,当两秒中之后,将该 div 的背景色设置为灰色。这个代码片段的神奇之处在于,在调用了 setTimeout 函数之后,con 依旧被保持在函数内部,当两秒钟之后,id 为 con 的 div 元素的背景色确实得到了改变。应该注意的是,setTimeout 在调用之后已经返回了,但是 con 没有被释放,这是因为 con 引用了全局作用域里的变量 con。
使用闭包可以使我们的代码更加简洁,关于闭包的更详细论述可以在参考信息中找到。由于闭包的特殊性,在使用闭包时一定要小心,我们再来看一个容易令人困惑的例子:
清单 10. 错误的使用闭包
1
2
3
4
5
6
7
8
9
10
11
12
13
var outter = [];
function clouseTest () {
var array = ["one", "two", "three", "four"];
for ( var i = 0; i < array.length;i++){
var x = {};
        x.no = i;
        x.text = array;
x.invoke =  function (){
print(i);
        }
        outter.push(x);
    }
}




上边的代码片段很简单,将多个这样的 JavaScript 对象存入 outter 数组:
清单 11. 匿名对象
1
2
3
4
5
6
7
{
no : Number,
text : String,
invoke :  function (){
// 打印自己的 no 字段
    }
}




我们来运行这段代码:
清单 12. 错误的结果
1
2
3
4
clouseTest();// 调用这个函数,向 outter 数组中添加对象
for ( var i = 0, len = outter.length; i < len; i++){
    outter.invoke();
}




出乎意料的是,这段代码将打印:
1
2
3
4
4
4
4
4




而不是 1,2,3,4 这样的序列。让我们来看看发生了什么事,每一个内部变量 x 都填写了自己的 no,text,invoke 字段,但是 invoke 却总是打印最后一个 i。原来,我们为 invoke 注册的函数为:
清单 13. 错误的原因
1
2
3
function invoke(){
print(i);
}




每一个 invoke 均是如此,当调用 outter.invoke 时,i 的值才会被去到,由于 i 是闭包中的局部变量,for 循环最后退出时的值为 4,因此调用 outter 中的每个元素都会得到 4。因此,我们需要对这个函数进行一些改造:
清单 14. 正确的使用闭包
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
var outter = [];
function clouseTest2(){
var array = ["one", "two", "three", "four"];
for ( var i = 0; i < array.length;i++){
var x = {};
        x.no = i;
        x.text = array;
x.invoke =  function (no){
return
                function (){
print(no);
            }
        }(i);
        outter.push(x);
    }  
}




通过将函数 柯里化,我们这次为 outter 的每个元素注册的其实是这样的函数:
1
2
3
4
5
6
7
8
//x == 0
x.invoke =  function (){print(0);}
//x == 1
x.invoke =  function (){print(1);}
//x == 2
x.invoke =  function (){print(2);}
//x == 3
x.invoke =  function (){print(3);}




这样,就可以得到正确的结果了。




欢迎光临 电子技术论坛_中国专业的电子工程师学习交流社区-中电网技术论坛 (http://bbs.eccn.com/) Powered by Discuz! 7.0.0