浅谈JS的作用域链(一)
JS的执行环境
执行环境(Execution context,EC)或执行上下文,是JS中一个极为重要的概念。
在JavaScript中有三种代码运行环境:
Global Code
- JavaScript代码开始运行的默认环境
Function Code
- 代码进入一个JavaScript函数
Eval Code
- 使用eval()执行代码
EC的组成
当JavaScript代码执行的时候,会进入不同的执行上下文,这些执行上下文会构成了一个执行上下文栈(Execution context stack,ECS)。
- 变量对象(Variable object,VO): 变量对象即包含变量的对象,除了我们无法访问它外,和普通对象没什么区别。
[[Scope]]
属性:作用域即变量对象,作用域链是一个由变量对象组成的带头结点的单向链表,其主要作用就是用来进行变量查找。而[[Scope]]属性是一个指向这个链表头节点的指针。- this: 指向一个环境对象,注意是一个对象,而且是一个普通对象,而不是一个执行环境。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
|
var a = "global var" ; function foo(){ console.log(a); } function outerFunc(){ var b = "var in outerFunc" ; console.log(b); function innerFunc(){ var c = "var in innerFunc" ; console.log(c); foo(); } innerFunc(); } outerFunc() |
代码首先进入Global Execution Context,然后依次进入outerFunc,innerFunc和foo的执行上下文,执行上下文栈就可以表示为:
当JavaScript代码执行的时候,第一个进入的总是默认的Global Execution Context,所以说它总是在ECS的最底部。
对于每个Execution Context都有三个重要的属性,变量对象(Variable object,VO),作用域链(Scope chain)和this。这三个属性跟代码运行的行为有很重要的关系,下面会一一介绍。
当然,除了这三个属性之外,根据实现的需要,Execution Context还可以有一些附加属性。
变量对象(Variable object)
变量对象是与执行上下文相关的数据作用域。它是一个与上下文相关的特殊对象,其中存储了在上下文中定义的变量和函数声明。也就是说,一般VO中会包含以下信息:
- 变量 (var, Variable Declaration);
- 函数声明 (Function Declaration, FD);
- 函数的形参
当JavaScript代码运行中,如果试图寻找一个变量的时候,就会首先查找VO。对于前面例子中的代码,Global Execution Context中的VO就可以表示如下:
注意,假如上面的例子代码中有下面两个语句,Global VO仍将不变。
(function bar(){}) // function expression, FE
baz = "property of global object"
也就是说,对于VO,是有下面两种特殊情况的:
- 函数表达式(与函数声明相对)不包含在VO之中
- 没有使用var声明的变量(这种变量是,"全局"的声明方式,只是给Global添加了一个属性,并不在VO中)
活动对象(Activation object)
只有全局上下文的变量对象允许通过VO的属性名称间接访问;在函数执行上下文中,VO是不能直接访问的,此时由激活对象(Activation Object,缩写为AO)扮演VO的角色。激活对象 是在进入函数上下文时刻被创建的,它通过函数的arguments属性初始化。
Arguments Objects 是函数上下文里的激活对象AO中的内部对象,它包括下列属性:
- callee:指向当前函数的引用
- length: 真正传递的参数的个数
- properties-indexes:就是函数的参数值(按参数列表从左到右排列)
对于VO和AO的关系可以理解为,VO在不同的Execution Context中会有不同的表现:当在Global Execution Context中,可以直接使用VO;但是,在函数Execution Context中,AO就会被创建。
当上面的例子开始执行outerFunc的时候,就会有一个outerFunc的AO被创建:
通过上面的介绍,我们现在了解了VO和AO是什么,以及他们之间的关系了。下面就需要看看JavaScript解释器是怎么执行一段代码,以及设置VO和AO了。
细看Execution Context
当一段JavaScript代码执行的时候,JavaScript解释器会创建Execution Context,其实这里会有两个阶段:
创建阶段(当函数被调用,但是开始执行函数内部代码之前)
- 创建Scope chain
- 创建VO/AO(variables, functions and arguments)
- 设置this的值
激活/代码执行阶段
- 设置变量的值、函数的引用,然后解释/执行代码
这里想要详细介绍一下"创建VO/AO"中的一些细节,因为这些内容将直接影响代码运行的行为。
对于"创建VO/AO"这一步,JavaScript解释器主要做了下面的事情:
- 根据函数的参数,创建并初始化arguments object
扫描函数内部代码,查找函数声明(Function declaration)
- 对于所有找到的函数声明,将函数名和函数引用存入VO/AO中
- 如果VO/AO中已经有同名的函数,那么就进行覆盖
扫描函数内部代码,查找变量声明(Variable declaration)
- 对于所有找到的变量声明,将变量名存入VO/AO中,并初始化为"undefined"
- 如果变量名称跟已经声明的形式参数或函数相同,则变量声明不会干扰已经存在的这类属性
看下面的例子:
1
2
3
4
5
6
7
8
9
10
11
|
function foo(i) { var a = 'hello' ; var b = function privateB() { }; function c() { } } foo(22); |
对于上面的代码,在"创建阶段",可以得到下面的Execution Context object:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
|
fooExecutionContext = { scopeChain: { ... }, variableObject: { arguments: { 0: 22, length: 1 }, i: 22, c: pointer to function c() a: undefined, b: undefined }, this : { ... } } |
在"激活/代码执行阶段",Execution Context object就被更新为:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
|
fooExecutionContext = { scopeChain: { ... }, variableObject: { arguments: { 0: 22, length: 1 }, i: 22, c: pointer to function c() a: 'hello' , b: pointer to function privateB() }, this : { ... } } |
例子分析
前面介绍了Execution Context,VO/AO等这么多的理论知识,当然是为了方便我们去分析代码中的一些行为。这里,就通过几个简单的例子,结合上面的概念来分析结果。
Example 1
首先看第一个例子:
1
2
3
4
5
6
7
8
9
10
11
|
( function (){ console.log(bar); console.log(baz); var bar = 20; function baz(){ console.log( "baz" ); } })() |
在Chrome中运行代码运行后将输出:
代码解释:匿名函数会首先进入"创建结果",JavaScript解释器会创建一个"Function Execution Context",然后创建Scope chain,VO/AO和this。根据前面的介绍,解释器会扫描函数和变量声明,如下的AO会被创建:
所以,对于bar,我们会得到"undefined"这个输出,表现的行为就是,我们在声明一个变量之前就访问了这个变量。这个就是JavaScript中"Hoisting"。
Example 2
接着上面的例子,进行一些修改:
1
2
3
4
5
6
7
8
9
10
11
12
13
|
( function (){ console.log(bar); console.log(baz); bar = 20; console.log(window.bar); console.log(bar); function baz(){ console.log( "baz" ); } })() |
运行这段代码会得到"bar is not defined(…)"错误。当代码执行到"console.log(bar);"的时候,会去AO中查找"bar"。但是,根据前面的解释,函数中的"bar"并没有通过var关键字声明,所有不会被存放在AO中,也就有了这个错误。
注释掉"console.log(bar);",再次运行代码,可以得到下面结果。"bar"在"激活/代码执行阶段"被创建。
Example 3
现在来看最后一个例子:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
|
( function (){ console.log(foo); console.log(bar); console.log(baz); var foo = function (){}; function bar(){ console.log( "bar" ); } var bar = 20; console.log(bar); function baz(){ console.log( "baz" ); } })() |
代码的运行结果为:
代码中,最"奇怪"的地方应该就是"bar"的输出了,第一次是一个函数,第二次是"20"。
其实也很好解释,回到前面对"创建VO/AO"的介绍,在创建VO/AO过程中,解释器会先扫描函数声明,然后"foo: <function>"就被保存在了AO中;但解释器扫描变量声明的时候,虽然发现"var bar = 20;",但是因为"foo"在AO中已经存在,所以就没有任何操作了。
但是,当代码执行到第二句"console.log(bar);"的时候,"激活/代码执行阶段"已经把AO中的"bar"重新设置了。
总结
本文介绍了JavaScript中的执行上下文(Execution Context),以及VO/AO等概念,最后通过几个例子展示了这几个概念对我们了解JavaScript代码运行的重要性。
通过对VO/AO在"创建阶段"的具体细节,如何扫描函数声明和变量声明,就可以对JavaScript中的"Hoisting"有清晰的认识。所以说,了解JavaScript解释器的行为,以及相关的概念,对理解JavaScript代码的行为是很有帮助的。
浅谈JS的作用域链(一)的更多相关文章
- 浅谈JS的作用域链(三)
前面两篇文章介绍了JavaScript执行上下文中两个重要属性:VO/AO和scope chain.本文就来看看执行上下文中的this. 首先看看下面两个对this的概括: this是执行上下文(Ex ...
- 浅谈JS的作用域链(二)
上一篇文章中介绍了Execution Context中的三个重要部分:VO/AO,scope chain和this,并详细的介绍了VO/AO在JavaScript代码执行中的表现. 本文就看看Exec ...
- 浅谈 js eval作用域
原文:浅谈 js eval作用域 就简单聊下如何全局 eval 一个代码. var x = 1; (function () { eval('var x = 123;'); })(); console. ...
- 浅谈js变量作用域
变量的作用域也是前端面试题常考的一个问题,掌握下面几个规律可以帮你更好的理解js的作用域. 1.作用域优先级遵循就近原则,函数内部的作用域优先级大于外部 var a=456; var b=111; f ...
- 浅谈JS中的闭包
浅谈JS中的闭包 在介绍闭包之前,我先介绍点JS的基础知识,下面的基础知识会充分的帮助你理解闭包.那么接下来先看下变量的作用域. 变量的作用域 变量共有两种,一种为全局变量,一种为局部变量.那么全局变 ...
- 浅谈 js 语句块与标签
原文:浅谈 js 语句块与标签 语句块是什么?其实就是用 {} 包裹的一些js代码而已,当然语句块不能独立作用域.可以详细参见这里<MDN block> 也许很多人第一印象 {} 不是对象 ...
- 浅谈JS严格模式
浅谈JS严格模式 简介 何为严格模式?严格模式(strict mode)即在严格的条件下运行,在严格模式下,很多正常情况下不会报错的问题语句,将会报错并阻止运行. 但是,严格模式可以显著提高代码的健壮 ...
- 浅谈JS中的!=、== 、!==、===的用法和区别 JS中Null与Undefined的区别 读取XML文件 获取路径的方式 C#中Cookie,Session,Application的用法与区别? c#反射 抽象工厂
浅谈JS中的!=.== .!==.===的用法和区别 var num = 1; var str = '1'; var test = 1; test == num //tr ...
- 浅谈JS中 var let const 变量声明
浅谈JS中 var let const 变量声明 用var来声明变量会出现的问题: 1. 允许重复的变量声明:导致数据被覆盖 2. 变量提升:怪异的数据访问.闭包问题 3. 全局变量挂载到全局对象:全 ...
随机推荐
- MySQL5.7中的sql_mode默认值
简介 在正常项目开发过程中,如果MySQL版本从5.6升级到5.7版本.作为DBA在考虑数据库版本升级带来的影响时,一般会有几个注意点: sql_mode 默认值的改变 optimizer_switc ...
- JavaScript高级特性-实现继承的七种方式
声明和约定: 在C++和Java中,我们可以通过关键字class来声明一个类,在JavaScript中没有这个关键字,但我们知道可以通过new一个function创建对象,这个function类似C+ ...
- python TCP socket套接字编程以及注意事项
TCPServer.py #coding:utf-8 import socket #s 等待链接 #c 实时通讯 s = socket.socket(socket.AF_INET,socket.SOC ...
- .NET Core tasks.json 简介
1.执行命令:dotnet> dotnet new console -o myApp 2.tasks.json文件配置: { "version": "2.0.0&q ...
- table表格(笔记)
<table class="table table-hover2 pick_carTable"> <thead> <tr class="bl ...
- 【PAT】1083 是否存在相等的差(20 分)
//这题不是我耍流氓,实在太简单,只能直接贴代码了,凑个数 #include<stdio.h> int aaa[10005]={0}; int main(){ int N;scanf(&q ...
- 在同一个服务器(同一个IP)为不同域名绑定的免费SSL证书
越来越多的浏览器不在支持http协议了,这就要求你为你的网站必须绑定SSL证书.谷歌浏览器也将要在今年取消对http协议的支持,申请CA证书迫在眉睫.我购买有两个域名,一个虚拟机,没事鼓捣鼓捣,图个乐 ...
- 转://oracle字符集
一.oracle字符集基础知识oracle数据库有国家字符集(national character set)与数据库字符集(database character set)之分.两者都是在创建数据库时需 ...
- 【js】 ==与===区别
1.对于string,number等基础类型,==和===是有区别的 1)不同类型间比较,==之比较“转化成同一类型后的值”看“值”是否相等,===如果类型不同,其结果就是不等 2)同类型比较,直接进 ...
- go标准库的学习-mime/multipart
参考:https://studygolang.com/pkgdoc 导入方式: import "mime/multipart" multipart实现了MIME的multipart ...