你不知道的JavaScript————作用域和闭包篇

编译原理

为什么要把这个放在重点呢?因为每门语言的最底层,那就是编译成机器语言了。了解编译原理。对理解语言的特殊现象有很大帮助。
先说说其他非脚本语言开始到结束。我在之前的计算机系统基础的篇目中学到C的编译。变成汇编语言之后,每个函数名字,每个变量名字,都会写入到一张表里面。而这张表,是将所有的变量放置在一起。查找匹配相应的变量,并寻找其变量地址。
应该是所有的语言都是相似的。在了解过javascript编译原理之后。发现这很多相似的地方。

分词/词法分析

将字符组成的字符串分解成有意义的代码块,代码块统称为词法单元
例如:在 var a = 2。将会分解成 var, a, =, 2

解析/语法分析

这个过程是将词法单元流(数组)转换成一个由元素逐级嵌套所组成的代表程序语法结构的树(抽象语法树)。
例如:var a = 2这个代码中,他可能有一个父节点,其本身节点a,其值为2

代码生成

将抽象语法树转换成可执行代码的过程统称为代码生成。
例如:var a = 2,创建一个a的变量,并且储存一个值为2在a中

ps:javascript中远复杂的多,在语法分析和代码生成阶段有特定性能进行优化。一般而言,javascript为了保证高效的执行代码,通常是函数片段执行钱然后进行编译。以保证代码性能的最佳

作用域理解

引擎、编译器、作用域。作用域的理解并不是那么简单。他包含引擎的查询执行,以及编译器编译。

编译器的处理

首先,编译器遇到var a,编译器会询问同一个作用域的集合中是否存在该变量。如果是,编译器会忽略该声明, 继续编译。否则它会要求作用域在当前作用域的集合中声明一个新的变量,并命名为a。
接下来,编译器会为引擎生成运行时所需的代码,这些代码被用来处理a = 2这个赋值操作。引擎运行时会首先询问作用域,在当前的作用域集合中是否存在一个叫作a的变量。如果是,引擎就会使用这个变量;如果否,引擎会继续查找该变量(从作用域链中)。如果引擎最终找到了a变量,就会将2赋值给它。否则引擎就会举手示意并抛出一个异常!
总结:变量的赋值操作会执行两个动作,首先编译器会在当前作用域中声明一个变量(如果之前没有声明过),然后在运行时引擎会在作用域中查找该变量,如果能够找到就会对它赋值。也正因为赋值是分开的。将var a和赋值2分开执行,才会导致后面的变量提升

左值(LHS)与右值(RHS)

从字面来看,有左值和右值之分。所谓左值,在等号的左侧;所谓右值,就在等号的右侧。即:左侧是被赋值,右侧是查询。当然,所有的查询都可以当作一次右值(至少我是这么理解的)。
而引擎和作用域则是如下工作的:
以下面为例

1
2
3
4
function foo (a) {
console.log(a)
}
foo(2)

1、引擎查询作用域中是否有foo
2、作用域查询到foo地址,将地址传给引擎
3、引擎执行foo,并查询作用域中是否有a
4、作用域查到a的地址,将其传给引擎
5、引擎给a赋值,并查询作用域中是否有console
6、作用域查到console地址,将其传给引擎
7、引擎使用console,并查询其log方法,并且查询作用域中的a是否改变
8、作用域查询a的值
9、引擎使用console.log方法,并将a的值传入

ps:1、正因为是直接从当前作用域开始查询,所以会有作用域屏蔽,当前作用域的变量会屏蔽上层同名变量。2、因为函数声明中,其名称也是变量,也导致了同一个作用域中后者函数会覆盖掉前者函数。3、所谓的作用域就是建表,当前作用域下所有的变量都会存入表中。待查询需要,直接查表即可。4、未声明的变量,查表后未发现变量,会抛出引用错误

欺骗词法

这是一种在运行时“修改”作用域的词法。因此也叫欺骗词法

