javascript 作用域-作用域链

最近看了一些javascript相关的知识,打算整理一下

一、作用域

先来看一段代码

for (var i = 0; i < 10; i ++) {
        console.log('test');
}
console.log(i);

如果是在其他语言里面,console.log(i)会报错,因为i的只在for循环这个块里面起作用。但是javascript里面这段代码打印结果是10

下面说一下javascript里面的作用域以及作用域链等相关知识

1.1 全局作用域 和 函数作用域

在javascript里,作用域是用来规定变量与函数可见性和生命周期一套规则。在ES6之前,作用域有两种,分别是全局作用域函数作用域

全局作用域中的对象在代码中的任何地方都能访问,其生命周期伴随着页面的生命周期。

函数作用域即函数内部定义的变量或者函数,并且定义的变量或者函数只能在函数内部被访问。

看下面这段示例代码:

var a = 3;
function foo () {
    console.log(a);
    if (1) {
        var a = 5;
        console.log(a);
    }
    console.log(a);
}
foo();

这段示例代码的打印结果是:

undefined
5
5

按照其他语言里面的逻辑,照理说第一个console.log(a)应该打印出3,但是在javascript里面,因为函数作用域以及变量提升的存在,所以第一个console.log(a)打印出了undefined

1.2 模拟块级作用域

在ES5里面可以利用函数来达到模拟块级作用域的目的,例如对最上方的for循环代码进行改造,看以下示例代码:

(function () {
    for (var i = 0; i < 10; i ++) {
        console.log('test');
    }
})();

console.log(i);

这段代码打印结果是:

Error: i is not defined

这种方式叫函数自执行,以下这种方式是最常用的写法。

(functino() {
	……
	})();

1.3 let 与 const关键字

ES6引入了let和const关键字,使javascript也能和其他语言一样拥有块级作用域。

使用let关键字声明的变量是可以被改变的,使用const关键字声明的变量是不可以被改变的。使用let/const命令声明的变量只在let/const命令所在代码块内有效。

使用let关键字来改造上方的代码,示例代码如下

var a = 3;
function foo () {
    console.log(a);
    if (1) {
        let a = 5;
      	console.log(a);
    }
    console.log(a);
}
foo();

以上这段示例代码打印出:

3
5
3

这样就实现了块内声明的变量不影响块外面的变量。

1.4 javascript是如何支持块级作用域的

块级作用域是通过词法环境的栈结构来实现的。(变量提升是通过变量环境来实现的)词法环境与变量环境结合,使得javascript同时支持变量提升和块级作用域。

下面以代码具体来说明执行上下文里面变量环境和词法环境的变化。

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

这段代码的打印结果是:

1
3
4
5
1
2
4

在foo函数的执行上下文刚被创建的时候,如下图所示,函数内部使用var声明的变量,在编译阶段被放到了变量环境中,使用let/const声明的变量,在编译阶段被放到了词法环境中。在函数内部的块作用域内,使用let/const声明的变量没有被放到词法环境中。

foo函数的执行上下文刚被创建的时候

当执行到块作用域时,执行上下文如下图所示

当执行到块作用域时

在执行到块作用域的console.log(a)时,执行上下文如下图所示。此时需要查找a,先在词法环境中从栈顶往栈底查找a,找不到再去变量环境中查找

执行到块作用域的console.log(a)时

当块作用域执行结束之后,其内部定义的变量就会从词法环境的栈顶弹出,如下图所示

当块作用域执行结束之后,其内部定义的变量就会从词法环境的栈顶弹出

注意: 在块作用域内,使用let/const声明的变量被提升,只是变量的创建被提升,初始化并没有被提升。看以下示例代码:

let a = 1
{
  console.log(a)
  let a = 2
}

打印结果是:

Error: Cannot access 'a' before initialization

二、作用域链

先看一段示例代码,看看打印结果是不是符合想象

function bar() {
    console.log(a)
}
function foo() {
    var a = 10
    bar()
}
var a = 5
foo()

这段代码的打印结果是5

5

本来猜测打印结果是10,但是实际上打印结果是5,这里就涉及到作用域链相关知识。

在每个执行上下文的变量环境中,都包含了一个外部引用,用来指向外部的执行上下文。在查找变量的时候,会首先在当前执行上下文中查找,找不到的话,再去外部执行上下文中查找。这个查找的链条就称为作用域链

而上述代码中,bar函数执行上下文的外部引用为全局执行上下文,foo函数执行上下文的外部引用也为全局执行上下文。外部引用的指向是由词法作用域决定的。

词法作用域是指作用域是有代码中函数声明的位置来决定的,是静态的作用域。词法作用域是代码阶段就决定好的,和函数是怎么调用的没有关系。根据代码中的位置,bar函数和foo函数的外部引用都是全局执行上下文

同一个作用域下,对同一个函数的不同的调用会产生不同的执行上下文环境,继而产生不同的变量的值,所以,作用域中变量的值是在执行过程中确定的,而作用域是在函数创建时就确定的。如果要查找一个作用域下某个变量的值,就需要找到这个作用域对应的执行上下文环境,再在其中找到变量的值

如果上方的代码改成下面这个样子,那么打印结果就不同了,如下方所示:

function foo() {
  	function bar() {
    	console.log(a)
	}
    var a = 10
    bar()
}
var a = 5
foo()

这段代码的打印结果是

10

关于块级作用域中变量的查找,可以看下方示例代码:

function fun1() {
    function fun2() {
        var a = 1
        let c = 100
        if (1) {
            let a = 2
            console.log(b)
        }
    }

    var a = 3
    let b = 50
    {
        let b = 60
        fun2()
        console.log(b)
    }
}
var a = 4
let b = 80
fun1()

打印结果是:

50
60

为什么打印出来的第一个是50而不是60呢,因为作用域在代码阶段就决定了,fun2里面使用了自由变量b,所以找不到的时候要去定义fun2的fun1的作用域里面找,而在fun1的作用域里面,只有值为50的那个b的作用域才是fun1,值为60的那个b的作用域只是一个块。

如果想要打印结果是两个60,则可以将代码改成这样:

function fun1() {
    
    var a = 3
    let b = 50
    {
    	function fun2() {
	        var a = 1
	        let c = 100
	        if (1) {
	            let a = 2
	            console.log(b)
	        }
    	}
        let b = 60
        fun2()
        console.log(b)
    }
}
var a = 4
let b = 80
fun1()
Show Comments