分享

JavaScript语言精髓与编程实践20071115 笔记5 第七章一般性的动态函数式语言技巧

 quasiceo 2013-11-10
p568
消除代码的全局变量名占用
说明:
解决函外执行的代码(行)可能占用大量全局变量名的问题。
示例背景:
一段代码运行在全局闭包中时,会占用一些全局标识符名。当后续代码使
用到相同标识符名时,可能导致不可预测的逻辑错误。因此可以将这些代码“包
装”在一个匿名函数闭包中,使用“声明即运行”的技术,既完成代码执行,
又将代码的副作用限制在函数内部的有限范围之中。
示例代码:
1 /**
2 *1.基本示例
3 */
4 void function() {
5 // ...
6 }();
7
8 /**
9 *2.带参数的示例
10 */
11 void function(x, y, z) {
12 //...
13 13 }(10, 20, 300);
14
15 /**
16 *3.执行后不会被释放的示例
17 */
18 void function() {
19 //将一个内部的(引用类型的)变量赋给全局变量或其成员
20 String.prototype.aMethod =function() {
21 // ...
22 }
23 }();

“声明即调用的匿名函数”。它主
要解决的问题是:用闭包隔离代码并执行。
如同模式中的单例一样,这种函数只被创建一个函数实例,并立即执行。
如果可能,它也将在执行后立即被释放和回收。由于函数本身具有“声明逻辑
相关的连续代码”的作用,因此你也可以用这种技巧来(结构化地)组织代码
并为它(这个匿名函数)添加有意义的注释。这比将代码行分散在全局闭包的
各处要好。
如果在这种匿名函数内部,将一些函数内引用赋值给了外部的、全局的变
量引用,例如某些内部对象、DOM对象的成员,那么这个匿名函数在执行后
并不会被释放(示例3)。这会导致匿名函数的生存周期依赖于该外部变量——
被重写或删除。因此如果的确有这样的需求,应当将更多的外部操作集中在同
一个匿名函数中,并减少该函数中无关的逻辑代码,例如在同一个“声明即调
用的匿名函数”内为String.prototype扩展多个原型方法。

只使用一次的构造器函数。
示例背景:
一个构造器函数在使用一次之后,标识符即被重写。但可以保证“<实例
>.constructor”正确的指向构造器,且允许使用new运算来创建更多的实例。
示例代码:
1 /**
2 *1.基本示例:使用标识符重写
3 */
4 Instance =function() { ... }
5 Instance.prototype ={
6 //直接量声明
7 ...,
8 //维护原型:置构造器属性
9 constractor: Instance
10 }
11 Instance =new Instance();
12 Instance2 =new Instance.constructor();
13
14 /**
15 *2.使用匿名函数
16 */
17 Instance =new function() {
18 // orarguments.callee.prototype
19 var proto =this.constructor.prototype;
20
21 //维护原型proto
22 // ...
23 }
24
25 /**
26 *3.在示例2中使用参数
27 */
28 Instance =new function(x,y) {
29 //...
30 }(1,2);

示例1使用了重写标识符的方法来解释了这一技巧的主要目的,也同时说
明了如何有效地在重写原型时维护constructor属性。当对象系统正确维护
constructor属性时,我们就总是可以使用“new<实例>.constructor()”来创建
更多的实例,而不必依赖于构造器函数的名字。
匿名函数同样具有“一次性”的特点,因此示例2做了一个相同功能的演
示。在示例2中,依赖于:
构造器函数的原型的constructor属性缺省指向自身
实例(示例2中的this 引用)从原型中继承constructor属性
这两个性质来找到匿名的构造器函数,并操作它的原型引用。此外也可以依赖
参数对象的属性arguments.callee来找到该构造器,这是在匿名函数内部访问
函数自身的标准方法。与示例1相同的是,如果示例2不是修改而是重写原型
的话,那么它也需要维护好原型的constructor属性。
示例3说明在new中使用参数表的特殊性。这与函数调用不同的是,我们
不必为这里的匿名函数后有参数表,而在使用一对括号来表明函数调用,例如 :
new (<匿名函数>(1, 2));
这样的代码事实上是可能会导致语法异常的。因为这个参数表实际上是new运
算符的一部分,而非构造器函数的一部分。因此它的语法含义事实上是:
new (<匿名函数>)(1,2);
关于这部分的语义分析细节,请参见“1.5.1.1 使用构造器创建对象实例”。

