几乎全部编程语言最基本的功能之一,就是可以储存变量当中的值,而且能在以后对这个 值进行访问或修改。事实上,正是这种储存和访问变量的值的能力将状态带给了程序。
接下来咱们介绍两个概念: LHS 查询和 RHS查询
我打赌你必定能猜到“L”和“R”的含义,它们分别表明左侧和右侧。
什么东西的左侧和右侧?是一个赋值操做的左侧和右侧。
换句话说,当变量出如今赋值操做的左侧时进行 LHS 查询,出如今右侧时进行 RHS 查询。编程
讲得更准确一点,RHS 查询与简单地查找某个变量的值别无二致,而 LHS 查询则是试图找到变量的容器自己,从而能够对其赋值。从这个角度说,RHS 并非真正意义上的“赋 值操做的右侧”,更准确地说是“非左侧”。编程语言
你能够将 RHS 理解成 retrieve his source value(取到它的源值),这意味着“获得某某的 值”。
让咱们继续深刻研究。
请看如下代码:console.log( a );
函数
其中对 a 的引用是一个 RHS 引用,由于这里 a 并无赋予任何值。相应地,须要查找并取得 a 的值,这样才能将值传递给 console.log(..)。性能
相比之下,例如:优化
a = 2;
code
这里对 a 的引用则是 LHS 引用,由于实际上咱们并不关心当前的值是什么,只是想要为 =2 这个赋值操做找到一个目标。对象
LHS 和 RHS 的含义是“赋值操做的左侧或右侧”并不必定意味着就是“= 赋值操做符的左侧或右侧”。赋值操做还有其余几种形式,所以在概念上最 好将其理解为“赋值操做的目标是谁(LHS)”以及“谁是赋值操做的源头(RHS)”。ip
做用域是一套规则,用于肯定在何处以及如何查找变量(标识符)。若是查找的目的是对变量进行赋值,那么就会使用 LHS 查询;若是目的是获取变量的值,就会使用 RHS 查询。作用域
赋值操做符会致使 LHS 查询。=操做符或调用函数时传入参数的操做都会致使关联做用域 的赋值操做。字符串
JavaScript引擎首先会在代码执行前对其进行编译,在这个过程当中,像var a = 2这样的声 明会被分解成两个独立的步骤:
1. 首先,var a 在其做用域中声明新变量。这会在最开始的阶段,也就是代码执行前进行。
2. 接下来,a = 2 会查询(LHS 查询)变量 a 并对其进行赋值。
不管函数在哪里被调用,也不管它如何被调用,它的词法做用域都只由函数被声明时所处 的位置决定。
若是词法做用域彻底由写代码期间函数所声明的位置来定义,怎样才能在运行时来“修改”(也能够说欺骗)词法做用域呢?
JavaScript 中有两种机制来实现这个目的。
eval和with
JavaScript 中的 eval(..) 函数能够接受一个字符串为参数,并将其中的内容视为好像在书 写时就存在于程序中这个位置的代码。换句话说,能够在你写的代码中用程序生成代码并 运行,就好像代码是写在那个位置的同样。
在执行 eval(..) 以后的代码时,引擎并不“知道”或“在乎”前面的代码是以动态形式插 入进来,并对词法做用域的环境进行修改的。引擎只会如往常地进行词法做用域查找。
eval(..) 调用中的 "var b = 3;" 这段代码会被看成原本就在那里同样来处理。因为那段代 码声明了一个新的变量 b,所以它对已经存在的 foo(..) 的词法做用域进行了修改。事实 上,和前面提到的原理同样,这段代码实际上在 foo(..) 内部建立了一个变量 b,并遮蔽 了外部(全局)做用域中的同名变量。
当 console.log(..) 被执行时,会在 foo(..) 的内部同时找到 a 和 b,可是永远也没法找到 外部的 b。所以会输出“1, 3”而不是正常状况下会输出的“1, 2”。
JavaScript中 还 有 其 他 一 些 功 能 效 果 和eval(..)很 相 似。setTimeout(..)和 setInterval(..) 的第一个参数能够是字符串,字符串的内容能够被解释为一段动态生成的 函数代码。这些功能已通过时且并不被提倡。不要使用它们!
在程序中动态生成代码的使用场景很是罕见,由于它所带来的好处没法抵消性能上的损 失。
词法做用域意味着做用域是由书写代码时函数声明的位置来决定的。编译的词法分析阶段 基本可以知道所有标识符在哪里以及是如何声明的,从而可以预测在执行过程当中如何对它 们进行查找。JavaScript 中有两个机制能够“欺骗”词法做用域:eval(..) 和 with。前者能够对一段包 含一个或多个声明的“代码”字符串进行演算,并借此来修改已经存在的词法做用域(在 运行时)。后者本质上是经过将一个对象的引用看成做用域来处理,将对象的属性看成做 用域中的标识符来处理,从而建立了一个新的词法做用域(一样是在运行时)。
这两个机制的反作用是引擎没法在编译时对做用域查找进行优化,由于引擎只能谨慎地认 为这样的优化是无效的。使用这其中任何一个机制都将致使代码运行变慢。不要使用它们。
var a = 2; function foo() { // <-- 添加这一行 var a = 3; console.log( a ); // 3 } // <-- 以及这一行 foo(); // <-- 以及这一行 console.log( a ); // 2
虽然这种技术能够解决一些问题,可是它并不理想,由于会致使一些额外的问题。首先, 必须声明一个具名函数 foo(),意味着 foo 这个名称自己“污染”了所在做用域(在这个 例子中是全局做用域)。其次,必须显式地经过函数名(foo())调用这个函数才能运行其 中的代码。
var a = 2; (function foo(){ // <-- 添加这一行 var a = 3; console.log( a ); })(); // <-- 以及这一行 3 console.log( a ); // 2
区分函数声明和表达式最简单的方法是看 function 关键字出如今声明中的位 置(不只仅是一行代码,而是整个声明中的位置)。若是 function 是声明中 的第一个词,那么就是一个函数声明,不然就是一个函数表达式。
函数声明和函数表达式之间最重要的区别是它们的名称标识符将会绑定在何处。
(function foo(){ .. })做为函数表达式意味着foo只能在..所表明的位置中 被访问,外部做用域则不行。foo 变量名被隐藏在自身中意味着不会非必要地污染外部做 用域。
var a = 2; (function foo() { var a = 3; console.log( a ); // 3 })(); console.log( a ); // 2
因为函数被包含在一对 ( ) 括号内部,所以成为了一个表达式,经过在末尾加上另一个 ( ) 能够当即执行这个函数,好比 (function foo(){ .. })()。第一个 ( ) 将函数变成表 达式,第二个 ( ) 执行了这个函数。
社区给它规定了一个术语:IIFE,表明当即执行函数表达式
相较于传统的 IIFE 形式,不少人都更喜欢另外一个改进的形式:(function(){ .. }())。仔 细观察其中的区别。第一种形式中函数表达式被包含在 ( ) 中,而后在后面用另外一个 () 括 号来调用。第二种形式中用来调用的 () 括号被移进了用来包装的 ( ) 括号中。
这两种形式在功能上是一致的。选择哪一个全凭我的喜爱。