“领地”意识——JavaScript中[作用域]的秘密

356 阅读6分钟

从代码执行入手

要了解作用域的秘密,我们先从编译入手。JavaScript 是一种解释型语言,但这个术语可能有点误导。实际上,现代JavaScript的执行过程涉及两个主要阶段:编译和执行,我们主要看编译过程。

简介编译

我们可以把它看作是老板的秘书,帮助老板完成时间、内容安排。在编码中就是整理代码中的规则,只有在V8中编译结束,将代码转换为可执行的形式,才能进行执行。编译有这样三个过程:

  • [ 1. 词法分析]:找出哪些有效的标识符,如var a = 1,编译器会找到‘var’、‘a’、‘=’、‘1’。最终记在属于‘秘书’的小本本上,表示这是一个声明了一个变量a.

  • [2. 解析]: 如:‘var’是定义变量的关键字。

  • [3. 生成代码]:V8会在运行前生成字节码或直接生成机器码。

认识作用域

介绍完了什么是编译,现在看看作用域。在JavaScript中,作用域(Scope)是一个非常核心的概念,它定义了变量、函数等标识符的可访问性。简单来说,作用域决定了你在哪里可以“看到”或访问到特定的变量。JavaScript中有两种主要的作用域类型:全局作用域局部作用域(也称为函数作用域),以及一种更细粒度的作用域——块级作用域(从ES6开始引入)。

简介全局作用域

作用域有个规则,内部作用域可以访问外部作用域,反之则不行,因为作用域放在调用栈中,只能从上往下访问。看这个例子:

var a = 1

var foo = function() {
    var a = 2
}

console.log(a)

大家可以想想最后得到的a会是多少?很简单最后a = 1,因为console语句是在全局作用域中。当V8执行这条语句的时候会理所当然的在全局中找到变量a声明并赋值的位置,而不会去到foo中。

简介局部作用域

如果是这种情况呢?在全局中并没有声明变量a,但是函数foo中声明了。那么最后得到的会是什么?

var foo = function() {
    var a = 2
}

console.log(a)

答案是'a is not defined',因为声明在函数体里面的变量,在全局中是无法获取的。编译器作为‘秘书’,会先把全局中的代码编译理清楚一边,但是函数作用域里却不先编译,等到执行时调用到函数foo才会让‘秘书’去把foo里面的内容事先编译,可以看下面的图来认识。

image.png

简介块级作用域

首先得明白var、const和let之间的区别。

  • 先来看看var:是JavaScript中声明变量或者对象的关键字,在全局声明的变量会被添加到Windows对象上,存在声明提升的情况。在编译时,‘秘书’会在全局中将变量先声明出来,方便之后执行时让V8知道这里声明过一个怎样的变量。
image.png
  • 接着是let:es6新加的关键字,基本用法与var相同,唯一不同的是当上面那样的情况发生时会显示报错,可能这样才会更让人理解,毕竟先声明才能调用是默认的思维了吧。

  • 最后是const:与let一样是es6引入的,用法也一样,区别是const声明的是常量,不可以修改值。

现在来看看什么是块级作用域,在任何大括号{}包裹的代码块中使用letconst声明的变量,其作用域仅限于那个块。这有助于避免出现变量泄漏到外部作用域的情况,后面会在欺骗词法作用域提高了代码的可读性和健壮性。

{
    let a = 1
    var b = 2
}

console.log('a = ' + a)
console.log('b = ' + b)

这里的输出可不会一样,上面的console会报错a is not defined,而下面的却可以输出b = 2

简介暂时性死区

说到let,就提一个暂时性死区。当代码执行流进入一个块级作用域,并且该作用域内存在letconst声明,从块的开始位置到声明这一变量的语句之间,该变量处于暂时性死区。

let a = 1
{
    console.log(a); 
    let a = 2
}

运行起来会报错,显示Cannot access 'a' before initialization,因为既无法拿到{}中的a,也无法在自己的作用域声明了变量的情况下跑到全局中去寻找变量a的声明。

简介欺骗词法作用域

介绍欺骗词法作用域前,先来看看什么是词法作用域。词法作用域,也称为静态作用域,是一种编程语言中作用域规则的定义方式。骗词法作用域,通过特定的机制在运行时修改或绕过词法作用域规则的行为。JavaScript中主要通过eval()函数和with语句实现这种“欺骗”。

  1. eval()函数:eval()函数接受一个字符串参数,并将其作为JavaScript代码执行。让原本不属于这里的代码,变得好像天生就定义在这里一样。
function foo(a,str) {
    eval(str)
    console.log(a,b);
}

foo(1,'var b = 2')

输出的答案为,a = 1,b = 2。'var b = 2'作为参数传入foo函数中,又作为参数传入eval()中。当‘秘书’编译时,就会认为是在foo中声明了变量b。

  1. with语句:with语句允许你将一个对象作为作用域链的一部分,使得在该语句块内部可以直接访问该对象的属性和方法,无需显示指定对象名。当修改对象中不存在的属性时,这个属性会被泄漏到全局,变成全局变量。
var obj = {
    a: 1,
    b: 2
}

with(obj) {
    a = 2
    b = 3
}

console.log(obj)

这里很正常的利用with()来对obj这一对象里的属性进行值修改,得到的输出是{ a: 2, b: 3 }
但是我们来看看下面的情况:

function fo(o) {
    with(o) {
        a = 4
    }
}

var ob = {
    b: 1
}

fo(ob)
console.log(ob.a)
console.log(a)

这里声明了一个函数fo和对象ob,在fo中使用with()来修改传进来的参数的属性a值为4,将ob作为参数传进fo中进行修改。但是ob中并没有属性a,最后输出的分别是undifined以及a = 4

到这里,今天的分享就结束了,希望你能够有所收获,再接再厉,一起加油! 😊