javascript 的上下文与作用域

news2024/9/24 0:04:48

目录

    • 1. 初步了解 上下文(context)
    • 2. 全局上下文(global context)
    • 3. 上下文栈 (context stack)
    • 4. 作用域链( scope chain)
    • 5. 作用域(scope)
    • 6. 作用域链增强
    • 7. 变量声明
      • 7.1 var 声明变量
      • 7.2 let 声明变量
      • 7.3 const 常量声明
    • 8. 标识符查找
    • 总结

带着疑问去学习,本章的疑问如下:

问: javascript 是如何设计 各个变量和函数的作用域的,用的什么数据结构?
答:栈结构的链表实现(先把答案说出来,理解的会快一些)

1. 初步了解 上下文(context)

   上下文(context) 全称 执行上下文 (execution context) 。

  变量和函数的上下文 决定了它们可以访问哪些数据,以及它们的行为。所以 上下文 也可以说是 变量和函数所处的环境

  每个上下文都有一个关联的变量对象,而这个上下文中定义的所有变量和函数都存在于这个对象上。虽然无法通过代码访问变量对象,但后台处理数据总是用到它。


2. 全局上下文(global context)

  全局上下文是最外层的上下文。浏览器中,全局上下文就是 window 对象,所有通过 var 定义的全局变量和函数都会成为window对象的属性和方法
  使用 let 和const 的顶级声明不会定义在全局上下文中,但在作用链解析上效果是一样的。上下文在其所有代码都执行完毕后会被销毁,包含定义在它上面的所有变量和函数,全局上下文在应用程序退出前才会被销毁,比如关闭网页或退出浏览器。


3. 上下文栈 (context stack)

  每个函数调用都有自己的上下文,当代码执行到某个函数时,函数的上下文被推到(push)一个 上下文的栈结构 中,忘了栈结构的,可以查看 栈的基本操作。在函数执行完之后,上下文栈会弹出(pop)该函数的上下文,将控制权返还给之前的执行上下文。ECMAScript程序的执行流就是通过这个上下文栈进行控制的。


4. 作用域链( scope chain)

   学习作用域,先了解一下做应用于链。这个链,其实就是上面说的上下文栈结构。
  上下文中的代码在执行的时候,会创建变量对象的一个作用域链。这个作用域链决定了各级上下文中的代码在访问变量和函数时的顺序。代码正在执行的上下文的变量对象始终处于作用域链的最前端。如果上下文是函数,则其活动对象(activation object) 用作 变量对象。活动对象最初只有一个变量:参数(arguments)。题外话:上面提到的全局上下文是没有这个变量的。作用域链中的下一个变量对象来自包含上下文,再下一个对象来自下一个包含上下文。依次类推直至全局上下文。全局上下文的变量对象始终是作用域链的最后一个变量对象。

  代码执行的标识符解析是通过沿作用域链逐级搜索标识符名称完成的。搜索过程始终从作用域链的最前端开始,然后逐级往后,直到找到标识符。(未找到标识符,通常会报错)


5. 作用域(scope)

作用域,就是 变量可用性的范围。通俗的讲,就是哪些函数可以访问到这个变量。
先看下面的例子:


var color = "blue";
function changeColor() {
	if (color === "blue") {
		color = "red";
	} 
	else{
		color = "blue";
	}
}
changeColor();

上述函数 changeColor() 的作用域包含两个对象:一个是它自己的变量对象(即 arguments 对象的那个),另一个是全局上下文的变量对象。这个函数内部之所以能够访问变量color,就是因为可以在作用域链中找到它。
局部作用域中定义的变量可用于在局部上下文中替换全局变量。看看下面这个例子:


var color = "blue";
function changeColor() {
	let anotherColor = "red";
	
	function swapColors(){
		let tempColor = anotherColor;
		anotherColor = color;
		color = tempColor;
		//此处为swapColors内部,可以访问color、anotherColor 和tempColor
	}
	//此处为changeColor内部,可以访问 color 和anotherColor ,但访问不到tempColor
}
// 这里只能访问color 
changeColor();

   以上代码涉及到3个上下文:全局上下文、changeColor()的局部上下文和 swapColors()的局部上下文。他们的关系是全局上下文包含 changeColor()上下文,changeColor()包含 swapColors()。
他们的关系如下图:
在这里插入图片描述

   对于swapColos来说,发现变量color, 先在自己的局部上下文 swapColors() 中搜索,发现自己并没有color的定义。往父级上下文changeColor () 中搜索,还是没有搜索到,则到全局作用域 window中搜索,本次搜索到了。


