JS原理 - 理解作用域

作用域

作用域(scope)是一套规则,用来管理引擎如何在当前作用域以及嵌套的子作用域中根据标识符名称进行变量查找。简单来说,作用域规定了如何查找变量。

JavaScript 采用了静态作用域,函数的作用域在函数被定义时就被确定的。无论函数在哪里被调用,无论如何被调用,它的作用域只由函数定义所处的位置决定。

看一个例子:

var value = 1;
function foo() {
    console.log(value);
}
function bar() {
    var value = 2;
    foo();
}
bar(); // 1

当执行 foo 函数时,先从 foo 函数内部查找是否有变量 value,结果没有,于是就沿定义函数的位置,查找上一层的代码,查找到全局变量 value ,所以函数执行的结果为 1

再看一个例子:

var scope = "global scope";
function checkscope(){
    var scope = "local scope";
    function f(){
        return scope;
    }
    return f();
}
checkscope(); // local scope

对此,权威指南的解释是这样的:JavaScript 函数的执行用到了作用域链,这个作用域链是在函数定义的时候创建的。嵌套的函数 f() 定义在这个作用域链里,其中的变量 scope 一定是局部变量,不管何时何地执行函数 f(),这种绑定在执行 f() 时依然有效。

最后看一个例子:

var bar = {
    myName:"time.geekbang.com",
    printName: function () {
        console.log(myName)
    }    
}
function foo() {
    let myName = "A"
    return bar.printName
}
let myName = "B"
let _printName = foo()
_printName();  // B
bar.printName();  // B

最后打印出来的是两个 B,这里是为了说明在寻找变量的时候是在当前作用域中寻找,而不是在代码结构内向上寻找,在一开始的时候,我经常犯这样的错误。

块级作用域

在ES6之前,ES的作用域只有两种:全局作用域和函数作用域。

  • 全局作用域中的对象在代码中的任何地方都能访问,只要页面没有被销毁,便一直存在。
  • 函数作用域就是在函数内部定义的变量或者函数,并且定义的变量或者函数只能在函数内部被访问。函数执行结束之后,函数内部定义的变量会被销毁。

那为什么要加入块级作用域呢?主要由于 JS 的变量提升存在着变量覆盖、变量污染等设计缺陷,所以 ES6 引入了块级作用域关键字来解决这些问题。

举个变量覆盖的例子:

var myname = "A"
function showName(){
  console.log(myname);
  if(0){
   var myname = "B"
  }
  console.log(myname);
}
showName()

最后打印的结果是两个undefined,因为函数showName内存在变量提升,但是没有进行赋值操作。

再看个变量污染的例子:

function foo(){
  for (var i = 0; i < 7; i++) {
  }
  console.log(i); 
}
foo()

同样是因为变量提升,在循环之后,i 并没有被销毁,所以打印出来的结果是 7。

那么 JS 是如何既支持变量提升又支持块级作用域的?

这就要从执行上下文入手了,看一个具体的例子:

function foo(){
    var a = 1
    let b = 2
    {
      let b = 3
      var c = 4
      let d = 5
      console.log(a)
      console.log(b)
    }
    console.log(b) 
    console.log(c)
    console.log(d)
}   
foo()

首先,JS 引擎会对函数代码进行编译,生成如下图所示的执行上下文

从图上可以看出

  • 函数内部通过 var 声明的变量,在编译阶段全都被存放到变量环境里面了。
  • 通过 let 声明的变量,在编译阶段会被存放到词法环境(Lexical Environment)中。

接下来,第二步继续执行代码,当执行到代码块里面时,变量环境中 a 的值已经被设置成了 1,词法环境中 b 的值已经被设置成了 2,与此同时作用域块中使用 let 声明的 b 并没有去影响词法环境中已有的 b 的值,而是将他们放在了相互独立的单独区域

当执行到作用域块中的console.log(a)这行代码时,就需要在词法环境和变量环境中查找变量a的值了,具体查找方式是:沿着词法环境的栈顶向下查询,如果在词法环境中的某个块中查找到了,就直接返回给JavaScript引擎,如果没有查找到,那么继续在变量环境中查找

作用域链

在每个执行上下文的变量环境中,都包含了一个外部引用,用来指向外部的执行上下文,我们把这个外部引用称为outer。

当一段代码使用了一个变量时,JS 引擎首先会在“当前的执行上下文”中查找该变量,如果在当前的变量环境中没有查找到,那么 JS 引擎会继续在 outer 所指向的执行上下文中查找,一直找到全局上下文。这样由多个执行上下文的变量对象构成的链表就叫做作用域链。

看个例子:

function bar() {
    console.log(myName)
}
function foo() {
    var myName = "极客邦"
    bar()
}
var myName = "极客时间"
foo()

例子中的执行上下文如下图:

JS原理 - 理解作用域

https://hashencode.github.io/post/7851/

作者

BiteByte

发布于

2022-07-18

更新于

2024-01-11

许可协议