<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0">
  <channel>
    <title>daroson (Daroson)</title>
    <link>https://ruby-china.org/daroson</link>
    <description/>
    <language>en-us</language>
    <item>
      <title>Javascript 语义详解 - 从浏览器角度理解代码</title>
      <description>&lt;p&gt;初级，或者资深初级程序员，永远是从人的角度去看代码，结果就会产生“变量提升”（variable hoisting）之类不存在的概念，误人误己。比如国外某程序员写的&lt;a href="http://blog.buymeasoda.com/advanced-javascript-fundamentals/" rel="nofollow" target="_blank" title=""&gt;“进阶教学”博客&lt;/a&gt;中，有这么一段代码：&lt;/p&gt;

&lt;p&gt;function foo() {
        return "A";
    }&lt;/p&gt;

&lt;p&gt;var oldFoo = foo;&lt;/p&gt;

&lt;p&gt;function foo() {
        return oldFoo() + "B";
    }&lt;/p&gt;

&lt;p&gt;console.log(foo()); // "AB"&lt;/p&gt;

&lt;p&gt;它的实际结果是 RangeError: Maximum call stack size exceeded，而在此之前，这位程序员才刚刚讲完什么是“variable hoisting”。&lt;/p&gt;

&lt;p&gt;本文旨在教会大家进阶级程序员的第一课，即不再从表面的人的角度去看代码，而是从内在的机器的角度去看代码。而从机器的角度出发，Javascript 代码的开始相当于一个全局 function 的调用，而 function 在调用后并不是马上一行行地运行下去，而是分为两个阶段：初始设定阶段以及正式运行阶段。&lt;/p&gt;
&lt;h3 id="设定阶段"&gt;设定阶段&lt;/h3&gt;
&lt;p&gt;例 1：&lt;/p&gt;

&lt;p&gt;console.log( a,b() )
    var a = 1
    function b() { return 2 }&lt;/p&gt;

&lt;p&gt;首先在执行前，浏览器会先把这些连着的“字符串”（string）分成一个个“字词”（token），期间会自动在有需要的行尾加上分号，然后再把这些字词组织成一个更方便操作的树状结构（AST），最后才开始执行（execution）。&lt;/p&gt;

&lt;p&gt;在设定阶段，会建立一个空的当前代码段（function）的变量表，它可以理解为一个变量名字（identifier）及其内存地址的对照表。&lt;/p&gt;

&lt;p&gt;然后会找到 function 这个字词，把它后面的那个字词 (b) 作为这个 function 的名字，再把 { 和 } 这两个字词之间的所有字词 (这里为 return 2) 和一些相关属性（property）生成一个 javascript 里的 Function object，作为这个 function 的值。并把这个值的内存地址和名字作为一个 binding（绑定）存到变量表里。假如名字重复，则覆盖掉之前的值。（在最开始的例子中，后面的 foo 就覆盖掉了前面的 foo，导致 foo/oldFoo 老在调用它自己，结果超出了 call stack 的大小）&lt;/p&gt;

&lt;p&gt;这之后会找到 var 这个字词，把它后面的那个字词 (a) 作为这个 variable（变量）的名字，这里会先检查这个名字是否已在变量表中，如果在则直接跳过，因此不会覆盖之前的值，如果不在则分配一个内存地址给它，这个内存地址的值为 undefined。最后把它们作为又一个 binding 存到变量表。&lt;/p&gt;

&lt;p&gt;到此设定阶段结束，开始调用 console.log( a,b() ) ，得到的结果为 undefined,2。&lt;/p&gt;

&lt;p&gt;例 2：&lt;/p&gt;

&lt;p&gt;var a = 1
    b(a)
    function b(c) { console.log(c) }&lt;/p&gt;

&lt;p&gt;一开始的设定阶段和例 1 一样，然后先是执行 a = 1，把 1 赋值给 a 的内存地址，于是 a 的值从 undefined 变为了 1。
之后调用 b(a) ，这里会暂停 全局代码 的执行，转而开始执行 b。&lt;/p&gt;

&lt;p&gt;在 b 的设定阶段，会先把 a 的值（1）作为 c 的值，并存到变量表里，然后由于没有 function 和 var 这两个字词，设定阶段结束，开始调用 console.log(c) ，得到的结果为 1。&lt;/p&gt;

&lt;p&gt;例 3：&lt;/p&gt;

&lt;p&gt;var a = 1
    b(2)
    function b(c) { 
        var d = 3
        console.log(d,c,a,this) 
    }&lt;/p&gt;

&lt;p&gt;首先还是先声名 b 和 a。而且在 b 的声名阶段，会把一个 b 的一个内部变量（无法直接在 Javascript 里访问），名字叫 [[scope]]，设为指向它声名时所在的变量表，在这里即全局的变量表，a 和 b 以及它们的内存地址就是被存在这里。&lt;/p&gt;

