js作用域

2020-11-18  本文已影响0人  时间的溺水者
比较下面两段代码,试述两段代码的不同之处
// A--------------------------
var scope = "global scope";
function checkscope(){
    var scope = "local scope";
    function f(){
        return scope;
    }
    return f();
}
checkscope();

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

首先A、B两段代码输出返回的都是 "local scope", 因为JavaScript采用的是词法作用域,函数的作用域基于函数创建的位置。

如果我们想要刨根问底给出标准答案,那么我们需要先理解下面几个概念:

变量对象(variable object)

    每一个执行上下文都会分配一个变量对象(variable object),变量对象的属性由 变量(variable) 和 函数声明(function declaration) 构成。在函数上下文情况下,参数列表(parameter list)也会被加入到变量对象(variable object)中作为属性。变量对象与当前作用域息息相关。不同作用域的变量对象互不相同,它保存了当前作用域的所有函数和变量。

    这里有一点特殊就是只有 函数声明(function declaration) 会被加入到变量对象中,而 ** 函数表达式(function expression) **则不会。看代码:
// 函数声明
function a(){}
console.log(typeof a); // "function"

// 函数表达式
var a = function _a(){};
console.log(typeof a); // "function"
console.log(typeof _a); // "undefined"

函数声明的方式下,a会被加入到变量对象中,故当前作用域能打印出 a。
函数表达式情况下,a作为变量会加入到变量对象中,_a作为函数表达式则不会加入,故 a 在当前作用域能被正确找到,_a则不会。

Global Object

    当js编译器开始执行的时候会初始化一个Global Object用于关联全局的作用域。对于全局环境而言,global object就是变量对象(variable object)。变量对象对于程序而言是不可读的,只有编译器才有权访问变量对象。在浏览器端,global object被具象成window对象,也就是说 global object === window === 全局环境的variable object。因此global object对于程序而言也是唯一可读的variable object。

活动对象(activation object)

  当函数被激活,那么一个活动对象(activation object)就会被创建并且分配给执行上下文。活动对象由特殊对象 arguments 初始化而成。随后,他被当做变量对象(variable object)用于变量初始化。
function a(name, age){
    var gender = "male";
    function b(){}
}
a(“k”,10);

a被调用时,在a的执行上下文会创建一个活动对象AO,并且被初始化为 AO = [arguments]。随后AO又被当做变量对象(variable object)VO进行变量初始化,此时 VO = [arguments].concat([name,age,gender,b])。

执行环境和作用域链

执行环境/执行上下文。
    在javascript中,执行环境可以抽象的理解为一个object,它由以下几个属性构成:
executionContext:{
    variable object:vars,functions,arguments,
    scope chain: variable object + all parents scopes
    thisValue: context object
}

此外在js解释器运行阶段还会维护一个环境栈,当执行流进入一个函数时,函数的环境就会被压入环境栈,当函数执行完后会将其环境弹出,并将控制权返回前一个执行环境。环境栈的顶端始终是当前正在执行的环境。

作用域链
   它在解释器进入到一个执行环境时初始化完成并将其分配给当前执行环境。每个执行环境的作用域链由当前环境的变量对象及父级环境的作用域链构成。

作用域链具体是如何构建起来的呢,先上代码:

function test(num){
    var a = "2";
    return a+num;
}
test(1);
  1.执行流开始 初始化function test,test函数会维护一个私有属性 [[scope]],并使用当前环境的作用域链初始化,在这里就是 test.[[Scope]]=global scope.

   2.test函数执行,这时候会为test函数创建一个执行环境,然后通过复制函数的[[Scope]]属性构建起test函数的作用域链。此时 test.scopeChain = [test.[[Scope]]]

    3.test函数的活动对象被初始化,随后活动对象被当做变量对象用于初始化。即 test.variableObject = test.activationObject.contact[num,a] = [arguments].contact[num,a]

    4.test函数的变量对象被压入其作用域链,此时 test.scopeChain = [ test.variableObject, test.[[scope]]];

至此test的作用域链构建完成。

那么A、B两段代码究竟不同在哪里?

第一个内部函数f在初始化时,会建立一个活动对象,它会添加一个属性名为scope的属性,会给它建立一个隐藏属性[[scope]],这个就是用于指向父级活动对象的。在到这个函数执行时,scope会被赋值,顺着它的[[scope]]就可以找到父级的值,返回一个代指的变量,继续返回到函数外部。输出local scope

第二个内部函数f在初始化的时候也是建立一个活动对象,这个活动对象上会添加一个属性名为scope属性。也会建立一个指向父级活动对象的[[scope]]隐藏属性。在checkscope第一次执行进入checkscope函数体的时候返回的是f指针值(对内部函数的一个引用),而非第一个返回的直接就是个原始值变量。第二次执行才进入f函数体,内部活动对象及[[scope]]私有属性已经建立,它便顺着这条链查找scope变量的值,并返回,形成闭包。

上一篇下一篇

猜你喜欢

热点阅读