你还要我怎样的JS系列(4) -- 作用域链

前言

上一章节我们讲了VO

我们回顾一下之前的内容。

进入执行上下文会创建VO对象、建立作用域链、确定this指向。执行上下文的数据(函数形参、变量声明、函数声明)是作为属性存储在VO中的。

我们也知道变量对象在每次进入上下文时创建,并填入初始值,值的更新出现在代码执行阶段。

这一章节我们继续深入了解执行上下文,我们来认识作用域链。

作用域链

这里引用ECMA-262-3的定义:

每一个执行上下文都与一个作用域链相关联。作用域链是一个对象组成的链表,求值标识 符的时候会搜索它。当控制进入执行上下文时,就根据代码类型创建一个作用域链,并用初始化对象填充。执行一个上下文的时候,其作用域链只会被 with 声明和 catch 语句所影响。

不能一下子看明白没关系,我们接着往下看,待会儿回过来自己思考思考。

一个demo:

 
 

这个例子的执行上下文创建和弹出的过程不明白的参见执行上下文

根据上边ECMA-262-3的定义,作用域链是一个变量对象组成的链表,用来进行变量查询。 比如上面的'bar'上下文的作用域链依次是 AO(bar)、AO(foo)、VO(global)。

我们这样模拟全局上下文:

 
 

其中:Scope = AO|VO + [[Scope]] 也就是当前变量对象加上所有父级变量对象的列表。

讲AO的时候我们说过了有两个阶段,进入上下文(初始化)、代码执行阶段(update值)。

我们还知道JS是词法作用域规则。直白的说就是你写代码的时候就确定了作用域。不考虑eval环境的话,[[scope]]与函数紧紧搂抱(有关)。

函数的生命周期

函数的生命周期分为两个阶段:创建、调用阶段。

  1. 函数创建

    思考:

     

    我们会得到预期的结果,没毛病。我们可能是这样分析的:

    在foo的作用域内只有b的声明,执行alert的时候对a进行RHS查询,没找到,就去外层作用域查找,ok,我们找到了。

    现在我们得用更底层的思路去分析。虽然上边的分析没毛病。

    首先我们可以确定 foo 的 AO 对象、全局的 VO 对象:

     

    那么它是怎么找到 a 的呢?

    联系上前边我们说过的作用域链,是不是有一点点触碰到了。

    总结:

    1. [[scope]]是所有 父级变量对象的层级链,处于当前函数上下文之上,在函数创建时存于其中。当然对a的查找是顺着层级往上的(可以理解为从数组的头开始往右一层一层的找)。

    2. [[scope]]在函数创建时被存储,静态(不变的),永远永远,直到函数被销毁。也就是说函数一旦创建, [[scope]]属性已经写入,并存储在函数对象中,无论函数是否调用。

    3. [[scope]]和作用域链不是一个概念哦,[[scope]]是函数的一个属性而已。

       
  2. 函数调用阶段

    前边提到了:Scope = AO|VO + [[Scope]]

    其实更容易理解的形式是这样:

     

    在函数调用(进入上下文)阶段,会把当前的VO|AO加入到当前执行函数[[scope]]属性的前边。

    这时我们再回到文章开头提到的ECMA262-3对于作用域链的定义。定义中提到了标识符。

    标识符是干什么呢?

    标识符的作用就是确定一个变量(或函数声明)属于哪个变量对象。标识符解析算法在ECMA262-3中也有定义:

    执行过程中,使用下面的算法进行标识符解析查找:

    1. 获取作用域链中的下一个对象。如果没有,转到步骤5。
    2. 调用 Result(1) 的 [[HasProperty]] 方法,把标识符作为属性名传递。
    3. 如果 Result(2) 为 true,返回一个引用类型的值,其基对象是 Result(1),属性名为标识符。
    4. 转到步骤1。
    5. 返回引用类型的值,基对象为 null,属性名为标识符。
    6. 求值标识符的结果总是一个引用类型的值,其成员名字组件与标识符字符串相等

    多读几遍。

    大体上说明标识符解析总是会返回一个引用类型,这个引用类型的 getBase() 结果是对应的变量对象(或若未找到则为null)。属性名是向上查找的标识符的名称。 在向上查找中,一个上下文中的局部变量较之于父作用域的变量拥有较高的优先级。可以理解为由内向外查找,如果找到了就会返回引用类型,外层有重名的也不会找到那儿去。

    引用类型在this的章节会详细说明。

    我们通过一个copy的复杂demo来熟悉熟悉:

     

    在上面代码的执行阶段:

    1. 首先全局上下文

       
    2. 对于foo

       
    3. 对于 bar

       

      对'x', 'y', 'z'标识符的解析过程:

      1. 对'x'进行查找:

         
      2. 对'y'进行查找:

         
      3. 对'z'进行查找:

         

      就是这样。虽然没有完全的解释清楚ECMA262-3中对于标识符解析的算法规则,但是这样容易理解,也就是这样找的。

闭包

闭包这玩意儿可以单独写一章节。这里只是说说其与[[scope]]属性的联系。