&lt;p&gt;前面提到的 function、var 还有{ }等符号都属于浏览器认识的关键字词（keyword），当浏览器读到不认识的字词（名字）时，会首先到当前所在的变量表里找，如果没找到，就会去这个 [[scope]]，即上一层的变量表里面找，直到 [[scope]] 为空（null）时，浏览器就会提示 xxx is not defined。&lt;/p&gt;

&lt;p&gt;这里声名完了后开始执行 a=1，之后调用 b(2)，，这里会先把把 Window，也就是 global object（全局对象）的内存地址作为 this 的值。然后把 2 赋值给 c，之后声名 d，于是这一层的变量表即只有 c 和 d。&lt;/p&gt;

&lt;p&gt;这时 b 的设定阶段结束，开始执行 d=3 和 console.log(d,c,a,this ) ，得到的结果为 3,2,1,Window{ ... } 。&lt;/p&gt;

&lt;p&gt;【总结】
在 function 声名时，会设置它的 [[scope]]，即它的上一层的变量表，以及 [[FormalParameters]]，即所有形式参数的名字，最后是 FunctionBody，即 { 和 } 之间的树形结构化了的代码（AST），并用它生成一个 Function object 作为以后的调用对象。&lt;/p&gt;

&lt;p&gt;而代码/function 的设定阶段为：
1.设置 this，不同情况下它的地址也会不同。
a.对一般 function 来说，它被设置成 global/Window。
b.当以 objectXX.methodXX() 形式调用时，它被设置成 objectXX。
c.当以 new XX() 形式时，它被设置成一个新的空 object。
d.当以 XX.call() 或 XX.apply() 形式时，它被设置成第一个参数。&lt;/p&gt;

&lt;p&gt;2.设定一个内存地址，它指向这个 function 声名时的 [[scope]]。&lt;/p&gt;

&lt;p&gt;3.设定这一层的变量表。这一步又依次分为 a.分配 function 的内存地址并赋值；b.如果 [[FormalParameters]] 里的参数名没有重复，则分配它一个内存地址并赋值；c.如果变量名没重复，则分配它一个内存地址但不赋值。&lt;/p&gt;

&lt;p&gt;以上这些共同构成了一个术语叫 execution context 的东西（代码执行上下文），而设定阶段其实就是把它创建起来的过程。&lt;/p&gt;

&lt;p&gt;【ES5】
Ecmascript 是 Javascript 的标准规范，以上的讲解主要是基于它的第 3 版（ES3），而当前的版本为 ES5。（第 4 版是个失败所以直接到了第 5 版）&lt;/p&gt;

&lt;p&gt;ES3 中的变量表就是一个对象叫做 VariableObject，这种实现更容易理解，但在访问多层以上的变量时速度会越来越慢，后来的浏览器进行了优化，不再使用 VariableObject，优化后的速度基本不会受层数影响。于是 ES5 里把 VariableObject 改为了一种抽象的标准，名字为 lexical environment（词语环境）。&lt;/p&gt;

&lt;p&gt;实际这个 lexical environment 就是由原来指向 [[scope]] 的内存地址和 VariableObject 组成，只不过这个内存地址现在叫 outer lexical environment（外部词语环境），而 VariableObject 现在叫 environment record（环境记录），而 this 在 ES5 标准里也改名叫 thisBinding，并与 lexical environment 共同构成 ES5 里的 execution context。&lt;/p&gt;

&lt;p&gt;ES3 和 ES5 的概念之间的差别其实不大，但却都有一个差别很大的名字，而 ES5 里还添加了一些这样的东西，因为我认为它们对本文的帮助不大却会大大增加理解难度，所以被我去掉了，想要彻底了解的话可以参考：&lt;/p&gt;

&lt;p&gt;&lt;a href="http://dmitrysoshnikov.com/ecmascript/es5-chapter-3-1-lexical-environments-common-theory/" rel="nofollow" target="_blank"&gt;http://dmitrysoshnikov.com/ecmascript/es5-chapter-3-1-lexical-environments-common-theory/&lt;/a&gt;
&lt;a href="http://ecma262-5.com/ELS5_HTML_with_CorrectionNotes.htm#Section_10.2" rel="nofollow" target="_blank"&gt;http://ecma262-5.com/ELS5_HTML_with_CorrectionNotes.htm#Section_10.2&lt;/a&gt;&lt;/p&gt;
&lt;h3 id="运行阶段"&gt;运行阶段&lt;/h3&gt;
&lt;p&gt;在 function 后面 { 和 } 之间的源代码在浏览器转换过后会变成一个 语句列表（StatementList），而它的每一行都是一条语句（Statement），function 的运行其实就是对这一条条语句依次 evaluate（求值）的过程。&lt;/p&gt;

