说明:本系列是本人读完《你不知道的javascript》的总结和感想。有兴趣的读者可以去看原书籍。
一. 作用域
编译原理
虽然通常把javascript分类为“动态”或“解释执行”语言,但事实上是一门编译语言。
对于传统编译语言,一段源代码在执行之前会经历三个步骤:
- 分词/词法分析
- 解析、语法分析
- 代码生成
相对只有这三个步骤的语言,javascript引擎要复杂的多。例如其在分词等有特定的步骤对运行性能进行优化。
这里我们并不对javascript引擎的编译原理进行深入的探讨,有兴趣的读者可以谷歌查询更详细的资料。
这里我们只需要知道任何javascript代码片段在执行前都要进行编译。理解作用域
例:处理var a = 2 ;
代码过程。
在这之前我们我先介绍三个参与处理的角色
- 引擎:从头到尾负责整个javascript程序的编译以及执行过程。
- 编译器:负责语词分析以及代码生成等脏活累活。
- 作用域:负责收集并维护有所有声明的标识符(变量)注册后能够的一系列查询。
开始介绍:
先是引擎 遇到var a = 2;这段代码,进行解析,然后将var a;
给到 编译器 =>
编译器 拿到var a;
就会向 作用域 询问是否有已经有该名称的变量存在在作用域的集合中。
如果是 编译器 就会忽略该声明,否则它会要求 作用域 在当前作用域的集合中声明一个新的变量。
接下来 编译器 就会为 引擎 生成运行需要的代码,用来处理a = 2
这个赋值操作。 引擎 运行时会询问作用域是否存在 a 变量,如果是,则使用这个变量;否则 引擎 会继续查找下去。如果查找不到就抛出异常。####为了进一步理解,接下来我们介绍编译器的一些术语。
在我们上面的例子中,var a=2;
中,引擎会为变量 a 进行 LHS查询 和 RHS查询 。
- LHS:试图找到变量容器本身。(赋值操作的目标是谁)
- RHS:获取值。(随时赋值操作的源头)
考虑下面的代码:(这里的代码既有LHS也有RHS引用。)这里最后一行foo函数的调用需要对foo进行 RHS 引用,意味着“去找到foo的值,并把它给我”。function foo(a){ console.log(a); } foo(2);
这里还有一个很重要又很容易被忽略的细节,代码中隐式的a=2
操作发生在2被当做参数传递到foo()函数,为了给参数a分配值,需进行一次 LHS。
这里console.log(a)
也对a进行 RHS 引用。
LHS和RHS查询都会从当前执行作用域开始查找,如果没有找到所需的标识符,就上向上级作用域查找目标标识符,这样每次升级一次作用域,直到到达全局作用域,无论有无找到都会停止查找。
不成功的RHS引用会抛出异常,在非严格模式下,会隐式地创建一个全局变量;在严格模式下,抛出ReferenceError异常。
小测:
function foo(a){
var b = a;
return a+b;
}
var c = foo(2);
请问这里有多少处LHS和RHS?
所有的LHS(有三处)
c = ..; a = 2 ;b = ..
所有的RHS(有四处)
foo(2.. 、 =a 、 a.. 、 ..b)
二.词法作用域
对比动态作用域
这里需要明确的是: javascript只有词法作用域,没有动态作用域。this机制只是某种程度像动态作用域。
这里是说明两者的区别,不细说动态作用域。
主要区别:词法作用域是在写代码或者说定义的时候确定的,而动态作用域实在运行时定义的;词法作用域关注函数在何处声明,而动态作用域光柱函数从何处调用。例子: function foo(a){ console.log(a); //2 } function bar(){ var a=3; foo(); } var a=2; bar(); 这里词法作用域让foo()中的 a 通过RHS引用到全局作用域总的 a ,因此会输出 2 。
词法阶段
词法作用域就是定义在词法阶段的作用域。由写代码时变量和快作用域的位置决定。
欺骗词法
词法作用域完全有写代码期间函数所声明的位置来定义,怎样才能在运行时来“欺骗”词法作用域。
这里有两种机制可以实现欺骗词法,但需要注意的,这两种机制会导致性能下降。eval
(严格模式下不可用)
javascript中 eval()函数可以接收一个字符串为参数,并将其中的内容视为写在当前位置的代码。 考虑以下的代码: function foo(str,a){ eval(str); console.log(a,b); } var b = 2; foo("var b=3;" , 1); //输出结果为 1,3; eval()调用中的 var b=3 这段代码会被当在本来的位置处理,因此在foo()函数中就声明了 b;并遮蔽了全局作用域中的 b;
with()
(严格模式下不可用)
with相对比较难以掌握。同时也可以用多种方法解释with,但由于其也不推荐使用。所以这里并不进行详细解释和介绍。 (其实就是笔者能力有限,就不进行解释误导读者。)
三.块作用域
介绍启示
一般来说,我们讲javascript没有块作用域。我们先来看一段代码:
for(var i=1;i<10;i++){ console.log(i); }
对于已经入门js的开发者来说,对这段代码一定十分熟悉了。其输出 10次 10。虽然对于大多数开发者对这段代码的解释是console.log()是异步操作,但笔者认为,这必然也跟块作用域脱不了关系。ES6中新声明的 let 语法,实现块作用域结果便不同了。在下文会介绍 let 语法。
实现javascript中块作用域的功能
with
用with从对象中创建的作用域仅在with声明中而非外部作用域中有效。
try/catch
很少人会注意ES3 规范中规定try/catch的catch分句会创建一个块作用域,其中声明的变量仅在catch内部有效。
例如; try{ undefined(); //执行一个非法操作强行制造一个异常。 } catch(err){ console.log(err); //正常运行 } console.log(err); //ReferenceError: err not found err 仅仅存在catch分句内部,当试图从别处引用它会抛出错误。
let(ES6)
let关键字可以将变量绑定到所在的任意作用域上(通常是{…}内部)
var foo = 1; if(foo){ let bar = foo *2; console.log(bar); //2 } console.log(bar); //ReferenceError
let循环:
for(let i=0;i<10;i++){ console.log(i); // 0,1,2,3,4,5,6,7,8,9 } for循环头部的let不仅将 i 绑定到for循环的快中, 事实上它将其重新绑定到了循环的每一个迭代中,确保使用上一次循环迭代结束时值重新进行赋值。
####const(ES6)
同样可以创建块作用域变量。但其值是固定的(常量),试图修改值的操作会报错。
四.提升
思考一段代码:
console.log(a);
var a = 2;
输出结果为undefined。
其实这段代码处理流程是:
var a;
console.log(a);
a = 2;
只有声明本身会被提升,而赋值或其他运行逻辑会留在原地。
函数声明会被提升,而函数表达式却不会被提升。
函数优先
foo();
var foo;
function foo(){
console.log(1);
}
foo = function(){
console.log(2);
}
输出的结果为1而不是2;
函数声明优先原则。
五.作用域闭包
提前说明:闭包是js中一块重要,高明的内容,有许多比我优秀的笔者写过更多有关闭包的文章。这里也仅对闭包进行引导性的理解和学习。
实质问题
认识:当函数可以记住并访问所在的词法作用域时,就产生了闭包,即使函数实在当前词法作用域外执行。
function foo(){
var a=2;
function bar(){
console.log(a);
}
}
var baz = foo();
baz(); //结果为2 --这就是闭包的效果。
通常来说,foo()执行后,就会期待foo()的整个内部作用域被销毁,因为引擎有垃圾回收机制用来释放不再使用的内存空间。
而闭包却可以阻止这样的事情发生,由于bar()所声明的位置,他拥有涵盖foo()内部作用域的闭包,使得这一作用域一直存活。
记住一句话:无论通过何种手段将内部函数传递到所在的词法作用域以外,他都会持有对原始定义作用域的引用,无论在何处执行这个函数都会使用闭包。
本质上,无论何时何地,如果将函数当做以及的值类型并到处传递,就能看到闭包在这些函数中的引用。在定时器,事件监听器,Ajax请求,跨窗口通信,Web Workers或者任何其他的异步(同步)任务中,只要使用了函数回调,实际上就是使用闭包。
循环和闭包
for(var i = 1; i<=5 ; i++){
(function(){
setTimeout(function timer(){
console.log(i);
},i*1000)
})(i);
}
在迭代中使用自运行函数(IIFE)会为每一个迭代都生成一个新的作用域,是的延迟函数的回调可以将新的作用域封闭在每个迭代内部,每个迭代中都会含有一个具有正确值供我们访问。
重返块作用域
闭包和let的联合。
for(let i = 1; i<=5 ; i++){
setTimeout(function(){
console.log(i);
},j*1000)
}