JS学习笔记(六)是什么执行上下文和作用域?var、let、const的区别?

JS学习笔记(六)

本系列更多文章,可以查看专栏 JS学习笔记



一、词法作用域和动态作用域

JavaScript 采用的是 词法作用域

1. 区别

  • 词法作用域:定义在词法阶段的作用域,取决于代码实际编写位置
  • 动态作用域:关联在程序执行阶段,取决于代码执行的位置

2. 词法作用域

var name = "Alice";
function f1() {
	console.log(name);
}
function f2() {
	var name = "Bob";
	f1();
}
f2(); // Alice

由于 f1() 中没有变量 name 的定义或赋值,故根据函数 f1() 编写位置,寻找上一层代码中变量 name 的值,故使用词法作用域,输出结果会是 Alice ,而不是 Bob

var name = "Alice";
// 函数表达式创建函数
const f1 = function () {
	console.log(name);
};
function f2() {
	var name = "Bob";
	return f1;
}
f2()(); // Alice

同理,由于JS使用词法作用域,输出也为 Alice


二、执行上下文和作用域

1. 作用域

(1)全局作用域

  • (1)网页运行时创建,关闭时消耗
  • (2)直接在script标签中的代码位于全局作用域
  • (3)全局作用域中的变量为全局变量,任意位置均可访问到

(2) 函数作用域

  • (1)函数作用域是一种局部作用域
  • (2)在函数调用时产生,调用结束后销毁
  • (3)函数作用域中的变量为局部变量,只能在函数内部访问
  • (4)函数每次调用都会产生一个全新的函数作用域

(3)块作用域

结合 { } 进行分组的代码,用let声明变量,会成为一个块作用域

  • (1)块作用域是一种局部作用域
  • (2)在代码执行时创建,代码执行完毕销毁
  • (3)块作用域中的变量为局部变量,只能在块内部访问

变量函数 的上下文,决定了它们可以访问哪些数据,以及它们的行为

2. 执行上下文

主要有全局上下文、函数上下文;eval()调用内部存在第三种上下文

  • (1)每个上下文都有一个关联的变量对象,而这个上下文中定义的所有变量和函数都存在于这个对象上
  • (2)每个函数调用都有自己的上下文,程序的执行流通过一个上下文栈控制
    • 当代码执行流进入函数时,该函数的上下文进栈;
    • 函数执行完毕后,该函数的上下文出栈
  • (3)上下文代码执行时,会创建变量对象的一个作用域链
    • 代码正在执行的上下文的变量对象,始终位于作用域链的最前端
    • 作用域链中的下一个变量对象,来自包含上下文……以此类推
    • 全局上下文的变量对象,始终是作用域链的最后一个变量对象
  • (4)上下文中的连接是线性的、有序的。
// 全局作用域中
var color1 = "red";
function outer() {
	// outer函数作用域中
	let color2 = "orange";
	function inner() {
		// inner函数作用域中
		let color3 = color2;
		color2 = color1;
		color1 = color3;
		// 沿作用域链可访问color3、color2、color1
	}
	// 沿作用域链可访问color2、color1
}
// 沿作用域链可访问color1

在这里插入图片描述
内部上下文,可以通过作用域链访问外部上下文中的一切,但外部上下文无法访问内部上下文中的任何东西。


3. 顶层对象的属性

顶层对象,即全局上下文关联的对象

  • 浏览器环境: 顶层对象指 window 对象
  • Node环境: 顶层对象指 global 对象

ES5中,顶层对象的属性与全局变量是等价的,即创建全局变量,可以用顶层对象获取此变量值。
ES6中,使用 varfunction 声明的全局变量,仍然是顶层对象的属性;但是,letconstclass 命令声明的全局变量,不属于顶层对象的属性


4. 作用域链增强

  • (1)try/catch 语句的 catch块 会创建一个新变量对象,包含要抛出的错误对象的声明
  • (2)with 语句: 会向作用域链前端添加指定的对象,即with语句括号中的对象

三、var、let、const关键字

1. var关键字

(1)var声明作用域——函数作用域

var声明的范围是函数作用域

  • var 操作符定义的变量,会成为包含它的函数的局部变量;
  • 若省略 var 操作符,在函数被调用一次以后,会可以创建一个全局变量(未调用,不会变成全局变量);
  • var 操作符定义的变量,如果在全局作用域中声明(直接声明在 script 标签中),会成为 window 对象的属性,即全局变量
<script>
	if (true) {
		var name = "Jack";
		consolo.log(name); // 输出Jack
	}
	consolo.log(name); // 函数作用域中可以找到name,输出Jack
			
	var message1 = "hi!";
	message2 = "how are you?";
	function test() {
		var message3 = "goodbye!";
		message4 = "ok!";
		console.log(message1);	// message1是全局变量,输出 hi!
		console.log(message2);	// message2是全局变量,输出 how are you?
		console.log(message3);	// message3是局部变量,在函数内部能输出 goodbye!
		console.log(message4);	// message4在函数被调用以后,会变成全局变量;未调用仅为局部变量。均输出 ok!
	}	
	test();
	console.log(message1); // hi!
	console.log(message2); // how are you?
	console.log(message3); // 报错 "Uncaught ReferenceError: message3 is not defined"
	console.log(message4); // ok!(若不调用test()函数,则会有"Uncaught ReferenceError"报错)