用一个对象来充当函数调用界面上,或针对特殊成员的识别器。利用了对
象实例在全局唯一性的特性(包括直接量空白对象“{}!=={}”)。//两个不同的直接对象
示例背景:
我们在一个函数闭包内声明的局部变量,是不会被外部代码直接访问到
的,但能在该闭包中、更内层的子函数闭包中通过upvalue访问。因此我们可
以用该局部变量作为一个识别器,来辨识函数是在内部亦或外部调用。
除了在函数调用界面上的识别外,我们也可以利用这种技巧来弥补in 运
算不能有效甑别对象内部成员的问题。
示例代码:
1 /**
2 *1.用作函数调用界面上的识别器
3 */
4 var myFunc = function() {
5 var handle = {};
6 function _myFunc(x,y,z) {
7 if (arguments[0] === handle)
8 //是内部调用...
9 else
10 //是外部调用...
11 }
12
13 //内部调用测试(可传入更多参数)
14 _myFunc(handle, 1, 2,3);
15
16 return _myFunc;
17 }
18 //外部调用测试(不能访问变量handle)
19 myFunc(1, 2, 3);
20
21 /**
22 *2.用作对象成员识别器
23 */
24 var obj =function() {
25 var yes ={};
26 var tbl ={v1: yes, v2: yes, v3: yes };
27 return {
28 query: function(id) {return tbl[id] === yes }
29 }
30 }();
31 //测试:查询指定id是否存在
32 obj.query('v1');
33 obj.query('constructor');

示例1使用一个内/外部调用的识别器handle来识别函数调
用的上下文环境。
另 一 个 识 别 函 数 调 用 的 上 下 文 环 境 的 方 法 , 是 直 接 检 测
arguments.callee.caller,以判定是否来自某个函数调用。

本技巧在检测handle与yes等识别器变量时,必须使用“===”运算符。





























一个函数被设计为既可以用new运算来产生对象实例,又可以作为普
通函数调用,或者可以作为对象方法调用,那么如何识别当前究竟在哪种环境
下被调用呢?
示例代码:
1 /**
2 *1.识别当前函数是否使用new运算来构建实例
3 */
4 function MyObject() {
5 if (this instanceof arguments.callee) {
6 //是使用new运算构建实例
7 }
8 else {
9 //是普通函数调用
10 }
11 }
12 var obj =new MyObject();
13
14 /**
15 *2.识别当前函数是否使用作为方法调用
16 */
17 function foo() {
18 if (this === window) {
19 //是普通函数调用
20 }
21 else {
22 //是方法调用
23 }
24 }
25 obj.aMethod = foo;
26 obj.aMethod();
示例说明:
当使用new运算时,构建过程运行在函数自身的闭包中,而且新构建的对
象总是构造器的一个实例。因此,示例1使用arguments.callee来找到这个构
造器函数自身,并检测this 对象是否是它的实例,从而可以检测是否运行在new
运算的实例构建过程中。
但这种方法存在一个(唯一的)问题。如果用户代码试图将构造器函数作
为它的实例的一个方法,那么示例1就会误判。例如:
obj.aMethod = MyObject;
obj.aMethod(); // <--这个方法调用会被识别为new()构建过程


