前言
JavaScript(简称JS)作为前端开发的核心语言,能够在浏览器和服务器端流畅运行,背后离不开JS引擎的支撑,而其代码执行过程、函数用法以及作用域规则,更是掌握JS的基础。本文将围绕JS引擎、执行机制、函数和作用域这几个核心知识点,结合基础概念与通俗解释,帮你理清JS的底层逻辑,夯实基础。
一、JS引擎:JS代码的"执行者"
JS本身是一门编程语言,它无法直接被计算机识别和执行,必须依靠JS引擎来"读懂"并运行代码。简单来说,JS引擎就像是一个专门解析和执行JS代码的"工具",而我们最常用的引擎,就是V8引擎。
-
浏览器中的JS引擎:每一款现代浏览器都内置了JS引擎,比如Chrome、Edge浏览器使用的V8引擎,Firefox使用的SpiderMonkey引擎,Safari使用的JavaScriptCore引擎。正是因为有了浏览器引擎,我们才能在网页上实现交互效果(如点击按钮、表单验证等)。
-
Node.js中的JS引擎:Node.js是一个基于Chrome V8引擎的JavaScript运行环境,它让JS摆脱了浏览器的束缚,能够在服务器端运行(如搭建后端接口、操作数据库等)。这里的核心依然是V8引擎,只不过Node.js对其进行了优化,适配了服务器端的运行场景。
值得注意的是,V8引擎本身也是一段庞大的函数(本质是由C++编写的程序),它的核心功能就是读取JS代码、解析代码,并将其转换为计算机能识别的机器语言,最终执行代码并返回结果。我们写的每一行JS代码,都要经过V8引擎的"处理",才能真正发挥作用。
二、JS代码的执行:先编译,后执行
很多人会误以为JS代码是"读取一行、执行一行",但实际上,当V8引擎读取到JS代码的第一时间,并不会立即执行,而是会先进行编译(也就是"梳理"),编译完成后,才会进入执行阶段。这个"梳理"过程主要分为三个步骤,层层递进,确保代码能够正确执行。
1. 分词/词法分析
分词,也叫词法分析,顾名思义就是将整段JS代码拆分成一个个独立的"单词"(专业术语称为"词法单元")。我们可以通过一段简单的代码,清晰理解分词的过程,比如以下这段代码:
javascript
var a
console.log(a);
a = 1
//console.log(a);
//var
//a
//=
//1
这段代码经过词法分析(分词)后,会被拆分成一系列独立的词法单元,每个单元都有明确的含义,具体拆分结果如下:var(关键字,用于声明变量)、a(标识符,即变量名)、console(标识符,全局对象名)、=(运算符,赋值运算符)、1(字面量,数字值)。
这一步的核心作用,是让引擎能够识别每一个"单词"的含义,区分开关键字、变量名、运算符、标点符号等,为后续的解析工作打下基础。就像我们读文章时,先会把句子拆分成一个个汉字和词语,才能理解句子的意思。
2. 解析/语法分析------生成AST(抽象语法树)
解析,也叫语法分析,是在分词的基础上,根据JS的语法规则,将拆分后的词法单元组合成一个结构化的"树",这个树就是AST(抽象语法树)。AST本质上是对代码结构的抽象描述,它会忽略代码中的空格、换行等无关符号,只保留代码的逻辑结构和语法关系。
举个我们最熟悉的简单例子,代码 var a = 1; 就是语法分析的典型场景,结合我们上一步分词的知识,先回顾它的词法单元:var(关键字)、a(标识符)、=(赋值运算符)、1(数字字面量)、;(语句结束符)。经过语法分析后,会生成一棵AST,这棵树会清晰描述这段代码的逻辑结构:首先识别出var是变量声明关键字,声明的标识符是a,然后将数字字面量1通过赋值运算符=,赋值给变量a,最后以;结束该声明语句。
简单来说,AST就像是代码的"骨架",它剥离了代码的外在形式,保留了核心的逻辑结构,是引擎编译过程中非常关键的一步。
3. 生成代码
在得到AST之后,引擎会将AST转换为计算机能够直接识别和执行的机器语言(也叫字节码或机器码),这个过程就是"生成代码"。我们结合一段具体代码,更直观理解这个过程,比如以下这段代码:
javascript
// console.log(a);
// var a=1
var a
console.log(a);
a = 1
前文我们已经对这段代码进行了分词、语法分析(生成AST):分词后得到var、a、console、\.、log等词法单元,语法分析后生成的AST,会明确描述"声明变量a、调用console.log输出a、给a赋值为1"的逻辑结构。而生成代码阶段,V8引擎会将这棵AST转换为机器语言,比如将"声明变量a"转换为计算机能识别的内存分配指令,将"console.log(a)"转换为调用控制台输出的指令,将"a=1"转换为赋值指令,最终这些机器语言会被CPU执行,得到运行结果(输出undefined,随后a的值变为1)。
总结一下JS的执行流程:V8引擎读取代码 → 分词(词法分析)→ 解析(语法分析生成AST)→ 生成机器码 → 执行机器码。整个过程是"先编译,后执行",而非逐行执行。
三、函数:可复用的代码逻辑块
在JS中,函数是一个非常核心的概念,它的本质是一段封装好的、可复用的代码逻辑。我们可以将某一段需要重复使用的代码,写在函数内部,然后通过调用函数,让这段代码执行------这就是函数存在的核心意义。
函数的基本形式如下:function foo(){},其中 function 是声明函数的关键字,foo 是函数名,() 中可以放置函数参数,{} 内部是函数体(也就是需要执行的代码逻辑)。
举个简单的例子,我们需要多次计算两个数字的和,如果每次都写一遍加法代码,会显得非常繁琐。这时候就可以用函数来封装这段逻辑:
javascript
// 声明一个计算两数之和的函数
function add(a, b) {
return a + b; // 函数体:计算a和b的和并返回
}
// 调用函数,执行函数体中的代码
console.log(add(1, 2)); // 输出3
console.log(add(3, 4)); // 输出7
在这个例子中,add 函数封装了"计算两数之和"的逻辑,我们只需要调用 add\(参数1, 参数2\),就可以执行这段逻辑,无需重复编写加法代码。这不仅简化了代码,还提高了代码的可维护性------如果需要修改计算逻辑,只需要修改函数内部的代码即可,无需修改所有调用的地方。
另外需要注意,函数声明之后,并不会自动执行,只有当我们主动调用函数(函数名+括号)时,函数体中的代码才会被执行。如果只声明函数而不调用,这段代码就相当于"闲置",不会产生任何效果。
四、作用域:JS变量的"访问规则"
作用域是JS中另一个核心概念,它规定了变量和函数的可访问范围,简单来说,就是"变量在哪里可以被访问,在哪里不能被访问"。理解作用域,能帮助我们避免变量污染、理清变量的访问逻辑,是写出规范JS代码的关键。
JS中的作用域主要分为三类,分别是全局作用域、函数作用域和块级作用域。
1. 全局作用域
全局作用域是最顶层的作用域,在整个JS代码中都能被访问。通常,在函数外部声明的变量和函数,都属于全局作用域。比如:
javascript
// 全局变量:在函数外部声明,属于全局作用域
let globalVar = "我是全局变量";
// 全局函数:在函数外部声明,属于全局作用域
function globalFunc() {
console.log(globalVar); // 可以访问全局变量
}
// 无论在哪个地方,都能访问全局变量和全局函数
console.log(globalVar); // 输出:我是全局变量
globalFunc(); // 输出:我是全局变量
需要注意的是,在浏览器环境中,全局作用域的载体是 window 对象;在Node.js环境中,全局作用域的载体是 global 对象。全局变量和全局函数,都会成为这个载体的属性和方法。
2. 函数作用域
函数作用域是指,在函数内部声明的变量和函数,只能在该函数内部被访问,函数外部无法访问。也就是说,函数会形成一个"封闭的作用域",将内部的变量和函数与外部隔离开来,避免变量污染。
这里有一个重要的知识点:函数的参数,也属于该函数作用域的有效标识,和函数内部声明的变量一样,只能在函数内部访问。举个例子:
javascript
function foo(param) {
// 函数内部声明的变量,属于函数作用域
let innerVar = "我是函数内部变量";
// 可以访问函数参数和内部变量
console.log(param); // 输出:函数参数
console.log(innerVar); // 输出:我是函数内部变量
}
foo("函数参数");
// 函数外部无法访问函数内部的变量和参数
console.log(param); // 报错:param is not defined
console.log(innerVar); // 报错:innerVar is not defined
在这个例子中,param(函数参数)和 innerVar(函数内部变量)都属于 foo 函数的作用域,只能在 foo 函数内部访问,外部访问会报错。这就是函数作用域的"封闭性"。
3. 块级作用域
块级作用域是ES6(ECMAScript 2015)新增的作用域类型,它是通过 let、const 关键字和 \{\}(花括号)配合使用形成的。简单来说,只要是用 \{\} 包裹的代码块,并且内部用 let 或 const 声明变量,那么这个变量就属于这个块级作用域,只能在该代码块内部访问。
需要注意的是,var 关键字声明的变量,不会形成块级作用域,它会提升到全局作用域或函数作用域中。举个例子:
javascript
{
// let声明的变量,属于块级作用域
let blockVar = "我是块级变量";
// const声明的变量,也属于块级作用域
const blockConst = "我是块级常量";
console.log(blockVar); // 输出:我是块级变量
console.log(blockConst); // 输出:我是块级常量
}
// 块级作用域外部,无法访问内部的变量
console.log(blockVar); // 报错:blockVar is not defined
console.log(blockConst); // 报错:blockConst is not defined
// var声明的变量,不会形成块级作用域
{
var noBlockVar = "我不会形成块级作用域";
}
console.log(noBlockVar); // 输出:我不会形成块级作用域(提升到全局作用域)
作用域的核心规则:由内往外查找
无论是全局作用域、函数作用域还是块级作用域,它们都遵循一个核心规则:外层作用域不能访问内层作用域的变量,而内层作用域可以访问外层作用域的变量,作用域的查找顺序是由内往外。
举个例子,我们可以通过嵌套作用域来理解这个规则:
javascript
// 全局作用域
let globalVar = "全局变量";
function outer() {
// 外层函数作用域
let outerVar = "外层函数变量";
function inner() {
// 内层函数作用域
let innerVar = "内层函数变量";
// 内层可以访问自身、外层、全局的变量(由内往外查找)
console.log(innerVar); // 自身作用域:内层函数变量
console.log(outerVar); // 外层作用域:外层函数变量
console.log(globalVar); // 全局作用域:全局变量
}
inner();
// 外层可以访问自身和全局的变量,但不能访问内层的变量
console.log(outerVar); // 自身作用域:外层函数变量
console.log(globalVar); // 全局作用域:全局变量
console.log(innerVar); // 报错:innerVar is not defined
}
outer();
// 全局只能访问全局变量,不能访问外层和内层的变量
console.log(globalVar); // 全局作用域:全局变量
console.log(outerVar); // 报错:outerVar is not defined
console.log(innerVar); // 报错:innerVar is not defined
这个例子清晰地展示了作用域的查找规则:当内层作用域需要访问某个变量时,会先在自身作用域中查找,如果找不到,就会向上查找外层作用域,直到找到全局作用域;如果全局作用域中也找不到,就会报错。而外层作用域,无法向下查找内层作用域的变量。
五、let、const关键字特性与暂时性死区
结合前文块级作用域的知识点,ES6引入的let和const关键字,能有效解决var变量提升带来的变量污染问题,二者均绑定块级作用域、无变量提升且存在暂时性死区,但在使用规则上有明确差异。下面结合规范代码示例,用通俗语言逐一解析核心知识点。
1. let关键字:可修改的块级变量(含暂时性死区)
let的核心特性:绑定块级作用域、无变量提升、存在暂时性死区,且声明的变量可修改,这是它与var、const的核心区别。
暂时性死区(极简理解:先声明,再使用)
核心规则:用let在{}代码块内声明变量后,该块内只能访问块内声明的此变量;且在let声明之前,变量处于"死区",无法访问(哪怕外部有同名变量)。
示例1:触发暂时性死区(报错)
javascript
let a = 1; // 全局声明a
if (true) {
// 此时a处于死区(未声明let a),访问报错
console.log(a); // 报错:Cannot access 'a' before initialization
let a = 2; // 块内声明a,绑定当前块级作用域
}
示例2:避免暂时性死区(正常执行)
javascript
let a = 1; // 全局声明a
if (true) {
let a = 2; // 先声明块内a
console.log(a); // 输出:2(访问块内的a)
}
console.log(a); // 输出:1(访问全局的a,互不干扰)
补充:let的可修改性与块级作用域隔离
javascript
// 1. let声明的变量,在作用域内可正常修改
let a = 1;
a = 2;
console.log(a); // 输出:2(无报错,正常修改)
// 2. 块级作用域隔离:块内let声明的变量,外部无法访问
{
let a = 100; // 块内声明a
}
console.log(a); // 报错:a is not defined(外部访问不到块内的a)
// 3. 循环中let的块级作用域:外部无法访问
for (let i = 0; i < 2; i++) {
let a = 100; // 循环块内声明a
}
console.log(a); // 报错:a is not defined(外部访问不到循环内的a)
2. const关键字:不可修改的块级变量
const与let的共性:绑定块级作用域、无变量提升、存在暂时性死区;
const的核心差异:声明的变量不可修改,且声明时必须赋值(初始化),否则报错。
示例1:修改const变量(报错)
javascript
const a = 1;
a = 2; // 尝试修改const变量
console.log(a); // 报错:Assignment to constant variable(无法修改)
示例2:const未初始化(报错)
javascript
// const b; // 报错:Missing initializer in const declaration(必须赋值)
小提示:const的"不可修改",指变量指向的内存地址不变(如声明数字、字符串时无法修改);若声明对象/数组,其内部属性、元素可修改(后续进阶内容详解)。
3. let、const与var的核心对比
重点区分"块级作用域"和"变量提升",用3组对比示例,清晰呈现三者差异:
javascript
// 对比1:var无块级作用域(if块内声明,外部可访问)
if (true) {
var a = 1; // var声明,无块级作用域,提升到全局
let b = 2; //// 有块级作用域,只在 {} 内部有效 (let, const 和{} 语法配合使用会导致 声明的变量 处在一个作用域中,不会影响到var声明的变量的作用域)
}
console.log(a); // 输出:1(外部可访问)
console.log(b); // 报错:b is not defined
// 对比2:var无块级作用域(循环内声明,外部可访问,值被覆盖)
for (var i = 0; i < 2; i++) {
var a = 100; // 每次循环覆盖a的值,提升到全局
}
console.log(a); // 输出:100(外部可访问,最终值为最后一次赋值)
// 对比3:let/const有块级作用域(块内声明,外部无法访问)
{
let b = 200; // let声明,绑定块级作用域
const c = 300; // const声明,绑定块级作用域
}
console.log(b); // 报错:b is not defined(外部访问不到)
console.log(c); // 报错:c is not defined(外部访问不到)
总结:
-
var:无块级作用域、有变量提升,易污染变量,不推荐使用; -
let:有块级作用域、无变量提升、有暂时性死区,变量可修改,日常常用; -
const:有块级作用域、无变量提升、有暂时性死区,变量不可修改(需初始化),声明常量时使用。
总结
本文围绕JS引擎、执行机制、函数、作用域和 let 关键字的核心知识点,进行了详细的补充和解释。我们可以简单梳理一下核心要点:
-
JS引擎是执行JS代码的核心工具,常用的是V8引擎,分为浏览器和Node.js两种使用场景;
-
JS代码的执行遵循"先编译,后执行",编译过程分为分词、解析(生成AST)、生成代码三步;
-
函数是封装可复用代码逻辑的载体,声明后需调用才能执行,函数参数属于函数作用域;
-
作用域规定了变量的访问范围,分为全局、函数、块级三类,查找顺序由内往外;
-
let关键字会形成块级作用域,且存在暂时性死区,需先声明后使用。
这些知识点是JS的基础,也是后续学习闭包、原型链、异步编程等高级知识点的前提。掌握这些基础逻辑,能帮助我们更清晰地理解JS代码的运行机制,写出更规范、更高效的代码。