第4章-函数(2)
递归 Recursion
递归函数就是会直接或间接地调用自身的一种函数
1 var walk_the_DOM = function(node,func){ 2 func(node); 3 node = node.firstChild; 4 while(node){ 5 walk_the_DOM(node,func); 6 node = node.nextSibling; 7 } 8 }; 9 10 //定义getElementsByAttributes函数,它以一个属性名字符串和一个可选的匹配值作为参数 11 //它调用walk_the_DOM并传递一个用来查找节点属性名的函数作为参数,匹配的节点会累加到一个数组中 12 13 var getElementsByAttributes = function(att,value){ 14 var result = []; 15 walk_the_DOM(document.body,function(node){ 16 var actual = node.nodeType === 1 && node.getAttribute(att); 17 if(typeof actual === 'string' && (actual === value || typeof value !== 'string')){ 18 result.push(node); 19 } 20 }); 21 return result; 22 }
作用域 Scope
在编程语言中,作用域控制着变量的与参数的可见性与生命周期
1 var foo = function(){ 2 var a = 3,b = 5; 3 var bar = function(){ 4 var b = 7; c = 11; 5 //此时 a=3,b=7,c=11 6 7 a += b + c; 8 //此时 a=21,b=7,c=11 9 }; 10 11 //此时 a=3,b=5,c还没有定义 12 13 bar(); 14 //此时 a=21,b=5 15 } 16
JavaScript确实有函数作用域,意味着定义在函数中的参数和变量在外部式不可见的,而在一个函数内部任何位置定义的变量,在该函数内部任何地方都是可见的。
闭包 Closure
作用域的好处是内部函数可以访问定义它们的外部函数和参数和变量(除了this和arguments)
一个更有趣的情形是内部函数拥有比它的外部函数更长的生命周期
和以前以对象字面量的形式去初始化myObject的方式不同,这次通过调用一个函数的形式去初始化myObject,该函数会返回一个对象字面量。函数里定义了一个value变量,该变量对increment和getValue方法总是可用的,但函数的作用域使得它对其他的程序来说是不可见的
1 var myObject = (function(){ 2 var value = 0; 3 return { 4 increment: function(){ 5 value += typeof inc === 'number' ? inc : 1; 6 }, 7 getValue: function(){ 8 return value; 9 } 10 }; 11 }());
我们并没有把一个函数复制给myObject。我们是把调用该函数狗返回的结果赋值给它。主语最后一行的()。该函数返回一个包含两个方法的对象,并且这些方法继续享有访问value的特权。
本章之前的Quo构造器产生一个带有status属性和get_status方法的对象,但那看起来并不是十分有趣,为什么要用一个getter方法去访问你本可以直接访问到的那个属性,如果status是私有属性,它才更有意义的。
1 var quo = function(status){ 2 return { 3 get_status: function(){ 4 return status; 5 } 6 }; 7 }; 8 9 var myQuo = quo('amazed'); 10 console.log(myQuo.get_status());
即使quo已经返回了,但get_status方法任然享有访问quo对象的status属性的特权。get_status方法并不是访问改参数的一个副本,它访问的就是该参数本身。
1 //定义一个函数,它设置一个DOM节点为黄色,然后把它渐变为白色 2 3 var fade = function(node){ 4 var level = 1; 5 var step = function(){ 6 var hex = level.toString(16); 7 node.style.backgroundColor = '#FFFF' + hex + hex; 8 if(level<15){ 9 level += 1; 10 setTimeout(step,100); 11 } 12 }; 13 setTime(step,100); 14 }; 15 fade(document.body);
为了避免下面的问题,理解内部函数能访问外部函数的实际变量而无需复制是很重要的:
1 //糟糕的例子 2 3 var add_the_handlers = function (nodes){ 4 var i; 5 for(i=0;i<nodes.length;i++){ 6 nodes[i].onclick = function(e){ 7 alert(i); 8 }; 9 } 10 }; 11 12 //结束糟糕的例子
add_the_handlers函数的本意是想传递给每个事件处理器一个唯一的(i)值。但它未能达到目的,因为事件处理器函数绑定了变量i本身,而不是函数在构造时的变量i的值。
1 //改良后的例子 2 3 var add_the_handlers = function (nodes){ 4 var helper = function(i){ 5 return function(e){ 6 alert(i); 7 } 8 }; 9 var i; 10 for(i=0;i<nodes.length;i++){ 11 nodes[i].onclick = helper(i); 12 } 13 };
避免在循环中创建函数,它可能只会带来无畏的计算,还会引起混淆,正如上面那个糟糕的例子。我们可以先在循环之外创建一个辅助函数,让这个辅助函数在返回一个绑定了当前i的值的函数,这样就不会导致混淆。
模块 Module
我们可以使用函数和闭包来构造模块,模块是一个提供接口却隐藏状态与实现的函数或对象。通过使用函数产生模块,我们几乎可以完全摒弃全局变量的使用。
1 String.method('deentityify',function(){ 2 //字符实体表,它映射字符实体的名字到对应的字符 3 //这个对象最好保存在闭包中,保存在全局变量中会有很多问题,保存在函数内部,但是会带来运行时的损耗 4 5 var entity = { 6 quot:'"', 7 lt: '<', 8 gt: '>' 9 }; 10 11 //返回deentityify方法 12 13 return function(){ 14 return this.replace(/&([^&;]+);/g 15 function (a,b){ 16 var r = entity[b]; 17 return typeof r === 'string' ? r : a; 18 } 19 ); 20 }; 21 }());
请注意最后一行,我们用()运算法立刻调用我们刚刚构造出来的函数。这个调用所创建并返回的函数才是deentityify方法。
模块模式利用了函数作用域和闭包来创建被绑定对象与私有成员的关联,在这个例子中,只有deentityify方法有权访问字符实体表这个数据对象。
模块模式的一般形式是:一个定义了私有变量和函数的函数;利用闭包创建可以访问私有变量和函数的特权函数;最后返回这个特权函数,或者把他们保存到一个可访问到的地方
使用模块模式就可以摒弃全局变量的使用,它促进了信息隐藏和其他优秀的设计实践。对于应用程序的封装,或者构造其他单例对象,模块模式非常有效
模块模式也可以用来产生安全的对象。假定我们想要构造一个用来产生序列号的对象:
1 var serial_maker = function(){ 2 var perfix = ''; 3 var seq = 0; 4 return { 5 set_prefix:function(p){ 6 prefix = String(p); 7 }, 8 set_seq:function(s){ 9 seq = s; 10 }, 11 gensym:function(){ 12 var result = prefix + seq; 13 seq += 1; 14 return result; 15 } 16 }; 17 }; 18 19 var seqer = serial_maker(); 20 seqer.set_prefix('Q'); 21 seqer.set_seq(1000); 22 var unique = seqer.gensym(); //"Q1000" 23 24 seqer.set_prefix = function(){ 25 prefix = 'QQQ'; // Uncaught ReferenceError: prefix is not definedd 26 }
除非调用对应的方法,否则没法改变prefix或seq的值,seqer对象是可变的,所以它的方法可能会被替换掉,但替换后的方法依然不能访问私有成员。
柯里化(局部套用) Curry
函数也是值,从而我们可以用有趣的方式去操作函数。柯里化允许我们把函数与传递给它的参数相结合,产生出一个新的函数。
1 Function.method('curry',function(){ 2 //arguments数组并非一个真正的数组,所以它并没有concat方法,要解决这个问题就必须在两个arguments数组上都应用数组的slice方法。 3 var slice = Arry.prototype.slice, 4 args = slice.apply(arguments), 5 that = this; 6 return function(){ 7 return that.apply(null,args.concat(slice.apply(arguments))); 8 }; 9 }); 10 11 var add1 = add.curry(1); 12 console.log(add1(6)); //7
记忆 Memoization
函数可以将先前的操作的结果记录在某个对象里,从而避免无谓的重复运算。这种优化被称为记忆
我们想要一个递归函数来计算Fibonacci数列。一个Fibonacci数字是之前两个Fiboacci数字之和。最前面的两个数字是0和1。
1 var fibonacci = function(n){ 2 return n < 2 ? n : fibonacci(n-1) + fibonacci(n-2); 3 }; 4 5 for(var i=0;i<=10;i++){ 6 document.writeln('//' + i + ': ' + fibonacci(i)); 7 } 8 9 //0: 0 10 //1: 1 11 //2: 1 12 //3: 2 13 //4: 3 14 //5: 5 15 //6: 8 16 //7: 13 17 //8: 21 18 //9: 34 19 //10: 55
这个程序做了很多无谓的工作,fibonacci函数被调用了453次
如果我们让该函数具备记忆功能,就可以显著减少运算量。我们在一个名为memo的数组里保存我们的存储结果,存储结果可以隐藏在闭包中,当函数被调用时,这个函数首先检查结果是否存在,如果存在就立即返回这个结果。
1 var fibonacci = function(){ 2 var memo = [0,1]; 3 var fib = function(n){ 4 var result = memo[n]; 5 if(typeof result !== 'number'){ 6 result = fib(n-1) + fib(n-2); 7 memo[n] = result; 8 } 9 return result; 10 }; 11 return fib; 12 }();
这样这个函数只被调用了29次。
我们可以把这种技术推而广之,编写一个函数来帮助我们构造记忆功能的函数
1 var memoizer = function(memo,formula){ 2 var recur = function(n){ 3 var result = memo[n]; 4 if(typeof result !== 'number'){ 5 result = formula(recur,n); 6 memo[n] = result; 7 } 8 return result; 9 }; 10 return recur; 11 };
现在我们可以使用memoizer函数来定义fibonacci函数
1 var fibonacci = memoizer([0,1],function(recur,n){ 2 return recur(n-1) + recur(n-2); 3 });
要产生一个可记忆的阶乘函数:
1 var factorial = memeoizer([1,1],function (recur,n){ 2 return n * recur(n-1); 3 });