6. 作用域链增强

上下文包含全局上下文和函数上下文两种方式,按照栈的结构形成作用域链,按照顺序向上级搜索,但是有两种情况增强作用域链:

 a.  try/catch 语句的catch块
 b.  with 语句

这两种情况 直接在作用域链前端临时添加一个上下文,在代码执行后被删除。
对于with语句来说,会想作用域链前端添加一个指定的对象;
对于catch语句而言,则会创建一个新的变量对象,这个变量对象会包含要抛出的错误对象的声明。

function buildUrl(){
	let qs = "?debug=true";
	let locHost;
	with (location) {
		let url = href + qs;
		locHost = host;
	}
	return url;  // 这里的url为 undefined
}

   上面代码中,with 语句将 location 对象作为上下文,因此 location会被添加到作用域链前端。buildUrl函数中定义了一个变量 qs。 当 with 语句中的代码应用变量 href 时, 实际上引用的是 location.href,也就是自己变量对象的属性。。
在这里插入图片描述

   在引用 qs 时,引用的则是定义在 buildUrl() 中的那个变量,它定义在函数上下文的变量对象上。而在with 语句中如果使用 var声明的变量 url 会成为函数上下文的一部分,可以作为函数的值返回;但这里使用 let 声明的变量url ,因为被限制在块级作用域,所以在with 块之外没有定义。


7. 变量声明

7.1 var 声明变量

   在使用 var 声明变量时,变量会被自动添加到最接近的上下文。在函数中,最近的上下文就是函数的局部上下文;在with语句中,最接近的上下文也是函数上下文。如果变量未经声明就被初始化了,那么它就会自动被添加到全局上下文。

	function add(num1,num2){
		var sum = num1 + num2 ;
		return sum;
	}
	let  result = add(10,20); // 30 
	console.log(sum); //这里会报错,因为变量 sum 是在 函数 add() 的上下文中。

   上面例子中,函数 add() 定义的局部变量 sum ,在函数外部是访问不了的。因为sum是在函数 add() 的上下文中。再看下面的例子:

	function add(num1,num2){
	  	sum = num1 + num2 ;
		return sum;
	}
	let  result = add(10,20);  // 30 
	console.log(sum); 				// 30,这里不会报错

   这一次 函数外部,就能直接访问sum。因为sum没有声明,直接初始化,就被当做全局变量了。

注意 未经声明而初始化变量是 JavaScript 编程中一个非常常见的错误,会导致很多问题。
为此,读者在初始化变量之前一定要先声明变量。在严格模式下,未经声明就初始化变量
会报错 。(摘自 《javascript 高级程序设计》)

   var 声明会被拿到函数或全局作用域的顶部,位于作用域中所有代码之前。这个现象叫作“提升”(hoisting)。提升让同一作用域中的代码不必考虑变量是否已经声明就可以直接使用。可是在实践中,提升也会导致合法却奇怪的现象,即在变量声明之前使用变量。下面的例子展示了在全局作用域中两段等价的代码:

var name = "bourne";
//等价于
name = "bourne";
var name;

两个等价函数:

	function func1(){
		var name ="bourne";
	}
	//等价于
	function func2(){
		var name;
		name = "bourne" ;
	}

   通过在声明之前打印变量,可以验证变量会被提升。由于声明的提升会输出 未赋值(undefined) ,而不是 引用错误(Reference Error)

 console.log(name);		//undefined(未赋值), 而不是 Reference Error (引用错误)
 var name = "bourne";
	
 function (){
 	console.log(name); //undefined(未赋值), 而不是 Reference Error (引用错误)
 	var name = "bourne";
 }

7.2 let 声明变量

ES6新增 的 let 关键字。 let 块级作用域, var 为函数级作用域。块级作用域由最近的一对包含花括号 { } 界定 。这也就意味着 ,if 块,while 块,function 块,甚至 单独的只有花括号的内部代码块 也是 let 声明 变量的作用域。


if (true) {
	let a ;
}
console.log(a); // Reference Error : a 无定义

while (true){
	let b;
}
console.log(b); // Reference Error : b 无定义

function func() {
	let c;
}
console.log(c); // Reference Error : c 无定义

