作用域
系列开篇
为进入前端的你建立清晰、准确、必要的概念和这些概念的之间清晰、准确、必要的关联, 让你不管在什么面试中都能淡定从容。没有目录,而是通过概念关联形成了一张知识网络,往下看你就明白了。当你遇到【关联概念】时,可先从括号中的(强/弱)判断简单这个关联是对你正在理解的概念是强相关(得先理解你才能继续往下)还是弱相关(知识拓展)从而提高你的阅读效率。我也会定期更新相关关联概念。
面试题
- 用你自己话说说什么是作用域
- 词法作用域,静态作用域,动态作用域,函数作用域,块级作用域等等分别都是啥意思
这是干什么的?
作用域是一套规则,它规定了如何查找变量,也就是确定当前执行代码对变量的访问权限。
简单来说,我们在写代码时,就已经把代码分隔成一个个代码块(区域),在这些代码块中定义许多变量,而作用域就规定哪些代码块能访问哪些变量。
这些变量的访问权限是在你代码写出来就已经确定的了,不能改了,是静态的了,所以也称为静态作用域,或 词法作用域(lexical scoping)。
如果一个变量或者其他表达式不在当前的作用域中,那么它就是不可用的。
作用域链
你在写代码时肯定不会是流水账,必然有嵌套的情况出现吧。作用域此时就发生了嵌套,具有了层次。
子作用域可以访问父作用域,反之则不行,这就是作用域链的规矩。
我们在程序中要使用声明的变量时,引擎总会从最近的一个域先找,没有则向上层逐次地查找,直到找到最外层全局作用域中。
想象儿子可以跟父亲或者爷爷要东西(外部作用域变量),长辈不好跟小孩要糖(内部作用域变量)
现在我们大概了解了作用域是什么,下面更细至地分析下。
词法作用域(静态作用域)
词法作用域就是定义在词法阶段的作用域
换句话说,词法作用域是由你在写代码时将变量和块作用域写在哪里来决定的,
因此当词法分析器处理代码时会保持作用域不变(大部分情况下是这样的,除了欺骗词法的几个方法)。
因为 JavaScript 采用的是词法作用域,函数的作用域在函数定义的时候就决定了。
遮蔽效应
作用域查找会在找到第一个匹配的标识符时停止。
在多层的嵌套作用域中可以定义同名的标识符,这叫作“遮蔽效应”(内部的标识符“遮蔽”了外部的标识符)。
抛开遮蔽效应,作用域查找始终从运行时所处的最内部作用域开始,逐级向外或者说向上进行,直到遇见第一个匹配的标识符为止。
// 作用域链示意
var value = 'global' // 全局作用域变量
function bar() {
var innerValue = 'inner'
var value = '遮蔽了全局的同名变量'
console.log(value); // 遮蔽了全局的同名变量 [先从最近的域开始找,找不到再往外]
console.log(innerValue); // inner
}
bar();
console.log(value); // golbal [先从最近的作用域找,看上去直接是全局作用域]
console.log(innerValue); // ReferenceError: innerValue is not defined [外不访内]
那如果这样呢
// 作用域链示意
var value = 'global' // 全局作用域变量
function bar() {
console.log(value); // golbal [不管这个函数在哪被调用,看他定义时(声明)的作用域]
}
function foo() {
var value = 'foo 内部的变量'
bar(); // 虽然bar在foo 内部调用,但是根据词法作用域要看定义时bar的作用域
}
foo();
关键看定义(函数声明)时的作用域
欺骗词法
- eval
- with 这两个机制的副作用是引擎无法在编译时对作用域查找进行优化,因为引擎只能谨慎地认为这样的优化是无效的。使用这其中任何一个机制都将导致代码运行变慢。不要使用它们。想了解去 MDN 查查看。
动态作用域
为了不让你搞混,首先下一个结论。
JavaScript 并不具有动态作用域。它只有词法作用域,简单明了。 但是 this 机制某种程度上很像动态作用域。
主要区别:词法作用域是在写代码或者说定义时确定的,而动态作用域是在运行时确定的。(this 也是!)
词法作用域关注函数在 何处声明,而动态作用域关注函数从何处调用。
所以分析 this 指向时,分析的是函数调用栈,这个在关联概念 this 【关联概念(弱)】中详细说。
Bash/Perl等语言使用动态作用域。
函数作用域
顾名思义,函数作用域的含义是指,属于这个函数的全部变量都可以在整个函数的范围内使用及复用(事实上在嵌套的作用域中也可以使用)。
块级作用域
简单来说,块级作用域就是包含在语法 { } 中的作用域。
- try/catch
- let / const
在 ES6 中引入了 let 关键字 (var 关键字的表亲),用来在任意代码块中声明变量。
if (..) { let a = 2; }
会声明一个劫持了 if 的 { .. } 块的变量,并且将变量添加到这个块中。
简单来说就是let 没有变量提升【关联概念】这回事。
从块顶部到该变量的初始化语句,这块区域叫做 TDZ(临时死区)。
再明白点就是你不声明就不能用,用了就报错。
if (true) {
//`*************** 临时死区 ***************`
console.log(a);
console.log(b);
//`*************** 临时死区 ***************`
var a = 2;
let b = 3;
}
首先会因为变量提升打出 undefined 然后因为在临时死区内使用let 定义的变量抛出错误。
实现原理是什么?
我们深刻理解了这个概念之后,可以探究下它的实现(面试也经常问到这方面源码),可能有人觉得没啥用,我觉得它的用处是拓展出其他相关联的【必知】概念,也可以看看你的硬编码能力,再不济看看你的记忆力如何也是好的。(^-^)
在词法环境执行上下文 -> 词法环境【关联子概念(强)】内外部的关联,其实就是作用域链原理,其实这里我们可以理解执行上下文中的词法环境就是这里通称的作用域。因为就是它确定了当前执行代码对变量的访问权限。
其他
参考
- You don't know Javascript
- developer.mozilla.org/zh-CN/docs/…
- developer.mozilla.org/zh-CN/docs/…