</script>

(2)var声明提升

var声明的变量会自动提升到函数作用域顶部

注:提升(hoist),就是把所有变量声明都拉到函数作用域的顶部,但先使用后赋值,其值为 undefined

function foo() {
	console.log(age); 
	var age = 26;
}
foo(); // 变量被提升,输出 undefined

当然,可以反复多次使用var声明同一个变量,后面的值会覆盖前面的值

function foo() {
	var age = 16;
	var age = 26;
	var age = 36;
	console.log(age);
}
foo(); // 多次声明同一个变量,输出 36

2. let关键字【ES6新增】

(1)let声明作用域——块作用域

let声明的范围是块作用域,代码块由 { } 进行划分,例如if块、while块、function块,或仅仅只有一对花括号。

  • 块作用域函数作用域 的子集,适用于 var 的作用域的限制也适用于 let
  • let 操作符定义的变量,不能在块外部被引用
  • let 操作符定义的变量,不能被冗余声明(在块中,多次声明同一变量)
let age = 18;
consolo.log(age); // 18
if (true) {
	let age = 26; // 与块作用域外部的age,不会造成冗余声明
	consolo.log(age); // 26
}

var name;
let name; // 报错 "Uncaught SyntaxError: Identifier 'name' has already been declared"

let sex;
var sex; // 报错 "Uncaught SyntaxError: Identifier 'name' has already been declared"

(2)立即执行函数(IIFE)

  • 只执行一次的匿名函数
  • 可以用IIFE创建一个执行一次的函数作用域,避免变量冲突
// 相当于普通函数声明时,没有函数名,而且在函数声明后面,直接加()进行调用
(function(){
	语句...
}())

因此在立即执行函数内部,即使使用 var 来声明全局变量,也无法在立即执行函数外部调用此变量。

(function () {
	var a = "a";
	console.log(a); // a
})();
// 立即执行函数执行完,变量a已经被销毁,会报错。
// console.log(a); // 报错 "Uncaught ReferenceError: a is not defined"

let产生的块作用域,可以替代立即执行函数。

{
	let b = "b";
	console.log(b); // b
}
// 立即执行函数执行完,变量b已经被销毁,会报错。
console.log(b); // 报错 "Uncaught ReferenceError: b is not defined"

注:当两个立即执行函数连续时 ,必须在最外层()后面加上分号 ; ,不可以省略,否则编译器无法正确识别,自动添加分号

(function(){
   语句...
}());
(function(){
   语句...
}());

(3)暂时性死区

let 声明之前的执行瞬间,被称为“暂时性死区”,此阶段引用任何后面才声明的变量,都会报错
注:let 声明的变量不会在作用域中被提升(实际上发生了提升,只是在为赋值之前禁止访问该变量)

console.log(test); // 报错 "Uncaught ReferenceError: Cannot access 'test' before initialization"
let test = "t";

(4)全局声明

使用 let 在全局作用域声明的变量,不会成为window对象的属性(var声明的变量会)

var test1 = "test1";
console.log(window.test1); // test1

let test2 = "test2";
console.log(window.test2); // window对象没有test2属性,输出 undefined

(5)条件声明

不可用 try/catch 语句或 typeof 检查前面是否使用 let 声明过同名变量,因为被限制在块级作用域中

// 希望用if来判断已经使用let声明变量name
if (typeof name === "undefined") {
	let name;
}
// 但此时name相当于省略var的全局变量
name = "name";

// 如果age没有声明过会报错
try {
	console.log(age);
} catch (error) {
             // 在catch块作用域声明age
	let age;
}
// 实际上为省略var的全局变量age
age = 26;

注:条件声明是一种反模式,不推荐使用;let 不依赖于条件声明是好事


(6)for循环中的let声明

for 循环定义中,使用 let 声明迭代变量,不会将值渗透到循环体外部;JavaScript引擎会为每个迭代循环声明一个新的迭代变量。
注:for循环是同步行为,setTimeout()是异步函数,函数作为参数传递是回调函数。【同步>异步>回调】

// 使用var定义,for循环执行完毕后,i=5
for (var i = 0; i < 5; ++i) {
	setTimeout(() => console.log(i), 0); // 退出循环后,执行超时逻辑时,i均为5
}
// 输出 5 5 5 5 5

此两部分代码,将会分别执行。若一起执行,输出 6 5 5 5 5 5 0 1 2 3 4

// 使用let定义,JS引擎在后台会为每个迭代循环声明一个新的迭代变量
for (let i = 0; i < 5; ++i) {
	setTimeout(() => console.log(i), 0); // 每个setTimeout引用的都是不同的变量实例
}
// 由于代码会按顺序执行完,才会开始执行异步函数,所以先输出6
console.log(6);
// 输出 6 0 1 2 3 4