闭包,几乎所有的JS书籍上的介绍都略有不同,每个人都有每个人的理解。

在<<你不知道的JavaScript>>中,闭包定义是:

函数拥有对其词法作用域的访问,哪怕是在当前作用域之外执行

红宝书中是:

闭包是指有权访问另一个函数作用域中的变量的函数。

我们通过一个demo来解读:

 
 

我提几点,然后自己思考。得到的结论就是你对闭包的初步认识了。

从本章节的内容来看,闭包与[[scope]]属性息息相关。首先之前提到了函数的[[scope]]属性是静态属性,函数创建的时候就被存储到函数对象中,函数销毁才会销毁。

闭包的特点恰好就是持久的保有对其定义的词法作用域的访问权限。

再来一点,[[scope]]中保存的是当前函数的所有上层变量对象。上面的demo中foo()持久访问的正是其上层匿名立即执行函数的AO对象中的属性。

所以,闭包是函数代码和其[[scope]]的结合?

Function构建的函数[[scope]]中只有全局的VO

这个Function是很有意思的,之前在一些讲解原型的高热度的文章中,发现作者很多会说所有的函数都有prototype属性。其实是错误的,比如Function.prototype.bind创建的函数就是没有prototype属性的。

同样Function构建的函数,其[[scope]]也比较特殊,里面只有全局的VO对象。

 
 

f3只能够访问全局VO中的属性,不能访问VO(foo);印证了[[scope]]里面只有全局的VO对象。

with & catch & eval

eval不建议使用,就简单提一提。代码eval的上下文与当前的调用上下文(calling context)拥有同样的作用域链:

 
 

文章开始部分引用的ECMA-262-3关于作用域的定义中提到了with & catch这一点。

事实上在代码执行阶段,可以通过with声明和catch语句修改作用域链。

它们将会被添加到作用域链的最前端:

 
 

很多资料对这个过程解释的很绕,我们通过一段代码具体分析:

 
 

我们一步一步分析。

  1. 代码开始执行

     
  2. 执行with语句

     
  3. with结束

     

OK,catch的过程是一样的,就两个核心: 第一是withObject|catchObject会被添加到作用域链前端,其实就是标识符解析从withObj|catchObj先开始,因为它在头上嘛。这也是我们说with、catch可以挟持作用域的原因。

第二就是声明完成之后会移除这些状态,回到最初的美好。

留一个思考题:

 
 

作用域链与原型链

二维作用域链查找,就是说在对象中没找到就去原型链上查找:

 
 

这个其实大家很熟悉,原型链末尾都是 -> Object.prototype -> null。

顺着原型链逐级的委托,最终会成功输出'jack ma'。

好的,接着:

 
 

这里输出了10,而不是顶层原型定义的50。上一个例子明明是输出的原型委托继承的值啊?

我们分析:

 
 

现在写出这个流程大家应该驾轻就熟。我们能够得到一些什么呢?

执行阶段会顺着原型链的层级进行标识符解析工作。而活动对象(AO)是没有原型的。 所以inner的AO中没有x属性,就去下一链级的outer中找。找到了,如果没有就去全局找了,全局中的x是可以通过原型委托继承得到的。

所以,假如你删除var x = 10;得到的是原型委托继承的50。

下一章

执行上下文 -- this

参考

ECMA-262-3
dmitrysoshnikov.com


原文发布时间:2017-11-30

本文来源掘金如需转载请紧急联系作者

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
在 Vue.js 中,`data-v-` 是作用于样式的 Scoped CSS 选择器,它的作用是限定 CSS 只作用于当前组件的范围内,避免全局样式污染。如果您需要去掉 `<option>` 后面的 `data-v-`,有两种可能的解释和做法: 1. 去掉 Scoped CSS 选择器 如果您只是需要去掉 Scoped CSS 选择器,而不是真正的删除 `data-v-` 属性,可以使用以下代码实现: ```javascript // 获取所有 <option> 元素 const options = document.querySelectorAll('option'); // 遍历所有 <option> 元素 options.forEach(option => { // 获取当前 <option> 元素的 data-v- 属性值 const dataV = option.getAttribute('data-v-'); // 如果 data-v- 属性值存在,则将其替换为空字符串 if (dataV) { option.setAttribute('data-v-', ''); } }); ``` 这样做的效果是,保留了 `data-v-` 属性,但是去掉了它的作用于,Scoped CSS 的限制将不再生效。 2. 删除整个 `data-v-` 属性 如果您需要彻底删除 `data-v-` 属性,可以使用以下代码实现: ```javascript // 获取所有 <option> 元素 const options = document.querySelectorAll('option'); // 遍历所有 <option> 元素 options.forEach(option => { // 删除当前 <option> 元素的 data-v- 属性 option.removeAttribute('data-v-'); }); ``` 这样做的效果是,直接删除了 `data-v-` 属性,Scoped CSS 的限制也将不再生效。需要注意的是,如果您使用了 Vue.js 的动态组件或异步组件等高级特性,可能会出现一些意想不到的问题。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值