&lt;p&gt;之所以取名叫 evaluate，可能是因为计算机实质上只会做两件事：对值进行 转换（运算）和 传输（载入/保存）。&lt;/p&gt;

&lt;p&gt;【转换】
转换通常是通过操作符（operator）实现的，最常见的是 + 和 - 等，比较特殊的是==和！等，比较少见的是&amp;gt;&amp;gt;和 typeof 等。 &lt;/p&gt;

&lt;p&gt;而因为 Javascript 源代码是以 UTF-16 为格式的一长串字母和符号（string），比如"123"其实是 3 个 16 位的"字母" 连成的，所以它在运算前会先转成 IEEE 754 格式的 64 位浮点数。（而它最小可以转成一个 8 位的整数）
undefined、true、false、数字（45092，1.345 等）、string（"abc", "a bc defg"等）还有“{ ... }”和“[ ... ]”，这种 UTF-16 格式的值可以直接转换为它真正的值的“词语”被术语称为 literal（字面的）。&lt;/p&gt;

&lt;p&gt;除了 literal 以外，还有两种“词语”会在运算前作转换：&lt;/p&gt;

&lt;p&gt;【a.变量】
变量，其实就是一个 named value（命名了的值），对变量的转换就是把它的名字变成它的值，方法是去查当前变量表，如果查到，则它的值为绑定的内存地址里的那个值；如果没查到，就去查上层变量表（[[scope]]），如果 [[scope]] 为空（null）则值它 is not defined。&lt;/p&gt;

&lt;p&gt;最近新出版了一本书花了 90 多页专门讲解 scope 和 closure，（不禁让我回想起那些整本都在讲 C 语言指针的书）在这里我只用 2 段话：&lt;/p&gt;

&lt;p&gt;scope 即变量表，包括当前的和所有上层的。（scope 中文可以翻译为 一眼可以看到的景物，对于 function 来说，scope 就是 一眼可以看到的变量）&lt;/p&gt;

&lt;p&gt;closure，宽泛地说，是 function 这个概念的一种实现，属于变量的 function 就是 closure，即像其它变量一样可传递和返回的 function；（在静态语言的实现中，function 无法传递和返回，因为它们的 function 只有在执行时才得到它需要的变量，执行完后这些变量就消除，而 Javascript 中 function 在声名时便生成了一个上层变量表（[[scope]]），只要 function 不消除这个 [[scope]] 就不会消除）而具体地说，为了与 Javascript 中的普通 function 区分，closure 特指那些访问了 [[scope]] 中的变量 的 function。&lt;/p&gt;

&lt;p&gt;【b.property】
当浏览器看到 a.b 的时候，它不会去变量表里查 b 的值，因为 b 是 a 的一个 property。property 英文意思除了属性外，更主要的意思是财产，或者说附属品，因此在编程里它的意思可以理解为“附属变量”。&lt;/p&gt;

&lt;p&gt;要理解 property，首先要理解 object。如果说 property 对应的是文件，那么 object 对应的就是文件夹，所谓的“一切皆 object”，就是把所有的变量都放到一个 object 里。虽然智商正常的人应该都不会说：“硬盘里的一切皆文件夹”。&lt;/p&gt;

&lt;p&gt;正确一点的说法是：“一切皆由 object 组织“。因为 object 可以理解为一种结构，从图像思维上看，多个 object 就会形成一个树状的结构，而这种结构可以用来组织任意数量的人或物，比如现实里的物种/图书/商品的分类，军队/公司/政府的编制等等。&lt;/p&gt;

&lt;p&gt;计算机里的数据假如不这么组织，那么一个纯粹的 string/array/function 就只有它自己，没有 property，这样虽然占用的空间少，但像 array.length 或 array.foreach(...) 这样方便的操作就无法实现，更不用说 a.b.c.d 这样的链式调用了。&lt;/p&gt;

&lt;p&gt;object，这里暂称它为“变量夹”，特殊的地方在于它里面的变量可以“继承”，使得在子变量夹里可以获取到父变量夹里的变量，但通常一个父变量夹里既有需要被继承的变量，也有不需要被继承的，因此 Javascript 可以在父变量夹里建一个叫 prototype 的变量夹，用来存放那些需要被继承的变量。之后子变量夹里会自动建一个叫 [[proto]] 的内部变量，它的值便是它父变量夹的 prototype。（的内存地址）&lt;/p&gt;

&lt;p&gt;任何 object 的 prototype 都可以赋值或更改，但 [[proto]] 是内置的无法直接更改，它取决于 object 的创建方式：&lt;/p&gt;