适用于所有风格的for循环,包括for-in和for-of


(7)块作用域和函数声明【ES5/ES6对比】

  • ES5中,函数只能在顶层作用域函数作用域中声明,不能在块级作用域中声明
  • ES6中,明确允许在块级作用域中声明函数; 此时的函数声明语句类似 let在块级作用域外不可使用

注:ES6的浏览器中可以不支持上述ES6特效,有自己的行为方式,如下所示

  • 允许块级作用域内声明函数
  • 函数声明类似于 var, 即会提升到全局作用域函数作用域的头部
  • 同时,函数声明会提升到所在的块级作用域的头部
// 原代码
function f() {
	console.log("outside");
}
(function () {
	if (false) {
		function f() {
			console.log("inside");
		}
	}
	f();
})();

以下为,浏览器的ES6环境

// 浏览器的ES6环境
function f() {
	console.log("outside");
}
(function () {
	// 函数声明提升
	var f = undefined;
	if (false) {
		function f() {
			console.log("inside");
		}
	}
	f();
})();
// 故会报错 "Uncaught TypeError: f is not a function"

因为环境会导致行为差异很大,故应避免在块作用域中声明函数;若需要声明函数,应采用函数表达式替代函数声明语句!!!


3. const关键字【ES6新增】

(1)const声明作用域——块作用域

const 行为与 let 基本一致,唯一一个重要区别是:声明变量时必须同时初始化变量,且尝试修改const声明的变量会导致运行时错误!
注:const声明限制只适用于它指向的变量的引用,若变量为对象,可以修改其元素或属性值。

(2)for循环中的const声明

  • for-in循环,会遍历对象的key或下标值(索引值)
  • for-of循环,会遍历对象的元素值或属性值
// 输出 a b
for (const key in { a: 1, b: 2 }) {
	console.log(key);
}

// 输出 1 2 3 4 5
for (const value of [1, 2, 3, 4, 5]) {
	console.log(value);
			}

尽量不使用 varconst 优先,let 次之!!!


四、练习

1. 练习1:作用域

var a1 = 1;
function f1() {
	a1 = 2;
	console.log(a1); // 2
}
f1();
console.log(a1); // 2

由于,var 声明的变量 a1 为全局变量,而在函数 f1() 中没有对 a1 变量的局部声明,因此函数中的 a1 使用的是全局变量 a1

当函数内部修改 a1 后,全局变量 a1 一同被修改,因此第二个结果也是 2

2. 练习2:作用域、变量提升

var a2 = 1;
function f2() {
	console.log(a2); // undefined
	var a2 = 2;
	console.log(a2); // 2
}
f2();
console.log(a2); // 1

由于在函数作用域中, a2var 声明,它会在函数所有代码执行之前被声明,故 a2 的值为 undefined

由于函数变量中的 a2 为局部变量,则修改局部变量值,不会影响全局变量 a2,因此第三个 a2 输出为 1

3. 练习3:作用域、函数形参

var a3 = 1;
function f3(a3) {
	console.log(a3); // undefined
	a3 = 2;
	console.log(a3); // 2
}
f3();
console.log(a3); // 1

函数拥有形参,相当于在函数内部声明形参变量,但是由于调用函数时没有传入实参,因此第一次输出 a3 时为 undefined

其余,和练习2中解释相同。

4. 练习4:变量提升、函数提升

console.log(a);  

var a = 1;

console.log(a);

function a() {
	alert(2);
}

console.log(a);

var a = 3;

console.log(a);

var a = function () {
	alert(4);
};

console.log(a);

var a

console.log(a);

运行结果如下图所示

在这里插入图片描述
解释:

(1) 由于var声明变量和函数声名创建的函数均会被提升,因此下图中红色区域提升,相当于在所有代码执行前,添加了 a 变量的声明和 a() 函数的声明
在这里插入图片描述
但是,由于var声明不会给变量a赋值,但是函数a()声明,会导致输出a,则是输出一个函数a(),所以第一个输出结果为:

ƒ a() {
	alert(2);
}

(2)显而易见,第二个 a 输出为 1 ,因为 var a = 1
(3)第四块的代码为函数a()声明,没有被调用不会有输出结果,于是继续执行下一行代码 console.log(a);,输出仍为 1
(4)下一行代码的变量 a 已经变量提升,相当于 a = 3,则输出为 3
(5)下一行的代码是用函数表达式为变量 a 进行赋值,则 a 的输出结果为这个函数

ƒ () {
	alert(4);
}

(6)倒数第二行代码再次声明变量 a ,由于变量提升,代码中所有再次声明的代码均无效,所以会跳过这行代码,执行下一行代码,故结果同上一步

ƒ () {
	alert(4);
}

结尾

部分内容参考《ECMAScript 6 入门》《JavaScript权威指南》《JavaScript高级程序设计》,如有错误,欢迎评论区指正。

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

想要大口炫榴莲

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值