You-Dont-Know-JS之作用域与闭包

March 24, 2021

前言

读的书多了,渐渐也能有自己的体会,书有平庸之作,也有佳作,但有些时候我觉得更多要看人。 在看了Vue和backbone的源码后,越发感觉自己的代码水平有待提高,但常见的各种动物书都看过了,一时间不知道看啥书好。于是在傻乎逛了一圈后,发现《你不知道的JavaScript》这本书,严格来说是上卷,很早以前就听大名,仿佛和高程三齐名,只是一直以没有时间为理由,没有接触。巧的是在傻乎上发现《你不知道的JavaScript》已经在github上出了中文版,而且是全套完整的,幸福来的太突然。细读之,颇有收获,故在此分享。

编译器

文中一开始就讲到编译器理论,离开大学后就没有接触过这东西,再次看到,觉得很nice; 编译过程通常来说分为三步:1.分词/此法分析,这个有点类似搜索引擎里面的拆字; 2.解析,将步骤一生成的代码片段表示为一个抽象语法树;3.生成代码就是将抽象语法树转化为可执行代码;

正如上文说所,引擎在执行代码前,代码会先经过编译器编译;如var a = 2,编译过程会出现编译器和作用域之间的互动,编译器在遇到var a的时候会通过作用域判断有无该声明,没有就声明一个a变量,有就略过;而代码编译后执行的情况,则是引擎向作用域请求,去寻找a变量,再进行赋值操作;就这样var a = 2被分成两步操作。 看到这里,你大概就发现作用域在里面扮演的中间角色,编译过程中将变量参数创建在作用域内,而执行阶段引擎通过访问作用域得到变量,并进行其他操作;分工是如此的明!!作用域有点像数据库的味道,被互相用来用去。。。。文中还提到了LHS和RHS查询,LHS查询简而言之赋值操作,RHS则是取值操作;如console.log(a)这里面就涉及到两个RHS,console的查找和a的RHS引用。 提到LHS和RHS可以加深我们对编译器,引擎和作用域三者之间关系的理解,书中还有趣的提到三者之间聊天的情况,值得玩味;关于LHS和RHS,还涉及到了引擎抛出的错误ReferenceErrorTypeError,前者正如字面解释引用错误,当RHS查询的时候,若在作用域没有发现这个变量,就抛出异常;相同的如果作用域解析成功了,但是进行非法操作,就会TypeError

词法作用域,函数与块儿作用域

在词法作用域里面提到了evalwith这些被好多人提到的黑暗操作,本以为一辈子都见不到了,没想到在自家项目上居然看到有人用。。。立马delete掉;eval这些是很灵活,但是它会大大的影响编译过程中的优化,因为eval这些的出现会导致编译中产生的词法作用域无效:

但如果 引擎 在代码中发现一个 eval(..) 或 with,它实质上就不得不 假定 自己知道的所有的标识符的位置可能是无效的,因为它不可能在词法分析时就知道你将会向eval(..)传递什么样的代码来修改词法作用域,或者你可能会向with传递的对象有什么样的内容来创建一个新的将被查询的词法作用域。

IIFE立即调用函数表达式,最开始接触的时候是在看jQuery里面用到的,这样可以把自己声明的变量和外界隔离开来,避免环境污染;let和const带来的块级作用域,自然是无可辩驳的,以前全局作用域和函数作用域,现在已经基本不用var只用let和const了。。。。。var重复声明就直接覆盖,而且变量提升还会带来undefined的覆盖;以下面例子

var tmp = new Date();

function f() {
  console.log(tmp);
  if (false) {
    var tmp = 'hello world';
  }
}

f(); // undefined

输出结果将会是undefined,而使用块级作用域就很不一样了

提升,作用域闭包

变量提升,在之前的博客中也有提到过,书中再次用编译的思想解释。对于变量声明,编译中自然是在的目的之一就是在作用域创建该变量,后面才轮到引擎执行可执行语句,于是产生声明提升,执行的时候才有初始化,正如下面代码

foo(); // 不是 ReferenceError, 而是 TypeError!

var foo = function bar() {
    // ...
};

提升部分有兴趣可以看看之前一篇博客:void 0 以及let const var 的理解

闭包的概念就有点老生常谈了,如今ES6有了块作用域的概念,一切都好办多了,模块间的import和export语法,也让代码运用更加灵巧,只是结合这本书出版时间,不难发现,在以前这概念可是相当新颖的;