深入理解js执行上下文、作用域链、变量作用域

894 阅读6分钟

理解javascript中这些相对复杂的基础概念,有利于自己加深对于javascript基础知识的理解,在以后的工作中,如果遇到类似的问题,可以知道为什么会这样,以及应该去如何解决当前的问题

1.执行上下文的概念

当你运行一段javascript代码的时候,实际上是在运行执行上下文中;下面3种类型的代码会创建一个新的执行上下文:

  • 全局上下文是为运行代码主体而创建的执行上下文,也就是说它是为那些存在于JavaScript 函数之外的任何代码而创建的。
  • 每个函数会在执行的时候创建自己的执行上下文。这个上下文就是通常说的 “本地上下文”。
  • 使用 eval() 函数也会创建一个新的执行上下文。

2.作用域链

上下文中的代码在执行的过程中,会创建变量的作用域链(scope chain),这个作用域链决定了各级上下文中的代码在访问变量和函数时的顺序,代码正在执行的上下文的变量对象始终位于作用域链的最顶端。如果上下文是函数,则其活动对象(activation object)用作边变量对象,活动对象最初只有一个变量:arguments。(全局上下文中没有这个变量),作用域中的下一个变量对象包含了上下文,在下一个变量对象来自下一个包含上下文。以此类推至全局上下文,全局上下文的变量对象始终是作用域链的最后一个变量。

var color = 'blue';
function changeColor () {
    if (color == 'blue') {
        color = 'red';
    } else {
        color = 'blue'
    }
}
changeColor()

对于以上代码示例而言,函数 changeColor()的作用域包含两个对象: 一个是它自己的变量对象(实际指的是arguments),另外一个是全局上下文的变量对象。这个函数之所以在内部能够访问到color这个变量,就是因为在作用域链上能够找到它。

var color = 'blue';
function changeColor () {
    let anotherColor = 'red';
    function swapColors () {
        let tempColor = anotherColor;
        anotherColor = color;
        color = tempColor;
        // 在这里可以访问 color、anotherColor、tempColor
    }
    // 在这里可以访问 color、anotherColor,但是无法访问到 tempColor
    swapColors();
}
// 在这里只能访问color
changeColor()

以上代码涉及到3个上下文: 全局上下文、changeColor()局部上下文 和 swapColor()函局部上下文。

  • 全局上下文只有一个变量color和changeColor()函数,
  • changeColor()局部上下文包含变量anotherColor 和函数 swapColor(),但是在此处也能访问到全局上下文中的变量或者函数,比如color;
  • swapColor()局部上下文包含 tempColor, 只有这个函数能够访问这个变量,全局上下文和changeColor()是无法访问到这个变量的, 但是swapColor()函数可以访问 swapColor和全局上下文。 以下图例很好的展示了当前代码示例的作用域链

image.png

图4-3中的矩形表示不同的上下文。内部的上下文可以通过作用域链访问外部上下文的一切信息,但是外部的上下文无法访问内部的上下文的任何信息(结合图片,再次看看上面是如何描述当前的作用域链的)

3.变量作用域

1.使用var声明变量,变量会被自动的添加到最近的上下文当中

// 定义在全局变量中,改变量会存在于整个应用的生命周期里面,任何子集的作用域链都可以访问
var name = ‘crazyu’;

// 使用var 在函数内部声明,改变量只能在函数内部访问,外部访问会直接报错
function addNum (num1, num2) {
    var sum = num1 + sum2;
    return sum;
}
var result = addNum(10, 20);
console.log(sum); // 报错,没有sum此变量

2.使用let的块极作用域(ES6 新增)

let 与 var的区别有两点:

  1. let的作用域是块极的,let的作用域由最近的{} 确定
function test {
    let name = 'crazyu';
}
console.log(name) // referenceError: name 未定义
  1. let声明的变量在相同的代码块里面不能重复声明,而var 可以
var time = ‘20’;
var time = ‘30’;
function test {
    let a = 20;
    let a = 30;
}
// 会提示错误, 变量a已经被声明

image.png

image.png

3.使用const的常量声明(ES6 新增) const定义的变量一旦声明了之后不能在做更改,原始类型的数据不能做更改,引用类型可以修改修改属性,如果你看过引用类型是如何在内存中存储的,就应该知道为什么引用类型可以进行修改属性,是因为引用类型数据在栈内存中保存的是实际数据(堆内存)的指针。

const num = 0; // 原始类型的数据在定义的时候,就已经确定了值,且在以后不能更改
const obj = {
    name: 'carzyu'
}
obj.age = 18;  // 引用类型的属性是可以更改的,但是不可以这样修改 obj = 其他原始类型

4.当函数内部与全局变量中存在相同的变量的时候,优先使用函数内部的变量

需要特别注意的是: 当代码在执行的时候,寻找变量的顺序永远是优先当前运行环境的上下文, 如果在当前的上下文中存在当前变量,则直接使用;如果在当前运行的上下文中无法找到,则会向上一级的上下文中寻找,如果没有,会一直找到最顶层, 如果最顶层没有,则会报错,这部分请结合图4-3进行理解。

var name = ‘Global scope’
function test () {
    var name = 'function scope'
    console.log(name);
}
test(); // Global scope
console.log(name); // Global scope

4.变量提升

console.log(name); // 输出undefined
var name = 'crazyu';

// 以上代码等价于
var name;
console.log(name);
name = 'crazyu'
// 此处补充一道经典的面试题, 最后输出的是什么?以及为什么是这样的?与当前的变量提升是否有关系?
const div = document.querySelectorAll('div');
for (var i = 0; i < div.length; i++) {
    div[i].addEventListener('click', function() {
        return function() {
            console.log('i:'+ i)
        }
    })
}
// 在控制台连续输出 div.length 个   i: (div.length - 1)
// 假如查询到的div个数有10个;则输出 10个想同的输出。i: 9

// 如何改成按照顺序输出呢? 思考下?以及有哪几种方案可以选择,为什么这样改就是能做到输出是顺序输出,
// 背后的原理又是什么样的?


// 第一种解决方案: 提示:立即执行函数
const div = document.querySelectorAll('div');
for (var i = 0; i < div.length; i++) {
    (div[i].addEventListener('click', function() {
        return function() {
            console.log('i:'+ i)
        }
    }))(i)
}

// 第二种解决方案:使用块级作用域
const div = document.querySelectorAll('div');
for (let i = 0; i < div.length; i++) {
    div[i].addEventListener('click', function() {
        return function() {
            console.log('i:'+ i)
        }
    })
}

5.结尾

至此,文章就分享完毕了。

我是crazyu,一位前端开发工程师。

  • 文中如有错误,欢迎在评论区指正,如果这篇文章帮到了你,欢迎点赞和关注😊
  • 本文首发于掘金,未经许可禁止转载