//下面只有花括号的情况,(js解释器默认其合法的)
{
	let d;
}
console.log(d); // Reference Error : d 无定义

   let 与 var 还有不同之处是 同一作用域内不能声明两次。重复的 var 声明 会被忽略(个人认为这是 js 设计之初考虑不周的地方),而重复的 let 声明 会抛出 语法错误(Syntax Error)。

var a ;
var a ; //这里不会报错
let b ;
let b ; // Syntax Error ,因为 标识符 b 已经声明过了

   let 的特性使它非常适合在循环中声明迭代变量。使用 var 声明的迭代变量会泄漏到循环外部,这种情况应该避免。来看下面两个例子:

for (var i = 0 ; i < 5 ; ++i){}
console.log(i); //5 此处仍然可以识别到 i

for (let j = 0; j < 10; ++j){}
console.log(j);		//Reference Error, 无法识别 j了

严格来讲, let 在 JavaScript 运行时中也会被提升,但由于“暂时性死区”(temporal dead zone)的缘故,实际上不能在声明之前使用 let 变量。因此,从写 JavaScript 代码的角度说, let 的提升跟 var是不一样的。(这一段摘自《javascript 高级程序设计》中原文,因为没看懂,直接摘抄过来了)

7.3 const 常量声明

ES6 新增关键字 const。使用const 声明变量必须同时初始化(因为常量放在内存的数据区中的常量区(只读的,所以不能后面修改))。声明以后,在其生命周期的任何时候都不能再重新赋予新值。

const a ;  //Syntax Error,语法错误,因为常量声明时,没有初始化
const b = 3;
console.log(b); //3
b = 4 ; //TypeError :给常量赋值

const 也是块级作用域,和 let 的作用域一样:

if (true) {
	const a ;
}
console.log(a); // Reference Error : a 无定义

while (true){
	const b;
}
console.log(b); // Reference Error : b 无定义

function func() {
	const c;
}
console.log(c); // Reference Error : c 无定义

//下面只有花括号的情况,(js解释器默认其合法的)
{
	const d;
}
console.log(d); // Reference Error : d 无定义

const 声明 引用类型变量,是否可以修改其属性?看一下下面的例子:


//第一种情况
const people = {};
people = {};	// TypeError: 类型错误,因为给常量赋值了

//第二种情况
const people2 = {name:"bourne"};
people2.name = "pitt"; 		
console.log(people2.name); 	//输出 pitt ,属性被修改了

   上面第二种情况,之所以能够修改对象的属性,是因为 变量 people 内保存的其实是 {name:“bourne”} 这个对象的地址。地址是常量,故不可以修改,但是对象存放在内存堆区,它的属性是可以修改的。

   那么想要连对象的属性也不能修改,有没有办法呢? 答案:有。就是 Object.freeze()。
看看下面代码:

 const people3 =Object.freeze({name:"bourne"});
 people3.name ="Pitt";
 console.log(people2.name);  //输出 bourne, 属性没有被修改。

   由于 const 声明暗示变量的值是单一类型且不可修改,JavaScript 运行时编译器可以将其所有实例都替换成实际的值,而不会通过查询表进行变量查找。(虽然 js是解释型语言,但是 谷歌的V8引擎 采用先编译后运行)

注意 :开发实践表明,如果开发流程并不会因此而受很大影响,就应该尽可能地多使用 const 声明,除非确实需要一个将来会重新赋值的变量。这样可以从根本上保证提前发现重新赋值导致的 bug。摘自《javascript 高级程序设计》


8. 标识符查找

   javascript 解释器在代码运行时,当在特定上下文中遇到一个标识符(如读取或写入一个变量标识符时),会通过搜索来确定变量标识符表示什么。搜索开始于作用域链前端,如果在局部上下文中找到该标识符,则搜索停止,变量确定;如果没有找到变量名,则继续沿作用域链搜索。这个过程一直持续到搜索至全局上下文的变量对象。

var color = "blue";
function getColor() {
	return color;
}

  上述例子中,调用getColor() 遇到color 变量,第一步 搜索 getColor() 的变量对象,未找到,第二步,继续搜索父级上下文(这里是全局上下文),此处找到了,搜索结束,就不会报错了。
   在搜索过程中,在局部上下文中一旦找到标识符,就不会再往父级搜索,哪怕在父级上下文中有一个相同名称的标识符。看看下面的示例:

	var color = "blue";
	function getColor(){
		let color ="red"
		return color;	//此处遇到 color ,在getColor()的变量对象中搜索到了,就不再往父级上下文去搜索了。
	}
	console.log(getColor()); // red

