[JS]作用域的“生产者”——词法作用域
【版权声明】未经博主同意,谢绝转载!(请尊重原创,博主保留追究权)
https://www.cnblogs.com/cnb-yuchen/p/18252500
出自【进步*于辰的博客】
参考笔记二,P43.3、P46.1、P9.3;笔记三,P70、P71。
先言
启发博文:(转发)
这两篇博文是我系统学习时参考的文章,二位博主总结得很全面,我受益颇多!有所总结,将这些总结罗列至本篇文章,相信这能够让你更为快速、便捷地掌握这些知识点。
当然,我所作的总结是“精简”的内容,并且一些是出于我个人的理解,这可能会使得你不易理解它们。如果你遇到这种情况,请移步至【启发博文】,二位同志将详细为你解惑。
1、作用域模型与作用域
1:作用域本质上是一套规则,此规则的底层逻辑称为“作用域模型”,从语言层面可分为:词法作用域与动态作用域。
2:词法作用域也称为“静态作用域”,由代码的书写位置与层级结构“生成”作用域,故在代码书写时完成划分,作用域链沿着变量定义的位置向外延伸。
3:使用“动态作用域”的语言相对“冷门”,如:Bash脚本、Peri。其在代码运行时完成划分,作用域链沿着调用栈向外延伸。
4:三种作用域:全局作用域、函数作用域和块级作用域,都是这两种作用域模型的“产物”。
2、三种作用域
2.1 全局作用域
三种情形的变量或函数具有全局作用域:
1:最外层变量或函数。
2:不使用var
等修饰符定义的变量(无论在哪一层),也称为“隐式全局变量”,其会经变量提升至全局作用域。示例:
function f1() {
a = 1
}
f1()
console.log(a);// 1
但要注意,经变量提升后,位置在“外层”往上的“第一行”。
console.log(a);// not defined
// ------变量 a 提升后的位置
function f1() {
a = 1
}
f1()
这也是解释型语言的一个特点,这种现象称为“懒散模式”,使用“严格模式”可规避这一弊端(意外创建全局变量)。(PS:如果你不了解“严格模式”,可查阅博文《[JS]知识点》)中的【严格模式】一栏。
3:window 对象的属性和函数,如:window.innerHeight、 window.alert()、 window.setTimeout()
。且在全局作用域(包括经变量提升后)下由var
声明的变量(包括“隐式全局变量”)和函数会作为 window 对象的属性或方法保存。示例:
function f1() {
a = 1
}
f1()
console.log(window)
控制台:
注意:这个特性的前提是“window
对象”,也就是说,JS脚本是嵌入html文件中运行,才存在window
对象。若JS脚本是直接运行,如:VsCode,则this
是{}
,this.a
返回undefined
。
补充一点:明明this
中没有属性a
,为何this.a
没有报错,而是返回undefined
?见文末下一篇中的【如今笔记】一栏。
全局作用域需要注意两点:
- 在引用多个JS文件时,难免变量重名,由于变量提升,可能会使得变量覆盖,故一般使用函数对变量声明进行封装。
- 如JS脚本在浏览器运行,全局作用域在网页打开时创建、关闭时销毁。
2.2 函数作用域
2.2.1 介绍
先说一下什么是“变量提升:
“变量提升”是指在“解释”时,解释器先扫描整个JS脚本,将所有声明(包括变量和函数)移动到作用域顶端的机制,且函数的变量提升优先于变量。
由var
声明的变量具有函数作用域,在函数调用时创建,调用结束销毁, 且var
允许重复声明和定义。
2.2.2 如何理解“函数的变量提升优先于变量”?
PS:一些资料中可能会这样阐述。
事实上,之前由于我的JS功底不够扎实,也误解了这句话,以为是这样:
console.log(b)
var a = 1
var b = function() {
return 2
}
经变量提升后:
var b
var a
console.log(b);// undefined
a = 1
b = function() {
return 2
}
如果真的如我之前这般理解,无意义,并且也理解错误。
那么,是何意?我从博文《JavaScript执行前的秘书——预编译》(转发)中取经得知,如下:(PS:也推荐你阅读这篇博文,相信它能让你对JS编译机制的理解更加深刻)
console.log(b)
var a = 1
function b() {
return 2
}
经变量提升后:
function b() {
return 2
}
var a
console.log(b);// [Function: b]
a = 1
所以,我之前是把var b = function() {}
的形式误解为"函数声明”,实际上,这也是“变量声明”,只是变量值定义为函数而已。
最后,引用一段“取经”博文中的阐述:
2.3 块级作用域
2.3.1 介绍
由let
或const
声明的变量具有块级作用域,且都不允许重复声明和定义。不同在于,前者用于声明变量,后者声明常量。
let
和const
都具有与var
相同的“变量提升”机制,不同的是,两者声明的变量存在“暂时性死区”,在定义之前访问或赋值会报错,示例:
console.log(str);
let str = 1
输出结果:
2.3.2 如何解释“let 不允许重复声明和定义”?
我们先来看由 var 修饰的情况,示例:
var a = 1
var a = 2
经变量提升后:
var a
a = 1
a = 2
也就是:变量提升会将重复声明进行覆盖。
再来看 let 的情况。如果两个同名的变量都由 let 修饰,报错,这是 let 的特性。大家疑惑的多是这种情况:
var a = 1
let a = 2
先解答:也会报错。为什么?这涉及到一个细节:
var 的变量提升的优先级高于 let。
也就是说,经变量提升后:
var a
let a
a = 1
a = 2
这种情况 let 同样不允许,故报错。
稍作修改:
let a = 1
var a = 2
这种情况与上述完全相同,故也不允许。
3、循环中的var与let
相信你在阅读作用域相关文章时曾看过这样的示例。
示例1。
let arr = new Array
for(var i = 0; i < 10; i++) {
arr[i] = function(x) {
return i + x;
}
}
arr.forEach(e => console.log(e(10)));// 打印:10个20
示例2。
let arr = new Array
for(let i = 0; i < 10; i++) {
arr[i] = function(x) {
return i + x;
}
}
arr.forEach(e => console.log(e(10)));// 打印:10 ~ 19
大家喜欢拿这个示例举例,并在示例后给予解释说明。
就我阅读的一些文章而言,示例说明不尽人意。并非贬低别人,我也同样解释不清。可能这个示例本身就不好解释,亦或者目前我对JS的理解不够。
步入正题,说说我的理解,关键在于作用域链,我们先标明出来。
let arr = new Array
var i;// 全局作用域------A
for(i = 0; i < 10; i++) {// 块作用域------B
arr[i] = function(x) {// 函数作用域------C
return i + x;
}
}
arr.forEach(e => console.log(e(10)));
执行for
循环时,只是将每个元素定义为函数,以i + x
作为返回值,具体返回值取决于调用时。
分析作用域链:当调用e(10)
时,在C中寻找i
,没有,向外查找,B也没有,在A中找到。此时for
循环已执行完成,i
是10
。因此,forEach
循环所有元素都返回20
。
再看示例2,let是块级作用域,变量提升就在for
循环中,基本不变,i
在B找到,故可获取到0 ~ 9
。
PS:可能不是很正确,但在目前,这有助于我的学习理解。另外,可能你会觉得我写的forEach()
有点奇怪,我在博文《[JS]同事:这次就算了,下班回去赶紧补补内置函数,再犯肯定被主管骂》中的【数组相关函数】一栏有具体说明,在此不作赘述。
4、欺骗词法作用域
词法作用域“生成”作用域是根据代码书写位置和层级结构决定的,故是“静态作用域”,与运行无关。(注:作用域会随着代码运行而改变是“动态作用域”的性质)
在JS中,有两种方法可实现“动态作用域”:(注:JS语言使用的“作用域模型”是词法作用域)
1:eval(str)
:会将 str 视为JS脚本插入至调用位置执行(无论str
是否为一段代码)。若这些脚本中包含变量声明或函数定义,则会导致作用域或作用域链被修改。
2:with(obj)
:此函数的规则是将其内的所有变量或函数视为obj
的成员,则引用时可省略前缀(obj.
)。存在的问题是,如:
with(obj){
a = 1
}
若obj
对象中存在属性a
,则是修改a
,否则将变为“隐式全局变量”,破坏“词法作用域”模型。
最后
var 是 ES5 的语法,let 是 ES6 的语法,什么是“ES6”?。
本文完结。