ES范例解读之作用域

一道js口试题激发的思索

原文写于 2015-02-11 原文链接

前阵子帮部门口试一前端,看了下口试题(年青的时刻写后端java所以没做过前端试题),其中有一道题是如许的

比较下面两段代码,试述两段代码的差别之处
// 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”,假如对这一点另有疑问的同砚请自发归去复习一下js作用域的相干学问。。
那末既然输出一样那这两段代码详细的差别在哪呢?大部分人会说实行环境和作用域不一样,但基础上是那里不一样就不是大家都能说清楚了。前阵子就这个题目从新翻了下js基本跟ecmascript规范,假如我们想要寻根究底给出规范答案,那末我们须要先明白下面几个观点:

变量对象(variable object)

原文:Every execution context has associated with it a variable object. Variables and functions declared in the source text are added as properties of the variable object. For function code, parameters are added as properties of the 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则不会。

别的,关于变量怎样初始化,看这里
《ES范例解读之作用域》

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

运动对象(activation object)

原文:When control enters an execution context for function code, an object called the activation object is created and associated with the execution context. The activation object is initialised with a property with name arguments and attributes { DontDelete }. The initial value of this property is the arguments object described below.

The activation object is then used as the variable object for the purposes of variable instantiation.

简言之:当函数被激活,那末一个运动对象(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].contact([name,age,gender,b])。

实行环境和作用域链(execution context and scope chain)

  • execution context

    望文生义 实行环境/实行上下文。在javascript中,实行环境能够笼统的明白为一个object,它由以下几个属性组成:  
    
    executionContext:{
        variable object:vars,functions,arguments,
        scope chain: variable object + all parents scopes
        thisValue: context object
    }
此外在js诠释器运转阶段还会保护一个环境栈,当实行流进入一个函数时,函数的环境就会被压入环境栈,当函数实行完后会将其环境弹出,并将控制权返回前一个实行环境。环境栈的顶端始终是当前正在实行的环境。  
  • scope chain
    作用域链,它在诠释器进入到一个实行环境时初始化完成并将其分派给当前实行环境。每一个实行环境的作用域链由当前环境的变量对象及父级环境的作用域链组成。
    作用域链详细是怎样构建起来的呢,先上代码:

    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两段代码终究差别在那里,个人以为规范答案在这里:

答案来了

起首是A:

  1. 进入全局环境上下文,全局环境被压入环境栈,contextStack = [globalContext]

  2. 全局上下文环境初始化,

    globalContext={
        variable object:[scope, checkscope],
        scope chain: variable object // 全局作用域链
    }
    ,同时checkscope函数被建立,此时 checkscope.[[Scope]] = globalContext.scopeChain
  3. 实行checkscope函数,进入checkscope函数上下文,checkscope被压入环境栈,contextStack=[checkscopeContext, globalContext]。随后checkscope上下文被初始化,它会复制checkscope函数的[[Scope]]变量构建作用域,即 checkscopeContext={ scopeChain : [checkscope.[[Scope]]] }

  4. checkscope的运动对象被建立 此时 checkscope.activationObject = [arguments], 随后运动对象被当作变量对象用于初始化,checkscope.variableObject = checkscope.activationObject = [arguments, scope, f],随后变量对象被压入checkscope作用域链前端,(checckscope.scopeChain = [checkscope.variableObject, checkscope.[[Scope]] ]) == [[arguments, scope, f], globalContext.scopeChain]

  5. 函数f被初始化,f.[[Scope]] = checkscope.scopeChain。

  6. checkscope实行流继承往下走到 return f(),进入函数f实行上下文。函数f实行上下文被压入环境栈,contextStack = [fContext, checkscopeContext, globalContext]。函数f反复 第4步 行动。末了 f.scopeChain = [f.variableObject,checkscope.scopeChain]

  7. 函数f实行终了,f的上下文从环境栈中弹出,此时 contextStack = [checkscopeContext, globalContext]。同时返回 scope, 诠释器依据f.scopeChain查找变量scope,在checkscope.scopeChain中找到scope(local scope)。

  8. checkscope函数实行终了,其上下文从环境栈中弹出,contextStack = [globalContext]

假如你明白了A的实行流程,那末B的流程在细节上一致,唯一的区分在于B的环境栈变化不一样,

A: contextStack = [globalContext] —> contextStack = [checkscopeContext, globalContext] —> contextStack = [fContext, checkscopeContext, globalContext] —> contextStack = [checkscopeContext, globalContext] —> contextStack = [globalContext]

B: contextStack = [globalContext] —> contextStack = [checkscopeContext, globalContext] —> contextStack = [fContext, globalContext] —> contextStack = [globalContext]

也就是说,真要说这两段代码有啥差别,那就是他们实行过程当中环境栈的变化不一样,其他的两种体式格局都一样。

实在关于明白这两段代码而言最基础的一点在于,javascript是运用静态作用域的言语,他的作用域在函数建立的时刻便已肯定(不含arguments)。

说了这么一大坨偏理论的东西,能对峙看下来的同砚预计都要睡着了…是的,这么一套理论性的东西纠结有什么用呢,我只需晓得函数作用域在建立时便已天生不就好了么。没有实践代价的理论每每得不到注重。那我们来看看,当我们相识到这一套理论以后我们的天下究竟会发生了什么变化:

如许一段代码

function setFirstName(firstName){
     
    return function(lastName){
        return firstName+" "+lastName;
    }
}
 
var setLastName = setFirstName("kuitos");
var name = setLastName("lau");
 
 
// 乍看之下这段代码没有任何题目,然则天下就是如许,大部分东西都禁不起精细精美(我仔细起来连自身都畏惧哈哈哈哈)。。
// 挪用setFirstName函数时返回一个匿名函数,该匿名函数会持有setFirstName函数作用域的变量对象(内里包括arguments和firstName),不论匿名函数是不是会运用该变量对象里的信息,这个持有逻辑均不会转变。
// 也就是当setFirstName函数实行完以后实在行环境被烧毁,然则他的变量对象会一向保留在内存中不被烧毁(因为被匿名函数hold)。一样的,渣滓接纳机制会因为变量对象被一向hold而不做接纳处置惩罚。这个时刻内存泄漏就发生了。这时刻我们须要做手动开释内存的处置惩罚。like this:
setLastName = null;
// 因为匿名函数的援用被置为null,那末其hold的setFirstName的运动对象就可以被平安接纳了。
// 固然,当代浏览器引擎(以V8为首)都邑尝试接纳闭包所占用的内存,所以这一点我们也没必要过量处置惩罚。

ps:末了,关于闭包引发的内存泄漏那都是因为浏览器的gc题目(IE8以下为首)致使的,跟js自身没有关系,所以,请不要再问js闭包会不会激发内存泄漏了,谢谢合作!

    原文作者:kuitos
    原文地址: https://segmentfault.com/a/1190000003785291
    本文转自网络文章,转载此文章仅为分享知识,如有侵权,请联系博主进行删除。
点赞