使用块级作用域声明并不会改变搜索流程,但可以给词法层级添加额外的层次:

var color = 'blue';
function getColor() {
	let color = 'red';
	{
		let color = 'green'; 
		return color;	//这里搜索到了花括号中的 color声明 ,就停止搜索。
	}
}
console.log(getColor()); // 'green'

总结

  本章深入了解了作用域,最关键点就是理解 作用域链 。作用域链,首先它是一个链表,其次它是栈结构(后进先出)的。这个链表的元素就是各个上下文关联的变量对象。栈顶(作用域链的前端,也可以理解为第一个数据结点)就是当前正在执行的代码的上下文,栈底(作用域链的尾端,也可以理解为最后一个数据结点)就是 全局上下文(window 对象)。

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

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

D3.高精度

1.分类情况 AB、A-B、A*a、A/b A和B指的是超大超长整数&#xff0c;长度<1e6; a的值<10000&#xff1b; 2.大整数的存储 int 变量肯定是存不了这么大的数的&#xff0c;做法是将大整数先存到string字符串&#xff0c;再使用字符串的访问方式&#xff0c;将每一位数存到…

C++树形结构(3 树的中心、重心)

目录 一.树的中心&#xff1a; 1.树的概念&#xff1a; 2.树的性质&#xff1a; 性质1&#xff1a; 性质2&#xff1a; 3.树的中心求解&#xff1a; 4.例题&#xff1a; 二.树的重心&#xff1a; 1.基础概念&#xff1a; 2.求解方法&#xff1a; 3.例题&#xff1a;…

运筹学笔记

计算的时间问题&#xff01;计算机解决了计算量的问题&#xff01; 计算机的发展对运筹学研究起到了极大的促进作用。 运筹学的一个特征之一是它常常会考虑寻求问题模型的最佳解决方案&#xff08;称为最优解&#xff09;。 没有人能成为运筹学所有方面的专家。 分析学越来越流…

反弹shell的方式——之NC反弹shell

反弹shell是指在攻击机监听某个端口&#xff0c;然后通过目标连接攻击机监听的端口&#xff0c;在攻击机反弹得到目标机的shell。通常在目标网络有防火墙或者其他因素限制&#xff0c;导致无法持续控制目标&#xff0c;或者执行命令受阻等情况时需要进行反弹shell 常见的反弹s…

Null和 Undefined 两者区别?

1、 Undefined 和 null 的 区 别 首 先 Undefined 和 Null 都 是 基 本 数 据 类 型 &#xff0c; 这 两 个 基 本 数 据 类 型 分 别 都 只 有 一 个 值 &#xff0c; 就 是 undefined 和 null。 2、undefined 代 表 的 含 义 是 未 定 义 &#xff0c; null 代 表 的 含 义 …

Python Flask入门到精通:详细教程和实战案例

前言 Flask是一个轻量级的Web框架&#xff0c;用于快速开发Web应用程序。它的设计理念是简洁、灵活和易于扩展&#xff0c;非常适合于从简单的单页应用到复杂的大型项目。通过Flask&#xff0c;可以创建各种Web应用程序&#xff0c;比如博客、电子商务网站、RESTful API等。 …

META 备受期待的 Llama 3 405B 即将发布

本心、输入输出、结果 文章目录 META 备受期待的 Llama 3 405B 即将发布前言Llama 3 405B或许会彻底改变专用模型的数据质量Llama 3 405B将形成新的模型生态系统:从基础模型到专家组合Llama 3 405B有最高效 API 的竞争Llama 3 405B 基准测试META 备受期待的 Llama 3 405B 即将…

韦东山嵌入式linux系列-具体单板的按键驱动程序(查询方式)

1 GPIO 操作回顾 &#xff08;1&#xff09;使能模块&#xff1b; &#xff08;2&#xff09;设置引脚的模式&#xff08;工作于GPIO模式&#xff09;&#xff1b; &#xff08;3&#xff09;设置GPIO本身&#xff08;输入/输出&#xff09;&#xff1b; &#xff08;4&…

Linux_make/Makefile的理解

1.make是一个命令&#xff0c;makefile是一个文件, 依赖关系和依赖方法. a.快速使用一下 i.创建一个Makefile文件(首字母也可以小写) b.依赖关系和依赖方法 i.依赖关系: 我为什么要帮你? mybin:mytest.c ii.依赖方法: 怎么帮? gcc -o mybin mytest.c make之前要注意先创建…

