编译原理
-
分词/词法分析
这个过程称为分解代码块(词法单元),比如 var a = 2;。这个程序通常会被分解成为下面这些代码块(词法单元): var、a、=、2、;一般情况下空格不会被当作词法单元。
-
解析\语法分析
将词法单元数组转化成语法结构树"抽象语法树(AST)"。
-
代码生成
将AST转换成可执行代码的过程被成为代码生成。就是将一段我们看得懂的代码转化成机器指令。
JavaScript的编译过程不是发生在构建之前的,大部分情况下发生在代码执行钱的几微秒。任何JavaScript代码片在执行之前都要进行编译,因此,JavaScript编译器首先会对var a = 2;这段程序进行编译,然后做好执行它的准备,并且通常马上就会执行它。
什么是作用域
就是能够储存变量当中的值,并且能再之后地这个值进行访问或修改。是一套设计良好的用来储存变量的规则。并且之后可以方便的访问到这些变量。这套规则就成为作用域。
如何工作
引擎
负责整个JavaScript程序的编译及执行过程编译器
负责语法分析及代码生成。作用域
收集维护所有声明的标识符(变量)组成的一系列查询,并实施一套严格的规则,确定当前执行的代码对这些标识符的访问权限。
工作过程
针对var a = 2;这一段代码来看,引擎会认为这里有两个不同的声明,一个是在编译器在编译的时候处理,另一个是引擎在运行的时候处理的。
首先是编译器进行处理:
1.遇到var a,编译器会询问作用域是否已经有一个该名称的变量存在与同一个作用域的集合中。如果是,编译器会忽略该声明,继续进行编译;否则它会要求作用域在当前作用域的集合中声明一个心的变量,命名为a。
2.接下来编译器回味引擎生成运行时所需的代码,这些代码被用来处理 a = 2 这个赋值操作。引擎运行的时候首先询问作用域,在当前的作用域几何中是否存在一个叫做a的变量,如果是,引擎就会使用这个变量;如果不存在这个变量,引擎就会继续查找这个变量,向上查找。之后会讲到如何向上查找
向上查找结束后如果找到了a变量就会将2赋值给它,否则一般情况下就会抛出一个错误(浏览器中需要严格模式),浏览器环境中非严格模式会在全局创建一个a变量。
LHS查询和RHS查询
可以理解成左查询和右查询。赋值操作的左右侧查询(不完全正确)
换句话说,当变量出现在赋值操作的左侧的时候进行LHS查询,出现在非左侧的时候进行RHS查询。更准确的说法就是,RHS查询就相当于是查找某个变量的值,而LHS查询则是是u找到变量的容器本身,从而可以对其赋值。
如下代码:
console.log(a)
其中这里对a是一个RHS引用,因为这里是要查找a的值,而不是查找a容器而去赋值给a。
a = 2
相比如上的代码,这里不用去关心当前的值是什么,我们只要把 = 2 这个赋值操作找到一个目标。
再继续深入:
function foo(a) {
console.log( a ); //2
}
foo(2)
上面的代码中,存在的一个隐式操作,当2被当作参数传递给foo(..)函数时会进行一次a = 2的赋值操作。所以这里需要进行一次LHS查询。当执行到console.log(a)的时候,这里引擎需要console这个对象进行RHS,并且检查得到的值中是否有一个叫做log的方法。log(..)函数再执行的时候假设log函数实现中接受参数,这个参数也需要进行LHS引用。
看如下代码:
function foo(a) {
var b = a;
return a + b;
}
var c = foo(2);
上面的代码中,首先var c = foo(2),会进行一次LHS查询 c 容器。然后回对foo进行一次RHS查询,然后执行foo(2)对函数里的a进行一次LHS查询,然后var b = a;中,对b进行一次LHS查询,对a进行一次RHS查询,然后return a + b; 进行一次对a的RHS查询,进行一次对b的RHS查询。
所以这里这段代码进行了三次LHS查询,四次RHS查询。
作用域嵌套
当一个快或函数嵌套再另一个快或者函数中时,就发生了作用域的嵌套。
在当前作用域中如果无法找打某个变量时,引擎就会在外层嵌套的作用域中继续查找,直到找到该变量,或者一直找到最外层作用域为止。
看以下代码:
var a = 22;
function geta() {
console.log(a)
}
对a进行的RHS查询无法在函数geta中完成,但是可以在上一级作用域中完成。
词法作用域
词法作用域就是定义在词法阶段的作用域,词法作用域是由你在写代码时将变量和块作用域写在哪里来决定的,因此当词法分析器处理代码时会保持作用域不变
function foo(a) {
var b = a * 2;
function bar(c) {
console.log( a, b, c );
}
bar( b * 3);
}
foo(2); // 2, 4, 12
这个例子中有三个逐级嵌套的作用域。如下图
- 全局作用域 : foo
- foo的作用域 : a ,bar ,b
- bar的作用域 : c
查找
在上一段代码中,引擎执行console.log(a ,b ,c),查找a,b,c三个变量的引用。首先是从最内部的bar作用域开始查找,引擎无法在这里找到a,因此会到上一级的foo中查找,再找不到会到全局作用域中查找。
作用域查找会在找到匹配的标识符之后停止。
多层嵌套作用域中定义同名的标识符,就照成了"屏蔽效应"。
函数作用域和块作用域
函数中的作用域
function foo(a) {
var b = 2;
function bar(){
}
var c = 3;
}
在这个代码片段中,foo和bar都拥有着自己的作用域气泡。全局也包含着foo标识符。
a,b,bar,都属于foo的作用域气泡,无法从foo的外部去访问。比如
bar(); //error
console.log(a,b,c); // error
内层是可以访问外层的。
隐藏内部实现
我们对函数传统的认知就是声明一个函数,然后在里面添加代码,反过来想想,从我们所写的代码中选一段出来用函数对他进行包装,实际上就是把这些代码隐藏起来了。
实际上我们是在这片代码周围创建了一个作用域气泡,这段代码中的变量或函数都将绑定在这个新建的作用域气泡中。
var name;
function getName(first, last) {
name = getFirstName(first) + getLastName(last);
console.log(name);
}
function getFirstName(firstName) {
return '帅哥: ' + firstName;
}
function getLastName(lastName) {
return lastName + ' 先生';
}
getName('hong','tao') // 帅哥: hongtao 先生
这段代码中,变量name和函数getFirstName、getLastName应该是getName内部具体实现的 私有内容。给与外部访问name和函数getFirstName、getLastName的权限不仅没有必要,而且是很危险的。
合理的设计会将这些私有的具体内容隐藏在getName内部。
function getName(first, last) {
var name;
function getFirstName(firstName) {
return '帅哥: ' + firstName;
}
function getLastName(lastName) {
return lastName + ' 先生';
}
name = getFirstName(first) + getLastName(last);
console.log(name);
}
getName('hong', 'tao') // 帅哥: hongtao 先生
这样,b和doSomethingElse都没法从外部访问。功能和效果都没有收到影响。在设计上,将具体内容私有化了。
规避冲突
"隐藏"作用域中的变量和函数,可以避免同名标识符之间的冲突,两个标识符可能具有相同的名字但用途却不一样,无意间可能造成命名冲突,冲突会导致变量的值被意外覆盖。
function foo() {
function bar(a) {
i = 3;
console.log(a + i)
}
for (var i = 0; i < 10; i++) {
bar(i * 2); // 无限循环
}
}
foo()
bar()中表达式 i = 3, 意外的覆盖了声明在for循环中的i, 所以for循环中的 i 会无限被设置成 3,从而导致无限循环。但是如果我们在bar的内部声明一个var i = 3; for循环中的i就会被"屏蔽"。或者是在for循环内部 使用 let 声明。关于let 请网上查阅es6资料
匿名函数和具名函数
通俗的说,匿名就是有没有给函数定义名字,而具名函数就是我们平常所定义的那样
如下
setTimeout(function() { // 我是匿名函数
}, 100);
document.body.onclick = function(){ //我是匿名函数
}
setInterval(function foo(){ // 我是具名函数
}, 100)
匿名函数的缺点
- 匿名函数难以追踪,调试困难
- 不能调用自身。如需想引用,可以使用过期的rguments.callee 方法引用
- 代码却上可读性
立即执行函数
其实就是将函数变成表达式,这种模式社区给他规定了一个术语"IIFE",代表立即执行函数表达式(Immediately Invoked Function Expression)
(function (){}())
(function (){})()
// 上面两种方式功能都一样
// 或者你也可以
!function(){}()
+function(){}()
-function(){}()
还有一种比较常见的用法
(function(global){
//...
})(window)
这种方式使得"全局的概念更加清晰"。同时也可以解决undefined标识符的默认值被覆盖导致异常。
undefined = true;
var a;
if( a === undefined) {
console.log(a)
}
解决
undefined = true; // 给其他代码挖了一个大坑!绝对不要这样做!
(function IIFE(undefined) {
var a;
if (a === undefined) {
console.log("Undefined is safe here!");
}
})();
IIFE 还有一种变化的用途是倒置代码的运行顺序,将需要运行的函数放在第二位,在 IIFE
执行之后当作参数传递进去。可查看jquery源码也是这样子使用
(function IIFE(def) {
def(window);
})(function def(global) {
var a = 3;
console.log(a); // 3
console.log(global.a); // 2
});
块作用域
JS中作用域有:全局作用域、函数作用域。没有块作用域的概念。ECMAScript 6(简称ES6)中新增了块级作用域。 块作用域由 { } 包括,if语句和for语句里面的{ }也属于块作用域。
- var定义的变量,没有块的概念,可以跨块访问, 不能跨函数访问。
- let定义的变量,只能在块作用域里访问,不能跨块访问,也不能跨函数访问。
- const用来定义常量,使用时必须初始化(即必须赋值),只能在块作用域里访问,而且不能修改。
看以下代码
for (let i = 0; i < 5; i++) {
i = 1;
console.log(i++)
}
上面的代码中,陷入了无限循环,应该变量i始终被赋值为1;再看
for (let i = 0; i < 5; i++) {
let i = 1;
console.log(i++)
}
上面代码输出了五次 1,说明了for()循环中,括号()中有自己的作用域,大括号{}中也存在着自己的作用域,并且{}的作用域的上级作用域是()中的作用域
var 不存在块级作用域
for (var i = 0; i < 5; i++) {
var i = 1;
console.log(i)
}
上面的代码执行后,照成了无限的循环。原因是{}内的变量i暴露在了外面,所以i变量就会一直被赋值为1,循环一直进行。要解决也很简单,我们直接将变量i进行封装
for (var i = 0; i < 5; i++) {
(function () {
var i = 1;
console.log(i)
})()
}
一些面试题中经常考到
看下面这道题
for (var i = 0; i < 5; i++) {
setTimeout(() => {
console.log(i)
}, 200);
}
稍微有些基础的人回答肯定是打印五个五,如果基础比较弱的话,会回答打印0-4
由于setTImeout是一个异步函数,而for()循环又是很快的,甚至几微秒的时间内就执行完毕了,当我们的for()循环执行完毕之后,会添加五个异步的函数到回调的事件队列中去。此时的i已经变成了5,有人可能会疑惑,为什么i不是4而是5,因为我们当i=4的时候满足了for()循环的条件,之后执行了一次i++所以变成了5;此时回调事件调用函数的时候for()循环作用域内的变量i此时等于5;
再稍加改造一下
for (var i = 0; i < 5; i++) {
setTimeout((i) => {
console.log(i)
}, 200);
}
这就要需要更深入的去了解才能知道答案了,这段代码打印的是五个undefined,我们可以观察,在setTImeout()里面是一个匿名函数,关于匿名函数,你可以往上翻阅,之前我有提到过。
(i) => {
console.log(i)
}
上面这段代码就是每次延迟200毫秒执行一次,当然 你可以 这样子来理解。
for (var i = 0; i < 5; i++) {
两百毫秒后执行 function(i) {
console.log(i)
}
}
当你执行的时候,这个匿名函数并没有接受到什么参数,所以这个i在这个匿名函数内部就会是undefined。
总结
声明一个函数可以有效的将内部的代码隐藏起来。
es6中新增了块级作用域的概念。let,const关键字声明的变量,会将变量添加到当前块中。
根据这些特性能让我们创造可读,可维护的代码。
注意 : 从 ES3 开始, try/catch 结构在 catch 分句中具有块作用域
注:本文是对《你不知道的JavaScript》这本书的一些学习总结和自身看法