EVAL
1
2
3
4
5
6
function foo (str, a) {
eval(str) // 欺骗……
console.log(a, b)
}
var b = 2
foo("var b = 3, 1")

eval的作用是将字符串转化成可执行的代码块。因此在执行eval代码时,前面的代码是以动态的插入进来的,达到词法作用域的修改。在严格模式之下,eval在运行时有自己的词法作用域,因此意味着在声明中无法修改所在的作用域。

SETTIMEOUT

setTimeout第一个参数也是可以传入一个字符串。他将会默认将字符串转化成代码快。这种虽然稍微更安全一些(不会修改)但是也是要少用。避免使用

ps: 上述两者都有性能问题。由于在预编译时,javascript将所有的变量进行提升,在代码执行前写入作用域,大多数都是在函数执行前进行编译。而使用上述两种情况之后。由于无法确定当前作用域中是否有该种情况,于是将不会进行编译。而是在当前代码块执行编译的时候,才会进行作用域的写入,调节。这种情况将所有的javascript代码,引擎无法进行优化,因此性能极低。所以不推荐使用

WITH

在严格模式之下使用报错。不推荐使用。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
// 好处
var obj = {
a:1,
b:2,
c:3
}
// 赋值很麻烦
obj1.a = 2
obj1.b = 3
obj1.c = 4
// 赋值相对快捷
with(obj) {
a = 3
b = 4
c = 5
}
// bad use
function foo(obj) {
with(obj){
a = 2
}
}
var o1 = {
a:3
}
var o2 = {
b:3
}
foo(o1)
console.log(o1.a) // 2
foo(o2)
console.log(o2.a) // undefined
console.log(a) // 2

出现上述原因是什么?
在非严格模式中,第一种情况o1出现的原因是因为在当前作用域之下有a的这个属性,因此将a赋值过去。
第二种情况是因为由于o2中并没有找到a这个属性,而出现一种左值赋值操作。因此将a赋值给2,而a在非var情况之下赋值,变成全局变量出现。

javascript在es6之前除try-catch没有块级作用域

什么是块级作用域?之前我有写过一篇文章。
所谓块级作用域就是在打括号的包裹之下,里面的变量不外泻。即:

1
2
3
4
{
var a = 0
}
console.log(a) // 0

能在外部访问到的,都不是块级作用域。
因此在es5之前,对于变量的使用,都要尽可能的使用var来达到变量不会冲突的情况。不然很可能使用到上一级的变量,导致出错。

ES6中的LET

出现let之后,便有了块级作用域。这种情况为javascript更容易

提升

文章之前也提到过。由于变量在引擎中是一个先写入作用域中,再将变量赋值的一个过程。所以有奇妙的提升。
例如:

1
2
3
4
foo()
function foo () {
console.log('hello')
}

在其他语言中,这样写代码是会报错的,但是在javascript中则不会。因为函数声明中,函数名称是一个变量。函数表达式则不会。在首先代码编译阶段,foo函数首先被提升到作用域中->然后执行代码。foo函数->引擎中发现作用域中有foo函数->引擎执行foo函数

闭包

闭包是为了函数外部使用函数内部变量,出现的一个名词

1
2
3
4
5
6
7
8
9
function foo() {
var a = 2;
function bar () {
console.log(a)
}
return bar
}
var baz = foo()
baz() // 2

这种就是闭包。闭包的详细,之前重读javascript一书中有写过。
ps: 所有的javascript的回调都是闭包

模块机制

之前使用模块机制,是框架中代码写入的。而现代机制使用commonjs的规范使用的。

小结:

学习到javascript第一章之后,发现很多javascript的一些现象可以通过底层来解释,真是太棒了!很开心的学玩了这一章节,明白了性能问题出现的原因,以及词法作用域的底层原理。还有javascript代码的执行。不禁感叹v8引擎的强大,对javascript的优化简直棒极了!对深入学习javascript又更上一层楼