day1.2_词法作用域和动态作用域

264 阅读5分钟

一、词法作用域和动态作用域

词法作用域(lexical scope)又称为静态作用域,是指在源码中变量和函数的定义位置决定了它们可见的范围,在词法作用域中,变量和函数的作用域是在编译阶段确定的,不会改变的,即函数的作用域在定义的时候就决定了,JavaScript中使用了词法作用域
动态作用域:与词法作用域相反,函数的作用域是在函数被调用的时候才决定的

二、词法作用域的场景

使用词法作用域,变量和函数能够访问他们在代码中定义的范围内的变量,当在一个作用域中引用一个变量时,解析器会先查找最近的定义该变量的作用域,如果找不到,就会继续向上级作用域查找,直到找到该变量的定义或者达到全家作用域。
这种作用域链的结构使得程序可以在不同的作用域中定义同名的变量,每个作用域的变量都是相互独立的,这样的设计有助于避免命名冲突,并支持模块化开发

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

person(); // 2
console.log(value); // 1

三、上下文执行栈

在JavaScript中,当我们的代码进入引擎运行时,会进入到上下文执行栈中,这个栈遵循先进后出原则。

  • 在上下文执行栈中,(es3时代中)包括
    • 全局执行上下文
    • 函数全局执行上下文
    • eval上下文(暂不考虑)
  • 每个执行上下文中包括
    • 变量对象VO(variable Object)
    • this
    • 作用域链(scope chain)
console.log(test); // ƒ test(){}

var test = 2323;

function test(){
    console.log(name); undefined;
    var name = '张三';
    console.log(name); // 张三
}

console.log(test); // 2323;

test();

当代码进入到这个执行栈后,会先创建一个全局上下文(Global Context),并将全局上下文压入执行栈中(栈底),并对全局代码进行分析,此时会先创建一个VO对象,当发现var、function定义的变量或函数时,会提前将其进行声明,且其中function的声明的优先级在var之上。

  • 上面的代码中,全局声明了var和function,但由于function的优先级高,所以第一个function打印的是函数
    • 且function和var最大的不同之处在于,function会直接声明,但var会赋值undefined
// 此时分析后,VO为
VO = {
    test: ƒ test(){},
    [[scope]]:[
            0:VO   // (或者说全局上下文 Global Context)
          ]
}

  • 分析完毕后,会开始运行代码
  • 执行第一句打印时,因为function已经提前声明,所以这里打印的是 test 函数
  • 紧接着 对 test 进行赋值
  • 因为function被提前了,所以这里的function声明不执行,直接跳过
  • 紧接着打印test,因为上面重新赋值了,所以这里是2323
  • 当执行到test调用时,又会创建一个新的函数执行上下文,并压入栈中,此时栈内为
    • [test Context,Global Context]
  • 此时对test函数进行分析
// 函数上下文又称为 AO (activation object)
AO = {
    argument:{
        length:0
    },
    name:undefined,
    [[scope]]:[
        0:Global Context
    ]
}

  • 当分析完毕后,开始执行代码
    • 此时因为var提前声明,所以第一个打印undefined
    • 紧接着对name进行赋值
    • 此时打印name就可以获取 张三
AO = {
   argument:{
    length:0
    },
    name:'张三',
    [[scope]]:[
        0:Global Context
    ] 
}
  • 当函数上下文执行完毕后,会弹出栈空间,此时栈内剩
    • [Global Context]
  • 紧接着继续执行全局上下文的代码,若全局上下文代码执行完毕,也会弹出栈空间
  • 至此,代码运行结束

三、思考题

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

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

因为js使用的是词法作用域,所以无论是返回函数的调用,还是返回一个函数,scope都是在定义的时候就确定了它的值,即定义时,就确定了他们的作用域链,所以两个都是local scope
以上两段代码执行结果一样,但是执行过程则不一样,我们都知道js运行的时候,会创建一个运行栈,且栈是先进后出,后入先出

  • 第一段代码
    • 先创建一个global作用域,并压入栈底
    • 执行到checkscope函数时,创建了一个 checkscope上下文,压入栈
    • 执行checkscope函数内部时,遇到f(),此时再次创建f上下文,压入栈
    • 当f作用域执行完毕后,压出栈并释放,此时剩checkscope上下文和global上下文
    • 之后依次执行完checkscope,释放checkscope,执行完global,释放global
  • 第二段代码
    • 先创建一个global上下文,并压入栈底
    • 执行到checkscope函数时,创建了一个 checkscope上下文,压入栈
    • 执行完checkscope函数后,返回一个f函数,此时checkscope执行完毕了,故释放掉
    • 回到global作用域,继续执行f()调用,此时会创建f上下文,并压入栈
    • 当f执行完毕后,释放掉,global执行完毕,也会释放掉

四、练习题

const value = 2;
function test1() {
    console.log(value);
}

function teset2() {
    const value = 999;
    console.log(value);
    test1();
}

test2(); // 999   2

上面的题,因为js的变量在定义的时候就决定了,所以test1中的定义时的作用域链为Global Context和自身,因为自身没有,所以沿着作用域链上找,就找到了2