├── README.md ├── 0-学习Javascript闭包(Closure).md ├── 11-执行上下文(Execution Contexts).md ├── 9-根本没有“JSON对象”这回事!.md ├── 8-S.O.L.I.D五大原则之里氏替换原则LSP.md ├── 3-全面解析Module模式.md ├── 5-强大的原型和原型链.md ├── 4-立即调用的函数表达式.md ├── 14-作用域链(Scope Chain).md ├── 7-S.O.L.I.D五大原则之开闭原则OCP.md ├── 12-变量对象(Variable Object).md ├── 13-This Yes, this!.md ├── 6-S.O.L.I.D五大原则之单一职责SRP.md ├── 10-JavaScript核心(晋级高手必读篇).md ├── 2-揭秘命名函数表达式.md └── 1-编写高质量JavaScript代码的基本要点.md /README.md: -------------------------------------------------------------------------------- 1 | ## Deep-Understanding-of-JavaScript-Series 2 | 3 | > 深入理解JavaScript系列教程,多搜集于网络,如有侵权,请联系本人,或者留言。 4 | 5 | > Email: -------------------------------------------------------------------------------- /0-学习Javascript闭包(Closure).md: -------------------------------------------------------------------------------- 1 | ## 学习Javascript闭包(Closure) 2 | 3 | > 作者: 阮一峰 4 | > 日期: 2009年8月30日 5 | > 闭包(closure)是Javascript语言的一个难点,也是它的特色,很多高级应用都要依靠闭包实现。 6 | > 下面就是我的学习笔记,对于Javascript初学者应该是很有用的。 7 | 8 | ## 一、变量的作用域 9 | 10 | - 要理解闭包,首先必须理解Javascript特殊的变量作用域。 11 | - 变量的作用域无非就是两种:全局变量和局部变量。 12 | - Javascript语言的特殊之处,就在于函数内部可以直接读取全局变量。 13 | 14 | var n=999; 15 | function f1(){ 16 | alert(n); 17 | } 18 | f1(); // 999  19 | 20 | > 另一方面,在函数外部自然无法读取函数内的局部变量。  21 | 22 | function f1(){ 23 |     var n=999; 24 |   } 25 |   alert(n); // error 26 | 27 | ## 二、如何从外部读取局部变量? 28 | 29 | > 出于种种原因,我们有时候需要得到函数内的局部变量。但是,前面已经说过了,正常情况下,这是办不到的,只有通过变通方法才能实现。 30 | > 那就是在函数的内部,再定义一个函数。 31 | 32 | function f1(){ 33 |     var n=999; 34 |     function f2(){ 35 |       alert(n); // 999 36 |     } 37 |   } 38 | 39 | > 在上面的代码中,函数f2就被包括在函数f1内部,这时f1内部的所有局部变量,对f2都是可见的。但是反过来就不行,f2内部的局部变量,对f1就是不可见的。这就是Javascript语言特有的"链式作用域"结构(chain scope),子对象会一级一级地向上寻找所有父对象的变量。所以,父对象的所有变量,对子对象都是可见的,反之则不成立。 40 | 41 | > 既然f2可以读取f1中的局部变量,那么只要把f2作为返回值,我们不就可以在f1外部读取它的内部变量了吗! 42 | 43 | function f1(){ 44 |     var n=999; 45 |     function f2(){ 46 |       alert(n); 47 |     } 48 |     return f2; 49 |   } 50 |   var result=f1(); 51 |   result(); // 999 52 | 53 | ## 三、闭包的概念 54 | 55 | > 上一节代码中的f2函数,就是闭包。 56 | 57 | > 各种专业文献上的"闭包"(closure)定义非常抽象,很难看懂。我的理解是,闭包就是能够读取其他函数内部变量的函数。 58 | 59 | > 由于在Javascript语言中,只有函数内部的子函数才能读取局部变量,因此可以把闭包简单理解成"定义在一个函数内部的函数"。 60 | 61 | > 所以,在本质上,闭包就是将函数内部和函数外部连接起来的一座桥梁。 62 | 63 | ## 四、闭包的用途 64 | 65 | > 闭包可以用在许多地方。它的最大用处有两个,一个是前面提到的可以读取函数内部的变量,另一个就是让这些变量的值始终保持在内存中。 66 | 67 | > 怎么来理解这句话呢?请看下面的代码。 68 | 69 | function f1(){ 70 |     var n=999; 71 |     nAdd=function(){n+=1} 72 |     function f2(){ 73 |       alert(n); 74 |     } 75 |     return f2; 76 |   } 77 |   var result=f1(); 78 |   result(); // 999 79 |   nAdd(); 80 |   result(); // 1000 81 | 82 | > 在这段代码中,result实际上就是闭包f2函数。它一共运行了两次,第一次的值是999,第二次的值是1000。这证明了,函数f1中的局部变量n一直保存在内存中,并没有在f1调用后被自动清除。 83 | 84 | > 为什么会这样呢?原因就在于f1是f2的父函数,而f2被赋给了一个全局变量,这导致f2始终在内存中,而f2的存在依赖于f1,因此f1也始终在内存中,不会在调用结束后,被垃圾回收机制(garbage collection)回收。 85 | 86 | > 这段代码中另一个值得注意的地方,就是"nAdd=function(){n+=1}"这一行,首先在nAdd前面没有使用var关键字,因此nAdd是一个全局变量,而不是局部变量。其次,nAdd的值是一个匿名函数(anonymous function),而这个匿名函数本身也是一个闭包,所以nAdd相当于是一个setter,可以在函数外部对函数内部的局部变量进行操作。 87 | 88 | ## 五、使用闭包的注意点 89 | 90 | 1. 由于闭包会使得函数中的变量都被保存在内存中,内存消耗很大,所以不能滥用闭包,否则会造成网页的性能问题,在IE中可能导致内存泄露。解决方法是,在退出函数之前,将不使用的局部变量全部删除。 91 | 92 | 2. 闭包会在父函数外部,改变父函数内部变量的值。所以,如果你把父函数当作对象(object)使用,把闭包当作它的公用方法(Public Method),把内部变量当作它的私有属性(private value),这时一定要小心,不要随便改变父函数内部变量的值。 93 | 94 | ## 六、思考题 95 | 96 | > 如果你能理解下面两段代码的运行结果,应该就算理解闭包的运行机制了。 97 | 98 | > **代码片段一。** 99 | 100 |   var name = "The Window"; 101 |   var object = { 102 |     name : "My Object", 103 |     getNameFunc : function(){ 104 |       return function(){ 105 |         return this.name; 106 |       }; 107 |     } 108 |   }; 109 |   alert(object.getNameFunc()()); 110 | 111 | > **代码片段二。** 112 | 113 | var name = "The Window"; 114 |   var object = { 115 |     name : "My Object", 116 |     getNameFunc : function(){ 117 |       var that = this; 118 |       return function(){ 119 |         return that.name; 120 |       }; 121 |     } 122 |   }; 123 |   alert(object.getNameFunc()()); 124 | 125 | ## 感谢 126 | 127 | > 原文:[学习Javascript闭包(Closure)](http://www.ruanyifeng.com/blog/2009/08/learning_javascript_closures.html "学习Javascript闭包(Closure)") 128 | 129 | > 作者: [阮一峰](http://www.ruanyifeng.com "阮一峰") 130 | 131 | > 修正/编辑:[ahkjxy](https://github.com/ahkjxy/) -------------------------------------------------------------------------------- /11-执行上下文(Execution Contexts).md: -------------------------------------------------------------------------------- 1 | ## 深入理解JavaScript系列(11):执行上下文(Execution Contexts) 2 | 3 | ### 简介 4 | 5 | > 从本章开始,我将陆续(翻译、转载、整理)http://dmitrysoshnikov.com/网站关于ECMAScript标标准理解的好文。 6 | 7 | > 本章我们要讲解的是ECMAScript标准里的执行上下文和相关可执行代码的各种类型。 8 | 9 | 原始作者:Dmitry A. Soshnikov 10 | 原始发布: 2009-06-26 11 | 俄文原文:http://dmitrysoshnikov.com/ecmascript/ru-chapter-1-execution-contexts/ 12 | 13 | 英文翻译:Dmitry A. Soshnikov. 14 | 发布时间:2010-03-11 15 | 英文翻译:http://dmitrysoshnikov.com/ecmascript/chapter-1-execution-contexts/ 16 | 17 | 本文参考了[博客园justinw的中文翻译](http://www.cnblogs.com/justinw/archive/2010/04/16/1713086.html),做了一些错误修正,感谢译者。 18 | 19 | ### 定义 20 | 21 | > 每次当控制器转到ECMAScript可执行代码的时候,即会进入到一个执行上下文。执行上下文(简称-EC)是ECMA-262标准里的一个抽象概念,用于同可执行代码(executable code)概念进行区分。 22 | 23 | > 标准规范没有从技术实现的角度定义EC的准确类型和结构,这应该是具体实现ECMAScript引擎时要考虑的问题。 24 | 25 | > 活动的执行上下文组在逻辑上组成一个堆栈。堆栈底部永远都是全局上下文(global context),而顶部就是当前(活动的)执行上下文。堆栈在EC类型进入和退出上下文的时候被修改(推入或弹出)。 26 | 27 | ### 可执行代码类型 28 | 29 | > 可执行代码的类型这个概念与执行上下文的抽象概念是有关系的。在某些时刻,可执行代码与执行上下文完全有可能是等价的。 30 | 31 | > 例如,我们可以定义执行上下文堆栈是一个数组: 32 | 33 | ECStack = []; 34 | 35 | > 每次进入function (即使function被递归调用或作为构造函数) 的时候或者内置的eval函数工作的时候,这个堆栈都会被压入。 36 | 37 | #### 全局代码 38 | 39 | > 这种类型的代码是在"程序"级处理的:例如加载外部的js文件或者本地标签内的代码。全局代码不包括任何function体内的代码。 40 | 41 | > 在初始化(程序启动)阶段,ECStack是这样的: 42 | 43 | ECStack = [ 44 | globalContext 45 | ]; 46 | 47 | #### 函数代码 48 | 49 | > 当进入funtion函数代码(所有类型的funtions)的时候,ECStack被压入新元素。需要注意的是,具体的函数代码不包括内部函数(inner functions)代码。如下所示,我们使函数自己调自己的方式递归一次: 50 | 51 | (function foo(bar) { 52 | if (bar) { 53 | return; 54 | } 55 | foo(true); 56 | })(); 57 | 58 | > 那么,ECStack以如下方式被改变: 59 | 60 | // 第一次foo的激活调用 61 | ECStack = [ 62 | functionContext 63 | globalContext 64 | ]; 65 | 66 | // foo的递归激活调用 67 | ECStack = [ 68 | functionContext – recursively 69 | functionContext 70 | globalContext 71 | ]; 72 | 73 | > 每次return的时候,都会退出当前执行上下文的,相应地ECStack就会弹出,栈指针会自动移动位置,这是一个典型的堆栈实现方式。一个抛出的异常如果没被截获的话也有可能从一个或多个执行上下文退出。相关代码执行完以后,ECStack只会包含全局上下文(global context),一直到整个应用程序结束。 74 | 75 | #### Eval 代码 76 | 77 | > eval 代码有点儿意思。它有一个概念: 调用上下文(calling context),例如,eval函数调用的时候产生的上下文。eval(变量或函数声明)活动会影响调用上下文(calling context)。 78 | 79 | eval('var x = 10'); 80 | 81 | (function foo() { 82 | eval('var y = 20'); 83 | })(); 84 | 85 | alert(x); // 10 86 | alert(y); // "y" 提示没有声明 87 | 88 | > ECStack的变化过程: 89 | 90 | ECStack = [ 91 | globalContext 92 | ]; 93 | 94 | // eval('var x = 10'); 95 | ECStack.push( 96 | evalContext, 97 | callingContext: globalContext 98 | ); 99 | 100 | // eval exited context 101 | ECStack.pop(); 102 | 103 | // foo funciton call 104 | ECStack.push( functionContext); 105 | 106 | // eval('var y = 20'); 107 | ECStack.push( 108 | evalContext, 109 | callingContext: functionContext 110 | ); 111 | 112 | // return from eval 113 | ECStack.pop(); 114 | 115 | // return from foo 116 | ECStack.pop(); 117 | 118 | > 也就是一个非常普通的逻辑调用堆栈。 119 | 120 | > 在版本号1.7以上的SpiderMonkey(内置于Firefox,Thunderbird)的实现中,可以把调用上下文作为第二个参数传递给eval。那么,如果这个上下文存在,就有可能影响“私有”(有人喜欢这样叫它)变量。 121 | 122 | function foo() { 123 | var x = 1; 124 | return function () { alert(x); }; 125 | }; 126 | 127 | var bar = foo(); 128 | 129 | bar(); // 1 130 | 131 | eval('x = 2', bar); // 传入上下文,影响了内部的var x 变量 132 | 133 | bar(); // 2 134 | 135 | ### 结论 136 | 137 | > 这篇文章是后面分析其他跟执行上下文相关的主题(例如变量对象,作用域链,等等)的最起码的理论基础,这些主题将在后续章节中讲到。 138 | 139 | ### 其他参考 140 | 141 | > 这篇文章的内容在ECMA-262-3 标准规范中对应的章节— [10. Execution Contexts.](http://bclary.com/2004/11/07/#a-10) 142 | 143 | ## 致谢 144 | 145 | > 翻译编辑:[汤姆大叔的博客](http://www.cnblogs.com/TomXu/ '汤姆大叔的博客') 146 | 147 | > 原文地址:[深入理解JavaScript系列(11):执行上下文(Execution Contexts)](http://www.cnblogs.com/TomXu/archive/2012/01/13/2308101.html '深入理解JavaScript系列(11):执行上下文(Execution Contexts)') 148 | 149 | > 修正/编辑:[ahkjxy](https://github.com/ahkjxy/) -------------------------------------------------------------------------------- /9-根本没有“JSON对象”这回事!.md: -------------------------------------------------------------------------------- 1 | ## 深入理解JavaScript系列(9):根本没有“JSON对象”这回事! 2 | 3 | ### 前言 4 | 5 | > 写这篇文章的目的是经常看到开发人员说:把字符串转化为JSON对象,把JSON对象转化成字符串等类似的话题,所以把之前收藏的一篇老外的文章整理翻译了一下,供大家讨论,如有错误,请大家指出,多谢。 6 | 7 | ### 正文 8 | 9 | > 本文的主题是基于ECMAScript262-3来写的,2011年的262-5新规范增加了JSON对象,和我们平时所说的JSON有关系,但是不是同一个东西,文章最后一节会讲到新增加的JSON对象。 10 | 11 | 英文原文:http://benalman.com/news/2010/03/theres-no-such-thing-as-a-json/ 12 | 13 | > 我想给大家澄清一下一个非常普遍的误解,我认为很多JavaScript开发人员都错误地把**JavaScript对象字面量(Object Literals)**称为**JSON对象(JSON Objects)**,因为他的语法和[JSON规范](http://json.org/)里描述的一样,但是该规范里也明确地说了JSON只是一个**数据交换语言**,只有我们将之用在string上下文的时候它才叫**JSON**。 14 | 15 | ### 序列化与反序列化 16 | 17 | > 2个程序(或服务器、语言等)需要交互通信的时候,他们倾向于使用string字符串因为string在很多语言里解析的方式都差不多。复杂的数据结构经常需要用到,并且通过各种各样的中括号{},小括号(),叫括号<>和空格来组成,这个字符串仅仅是按照要求规范好的字符。 18 | 19 | > 为此,我们为了描述这些复杂的数据结构作为一个string字符串,制定了标准的规则和语法。JSON只是其中一种语法,它可以在string上下文里描述对象,数组,字符串,数字,布尔型和null,然后通过程序间传输,并且反序列化成所需要的格式。[YAML](http://en.wikipedia.org/wiki/YAML)和[XML](http://en.wikipedia.org/wiki/XML)(甚至[request params](http://benalman.com/news/2009/12/jquery-14-param-demystified/))也是流行的数据交换格式,但是,我们喜欢JSON,谁叫我们是JavaScript开发人员呢! 20 | 21 | ### 字面量 22 | 23 | > 引用Mozilla Developer Center里的几句话,供大家参考: 24 | 25 | 1. 他们是固定的值,不是变量,让你从“字面上”理解脚本。 ([Literals](https://developer.mozilla.org/en/Core_JavaScript_1.5_Guide/Core_Language_Features#Literals)) 26 | 1. 字符串字面量是由双引号(")或单引号(')包围起来的零个或多个字符组成的。([Strings Literals](https://developer.mozilla.org/en/Core_JavaScript_1.5_Guide/Core_Language_Features#String_Literals)) 27 | 1. 对象字面量是由大括号({})括起来的零个或多个对象的属性名-值对。([Object Literals](https://developer.mozilla.org/en/Core_JavaScript_1.5_Guide/Core_Language_Features#Object_Literals)) 28 | 29 | ### 何时是JSON,何时不是JSON? 30 | 31 | > JSON是设计成描述数据交换格式的,他也有自己的语法,这个语法是JavaScript的一个子集。 32 | 33 | > { "prop": "val" } 这样的声明有可能是JavaScript对象字面量也有可能是JSON字符串,取决于什么上下文使用它,如果是用在string上下文(用单引号或双引号引住,或者从text文件读取)的话,那它就是JSON字符串,如果是用在对象字面量上下文中,那它就是对象字面量。 34 | 35 | // 这是JSON字符串 36 | var foo = '{ "prop": "val" }'; 37 | 38 | // 这是对象字面量 39 | var bar = { "prop": "val" }; 40 | 41 | > 而且要注意,JSON有非常严格的语法,在string上下文里**{ "prop": "val" }** 是个合法的JSON,但**{ prop: "val" }**和**{ 'prop': 'val' }**确实不合法的。所有属性名称和它的值都必须用双引号引住,不能使用单引号。另外,即便你用了转义以后的单引号也是不合法的,详细的语法规则可以到[这里查看](http://json.org/)。 42 | 43 | ### 放到上下文里来看 44 | 45 | > 大家伙可能嗤之以鼻:难道JavaScript代码不是一个大的字符串? 46 | 47 | > 当然是,所有的JavaScript代码和HTML(可能还有其他东西)都是字符串,直到浏览器对他们进行解析。这时候.jf文件或者inline的JavaScript代码已经不是字符串了,而是被当成真正的JavaScript源代码了,就像页面里的innterHTML一样,这时候也不是字符串了,而是被解析成DOM结构了。 48 | 49 | > 再次说一下,这取决于上下文,在string上下文里使用带有大括号的JavaScript对象,那它就是JSON字符串,而如果在对象字面量上下文里使用的话,那它就是对象字面量。 50 | 51 | ### 真正的JSON对象 52 | 53 | > 开头已经提到,对象字面量不是JSON对象,但是[有真正的JSON对象](https://developer.mozilla.org/en/Using_native_JSON)。但是两者完全不一样概念,在新版的浏览器里JSON对象已经被原生的内置对象了,目前有2个静态方法:**JSON.parse**用来将JSON字符串反序列化成对象,**JSON.stringify**用来将对象序列化成JSON字符串。老版本的浏览器不支持这个对象,但你可以通过[json2.js](https://github.com/douglascrockford/JSON-js)来实现同样的功能。 54 | 55 | > 如果还不理解,别担心,参考一下的例子就知道了: 56 | 57 | // 这是JSON字符串,比如从AJAX获取字符串信息 58 | var my_json_string = '{ "prop": "val" }'; 59 | 60 | // 将字符串反序列化成对象 61 | var my_obj = JSON.parse( my_json_string ); 62 | 63 | alert( my_obj.prop == 'val' ); // 提示 true, 和想象的一样! 64 | 65 | // 将对象序列化成JSON字符串 66 | var my_other_json_string = JSON.stringify( my_obj ); 67 | 68 | > 另外,[Paul Irish](http://paulirish.com/)提到Douglas Crockford在[JSON RFC](http://www.ietf.org/rfc/rfc4627.txt?number=4627)里用到了“JSON object”,但是在那个上下文里,他的意思是“对象描述成JSON字符串”不是“对象字面量”。 69 | 70 | ### 更多资料 71 | 72 | > 如果你想了解更多关于JSON的资料,下面的连接对你绝对有用: 73 | 74 | - [JSON specification](http://json.org/) 75 | - [JSON RFC](http://www.ietf.org/rfc/rfc4627.txt?number=4627) 76 | - [JSON on Wikipedia](http://en.wikipedia.org/wiki/JSON) 77 | - [JSONLint The JSON Validator](http://www.jsonlint.com/) 78 | - [JSON is not the same as JSON](http://james.padolsey.com/javascript/json-is-not-the-same-as-json/) 79 | 80 | ## 致谢 81 | 82 | > 翻译编辑:[汤姆大叔的博客](http://www.cnblogs.com/TomXu/ '汤姆大叔的博客') 83 | 84 | > 原文地址:[深入理解JavaScript系列(9):根本没有“JSON对象”这回事!](http://www.cnblogs.com/TomXu/archive/2012/01/11/2311956.html '深入理解JavaScript系列(9):根本没有“JSON对象”这回事!') 85 | 86 | > 修正/编辑:[ahkjxy](https://github.com/ahkjxy/) -------------------------------------------------------------------------------- /8-S.O.L.I.D五大原则之里氏替换原则LSP.md: -------------------------------------------------------------------------------- 1 | ## 深入理解JavaScript系列(8):S.O.L.I.D五大原则之里氏替换原则LSP 2 | 3 | ### 前言 4 | 5 | > 本章我们要讲解的是S.O.L.I.D五大原则JavaScript语言实现的第3篇,里氏替换原则LSP(The Liskov Substitution Principle )。 6 | 7 | 英文原文:http://freshbrewedcode.com/derekgreer/2011/12/31/solid-javascript-the-liskov-substitution-principle/ 8 | 9 | > 开闭原则的描述是: 10 | 11 | Subtypes must be substitutable for their base types. 12 | 派生类型必须可以替换它的基类型。 13 | 14 | > 在面向对象编程里,继承提供了一个机制让子类和共享基类的代码,这是通过在基类型里封装通用的数据和行为来实现的,然后已经及类型来声明更详细的子类型,为了应用里氏替换原则,继承子类型需要在语义上等价于基类型里的期望行为。 15 | 16 | > 为了来更好的理解,请参考如下代码: 17 | 18 | function Vehicle(my) { 19 | var my = my || {}; 20 | my.speed = 0; 21 | my.running = false; 22 | 23 | this.speed = function() { 24 | return my.speed; 25 | }; 26 | this.start = function() { 27 | my.running = true; 28 | }; 29 | this.stop = function() { 30 | my.running = false; 31 | }; 32 | this.accelerate = function() { 33 | my.speed++; 34 | }; 35 | this.decelerate = function() { 36 | my.speed--; 37 | }, this.state = function() { 38 | if (!my.running) { 39 | return "parked"; 40 | } 41 | else if (my.running && my.speed) { 42 | return "moving"; 43 | } 44 | else if (my.running) { 45 | return "idle"; 46 | } 47 | }; 48 | } 49 | 50 | > 上述代码我们定义了一个Vehicle函数,其构造函数为vehicle对象提供了一些基本的操作,我们来想想如果当前函数当前正运行在服务客户的产品环境上,如果现在需要添加一个新的构造函数来实现加快移动的vehicle。思考以后,我们写出了如下代码: 51 | 52 | function FastVehicle(my) { 53 | var my = my || {}; 54 | 55 | var that = new Vehicle(my); 56 | that.accelerate = function() { 57 | my.speed += 3; 58 | }; 59 | return that; 60 | } 61 | 62 | > 在浏览器的控制台我们都测试了,所有的功能都是我们的预期,没有问题,FastVehicle的速度增快了3倍,而且继承他的方法也是按照我们的预期工作。此后,我们开始部署这个新版本的类库到产品环境上,可是我们却接到了新的构造函数导致现有的代码不能支持执行了,下面的代码段揭示了这个问题: 63 | 64 | var maneuver = function(vehicle) { 65 | write(vehicle.state()); 66 | vehicle.start(); 67 | write(vehicle.state()); 68 | vehicle.accelerate(); 69 | write(vehicle.state()); 70 | write(vehicle.speed()); 71 | vehicle.decelerate(); 72 | write(vehicle.speed()); 73 | if (vehicle.state() != "idle") { 74 | throw "The vehicle is still moving!"; 75 | } 76 | vehicle.stop(); 77 | write(vehicle.state()); 78 | }; 79 | 80 | > 根据上面的代码,我们看到抛出的异常是“The vehicle is still moving!”,这是因为写这段代码的作者一直认为加速(accelerate)和减速(decelerate)的数字是一样的。但FastVehicle的代码和Vehicle的代码并不是完全能够替换掉的。因此,FastVehicle违反了里氏替换原则。 81 | 82 | > 在这点上,你可能会想:“但,客户端不能老假定vehicle都是按照这样的规则来做”,里氏替换原则(LSP)的妨碍(译者注:就是妨碍实现LSP的代码)不是基于我们所想的继承子类应该在行为里确保更新代码,而是这样的更新是否能在当前的期望中得到实现。 83 | 84 | > 上述代码这个case,解决这个不兼容的问题需要在vehicle类库或者客户端调用代码上进行一点重新设计,或者两者都要改。 85 | 86 | ### 减少LSP妨碍 87 | 88 | > 那么,我们如何避免LSP妨碍?不幸的话,并不是一直都是可以做到的。我们这里有几个策略我们处理这个事情。 89 | 90 | #### 契约(Contracts) 91 | 92 | > 处理LSP过分妨碍的一个策略是使用契约,契约清单有2种形式:执行说明书(executable specifications)和错误处理,在执行说明书里,一个详细类库的契约也包括一组自动化测试,而错误处理是在代码里直接处理的,例如在前置条件,后置条件,常量检查等,可以从Bertrand Miller的大作《契约设计》中查看这个技术。虽然自动化测试和契约设计不在本篇文字的范围内,但当我们用的时候我还是推荐如下内容: 93 | 94 | 1. 检查使用测试驱动开发(Test-Driven Development)来指导你代码的设计 95 | 1. 设计可重用类库的时候可随意使用契约设计技术 96 | 97 | > 对于你自己要维护和实现的代码,使用契约设计趋向于添加很多不必要的代码,如果你要控制输入,添加测试是非常有必要的,如果你是类库作者,使用契约设计,你要注意不正确的使用方法以及让你的用户使之作为一个测试工具。 98 | 99 | #### 避免继承 100 | 101 | > 避免LSP妨碍的另外一个测试是:如果可能的话,尽量不用继承,在Gamma的大作[《Design Patterns – Elements of Reusable Object-Orineted Software》](http://www.amazon.com/Design-Patterns-Elements-Reusable-Object-Oriented/dp/0201633612)中,我们可以看到如下建议: 102 | 103 | Favor object composition over class inheritance 104 | 尽量使用对象组合而不是类继承 105 | 106 | > 有些书里讨论了组合比继承好的唯一作用是静态类型,基于类的语言(例如,在运行时可以改变行为),与JavaScript相关的一个问题是耦合,当使用继承的时候,继承子类型和他们的基类型耦合在一起了,就是说及类型的改变会影响到继承子类型。组合倾向于对象更小化,更容易想静态和动态语言语言维护。 107 | 108 | ### 与行为有关,而不是继承 109 | 110 | > 到现在,我们讨论了和继承上下文在内的里氏替换原则,指示出JavaScript的面向对象实。不过,里氏替换原则(LSP)的本质不是真的和继承有关,而是行为兼容性。JavaScript是一个动态语言,一个对象的契约行为不是对象的类型决定的,而是对象期望的功能决定的。里氏替换原则的初始构想是作为继承的一个原则指南,等价于对象设计中的隐式接口。 111 | 112 | > 举例来说,让我们来看一下Robert C. Martin的大作[《敏捷软件开发 原则、模式与实践》](http://www.amazon.com/Software-Development-Principles-Patterns-Practices/dp/0135974445)中的一个矩形类型: 113 | 114 | #### 矩形例子 115 | 116 | > 考虑我们有一个程序用到下面这样的一个矩形对象: 117 | 118 | var rectangle = { 119 | length: 0, 120 | width: 0 121 | }; 122 | 123 | > 过后,程序有需要一个正方形,由于正方形就是一个长(length)和宽(width)都一样的特殊矩形,所以我们觉得创建一个正方形代替矩形。我们添加了length和width属性来匹配矩形的声明,但我们觉得使用属性的getters/setters一般我们可以让length和width保存同步,确保声明的是一个正方形: 124 | 125 | var square = {}; 126 | (function() { 127 | var length = 0, width = 0; 128 | // 注意defineProperty方式是262-5版的新特性 129 | Object.defineProperty(square, "length", { 130 | get: function() { return length; }, 131 | set: function(value) { length = width = value; } 132 | }); 133 | Object.defineProperty(square, "width", { 134 | get: function() { return width; }, 135 | set: function(value) { length = width = value; } 136 | }); 137 | })(); 138 | 139 | > 不幸的是,当我们使用正方形代替矩形执行代码的时候发现了问题,其中一个计算矩形面积的方法如下: 140 | 141 | var g = function(rectangle) { 142 | rectangle.length = 3; 143 | rectangle.width = 4; 144 | write(rectangle.length); 145 | write(rectangle.width); 146 | write(rectangle.length * rectangle.width); 147 | }; 148 | 149 | > 该方法在调用的时候,结果是16,而不是期望的12,我们的正方形square对象违反了LSP原则,square的长度和宽度属性暗示着并不是和矩形100%兼容,但我们并不总是这样明确的暗示。解决这个问题,我们可以重新设计一个shape对象来实现程序,依据多边形的概念,我们声明rectangle和square,relevant。不管怎么说,我们的目的是要说里氏替换原则并不只是继承,而是任何方法(其中的行为可以另外的行为)。 150 | 151 | ## 总结 152 | 153 | > 里氏替换原则(LSP)表达的意思不是继承的关系,而是任何方法(只要该方法的行为能体会另外的行为就行)。 154 | 155 | > 里氏替换原则(LSP):[子类可以扩展父类的功能,但不能修改父类原有的功能](http://blog.csdn.net/zhengzhb/article/details/7281833 '子类可以扩展父类的功能,但不能修改父类原有的功能')。 156 | 157 | ## 致谢 158 | 159 | > 翻译编辑:[汤姆大叔的博客](http://www.cnblogs.com/TomXu/ '汤姆大叔的博客') 160 | 161 | > 原文地址:[深入理解JavaScript系列(8):S.O.L.I.D五大原则之里氏替换原则LSP](http://www.cnblogs.com/TomXu/archive/2012/01/10/2310244.html '深入理解JavaScript系列(8):S.O.L.I.D五大原则之里氏替换原则LSP) 162 | 163 | > 修正/编辑:[ahkjxy](https://github.com/ahkjxy/) -------------------------------------------------------------------------------- /3-全面解析Module模式.md: -------------------------------------------------------------------------------- 1 | ## 深入理解JavaScript系列(3):全面解析Module模式 2 | 3 | ### 简介 4 | 5 | > Module模式是JavaScript编程中一个非常通用的模式,一般情况下,大家都知道基本用法,本文尝试着给大家更多该模式的高级使用方式。 6 | 7 | > 首先我们来看看Module模式的基本特征: 8 | 9 | - 模块化,可重用 10 | - 封装了变量和function,和全局的namaspace不接触,松耦合 11 | - 只暴露可用public的方法,其它私有方法全部隐藏 12 | 13 | > 关于Module模式,最早是由YUI的成员Eric Miraglia在4年前提出了这个概念,我们将从一个简单的例子来解释一下基本的用法(如果你已经非常熟悉了,请忽略这一节)。 14 | 15 | ### 基本用法 16 | 17 | > 先看一下最简单的一个实现,代码如下: 18 | 19 | var Calculator = function (eq) { 20 | //这里可以声明私有成员 21 | 22 | var eqCtl = document.getElementById(eq); 23 | 24 | return { 25 | // 暴露公开的成员 26 | add: function (x, y) { 27 | var val = x + y; 28 | eqCtl.innerHTML = val; 29 | } 30 | }; 31 | }; 32 | 我们可以通过如下的方式来调用: 33 | 34 | var calculator = new Calculator('eq'); 35 | calculator.add(2, 2); 36 | 37 | > 大家可能看到了,每次用的时候都要new一下,也就是说每个实例在内存里都是一份copy,如果你不需要传参数或者没有一些特殊苛刻的要求的话,我们可以在最后一个}后面加上一个括号,来达到自执行的目的,这样该实例在内存中只会存在一份copy,不过在展示他的优点之前,我们还是先来看看这个模式的基本使用方法吧。 38 | 39 | ### 匿名闭包 40 | 41 | > 匿名闭包是让一切成为可能的基础,而这也是JavaScript最好的特性,我们来创建一个最简单的闭包函数,函数内部的代码一直存在于闭包内,在整个运行周期内,该闭包都保证了内部的代码处于私有状态。 42 | 43 | (function () { 44 | // ... 所有的变量和function都在这里声明,并且作用域也只能在这个匿名闭包里 45 | // ...但是这里的代码依然可以访问外部全局的对象 46 | }()); 47 | 48 | > 注意,匿名函数后面的括号,这是JavaScript语言所要求的,因为如果你不声明的话,JavaScript解释器默认是声明一个function函数,有括号,就是创建一个函数表达式,也就是自执行,用的时候不用和上面那样在new了,当然你也可以这样来声明: 49 | 50 | (function () {/* 内部代码 */})(); 51 | 52 | > 不过我们推荐使用第一种方式,关于函数自执行,我后面会有专门一篇文章进行详解,这里就不多说了。 53 | 54 | ### 引用全局变量 55 | 56 | > JavaScript有一个特性叫做隐式全局变量,不管一个变量有没有用过,JavaScript解释器反向遍历作用域链来查找整个变量的var声明,如果没有找到var,解释器则假定该变量是全局变量,如果该变量用于了赋值操作的话,之前如果不存在的话,解释器则会自动创建它,这就是说在匿名闭包里使用或创建全局变量非常容易,不过比较困难的是,代码比较难管理,尤其是阅读代码的人看着很多区分哪些变量是全局的,哪些是局部的。 57 | 58 | > 不过,好在在匿名函数里我们可以提供一个比较简单的替代方案,我们可以将全局变量当成一个参数传入到匿名函数然后使用,相比隐式全局变量,它又清晰又快,我们来看一个例子: 59 | 60 | (function ($, YAHOO) { 61 | // 这里,我们的代码就可以使用全局的jQuery对象了,YAHOO也是一样 62 | } (jQuery, YAHOO)); 63 | 64 | > 现在很多类库里都有这种使用方式,比如jQuery源码。 65 | 66 | > 不过,有时候可能不仅仅要使用全局变量,而是也想声明全局变量,如何做呢?我们可以通过匿名函数的返回值来返回这个全局变量,这也就是一个基本的Module模式,来看一个完整的代码: 67 | 68 | var blogModule = (function () { 69 | var my = {}, privateName = "博客园"; 70 | 71 | function privateAddTopic(data) { 72 | // 这里是内部处理代码 73 | } 74 | 75 | my.Name = privateName; 76 | my.AddTopic = function (data) { 77 | privateAddTopic(data); 78 | }; 79 | 80 | return my; 81 | } ()); 82 | 83 | > 上面的代码声明了一个全局变量blogModule,并且带有2个可访问的属性:blogModule.AddTopic和blogModule.Name,除此之外,其它代码都在匿名函数的闭包里保持着私有状态。同时根据上面传入全局变量的例子,我们也可以很方便地传入其它的全局变量。 84 | 85 | ### 高级用法 86 | 87 | > 上面的内容对大多数用户已经很足够了,但我们还可以基于此模式延伸出更强大,易于扩展的结构,让我们一个一个来看。 88 | 89 | #### 扩展 90 | 91 | > Module模式的一个限制就是所有的代码都要写在一个文件,但是在一些大型项目里,将一个功能分离成多个文件是非常重要的,因为可以多人合作易于开发。再回头看看上面的全局参数导入例子,我们能否把blogModule自身传进去呢?答案是肯定的,我们先将blogModule传进去,添加一个函数属性,然后再返回就达到了我们所说的目的,上代码: 92 | 93 | var blogModule = (function (my) { 94 | my.AddPhoto = function () { 95 | //添加内部代码 96 | }; 97 | return my; 98 | } (blogModule)); 99 | 100 | > 这段代码,看起来是不是有C#里扩展方法的感觉?有点类似,但本质不一样哦。同时尽管var不是必须的,但为了确保一致,我们再次使用了它,代码执行以后,blogModule下的AddPhoto就可以使用了,同时匿名函数内部的代码也依然保证了私密性和内部状态。 101 | 102 | #### 松耦合扩展 103 | 104 | > 上面的代码尽管可以执行,但是必须先声明blogModule,然后再执行上面的扩展代码,也就是说步骤不能乱,怎么解决这个问题呢?我们来回想一下,我们平时声明变量的都是都是这样的: 105 | 106 | var cnblogs = cnblogs || {} ; 107 | 108 | > 这是确保cnblogs对象,在存在的时候直接用,不存在的时候直接赋值为{},我们来看看如何利用这个特性来实现Module模式的任意加载顺序: 109 | 110 | var blogModule = (function (my) { 111 | 112 | // 添加一些功能 113 | 114 | return my; 115 | } (blogModule || {})); 116 | 117 | > 通过这样的代码,每个单独分离的文件都保证这个结构,那么我们就可以实现任意顺序的加载,所以,这个时候的var就是必须要声明的,因为不声明,其它文件读取不到哦。 118 | 119 | #### 紧耦合扩展 120 | 121 | > 虽然松耦合扩展很牛叉了,但是可能也会存在一些限制,比如你没办法重写你的一些属性或者函数,也不能在初始化的时候就是用Module的属性。紧耦合扩展限制了加载顺序,但是提供了我们重载的机会,看如下例子: 122 | 123 | var blogModule = (function (my) { 124 | var oldAddPhotoMethod = my.AddPhoto; 125 | 126 | my.AddPhoto = function () { 127 | // 重载方法,依然可通过oldAddPhotoMethod调用旧的方法 128 | }; 129 | 130 | return my; 131 | } (blogModule)); 132 | 133 | > 通过这种方式,我们达到了重载的目的,当然如果你想在继续在内部使用原有的属性,你可以调用oldAddPhotoMethod来用。 134 | 135 | #### 克隆与继承 136 | 137 | var blogModule = (function (old) { 138 | var my = {}, 139 | key; 140 | 141 | for (key in old) { 142 | if (old.hasOwnProperty(key)) { 143 | my[key] = old[key]; 144 | } 145 | } 146 | 147 | var oldAddPhotoMethod = old.AddPhoto; 148 | my.AddPhoto = function () { 149 | // 克隆以后,进行了重写,当然也可以继续调用oldAddPhotoMethod 150 | }; 151 | 152 | return my; 153 | } (blogModule)); 154 | 155 | > 这种方式灵活是灵活,但是也需要花费灵活的代价,其实该对象的属性对象或function根本没有被复制,只是对同一个对象多了一种引用而已,所以如果老对象去改变它,那克隆以后的对象所拥有的属性或function函数也会被改变,解决这个问题,我们就得是用递归,但递归对function函数的赋值也不好用,所以我们在递归的时候eval相应的function。不管怎么样,我还是把这一个方式放在这个帖子里了,大家使用的时候注意一下就行了。 156 | 157 | #### 跨文件共享私有对象 158 | 159 | > 通过上面的例子,我们知道,如果一个module分割到多个文件的话,每个文件需要保证一样的结构,也就是说每个文件匿名函数里的私有对象都不能交叉访问,那如果我们非要使用,那怎么办呢? 我们先看一段代码: 160 | 161 | var blogModule = (function (my) { 162 | var _private = my._private = my._private || {}, 163 | 164 | _seal = my._seal = my._seal || function () { 165 | delete my._private; 166 | delete my._seal; 167 | delete my._unseal; 168 | 169 | }, 170 | 171 | _unseal = my._unseal = my._unseal || function () { 172 | my._private = _private; 173 | my._seal = _seal; 174 | my._unseal = _unseal; 175 | }; 176 | 177 | return my; 178 | } (blogModule || {})); 179 | 180 | > 任何文件都可以对他们的局部变量_private设属性,并且设置对其他的文件也立即生效。一旦这个模块加载结束,应用会调用 blogModule._seal()"上锁",这会阻止外部接入内部的_private。如果这个模块需要再次增生,应用的生命周期内,任何文件都可以调用_unseal() ”开锁”,然后再加载新文件。加载后再次调用 _seal()”上锁”。 181 | 182 | #### 子模块 183 | 184 | > 最后一个也是最简单的使用方式,那就是创建子模块 185 | 186 | blogModule.CommentSubModule = (function () { 187 | var my = {}; 188 | // ... 189 | 190 | return my; 191 | } ()); 192 | 193 | > 尽管非常简单,我还是把它放进来了,因为我想说明的是子模块也具有一般模块所有的高级使用方式,也就是说你可以对任意子模块再次使用上面的一些应用方法。 194 | 195 | ### 总结 196 | 197 | > 上面的大部分方式都可以互相组合使用的,一般来说如果要设计系统,可能会用到松耦合扩展,私有状态和子模块这样的方式。另外,我这里没有提到性能问题,但我认为Module模式效率高,代码少,加载速度快。使用松耦合扩展允许并行加载,这更可以提升下载速度。不过初始化时间可能要慢一些,但是为了使用好的模式,这是值得的。 198 | 199 | > **参考文章:** 200 | 201 | > [http://yuiblog.com/blog/2007/06/12/module-pattern/](http://yuiblog.com/blog/2007/06/12/module-pattern/) 202 | 203 | > [http://www.adequatelygood.com/2010/3/JavaScript-Module-Pattern-In-Depth](http://www.adequatelygood.com/2010/3/JavaScript-Module-Pattern-In-Depth) 204 | 205 | ### 致谢 206 | 207 | > 翻译编辑:[汤姆大叔的博客](http://www.cnblogs.com/TomXu/ '汤姆大叔的博客') 208 | 209 | > 原文地址:[深入理解JavaScript系列(3):全面解析Module模式](http://www.cnblogs.com/TomXu/archive/2011/12/30/2288372.html '深入理解JavaScript系列(3):全面解析Module模式') 210 | 211 | > 修正/编辑:[ahkjxy](https://github.com/ahkjxy/) -------------------------------------------------------------------------------- /5-强大的原型和原型链.md: -------------------------------------------------------------------------------- 1 | ## 深入理解JavaScript系列(5):强大的原型和原型链 2 | 3 | ### 前言 4 | 5 | > JavaScript 不包含传统的类继承模型,而是使用 prototypal 原型模型。 6 | 7 | > 虽然这经常被当作是 JavaScript 的缺点被提及,其实基于原型的继承模型比传统的类继承还要强大。实现传统的类继承模型是很简单,但是实现 JavaScript 中的原型继承则要困难的多。 8 | 9 | > 由于 JavaScript 是唯一一个被广泛使用的基于原型继承的语言,所以理解两种继承模式的差异是需要一定时间的,今天我们就来了解一下原型和原型链。 10 | 11 | ### 原型 12 | 13 | > 10年前,我刚学习JavaScript的时候,一般都是用如下方式来写代码: 14 | 15 | var decimalDigits = 2, 16 | tax = 5; 17 | 18 | function add(x, y) { 19 | return x + y; 20 | } 21 | 22 | function subtract(x, y) { 23 | return x - y; 24 | } 25 | 26 | //alert(add(1, 3)); 27 | 28 | > 通过执行各个function来得到结果,学习了原型之后,我们可以使用如下方式来**美化**一下代码。 29 | 30 | 31 | #### 原型使用方式1: 32 | 33 | > 在使用原型之前,我们需要先将代码做一下小修改: 34 | 35 | var Calculator = function (decimalDigits, tax) { 36 | this.decimalDigits = decimalDigits; 37 | this.tax = tax; 38 | }; 39 | 40 | > 然后,通过给Calculator对象的prototype属性赋值**[对象字面量](http://www.cnblogs.com/yxf2011/archive/2012/04/01/2428225.html)**来设定Calculator对象的原型。 41 | 42 | Calculator.prototype = { 43 | add: function (x, y) { 44 | return x + y; 45 | }, 46 | 47 | subtract: function (x, y) { 48 | return x - y; 49 | } 50 | }; 51 | //alert((new Calculator()).add(1, 3)); 52 | 53 | > 这样,我们就可以new Calculator对象以后,就可以调用add方法来计算结果了。 54 | 55 | #### 原型使用方式2: 56 | 57 | > 第二种方式是,在赋值原型prototype的时候使用function立即执行的表达式来赋值,即如下格式: 58 | 59 | Calculator.prototype = function () { } (); 60 | 61 | > 它的好处在前面的帖子里已经知道了,就是可以封装私有的function,通过return的形式暴露出简单的使用名称,以达到public/private的效果,修改后的代码如下: 62 | 63 | Calculator.prototype = function () { 64 | add = function (x, y) { 65 | return x + y; 66 | }, 67 | 68 | subtract = function (x, y) { 69 | return x - y; 70 | } 71 | return { 72 | add: add, 73 | subtract: subtract 74 | } 75 | } (); 76 | 77 | //alert((new Calculator()).add(11, 3)); 78 | 79 | > 同样的方式,我们可以new Calculator对象以后调用add方法来计算结果了。 80 | 81 | ### 再来一点 82 | 83 | #### 分步声明: 84 | 85 | > 上述使用原型的时候,有一个限制就是一次性设置了原型对象,我们再来说一下如何分来设置原型的每个属性吧。 86 | 87 | var BaseCalculator = function () { 88 | //为每个实例都声明一个小数位数 89 | this.decimalDigits = 2; 90 | }; 91 | 92 | //使用原型给BaseCalculator扩展2个对象方法 93 | BaseCalculator.prototype.add = function (x, y) { 94 | return x + y; 95 | }; 96 | 97 | BaseCalculator.prototype.subtract = function (x, y) { 98 | return x - y; 99 | }; 100 | 101 | > 首先,声明了一个BaseCalculator对象,构造函数里会初始化一个小数位数的属性decimalDigits,然后通过原型属性设置2个function,分别是add(x,y)和subtract(x,y),当然你也可以使用前面提到的2种方式的任何一种,我们的主要目的是看如何将BaseCalculator对象设置到真正的Calculator的原型上。 102 | 103 | var BaseCalculator = function() { 104 | this.decimalDigits = 2; 105 | }; 106 | 107 | BaseCalculator.prototype = { 108 | add: function(x, y) { 109 | return x + y; 110 | }, 111 | subtract: function(x, y) { 112 | return x - y; 113 | } 114 | }; 115 | 116 | > 创建完上述代码以后,我们来开始: 117 | 118 | var Calculator = function () { 119 | //为每个实例都声明一个税收数字 120 | this.tax = 5; 121 | }; 122 | 123 | Calculator.prototype = new BaseCalculator(); 124 | 125 | > 我们可以看到Calculator的原型是指向到BaseCalculator的一个实例上,目的是让Calculator集成它的add(x,y)和subtract(x,y)这2个function,还有一点要说的是,由于它的原型是BaseCalculator的一个实例,所以不管你创建多少个Calculator对象实例,他们的原型指向的都是同一个实例。 126 | 127 | var calc = new Calculator(); 128 | alert(calc.add(1, 1)); 129 | //BaseCalculator 里声明的decimalDigits属性,在 Calculator里是可以访问到的 130 | alert(calc.decimalDigits); 131 | 132 | > 上面的代码,运行以后,我们可以看到因为Calculator的原型是指向BaseCalculator的实例上的,所以可以访问他的decimalDigits属性值,那如果我不想让Calculator访问BaseCalculator的构造函数里声明的属性值,那怎么办呢?这么办: 133 | 134 | var Calculator = function () { 135 | this.tax= 5; 136 | }; 137 | 138 | Calculator.prototype = BaseCalculator.prototype; 139 | 140 | > 通过将BaseCalculator的原型赋给Calculator的原型,这样你在Calculator的实例上就访问不到那个decimalDigits值了,如果你访问如下代码,那将会提升出错。 141 | 142 | var calc = new Calculator(); 143 | alert(calc.add(1, 1)); 144 | alert(calc.decimalDigits); 145 | 146 | #### 重写原型: 147 | 148 | > 在使用第三方JS类库的时候,往往有时候他们定义的原型方法是不能满足我们的需要,但是又离不开这个类库,所以这时候我们就需要重写他们的原型中的一个或者多个属性或function,我们可以通过继续声明的同样的add代码的形式来达到覆盖重写前面的add功能,代码如下: 149 | 150 | //覆盖前面Calculator的add() function 151 | Calculator.prototype.add = function (x, y) { 152 | return x + y + this.tax; 153 | }; 154 | 155 | var calc = new Calculator(); 156 | alert(calc.add(1, 1)); 157 | 158 | > 这样,我们计算得出的结果就比原来多出了一个tax的值,但是有一点需要注意:那就是重写的代码需要放在最后,这样才能覆盖前面的代码。 159 | 160 | 161 | ### 原型链 162 | 163 | > 在讲原型链之前,我们先上一段代码: 164 | 165 | function Foo() { 166 | this.value = 42; 167 | } 168 | Foo.prototype = { 169 | method: function() {} 170 | }; 171 | 172 | function Bar() {} 173 | 174 | // 设置Bar的prototype属性为Foo的实例对象 175 | Bar.prototype = new Foo(); 176 | Bar.prototype.foo = 'Hello World'; 177 | 178 | // 修正Bar.prototype.constructor为Bar本身 179 | Bar.prototype.constructor = Bar; 180 | 181 | var test = new Bar() // 创建Bar的一个新实例 182 | 183 | // 原型链 184 | test [Bar的实例] 185 | Bar.prototype [Foo的实例] 186 | { foo: 'Hello World' } 187 | Foo.prototype 188 | {method: ...}; 189 | Object.prototype 190 | {toString: ... /* etc. */}; 191 | 192 | > 上面的例子中,**test **对象从** Bar.prototype** 和 **Foo.prototype** 继承下来;因此,它能访问 **Foo **的原型方法 **method**。同时,它也能够访问那个定义在原型上的** Foo** 实例属性 **value**。需要注意的是 **new Bar()** 不会创造出一个新的** Foo** 实例,而是重复使用它原型上的那个实例;因此,所有的** Bar** 实例都会共享相同的 **value **属性。 193 | 194 | #### 属性查找: 195 | 196 | > 当查找一个对象的属性时,JavaScript 会向上遍历原型链,直到找到给定名称的属性为止,到查找到达原型链的顶部 - 也就是 Object.prototype - 但是仍然没有找到指定的属性,就会返回 undefined,我们来看一个例子: 197 | 198 | function foo() { 199 | this.add = function (x, y) { 200 | return x + y; 201 | } 202 | } 203 | 204 | foo.prototype.add = function (x, y) { 205 | return x + y + 10; 206 | } 207 | 208 | Object.prototype.subtract = function (x, y) { 209 | return x - y; 210 | } 211 | 212 | var f = new foo(); 213 | alert(f.add(1, 2)); //结果是3,而不是13 214 | alert(f.subtract(1, 2)); //结果是-1 215 | 216 | > 通过代码运行,我们发现subtract是安装我们所说的向上查找来得到结果的,但是add方式有点小不同,这也是我想强调的,就是属性在查找的时候是先查找自身的属性,如果没有再查找原型,再没有,再往上走,一直插到Object的原型上,所以在某种层面上说,用 for in语句遍历属性的时候,效率也是个问题。 217 | 218 | > 还有一点我们需要注意的是,我们可以赋值任何类型的对象到原型上,但是不能赋值原子类型的值,比如如下代码是无效的: 219 | 220 | function Foo() {} 221 | Foo.prototype = 1; // 无效 222 | 223 | #### hasOwnProperty函数: 224 | 225 | > hasOwnProperty是Object.prototype的一个方法,它可是个好东西,他能判断一个对象是否包含自定义属性而不是原型链上的属性,因为hasOwnProperty 是 JavaScript 中唯一一个处理属性但是不查找原型链的函数。 226 | 227 | // 修改Object.prototype 228 | Object.prototype.bar = 1; 229 | var foo = {goo: undefined}; 230 | 231 | foo.bar; // 1 232 | 'bar' in foo; // true 233 | 234 | foo.hasOwnProperty('bar'); // false 235 | foo.hasOwnProperty('goo'); // true 236 | 237 | > 只有 hasOwnProperty 可以给出正确和期望的结果,这在遍历对象的属性时会很有用。 没有其它方法可以用来排除原型链上的属性,而不是定义在对象自身上的属性。 238 | 239 | > 但有个恶心的地方是:JavaScript 不会保护 hasOwnProperty 被非法占用,因此如果一个对象碰巧存在这个属性,就需要使用外部的 hasOwnProperty 函数来获取正确的结果。 240 | 241 | var foo = { 242 | hasOwnProperty: function() { 243 | return false; 244 | }, 245 | bar: 'Here be dragons' 246 | }; 247 | 248 | foo.hasOwnProperty('bar'); // 总是返回 false 249 | 250 | // 使用{}对象的 hasOwnProperty,并将其上下为设置为foo 251 | {}.hasOwnProperty.call(foo, 'bar'); // true 252 | 253 | > 当检查对象上某个属性是否存在时,hasOwnProperty 是唯一可用的方法。同时在使用 for in loop 遍历对象时,推荐总是使用 hasOwnProperty 方法,这将会避免原型对象扩展带来的干扰,我们来看一下例子: 254 | 255 | // 修改 Object.prototype 256 | Object.prototype.bar = 1; 257 | 258 | var foo = {moo: 2}; 259 | for(var i in foo) { 260 | console.log(i); // 输出两个属性:bar 和 moo 261 | } 262 | 263 | > 我们没办法改变for in语句的行为,所以想过滤结果就只能使用hasOwnProperty 方法,代码如下: 264 | 265 | // foo 变量是上例中的 266 | for(var i in foo) { 267 | if (foo.hasOwnProperty(i)) { 268 | console.log(i); 269 | } 270 | } 271 | 272 | > 这个版本的代码是唯一正确的写法。由于我们使用了 hasOwnProperty,所以这次只输出 moo。如果不使用 hasOwnProperty,则这段代码在原生对象原型(比如 Object.prototype)被扩展时可能会出错。 273 | 274 | > 总结:推荐使用 hasOwnProperty,不要对代码运行的环境做任何假设,不要假设原生对象是否已经被扩展了 275 | 276 | > 原型极大地丰富了我们的开发代码,但是在平时使用的过程中一定要注意上述提到的一些注意事项。 277 | 278 | 279 | ## 致谢 280 | 281 | > 参考内容:[http://bonsaiden.github.com/JavaScript-Garden/zh/](http://bonsaiden.github.com/JavaScript-Garden/zh/) 282 | 283 | > 翻译编辑:[汤姆大叔的博客](http://www.cnblogs.com/TomXu/ '汤姆大叔的博客') 284 | 285 | > 原文地址:[深入理解JavaScript系列(5):强大的原型和原型链](http://www.cnblogs.com/TomXu/archive/2012/01/05/2305453.html '深入理解JavaScript系列(5):强大的原型和原型链') 286 | 287 | > 修正/编辑:[ahkjxy](https://github.com/ahkjxy/) -------------------------------------------------------------------------------- /4-立即调用的函数表达式.md: -------------------------------------------------------------------------------- 1 | ## 深入理解JavaScript系列(4):立即调用的函数表达式 2 | 3 | ### 前言 4 | 5 | > 大家学JavaScript的时候,经常遇到自执行匿名函数的代码,今天我们主要就来说说自执行函数([IIFE](http://bbs.9ria.com/thread-113177-1-1.html))。 6 | 7 | > 在详细了解这个之前,我们来谈了解一下“自执行”这个叫法(翻译中的叫法),本文对这个功能的叫法也不一定完全对,主要是看个人如何理解,因为有的人说立即调用,有的人说自动执行,所以你完全可以按照你自己的理解来取一个名字,不过我听很多人都叫它为“自执行”,但作者后面说了很多,来说服大家称呼为“立即调用的函数表达式”。 8 | 9 | > 本文英文原文地址: 10 | 11 | ### 什么是自执行? 12 | 13 | > 在JavaScript里,任何function在执行的时候都会创建一个[执行上下文(Execution Context)](http://blogread.cn/it/article/6178 '深入理解Javascript之执行上下文(Execution Context)'),因为function声明的变量和function有可能只在该function内部、这个上下文,在调用function的时候,提供了一种简单的方式来创建自由变量或私有方法function。 14 | 15 | // 由于该function里返回了另外一个function,其中这个function可以访问自由变量i 16 | // 所有说,这个内部的function实际上是有权限可以调用内部的对象。 17 | 18 | function makeCounter() { 19 | // 只能在makeCounter内部访问i 20 | var i = 0; 21 | 22 | return function () { 23 | console.log(++i); 24 | }; 25 | } 26 | 27 | // 注意,counter和counter2是不同的实例,分别有自己范围内的i。 28 | 29 | var counter = makeCounter(); 30 | counter(); // logs: 1 31 | counter(); // logs: 2 32 | 33 | var counter2 = makeCounter(); 34 | counter2(); // logs: 1 35 | counter2(); // logs: 2 36 | 37 | alert(i); // Uncaught ReferenceError: i is not defined(因为i是存在于makeCounter内部)。 38 | 39 | > 很多情况下,我们不需要makeCounter多个实例,甚至某些case下,我们也不需要显示的返回值,OK,往下看。 40 | 41 | ### 问题的核心 42 | 43 | > 当你声明类似function foo(){}或var foo = function(){}函数的时候,通过在后面加个括弧就可以实现自执行,例如foo(),看代码: 44 | 45 | // 因为想下面第一个声明的function可以在后面加一个括弧()就可以自己执行了,比如foo(), 46 | // 因为foo仅仅是function() { /* code */ }这个表达式的一个引用 47 | 48 | var foo = function(){ /* code */ } // undefined 49 | 50 | // ...是不是意味着后面加个括弧都可以自动执行? 51 | 52 | function(){ /* code */ }(); // Uncaught SyntaxError: Unexpected token 53 | 54 | > 上述代码,运行的话第2个代码会出错,因为在解析器解析全局的function或者function内部function关键字的时候,默认是认为function声明,而不是function表达式,如果你不显示告诉编译器,它默认会声明成一个缺少名字的function,并且抛出一个语法错误信息,**因为function声明需要一个名字**。 55 | 56 | ### 旁白:函数(function),括弧(paren),语法错误(SyntaxError) 57 | 58 | > 有趣的是,即便你为上面那个错误的代码加上一个名字,他也会提示语法错误,只不过和上面的原因不一样。在一个表达式后面加上括号(),该表达式会立即执行,但是在一个语句后面加上括号(),是完全不一样的意思,他只是表示分组操作符。 59 | 60 | // 下面这个function在语法上是没问题的,但是依然只是一个语句 61 | // 加上括号()以后依然会报错,因为分组操作符需要包含表达式 62 | 63 | function foo(){ /* code */ }(); // Uncaught SyntaxError: Unexpected token ) 64 | 65 | // 但是如果你在括弧()里传入一个表达式,将不会有异常抛出 66 | // 但是foo函数依然不会执行 67 | function foo(){ /* code */ }( 1 ); 68 | 69 | // 因为它完全等价于下面这个代码,一个function声明后面,又声明了一个毫无关系的表达式: 70 | function foo(){ /* code */ } 71 | 72 | ( 1 ); 73 | 74 | > 你可以访问[ECMA-262-3 in detail. Chapter 5. Functions](http://dmitrysoshnikov.com/ecmascript/chapter-5-functions/#question-about-surrounding-parentheses 'ECMA-262-3 in detail. Chapter 5. Functions') 获取进一步的信息。 75 | 76 | ### 自执行函数表达式 77 | 78 | > 要解决上述问题,非常简单,我们只需要用大括弧将代码的代码全部包裹起来就行了,因为JavaScript里括弧()里面不能包含语句,所以在这一点上,解析器在解析function关键字的时候,会将相应的代码解析成function表达式,而不是function声明。 79 | 80 | // 下面2个括弧()都会立即执行 81 | 82 | (function () { /* code */ } ()); // 推荐使用这个 83 | (function () { /* code */ })(); // 但是这个也是可以用的 84 | 85 | // 由于括弧()和JS的&&,异或,逗号等操作符是在函数表达式和函数声明上消除歧义的 86 | // 所以一旦解析器知道其中一个已经是表达式了,其它的也都默认为表达式了 87 | // 不过,请注意下一章节的内容解释 88 | 89 | var i = function () { return 10; } (); 90 | true && function () { /* code */ } (); 91 | 0, function () { /* code */ } (); 92 | 93 | // 如果你不在意返回值,或者不怕难以阅读 94 | // 你甚至可以在function前面加一元操作符号 95 | 96 | !function () { /* code */ } (); 97 | ~function () { /* code */ } (); 98 | -function () { /* code */ } (); 99 | +function () { /* code */ } (); 100 | 101 | // 还有一个情况,使用new关键字,也可以用,但我不确定它的效率 102 | // http://twitter.com/kuvos/status/18209252090847232 103 | 104 | new function () { /* code */ } 105 | new function () { /* code */ } () // 如果需要传递参数,只需要加上括弧() 106 | 107 | > 上面所说的括弧是消除歧义的,其实压根就没必要,因为括弧本来内部本来期望的就是函数表达式,但是我们依然用它,主要是为了方便开发人员阅读,当你让这些已经自动执行的表达式赋值给一个变量的时候,我们看到开头有括弧(,很快就能明白,而不需要将代码拉到最后看看到底有没有加括弧。 108 | 109 | ### 用闭包保存状态 110 | 111 | > 和普通function执行的时候传参数一样,自执行的函数表达式也可以这么传参,因为闭包直接可以引用传入的这些参数,利用这些被lock住的传入参数,自执行函数表达式可以有效地保存状态。 112 | 113 | // 这个代码是错误的,因为变量i从来就没被locked住 114 | // 相反,当循环执行以后,我们在点击的时候i才获得数值 115 | // 因为这个时候i才真正获得值 116 | // 所以说无论点击那个链接,最终显示的都是I am link #10(如果有10个a元素的话) 117 | 118 | var elems = document.getElementsByTagName('a'); 119 | 120 | for (var i = 0; i < elems.length; i++) { 121 | 122 | elems[i].addEventListener('click', function (e) { 123 | e.preventDefault(); 124 | alert('I am link #' + i); 125 | }, 'false'); 126 | 127 | } 128 | 129 | // 这个是可以用的,因为他在自执行函数表达式闭包内部 130 | // i的值作为locked的索引存在,在循环执行结束以后,尽管最后i的值变成了a元素总数(例如10) 131 | // 但闭包内部的lockedInIndex值是没有改变,因为他已经执行完毕了 132 | // 所以当点击连接的时候,结果是正确的 133 | 134 | var elems = document.getElementsByTagName('a'); 135 | 136 | for (var i = 0; i < elems.length; i++) { 137 | 138 | (function (lockedInIndex) { 139 | 140 | elems[i].addEventListener('click', function (e) { 141 | e.preventDefault(); 142 | alert('I am link #' + lockedInIndex); 143 | }, 'false'); 144 | 145 | })(i); 146 | 147 | } 148 | 149 | // 你也可以像下面这样应用,在处理函数那里使用自执行函数表达式 150 | // 而不是在addEventListener外部 151 | // 但是相对来说,上面的代码更具可读性 152 | 153 | var elems = document.getElementsByTagName('a'); 154 | 155 | for (var i = 0; i < elems.length; i++) { 156 | 157 | elems[i].addEventListener('click', (function (lockedInIndex) { 158 | return function (e) { 159 | e.preventDefault(); 160 | alert('I am link #' + lockedInIndex); 161 | }; 162 | })(i), 'false'); 163 | 164 | } 165 | 166 | > 其实,上面2个例子里的lockedInIndex变量,也可以换成i,因为和外面的i不在一个作用于,所以不会出现问题,这也是匿名函数 + 闭包的威力。 167 | 168 | ### 自执行匿名函数和立即执行的函数表达式区别 169 | 170 | > 在这篇帖子里,我们一直叫自执行函数,确切的说是自执行匿名函数(Self-executing anonymous function),但英文原文作者一直倡议使用立即调用的函数表达式(Immediately-Invoked Function Expression)这一名称,作者又举了一堆例子来解释,好吧,我们来看看: 171 | 172 | // 这是一个自执行的函数,函数内部执行自身,递归 173 | function foo() { foo(); } 174 | 175 | // 这是一个自执行的匿名函数,因为没有标示名称 176 | // 必须使用arguments.callee属性来执行自己 177 | var foo = function () { arguments.callee(); }; 178 | 179 | // 这可能也是一个自执行的匿名函数,仅仅是foo标示名称[引用它自身](https://segmentfault.com/q/1010000002867883) 180 | // 如果你将foo改变成其它的,你将得到一个used-to-self-execute匿名函数 181 | var foo = function () { foo(); }; 182 | 183 | // 有些人叫这个是自执行的匿名函数(即便它不是),因为它没有调用自身,它只是立即执行而已。 184 | (function () { /* code */ } ()); 185 | 186 | // 为函数表达式添加一个标示名称,可以方便Debug 187 | // 但一定命名了,这个函数就不再是匿名的了 188 | (function foo() { /* code */ } ()); 189 | 190 | // 立即调用的函数表达式(IIFE)也可以自执行,不过可能不常用罢了 191 | (function () { arguments.callee(); } ()); 192 | (function foo() { foo(); } ()); 193 | 194 | // 另外,下面的代码在黑莓5里执行会出错,因为在一个命名的函数表达式里,他的名称是undefined 195 | // 呵呵,奇怪 196 | (function foo() { foo(); } ()); 197 | 198 | > 希望这里的一些例子,可以让大家明白,什么叫自执行,什么叫立即调用。 199 | 200 | > **注:arguments.callee在[ECMAScript 5 strict mode](https://developer.mozilla.org/en/JavaScript/Strict_mode#Differences_in_functions 'ECMAScript 5 strict mode')里被废弃了,所以在这个模式下,其实是不能用的。** 201 | 202 | ### 最后的旁白:Module模式 203 | 204 | > 在讲到这个立即调用的函数表达式的时候,我又想起来了Module模式,如果你还不熟悉这个模式,我们先来看看代码: 205 | 206 | // 创建一个立即调用的匿名函数表达式 207 | // return一个变量,其中这个变量里包含你要暴露的东西 208 | // 返回的这个变量将赋值给counter,而不是外面声明的function自身 209 | 210 | var counter = (function () { 211 | var i = 0; 212 | 213 | return { 214 | get: function () { 215 | return i; 216 | }, 217 | set: function (val) { 218 | i = val; 219 | }, 220 | increment: function () { 221 | return ++i; 222 | } 223 | }; 224 | } ()); 225 | 226 | // counter是一个带有多个属性的对象,上面的代码对于属性的体现其实是方法 227 | 228 | counter.get(); // 0 229 | counter.set(3); 230 | counter.increment(); // 4 231 | counter.increment(); // 5 232 | 233 | counter.i; // undefined 因为i不是返回对象的属性 234 | i; // 引用错误: i 没有定义(因为i只存在于闭包) 235 | 236 | > 关于更多Module模式的介绍,请访问我的上一篇帖子:深入理解JavaScript系列(2):全面解析Module模式 。 237 | 238 | 239 | ## 更多阅读 240 | 241 | > 希望上面的一些例子,能让你对立即调用的函数表达(也就是我们所说的自执行函数)有所了解,如果你想了解更多关于function和Module模式的信息,请继续访问下面列出的网站: 242 | 243 | 1. [ECMA-262-3 in detail. Chapter 5. Functions.](http://dmitrysoshnikov.com/ecmascript/chapter-5-functions/#question-about-surrounding-parentheses 'ECMA-262-3 in detail. Chapter 5. Functions.') - Dmitry A. Soshnikov 244 | 2. [Functions and function scope](https://developer.mozilla.org/en/JavaScript/Reference/Functions_and_function_scope 'Functions and function scope') - Mozilla Developer Network 245 | 3. [Named function expressions](http://kangax.github.com/nfe/ 'Named function expressions') - Juriy “kangax” Zaytsev 246 | 4. [全面解析Module模式](http://www.cnblogs.com/TomXu/archive/2011/12/30/2288372.html '全面解析Module模式')- Ben Cherry(大叔翻译整理) 247 | 5. [Closures explained with JavaScript](http://skilldrick.co.uk/2011/04/closures-explained-with-javascript/ 'Closures explained with JavaScript') - Nick Morgan 248 | 6. [在JavaScript的立即执行的具名函数A内修改A的值时到底发生了什么?](https://segmentfault.com/q/1010000002810093 '在JavaScript的立即执行的具名函数A内修改A的值时到底发生了什么?') Foolyou 249 | 250 | 251 | ## 致谢 252 | 253 | > 翻译编辑:[汤姆大叔的博客](http://www.cnblogs.com/TomXu/ '汤姆大叔的博客') 254 | 255 | > 原文地址:[深入理解JavaScript系列(4):立即调用的函数表达式](http://www.cnblogs.com/TomXu/archive/2011/12/31/2289423.html '深入理解JavaScript系列(4):立即调用的函数表达式') 256 | 257 | > 修正/编辑:[ahkjxy](https://github.com/ahkjxy/) -------------------------------------------------------------------------------- /14-作用域链(Scope Chain).md: -------------------------------------------------------------------------------- 1 | ## 深入理解JavaScript系列(14):作用域链(Scope Chain) 2 | 3 | ### 前言 4 | 5 | > 在第12章关于变量对象的描述中,我们已经知道一个执行上下文 的数据(变量、函数声明和函数的形参)作为属性存储在变量对象中。 6 | 7 | > 同时我们也知道变量对象在每次进入上下文时创建,并填入初始值,值的更新出现在代码执行阶段。 8 | 9 | > 这一章专门讨论与执行上下文直接相关的更多细节,这次我们将提及一个议题——作用域链。 10 | 11 | 英文原文:http://dmitrysoshnikov.com/ecmascript/chapter-4-scope-chain/ 12 | 中文参考:http://www.denisdeng.com/?p=908 13 | 本文绝大部分内容来自上述地址,仅做少许修改,感谢作者 14 | 15 | ### 定义 16 | 17 | > 如果要简要的描述并展示其重点,那么作用域链大多数与内部函数相关。 18 | 19 | > 我们知道,ECMAScript 允许创建内部函数,我们甚至能从父函数中返回这些函数。 20 | 21 | var x = 10; 22 | 23 | function foo() { 24 | var y = 20; 25 | function bar() { 26 | alert(x + y); 27 | } 28 | return bar; 29 | } 30 | 31 | foo()(); // 30 32 | 33 | > 这样,很明显每个上下文拥有自己的变量对象:对于全局上下文,它是全局对象自身;对于函数,它是活动对象。 34 | 35 | > 作用域链正是内部上下文所有变量对象(包括父变量对象)的列表。此链用来变量查询。即在上面的例子中,“bar”上下文的作用域链包括AO(bar)、AO(foo)和VO(global)。 36 | 37 | > 但是,让我们仔细研究这个问题。 38 | 39 | > 让我们从定义开始,并进深一步的讨论示例。 40 | 41 | 作用域链与一个执行上下文相关,变量对象的链用于在标识符解析中变量查找。 42 | 43 | > 函数上下文的作用域链在函数调用时创建的,包含活动对象和这个函数内部的[[scope]]属性。下面我们将更详细的讨论一个函数的[[scope]]属性。 44 | 45 | > 在上下文中示意如下: 46 | 47 | activeExecutionContext = { 48 | VO: {...}, // or AO 49 | this: thisValue, 50 | Scope: [ // Scope chain 51 | // 所有变量对象的列表 52 | // for identifiers lookup 53 | ] 54 | }; 55 | 56 | > 其scope定义如下: 57 | 58 | Scope = AO + [[Scope]] 59 | 60 | > 这种联合和标识符解析过程,我们将在下面讨论,这与函数的生命周期相关。 61 | 62 | ### 函数的生命周期 63 | 64 | > 函数的的生命周期分为创建和激活阶段(调用时),让我们详细研究它。 65 | 66 | #### 函数创建 67 | 68 | > 众所周知,在进入上下文时函数声明放到变量/活动(VO/AO)对象中。让我们看看在全局上下文中的变量和函数声明(这里变量对象是全局对象自身,我们还记得,是吧?) 69 | 70 | var x = 10; 71 | 72 | function foo() { 73 | var y = 20; 74 | alert(x + y); 75 | } 76 | 77 | foo(); // 30 78 | 79 | > 在函数激活时,我们得到正确的(预期的)结果--30。但是,有一个很重要的特点。 80 | 81 | 此前,我们仅仅谈到有关当前上下文的变量对象。这里,我们看到变量“y”在函数“foo”中定义(意味着它在foo上下文的AO中),但是变量“x”并未在“foo”上下文中定义,相应地,它也不会添加到“foo”的AO中。乍一看,变量“x”相对于函数“foo”根本就不存在;但正如我们在下面看到的——也仅仅是“一瞥”,我们发现,“foo”上下文的活动对象中仅包含一个属性--“y”。 82 | 83 | fooContext.AO = { 84 | y: undefined // undefined – 进入上下文的时候是20 – at activation 85 | }; 86 | 87 | > 函数“foo”如何访问到变量“x”?理论上函数应该能访问一个更高一层上下文的变量对象。实际上它正是这样,这种机制是通过函数内部的[[scope]]属性来实现的。 88 | 89 | > [[scope]]是所有父变量对象的层级链,处于当前函数上下文之上,在函数创建时存于其中。 90 | 91 | > 注意这重要的一点--[[scope]]在函数创建时被存储--静态(不变的),永远永远,直至函数销毁。即:函数可以永不调用,但[[scope]]属性已经写入,并存储在函数对象中。 92 | 93 | > 另外一个需要考虑的是--与作用域链对比,[[scope]]是函数的一个属性而不是上下文。考虑到上面的例子,函数“foo”的[[scope]]如下: 94 | 95 | foo.[[Scope]] = [ 96 | globalContext.VO // === Global 97 | ]; 98 | 99 | > 举例来说,我们用通常的ECMAScript 数组展现作用域和[[scope]]。 100 | 101 | > 继续,我们知道在函数调用时进入上下文,这时候活动对象被创建,this和作用域(作用域链)被确定。让我们详细考虑这一时刻。 102 | 103 | #### 函数激活 104 | 105 | > 正如在定义中说到的,进入上下文创建AO/VO之后,上下文的Scope属性(变量查找的一个作用域链)作如下定义: 106 | 107 | Scope = AO|VO + [[Scope]] 108 | 109 | > 上面代码的意思是:活动对象是作用域数组的第一个对象,即添加到作用域的前端。 110 | 111 | Scope = [AO].concat([[Scope]]); 112 | 113 | > 这个特点对于标示符解析的处理来说很重要。 114 | 115 | > 标示符解析是一个处理过程,用来确定一个变量(或函数声明)属于哪个变量对象。 116 | 117 | > 这个算法的返回值中,我们总有一个引用类型,它的base组件是相应的变量对象(或若未找到则为null),属性名组件是向上查找的标示符的名称。引用类型的详细信息在第13章.this中已讨论。 118 | 119 | > 标识符解析过程包含与变量名对应属性的查找,即作用域中变量对象的连续查找,从最深的上下文开始,绕过作用域链直到最上层。 120 | 121 | > 这样一来,在向上查找中,一个上下文中的局部变量较之于父作用域的变量拥有较高的优先级。万一两个变量有相同的名称但来自不同的作用域,那么第一个被发现的是在最深作用域中。 122 | 123 | > 我们用一个稍微复杂的例子描述上面讲到的这些。 124 | 125 | var x = 10; 126 | 127 | function foo() { 128 | var y = 20; 129 | 130 | function bar() { 131 | var z = 30; 132 | alert(x + y + z); 133 | } 134 | 135 | bar(); 136 | } 137 | 138 | foo(); // 60 139 | 140 | > 对此,我们有如下的变量/活动对象,函数的的[[scope]]属性以及上下文的作用域链: 141 | 142 | > 全局上下文的变量对象是: 143 | 144 | globalContext.VO === Global = { 145 | x: 10 146 | foo: 147 | }; 148 | 149 | > 在“foo”创建时,“foo”的[[scope]]属性是: 150 | 151 | foo.[[Scope]] = [ 152 | globalContext.VO 153 | ]; 154 | 155 | > 在“foo”激活时(进入上下文),“foo”上下文的活动对象是: 156 | 157 | fooContext.AO = { 158 | y: 20, 159 | bar: 160 | }; 161 | 162 | > “foo”上下文的作用域链为: 163 | 164 | fooContext.Scope = fooContext.AO + foo.[[Scope]] // i.e.: 165 | 166 | fooContext.Scope = [ 167 | fooContext.AO, 168 | globalContext.VO 169 | ]; 170 | 171 | > 内部函数“bar”创建时,其[[scope]]为: 172 | 173 | bar.[[Scope]] = [ 174 | fooContext.AO, 175 | globalContext.VO 176 | ]; 177 | 178 | > 在“bar”激活时,“bar”上下文的活动对象为: 179 | 180 | barContext.AO = { 181 | z: 30 182 | }; 183 | 184 | > “bar”上下文的作用域链为: 185 | 186 | barContext.Scope = barContext.AO + bar.[[Scope]] // i.e.: 187 | 188 | barContext.Scope = [ 189 | barContext.AO, 190 | fooContext.AO, 191 | globalContext.VO 192 | ]; 193 | 194 | > 对“x”、“y”、“z”的标识符解析如下: 195 | 196 | - "x" 197 | -- barContext.AO // not found 198 | -- fooContext.AO // not found 199 | -- globalContext.VO // found - 10 200 | 201 | - "y" 202 | -- barContext.AO // not found 203 | -- fooContext.AO // found - 20 204 | 205 | - "z" 206 | -- barContext.AO // found - 30 207 | 208 | ### 作用域特征 209 | 210 | > 让我们看看与作用域链和函数[[scope]]属性相关的一些重要特征。 211 | 212 | ### 闭包 213 | 214 | > 在ECMAScript中,闭包与函数的[[scope]]直接相关,正如我们提到的那样,[[scope]]在函数创建时被存储,与函数共存亡。实际上,闭包是函数代码和其[[scope]]的结合。因此,作为其对象之一,[[Scope]]包括在函数内创建的词法作用域(父变量对象)。当函数进一步激活时,在变量对象的这个词法链(静态的存储于创建时)中,来自较高作用域的变量将被搜寻。 215 | 216 | > 例如: 217 | 218 | var x = 10; 219 | 220 | function foo() { 221 | alert(x); 222 | } 223 | 224 | (function () { 225 | var x = 20; 226 | foo(); // 10, but not 20 227 | })(); 228 | 229 | > 我们再次看到,在标识符解析过程中,使用函数创建时定义的词法作用域--变量解析为10,而不是30。此外,这个例子也清晰的表明,一个函数(这个例子中为从函数“foo”返回的匿名函数)的[[scope]]持续存在,即使是在函数创建的作用域已经完成之后。 230 | 231 | > 关于ECMAScript中闭包的理论和其执行机制的更多细节,阅读16章闭包。 232 | 233 | #### 通过构造函数创建的函数的[[scope]] 234 | 235 | > 在上面的例子中,我们看到,在函数创建时获得函数的[[scope]]属性,通过该属性访问到所有父上下文的变量。但是,这个规则有一个重要的例外,它涉及到通过函数构造函数创建的函数。 236 | 237 | var x = 10; 238 | 239 | function foo() { 240 | 241 | var y = 20; 242 | 243 | function barFD() { // 函数声明 244 | alert(x); 245 | alert(y); 246 | } 247 | 248 | var barFE = function () { // 函数表达式 249 | alert(x); 250 | alert(y); 251 | }; 252 | 253 | var barFn = Function('alert(x); alert(y);'); 254 | 255 | barFD(); // 10, 20 256 | barFE(); // 10, 20 257 | barFn(); // 10, "y" is not defined 258 | 259 | } 260 | 261 | foo(); 262 | 263 | > 我们看到,通过函数构造函数(Function constructor)创建的函数“bar”,是不能访问变量“y”的。但这并不意味着函数“barFn”没有[[scope]]属性(否则它不能访问到变量“x”)。问题在于通过函构造函数创建的函数的[[scope]]属性总是唯一的全局对象。考虑到这一点,如通过这种函数创建除全局之外的最上层的上下文闭包是不可能的。 264 | 265 | #### 二维作用域链查找 266 | 267 | > 在作用域链中查找最重要的一点是变量对象的属性(如果有的话)须考虑其中--源于ECMAScript 的原型特性。如果一个属性在对象中没有直接找到,查询将在原型链中继续。即常说的二维链查找。(1)作用域链环节;(2)每个作用域链--深入到原型链环节。如果在Object.prototype 中定义了属性,我们能看到这种效果。 268 | 269 | function foo() { 270 | alert(x); 271 | } 272 | 273 | Object.prototype.x = 10; 274 | 275 | foo(); // 10 276 | 277 | > 活动对象没有原型,我们可以在下面的例子中看到: 278 | 279 | function foo() { 280 | 281 | var x = 20; 282 | 283 | function bar() { 284 | alert(x); 285 | } 286 | 287 | bar(); 288 | } 289 | 290 | Object.prototype.x = 10; 291 | 292 | foo(); // 20 293 | 294 | > 如果函数“bar”上下文的激活对象有一个原型,那么“x”将在Object.prototype 中被解析,因为它在AO中不被直接解析。但在上面的第一个例子中,在标识符解析中,我们到达全局对象(在一些执行中并不全是这样),它从Object.prototype继承而来,响应地,“x”解析为10。 295 | 296 | > 同样的情况出现在一些版本的SpiderMokey 的命名函数表达式(缩写为NFE)中,在那里特定的对象存储从Object.prototype继承而来的函数表达式的可选名称,在Blackberry中的一些版本中,执行时激活对象从Object.prototype继承。但是,关于该特色的更多细节在第15章函数讨论。 297 | 298 | #### 全局和eval上下文中的作用域链 299 | 300 | > 这里不一定很有趣,但必须要提示一下。全局上下文的作用域链仅包含全局对象。代码eval的上下文与当前的调用上下文(calling context)拥有同样的作用域链。 301 | 302 | globalContext.Scope = [ 303 | Global 304 | ]; 305 | 306 | evalContext.Scope === callingContext.Scope; 307 | 308 | #### 代码执行时对作用域链的影响 309 | 310 | > 在ECMAScript 中,在代码执行阶段有两个声明能修改作用域链。这就是with声明和catch语句。它们添加到作用域链的最前端,对象须在这些声明中出现的标识符中查找。如果发生其中的一个,作用域链简要的作如下修改: 311 | 312 | Scope = withObject|catchObject + AO|VO + [[Scope]] 313 | 314 | > 在这个例子中添加对象,对象是它的参数(这样,没有前缀,这个对象的属性变得可以访问)。 315 | 316 | var foo = {x: 10, y: 20}; 317 | 318 | with (foo) { 319 | alert(x); // 10 320 | alert(y); // 20 321 | } 322 | 323 | > 作用域链修改成这样: 324 | 325 | Scope = foo + AO|VO + [[Scope]] 326 | 327 | > 我们再次看到,通过with语句,对象中标识符的解析添加到作用域链的最前端: 328 | 329 | var x = 10, y = 10; 330 | 331 | with ({x: 20}) { 332 | 333 | var x = 30, y = 30; 334 | 335 | alert(x); // 30 336 | alert(y); // 30 337 | } 338 | 339 | alert(x); // 10 340 | alert(y); // 30 341 | 342 | > 在进入上下文时发生了什么?标识符“x”和“y”已被添加到变量对象中。此外,在代码运行阶段作如下修改: 343 | 344 | 1. x = 10, y = 10; 345 | 1. 对象{x:20}添加到作用域的前端; 346 | 1. 在with内部,遇到了var声明,当然什么也没创建,因为在进入上下文时,所有变量已被解析添加; 347 | 1. 在第二步中,仅修改变量“x”,实际上对象中的“x”现在被解析,并添加到作用域链的最前端,“x”为20,变为30; 348 | 1. 同样也有变量对象“y”的修改,被解析后其值也相应的由10变为30; 349 | 1. 此外,在with声明完成后,它的特定对象从作用域链中移除(已改变的变量“x”--30也从那个对象中移除),即作用域链的结构恢复到with得到加强以前的状态。 350 | 1. 在最后两个alert中,当前变量对象的“x”保持同一,“y”的值现在等于30,在with声明运行中已发生改变。 351 | 352 | > 同样,catch语句的异常参数变得可以访问,它创建了只有一个属性的新对象--异常参数名。图示看起来像这样: 353 | 354 | try { 355 | ... 356 | } catch (ex) { 357 | alert(ex); 358 | } 359 | 360 | > 作用域链修改为: 361 | 362 | var catchObject = { 363 | ex: 364 | }; 365 | 366 | Scope = catchObject + AO|VO + [[Scope]] 367 | 368 | > 在catch语句完成运行之后,作用域链恢复到以前的状态。 369 | 370 | ### 结论 371 | 372 | > 在这个阶段,我们几乎考虑了与执行上下文相关的所有常用概念,以及与它们相关的细节。按照计划--函数对象的详细分析:函数类型(函数声明,函数表达式)和闭包。顺便说一下,在这篇文章中,闭包直接与[[scope]]属性相关,但是,关于它将在合适的篇章中讨论。我很乐意在评论中回答你的问题。 373 | 374 | ## 致谢 375 | 376 | > 8.6.2 – [[[Scope]]](http://bclary.com/2004/11/07/#a-8.6.2) 377 | 378 | > 10.1.4 – [Scope Chain and Identifier Resolution](http://bclary.com/2004/11/07/#a-10.1.4) 379 | 380 | > 翻译编辑:[汤姆大叔的博客](http://www.cnblogs.com/TomXu/ '汤姆大叔的博客') 381 | 382 | > 原文地址:[深入理解JavaScript系列(14):作用域链(Scope Chain)](http://www.cnblogs.com/TomXu/archive/2012/01/18/2312463.html '深入理解JavaScript系列(14):作用域链(Scope Chain)') 383 | 384 | > 修正/编辑:[ahkjxy](https://github.com/ahkjxy/) -------------------------------------------------------------------------------- /7-S.O.L.I.D五大原则之开闭原则OCP.md: -------------------------------------------------------------------------------- 1 | ## 深入理解JavaScript系列(7):S.O.L.I.D五大原则之开闭原则OCP 2 | 3 | ### 前言 4 | 5 | > 本章我们要讲解的是S.O.L.I.D五大原则JavaScript语言实现的第2篇,开闭原则OCP(The Open/Closed Principle )。 6 | 7 | > 开闭原则的描述是: 8 | 9 | Software entities (classes, modules, functions, etc.) should be open for extension but closed for modification. 10 | 软件实体(类,模块,方法等等)应当对扩展开放,对修改关闭,即软件实体应当在不修改的前提下扩展。 11 | 12 | > open for extension(对扩展开放)的意思是说当新需求出现的时候,可以通过扩展现有模型达到目的。而Close for modification(对修改关闭)的意思是说不允许对该实体做任何修改,说白了,就是这些需要执行多样行为的实体应该设计成不需要修改就可以实现各种的变化,坚持开闭原则有利于用最少的代码进行项目维护。 13 | 14 | > 英文原文: 15 | 16 | ### 问题代码 17 | 18 | > 为了直观地描述,我们来举个例子演示一下,下属代码是动态展示question列表的代码(没有使用开闭原则)。 19 | 20 | // 问题类型 21 | var AnswerType = { 22 | Choice: 0, 23 | Input: 1 24 | }; 25 | 26 | // 问题实体 27 | function question(label, answerType, choices) { 28 | return { 29 | label: label, 30 | answerType: answerType, 31 | choices: choices // 这里的choices是可选参数 32 | }; 33 | } 34 | 35 | var view = (function () { 36 | // render一个问题 37 | function renderQuestion(target, question) { 38 | var questionWrapper = document.createElement('div'); 39 | questionWrapper.className = 'question'; 40 | 41 | var questionLabel = document.createElement('div'); 42 | questionLabel.className = 'question-label'; 43 | var label = document.createTextNode(question.label); 44 | questionLabel.appendChild(label); 45 | 46 | var answer = document.createElement('div'); 47 | answer.className = 'question-input'; 48 | 49 | // 根据不同的类型展示不同的代码:分别是下拉菜单和输入框两种 50 | if (question.answerType === AnswerType.Choice) { 51 | var input = document.createElement('select'); 52 | var len = question.choices.length; 53 | for (var i = 0; i < len; i++) { 54 | var option = document.createElement('option'); 55 | option.text = question.choices[i]; 56 | option.value = question.choices[i]; 57 | input.appendChild(option); 58 | } 59 | } 60 | else if (question.answerType === AnswerType.Input) { 61 | var input = document.createElement('input'); 62 | input.type = 'text'; 63 | } 64 | 65 | answer.appendChild(input); 66 | questionWrapper.appendChild(questionLabel); 67 | questionWrapper.appendChild(answer); 68 | target.appendChild(questionWrapper); 69 | } 70 | 71 | return { 72 | // 遍历所有的问题列表进行展示 73 | render: function (target, questions) { 74 | for (var i = 0; i < questions.length; i++) { 75 | renderQuestion(target, questions[i]); 76 | }; 77 | } 78 | }; 79 | })(); 80 | 81 | var questions = [ 82 | question('Have you used tobacco products within the last 30 days?', AnswerType.Choice, ['Yes', 'No']), 83 | question('What medications are you currently using?', AnswerType.Input) 84 | ]; 85 | 86 | var questionRegion = document.getElementById('questions'); 87 | view.render(questionRegion, questions); 88 | 89 | > 上面的代码,view对象里包含一个render方法用来展示question列表,展示的时候根据不同的question类型使用不同的展示方式,一个question包含一个label和一个问题类型以及choices的选项(如果是选择类型的话)。如果问题类型是Choice那就根据选项生产一个下拉菜单,如果类型是Input,那就简单地展示input输入框。 90 | 91 | > 该代码有一个限制,就是如果再增加一个question类型的话,那就需要再次修改renderQuestion里的条件语句,这明显违反了开闭原则。 92 | 93 | ### 重构代码 94 | 95 | > 让我们来重构一下这个代码,以便在出现新question类型的情况下允许扩展view对象的render能力,而不需要修改view对象内部的代码。 96 | 97 | > 先来创建一个通用的questionCreator函数: 98 | 99 | function questionCreator(spec, my) { 100 | var that = {}; 101 | 102 | my = my || {}; 103 | my.label = spec.label; 104 | 105 | my.renderInput = function () { 106 | throw "not implemented"; 107 | // 这里renderInput没有实现,主要目的是让各自问题类型的实现代码去覆盖整个方法 108 | }; 109 | 110 | that.render = function (target) { 111 | var questionWrapper = document.createElement('div'); 112 | questionWrapper.className = 'question'; 113 | 114 | var questionLabel = document.createElement('div'); 115 | questionLabel.className = 'question-label'; 116 | var label = document.createTextNode(spec.label); 117 | questionLabel.appendChild(label); 118 | 119 | var answer = my.renderInput(); 120 | // 该render方法是同样的粗合理代码 121 | // 唯一的不同就是上面的一句my.renderInput() 122 | // 因为不同的问题类型有不同的实现 123 | 124 | questionWrapper.appendChild(questionLabel); 125 | questionWrapper.appendChild(answer); 126 | return questionWrapper; 127 | }; 128 | 129 | return that; 130 | } 131 | 132 | > 该代码的作用组合要是render一个问题,同时提供一个未实现的renderInput方法以便其他function可以覆盖,以使用不同的问题类型,我们继续看一下每个问题类型的实现代码: 133 | 134 | function choiceQuestionCreator(spec) { 135 | 136 | var my = {}, 137 | that = questionCreator(spec, my); 138 | 139 | // choice类型的renderInput实现 140 | my.renderInput = function () { 141 | var input = document.createElement('select'); 142 | var len = spec.choices.length; 143 | for (var i = 0; i < len; i++) { 144 | var option = document.createElement('option'); 145 | option.text = spec.choices[i]; 146 | option.value = spec.choices[i]; 147 | input.appendChild(option); 148 | } 149 | 150 | return input; 151 | }; 152 | 153 | return that; 154 | } 155 | 156 | function inputQuestionCreator(spec) { 157 | 158 | var my = {}, 159 | that = questionCreator(spec, my); 160 | 161 | // input类型的renderInput实现 162 | my.renderInput = function () { 163 | var input = document.createElement('input'); 164 | input.type = 'text'; 165 | return input; 166 | }; 167 | 168 | return that; 169 | } 170 | 171 | > choiceQuestionCreator函数和inputQuestionCreator函数分别对应下拉菜单和input输入框的renderInput实现,通过内部调用统一的questionCreator(spec, my)然后返回that对象(同一类型哦)。 172 | 173 | > view对象的代码就很固定了。 174 | 175 | var view = { 176 | render: function(target, questions) { 177 | for (var i = 0; i < questions.length; i++) { 178 | target.appendChild(questions[i].render()); 179 | } 180 | } 181 | }; 182 | 183 | > 所以我们声明问题的时候只需要这样做,就OK了: 184 | 185 | var questions = [ 186 | choiceQuestionCreator({ 187 | label: 'Have you used tobacco products within the last 30 days?', 188 | choices: ['Yes', 'No'] 189 |   }), 190 | inputQuestionCreator({ 191 | label: 'What medications are you currently using?' 192 |   }) 193 | ]; 194 | 195 | > 最终的使用代码,我们可以这样来用: 196 | 197 | var questionRegion = document.getElementById('questions'); 198 | 199 | view.render(questionRegion, questions); 200 | 201 | > 最终代码: 202 | 203 | function questionCreator(spec, my) { 204 | var that = {}; 205 | 206 | my = my || {}; 207 | my.label = spec.label; 208 | 209 | my.renderInput = function() { 210 | throw "not implemented"; 211 | }; 212 | 213 | that.render = function(target) { 214 | var questionWrapper = document.createElement('div'); 215 | questionWrapper.className = 'question'; 216 | 217 | var questionLabel = document.createElement('div'); 218 | questionLabel.className = 'question-label'; 219 | var label = document.createTextNode(spec.label); 220 | questionLabel.appendChild(label); 221 | 222 | var answer = my.renderInput(); 223 | 224 | questionWrapper.appendChild(questionLabel); 225 | questionWrapper.appendChild(answer); 226 | return questionWrapper; 227 | }; 228 | 229 | return that; 230 | } 231 | 232 | function choiceQuestionCreator(spec) { 233 | 234 | var my = {}, 235 | that = questionCreator(spec, my); 236 | 237 | my.renderInput = function() { 238 | var input = document.createElement('select'); 239 | var len = spec.choices.length; 240 | for (var i = 0; i < len; i++) { 241 | var option = document.createElement('option'); 242 | option.text = spec.choices[i]; 243 | option.value = spec.choices[i]; 244 | input.appendChild(option); 245 | } 246 | 247 | return input; 248 | }; 249 | 250 | return that; 251 | } 252 | 253 | function inputQuestionCreator(spec) { 254 | 255 | var my = {}, 256 | that = questionCreator(spec, my); 257 | 258 | my.renderInput = function() { 259 | var input = document.createElement('input'); 260 | input.type = 'text'; 261 | return input; 262 | }; 263 | 264 | return that; 265 | } 266 | 267 | var view = { 268 | render: function(target, questions) { 269 | for (var i = 0; i < questions.length; i++) { 270 | target.appendChild(questions[i].render()); 271 | } 272 | } 273 | }; 274 | 275 | var questions = [ 276 | choiceQuestionCreator({ 277 | label: 'Have you used tobacco products within the last 30 days?', 278 | choices: ['Yes', 'No'] 279 | }), 280 | inputQuestionCreator({ 281 | label: 'What medications are you currently using?' 282 | }) 283 | ]; 284 | 285 | var questionRegion = document.getElementById('questions'); 286 | 287 | view.render(questionRegion, questions); 288 | 289 | > 上面的代码里应用了一些技术点,我们来逐一看一下: 290 | 291 | 1. 首先,questionCreator方法的创建,可以让我们使用[模板方法模式](http://en.wikipedia.org/wiki/Template_method_pattern)将处理问题的功能delegat给针对每个问题类型的扩展代码renderInput上。 292 | 2. 其次,我们用一个私有的spec属性替换掉了前面question方法的构造函数属性,因为我们封装了render行为进行操作,不再需要把这些属性暴露给外部代码了。 293 | 3. 第三,我们为每个问题类型创建一个对象进行各自的代码实现,但每个实现里都必须包含renderInput方法以便覆盖questionCreator方法里的renderInput代码,这就是我们常说的[策略模式](http://en.wikipedia.org/wiki/Strategy_pattern)。 294 | 295 | > 通过重构,我们可以去除不必要的问题类型的枚举AnswerType,而且可以让choices作为choiceQuestionCreator函数的必选参数(之前的版本是一个可选参数)。 296 | 297 | ## 总结 298 | 299 | > 重构以后的版本的view对象可以很清晰地进行新的扩展了,为不同的问题类型扩展新的对象,然后声明questions集合的时候再里面指定类型就行了,view对象本身不再修改任何改变,从而达到了开闭原则的要求。 300 | 301 | > 另:懂C#的话,不知道看了上面的代码后是否和多态的实现有些类似?其实上述的代码用原型也是可以实现的,大家可以自行研究一下。 302 | 303 | ## 致谢 304 | 305 | > 翻译编辑:[汤姆大叔的博客](http://www.cnblogs.com/TomXu/ '汤姆大叔的博客') 306 | 307 | > 原文地址:[深入理解JavaScript系列(7):S.O.L.I.D五大原则之开闭原则OCP](http://www.cnblogs.com/TomXu/archive/2012/01/09/2306329.html '深入理解JavaScript系列(7):S.O.L.I.D五大原则之开闭原则OCP') 308 | 309 | > 修正/编辑:[ahkjxy](https://github.com/ahkjxy/) -------------------------------------------------------------------------------- /12-变量对象(Variable Object).md: -------------------------------------------------------------------------------- 1 | ## 深入理解JavaScript系列(12):变量对象(Variable Object) 2 | 3 | ### 介绍 4 | 5 | > JavaScript编程的时候总避免不了声明函数和变量,以成功构建我们的系统,但是解释器是如何并且在什么地方去查找这些函数和变量呢?我们引用这些对象的时候究竟发生了什么? 6 | 7 | 原始发布:Dmitry A. Soshnikov 8 | 发布时间:2009-06-27 9 | 俄文地址:http://dmitrysoshnikov.com/ecmascript/ru-chapter-2-variable-object/ 10 | 11 | 英文翻译:Dmitry A. Soshnikov 12 | 发布时间:2010-03-15 13 | 英文地址:http://dmitrysoshnikov.com/ecmascript/chapter-2-variable-object/ 14 | 15 | 部分难以翻译的句子参考了[justinw的中文翻译](http://www.cnblogs.com/justinw/archive/2010/04/23/1718733.html) 16 | 17 | > 大多数ECMAScript程序员应该都知道变量与执行上下文有密切关系: 18 | 19 | var a = 10; // 全局上下文中的变量 20 | 21 | (function () { 22 | var b = 20; // function上下文中的局部变量 23 | })(); 24 | 25 | alert(a); // 10 26 | alert(b); // Uncaught ReferenceError: b is not defined 27 | 28 | > 并且,很多程序员也都知道,当前ECMAScript规范指出独立作用域只能通过“函数(function)”代码类型的执行上下文创建。也就是说,相对于C/C++来说,ECMAScript里的for循环并不能创建一个局部的上下文。 29 | 30 | for (var k in {a: 1, b: 2}) { 31 | alert(k); 32 | } 33 | 34 | alert(k); // 尽管循环已经结束但变量k依然在当前作用域 alert: a alert: b alert: b 35 | 36 | > 我们来看看一下,我们声明数据的时候到底都发现了什么细节。 37 | 38 | ### 数据声明 39 | 40 | > 如果变量与执行上下文相关,那变量自己应该知道它的数据存储在哪里,并且知道如何访问。这种机制称为变量对象(variable object)。 41 | 42 | 变量对象(缩写为VO)是一个与执行上下文相关的特殊对象,它存储着在上下文中声明的以下内容: 43 | 变量 (var, 变量声明); 44 | 函数声明 (FunctionDeclaration, 缩写为FD); 45 | 函数的形参 46 | 47 | > 举例来说,我们可以用普通的ECMAScript对象来表示一个变量对象: 48 | 49 | VO = {}; 50 | 51 | > 就像我们所说的, VO就是执行上下文的属性(property): 52 | 53 | activeExecutionContext = { 54 | VO: { 55 | // 上下文数据(var, FD, function arguments) 56 | } 57 | }; 58 | 59 | > 只有全局上下文的变量对象允许通过VO的属性名称来间接访问(因为在全局上下文里,全局对象自身就是变量对象,稍后会详细介绍),在其它上下文中是不能直接访问VO对象的,因为它只是内部机制的一个实现。 60 | 61 | > 当我们声明一个变量或一个函数的时候,和我们创建VO新属性的时候一样没有别的区别(即:有名称以及对应的值)。 62 | 63 | > 例如: 64 | 65 | var a = 10; 66 | 67 | function test(x) { 68 | var b = 20; 69 | }; 70 | 71 | test(30); // undefined 72 | 73 | > 对应的变量对象是: 74 | 75 | // 全局上下文的变量对象 76 | VO(globalContext) = { 77 | a: 10, 78 | test: 79 | }; 80 | 81 | // test函数上下文的变量对象 82 | VO(test functionContext) = { 83 | x: 30, 84 | b: 20 85 | }; 86 | 87 | > 在具体实现层面(以及规范中)变量对象只是一个抽象概念。(从本质上说,在具体执行上下文中,VO名称是不一样的,并且初始结构也不一样。 88 | 89 | ### 不同执行上下文中的变量对象 90 | 91 | > 对于所有类型的执行上下文来说,变量对象的一些操作(如变量初始化)和行为都是共通的。从这个角度来看,把变量对象作为抽象的基本事物来理解更为容易。同样在函数上下文中也定义和变量对象相关的额外内容。 92 | 93 | 抽象变量对象VO (变量初始化过程的一般行为) 94 | ║ 95 | ╠══> 全局上下文变量对象GlobalContextVO 96 | ║ (VO === this === global) 97 | ║ 98 | ╚══> 函数上下文变量对象FunctionContextVO 99 | (VO === AO, 并且添加了) 100 | 101 | > 我们来详细看一下: 102 | 103 | #### 全局上下文中的变量对象 104 | 105 | > 首先,我们要给全局对象一个明确的定义: 106 | 107 | 全局对象(Global object) 是在进入任何执行上下文之前就已经创建了的对象; 108 | 这个对象只存在一份,它的属性在程序中任何地方都可以访问,全局对象的生命周期终止于程序退出那一刻。 109 | 110 | > 全局对象初始创建阶段将Math、String、Date、parseInt作为自身属性,等属性初始化,同样也可以有额外创建的其它对象作为属性(其可以指向到全局对象自身)。例如,在DOM中,全局对象的window属性就可以引用全局对象自身(当然,并不是所有的具体实现都是这样): 111 | 112 | global = { 113 | Math: <...>, 114 | String: <...> 115 | ... 116 | ... 117 | window: global //引用自身 118 | }; 119 | 120 | > 当访问全局对象的属性时通常会忽略掉前缀,这是因为全局对象是不能通过名称直接访问的。不过我们依然可以通过全局上下文的this来访问全局对象,同样也可以递归引用自身。例如,DOM中的window。综上所述,代码可以简写为: 121 | 122 | String(10); // 就是global.String(10); 123 | 124 | // 带有前缀 125 | window.a = 10; // === global.window.a = 10 === global.a = 10; 126 | this.b = 20; // global.b = 20; 127 | 128 | > 因此,回到全局上下文中的变量对象——在这里,变量对象就是全局对象自己: 129 | 130 | VO(globalContext) === global; 131 | 132 | > 非常有必要要理解上述结论,基于这个原理,在全局上下文中声明的对应,我们才可以间接通过全局对象的属性来访问它(例如,事先不知道变量名称)。 133 | 134 | var a = new String('test'); 135 | 136 | alert(a); // 直接访问,在VO(globalContext)里找到:"test" 137 | 138 | alert(window['a']); // 间接通过global访问:global === VO(globalContext): "test" 139 | alert(a === this.a); // true 140 | 141 | var aKey = 'a'; 142 | alert(window[aKey]); // 间接通过动态属性名称访问:"test" 143 | 144 | #### 函数上下文中的变量对象 145 | 146 | > 在函数执行上下文中,VO是不能直接访问的,此时由活动对象(activation object,缩写为AO)扮演VO的角色。 147 | 148 | VO(functionContext) === AO; 149 | 150 | > 活动对象是在进入函数上下文时刻被创建的,它通过函数的arguments属性初始化。arguments属性的值是Arguments对象: 151 | 152 | AO = { 153 | arguments: 154 | }; 155 | 156 | > Arguments对象是活动对象的一个属性,它包括如下属性: 157 | 158 | 1. callee — 指向当前函数的引用 159 | 1. length — 真正传递的参数个数 160 | 1. properties-indexes (字符串类型的整数) 属性的值就是函数的参数值(按参数列表从左到右排列)。 properties-indexes内部元素的个数等于arguments.length. properties-indexes 的值和实际传递进来的参数之间是共享的。 161 | 162 | > 例如: 163 | 164 | function foo(x, y, z) { 165 | 166 | // 声明的函数参数数量arguments (x, y, z) 167 | alert(foo.length); // 3 168 | 169 | // 真正传进来的参数个数(only x, y) 170 | alert(arguments.length); // 2 171 | 172 | // 参数的callee是函数自身 173 | alert(arguments.callee === foo); // true 174 | 175 | // 参数共享 176 | 177 | alert(x === arguments[0]); // true 178 | alert(x); // 10 179 | 180 | arguments[0] = 20; 181 | alert(x); // 20 182 | 183 | x = 30; 184 | alert(arguments[0]); // 30 185 | 186 | // 不过,没有传进来的参数z,和参数的第3个索引值是不共享的 187 | 188 | z = 40; 189 | alert(arguments[2]); // undefined 190 | 191 | arguments[2] = 50; 192 | alert(z); // 40 193 | 194 | } 195 | 196 | foo(10, 20); 197 | 198 | ### 处理上下文代码的2个阶段 199 | 200 | > 现在我们终于到了本文的核心点了。执行上下文的代码被分成两个基本的阶段来处理: 201 | 202 | 1. 进入执行上下文 203 | 1. 执行代码 204 | 205 | > 变量对象的修改变化与这两个阶段紧密相关。 206 | 207 | > 注:这2个阶段的处理是一般行为,和上下文的类型无关(也就是说,在全局上下文和函数上下文中的表现是一样的)。 208 | 209 | #### 进入执行上下文 210 | 211 | > 当进入执行上下文(代码执行之前)时,VO里已经包含了下列属性(前面已经说了): 212 | 213 | > **函数的所有形参(如果我们是在函数执行上下文中)** 214 | 215 | > — 由名称和对应值组成的一个变量对象的属性被创建;没有传递对应参数的话,那么由名称和undefined值组成的一种变量对象的属性也将被创建。 216 | 217 | > **所有函数声明(FunctionDeclaration, FD)** 218 | 219 | > —由名称和对应值(函数对象(function-object))组成一个变量对象的属性被创建;如果变量对象已经存在相同名称的属性,则完全替换这个属性。 220 | 221 | > **所有变量声明(var, VariableDeclaration)** 222 | 223 | > — 由名称和对应值(undefined)组成一个变量对象的属性被创建;如果变量名称跟已经声明的形式参数或函数相同,则变量声明不会干扰已经存在的这类属性。 224 | 225 | > 让我们看一个例子: 226 | 227 | function test(a, b) { 228 | var c = 10; 229 | function d() {} 230 | var e = function _e() {}; 231 | (function x() {}); 232 | } 233 | 234 | test(10); // call undefined 235 | 236 | > 当进入带有参数10的test函数上下文时,AO表现为如下: 237 | 238 | AO(test) = { 239 | a: 10, 240 | b: undefined, 241 | c: undefined, 242 | d: 243 | e: undefined 244 | }; 245 | 246 | > 注意,AO里并不包含函数“x”。这是因为“x” 是一个函数表达式(FunctionExpression, 缩写为 FE) 而不是函数声明,函数表达式不会影响VO。 不管怎样,函数“_e” 同样也是函数表达式,但是就像我们下面将看到的那样,因为它分配给了变量 “e”,所以它可以通过名称“e”来访问。 函数声明FunctionDeclaration与函数表达式FunctionExpression 的不同,将在第15章Functions进行详细的探讨,也可以参考本系列[第2章揭秘命名函数表达式](http://www.cnblogs.com/TomXu/archive/2011/12/29/2290308.html)来了解。 247 | 248 | > 这之后,将进入处理上下文代码的第二个阶段 — 执行代码。 249 | 250 | #### 代码执行 251 | 252 | > 这个周期内,AO/VO已经拥有了属性(不过,并不是所有的属性都有值,大部分属性的值还是系统默认的初始值undefined )。 253 | 254 | > 还是前面那个例子, AO/VO在代码解释期间被修改如下: 255 | 256 | AO['c'] = 10; 257 | AO['e'] = ; 258 | 259 | > 再次注意,因为FunctionExpression“_e”保存到了已声明的变量“e”上,所以它仍然存在于内存中。而FunctionExpression “x”却不存在于AO/VO中,也就是说如果我们想尝试调用“x”函数,不管在函数定义之前还是之后,都会出现一个错误“x is not defined”,未保存的函数表达式只有在它自己的定义或递归中才能被调用。 260 | 261 | > 另一个经典例子: 262 | 263 | alert(x); // function 264 | 265 | var x = 10; 266 | alert(x); // 10 267 | 268 | x = 20; 269 | 270 | function x() {}; 271 | 272 | alert(x); // 20 273 | 274 | > 为什么第一个alert “x” 的返回值是function,而且它还是在“x” 声明之前访问的“x” 的?为什么不是10或20呢?因为,根据规范函数声明是在当进入上下文时填入的; 同意周期,在进入上下文的时候还有一个变量声明“x”,那么正如我们在上一个阶段所说,变量声明在顺序上跟在函数声明和形式参数声明之后,而且在这个进入上下文阶段,变量声明不会干扰VO中已经存在的同名函数声明或形式参数声明,因此,在进入上下文时,VO的结构如下: 275 | 276 | VO = {}; 277 | 278 | VO['x'] = 279 | 280 | // 找到var x = 10; 281 | // 如果function "x"没有已经声明的话 282 | // 这时候"x"的值应该是undefined 283 | // 但是这个case里变量声明没有影响同名的function的值 284 | 285 | VO['x'] = 286 | 287 | > 紧接着,在执行代码阶段,VO做如下修改: 288 | 289 | VO['x'] = 10; 290 | VO['x'] = 20; 291 | 292 | > 我们可以在第二、三个alert看到这个效果。 293 | 294 | > 在下面的例子里我们可以再次看到,变量是在进入上下文阶段放入VO中的。(因为,虽然else部分代码永远不会执行,但是不管怎样,变量“b”仍然存在于VO中。) 295 | 296 | if (true) { 297 | var a = 1; 298 | } else { 299 | var b = 2; 300 | } 301 | 302 | alert(a); // 1 303 | alert(b); // undefined,不是b没有声明,而是b的值是undefined 304 | 305 | ### 关于变量 306 | 307 | > 通常,各类文章和JavaScript相关的书籍都声称:“不管是使用var关键字(在全局上下文)还是不使用var关键字(在任何地方),都可以声明一个变量”。请记住,这是错误的概念: 308 | 309 | **任何时候,变量只能通过使用var关键字才能声明。** 310 | 311 | > 上面的赋值语句: 312 | 313 | a = 10; 314 | 315 | > 这仅仅是给全局对象创建了一个新属性(但它不是变量)。“不是变量”并不是说它不能被改变,而是指它不符合ECMAScript规范中的变量概念,所以它“不是变量”(它之所以能成为全局对象的属性,完全是因为VO(globalContext) === global,大家还记得这个吧?)。 316 | 317 | > 让我们通过下面的实例看看具体的区别吧: 318 | 319 | alert(a); // undefined 320 | alert(b); // "b" 没有声明 321 | 322 | b = 10; 323 | var a = 20; 324 | 325 | > 所有根源仍然是VO和进入上下文阶段和代码执行阶段: 326 | 327 | > 进入上下文阶段: 328 | 329 | VO = { 330 | a: undefined 331 | }; 332 | 333 | > 我们可以看到,因为“b”不是一个变量,所以在这个阶段根本就没有“b”,“b”将只在代码执行阶段才会出现(但是在我们这个例子里,还没有到那就已经出错了)。 334 | 335 | > 让我们改变一下例子代码: 336 | 337 | alert(a); // undefined, 这个大家都知道, 338 | 339 | b = 10; 340 | alert(b); // 10, 代码执行阶段创建 341 | 342 | var a = 20; 343 | alert(a); // 20, 代码执行阶段修改 344 | 345 | > 关于变量,还有一个重要的知识点。变量相对于简单属性来说,变量有一个特性(attribute):{DontDelete},这个特性的含义就是不能用delete操作符直接删除变量属性。 346 | 347 | a = 10; 348 | alert(window.a); // 10 349 | 350 | alert(delete a); // true 351 | 352 | alert(window.a); // undefined 353 | 354 | var b = 20; 355 | alert(window.b); // 20 356 | 357 | alert(delete b); // false 358 | 359 | alert(window.b); // still 20 360 | 361 | > 但是这个规则在有个上下文里不起走样,那就是eval上下文,变量没有{DontDelete}特性。 362 | 363 | eval('var a = 10;'); 364 | alert(window.a); // 10 365 | 366 | alert(delete a); // true 367 | 368 | alert(window.a); // undefined 369 | 370 | > 使用一些调试工具(例如:Firebug)的控制台测试该实例时,请注意,Firebug同样是使用eval来执行控制台里你的代码。因此,变量属性同样没有{**DontDelete**}特性,可以被删除。 371 | 372 | ### 特殊实现: __parent__ 属性 373 | 374 | > 前面已经提到过,按标准规范,活动对象是不可能被直接访问到的。但是,一些具体实现并没有完全遵守这个规定,例如SpiderMonkey和Rhino;的实现中,函数有一个特殊的属性 __parent__,通过这个属性可以直接引用到活动对象(或全局变量对象),在此对象里创建了函数。 375 | 376 | > 例如 (SpiderMonkey, Rhino): 377 | 378 | var global = this; 379 | var a = 10; 380 | 381 | function foo() {} 382 | 383 | alert(foo.__parent__); // global 384 | 385 | var VO = foo.__parent__; 386 | 387 | alert(VO.a); // 10 388 | alert(VO === global); // true 389 | 390 | > 在上面的例子中我们可以看到,函数foo是在全局上下文中创建的,所以属性__parent__ 指向全局上下文的变量对象,即全局对象。 391 | 392 | > 然而,在SpiderMonkey中用同样的方式访问活动对象是不可能的:在不同版本的SpiderMonkey中,内部函数的__parent__ 有时指向null ,有时指向全局对象。 393 | 394 | > 在Rhino中,用同样的方式访问活动对象是完全可以的。 395 | 396 | > 例如 (Rhino): 397 | 398 | var global = this; 399 | var x = 10; 400 | 401 | (function foo() { 402 | 403 | var y = 20; 404 | 405 | // "foo"上下文里的活动对象 406 | var AO = (function () {}).__parent__; 407 | 408 | print(AO.y); // 20 409 | 410 | // 当前活动对象的__parent__ 是已经存在的全局对象 411 | // 变量对象的特殊链形成了 412 | // 所以我们叫做作用域链 413 | print(AO.__parent__ === global); // true 414 | 415 | print(AO.__parent__.x); // 10 416 | 417 | })(); 418 | 419 | ## 总结 420 | 421 | > 在这篇文章里,我们深入学习了跟执行上下文相关的对象。我希望这些知识对您来说能有所帮助,能解决一些您曾经遇到的问题或困惑。按照计划,在后续的章节中,我们将探讨作用域链,标识符解析,闭包。 422 | 423 | > 有任何问题,我很高兴在下面评论中能帮你解答。 424 | 425 | ## 致谢 426 | 427 | > 10.1.3 – [Variable Instantiation;](http://bclary.com/2004/11/07/#a-10.1.3) 428 | > 429 | > 10.1.5 – [Global Object;](http://bclary.com/2004/11/07/#a-10.1.5) 430 | > 431 | > 10.1.6 – [Activation Object;](http://bclary.com/2004/11/07/#a-10.1.6) 432 | > 433 | > 10.1.8 – [Arguments Object.](http://bclary.com/2004/11/07/#a-10.1.8) 434 | 435 | > 翻译编辑:[汤姆大叔的博客](http://www.cnblogs.com/TomXu/ '汤姆大叔的博客') 436 | 437 | > 原文地址:[深入理解JavaScript系列(12):变量对象(Variable Object)](http://www.cnblogs.com/TomXu/archive/2012/01/16/2309728.html '深入理解JavaScript系列(12):变量对象(Variable Object)') 438 | 439 | > 修正/编辑:[ahkjxy](https://github.com/ahkjxy/) -------------------------------------------------------------------------------- /13-This Yes, this!.md: -------------------------------------------------------------------------------- 1 | ## 深入理解JavaScript系列(13):This? Yes, this! 2 | 3 | ### 介绍 4 | 5 | > 在这篇文章里,我们将讨论跟执行上下文直接相关的更多细节。讨论的主题就是this关键字。实践证明,这个主题很难,在不同执行上下文中this的确定经常会发生问题。 6 | 7 | > 许多程序员习惯的认为,在程序语言中,this关键字与面向对象程序开发紧密相关,其完全指向由构造器新创建的对象。在ECMAScript规范中也是这样实现的,但正如我们将看到那样,在ECMAScript中,this并不限于只用来指向新创建的对象。 8 | 9 | 英文翻译: Dmitry A. Soshnikov在Stoyan Stefanov的帮助下 10 | 发布: 2010-03-07 11 | http://dmitrysoshnikov.com/ecmascript/chapter-3-this/ 12 | 13 | 俄文原文: Dmitry A. Soshnikov 14 | 修正: Zeroglif 15 | 发布: 2009-06-28; 16 | 更新:2010-03-07 17 | http://dmitrysoshnikov.com/ecmascript/ru-chapter-3-this/ 18 | 19 | 本文绝大部分内容参考了:http://www.denisdeng.com/?p=900 20 | 部分句子参考了:[justin的中文翻译](http://www.cnblogs.com/justinw/archive/2010/05/04/1727295.html#this-value-in-the-global-code) 21 | 22 | > 让我们更详细的了解一下,在ECMAScript中this到底是什么? 23 | 24 | ### 定义 25 | 26 | > this是执行上下文中的一个属性: 27 | 28 | activeExecutionContext = { 29 | VO: {...}, 30 | this: thisValue 31 | }; 32 | 33 | > 这里VO是我们前一章讨论的变量对象。 34 | 35 | > this与上下文中可执行代码的类型有直接关系,this值在进入上下文时确定,并且在上下文运行期间永久不变。 36 | 37 | > 下面让我们更详细研究这些案例: 38 | 39 | ### 全局代码中的this 40 | 41 | > 在这里一切都简单。在全局代码中,this始终是全局对象本身,这样就有可能间接的引用到它了。 42 | 43 | // 显示定义全局对象的属性 44 | this.a = 10; // global.a = 10 45 | alert(a); // 10 46 | 47 | // 通过赋值给一个无标示符隐式 48 | b = 20; 49 | alert(this.b); // 20 50 | 51 | // 也是通过变量声明隐式声明的 52 | // 因为全局上下文的变量对象是全局对象自身 53 | var c = 30; 54 | alert(this.c); // 30 55 | 56 | ### 函数代码中的this 57 | 58 | > 在函数代码中使用this时很有趣,这种情况很难且会导致很多问题。 59 | 60 | > 这种类型的代码中,this值的首要特点(或许是最主要的)是它不是静态的绑定到一个函数。 61 | 62 | > 正如我们上面曾提到的那样,this是进入上下文时确定,在一个函数代码中,这个值在每一次完全不同。 63 | 64 | > 不管怎样,在代码运行时的this值是不变的,也就是说,因为它不是一个变量,就不可能为其分配一个新值(相反,在Python编程语言中,它明确的定义为对象本身,在运行期间可以不断改变)。 65 | 66 | var foo = {x: 10}; 67 | 68 | var bar = { 69 | x: 20, 70 | test: function () { 71 | 72 | alert(this === bar); // true 73 | alert(this.x); // 20 74 | 75 | this = foo; // 错误,任何时候不能改变this的值 76 | 77 | alert(this.x); // 如果不出错的话,应该是10,而不是20 78 | 79 | } 80 | 81 | }; 82 | 83 | // 在进入上下文的时候 84 | // this被当成bar对象 85 | // determined as "bar" object; why so - will 86 | // be discussed below in detail 87 | 88 | bar.test(); // true, 20 89 | 90 | foo.test = bar.test; 91 | 92 | // 不过,这里this依然不会是foo 93 | // 尽管调用的是相同的function 94 | 95 | foo.test(); // false, 10 96 | 97 | > 那么,影响了函数代码中this值的变化有几个因素: 98 | 99 | > 首先,在通常的函数调用中,this是由激活上下文代码的调用者来提供的,即调用函数的父上下文(parent context )。this取决于调用函数的方式。 100 | 101 | > 为了在任何情况下准确无误的确定this值,有必要理解和记住这重要的一点。正是调用函数的方式影响了调用的上下文中的this值,没有别的什么(我们可以在一些文章,甚至是在关于javascript的书籍中看到,它们声称:“this值取决于函数如何定义,如果它是全局函数,this设置为全局对象,如果函数是一个对象的方法,this将总是指向这个对象。–这绝对不正确”)。继续我们的话题,可以看到,即使是正常的全局函数也会被调用方式的不同形式激活,这些不同的调用方式导致了不同的this值。 102 | 103 | function foo() { 104 | alert(this); 105 | } 106 | 107 | foo(); // global 108 | 109 | alert(foo === foo.prototype.constructor); // true 110 | 111 | // 但是同一个function的不同的调用表达式,this是不同的 112 | 113 | foo.prototype.constructor(); // foo.prototype 114 | 115 | > 有可能作为一些对象定义的方法来调用函数,但是this将不会设置为这个对象。 116 | 117 | var foo = { 118 | bar: function () { 119 | alert(this); 120 | alert(this === foo); 121 | } 122 | }; 123 | 124 | foo.bar(); // foo, true 125 | 126 | var exampleFunc = foo.bar; 127 | 128 | alert(exampleFunc === foo.bar); // true 129 | 130 | // 再一次,同一个function的不同的调用表达式,this是不同的 131 | 132 | exampleFunc(); // global, false 133 | 134 | > 那么,调用函数的方式如何影响this值?为了充分理解this值的确定,需要详细分析其内部类型之一——引用类型(Reference type)。 135 | 136 | #### 引用类型(Reference type) 137 | 138 | > 使用伪代码我们可以将引用类型的值可以表示为拥有两个属性的对象——base(即拥有属性的那个对象),和base中的propertyName 。 139 | 140 | var valueOfReferenceType = { 141 | base: , 142 | propertyName: 143 | }; 144 | 145 | > 引用类型的值只有两种情况: 146 | 147 | 1.当我们处理一个标示符时 148 | 2.或一个属性访问器 149 | 150 | > 标示符的处理过程在下一篇文章里详细讨论,在这里我们只需要知道,在该算法的返回值中,总是一个引用类型的值(这对this来说很重要)。 151 | 152 | > 标识符是变量名,函数名,函数参数名和全局对象中未识别的属性名。例如,下面标识符的值: 153 | 154 | var foo = 10; 155 | function bar() {} 156 | 157 | > 在操作的中间结果中,引用类型对应的值如下: 158 | 159 | var fooReference = { 160 | base: global, 161 | propertyName: 'foo' 162 | }; 163 | 164 | var barReference = { 165 | base: global, 166 | propertyName: 'bar' 167 | }; 168 | 169 | > 为了从引用类型中得到一个对象真正的值,伪代码中的GetValue方法可以做如下描述: 170 | 171 | function GetValue(value) { 172 | 173 | if (Type(value) != Reference) { 174 | return value; 175 | } 176 | 177 | var base = GetBase(value); 178 | 179 | if (base === null) { 180 | throw new ReferenceError; 181 | } 182 | 183 | return base.[[Get]](GetPropertyName(value)); 184 | 185 | } 186 | 187 | > 内部的[[Get]]方法返回对象属性真正的值,包括对原型链中继承的属性分析。 188 | 189 | GetValue(fooReference); // 10 190 | GetValue(barReference); // function object "bar" 191 | 192 | > 属性访问器都应该熟悉。它有两种变体:点(.)语法(此时属性名是正确的标示符,且事先知道),或括号语法([])。 193 | 194 | foo.bar(); 195 | foo['bar'](); 196 | 197 | > 在中间计算的返回值中,我们有了引用类型的值。 198 | 199 | var fooBarReference = { 200 | base: foo, 201 | propertyName: 'bar' 202 | }; 203 | 204 | GetValue(fooBarReference); // function object "bar" 205 | 206 | > 引用类型的值与函数上下文中的this值如何相关?——从最重要的意义上来说。 这个关联的过程是这篇文章的核心。 一个函数上下文中确定this值的通用规则如下: 207 | 208 | > 在一个函数上下文中,this由调用者提供,由调用函数的方式来决定。如果调用括号()的左边是引用类型的值,this将设为引用类型值的base对象(base object),在其他情况下(与引用类型不同的任何其它属性),这个值为null。不过,实际不存在this的值为null的情况,因为当this的值为null的时候,其值会被隐式转换为全局对象。注:**第5版的ECMAScript中,已经不强迫转换成全局变量了,而是赋值为undefined。** 209 | 210 | > 我们看看这个例子中的表现: 211 | 212 | function foo() { 213 | return this; 214 | } 215 | 216 | foo(); // global 217 | 218 | > 我们看到在调用括号的左边是一个引用类型值(因为foo是一个标示符)。 219 | 220 | var fooReference = { 221 | base: global, 222 | propertyName: 'foo' 223 | }; 224 | 225 | > 相应地,this也设置为引用类型的base对象。即全局对象。 226 | 227 | > 同样,使用属性访问器: 228 | 229 | var foo = { 230 | bar: function () { 231 | return this; 232 | } 233 | }; 234 | 235 | foo.bar(); // foo 236 | 237 | > 我们再次拥有一个引用类型,其base是foo对象,在函数bar激活时用作this。 238 | 239 | var fooBarReference = { 240 | base: foo, 241 | propertyName: 'bar' 242 | }; 243 | 244 | > 但是,用另外一种形式激活相同的函数,我们得到其它的this值。 245 | 246 | var test = foo.bar; 247 | test(); // global 248 | 249 | > 因为test作为标示符,生成了引用类型的其他值,其base(全局对象)用作this 值。 250 | 251 | var testReference = { 252 | base: global, 253 | propertyName: 'test' 254 | }; 255 | 256 | > 现在,我们可以很明确的告诉你,为什么用表达式的不同形式激活同一个函数会不同的this值,答案在于引用类型(type Reference)不同的中间值。 257 | 258 | function foo() { 259 | alert(this); 260 | } 261 | 262 | foo(); // global, because 263 | 264 | var fooReference = { 265 | base: global, 266 | propertyName: 'foo' 267 | }; 268 | 269 | alert(foo === foo.prototype.constructor); // true 270 | 271 | // 另外一种形式的调用表达式 272 | 273 | foo.prototype.constructor(); // foo.prototype, because 274 | 275 | var fooPrototypeConstructorReference = { 276 | base: foo.prototype, 277 | propertyName: 'constructor' 278 | }; 279 | 280 | > 另外一个通过调用方式动态确定this值的经典例子: 281 | 282 | function foo() { 283 | alert(this.bar); 284 | } 285 | 286 | var x = {bar: 10}; 287 | var y = {bar: 20}; 288 | 289 | x.test = foo; 290 | y.test = foo; 291 | 292 | x.test(); // 10 293 | y.test(); // 20 294 | 295 | #### 函数调用和非引用类型 296 | 297 | > 因此,正如我们已经指出,当调用括号的左边不是引用类型而是其它类型,这个值自动设置为null,结果为全局对象。 298 | 299 | > 让我们再思考这种表达式: 300 | 301 | (function () { 302 | alert(this); // null => global 303 | })(); 304 | 305 | > 在这个例子中,我们有一个函数对象但不是引用类型的对象(它不是标示符,也不是属性访问器),相应地,this值最终设为全局对象。 306 | 307 | > 更多复杂的例子: 308 | 309 | var foo = { 310 | bar: function () { 311 | alert(this); 312 | } 313 | }; 314 | 315 | foo.bar(); // Reference, OK => foo 316 | (foo.bar)(); // Reference, OK => foo 317 | 318 | (foo.bar = foo.bar)(); // global? 319 | (false || foo.bar)(); // global? 320 | (foo.bar, foo.bar)(); // global? 321 | 322 | > 为什么我们有一个属性访问器,它的中间值应该为引用类型的值,在某些调用中我们得到的this值不是base对象,而是global对象? 323 | 324 | > 问题在于后面的三个调用,在应用一定的运算操作之后,在调用括号的左边的值不在是引用类型。 325 | 326 | 1. 第一个例子很明显———明显的引用类型,结果是,this为base对象,即foo。 327 | 1. 在第二个例子中,组运算符并不适用,想想上面提到的,从引用类型中获得一个对象真正的值的方法,如GetValue。相应的,在组运算的返回中———我们得到仍是一个引用类型。这就是this值为什么再次设为base对象,即foo。 328 | 1. 第三个例子中,与组运算符不同,赋值运算符调用了GetValue方法。返回的结果是函数对象(但不是引用类型),这意味着this设为null,结果是global对象。 329 | 1. 第四个和第五个也是一样——逗号运算符和逻辑运算符(OR)调用了GetValue 方法,相应地,我们失去了引用而得到了函数。并再次设为global。 330 | 331 | #### 引用类型和this为null 332 | 333 | > 有一种情况是这样的:当调用表达式限定了call括号左边的引用类型的值, 尽管this被设定为null,但结果被隐式转化成global。当引用类型值的base对象是被活动对象时,这种情况就会出现。 334 | 335 | > 下面的实例中,内部函数被父函数调用,此时我们就能够看到上面说的那种特殊情况。正如我们在第12章知道的一样,局部变量、内部函数、形式参数储存在给定函数的激活对象中。 336 | 337 | function foo() { 338 | function bar() { 339 | alert(this); // global 340 | } 341 | bar(); // the same as AO.bar() 342 | } 343 | 344 | > 活动对象总是作为this返回,值为null——(即伪代码的AO.bar()相当于null.bar())。这里我们再次回到上面描述的例子,this设置为全局对象。 345 | 346 | > 有一种情况除外:如果with对象包含一个函数名属性,在with语句的内部块中调用函数。With语句添加到该对象作用域的最前端,即在活动对象的前面。相应地,也就有了引用类型(通过标示符或属性访问器), 其base对象不再是活动对象,而是with语句的对象。顺便提一句,它不仅与内部函数相关,也与全局函数相关,因为with对象比作用域链里的最前端的对象(全局对象或一个活动对象)还要靠前。 347 | 348 | var x = 10; 349 | 350 | with ({ 351 | 352 | foo: function () { 353 | alert(this.x); 354 | }, 355 | x: 20 356 | 357 | }) { 358 | 359 | foo(); // 20 360 | 361 | } 362 | 363 | // because 364 | 365 | var fooReference = { 366 | base: __withObject, 367 | propertyName: 'foo' 368 | }; 369 | 370 | > 同样的情况出现在catch语句的实际参数中函数调用:在这种情况下,catch对象添加到作用域的最前端,即在活动对象或全局对象的前面。但是,这个特定的行为被确认为ECMA-262-3的一个bug,这个在新版的ECMA-262-5中修复了。这样,在特定的活动对象中,this指向全局对象。而不是catch对象。 371 | 372 | try { 373 | throw function () { 374 | alert(this); 375 | }; 376 | } catch (e) { 377 | e(); // ES3标准里是__catchObject, ES5标准里是global 378 | } 379 | 380 | // on idea 381 | 382 | var eReference = { 383 | base: __catchObject, 384 | propertyName: 'e' 385 | }; 386 | 387 | // ES5新标准里已经fix了这个bug, 388 | // 所以this就是全局对象了 389 | var eReference = { 390 | base: global, 391 | propertyName: 'e' 392 | }; 393 | 394 | > 同样的情况出现在命名函数(函数的更对细节参考第15章Functions)的递归调用中。在函数的第一次调用中,base对象是父活动对象(或全局对象),在递归调用中,base对象应该是存储着函数表达式可选名称的特定对象。但是,在这种情况下,this总是指向全局对象。 395 | 396 | (function foo(bar) { 397 | 398 | alert(this); 399 | 400 | !bar && foo(1); // "should" be special object, but always (correct) global 401 | 402 | })(); // global 403 | 404 | #### 作为构造器调用的函数中的this 405 | 406 | > 还有一个与this值相关的情况是在函数的上下文中,这是一个构造函数的调用。 407 | 408 | function A() { 409 | alert(this); // "a"对象下创建一个新属性 410 | this.x = 10; 411 | } 412 | 413 | var a = new A(); 414 | alert(a.x); // 10 415 | 416 | > 在这个例子中,new运算符调用“A”函数的内部的[[Construct]] 方法,接着,在对象创建后,调用内部的[[Call]] 方法。 **所有相同的函数“A”都将this的值设置为新创建的对象。** 417 | 418 | #### 函数调用中手动设置this 419 | 420 | > 在函数原型中定义的两个方法(因此所有的函数都可以访问它)允许去手动设置函数调用的this值。它们是.apply和.call方法。他们用接受的第一个参数作为this值,this 在调用的作用域中使用。这两个方法的区别很小,对于.apply,第二个参数必须是数组(或者是类似数组的对象,如arguments,反过来,.call能接受任何参数。两个方法必须的参数是第一个——this。 421 | 422 | > 例如: 423 | 424 | var b = 10; 425 | 426 | function a(c) { 427 | alert(this.b); 428 | alert(c); 429 | } 430 | 431 | a(20); // this === global, this.b == 10, c == 20 432 | 433 | a.call({b: 20}, 30); // this === {b: 20}, this.b == 20, c == 30 434 | a.apply({b: 30}, [40]) // this === {b: 30}, this.b == 30, c == 40 435 | 436 | ### 结论 437 | 438 | > 在这篇文章中,我们讨论了ECMAScript中this关键字的特征(对比于C++ 和 Java,它们的确是特色)。我希望这篇文章有助于你准确的理解ECMAScript中this关键字如何工作。同样,我很乐意在评论中回到你的问题。 439 | 440 | ## 致谢 441 | 442 | > 10.1.7 – [This](http://bclary.com/2004/11/07/#a-10.1.7) 443 | 444 | > 11.1.1 – [The this keyword](http://bclary.com/2004/11/07/#a-11.1.1) 445 | 446 | > 11.2.2 – [The new operator](http://bclary.com/2004/11/07/#a-11.2.2) 447 | 448 | > 11.2.3 – [Function calls](http://bclary.com/2004/11/07/#a-11.2.3) 449 | 450 | > 翻译编辑:[汤姆大叔的博客](http://www.cnblogs.com/TomXu/ '汤姆大叔的博客') 451 | 452 | > 原文地址:[深入理解JavaScript系列(13):This? Yes, this!](http://www.cnblogs.com/TomXu/archive/2012/01/17/2310479.html '深入理解JavaScript系列(13):This? Yes, this!') 453 | 454 | > 修正/编辑:[ahkjxy](https://github.com/ahkjxy/) -------------------------------------------------------------------------------- /6-S.O.L.I.D五大原则之单一职责SRP.md: -------------------------------------------------------------------------------- 1 | ## 深入理解JavaScript系列(6):S.O.L.I.D五大原则之单一职责SRP 2 | 3 | ### 前言 4 | 5 | > Bob大叔提出并发扬了S.O.L.I.D五大原则,用来更好地进行面向对象编程,五大原则分别是: 6 | 7 | 1. The Single Responsibility Principle(单一职责SRP) 8 | 1. The Open/Closed Principle(开闭原则OCP) 9 | 1. The Liskov Substitution Principle(里氏替换原则LSP) 10 | 1. The Interface Segregation Principle(接口分离原则ISP) 11 | 1. The Dependency Inversion Principle(依赖反转原则DIP) 12 | 13 | > 五大原则,我相信在博客园已经被讨论烂了,尤其是C#的实现,但是相对于JavaScript这种以原型为base的动态类型语言来说还为数不多,该系列将分5篇文章以JavaScript编程语言为基础来展示五大原则的应用。 OK,开始我们的第一篇:单一职责。 14 | 15 | > 英文原文: 16 | 17 | ### 单一职责 18 | 19 | > 单一职责的描述如下: 20 | 21 | > A class should have only one reason to change 22 | > 类发生更改的原因应该只有一个 23 | 24 | > 一个类(JavaScript下应该是一个对象)应该有一组紧密相关的行为的意思是什么?遵守单一职责的好处是可以让我们很容易地来维护这个对象,当一个对象封装了很多职责的话,一旦一个职责需要修改,势必会影响该对象想的其它职责代码。通过解耦可以让每个职责工更加有弹性地变化。 25 | 26 | > 不过,我们如何知道一个对象的多个行为构造多个职责还是单个职责?我们可以通过参考Object Design: Roles, Responsibilies, and Collaborations一书提出的Role Stereotypes概念来决定,该书提出了如下Role Stereotypes来区分职责: 27 | 28 | 1. Information holder – 该对象设计为存储对象并提供对象信息给其它对象。 29 | 2. Structurer – 该对象设计为维护对象和信息之间的关系 30 | 3. Service provider – 该对象设计为处理工作并提供服务给其它对象 31 | 4. Controller – 该对象设计为控制决策一系列负责的任务处理 32 | 5. Coordinator – 该对象不做任何决策处理工作,只是delegate工作到其它对象上 33 | 6. Interfacer – 该对象设计为在系统的各个部分转化信息(或请求) 34 | 35 | > 一旦你知道了这些概念,那就狠容易知道你的代码到底是多职责还是单一职责了。 36 | 37 | ### 实例代码 38 | 39 | > 该实例代码演示的是将商品添加到购物车,代码非常糟糕,代码如下: 40 | 41 | function Product(id, description) { 42 | this.getId = function () { 43 | return id; 44 | }; 45 | this.getDescription = function () { 46 | return description; 47 | }; 48 | } 49 | 50 | function Cart(eventAggregator) { 51 | var items = []; 52 | 53 | this.addItem = function (item) { 54 | items.push(item); 55 | }; 56 | } 57 | 58 | (function () { 59 | var products = [new Product(1, "Star Wars Lego Ship"), 60 | new Product(2, "Barbie Doll"), 61 | new Product(3, "Remote Control Airplane")], 62 | cart = new Cart(); 63 | 64 | function addToCart() { 65 | var productId = $(this).attr('id'); 66 | var product = $.grep(products, function (x) { 67 | return x.getId() == productId; 68 | })[0]; 69 | cart.addItem(product); 70 | 71 | var newItem = $('
  • ').html(product.getDescription()).attr('id-cart', product.getId()).appendTo("#cart"); 72 | } 73 | 74 | products.forEach(function (product) { 75 | var newItem = $('
  • ').html(product.getDescription()) 76 | .attr('id', product.getId()) 77 | .dblclick(addToCart) 78 | .appendTo("#products"); 79 | }); 80 | })(); 81 | 82 | > 该代码声明了2个function分别用来描述product和cart,而匿名函数的职责是更新屏幕和用户交互,这还不是一个很复杂的例子,但匿名函数里却包含了很多不相关的职责,让我们来看看到底有多少职责: 83 | 84 | 1. 首先,有product的集合的声明 85 | 1. 其次,有一个将product集合绑定到#product元素的代码,而且还附件了一个添加到购物车的事件处理 86 | 1. 第三,有Cart购物车的展示功能 87 | 1. 第四,有添加product item到购物车并显示的功能 88 | 89 | ### 重构代码 90 | 91 | > 让我们来分解一下,以便代码各自存放到各自的对象里,为此,我们参考了martinfowler的[事件聚合(Event Aggregator)](http://martinfowler.com/eaaDev/EventAggregator.html)理论在处理代码以便各对象之间进行通信。 92 | 93 | > 首先我们先来实现事件聚合的功能,该功能分为2部分,1个是Event,用于Handler回调的代码,1个是EventAggregator用来订阅和发布Event,代码如下: 94 | 95 | function Event(name) { 96 | var handlers = []; 97 | 98 | this.getName = function () { 99 | return name; 100 | }; 101 | 102 | this.addHandler = function (handler) { 103 | handlers.push(handler); 104 | }; 105 | 106 | this.removeHandler = function (handler) { 107 | for (var i = 0; i < handlers.length; i++) { 108 | if (handlers[i] == handler) { 109 | handlers.splice(i, 1); 110 | break; 111 | } 112 | } 113 | }; 114 | 115 | this.fire = function (eventArgs) { 116 | handlers.forEach(function (h) { 117 | h(eventArgs); 118 | }); 119 | }; 120 | } 121 | 122 | function EventAggregator() { 123 | var events = []; 124 | 125 | function getEvent(eventName) { 126 | return $.grep(events, function (event) { 127 | return event.getName() === eventName; 128 | })[0]; 129 | } 130 | 131 | this.publish = function (eventName, eventArgs) { 132 | var event = getEvent(eventName); 133 | 134 | if (!event) { 135 | event = new Event(eventName); 136 | events.push(event); 137 | } 138 | event.fire(eventArgs); 139 | }; 140 | 141 | this.subscribe = function (eventName, handler) { 142 | var event = getEvent(eventName); 143 | 144 | if (!event) { 145 | event = new Event(eventName); 146 | events.push(event); 147 | } 148 | 149 | event.addHandler(handler); 150 | }; 151 | } 152 | 153 | > 然后,我们来声明Product对象,代码如下: 154 | 155 | function Product(id, description) { 156 | this.getId = function () { 157 | return id; 158 | }; 159 | this.getDescription = function () { 160 | return description; 161 | }; 162 | } 163 | 164 | > 接着来声明Cart对象,该对象的**addItem**的**function**里我们要触发发布一个事件**itemAdded**,然后将**item**作为参数传进去。 165 | 166 | function Cart(eventAggregator) { 167 | var items = []; 168 | 169 | this.addItem = function (item) { 170 | items.push(item); 171 | eventAggregator.publish("itemAdded", item); 172 | }; 173 | } 174 | 175 | > CartController主要是接受cart对象和事件聚合器,通过订阅**itemAdded**来增加一个li元素节点,通过订阅**productSelected**事件来添加product。 176 | 177 | function CartController(cart, eventAggregator) { 178 | eventAggregator.subscribe("itemAdded", function (eventArgs) { 179 | var newItem = $('
  • ').html(eventArgs.getDescription()).attr('id-cart', eventArgs.getId()).appendTo("#cart"); 180 | }); 181 | 182 | eventAggregator.subscribe("productSelected", function (eventArgs) { 183 | cart.addItem(eventArgs.product); 184 | }); 185 | } 186 | 187 | > Repository的目的是为了获取数据(可以从ajax里获取),然后暴露get数据的方法。 188 | 189 | function ProductRepository() { 190 | var products = [new Product(1, "Star Wars Lego Ship"), 191 | new Product(2, "Barbie Doll"), 192 | new Product(3, "Remote Control Airplane")]; 193 | 194 | this.getProducts = function () { 195 | return products; 196 | } 197 | } 198 | 199 | > **ProductController**里定义了一个onProductSelect方法,主要是发布触发**productSelected**事件,**forEach**主要是用于绑定数据到产品列表上,代码如下: 200 | 201 | function ProductController(eventAggregator, productRepository) { 202 | var products = productRepository.getProducts(); 203 | 204 | function onProductSelected() { 205 | var productId = $(this).attr('id'); 206 | var product = $.grep(products, function (x) { 207 | return x.getId() == productId; 208 | })[0]; 209 | eventAggregator.publish("productSelected", { 210 | product: product 211 | }); 212 | } 213 | 214 | products.forEach(function (product) { 215 | var newItem = $('
  • ').html(product.getDescription()) 216 | .attr('id', product.getId()) 217 | .dblclick(onProductSelected) 218 | .appendTo("#products"); 219 | }); 220 | } 221 | 222 | > 最后声明匿名函数(需要确保HTML都加载完了才能执行这段代码,比如放在jQuery的ready方法里): 223 | 224 | (function () { 225 | var eventAggregator = new EventAggregator(), 226 | cart = new Cart(eventAggregator), 227 | cartController = new CartController(cart, eventAggregator), 228 | productRepository = new ProductRepository(), 229 | productController = new ProductController(eventAggregator, productRepository); 230 | })(); 231 | 232 | > 可以看到匿名函数的代码减少了很多,主要是一个对象的实例化代码,代码里我们介绍了Controller的概念,他接受信息然后传递到action,我们也介绍了Repository的概念,主要是用来处理product的展示,重构的结果就是写了一大堆的对象声明,但是好处是每个对象有了自己明确的职责,该展示数据的展示数据,改处理集合的处理集合,这样耦合度就非常低了。 233 | 234 | > 最终代码: 235 | 236 | function Event(name) { 237 | var handlers = []; 238 | 239 | this.getName = function () { 240 | return name; 241 | }; 242 | 243 | this.addHandler = function (handler) { 244 | handlers.push(handler); 245 | }; 246 | 247 | this.removeHandler = function (handler) { 248 | for (var i = 0; i < handlers.length; i++) { 249 | if (handlers[i] == handler) { 250 | handlers.splice(i, 1); 251 | break; 252 | } 253 | } 254 | }; 255 | 256 | this.fire = function (eventArgs) { 257 | handlers.forEach(function (h) { 258 | h(eventArgs); 259 | }); 260 | }; 261 | } 262 | 263 | function EventAggregator() { 264 | var events = []; 265 | 266 | function getEvent(eventName) { 267 | return $.grep(events, function (event) { 268 | return event.getName() === eventName; 269 | })[0]; 270 | } 271 | 272 | this.publish = function (eventName, eventArgs) { 273 | var event = getEvent(eventName); 274 | 275 | if (!event) { 276 | event = new Event(eventName); 277 | events.push(event); 278 | } 279 | event.fire(eventArgs); 280 | }; 281 | 282 | this.subscribe = function (eventName, handler) { 283 | var event = getEvent(eventName); 284 | 285 | if (!event) { 286 | event = new Event(eventName); 287 | events.push(event); 288 | } 289 | 290 | event.addHandler(handler); 291 | }; 292 | } 293 | 294 | function Product(id, description) { 295 | this.getId = function () { 296 | return id; 297 | }; 298 | this.getDescription = function () { 299 | return description; 300 | }; 301 | } 302 | 303 | function Cart(eventAggregator) { 304 | var items = []; 305 | 306 | this.addItem = function (item) { 307 | items.push(item); 308 | eventAggregator.publish("itemAdded", item); 309 | }; 310 | } 311 | 312 | function CartController(cart, eventAggregator) { 313 | eventAggregator.subscribe("itemAdded", function (eventArgs) { 314 | var newItem = $('
  • ').html(eventArgs.getDescription()).attr('id-cart', eventArgs.getId()).appendTo("#cart"); 315 | }); 316 | 317 | eventAggregator.subscribe("productSelected", function (eventArgs) { 318 | cart.addItem(eventArgs.product); 319 | }); 320 | } 321 | 322 | function ProductRepository() { 323 | var products = [new Product(1, "Star Wars Lego Ship"), 324 | new Product(2, "Barbie Doll"), 325 | new Product(3, "Remote Control Airplane")]; 326 | 327 | this.getProducts = function () { 328 | return products; 329 | } 330 | } 331 | 332 | function ProductController(eventAggregator, productRepository) { 333 | var products = productRepository.getProducts(); 334 | 335 | function onProductSelected() { 336 | var productId = $(this).attr('id'); 337 | var product = $.grep(products, function (x) { 338 | return x.getId() == productId; 339 | })[0]; 340 | eventAggregator.publish("productSelected", { 341 | product: product 342 | }); 343 | } 344 | 345 | products.forEach(function (product) { 346 | var newItem = $('
  • ').html(product.getDescription()) 347 | .attr('id', product.getId()) 348 | .dblclick(onProductSelected) 349 | .appendTo("#products"); 350 | }); 351 | } 352 | 353 | (function () { 354 | var eventAggregator = new EventAggregator(), 355 | cart = new Cart(eventAggregator), 356 | cartController = new CartController(cart, eventAggregator), 357 | productRepository = new ProductRepository(), 358 | productController = new ProductController(eventAggregator, productRepository); 359 | })(); 360 | 361 | ## 总结 362 | 363 | > 看到这个重构结果,有博友可能要问了,真的有必要做这么复杂么?我只能说:要不要这么做取决于你项目的情况。 364 | 365 | > 如果你的项目是个是个非常小的项目,代码也不是很多,那其实是没有必要重构得这么复杂,但如果你的项目是个很复杂的大型项目,或者你的小项目将来可能增长得很快的话,那就在前期就得考虑SRP原则进行职责分离了,这样才有利于以后的维护。 366 | 367 | ## 致谢 368 | 369 | > 翻译编辑:[汤姆大叔的博客](http://www.cnblogs.com/TomXu/ '汤姆大叔的博客') 370 | 371 | > 原文地址:[深入理解JavaScript系列(6):S.O.L.I.D五大原则之单一职责SRP](http://www.cnblogs.com/TomXu/archive/2012/01/06/2305513.html '深入理解JavaScript系列(6):S.O.L.I.D五大原则之单一职责SRP') 372 | 373 | > 修正/编辑:[ahkjxy](https://github.com/ahkjxy/) -------------------------------------------------------------------------------- /10-JavaScript核心(晋级高手必读篇).md: -------------------------------------------------------------------------------- 1 | ## 深入理解JavaScript系列(10):JavaScript核心(晋级高手必读篇) 2 | 3 | > 本篇是[ECMA-262-3 in detail](http://dmitrysoshnikov.com/tag/ecma-262-3/ "ECMA-262-3 in detail")系列的一个概述(本人后续会翻译整理这些文章到本系列(第11-19章)。每个章节都有一个更详细的内容链接,你可以继续读一下每个章节对应的详细内容链接进行更深入的了解。 4 | 5 | > 适合的读者:有经验的开发员,专业前端人员。 6 | 7 | 原作者: Dmitry A. Soshnikov 8 | 发布时间: 2010-09-02 9 | 原文:http://dmitrysoshnikov.com/ecmascript/javascript-the-core/ 10 | 参考1:http://ued.ctrip.com/blog/?p=2795 11 | 参考2:http://www.cnblogs.com/ifishing/archive/2010/12/08/1900594.html 12 | 主要是综合了上面2位高手的中文翻译,将两篇文章的精华部分都结合在一起了。 13 | 14 | > 我们首先来看一下对象[Object]的概念,这也是ECMASript中最基本的概念。 15 | 16 | ### 对象Object 17 | 18 | > ECMAScript是一门高度抽象的面向对象(object-oriented)语言,用以处理Objects对象. 当然,也有基本类型,但是必要时,也需要转换成object对象来用。 19 | 20 | An object is a collection of properties and has a single prototype object. The prototype may be either an object or the null value. 21 | 22 | Object是一个属性的集合,并且都拥有一个单独的原型对象[prototype object]. 这个原型对象[prototype object]可以是一个object或者null值。 23 | 24 | > 让我们来举一个基本Object的例子,首先我们要清楚,一个Object的prototype是一个内部的[[prototype]]属性的引用。 25 | 26 | > 不过一般来说,我们会使用**__<内部属性名>__ **下划线来代替双括号,例如**__proto__**(这是某些脚本引擎比如SpiderMonkey的对于原型概念的具体实现,尽管并非标准)。 27 | 28 | var foo = { 29 | x: 10, 30 | y: 20 31 | }; 32 | 33 | > 上述代码foo对象有两个显式的属性[explicit own properties]和一个自带隐式的 __proto__ 属性[implicit __proto__ property],指向foo的原型。 34 | 35 | ![图 1. 一个含有原型的基本对象](http://pic002.cnblogs.com/images/2011/349491/2011123111182485.png '图 1. 一个含有原型的基本对象') 36 | >图 1. 一个含有原型的基本对象 37 | 38 | > 为什么需要原型呢,让我们考虑 原型链 的概念来回答这个问题。 39 | 40 | ### 原型链(Prototype chain) 41 | 42 | > 原型对象也是普通的对象,并且也有可能有自己的原型,如果一个原型对象的原型不为null的话,我们就称之为原型链(prototype chain)。 43 | 44 | A prototype chain is a finite chain of objects which is used to implemented inheritance and shared properties. 45 | 原型链是一个由对象组成的有限对象链由于实现继承和共享属性。 46 | 47 | > 想象一个这种情况,2个对象,大部分内容都一样,只有一小部分不一样,很明显,在一个好的设计模式中,我们会需要重用那部分相同的,而不是在每个对象中重复定义那些相同的方法或者属性。在基于类[class-based]的系统中,这些重用部分被称为类的继承 – 相同的部分放入**class A**,然后**class B**和**class C**从**A**继承,并且可以声明拥有各自的独特的东西。 48 | 49 | > ECMAScript没有类的概念。但是,重用[reuse]这个理念没什么不同(某些方面,甚至比class-更加灵活),可以由prototype chain原型链来实现。这种继承被称为delegation based inheritance-基于继承的委托,或者更通俗一些,叫做原型继承。 50 | 51 | > 类似于类”A”,”B”,”C”,在ECMAScript中尼创建对象类”a”,”b”,”c”,相应地, 对象“a” 拥有对象“b”和”c”的共同部分。同时对象“b”和”c”只包含它们自己的附加属性或方法。 52 | 53 | var a = { 54 | x: 10, 55 | calculate: function (z) { 56 | return this.x + this.y + z 57 | } 58 | }; 59 | 60 | var b = { 61 | y: 20, 62 | __proto__: a 63 | }; 64 | 65 | var c = { 66 | y: 30, 67 | __proto__: a 68 | }; 69 | 70 | // 调用继承过来的方法 71 | b.calculate(30); // 60 72 | c.calculate(40); // 80 73 | 74 | > 这样看上去是不是很简单啦。b和c可以使用a中定义的calculate方法,这就是有原型链来[prototype chain]实现的。 75 | 76 | > 原理很简单:如果在对象b中找不到calculate方法(也就是对象b中没有这个calculate属性), 那么就会沿着原型链开始找。如果这个calculate方法在b的prototype中没有找到,那么就会沿着原型链找到a的prototype,一直遍历完整个原型链。记住,一旦找到,就返回第一个找到的属性或者方法。因此,第一个找到的属性成为继承属性。如果遍历完整个原型链,仍然没有找到,那么就会返回**undefined。** 77 | 78 | > 注意一点,this这个值在一个继承机制中,仍然是指向它原本属于的对象,而不是从原型链上找到它时,它所属于的对象。例如,以上的例子,this.y是从b和c中获取的,而不是a。当然,你也发现了this.x是从a取的,因为是通过原型链机制找到的。 79 | 80 | > 如果一个对象的prototype没有显示的声明过或定义过,那么__prototype__的默认值就是object.prototype, 而object.prototype也会有一个__prototype__, 这个就是原型链的终点了,被设置为null。 81 | 82 | > 下面的图示就是表示了上述a,b,c的继承关系 83 | 84 | ![图 2. 原型链](http://pic002.cnblogs.com/images/2011/349491/2011123111374453.png '图 2. 原型链') 85 | > 图 2. 原型链 86 | 87 | > 原型链通常将会在这样的情况下使用:对象拥有 相同或相似的状态结构(same or similar state structure) (即相同的属性集合)与 不同的状态值(different state values)。在这种情况下,我们可以使用 构造函数(Constructor) 在 特定模式(specified pattern) 下创建对象。 88 | 89 | ### 构造函数(Constructor) 90 | 91 | > 除了创建对象,构造函数(constructor) 还做了另一件有用的事情—自动为创建的新对象设置了原型对象(prototype object) 。原型对象存放于 ConstructorFunction.prototype 属性中。 92 | 93 | > 例如,我们重写之前例子,使用构造函数创建对象“b”和“c”,那么对象”a”则扮演了“Foo.prototype”这个角色: 94 | 95 | // 构造函数 96 | function Foo(y) { 97 | // 构造函数将会以特定模式创建对象:被创建的对象都会有"y"属性 98 | this.y = y; 99 | } 100 | 101 | // "Foo.prototype"存放了新建对象的原型引用 102 | // 所以我们可以将之用于定义继承和共享属性或方法 103 | // 所以,和上例一样,我们有了如下代码: 104 | 105 | // 继承属性"x" 106 | Foo.prototype.x = 10; 107 | 108 | // 继承方法"calculate" 109 | Foo.prototype.calculate = function (z) { 110 | return this.x + this.y + z; 111 | }; 112 | 113 | // 使用foo模式创建 "b" and "c" 114 | var b = new Foo(20); 115 | var c = new Foo(30); 116 | 117 | // 调用继承的方法 118 | b.calculate(30); // 60 119 | c.calculate(40); // 80 120 | 121 | // 让我们看看是否使用了预期的属性 122 | 123 | console.log( 124 | 125 | b.__proto__ === Foo.prototype, // true 126 | c.__proto__ === Foo.prototype, // true 127 | 128 | // "Foo.prototype"自动创建了一个特殊的属性"constructor" 129 | // 指向a的构造函数本身 130 | // 实例"b"和"c"可以通过授权找到它并用以检测自己的构造函数 131 | 132 | b.constructor === Foo, // true 133 | c.constructor === Foo, // true 134 | Foo.prototype.constructor === Foo // true 135 | 136 | b.calculate === b.__proto__.calculate, // true 137 | b.__proto__.calculate === Foo.prototype.calculate // true 138 | 139 | ); 140 | 141 | > 上述代码可表示为如下的关系: 142 | 143 | ![图 3. 构造函数与对象之间的关系](http://pic002.cnblogs.com/images/2011/349491/2011123111482169.png '图 3. 构造函数与对象之间的关系') 144 | > 图 3. 构造函数与对象之间的关系 145 | 146 | > 上述图示可以看出,每一个object都有一个prototype. 构造函数Foo也拥有自己的__proto__, 也就是Function.prototype, 而Function.prototype的__proto__指向了Object.prototype. 重申一遍,Foo.prototype只是一个显式的属性,也就是b和c的__proto__属性。 147 | 148 | >这个问题完整和详细的解释可以在大叔即将翻译的第18、19两章找到。有两个部分:**面向对象编程.一般理论(OOP. The general theory)**,描述了不同的面向对象的范式与风格(OOP paradigms and stylistics),以及与ECMAScript的比较, **面向对象编程.ECMAScript实现(OOP. ECMAScript implementation)**, 专门讲述了ECMAScript中的面向对象编程。 149 | 150 | > 现在,我们已经了解了基本的object原理,那么我们接下去来看看ECMAScript里面的程序执行环境[runtime program execution]. 这就是通常称为的“执行上下文堆栈”[execution context stack]。每一个元素都可以抽象的理解为object。你也许发现了,没错,在ECMAScript中,几乎处处都能看到object的身影。 151 | 152 | ### 执行上下文栈(Execution Context Stack) 153 | 154 | > 在ECMASscript中的代码有三种类型:global, function和eval。 155 | 156 | > 每一种代码的执行都需要依赖自身的上下文。当然global的上下文可能涵盖了很多的function和eval的实例。函数的每一次调用,都会进入函数执行中的上下文,并且来计算函数中变量等的值。eval函数的每一次执行,也会进入eval执行中的上下文,判断应该从何处获取变量的值。 157 | 158 | > 注意,一个function可能产生无限的上下文环境,因为一个函数的调用(甚至递归)都产生了一个新的上下文环境。 159 | 160 | function foo(bar) {} 161 | 162 | // 调用相同的function,每次都会产生3个不同的上下文 163 | //(包含不同的状态,例如参数bar的值) 164 | 165 | foo(10); 166 | foo(20); 167 | foo(30); 168 | 169 | > 一个执行上下文可以激活另一个上下文,就好比一个函数调用了另一个函数(或者全局的上下文调用了一个全局函数),然后一层一层调用下去。逻辑上来说,这种实现方式是栈,我们可以称之为上下文堆栈。 170 | 171 | > 激活其它上下文的某个上下文被称为 **调用者(caller)** 。被激活的上下文被称为**被调用者(callee)** 。被调用者同时也可能是调用者(比如一个在全局上下文中被调用的函数调用某些自身的内部方法)。 172 | 173 | > 当一个**caller**激活了一个**callee**,那么这个caller就会暂停它自身的执行,然后将控制权交给这个callee. 于是这个callee被放入堆栈,称为进行中的上下文[running/active execution context]. 当这个callee的上下文结束之后,会把控制权再次交给它的caller,然后caller会在刚才暂停的地方继续执行。在这个caller结束之后,会继续触发其他的上下文。一个callee可以用返回(return)或者抛出异常(exception)来结束自身的上下文。 174 | 175 | > 如下图,所有的ECMAScript的程序执行都可以看做是一个执行上下文堆栈[execution context (EC) stack]。堆栈的顶部就是处于激活状态的上下文。 176 | 177 | ![图 4. 执行上下文栈](http://pic002.cnblogs.com/images/2011/349491/2011123113153760.png '图 4. 执行上下文栈') 178 | >图 4. 执行上下文栈 179 | 180 | > 当一段程序开始时,会先进入全局执行上下文环境[global execution context], 这个也是堆栈中最底部的元素。此全局程序会开始初始化,初始化生成必要的对象[objects]和函数[functions]. 在此全局上下文执行的过程中,它可能会激活一些方法(当然是已经初始化过的),然后进入他们的上下文环境,然后将新的元素压入堆栈。在这些初始化都结束之后,这个系统会等待一些事件(例如用户的鼠标点击等),会触发一些方法,然后进入一个新的上下文环境。 181 | 182 | > 见图5,有一个函数上下文“EC1″和一个全局上下文“Global EC”,下图展现了从“Global EC”进入和退出“EC1″时栈的变化: 183 | 184 | ![ 图 5. 执行上下文栈的变化](http://pic002.cnblogs.com/images/2011/349491/2011123113175418.png ' 图 5. 执行上下文栈的变化') 185 | > 图 5. 执行上下文栈的变化 186 | 187 | > ECMAScript运行时系统就是这样管理代码的执行。 188 | 189 | > 关于ECMAScript执行上下文栈的内容请查阅本系列教程的第11章执行上下文(Execution context)。 190 | 191 | > 如上所述,栈中每一个执行上下文可以表示为一个对象。让我们看看上下文对象的结构以及执行其代码所需的 状态(state) 。 192 | 193 | ### 执行上下文(Execution Context) 194 | 195 | > 一个执行的上下文可以抽象的理解为object。每一个执行的上下文都有一系列的属性(我们称为上下文状态),他们用来追踪关联代码的执行进度。这个图示就是一个context的结构。 196 | 197 | ![图 6. 上下文结构](http://pic002.cnblogs.com/images/2011/349491/2011123113224058.png '图 6. 上下文结构') 198 | > 图 6. 上下文结构 199 | 200 | > 除了这3个所需要的属性(**变量对象(variable object)**,**this指针(this value)**,**作用域链(scope chain)** ),执行上下文根据具体实现还可以具有任意额外属性。接着,让我们仔细来看看这三个属性。 201 | 202 | #### 变量对象(Variable Object) 203 | 204 | A variable object is a scope of data related with the execution context. 205 | It’s a special object associated with the context and which stores variables and function declarations are being defined within the context. 206 | 207 | 变量对象(variable object) 是与执行上下文相关的 数据作用域(scope of data) 。 208 | 它是与上下文关联的特殊对象,用于存储被定义在上下文中的 变量(variables) 和 函数声明(function declarations) 。 209 | 210 | > 注意:函数表达式[function expression](而不是函数声明[function declarations,区别请参考[本系列第2章](http://www.cnblogs.com/TomXu/archive/2011/12/29/2290308.html)])是不包含在VO[variable object]里面的。 211 | 212 | > 变量对象(Variable Object)是一个抽象的概念,不同的上下文中,它表示使用不同的object。例如,在global全局上下文中,变量对象也是全局对象自身[global object]。(这就是我们可以通过全局对象的属性来指向全局变量)。 213 | 214 | > 让我们看看下面例子中的全局执行上下文情况: 215 | 216 | var foo = 10; 217 | 218 | function bar() {} // // 函数声明 219 | (function baz() {}); // 函数表达式 220 | 221 | console.log( 222 | this.foo == foo, // true 223 | window.bar == bar // true 224 | ); 225 | 226 | console.log(baz); // 引用错误,baz没有被定义 227 | 228 | > 全局上下文中的变量对象(VO)会有如下属性: 229 | 230 | ![图 7. 全局变量对象](http://pic002.cnblogs.com/images/2011/349491/2011123113340977.png '图 7. 全局变量对象') 231 | > 图 7. 全局变量对象 232 | 233 | > 如上所示,函数“baz”如果作为函数表达式则不被不被包含于变量对象。这就是在函数外部尝试访问产生引用错误(ReferenceError) 的原因。请注意,ECMAScript和其他语言相比(比如C/C++),仅有函数能够创建新的作用域。在函数内部定义的变量与内部函数,在外部非直接可见并且不污染全局对象。使用 eval 的时候,我们同样会使用一个新的(eval创建)执行上下文。eval会使用全局变量对象或调用者的变量对象(eval的调用来源)。 234 | 235 | > 那函数以及自身的变量对象又是怎样的呢?在一个函数上下文中,变量对象被表示为活动对象(activation object)。 236 | 237 | #### 活动对象(activation object) 238 | 239 | > 当函数被调用者激活,这个特殊的活动对象(activation object) 就被创建了。它包含普通参数(formal parameters) 与特殊参数(arguments)对象(具有索引属性的参数映射表)。活动对象在函数上下文中作为变量对象使用。 240 | 241 | > 即:函数的变量对象保持不变,但除去存储变量与函数声明之外,还包含以及特殊对象arguments 。 242 | 243 | > 考虑下面的情况: 244 | 245 | function foo(x, y) { 246 | var z = 30; 247 | function bar() {} // 函数声明 248 | (function baz() {}); // 函数表达式 249 | } 250 | 251 | foo(10, 20); 252 | 253 | > “foo”函数上下文的下一个激活对象(AO)如下图所示: 254 | 255 | ![图 8. 激活对象](http://pic002.cnblogs.com/images/2011/349491/2011123113410532.png '图 8. 激活对象') 256 | > 图 8. 激活对象 257 | 258 | > 同样道理,function expression不在AO的行列。 259 | 260 | > 对于这个AO的详细内容可以通过本系列教程第9章找到。 261 | 262 | > 我们接下去要讲到的是第三个主要对象。众所周知,在ECMAScript中,我们会用到内部函数[inner functions],在这些内部函数中,我们可能会引用它的父函数变量,或者全局的变量。我们把这些变量对象成为上下文作用域对象[scope object of the context]. 类似于上面讨论的原型链[prototype chain],我们在这里称为作用域链[scope chain]。 263 | 264 | #### 作用域链(Scope Chains) 265 | 266 | A scope chain is a list of objects that are searched for identifiers appear in the code of the context. 267 | 作用域链是一个 对象列表(list of objects) ,用以检索上下文代码中出现的 标识符(identifiers) 。 268 | 269 | > 作用域链的原理和原型链很类似,如果这个变量在自己的作用域中没有,那么它会寻找父级的,直到最顶层。 270 | 271 | > 标示符[Identifiers]可以理解为变量名称、函数声明和普通参数。例如,当一个函数在自身函数体内需要引用一个变量,但是这个变量并没有在函数内部声明(或者也不是某个参数名),那么这个变量就可以称为自由变量[free variable]。那么我们搜寻这些自由变量就需要用到作用域链。 272 | 273 | > 在一般情况下,一个作用域链包括父级变量对象(variable object)(作用域链的顶部)、函数自身变量VO和活动对象(activation object)。不过,有些情况下也会包含其它的对象,例如在执行期间,动态加入作用域链中的—例如with或者catch语句。[译注:with-objects指的是with语句,产生的临时作用域对象;catch-clauses指的是catch从句,如catch(e),这会产生异常对象,导致作用域变更]。 274 | 275 | > 当查找标识符的时候,会从作用域链的活动对象部分开始查找,然后(如果标识符没有在活动对象中找到)查找作用域链的顶部,循环往复,就像作用域链那样。 276 | 277 | var x = 10; 278 | 279 | (function foo() { 280 | var y = 20; 281 | (function bar() { 282 | var z = 30; 283 | // "x"和"y"是自由变量 284 | // 会在作用域链的下一个对象中找到(函数”bar”的互动对象之后) 285 | console.log(x + y + z); 286 | })(); 287 | })(); 288 | 289 | > 我们假设作用域链的对象联动是通过一个叫做__parent__的属性,它是指向作用域链的下一个对象。这可以在Rhino Code中测试一下这种流程,这种技术也确实在ES5环境中实现了(有一个称为outer链接).当然也可以用一个简单的数据来模拟这个模型。使用__parent__的概念,我们可以把上面的代码演示成如下的情况。(因此,父级变量是被存在函数的[[Scope]]属性中的)。 290 | 291 | ![图 9. 作用域链](http://pic002.cnblogs.com/images/2011/349491/2011123113534163.png '图 9. 作用域链') 292 | > 图 9. 作用域链 293 | 294 | > 在代码执行过程中,如果使用with或者catch语句就会改变作用域链。而这些对象都是一些简单对象,他们也会有原型链。这样的话,作用域链会从两个维度来搜寻。 295 | 296 | 1. 首先在原本的作用域链 297 | 1. 每一个链接点的作用域的链(如果这个链接点是有prototype的话) 298 | 299 | > 我们再看下面这个例子: 300 | 301 | Object.prototype.x = 10; 302 | 303 | var w = 20; 304 | var y = 30; 305 | 306 | // 在SpiderMonkey全局对象里 307 | // 例如,全局上下文的变量对象是从"Object.prototype"继承到的 308 | // 所以我们可以得到“没有声明的全局变量” 309 | // 因为可以从原型链中获取 310 | 311 | console.log(x); // 10 312 | 313 | (function foo() { 314 | 315 | // "foo" 是局部变量 316 | var w = 40; 317 | var x = 100; 318 | 319 | // "x" 可以从"Object.prototype"得到,注意值是10哦 320 | // 因为{z: 50}是从它那里继承的 321 | 322 | with ({z: 50}) { 323 | console.log(w, x, y , z); // 40, 10, 30, 50 324 | } 325 | 326 | // 在"with"对象从作用域链删除之后 327 | // x又可以从foo的上下文中得到了,注意这次值又回到了100哦 328 | // "w" 也是局部变量 329 | console.log(x, w); // 100, 40 330 | 331 | // 在浏览器里 332 | // 我们可以通过如下语句来得到全局的w值 333 | console.log(window.w); // 20 334 | 335 | })(); 336 | 337 | > 我们就会有如下结构图示。这表示,在我们去搜寻__parent__之前,首先会去__proto__的链接中。 338 | 339 | ![图 10. with增大的作用域链](http://pic002.cnblogs.com/images/2011/349491/2011123114031781.png '图 10. with增大的作用域链') 340 | > 图 10. with增大的作用域链 341 | 342 | > 注意,不是所有的全局对象都是由Object.prototype继承而来的。上述图示的情况可以在SpiderMonkey中测试。 343 | 344 | > 只要所有外部函数的变量对象都存在,那么从内部函数引用外部数据则没有特别之处——我们只要遍历作用域链表,查找所需变量。然而,如上文所提及,当一个上下文终止之后,其状态与自身将会被 销毁(destroyed) ,同时内部函数将会从外部函数中返回。此外,这个返回的函数之后可能会在其他的上下文中被激活,那么如果一个之前被终止的含有一些自由变量的上下文又被激活将会怎样?通常来说,解决这个问题的概念在ECMAScript中与作用域链直接相关,被称为 (词法)闭包((lexical) closure)。 345 | 346 | #### 闭包(Closures) 347 | 348 | > 在ECMAScript中,函数是“第一类”对象。这个名词意味着函数可以作为参数被传递给其他函数使用 (在这种情况下,函数被称为“funargs”——“functional arguments”的缩写[译注:这里不知翻译为泛函参数是否恰当])。接收“funargs”的函数被称之为 高阶函数(higher-order functions) ,或者更接近数学概念的话,被称为 运算符(operators) 。其他函数的运行时也会返回函数,这些返回的函数被称为 function valued 函数 (有 functional value 的函数)。 349 | 350 | > “funargs”与“functional values”有两个概念上的问题,这两个子问题被称为“Funarg problem” (“泛函参数问题”)。要准确解决泛函参数问题,需要引入 闭包(closures) 到的概念。让我们仔细描述这两个问题(我们可以见到,在ECMAScript中使用了函数的[[Scope]]属性来解决这个问题)。 351 | 352 | > “funarg problem”的一个子问题是“upward funarg problem”[译注:或许可以翻译为:向上查找的函数参数问题]。当一个函数从其他函数返回到外部的时候,这个问题将会出现。要能够在外部上下文结束时,进入外部上下文的变量,内部函数 在创建的时候(at creation moment) 需要将之存储进[[Scope]]属性的父元素的作用域中。然后当函数被激活时,上下文的作用域链表现为激活对象与[[Scope]]属性的组合(事实上,可以在上图见到): 353 | 354 | Scope chain = Activation object + [[Scope]] 355 | 作用域链 = 活动对象 + [[Scope]] 356 | 357 | > 请注意,最主要的事情是——函数在被创建时保存外部作用域,是因为这个 被保存的作用域链(saved scope chain) 将会在未来的函数调用中用于变量查找。 358 | 359 | function foo() { 360 | var x = 10; 361 | return function bar() { 362 | console.log(x); 363 | }; 364 | } 365 | 366 | // "foo"返回的也是一个function 367 | // 并且这个返回的function可以随意使用内部的变量x 368 | 369 | var returnedFunction = foo(); 370 | 371 | // 全局变量 "x" 372 | var x = 20; 373 | 374 | // 支持返回的function 375 | returnedFunction(); // 结果是10而不是20 376 | 377 | > 这种形式的作用域称为静态作用域[static/lexical scope]。上面的x变量就是在函数bar的[[Scope]]中搜寻到的。理论上来说,也会有动态作用域[dynamic scope], 也就是上述的x被解释为20,而不是10. 但是EMCAScript不使用动态作用域。 378 | 379 | > “funarg problem”的另一个类型就是自上而下[”downward funarg problem”].在这种情况下,父级的上下会存在,但是在判断一个变量值的时候会有多义性。也就是,这个变量究竟应该使用哪个作用域。是在函数创建时的作用域呢,还是在执行时的作用域呢?为了避免这种多义性,可以采用闭包,也就是使用静态作用域。 380 | 381 | > 请看下面的例子: 382 | 383 | // 全局变量 "x" 384 | var x = 10; 385 | 386 | // 全局function 387 | function foo() { 388 | console.log(x); 389 | } 390 | 391 | (function (funArg) { 392 | 393 | // 局部变量 "x" 394 | var x = 20; 395 | 396 | // 这不会有歧义 397 | // 因为我们使用"foo"函数的[[Scope]]里保存的全局变量"x", 398 | // 并不是caller作用域的"x" 399 | 400 | funArg(); // 10, 而不是20 401 | 402 | })(foo); // 将foo作为一个"funarg"传递下去 403 | 404 | > 从上述的情况,我们似乎可以断定,在语言中,使用静态作用域是闭包的一个强制性要求。不过,在某些语言中,会提供动态和静态作用域的结合,可以允许开发员选择哪一种作用域。但是在ECMAScript中,只采用了静态作用域。所以ECMAScript完全支持使用[[Scope]]的属性。我们可以给闭包得出如下定义: 405 | 406 | A closure is a combination of a code block (in ECMAScript this is a function) and statically/lexically saved all parent scopes. 407 | Thus, via these saved scopes a function may easily refer free variables. 408 | 闭包是一系列代码块(在ECMAScript中是函数),并且静态保存所有父级的作用域。通过这些保存的作用域来搜寻到函数中的自由变量。 409 | 410 | > 请注意,因为每一个普通函数在创建时保存了[[Scope]],理论上,ECMAScript中所有函数都是闭包。 411 | 412 | > 还有一个很重要的点,几个函数可能含有相同的父级作用域(这是一个很普遍的情况,例如有好几个内部或者全局的函数)。在这种情况下,在[[Scope]]中存在的变量是会共享的。一个闭包中变量的变化,也会影响另一个闭包的。 413 | 414 | function baz() { 415 | var x = 1; 416 | return { 417 | foo: function foo() { return ++x; }, 418 | bar: function bar() { return --x; } 419 | }; 420 | } 421 | 422 | var closures = baz(); 423 | 424 | console.log( 425 | closures.foo(), // 2 426 | closures.bar() // 1 427 | ); 428 | 429 | > 上述代码可以用这张图来表示: 430 | 431 | ![图 11. 共享的[[Scope]]](http://pic002.cnblogs.com/images/2011/349491/2011123114184023.png '图 11. 共享的[[Scope]]') 432 | > 图 11. 共享的[[Scope]] 433 | 434 | > 在某个循环中创建多个函数时,上图会引发一个困惑。如果在创建的函数中使用循环变量(如”k”),那么所有的函数都使用同样的循环变量,导致一些程序员经常会得不到预期值。现在清楚为什么会产生如此问题了——因为所有函数共享同一个[[Scope]],其中循环变量为最后一次复赋值。 435 | 436 | > var data = []; 437 | 438 | for (var k = 0; k < 3; k++) { 439 | data[k] = function () { 440 | alert(k); 441 | }; 442 | } 443 | 444 | data[0](); // 3, but not 0 445 | data[1](); // 3, but not 1 446 | data[2](); // 3, but not 2 447 | 448 | > 有一些用以解决这类问题的技术。其中一种技巧是在作用域链中提供一个额外的对象,比如增加一个函数: 449 | 450 | var data = []; 451 | 452 | for (var k = 0; k < 3; k++) { 453 | data[k] = (function (x) { 454 | return function () { 455 | alert(x); 456 | }; 457 | })(k); // 将k当做参数传递进去 458 | } 459 | 460 | // 结果正确 461 | data[0](); // 0 462 | data[1](); // 1 463 | data[2](); // 2 464 | 465 | > 闭包理论的深入研究与具体实践可以在本系列教程第16章闭包(Closures)中找到。如果想得到关于作用域链的更多信息,可以参照本系列教程第14章作用域链(Scope chain)。 466 | 467 | > 下一章节将会讨论一个执行上下文的最后一个属性——this指针的概念。 468 | 469 | ### This指针 470 | 471 | A this value is a special object which is related with the execution context. 472 | Therefore, it may be named as a context object (i.e. an object in which context the execution context is activated). 473 | this适合执行的上下文环境息息相关的一个特殊对象。因此,它也可以称为上下文对象[context object](激活执行上下文的上下文)。 474 | 475 | > 任何对象都可以作为上下文的this值。我想再次澄清对与ECMAScript中,与执行上下文相关的一些描述——特别是this的误解。通常,this 被错误地,描述为变量对象的属性。最近比如在[这本书](http://yuiblog.com/assets/High_Perf_JavaScr_Ch2.pdf)中就发现了(尽管书中提及this的那一章还不错)。 请牢记: 476 | 477 | a this value is a property of the execution context, but not a property of the variable object. 478 | this是执行上下文环境的一个属性,而不是某个变量对象的属性 479 | 480 | > 这个特点很重要,因为和变量不同,this是没有一个类似搜寻变量的过程。当你在代码中使用了this,这个 this的值就直接从执行的上下文中获取了,而不会从作用域链中搜寻。this的值只取决中进入上下文时的情况。 481 | 482 | > 顺便说一句,和ECMAScript不同,Python有一个self的参数,和this的情况差不多,但是可以在执行过程中被改变。在ECMAScript中,是不可以给this赋值的,因为,还是那句话,this不是变量。 483 | 484 | > 在global context(全局上下文)中,this的值就是指全局这个对象,这就意味着,this值就是这个变量本身。 485 | 486 | var x = 10; 487 | 488 | console.log( 489 | x, // 10 490 | this.x, // 10 491 | window.x // 10 492 | ); 493 | 494 | > 在函数上下文[function context]中,this会可能会根据每次的函数调用而成为不同的值.this会由每一次caller提供,caller是通过调用表达式[call expression]产生的(也就是这个函数如何被激活调用的)。例如,下面的例子中foo就是一个callee,在全局上下文中被激活。下面的例子就表明了不同的caller引起this的不同。 495 | 496 | // "foo"函数里的alert没有改变 497 | // 但每次激活调用的时候this是不同的 498 | 499 | function foo() { 500 | alert(this); 501 | } 502 | 503 | // caller 激活 "foo"这个callee, 504 | // 并且提供"this"给这个 callee 505 | 506 | foo(); // 全局对象 507 | foo.prototype.constructor(); // foo.prototype 508 | 509 | var bar = { 510 | baz: foo 511 | }; 512 | 513 | bar.baz(); // bar 514 | 515 | (bar.baz)(); // also bar 516 | (bar.baz = bar.baz)(); // 这是一个全局对象 517 | (bar.baz, bar.baz)(); // 也是全局对象 518 | (false || bar.baz)(); // 也是全局对象 519 | 520 | var otherFoo = bar.baz; 521 | otherFoo(); // 还是全局对象 522 | 523 | > 如果要深入思考每一次函数调用中,this值的变化(更重要的是怎样变化),你可以阅读本系列教程第10章This。上文所提及的情况都会在此章内详细讨论。 524 | 525 | ## 总结(Conclusion) 526 | 527 | > 在此我们完成了一个简短的概述。尽管看来不是那么简短,但是这些话题若要完整表述完毕,则需要一整本书。.我们没有提及两个重要话题:函数(functions) (以及不同类型的函数之间的不同,比如函数声明与函数表达式)与ECMAScript的 求值策略(evaluation strategy) 。这两个话题可以分别查阅本系列教程第15章函数(Functions) 与第19章求值策略(Evaluation strategy)。 528 | 529 | > 如果你有任何评论,问题或者补充,我很欢迎在文章评论中讨论。 530 | 531 | > 祝大家学习ECMAScript顺利。 532 | 533 | ## 致谢 534 | 535 | > 翻译编辑:[汤姆大叔的博客](http://www.cnblogs.com/TomXu/ '汤姆大叔的博客') 536 | 537 | > 原文地址:[深入理解JavaScript系列(10):JavaScript核心(晋级高手必读篇)](http://www.cnblogs.com/TomXu/archive/2012/01/12/2308594.html '深入理解JavaScript系列(10):JavaScript核心(晋级高手必读篇)') 538 | 539 | > 修正/编辑:[ahkjxy](https://github.com/ahkjxy/) -------------------------------------------------------------------------------- /2-揭秘命名函数表达式.md: -------------------------------------------------------------------------------- 1 | ## 深入理解JavaScript系列(2):揭秘命名函数表达式 2 | 3 | ### 前言 4 | 5 | > 网上还没发现有人对命名函数表达式进去重复深入的讨论,正因为如此,网上出现了各种各样的误解,本文将从原理和实践两个方面来探讨JavaScript关于命名函数表达式的优缺点。 6 | 7 | > 简单的说,命名函数表达式只有一个用户,那就是在Debug或者Profiler分析的时候来描述函数的名称,也可以使用函数名实现递归,但很快你就会发现其实是不切实际的。当然,如果你不关注调试,那就没什么可担心的了,否则,如果你想了解兼容性方面的东西的话,你还是应该继续往下看看。 8 | 9 | > 我们先开始看看,什么叫函数表达式,然后再说一下现代调试器如何处理这些表达式,如果你已经对这方面很熟悉的话,请直接跳过此小节。 10 | 11 | ### 函数表达式和函数声明 12 | 13 | > 在ECMAScript中,创建函数的最常用的两个方法是函数表达式和函数声明,两者期间的区别是有点晕,因为ECMA规范只明确了一点:函数声明必须带有标示符(Identifier)(就是大家常说的函数名称),而函数表达式则可以省略这个标示符: 14 | 15 | 函数声明: 16 | 17 |   function 函数名称 (参数:可选){ 函数体 } 18 | 19 |   函数表达式: 20 | 21 |   function 函数名称(可选)(参数:可选){ 函数体 } 22 | 23 | > 所以,可以看出,如果不声明函数名称,它肯定是表达式,可如果声明了函数名称的话,如何判断是函数声明还是函数表达式呢?ECMAScript是通过上下文来区分的,如果function foo(){}是作为赋值表达式的一部分的话,那它就是一个函数表达式,如果function foo(){}被包含在一个函数体内,或者位于程序的最顶部的话,那它就是一个函数声明。 24 | 25 | function foo(){} // 声明,因为它是程序的一部分 26 | var bar = function foo(){}; // 表达式,因为它是赋值表达式的一部分 27 | 28 | new function bar(){}; // 表达式,因为它是new表达式 29 | 30 | (function(){ 31 | function bar(){} // 声明,因为它是函数体的一部分 32 | })(); 33 | 34 | > 还有一种函数表达式不太常见,就是被括号括住的(function foo(){}),他是表达式的原因是因为括号 ()是一个分组操作符,它的内部只能包含表达式,我们来看几个例子: 35 | 36 | function foo(){} // 函数声明 37 | (function foo(){}); // 函数表达式:包含在分组操作符内 38 | 39 | try { 40 | (var x = 5); // 分组操作符,只能包含表达式而不能包含语句:这里的var就是语句 41 | } catch(err) { 42 | // SyntaxError 43 | } 44 | 45 | > 你可以会想到,在使用eval对JSON进行执行的时候,JSON字符串通常被包含在一个圆括号里:eval('(' + json + ')'),这样做的原因就是因为分组操作符,也就是这对括号,会让解析器强制将JSON的花括号解析成表达式而不是代码块。 46 | 47 | try { 48 | { "x": 5 }; // "{" 和 "}" 做解析成代码块 49 | } catch(err) { 50 | // SyntaxError 51 | } 52 | ({ "x": 5 }); // 分组操作符强制将"{" 和 "}"作为对象字面量来解析 53 | 54 | > 表达式和声明存在着十分微妙的差别,首先,函数声明会在任何表达式被解析和求值之前先被解析和求值,即使你的声明在代码的最后一行,它也会在同作用域内第一个表达式之前被解析/求值,参考如下例子,函数fn是在alert之后声明的,但是在alert执行的时候,fn已经有定义了: 55 | 56 | alert(fn()); 57 | 58 | function fn() { 59 | return 'Hello world!'; 60 | } 61 | 62 | > 另外,还有一点需要提醒一下,函数声明在条件语句内虽然可以用,但是没有被标准化,也就是说不同的环境可能有不同的执行结果,所以这样情况下,最好使用函数表达式: 63 | 64 | // 千万别这样做! 65 | // 因为有的浏览器会返回first的这个function,而有的浏览器返回的却是第二个 66 | 67 | if (true) { 68 | function foo() { 69 | return 'first'; 70 | } 71 | } else { 72 | function foo() { 73 | return 'second'; 74 | } 75 | } 76 | foo(); 77 | 78 | // 相反,这样情况,我们要用函数表达式 79 | var foo; 80 | if (true) { 81 | foo = function() { 82 | return 'first'; 83 | }; 84 | } else { 85 | foo = function() { 86 | return 'second'; 87 | }; 88 | } 89 | foo(); 90 | 91 | > 函数声明的实际规则如下: 92 | 93 | > 函数声明只能出现在程序或函数体内。从句法上讲,它们不能出现在Block(块)({ ... })中,例如不能出现在 if、while 或 for 语句中。因为 Block(块) 中只能包含Statement语句, 而不能包含函数声明这样的源元素。另一方面,仔细看一看规则也会发现,唯一可能让表达式出现在Block(块)中情形,就是让它作为表达式语句的一部分。但是,规范明确规定了表达式语句不能以关键字function开头。而这实际上就是说,函数表达式同样也不能出现在Statement语句或Block(块)中(因为Block(块) 94 | 就是由Statement语句构成的)。 95 | 96 | ### 函数语句 97 | 98 | > 在ECMAScript的语法扩展中,有一个是函数语句,目前只有基于Gecko的浏览器实现了该扩展,所以对于下面的例子,我们仅是抱着学习的目的来看,一般来说不推荐使用(除非你针对Gecko浏览器进行开发)。 99 | 100 | #### 1.一般语句能用的地方,函数语句也能用,当然也包括Block块中: 101 | 102 | if (true) { 103 | function f(){ } 104 | } 105 | else { 106 | function f(){ } 107 | } 108 | 109 | #### 2.函数语句可以像其他语句一样被解析,包含基于条件执行的情形 110 | 111 | if (true) { 112 | function foo(){ return 1; } 113 | } 114 | else { 115 | function foo(){ return 2; } 116 | } 117 | foo(); // 1 118 | // 注:其它客户端会将foo解析成函数声明 119 | // 因此,第二个foo会覆盖第一个,结果返回2,而不是1 120 | 121 | #### 3.函数语句不是在变量初始化期间声明的,而是在运行时声明的——与函数表达式一样。不过,函数语句的标识符一旦声明能在函数的整个作用域生效了。标识符有效性正是导致函数语句与函数表达式不同的关键所在(下一小节我们将会展示命名函数表达式的具体行为)。 122 | 123 | // 此刻,foo还没用声明 124 | typeof foo; // "undefined" 125 | if (true) { 126 | // 进入这里以后,foo就被声明在整个作用域内了 127 | function foo(){ return 1; } 128 | } 129 | else { 130 | // 从来不会走到这里,所以这里的foo也不会被声明 131 | function foo(){ return 2; } 132 | } 133 | typeof foo; // "function" 134 | 135 | > 不过,我们可以使用下面这样的符合标准的代码来模式上面例子中的函数语句: 136 | 137 | var foo; 138 | if (true) { 139 | foo = function foo(){ return 1; }; 140 | } 141 | else { 142 | foo = function foo() { return 2; }; 143 | } 144 | 145 | #### 4.函数语句和函数声明(或命名函数表达式)的字符串表示类似,也包括标识符: 146 | 147 | if (true) { 148 | function foo(){ return 1; } 149 | } 150 | String(foo); // function foo() { return 1; } 151 | 152 | #### 5.另外一个,早期基于Gecko的实现(Firefox 3及以前版本)中存在一个bug,即函数语句覆盖函数声明的方式不正确。在这些早期的实现中,函数语句不知何故不能覆盖函数声明: 153 | 154 | // 函数声明 155 | function foo(){ return 1; } 156 | if (true) { 157 | // 用函数语句重写 158 | function foo(){ return 2; } 159 | } 160 | foo(); // FF3以下返回1,FF3.5以上返回2 161 | 162 | // 不过,如果前面是函数表达式,则没用问题 163 | var foo = function(){ return 1; }; 164 | if (true) { 165 | function foo(){ return 2; } 166 | } 167 | foo(); // 所有版本都返回2 168 | 169 | > **再次强调一点,上面这些例子只是在某些浏览器支持,所以推荐大家不要使用这些,除非你就在特性的浏览器上做开发。** 170 | 171 | ### 命名函数表达式 172 | 173 | > 函数表达式在实际应用中还是很常见的,在web开发中友个常用的模式是基于对某种特性的测试来伪装函数定义,从而达到性能优化的目的,但由于这种方式都是在同一作用域内,所以基本上一定要用函数表达式: 174 | 175 | // 该代码来自Garrett Smith的APE Javascript library库(http://dhtmlkitchen.com/ape/) 176 | var contains = (function() { 177 | var docEl = document.documentElement; 178 | 179 | if (typeof docEl.compareDocumentPosition != 'undefined') { 180 | return function(el, b) { 181 | return (el.compareDocumentPosition(b) & 16) !== 0; 182 | }; 183 | } 184 | else if (typeof docEl.contains != 'undefined') { 185 | return function(el, b) { 186 | return el !== b && el.contains(b); 187 | }; 188 | } 189 | return function(el, b) { 190 | if (el === b) return false; 191 | while (el != b && (b = b.parentNode) != null); 192 | return el === b; 193 | }; 194 | })(); 195 | 196 | > 提到命名函数表达式,理所当然,就是它得有名字,前面的例子var bar = function foo(){};就是一个有效的命名函数表达式,但有一点需要记住:这个名字只在新定义的函数作用域内有效,因为规范规定了标示符不能在外围的作用域内有效: 197 | 198 | var f = function foo(){ 199 | return typeof foo; // foo是在内部作用域内有效 200 | }; 201 | // foo在外部用于是不可见的 202 | typeof foo; // "undefined" 203 | f(); // "function" 204 | 205 | - 既然,这么要求,那命名函数表达式到底有啥用啊?为啥要取名? 206 | - 正如我们开头所说:给它一个名字就是可以让调试过程更方便,因为在调试的时候,如果在调用栈中的每个项都有自己的名字来描述,那么调试过程就太爽了,感受不一样嘛。 207 | 208 | ### 调试器中的函数名 209 | 210 | > 如果一个函数有名字,那调试器在调试的时候会将它的名字显示在调用的栈上。有些调试器(Firebug)有时候还会为你们函数取名并显示,让他们和那些应用该函数的便利具有相同的角色,可是通常情况下,这些调试器只安装简单的规则来取名,所以说没有太大价格,我们来看一个例子: 211 | 212 | function foo(){ 213 | return bar(); 214 | } 215 | function bar(){ 216 | return baz(); 217 | } 218 | function baz(){ 219 | debugger; 220 | } 221 | foo(); 222 | 223 | // 这里我们使用了3个带名字的函数声明 224 | // 所以当调试器走到debugger语句的时候,Firebug的调用栈上看起来非常清晰明了 225 | // 因为很明白地显示了名称 226 | baz 227 | bar 228 | foo 229 | expr_test.html() 230 | 231 | > 通过查看调用栈的信息,我们可以很明了地知道foo调用了bar, bar又调用了baz(而foo本身有在expr_test.html文档的全局作用域内被调用),不过,还有一个比较爽地方,就是刚才说的Firebug为匿名表达式取名的功能: 232 | 233 | function foo(){ 234 | return bar(); 235 | } 236 | var bar = function(){ 237 | return baz(); 238 | } 239 | function baz(){ 240 | debugger; 241 | } 242 | foo(); 243 | 244 | // Call stack 245 | baz 246 | bar() //看到了么? 247 | foo 248 | expr_test.html() 249 | 250 | > 然后,当函数表达式稍微复杂一些的时候,调试器就不那么聪明了,我们只能在调用栈中看到问号: 251 | 252 | function foo(){ 253 | return bar(); 254 | } 255 | var bar = (function(){ 256 | if (window.addEventListener) { 257 | return function(){ 258 | return baz(); 259 | }; 260 | } else if (window.attachEvent) { 261 | return function() { 262 | return baz(); 263 | }; 264 | } 265 | })(); 266 | function baz(){ 267 | debugger; 268 | } 269 | foo(); 270 | 271 | // Call stack 272 | baz 273 | (?)() // 这里可是问号哦 274 | foo 275 | expr_test.html() 276 | 277 | > 另外,当把函数赋值给多个变量的时候,也会出现令人郁闷的问题: 278 | 279 | function foo(){ 280 | return baz(); 281 | } 282 | var bar = function(){ 283 | debugger; 284 | }; 285 | var baz = bar; 286 | bar = function() { 287 | alert('spoofed'); 288 | }; 289 | foo(); 290 | 291 | // Call stack: 292 | bar() 293 | foo 294 | expr_test.html() 295 | 296 | > 这时候,调用栈显示的是foo调用了bar,但实际上并非如此,之所以有这种问题,是因为baz和另外一个包含alert('spoofed')的函数做了引用交换所导致的。 297 | 298 | > 归根结底,只有给函数表达式取个名字,才是最委托的办法,也就是使用命名函数表达式。我们来使用带名字的表达式来重写上面的例子(注意立即调用的表达式块里返回的2个函数的名字都是bar): 299 | 300 | function foo(){ 301 | return bar(); 302 | } 303 | var bar = (function(){ 304 | if (window.addEventListener) { 305 | return function bar(){ 306 | return baz(); 307 | }; 308 | } else if (window.attachEvent) { 309 | return function bar() { 310 | return baz(); 311 | }; 312 | } 313 | })(); 314 | function baz(){ 315 | debugger; 316 | } 317 | foo(); 318 | 319 | // 又再次看到了清晰的调用栈信息了耶! 320 | baz 321 | bar 322 | foo 323 | expr_test.html() 324 | 325 | > OK,又学了一招吧?不过在高兴之前,我们再看看不同寻常的JScript吧。 326 | 327 | ### JScript的Bug 328 | 329 | > 比较恶的是,IE的ECMAScript实现JScript严重混淆了命名函数表达式,搞得现很多人都出来反对命名函数表达式,而且即便是最新的一版(IE8中使用的5.8版)仍然存在下列问题。 330 | 331 | > 下面我们就来看看IE在实现中究竟犯了那些错误,俗话说知已知彼,才能百战不殆。我们来看看如下几个例子: 332 | 333 | #### 例1:函数表达式的标示符泄露到外部作用域 334 | 335 | var f = function g(){}; 336 | typeof g; // "function" 337 | 338 | > 上面我们说过,命名函数表达式的标示符在外部作用域是无效的,但JScript明显是违反了这一规范,上面例子中的标示符g被解析成函数对象,这就乱了套了,很多难以发现的bug都是因为这个原因导致的。 339 | 340 | >> 注:IE9貌似已经修复了这个问题 341 | 342 | #### 例2:将命名函数表达式同时当作函数声明和函数表达式 343 | 344 | typeof g; // "function" 345 | var f = function g(){}; 346 | 347 | > 特性环境下,函数声明会优先于任何表达式被解析,上面的例子展示的是JScript实际上是把命名函数表达式当成函数声明了,因为它在实际声明之前就解析了g。 **这个例子引出了下一个例子。** 348 | 349 | #### 例3:命名函数表达式会创建两个截然不同的函数对象! 350 | 351 | var f = function g(){}; 352 | f === g; // false 353 | 354 | f.expando = 'foo'; 355 | g.expando; // undefined 356 | 357 | > 看到这里,大家会觉得问题严重了,因为修改任何一个对象,另外一个没有什么改变,这太恶了。通过这个例子可以发现,创建2个不同的对象,也就是说如果你想修改f的属性中保存某个信息,然后想当然地通过引用相同对象的g的同名属性来使用,那问题就大了,因为根本就不可能。**再来看一个稍微复杂的例子:** 358 | 359 | #### 例4:仅仅顺序解析函数声明而忽略条件语句块 360 | var f = function g() { 361 | return 1; 362 | }; 363 | if (false) { 364 | f = function g(){ 365 | return 2; 366 | }; 367 | } 368 | g(); // 2 369 | 370 | > 这个bug查找就难多了,但导致bug的原因却非常简单。首先,g被当作函数声明解析,由于JScript中的函数声明不受条件代码块约束,所以在这个很恶的if分支中,g被当作另一个函数function g(){ return 2 },也就是又被声明了一次。然后,所有“常规的”表达式被求值,而此时f被赋予了另一个新创建的对象的引用。由于在对表达式求值的时候,永远不会进入“这个可恶if分支,因此f就会继续引用第一个函数function g(){ return 1 }。分析到这里,问题就很清楚了:假如你不够细心,在f中调用了g,那么将会调用一个毫不相干的g函数对象。 371 | 372 | > 你可能会文,将不同的对象和arguments.callee相比较时,有什么样的区别呢?我们来看看: 373 | 374 | var f = function g(){ 375 | return [ 376 | arguments.callee == f, 377 | arguments.callee == g 378 | ]; 379 | }; 380 | f(); // [true, false] 381 | g(); // [false, true] 382 | 383 | > 可以看到,arguments.callee的引用一直是被调用的函数,实际上这也是好事,稍后会解释。 384 | 385 | > 还有一个有趣的例子,那就是在不包含声明的赋值语句中使用命名函数表达式: 386 | 387 | (function(){ 388 | f = function f(){}; 389 | })(); 390 | 391 | > 按照代码的分析,我们原本是想创建一个全局属性f(注意不要和一般的匿名函数混淆了,里面用的是带名字的生命),JScript在这里捣乱了一把,首先他把表达式当成函数声明解析了,所以左边的f被声明为局部变量了(和一般的匿名函数里的声明一样),然后在函数执行的时候,f已经是定义过的了,右边的function f(){}则直接就赋值给局部变量f了,所以f根本就不是全局属性。 392 | 393 | > 了解了JScript这么变态以后,我们就要及时预防这些问题了,首先**防范标识符泄漏带外部作用域**,其次,应该永远**不引用被用作函数名称的标识符**;还记得前面例子中那个讨人厌的标识符g吗?——如果我们能够当g不存在,可以避免多少不必要的麻烦哪。因此,关键就在于始终要通过f或者arguments.callee来引用函数。如果你使用了命名函数表达式,那么应该只在调试的时候利用那个名字。最后,还要记住一点,一定要**把命名函数表达式声明期间错误创建的函数清理干净**。 394 | 395 | > 对于,上面最后一点,我们还得再解释一下。 396 | 397 | ### JScript的内存管理 398 | 399 | > 知道了这些不符合规范的代码解析bug以后,我们如果用它的话,就会发现内存方面其实是有问题的,来看一个例子: 400 | 401 | var f = (function(){ 402 | if (true) { 403 | return function g(){}; 404 | } 405 | return function g(){}; 406 | })(); 407 | 408 | > 我们知道,这个匿名函数调用返回的函数(带有标识符g的函数),然后赋值给了外部的f。我们也知道,命名函数表达式会导致产生多余的函数对象,而该对象与返回的函数对象不是一回事。所以这个多余的g函数就死在了返回函数的闭包中了,因此内存问题就出现了。这是因为if语句内部的函数与g是在同一个作用域中被声明的。这种情况下 ,除非我们显式断开对g函数的引用,否则它一直占着内存不放。 409 | 410 | var f = (function(){ 411 | var f, g; 412 | if (true) { 413 | f = function g(){}; 414 | } 415 | else { 416 | f = function g(){}; 417 | } 418 | // 设置g为null以后它就不会再占内存了 419 | g = null; 420 | return f; 421 | })(); 422 | 423 | > 通过设置g为null,垃圾回收器就把g引用的那个隐式函数给回收掉了,为了验证我们的代码,我们来做一些测试,以确保我们的内存被回收了。 424 | 425 | > 测试 426 | 427 | > 测试很简单,就是命名函数表达式创建10000个函数,然后把它们保存在一个数组中。等一会儿以后再看这些函数到底占用了多少内存。然后,再断开这些引用并重复这一过程。下面是测试代码: 428 | 429 | function createFn(){ 430 | return (function(){ 431 | var f; 432 | if (true) { 433 | f = function F(){ 434 | return 'standard'; 435 | }; 436 | } 437 | else if (false) { 438 | f = function F(){ 439 | return 'alternative'; 440 | }; 441 | } 442 | else { 443 | f = function F(){ 444 | return 'fallback'; 445 | }; 446 | } 447 | // var F = null; 448 | return f; 449 | })(); 450 | } 451 | 452 | var arr = [ ]; 453 | for (var i=0; i<10000; i++) { 454 | arr[i] = createFn(); 455 | } 456 | 457 | > 通过运行在Windows XP SP2中的任务管理器可以看到如下结果: 458 | 459 | IE6: 460 | 461 | without `null`: 7.6K -> 20.3K 462 | with `null`: 7.6K -> 18K 463 | 464 | IE7: 465 | 466 | without `null`: 14K -> 29.7K 467 | with `null`: 14K -> 27K 468 | 469 | 470 | > 如我们所料,显示断开引用可以释放内存,但是释放的内存不是很多,10000个函数对象才释放大约3M的内存,这对一些小型脚本不算什么,但对于大型程序,或者长时间运行在低内存的设备里的时候,这是非常有必要的。 471 | 472 | > 关于在Safari 2.x中JS的解析也有一些bug,但介于版本比较低,所以我们在这里就不介绍了,大家如果想看的话,请仔细查看英文资料。 473 | 474 | ### [SpiderMonkey](http://baike.baidu.com/link?url=UB1tPykg7oW5-346bBncUi2Byr9bkF4FdzGlcXIza5pBHw-IT0audhziNDPIJslRNPaCnKAyK9Rp0MUyxw5ztq 'SpiderMonkey介绍')的怪癖 475 | 476 | > 大家都知道,命名函数表达式的标识符只在函数的局部作用域中有效。但包含这个标识符的局部作用域又是什么样子的吗?其实非常简单。在命名函数表达式被求值时,**会创建一个特殊的对象**,该对象的唯一目的就是保存一个属性,而这个属性的名字对应着函数标识符,属性的值对应着那个函数。这个对象会被注入到当前作用域链的前端。然后,被“扩展”的作用域链又被用于初始化函数。 477 | 478 | > 在这里,有一点十分有意思,那就是ECMA-262定义这个(保存函数标识符的)“特殊”对象的方式。标准说“**像调用new Object()表达式那样**”创建这个对象。如果从字面上来理解这句话,那么这个对象就应该是全局Object的一个实例。然而,只有一个实现是按照标准字面上的要求这么做的,这个实现就是SpiderMonkey。因此,在SpiderMonkey中,扩展Object.prototype有可能会干扰函数的局部作用域: 479 | 480 | Object.prototype.x = 'outer'; 481 | 482 | (function(){ 483 | 484 | var x = 'inner'; 485 | 486 | /* 487 | 函数foo的作用域链中有一个特殊的对象——用于保存函数的标识符。这个特殊的对象实际上就是{ foo: }。 488 | 当通过作用域链解析x时,首先解析的是foo的局部环境。如果没有找到x,则继续搜索作用域链中的下一个对象。下一个对象 489 | 就是保存函数标识符的那个对象——{ foo: },由于该对象继承自Object.prototype,所以在此可以找到x。 490 | 而这个x的值也就是Object.prototype.x的值(outer)。结果,外部函数的作用域(包含x = 'inner'的作用域)就不会被解析了。 491 | */ 492 | 493 | (function foo(){ 494 | 495 | alert(x); // 提示框中显示:outer 496 | 497 | })(); 498 | })(); 499 | 500 | > 不过,更高版本的SpiderMonkey改变了上述行为,原因可能是认为那是一个安全漏洞。也就是说,“特殊”对象不再继承Object.prototype了。不过,如果你使用Firefox 3或者更低版本,还可以“重温”这种行为。 501 | 502 | > 另一个把内部对象实现为全局Object对象的是黑莓(Blackberry)浏览器。目前,它的活动对象(Activation Object)仍然继承Object.prototype。可是,ECMA-262并没有说活动对象也要“像调用new Object()表达式那样”来创建(或者说像创建保存NFE标识符的对象一样创建)。 人家规范只说了活动对象是规范中的一种机制。 503 | 504 | > 那我们就来看看黑莓里都发生了什么: 505 | 506 | Object.prototype.x = 'outer'; 507 | 508 | (function(){ 509 | 510 | var x = 'inner'; 511 | 512 | (function(){ 513 | 514 | /* 515 | 在沿着作用域链解析x的过程中,首先会搜索局部函数的活动对象。当然,在该对象中找不到x。 516 | 可是,由于活动对象继承自Object.prototype,因此搜索x的下一个目标就是Object.prototype;而 517 | Object.prototype中又确实有x的定义。结果,x的值就被解析为——outer。跟前面的例子差不多, 518 | 包含x = 'inner'的外部函数的作用域(活动对象)就不会被解析了。 519 | */ 520 | 521 | alert(x); // 显示:outer 522 | 523 | })(); 524 | })(); 525 | 526 | > 不过神奇的还是,函数中的变量甚至会与已有的Object.prototype的成员发生冲突,来看看下面的代码: 527 | 528 | (function(){ 529 | 530 | var constructor = function(){ return 1; }; 531 | 532 | (function(){ 533 | 534 | constructor(); // 求值结果是{}(即相当于调用了Object.prototype.constructor())而不是1 535 | 536 | constructor === Object.prototype.constructor; // true 537 | toString === Object.prototype.toString; // true 538 | 539 | // …… 540 | 541 | })(); 542 | })(); 543 | 544 | > 要避免这个问题,要避免使用Object.prototype里的属性名称,如toString, valueOf, hasOwnProperty等等。 545 | 546 | > **JScript解决方案** 547 | 548 | var fn = (function(){ 549 | 550 | // 声明要引用函数的变量 551 | var f; 552 | 553 | // 有条件地创建命名函数 554 | // 并将其引用赋值给f 555 | if (true) { 556 | f = function F(){ } 557 | } 558 | else if (false) { 559 | f = function F(){ } 560 | } 561 | else { 562 | f = function F(){ } 563 | } 564 | 565 | // 声明一个与函数名(标识符)对应的变量,并赋值为null 566 | // 这实际上是给相应标识符引用的函数对象作了一个标记, 567 | // 以便垃圾回收器知道可以回收它了 568 | var F = null; 569 | 570 | // 返回根据条件定义的函数 571 | return f; 572 | })(); 573 | 574 | > 最后我们给出一个应用上述技术的应用实例,这是一个跨浏览器的addEvent函数代码: 575 | 576 | // 1) 使用独立的作用域包含声明 577 | var addEvent = (function(){ 578 | 579 | var docEl = document.documentElement; 580 | 581 | // 2) 声明要引用函数的变量 582 | var fn; 583 | 584 | if (docEl.addEventListener) { 585 | 586 | // 3) 有意给函数一个描述性的标识符 587 | fn = function addEvent(element, eventName, callback) { 588 | element.addEventListener(eventName, callback, false); 589 | } 590 | } 591 | else if (docEl.attachEvent) { 592 | fn = function addEvent(element, eventName, callback) { 593 | element.attachEvent('on' + eventName, callback); 594 | } 595 | } 596 | else { 597 | fn = function addEvent(element, eventName, callback) { 598 | element['on' + eventName] = callback; 599 | } 600 | } 601 | 602 | // 4) 清除由JScript创建的addEvent函数 603 | // 一定要保证在赋值前使用var关键字 604 | // 除非函数顶部已经声明了addEvent 605 | var addEvent = null; 606 | 607 | // 5) 最后返回由fn引用的函数 608 | return fn; 609 | })(); 610 | 611 | ### 替代方案 612 | 613 | > 其实,如果我们不想要这个描述性名字的话,我们就可以用最简单的形式来做,也就是在函数内部声明一个函数(而不是函数表达式),然后返回该函数: 614 | 615 | var hasClassName = (function(){ 616 | 617 | // 定义私有变量 618 | var cache = { }; 619 | 620 | // 使用函数声明 621 | function hasClassName(element, className) { 622 | var _className = '(?:^|\\s+)' + className + '(?:\\s+|$)'; 623 | var re = cache[_className] || (cache[_className] = new RegExp(_className)); 624 | return re.test(element.className); 625 | } 626 | 627 | // 返回函数 628 | return hasClassName; 629 | })(); 630 | 631 | > 显然,当存在多个分支函数定义时,这个方案就不行了。不过有种模式貌似可以实现:那就是提前使用函数声明来定义所有函数,并分别为这些函数指定不同的标识符: 632 | 633 | var addEvent = (function(){ 634 | 635 | var docEl = document.documentElement; 636 | 637 | function addEventListener(){ 638 | /* ... */ 639 | } 640 | function attachEvent(){ 641 | /* ... */ 642 | } 643 | function addEventAsProperty(){ 644 | /* ... */ 645 | } 646 | 647 | if (typeof docEl.addEventListener != 'undefined') { 648 | return addEventListener; 649 | } 650 | elseif (typeof docEl.attachEvent != 'undefined') { 651 | return attachEvent; 652 | } 653 | return addEventAsProperty; 654 | })(); 655 | 656 | > 虽然这个方案很优雅,但也不是没有缺点。第一,由于使用不同的标识符,导致丧失了命名的一致性。且不说这样好还是坏,最起码它不够清晰。有人喜欢使用相同的名字,但也有人根本不在乎字眼上的差别。可毕竟,不同的名字会让人联想到所用的不同实现。例如,在调试器中看到attachEvent,我们就知 道addEvent是基于attachEvent的实现。当 然,基于实现来命名的方式也不一定都行得通。假如我们要提供一个API,并按照这种方式把函数命名为inner。那么API用户的很容易就会被相应实现的 细节搞得晕头转向。 657 | 658 | > 要解决这个问题,当然就得想一套更合理的命名方案了。但关键是不要再额外制造麻烦。我现在能想起来的方案大概有如下几个: 659 | 660 | 'addEvent', 'altAddEvent', 'fallbackAddEvent' 661 | // 或者 662 | 'addEvent', 'addEvent2', 'addEvent3' 663 | // 或者 664 | 'addEvent_addEventListener', 'addEvent_attachEvent', 'addEvent_asProperty' 665 | 666 | 667 | > 另外,这种模式还存在一个小问题,即增加内存占用。提前创建N个不同名字的函数,等于有N-1的函数是用不到的。具体来讲,如果**document.documentElement** 中包含**attachEvent**,那么**addEventListener** 和**addEventAsProperty**则根本就用不着了。可是,他们都占着内存哪;而且,这些内存将永远都得不到释放,原因跟JScript臭哄哄的命名表达式相同——这两个函数都被“截留”在返回的那个函数的闭包中了。 668 | 669 | 不过,增加内存占用这个问题确实没什么大不了的。如果某个库——例如Prototype.js——采用了这种模式,无非也就是多创建一两百个函数而已。只要不是(在运行时)重复地创建这些函数,而是只(在加载时)创建一次,那么就没有什么好担心的。 670 | 671 | ### WebKit的displayName 672 | 673 | > WebKit团队在这个问题采取了有点儿另类的策略。介于匿名和命名函数如此之差的表现力,WebKit引入了一个“特殊的”displayName属性(本质上是一个字符串),如果开发人员为函数的这个属性赋值,则该属性的值将在调试器或性能分析器中被显示在函数“名称”的位置上。[Francisco Tolmasky详细地解释了这个策略的原理和实现](http://www.alertdebugging.com/2009/04/29/building-a-better-javascript-profiler-with-webkit/ 'Francisco Tolmasky详细地解释了这个策略的原理和实现')。 674 | 675 | ### 未来考虑 676 | 677 | > 将来的ECMAScript-262第5版(目前还是草案)会引入所谓的**严格模式(strict mode)**。开启严格模式的实现会禁用语言中的那些不稳定、不可靠和不安全的特性。据说出于安全方面的考虑,**arguments.callee**属性将在严格模式下被“封杀”。因此,在处于严格模式时,访问**arguments.callee**会导致**TypeError**(参见ECMA-262第5版的10.6节)。而我之所以在此提到严格模式,是因为如果在基于第5版标准的实现中无法使用**arguments.callee**来执行递归操作,那么使用命名函数表达式的可能性就会大大增加。从这个意义上来说,理解命名函数表达式的语义及其bug也就显得更加重要了。 678 | 679 | // 此前,你可能会使用arguments.callee 680 | (function(x) { 681 | if (x <= 1) return 1; 682 | return x * arguments.callee(x - 1); 683 | })(10); 684 | 685 | // 但在严格模式下,有可能就要使用命名函数表达式 686 | (function factorial(x) { 687 | if (x <= 1) return 1; 688 | return x * factorial(x - 1); 689 | })(10); 690 | 691 | // 要么就退一步,使用没有那么灵活的函数声明 692 | function factorial(x) { 693 | if (x <= 1) return 1; 694 | return x * factorial(x - 1); 695 | } 696 | factorial(10); 697 | 698 | ### 致谢 699 | 700 | > **理查德· 康福德(Richard Cornford)**,是他率先解释了[JScript中命名函数表达式所存在的bug](http://groups.google.com/group/comp.lang.javascript/msg/5b508b03b004bce8 'JScript中命名函数表达式所存在的bug')。理查德解释了我在这篇文章中提及的大多数bug,所以我强烈建议大家去看看他的解释。我还要感谢**Yann-Erwan Perio**和**道格拉斯·克劳克佛德(Douglas Crockford)**,他们早在2003年就在[comp.lang.javascript论坛中提及并讨论NFE问题了](http://groups.google.com/group/comp.lang.javascript/msg/03d53d114d176323 'comp.lang.javascript论坛中提及并讨论NFE问题了')。 701 | 702 | > **约翰-戴维·道尔顿(John-David Dalton)**对“最终解决方案”提出了很好的建议。 703 | 704 | > **托比·兰吉**的点子被我用在了“替代方案”中。 705 | 706 | > **盖瑞特·史密斯(Garrett Smith)**和**德米特里·苏斯尼科(Dmitry Soshnikov)**对本文的多方面作出了补充和修正。 707 | 708 | > 英文原文:[Named function expressions demystified](http://kangax.github.com/nfe/ 'Named function expressions demystified') 709 | 710 | > 翻译编辑:[汤姆大叔的博客](http://www.cnblogs.com/TomXu/ '汤姆大叔的博客') 711 | 712 | > 原文地址:[深入理解JavaScript系列(2):揭秘命名函数表达式](http://www.cnblogs.com/TomXu/archive/2011/12/29/2290308.html '深入理解JavaScript系列(2):揭秘命名函数表达式') 713 | 714 | > 修正/编辑:[ahkjxy](https://github.com/ahkjxy/) -------------------------------------------------------------------------------- /1-编写高质量JavaScript代码的基本要点.md: -------------------------------------------------------------------------------- 1 | ## 深入理解JavaScript系列(1):编写高质量JavaScript代码的基本要点 2 | 3 | > 才华横溢的Stoyan Stefanov,在他写的由O’Reilly初版的新书[《JavaScript Patterns》](http://amzn.to/93szK7 '《JavaScript Patterns》')(JavaScript模式)中,我想要是为我们的读者贡献其摘要,那会是件很美妙的事情。具体一点就是编写高质量JavaScript的一些要素,例如避免全局变量,使用单变量声明,在循环中预缓存length(长度),遵循代码阅读,以及更多。 4 | 5 | > 此摘要也包括一些与代码不太相关的习惯,但对整体代码的创建息息相关,包括撰写API文档、执行同行评审以及运行JSLint。这些习惯和最佳做法可以帮助你写出更好的,更易于理解和维护的代码,这些代码在几个月或是几年之后再回过头看看也是会觉得很自豪的。 6 | 7 | ### 书写可维护的代码(Writing Maintainable Code ) 8 | 9 | > 软件bug的修复是昂贵的,并且随着时间的推移,这些bug的成本也会增加,尤其当这些bug潜伏并慢慢出现在已经发布的软件中时。当你发现bug 的时候就立即修复它是最好的,此时你代码要解决的问题在你脑中还是很清晰的。否则,你转移到其他任务,忘了那个特定的代码,一段时间后再去查看这些代码就需要: 10 | 11 | - 花时间学习和理解这个问题 12 | - 花时间是了解应该解决的问题代码 13 | 14 | > 还有问题,特别对于大的项目或是公司,修复bug的这位伙计不是写代码的那个人(且发现bug和修复bug的不是同一个人)。因此,必须降低理解代 码花费的时间,无论是一段时间前你自己写的代码还是团队中的其他成员写的代码。这关系到底线(营业收入)和开发人员的幸福,因为我们更应该去开发新的激动 人心的事物而不是花几小时几天的时间去维护遗留代码。 15 | 16 | > 另一个相关软件开发生命的事实是,读代码花费的时间要比写来得多。有时候,当你专注并深入思考某个问题的时候,你可以坐下来,一个下午写大量的代码。 17 | 18 | > 你的代码很能很快就工作了,但是,随着应用的成熟,还会有很多其他的事情发生,这就要求你的进行进行审查,修改,和调整。例如: 19 | 20 | - bug是暴露的 21 | - 新功能被添加到应用程序 22 | - 程序在新的环境下工作(例如,市场上出现较新(版本)的浏览器) 23 | - 代码改变用途 24 | - 代码得完全从头重新,或移植到另一个架构上或者甚至使用另一种语言 25 | 26 | > 由于这些变化,很少人力数小时写的代码最终演变成花数周来阅读这些代码。这就是为什么创建可维护的代码对应用程序的成功至关重要。 27 | 可维护的代码意味着: 28 | 29 | - 可读的 30 | - 一致的 31 | - 可预测的 32 | - 看上去就像是同一个人写的 33 | - 已记录 34 | 35 | ### 最小全局变量(Minimizing Globals) 36 | 37 | > JavaScript通过函数管理作用域。在函数内部声明的变量只在这个函数内部,函数外面不可用。另一方面,全局变量就是在任何函数外面声明的或是未声明直接简单使用的。 38 | 39 | > 每个JavaScript环境有一个全局对象,当你在任意的函数外面使用this的时候可以访问到。你创建的每一个全部变量都成了这个全局对象的属 性。在浏览器中,方便起见,该全局对象有个附加属性叫做window,此window(通常)指向该全局对象本身。下面的代码片段显示了如何在浏览器环境 中创建和访问的全局变量: 40 | 41 | myglobal = "hello"; // 不推荐写法 42 | console.log(myglobal); // "hello" 43 | console.log(window.myglobal); // "hello" 44 | console.log(window["myglobal"]); // "hello" 45 | console.log(this.myglobal); // "hello" 46 | 47 | ### 全局变量的问题 48 | 49 | > 全局变量的问题在于,你的JavaScript应用程序和web页面上的所有代码都共享了这些全局变量,他们住在同一个全局命名空间,所以当程序的两个不同部分定义同名但不同作用的全局变量的时候,命名冲突在所难免。 50 | 51 | > web页面包含不是该页面开发者所写的代码也是比较常见的,例如: 52 | 53 | - 第三方的JavaScript库 54 | - 广告方的脚本代码 55 | - 第三方用户跟踪和分析脚本代码 56 | - 不同类型的小组件,标志和按钮 57 | 58 | > 比方说,该第三方脚本定义了一个全局变量,叫做result;接着,在你的函数中也定义一个名为result的全局变量。其结果就是后面的变量覆盖前面的,第三方脚本就一下子嗝屁啦! 59 | 60 | > 因此,要想和其他脚本成为好邻居的话,尽可能少的使用全局变量是很重要的。在书中后面提到的一些减少全局变量的策略,例如命名空间模式或是函数立即自动执行,但是要想让全局变量少最重要的还是始终使用var来声明变量。 61 | 62 | > 由于JavaScript的两个特征,不自觉地创建出全局变量是出乎意料的容易。首先,你可以甚至不需要声明就可以使用变量;第二,JavaScript有隐含的全局概念,意味着你不声明的任何变量都会成为一个全局对象属性。参考下面的代码: 63 | 64 | function sum(x, y) { 65 | // 不推荐写法: 隐式全局变量 66 | result = x + y; 67 | return result; 68 | } 69 | 70 | > 此段代码中的result没有声明。代码照样运作正常,但在调用函数后你最后的结果就多一个全局命名空间,这可以是一个问题的根源。 71 | 经验法则是始终使用var声明变量,正如改进版的sum()函数所演示的: 72 | 73 | function sum(x, y) { 74 | var result = x + y; 75 | return result; 76 | } 77 | 78 | > 另一个创建隐式全局变量的反例就是使用任务链进行部分var声明。下面的片段中,a是本地变量但是b确实全局变量,这可能不是你希望发生的: 79 | 80 | // 反例,勿使用 81 | function foo() { 82 | var a = b = 0; 83 | // ... 84 | } 85 | 86 | > 此现象发生的原因在于这个从右到左的赋值,首先,是赋值表达式b = 0,此情况下b是未声明的。这个表达式的返回值是0,然后这个0就分配给了通过var定义的这个局部变量a。换句话说,就好比你输入了: 87 | 88 | var a = (b = 0); 89 | 90 | > 如果你已经准备好声明变量,使用链分配是比较好的做法,不会产生任何意料之外的全局变量,如: 91 | 92 | function foo() { 93 | var a, b; 94 | // ... a = b = 0; // 两个均局部变量 95 | } 96 | 97 | > **然而,另外一个避免全局变量的原因是可移植性。如果你想你的代码在不同的环境下(主机下)运行,使用全局变量如履薄冰,因为你会无意中覆盖你最初环境下不存在的主机对象(所以你原以为名称可以放心大胆地使用,实际上对于有些情况并不适用)。** 98 | 99 | ### 忘记var的副作用(Side Effects When Forgetting var) 100 | 101 | > 隐式全局变量和明确定义的全局变量间有些小的差异,就是通过delete操作符让变量未定义的能力。 102 | 103 | - 通过var创建的全局变量(任何函数之外的程序中创建)是不能被删除的。 104 | - 无var创建的隐式全局变量(无视是否在函数中创建)是能被删除的。 105 | 106 | > 这表明,在技术上,隐式全局变量并不是真正的全局变量,但它们是全局对象的属性。属性是可以通过delete操作符删除的,而变量是不能的: 107 | 108 | // 定义三个全局变量 109 | var global_var = 1; 110 | global_novar = 2; // 反面教材 111 | (function () { 112 | global_fromfunc = 3; // 反面教材 113 | }()); 114 | 115 | // 试图删除 116 | delete global_var; // false 117 | delete global_novar; // true 118 | delete global_fromfunc; // true 119 | 120 | // 测试该删除 121 | typeof global_var; // "number" 122 | typeof global_novar; // "undefined" 123 | typeof global_fromfunc; // "undefined" 124 | 125 | > 在ES5严格模式下,未声明的变量(如在前面的代码片段中的两个反面教材)工作时会抛出一个错误。 126 | 127 | ### 访问全局对象(Access to the Global Object) 128 | 129 | > 在浏览器中,全局对象可以通过window属性在代码的任何位置访问(除非你做了些比较出格的事情,像是声明了一个名为window的局部变量)。但是在其他环境下,这个方便的属性可能被叫做其他什么东西(甚至在程序中不可用)。如果你需要在没有硬编码的window标识符下访问全局对象,你可以在任何层级的函数作用域中做如下操作: 130 | 131 | var global = (function () { 132 | return this; 133 | }()); 134 | 135 | > 这种方法可以随时获得全局对象,因为其在函数中被当做函数调用了(不是通过new构造),this总是指向全局对象。实际上这个病不适用于ECMAScript 5严格模式,所以,在严格模式下时,你必须采取不同的形式。例如,你正在开发一个JavaScript库,你可以将你的代码包裹在一个立即执行函数(IIFE)中,然后从全局作用域中,传递一个引用指向this作为你即时函数的参数。 136 | 137 | ### 单var形式(Single var Pattern) 138 | 139 | > 在函数顶部使用单var语句是比较有用的一种形式,其好处在于: 140 | 141 | - 提供了一个单一的地方去寻找功能所需要的所有局部变量 142 | - 防止变量在定义之前使用的逻辑错误 143 | - 帮助你记住声明的全局变量,因此较少了全局变量//zxx:此处我自己是有点晕乎的… 144 | - 少代码(类型啊传值啊单线完成) 145 | 146 | > 单var形式长得就像下面这个样子: 147 | 148 | function func() { 149 | var a = 1, 150 | b = 2, 151 | sum = a + b, 152 | myobject = {}, 153 | i, 154 | j; 155 | // function body... 156 | } 157 | 158 | > 您可以使用一个var语句声明多个变量,并以逗号分隔。像这种初始化变量同时初始化值的做法是很好的。这样子可以防止逻辑错误(所有未初始化但声明的变量的初始值是undefined)和增加代码的可读性。在你看到代码后,你可以根据初始化的值知道这些变量大致的用途,例如是要当作对象呢还是当作整数来使。 159 | 160 | > 你也可以在声明的时候做一些实际的工作,例如前面代码中的sum = a + b这个情况,另外一个例子就是当你使用DOM(文档对象模型)引用时,你可以使用单一的var把DOM引用一起指定为局部变量,就如下面代码所示的: 161 | 162 | function updateElement() { 163 | var el = document.getElementById("result"), 164 | style = el.style; 165 | // 使用el和style干点其他什么事... 166 | } 167 | 168 | ### 预解析:var散布的问题(Hoisting: A Problem with Scattered vars) 169 | 170 | > JavaScript中,你可以在函数的任何位置声明多个var语句,并且它们就好像是在函数顶部声明一样发挥作用,这种行为称为 hoisting(悬置/置顶解析/预解析)。当你使用了一个变量,然后不久在函数中又重新声明的话,就可能产生逻辑错误。对于JavaScript,只 要你的变量是在同一个作用域中(同一函数),它都被当做是声明的,即使是它在var声明前使用的时候。看下面这个例子: 171 | 172 | // 反例 173 | myname = "global"; // 全局变量 174 | function func() { 175 | alert(myname); // "undefined" 176 | var myname = "local"; 177 | alert(myname); // "local" 178 | } 179 | func(); 180 | 181 | > 在这个例子中,你可能会以为第一个alert弹出的是”global”,第二个弹出”loacl”。这种期许是可以理解的,因为在第一个alert 的时候,myname未声明,此时函数肯定很自然而然地看全局变量myname,但是,实际上并不是这么工作的。第一个alert会弹 出”undefined”是因为myname被当做了函数的局部变量(尽管是之后声明的),所有的变量声明当被悬置到函数的顶部了。因此,为了避免这种混 乱,最好是预先声明你想使用的全部变量。 182 | 183 | > 上面的代码片段执行的行为可能就像下面这样: 184 | 185 | myname = "global"; // global variable 186 | function func() { 187 | var myname; // 等同于 -> var myname = undefined; 188 | alert(myname); // "undefined" 189 | myname = "local"; 190 | alert(myname); // "local"} 191 | func(); 192 | 193 | > **为了完整,我们再提一提执行层面的稍微复杂点的东西。代码处理分两个阶段,第一阶段是变量,函数声明,以及正常格式的参数创建,这是一个解析和进入上下文 的阶段。第二个阶段是代码执行,函数表达式和不合格的标识符(为声明的变量)被创建。但是,出于实用的目的,我们就采用了”hoisting”这个概念, 这种ECMAScript标准中并未定义,通常用来描述行为。** 194 | 195 | ### for循环(for Loops) 196 | 197 | > 在for循环中,你可以循环取得数组或是数组类似对象的值,譬如arguments和HTMLCollection对象。通常的循环形式如下: 198 | 199 | // 次佳的循环 200 | for (var i = 0; i < myarray.length; i++) { 201 | // 使用myarray[i]做点什么 202 | } 203 | 204 | > 这种形式的循环的不足在于每次循环的时候数组的长度都要去获取下。这回降低你的代码,尤其当myarray不是数组,而是一个HTMLCollection对象的时候。 205 | 206 | > HTMLCollections指的是DOM方法返回的对象,例如: 207 | 208 | document.getElementsByName() 209 | document.getElementsByClassName() 210 | document.getElementsByTagName() 211 | 212 | > 还有其他一些HTMLCollections,这些是在DOM标准之前引进并且现在还在使用的。有:' 213 | 214 | document.images: 页面上所有的图片元素 215 | document.links : 所有a标签元素 216 | document.forms : 所有表单 217 | document.forms[0].elements : 页面上第一个表单中的所有域 218 | 219 | > 集合的麻烦在于它们实时查询基本文档(HTML页面)。这意味着每次你访问任何集合的长度,你要实时查询DOM,而DOM操作一般都是比较昂贵的。 220 | 这就是为什么当你循环获取值时,缓存数组(或集合)的长度是比较好的形式,正如下面代码显示的: 221 | 222 | for (var i = 0, max = myarray.length; i < max; i++) { 223 | // 使用myarray[i]做点什么 224 | } 225 | 226 | > 这样,在这个循环过程中,你只检索了一次长度值。 227 | > 在所有浏览器下,循环获取内容时缓存HTMLCollections的长度是更快的,2倍(Safari3)到190倍(IE7)之间。//zxx:此数据貌似很老,仅供参考 228 | > 注意到,当你明确想要修改循环中的集合的时候(例如,添加更多的DOM元素),你可能更喜欢长度更新而不是常量。 229 | > 伴随着单var形式,你可以把变量从循环中提出来,就像下面这样: 230 | 231 | function looper() { 232 | var i = 0, 233 | max, 234 | myarray = []; 235 | // ... 236 | for (i = 0, max = myarray.length; i < max; i++) { 237 | // 使用myarray[i]做点什么 238 | } 239 | } 240 | 241 | > 这种形式具有一致性的好处,因为你坚持了单一var形式。不足在于当重构代码的时候,复制和粘贴整个循环有点困难。例如,你从一个函数复制了一个循环到另一个函数,你不得不去确定你能够把i和max引入新的函数(如果在这里没有用的话,很有可能你要从原函数中把它们删掉)。 242 | 243 | > 最后一个需要对循环进行调整的是使用下面表达式之一来替换i++。 244 | 245 | i = i + 1 246 | i += 1 247 | 248 | > JSLint提示您这样做,原因是++和–-促进了“过分棘手(excessive trickiness)”。//zxx:这里比较难翻译,我想本意应该是让代码变得更加的棘手如果你直接无视它,JSLint的plusplus选项会是false(默认是default)。 249 | 250 | > 还有两种变化的形式,其又有了些微改进,因为: 251 | 252 | - 少了一个变量(无max) 253 | - 向下数到0,通常更快,因为和0做比较要比和数组长度或是其他不是0的东西作比较更有效率 254 | 255 | //第一种变化的形式: 256 | var i, myarray = []; 257 | for (i = myarray.length; i–-;) { 258 | // 使用myarray[i]做点什么 259 | } 260 | 261 | //第二种使用while循环: 262 | 263 | var myarray = [], 264 | i = myarray.length; 265 | while (i–-) { 266 | // 使用myarray[i]做点什么 267 | } 268 | 269 | > 这些小的改进只体现在性能上,此外JSLint会对使用i–-加以抱怨。 270 | 271 | ### for-in循环(for-in Loops) 272 | 273 | > for-in循环应该用在非数组对象的遍历上,使用for-in进行循环也被称为“枚举”。 274 | 275 | > 从技术上将,你可以使用for-in循环数组(因为JavaScript中数组也是对象),但这是不推荐的。因为如果数组对象已被自定义的功能增强,就可能发生逻辑错误。另外,在for-in中,属性列表的顺序(序列)是不能保证的。所以最好数组使用正常的for循环,对象使用for-in循环。 276 | 277 | > 有个很重要的hasOwnProperty()方法,当遍历对象属性的时候可以过滤掉从原型链上下来的属性。 278 | 279 | > 思考下面一段代码: 280 | 281 | // 对象 282 | var man = { 283 | hands: 2, 284 | legs: 2, 285 | heads: 1 286 | }; 287 | 288 | // 在代码的某个地方 289 | // 一个方法添加给了所有对象 290 | if (typeof Object.prototype.clone === "undefined") { 291 | Object.prototype.clone = function () {}; 292 | } 293 | 294 | > 在这个例子中,我们有一个使用对象字面量定义的名叫man的对象。在man定义完成后的某个地方,在对象原型上增加了一个很有用的名叫 clone()的方法。此原型链是实时的,这就意味着所有的对象自动可以访问新的方法。为了避免枚举man的时候出现clone()方法,你需要应用hasOwnProperty()方法过滤原型属性。如果不做过滤,会导致clone()函数显示出来,在大多数情况下这是不希望出现的。 295 | 296 | // 1. 297 | // for-in 循环 298 | for (var i in man) { 299 | if (man.hasOwnProperty(i)) { // 过滤 300 | console.log(i, ":", man[i]); 301 | } 302 | } 303 | /* 控制台显示结果 304 | hands : 2 305 | legs : 2 306 | heads : 1 307 | */ 308 | // 2. 309 | // 反面例子: 310 | // for-in loop without checking hasOwnProperty() 311 | for (var i in man) { 312 | console.log(i, ":", man[i]); 313 | } 314 | /* 315 | 控制台显示结果 316 | hands : 2 317 | legs : 2 318 | heads : 1 319 | clone: function() 320 | */ 321 | 322 | > 另外一种使用hasOwnProperty()的形式是取消Object.prototype上的方法。像是: 323 | 324 | for (var i in man) { 325 | if (Object.prototype.hasOwnProperty.call(man, i)) { // 过滤 326 | console.log(i, ":", man[i]); 327 | } 328 | } 329 | 330 | > 其好处在于在man对象重新定义hasOwnProperty情况下避免命名冲突。也避免了长属性查找对象的所有方法,你可以使用局部变量“缓存”它。 331 | 332 | var i, hasOwn = Object.prototype.hasOwnProperty; 333 | for (i in man) { 334 | if (hasOwn.call(man, i)) { // 过滤 335 | console.log(i, ":", man[i]); 336 | } 337 | } 338 | 339 | > **严格来说,不使用hasOwnProperty()并不是一个错误。根据任务以及你对代码的自信程度,你可以跳过它以提高些许的循环速度。但是当你对当前对象内容(和其原型链)不确定的时候,添加hasOwnProperty()更加保险些。** 340 | 341 | > 格式化的变化(通不过JSLint)会直接忽略掉花括号,把if语句放到同一行上。其优点在于循环语句读起来就像一个完整的想法(每个元素都有一个自己的属性”X”,使用”X”干点什么) 342 | 343 | // 警告: 通不过JSLint检测 344 | var i, hasOwn = Object.prototype.hasOwnProperty; 345 | for (i in man) if (hasOwn.call(man, i)) { // 过滤 346 | console.log(i, ":", man[i]); 347 | } 348 | 349 | ### (不)扩展内置原型((Not) Augmenting Built-in Prototypes) 350 | 351 | > 扩增构造函数的prototype属性是个很强大的增加功能的方法,但有时候它太强大了。 352 | 353 | > 增加内置的构造函数原型(如Object(), Array(), 或Function())挺诱人的,但是这严重降低了可维护性,因为它让你的代码变得难以预测。使用你代码的其他开发人员很可能更期望使用内置的 JavaScript方法来持续不断地工作,而不是你另加的方法。 354 | 355 | > 另外,属性添加到原型中,可能会导致不使用hasOwnProperty属性时在循环中显示出来,这会造成混乱。 356 | 357 | > 因此,不增加内置原型是最好的。你可以指定一个规则,仅当下面的条件均满足时例外: 358 | 359 | - 可以预期将来的ECMAScript版本或是JavaScript实现将一直将此功能当作内置方法来实现。例如,你可以添加ECMAScript 5中描述的方法,一直到各个浏览器都迎头赶上。这种情况下,你只是提前定义了有用的方法。 360 | - 如果您检查您的自定义属性或方法已不存在——也许已经在代码的其他地方实现或已经是你支持的浏览器JavaScript引擎部分。 361 | - 你清楚地文档记录并和团队交流了变化。 362 | 363 | > 如果这三个条件得到满足,你可以给原型进行自定义的添加,形式如下: 364 | 365 | if (typeof Object.protoype.myMethod !== "function") { 366 | Object.protoype.myMethod = function () { 367 | // 实现... 368 | }; 369 | } 370 | 371 | ### switch模式(switch Pattern) 372 | 373 | > 你可以通过类似下面形式的switch语句增强可读性和健壮性: 374 | 375 | var inspect_me = 0, 376 | result = ''; 377 | switch (inspect_me) { 378 | case 0: 379 | result = "zero"; 380 | break; 381 | case 1: 382 | result = "one"; 383 | break; 384 | default: 385 | result = "unknown"; 386 | } 387 | 388 | > 这个简单的例子中所遵循的风格约定如下: 389 | 390 | - 每个case和switch对齐(花括号缩进规则除外) 391 | - 每个case中代码缩进 392 | - 每个case以break清除结束 393 | - 避免贯穿(故意忽略break)。如果你非常确信贯穿是最好的方法,务必记录此情况,因为对于有些阅读人而言,它们可能看起来是错误的。 394 | - 以default结束switch:确保总有健全的结果,即使无情况匹配。 395 | 396 | ### 避免隐式类型转换(Avoiding Implied Typecasting ) 397 | 398 | > JavaScript的变量在比较的时候会隐式类型转换。这就是为什么一些诸如:false == 0 或 “” == 0 返回的结果是true。为避免引起混乱的隐含类型转换,在你比较值和表达式类型的时候始终使用===和!==操作符。 399 | 400 | var zero = 0; 401 | if (zero === false) { 402 | // 不执行,因为zero为0, 而不是false 403 | } 404 | 405 | // 反面示例 406 | if (zero == false) { 407 | // 执行了... 408 | } 409 | 410 | > 还有另外一种思想观点认为==就足够了===是多余的。例如,当你使用typeof你就知道它会返回一个字符串,所以没有使用严格相等的理由。然而,JSLint要求严格相等,它使代码看上去更有一致性,可以降低代码阅读时的精力消耗。(“==是故意的还是一个疏漏?”) 411 | 412 | ### 避免(Avoiding) eval() 413 | 414 | > 如果你现在的代码中使用了eval(),记住该咒语“eval()是魔鬼”。此方法接受任意的字符串,并当作JavaScript代码来处理。当有 问题的代码是事先知道的(不是运行时确定的),没有理由使用eval()。如果代码是在运行时动态生成,有一个更好的方式不使用eval而达到同样的目 标。例如,用方括号表示法来访问动态属性会更好更简单: 415 | 416 | // 反面示例 417 | var property = "name"; 418 | alert(eval("obj." + property)); 419 | 420 | // 更好的 421 | var property = "name"; 422 | alert(obj[property]); 423 | 424 | > 使用eval()也带来了安全隐患,因为被执行的代码(例如从网络来)可能已被篡改。这是个很常见的反面教材,当处理Ajax请求得到的JSON 相应的时候。在这些情况下,最好使用JavaScript内置方法来解析JSON相应,以确保安全和有效。若浏览器不支持JSON.parse(),你可 以使用来自JSON.org的库。 425 | 426 | > 同样重要的是要记住,给setInterval(), setTimeout()和Function()构造函数传递字符串,大部分情况下,与使用eval()是类似的,因此要避免。在幕后,JavaScript仍需要评估和执行你给程序传递的字符串: 427 | 428 | // 反面示例 429 | setTimeout("myFunc()", 1000); 430 | setTimeout("myFunc(1, 2, 3)", 1000); 431 | 432 | // 更好的 433 | setTimeout(myFunc, 1000); 434 | setTimeout(function () { 435 | myFunc(1, 2, 3); 436 | }, 1000); 437 | 438 | > 使用新的Function()构造就类似于eval(),应小心接近。这可能是一个强大的构造,但往往被误用。如果你绝对必须使用eval(),你可以考虑使用new Function()代替。有一个小的潜在好处,因为在新Function()中作代码评估是在局部函数作用域中运行,所以代码中任何被评估的通过var 定义的变量都不会自动变成全局变量。另一种方法来阻止自动全局变量是封装eval()调用到一个即时函数中。 439 | 440 | > 考虑下面这个例子,这里仅un作为全局变量污染了命名空间。 441 | 442 | console.log(typeof un);// "undefined" 443 | console.log(typeof deux); // "undefined" 444 | console.log(typeof trois); // "undefined" 445 | 446 | var jsstring = "var un = 1; console.log(un);"; 447 | eval(jsstring); // logs "1" 448 | 449 | jsstring = "var deux = 2; console.log(deux);"; 450 | new Function(jsstring)(); // logs "2" 451 | 452 | jsstring = "var trois = 3; console.log(trois);"; 453 | (function () { 454 | eval(jsstring); 455 | }()); // logs "3" 456 | 457 | console.log(typeof un); // number 458 | console.log(typeof deux); // "undefined" 459 | console.log(typeof trois); // "undefined" 460 | 461 | > 另一间eval()和Function构造不同的是eval()可以干扰作用域链,而Function()更安分守己些。不管你在哪里执行 Function(),它只看到全局作用域。所以其能很好的避免本地变量污染。在下面这个例子中,eval()可以访问和修改它外部作用域中的变量,这是 Function做不来的(注意到使用Function和new Function是相同的)。 462 | 463 | (function () { 464 | var local = 1; 465 | eval("local = 3; console.log(local)"); // "3" "3" 466 | console.log(local); // logs "3" 467 | }()); 468 | 469 | (function () { 470 | var local = 1; 471 | Function("console.log(typeof local);")(); // undefined 472 | }()); 473 | 474 | ### parseInt()下的数值转换(Number Conversions with parseInt()) 475 | 476 | > 使用parseInt()你可以从字符串中获取数值,该方法接受另一个基数参数,这经常省略,但不应该。当字符串以”0″开头的时候就有可能会出问 题,例如,部分时间进入表单域,在ECMAScript 3中,开头为”0″的字符串被当做8进制处理了,但这已在ECMAScript 5中改变了。为了避免矛盾和意外的结果,总是指定基数参数。 477 | 478 | var month = "06", year = "09"; 479 | month = parseInt(month, 10); 480 | year = parseInt(year, 10); 481 | 482 | > 此例中,如果你忽略了基数参数,如parseInt(year),返回的值将是0,因为“09”被当做8进制(好比执行 parseInt( year, 8 )),而09在8进制中不是个有效数字。 483 | 484 | +"08" // 结果是 8 485 | Number("08") // 8 486 | 487 | > 这些通常快于parseInt(),因为parseInt()方法,顾名思意,不是简单地解析与转换。但是,如果你想输入例如“08 hello”,parseInt()将返回数字,而其它以NaN告终。 488 | 489 | ### 编码规范(Coding Conventions) 490 | 491 | > 建立和遵循编码规范是很重要的,这让你的代码保持一致性,可预测,更易于阅读和理解。一个新的开发者加入这个团队可以通读规范,理解其它团队成员书写的代码,更快上手干活。 492 | 493 | > 许多激烈的争论发生会议上或是邮件列表上,问题往往针对某些代码规范的特定方面(例如代码缩进,是Tab制表符键还是space空格键)。如果你是 你组织中建议采用规范的,准备好面对各种反对的或是听起来不同但很强烈的观点。要记住,建立和坚定不移地遵循规范要比纠结于规范的细节重要的多。 494 | 495 | ### 缩进(Indentation) 496 | 497 | > 代码没有缩进基本上就不能读了。唯一糟糕的事情就是不一致的缩进,因为它看上去像是遵循了规范,但是可能一路上伴随着混乱和惊奇。重要的是规范地使用缩进。 498 | 499 | > 一些开发人员更喜欢用tab制表符缩进,因为任何人都可以调整他们的编辑器以自己喜欢的空格数来显示Tab。有些人喜欢空格——通常四个,这都无所谓,只要团队每个人都遵循同一个规范就好了。这本书,例如,使用四个空格缩进,这也是JSLint中默认的缩进。 500 | 501 | > 什么应该缩进呢?规则很简单——花括号里面的东西。这就意味着函数体,循环 (do, while, for, for-in),if,switch,以及对象字面量中的对象属性。下面的代码就是使用缩进的示例: 502 | 503 | function outer(a, b) { 504 | var c = 1, 505 | d = 2, 506 | inner; 507 | if (a > b) { 508 | inner = function () { 509 | return { 510 | r: c - d 511 | }; 512 | }; 513 | } else { 514 | inner = function () { 515 | return { 516 | r: c + d 517 | }; 518 | }; 519 | } 520 | return inner; 521 | } 522 | 523 | 524 | ### 花括号{}(Curly Braces) 525 | 526 | > 花括号(亦称大括号,下同)应总被使用,即使在它们为可选的时候。技术上将,在in或是for中如果语句仅一条,花括号是不需要的,但是你还是应该总是使用它们,这会让代码更有持续性和易于更新。 527 | 528 | > 想象下你有一个只有一条语句的for循环,你可以忽略花括号,而没有解析的错误。 529 | 530 | // 糟糕的实例 531 | for (var i = 0; i < 10; i += 1) 532 | alert(i); 533 | 534 | > 但是,如果,后来,主体循环部分又增加了行代码? 535 | 536 | // 糟糕的实例 537 | for (var i = 0; i < 10; i += 1) 538 | alert(i); 539 | alert(i + " is " + (i % 2 ? "odd" : "even")); 540 | 541 | > 第二个alert已经在循环之外,缩进可能欺骗了你。为了长远打算,最好总是使用花括号,即时值一行代码: 542 | 543 | // 好的实例 544 | for (var i = 0; i < 10; i += 1) { 545 | alert(i); 546 | } 547 | 548 | > if条件类似: 549 | 550 | // 坏 551 | if (true) 552 | alert(1); 553 | else 554 | alert(2); 555 | 556 | // 好 557 | if (true) { 558 | alert(1); 559 | } else { 560 | alert(2); 561 | } 562 | 563 | ### 左花括号的位置(Opening Brace Location) 564 | 565 | > 开发人员对于左大括号的位置有着不同的偏好——在同一行或是下一行。 566 | 567 | if (true) { 568 | alert("It's TRUE!"); 569 | } 570 | 571 | //或 572 | 573 | if (true) 574 | { 575 | alert("It's TRUE!"); 576 | } 577 | 578 | > 这个实例中,仁者见仁智者见智,但也有个案,括号位置不同会有不同的行为表现。这是因为分号插入机制(semicolon insertion mechanism)——JavaScript是不挑剔的,当你选择不使用分号结束一行代码时JavaScript会自己帮你补上。这种行为可能会导致麻 烦,如当你返回对象字面量,而左括号却在下一行的时候: 579 | 580 | // 警告: 意外的返回值 581 | function func() { 582 | return 583 | // 下面代码不执行 584 | { 585 | name : "Batman" 586 | } 587 | } 588 | 589 | > 如果你希望函数返回一个含有name属性的对象,你会惊讶。由于隐含分号,函数返回undefined。前面的代码等价于: 590 | 591 | // 警告: 意外的返回值 592 | function func() { 593 | return undefined; 594 | // 下面代码不执行 595 | { 596 | name : "Batman" 597 | } 598 | } 599 | 600 | > 总之,总是使用花括号,并始终把在与之前的语句放在同一行: 601 | 602 | function func() { 603 | return { 604 | name : "Batman" 605 | }; 606 | } 607 | 608 | > **关于分号注:就像使用花括号,你应该总是使用分号,即使他们可由JavaScript解析器隐式创建。这不仅促进更科学和更严格的代码,而且有助于解决存有疑惑的地方,就如前面的例子显示。** 609 | 610 | ### 空格(White Space) 611 | 612 | > 空格的使用同样有助于改善代码的可读性和一致性。在写英文句子的时候,在逗号和句号后面会使用间隔。在JavaScript中,你可以按照同样的逻辑在列表模样表达式(相当于逗号)和结束语句(相对于完成了“想法”)后面添加间隔。 613 | 614 | > 适合使用空格的地方包括: 615 | 616 | - for循环分号分开后的的部分:如for (var i = 0; i < 10; i += 1) {...} 617 | - for循环中初始化的多变量(i和max):for (var i = 0, max = 10; i < max; i += 1) {...} 618 | - 分隔数组项的逗号的后面:var a = [1, 2, 3]; 619 | - 对象属性逗号的后面以及分隔属性名和属性值的冒号的后面:var o = {a: 1, b: 2}; 620 | - 限定函数参数:myFunc(a, b, c) 621 | - 函数声明的花括号的前面:function myFunc() {} 622 | - 匿名函数表达式function的后面:var myFunc = function () {}; 623 | 624 | // 宽松一致的间距 625 | // 使代码更易读 626 | // 使得更加“透气” 627 | var d = 0, 628 | a = b + 1; 629 | if (a && b && c) { 630 | d = a % c; 631 | a += d; 632 | } 633 | 634 | // 反面例子 635 | // 缺失或间距不一 636 | // 使代码变得疑惑 637 | var d = 0, 638 | a = b + 1; 639 | if (a&&b&&c) { 640 | d=a % c; 641 | a+= d; 642 | } 643 | 644 | > 最后需要注意的一个空格——花括号间距。最好使用空格: 645 | 646 | - 函数、if-else语句、循环、对象字面量的左花括号的前面({) 647 | - else或while之间的右花括号(}) 648 | 649 | > 空格使用的一点不足就是增加了文件的大小,但是压缩无此问题。 650 | 651 | > **有一个经常被忽略的代码可读性方面是垂直空格的使用。你可以使用空行来分隔代码单元,就像是文学作品中使用段落分隔一样。** 652 | 653 | ### 命名规范(Naming Conventions) 654 | 655 | > 另一种方法让你的代码更具可预测性和可维护性是采用命名规范。这就意味着你需要用同一种形式给你的变量和函数命名。 656 | 657 | > 下面是建议的一些命名规范,你可以原样采用,也可以根据自己的喜好作调整。同样,遵循规范要比规范是什么更重要。 658 | 659 | ### 以大写字母写构造函数(Capitalizing Constructors) 660 | 661 | > JavaScript并没有类,但有new调用的构造函数: 662 | 663 | var adam = new Person(); 664 | 665 | > 因为构造函数仍仅仅是函数,仅看函数名就可以帮助告诉你这应该是一个构造函数还是一个正常的函数。 666 | 667 | > 命名构造函数时首字母大写具有暗示作用,使用小写命名的函数和方法不应该使用new调用: 668 | 669 | function MyConstructor() {...} 670 | function myFunction() {...} 671 | 672 | ### 分隔单词(Separating Words) 673 | 674 | > 当你的变量或是函数名有多个单词的时候,最好单词的分离遵循统一的规范,有一个常见的做法被称作“驼峰(Camel)命名法”,就是单词小写,每个单词的首字母大写。 675 | 676 | > 对于构造函数,可以使用大驼峰式命名法(upper camel case),如**MyConstructor()**。对于函数和方法名称,你可以使用小驼峰式命名法(lower camel case),像是**myFunction()**, **calculateArea()**和**getFirstName()**。 677 | 678 | > 要是变量不是函数呢?开发者通常使用小驼峰式命名法,但还有另外一种做法就是所有单词小写以下划线连接:例如,first_name, favorite_bands, 和 old_company_name,这种标记法帮你直观地区分函数和其他标识——原型和对象。 679 | 680 | > ECMAScript的属性和方法均使用Camel标记法,尽管多字的属性名称是罕见的(正则表达式对象的lastIndex和ignoreCase属性)。 681 | 682 | ### 其它命名形式(Other Naming Patterns) 683 | 684 | > 有时,开发人员使用命名规范来弥补或替代语言特性。 685 | 686 | > 例如,JavaScript中没有定义常量的方法(尽管有些内置的像Number, MAX_VALUE),所以开发者都采用全部单词大写的规范来命名这个程序生命周期中都不会改变的变量,如: 687 | 688 | // 珍贵常数,只可远观 689 | var PI = 3.14, 690 | MAX_WIDTH = 800; 691 | 692 | > 还有另外一个完全大写的惯例:全局变量名字全部大写。全部大写命名全局变量可以加强减小全局变量数量的实践,同时让它们易于区分。 693 | 694 | > 另外一种使用规范来模拟功能的是私有成员。虽然可以在JavaScript中实现真正的私有,但是开发者发现仅仅使用一个下划线前缀来表示一个私有属性或方法会更容易些。考虑下面的例子: 695 | 696 | var person = { 697 | getName: function () { 698 | return this._getFirst() + ' ' + this._getLast(); 699 | }, 700 | 701 | _getFirst: function () { 702 | // ... 703 | }, 704 | _getLast: function () { 705 | // ... 706 | } 707 | }; 708 | 709 | > 在此例中,**getName()**就表示公共方法,部分稳定的API。而**_getFirst()**和**_getLast()**则表明了私有。它们仍然是正常的公共方法,但是使用下划线前缀来警告person对象的使用者这些方法在下一个版本中时不能保证工作的,是不能直接使用的。注意,JSLint有些不鸟下划线前缀,除非你设置了noman选项为:false。 710 | 711 | > 下面是一些常见的**_private**规范: 712 | 713 | - 使用尾下划线表示私有,如**name_**和**getElements_()** 714 | - 使用一个下划线前缀表**_protected(**保护)属性,两个下划线前缀表示**__private **(私有)属性 715 | - Firefox中一些内置的变量属性不属于该语言的技术部分,使用两个前下划线和两个后下划线表示,如:**__proto__**和**__parent__**。 716 | 717 | ### 注释(Writing Comments) 718 | 719 | > 你必须注释你的代码,即使不会有其他人向你一样接触它。通常,当你深入研究一个问题,你会很清楚的知道这个代码是干嘛用的,但是,当你一周之后再回来看的时候,想必也要耗掉不少脑细胞去搞明白到底怎么工作的。 720 | 721 | > 很显然,注释不能走极端:每个单独变量或是单独一行。但是,你通常应该记录所有的函数,它们的参数和返回值,或是任何不寻常的技术和方法。要想到注 释可以给你代码未来的阅读者以诸多提示;阅读者需要的是(不要读太多的东西)仅注释和函数属性名来理解你的代码。例如,当你有五六行程序执行特定的任务, 如果你提供了一行代码目的以及为什么在这里的描述的话,阅读者就可以直接跳过这段细节。没有硬性规定注释代码比,代码的某些部分(如正则表达式)可能注释要比代码多。 722 | 723 | > **最重要的习惯,然而也是最难遵守的,就是保持注释的及时更新,因为过时的注释比没有注释更加的误导人。** 724 | 725 | ### 关于作者(About the Author ) 726 | 727 | > [Stoyan Stefanov](www.phpied.com 'Stoyan Stefanov')是Yahoo!web开发人员,多个O'Reilly书籍的作者、投稿者和技术评审。他经常在会议和他的博客[www.phpied.com](www.phpied.com 'Stoyan Stefanov')上发表web开发主题的演讲。Stoyan还是smush.it图片优化工具的创造者,YUI贡献者,雅虎性能优化工具YSlow 2.0的架构设计师。 728 | 729 | > 原文地址: [翻译-高质量javascript代码书写基本要点](http://www.zhangxinxu.com/wordpress/2010/10/%E7%BF%BB%E8%AF%91-%E9%AB%98%E8%B4%A8%E9%87%8Fjavascript%E4%BB%A3%E7%A0%81%E4%B9%A6%E5%86%99%E5%9F%BA%E6%9C%AC%E8%A6%81%E7%82%B9/ '翻译-高质量javascript代码书写基本要点') / [深入理解JavaScript系列(1):编写高质量JavaScript代码的基本要点](http://www.cnblogs.com/TomXu/archive/2011/12/28/2286877.html '深入理解JavaScript系列(1):编写高质量JavaScript代码的基本要点') 730 | 731 | > 原文作者:[Stoyan Stefanov](www.phpied.com 'Stoyan Stefanov') 732 | 733 | > 原文链接:[The Essentials of Writing High Quality JavaScript](http://net.tutsplus.com/tutorials/javascript-ajax/the-essentials-of-writing-high-quality-javascript/ 'The Essentials of Writing High Quality JavaScript') 734 | 735 | > 翻译编辑:[张鑫旭](http://www.zhangxinxu.com/ '张鑫旭') / [汤姆大叔的博客](http://www.cnblogs.com/TomXu/ '汤姆大叔的博客') 736 | 737 | > 修正/编辑:[ahkjxy](https://github.com/ahkjxy/) --------------------------------------------------------------------------------