&lt;p&gt;var o = { ... }
o.[[proto]] 指向 Object.prototype，这个 Object 是 Javascript 里一个全局 object 变量的名字，所有 Javascript 里的 object 都默认以它为原型。&lt;/p&gt;

&lt;p&gt;var o = new Xxx( ... )
o.[[proto]] 指向 Xxx.prototype。&lt;/p&gt;

&lt;p&gt;var o =  Object.create( Xxx, { ... } )
o.[[proto]] 指向 Xxx，Xxx 可以是 null 也可以是任意 object。&lt;/p&gt;

&lt;p&gt;所谓的继承，就是当浏览器在当前变量夹找不到某个 property 时，会自动去它的 [[proto]] 里面找，依此类推，直到 [[proto]] 为 null 为止（property 与变量的查找原理一模一样，这就是为什么 ES3 里会用 VariableObject 来表示变量表）&lt;/p&gt;

&lt;p&gt;比如浏览器在 evaluate a.b 时，会先去变量表找 a 的内存地址，而它值就是一个变量夹，然后再到这个变量夹里找 b。而如果是 a[b] 这种形式，则是会先转换 b 的值（evaluate），这个值如果不是字符串（string）会强制转换为字符串，然后再到 a 变量夹里找这个字符串对应的变量。&lt;/p&gt;

&lt;p&gt;【传递】
计算机语言中最误导人的运算符无疑是 =，它其实不是在表达一种相等的关系，而是把右边求得的值传递到左边求得的 内存地址 上。我想一个更恰当的符号应该是 ←，比如 a ← b，a ← 3+5。&lt;/p&gt;

&lt;p&gt;而值传递在 Javascript 中具体还分为两种，直接和间接传递。直接传递通常用于数字或可以用数字表示的值，比如 true/false 和内存地址（假如 CPU 是 32 位则内存地址就是一个 32 位的 2 进制数，64 位 CPU 则是 64 位，依此类推），而比较大的、无法用一个数表示的值，则不会被直接传递，而是只传递这个值所在的内存地址。比如&lt;/p&gt;

&lt;p&gt;a = { a:1,b:2,c:3 }&lt;/p&gt;

&lt;p&gt;这里浏览器在把 { a:1,b:2,c:3 } 转换成对应的值后，会返回这个值所在的内存地址，亦即一个数字，然后把这个数字传递给 a 在变量表里对应的内存地址。&lt;/p&gt;

&lt;p&gt;内存地址之所以是个数字，是因为某种程度上说内存里的每一个字节（byte）都有一个编号，一个 1GB（giga byte）的内存就有 10 亿以上（2 的 20 次方）个编号，而在硬件底层有一个类似于 getMem(编号，大小) 的机器指令，假如编号是 123，大小是 20，那么这个指令就会把编号 123 到 142 里的 20 个字节取出来。（一台电脑主要有 3 块内存：CPU 内存，显卡内存，声卡内存；想运行软件就要往 CPU 内存传递数据，想播放声音就要往声卡内存传递数据，想显示图像就要往显卡内存传递数据）&lt;/p&gt;

&lt;p&gt;Javascript 中除了 =，还有一些其它的值传递，它们理论上说都应该改为用←来表示：
{ a: 1,b: 'abc' } ：{ a← 1,b← 'abc' }
a(1,'abc') : a← 1,'abc'
return 1 : 1 →&lt;/p&gt;

&lt;p&gt;CPU 里有一个寄存器（功能与内存一样，但更快也更贵，只用来存放最经常转换/传递的值），它的值是当前正在执行的机器指令的内存地址，当浏览器往这里传递一个 代表内存地址的数字 时，程序就会跳转到那个地址开始继续执行里面的指令，所以 if、while、switch 等语句实质上也是在传递值。&lt;/p&gt;

&lt;p&gt;具体 Javascript 里共有多少种操作符和语句，可以参考 &lt;a href="http://es5.github.io/#x11" rel="nofollow" target="_blank" title=""&gt;Expressions&lt;/a&gt; 和 &lt;a href="http://es5.github.io/#x12" rel="nofollow" target="_blank" title=""&gt;Statements&lt;/a&gt;。&lt;/p&gt;

&lt;p&gt;【理论与实践】
这篇文章里总结了我认为 Javascript 中最重要的理论知识，应该说理解之后除了 EMCA 标准外就不用再看理论相关的书或文章了，这时应把时间花在动手实践上，去熟悉和掌握 原生的 DOM API、第 3 方的库/框架/插件、代码的组织与测试等等。&lt;/p&gt;

&lt;hr&gt;</description>
      <author>daroson</author>
      <pubDate>Fri, 09 May 2014 19:25:27 +0800</pubDate>
      <link>https://ruby-china.org/topics/19135</link>
      <guid>https://ruby-china.org/topics/19135</guid>
    </item>
  </channel>
</rss>