UE4-构建光照后导入的静态网格体变黑

当我们将我们的静态网格体导入到项目当中的时候&#xff0c;此时我们进行重新构建光照&#xff0c;我们在从新构建完光照后&#xff0c;会发现我们的静态网格体全部变黑了&#xff0c;此时是因为没有设置光照贴图分辨率和坐标索引引起的。 将General Settings中的L…

Unite 上海 强势回归

​​​ 他回归了 Unite 大会是一年一度的 Unity 全球开发者盛会。今年&#xff0c;Unite 将于 7 月盛夏点亮上海外滩。此次盛会&#xff0c;我们将以“团结”为核心&#xff0c;凝聚全球 3000 多位 Unity 社区精英的力量&#xff0c;共同开启 Unity 技术的新纪元。 在这里&am…

【C++】透析类和对象(上)

有不懂的&#xff0c;可翻阅我之前文章哦&#xff01; 个人主页&#xff1a;CSDN_小八哥向前冲 所属专栏&#xff1a;C入门 目录 类的定义 访问限定符 类域 类的实例化 实例化概念 对象大小 this指针 类的默认成员函数 构造函数 析构函数 模拟栈&#xff08;初学者&…

(最最最全)远程服务器连接新手教程-服务器基本指令、连接服务器、安装Anaconda、配置Conda、配置环境、bashrc环境变量修改(为空怎么办)

一、服务器基本指令 ls - 列出当前目录的文件和子目录cd - 改变当前目录pwd - 显示当前目录的路径df - 查看当前内存mkdir - 创建新目录rm - 删除文件cp - 复制文件mv - 移动或重命名文件 https://blog.csdn.net/weixin_43693391/article/details/133984143?ops_request_mis…

Ubuntu20.04版本升级openssh9.8p1方法

一、问题描述&#xff1a; 8.5p1 和 9.7p1 之间的openssh版本漏洞可能会导致linux系统以root身份进行RCE&#xff0c;所以需安装最新版本 二、解决方法&#xff1a; 将当前openssh版本升级到最新的版本即openssh-9.8p1版本&#xff0c;OpenSSL大版本升级且OpenSSH有新稳定版本…

今天我们聊聊C#的并发和并行

并发和并行是现代编程中的两个重要概念&#xff0c;它们可以帮助开发人员创建高效、响应迅速、高性能的应用程序。在C#中&#xff0c;这些概念尤为重要&#xff0c;因为该语言提供了对多线程和异步编程的强大支持。本文将介绍C#中并发和并行编程的关键概念、优点&#xff0c;并…

CSS(二)——CSS 背景

CSS 背景 CSS 背景属性用于定义HTML元素的背景。 CSS 背景属性 Property描述background简写属性&#xff0c;作用是将背景属性设置在一个声明中。background-attachment背景图像是否固定或者随着页面的其余部分滚动。background-color设置元素的背景颜色。background-image把…

秋招突击——7/24——知识补充——JVM类加载机制

文章目录 引言类加载机制知识点复习类的生命周期1、加载2、连接——验证3、连接——准备4、连接——解析5、初始化 类加载器和类加载机制类加载器类加载机制——双亲委派模型 面试题整理1、类加载是什么2、类加载的过程是什么3、有哪些类加载器&#xff1f;4、双亲委派模型是什…

FineBI连接MySQL5.7

一、在FineBI系统管理中&#xff0c;点击【新建数据库连接】 选择MySQL数据库 配置数据库连接&#xff0c;如下&#xff0c;其中数据库名称就是需要连接的目标数据库

STM32工业物联网系统教程

目录 引言环境准备工业物联网系统基础代码实现&#xff1a;实现工业物联网系统 4.1 数据采集模块 4.2 数据处理与分析模块 4.3 通信与网络系统实现 4.4 用户界面与数据可视化应用场景&#xff1a;工业监测与优化问题解决方案与优化收尾与总结 1. 引言 工业物联网&#xff08…

QCefView 在Clion+vs2022下编译

目录 QCefView 的编译Note下载代码方式一:可直接通过github下载方式二: csdn下载编译代码1. 解压文件2. 按照规定重新放置代码文件3. 将cef 的zip 放入CefViewCore中的dep文件夹内4. 使用Clion打开QCefView工程文件夹测试代码附QCefView 的编译 Note 需要使用VS2022 编译,VS…