-
Notifications
You must be signed in to change notification settings - Fork 81
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
Comments
总结来说,就是js是一门基于静态作用域的语言。如果这里是在动态作用域语言(如Lisp)环境下,那么B返回的就是global scope了。 |
非常牛的文章,受益匪浅! 有一点不明白,活动对象那部分,最后一段
name, age 不就是arguments吗?为什么还会被contact? |
对你文章我有些不同看法: 比如你说的: 我认为是这样的: js 中有函数/eval/global 这三种代码类型才会产生上下文,作用域. 比如 if/for等级块 就没有自己的作用域.他们需要依附其所在的作用域. js 执行阶段,会初始化一个上下文这个上下文是用一个叫变量对象的不可见成员表述. 但是函数存在特殊性.他的变量对象不能和宿主 global 处理方式一样. 在函数中这个变量对象被表述为活动对象.当一进入函数初始化上下文时,活动对象开始创建.活动对象的第一个成员就是 arguments. 另一个地方: 也有不同看法,在这个例子中 这就是一个简单的闭包. 作用域链我没细看你说的,但是我觉得作用域链就是一个单向链表而已. 节点不过是普通的 js 对象.无非是对外不可见. 其实你说了这么些整片文章就在讨论一个东西: 词法作用域. js 中普通变量(不要和this混淆了)寻址原则依据词法作用域寻址. 其实闭包不过就是此法作用域的表象而已. 匆忙看的,匆忙写的. 欢迎指正 |
@keenwon 这里的arguments就是js里的关键字 |
@hcforbaidu
确认一点,我这篇是从ES3 spec的角度来解读
我的文章中里应该没有提到过 |
首先谈下个人对这道题的理解: 第二个内部函数f在初始化的时候也建立一个活动对象,这个活动对象上会添加一个属性名为scope的属性。也会建立一个指向父级活动对象的[[scope]]隐藏属性。但在checkscope第一次执行进入checkscope函数体的时候返回的是f指针值,或者说对内部函数的一个引用,而非第一个返回的直接就是个原始值变量。第二次执行才进入f函数体,内部的活动对象及[[scope]]私有属性已经建立,它便顺着这条链查找scope变量的值,并返回,形成闭包。因为按常规的函数对象来说,当外层函数执行完就该销毁所有变量的,但此时一个函数指针被返回了,就意味着外部跟函数内部建立了联系,这个指针仍旧指着函数内部那个区块,它无法销毁,那条作用域链还在,因此内部那个函数也自然就可以访问到内部的私有变量了。 |
比较下面两段代码,试述两段代码的不同之处 // B--------------------------- 首先谈下个人对这道题的理解: 加一个属性名为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的执行顺序那段描述的相当精彩。虽然内存中未必完全遵 照那样的顺序,细节上可能有出入,但大体不会错。赞!! |
@keenwon,他首先推入的是arguments对象,不是形参。 个人见解,欢迎批评指正。 @kuitos ,ES3中词法作用域就是scope chain,这个说法貌似不能完全涵盖词法作用域的意义。那里面被链接的是个变量对象,也就是说变量就在这条链上回溯和作用,又称静态作用域(hi,又引出一概念)。所谓静态就是一旦定义就定死了呗!这么说来实际词法作用域既然叫作用域实际也就是针对变量来设定的概念,也就是变量是有作用范围的。它由词法作用域,也就是静态作用域,或者叫作用域链来限定。 |
第二道题的执行过程梳理: globalContext = checkscope.[[Scope]] = globalChain contextStack = checkscopeContext = [[Scope]]] f.[[Scope]] = checkscopeChain fContext = ` |
我只是觉得第一个只是返回了一个值,而第二个是一个闭包.... |
大牛的文章真是让人眼花缭乱 |
我一直区别是闭包...第二个引用了外层变量 |
这种执行机制类的信息,没有相应的文档之类的么。完全靠我们去根据执行效果去推测? |
@poolKylin 看 ecmascript spec,文中都是有引文的 |
是不是可以这样理解实际上函数表达式可以这样写: |
一道js面试题引发的思考
原文写于 2015-02-11
前阵子帮部门面试一前端,看了下面试题(年轻的时候写后端java所以没做过前端试题),其中有一道题是这样的
首先A、B两段代码输出返回的都是 "local scope",如果对这一点还有疑问的同学请自觉回去温习一下js作用域的相关知识。。
那么既然输出一样那这两段代码具体的差异在哪呢?大部分人会说执行环境和作用域不一样,但根本上是哪里不一样就不是人人都能说清楚了。前阵子就这个问题重新翻了下js基础跟ecmascript标准,如果我们想要刨根问底给出标准答案,那么我们需要先理解下面几个概念:
变量对象(variable object)
简言之就是:每一个执行上下文都会分配一个变量对象(variable object),变量对象的属性由 变量(variable) 和 函数声明(function declaration) 构成。在函数上下文情况下,参数列表(parameter list)也会被加入到变量对象(variable object)中作为属性。变量对象与当前作用域息息相关。不同作用域的变量对象互不相同,它保存了当前作用域的所有函数和变量。
这里有一点特殊就是只有 函数声明(function declaration) 会被加入到变量对象中,而 **函数表达式(function expression)**则不会。看代码:
函数声明的方式下,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)用于变量初始化。
用代码来说明就是:
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,它由以下几个属性构成:
此外在js解释器运行阶段还会维护一个环境栈,当执行流进入一个函数时,函数的环境就会被压入环境栈,当函数执行完后会将其环境弹出,并将控制权返回前一个执行环境。环境栈的顶端始终是当前正在执行的环境。
scope chain
作用域链,它在解释器进入到一个执行环境时初始化完成并将其分配给当前执行环境。每个执行环境的作用域链由当前环境的变量对象及父级环境的作用域链构成。
作用域链具体是如何构建起来的呢,先上代码:
至此test的作用域链构建完成。
说了这么多概念,回到面试题上,返回结果相同那么A、B两段代码究竟不同在哪里,个人觉得标准答案在这里:
答案来了
首先是A:
进入全局环境上下文,全局环境被压入环境栈,contextStack = [globalContext]
全局上下文环境初始化,
,同时checkscope函数被创建,此时 checkscope.[[Scope]] = globalContext.scopeChain
执行checkscope函数,进入checkscope函数上下文,checkscope被压入环境栈,contextStack=[checkscopeContext, globalContext]。随后checkscope上下文被初始化,它会复制checkscope函数的[[Scope]]变量构建作用域,即 checkscopeContext={ scopeChain : [checkscope.[[Scope]]] }
checkscope的活动对象被创建 此时 checkscope.activationObject = [arguments], 随后活动对象被当做变量对象用于初始化,checkscope.variableObject = checkscope.activationObject = [arguments, scope, f],随后变量对象被压入checkscope作用域链前端,(checckscope.scopeChain = [checkscope.variableObject, checkscope.[[Scope]] ]) == [[arguments, scope, f], globalContext.scopeChain]
函数f被初始化,f.[[Scope]] = checkscope.scopeChain。
checkscope执行流继续往下走到 return f(),进入函数f执行上下文。函数f执行上下文被压入环境栈,contextStack = [fContext, checkscopeContext, globalContext]。函数f重复 第4步 动作。最后 f.scopeChain = [f.variableObject,checkscope.scopeChain]
函数f执行完毕,f的上下文从环境栈中弹出,此时 contextStack = [checkscopeContext, globalContext]。同时返回 scope, 解释器根据f.scopeChain查找变量scope,在checkscope.scopeChain中找到scope(local scope)。
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)。
说了这么一大坨偏理论的东西,能坚持看下来的同学估计都要睡着了...是的,这么一套理论性的东西纠结有什么用呢,我只要知道函数作用域在创建时便已经生成不就好了么。没有实践价值的理论往往得不到重视。那我们来看看,当我们了解到这一套理论之后我们的世界到底会发生了什么变化:
这样一段代码
ps:最后,关于闭包引起的内存泄露那都是因为浏览器的gc问题(IE8以下为首)导致的,跟js本身没有关系,所以,请不要再问js闭包会不会引发内存泄露了,谢谢合作!
The text was updated successfully, but these errors were encountered: