Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

一道js面试题引发的思考 #18

Open
kuitos opened this issue Aug 23, 2015 · 16 comments
Open

一道js面试题引发的思考 #18

kuitos opened this issue Aug 23, 2015 · 16 comments

Comments

@kuitos
Copy link
Owner

kuitos commented Aug 23, 2015

一道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则不会。
另外,关于变量如何初始化,看这里

关于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].concat([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
Copy link
Owner Author

kuitos commented Aug 30, 2015

总结来说,就是js是一门基于静态作用域的语言。如果这里是在动态作用域语言(如Lisp)环境下,那么B返回的就是global scope了。

@keenwon
Copy link

keenwon commented May 28, 2016

非常牛的文章,受益匪浅!

有一点不明白,活动对象那部分,最后一段

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

name, age 不就是arguments吗?为什么还会被contact?
如果不是,那arguments又是什么?

@hooper-hc
Copy link

hooper-hc commented May 28, 2016

对你文章我有些不同看法: 比如你说的:
'当函数被激活,那么一个活动对象(activation object)就会被创建并且分配给执行上下文。活动对象由特殊对象 arguments 初始化而成。随后,他被当做变量对象(variable object)用于变量初始化。'

我认为是这样的: js 中有函数/eval/global 这三种代码类型才会产生上下文,作用域. 比如 if/for等级块 就没有自己的作用域.他们需要依附其所在的作用域.

js 执行阶段,会初始化一个上下文这个上下文是用一个叫变量对象的不可见成员表述. 但是函数存在特殊性.他的变量对象不能和宿主 global 处理方式一样. 在函数中这个变量对象被表述为活动对象.当一进入函数初始化上下文时,活动对象开始创建.活动对象的第一个成员就是 arguments.

另一个地方:
// 也就是当setFirstName函数执行完之后其执行环境被销毁,但是他的变量对象会一直保存在内存中不被销毁(因为被匿名函数hold)。同样的,垃圾回收机制会因为变量对象被一直hold而不做回收处理。这个时候内存泄露就发生了。这时候我们需要做手动释放内存的处理。

也有不同看法,在这个例子中
function setFirstName(firstName){
return function(lastName){
return firstName+" "+lastName;
}
}
由于 return 了一个内部函数,这个函数中访问了外部函数中的参数变量. 此时只要存在setFirstName 函数return的内部函数的引用, setFirstName这个函数的函数栈就不会被销毁.他还在函数栈里.
从侧面来看也是这样的结论: firstName 可以在外部访问的话.他所在的变量对象一定在.他所在的函数也一定没有被销毁.

这就是一个简单的闭包.

作用域链我没细看你说的,但是我觉得作用域链就是一个单向链表而已. 节点不过是普通的 js 对象.无非是对外不可见.

其实你说了这么些整片文章就在讨论一个东西: 词法作用域. js 中普通变量(不要和this混淆了)寻址原则依据词法作用域寻址. 其实闭包不过就是此法作用域的表象而已.
此法作用域大致就是: 变量只在定义它的域里执行.

匆忙看的,匆忙写的. 欢迎指正

@kuitos
Copy link
Owner Author

kuitos commented May 28, 2016

@keenwon 这里的arguments就是js里的关键字arguments

@kuitos
Copy link
Owner Author

kuitos commented May 28, 2016

@hcforbaidu

js 中有函数/eval/global 这三种代码类型才会产生上下文,作用域. 比如 if/for等级块 就没有自己的作用域.他们需要依附其所在的作用域....
其实你说了这么些整片文章就在讨论一个东西: 词法作用域....

确认一点,我这篇是从ES3 spec的角度来解读词法作用域的(ES3里面还是叫scope chain),也就是我第一个comment中说的静态作用域。抛开新增的块作用域不谈,在此之前js都是函数作用域的,即便是global。eval有点特殊,它能造出一些类似动态作用域的假象,能根据上下文环境set source text的词法环境,跟new Function一样。

由于 return 了一个内部函数,这个函数中访问了外部函数中的参数变量. 此时只要存在setFirstName 函数return的内部函数的引用, setFirstName这个函数的函数栈就不会被销毁.....

我的文章中里应该没有提到过函数栈。我一直说的是执行环境调用栈(call stack),栈顶的上下文执行完之后会被pop并销毁(ThisBinding&LexicalEnvironment&VariableEnvironment),但是并不等同于函数本身会被销毁。

@jiangtian83
Copy link

首先谈下个人对这道题的理解:
第一个内部函数f在初始化的时候会建立一个活动对象,这个活动对象上会添加一个属性名为scope的属性。其次会给他建立一个隐藏属性[[scope]],这个就是用于指向父级活动对象的。在到这个函数执行的时候,scope会被赋值,顺着它的[[scope]]就可以找到父级的值。然后返回一个带值的变量,继续返回到函数外部。输出为local scope。

第二个内部函数f在初始化的时候也建立一个活动对象,这个活动对象上会添加一个属性名为scope的属性。也会建立一个指向父级活动对象的[[scope]]隐藏属性。但在checkscope第一次执行进入checkscope函数体的时候返回的是f指针值,或者说对内部函数的一个引用,而非第一个返回的直接就是个原始值变量。第二次执行才进入f函数体,内部的活动对象及[[scope]]私有属性已经建立,它便顺着这条链查找scope变量的值,并返回,形成闭包。因为按常规的函数对象来说,当外层函数执行完就该销毁所有变量的,但此时一个函数指针被返回了,就意味着外部跟函数内部建立了联系,这个指针仍旧指着函数内部那个区块,它无法销毁,那条作用域链还在,因此内部那个函数也自然就可以访问到内部的私有变量了。

@jiangtian83
Copy link

比较下面两段代码,试述两段代码的不同之处
// 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()();

首先谈下个人对这道题的理解:
第一个内部函数f在初始化的时候会建立一个活动对象,这个活动对象上会添

加一个属性名为scope的属性。其次会给他建立一个隐藏属性[[scope]],这

个就是用于指向父级活动对象的。在到这个函数执行的时候,scope会被赋值

,顺着它的[[scope]]就可以找到父级的值。然后返回一个带值的变量,继续

返回到函数外部。输出为local scope。

第二个内部函数f在初始化的时候也建立一个活动对象,这个活动对象上会添

加一个属性名为scope的属性。也会建立一个指向父级活动对象的[[scope]]

隐藏属性。但在checkscope第一次执行进入checkscope函数体的时候返回的

是f指针值,或者说对内部函数的一个引用,而非第一个返回的直接就是个原

始值变量。第二次执行才进入f函数体,内部的活动对象及[[scope]]私有属

性已经建立,它便顺着这条链查找scope变量的值,并返回,形成闭包。因为

按常规的函数对象来说,当外层函数执行完就该销毁所有变量的,但此时一

个函数指针被返回了,就意味着外部跟函数内部建立了联系,这个指针仍旧

指着函数内部那个区块,它无法销毁,那条作用域链还在,因此内部那个函

数也自然就可以访问到内部的私有变量了。

—————————————————————————————————

变量对象:

每一个执行上下文都会分配一个变量对象(variable object),变量对象的

属性由 变量(variable) 和 函数声明(function declaration) 构成。在

函数上下文情况下,参数列表(parameter list)也会被加入到变量对象

(variable object)中作为属性。变量对象与当前作用域息息相关。不同作

用域的变量对象互不相同,它保存了当前作用域的所有函数和变量。

变量对象由形参列表,内部普通变量和函数声明构成。this和arguments是

另外两个特殊对象。不参与变量查找的过程。函数声明被加入变量对象,函

数表达式不会,也即不会提前。

全局初始化会初始化也会初始化一个全局变量对象。变量对象对于程序不可

读,只有编译器才有权访问。

上面理解貌似有些误差:arguments应该在活动对象建立的时候被添加到了活

动对象里,一个类数组对象。暂且这么说吧!this作为一个特殊对象跟活动

对象及[[scope]]并列。也或许this也被添加到了活动对象里。这是个小疑

点。

关于scope chain的执行顺序那段描述的相当精彩。虽然内存中未必完全遵

照那样的顺序,细节上可能有出入,但大体不会错。赞!!

@jiangtian83
Copy link

@keenwon,他首先推入的是arguments对象,不是形参。
@hcforbaidu 其实你的描述跟楼主的描述是一样的,不过你是将活动对象认为就是活动对象,而楼主将其从活动对象转化为变量对象了。都是很抽象的过程,具体还得看js内部实现机理,作为理解来说,二者都可以。
你说函数没被销毁,活动对象仍在,作用域链仍在,从这个角度来说的确是,但是它肯定从环境栈中被弹出了。只不过执行上下文还在。说没被销毁说的过去。
作用域链是不是个链表不清楚,但它可以沿着这个途径找到变量倒是真的。或许是个数组,或对象,使用遍历去找这个变量。你说这个链表上都是普通对象,也没错,js中皆对象,这些活动变量对象自然也是对象咯。包括维护的那个链本身说不定也是个数组或对象呢!
词法作用域这个概念我觉得别引了,引多了容易让人糊涂,什么是词法作用域?词,单词,法,语法,就是单词(标识符,原始值,操作符等),语法自然就是js中的各种语法规则了,那词法作用域在js中就两种,一种全局一种函数。变量只在定义它的域里执行。这句话从这个角度理解就好理解了,定义在全局那就在全局找到,定义在函数中就在函数中找到,父级,爷爷级就到对应位置去找。不过使用执行一词貌似不妥啊!容易误导人。

个人见解,欢迎批评指正。

@kuitos ,ES3中词法作用域就是scope chain,这个说法貌似不能完全涵盖词法作用域的意义。那里面被链接的是个变量对象,也就是说变量就在这条链上回溯和作用,又称静态作用域(hi,又引出一概念)。所谓静态就是一旦定义就定死了呗!这么说来实际词法作用域既然叫作用域实际也就是针对变量来设定的概念,也就是变量是有作用范围的。它由词法作用域,也就是静态作用域,或者叫作用域链来限定。

@jiangtian83
Copy link

第二道题的执行过程梳理:
`1.
contextStack =
[
globalContext
]

globalContext =
[
globalariableObject: [scope,checkscope],
globalChain: globalariableObject
]

checkscope.[[Scope]] = globalChain

contextStack =
[
checkscopeContext,
globalContext
]

checkscopeContext =
[
checkscopeariableObject = [arguments,scope,f],
checkscopeChain = [checkscopeariableObject,checkscope.

[[Scope]]]
]

f.[[Scope]] = checkscopeChain
3.
contextStack =
[
fContext,
globalContext
]

fContext =
[
fariableObject = [arguments, scope],
fscopeChain = [fariableObject, f.[[Scope]]]
]

`

@wubing0324
Copy link

我只是觉得第一个只是返回了一个值,而第二个是一个闭包....

@Lanveer
Copy link

Lanveer commented Aug 10, 2018

大牛的文章真是让人眼花缭乱

@niubaobaozsy
Copy link

我一直区别是闭包...第二个引用了外层变量

@shimengno1
Copy link

一道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则不会。
另外,关于变量如何初始化,看这里

关于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].concat([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闭包会不会引发内存泄露了,谢谢合作!

你好。如你所说,并非js本身的关系,那在node环境下使用闭包,不是一样会存在这个问题。

@poolKylin
Copy link

这种执行机制类的信息,没有相应的文档之类的么。完全靠我们去根据执行效果去推测?

@kuitos
Copy link
Owner Author

kuitos commented May 21, 2021

@poolKylin 看 ecmascript spec,文中都是有引文的

@424525600
Copy link

424525600 commented Oct 27, 2021

是不是可以这样理解实际上函数表达式可以这样写:
var a = function(){};
所以
var a = function _a(){};
这段代码中的_a相当于是无效的,后面也就是undefined

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests

10 participants