├── .gitignore ├── README.md ├── SUMMARY.md ├── assets ├── cover1.jpg ├── cover2.jpg ├── cover3.jpg ├── fig1.png ├── fig2.png ├── fig3.png ├── fig4.png └── fig5.png ├── cover.jpg ├── part1 ├── README.md ├── ch1.md ├── ch2.md └── ch3.md ├── part2 ├── README.md ├── apdA.md ├── apdB.md ├── ch1.md ├── ch2.md ├── ch3.md ├── ch4.md └── ch5.md ├── part3 ├── README.md └── ch1.md └── preface.md /.gitignore: -------------------------------------------------------------------------------- 1 | # Node rules: 2 | ## Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 3 | .grunt 4 | 5 | ## Dependency directory 6 | ## Commenting this out is preferred by some people, see 7 | ## https://docs.npmjs.com/misc/faq#should-i-check-my-node_modules-folder-into-git 8 | node_modules 9 | 10 | # Book build output 11 | _book 12 | 13 | # eBook build output 14 | *.epub 15 | *.mobi 16 | *.pdf -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # 你不了解的JS 2 | 3 | 本文档翻译自Kyle Simpson编写的《[**You Dont Know JS**](https://github.com/getify/You-Dont-Know-JS) 》系列书籍,该系列书籍深入剖析Javascript语言的核心机理,探索JS中那些我们自认为理解了但实际上并没有理解的知识点。 4 | 5 | ![](/cover.jpg) 6 | 7 | --- 8 | 9 | ## 目录 10 | * [简介](README.md) 11 | * [前言](preface.md) 12 | * [第一部分:新手入门](part1/README.md) 13 | * [第一章:深入程序设计](part1/ch1.md) 14 | * [第二章:深入JavaScript](part1/ch2.md) 15 | * [第三章:深入YDKJS](part1/ch3.md) 16 | * [第二部分:作用域&闭包](part2/README.md) 17 | * [第一章:什么是作用域?](part2/ch1.md) 18 | * [第二章:词法作用域](part2/ch2.md) 19 | * [第三章:函数 VS 块级作用域](part2/ch3.md) 20 | * [第四章:变量提升](part2/ch4.md) 21 | * [第五章:作用域闭包](part2/ch5.md) 22 | * [附录A:动态作用域](part2/apdA.md) 23 | * [附录B:词法作用域中的this](part2/apdB.md) 24 | * [第三部分:`this`&对象原型](part3/README.md) 25 | * [第一章:`this`还是that?](part3/ch1.md) 26 | * [第二章:剖析`this`](part3/ch2.md) 27 | * [第三章:对象](part3/ch3.md) 28 | * [第四章:“类”对象](part3/ch4.md) 29 | * [第五章:原型](part3/ch5.md) 30 | * [第六章:行为委托](part3/ch6.md) 31 | * [附录A:ES6 `class`](part3/apdA.md) 32 | 33 | --- 34 | -------------------------------------------------------------------------------- /SUMMARY.md: -------------------------------------------------------------------------------- 1 | # Summary 2 | 3 | * [简介](README.md) 4 | * [前言](preface.md) 5 | * [第一部分:新手入门](part1/README.md) 6 | * [第一章:深入程序设计](part1/ch1.md) 7 | * [第二章:深入JavaScript](part1/ch2.md) 8 | * [第三章:深入YDKJS](part1/ch3.md) 9 | * [第二部分:作用域&闭包](part2/README.md) 10 | * [第一章:什么是作用域?](part2/ch1.md) 11 | * [第二章:词法作用域](part2/ch2.md) 12 | * [第三章:函数 VS 块级作用域](part2/ch3.md) 13 | * [第四章:变量提升](part2/ch4.md) 14 | * [第五章:作用域闭包](part2/ch5.md) 15 | * [附录A:动态作用域](part2/apdA.md) 16 | * [附录B:词法作用域中的this](part2/apdB.md) 17 | * [第三部分:`this`&对象原型](part3/README.md) 18 | * [第一章:`this`还是that?](part3/ch1.md) 19 | * [第二章:剖析`this`](part3/ch2.md) 20 | * [第三章:对象](part3/ch3.md) 21 | * [第四章:“类”对象](part3/ch4.md) 22 | * [第五章:原型](part3/ch5.md) 23 | * [第六章:行为委托](part3/ch6.md) 24 | * [附录A:ES6 `class`](part3/apdA.md) 25 | 26 | -------------------------------------------------------------------------------- /assets/cover1.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Hankszhang/you-dont-know-js/2128a175dd8dad5d80dbe26088c2471af54e4038/assets/cover1.jpg -------------------------------------------------------------------------------- /assets/cover2.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Hankszhang/you-dont-know-js/2128a175dd8dad5d80dbe26088c2471af54e4038/assets/cover2.jpg -------------------------------------------------------------------------------- /assets/cover3.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Hankszhang/you-dont-know-js/2128a175dd8dad5d80dbe26088c2471af54e4038/assets/cover3.jpg -------------------------------------------------------------------------------- /assets/fig1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Hankszhang/you-dont-know-js/2128a175dd8dad5d80dbe26088c2471af54e4038/assets/fig1.png -------------------------------------------------------------------------------- /assets/fig2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Hankszhang/you-dont-know-js/2128a175dd8dad5d80dbe26088c2471af54e4038/assets/fig2.png -------------------------------------------------------------------------------- /assets/fig3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Hankszhang/you-dont-know-js/2128a175dd8dad5d80dbe26088c2471af54e4038/assets/fig3.png -------------------------------------------------------------------------------- /assets/fig4.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Hankszhang/you-dont-know-js/2128a175dd8dad5d80dbe26088c2471af54e4038/assets/fig4.png -------------------------------------------------------------------------------- /assets/fig5.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Hankszhang/you-dont-know-js/2128a175dd8dad5d80dbe26088c2471af54e4038/assets/fig5.png -------------------------------------------------------------------------------- /cover.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Hankszhang/you-dont-know-js/2128a175dd8dad5d80dbe26088c2471af54e4038/cover.jpg -------------------------------------------------------------------------------- /part1/README.md: -------------------------------------------------------------------------------- 1 | # 你不了解的JS: 新手入门 2 | 3 | --- 4 | 5 | ![](/assets/cover1.jpg) 6 | 7 | --- 8 | ### 目录 9 | * [第一章: 深入程序设计](ch1.md) 10 | * 代码 11 | * 动手试一试 12 | * 操作符 13 | * 值&类型 14 | * 代码注释 15 | * 变量 16 | * 代码块 17 | * 条件语句 18 | * 循环 19 | * 函数 20 | * 练习 21 | * [第二章: Into JavaScript](ch2.md) 22 | * 值&类型 23 | * 变量 24 | * 条件语句 25 | * 严格模式 26 | * 函数作为值 27 | * `this`关键字 28 | * 原型 29 | * 旧&新 30 | * 非JavaScript 31 | * [第三章: Into YDKJS](ch3.md) 32 | * 作用域&闭包 33 | * this&对象原型 34 | * 类型&语法 35 | * 异步&性能 36 | * ES6&未来 37 | 38 | -------------------------------------------------------------------------------- /part1/ch1.md: -------------------------------------------------------------------------------- 1 | # 第一章:深入程序设计 2 | 3 | 欢迎阅读 *你不了解的JS(YDKJS)* 系列。 4 | 5 | *新手入门(Up & Going)* 一书主要介绍程序设计(编程)的几个基本概念——当然我们侧重讲JavaScirpt(通常缩写为JS)——以及如何掌握并理解本系列书籍剩下的标题(的知识点)。尤其是对于刚接触编程或者刚接触Java的读者来说,本书将简要浏览你从入门到进阶所需要掌握的知识。 6 | 7 | 本书一开始从高层次水平来解释程序设计的基本概念。按照*YDKJS*的标题,本书主要针对没有编程经验的读者,从JavaScirpt语言的角度帮助读者逐步理解编程的概念。 8 | 9 | 第一章主要概括*深入程序设计*过程中你想深入了解的知识和实践,也有许多其他能够帮助你在未来深入这些话题的很棒的编程入门资源,希望读者能在本章之外好好学习它们。 10 | 11 | 一旦你掌握了一般的编程基础,第二章会指导你熟悉JavaScirpt的编程风格。第二章介绍了JavaScirpt是什么,再一次声明,它不是一份完整的指南——这是*YDKJS*系列剩下的部分要做的事情。 12 | 13 | 如果你已经相当熟悉JavaScirpt了,先阅读第三章来快速预览你能从*YDKJS*中学到什么,然后开始学习吧! 14 | 15 | ## 代码 Code 16 | 17 | 让我们从头开始吧! 18 | 19 | 一个程序,也常叫做*源代码*或*代码*,是一系列告诉计算机该执行什么任务的特殊指令集合。通常代码被保存在文本文件中,当然你也可以直接在浏览器的控制台中输入JavaScript,我们后面会讲到。 20 | 21 | *计算机语言* 即是有效的格式和指令组合的规则,有时候也称为 *语法*,就像英语告诉你怎样拼写单词,怎样利用单词和标点构建有效语句一样。 22 | 23 | ### 语句 24 | 25 | 在计算机语言中,由词语、数字和操作符组合并执行指定任务的即是*语句*。在JavaScript中,一条语句可能如下所示: 26 | ```js 27 | a = b * 2; 28 | ``` 29 | 字符`a`和`b`称为 *变量*,就好比你用来存储东西的盒子。在程序中,变量用于保存程序中用到的值(如数字`42`)。可以把变量想象成真实值的占位符。 30 | 31 | 相比之下,`2`则就是一个值,称为字面值,因为它是独立的而不是保存在变量中。 32 | 33 | 字符`=`和`*`是操作符——它们对值和变量执行赋值和算术乘法等操作。 34 | 35 | JavaScript中的语句末尾以分号(;)结束。 36 | 37 | 语句`a = b * 2;`告诉计算机先取得保存在变量`b`中当前值,乘以`2`,然后将计算结果保存在另一个变量`a`中。 38 | 39 | 程序就是多条这样的语句的组合,它们描述了实现程序的目的所需要的所有步骤。 40 | 41 | ### 表达式 42 | 43 | 语句由一个或多个表达式组成。一个表达式可以是对一个变量或值的引用,也可以由一系列变量和值通过操作符连接组合而成。 44 | 45 | 例如: 46 | ```js 47 | a = b * 2; 48 | ``` 49 | 这条语句中包含了4个表达式: 50 | * `2`是 _字面值表达式_ 51 | * `b`是 _变量表达式_,表示取得变量的当前值 52 | * `b*2`_算术表达式_,表示乘法运算 53 | * `a = b * 2`是一个 _赋值表达式_,表示将`b*2`的计算结果赋给变量`a`(后文将介绍更多赋值操作) 54 | 55 | 一般单独存在的表达式也叫做 _表达式语句_,如: 56 | ```js 57 | b * 2; 58 | ``` 59 | 这种风格的表达式语句不常见,也不实用,通常不会对程序的运行产生任何影响——它会取得变量b的值并乘以2,但是不会对计算结果做任何操作。 60 | 61 | 更常见的表达式语句是 _调用表达式语句_,因为整条语句本身即为函数调用表达式: 62 | ```js 63 | alert(a); 64 | ``` 65 | 66 | ### 执行程序 67 | 68 | 这些编程语句的组合是怎样告诉计算机该做什么呢?程序需要被执行(才能起作用),或者说_运行程序_。 69 | 70 | 像`a = b * 2`这样的语句有助于开发者阅读和书写,但这并不是计算机能够直接理解的形式。所以计算机里需要有特定的工具(_解释器_ 或 _编译器_)来将你编写的代码翻译成计算机能够理解的命令。 71 | 72 | 对有些计算机语言来说,每次运行程序时,命令的翻译(通常叫做 _解释_ 代码)一般是自上而下、一行接一行进行的。而对于其他语言,翻译过程(通常叫做 _编译_ 代码)在程序运行之前进行,所以之后当程序运行时,实际上运行的是已经编译好的计算机能够理解的指令。 73 | 74 | 我们通常认为JavaScript是一门解释型语言,因为JavaScript源代码每次运行时都会被执行。但是这种说法不完全准确,JavaScript引擎实际上在运行程序时先编译然后立即执行编译后的代码。 75 | 76 | **注:** 更多关于JavaScript编译的知识,请参考本系列的 *作用域&闭包* 一书的前两章。 77 | 78 | ## 动手试一试 79 | 80 | 本章将通过一些简单的代码片段来介绍每个编程概念,当然都是用JavaScript写的。 81 | 82 | 再一次强调:读者在阅读本章时——可能需要花时间复习几遍——应该自己动手敲这些代码来熟悉这些概念。最简单的方法是打开你所用的浏览器(Firefox、Chrome、IE等)的开发者工具。 83 | 84 | **建议:** 一般来说,可以通过快捷键或菜单选项来打开开发者工具。更多关于在你喜欢的浏览器中启动和使用开发者工具的信息,请参考"[Mastering The Developer Tools Console](http://blog.teamtreehouse.com/mastering-developer-tools-console)"。想在控制台中一次输入多行,可以通过` + `来换行。一旦输入``,控制台就会执行你刚刚输入的所有内容。 85 | 86 | 我们先来熟悉下如何在控制台中执行代码。首先,建议在浏览器中打开一个新的空白标签页,我喜欢在地址栏中输入`about:blank`来打开。然后,确保按照刚刚提到的方法打开了开发者控制台。 87 | 88 | 现在,输入下面的代码,看看是怎么执行的: 89 | ```js 90 | a = 21; 91 | b = a * 2; 92 | 93 | console.log( b ); 94 | ``` 95 | 在Chrome的控制台中输入上面的代码会得到下面的输出: 96 | 97 | 98 | 99 | 好了,试一试吧。学习编程的最佳方法就是动手敲代码! 100 | 101 | ### 输出 102 | 103 | 在前面的代码片段中,我们用到了`console.log(..)`,我们来简要看下这行代码做了什么。 104 | 105 | 你可能已经猜到了,这就是通过它在控制台中打印文本(对用户来说也叫*输出*)。这条语句有两点需要解释下。 106 | 107 | 第一,`log( b )`部分是函数调用,我们将变量`b`传递给该函数,函数得到`b`的值并打印到控制台。 108 | 第二,`console.`部分是`log(..)`函数所在的对象的对象引用,我们会在第二章中详细介绍对象及其属性。 109 | 110 | 另一种输出的方式是执行`alert(..)`语句,如: 111 | ```js 112 | alert( b ); 113 | ``` 114 | 执行上述语句,不会在控制台打印输出,而是弹出一个包含`b`变量内容的"OK"弹出框。一般在控制台中用`console.log(..)`比用`alert(..)`更有助于学习编程和执行你的程序,因为你可以一次输出很多值而不影响浏览器的运行和交互。 115 | 116 | 本书中,我们都用`console.log(..)`来输出。 117 | 118 | ### 输入 119 | 120 | 我们在讨论输出的同时,也对 _输入_(即:从用户那里获得信息)好奇。 121 | 122 | 获得输入最常见的方式是在HTML页面中想用户展示一个可以输入内容的元素(如文本框),然后用JS读取这些值并保存到程序的变量中。 123 | 124 | 对于像本书一样只是简单的学习和展示来说,也可以用另一种方法来获得输入,用`prompt(..)`函数: 125 | ```js 126 | age = prompt( "Please tell me your age:" ); 127 | 128 | console.log( age ); 129 | ``` 130 | 可以猜到,传递给`prompt(..)`的信息——本例中的"Please tell me your age:"——会打印到弹出框中,结果如下所示: 131 | ![](/assets/fig2.png) 132 | 点击"OK"提交输入的文本之后,你会发现刚才输入的值被保存到了变量age中,并通过console.log(..)函数输出。 133 | 134 | 在学习基本编程概念的过程中,为了简单起见,本书中的实例不要求输入。但你现在已经知道如何使用prompt(..)了,如果你想挑战一下自己,可以在你的练习中使用输入。 135 | 136 | ## 操作符 137 | 138 | 操作符是对变量和值进行的操作。我们已经用过两个JavaScript操作符=和*了。 139 | `*`操作符执行算术乘。很简单,对吗? 140 | `=`操作符用于赋值——先计算=号右侧的值(源值),然后将其存入左边指定的变量(目标变量)中。 141 | 142 | __注意:__ 指定赋值可能看起来是一个奇怪的逆序。有些人可能喜欢将源值放在左边而将目标值放在右边,如42 -> a(这在JavaScript中是非法的),而不是a = 42。不幸的是,a=42这样的形式及其类似的变体形式,在现代编程语言中是相当普遍的。如果觉得不自然,请花点时间在脑海中默记它然后习惯它。 143 | 144 | 想象: 145 | ```js 146 | a = 2; 147 | b = a + 1; 148 | ``` 149 | 这里,我们将值2赋给变量a,然后我们得到变量a的值(仍为2),加上1后结果等于3,并保存在变量b中。 150 | 151 | 从技术上讲,关键词`var`不是操作符,但是在每个程序中都要用到它,因为它是 _声明_(也叫 _创建_)_变量_ 的主要方式。 152 | 153 | 记住要在使用变量之前声明它。但是在每个 _作用域_ 中的变量只能声明一次;声明之后便可使用任意多次。例如: 154 | ```js 155 | var a = 20; 156 | 157 | a = a + 1; 158 | a = a * 2; 159 | 160 | console.log( a ); // 42 161 | ``` 162 | 这里列举下JavaScript中常见的操作符: 163 | * 赋值:如a=2中的“=” 164 | * 算术计算:+(加),-(减),`*`(乘)和/(除),如a*3 165 | * 复合赋值:`+=`,`-=`,`*=`, `/=`是将算术操作与赋值操作组合起来的操作符,如a+=2(等效于a = a+2) 166 | * 自增/自减:`++`,`--`,如a++(等效于a = a+1) 167 | * 访问对象属性:`.`,如console.log() 168 | 169 | 对象是保存其他具有特定名字(属性)的值的值。obj.a表示对象obj有一个名字为a的属性。也可以通过obj["a"]的方式被访问对象的属性。 170 | * 相等:`==`(松散相等),`===`(严格相等),`!=`(松散不等),`!==`(严格不等),如a==b。参见第二章及“值&类型”。 171 | * 比较:`<`,`>`,`<=`,`>=`,如a<=b 172 | * 逻辑:`&&`,`||`, 如 a||b表示选择a或b。 173 | 174 | __注:__ 更多关于操作符的知识,请参考[MDN(Mozilla Developer Network)的官方手册](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Expressions_and_Operators)。 175 | 176 | ## 值和类型 177 | 178 | 如果你询问手机店的员工某款手机的售价是多少,他们可能会说“99,99”(即$99.99),他们告诉你的是一个实际数字值,表示购买该款手机你需要支付的金额(含税)。如果你买两台手机,只需将这个数值乘以2得到$199.98,即为你的消费额。 179 | 180 | 如果同一个员工拿另一款相似的手机说“free”(可能用手势),他们并没有给你一个确切的数值来表示你需要花费的金额($0.00),而是单词“free”。 181 | 182 | 随后你继续问手机是否带充电器时,得到的答案可能仅仅是“yes”或“no”。 183 | 184 | 类似的,在程序中表示值的时候,基于你想用这些值来做什么来选择这些值的不同表示方式。 185 | 186 | 在编程术语里,值的这些不同表示叫做 *类型*。JavaScript有几种内置类型,每种类型都有其初始值: 187 | * 当你想做数学运算时,你需要`number`类型; 188 | * 当你想在屏幕上打印某个值时,你需要`string`类型(一个或多个字符、单词、句子); 189 | * 当你想在程序中做判断时,你需要`boolean`类型(`true`或`false`)。 190 | 191 | 直接包含在源代码中的值叫做 *字面量*。string字面量由双引号`"..."`或单引号`'...'`括起来——两者没有什么区别。number字面量和boolean字面量就是他们表示的字面意思(即:42、true等)。 192 | 193 | 比如: 194 | ```js 195 | "I am a string"; 196 | 'I am also a string'; 197 | 198 | 42; 199 | 200 | true; 201 | false; 202 | ``` 203 | 204 | 除了string/number/boolean值类型之外,编程语言中一般还支持_数组_,_对象_,_函数_ 等类型。我们会在之后的章节中讨论更多的值和类型。 205 | 206 | ### 类型之间的转换 207 | 208 | 如果你有一个number类型的值,但是想要打印到屏幕上,那么你需要将其转换为string类型的值,在JavaScript中这种转换是强制的。相似的,如果有人在一个商务网站上的表单中输入了一串数字字符,这是string类型的值,但是如果之后你要用这个值进行算术运算的话,你需要 _强制_ 将其转换为number类型。 209 | 210 | JavaScript提供了几种不同的方式来在不同 _类型_ 之间强制转换,如: 211 | ```js 212 | var a = "42"; 213 | var b = Number( a ); 214 | 215 | console.log( a ); // "42" 216 | console.log( b ); // 42 217 | ``` 218 | 用`Number(...)`(内置函数)将任意其他类型 _显式_ 转换为number类型,这种方式简单粗暴。 219 | 220 | 但是当你试着比较两个不是同一类型的值时会产生歧义,这就需要 _隐式_ 转换了。 221 | 222 | 当将字符串"99.99"与数字99.99进行比较时,大部分人都认为它们是相等的,但是它们并不严格相等,不是吗?这是同一个值的两种不同表示,属于两种类型。你可以说它们是“松散相等”,是吗? 223 | 224 | 所以如果你用 == 对两者进行比较:"99.99" == 99.99,JavaScript会将左边的"99.99"转换为number类型的99.99。现在比较就变成了:99.99 == 99.99,结果当然是true。 225 | 226 | 尽管设计隐式转换是为了帮助你,但是如果不花时间学习隐式转换的规则并熟练掌握它的话它也可能带来困扰。大多数JS开发者都没有掌握隐式转换,所以共识是隐式转换容易造成困扰,可能给程序带来意想不到的bug,因此应该避免使用。有时候这甚至被认为是JavaScript语言的设计缺陷。 227 | 228 | 但是,隐式转换机制是可以掌握的,而且也是每个想要掌握JavaScript的人必须掌握的。一旦你掌握了它的规则,它不但不会给你造成困扰,实际上还会帮助你写出更好的程序!所以花精力学习它是值得的。 229 | 230 | _注:_ 更多类型转换的知识,请参考本书的第二章和 *类型&语法* 一书的第四章。 231 | 232 | ## 代码注释 233 | 234 | 手机店的员工可能会需要记一些新发布的手机的特性或者公司公布的新计划等笔记。这些笔记仅供员工查看——而不是供消费者阅读的。无论如何,通过记录所有这些销售相关的信息,这些笔记帮助员工更好的完成他们的工作。 235 | 236 | 在学习码代码的过程中学到的最重要的一条经验是,代码不仅仅是给计算执行的。代码对于开发者与编译器而言都是一样的,每一bit都至关重要。 237 | 238 | 你的电脑只关心机器码,汇编产生的一系列二进制0和1。你几乎可以写出无限多种程序来产生相同的二进制序列。你选择怎样编写你的程序不仅关乎你自己,更关乎你组里面的其他成员,甚至是未来的自己。 239 | 240 | 一方面,你应该努力写出可以正确工作的程序;另一方面,当你的程序被他人阅读时应该清晰合理。为你的变量和函数取好的名字会极大地加强程序的易读性。 241 | 242 | 另一个重要的方面是代码注释。注释是插入程序中纯粹为了帮助人理解代码的文本。解释器/编译器会忽略这些注释。 243 | 244 | 关于怎样才能写出注释良好的代码有很多不同的观点;我们没办法制定绝对的通用规则。但是有些原则和指南还是很有帮助的: 245 | * 没有注释的代码肯定是次优的; 246 | * 太多的注释(例如每行一句)是糟糕代码的标志; 247 | * 注释应该解释 _why_ 而不是 _what_。如果代码特别难理解,注释也可以用来解释_how_。 248 | 249 | 在JavaScript中有两种类型的注释:单行注释和多行注释。 250 | 例如: 251 | ```js 252 | // This is a single-line comment 253 | 254 | /* But this is 255 | a multiline 256 | comment. 257 | */ 258 | ``` 259 | 如果你想在某条语句上或行末加一句注释,可以用“//”单行注释。“//”后面的所有内容都会被当作注释(因而会被编译器忽略),直到行末。对于单行注释中可以写什么没有限制。 260 | 如: 261 | ```js 262 | var a = 42; // 42 is the meaning of life 263 | ``` 264 | 如果你的注释需要几行才能解释清楚,可以用`/* .. */`多行注释。 265 | 266 | 这里有一个多行注释的通常用法: 267 | ```js 268 | /* The following value is used because 269 | it has been shown that it answers 270 | every question in the universe. */ 271 | var a = 42; 272 | ``` 273 | 多行注释也可以用在一行中的任何位置,甚至是一行的中间,因为有`*/`表示注释的结尾。如: 274 | ```js 275 | var a = /* arbitrary value */ 42; 276 | 277 | console.log( a ); // 42 278 | ``` 279 | 多行注释中唯一不能出现的内容是`*/`,因为它会中断注释。 280 | 281 | 你肯定会想带着一个好的注释代码的习惯来开启编程的学习之旅。在本章的后面,你会看到我用注释帮助解释一些东西,所以在你的练习中你也应该这么做。相信我,每一个阅读你的代码的人都会感谢你! 282 | 283 | ## 变量 284 | 285 | 大多数有用的程序需要追踪某个值,这个值会随着程序过程的变化、程序中特定任务调用不同的运算符处理它而发生改变。 286 | 287 | 在程序中实现这个目的的最简单方式是将这个值赋给一个符号容器,称为 _变量_——这么叫是因为容器中的值会随着时间的变化而变化。 288 | 289 | 有些编程语言需要声明变量(容器)类型来保存特定类型的值,如`number`或`string`。*静态类型*,或称为*强类型*,主要是为了提高程序的健壮性,因为可以避免意外的值类型转换。 290 | 291 | 另外一些语言则强调值的类型而不是变量的类型。*弱类型*,或称为*动态类型*,允许变量在任意时刻保存任何类型的值。这有利于提升程序的灵活性,因为单个变量可以在程序的逻辑流程中的任意时刻表示任何类型的值。 292 | 293 | JavaScirpt是 *动态类型* 语言,这意味着变量可以保存任意类型的值而不用强制声明类型。 294 | 295 | 如前所述,我们用`var`语句来声明变量——没有其他的方式来声明变量。考虑这个简单的程序: 296 | ```js 297 | var amount = 99.99; 298 | amount = amount * 2; 299 | console.log(amount); // 199.98 300 | 301 | // convert `amount` to a string, and 302 | // add "$" on the beginning 303 | amount = "$" + String( amount ); 304 | 305 | console.log(amount); // "$199.98" 306 | ``` 307 | 变量amount最开始保存了数字99.99,然后保存了`amount*2`的结果,数字199.98。第一个`console.log(...)`命令将number类型的值隐式地转换为string类型并打印出来。 308 | 309 | 然后语句`amount="$"+String(amount)`显示地将值199.98转换为string类型,然后在前面加上字符`$`。现在变量amount保存的是string类型的值`"$199.98"`,所以第二个`console.log(...)`在输出时就不需要做类型转换了。 310 | 311 | JavaScirpt开发者要注意使用amount变量的灵活性,它的值可以是99.99、199.98和"$199.98"。为便于区分,喜欢使用静态类型的人可能更喜欢用`amountStr`来保存最后的值"$199.98",因为这是另外一种类型的值了。 312 | 313 | 无论使用那种方式,你需要注意的是变量amount保存了一个随着程序运行而变化的值,这正式变量的主要作用:管理程序 *状态*。 314 | 315 | 也就是说,*状态* 用于追踪程序运行时值的变化。 316 | 317 | 变量的另一个作用是统一管理值的设置。当你声明了一个变量并赋值,在整个程序中这个变量的值都不会改变时,这个变量就称为 *常量*。 318 | 319 | 通常在程序的顶部声明这些 *常量*,方便需要改变常量的值时能够快速定位到。按照惯例,JavaScirpt中的常量应该大写,并以`_`来连接单词。 320 | 这里有个简单的例子: 321 | ```js 322 | var TAX_RATE = 0.08; // 8% sales tax 323 | var amount = 99.99; 324 | amount = amount * 2; 325 | amount = amount + (amount * TAX_RATE); 326 | 327 | console.log( amount ); // 215.9784 328 | console.log( amount.toFixed( 2 ) ); // "215.98" 329 | ``` 330 | **注意:** 跟console.log(...)中log(...)是console对象的一个属性一样,这里的toFixed(...)是number类型值的一个函数属性。JavaScript中number不会自动格式化为美元格式——引擎不知道你要格式化成什么,因此没有货币的对应类型。toFixed(...)用于指定number类型的值小数点后保留的位数,并返回string值。 331 | 332 | 照例,变量TAX_RATE表示的是一个常量——其实上面的程序中它的值也是能够被改变的。但是如果将营业税率提高至9%,我们只需在一处将TAX_RATE的值设置为0.09就可以了,而不用遍历整个程序找到所有值为0.08的地方,然后把它们都改为0.09。 333 | 334 | 最新版的JavaScript(ES6)引入了用`const`关键字来声明 *常量* 新方法,而不是用`var`: 335 | ```js 336 | // as of ES6: 337 | const TAX_RATE = 0.08; 338 | var amount = 99.99; 339 | // .. 340 | ``` 341 | 常量与保存不变值的变量一样,是很有用的。除此之外,常量还能够在初始化之后阻止意外改变常量值的行为。如果你在第一次声明之后尝试给TAX_RATE重新赋值,程序会拒绝这个改变(在严格模式中会报错——见第二章中的“严格模式”)。 342 | 343 | 另外,这种防止出错的“保护机制”与静态类型的强制类型转换相似,因此你会发现其他语言中的静态类型是很优美的! 344 | 345 | **注:** 更多有关程序中变量支持的不同类型的值,请参见 *类型&语法* 一书。 346 | 347 | ## 代码块 348 | 349 | 你到手机店买新手机时,店里的员工需要经过一系列步骤才能完成结账。 350 | 351 | 类似地,写代码时我们经常需要将一系列语句组合在一起,我们称之为 *代码块*。在JavaScript中,代码块由一对花括号`{...}`包裹的一条或多条语句组成。如: 352 | ```js 353 | var amount = 99.99; 354 | 355 | // a general block 356 | { 357 | amount = amount * 2; 358 | console.log( amount ); // 199.98 359 | } 360 | ``` 361 | 这种独立使用`{...}`的普通代码块是合法的,不过在JS程序中不常见。通常,代码块与其他的控制语句一起使用,如`if`语句或循环语句。例如: 362 | ```js 363 | var amount = 99.99; 364 | 365 | // is amount big enough? 366 | if (amount > 10) { // <-- block attached to `if` 367 | amount = amount * 2; 368 | console.log( amount ); // 199.98 369 | } 370 | ``` 371 | 下一节中我们会讨论if语句,上面的程序中,`if(amount>10)`后面紧接着`{...}`及其中的两条语句;之后if条件语句成立时,代码块内的语句才会被执行。 372 | 373 | **注:** 与`console.log(aomunt)`等语句不一样,代码块语句后面不需要加分号`;` 374 | 375 | ## 条件语句 376 | 377 | "多加$9.99即可买一张屏幕保护膜,需要吗?"友好的手机店店员请你做一个选择。而你首先需要考虑你的财务状况再来回答这个问题。但很明显,这是一个简单的“yes or no”问题。 378 | 379 | 在程序中我们可以用很多方式来表示 *条件判断* (也即判断)。 380 | 381 | 最常见的是`if`语句,实际上表达的意思是“如果这个条件成立,就做下面的事”。例如: 382 | ```js 383 | var bank_balance = 302.13; 384 | var amount = 99.99; 385 | 386 | if (amount < bank_balance) { 387 | console.log( "I want to buy this phone!" ); 388 | } 389 | ``` 390 | if语句的`( )`中要求有一个结果为true或false的表达式。上面的例子中,我们的表达式是`amount 0) { 430 | console.log( "How may I help you?" ); 431 | // help the customer... 432 | numOfCustomers = numOfCustomers - 1; 433 | } 434 | 435 | // versus: 436 | do { 437 | console.log( "How may I help you?" ); 438 | // help the customer... 439 | numOfCustomers = numOfCustomers - 1; 440 | } while (numOfCustomers > 0); 441 | ``` 442 | 这两个循环语句的不同之处在于条件的判断是在第一次迭代之前(while)还是在第一次迭代之后(do...while)。不管是那种形式,如果条件判断为false,则不再执行下一次迭代。这意味着,如果初识条件为false,那么while循环将永远不会运行,但是do...while循环仍旧会运行一次。 443 | 444 | 有时候需要让循环执行给定数字表示的次数来实现某种功能,如从0到9(10个数字)。可以设置循环的初始变量如i为0,然后在每次迭代中自加1。 445 | 446 | **注意:** 出于诸多历史原因,编程语言中几乎总是从0开始计数,而不是同1开始。如果不习惯这种思维,会觉得很困扰。花点时间来练习从0开始计数并习惯这种方式吧! 447 | 448 | 每次迭代都会做条件判断,就好像循环中有一个隐式的if语句一样。 449 | 450 | 我们可以用JavaScript的`break`语句来中止一个循环。当然我们会发现很容易就会写一个不能被中止的死循环。 451 | 例如: 452 | ```js 453 | var i = 0; 454 | 455 | // a `while..true` loop would run forever, right? 456 | while (true) { 457 | // stop the loop? 458 | if ((i <= 9) === false) { 459 | break; 460 | } 461 | 462 | console.log( i ); 463 | i = i + 1; 464 | } 465 | // 0 1 2 3 4 5 6 7 8 9 466 | ``` 467 | 除了while和do...while之外,另一种循环的语法格式是`for`循环: 468 | ```js 469 | for (var i = 0; i <= 9; i = i + 1) { 470 | console.log( i ); 471 | } 472 | // 0 1 2 3 4 5 6 7 8 9 473 | ``` 474 | 可以看到,两个例子中前10次迭代的条件`i<=9`都为true(i的值从0到9),但是i值为10时则变为false。 475 | 476 | for循环有三条子句:初始化子句(var i=0)、条件测试子句(i <=9)以及更新子句(i = i+1)。所以如果你想用循环来计数的话,for循环更加简洁且便于理解和书写。 477 | 478 | 还有其他的用于对特定值进行迭代的特殊循环形式,如迭代对象的属性(见第二章),它的隐性条件测试为是否所有的属性都被处理了。不管是何种形式的循环,“条件不成立则中止循环”的概念都是适用的。 479 | 480 | ## 函数 481 | 482 | 手机店店员可能没有带计算器,但是她需要计算出税费和应付金额。这是一个定义一次,可以反复使用的工作。公司很有可能有一个内置了这些功能的结账寄存器(电脑、平板等)。 483 | 484 | 相似地,你的程序几乎肯定会将代码完成的任务分成一个个可重复使用的片段,而不是你自己反反复复地重写。我们可以通过定义`function`来实现。 485 | 486 | 函数通常是一个命名的且可以通过该名字被调用的代码片段,每次被调用时,函数内部的代码都会执行一遍。例如: 487 | ```js 488 | function printAmount() { 489 | console.log( amount.toFixed( 2 ) ); 490 | } 491 | 492 | var amount = 99.99; 493 | printAmount(); // "99.99" 494 | amount = amount * 2; 495 | 496 | printAmount(); // "199.98" 497 | ``` 498 | 函数也可以接收参数——你传入的值。也可以选择性地返回一个值。 499 | ```js 500 | function printAmount(amt) { 501 | console.log( amt.toFixed( 2 ) ); 502 | } 503 | 504 | function formatAmount() { 505 | return "$" + amount.toFixed( 2 ); 506 | } 507 | 508 | var amount = 99.99; 509 | printAmount( amount * 2 ); // "199.98" 510 | amount = formatAmount(); 511 | console.log( amount ); // "$99.99" 512 | ``` 513 | 函数printAmount(..)接收一个叫做amt的参数。函数formatAmount()返回一个值。当然我们也可以在同一个函数中既接收参数又返回值。 514 | 515 | 通常将需要被多次调用的代码定义成函数,但是有时候尽管你只会调用一次,将代码组织成命名的片段集合也是很有用的。如: 516 | ```js 517 | const TAX_RATE = 0.08; 518 | 519 | function calculateFinalPurchaseAmount(amt) { 520 | // calculate the new amount with the tax 521 | amt = amt + (amt * TAX_RATE); 522 | 523 | // return the new amount 524 | return amt; 525 | } 526 | 527 | var amount = 99.99; 528 | amount = calculateFinalPurchaseAmount( amount ); 529 | console.log( amount.toFixed( 2 ) ); // "107.99" 530 | ``` 531 | 尽管calculateFinalPurchaseAmount(..)只被调用了一次,将它的行为组织为一个单独的命名函数使得使用这段逻辑的代码更加清晰。如果函数有很多条语句,这样做带来的好处将更加明显。 532 | 533 | ### 作用域 534 | 如果你向店员询问这个手机店没有的手机型号,店员将没法买给你你想要的手机。她只能拿到她的店里库存的手机。你不得不换一家店看能不能找到你想要的手机。 535 | 536 | 编程中这个概念的术语是:*作用域*(技术上称为 *词法作用域*)。在JavaScript中,每个函数都有自己的作用域。作用域实际上包括变量的集合和通过名字访问这些变量的规则。只有函数内部的代码才能访问该函数作用域内的变量。 537 | 538 | 在同一个作用域内的变量名必须是唯一的——不能同时存在两个a变量。但是同一个变量名a可以出现在不同的作用域中。 539 | ```js 540 | function one() { 541 | // this `a` only belongs to the `one()` function 542 | var a = 1; 543 | console.log( a ); 544 | } 545 | 546 | function two() { 547 | // this `a` only belongs to the `two()` function 548 | var a = 2; 549 | console.log( a ); 550 | } 551 | 552 | one(); // 1 553 | two(); // 2 554 | ``` 555 | 当然,一个作用域可以被嵌套在另一个作用域内,就好像生日Party上小丑刺破一个气球,然后刺破这个气球里面的气球一样。如果一个作用域被另一个作用域嵌套,那么最内层作用域中的代码可以访问到其他作用域中的变量。 556 | 如: 557 | ```js 558 | function outer() { 559 | var a = 1; 560 | function inner() { 561 | var b = 2; 562 | // we can access both `a` and `b` here 563 | console.log( a + b ); // 3 564 | } 565 | inner(); 566 | // we can only access `a` here 567 | console.log( a ); // 1 568 | } 569 | outer(); 570 | ``` 571 | 词法作用域的规则:一个作用域中的代码能够访问这个作用域中的变量,也能访问到这个作用域外面的任何作用域中的变量。 572 | 因此函数inner()内的代码可以同时访问变量a和b,但是outer()只能够访问a——不能访问b,因为它只存在与inner()内部。 573 | 574 | 回忆前面的代码片段: 575 | ```js 576 | const TAX_RATE = 0.08; 577 | 578 | function calculateFinalPurchaseAmount(amt) { 579 | // calculate the new amount with the tax 580 | amt = amt + (amt * TAX_RATE); 581 | // return the new amount 582 | return amt; 583 | } 584 | ``` 585 | 因为有词法作用域,常量TAX_RATE可以在函数calculateFinalPurchaseAmount(..)内被访问到,尽管我们没有将其传入函数中。 586 | 587 | **注:** 更多词法作用域的知识,请参考 *作用域&闭包* 一书的前三章。 588 | 589 | ## 练习 590 | 591 | 学习编程,除了多练习绝对别无它法。仅仅阅读我写的这些表达性文字不能使成为一名程序员。 592 | 593 | 牢记这一点,现在我们来练习在本章中学到的了一些概念。我给出要求,你们先自己动手尝试,然后参考我在下文给出的代码,看我是怎么实现的。 594 | * 写一个程序计算你购买手机所需的总金额。你一直买直到你的银行卡上没钱了。只要售价低于你的心理预期值,你就会给每台手机购买配件。 595 | * 计算好你的消费总额之后,加入税费,输出计算后的消费额,需要格式化。 596 | * 最后,检查你的银行账户余额,看是否足够支付。 597 | * 你应该将“税率”、“手机价格”、“配件价格”及“心理预期值”设为常量,将银行账户余额设为变量。 598 | * 你应该定义函数来计算税费、格式化价格(添加$,保留2位小数)。 599 | * **加分项**:在程序中加入输入,可以用prompt(..)函数。例如,你可以提示用户输入银行帐号余额。 600 | 好了,开始练习吧。在你自己尝试之前不要偷看我下面的代码! 601 | 602 | 下面是我用JavaScript写的参考答案: 603 | ```js 604 | const SPENDING_THRESHOLD = 200; 605 | const TAX_RATE = 0.08; 606 | const PHONE_PRICE = 99.99; 607 | const ACCESSORY_PRICE = 9.99; 608 | 609 | var bank_balance = 303.91; 610 | var amount = 0; 611 | 612 | function calculateTax(amount) { 613 | return amount * TAX_RATE; 614 | } 615 | 616 | function formatAmount(amount) { 617 | return "$" + amount.toFixed( 2 ); 618 | } 619 | 620 | // keep buying phones while you still have money 621 | while (amount < bank_balance) { 622 | // buy a new phone! 623 | amount = amount + PHONE_PRICE; 624 | 625 | // can we afford the accessory? 626 | if (amount < SPENDING_THRESHOLD) { 627 | amount = amount + ACCESSORY_PRICE; 628 | } 629 | } 630 | 631 | // don't forget to pay the government, too 632 | amount = amount + calculateTax( amount ); 633 | 634 | console.log( 635 | "Your purchase: " + formatAmount( amount ) 636 | ); 637 | // Your purchase: $334.76 638 | 639 | // can you actually afford this purchase? 640 | if (amount > bank_balance) { 641 | console.log( 642 | "You can't afford this purchase. :(" 643 | ); 644 | } 645 | // You can't afford this purchase. :( 646 | ``` 647 | 怎么样?现在看过我的代码之后再返回去试一试吧。试着改变一些常量的的值,看看程序的运行结果会有什么不同。 648 | 649 | ## 复习 650 | 651 | 学习编程不一定是一个复杂而艰难的过程,你只需在脑海中谨记一些基本概念。 652 | 653 | 这个过程就更积木游戏一样,想搭建一座高塔,首先需要一块一块堆叠开始。编程也是这样的。下面是一些编程中必要的砖块: 654 | * 你需要 *操作符* 来对值进行操作 655 | * 你需要值和 *类型* 来执行不同的操作,如对number进行计算,或输出string。 656 | * 在程序执行时,你需要 *变量* 来保存数据(也即 *状态*) 657 | * 你需要类似if语句一样的 *条件语句* 来做判断 658 | * 你需要 *循环* 来重复执行特定工作,直到条件变为false 659 | * 你需要 *函数* 将代码组织成逻辑性强、可复用的代码块。 660 | 661 | 使用代码注释可以使代码更具可读性,使得你的代码更易于理解、便于维护、如果出现bug也更好解决。 662 | 663 | 最后,不要忽略练习的重要性。学习怎么写代码的最佳方法是动手写代码。 664 | 665 | 现在,我很高兴你在学习如何编写代码!继续坚持。别忘了查阅其他的编程入门资料(书籍、博客、在线培训等)。学习本章及本书是一个不错的开始,但是这仅仅是简要介绍而已。 666 | 667 | 下一章将重温本章中的很多概念,但是会更多地从JavaScript的角度出发,突出本系列剩下的内容中将进行深入讨论的主题。 668 | 669 | 670 | 671 | 672 | 673 | 674 | 675 | 676 | 677 | 678 | 679 | 680 | 681 | 682 | 683 | 684 | 685 | 686 | 687 | 688 | 689 | 690 | -------------------------------------------------------------------------------- /part1/ch2.md: -------------------------------------------------------------------------------- 1 | # 第二章:深入JavaScript 2 | 3 | 在上一章中,我介绍了编程的基本要素,如变量、循环、条件语句和函数等。当然,所有的代码也都是用JavaScript表示的。本章中,我们将主要关注一个JS开发者从入门到进阶需要了解的JavaScript的方方面面。 4 | 5 | 本章我我将介绍一些在以后的 *YDKJS* 书籍中才会详细讲解的概念。你可以把本章作为一个后续书籍深入讨论的主题的一个总览。 6 | 7 | 如果你是JavaScript的初学者,你应该多花些时间反复复习本章的概念和代码示例。任何伟大的建筑都是由一砖一瓦堆砌而成的,所以不要期望能够第一遍就能完全掌握所有的知识点。 8 | 9 | 现在让我们开始深入学习JavaScript之旅吧! 10 | 11 | ## 值&类型 12 | 我们在第一章中提到过,JavaScript的值是有类型的,而变量是没有类型的。JavaScript有下列的内置类型: 13 | * `string` 14 | * `number` 15 | * `boolean` 16 | * `null` 和 `undefined` 17 | * `object` 18 | * `symbol` (ES6中新引入) 19 | 20 | JavaScript中可以用`typeof`操作符来查看某个值属于什么类型: 21 | ```js 22 | var a; 23 | typeof a; // "undefined" 24 | 25 | a = "hello world"; 26 | typeof a; // "string" 27 | 28 | a = 42; 29 | typeof a; // "number" 30 | 31 | a = true; 32 | typeof a; // "boolean" 33 | 34 | a = null; 35 | typeof a; // "object" -- weird, bug 36 | 37 | a = undefined; 38 | typeof a; // "undefined" 39 | 40 | a = { b: "c" }; 41 | typeof a; // "object" 42 | ``` 43 | 44 | `typeof`操作符的返回值永远是六种类型之一(ES6中是7种类型,包括'symbol'类型)的字符串值。例如,`typeof "abc"`返回`"string"`,而不是`string`。 45 | 46 | 注意上面的代码中变量a可以保存不同类型的值,`typeof a`不是获取变量a的类型,而是获取变量a中当前值的类型。JavaScript中只有值才有类型,而变量仅仅是存放这些值的容器。 47 | 48 | `typeof null`是一个有意思的例子,因为它会错误地返回"object",而不是预料中的"null"。 49 | **注意:** 这是JS的一个遗留bug,但是可能永远也不会修复。由于Web中有太多的代码依赖于这个bug,因此修正这个bug可能会导致更多的bug! 50 | 51 | 还要注意`a = undefined`。这里显示地给变量a赋值为undefined值,这在表现上与没有给变量a设置任何值的情况是一样的,如上面代码第一行的var a。有几种情况会使得变量的值为"undefined",包括没有返回值和使用了void操作符的函数。 52 | 53 | ### 对象 54 | `object`类型表示一类可以设置属性(命名的位置符)的复合值,每个属性保存它们自己的任意类型的值。对象可能是JavaScirpt中最有用的值类型了。 55 | ```js 56 | var obj = { 57 | a: "hello world", 58 | b: 42, 59 | c: true 60 | }; 61 | 62 | obj.a; // "hello world" 63 | obj.b; // 42 64 | obj.c; // true 65 | 66 | obj["a"]; // "hello world" 67 | obj["b"]; // 42 68 | obj["c"]; // true 69 | ``` 70 | 将这个obj对象可视化为如下形式有助于理解: 71 | 72 | |a |b |c | 73 | |:------------:|:--:|:----:| 74 | |"hello world" | 42 | true | 75 | 76 | 可以用 *点记法*(即`obj.a`)或 *括号记法*(即`obj['a']`)来访问对象的属性。点记法更简单且易读,因此尽可能的使用点记法。 77 | 如果属性名中包含特殊字符,则应该使用括号记法,如`obj["hello world!"]`——通过括号记法访问的属性通常作为 *键值*。`[ ]`中要么是一个变量(稍后解释),要么是一个 *字面量字符串*(包裹在`".."`或`'..'`中)。 78 | 当然如果你想访问的键值对的名字保存在另一个变量中,也应该使用括号记法,如: 79 | ```js 80 | var obj = { 81 | a: "hello world", 82 | b: 42 83 | }; 84 | 85 | var b = "a"; 86 | 87 | obj[b]; // "hello world" 88 | obj["b"]; // 42 89 | ``` 90 | **注:** 查看更多有关JavaScirpt对象的知识点,参考 *this&对象原型* 一书,特别是第三章。 91 | 92 | JavaScirpt中还有两种常见的值类型:*数组* 和 *函数*。它们不是内置的值类型,而更像是子类型——特殊的`object`类型。 93 | 94 | #### 数组 95 | 数组是按数字索引顺序保存属性值的对象,如: 96 | ```js 97 | var arr = [ 98 | "hello world", 99 | 42, 100 | true 101 | ]; 102 | 103 | arr[0]; // "hello world" 104 | arr[1]; // 42 105 | arr[2]; // true 106 | arr.length; // 3 107 | 108 | typeof arr; // "object" 109 | ``` 110 | **注:** 编程语言从0开始计数,JS也不例外,将`0`作为数组第一个元素的索引。 111 | `arr`的可视化形式如下: 112 | 113 | |0 |1 |2 | 114 | |:------------:|:--:|:----:| 115 | |"hello world" | 42 | true | 116 | 117 | 由于数组是特殊的对象(如`typeof`的结果所示),因此也可以有属性,包括自动更新的`length`属性。 118 | 119 | 理论上通过自定义属性名,可以把数组当作正常的对象使用,也可以用`object`来模拟数组,只需将其属性用数字(0,1,2等)即可。当然这样的用法不利于区别不同的值类型。 120 | 121 | 最好且最自然的方式是用数组表示以数字做索引的值,而用`object`表示具有命名属性的值。 122 | 123 | #### 函数 124 | JS程序中常见的另一个`object`子类型是函数: 125 | ```js 126 | function foo() { 127 | return 42; 128 | } 129 | 130 | foo.bar = "hello world"; 131 | 132 | typeof foo; // "function" 133 | typeof foo(); // "number" 134 | typeof foo.bar; // "string" 135 | ``` 136 | 再次强调,函数是`object`的子类型——`typeof`返回`"function"`,表明function是一个主要类型——因此可以有属性,但是一般只有在少数情况下才会用函数的对象属性(如`foo.bar`)。 137 | 138 | **注:** 更多关于JS值及其类型的知识,参考 *类型&语法* 一书的第二章。 139 | 140 | ### 内置类型的方法 141 | 我们前面讨论的内置类型和子类型暴露了很多强大而实用的属性和方法。如: 142 | ```js 143 | var a = "hello world"; 144 | var b = 3.14159; 145 | 146 | a.length; // 11 147 | a.toUpperCase(); // "HELLO WORLD" 148 | b.toFixed(4); // "3.1416" 149 | ``` 150 | 这里`a.toUpperCase()`函数背后的调用机制比表面上看起来复杂的多。简单来说,与原始`string`类型对应地有一个“原生”`String`(大写字母S)的对象封装形式;正是在这个对象封装器的原型上定义了`toUpperCase()`方法。 151 | 152 | 当引用原始字符串值如"hello world"的属性或方法时(如a.toUpperCase()),JS自动将这个值“放入”对应的对象封装器中(表面上看不出区别)。 153 | 154 | string值可以被String对象封装,number值可以被Number对象封装,而boolean值可以被Boolean对戏那个封装。在大多数情况下,我们直接使用值的对象封装形式就行——几乎任何时候原始值形式都用得更多,JavaScript会处理剩下的事情。 155 | 156 | **注:** 更多关于JS原生和“封装”的知识,参考 *类型&语法* 一书的第三章。想深入理解对象原型的概念,参考 *this&对象原型* 一书的第五章。 157 | 158 | ### 比较值 159 | 在JS程序中主要需要做两种类型的值比较:*等式* 和 *不等式*。不管进行何种值的比较,结果都是一个boolean值(true或false)。 160 | 161 | #### 强制转换 162 | 我们在第一章中简要讨论了强制转换,现在我们温习一下。 163 | 164 | JavaScript中有两种形式的强制转换:*显示* 和 *隐式*。从一种类型值转换为另一种类型值的代码中就会发生显示强制转换,而隐式转换在一些操作符不带来副作用的情况上才会发生。 165 | 166 | 你可能听说过“强制转换是恶魔”的论调,因为很多情况下强制转换会带来意想不到的结果。也许没有什么能比JS语言带给开发者更多挫败感了。 167 | 168 | 强制转换不是恶魔,也不一定不可控。实际上,在大多数情况下使用强制类型转换都是合理且可理解的,甚至可以有效地提高代码的可读性。这里我们不做详细讨论——*类型&语法* 一书的第四章会详细阐述。 169 | 170 | 下面是一个显示转换的例子: 171 | ```js 172 | var a = "42"; 173 | 174 | var b = Number( a ); 175 | 176 | a; // "42" 177 | b; // 42 -- the number! 178 | ``` 179 | 这是一个隐式转换的例子: 180 | ```js 181 | var a = "42"; 182 | 183 | var b = a * 1; // "42" implicitly coerced to 42 here 184 | 185 | a; // "42" 186 | b; // 42 -- the number! 187 | ``` 188 | 189 | #### 真值&假值 190 | 第一章中,我们简要提到了真值和假值的性质:当一个非boolean值强制转换为boolean值时,它的值是true还是false呢? 191 | 192 | JavaScript中表示“假值”的有如下值: 193 | * `""`(空字符串) 194 | * `0`, `-0`, `NaN`(无效的`number`) 195 | * `null`, `undefined` 196 | * `false` 197 | 不在“假值”列表中的其他任何值都表示“真值”,如: 198 | * "hello" 199 | * 42 200 | * true 201 | * [ ], [1,"2",3] 202 | * { }, {a:42} 203 | * function foo(){..} 204 | 需要谨记的是非boolean值强制转换为boolean值时只会做“真/假”转换。注意不要混淆了一个值看起来转换为boolean值,实际上并没有(转换为boolean值)的情况。 205 | 206 | #### 等式 207 | 有四个等式运算符:`==`,`===`,`!=`,`!==`。其中`!`表示对应的“不相等”操作符;不要混淆了 *不相等* 和 *不等式* 的概念。 208 | 209 | 一般来说,==和===的区别在于:==仅检查两个值是否相等,而===既检查值是否相等又检查类型是否相同。然而,这种说法也不准确。更准确的说法应该是:==在允许强制类型转换的情况下来检查两个值是否相等,而===不允许强制转换来检查两个值是否相等;因此===常称作“严格相等”。 210 | 211 | 看一个==允许隐式强制转换而===不允许强制转换的例子: 212 | ```js 213 | var a = "42"; 214 | var b = 42; 215 | 216 | a == b; // true 217 | a === b; // false 218 | ``` 219 | 在`a == b`的等式中,JS发现两个值的类型不相同,所以它会经过一系列步骤将其中一个或两个值强制转换为不同的类型,使得两者的类型相同,然后再检查值是否相等。 220 | 读者会发现,经过类型转换后,`a == b`可能有两种相等的方式:一个是`42 == 42`,另一种是`"42" == "42"`。哪个是对的呢? 221 | 222 | 答案是:"42"变成42,因此等式变成了42 == 42。在简单的情况下,哪种比较方式其实没什么影响,因为结果是一样的。但是在更复杂的情况下,比较结果以及如何得到这个结果的过程都会对程序有影响。 223 | 224 | `a === b`的结果是false,因为不允许强制转换,所以简单的值比较肯定不相等。很多开发者觉得===更可控,因此建议总是用`===`而抛弃`==`。我觉得这种观点很片面。我认为 *如果花时间掌握==是怎样工作的*,它将会是一个提升程序质量的强大工具。 225 | 226 | 这里我们不详细展开讨论==比较时强制转换是怎样工作的。相关的知识大部分都很好理解,但是也要注意一些重要的特殊情况。完整的转换规则请参考[ES5规范的11.9.3节](http://www.ecma-international.org/ecma-262/5.1/),你会发现与一片唱衰声相比,这其中的机制简单直接到让你大吃一惊。 227 | 228 | 这里把详细细节总结为几条简单的规则,以帮助你根据不同的情况选择用==还是===,规则如下: 229 | * 如果比较的两个值中有一个可能是true或false值,应用===,不用==。 230 | * 如果比较的两个值中有一个可能是这些特定值(0, ""或[]——空数组),应用===,不用==。 231 | * 在其他所有情况下,都可以放心的用==。用==不仅安全无害,而且在很多情况下都可以简化你的代码,从而提高程序的可读性。 232 | 列举的这些规则要求你认真思考你的代码,仔细考虑对变量进行比较时得到的具体是什么类型的值。如果你能够确定值的类型,用==就很安全,大胆的用吧!如果你不能确定值的类型,就用===。就这么简单。 233 | 234 | 不等于!=与==相反,而!==与===相反。我们刚刚讨论的规则和结论相应的也都适用于它们。 235 | 236 | 如果你对两个非原始值,如object(包括function和array)进行比较,需要特别注意==和===的比较规则。因为这些值实际上是引用值,==和===只会简单的检查这两个引用值是否相等,而不是底层的真实值。 237 | 例如,默认情况下array值的强制转换会用逗号(,)将数组内所有值拼接成string类型的值。你可能觉得两个内容相同的数组用==比较时结果应该是true,但实际上并不是: 238 | ```js 239 | var a = [1,2,3]; 240 | var b = [1,2,3]; 241 | var c = "1,2,3"; 242 | 243 | a == c; // true 244 | b == c; // true 245 | a == b; // false 246 | ``` 247 | **注:** 更多有关==比较规则的知识,请参考ES5规范(11.9.3节)以及 *类型&语法* 一书的第四章,第二章有引用类型值的详细阐述。 248 | 249 | #### 不等式 250 | 操作符`<`,`>`,`<=`和`>=`用于不等式比较,规范中也称为“关系比较”。通常它们用于有序的可比较的值,如number。不等式3 < 4是很容易理解的。 251 | 252 | 但是JavaScript的string类型的值根据一般的字母顺序("bar" < "foo")也可以用于不等式比较。 253 | 254 | 那么强制转换呢? 与`==`比较类似的规则(虽然不完全一致)也适用于不等式操作符。值得一提的是,不等比较没有像“严格相等”一样不允许强制转换的“严格不等”操作符。 255 | 如: 256 | ```js 257 | var a = 41; 258 | var b = "42"; 259 | var c = "43"; 260 | 261 | a < b; // true 262 | b < c; // true 263 | ``` 264 | 这其中发生了什么? 如ES5规范11.8.5节所述,如果`<`比较的两个值都是string类型,如上的`b < c`,那么就进行字典化(也即按照字典的字母顺序)比较。但是如果有一个值不是或者两个值都不是string类型,如上的`a < b`,两个值都会强制转换为number类型,然后进行数字比较。 265 | 266 | 比较不同类型值时你可能遇到的最大的问题——记住,没有“严格不等”形式——是其中一个值不能转换为合法的number,如: 267 | ```js 268 | var a = 42; 269 | var b = "foo"; 270 | 271 | a < b; // false 272 | a > b; // false 273 | a == b; // false 274 | ``` 275 | 等等,为什么这三个比较的结果都是false?因为在<和>比较中b值被转换为“无效的number值”NaN,规范明确指出NaN不能进行大于或小于比较。 276 | 277 | `==`比较为false的原因有点不一样。前面我们讨论过,a==b转换为`42 == NaN`进行比较,因此结果也是false。 278 | 279 | **注:** 更多有关不等式比较规则的知识,请参考ES5规范的第11.8.5节以及 *类型&语法* 一书的第四章。 280 | 281 | ## 变量 282 | 283 | 在JavaScript中,变量名(包括函数名)必须是合法的 *标识符*。如果考虑如Unicode等传统字符在内的话,标识符中关于合法字符的严格完整的规则就很复杂了。如果仅考虑普通的ASCII字母数字字符的话,这些规则就相当简单了。 284 | 285 | 标识符的首字符必须是a-z, A-Z,$或_,之后可以接任意字符,包括数字0-9。 286 | 287 | 一般来说,适用于属性名的规则也同样适用于变量标识符。然而,有些特殊单词不能作为变量名却可以作为属性名。这些单词称为“保留字”,也包括JS关键字在内(for, in, if, null, true, false等)。 288 | 289 | ### 函数作用域 290 | 291 | 如果用var关键字声明一个变量,那么这个变量属于当前函数的作用域,如果这个变量是声明在任何函数之外的最顶层,则属于全局作用域。 292 | 293 | #### 变量提升 294 | 无论var出现在作用域中的什么位置,该条语句声明的变量都属于整个作用域,且在整个作用域中都是可以被访问的。 295 | 296 | 由于var声明的变量在概念上被“移动”到其所在作用域的最上面,因此这种行为被形象地称为 *提升*。从技术上,代码的编译机制可以更准确地解释这个过程,这里我们先略过不做详细讨论。 297 | 考虑: 298 | ```js 299 | var a = 2; 300 | 301 | foo(); // works because `foo()` 302 | // declaration is "hoisted" 303 | function foo() { 304 | a = 3; 305 | console.log( a ); // 3 306 | var a; // declaration is "hoisted" 307 | // to the top of `foo()` 308 | } 309 | 310 | console.log( a ); // 2 311 | ``` 312 | **注意:** 依赖变量 *提升* 来在用var定义之前使用变量的方式既不常见也不是一个好的习惯;这可能会造成程序混乱。我们更多的是使用函数声明提升,上面的代码中我们在`foo()`函数的正式声明之前就调用了它。 313 | 314 | #### 嵌套作用域 315 | 当你声明了一个变量,它就可以在作用域中的任何位置被访问到,包括任何在更低级/次级的作用域内。例如: 316 | ```js 317 | function foo() { 318 | var a = 1; 319 | function bar() { 320 | var b = 2; 321 | function baz() { 322 | var c = 3; 323 | console.log( a, b, c ); // 1 2 3 324 | } 325 | baz(); 326 | console.log( a, b ); // 1 2 327 | } 328 | bar(); 329 | console.log( a ); // 1 330 | } 331 | 332 | foo(); 333 | ``` 334 | 注意在函数bar()内无法访问变量c,因为它是在内部的baz()函数内声明的,同样的,函数foo()内也无法访问变量b。 335 | 336 | 如果试着访问一个在该作用域不能被访问的变量,会抛出`ReferenceError`错误。如果试着对一个未声明的变量赋值,在“严格模式”下会报错,但是非严格模式下则会在全局作用域创建这个变量(不好!)。我们看下这个例子: 337 | ```js 338 | function foo() { 339 | a = 1; // `a` not formally declared 340 | } 341 | 342 | foo(); 343 | a; // 1 -- oops, auto global variable :( 344 | ``` 345 | 这是一个典型的反面案例。千万不要这么做!记住永远正确地声明变量。 346 | 347 | 另外,ES6支持函数级的变量声明,用'let'关键字声明的变量只属于独立的代码块(花括号`{..}`)内。除了一些细节不一样之外,这种作用域的规则基本上与函数中的表现一致: 348 | ```js 349 | function foo() { 350 | var a = 1; 351 | 352 | if (a >= 1) { 353 | let b = 2; 354 | 355 | while (b < 5) { 356 | let c = b * 2; 357 | b++; 358 | 359 | console.log( a + c ); 360 | } 361 | } 362 | } 363 | 364 | foo(); 365 | // 5 7 9 366 | ``` 367 | 由于使用let而不是var,变量b仅属于if语句而不属于整个foo()函数的作用域。同样的,c只属于while循环。块级作用域有助于更好更精细地管理变量作用域,从而简化代码的维护。 368 | 369 | **注:** 更多关于作用域的知识,参考 *作用域&闭包* 一书。关于let块级作用域的知识参考 *ES6&未来* 一书。 370 | 371 | ## 条件语句 372 | 373 | 除了我们在第一章介绍的if语句之外,JavaScript还支持其他几种条件机制,我们后面再讨论。 374 | 375 | 有时候,你可能会写一大串的`if...else...if`语句,像这样: 376 | ```js 377 | if (a == 2) { 378 | // do something 379 | } 380 | else if (a == 10) { 381 | // do another thing 382 | } 383 | else if (a == 42) { 384 | // do yet another thing 385 | } 386 | else { 387 | // fallback to here 388 | } 389 | ``` 390 | 这种写法没什么问题,但是显得很冗余,因为每个子句都需要做一次条件判断。这种情况下,可以用`switch`语句: 391 | ```js 392 | switch (a) { 393 | case 2: 394 | // do something 395 | break; 396 | case 10: 397 | // do another thing 398 | break; 399 | case 42: 400 | // do yet another thing 401 | break; 402 | default: 403 | // fallback to here 404 | } 405 | ``` 406 | 如果希望每个子句中的语句只执行一次,则必须加上`break`。如果子句中没有break语句,当执行这个子句后,会继续执行下一个子句里面的语句,而不管下一个子句是否匹配条件判断语句。这种现象成为“通过”,有时候是很有用且希望出现的。 407 | ```js 408 | switch (a) { 409 | case 2: 410 | case 10: 411 | // some cool stuff 412 | break; 413 | case 42: 414 | // other stuff 415 | break; 416 | default: 417 | // fallback 418 | } 419 | ``` 420 | 这里,不管a等于2还是10,都会执行"some cool stuff"代码语句。 421 | 422 | JavaScript中另一个条件语句是被称为“三元操作符”的“条件操作符”。它更像是单个`if...else`语句的简写,如: 423 | ```js 424 | var a = 42; 425 | 426 | var b = (a > 41) ? "hello" : "world"; 427 | 428 | // 等同于: 429 | 430 | // if (a > 41) { 431 | // b = "hello"; 432 | // } 433 | // else { 434 | // b = "world"; 435 | // } 436 | ``` 437 | 如果测试表达式(这里是`a>41`)结果为true,结果是第一个子句("hello"),否则结果就是第二个子句("world")。然后再将结果赋值给变量b。 438 | 439 | 条件操作符不一定要用于赋值语句,但这无疑是它最常见的用法。 440 | 441 | ## 严格模式 442 | 443 | ES5中为JS加入了“严格模式”,收紧了某些特定行为的规则。一般来说,这些限制可以使代码更安全、更符合规范的要求。当然,遵循严格模式会让你的代码最大化利用引擎。严格模式对代码有重要意义,你应该在你的所有程序中使用它。 444 | 445 | 你可以选择对单个函数或整个文件使用严格模式,这取决于严格模式标识所在的位置: 446 | ```js 447 | function foo() { 448 | "use strict"; 449 | 450 | // this code is strict mode 451 | 452 | function bar() { 453 | // this code is strict mode 454 | } 455 | } 456 | // this code is not strict mode 457 | ``` 458 | 另外一种情况是: 459 | ```js 460 | "use strict"; 461 | 462 | function foo() { 463 | // this code is strict mode 464 | 465 | function bar() { 466 | // this code is strict mode 467 | } 468 | } 469 | // this code is strict mode 470 | ``` 471 | 严格模式的一个主要不同(改进!)是在忽略var时隐式地将变量声明为全局变量: 472 | ```js 473 | function foo() { 474 | "use strict"; // turn on strict mode 475 | a = 1; // `var` missing, ReferenceError 476 | } 477 | 478 | foo(); 479 | ``` 480 | 如果代码中开启了严格模式,上面的代码会报错,你的程序会出现bug,导致你会想避免使用严格模式。但是纵容这种想法是不应该的。如果因为使用严格模式导致你的程序出现bug,几乎可以肯定是你的程序本身的问题,应及时修复。 481 | 482 | 严格模式不仅可以保证代码更安全、更优,而且代表了JS语言的未来发展方向。与其抛弃严格模式,现在就开始习惯严格模式将会更容易——以后再转换只会更加困难! 483 | 484 | ## 函数作为值 485 | 486 | 目前为止我们讨论的函数是JavaScript中作用域的主要机制。通常通过下面的语法来声明`function`: 487 | ```js 488 | function foo() { 489 | // ... 490 | } 491 | ``` 492 | 从上面的语法中可能不是很明显,其实foo只是外层作用域中的一个变量,它是声明的`function`的一个引用。也就是说,这个`function`本身是一个像42或[1,2,3]一样的值。 493 | 494 | 这个概念可能一开始听起来比较奇怪,所以花点时间理解下吧。我们不仅可以给函数传递值(参数),而且 *函数本身就可以作为一个值* 赋给某个变量,传递给其他函数或其他函数返回。 495 | 496 | 因此,函数值应该被当作表达式,就像其他值或表达式一样。 497 | 498 | 例如: 499 | ```js 500 | var foo = function() { 501 | // .. 502 | }; 503 | 504 | var x = function bar(){ 505 | // .. 506 | }; 507 | ``` 508 | 第一个函数表达式将一个 *匿名函数* 赋给foo变量。第二个 *命名的*(bar)函数表达式作为一个引用赋给变量x。一般更喜欢用 *命名函数表达式*,尽管 *匿名函数表达式* 也很常见。 509 | 510 | 更多的细节参考 *作用域&闭包* 一书。 511 | 512 | ### 立即执行函数表达式(IIFEs) 513 | 514 | 在上一个例子中,两个函数都没有被执行——我们可以通过foo()或x()来调用。 515 | 516 | 还有另一种方式来执行函数表达式,通常称为 *立即执行函数表达式*(IIFE): 517 | ```js 518 | (function IIFE(){ 519 | console.log( "Hello!" ); 520 | })(); 521 | // "Hello!" 522 | ``` 523 | 函数表达式`(function IIFE(){ .. })`外面的`(..)`是为了与正常的函数声明区分开来的JS语法。表达式最后的`()`表示立即执行它前面的函数表达式。 524 | 525 | 这种写法看起来很奇怪,但也没有第一眼看起来那么陌生。这里注意foo函数和IIFE的区别: 526 | ```js 527 | function foo() { .. } 528 | 529 | // `foo` function reference expression, 530 | // then `()` executes it 531 | foo(); 532 | 533 | // `IIFE` function expression, 534 | // then `()` executes it 535 | (function IIFE(){ .. })(); 536 | ``` 537 | 可以发现,`(function IIFE(){ .. })`之后接`()`表示执行本质上与`foo`之后接`()`来执行是相同的;两种方式都是在函数引用之后接`()`来执行函数。 538 | 539 | 由于IIFE 也是函数,而函数会创建变量作用域,因此通常使用IIFE的方式来声明不会影响IIFE外面代码的变量: 540 | ```js 541 | var a = 42; 542 | 543 | (function IIFE(){ 544 | var a = 10; 545 | console.log( a ); // 10 546 | })(); 547 | 548 | console.log( a ); // 42 549 | ``` 550 | IIFE也可以有返回值: 551 | ```js 552 | var x = (function IIFE(){ 553 | return 42; 554 | })(); 555 | 556 | x; // 42 557 | ``` 558 | 命名的IIFE函数被执行时返回42,然后赋给变量x。 559 | 560 | ### 闭包 561 | 562 | *闭包* 是JavaScript中最重要也是最难理解的概念。我会在 *作用域&闭包* 书中详细阐述,这里不再赘述。但是我还是要简单介绍几点以便读者能有个基本概念。这会是你JS技能包里最重要的技术。 563 | 564 | 你可以把闭包想象成:函数结束运行之后,“保留”并继续访问函数作用域(它的变量)的一种方式。 565 | 例如: 566 | ```js 567 | function makeAdder(x) { 568 | // parameter `x` is an inner variable 569 | 570 | // inner function `add()` uses `x`, so 571 | // it has a "closure" over it 572 | function add(y) { 573 | return y + x; 574 | }; 575 | 576 | return add; 577 | } 578 | ``` 579 | 每次调用外层函数makeAdder()都返回内部的add()函数的引用,因此传递给makeAdder()函数的变量x的值会被保留。现在,我们来使用makeAdder()函数: 580 | ```js 581 | // `plusOne` gets a reference to the inner `add(..)` 582 | // function with closure over the `x` parameter of 583 | // the outer `makeAdder(..)` 584 | var plusOne = makeAdder( 1 ); 585 | 586 | // `plusTen` gets a reference to the inner `add(..)` 587 | // function with closure over the `x` parameter of 588 | // the outer `makeAdder(..)` 589 | var plusTen = makeAdder( 10 ); 590 | 591 | plusOne( 3 ); // 4 <-- 1 + 3 592 | plusOne( 41 ); // 42 <-- 1 + 41 593 | 594 | plusTen( 13 ); // 23 <-- 10 + 13 595 | ``` 596 | 这里解释一下上面的代码是如何运行的: 597 | 1. 当执行makeAdder(1)时,返回内部函数add()的引用,该函数会保存变量x值为1。这个函数引用名字为plusOne()。 598 | 2. 当执行makeAdder(10)时,返回内部函数add()的另一个引用,该函数会保存变量x值为10。这个函数引用名字为plusTen()。 599 | 3. 当执行plusOne(3)时,将3(内部的y)加上1(x中保存的值),就得到结果4。 600 | 4. 当执行plusTen(13)时,将13(内部的y)加上10(x中保存的值),就得到结果为23。 601 | 如果开始被这个绕晕了,别担心——这是正常的!需要多加练习才能完全理解闭包。 602 | 603 | 相信我,一旦掌握了闭包,它就是以后编程中最强大最有用的技术。绝对值得花精力让你的大脑熟悉闭包的概念。我们会在下一节中学习关于闭包的更多实践。 604 | 605 | #### 模块 606 | JavaScript中闭包最常见的用法是模块模式。模块允许你定义对外部不可见的私有实现细节(变量、函数),外部通过公开的API来访问模块。 607 | 如: 608 | ```js 609 | function User(){ 610 | var username, password; 611 | 612 | function doLogin(user,pw) { 613 | username = user; 614 | password = pw; 615 | 616 | // do the rest of the login work 617 | } 618 | 619 | var publicAPI = { 620 | login: doLogin 621 | }; 622 | 623 | return publicAPI; 624 | } 625 | 626 | // create a `User` module instance 627 | var fred = User(); 628 | 629 | fred.login( "fred", "12Battery34!" ); 630 | ``` 631 | User()函数是一个保存了变量username和password及内部函数doLogin()的外部作用域;这些都是Usr模块私有的内部细节,在外部不能被访问到。 632 | 633 | **注:** 这里我们有意不用new User(),虽然这种方式更常见。User()只是一个函数,而不是一个需要实例化的类,所以只需正常调用即可。这里不适合用new,实际上用了还会浪费资源。 634 | 635 | 执行User()会创建一个User()模块的 *实例*——创建一个完整的新作用域,也即内部每个变量/函数的完整新副本。然后把这个实例赋给fred变量。如果我们再次执行User(),我们会得到一个与fred完全独立的新实例。 636 | 637 | 内部的doLogin()函数对username和password有闭包引用,这意味着即使User()函数结束运行后,仍能访问到它们。 638 | 639 | publicAPI是一个具有一个login属性/方法的对象,这个login是对内部的doLogin()函数的引用。当我们从User()返回publicAPI时,它实例fred。 640 | 641 | 此时,外层函数User()已经结束执行。按理说,内部变量username和password应该被销毁了。但是这里没有,因为login()函数内的闭包使它们得以保留。 642 | 643 | 这就是为什么我们调用fred.login(..)时——与调用内部doLogin()相同——仍能访问到内部变量username和password。 644 | 645 | 这个例子可以很好的帮助读者了解闭包和模块模式,可能还有一些不好理解,没关系,你的大脑需要花点时间来接受它! 646 | 647 | 阅读 *作用域&闭包* 一书进行更深入的探索吧。 648 | 649 | ## `this`关键字 650 | 651 | JavaScript中另一个容易被误解的概念是`this`关键字。 652 | 653 | 尽管this可能通常与“面向对象模式”相关,但是JS中的this具有不同的机制。 654 | 655 | 如果一个函数内部有this引用,那么这个this引用实际上指向一个对象,而具体指向哪个对象取决于这个函数被调用的方式。 656 | 657 | 需要记住的是这个this不是指向函数本身,而这是最常见的误解。 658 | 659 | 这里有一个快速的说明: 660 | ```js 661 | function foo() { 662 | console.log( this.bar ); 663 | } 664 | 665 | var bar = "global"; 666 | 667 | var obj1 = { 668 | bar: "obj1", 669 | foo: foo 670 | }; 671 | 672 | var obj2 = { 673 | bar: "obj2" 674 | }; 675 | 676 | // -------- 677 | 678 | foo(); // "global" 679 | obj1.foo(); // "obj1" 680 | foo.call( obj2 ); // "obj2" 681 | new foo(); // undefined 682 | ``` 683 | 下面四条规则解释了如何确定this的指向,正好对应上面代码段中的最后四行。 684 | 1. 在非严格模式下,foo()最后的this指向全局对象——而严格模式下,this是undefined,访问bar属性是会报错——所以this.bar的值为"global"。 685 | 2. obj1.foo()中的this指向对象obj1。 686 | 3. foo.call(obj2)中的this指向对象obj2。 687 | 4. new foo()中的this指向一个全新的空对象。 688 | 结论:要理解this实际指向什么,需要弄清楚this所在函数是被怎么样调用的。调用方式是上面四种方式之一,然后就可以确定this的具体指向了。 689 | 690 | **注:** 更多关于this的知识,参考 *this&对象原型* 一书的第一章和第二章。 691 | 692 | ## 原型 693 | 694 | JavaScript中的原型机制相当复杂。我们这里只是简单了解下。你需要花大量时间阅读 *this&对象原型* 一书的第4-6章来学习详细知识。 695 | 696 | 当引用对象上的某个属性时,如果这个属性不存在,JavaScript会根据这个对象内部的原型引用自动到另一个对象上去查找这个属性。可以认为这是属性不存在时的一个回溯。 697 | 698 | 对象内部的原型引用是在这个对象被创建时建立的与它的回溯的连接。JS内置的方法`Object.create(..)`阐明了这个过程。 699 | 考虑: 700 | ```js 701 | var foo = { 702 | a: 42 703 | }; 704 | 705 | // create `bar` and link it to `foo` 706 | var bar = Object.create( foo ); 707 | 708 | bar.b = "hello world"; 709 | 710 | bar.b; // "hello world" 711 | bar.a; // 42 <-- delegated to `foo` 712 | ``` 713 | 下面的图表示了foo与bar之间的关系: 714 | 715 | ![](/assets/fig3.png) 716 | 717 | 实际上bar对象上不存在属性a,但是因为bar与foo之间有原型链,JavaScript会自动回溯到对象foo上寻找属性a。 718 | 719 | 这种链接似乎是一个很奇怪的特性。这个特性用的最多的场景是——我会说,滥用——用于模拟/伪造具有“继承”特性的“类”机制。 720 | 721 | 另一种应用原型的更自然的方式是“行为委托”模式,在这种模式中,需要特意设计连接对象来从一个对象委托部分需要的行为到另一个对象。 722 | 723 | **注:** 更多关于原型和行为委托的知识,参考 *this&对象原型* 一书的第4-6章。 724 | 725 | ## 旧&新 726 | 727 | 我们已经了解了一些,当然后面我们会了解更多,新引入的JS特性,这些特性在老旧浏览器中不一定被支持。实际上,规范中的一些最新的特性即使是在最稳定的浏览器中都没有被支持。 728 | 729 | 所以,我们该怎么处理新特性?我们就干等几年或几十年,等着所有的老旧浏览器退出历史舞台吗?很多人确实是这么对待这个问题的,但是这真的不是一个对JS有益的方法。 730 | 731 | 目前主要有两种技术将较新的JavaScript特性“带入”老旧浏览器中:polyfilling和转译(transpiling) 732 | 733 | ### Polyfilling 734 | [术语"polyfill"](https://remysharp.com/2010/10/08/what-is-a-polyfill)是Remy Sharp发明的,表示根据较新特性的定义写一段可以在较老的JS环境中运行的代码来实现相同的行为。 735 | 736 | 例如,ES6定义了一个叫做`Number.isNaN(..)`的方法,用来准确无误地检查NaN值,而摒弃了原来的`isNaN(..)`方法。但是这个方法很容易被polyfill,因此你可以在你的代码中使用这个方法,而不用管用户的浏览器是否支持ES6. 737 | 例如: 738 | ```js 739 | if (!Number.isNaN) { 740 | Number.isNaN = function isNaN(x) { 741 | return x !== x; 742 | }; 743 | } 744 | ``` 745 | if语句表示在支持ES6的浏览器中不应用polyfill。如果这个方法还不存在,就定义这个`Number.isNaN(..)`方法。 746 | 747 | **注:** 这里我们做的检查利用了NaN值的怪癖:NaN是JS中唯一不等于本身的值。所以NaN值是唯一使得 x != x结果为true的值。 748 | 749 | 不是所有的新特性都可以被polyfill。虽然大部分行为都可以被polyfill,但是会有一些偏差。因此你自己在实现polyfill的时候,应该要十分小心,尽可能确保严格遵守规范。 750 | 751 | 最好是使用已经被检验过的可信的polyfill,如[ES5-Shim](https://github.com/es-shims/es5-shim)和[ES6-Shim](https://github.com/es-shims/es6-shim)。 752 | 753 | ### 转译 754 | 如果JS中新增的语法不能被polyfill,那么在老旧JS引擎中会因为不能被识别/非法而抛出错误。 755 | 756 | 因此通过工具将较新的代码转换为等价的较老的代码是更好的选择。这个过程通常称为术语“转译”,表示转换+编译。 757 | 758 | 本质上,你的代码是用新语法格式编写的,但是部署到浏览器中的转译后的旧语法格式的代码。一般把转译器放在构建过程中,跟代码检测和压缩类似。 759 | 760 | 你可能会想为什么要费劲用新语法格式编写代码然后又转译为旧语法格式,为什么不直接按旧代码格式编写呢? 761 | 762 | 有几个重要的原因可以回答这个问题: 763 | * JS中新增的语法被设计出来是为了提高程序的可读性和可维护性。其相应的旧语法通常更复杂。为了自己也为了团队中的其他成员,都应该使用更新更简洁的语法。 764 | * 如果转译仅是为了兼容旧浏览器,而对新浏览器使用新语法,那么新语法就可以充分利用浏览器的性能优化。这也可以帮助浏览器生产商用实际的代码来测试浏览器的功能和性能优化。 765 | * 更早使用新语法可以根据实际情况来测试新语法的稳定性,这也更早地提供反馈给JavaScript委员会(TC39)。如果能尽早发现问题,就可以在这些语言设计的错误变成遗留问题之前改变/修复它们。 766 | 767 | 这里有一个转译的简单例子,ES6新增了“默认参数值”的特性,它的用法如下: 768 | ```js 769 | function foo(a = 2) { 770 | console.log( a ); 771 | } 772 | 773 | foo(); // 2 774 | foo( 42 ); // 42 775 | ``` 776 | 很简单,对吧?也很有用!但是这个新语法在ES6之前的引擎中是非法的。那么转译器怎样将这段代码转译成可以在旧环境中运行的代码呢? 777 | ```js 778 | function foo() { 779 | var a = arguments[0] !== (void 0) ? arguments[0] : 2; 780 | console.log( a ); 781 | } 782 | ``` 783 | 如你所见,先检查`arguments[0]`的值是否是`void 0`(也即undefined),如果是,则提供默认值2;如果不是,则将传入的值赋给a。 784 | 785 | 除了可以在旧浏览器中可以使用更简洁的语法之外,转译后的代码实际上可以更好地解释程序的逻辑。 786 | 787 | 仅从ES6的这个例子中你可能不会发现,undefined是唯一一个不能作为默认参数显示传递的值,但是经转译的代码可以使这个问题看得更清晰。 788 | 789 | 关于转译器最后一个需要强调的重要的细节是应该将转译作为JS开发生态系统和开发过程中的标准部分。JS是不断发展的,而且比以前快得多,因此每隔几个月就有新增一些新特性和新语法。如果默认使用转译器的话,就可以在新语法可用时随时可以用它,而不是等几年之后不支持的浏览器被淘汰之后再使用。 790 | 791 | 现在已有一些优秀的转译器可以选择: 792 | * [Babel](https://babeljs.io/):将ES6转译成ES5 793 | * [Traceur](https://github.com/google/traceur-compiler):将ES6/ES7及更高的版本转译成ES5 794 | 795 | ## 非-JavaScript 796 | 797 | 到目前为止,我们了解都是JS语言本身。现实中,大多数JS都是运行在浏览器环境并与浏览器进行交互。严格来说,你在代码中写的很酷炫的东西并不是直接受JavaScript控制。这可能听起来有点奇怪。 798 | 799 | 你将遇到的最常见的非-JavaScript的JavaScript是DOM API。例如: 800 | ```js 801 | var el = document.getElementById( "foo" ); 802 | ``` 803 | 变量`document`是你的代码在浏览器中运行时才存在的一个全局变量。它不是由JS引擎提供,也不在JavaScript规范的范围内。它的形式比正常JS对象复杂的多,但它也不完全是一个对象,它是一个特殊的对象,称为“宿主对象”。 804 | 805 | 另外,document上的`getElementById(..)`方法看起来像一个普通的JS函数,实际上它是浏览器DOM提供的内置方法的对外接口。在有些浏览器(新一代)中,这一层的函数可能也包括在JS中,但是一般DOM及其方法用C/C++来实现。 806 | 807 | 另一个非-JavaScript的例子是输入/输出(I/O)。 808 | 809 | 大家都喜欢用`alert(..)`在用户浏览器中弹出消息框。`alert(..)`是由浏览器提供给JS程序的,而不是JS引擎提供的。调用这个函数时,将消息传递给浏览器内核,然后内核来处理并显示消息框。 810 | 811 | 类似的函数还有`console.log(..)`;浏览器提供这些接口,并作为开发者工具钩子传递信息。 812 | 813 | 本书及本系列书只讨论JavaScript语言。因此读者不会看到对这些非-JavaScript的JavaScript机制的讨论。无论如何,你需要了解它们,因为它们会出现在你写的每个JS程序中! 814 | 815 | ## 复习 816 | 817 | 学习JavaScript风格编程的第一步就是基本理解JavaScript的核心机制,如值、类型、函数闭包、`this`和原型。 818 | 819 | 当然,这些主题都需要花比这里多得多的笔墨来阐述,因此本系列剩下的部分都有相应的章节和书来阐述它们。当你熟悉了本章中的这些概念和代码示例之后,就可以真正深入学习后续的部分并完全掌握这门语言。 820 | 821 | 本书的最后一章将简要总结本系列书籍每一本的主要内容以及除我们以及提及的概念之外的其他概念。 -------------------------------------------------------------------------------- /part1/ch3.md: -------------------------------------------------------------------------------- 1 | # 第三章:深入YDKJS 2 | 3 | 本系列书籍是讲什么的?简单的说,本系列书籍的目的是认真学习 *JavaScript的所有部分*,而不是这门语言的一部分而已,如有些人所谓的“精粹部分”,也不仅仅是完成工作所需掌握的最少知识点。 4 | 5 | 其他语言的优秀开发者都希望投入精力去学习它们主要使用的语言的大部分或所有部分内容,但是JS开发者似乎与众不同地不愿意学号这门语言。这不是一个好的现象,我们也不应该让这继续成为常态。 6 | 7 | *You Don't Know JS (YDKJS)* 系列与你阅读的大部分传统学习JS的教材形成鲜明对比。它让你超越你能理解的部分,对你遇到的每一个问题都深入得询问“为什么”。你准备好接受挑战了吗? 8 | 9 | 我将用这最后一章简要概括本系列书籍你能学到的内容,以及在学习YDKJS的基础上如何最有效地夯实JS基础。 10 | 11 | ## 作用域&闭包 12 | 13 | 你需要快速掌握的最基础的知识可能就是JavaScript中的变量作用域是怎么工作的。对作用域一知半解是远远不够的。 14 | 15 | *作用域&闭包* 一书首先纠正一个常见的误解:JS是“解释型语言”因此不需要编译。大错特错。 16 | 17 | JS引擎会在将要执行之前(而不是执行期间)编译代码。所以我们会更深入解释编译器编译代码的方式,理解它是如何找到并处理变量和函数声明的。学习过程中,我们会掌握JS变量作用域管理中的典型比喻——“提升”。 18 | 19 | 理解了“词法作用域”之后,我们会在其基础上在最后一张探索闭包的知识。闭包可能是JS所有概念中最重要的一个,但是如果你没有先熟练掌握作用域,也不可能很好的掌握闭包。 20 | 21 | 闭包的一个重要应用是模块模式。模块模式可能是JS中最流行的代码组织模式,因此你也应该优先掌握她。 22 | 23 | ## this&对象原型 24 | 25 | 可能关于JavaScript最流行的也是被误解最多的就是函数中`this`关键字的指向问题。彻底被错误理解! 26 | 27 | this关键字基于所在函数被执行的方式而动态改变指向,大概有四种简单的规则来理解并确定this的指向。 28 | 29 | 与this关键字紧密相关的是对象原型机制,它是一个属性的查找链,与词法作用域链类似。但是对原型的理解存在另一个对JS的巨大误解:模拟(伪造)类和继承的思想。 30 | 31 | 不幸的是,将类和继承的设计模型思想引入JavaScript是你尝试做得最坏的事情之一,因为虽然这种语法可能会让你误认为有类的存在,但是实际上原型链机制与类的行为基本上完全相反。 32 | 33 | 问题是是否忽略这种不匹配并当作在实现“继承”更好,还是学习并掌握对象原型的实际工作原理更合适。后者称为“行为委托”更合适。 34 | 35 | 这不仅仅是对于语法的偏好问题。委托是完全不同且更强大的设计模式,它可以替代类和继承的设计。这些结论绝对会出现在跟完整的JavaScript生命周期主题相关的其他博客文章、书籍以及会议演讲里面。 36 | 37 | 我的结论——委托比继承更好——不是因为不喜欢这门语言及其语法,而是希望这门语言的能力能够被正确地衡量,希望冲散无止尽的不解与困惑。 38 | 39 | 但是我认为原型和委托是一个比我在这里阐述的高深的多的主题。如果你准备重塑你对JS的“类”和“继承”的所有认识,希望你能仔细阅读 *this&对象原型* 一书的第4-6章。 40 | 41 | ## 类型&语法 42 | 43 | 本系列的第三部分主要讨论另一个充满争议性的话题:类型强制转换。可能没有什么主题能比隐式类型转换给JS开发者带来更多的困扰和挫败感。 44 | 45 | 到目前为止,普遍的看法是隐式类型转换是这门语言的“糟粕”应不惜代价避免使用。实际上,更有人认为这是JS设计上的一个“错误”。的确,现在有一些工具专门用来检查你的代码是否使用了像强制类型转换这样不好的语法。 46 | 47 | 但是强制类型转换真的这么困惑、这么不好、这么危险,以致于一旦用了它你的代码就注定有问题吗? 48 | 49 | 我的答案是:不是!第1-3章阐述类型和值的实际工作原理,第4章将主要讨论这个争论,完整地解释强制类型转换的原理。我们会明白强制类型转换的哪些部分确实是不可控的,哪些部分实际上是很有意义的。 50 | 51 | 但是我不只是认为强制类型转换是合理且值得掌握,我也认为它是非常有用且是一个应该在代码中使用的被低估的工具。我认为,如果使用得当,强制类型转换不仅有用而且会使代码更好。所有的反对者和怀疑者肯定会反对这种立场,但是我相信这是提升你的JS技能的关键之一。 52 | 53 | 你是想继续随大众所想,还是将所有假设放一边,以全新的视角来看待强制类型转换呢? *类型&语法* 会改变你的想法的。 54 | 55 | ## 异步&性能 56 | 57 | 本系列的前三部分主要讨论JS语言的核心机制,而第四部分在语言机制的之上讨论异步编程模式。异步不仅是提升应用性能的关键,也逐渐成为提升可写性和可维护性的关键因素。 58 | 59 | 本书首先梳理了一些易混淆的术语和概念,如“异步”、“平行”和“并发”,并详细解释了这些东西为什么应用或为什么不应用与JS。 60 | 61 | 随后,我们探讨了异步的主要实现方式——回调。但是我们很快就会发现单独的回调不能完全满足现代异步编程的要求。我们指出了纯回调编程的两点不足:*控制翻转*(IoC)信任损失和缺失线性理论。 62 | 63 | 为了弥补这两点不足,ES6引入了两种新机制(模式):promises和generators。 64 | 65 | Promises是一个与时间无关的用来包装“将来值”的包装器,它允许你在不知道值是否已经准备好的情况下推导并组合代码。另外,通过受信任且可组合的promise机制对回调进行路由控制,有效地解决了IoC信任问题。 66 | 67 | Generators引入一个执行JS函数的新模式,由此,generator可以在yield点暂停,然后在异步地恢复执行。这种暂停-恢复的能力使异步成为可能,根据不同的场景异步执行generator中顺序排列的代码。通过这种方式,可以解决回调中的非线性、非本地跳转的问题,因而是我们的异步代码看起来更像同步,从而看起来更合理。 68 | 69 | 但是在JavaScript中组合使用promises和generators才能使我们的异步编程模式发挥最大的效率。实际上,ES7及以后将引入的异步的大部分复杂性就是基于此。想要在异步的世界中高效地编程,你需要熟练的掌握promises和generators。 70 | 71 | promises和generators模式讨论的如何让我们的程序运行更多地并发,从而在短时间内完成更多的操作。JS还有其他很多性能优化方面值得我们去探索。 72 | 73 | 第5章深入探讨的主题有:Web Workers的程序并发性、SIMD数据并行性、ASM.JS底层优化技术等。第6章从基准技术角度探讨性能优化,包括哪些性能需要考虑而哪些可以忽略。 74 | 75 | 编写高效的JavaScript意味着编写的代码能够在广泛的浏览器和环境中动态运行。这需要大量的复杂而详细的规划,努力让程序从“能运行”变成“很好地运行”。 76 | 77 | *异步&性能* 帮助你掌握编写合理且高性能的JavaScript代码的所有工具和技能。 78 | 79 | ## ES6&未来 80 | 81 | 不管此时你认为自己对JavaScript掌握的又多好,事实是JavaScript总是在不停的发展,而且发展的速度非常迅速。这个事实也是本系列书籍的精神所在,去拥抱我们永远不可能完全掌握的JS的每一部分,因为当你掌握了所有部分时,又会有新的东西需要去学习。 82 | 83 | 这一部分主要讨论这门语言的短期和中期发展愿景,不仅有大家已知的东西,如ES6,也有未来可能加入的东西。 84 | 85 | 虽然本系列所有书都包含了编写时的JavaScript的最新状态,也即ES6半采纳状态,本系列主要聚焦于ES5的讨论。现在,我们需要把我们的注意力转移到ES6,ES7... 86 | 87 | 由于本书编写时ES6已经接近完成,本部分首先将ES6的具体内容划分为几个主要类别,包括新语法、新数据结构(集合)、新处理能力和新API等。我们会从不同的细节水平讨论每一个新ES6特性,也会回顾在本系列其他部分涉及的细节。 88 | 89 | ES6中一些令人兴奋的值得阅读的内容包括:解构、默认参数值、symbols、方法简写、计算后的属性、箭头函数、块级作用域、promises、generators、迭代器、模块、代理、弱映射、等等等等! 90 | 91 | 本书的前一部分是一份关于你在接下来的几年中编写和探索的改进的JavaScript的所有内容的学习路线。 92 | 93 | 后一部分则简单介绍在不远的将来JavaScript中可能会加入的内容。这里最重要的实现是post-ES6,JS更像是一个特征一个特征,而不是一个版本一个版本向前发展,这意味着我们预期的不久的将来的内容会来得比我们想象中的快。 94 | 95 | JavaScript的未来一片光明,现在不正是我们开始学习它的好时机吗? -------------------------------------------------------------------------------- /part2/README.md: -------------------------------------------------------------------------------- 1 | # 你不了解的JS: 作用域&闭包 2 | 3 | --- 4 | 5 | ![](/assets/cover2.jpg) 6 | 7 | --- 8 | ### 目录 9 | * [第一章:什么是作用域?](ch1.md) 10 | * 编译器理论 11 | * 理解作用域 12 | * 嵌套作用域 13 | * 报错 14 | * [第二章:词法作用域](ch2.md) 15 | * 词法分析 16 | * 欺骗词法 17 | * [第三章:函数 VS 块级作用域](ch3.md) 18 | * 函数的作用域 19 | * 从普通作用域中隐藏 20 | * 函数作用域 21 | * 块级作用域 22 | * [第四章:变量提升](ch4.md) 23 | * [第五章:作用域闭包](ch5.md) 24 | * 闭包的实质 25 | * 其他形式的闭包 26 | * 循环+闭包 27 | * 模块 28 | * [附录A:动态作用域](apdA.md) 29 | * [附录B:词法作用域中的this](apdB.md) -------------------------------------------------------------------------------- /part2/apdA.md: -------------------------------------------------------------------------------- 1 | # 附录A:动态作用域 2 | 3 | 动态作用域是在代码运行时(而非编写时)动态确定作用域的模型。 4 | 先看下面的代码: 5 | ```js 6 | function foo() { 7 | console.log( a ); // 2 8 | } 9 | 10 | function bar() { 11 | var a = 3; 12 | foo(); 13 | } 14 | 15 | var a = 2; 16 | 17 | bar(); 18 | ``` 19 | 根据词法作用域规则,foo函数引用的变量a是全局变量a,所以输出结果是2。 20 | 21 | 动态作用域不考虑函数和作用域如何定义、定义在何处,而是强调他们 **从哪里被调用**。也就是说,**动态作用域的作用域链基于调用堆栈而非代码中作用域的嵌套关系**。 22 | 23 | 因此,如果JavaScript具有动态作用域,当foo()被执行时,理论上上面的代码会输出3,而不是2。这是因为当foo()找不到变量a时,不是根据词法作用域规则去外一层作用域查找,而是沿着调用栈查找,去foo函数被调用的地方查找。而foo函数是在bar函数中被调用,因此会检查bar的作用域是否有变量a,最后放回3。 24 | 25 | JavaScript本身没有动态作用域,但是有词法作用域。但是JavaScript可以通过`this`机制实现动态作用域。 26 | 27 | **关键点**:词法作用域是编写代码时就确定的,而动态作用域(以及this)是在运行时才确定的。词法作用域关心的是函数被声明的位置,而动态作用域关心的是函数被谁调用。 28 | -------------------------------------------------------------------------------- /part2/apdB.md: -------------------------------------------------------------------------------- 1 | # 附录B:词法作用域的this 2 | 3 | ES6中引入了一个新的函数语法:箭头函数。先看下面的代码: 4 | ```js 5 | var obj = { 6 | id: "awesome", 7 | cool: function coolFn() { 8 | console.log( this.id ); 9 | } 10 | }; 11 | 12 | var id = "not awesome"; 13 | 14 | obj.cool(); // awesome 15 | 16 | setTimeout( obj.cool, 100 ); // not awesome 17 | ``` 18 | 这里的问题是cool()函数的this没有绑定到对象obj上。有很多解决这个问题的办法,其中最常用的是`var self = this;`。如下: 19 | ```js 20 | var obj = { 21 | count: 0, 22 | cool: function coolFn() { 23 | var self = this; 24 | 25 | if (self.count < 1) { 26 | setTimeout( function timer(){ 27 | self.count++; 28 | console.log( "awesome?" ); 29 | }, 100 ); 30 | } 31 | } 32 | }; 33 | 34 | obj.cool(); // awesome? 35 | ``` 36 | 这种方式其实是通过词法作用域来实现的:self只不过是一个可以被词法作用域和闭包解析的标识符,保存了this的指向,但是它并不知道this到底发生了什么。 37 | 38 | ES6的解决方案——箭头函数,引入了一种称为“词法this”的行为: 39 | ```js 40 | var obj = { 41 | count: 0, 42 | cool: function coolFn() { 43 | if (this.count < 1) { 44 | setTimeout( () => { // arrow-function ftw? 45 | this.count++; 46 | console.log( "awesome?" ); 47 | }, 100 ); 48 | } 49 | } 50 | }; 51 | 52 | obj.cool(); // awesome? 53 | ``` 54 | 与普通函数相比,箭头函数对this的处理不一样。箭头函数会忽视this绑定的所有正常规则,而是永远将this值设置为当前函数被包裹的词法作用域。因此这个例子中,箭头函数的this直接”继承”自cool()函数的this。 -------------------------------------------------------------------------------- /part2/ch1.md: -------------------------------------------------------------------------------- 1 | # 第一章:什么是作用域? 2 | 3 | 几乎所有编程语言的最基本范式之一都是将值存储于变量中,并在以后可以访问或修改这些值。实际上,正是这种对变量存取值的能力才使得程序具有 *状态*。 4 | 5 | 当然,即使没有这个概念,程序也可以执行某些任务,但是会受到极大的限制,也会失去很多乐趣。 6 | 7 | 在程序中引入变量带来了一系列我们现在要讨论的有趣问题:这些变量 *存在* 于哪里?换言之,它们保存在哪里?最重要的是,当我们的程序需要用它们时该如何找到它们? 8 | 9 | 这些问题说明我们需要一套完善的规则来规定变量的存储位置以及需要时如何找到这些变量。我们把这套规则称为:*作用域*。 10 | 11 | 但是,我们在何处以及如何设置这些作用域规则呢? 12 | 13 | ## 编译器理论 14 | 15 | 尽管JavaScript通常被归类为“动态”或“解释型”语言,但是实际上它是一门编译型语言。读者可能觉得这是不言而喻的,也可能感到很惊讶,这取决于你对各类语言的掌握程度。与许多传统编译型语言和各种分布式系统中跨平台编译结果不同的是,JavaScript不会预先编译好。 16 | 17 | 虽然与其他传统语言编译器相比,JavaScript引擎以比我们通常了解的更复杂的方式执行代码,但是它们执行的很多步骤是一样的。 18 | 19 | 在传统的语言编译过程中,你的程序,一大堆源代码,在被执行前会经历三个主要步骤,大致称为“编译”: 20 | 21 | 1. **标记化/分词**:将一串字符串拆分成(对这门语言来说)有意义的小块,称为标记。例如,对于程序:`var a=2;`,可能会被拆分成下列标记:`var`, `a`, `=`, `2`和`;`。如果标记中的空白有意义,则会保留,否则就会被去掉。 22 | 23 | **注:** 标记化(Tokenizing)和分词(Lexing)的区别微乎其微,主要区别是这些标记是以 *无状态* 方式还是以 *有状态* 方式被识别。简而言之,如果分词器根据有状态解析规则来判断`a`应该是一个单独的标记,还是仅仅是另一个标记的一部分,那么这种方式就是 __分词__。 24 | 2. **解析**:将标记流(数组)转换为代表程序语法结构的嵌套元素树。这棵树称为“AST”(Abstract Syntax Tree 抽象语法树)。 25 | 26 | `var a=2;`的AST的顶层节点称为`VariableDeclaration`,它有两个子节点:一个是`Identifier`(值为a);另一个是`AssignmentExpression`,它又有一个子节点称为`NumbericLiteral`(值为2)。 27 | 3. **代码生成**:将AST转换为可执行代码的过程。该步骤根据不同语言、不同目标平台而有很大不同。 28 | 29 | 因此,这里我们不具体展开细节,只需要知道通过某种方法将上文所述的“var a=2;”的AST转换成了一组机器指令,这组指令才实际上创建了一个变量a(包括分配内存等),然后在a中保存了一个值。 30 | 31 | **注**:引擎是如何管理系统资源的知识超出了我们的学习范畴,因此我们只需知道引擎能够创建和保存变量即可。 32 | 33 | 和大多数其他语言编译器一样,JavaScript引擎的工作比上述的三个步骤复杂的多。例如,在解析和代码生成阶段,肯定需要有消除冗余元素等优化执行性能的步骤。 34 | 35 | 因此,这里我只简略提及,但是我相信读者很快就会明白我们为什么要从一个相对高的层面介绍这些细节。 36 | 37 | 还有一点需要注意,JavaScript引擎没有多余的时间(其他语言编译器则有)来做优化,因为JavaScript编译不像其他语言一样提前在构建时编译。 38 | 39 | 对JavaScript来说,大多数情况下编译发生在代码被执行前几微秒(或更少)内。JS引擎使用了很多的技巧来保证最快的性能(如进行懒编译、甚至热重新编译的JITs等),这些技巧超出了我们讨论的“作用域”的范畴。 40 | 41 | 简单来说,任何JavaScript代码片段都必须在它被执行之前(通常就在之前)被编译。因此,JS编译器处理程序时,会先对他进行编译,然后再准备执行它,通知是立马就执行。 42 | 43 | ## 理解作用域 44 | 45 | 我们把学习作用域的方法想像成交谈的过程,那么,由谁来进行交谈呢? 46 | 47 | ### 演员表 48 | 我们先来认识处理程序`var a = 2;`过程中的几个角色,以便我们理解马上将要听到的对话: 49 | 1. *引擎*:负责整个编译过程和执行JavaScript程序 50 | 2. *编译器*:引擎的朋友之一;处理解析和代码生成等脏活累活 51 | 3. *作用域*:引擎的另一个朋友;收集并维护由所有声明的标识符(变量)组成的查询表,对当前执行的代码如何访问这些变量强加一系列严格的规则 52 | 如果你想 *彻底理解* JavaScript是如何工作的,那么你需要从引擎的角度,想其所想,问其所问,答其所答。 53 | 54 | ### 来来回回 55 | 第一眼看到程序`var a = 2;`,你很可能认为它是一条语句。但是我们的新朋友引擎可不是这么认为的。实际上,引擎会把它当作两条不同的语句,其中一条编译器在编译时会处理,另一条引擎在执行时会处理。 56 | 57 | 现在我们分开来看引擎和它的朋友们是怎么处理程序`var a = 2;`的。 58 | 59 | 拿到这个程序后,编译器做的第一件事是分词,把程序拆分成各个标记,然后将它们解析成语法树。但是当编译器到代码生成阶段时,它处理这个程序的方式可能与我们设想的不一样。 60 | 61 | 一个合理的解释是编译器会综合这样的伪代码:“给一个变量分配内存,将其命名为a,然后把值2保存到这个变量中”来生成代码。很遗憾,这是不准确的。 62 | 63 | 其实,编译器会这样处理: 64 | 1. 遇到`var a`时,编译器询问作用域是否在特定的作用域范围中已经存在变量a。如果存在,编译器忽略这条声明语句并继续向前执行。否则,编译器要求作用域在其范围内声明一个新的变量a。 65 | 2. 然后编译器生成供引擎执行的代码来处理赋值语句`a = 2`。引擎运行的代码会先询问作用域在当前作用域范围内是否存在一个可被访问的变量a。如果存在,引擎就用这个变量;如果不存在,引擎会到别的地方去找(参见下文的嵌套作用域)。 66 | 如果引擎最终找到了这个变量,则把值2赋给这个变量。如果没有找到,引擎会举手并大喊出错了! 67 | 68 | 总结:变量赋值被当作两个不同的行为:首先,编译器声明一个变量(如果在当前作用域中还未被声明),然后在执行的时候,引擎在作用域中查找这个变量,如果找到便对其赋值。 69 | 70 | ### 编译器发言 71 | 为进一步理解编译器工作原理,我们需要了解更多相关术语。 72 | 73 | 当引擎在执行步骤(2)中编译器生成的代码时,它会去查询变量a是否已经被声明,而这个查找过程会咨询作用域。但是引擎执行的查找类型会影响查找结果。 74 | 75 | 在我们这个例子中,引擎会用“LHS”方式来查找变量a,另一种查找方式是“RHS”。 76 | 77 | 我想你应该能猜到“L”和“R”表示的意思。这两个属于表示“Left-hand Side”和“Right-hand Side”。当然,这里的左边和右边是 *相对赋值操作符来说的*。换言之,当变量出现在赋值操作的左边时进行LHS查找,当变量出现在赋值操作的右边时进行RHS查找。 78 | 79 | 实际上,更准确的表述应该是:RHS查找仅仅是查找某些变量的值,然而LHS查找会尝试找到变量容器本身,这样才能进行赋值操作。这样一来,RHS本身不真正表示“赋值的右边”,而仅仅表示,准确的说是,“非左边”。 80 | 81 | 我们说得更巧妙一点,可以认为“RHS”表示“检索他/她的源头(值)”,也就是说RHS表示“去获得...的值”。 82 | 83 | 我们在深入一点。 84 | 当我说: 85 | ```js 86 | console.log(a); 87 | ``` 88 | 对a的引用是一个RHS引用,因为这里没有任何东西赋给a。因此,我们通过查找取得a的值,然后将这个值传给`console.log(...)`。 89 | 作为对比: 90 | ```js 91 | a = 2; 92 | ``` 93 | 这里对a的引用是一个LHS引用,因为我们并不关心当前值是什么,我们只是想找到这个变量作为赋值操作`=2`的目标。 94 | 95 | **注**:LHS和RHS表示“赋值操作的左/右”,不一定是字面意思表示的“赋值操作符的左/右边”。赋值还有其他几种方式,因此最好从概念上区分:“谁是赋值操作的目标(LHS)”,而“谁是赋值操作的源(RHS)”。 96 | 97 | 考虑下面即有LHS又有RHS的程序: 98 | ```js 99 | function foo(a) { 100 | console.log( a ); // 2 101 | } 102 | 103 | foo( 2 ); 104 | ``` 105 | 最后一行把`foo(...)`作为函数调用进行调用时,要求一个对foo的RHS引用,表示,“去找到foo的值并给我”。另外,`(...)`表示foo的值要被执行,因此实际上它最好是一个函数! 106 | 107 | 这里有一个容易忽略但是很重要的赋值操作,你发现了吗? 108 | 109 | 你可能没有发现这段代码中隐式的`a=2`。值2作为参数传递给foo函数,在函数中值2被赋给参数a。(隐式地)给变量a赋值执行的是LHS查找。 110 | 111 | 上面的代码还对a进行了一次RHS引用,取得a的值并传递给console.log(...)。console.log(...)需要执行一个引用。执行RHS查找console对象,然后进行属性分析查找是否有一个叫做log的方法。 112 | 113 | 最后,我们从概念上理解,将值2传递(通过变量a的RHS查找)给log(...)会发生一次LHS/RHS交换。我们可以假设log(...)的原生实现中会接收参数,而在将2赋给第一个参数(可能称为arg1)时,会先进行一个LHS查找。 114 | 115 | **注**:从概念上,可以把函数声明`function foo(a) {...}`当作一个普通的变量声明和赋值,如`var foo`和`foo = function(a){...}`。这样的话,可以认为这个函数声明中包含一个LHS查找。然而,一个细微但是很重要的区别是,编译器在代码生成阶段会同时处理声明和值定义,所以当引擎执行代码时,无需将函数值赋给foo。因此将函数声明视为一个LHS查找赋值不是恰当的。 116 | 117 | ### 引擎/作用域 对话 118 | ```js 119 | function foo(a) { 120 | console.log( a ); // 2 121 | } 122 | 123 | foo( 2 ); 124 | ``` 125 | 我们把上述的交换(执行这个段代码)想象为一次对话。那么这次对话应该会是这样的: 126 | > ***引擎***:嘿 *作用域*, 我这里有一个`foo`的RHS引用,你听说过吗? 127 | 128 | > ***作用域***:当然听过。 *编译器* 刚刚声明了它。它是一个函数,给你。 129 | 130 | > ***引擎***:太好了,谢谢你! OK,我现在执行 `foo`. 131 | 132 | > ***引擎***:嘿, *作用域*, 我这里有一个`a`的LHS引用,你听说过吗? 133 | 134 | > ***作用域***:当然听过。 *编译器* 刚刚把他声明为`foo`的一个形参,给你。 135 | 136 | > ***引擎***:一如既往地乐于助人, *作用域*,再次谢谢你. 现在,是时候把`2`赋给`a`了. 137 | 138 | > ***引擎***:嘿, *作用域*, 抱歉再次打扰你。 我需要`console`的RHS查找。你听说过吗? 139 | 140 | > ***作用域***:没关系, *引擎*, 这是我的本职工作。我这里有 `console`,它是一个内置对象,给你。 141 | 142 | > ***引擎***:太好了。 找一下 `log(..)`。 OK, 太好了, 它是一个函数。 143 | 144 | > ***引擎***: 喲, *作用域*. 你能帮我RHS引用到`a`吗?我记得有它,只不过想确认下。 145 | 146 | > ***作用域***: 你是对的 *引擎*。跟刚才的是同一个家伙,给你。 147 | 148 | > ***引擎***:太酷了。 把 `a`的值`2`传递给`log(..)`。 149 | 150 | > ... 151 | 152 | ### 小测试 153 | 现在检查下到目前为止你是否完全理解了。一定要扮演引擎的角色与作用域进行交谈: 154 | ```js 155 | function foo(a) { 156 | var b = a; 157 | return a + b; 158 | } 159 | 160 | var c = foo( 2 ); 161 | ``` 162 | 1. 指出所有的LHS查找(有3个!) 163 | 2. 指出所有的RHS查找(有4个!) 164 | 165 | ## 嵌套作用域 166 | 167 | 我们说过 *作用域* 是一组通过标识符名字查找变量的规则。然而,通常需要考虑的作用域不止一个。 168 | 169 | 就如一个代码块或函数嵌套在另一个代码块或函数内,作用域也可以被嵌套在另一个作用域中。因此,如果在最近的作用域中找不到某个变量,引擎会在外一层的作用域中去查找,直到找到或达到最外层作用域(也即,global)。 170 | 171 | 考虑: 172 | ```js 173 | function foo(a) { 174 | console.log( a + b ); 175 | } 176 | 177 | var b = 2; 178 | 179 | foo( 2 ); // 4 180 | ``` 181 | 在函数foo中无法解析变量b的RHS引用,但是在外层的作用域(这里是全局作用域global)中能够被解析。 182 | 183 | 因此,再看一下引擎和作用域的对话,我们会偷听到: 184 | > ***引擎***:“嘿,foo函数的作用域,听说过b吗? 我这里有一个对它的RHS引用。” 185 | 186 | > ***作用域***: “没,没听过,去钓鱼吧。” 187 | 188 | > ***引擎***:“嘿,foo函数外层的作用域,噢,你是全局作用域。你听说过b吗?我这里有一个对它的RHS引用。” 189 | 190 | > ***作用域***:“有的,给你” 191 | 192 | 遍历嵌套作用域的简单规则:引擎首先在当前正在执行的作用域查找这个变量,如果没有找到的话,则往上一层查找,如此往复。如果到达了最外层的全局作用域,不管找到这个变量还是没有,查找都会停止。 193 | 194 | ### 比喻成高楼 195 | 为了形象展示嵌套作用域的解析过程,可以想象下面这栋高楼: 196 | 197 | 198 | 199 | 这栋楼表示程序的嵌套作用域。第一层表示当前正在执行的作用域,不管在哪里,顶层是全局作用域。我们在当前楼层查找来解析LHS和RHS引用,如果没有找到,就坐电梯到上一层去查找,然后再到上一层。一旦到达顶层(全局作用域),要么找到我们正在查找的东西,要么没有找到。但无论如何都要停止继续查找。 200 | 201 | ## 错误 202 | 203 | 为什么我们用LHS还是RHS查找会对结果有影响呢? 204 | 205 | 因为这两种类型的查找在变量还没有被声明的环境中(在任何作用域都没有找到)的行为机制不同。 206 | 例如: 207 | ```js 208 | function foo(a) { 209 | console.log( a + b ); 210 | b = a; 211 | } 212 | 213 | foo( 2 ); 214 | ``` 215 | 当第一次对b进行RHS查找时,将找不到它,也就是说它是一个“未被声明”的变量,因为在作用域中找不到它。 216 | 217 | 如果RHS查找在嵌套作用域中的任何地方都没有找到某个变量,引擎会抛出一个`ReferenceError`。重要的是要记住这个错误是`ReferenceError`类型。 218 | 219 | 相比之下,如果引擎执行LHS查找,到达顶层(全局作用域)时仍没找到某个变量,且程序不是在“严格模式”下执行的话,那么全局作用域就会在 *全局作用域* 中新建这个变量,然后把它返回给引擎。 220 | 221 | “我这里之前没有,但是我很乐意为你创建一个。” 222 | 223 | ES5中引入的“严格模式”与正常/松散/懒惰模式相比有很多不同的行为表现。其中一个就是它不允许自动/隐式创建全局变量。在我们这个例子中,没有全局的变量返回给LHS查找,引擎会抛出一个与RHS情况相同的错误`ReferenceError`。 224 | 225 | 现在,如果RHS查找找到了某个变量,但是如果你对这个值做一些它不能做的事情,如把一个非函数值作为函数执行,对null或undefined值引用其属性,这时候引擎会抛出另一种类型的错误“TypeError”。 226 | 227 | `ReferenceError`是作用域解析失败相关的错误,而`TypeError`表明作用域解析成功,但是尝试对解析结果进行非法/不可能的操作。 228 | 229 | ## 复习 230 | 231 | 作用域是一组规定变量(标识符)保存在哪里、如何查找到它的规则。通过这种查找可以给一个LHS(Left-hand-side)引用的变量赋值,也可以获取某个RHS(Right-hand-side)引用的变量的值。 232 | 233 | LHS引用来自赋值操作。作用域相关的赋值随着=操作符出现,也可以出现在给函数传参时。 234 | 235 | JavaScript引擎首先在代码执行前编译,在这个过程中,它把像`var a = 2;`这样的语句分成下面两个步骤: 236 | 1. 首先,通过`var a`在作用域中声明变量,这是在一开始就进行的,在代码执行之前; 237 | 2. 然后,`a = 2`会去查找这个变量(LHS引用),如果找到就给他赋值。 238 | 239 | LHS和RHS引用查询都是从当前执行的作用域开始查找,如果需要(即在这里没有找到需要的变量),则按照嵌套作用域的方式,一层一层向上查询,直到到达全局作用域,不管找没找到都停止查找。 240 | 241 | 未成功的RHS引用会抛出`ReferenceError`,未成功的LHS引用则会自动隐式的创建一个全局变量(非“严格模式”下),或抛出`ReferenceError`(严格模式下)。 -------------------------------------------------------------------------------- /part2/ch2.md: -------------------------------------------------------------------------------- 1 | # 第二章:词法作用域 2 | 3 | 目前主要有两种模型来解释作用域的工作原理:**词法作用域** 和 **动态作用域**。我们这里主要讨论JavaScript使用的词法作用域,附录A中对动态作用域进行了阐述。 4 | 5 | ## 词法分析 6 | 正如我们在第一章中讨论过的,标准的语言编译器的第一步一般都是词法分析,它正是理解词法作用域的基础。 7 | 8 | 词法作用域是在进行词法分析时定义的作用域。也就是说,词法作用域基于你编写代码时创建的变量和作用域块,因此在分词器执行你的代码时它是不可更改的。 9 | 10 | **注**:采取某些方法可以欺骗词法作用域,从而在分词之后还可以修改作用域,但是不提倡这么做。 11 | 12 | 考虑下面这段代码: 13 | ```js 14 | function foo(a) { 15 | var b = a * 2; 16 | function bar(c) { 17 | console.log( a, b, c ); 18 | } 19 | bar(b * 3); 20 | } 21 | foo( 2 ); // 2 4 12 22 | ``` 23 | 这段代码中包含了三层嵌套作用域。可以将其想象成三个依次包裹的气泡。 24 | 25 | 26 | **气泡1**:包含全局作用域,只有一个标识符:foo; 27 | 28 | **气泡2**:包含函数foo的作用域,有三个标识符:a、bar和b; 29 | 30 | **气泡3**:包含函数bar的作用域,只有一个标识符:c 31 | 32 | 作用域气泡在作用域块被书写的位置定义,一个嵌套在另一个内。气泡bar被完全包含在气泡foo内,因为这正是我们定义函数bar的位置。 33 | 34 | ### 查找 35 | 上文中的这些气泡的结构和相对位置,很好的解释了引擎是如何找到它需要的标识符的。 36 | 37 | 上面的代码中,引擎执行`congsole.log(..)`语句时,会查找三个相关的变量a、b和c。首先从最内层的气泡,也即bar函数的作用域,开始查找。没找到a,因此往上一层,来到上一层嵌套的气泡,也即foo函数的作用域,在这里找到了a,因此直接使用它。查找b也是同样的过程,但是对于c,可以直接在bar函数内找到。 38 | 39 | **作用域查找一旦找到第一个匹配则立即停止**。在嵌套作用域的不同层级可以使用相同名字的标识符,内部的标识符会“遮蔽”外部的同名标识符。不论有无“遮蔽”效应,作用域查找总是从当前正在执行的作用域开始,然后一次向外/向上直到找到第一个匹配,则立即停止。 40 | 41 | **注**:全局变量是全局对象(浏览器中是window对象)的的属性,因此也可以不直接通过词法作用域方式引用全局变量,而是作为全局对象的属性间接引用:window.a。这种方法使得全局变量即使被遮蔽了依然可以被访问到。 42 | 43 | 不管函数在哪里被调用、也不管是怎样被调用,它的词法作用域**只**由其被声明的位置确定。 44 | 45 | ## 欺骗词法 46 | 47 | JavaScript中有两种方法来在执行时“修改”函数的词法作用域,但是JS社区中都不提倡使用这两种方法,因为欺骗词法作用域会影响程序性能。 48 | 49 | ### `eval` 50 | JavaScript中的`eval(..)`函数接受一个字符串参数,将其当前程序中运行这段字符串的内容,就好像这代字符串是写在这里一样。换句话说,你可以在你写的代码中用程序生成代码并且运行,就好像这段代码在你写程序时就在这里一样。 51 | 52 | `eval(..)`执行之后的后续代码时,引擎不会知道,也不会关心之前执行的代码是动态插入的,因此会修改词法作用域环境。引擎会按照正常情况进行词法查找。 53 | 例如下面的代码: 54 | ```js 55 | function foo(str, a) { 56 | eval( str ); // cheating! 57 | console.log( a, b ); 58 | } 59 | var b = 2; 60 | foo( "var b = 3;", 1 ); // 1, 3 61 | ``` 62 | eval(..)函数被调用时,字符串"var b = 3;"被执行了,就好像它是一直就在这里的代码一样。而这段代码中正好声明了变量b,它会修改函数foo的已有的词法作用域。实际上,这段代码创建了一个变量b,这个变量会把全局作用域中的b遮蔽掉。 63 | 64 | 默认情况下,如果eval(..)执行的字符串包含一个或多个声明(变量或函数)时,它会修改eval(..)所在位置的词法作用域。技术上来讲,可以通过一些技巧间接地调用eval,这会使得它在全局作用域的上下文中执行,并修改全局作用域。 65 | 66 | JavaScript中的一些其他函数也有类似于eval(..)的作用。`setTimeout(..)`和`setInterval(..)`可以接受一个字符串作为第一个参数,字符串的内容被当作动态生成的函数执行。这种方法已经被废弃了,不要再使用! 67 | 68 | 类似地,函数构造函数`new Function(..)`的最后一个参数也可以接受一个字符串,并将其转为动态生成的函数(前面的参数则作为新函数的命名参数)。这个函数构造器的语法比eval(..)更安全一些,但在代码中也应避免使用。 69 | 70 | ### `with` 71 | with关键字欺骗词法作用域的方式现在已经被弃用。下面也简单介绍下它是如何与词法作用域打交道并影响它的。 72 | 73 | with一般作为引用一个对象的多个属性而不重复引用对象本身时的一种简写方式。例如: 74 | ```js 75 | var obj = { 76 | a: 1, 77 | b: 2, 78 | c: 3 79 | }; 80 | 81 | // more "tedious" to repeat "obj" 82 | obj.a = 2; 83 | obj.b = 3; 84 | obj.c = 4; 85 | 86 | // "easier" short-hand 87 | with (obj) { 88 | a = 3; 89 | b = 4; 90 | c = 5; 91 | } 92 | ``` 93 | 但是实际使用过程中,with带来的问题比访问对象属性带来的便捷性多得多。例如: 94 | ```js 95 | function foo(obj) { 96 | with (obj) { 97 | a = 2; 98 | } 99 | } 100 | 101 | var o1 = { 102 | a: 3 103 | }; 104 | 105 | var o2 = { 106 | b: 3 107 | }; 108 | 109 | foo( o1 ); 110 | console.log( o1.a ); // 2 111 | 112 | foo( o2 ); 113 | console.log( o2.a ); // undefined 114 | console.log( a ); // 2 -- Oops, leaked global! 115 | ``` 116 | `with`语句接受一个对象,这个对象可以有零个或多个属性,并将这个对象当作一个 *完全隔离的词法作用域*,因此在这个“作用域”中这个对象的属性会被当作词法上定义的变量。 117 | 118 | **注意**:尽管with代码块将对象当作一个词法作用域,with代码块中正常的var变量声明不会被包含在with语句的作用域内,而是声明在with语句所在的函数中。 119 | 120 | 这就很好理解了,当我们将o1传给with语句时,它声明的“作用域”就是o1,这个“作用域”有一个对应于o1.a属性的“标识符”。但是当我们用o2作为“作用域”时,它没有“标识符”a,因此会按正常的LHS标识符规则进行查询。 121 | 122 | o2的“作用域”、foo(..)的作用域,甚至是全局作用域都没有a标识符,因此当a=2被执行时,会自动在全局作用域创建这个变量。 123 | 124 | ### 性能 125 | eval和with都是通过在运行时修改或创建新词法作用域来达到欺骗编写代码时定义好的词法作用域的目的。那么这有什么影响呢? 126 | 127 | JavaScript引擎在编译阶段做了很多性能优化,其中有些优化要求JS引擎在分词时能基本上做到静态分析代码,并需要预先确定所有变量和函数声明的位置,这样在执行的时候就可以很快的解析变量。但是如果引擎发现代码中有eval或with语句,它将不得不先假设所有已知的变量位置都是无效的,因为引擎在词法分析时不知道会将什么代码传入eval中来修改词法作用域,也不知道传入with语句的对象的内容是什么。 128 | 129 | 换言之,如果使用了eval或with语句,引擎做得这些优化就没有意义了,所以它干脆不做任何优化。因此不管在你的代码的任何位置出现了eval或with语句,你的整个代码都会运行的更慢。 -------------------------------------------------------------------------------- /part2/ch3.md: -------------------------------------------------------------------------------- 1 | # 第三章:函数 VS 块级作用域 2 | 3 | 第二章中我们讨论了嵌套作用域由一系列作为容器保存标识符(变量、函数)的“气泡”组成。这些“气泡”整齐地嵌套在彼此的里面,这种嵌套关系是在编写代码是就定义好的。 4 | 5 | 但是什么情况下会生成一个新的气泡呢?只有函数可以吗?JavaScript中的其他结构可以创建作用域气泡吗? 6 | 7 | JavaScript中最常见的作用域是基于函数的作用域,但是也有其他的结构可以创建新作用域。我先来看看函数作用域及其含义。 8 | 9 | ## 函数的作用域 10 | 11 | 考虑如下代码: 12 | ```js 13 | function foo(a) { 14 | var b = 2; 15 | // some code 16 | function bar() { 17 | // ... 18 | } 19 | // more code 20 | var c = 3; 21 | } 22 | ``` 23 | 函数foo的作用域中包括标识符a、b、c和bar,函数bar的作用域有自己的气泡。全局作用域也有自己的气泡,其中只有一个对应的标识符:foo。 24 | 25 | 因为a、b、c和bar属于foo的作用域,因此它们在foo函数之外是不能被访问到的,否则会导致`RenferenceError`错误。但是它们可以在函数foo内被访问到,而且在bar函数内也能被访问到。 26 | 27 | 函数作用域的思想是:所有变量都属于函数,在整个函数内都可以使用和重复使用这些变量(即使在内部嵌套的作用域中也可以被访问到)。这种设计方法非常有用,且可以充分利用JavaScript变量的“动态”特性来按需接收不同类型的值。 28 | 29 | 另一方面,如果不注意的话,存在于整个作用域的变量可能会产生一些意想不到的坑。 30 | 31 | ## 从普通作用域中隐藏 32 | 33 | 通常,我们定义一个函数,然后在函数内部添加代码。但是如果反过来思考:写任意一段代码片段,然后用一个函数声明来包裹它,这实际上将这段代码“藏起来”了。实际效果是在这段代码外面创建了一个“气泡”,也就意味着这段代码中所有的声明(函数或变量)都被绑定到这个新建函数的作用域上,而非原来包裹在外面的作用域。也就是说,通过将这些变量和函数包裹在函数作用域中可以把它们“藏起来”。 34 | 35 | 为什么“隐藏”变量和函数是一个很有用的技术呢? 36 | 37 | 很多情况下我们需要这种基于作用域的隐藏。根据软件设计原则“最低权限”原则,有时也叫“最小授权”或“最小暴露”原则。这一原则要求在软件设计中,如一个模块/对象的API接口,应仅仅暴露所需的最小量,而将其他所有东西“隐藏”。 38 | 39 | 将这一原则应用到作用域中。如果所有的变量和函数都在全局作用域,当然在所有嵌套作用域中都可以访问到它们,但是这就违背了“最小”原则,因为你将应该保持私有的所有变量和函数都暴露出来了,而这些变量是你不希望被外部代码访问到的。 40 | 例如: 41 | ```js 42 | function doSomething(a) { 43 | b = a + doSomethingElse( a * 2 ); 44 | console.log( b * 3 ); 45 | } 46 | function doSomethingElse(a) { 47 | return a - 1; 48 | } 49 | var b; 50 | doSomething( 2 ); // 15 51 | ``` 52 | 变量b和函数doSomethingElse()应该是doSomething()的私有成员,将它们定义在函数外部不仅是没有必要,而且是很危险的,因为别的代码可能以意想不到的方式使用它们。一个更“合适的”设计是将这些私有细节隐藏在函数内部,如: 53 | ```js 54 | function doSomething(a) { 55 | function doSomethingElse(a) { 56 | return a - 1; 57 | } 58 | 59 | var b; 60 | 61 | b = a + doSomethingElse( a * 2 ); 62 | 63 | console.log( b * 3 ); 64 | } 65 | 66 | doSomething( 2 ); // 15 67 | ``` 68 | 现在变量b和函数doSomethingElse()在函数外部不能被访问到了,而仅受doSomething()函数的控制。 69 | 70 | ### 避免冲突 71 | 将变量和函数“隐藏”在作用域中的另一个好处是可以避免标识符之间的命名冲突。 72 | 如: 73 | ```js 74 | function foo() { 75 | function bar(a) { 76 | i = 3; // changing the `i` in the enclosing scope's for-loop 77 | console.log( a + i ); 78 | } 79 | 80 | for (var i=0; i<10; i++) { 81 | bar( i * 2 ); // oops, infinite loop ahead! 82 | } 83 | } 84 | 85 | foo(); 86 | ``` 87 | for循环内调用bar函数时,bar函数内的赋值语句`i = 3`会覆盖for循环中的i变量,for循环判断中止条件时i永远小于10,从而导致无限循环。要解决这个问题,bar函数应该声明一个局部变量,如`var i = 3;`,这样就不会覆盖for循环中的同名变量i。 88 | 89 | #### 全局“命名空间” 90 | 在全局作用域中最容易出现变量名冲入的情况。程序中加载的多个库如果没有很好的隐藏它们的内部/私有函数和变量的话,很容易发生冲突。这些库一般会在全局作用域中创建一个单例变量,通常是一个名字很独特的对象。之后这个对象就被用作这个库的“命名空间”,所有对外暴露的特定函数都是这个对象(命名空间)的属性,而不是在这些标识符之上的更高一层的词法作用域。 91 | 例如: 92 | ```js 93 | var MyReallyCoolLibrary = { 94 | awesome: "stuff", 95 | doSomething: function() { 96 | // ... 97 | }, 98 | doAnotherThing: function() { 99 | // ... 100 | } 101 | }; 102 | ``` 103 | 104 | #### 模块管理 105 | 另一种避免命名冲突的方案是更现代化的利用各种依赖管理器的“模块化”方法。使用这些工具的话,不会将这些库的任何标识符添加到全局作用域,而是通过依赖管理器的各种机制将这些库的标识符显式地载入另一个指定作用域。 106 | 107 | 需要注意的是这些工具并没有可以不遵守词法作用域规则的“魔法”。他们只不过是根据作用域规则来保证不会有标识符被注入到任何共享作用域中,而是保存在私有的、不易冲突的作用域中,从而避免意外的作用域冲突。 108 | 109 | ## 函数作用域 110 | 111 | 我们已经知道了将任意代码片段放入函数中可以将代码中的变量和函数声明“隐藏”在函数内部作用域,而对外层作用域不可见。尽管这种方法有用,但是也会带来一些问题。首先是我们需要声明一个命名函数,而这个函数的名字本身会“污染”外部作用域。另外,我们必须显示地根据名字调用函数来执行包裹在函数中的代码。如果函数不需要名字(或,名字不会污染作用域)且可以自动执行,将会更理想。 112 | 113 | 幸运的是JavaScript提供了一种可以同时解决这个两个问题的方案。 114 | ```js 115 | var a = 2; 116 | 117 | (function foo(){ // <-- insert this 118 | 119 | var a = 3; 120 | console.log( a ); // 3 121 | 122 | })(); // <-- and this 123 | 124 | console.log( a ); // 2 125 | ``` 126 | 我们来分步看下这里发生了什么。 127 | 128 | 首先,注意函数声明前面是以`(`开始的,这虽然是一个很微小的改动,但是却是最重要的改变。这种情况下函数会被当作函数表达式,而不是一个标准的函数声明。 129 | 130 | **注**:区别声明和表达式最简单的方法是判断语句中关键字“function”的位置,如果“function”在一条语句的最开始处,那么就是一个函数声明,否则就是一个函数表达式。 131 | 132 | `(function foo(){..})`作为表达式意味着标识符`foo`只能在`..`表示的作用域中被访问到,在外部作用域是访问不到的。将名字foo隐藏在自身内部意味着不会污染外部作用域。 133 | 134 | ### 匿名 VS. 命名 135 | 你可能很熟悉将函数表达式作为回调参数,如: 136 | ```js 137 | setTimeout( function(){ 138 | console.log("I waited 1 second!"); 139 | }, 1000 ); 140 | ``` 141 | 这称为“匿名函数表达式”,因为`function()...`没有名字标识符。函数表达式可以是匿名的,但是函数声明不能省略名字——这是非法的JS语法。 142 | 143 | 匿名函数灵活且易于书写,很多库和工具都推崇这种惯用编程风格。但是也有几个缺点需要注意: 144 | 1. 在堆栈跟踪时匿名函数没有名字,这会增加调试的难度; 145 | 2. 如果函数没有名字,则它没法引用自己,如递归调用等。已经废弃的`arguments.callee`引用也不符合要求。另一种需要调用自己的情况是事件处理函数在触发事件之后想要对自己解除绑定; 146 | 3. 匿名函数缺少名字会影响程序的可读性。 147 | 148 | **内联函数表达式** 强大而实用。给你的函数表达式一个名字可以很好的避免所有这些缺点,而不会引入其他缺点。因此最佳实践是总是命名你的函数表达式: 149 | ```js 150 | setTimeout( function timeoutHandler(){ // <-- Look, I have a name! 151 | console.log( "I waited 1 second!" ); 152 | }, 1000 ); 153 | ``` 154 | 155 | ### 立即执行函数表达式 156 | ```js 157 | var a = 2; 158 | 159 | (function foo(){ 160 | 161 | var a = 3; 162 | console.log( a ); // 3 163 | 164 | })(); 165 | 166 | console.log( a ); // 2 167 | ``` 168 | 我们可以在用括号'()'包裹的函数表达式后面再用一对括号‘()’来执行这个函数,像这样的形式`(function foo(){ ..})()`。第一对括号将这个函数变为一个表达式,第二对括号执行这个函数。 169 | 170 | 这种写法非常常见,因此社区用一个术语:IIFE 来给它命名,表示“Immediately Invoked Function Expression”(立即执行函数表达式)。 171 | 172 | 当然,IIFE 可以没有名字 —— 大多数情况下IIFE使用匿名函数表达式。但是尽管用的不多,但是给 IIFE 表达式命名总是会带来好处(上文已述)。 173 | 174 | ```js 175 | var a = 2; 176 | 177 | (function IIFE(){ 178 | 179 | var a = 3; 180 | console.log( a ); // 3 181 | 182 | }()); 183 | 184 | console.log( a ); // 2 185 | ``` 186 | 187 | 有些人喜欢用这种IIFE 形式:`(function(){...}())`。仔细辨别这种两种形式的区别。第一种形式中,函数表达式包裹在`( )`中,然后再在最后通过`()`调用函数。在第二种形式中,用于调用函数的括号对`()`被放到了外层括号的里面。这两种形式的作用是完全一样的,纯粹根据个人喜好来选择即可。 188 | 189 | 调用立即执行函数的时候我们可以给他传入参数,如: 190 | ```js 191 | var a = 2; 192 | 193 | (function IIFE( global ){ 194 | 195 | var a = 3; 196 | console.log( a ); // 3 197 | console.log( global.a ); // 2 198 | 199 | })( window ); 200 | 201 | console.log( a ); // 2 202 | ``` 203 | 这种传参形式可用于解决默认的undefined 标识符可能被覆盖的问题。为IIFE定义一个参数`undefined`, 但是给这个参数传入值,就可以保证立即执行函数中的 undefined 标识符确实等于 undefined 值: 204 | ```js 205 | undefined = true; // setting a land-mine for other code! avoid! 206 | 207 | (function IIFE( undefined ){ 208 | 209 | var a; 210 | if (a === undefined) { 211 | console.log( "Undefined is safe here!" ); 212 | } 213 | 214 | })(); 215 | ``` 216 | IIFE还有一种可以改变代码执行顺序的变体形式: 217 | ```js 218 | var a = 2; 219 | 220 | (function IIFE( def ){ 221 | def( window ); 222 | })(function def( global ){ 223 | 224 | var a = 3; 225 | console.log( a ); // 3 226 | console.log( global.a ); // 2 227 | 228 | }); 229 | ``` 230 | 这段代码中,def 函数作为参数传入 IIFE 中,然后 def 函数被调用时传入参数window(定义中的形式参数 global)。 231 | 232 | 这种形式通常用在 通用模块定义中(UMD,Universal Module Definition)。 233 | 234 | ## 块级作用域 235 | 236 | ES6之前,JS 不支持块级作用域,块级作用域中的变量可以在定义其的代码块之外使用。因此,开发者应该强制自己养成一种块级作用域习惯,在块级作用域中定义的变量只在块级作用域中使用。 237 | 238 | ### `with` 239 | with 语句中,对象创建的作用域仅存在于 with 语句内,而不属于外层作用域。 240 | 241 | ### `try/catch` 242 | ES3中指出在`try/catch`语句的`catch`子句中声明的变量只属于`catch`块中,例如: 243 | ```js 244 | try { 245 | undefined(); // illegal operation to force an exception! 246 | } 247 | catch (err) { 248 | console.log( err ); // works! 249 | } 250 | 251 | console.log( err ); // ReferenceError: `err` not found 252 | ``` 253 | 254 | ### `let` 255 | ES6中引入了关键字`let`,以不同于`var`的方式声明变量。let 关键字声明的变量只属于包含该条声明语句的代码块(通常是一个{}括号对)中。如: 256 | ```js 257 | var foo = true; 258 | 259 | if (foo) { 260 | let bar = foo * 2; 261 | bar = something( bar ); 262 | console.log( bar ); 263 | } 264 | 265 | console.log( bar ); // ReferenceError 266 | ``` 267 | 需要注意的是,let 关键字声明的变量不会提升到其所在作用域的顶部,因此这些变量在声明语句出现之前都是不可用的。 268 | ```js 269 | { 270 | console.log( bar ); // ReferenceError! 271 | let bar = 2; 272 | } 273 | ``` 274 | 275 | #### 内存回收 276 | 块级作用域有助于闭包相关的内存回收。闭包机制会在第五章中详细阐述。 277 | 考虑: 278 | ```js 279 | function process(data) { 280 | // do something interesting 281 | } 282 | 283 | var someReallyBigData = { .. }; 284 | 285 | process( someReallyBigData ); 286 | 287 | var btn = document.getElementById( "my_button" ); 288 | 289 | btn.addEventListener( "click", function click(evt){ 290 | console.log("button clicked"); 291 | }, /*capturingPhase=*/false ); 292 | ``` 293 | 这个例子中,click 处理函数完全不会用到someReallyBigData变量,理论上,process 函数执行完后,这个占用大量内存的数据结构就会被当做垃圾回收了。然而,很有可能 JS 引擎仍会保留这个变量,因为 click 函数的作用域外有一个闭包。 294 | 295 | 块级作用域就可以很好的解决这个问题,它明确的告诉 JS 引擎不需要保留 someReallyBigData 变量: 296 | ```js 297 | function process(data) { 298 | // do something interesting 299 | } 300 | 301 | // anything declared inside this block can go away after! 302 | { 303 | let someReallyBigData = { .. }; 304 | 305 | process( someReallyBigData ); 306 | } 307 | 308 | var btn = document.getElementById( "my_button" ); 309 | 310 | btn.addEventListener( "click", function click(evt){ 311 | console.log("button clicked"); 312 | }, /*capturingPhase=*/false ); 313 | ``` 314 | 为变量显示的声明作用域块以达到局部绑定的目的,是可以加入到你编码技能包里的一个强大技能。 315 | 316 | #### 循环中的`let` 317 | 318 | let用于for循环最能体现其优点。 319 | ```js 320 | for (let i=0; i<10; i++) { 321 | console.log( i ); 322 | } 323 | 324 | console.log( i ); // ReferenceError 325 | ``` 326 | 上面的代码中,for循环头部的let不仅将变量i绑定到for循环体内,而且对每一次迭代都会重新绑定,这样就能确保将上一次迭代后的i值赋给当前迭代的i值。上面的代码等效于: 327 | ```js 328 | { 329 | let j; 330 | for (j=0; j<10; j++) { 331 | let i = j; // re-bound for each iteration! 332 | console.log( i ); 333 | } 334 | } 335 | ``` 336 | 由于let声明的变量是块级作用域,而不仅仅属于函数封装的作用域,因此用let替换var重构代码时需要格外注意,因为已有的代码可能会对var声明的函数级作用域有隐藏依赖关系。 337 | 考虑: 338 | ```js 339 | var foo = true, baz = 10; 340 | 341 | if (foo) { 342 | var bar = 3; 343 | 344 | if (baz > bar) { 345 | console.log( baz ); 346 | } 347 | // ... 348 | } 349 | ``` 350 | 这段代码可以很简单的重构为: 351 | ```js 352 | var foo = true, baz = 10; 353 | 354 | if (foo) { 355 | var bar = 3; 356 | // ... 357 | } 358 | if (baz > bar) { 359 | console.log( baz ); 360 | } 361 | ``` 362 | 但是如果用块级作用域变量重构时,就需要注意了: 363 | ```js 364 | var foo = true, baz = 10; 365 | 366 | if (foo) { 367 | let bar = 3; 368 | 369 | if (baz > bar) { // <-- don't forget `bar` when moving! 370 | console.log( baz ); 371 | } 372 | } 373 | ``` 374 | 375 | #### `const` 376 | 除了let之外,ES6中还引入了const关键字,它也可以创建一个块级作用域变量,只不过它的值是固定的(常量)。一旦声明之后,任何尝试修改该值的行为都会报错。 377 | ```js 378 | var foo = true; 379 | 380 | if (foo) { 381 | var a = 2; 382 | const b = 3; // block-scoped to the containing `if` 383 | 384 | a = 3; // just fine! 385 | b = 4; // error! 386 | } 387 | 388 | console.log( a ); // 3 389 | console.log( b ); // ReferenceError! 390 | ``` 391 | -------------------------------------------------------------------------------- /part2/ch4.md: -------------------------------------------------------------------------------- 1 | # 第四章:变量提升 2 | 3 | 到目前为止,读者应该已经熟悉了作用域的概念,以及变量是怎样根据声明方式和声明位置的不同而归属于不同的作用域层级。函数作用域和块级作用域都遵从同一个规则:某个作用域内声明的任何变量都归属于这个作用域。 4 | 5 | 本章我们详细讨论下作用域内不同位置声明的变量是如何添加到这个作用域内的。 6 | 7 | ## 先有鸡还是先有蛋? 8 | 9 | 我们通常很容易认为在Javascript程序执行时,你看到的所有代码都是按顺序从上到下逐行被解释的。虽然程序确实是这样执行的,但是有一点需要特别注意! 10 | 考虑: 11 | ```js 12 | a = 2; 13 | 14 | var a; 15 | 16 | console.log( a ); 17 | ``` 18 | 会输出什么呢?很多开发者认为会输出`undefined`,理由是`var a`语句在`a=2`之后,自然会对变量a重新赋值,因此a的值会是默认值undefined。然而,实际输出是2。 19 | 20 | 考虑另一段代码: 21 | ```js 22 | console.log(a); 23 | var a = 2; 24 | ``` 25 | 根据上一段代码中的行为,你可能认为输出结果会是2,或者由于使用变量a之前未定义该变量而抛出引用错误。 26 | 27 | 很遗憾的告诉你,这两种猜测都不对。输出结果是`undefined`。这是怎么回事?到底是先有鸡还是先有蛋呢? 28 | 29 | ## 再次请出编译器 30 | 31 | 回忆第一章中我们对编译器的讨论:实际上引擎会在解释之前先编译JS代码,编译阶段的一个主要作用就是找到并将所有的声明语句与它们各自的作用域联系起来。第二章解释了这正式词法作用域的核心。 32 | 33 | 因此,这个问题的正确答案是:所有的声明,包括变量和函数都会在任何代码被执行之前先被编译器处理。 34 | 35 | 对于声明语句`var a = 2;`,Javascript时间上把它当作两条语句来对待:`var a;`和`a = 2;`。第一个语句是声明,在编译阶段被处理,第二条语句是赋值则是在执行阶段被处理。 36 | 37 | 因此,上文中的第一段代码可以被认为是: 38 | ```js 39 | // the compilation 40 | var a; 41 | // the execution 42 | a = 2; 43 | console.log( a ); 44 | ``` 45 | 同样的对于第二段代码,实际上被处理为: 46 | ```js 47 | var a; 48 | console.log( a ); 49 | a = 2; 50 | ``` 51 | 综上,可以这样来理解这个处理过程:变量和函数声明从它们在代码流中的位置被“移”到了代码的顶部。这个行为被叫做“变量提升”。 52 | 53 | 也就是说,先有蛋(声明),再有鸡(赋值)。 54 | 55 | **注意**:只有声明本身会被提升,而其他任何赋值或其他可执行逻辑都保持不变。 56 | 57 | 另外一个需要注意的是每个作用域的变量都会被提升。 58 | ```js 59 | foo(); 60 | 61 | function foo() { 62 | console.log( a ); // undefined 63 | 64 | var a = 2; 65 | } 66 | ``` 67 | 上面代码中除了foo函数在全局作用域内会被提升之外,foo函数本身也会将`var a`提升到`foo(){ .. }`的顶部,而不是整段代码的顶部。实际效果如下: 68 | ```js 69 | function foo() { 70 | var a; 71 | 72 | console.log( a ); // undefined 73 | 74 | a = 2; 75 | } 76 | 77 | foo(); 78 | ``` 79 | 需要注意的是,函数声明会被提升,但是函数表达式不会被提升。 80 | ```js 81 | foo(); // not ReferenceError, but TypeError! 82 | 83 | var foo = function bar() { 84 | // ... 85 | }; 86 | ``` 87 | 变量标识符foo被提升,因此foo()被执行时不会导致`ReferenceError`。但是foo还没有值。因此foo()尝试调用undefined值,从而导致非法的操作,抛出`TypeError`。 88 | 89 | 还需要注意的是,尽管这是一个命名函数表达式,函数名标识符在作用域内也是不可用的: 90 | ```js 91 | foo(); // TypeError 92 | bar(); // ReferenceError 93 | 94 | var foo = function bar() { 95 | // ... 96 | }; 97 | ``` 98 | 这段代码实际上被解释为: 99 | ```js 100 | var foo; 101 | 102 | foo(); // TypeError 103 | bar(); // ReferenceError 104 | 105 | foo = function() { 106 | var bar = ...self... 107 | // ... 108 | } 109 | ``` 110 | ## 函数优先提升 111 | 112 | 函数声明和变量声明都会被提升,但是函数声明会先被提升,其次才是变量。 113 | 考虑: 114 | ```js 115 | foo(); // 1 116 | 117 | var foo; 118 | 119 | function foo() { 120 | console.log( 1 ); 121 | } 122 | 123 | foo = function() { 124 | console.log( 2 ); 125 | }; 126 | ``` 127 | 输出了1而不是2!引擎将上面的代码解释为: 128 | ```js 129 | function foo() { 130 | console.log( 1 ); 131 | } 132 | 133 | foo(); // 1 134 | 135 | foo = function() { 136 | console.log( 2 ); 137 | }; 138 | ``` 139 | 需要注意的是`var foo`是重复声明,因此会被忽略,尽管它在function foo()...声明之前。 140 | 141 | 尽管多重的var声明会被有效的忽略,但是后面的函数声明会覆盖之前声明的函数。 142 | ```js 143 | foo(); // 3 144 | 145 | function foo() { 146 | console.log( 1 ); 147 | } 148 | 149 | var foo = function() { 150 | console.log( 2 ); 151 | }; 152 | 153 | function foo() { 154 | console.log( 3 ); 155 | } 156 | ``` 157 | 尽管JS引擎能有效处理多重声明,但是在同一个作用域内多重声明通常会导致意想不到的结果。 158 | 159 | 普通代码块内的函数声明通常会被提升到外层的作用域,而不是函数所在的条件分支: 160 | ```js 161 | foo(); // "b" 162 | 163 | var a = true; 164 | if (a) { 165 | function foo() { console.log( "a" ); } 166 | } 167 | else { 168 | function foo() { console.log( "b" ); } 169 | } 170 | ``` 171 | 然而,一定要注意的是,上述行为是不可靠的,在未来的JS版本中可能会改变,因此要避免在代码块内声明函数。 -------------------------------------------------------------------------------- /part2/ch5.md: -------------------------------------------------------------------------------- 1 | # 第五章:作用域闭包 2 | 3 | **JavaScript中闭包无处不在,你必须要意识到这一点并拥抱它!** 4 | 5 | 根据词法作用域来编写代码就会写出闭包,这是自然而然的。 6 | 7 | ## 闭包的实质 8 | 9 | 定义: 10 | > 当一个函数在其词法作用域之外执行时仍能记住并访问其词法作用域时,这个函数就是闭包。 11 | 12 | 我们先通过一段代码来解释这个定义: 13 | ```js 14 | function foo() { 15 | var a = 2; 16 | function bar() { 17 | console.log(a); //2 18 | } 19 | bar(); 20 | } 21 | foo(); 22 | ``` 23 | 函数 bar() 通过词法作用域查找规则(这里是 RHS 引用查找)能访问外层作用域的变量a。这是闭包吗? 24 | 25 | 从纯学术角度来说,上面的代码中,函数`bar()`有一个对`foo()`作用域的闭包。换种不同的说法:`bar()`结束了`foo()`的作用域。为什么?因为`bar()`出现在 foo() 函数内部。简单明了。 26 | 27 | 但是这种方式定义的闭包不是很直观。下面看另一种使用闭包的方式: 28 | ```js 29 | function foo() { 30 | var a = 2; 31 | 32 | function bar() { 33 | console.log( a ); 34 | } 35 | 36 | return bar; 37 | } 38 | 39 | var baz = foo(); 40 | 41 | baz(); // 2 -- Whoa, closure was just observed, man. 42 | ``` 43 | 这段代码中,函数bar通过词法作用域访问foo的内部作用域。但是我们将bar函数本身当做一个值返回了。当执行foo函数时,将其返回的值(内部的bar函数)赋给变量baz,然后再调用baz()函数,而实际上是调用内部函数bar(),只不过是使用了不同的引用标识符罢了。 44 | 45 | bar函数被执行了,而且是在其被声明时所在的词法作用域之外被执行的。 46 | 47 | 通常来说,foo函数被执行之后,foo作用域内的所有内容都会消失,因为JS引擎的垃圾回收机制会在释放不再使用的内存。 48 | 49 | 闭包的神奇之处就在于它能防止被回收。实际上内部的作用域仍处于“使用中”状态,因此不会消失。**函数bar在使用它自己**。bar函数定义在foo函数内部,因此它对foo的内部作用域有一个闭包,这就使得foo函数的作用域得以保持,以便之后被bar函数引用。 50 | 51 | **bar函数仍然能够引用到这个作用域,这个引用就是闭包。** 闭包使得函数能够一直访问的到该函数被定义时所在的词法作用域。 52 | 53 | 当然,任何将函数作为值传递然后在别的地方调用该函数的方式都是闭包。 54 | ```js 55 | function foo() { 56 | var a = 2; 57 | 58 | function baz() { 59 | console.log( a ); // 2 60 | } 61 | 62 | bar( baz ); 63 | } 64 | 65 | function bar(fn) { 66 | fn(); // look ma, I saw closure! 67 | } 68 | ``` 69 | 将函数作为值传递也可以是间接的: 70 | ```js 71 | var fn; 72 | 73 | function foo() { 74 | var a = 2; 75 | 76 | function baz() { 77 | console.log( a ); 78 | } 79 | 80 | fn = baz; // assign `baz` to global variable 81 | } 82 | 83 | function bar() { 84 | fn(); // look ma, I saw closure! 85 | } 86 | 87 | foo(); 88 | 89 | bar(); // 2 90 | ``` 91 | 无论我们通过什么方式将一个内部函数放到其词法作用域之外的地方,它都会保持一个对其原始定义所在作用域的引用,不管它在哪里被执行,这个闭包都会生效。 92 | 93 | ## 其他形式的闭包 94 | 95 | ```js 96 | function wait(message) { 97 | 98 | setTimeout( function timer(){ 99 | console.log( message ); 100 | }, 1000 ); 101 | 102 | } 103 | 104 | wait( "Hello, closure!" ); 105 | ``` 106 | 在JS引擎中,内置方法`setTimeout(..)`会引用一些参数,可能叫做fn或func,引擎会调用这个函数,也即调用内部的timer函数。 107 | 108 | 实际上,无论何时无论何地当你将函数作为值来传递时,这些函数都会使用闭包。定时器、事件处理函数、Ajax请求、跨窗口通信、web workers,或者任意其他异步(或同步)任务中,当你传递一个 *回调函数* 时,就会产生闭包! 109 | 110 | 按照上文的定义,IIFE模式不能算是严格的闭包: 111 | ```js 112 | var a = 2; 113 | 114 | (function IIFE(){ 115 | console.log( a ); 116 | })(); 117 | ``` 118 | 函数IIFE被执行时所在的作用域与其引用的变量a处于同一个作用域,执行时只是进行普通的词法作用域查找,而不是通过闭包。虽然IIFE不是闭包,但是它可以创建作用域,也是最常用的用于创建可被关闭的作用域的方式之一。因此,IIFE与闭包联系非常密切。 119 | 120 | ## 循环+闭包 121 | 122 | for循环是永不阐述闭包的一个典型例子: 123 | ```js 124 | for (var i=1; i<=5; i++) { 125 | setTimeout( function timer(){ 126 | console.log( i ); 127 | }, i*1000 ); 128 | } 129 | ``` 130 | 上述代码中,定时函数在for循环之后才被执行,尽管5个定时函数在各自的迭代中被定义,但是它们引用的是同一个作用域里的同一个变量,因此结果是都打印出6。 131 | 再尝试一下使用IIFE模式: 132 | ```js 133 | for (var i=1; i<=5; i++) { 134 | (function(){ 135 | setTimeout( function timer(){ 136 | console.log( i ); 137 | }, i*1000 ); 138 | })(); 139 | } 140 | ``` 141 | IIFE的确在每个迭代中创建了一个可关闭的作用域,但是这些作用域都是空的,空的作用域等于没有,因为它什么都没干。我们将IIFE形成的作用域中保存i的值: 142 | ```js 143 | for (var i=1; i<=5; i++) { 144 | (function(){ 145 | var j = i; 146 | setTimeout( function timer(){ 147 | console.log( j ); 148 | }, j*1000 ); 149 | })(); 150 | } 151 | ``` 152 | 成功了! 153 | 另一种写法是: 154 | ```js 155 | for (var i=1; i<=5; i++) { 156 | (function(j){ 157 | setTimeout( function timer(){ 158 | console.log( j ); 159 | }, j*1000 ); 160 | })( i ); 161 | } 162 | ``` 163 | 164 | ### 重新审视块级作用域 165 | 166 | 上面的示例中,我们使用IIFE在每个迭代中创建一个新的块级作用域,这个块级作用域也可以通过使用let关键字来创建。let关键字声明的变量也会将声明坐在的代码块变为可关闭的作用域,如: 167 | ```js 168 | for (var i=1; i<=5; i++) { 169 | let j = i; // yay, block-scope for closure! 170 | setTimeout( function timer(){ 171 | console.log( j ); 172 | }, j*1000 ); 173 | } 174 | ``` 175 | 如果是在for循环头部用let声明变量,该变量会在每次迭代中被声明一次。这样下面的代码就等效于上面的代码: 176 | ```js 177 | for (let i=1; i<=5; i++) { 178 | setTimeout( function timer(){ 179 | console.log( i ); 180 | }, i*1000 ); 181 | } 182 | ``` 183 | 是不是很酷? 184 | 185 | ## 模块 186 | 187 | 除了容易理解的使用回调函数之外,模块化模式是充分践行闭包优点的另一种方式。 188 | ```js 189 | function foo() { 190 | var something = "cool"; 191 | var another = [1, 2, 3]; 192 | 193 | function doSomething() { 194 | console.log( something ); 195 | } 196 | 197 | function doAnother() { 198 | console.log( another.join( " ! " ) ); 199 | } 200 | } 201 | ``` 202 | 这段代码没有什么特别,现在考虑这段代码: 203 | ```js 204 | function CoolModule() { 205 | var something = "cool"; 206 | var another = [1, 2, 3]; 207 | 208 | function doSomething() { 209 | console.log( something ); 210 | } 211 | 212 | function doAnother() { 213 | console.log( another.join( " ! " ) ); 214 | } 215 | 216 | return { 217 | doSomething: doSomething, 218 | doAnother: doAnother 219 | }; 220 | } 221 | 222 | var foo = CoolModule(); 223 | 224 | foo.doSomething(); // cool 225 | foo.doAnother(); // 1 ! 2 ! 3 226 | ``` 227 | 首先,CoolModule()只是一个函数,但是这里它必须调用来创建一个模块实例。如果不执行外部的函数,内部作用域和闭包就不会被创建。 228 | 229 | 然后,CoolModule()返回一个字面量表示的对象。这个返回的对象引用了内部的函数,但没有引用内部的数据变量,这些变量对外是隐藏的。可以将这个对象返回值作为 **模块的公共API**。这个对象返回值最后被赋给了变量foo,然后我们就可以访问API上的属性方法,如foo.doSomething()。 230 | 231 | doSomething()和doAnother()函数有对模块内部作用域的闭包(调用CoolModule()时形成)。当我们在词法作用域的外部通过API使用这些函数时,就构成了闭包的条件。 232 | 233 | 因此,实践模块模式有两个要求: 234 | 1. 必须有一个外层包裹函数,且这个函数必须至少被调用一次(每调用一次创建一个新模块实例)。 235 | 2. 外层包裹函数必须返回至少一个内部函数,这样内部函数才能形成对私有作用域的闭包。 236 | 237 | 单例模式示例: 238 | ```js 239 | var foo = (function CoolModule() { 240 | var something = "cool"; 241 | var another = [1, 2, 3]; 242 | 243 | function doSomething() { 244 | console.log( something ); 245 | } 246 | 247 | function doAnother() { 248 | console.log( another.join( " ! " ) ); 249 | } 250 | 251 | return { 252 | doSomething: doSomething, 253 | doAnother: doAnother 254 | }; 255 | })(); 256 | 257 | foo.doSomething(); // cool 258 | foo.doAnother(); // 1 ! 2 ! 3 259 | ``` 260 | 这里,我们将模块函数作为一个IIFE使用。 261 | 262 | 模块也是函数,也可以接受参数: 263 | ```js 264 | function CoolModule(id) { 265 | function identify() { 266 | console.log( id ); 267 | } 268 | 269 | return { 270 | identify: identify 271 | }; 272 | } 273 | 274 | var foo1 = CoolModule( "foo 1" ); 275 | var foo2 = CoolModule( "foo 2" ); 276 | 277 | foo1.identify(); // "foo 1" 278 | foo2.identify(); // "foo 2" 279 | ``` 280 | 281 | 模块模式的另一个变体是:通过保留模块实例内部对公共API的内部引用,这样就可以从内部修改模块实例,比如增加、删除、修改方法和属性等,如: 282 | ```js 283 | var foo = (function CoolModule(id) { 284 | function change() { 285 | // modifying the public API 286 | publicAPI.identify = identify2; 287 | } 288 | 289 | function identify1() { 290 | console.log( id ); 291 | } 292 | 293 | function identify2() { 294 | console.log( id.toUpperCase() ); 295 | } 296 | 297 | var publicAPI = { 298 | change: change, 299 | identify: identify1 300 | }; 301 | 302 | return publicAPI; 303 | })( "foo module" ); 304 | 305 | foo.identify(); // foo module 306 | foo.change(); 307 | foo.identify(); // FOO MODULE 308 | ``` 309 | 310 | ### 现代的模块 311 | 312 | 各种各样的依赖加载器、管理器基本上将模块定义模式包装成了友好的API。这里只做简要阐述: 313 | ```js 314 | var MyModules = (function Manager() { 315 | var modules = {}; 316 | 317 | function define(name, deps, impl) { 318 | for (var i=0; i 任何足够*先进*的技术都与魔法无异。-- Arthur C. Clarke 6 | 7 | 实际上,Javascript中的`this`机制其实并没有那么先进,但是开发者往往看到这个单词就将其与“复杂”或‘’令人困扰”联系起来。毫无疑问,如果对`this`机制缺乏足够了解,自然会认为它是很神奇的。 8 | 9 | ## 为什么需要`this`? -------------------------------------------------------------------------------- /preface.md: -------------------------------------------------------------------------------- 1 | # 前言 2 | 3 | 相信读者都有过吐槽JavaScript语言本身的怪癖,这是共识,但是本系列书籍书名中的JS不是用来吐槽JavaScript的缩略词。 4 | 5 | 在Web开发的早期,JavaScript是驱动我们与页面内容进行交互的基础技术,那时候的JavaScript可以完成鼠标轨迹闪烁、弹出烦人的提示窗等功能。经过20多年的发展,JavaScript的技术和能力实现了指数级的增长,没有人再怀疑它对于现在世界上应用最广泛的软件平台的核心——Web的重要性。 6 | 7 | 但是作为一门语言,JS一直饱受争议,这些争议一部分源自其历史遗留问题, 更多的还是针对其设计理念。正如Brendan Eich曾说,从JavaScript的名字可以看出,相比于成熟的大哥Java,它只是一个不愿说话的小弟弟。但是它的名字只不过是市场营销的一个偶然罢了。这两门语言在很多重要方面都有着巨大的差异,“JavaScript”与“Java”的关系犹如“Carnival”(狂欢)与“Car”(汽车)的关系。 8 | 9 | 由于JavaScript借鉴了其他几门语言的一些概念和语法习惯,包括C语言风格的面向过程特性,以及一些不明显的Scheme/Lisp风格的函数式编程特性。对于绝大多数开发者,甚至是没有编程经验的人,都很容易接受JavaScript。它的“Hello World”是如此的简单,因此新手在接触初期便可快速上手。 10 | 11 | 虽然JavaScript可能是从起到到运行最容易的语言之一,但它的一些怪癖使得它不如其他语言一样容易精通。用C或者C++一样开发一个完善的程序、完善的产品需要深入掌握这门语言的知识,JavaScript也不例外,只是蜻蜓点水是不行的。 12 | 13 | 在简单易用的外表下,一些复杂的概念,深深地根植于这门语言中。例如传递函数作为回调可以帮助JavaScript开发者放心使用这门语言而不必关心内部实现。 14 | 15 | JavaScript既是一门被广泛使用的简单易用的语言,同时也具有一些复杂而微妙的语言机制。即使是经验丰富的JavaScript开发者,也必须仔细学习才能真正掌握它。 16 | 17 | Java中有一个悖论,就像这门语言的阿基里斯之锺,也是我们正在面临的挑战。由于可以在不理解的情况下使用JavaScript,可能就永远不会理解这门语言了。 18 | 19 | ## 目标 20 | 21 | 如果在JavaScript中遇到了意料之外或者令人沮丧的东西,很多人的的自然反应是将它放入黑名单,长此以往,你接触的将只是JavaScript丰富宝矿的一个空壳。 22 | 23 | 尽管JavaScript的子集被描述为著名的“The Good Parts”,亲爱的读者,我恳求你不要老想着“The Easy Parts”,“The Safe Parts”或者“The Incomplete Parts”。 24 | 25 | *你不了解的JS*系列书籍提出一个相反的挑战:学习且深入理解JavaScript的*全部*,尤其是“The Tough Parts”。 26 | 27 | 在这里,我们倾向于让JS开发者学会“足够多”的知识,而不是强迫他们去学习这门语言是如何运行的以及为什么这么运行。同时,随着学习的深入,我们避开常用的方法以退为进。 28 | 29 | 我是永不满足的,你们也不应该仅仅停留在知道这个东西怎么工作,而不知道为什么这么工作。我希望你们能够坚持走完这段“少有人走过”的崎岖之路,拥抱JavaScript的方方面面。掌握了这些知识,没有哪种技术、没有哪个框架、没有哪个当下流行的首字母缩略词是你不能理解的。 30 | 31 | 这一系列书中的每一本都深入且详尽地阐述这门语言的一个核心主题,这些主题常被误解或者理解不透彻。在阅读时,不要受限于自己已有的知识,不要先入为主,不要仅停留在理论层面,而要多动手练习“你需要了解”的部分。 32 | 33 | *目前*你所了解的JavaScript可能是某些本身就一知半解的人传授给你的,**这个**JavaScript仅仅是真正的JavaScript的一个影子。你并没有真正理解JavaScript,但是如果你深入学习本系列书,你将理解。读起来吧,我的朋友,JavaScript在召唤你。 34 | 35 | ## 总结 36 | 37 | JavaScript是奇妙的。学习它的一部分很简单,但是要完全地(或者充分地)掌握它则难得多。开发者遇到困恼时,他们尝尝归咎于语言本身,而不是自身理解的缺失。本系列书籍旨在纠正这一点,以激起你对JavaScript的极大兴趣,这是你现在能够,也应该铭记于心的。 38 | 39 | 注:书中的很多例子都运行于现代(即将到来)JavaScript引擎,如ES6。有些代码可能在老旧(ES6之前)引擎中不能正常运行。 --------------------------------------------------------------------------------