一、JS 引擎是什么?
相信大家第一次接触 JS 的时候,都会听到一个说法:JS 是一门解释型语言,代码一行一行执行。不过,实际情况比我们最初了解的要稍复杂一些。
JS 代码在运行之前,JS 引擎会先对代码进行编译 ,常见的 JS 引擎有 Chrome 浏览器的 V8 引擎、Node.js 所使用的 V8 引擎(二者核心一致,适配不同运行环境),这里和大家简单分享下我的理解。
V8 引擎的执行流程:
-
分词 :将代码拆分成一个个词法单元(token),比如把
var a = 10;拆分为var、a、=、10、;这些独立单元。 -
解析/语法分析:将词法单元组织成抽象语法树(AST),简单说就是把零散的词法单元,按照 JS 语法规则,整理成一个有逻辑结构的"语法树",方便引擎后续理解和执行。
-
执行:根据 AST 执行代码,引擎会按照语法树的逻辑,逐行执行代码,完成变量赋值、函数调用等操作。
简单理解就是:V8 引擎不会读取代码后立刻执行,而是先"梳理"(编译)代码,读懂逻辑后再执行,这也是为什么有些语法错误在运行之前就能被检测出来,分享给刚开始学习的小伙伴参考。
二、作用域 ------ 变量的"可见范围"
作用域的核心是决定变量在代码中哪些地方可以被访问,JS 中主要有三种作用域(全局、函数、块级),下面和大家慢慢梳理。
1. 全局作用域
在函数和代码块(\{\})外部声明的变量,属于全局变量,通常可以在代码的任何地方被访问。全局作用域贯穿整个代码执行过程,直到页面关闭或程序结束,全局变量才会被销毁。
javascript
var a = 1; // 全局变量
console.log(a); // 1,全局作用域下可访问
function foo() {
console.log(a); // 1,函数内部可访问全局变量
}
foo();
2. 函数作用域
函数内部声明的变量(包括函数参数),属于局部变量,一般只能在函数内部访问,函数执行结束后,内部的局部变量会被销毁,无法再外部访问。
javascript
var a = 1; // 全局变量
function foo () {
var a = 2; // 函数内部的局部变量,仅在foo内部有效
}
foo(); // 执行函数,内部变量a被创建后销毁
console.log(a); // 输出什么?答案是 1
foo 函数内部的 var a = 2是在函数作用域内声明的,它和外部的全局变量 a 是两个独立的变量,互不影响。函数执行完后,内部的局部变量 a 就会被销毁,所以最后打印的还是全局的 a = 1。
另外补充案例,进一步理解函数参数的作用域:
javascript
var a = 1;
function foo (b) { // b是函数参数,属于函数作用域内的局部变量,等同于 var b = 2;
var a = 3; // 函数内部局部变量
console.log(a,b); // 输出 3, 2,仅在函数内部可访问a和b
}
foo(2); // 传入实参2,赋值给形参b
console.log(a); // 输出1,全局变量不受影响
console.log(b); // 报错:b is not defined,函数参数b无法在外部访问
3. 块级作用域(ES6 新增)
ES6 新增了块级作用域,用 let 和 const 配合\{\} 就能形成(if 语句、for 循环、直接用 \{\} 包裹的代码块,都属于块级作用域),这也是 ES6 中很实用的一个特性,能有效避免变量污染.
先看这段代码,帮大家区分 var 和 let 在块级作用域中的差异:
javascript
if(true){ // boolean 条件为真,执行代码块
var b = 2; // var 没有块级作用域,变量b提升到全局作用域
}
console.log(b); // 2 ------ 可访问到,因为b是全局变量
但如果用 let:
javascript
if(true){
let b = 2; // let 有块级作用域,变量b仅在当前if块内有效
}
console.log(b); // 报错:b is not defined,外部无法访问块内let声明的变量
再补充一个案例,更清晰区分 var 和 let 的作用域差异:
javascript
if(true){
let b = 2; // 块级作用域,仅在当前块内有效
var c = 3; // 无块级作用域,提升到全局
}
console.log(b); // 报错:b is not defined
console.log(c); // 3,可访问到全局变量c
因为 let 声明的变量只在 \{\} 块内有效,外部无法访问。
再来个经典的 for 循环案例,帮助大家进一步理解块级作用域:
javascript
for (let i = 0; i < 10; i++) {
let a = 10; // 每次迭代都会创建一个新的a,仅在当前迭代块内有效
}
console.log(a); // 报错:a is not defined,循环结束后,块内的a被销毁
console.log(i); // 报错:i is not defined,let声明的循环变量i,仅在循环块内有效
这里补充一个小细节:如果 for 循环中用 var 声明 i,i 会提升到全局作用域,循环结束后仍可访问,且值为循环结束时的最终值(10),这也是 let 和 var 在 for 循环中最常见的差异,大家可以对比理解,后续也会提到。
循环内部的 a 只在每次迭代的块级作用域中存在,循环结束后就无法访问了,这也是块级作用域的一个典型应用场景。
三、作用域链 ------ 变量查找机制
当我们在某个作用域中使用一个变量时,JS 引擎会有一套固定的查找机制:
-
先在当前作用域查找,找到变量就直接使用;
-
找不到就去外层作用域查找(一层一层向外查找);
-
一直找到全局作用域,若找到则使用;
-
还是找不到就报错
xxx is not defined。
来看这段代码:
javascript
var a = 1; // 全局作用域的a
function foo() {
var a = 2; // foo函数作用域的a
function bar() {
console.log(a); // 查找a:bar内部没有,去外层foo作用域查找
}
bar(); // 调用bar函数
}
foo(); // 输出什么?答案是 2
bar 函数内部没有声明 a,所以它会向外层作用域查找。这里需要注意,外层作用域是 foo 函数的作用域,而不是全局(作用域查找是"就近原则",先找直接外层),foo 内部有 var a = 2,所以输出 2。
四、变量提升 ------ var 的"小特性"
var 有一个比较特别的特性------变量提升,大家可以感受一下:
javascript
console.log(a); // undefined,不是报错!
var a = 10;
刚开始接触的时候,我也觉得很神奇,变量 a 还没声明就能使用,结果是 undefined 而不是报错。
变量提升的含义是:var 声明的变量会被提升到当前作用域的顶部,但只提升声明,不提升赋值(赋值操作仍留在原地)。
上面的代码实际执行时,大概相当于这样:
javascript
var a; // 声明提升到当前作用域(全局)顶部
console.log(a); // undefined,此时仅声明未赋值,默认值为undefined
a = 10; // 赋值留在原地,执行到这里才给a赋值
补充一个重复声明案例,进一步理解 var 的变量提升:
javascript
var a = 1;
var a = 2; // var 允许重复声明,后面的声明会覆盖前面的
console.log(a); // 2
// 实际执行时,两个var a的声明都会被提升,最终赋值为2
这里提醒大家,虽然 var 允许重复声明,但实际开发中尽量避免,容易造成变量值混乱,后续我们会讲到 let/const 如何解决这个问题。
五、let 和 const ------ 更安全的声明方式
ES6 引入了 let 和 const,它们和 var 有很大区别,也是我们日常开发中更常用的声明方式,下面和大家分享一下它们的主要差异。
1. let 有块级作用域,var 没有
这一点我们在块级作用域部分已经提到,这里再用一个简单案例巩固:
javascript
{
var x = 1; // 无块级作用域,提升到全局
let y = 2; // 有块级作用域,仅在当前块内有效
}
console.log(x); // 1,全局可访问
console.log(y); // 报错:y is not defined,外部无法访问
2. let 不存在变量提升(准确说是"暂时性死区")
javascript
console.log(a); // 报错:Cannot access 'a' before initialization
let a = 10;
用 let 声明前使用变量会报错,这个区域叫做暂时性死区(TDZ) ,这是 let 和 var 一个很关键的区别。 暂时性死区的核心作用是:避免在变量声明前意外使用变量,让代码更规范。
来看一个容易混淆的例子:
javascript
var a = 100; // 全局变量a
if (true) { // 进入块级作用域,let a 触发暂时性死区
console.log(a); // 报错!无法访问a
let a = 10; // let声明a,绑定到当前块级作用域
}
很多刚开始学习的小伙伴可能会以为输出 100,但实际上会报错。实际上,let a 声明让这个块形成了独立的作用域,变量 a 被绑定在这个块内,且从进入该块开始,就进入了暂时性死区,直到 let a 声明完成,在此之前使用 a 都会报错。
3. const 声明常量,不能重新赋值
const 用于声明常量,核心特性是 "一旦声明就必须初始化,且之后不能重新赋值" ,但需要注意:如果 const 声明的是对象/数组,对象/数组内部的属性/元素可以修改,只是不能重新赋值整个对象/数组。
javascript
const d = 1;
d = 2; // 报错:Assignment to constant variable(不能重新赋值)
// 补充案例:const声明对象
const obj = { name: 'js' };
obj.name = 'javascript'; // 允许,修改对象内部属性
obj = {}; // 报错,不能重新赋值整个对象
const 一旦声明就必须初始化,且之后不能改变(指不能重新赋值),这是 const 的核心特性,也是我们声明不修改的变量时的首选方式,能有效避免变量被意外修改。
4. 关于重复声明
javascript
// var 允许重复声明,后面的覆盖前面的
var a = 1;
var a = 2;
console.log(a); // 2
// let 和 const 不允许在同一作用域内重复声明
let b = 1;
let b = 2; // 报错:Identifier 'b' has already been declared
const c = 100;
const c = 200; // 报错:Identifier 'c' has already been declared
// let/const 也不能和同一作用域内的var重复声明
var d = 1;
let d = 2; // 报错
let 和 const 不允许在同一作用域内重复声明,这也是为了避免变量声明冲突,让代码更规范,这也是 ES6 引入 let/const 相较于 var 的一大改进。
另外补充 let 重新赋值的案例:let 允许重新赋值,这是它和 const 的核心区别之一:
javascript
let c = 100;
c = 200; // 允许,let声明的变量可以重新赋值
console.log(c); // 200
六、函数 ------ 一等公民
JS 中函数是"一等公民",简单来说,就是函数可以像普通变量一样被赋值、作为参数传递、作为返回值、存储在数组/对象中。
javascript
function foo (a,b) { // 形参a,b,函数作用域内的局部变量
return a + b; // 函数返回值
}
foo(1,2); // 1,2 是实参,调用函数时传入,赋值给形参a,b
console.log(foo(1,2)); // 输出3
补充几个"函数作为一等公民"的案例,帮助大家进一步理解:
javascript
// 1. 函数作为变量赋值
const add = function(a, b) {
return a + b;
};
console.log(add(3,4)); // 7
// 2. 函数作为参数传递
function calculate(a, b, fn) {
return fn(a, b);
}
calculate(2, 3, add); // 5(add函数作为参数传入)
// 3. 函数作为返回值
function createAdd() {
return function(a, b) {
return a + b;
};
}
const newAdd = createAdd();
console.log(newAdd(1,1)); // 2
七、总结
| 特性 | var | let | const |
|---|---|---|---|
| 作用域 | 函数作用域(无块级作用域) | 块级作用域 | 块级作用域 |
| 变量提升 | 有(值为 undefined) | 有(但存在 TDZ,提前访问报错) | 有(但存在 TDZ,提前访问报错) |
| 重复声明 | 允许(后面覆盖前面) | 不允许 | 不允许 |
| 重新赋值 | 允许 | 允许 | 不允许(对象/数组内部可修改) |
| 必须初始化 | 否 | 否 | 是 |
写在最后
刚开始学这些内容的时候,我也觉得很绕,后来慢慢多写代码、多调试,才逐渐理解。这里分享几个自己的小建议,供大家参考:
- 编码规范:优先用 const ,需修改变量用 let,尽量不用 var,减少变量污染。
- 多调试:多用
console.log打印变量,结合报错理解作用域、变量提升、TDZ。 - 读懂报错:JS 报错提示明确,可快速定位作用域、重复声明、暂时性死区等问题。
参考教程:阮一峰 ES6 入门教程 es6.ruanyifeng.com/
如果文中有哪里写得不对,或者大家有更好的理解方式,欢迎评论区指正、交流,我们一起学习、共同进步~