作用域和作用域链

概述

本文将讲解作用域的形成和应用,并且在这基础上简单讲解for循环中的let创建的块级作用域原理。

一,作用域

1.1,作用域的概念

作用域是指在程序中定义变量的区域,该位置决定了变量的生命周期。

function testFn(){var a=1
}
testFn()
console.log(a)//a is not defined

如上代码,之所以a is not defined就是因为a定义在函数作用域中,全局无法获取。

通俗理解,作用域就是变量与函数的可访问范围,它的最大作用就是隔离变量

它有这样的特性:作用域在代码书写时被确定(静态作用域),代码执行时使用(作用域链)。

在ES6出来之前,js中存在两种作用域:【全局作用域】和【函数作用域】。而在es6中又引入了const和let的【块级作用域】。

1.2,全局作用域

全局作用域的特点是:任何地方的代码都能够访问到。主要的创建方式有以下几种:

1.2.1,最外层定义的函数和变量
var a="最外层变量"
function testFn(){var a="函数内变量"console.log(a)//函数内变量
}
testFn()
console.log(a)//最外层变量
1.1.2,未定义直接赋值的变量自动声明为全局作用域
function testFn(){a="未定义直接赋值的变量,自动声明为全局变量"var b="函数内定义的变量,外层无法访问"}
testFn()
console.log(a)//未定义直接赋值的变量,自动声明为全局变量
console.log(b)//b is not defined
1.1.3,window对象的属性拥有全局作用域

window对象上的属性也拥有全局作用域,比如我们常用的window.location,window.href,window.scrollTo等,就算不使用window.而是直接访问也是可以的。

1.2,函数作用域

全局作用域看起来很方便,只要一定义,任何地方都能访问到,但是存在一个很大的坑:污染全局空间。

如果变量命名重复,后面的将会覆盖前面的。要想写完备可靠的代码,就只能不断地想新的变量/函数名。这无疑是种痛苦。

所以js中除了全局作用域外,还有函数作用域。

简单点理解就是js编译解析的时候,发现这是个函数,则在全局作用域中单独划拉出来个空间,用来做函数作用域。

在函数内部定义的变量或者函数,并且定义的变量或者函数只能在函数内部被访问。函数执行结束之后,函数内部定义的变量会被销毁
function testFn(){var a="函数内定义的变量"console.log(a)//这里能访问到,控制台打印:函数内定义的变量
}
testFn()
console.log(a)//a is not defined

就是因为函数作用域内部的变量能够被隔离,不会污染全局作用域,所以JQuery等库都是利用函数自调用的形式,来进行变量隔离:

var b=10;
(function() {var b = 20;
})();
console.log(b)//10

值得注意的是,js中没有块级作用域(使用{}包裹),而只有函数作用域(function中使用{}包裹)。

也就是if等语句并不会创建新的作用域:

if (true) {var name = 'test'; // name 依然在全局作用域中
}
console.log(name); //test
1.3,ES6的块级作用域

在es6中,可以通过let和const创建块级作用域。块级作用域和函数作用域一样,可以变量隔离,不能被外层访问。

1.3.1,花括号({})中,如果使用了let或者const,那么这个{}会生成一个作用域,并且该作用域只针对该变量生效。
if (true) {var a="全局变量"let name = 'test'; // name创建了个块级作用域,这个块级作用域只针对name生效
}
console.log(a); 
console.log(name); //name is not defined

可以看到,这个话括号中使用let生成的块级作用域,只对name起作用,而对于变量a,它依旧是全局变量,可以在外层直接访问。

1.3.2,const/let修饰的变量不会提升

在es6之前的js,我们使用var来创建新的变量,是会将变量自动提升到当前作用域的顶部。(关于变量提升,可以看我另一篇文章)。

而const/let修饰的变量不会提升,这就意味着,如果我们要声明并使用const/let修饰的变量,需要将它们放在作用域的顶部(最不济也要放在调用位置的前面。)

if (true) {let a="第一个变量"console.log(a); //正常执行console.log(b); //Cannot access 'b' before initializationlet b="第二个变量"
}
1.3.3,同个作用域内禁止重复声明

如果一个标识符在该作用域已经被声明,则不能再使用let/const声明了。

if (true) {var a="第一个变量"let a="第二个变量"//Identifier 'a' has already been declared
}

而不同作用域内,则可以重复声明,且归属于不同作用域:

var a=1
if (true) {let a=2console.log("内部",a)//内部 2
}
console.log("外部",a)//内部 1
1.3.4,for循环中的变量提升和块级作用域

在for循环中,新手常常遇到的是如下代码产生的问题:

<!DOCTYPE html>
<html><head><meta charset="utf-8"><title></title><style type="text/css">li{list-style: none;width: 100px;height: 30px;background-color: blue;text-align: center;line-height: 30px;margin-bottom: 10px;}</style></head><body><ul id="list"><li>0</li><li>1</li><li>2</li><li>3</li><li>4</li><li>5</li><li>6</li><li>7</li></ul><script type="text/javascript">var lisarr=document.getElementsByTagName("li");for(var i=0;i<lisarr.length;i++){lisarr[i].onclick=function(i){console.log(i)};}</script></body>
</html>

这时候,无论点击哪个li标签,都会打印8,而不是打印对应的次序。这是因为for中使用的是var来创建变量,由于变量提升,它是一个全局作用域中的变量。那么在所有的li标签绑定点击事件后,全局的i已经变成了8,这时候我们点击任一个li标签执行如下方法:

function(i){console.log(i)
};

取到的自然是全局的i=8。

而当我们使用let的时候:

var lisarr=document.getElementsByTagName("li");
for(let i=0;i<lisarr.length;i++){lisarr[i].onclick=function(i){console.log(i)};
}

每次遍历,生成的i是一个新的变量,有自己对应的作用域。(因为同于作用域,let不能重复声明的),这样每个点击回调函数中的i就是对应的序号了。(这里的具体解释,见下文作用域链中描述。)

二,作用域链

2.1,自由变量和约束变量的区别

既不是形参也不是函数内部定义的局部变量的变量即自由变量。形参或函数内部定义的局部变量即约束变量

let a=1
function testFn(c){let b=2console.log(a)//a:自由变量console.log(b)//b约束变量console.log(c)//c约束变量
}
testFn(3)
2.2,自由变量的取值流程-顺着作用域链向上查找

上文已经讲了作用域的概念,如下图所示,不同的颜色是一层作用域(值得注意的是,函数形参是归属于函数作用域的),作用域是分层的,内层作用域可以访问外层作用域的变量,反之则不行

1.PNG

对于约束变量而言,自己的作用域中已经定义了,所以必然能够直接读取值。

而对于自由变量而言,当前作用域并没有对应变量,于是就需要顺着作用域向外层查找,直到找到为止。若是到全局作用域还是没找到,就宣布放弃。这种一层一层的关系,就是作用域链 。

