JavaScript中作用域相关的那些点

longwenli7 7年前
   <p>本文为《你不知道的JavaScript(上卷)》中关于作用域相关的知识点的总结。</p>    <h2>作用域</h2>    <h3>赋值操作</h3>    <p>变量的赋值操作实际上有两个动作,首先编译器会在当前作用域中声明一个变量(如果之前没有声明过),然后在运行时引擎会在作用域中查找该变量,如果能够找到就对它进行赋值。</p>    <p>看完这段话,我相信你一定想到了前端入门时,一定会接触的一个名词“变量提升”。</p>    <p>举个最简单的例子:</p>    <pre>  <code class="language-javascript">alert(a); // undefined  var a = 12;</code></pre>    <p>有同样作用的是 function ,例如:</p>    <pre>  <code class="language-javascript">alert(func); // function func(){}  function func() {};</code></pre>    <p>但是函数表达式不会提升:</p>    <pre>  <code class="language-javascript">foo(); // TypeError    var foo = function bar() {      ...  }</code></pre>    <p>注意:仅有 var 和 function 这两个关键字才可以变量提升。</p>    <p>ES6 中新增的 let 以及 const 关键字不可以进行变量提升,我们可以尝试一下:</p>    <pre>  <code class="language-javascript">// 1. let  alert(a); // Uncaught ReferenceError: a is not defined  let a = 'abc';    // 2. const  alert(b); // Uncaught ReferenceError: b is not defined  const b = 123;</code></pre>    <p>LHS以及RHS</p>    <p>在运行时引擎会在作用域中查找该变量</p>    <p>引擎对变量所做的查找分为 LHS查询 以及 RHS查询 , L 和 R 分别代表一个赋值操作的左侧以及右侧。</p>    <p>我们可以简单的记忆:</p>    <p>当变量出现在赋值操作的左侧时进行 LHS查询 ,出现在赋值操作的右侧时进行 RHS查询 .</p>    <p>注意:作用域查找会在找到第一个匹配的标识符时停止</p>    <h3>作用域嵌套</h3>    <p>作用域是根据名称查找变量的 <strong>一套规则</strong></p>    <p>作用域嵌套的定义如下:</p>    <p>当一个块或者函数嵌套在另一个块或函数中时,就发生了作用域的嵌套。</p>    <p>理解作用域嵌套这一机制,我们就可以理解变量查找的顺序:</p>    <ol>     <li> <p>在当前作用域查找变量。如果没有,则进行下一步</p> </li>     <li> <p>判断是否是全局作用域。如果是,则停止查找过程;如果不是,则进行下一步</p> </li>     <li> <p>进入当前作用域的外层作用域,并进行第一步</p> </li>    </ol>    <p>形象一点,我们可以把作用域查找想象成在大楼中找人。</p>    <p>第一层代表当前作用域,大楼的顶层代表全局作用域。</p>    <p>首先在当前楼层查找,如果没有找到,则上一楼进行查找,一直到找到这个人或者找完整个大楼依然没有找到为止。</p>    <h3>异常报错的种类</h3>    <p>如果能将 LHS 以及 RHS 进行很好的区分,那我们就能够很好的理解浏览器所抛出的各种异常。</p>    <p>下举几种特别常见的报错:</p>    <ul>     <li> <p>ReferenceError :</p> <pre>  <code class="language-javascript">RHS  LHS  </code></pre> </li>     <li> <p>TypeError :</p>      <ol>       <li> <p>RHS 找到该变量值,但尝试对这个变量的值进行不合理的操作(例如,引用 null 或者 undefined 类型的值中的属性)</p> </li>      </ol> </li>    </ul>    <h2>词法作用域</h2>    <p>词法作用域完全由写代码期间函数所声明的位置来定义</p>    <h3>欺骗词法作用域</h3>    <p>注意:欺骗词法作用域会导致性能下降</p>    <p>eval</p>    <p>eval() 是一个危险的函数, 他执行的代码拥有着执行者的权利。如果你运行eval()伴随着字符串,那么你的代码可能被恶意方(不怀好意的人)影响, 通过在使用方的机器上使用恶意代码,可能让你失去在网页或者扩展程序上的权限。更重要的是,第三方代码可以看到作用域在某一个eval()被调用的时候,这有可能导致一些不同方式的攻击。相似的Function就是不容易被攻击的。</p>    <p>with</p>    <p>根据你所传递给它的对象凭空创建了一个 <em>全新的词法作用域</em></p>    <p>性能问题</p>    <p>欺骗词法作用域会导致性能下降,其原因在于 <strong>编译阶段的性能优化不起作用</strong> 。</p>    <p>JavaScript引擎会在编译阶段进行数项的性能优化。其中的某些优化依赖于能够根据代码的词法进行静态分析,并预先确定所有变量和函数的定义位置,才能在执行的过程中快速找到标识符。</p>    <p>但是,编译到含有 eval 和 with 的代码时,编译器无法知道 eval 或者 with 会接受什么代码,自然无法做代码优化。</p>    <h2>函数作用域以及块作用域</h2>    <p>函数作用域:属于这个函数的全部变量都可以在整个函数的范围内使用及复用(事实上在嵌套的作用域中也可以使用)。</p>    <h3>隐藏组件内部实现</h3>    <p>开发者最主要是利用函数作用域实现隐藏组件或者API的内部实现,最小限度的暴露必要内容。</p>    <p>比如对于一些组件的开发,大家习惯于利用立即执行函数 (function() {})() 进行内部实现的封装。</p>    <h3>规避冲突</h3>    <p>利用函数作用域将变量保持在私有、无冲突的作用域中,这样可以有效规避掉所有的冲突。</p>    <p>举个例子, underscore 这个库里面有跟原生js一样的方法 map ,那怎么区分这两个方法呢?通过将 map 当做一个属性挂载在 underscore 上面,这样可以避免两者的冲突。</p>    <h3>立即执行函数表达式</h3>    <p>形式如下:</p>    <pre>  <code class="language-javascript">(function() {...})()  (function() {...})()  </code></pre>    <p>上面两种形式没有区别,可依个人兴趣随意使用。</p>    <p>立即执行函数表达式的一种进阶用法就是把它们当做函数调用并传递参数进去。</p>    <p>各种类库常见的用法是:</p>    <pre>  <code class="language-javascript">(function(global) {      ...  })(window)</code></pre>    <h3>块作用域</h3>    <p>块作用域目前在 ES6 中有如下体现:</p>    <ol>     <li> <p>let</p> </li>     <li> <p>const</p> </li>     <li> <p>with :用 with 从对象创建出的作用域仅在 with 声明而非外部作用域中有效。</p> </li>     <li> <p>try/catch : catch 分句会创建一个块作用域,其中声明的变量仅在 catch 内部有效。</p> </li>    </ol>    <p>例如:</p>    <pre>  <code class="language-javascript">for (let i; i < 4; i ++) {      ...  }    console.log(i) // Uncaught ReferenceError: i is not defined</code></pre>    <pre>  <code class="language-javascript">try {      undefined();  } catch (err) {      console.log(err);  }    console.log(err); // Uncaught ReferenceError: err is not defined</code></pre>    <h2>提升</h2>    <h3>函数优先</h3>    <p>先来看下面的代码:</p>    <pre>  <code class="language-javascript">foo(); // 1  var foo;    function foo() {      console.log(1);  }    foo = function() {      console.log(2);  }</code></pre>    <p>上面的例子说明:</p>    <p>函数会被首先提升,然后才是变量</p>    <p>上面的代码实际等于:</p>    <pre>  <code class="language-javascript">function foo() {      console.log(1);  }    foo(); // 1    var foo;    foo = function() {      console.log(2);  }</code></pre>    <h2>作用域闭包</h2>    <p>知乎上面有关于闭包的问题: 什么是闭包?</p>    <p>其中寸志老师的解释我认为是比较好的。</p>    <p>对于闭包,《你不知道的JavaScript(上卷)》这本书的解释是:</p>    <p>当函数可以记住并访问所在的词法作用域时,就产生了闭包。</p>    <p>我们实际上来理解闭包时,需要特别注意是两个点: 函数 和 作用域 。</p>    <p>简单的来说,就是函数以及作用域的结合,注意,作用域必须是封闭的,其主要的表现形式就是函数中返回一个函数。</p>    <p>闭包在类库、组件封装中有太多的示例了,本文就不拓展了。</p>    <h3>块作用域与闭包的结合</h3>    <p>首先看一个单纯的闭包的代码:</p>    <pre>  <code class="language-javascript">for (var i = 0; i <= 5; i++) {      (function() {         var j = i;         setTimeout(function timer(){             console.log(j);         }, j * 1000)       })()  }</code></pre>    <p>这段代码就是在每次循环的时候创建一个新的封闭作用域,保存当次循环的i值。</p>    <p>再看一下下面的代码:</p>    <pre>  <code class="language-javascript">for (let i = 0; i <= 5; i++) {      setTimeout(function timer(){          console.log(i);      }, i*1000)  }</code></pre>    <p>利用let创建块作用域,当块作用域与闭包结合之后,我们可以减少创建新的封闭作用域这一操作( var j = i );</p>    <p>that's cool!</p>    <h3>模块</h3>    <p>模块这一利器,在以前封装插件用的非常多,示例如下:</p>    <pre>  <code class="language-javascript">var foo = (function CoolModule() {      var something = "cool";      var another = [1, 2, 3];            function doSomething() {          console.log(something)      }            function doAnother() {          console.log(another.join("!"));      }            return  {          doSomething: doSomething,          doAnother: doAnother      }  })()</code></pre>    <p>模块模式必备条件如下:</p>    <ol>     <li> <p>必须有外部的封闭函数,该函数必须至少被调用一次(每次调用都会创建一个新的莫模块实例)。</p> </li>     <li> <p>封闭函数必须返回至少一个内部函数,这样内部函数才能在私有作用域中形成闭包,并且可以访问或者修改私有的状态。</p> </li>    </ol>    <p>当然,说到模块,我们不得不提到 CMD 、 AMD 、 ES6 module 等模块机制了。</p>    <p>我这里简单提一下两者的区别:</p>    <ul>     <li> <p>AMD:</p>      <ul>       <li> <p>early executing(提前执行)</p> </li>       <li> <p>推荐依赖前置</p> </li>       <li> <p>示例: requireJs</p> </li>      </ul> </li>     <li> <p>CMD:</p>      <ul>       <li> <p>as lazy as possible(延迟执行)</p> </li>       <li> <p>推荐依赖就近</p> </li>       <li> <p>示例: seaJs</p> </li>      </ul> </li>    </ul>    <p>继续聊一下 ES6 的模块机制( import 、 export )。</p>    <p>import 可以将一个模块中的一个或多个API导入到当前的作用域中,并分别绑定在一个变量上。</p>    <p>export 会将当前模块的一个标识符(变量、函数)导出为公共API。</p>    <p>Github 有很多基于 es6 实现的代码功能,请自行查阅。</p>    <h3>动态词法作用域</h3>    <p>动态作用域链是基于调用栈的,而不是代码中的作用域嵌套。</p>    <p>对于 JavaScript ,不存在动态作用域。如果一定要找一个点与动态词法作用域扯上关系的话,那就是 this 值了。 </p>    <p>好了,作用域相关的点整理完了,如果有遗漏,欢迎指正~</p>    <p> </p>    <p>来自:https://segmentfault.com/a/1190000008501331</p>    <p> </p>