前言
上一节中,我们讲到 js 引擎在真正执行代码之前,会首先会进入一个执行环境,我们将这个执行环境,称为执行上下文。
而执行上下文的生命周期包括两个阶段 —— 创建阶段和执行阶段。
在创建阶段,会进行变量对象的创建,作用域链的生成已以及 This 指向的确定。
变量对象的知识已经说了,这节课来说说第二件事 —— 建立作用域链。
1. 作用域
1.1 什么叫作用域
域,是指一片范围,而作用域,就是能够起作用的范围。
比如说,不同企业就是不同的域,你属于哪一个企业,你就可以享用这个企业提供的权利,你不能因为隔壁公司中午管饭你们不管,就跑去对面公司吃饭,除非他们公司同意。
1.2 JavaScript 中的作用域
在 JavaScript 代码中,也存在类似这样的作用域。这个作用域规定了 js 引擎执行代码时根据标志符名称进行变量查找的范围。
这里标识符就是指变量和函数名称。
js 中的作用域主要有几下几种:全局作用域、函数作用域、块作用域及eval作用域(一般不用,不考虑)。
1.3 全局作用域
我们知道,全局执行上下文会有相应的变量对象 —— widow,用来存储全局执行上下文中的变量、函数和其他方法。
在全局环境中执行 js 代码时,js 引擎便会去这个变量对象中查找标志符。
var friendA = '小刘';
var friendB = '小王';
console.log(`小茗能在朋友中找到${friendA} 和 ${friendB}`);
// 小茗能在朋友中找到小刘和小王
在真正执行代码之前,全局执行上下文的变量对象已经生成。当进行到 console.log()
时,变量对象将已变成活动对象:
// 此时已经成为活动对象AO
GlobalAO = {
friendA = '小刘';
friendB = '小王';
}
此时,js 引擎就会从活动对象 GlobalAO
中查询有没有 friendA
和 FriendB
这两个变量,并进行后续操作。
此时,活动对象 GlobalAO
就是全局作用域。 因为它就是全局环境中代码执行时进行标识符查询的范围。
在全局执行上下文中,可以引用到全局作用域中的变量或函数。
1.4 函数作用域
同全局执行上下文一样,函数执行上下文也有相应的变量对象,里面存放着函数内定义的变量和函数声明。
每个函数执行上下文都会生成各自的变量对象,它们互相隔离,默认情况下,在函数作用域外,是无法读取到函数内部定义的变量或函数的。
看个例子:
const friendA = '小刘';
const friendB = '小王';
function Family() {
const father = '老爸';
const mother = '老妈';
console.log(`小茗能在亲友中找到${father}、${mother}和${friendA}`;
// 小茗能在家庭中找到老爸、老妈和小刘
}
console.log(father); // ReferenceError: father is not defined
对于 Family
函数来讲,它最终活动对象是:
toFamilyAO = {
father: '老李',
mother: '老妈',
}
我们尝试在全局执行上下文中读取 Family
函数内部定义的变量,结果报错了。
但是,我们在 Family
内部,却能够取到全局作用域中的变量 friendA
,这是为什么?
这便和后面要讲的作用域链有关了。
1.5 块作用域
JavaScript 中有块作用域吗?
在 ES6 之前,我们很少会提到「块作用域」的概念。因为它在 JavaScript 中并不常用,以至于很多人都说,JavaScript 没有块作用域,这显然是不对的。
我们知道,每声明一个函数,则会生成一个函数作用域,在这个作用域中定义的变量或函数声明,只能在函数内部使用。
相应的,块作用域就是在某一块范围内声明的变量或函数,只能在这个代码块中使用。
什么叫代码块?
笼统来说,放在 {...}
中的代码就是一段代码块。常见的如 if(){...}
、 for(var i = 0; i < 100; i += 1 ) {...}
、with
及 try/catch
语句。
代码块与块作用域的关系
我们举个例子吧:
const b = 3;
// 下面就是代码块,但没有形成块作用域,因为在块中定义的变量外部可以访问
{
var a = 5;
console.log(a); // 5
}
console.log(a) // 5
// try catch 语句形成块作用域
// 块中的err,只有在catch 语句中使用
try {
throw 'error'
} catch(err) {
console.log(err); // 'error'
}
console.log(err); // err is not defined
// 下面是通过 ES6 的 let 和 const 形成块作用域的代码块
{
let c = 23;
const d = 'string';
console.log(c,d); // 23 , 'string'
}
console.log(c,d); // c is not defined
上面有三个例子,分别说明三个问题:
第一个例子:JavaScript 中不是所有的块都能形成块作用域。
第二个例子: JavaScript 是有块作用域的。
第三个例子:通过 const
和 let
我们可以让某一块代码形成块作用域。
2. 作用域链
了解了作用域,作用域链的概念就好理解多了。一句话:作用域链是当前环境作用域和其所有上层环境作用域组合起来的一个对象数组。 它规定了在当前环境中如何对外层环境中的变量进行有序访问。
在上面的例子中,有两个作用域 —— 全局作用域和 toFamily
函数作用域。
他们分别是变量对象 GlobalAO
和 toFamilyAO
。
跟着概念,我们来看一下全局执行上下文中创建的作用域链和 toFamily
中创建的作用域链。
2.1 全局执行上下文中的作用域链
让我们跟着定义先来思考几个问题。
第一,当前的作用域是什么?
没错,是 GlobalAO
。(前面已经说过了)
第二,全局作用域外层的作用域是什么?
没有,全局作用域已经是最外层了,它没有上层的作用域了。
因此,全局执行上下文的作用域链,也就等同于它的作用域 —— Gloabl
。
// 全局执行上下文
GlobalEC = {
// 全局执行上下文的活动对象
GlobalAO = {
friendA = '小刘';
friendB = '小王';
},
// 全局执行上下文中的作用域链
ScopeChain: [ GlobalAO ],
// this
this: window
}
2.2 函数执行上下文中的作用域链
同样,根据定义,我们来看下 toFamily
函数的作用域链。
第一,当前执行环境的作用域是什么?
没错,是函数作用域 toFamilyAO
。
第二,那它的上层的作用域呢?
我们看到,toFamily
上层是全局上下文了,因此,它上层的作用域就是 GlobalAO
。
第三,定义中说的是所有上层,那再看全局执行上下文还有上层环境吗?
没了,因此,函数的作用域链到此结束。
从上面我们可以看到,全局作用域,是作用域链的最末端。
一向如此吗?一向如此。
最后,我们看下 toFamily
的执行上下文。
// toFamily 执行上下文
toFamilyEC = {
// 函数执行上下文的活动对象
toFamilyAO = {
friendA = '小刘';
friendB = '小王';
},
// 全局执行上下文中的作用域链
ScopeChain: [ toFamilyAO, GlobalAO ],
// this
this: thisValue
}
我们看到,函数 toFalmily
的作用域链的最前端是当前环境的作用域 toFamilyAO
, 接着,是全局作用域 GlobalAO
(这也是最末端)。
这也就是为什么,在函数内部,可以访问到全局环境中的变量。
2.3 遮蔽效应
我们知道了,在某一执行上下文中,查询变量是沿着作用域链从前往后查找的,那假如当前作用域和上层作用域中都包含同一个变量时,会使用哪一个?
const a = 4;
const b = 5
function bar() {
const a = 3;
console.log(a,b); // 3 5
}
bar();
我们看到,bar
函数内部打印 a
,打印出的是在函数作用域中的值,而打印 b
, 打印的是全局作用域中的 b
。
执行上下文中的变量查找总是会沿着作用域链从前往后进行,一旦找到,便会停止查找。这便是作用域链中的遮蔽效应。
3. 最后的题目
最后,照例留一个题目,大家可以用来检验自己是否理解了上面的内容。
const a = 1;
if(a == 1) {
var b = 2;
const c = 3;
console.log(a, b ,c ); // ?
}
console.log(a, b, c); // ?
function bar() {
a = 4;
const e = 5;
console.log(a,e); // ?
function foo() {
const f = 6;
console.log(a, e, f); // ?
}
}
bar()
- 请尝试写出上面代码的打印值。
- 尝试写出上例中所有的作用域及作用域链。