劝你有点边界感——JavaScript中的作用域
前言
不知道码友们在编写JS代码时有没有注意到这样一个问题:明明定义了一个变量的值,想要得到它,最后却得到一个不是自己想要的结果:可能是另外的一个值,也可能报告一个错误信息。出现这种现象,我们不得不把锅甩给js中的作用域。因为作用域,我们得到的结果不一定是我们认为能得到的结果。下面,我将由最简单的代码,最形象生动的描述来给大家说道说道这个作用域。
学前必备知识
**首先请看下面这段代码 **
console.log(a); var a = 123;
这段代码大家觉得应该是输出什么结果呢?是不是都觉得这是个错误的代码?你们可以在自己的vscode运行一下这段代码。
在解释这段代码前我们必须得知道js中程序的执行过程。简单来说程序的执行主要分为两个阶段,分别是编译和执行,且必须是先编译后执行,编译是在执行的前一刻进行的。而我们要讲的作用域则和编译这个过程密不可分。为了让大家更深刻的了解何为编译,我将其比作老板身边的秘书。老板一天的工作安排全由秘书整理好,然后将整理好的安排交给老板,老板在安排执行下去。在程序执行过程中,编译这位秘书将完成三项工作:
- 词法分析: 即找出程序中的有效标识符,如在
var a = 1
这段代码中,编译器会找到'var', 'a', '=', '1'这几个标识符 - 解析: 即确定这些标识符的含义,得到这些标识符想要表达的意思。
- 生成代码: 然后将得到的意思在转化为可执行的代码语句
而上面那段代码经过秘书的手之后就变成了这个样子
var a;
console.log(a);
a = 123;
现在是不是可以理解vscode的运行结果了呢,感觉被骗了吧?
大家再想想这个编译过程是不是类似于秘书得到老板一天的工作内容后,确定老板的工作安排,然后在用最简单,老板最爱听的话来告诉老板工作安排呢?
初识作用域
在js中作用域主要有两类,一类是全局作用域,一类是函数作用域。显而易见,全局作用域就是作用在全局的,函数作用域仅仅是在函数中的。所以,现在再回过头去看我们刚开始提到的那个问题,是不是我们很多人在写代码的时候忽视了作用域啊,所以才没有得到我们想要的结果。
深入了解作用域
下面我将用几段简短的代码给大家介绍一下作用域。
var a = 1
function foo() {
var a = 2;
}
foo();
console.log(a);
这段代码应该输出多少呢?是1还是2 !!!是不是有人就有点糊涂了
正确答案是1
因为在函数foo
内部声明了变量a
并赋值为2时,这个a
只存在于foo
函数的作用域内。它并没有改变外部(全局作用域)中a
的值。且调用这个函数之后,函数作用域中的a
就会被销毁,所以,当执行console.log(a);
时,输出的是全局作用域中的a
,其值为1。
用这个图可能好理解一点,这个调用栈就是秘书的小本本,首先秘书先将全局变量a
,foo
给记在全局作用域这一块,然后交由老板,让老板执行给a
赋值为1,然后给foo
赋值fuction,最后执行foo
,但是foo
里面的东西并没有给秘书整理,于是秘书再整理foo
,画出一个foo作用域专门记录foo里面的变量,老板再将foo作用域中的a
赋值为2,但是老板执行完后秘书就应该把foo这个事件即foo作用域划掉,因为已经没有用了。再输出时就应该是a=1
var a = 1
function foo() {
var a = 2;
console.log(a);
}
foo();
这个就简单了吧,这个一看就知道答案是2
//var a = 1
function foo() {
var a = 2;
}
foo();
console.log(a);
大家猜猜这个答案是多少?这个答案是a is not defined
这个和第一个差不多,因为foo作用域中的东西用完就销毁了,根本找不到a的值,所以提示a没有被定义。
var a = 1
function foo() {
var b = 2;
console.log(a);
}
foo();
加油,看到这里就在坚持一下,快完了。
这段代码的值是1,为什么?有图有真相
与前面类似,但是这个console.log
语句在foo作用域内,当在此作用域内找不到的时候,就会往外去找,直到找到为止。
划重点:作用域的规则:内部的作用域可以访问外部的作用域,反之则不行(一种栈结构,只能从上往下访问)
通过刚才的几个例子大家应该也能有所了解,程序的运行就是一个入栈出栈的过程。为了检验一下学习成果,下面请大家将这段代码的结果告诉我,可以在评论区留言哦!
function foo(a){
var b = a*2
function bar(c){
console.log(a,b,c);
}
bar(b*3)
}
foo(2)
再深入——欺骗性词法作用域
先给大家讲一下词法作用域的概念,词法作用域就是变量声明的地方,简单点就是自己的上一级域。
function foo() {
var b = 2;
}
例如在这段代码中,b的词法作用域是foo,foo的词法作用域是全局作用域。
而欺骗性词法作用域从名字上来看就知道这个与骗有关,就是原来不是的这里的,让编译器错误的认为是这里的,所以我认为这也是一个bug。常见的欺骗性词法作用域有两种:
- eval():让原本不属于这里的代码变得好像天生就定义在这里一样
function foo (a,str){
eval(str) //把原本不属于这里的代码搬到这里来,好像天生在这里一样
console.log(a,b);
}
foo(1,"var b = 2")
在我们的印象中整个作用域中就没有b这个变量,肯定会报错啊。实则不然
eval()这个方法将把原本不属于这里的代码搬到这里来,进而能让v8识别出来
- with(){} 当修改对象中不存在的属性时,这个属性会被泄露到全局,变成全局变量
with(){}这个主要用于快捷修改对象中的值
使用方法如下:
var obj = {
a:1,
b:2,
c:2
};
with(obj){
a = 3;
b = 4;
c = 5;
}
通过这个方法能够快捷的修改对象中的值,但是如果修改的值对象中不存在这个属性,那还会不会修改成功呢?答案是肯定不会的,都没有那个值怎么会修改成功啊。那会报错吗?也不会。那修改后的那个属性到底去哪了呢?他其实是泄漏到了全局作用域中,成为了潜在的全局变量。
function foo(obj){
with(obj){
a = 2;
}
}
var o2 = {b:1}
foo(o2)
console.log(o2); //此时输出{ b: 1 }
console.log(a); //输出2
通过这段代码输出的结果可以看出来,with语句把a泄漏到了全局中
关于欺骗性词法作用域只要知道这两个就够了,因为我目前我也就只知道这两个
作用域拓展——块级作用域
其实js中的作用域不止有两个,还有一个块级作用域。简单点,块级作用域也包含两种:
一种是{}+let
一种是const
在js中定义变量不仅仅可以用var
来定义,还可以用let
,const
来定义,用let来定义变量时就不会出现像用var定义变量时的那种变量还没定义就可以输出变量的值的那种情况(虽然输出的值是none),但是太不符合我们程序员的思维了,于是js维护组就将设置了个let
标识符来定义变量,迎合我们程序员的思维。而当用{}+let
时就形成了一种新的作用域即块作用域。众所周知,{}
里面包括的就是一个程序块,而块作用域就是在这个块里面,{let a}
则表明a仅在这个块中有效。
{
let a = 1
var b = 2
}
console.log(a); //报错,a未被定义
console.log(b); //可以运行,得出b
这段代码已经把我想说的都表示出来了,这个块作用域仅对于let 定义的变量有用,而对var定义的变量无效。通俗一点,我妈妈的话只对我有效,对别人家的孩子无效。
const
就不用细说了,const定义的是一个常量,任何人都不能改变它的值。
暂时性死区
暂时性死区(Temporal Dead Zone, TDZ)是ES6中引入的一个概念,主要与let
和const
声明的变量有关。在ES6之前,使用var
声明的变量会存在变量提升(hoisting)现象,即变量声明会被提升到当前作用域的顶部,但赋值操作不会提升。这导致在变量声明之前访问变量是合法的,尽管值是undefined
。
然而,let
和const
改变了这一行为。它们不允许变量提升,并且在它们所在的块作用域开始处到声明语句之间的区域定义了一个暂时性死区。在这个区域内,任何试图访问该变量的操作都会导致引用错误(ReferenceError),即使是在声明之前访问也不行。
下面请看代码示例:
let a = 1
{
console.log(a);
let a = 2
}
大家来说说这段代码最终结果是什么?相信大家知道概念了之后应该都知道结果了吧!对,这段代码他会报错,因为let不能像var一样进行声明提升,且块作用域中有a这个变量,所以也不能访问外面的那个a,从而导致给活活憋死了。为了便于大家记忆,就好比你有一个异地的女朋友,凭着良心就不能去外面找小姐姐。
结束语
今天的分享就这些了,这是纯干货加上了一些个人的一些理解,可能还有一些地方没有讲到。如果有大佬看到了不足之处,可以指导指导我,感谢感谢。最后,写小作文不易,大家看完给个小赞吧,跪谢!爱你
转载自:https://juejin.cn/post/7372376472436441127