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的表达式之间的任何操作。 |
|
来自: quasiceo > 《javascript》