前言
根据上篇关于 新手秒懂 - 高逼格解释变量提升 的文章中说明了,在生成执行上下文的创建阶段,生成变量对象后会建立作用域链。那我们接下里就看看作用域和作用域链到底是个啥子玩意。
作用域
作用域是一套规则, 用于确定在何处以及如何查找变量(标识符)。(说白了就是你写代码的那块旮旯里,来确定你之后怎么查找变量,简单粗暴。。)
词法作用域 & 动态作用域
- 词法作用域: 函数的作用域在函数定义的时候就决定了。(javascript 采用的是静态作用域)
- 动态作用域:函数的作用域是在函数调用的时候才决定的。
简单的例子表述一下:
var value = 1;
function foo() {
console.log(value);
}
function bar() {
var value = 2;
foo();
}
bar(); // 1
我将以最简单的大白话告诉您发生了啥: foo函数执行 -> 查询value值(没有) -> 向上查找(var value = 1), so, 打印 1。
静态作用域,只看定义时的位置,就像你跟别人做了邻居,哪天你老婆吵架了跑出去了,不在家里。你只要去外面找就行了,别人家就算也有老婆,但肯定不是你要找的老婆啊对不对??
函数作用域 & 块级作用域
-
函数作用域: 已声明函数的形式, 将内部代码"隐藏"起来。从而形成函数作用域
-
级作用域: 从 ES3 开始,try/catch 结构在catch 分句中具有块作用域。在 ES6 中引入了 let/const 关键字( var 关键字的表亲), 用来在任意代码块中声明变量。 if(…) { let a = 2; } 会声明一个劫持了 if 的 { … } 块的变量,并且将变量添加到这个块 中。如下例所示:
var foo = true;
if (foo) {
let bar = foo * 2;
bar = something( bar );
console.log( bar );
}
console.log( bar ); // ReferenceError
匿名函数表达式 & IIFE
以前刚入门的时候被人问到一个问题:请问,立即执行函数表达式的作用是什么??
白痴的我竟然把匿名函数和 IIFE(立即执行函数表达式) 认为是同一个东西。
-
匿名函数表达式: 顾名思义,就是没有名字标识的函数表达式(注意: 函数声明则不可以省略函数名)
-
IIFE:最常见的用法其实就是使用了匿名函数表达式并最后加入(),让它立即执行。
var a = 2; (function () { var a = 3; console.log( a ); // 3 // 匿名函数表达式内及是块级作用域 })(); console.log( a ); // 2
而它的作用主要包括几点:
- 避免命名冲突
- 减少内存占用
- 产生块级作用域,造成作用域隔离。关于为啥要存在块级作用域,请参照《ECMAScript 6 入门》
作用域链
定义
当查找变量的时候,会先从当前上下文的变量对象中查找,如果没有找到,就会从父级(词法层面上的父级)执行上下文的变量对象中查找,一直找到全局上下文的变量对象,也就是全局对象。这样由多个执行上下文的变量对象构成的链表就叫做作用域链。
形象一点就是:
作用域链就像一栋楼,当前作用域在一楼,全局作用域在顶楼,就是一直往上找你要用的变量。
而编译器的查找方式有两种:
- 如果目的是获取变量的值,就会使用 RHS 查询, 不成功的RHS引用会导致抛出 ReferenceError 异常。 // console.log(a) --> VM130:1 Uncaught ReferenceError: a is not defined
- 赋值操作符会导致 LHS 查询, 不成功的 LHS 引用会导致自动隐式地创建一个全局变量(非严格模式下),该变量使用 LHS 引用的目标作为标识符,或者抛 出 ReferenceError 异常(严格模式下)。 // a = 1
深入理解
因为javascript 是静态作用域,函数的作用域在函数定义的时候就决定了。
这是因为函数有一个内部属性 [[scope]],当函数创建的时候,就会保存所有父变量对象到其中,你可以理解 [[scope]] 就是所有父变量对象的层级链,但是注意:[[scope]] 并不代表完整的作用域链!(意思就是在函数创建时就可以拿到父级的变量对象VO)
文字比较难理解没关系,咱们以一个例子说明
function foo() {
function bar() {
...
}
}
在函数创建时, 各自的[[scope]]为:
foo.[[scope]] = [
globalContext.VO
];
bar.[[scope]] = [
fooContext.AO,
globalContext.VO
]; // 父级对象的 AO/VO(表示变量对象),俺的上篇文章提到过
而之后函数激活, 变量对象就会添加到作用链的前端
作用域链 = [AO].concat([[Scope]]);
下面我们结合示例具体说说实现过程:
var scope = "global scope";
function checkscope(){
var scope2 = 'local scope';
return scope2;
}
checkscope();
执行过程如下:
1.checkscope 函数被创建,保存作用域链到 内部属性 [[scope]]
checkscope.[[scope]] = [
globalContext.VO // 创建时就可以获取父变量对象(静态作用域)
];
2.执行 checkscope 函数,创建 checkscope 函数执行上下文,checkscope 函数执行上下文被压入执行上下文栈
执行上下文栈:当执行一个函数的时候,就会创建一个执行上下文,并且压入执行上下文栈,当函数执行完毕的时候,就会将函数的执行上下文从栈中弹出。
ECStack = [ // 执行上下文栈
checkscopeContext, // checkscope上下文
globalContext // 全局上下文
];
3.checkscope 函数启动(不执行函数内的请求)。开始做准备工作,第一步:复制函数[[scope]]属性创建作用域链
checkscopeContext = {
作用域链: checkscope.[[scope]], //上面提到的创建时生成的[[scope]]
}
4.第二步:用 arguments 创建活动对象,随后初始化活动对象,加入形参、函数声明、变量声明(生成变量对象的的几个过程)
checkscopeContext = {
VO: { // 变量对象
arguments: {
length: 0
},
scope2: undefined
}
}
5.第三步:将活动对象压入 checkscope 作用域链顶端
checkscopeContext = {
VO: {
arguments: {
length: 0
},
scope2: undefined
},
作用域链: [VO, [[Scope]]]
}
6.准备工作做完,开始执行函数,随着函数的执行,修改 AO (活动变量,变量对象的执行阶段)的属性值
checkscopeContext = {
AO: {
arguments: {
length: 0
},
scope2: 'local scope' // 函数执行后 scope2 获取到值
},
作用域链: [AO, [[Scope]]]
}
7.查找到 scope2 的值,返回后函数执行完毕,函数上下文从执行上下文栈中弹出
ECStack = [
globalContext
];
结尾
这篇主要分享的是作用域相关知识,感觉大致了解就差不多了,写的都是我自己的浅薄理解,有错误的地方欢迎指出,对于变量对象不了解的小伙伴请参照我的上篇文章 新手秒懂 - 高逼格解释变量提升,还是一句话,努力,奋斗??
参考文献
《你不知道的JavaScript(上卷)》
我的前端学习交流群562862926,2019前端开发全套资料开发工具欢迎大家加群领取