替你读《你不知道的JavaScript》-(一)作用域

您好,我是沧沧凉凉,是一名前端开发者,目前在掘金知乎以及个人博客上同步发表一些学习前端时遇到的趣事和知识,欢迎关注。


相信大多数人包括我自己,都是不太爱读书的,但是当你在前端开发中有一定的基础后,你会发现各个编程网站上面的文章大多数都千篇一律,要获得新的好的知识就如浪里淘沙,掘金相对来说还好一点,非常多的干活,也是我个人访问最多的论坛。

这个时候,你就需要静下心来,好好阅读一些前端方面的书籍,因为书籍上面无论是知识的深度还是广度,都胜于论坛,如果你实在是不爱看书怎么办,没关系,我替你读这些书籍,并且将这些知识整理出来。

平均每篇文章字数大概在2000字左右,为什么要选择这个字数,其一是因为太多的字数对于阅读来说并不是太友好,容易出现字多不看,其二是因为字数过多容易导致我自己事后查阅也变得困难。


编译原理

JavaScript尽管被归类为“动态”或“解释执行”语言,但其实它是一门编译语言,当就我查阅到的知识来说,编译语言和解释语言的界限在逐步的变淡,很多语言即能说是编译语言,又能说是解释语言,所以不用太过于纠结名称的问题。

传统编译语言的三个步骤:

  • 分词/词法分析(Tokenizing/Lexing)
  • 解析/语法分析(Parsing)
  • 代码生成

由于原文中对这三个步骤讲解的过于生涩,大致讲解一下意思就是:例如语句var a = 2;,那么它会先把语句进行分词拆解,拆解完了后组装成为一棵树(“抽象语法树”(Abstract Syntax Tree,AST)),最后将这棵树转换成为计算机能够识别的代码。

比起那些编译过程只有三个步骤的语言的编译器,JavaScript 引擎要复杂得多。当下最著名的JavaScript引擎要属谷歌公司开源的V8引擎,Nodejs也是基于V8引擎进行开发的,由于V8引擎超快的执行速度,更是给前端行业带来了翻天覆地的变化。也正因为Nodejs的出现,使得前端开始出现工程化,前端项目开始走向复杂化,甚至能使用JavaScript一门语言就可以做到全栈开发!

在对语句的处理过程中,参与到其中的分别有:

  • 引擎:从头到尾负责整个 JavaScript 程序的编译及执行过程。
  • 编译器:负责语法分析及代码生成。
  • 作用域:收集并维护由所有声明的标识符(变量)组成的一系列查询,并实施一套非常严格的规则,确定当前执行的代码对这些标识符的访问权限。

引擎和编译器基本不在本篇文章的讨论范围中,本文中我们需要着重了解的就是作用域。

由于JavaScript当时设计时候的缺陷在ES5即以前的时候声明变量使用var关键字,在ES6后引入了新的变量声明方式let、const。

var、let、const

var

聊到这里就不得不说一下var的缺陷。

可以重复声明

var greeter = "hey hi";
var greeter = "say Hello";

可以看到,使用var关键字,即便是声明了同一个变量,也不会进行报错,这就可能会出现莫名其妙覆盖了之前声明的变量,造成一些意想不到的BUG。

变量提升

console.log(greeter) // undefined
var greeter = "hey hi";

可以看到上面的代码明明greeter在使用后才进行了声明,但是运行代码却不会报错,这就涉及到后文会讲到的变量提升问题,其实在执行这段代码时,greeter变量的声明会被提升到最顶部,就等于下面的代码:

var greeter;
console.log(greeter); // undefined
greeter = "say hello";

具体变量是如何提升的,会在后面的文章中进行讲解。

{}无法提供作用域

var greeter = "hey hi";
var times = 4;

if (times > 3) {
  var greeter = "say Hello";
}

console.log(greeter); // "say Hello"

使用var声明的变量是不会受到块级作用域的影响,即在if,for等声明的变量,在其外部也是可以进行访问的,这些情况可能也会造成一些BUG的发生。

let、const

因为上面的那些缺陷,所以在ES6的时候发布了两个新的关键字,使用这两个关键字可以完美的避开上面的那些雷点。

综上所述,在现代JavaScript项目的开发中,不推荐再使用var,第一个原因是因为现代浏览器(除IE)对JavaScript的新语法兼容性非常好,第二个原因是因为babel可以提供语法之间的转换。

至于let和const的其它好处,会在后面进行讲解。

作用域

回到var a = 2;这段代码中,引擎会认为这是两个完全不同的声明,一个由编译器在编译时声明,另一个则由引擎在运行时处理。

简单的说就是编译器会寻找当前作用域中是否有a这个变量,如果没有则进行声明,如果有会忽略该声明,然后再运行时引擎会在作用域中查找该变量,如果找到该变量则会把2赋值给它,如果没有找到则会到上一层继续查找。

无论函数在哪里被调用, 也无论它如何被调用, 它的词法作用域都只由函数被声明时所处的位置决定。

全局作用域

局部作用域

函数作用域

函数作用域的含义是指, 属于这个函数的全部变量都可以在整个函数的范围内使用及复用( 事实上在嵌套的作用域中也可以使用)。

欺骗词法作用域

eval

with

尽量不要用var

在现在babel可以将ES6+语法转为ES5语法的时代,而且现在现代浏览器对ES6的支持已经非常良好了,所以尽量不要再使用var来声明变量,因为当时var设计的有一些缺陷,使用var会带来一些不可预测的结果。

而且在非严格模式下,如果不使用var声明变量,那么变量会直接被声明在全局,这会引起一些BUG。

function doSomething(a) {
  b = a + doSomethingElse(a * 2);
  console.log(b * 3);
}

function doSomethingElse(a) {
  return a - 1;
}

doSomething(2); // 15
console.log(b); // 5

可以看到上面的变量b虽然在函数内部,但是没有使用var声明,所以它还是被声明在全局变量上,即使在函数外部也是能够访问的,而使用了严格模式会直接报错。