什么是作用域?
作用域是一套规则,用于确定在何处以及如何查询变量(标识符),通常也被称作词法环境(lexical environment)。
作用域链
什么是作用域链
当一个函数或代码块嵌套在另一个函数或代码块中,就形成了作用域链。这么说可能不够直观,参考《你不知道的JavaScript》的插图如下
如何在作用域链中查找变量
要准确查找作用域中的变量首先必须明白的三点:
- 函数声明与变量声明都会提升,但是函数声明会首先被提升,然后才是变量声明提升。
- 作用域查找都是从内部作用域开始,然后往上级作用域查找,直到遇到第一个匹配的标识符为止,如果没有遇到标识符则会一直查找到最外层的全局作用域。
- 无论函数在哪里被调用,以及如何被调用,函数的词法作用域都只是由函数声明所处的位置决定。而不是函数调用是的位置所决定。
当然也需要掌握JavaScript提升的相关知识点JavaScript之var变量声明提升及函数声明提升,下面通过示例剖析。
示例剖析
示例1
var x = 10
fn()
function foo() {
console.log(x)
}
function fn(){
var x = 30
foo()
}
// 等价于👇🏻的代码
/*
function foo() {
console.log(x)
}
function fn(){
var x = 30
foo()
}
var x;
x = 10;
fn()
*/
控制台打印的值为:10
代码剖析:
首先声明的函数fn、foo会提升到头部即fn、foo函数声明前置。查找函数foo内部的标识符x,函数foo内部的作用域并没有匹配到该标识符,往其上层作用域(全局作用域)匹配到一个对应的标识符即是全局变量var x = 10,则执行fn()输出在控制台的结果值为10。
示例2
var a = 1
function fn1(){
// var a = 2;语句声明并赋值的变量a会提升到函数fn1内部的顶层
function fn2(){
console.log(a)
}
function fn3(){
var a = 4
fn2()
}
// 被fn1执行过后,a会被赋值为2
var a = 2
return fn3
}
var fn = fn1()
fn() //输出多少
控制台打印的值为:2
大致划分出四个作用域:
1️⃣ 全局作用域包含 变量`a`、函数`fn1`、函数`fn`
2️⃣ 函数`fn1`所创造的的作用域,包含`fn2`、`fn3`、变量`a`
3️⃣ 函数fn3内部作用域,包含一个变量`a`
代码剖析:
执行fn函数输出的值,即是执行函数fn2内部语句console.log(a)在控制台打印的值。函数fn2变量a对应的作用域,首先从函数fn2()内部作用域找是否能匹配的标识符。当前fn2内部无法匹配,则往上层作用域(fn1()所创建的作用域)查找变量a。fn1内部var a = 2;变量a提升到函数fn1作用域的顶部 ,则fn2内部变量a匹配的正是fn1作用域声明的变量a。在var fn = fn1() 函数fn1被调用了,return fn3前面的语句a = 2给变量a赋值。最终console.log(a)首先打印变量a,且被赋值为2,所以控制台打印的值为2。
为了更直观的查看,可以在浏览器中运行代码,并通过控制断点进而控制代码的执行,查看代码块作用域的变化。具体操作可以通过代码断点,与控制【进入下一个函数调用】按钮控制代码的运行
首先将代码断点设置在fn()代码行号处
- 鼠标左键第一次击控制台【下箭头】
函数fn创建新的函数执行上下文,并被推入执行栈的顶部。此刻上图中【本地】作用域中的变量a,指的是函数fn3内部声明的变量a。且由于函数内部声明变量提升var a = 2;会被JS引擎当做var 2; 、a = 4;两步执行。且可以观察到,函数fn1内部的fn2与 后面声明的变量 var a = 2;构成了闭包
- 鼠标左键第二次击控制台【下箭头】
fn3内部声明的变量a,被赋值为4
- 鼠标左键第三次击控制台【下箭头】
创建
fn2函数的执行上下文,函数fn2被推入执行栈顶部。此时fn2函数内部的变量为var a = 2;的值。 - 鼠标左键第四次击控制台【下箭头】
fn2函数被执行,控制台输出的内容为2 - 鼠标左键第五次击控制台【下箭头】
fn2函数被执行完成,被推出执行栈。此时函数fn3的执行上下文被重新激活
-
鼠标左键第六次击控制台【下箭头】
函数
fn3执行完成,被同样推出执行栈。
示例3
var a = 1
function fn1(){
function fn3(){
var a = 4
fn2()
}
var a = 2
return fn3
}
function fn2(){
console.log(a)
}
var fn = fn1()
fn() //输出多少
控制台打印的值为:1
代码剖析:
查找作用域中的变量要记住记住上面三条规则,其中一条就是“函数运行在它们被定义的作用域,而非被执行的作用域”。函数_fn2_在函数_fn3_被调用,但是函数_fn2_是被声明在全局作用域中的。从函数_fn2()_内部查找匹配变量_a__,内部没有声明的该变量,则_往上层查找即是全局变量 a (a = 1)。
如果注释掉上例中全局作用域中的var a = 1,则fn()控制台会报错“Uncaught ReferenceError: a is not defined”
示例4
var a = 1
function fn1(){
function fn3(){
function fn2(){
console.log(a)
}
fn2()
// fn2() 被调用之后,才会给a赋值为4
var a = 4 // 该语句放置在fn2()前面,则打印的结果值是4
}
var a = 2
return fn3
}
var fn = fn1()
fn() //输出多少
控制台打印的值为:undefined
代码剖析:
返回fn1内部声明的fn3函数,执行fn函数则会执行函数fn3。fn2函数体内部变量a匹配的作用域为它的上级作用域fn3函数所创建的作用域。
function fn3(){
function fn2(){
console.log(a)
}
fn2()
var a = 4
}
fn2()的的输出结果是undefined,这是由于在fn3()所创建的作用域变量a 提升到函数内顶部,但是变量a赋值是在调用fn2()之后,所以console.log(a)中的a,并没有被赋值,既输出结果是undefined。
如果把 var a = 4语句放在调用fn2()之前,则输出的结果是4