javascript 的执行上下文与作用域

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

问: 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 对象)。

  • 12
    点赞
  • 16
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值