除了示例1所示的方法之外,用户代码也可以用一种简洁的方法来识别
new运算中的构造器调用:
5 if (this.constructor === arguments.callee) {
6 ...
这种方法依赖于构建器原型的constructor属性,因此要求MyObject的原型或
该原型的constructor成员未被重写过。这种方法虽然有些限制,但仍然是常常
用到的。
示例2利用了一条简单的规则:在普通函数调用中,this 引用指向宿主对
象——在浏览器环境中是window,如果是在其它环境中,请参考相关文档。
当用户代码使用aFunction.apply或aFunction.call来调用函数自身时,可
能会显式地指定this 引用为null、undefined等无意义的值。这种情况下,
JavaScript引擎内部会忽略该值,并以宿主对象作为this 引用传入函数。



对象其实就是一个特殊的关联数组

/**
2 *示例1:用对象模拟数组并应用(某些)数组方法
3 */
4 var obj ={};
5 [].push.apply(obj, [1,2,3]);
6 alert(obj.length);
7
8 /**
9 *示例2:检测hasOwnProperty是否被重写
10 *(限用于JScript)
11 */
12 var obj ={
13 hasOwnProperty:function() {} //重写
14 }
15 //显示true,表明该属性被重写
16 alert( {}.hasOwnProperty.call(obj,'hasOwnProperty') );

JavaScript是基于毫秒值来表达时间的。更具体的说,一个JavaScript的日
期对象的数值含义其实是“1970 年1 月1 日午夜间全球标准时间以来逝去
的毫秒数”。基于这种认识,我们可以非常方便地做一些日期相关的运算。
示例代码:
1 /**
2 *三天前是星期几?
3 */
4 alert(new Date(new Date() - 3*24*60*60*1000).getDay());
5
6 /**
7 *两个日期值之间相差的小时数/天数/...
8 */
9 var d1= new Date(2007, 1,1, 12, 1,30, 300);
10 var d2= new Date(2006, 10, 15);
11 //分别显示1884(小时),78(天)
12 alert(parseInt((d1-d2)/1000/60/60));
13 alert(parseInt((d1-d2)/1000/60/60/24));
示例说明:
日期对象的奥秘在它的valueOf方法(而不是其它那些复杂的getXXX或
setXXX方法)上。日期对象可以直接使用数学运算符(加、减、乘、除等),
并不是JavaScript引擎为该对象进行了什么特别的设计,而是因为valueOf()方
法返回了一个数值——即上面所说的“JavaScript的日期对象的数值含义”。

利用钩子函数来扩展功能
说明:
用简单的方法来实现钩子函数,以及以相同的技巧来扩展原型方法或原始
函数。
示例背景:
钩子函数,是指模拟原始函数的功能与接口,在原始函数执行前、执行后
增加一些特殊功能或检测代码的技术。在更接近系统层次上的技术中,它被称
为HOOK,而在更面向应用的高级开发,一般使用子类继承或切面技术来实现
相同的功能。
本技巧通过动态特性中的“重写”,来实现钩子以及脱钩(unhook)技术。
示例代码:
25 /**
26 *1.基本示例
27 */
28 function myFunc() {... }
29
30 myFunc = function(foo) {
31 return function() {
32 //钩子的功能代码
33 // ...
34 return foo.apply(this, arguments);
35 }
36 }(myFunc);
37
38 /**
39 *2.高级示例:对原型方法进行扩展且不影响原有功能
40 */
41 Number.prototype.toString = function(foo){
42 return function(radix, length) {
43 var result = foo.apply(this, arguments).toUpperCase();
44 //钩子的功能代码
45 ...
46 }
47 }(Number.prototype.toString);
48
49 /**
50 *3.高级示例:脱钩(unhook), 以及只执行一次的钩子
51 */
52 myFunc = function(foo) {
53 return function() {
54 //脱钩(unhook)
55 myFunc = foo;
56
57 //钩子的功能代码
58 // ...
59 return foo.apply(this, arguments);
60 }
61 }(myFunc);
示例说明:
这个技巧充分考虑了标识符重写时赋值运算符的优先级,以及重写对引用
计数的影响。对于下面的表达式模式来说:
myFunc = function(foo) {
return function() {... }
}(myFunc);
赋值表达式右边的一个函数调用运算,该调用的入口参数表获得了myFunc的
一个引用并暂存为形式参数foo。在完成函数调用运算之后,发生赋值运算并
产生重写效果,这时myFunc的引用因为标识符重写而减一。然而由于形式参
数foo 持有了myFunc的一个引用,因此尽管myFunc标识符被重写,但该函
数实例却并不会被销毁。所以,在内层的匿名函数——也就是钩子函数中,可
以通过upvalue该问foo 参数,以调用原始的代码。
在这个过程中,关键的地方在于:
赋值运算符的优先级非常低(仅高于连续运算符“,”逗号与语句分隔符“;”
分号),所以它两边的表达式总是被更优先的运算。因此,标识符myFunc能
在被重写之前,向内部匿名函数传入一个引用;
函数闭包包括该函数通过形式参数建立的数据或引用;
函数(包括匿名函数)在执行后并不销毁也不重置闭包内数据,它的生存周期
取决于函数实例是否仍外引用。
上例的1~3所示的模式中,该匿名函数有一个内嵌的匿名函数,被作为
返回值由全局标识符(myFunc)或对象成员(Number.prototype.toString)持有
了引用,因此整个闭包总是不被销毁。
在示例3中,通过unhook过程可以清除外部标识符对这个闭包的引用。
在示例代码的行31中:
myFunc = foo;
同样是通过重写,来清除myFunc标识符对“当前函数闭包”的引用。这样,
在函数执行完之后,不但完成了“脱钩”、“只执行一次”的行为,也完成了对
闭包引用计数的维护——如果没有别的引用,该函数实例会被引擎自动回收。

只要字符串能被转换为字符串形式——
无论是JavaScript引擎调用toString()方法,还COM/ActiveX内部通过变体类
型转换,都可在表达式运算过程中隐式地转换为字符串(参见“1.7.3.1运算导
致的类型转换”)。
// aStr +='';  
aStr =aStr + '';空字符串



闭包(closure)
闭包包括相应函数原型的引用、环境(environment,用来查找全局变量的表)的引用以及一个由
所有upvalue引用组成的数组,每个闭包可以保有自己的upvalue值。函数是编译期概念,是
静态的,而闭包是运行期概念,是动态的。

lambda 运算
“λ”(lambda、兰姆达)是希腊字母,在计算系统中,它表示一种运算型式。具体来说,λ代
表一个函数,因此lambda运算不妨直接理解为函数运算。在运算表达式中,一个运算符其实
是一个函数的指代,因此表达式运算的本质,就变成了函数运算。

null
null值指出一个变量中没有包含有效的数据。产生null的原因是:
对一个变量显式地赋值为null。
包含null的表达式之间的任何操作。



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

    0条评论

    发表

    请遵守用户 评论公约

    类似文章 更多