一:前置知识
1.1:JS引擎
在认识JS作用域之前,我们要明白JS代码是交给浏览器去解读的,浏览器中含有JS引擎进行解读。其中不得不提的就是谷歌的v8执行引擎,可以说是“遥遥领先”了,以下以v8为例进行讲解。
v8引擎解读JS代码可分为三步
- 进行词法分析,得到词法单元
- 进行解析,将词法单元转换成一个逐级嵌套的程序语法结构树
- 生成代码
一个简单的例子:JS代码为var a = 2
,词法分析得到的词法单元为var, a, =, 2
,通过解析后生成代码var a = 2
,虽然看上去解析前后的JS代码是一样的,但v8读的是最终生成的代码,而不是最开始用户写的代码。
1.2:有效标识符
有效标识符是指用来给变量、函数等起名字的字符串,需满足以下简单规则:
- 以字母、下划线
_
或美元符号$
开头。 - 后续可以包含字母、数字(0-9)、下划线或美元符号。
- 不能使用JavaScript的保留关键字作为名称,比如
if
、else
、while
等。 - 区分大小写,
myVar
和MyVar
被视为两个不同标识符。
二:全局作用域与函数作用域
请看下面这个例子,其中红色框内为foo函数的作用域,黄色框内为bar函数的作用域,编译运行可知,我们能够得到正确答案-->6。我们在全局调用了foo函数,变量a在foo函数中声明,能够得到正确答案说明在调用bar函数时,内层领域能够拿到外层领域b的值,也就是说,内层领域能够访问外层领域。
然而,在下面这个例子当中,却无法正确编译,报错:a is not defined
,为什么呢?
我们可以发现,代码在全局声明了一个函数体foo,在foo函数中声明了一个变量a,故a是函数foo中的有效标识符,也就是说,a是处在foo函数的作用域中。然而console.log(a)
是写在了全局作用域中,看到这里,诸君心里是不是有了答案-->外层作用域无法访问内层作用域
function foo(){
var a = 1
}
foo()
console.log(a);
那么,问题来了,既然内层作用域可以访问外层作用域,外层作用域不可以访问内层作用域,那直接哐哐往在全局声明变量,这不就可以避免作用域带来的无法访问问题吗?显然是错误的想法,在实际开发中,全局声明过多变量,是容易混淆和重复的,正确的做法是根据函数功能,合理的封装,请看下列代码,试着自己分析下,体会代码的优雅
function foo(a){
var b
function bar(a<img src="){" alt="" width="70%" />
return a-1
}
bar(a*2)
console.log(b*3)
}
foo(2)
三:作用域的底层逻辑
v8在执行代码时,第一时间是创建一个调用栈,首先碰到的一定是全局作用域,会创建一个全局执行上下文,再碰到局部作用域,会创建一个局部执行上下文。之所以内层作用域可以访问外层作用域,本质上是因为栈的先进后出,单个出口的性质。一定是取完上面的,才能取下面的。请看下面的代码分析示例,相信诸君会对其有更深的理解。
var a = 1
function foo(){
var a = 2
console.log(a);//输出2
}
foo()
四:let与var
4.1:声明提升
var
声明的变量会进行声明提升(hoisting),这意味着变量可以在声明之前被访问,尽管其值会是undefined
- 而
let
声明的变量不会被提升,必须在声明之后才能使用,否则会导致引用错误(ReferenceError)。
console.log(a);//不会报错,输出undefined
var a = 1
console.log(a);//报错
let a = 1
4.2:块级作用域
var
声明的变量具有函数作用域或全局作用域。如果在任何函数外部声明,它将成为全局变量。即使在块级作用域(如if语句或for循环内)中声明,var
变量也会提升到包含它的函数或全局作用域的顶部。let
提供了块级作用域,这意味着变量只在它被声明的代码块(例如,if语句、for循环或一对花括号内)中可用。
if(true){
var a=1
}
console.log(a);//输出1
if(true){
let a=1
}
console.log(a);//a is not defined
for(var i=0;i<10;i++){
console.log(i);//输出0-9
}
console.log(i);//输出10
{
let i;
for(i=0;i<10;i++){
}
}
console.log(i);//i is not defined
4.3:重复声明
var
允许在同一作用域内多次声明同名变量,不会报错,可能会导致意料之外的行为。- 在同一作用域内,不允许重复声明同一个
let
变量,这样做会抛出语法错误。
4.4:暂时性死区
用let
声明的变量,在变量声明之前,该变量在其作用域内处于暂时性死区,任何访问都会导致错误。
let a=1
if(1){
console.log(a);//暂时性死区
let a=2
}//编译报错Cannot access 'a' before initialization
五:欺骗词法作用域
5.1:eval
eval()能够“欺骗”词法作用域,将原本不属于这里的代码变成就像天生定义在了这里一样。下例中eval(str)
等同于var b=3
function foo(str,a){
eval(str);
console.log(a,b);//1,3
}
var b=2
foo('var b=3',1)
5.2:with
with(){} 用于修改一个对象中的属性值,但如果修改的属性在原对象中不存在,那么该属性就会被泄露在全局域中。下例中with(obj){ a=2 }
将a
泄露在全局之中,所以console.log(a)
能够得到2
function foo(obj){
with(obj){
a=2
}
}
var o2={
b:3
}
foo(o2)
console.log(a);//输出2
六:知识点get
- JS引擎解读代码的过程(见前置知识)
- 有效标识符
- 内层作用域可以访问外层作用域
- 外层作用域不可以访问内层作用域
- 作用域的底层逻辑,引擎执行代码时会第一时间创建调用栈
- let和var的与作用域相关的四个区别
- eval和with能够欺骗词法作用域
最后再总结下三种作用域:
-
全局作用域 :
- 全局变量在整个脚本或文件中都是可见的,从脚本的任何位置都可以访问。
- 在函数外部定义的变量拥有全局作用域。
- 在浏览器环境中,全局变量默认绑定到
window
对象上。
-
函数作用域 :
- 函数内部声明的变量(使用
var
关键字)仅在该函数内部可见,称为局部变量。 - 函数参数也属于函数作用域。
- 在函数内部可以访问全局变量,但全局变量无法直接访问函数内部的局部变量。
- 函数内部声明的变量(使用
-
块级作用域 (ES6新增):
- 使用
let
和const
关键字声明的变量在它们所在的代码块(如{}
内的代码)内可见,而不是整个函数。 - 区块作用域包括
if
、for
、while
等控制结构的花括号内,以及函数表达式和箭头函数的花括号内。 let
变量可以在其作用域内重新赋值,但const
变量一旦赋值后不可更改。
- 使用
本文的下篇:https://juejin.cn/post/7377647067575418932,配合食用,效果更佳