分享

JavaScript语言精髓与编程实践20071115 笔记3 第四章JavaScript的函数式语言特性

 quasiceo 2013-11-09
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 }
更细致地追究这个问题,其实匿名函数“添加到闭包链顶端”也与“执行”
无关,而是在它作为一个直接量被“创建”时,由引擎动态添加在闭包链上的 。
也就是说,匿名函数直接量的创建位置决定了它所在闭包链的位置。

    本站是提供个人知识管理的网络存储空间,所有内容均由用户发布,不代表本站观点。请注意甄别内容中的联系方式、诱导购买等信息,谨防诈骗。如发现有害或侵权内容,请点击一键举报。
    转藏 分享 献花(0

    0条评论

    发表

    请遵守用户 评论公约

    类似文章 更多