[JS]作用域的“生产者”——词法作用域

简介: 本文介绍了JavaScript中的作用域模型与作用域,包括词法作用域和动态作用域的区别,以及全局作用域、函数作用域和块级作用域的特点。通过具体示例详细解析了变量提升、块级作用域中的暂时性死区等问题,并探讨了如何在循环中使用`var`和`let`的不同效果。最后,介绍了两种可以“欺骗”词法作用域的方法:`eval(str)`和`with(obj)`。文章结合了多位博主的总结,帮助读者更快速、便捷地掌握这些知识点。

【版权声明】未经博主同意,谢绝转载!(请尊重原创,博主保留追究权)
https://developer.aliyun.com/article/1635708
出自【进步*于辰的博客

参考笔记二,P43.3、P46.1、P9.3;笔记三,P70、P71。

先言

启发博文:(转发)

  1. 【JavaScript】深入理解JS中的词法作用域与作用域链
  2. JavaScript执行机制:变量提升、作用域链、词法作用域、块级作用域、闭包和this

这两篇博文是我系统学习时参考的文章,二位博主总结得很全面,我受益颇多!有所总结,将这些总结罗列至本篇文章,相信这能够让你更为快速、便捷地掌握这些知识点。

当然,我所作的总结是“精简”的内容,并且一些是出于我个人的理解,这可能会使得你不易理解它们。如果你遇到这种情况,请移步至【启发博文】,二位同志将详细为你解惑。

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?见文末下一篇中的【如今笔记】一栏。

全局作用域需要注意两点:

  1. 在引用多个JS文件时,难免变量重名,由于变量提升,可能会使得变量覆盖,故一般使用函数对变量声明进行封装。
  2. 如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 介绍

letconst声明的变量具有块级作用域,且都不允许重复声明和定义。不同在于,前者用于声明变量,后者声明常量。

letconst都具有与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循环已执行完成,i10。因此,forEach循环所有元素都返回20

再看示例2,let是块级作用域,变量提升就在for循环中,基本不变,i在B找到,故可获取到0 ~ 9

PS:可能不是很正确,但在目前,这有助于我的学习理解。另外,可能你会觉得我写的forEach()有点奇怪,我在博文《[JS]同事:这次就算了,下班回去赶紧补补内置函数,再犯肯定被主管骂》中的【数组相关函数】一栏有具体说明,在此不作赘述。

4、欺骗词法作用域

词法作用域“生成”作用域是根据代码书写位置和层级结构决定的,故是“静态作用域”,与运行无关。(注:作用域会随着代码运行而改变是“动态作用域”的性质)

在JS中,有两种方法可实现“动态作用域”:(注:JS语言使用的“作用域模型”是词法作用域)

1eval(str):会将 str 视为JS脚本插入至调用位置执行(无论str是否为一段代码)。若这些脚本中包含变量声明或函数定义,则会导致作用域或作用域链被修改。

2with(obj):此函数的规则是将其内的所有变量或函数视为obj的成员,则引用时可省略前缀(obj.)。存在的问题是,如:

with(obj){
   
    a = 1
}

obj对象中存在属性a,则是修改a,否则将变为“隐式全局变量”,破坏“词法作用域”模型。

最后

var 是 ES5 的语法,let 是 ES6 的语法,什么是“ES6”?

本文完结。

下一篇:[JS]同事看了我做的this笔记,直摇头,坦言:我还是参考启发博文吧

相关文章
|
1月前
|
JavaScript 前端开发
js的作用域作用域链
【10月更文挑战第29天】理解JavaScript的作用域和作用域链对于正确理解变量的访问和生命周期、避免变量命名冲突以及编写高质量的JavaScript代码都具有重要意义。在实际开发中,需要合理地利用作用域和作用域链来组织代码结构,提高代码的可读性和可维护性。
|
1月前
|
前端开发 JavaScript 数据处理
CSS 变量的作用域和 JavaScript 变量的作用域有什么不同?
【10月更文挑战第28天】CSS变量和JavaScript变量虽然都有各自的作用域概念,但由于它们所属的语言和应用场景不同,其作用域的定义、范围、覆盖规则以及与其他语言特性的交互方式等方面都存在明显的差异。理解这些差异有助于更好地在Web开发中分别运用它们来实现预期的页面效果和功能逻辑。
|
1月前
|
JavaScript 前端开发
如何在 JavaScript 中实现块级作用域?
【10月更文挑战第29天】通过使用 `let`、`const` 关键字、立即执行函数表达式以及模块模式等方法,可以在JavaScript中有效地实现块级作用域,更好地控制变量的生命周期和访问权限,提高代码的可维护性和可读性。
|
1月前
|
JavaScript 前端开发
javascript的作用域
【10月更文挑战第19天javascript的作用域
|
2月前
|
JavaScript 前端开发
JavaScript 作用域
JavaScript 作用域是指程序中可访问的变量、对象和函数的集合。它分为函数作用域和局部作用域。函数作用域内的变量仅在函数内部可见,而全局作用域的变量在整个网页中均可访问。局部变量在函数执行完毕后会被销毁,而全局变量则在整个脚本生命周期中都存在。未使用 `var` 关键字声明的变量默认为全局变量。
|
2月前
|
JavaScript 前端开发
js作用域
js作用域
17 1
|
3月前
|
JavaScript 前端开发
js 变量作用域与解构赋值| 22
js 变量作用域与解构赋值| 22
|
3月前
|
缓存 JavaScript 前端开发
了解js基础知识中的作用域和闭包以及闭包的一些应用场景,浅析函数柯里化
该文章详细讲解了JavaScript中的作用域、闭包概念及其应用场景,并简要分析了函数柯里化的使用。
了解js基础知识中的作用域和闭包以及闭包的一些应用场景,浅析函数柯里化
|
3月前
|
JavaScript 前端开发
JavaScript 作用域
JavaScript 作用域
29 9
|
7月前
|
JavaScript 前端开发
js变量的作用域、作用域链、数据类型和转换应用案例
【4月更文挑战第27天】JavaScript 中变量有全局和局部作用域,全局变量在所有地方可访问,局部变量只限其定义的代码块。作用域链允许变量在当前块未定义时向上搜索父级作用域。语言支持多种数据类型,如字符串、数字、布尔值,可通过 `typeof` 检查类型。转换数据类型用 `parseInt` 或 `parseFloat`,将字符串转为数值。
42 1