每日一学: JS中的作用域
在编程语言中,特别是像JavaScript这样的语言中,作用域是一个非常重要的概念,它决定了变量、函数以及其它标识符在何处可以被访问
首先
在了解作用域之前,让我们先来了解一些JS中的基础概念
类型
在JS中有以下几种基础类型
:
- Number: 用于存储整数或浮点数,例如
42
或3.14
。 - String: 用于存储文本,需要用单引号
' '
或双引号" "
包围,例如'Hello, world!'
。 - Boolean: 表示真或假的值,只有两个可能的值:
true
和false
。 - Undefined: 表示变量已被声明但没有被赋值,默认的初始值。
- Null: 表示一个刻意的空值或缺少对象值,它是一个特殊的原始值。 代码表示为:
var str = 'hello world'; // string 字符串类型
var num = 123; // Number 数字
var flag = true; // Boolean 布尔类型
var u = undefined; // 强调未定义
var n = null; // 强调为空值
以下引用类型
:
- Object: 用于存储键值对集合,可以是复杂的数据结构。所有非原始类型都归为此类,包括数组(
Array
)、函数(Function
)、日期(Date
)、正则表达式(RegExp
)等。 - Array: 特殊类型的对象,用于存储有序的数据集合。
- Function: 在JavaScript中,函数也是对象,可以被赋值给变量、作为参数传递给其他函数或作为其他函数的返回值。 代码表示为:
var obj = {}; // Object 对象
var arr = []; // Arrar 数组
var fn = function(){}// function 函数
判断 条件语句
-
if 语句: 基础的条件判断结构,如果条件为真,则执行相应的代码块。
if (条件) { // 条件为真时执行的代码 }
-
if...else 语句: 在条件为真时执行一块代码,在条件为假时执行另一块代码。
if (条件) { // 条件为真时执行的代码 } else { // 条件为假时执行的代码 }
-
if...else if...else 语句: 用于测试多个条件,依次检查每个条件,直到找到第一个为真的条件并执行对应的代码块。
if (条件1) { // 条件1为真时执行的代码 } else if (条件2) { // 条件1为假且条件2为真时执行的代码 } else { // 所有条件都为假时执行的代码 }
-
switch 语句: 当有多重分支选择时使用,基于不同的情况执行不同的代码块。每个
case
后面通常跟着一个break
语句,以防止代码穿透到下一个case
。switch (表达式) { case 值1: // 表达式等于值1时执行的代码 break; case 值2: // 表达式等于值2时执行的代码 break; default: // 没有匹配的case时执行的代码 10}
判断语句
-
for循环: 通常用于已知循环次数的情况。其基本语法结构如下:
for (初始化表达式; 条件表达式; 更新表达式) { // 循环体(需要重复执行的代码) }
- 初始化表达式:在循环开始前执行一次,通常用于设置循环变量。
- 条件表达式:在每次循环迭代前检查,如果为真,则执行循环体。
- 更新表达式:在每次循环迭代后执行,通常用于更新循环变量。
-
while循环: 当给定条件为真时,重复执行代码块。语法如下:
while (条件表达式) { // 循环体(需要重复执行的代码) }
-
条件表达式:在每次循环开始前评估,只要条件为真,循环就会继续。
-
好的,基础知识了解完毕,下面进入正题。
作用域
在JS中,在代码执行时,先要进行编译
就像执行var a = 1
这一句代码,在v8引擎(谷歌内置JS执行引擎,node就是类似的引擎)眼里,他会先编译这段代码将这段代码分解为这样一段过程var ,a ,= ,1
,这叫解析,然后又重新生成代码var a = 1
,然后把这段过程的结果交给执行部分,执行代码。我们写出的代码,首先就会经过这样的编译,如果我们的代码就是简单的输入输出,那么就是流畅的编译->执行
,但一旦有了特殊的需求,这样简单的过程明显无法满足需求,这个时候作用域的作用就出来了,我们来看这一段代码:
var a = 1
function foo(){
var a = 2
}
foo()
console.log(a);
这段代码在控制台中的输出是什么呢?输出a
的值我们当然知道,问题是a
的值是多少呢?是2
吗?我刚开始也是这么认为的,这里不是有一个函数把a
的值又赋为了2吗?但最后却输出了1
,,很奇怪对吧,这就是作用域的作用了,这里我们先了解一个概念,那就是作用域主要分为了
- 全局作用域:在程序的最外层定义的变量拥有全局作用域,这意味着从定义处开始,直到程序结束,都可以访问到这些变量。在浏览器环境中,全局变量属于
window
对象(在Node.js中则是global
对象)的一部分。尽量避免过多使用全局变量,因为它们容易造成命名冲突和数据污染。 - 函数作用域:也称为局部作用域,当在一个函数内部定义变量时,这个变量就只在这个函数内部可见。每次函数调用都会创建一个新的作用域,因此相同名称的变量在不同的函数中互不影响。
在代码执行时会先考虑全局,再考虑函数
然后我们来模拟一下这段代码到底是怎么执行的。
首先,在董事长
面前放着这一份代码,“董事长”
表示我看不懂这一份代码,“秘书”
你来帮我安排一下怎么去执行这段代码。然后“秘书”
就来分析这段代码了,欸,然后“秘书”
就在他的小本本上开始记了
首先
好了,这里“秘书”
的工作就完成了,在第一步全局作用域编译时,“秘书”
首先识别到了a
这个数据,但“秘书”
又不能执行,所以在“秘书”
这里就定义为了undefined
,未被定义但应该有个值,然后就是foo
,这是个function
函数对象,然后完了,那么就要问了,函数里面呢?后面的函数调用呢?控制台输出呢?
所以这就是全局作用域和函数作用域的区别了,函数在这时的“秘书”
眼里是空的,是需要定义的,有东西但我识别不了的意思。
而函数调用以及控制台输出是要执行部分干的事了。
好了,“秘书”
把这份笔记交给了“董事长”
,说你按照这里执行就可以了,“董事长”
说好的,这就来执行,于是就有了
“董事长”
将a赋值为1后,到foo()时愣住了,说,欸,秘书,这函数你还没告诉我怎么搞呢,你去帮我搞一下。
好,“秘书”
就去搞函数了,就有了
函数里就一个给a赋值的操作对吧,于是秘书又交给了“董事长”
,然后就又有了
好了,调用函数结束,到了最后的输出a的阶段了,那么,我们现在有两个a,输出那个呢?输出全局的那个1 这里我们就有了,作用域的一个特点:
内部作用域可以访问外部作用域,反之则不行
为什么呢?因为这是栈
的关系先进后出,后进先出,所以在这时,a的输出无法调用函数作用域内的2,于是输出了处在全局作用域的a=1
除去这两个作用域,还有
- 块级作用域:在一些现代编程语言中(如JavaScript ES6引入了
let
和const
关键字),支持块级作用域。这意味着在if语句、for循环或者任何一对大括号{}
内定义的变量,其作用域仅限于那个块内部。这有助于减少变量泄露到外部作用域的可能性,增强代码的模块性和可维护性。
就是说{} + let||const
就是块级作用域,而且只作用于这两个关键字,不会影响其他的关键字
- 词法作用域(静态作用域):JavaScript采用词法作用域,这意味着变量的作用域在代码编写时就已经确定,而非在运行时决定。这意味着函数内部可以访问包含它的函数(父作用域)中的变量,即使父函数已经执行完毕。
变量声明的地方-- 所处的作用域就是词法作用域,就像是我处在这个房间内,那么对于这句话中的我来说,这个房间就是我的词法作用域。
- 欺骗词法作用域
(1)eval() 让原本不属于这里的代码,变得好像天生就定义在这里一样
function foo(a,str){
eval(str);
console.log(a,b);
}
foo(1,'var b = 2')
就像是这段代码的str
,它就被eval()执行为了foo中的属性b=2
(2)with(){} 当修改对象中不存在的属性时,这个属性会被泄露到全局,变成全局变量
结语
作用域理解起来稍难,但请记住,实践是学习编程的最佳途径,不断尝试、勇于探索,我们将能更熟练地运用这些知识解决实际问题。在编程的旅途中,遇到挑战是常有的事,请坚持下去,每解决一个问题,我们就会变得更加强大。最后,希望我的理解能帮助到你,我是Ace,我们下次分享再见!!!
转载自:https://juejin.cn/post/7372441501609656346