let a=1
function testFn(c){let b=2function testFn2(){let c=3console.log(a)//顺着作用域链查找,在全局作用域找到console.log(b)//顺着作用域链查找,在testFn函数的作用域找到console.log(c)//在本作用域找到}
}
testFn(3)
2.3,作用域链基于静态作用域

看如下代码:

function testFn1(){let a=1function testFn2(){console.log(a)}return testFn2
}
function testFn3(fn){let a=2fn()
}
testFn3(testFn1())

结果打印的是1,而不是2,这是因为js作用域链是基于静态作用域(或者说词法作用域),函数的作用域在函数定义的时候就决定了

在上面的代码中,静态作用域只看函数定义时代码是怎么写的,和最后怎么调用的没关系。于是testFn2函数作用域中没有a变量,就会向外层查找,在testFn1中找到了,于是就会取这个1。

2.4,静态作用域的创建时机

那为啥说静态作用域只和变量定义时有关系,而和调用没关系呢?

这是因为JavaScript属于解释型语言,JavaScript的执行分为:解释执行两个阶段,这两个阶段所做的事并不一样:

解释阶段做的事情:

词法分析
语法分析
生成可执行代码

执行阶段做的事情:

创建执行上下文
执行函数代码
垃圾回收

作用域的规则是在解释阶段确定的。在识别变量和函数声明时,JavaScript引擎会确定它们的作用域。而在执行阶段,JavaScript引擎将根据作用域规则来查找变量和函数。

也就是这个阶段,作用域被确定。所以才说作用域只是静态作用域,和什么时候调用执行没关系。

2.5,代码中作用域链的实现过程

Javascript中一切皆对象,这些对象有一个[[Scope]]属性,该属性包含了函数被创建时的作用域中对象的集合,这个集合被称为函数的作用域链(Scope Chain),它决定了哪些数据能被函数访问。当函数创建的时候,它的[[scope]]属性自动添加好全局作用域。之所以要强调创建是因为JavaScript采用词法作用域(lexical scoping),也就是静态作用域.

作用链其实就是通过每个执行上下文的outer链接起来形成的。实际上,每个执行上下文在创建的时候,都会生成一个名为[[scoped]]的属性。

它是一个数组,在创建执行上下文的时候,就会顺着outer连接的链查找自由变量(既不是形参也不是函数内部定义的局部变量的变量即自由变量),outer变量每连接上一个执行上下文(或者闭包),它就会往数组添加一条。

var a=1
function foo(){var b=3var d=4function test1(){var c=5console.log(a,b,c)}console.dir(test1)test1()
}
foo()

console.dir(test1)打印出来的结果就是:

ƒ test1()
arguments: null
caller: null
length: 0
name: "test1"
prototype: {constructor: ƒ}[[FunctionLocation]]: index.js:5[[Prototype]]: ƒ ()[[Scopes]]: Scopes[2]0: Closure (foo) {b: 3}1: Global {window: Window, a: 1,...}

可以看到在test1生成的执行上下文,它内部的自由变量是a和b,于是顺着outer连接的执行上下文查找,第一个找到了foo执行上下文中的b,第二个找到了全局执行上下文的a,所以最后生成了Scopes[2]是两个值的数组。

2.6,for循环中的作用域解析

上文1.3.4节中提到的代码,在了解了作用域链之后,就可以继续深入讲解了。

其实,for循环中()是个独立的作用域,我们可以验证这一点:之前说过同一个作用域下,let不允许重复声明(上文1.3.3)。

于是可以写如下代码:

let i =10
for(let i =0;i<5;i++){let i=20console.log(i)
}

结果是打印了五次20而没有报错,说明这里有三层作用域:全局作用域,for的括号中一层作用域,for的花括号中一层作用域。

另外,每次遍历for的()中都会生成一个新的作用域给对应的i。所以说这段代码又有了更深一层的解释:

var lisarr=document.getElementsByTagName("li");
for(let i=0;i<lisarr.length;i++){lisarr[i].onclick=function(i){console.log(i)};
}

如下图所示,每次遍历都会生成一个新的作用域,并创建一个新的变量i,而{}内的代码的作用域中因为没有i,则会取外层这个i值,从而取到对应的序号。

2.png

2.7,延长作用域

除了let、const之外,其实还有try……catch、with、eval来创建块作用域,其他用的相对少,暂且不谈。主要讲讲try……catch对作用域链的延长作用。

catch可以在作用域链的前端临时增加一个变量对象,该变量对象会在代码执行后被移除。

如下所示:

let k=0
try {let i = 1;throw new Error('error occurred');
} catch (e) {var j = 2;console.log(k); // 0console.log(j); // 2console.log(i); // i is not defined
}

从这里看,catch会临时创建一个内层的作用域。也就是上面代码中存在三个作用域:全局作用域、try作用域、catch作用域,其中try作用域和catch作用域同级。

具体来说,变量会被放置到 catch 块的作用域中,其作用域链的顶端指向当前作用域中的变量,然后再向上查找,直到全局作用域。

延长表现为:

1. catch 块内部可以访问和操作 catch 块外层作用域的变量;
2. catch 块内部声明的变量,在块外不能被访问;
3. catch 块内部声明的变量在 catch 块内部与外部是可以同时存在的,并且名字可以相同,不会发生冲突;
4. var 声明变量的提升仍然存在,但是定义未赋初值的声明不会被提升,否则会被赋值为 undefined;

按照这样的理解,这样延长作用域,也只是在cacth中新增了一节内层的作用域给catch中的代码使用。(红宝书里的说法是在作用域的前端生成一个临时的作用域,所以说是延长)

这和新建个函数作用域有啥区别?

与函数中新创建一个函数不同的是,catch块语句中的作用域只在catch块语句内部有效,在catch块语句执行完毕后该作用域就会被销毁。而函数作用域则是在函数被调用时创建,在函数执行完后才被销毁。

本文相关系列文章

相关系列文章

js从编译到执行过程 - 掘金 (juejin.cn)

从异步到promise - 掘金 (juejin.cn)

从promise到await - 掘金 (juejin.cn)

浅谈异步编程中错误的捕获 - 掘金 (juejin.cn)

作用域和作用域链 - 掘金 (juejin.cn)

原型链和原型对象 - 掘金 (juejin.cn)

this的指向原理浅谈 - 掘金 (juejin.cn)

js的函数传参之值传递 - 掘金 (juejin.cn)

js的事件循环机制 - 掘金 (juejin.cn)

从作用域链和内存角度重新理解闭包 - 掘金 (juejin.cn)

js的垃圾回收机制 - 掘金 (juejin.cn)

js的模块化 - 掘金 (juejin.cn)

js设计模式-创建型 - 掘金 (juejin.cn)

js设计模式-结构型 - 掘金 (juejin.cn)

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.rhkb.cn/news/163994.html

如若内容造成侵权/违法违规/事实不符,请联系长河编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

Zookeeper 和 Kafka 工作原理及如何搭建 Zookeeper集群 + Kafka集群

目录 1 Zookeeper 1.1 Zookeeper 定义 1.2 Zookeeper 工作机制 1.3 Zookeeper 特点 1.4 Zookeeper 数据结构 1.5 Zookeeper 应用场景 1.6 Zookeeper 选举机制 2 部署 Zookeeper 集群 2.1 安装前准备 2.2 安装 Zookeeper 3 Kafka 3.1 为什么需要消息队列&#xff08;…

SSH连接华为交换机慢

ssh连接交换机慢是因为交换计算密钥算法阶段默认使用安全性更高的秘钥&#xff0c;由于性能问题导致连接比较慢&#xff0c;如一台华为S5735S-L24T4S-QA2的交换机默认使用如下秘钥&#xff0c;安全行由高到低。 ssh server key-exchange dh_group16_sha512 dh_group15_sha512 …

Redux详解(二)

1. 认识Redux Toolkit Redux Toolkit 是官方推荐的编写 Redux 逻辑的方法。 通过传统的redux编写逻辑方式&#xff0c;会造成文件分离过多&#xff0c;逻辑抽离过于繁琐&#xff08;具体可看上篇文章 Redux详解一&#xff09;&#xff0c;React官方为解决这一问题&#xff0c;推…

HCIP静态路由综合实验

题目&#xff1a; 步骤&#xff1a; 第一步&#xff1a;搭建上图所示拓扑; 第二步&#xff1a;为路由器接口配置IP地址&#xff1b; R1&#xff1a; [R1]display current-configuration intinterface GigabitEthernet0/0/0ip address 192.168.1.1 255.255.255.252 interfa…

【试题026】赋值运算符小例题

1.题目&#xff1a;设int x100;表达式x%2(x1)%2的值是&#xff1f; 2.代码解析&#xff1a; #include <stdio.h> int main() {//设int x100;表达式x%2(x1)%2的值是&#xff1f;int x 100,y,c;printf("y%d\n", (y(x 1) % 2));c x % 2;printf("c1%d\n&…

全球二氧化碳排放数据1deg产品(ODIAC)数据

简介 全球二氧化碳排放数据1deg产品(ODIAC)是一个空间分辨率为1deg*1deg的全球化石燃料燃烧产生的二氧化碳空间分布产品。它率先将基于空间的夜间灯光数据与单个发电厂的排放/位置相结合来估计化石燃料二氧化碳的排放。该产品被国际研究界广泛用于各种研究应用&#xff08;例如…

前端(十九)——vue/react脚手架的搭建方式

&#x1f604;博主&#xff1a;小猫娃来啦 &#x1f604;文章核心&#xff1a;前端&#xff08;十九&#xff09;——vue/react脚手架的搭建方式 文章目录 前言Vue脚手架搭建方法Vue CLI脚手架Vite脚手架其他方式 React脚手架搭建方法Create React App脚手架Vite脚手架其他方式…

音频抓取代码示例

以下是一个使用DefaultsKit库的简单爬虫程序&#xff0c;用于爬取音频。代码中使用了https://www.duoip.cn/get_proxy的API获取代理服务器。 import Foundation import DefaultsKit ​ let url "https://www.douban.com/music" // 目标网站URL let proxyUrl "…

02、Python ------- 简单爬取下载小视频

简单爬取小视频 1、装模块 按键盘 winr 输入cmd , 输入命令&#xff1a; pip install requests 也有说在这个目录下面执行命令 pip install requests 执行失败 执行如果失败&#xff0c;在命令后面添加镜像 pip install requests -i https://mirrors.aliyun.com/pypi/sim…

linux java 环境变量配置

前提已经存在jdk部署包&#xff0c;并且上传到服务器上 编辑&#xff1a;/etc/profile export JAVA_HOME/home/jdk1.8.0_211/ export JRE_HOME/home/jdk1.8.0_211/jre/ export PATH$JAVA_HOME/bin:$JRE_HOME/bin:$PATH编辑位置&#xff1a; 之后source /etc/profile 查看java…

Kingbase备份与还原及表的约束(Kylin)

备份与还原 逻辑备份是对整个数据库好数据库中的部分对象利用逻辑备份工具导出数据到备份文件在需要数据恢复的情况下利用逻辑还原工具把备份文件恢复到数据库中 使用场景 逻辑备份主要用于数据库逻辑错误的恢复&#xff0c;恢复后对其他数据没有太大影响逻辑备份可用于在大…

Nmap端口服务 之 CentOS7 关于启动Apache(httpd)服务、telnet服务、smtp服务、ftp服务、sftp服务

Nmap端口服务 之 CentOS7 关于启动Apache(httpd)服务、telnet服务、smtp服务、ftp服务、sftp服务 一. CentOS7 安装配置SFTP服务器详解一、SFTP简介二、关闭防火墙三、安装SSH服务在CentOS7中,sftp只是ssh的一部分,所以采用yum来安装ssh服务即可1. 查看是否已经安装了ssh2.…

数据挖掘(6)聚类分析

一、什么是聚类分析 1.1概述 无指导的&#xff0c;数据集中类别未知类的特征&#xff1a; 类不是事先给定的&#xff0c;而是根据数据的相似性、距离划分的聚类的数目和结构都没有事先假定。挖掘有价值的客户: 找到客户的黄金客户ATM的安装位置 1.2区别 二、距离和相似系数 …

Linux实用指令-指定运行级别、帮助指令

一、 指定运行级别 1.运行级别说明&#xff1a; 0:关机 1:单用户[找回丢失密码] 2:多用户状态没有网络服务 3:多用户状态有网络服务 4:系统未使用保留给用户 5:图形界面 6:系统重启 常用运行级别是3和5&#xff0c;要修改默认的运行级别。可改文件/etc/inittab 的id:5:initd…

Spring学习笔记(2)

Spring学习笔记&#xff08;2&#xff09; 一、Spring配置非定义Bean1.1 DruidDataSource1.2、Connection1.3、Date1.4、SqlSessionFactory 二、Bean实例化的基本流程2.1 BeanDefinition2.2 单例池和流程总结 三、Spring的bean工厂后处理器3.1 bean工厂后处理器入门3.2、注册Be…

Dreambooth工作原理

什么是Dreambooth 中文名&#xff1a;梦想亭。 Dreambooth 由 Google 研究团队于 2022 年发布&#xff0c;是一种通过向模型注入自定义主题来微调扩散模型&#xff08;如稳定扩散&#xff09;的技术。 所谓自定义主体&#xff0c;就是一张照片&#xff0c;但是照片主体要鲜明…

idea启动vue项目:Error:0308010C:digital envelope routines::unsupported

此问题是因为Node.js的版本原因&#xff0c;此处安装的Node.js是最新长期维护版: 18.16.0 (includes npm 9.5.1) 有两种解决办法&#xff1a; #1、方法一 重新安装低版本的node.js#2、方法二 在package.json文件中进行配置【此种方法较简单】介绍一下第二种方法&#xff1a; …

win32汇编-使用子程序

当程序中相同功能的一段代码用得比较频繁时&#xff0c;可以将它分离出来写成一个子程序&#xff0c;在主程序中用call指令来调用它。这样可以不用重复写相同的代码&#xff0c; 仅仅用call指令就可以完成多次同样的工作了。Win 32汇编中的子程序也采用堆栈来传递参数&#xff…

Python学习-----Day09

一、利用装饰器来获取函数运行的时间、 #导入time模块 import timedef decorated(fn):def inner():#time.time获取函数执行的时间a time.time() # func开始的时间fn()b time.time() # func结束的时间print(f"{fn.__name__}程序运行的总数时间:{b - a}秒")return…

Win系统VMware虚拟机安装配置(二)

系统的安装得分两个步骤&#xff0c;第一步得配置一台电脑&#xff0c;选配 cpu&#xff0c;内存&#xff0c;磁盘&#xff0c;网卡等硬 件。第二步才是安装系统。 一、配置电脑 1、 进入 VMware 双击 VMware 图标&#xff0c;看到如下界面。 2、 自定义新的虚拟机 3、…