(二)JS基础:作用域与作用域链

作用域概念

每一个变量,函数都有其作用的范围,超出了这个范围,就不能被访问,这就是作用域。作用域决定了变量与函数的可访问范围。

JS中的作用域

  1. 全局作用域
  2. 局部作用域

在ES6之前局部作用域只包含了函数作用域,在ES6新增的块级作用域,也属于局部作用域

2.1 全局作用域

  1. 浏览器会在计算机的内存中开辟一块内存, 用于执行JS代码, 这就是栈内存:执行上下文:EC Stack。 JS执行上下文栈可以认为是一个存储函数调用的栈结构,遵循先进后出的原则
  2. 一开始浏览器执行全局的代码时,首先创建全局的执行上下文:EC(G),压入执行栈的顶部
  3. 在这个全局上下文里声明的变量和函数,都是在全局作用域中
var num = 12

function fn() {
    console.log('fn')
}
复制代码

在函数内部,没有声明就直接赋值的变量,由于JS的变量提升的机制,会使这个变量称为全局变量。如下:

function fn() {
    num = 10 // 未声明且直接赋值
}
console.log(num) // 10
复制代码

全局作用域中声明的变量称之为全局变量。大量使用全局变量会污染全局命名空间, 容易引起命名冲突等问题。这使得代码很难维护,遇到问题时也不利于问题的定位。

  1. 建议使用const或者let声明变量, 当命名发生冲突时会报错。
  2. 避免大量使用全局变量
var num = 12
let num = 13 // Uncaught SyntaxError: Identifier 'num' has already been declared
复制代码

2.2 局部作用域

局部作用域一般只能在固定代码片段内可以访问到,最常见的例如函数内部。在这个函数内部声明的全部变量可以在整个函数的范围内使用和访问

var num = 12

function fn() {
    var n = 100
    console.log(num) // 12
    console.log(n) // 100
}

console.log(n) // Uncaught ReferenceError: n is not defined
复制代码

由以上代码可知,函数作用域可以访问外层作用里声明的变量。但是外层作用域不能直接访问到函数内部声明的变量。

函数创建:

  1. 创建函数和创建变量的差不多,函数名其实就是变量。创建函数会单独开辟一块内存空间:堆内存。函数堆内存存储的是函数体中的代码字符串
  2. 创建函数的时候,就声明了它的作用域scope,也就是所在的上下文
  3. 堆内存都有一个16进制的地址,这个地址会放到栈中存储,方便后期变量的引用

函数执行:

  1. 执行函数的时候,会形成一个全新的、私有的上下文环境:EC(fn),也就是函数作用域
  2. 当前的上下文环境中,有一个存放当前上下文环境内部声明变量的地方AO(fn):私有环境对象。这里的变量都是当前函数作用域内的私有变量,如:函数的形参,函数内部声明的变量
  3. 函数进入执行栈中执行
  4. 函数内部的代码执行之前,还要做很多的事情:
  • 初始化作用域链[scope-chain],<当前上下文环境,…, 全局上下文环境>,函数代码在执行的时候,遇到一个变量,首先会在当前的作用域的私有变量中开始查找,如果不是自己的私有变量,就会向上级作用域继续查找,一直到EC(G)全局作用域为止,这个也就是作用域链的查找机制
  • 初始化this
  • 初始化arguments
  • 形参赋值
  • 变量提升
  1. 代码自上而下执行
  2. 一般情况下(不是闭包的情况),函数执行形成的上下文环境,进栈执行结束后,会默认出栈释放掉(私有上下文中存储的一些私有变量和值都会释放),其目的是为了优化内存空间,减少栈内存的消耗。

2.3 块级作用域

块级作用域可通过新增命令let和const声明,所声明的变量在指定块的作用域外无法被访问。块级作用域在如下情况被创建:

  1. 在函数内部
  2. 代码块内部: {}

let 声明的语法与 var 的语法一致。你基本上可以用 let 来代替 var 进行变量声明,但会将变量的作用域限制在当前代码块中。块级作用域有几个特点:

  1. 声明变量不会提升到代码块顶部
  2. 不允许从外部访问块级作用域内部变量
  3. 禁止重复声明
{
    // 因为在解析代码时,JavaScript引擎也会注意到出现在块后面的let声明
    // 只不过在此之前不能以任何方式来引用未声明的变量
    // 在let声明之前的执行瞬间被称为“暂时性死区”
    // 在此阶段引用任何后面的才声明的变量都会抛出ReferenceError
    console.log(str) // ReferenceError: Cannot access 'str' before initialization
    let str = 'abc'
}

{
    let n = 100
}
console.log(n) // ReferenceError: n is not defined

// var可以重复声明,不过浏览器之后解析第一个,后面的是赋值操作
// ES6的let和const则不允许反复声明
var num = 12
let num = 13 // Uncaught SyntaxError: Identifier 'num' has already been declared
复制代码

作用域链

在函数内部没有声明但被使用的变量,称之为自由变量

作用域链的形成过程其实就是查找自由变量的过程。在上面也介绍了一下函数的执行过程,在函数执行的时候,会形成一个全新的、私有的上下文环境,也就是函数作用域

函数代码在执行的时候,遇到一个变量,首先会在当前的作用域的私有变量中开始查找,如果不是自己的私有变量,就会向上级作用域继续查找,一直到EC(G)全局作用域为止,这个也就是作用域链的查找机制

var a = 100
function n1() {
    var b = 200
    function fn2() {
        var c = 300
        /*
            在函数内部没有声明但被使用的变量:自由变量
            此时会先在当前函数作用域没有找到这个变量,
            就会向父级作用域继续找,一直到全局作用域,这就是作用域链的形成过程
        */
        console.log(a) //自由变量,一直找到全局作用域,打印出100
        console.log(b) //自由变量,找到上级的作用域EC(fn1),打印出200
        console.log(c)
    }
    fn2()
}
fn1()
复制代码
© 版权声明
THE END
喜欢就支持一下吧
点赞0 分享