p231 函数式语言的特征之一就是连续运算,运算一个 输入,产生一个输出,输出的结果即是下一个运算的输入。并不需要中间变量 来“寄存”。 冯·诺依曼体系的计算机系统是 基于存储与指令系统的,而并不是基于运算的。 函数式语言强调运算过程 函数式语言中的“函数(function)”除了能被调用之外,还具有一些其它的性质。这 包括: 函数是运算元 在函数内保存数据 函数内的运算对函数外无副作用 在JavaScript的函数中,函数内的私有变量可以被修改,而且当再次“进 入”到该函数内部时,这个被修改的状态仍将持续。 在函数内保持数据的特性被称为“闭包(Closure)”, 为什么“连续求值”会成为函数式语言的 因为函数是值,所以函数可以被作为值来存储到变量,也可声明它的直接 量。例如: //将一个函数直接量赋值给变量func存储 var func =function() { //... } //声明一个(命名的)函数变量 function myFunc() { // ... } 因为函数是值,所以函数直接参与表达式运算。例如: //直接参与布尔运算 if (!myFunc) { // ... } //与字符串等其它类型数据混合运算 value = 'the function context: ' +myFunc; 因为函数是值,所以它也可以作为其它函数的参数传入,或作为结果值传 出——上一小节说这是“高阶函数”的特性,放在本小节中,作为“值的特性” 来解释,也是一样的。例如: //函数作为参数传入 function test(foo) { //函数参与表达式运算,以及作为函数返回值传出 return (foo !== myFunc ?foo :function() { // ... }); } //将test()函数调用的返回值作为新的函数调用 test(myFunc)(); function fun() { } f1=new Function; alert(typeof fun); alert(typeof f1); f2=new fun; alert(typeof f2);//Object alert(f2 instanceof Function); 基本特性呢?这是因为函数式语言是基于对lambda 演算的实现而产生的,其 理论基础就是: (表达式)运算产生结果; 结果(值)用于更进一步的运算。 函数式语言中,“函数”并不是真正的精髓。 真正的精髓在于“运算”,而函数只是封装“运算”的一种手段。 到了这里,我们如果“假设系统的结果只是一个值”,那么“我们必然可 以通过一系列连续的运算来得到这个值”。 JavaScript中逻辑运算的实质——逻辑或运算并非为布尔值而专设。 JavaScript特性:对象的构造、函数与方法的调用等等,本质上是都表达式运算,而非语句。 var obj =new ((obj=='String') ? String : Object ); 这行代码是用一个运算来作为new运算的入口参数——注意new不是语 句的语法关键字,而是运算符。 所以在JavaScript中,我们事实上可以用下面的连续运算来完成上一小节 的示例: var obj =new ( (objType =='String') ? String : (objType =='Array') ? Array : (objType =='Number') ? Number : (objType =='Boolean') ?Boolean : (objType =='RegExp') ? RegExp : Object ); 连续的一组三元表达式的运算结果,成为了new运算符的输入,而new 运算符后面的一对括号“()”,在这里的起到的是强制运算符的作用。 更进一步的说,new运算的结果(构造一个对象实例),被作为赋值运算的 运算元,然后赋给了变量obj。 运算的实质,是值运算. 所有的运算都产生“值类型”的结果值。 运算式语言的特点在于强调“求值运算”。我们也列举了 JavaScript中的表达式运算符,我们看到这些运算符处理的运算元都是“值类 型数据”。由此我们可以认为:JavaScript语言特性中的某个子集,可以作为一 个最小化的运算式语言来使用——事实上我们也看到在InternetExplorer中将 JavaScript的一个子集作为CSS的表达式(expresstion)来使用。 我们也讨论到,如果要在运算式语言中完成全部的逻辑,那么它需要通过 表达式或者运算来填补“被消灭掉的语句”,更确切地来说,它必须有能力实 现“顺序、分支和循环”三种逻辑结构。在JavaScript中,可以通过“连续运 算符(逗号)”和“三元条件表达式”来替代顺序和分支。因此,最后的问题 是:如何在表达式级别实现循环? 我们也在前面给出了答案:使用函数递归来消灭循环语句。 “函数”==“lambda” “我在学习函数式编程的时候,很不喜欢术语lambda,因为我没有真正理解它的意 义。在这个环境里,lambda 是一个函数,那个希腊字母(λ)只是方便书写的数学记法。 每当你听到lambda 时,只要在脑中把它翻译成函数即可。” “lambda 演算(lambda calculus)”其实就是一套用于研究函数定义、函数应用和递归的系统。 从数学上,已经论证过lambda 运算是一个图灵等价的运算系统;从历史 上,我们已经知道函数式语言就是基于lambda 运算而产生的运算范型。所以, 在本质上来讲,函数式语言中的函数这个概念,其实应该是“lambda(函数)”, 而不是在我们现在的通用语言(我是指的象C、Pascal这样的命令式语言)中 讲到的function。 1.5.2参数的非惰性求值 在下面这个例子中,代码的两个输出值将是什么呢? /** *示例1:在参数中使用表达式时的求值规则 */ var i= 100; //输出A alert( i+=20, i*=2, 'value: '+i); //输出B alert( i ); 此处alert接受了3个参数,逗号是参数分隔符,而非,运算符。 javascript中调用时实参的个数是不确定的! 即使函数的定义中只有一个形参! 输出A将显示数值“120”,输出B则将显示数值“240”。 这里就体现了JavaScript的“非惰性求值”的特性——对于函数来说,如 果一个参数是需要用到时,才会完成求值(或取值),那么他就是“惰性求值” 的,反之则是“非惰性求值”。而JavaScript使用“非惰性求值”的很大一部分 原因,在于它的表达式还支持赋值,这也就意味着表达式会产生对系统的副作用。 函数是值,所以函数可以被作为值来存储到变量,也可声明它的直接 量。例如: //将一个函数直接量赋值给变量func存储 var func =function() { //... } //声明一个(命名的)函数变量 function myFunc() { // ... } 因为函数是值,所以函数直接参与表达式运算。例如: //直接参与布尔运算 if (!myFunc) { // ... } //与字符串等其它类型数据混合运算 value = 'the function context: ' +myFunc; 因为函数是值,所以它也可以作为其它函数的参数传入,或作为结果值传 出——上一小节说这是“高阶函数”的特性,放在本小节中,作为“值的特性” 来解释,也是一样的。例如: //函数作为参数传入 function test(foo) { //函数参与表达式运算,以及作为函数返回值传出 return (foo !== myFunc ?foo :function() { // ... }); } //将test()函数调用的返回值作为新的函数调用 test(myFunc)(); JavaScript的函数的对象特性有一个非常特殊的地方:通过new方 式构造的函数对象,其typeof 值仍然为'function'。 caller:调用者 teacher employer 雇主,老板;雇佣者 callee:被调用者 employee 雇员 函数的递归就总是可以写成: void function() { // ...(略) arguments.callee(); }(); function showIt(func) { var name =(!(func instanceof Function) ?'unknow type' : _r_function.test(func.toString()) ? RegExp.$1 : 'anonymous'); alert('-> '+ name); } function enumStack(callback) { var f= arguments.callee; while (f.caller) { callback( f= f.caller ); } } function level_n() { enumStack(showIt); } function level_2() { // ... level_n(); } function test() { level_2(); } test(); 我们看到如下的显示: -> level_n -> level_2 -> test 亦即是enumStack()函数被调用时的栈信息——排除enumStack()函数自身。 接下来我们提一个问题:既然caller是Function的一个成员。那么在栈上 如果出现两次、多次同一个函数,该函数的caller又如何处理呢?——更加严 重的是,如果这个函数是来自几次不同的调用,又怎么办呢? 答案是:JavaScript中并不能识别这种情况,所以导致遍历出错。 f1与f2相互递归调用 函数闭包“在内部保存数据和对外无副作用”这两大特性,在JavaScript中都是通过”函数 闭包(functionclosure)“来实现的。 除了函数闭包之外,在JavaScript中还存在一种特殊的“对象闭包(object closure)”。它是与with语句实现直接相关的一种闭包。 在JavaScript中,一个函数只是一 段静态的代码、脚本文本,因此它是一个代码书写时,以及编译期的、静态的 概念;而闭包则是函数的代码在运行过程中的一个动态环境,是一个运行期的 、 动态的概念。由于引擎对每个函数建立其独立的上下文环境,因此当函数被再 次执行或进入函数体内的代码时,就将会得到闭包内的全部信息。 闭包具有两个特点:第一是闭包作为与函数成对的数据,在函数执行过程 中处于激活(即可访问)状态;第二是闭包在函数运行结束后,保持运行过程 的最终数据状态。因此函数的闭包总的来说决定了两件事:闭包所对应的函数 代码如何访问数据,以及闭包内的数据何时销毁。对于前者来说,涉及作用域 (可见性)的问题;对于后者来说,涉及数据引用的识别。 闭包包括的是函数运行实例的引用、环境(environment,用来查找全局变 量的表)、以及一个由所有upvalue引用组成的数组,每个闭包可以保有自己的 upvalue值。 在运行过程中,子函数闭包(闭包2~n)可以访问upvalue; 同一个函数中的所有子函数(闭包2~n),访问一份相同值的upvalue。 闭包是对应于运行期的函数实例的,而不是对应函数(代码块)的。 由于闭包对应于函数实例,那么我们只需要分析哪些情况下产生实例, 就可以清楚地知道运行的闭包环境。 返回同一个的函数实例,只是比较麻烦一点: //例7:有函数实例产生 var aFunc_3 = function (){ var MyFunc = function () { // ... } //返回一个函数到aFunc_3 return function() { return MyFunc; } }() 4.6对象属性与变量没有本质差异 4.6.1全局变量其实是“全局对象(globalobject)”的属性 4.6.2局部变量其实是“调用对象(call object)”的属性 “全局对象(global object)”与“调用对象(callobject)”其实是下图中的ScriptObject结构: “调用对象”的局部变量维护规则 规则一:在函数开始执行时,varDecls中所有值将被置为undefined。因此 我们无论如何访问函数,变量初始值总是undefined。 //变量初始化 function myFunc() { alert(i); var i= 100; } //输出值总是undefined myFunc(); myFunc(); 由于varDecls总是在语法分析阶段就已经创建好了,因此在myFunc()内 部,即使是在“vari”这个声明之前访问该变量,也不会有语法错误,而是访 问到位于ScriptObject结构中的varDecls中该变量的初值:undefined。由于 varDecls 总是 在执 行前 被初 始化 ,因 此第 二次调 用myFunc()时, 值仍 是 undefined。 规则二:函数执行结束并退出时,varDecls不被重置。正因为varDecls不 被重置,所以JavaScript中的函数能够提供“在函数内保存数据”这种函数式 语言特性。需要说明的是,该规则与规则一并不冲突。上例中第二次调用 myFunc()时,没有显示“在函数内所保存的数据(该例中是数值100)”的原因 是:第二次执行函数时,在进入函数前varDecls被再次初始化了。 规则三:函数内数据持续(即“在函数内保存数据”)的生存周期,取决 于该函数实例是否存在活动的引用,当没有活动引用时,由内存回收机制销毁 函数和“调用对象(ScriptObject)”。 导致闭包持有外部变量的原因是:当foo 是一个引用,且被闭包内的某个变量赋 值、传递或收集(例如数组的push()操作)。 引用正是“赋值”这样的运算带来的。 函数返回值是否有引用效果呢?例如: function myFunc() { function foo() { //... } return foo; } 这个例子其实可以用于证明函数返回(return子句)并不导致引用。因为如果 我们这样来使用myFunc()函数: myFunc()(); 那么函数foo()在返回后立即被执行了,然后就会释放。因此return子句并不是 导致引用的必然条件。相反,如果我们下面这样使用: func =myFunc(); // <--在这里产生一个引用 func(); 那么将产生引用,而后myFunc()与foo()的生存周期就将依赖于变量func 的生存周期了。 p294 11 var checker; 2 3 function myFunc() { 4 if (checker) { 5 checker(); 6 } 7 8 alert('do myFunc: '+str); 9 var str ='test.'; 10 11 if (!checker) { 12 checker =function() { 13 alert('do Check:' +str); 14 } 15 } 16 17 return arguments.callee; 18 } 19 20 //连续执行两次myFunc() 21 myFunc()(); 在这个例子中,myFunc函数将执行两次。在第21行的第一次执行结束时 , 在第17行代码处将“函数实例自身的一个引用”(callee)作为结果值返回; 接下来,在21行处会遇到第二个函数调用运算符,于是myFunc函数就被第 二次调用了。由于第二次执行的只是一个引用,因此这个示例中myFunc只创 建过一个函数实例。 运行这个例子将输出三个信息: do myFunc: undefined do Check: test. do myFunc: undefined 其中第一、三个信息都由第8行代码输出。输出undefined的原因在于: 函数被调用时,函数内的局部变量被声明并被初始化为undefined; 局部变量表被保存在该函数闭包中的varDecls域中。 而checker是在第一次调用中被赋的值,它是一个局部函数引用;并且在 它的闭包中,还引用了upvalue变量str。由于这个变量是第一次调用的myFunc 函数的闭包中的,因此在第一次myFunc调用结束后,闭包并没有被销毁—— 闭包中存在被其它对象引用的变量/数据。 所以,myFunc第二次调用、并以函数形式调用全局变量checker时 ,checker 实际上是输出了“第一次myFunc调用过程中形成的闭包”中的str——显然, 这个值是"test."。这就是第二行输出信息的由来。 这个例子传达出的信息是: JavaScript中函数实例可能拥有多个闭包; JavaScript中函数实例与闭包的生存周期是分别管理的; JavaScript中函数被调用时总是初始化一个闭包;而上次调用中的闭包是否销 毁,取决于该闭包中是否有被(其它闭包)引用的变量/数据。 我们注意到这里提及“函数实例与闭包的生存周期是分别管理的”。因此 一个函数实例(以及其可能的多个引用)的生存周期,与闭包是没有直接关系 的。换而言之,会存在函数变量没有持有闭包的情况。这是因为: 闭包创建自函数执行开始之时;接下来, 在执行中闭包没有被其它对象引用;接下来, 在函数执行结束时闭包被销毁了。 而这时函数实例及其引用(例如变量、对象成员、数组元素等)都还存在,只 是没有与之对应的闭包了。所以第1.6.3小节的标题是“(在被调用时,)每个 函数实例至少拥有一个闭包”,以强调“在调用过程中”这样的事实。 var obj =new Object(); var events = {m1: "clicked", m2: "changed"}; for (ein events) { obj[e]= function(){ alert(events[e]); }; } //显示false, 表明是不同的函数实例 alert( obj.m1 === obj.m2 ); //方法m1()与m2()都输出相同值 //其原因,在于两个方法都访问全局闭包中的同一个upvalue值e obj.m1(); 对obj[e]赋值时,function后面的内容仅仅相当于文本,因此不会对其中的变量e进行替换. 按照这段代码的本意,应该是每个函数实例输出不同值。对这个问题的处 理方法之一,是再添加一个外层函数,利用“在函数内保存数据”的特性来为 内部函数保存一个可访问的upvalue: var obj =new Object(); var events = {m1: "clicked", m2: "changed"}; for (ein events) { obj[e]= function(aValue){ //闭包lv1 return function() { //闭包lv2 alert(events[aValue]); } }(e); } /*或用如下代码,在闭包内通过局部变量保存数据 for (ein events) { function() { //闭包lv1 var aValue = e; obj[e]= function() { //闭包lv2 alert(events[aValue]); } }(); } */ //下面将输出不同的值 obj.m1(); obj.m2(); 由于闭包lv2 引用 参数或者新的局部变量引用了外部变量,并且引用被永久的保存下来了. var obj =new Object(); var events = {m1: "clicked", m2: "changed"}; for (ein events) { (obj[e] =function() { // arguments.callee指向匿名函数自身 alert(events[arguments.callee.aValue]); } ).aValue =e; } //下面将输出不同的值 obj.m1(); obj.m2(); //示例4:输出值'test' function foo(str) { var str; alert(str); } foo('test'); 这个例子中,变量str显然没有作为局部变量(varDecls)处理,而是函数入口处 的形式参数,所以才输出值'test'。但接下来这个例子却又有点不同: //示例5:输出值'member' function foo(str) { var str ='member'; alert(str); } foo('test'); 这个例子表明:局部变量(varDecls)优先级高于函数形式参数。这又与上一个 例子是矛盾的。比较来看,我们可以直观地认为: 当形式参数名与未赋值的局部变量名重复时, 取形式参数值; 当形式参数与有值的局部变量名重复时,取局部变量值。 我并不确知这样实现的根源,但是这显然带来了一种语义上的合理性: //示例6:输出值'test' function foo(str) { var str =str; alert(str); } foo('test'); 如果仅认为“局部变量(varDecls)优先级高于函数形式参数”,那么该代码 “varstr=str”赋值运算的左右两侧都应该是局部变量str;而局部变量声明后 初值为undefined,所以该行代码的结果“(看起来)应该”是undefined。 然而这可能与我们的目的不一致 ① ,我们的目的,可能是将入口参数str 赋值局部变量str。那么,就只能是上述两条假设都同时能立,才能使接下来 这行“alert(str)”既访问局部变量,又能输出有效值。这一语义上的合理性是: 赋值运算符的左侧是局部变量——这是由var关键字决定的,而右侧是形式参 数——这是因为此时局部变量str“尚未赋值”。 这样一来,赋值语句就成立了,接下来str变成有值的局部变量,而alert() 也就会访问它了。不过,这里的“有或没有赋值”是指显式的赋值操作。 使用Function()构造器创建的函数。它与函 数直接量声明、匿名函数不同,它在任意位置创建的实例,都处于全局闭包中 。 亦即是说,Function()的实例的upvalue总是指向全局闭包。 Function()构造器传入的参数全部都是字符串,因此 不必要与函数局部变量建立引用。 var obj =new Object(); var events = {m1: "clicked", m2: "changed"}; for (ein events) { obj[e]= new Function('alert(events["' +e +'"])'); } //下面将输出不同的值 obj.m1(); obj.m2(); 这里Function后面的内容当作普通字符串处理,会对e进行处理 正是由于每一个函数实例都有一份ScriptObject的副本, 因此“不同的闭包访问到的私有变量不一致”。 “foo()函数中不能访问obj.value”的问题 。 这种做法是:将函数foo()添加为对象obj的方法。例如: 1 (部分代码参见上例) 2 with (obj){ //<--对象闭包 3 obj.foo =function() { //<--匿名函数的闭包 4 value *= 2; // <--依赖于函数闭包所在的当前闭包链 5 } 6 obj.foo(); 7 } 这样一来,obj.value值将变成400,而全局的value值不变。这是因为匿名函 数的闭包也是如同对象闭包一样,动态地添加到当前闭包链的顶端——而代码 执行到第3行时,“当前闭包”正好是with所打开的obj对象闭包。 由于这里的“添加到闭包链顶端”的行为只是(引擎针对于)匿名函数自 身的行为,所以它与上述代码的赋值操作无关。也就是说,即使该匿名函数没 有添加为对象obj的方法——而仅是即用即声明,那么它所操作的仍然是对象 闭包中的value值。例如: 1 (部分代码参见上例) 2 with (obj){ //<--对象闭包 3 void function() { //<--函数闭包 4 value *= 2; 5 }(); 6 } 更细致地追究这个问题,其实匿名函数“添加到闭包链顶端”也与“执行” 无关,而是在它作为一个直接量被“创建”时,由引擎动态添加在闭包链上的 。 也就是说,匿名函数直接量的创建位置决定了它所在闭包链的位置。 |
|
来自: quasiceo > 《javascript》