作用域是什么
- 简单来说,它体现了JS引擎对变量名的处理机制。这个机制在不同语言中有不同实现,大体可以分两类:静态作用域(也称词法作用域)与动态作用域。
- 静态作用域(词法作用域)里,对于函数体中的一个符号,不会逐层检查函数的调用链,而是检查函数定义时的外部环境,即捕捉的是函数定义时该符号的绑定。
先来看一段代码
function func (){
var vari = 12;
}
func()
console.log(vari)
function func (){
console.log(vari)
}
var vari = 12
func()
这里体现的就是JS中的变量作用域。 JS采用的是静态作用域。举个例子,
var a = 10;
function func (){
console.log(a);
}
function func1 (){
var a=5;
func()
}
由于JS采用静态作用域,所以func定义时保留了对外层a变量的引用,结果为10。
JS 作用域的特点
- 在JS中,作用域就像套娃一般,外层套内层。变量在调用中将会由调用点出发,向外层寻找符合变量名的变量,直至到达全局作用域。
- 一共有三种方式可以形成作用域,全局、函数、块。全局作用域在运行时便默认存在,我们可以使用的是函数与块作用域。
还是举个例子,
//全局
function foo(){
let vari = 10;//函数作用域
if(vari==10){
let vari = 20//块级作用域
console.log(vari)
}
{
let abc = 10//就算只有花括号也可以构成块级作用域
}
console.log(abc)
console.log(vari)
}
foo()
上面这个例子简单展示了块级作用域与函数作用域。
此外,函数(包括回调函数)及对象方法将会使用其被定义时所处的作用域,而非使用其实际调用时的作用域。这也引出了闭包,不过这里我们先不展开讲闭包。一个函数作用域的例子:
function foo(){
console.log(a)
}
function bar(func){
let a =10;
func()
}
bar(foo)
好了,你已经学会了作用域的基本操作,那来试试这道题吧:
function foo(){
let a = 10;
{
a=20;
let b=30;
let d=()=>{
console.log(b)
}
{
let b=40;
console.log(a);
console.log(b);
d()
}
console.log(b)
{
let c=50;
console.log(b);
}
console.log(c);
}
console.log(a)
}
foo()
注意,块级作用域只在使用let与const声明变量时可用,使用var声明变量不会触发块级作用域。在正常情况下我们推荐使用let声明变量,它拥有块级作用域,在重复定义时let会报错,而var只会默默地进行赋值操作。比如这个例子:
function foo(){
var a=10;
if(a){
var a=20;
console.log(a)
}
console.log(a);
}
变量提升
在引擎层面上,JS会将每个函数作用域中的变量声明提升至函数顶端,无论他在哪(哪怕是不可能被执行的代码块),而对变量的赋值则会推迟到变量语句实际所处的位置。let与const也会有提升情况,不过这两个存在暂时性死区(TDZ)现象以模拟块级作用域。
function foo(){
console.log(a)
{
console.log(a)
let a=10;
}
}
遮蔽
function foo(){
let a=10;
{
let a=20;
console.log(a);
}
console.log(a);
}
全局作用域
- 在JS中,全局作用域可以使用globalThis进行引用。除此之外在各个环境中还有不同的引用全局作用域的方法,比如浏览器的window,Node.js的global,web Worker的self。
- 由于模块(module)机制的出现,全局作用域的重要性逐渐降低,语句更多的是在自己所属的函数作用域下执行。
LHS与RHS
- 我们之前讲到JS引擎会沿着作用域向上寻找变量、直至全局作用域。那么问题来了,假如某个变量名直到全局作用域都没有找到(比如还未定义就使用这个变量)会发生什么呢?一般这种情况常见于书写错误,比如经典的mian、jvav。此时我们需要了解JS引擎的LHS与RHS(left hand search,左侧搜索以及右侧搜索)。
- LHS与RHS最原始的定义是对在表达式两侧变量的查找。比如一个赋值表达式:a=b,对a的查找是LHS,对b的查找是RHS。引申之后LHS即对变量的赋值,RHS即对变量的引用。看如下代码:
function foo (a){ let b = a; return a+b } let c = foo(2) - 我们从c声明开始,c赋值触发一次LHS,调用函数触发一次RHS,参数赋值触发一次LHS,b赋值触发一次LHS,a调值触发一次RHS,a+b触发两次RHS。
- 那么学习它对开发的意义是什么呢?意义在于全局作用域对LHS与RHS搜索失败的处理方法不同。LHS、即赋值搜索、失败后会在全局作用域创建同名变量,并对其赋值;而RHS、即取值搜索、失败后会抛出Reference Error。LHS的默认行为可能会导致调试过程中出现问题。不过在严格模式下,LHS失败也会抛出Reference Error。
- 实践上不要刻意使用这个特性!每一个使用的变量都要尽可能的在作用域顶端完成声明! 这里讲解是用于排查错误使用。