[JavaScript]作用域的“生产者”——词法作用域
【版权声明】未经博主同意,谢绝转载!(请尊重原创,博主保留追究权)
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()
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 函数作用域
先说一下什么是“变量提升:
“变量提升”是指在“解释”时,解释器先扫描整个JS脚本,将所有声明(包括变量和函数)移动到作用域顶端的机制,且函数的变量提升优先于变量。
由var
声明的变量具有函数作用域,在函数调用时创建,调用结束销毁, 且var
允许重复声明和定义。
2.3 块级作用域
由let
或const
声明的变量具有块级作用域,且都不允许重复声明和定义。不同在于,前者用于声明变量,后者声明常量。
let
和const
都具有与var
相同的“变量提升”机制,不同的是,两者声明的变量存在“暂时性死区”,在定义之前访问或赋值会报错,示例:
console.log(str);
let str = 1
输出结果:
3、解答
1:如何理解“函数的变量提升优先于变量?”(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执行前的秘书——预编译》(转发)中取经得知,如下:
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:如何解释“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
这种情况与上述完全相同,故也不允许。
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”?。
也推荐你阅读这篇博文《JavaScript执行前的秘书——预编译》(转发),相信它能让你对JS编译机制的理解更加深刻。
本文完结。