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中,使用 var
和 function
声明的全局变量,仍然是顶层对象的属性;但是,let
、const
和 class
命令声明的全局变量,不属于顶层对象的属性。
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);
}
尽量不使用 var
,const
优先,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
由于在函数作用域中, a2
用 var
声明,它会在函数所有代码执行之前被声明,故 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高级程序设计》,如有错误,欢迎评论区指正。