JavaScript 引擎、作用域与预编译机制深度解析
本文深入解析JavaScript引擎的工作流程,详细阐述全局、函数及块级三种作用域的区别。重点讲解预编译阶段的变量提升与函数声明提升规则,以及AO/GO对象的创建过程。通过作用域链[[scope]]属性,揭示内部函数访问外部变量的底层逻辑,帮助开发者理解变量查找机制。
前言
首先需要明确引擎的概念,其核心职责是负责JavaScript代码从开始到结束的完整执行过程。与引擎紧密配合的是编译器,主要承担代码分析与编译任务;而今天讨论的核心主角则是作用域。接下来将探讨作用域的具体用途,以及它与作用域链之间的关联。
一、作用域(scope)
作用域定义了变量、函数和对象在运行时代码特定部分中的可访问性范围。
1.作用域的分类
- 全局作用域
var name="global";
function foo(){
console.log(name);
}
foo();//global
在函数foo()内部未显式声明name变量的情况下,依然能够打印出name的值,这证明函数内部可以访问全局作用域中的变量。参考以下示例:
hobby='music';
function foo(){
hobby='book';
console.log(hobby);
}
foo();//book
当全局作用域和函数foo()内部均未声明hobby变量时,代码并未报错。原因在于赋值语句hobby='music';位于全局作用域中,即使没有使用var、let或const进行声明,该变量也会被挂载到window对象上。因此,函数foo()不仅能读取hobby的值,还能修改它。这表明hobby='music';等价于window.hobby='music';。
2.函数体作用域
函数体作用域通过隐藏内部实现细节来运作。简而言之,内层作用域可以访问外层作用域,但外层作用域无法访问内层作用域。这一现象在理解作用域链时将变得清晰。
function foo(){
var age=19;
console.log(age);
}
console.log(age);//ReferenceError:age is not defined
全局作用域中不存在age变量,但函数foo()内部定义了该变量。由于外部无法访问内部变量,尝试访问age会抛出错误。此外,若函数foo()未被调用,则其内部代码不会执行。
3.块级作用域
块级作用域在开发中十分常见,例如使用let声明的变量、代码块{}、for循环中使用let的情况,以及if、while、switch语句等。要深入理解块级作用域,需先掌握以下关键概念:
--标识符:指在作用域内生效的变量,包括函数参数、变量名和函数名。需特别注意:函数体内部的标识符在外部无法访问。
--函数声明:语法格式为function 函数名(){}
--函数表达式:语法格式为var 函数名=function(){}
--自执行函数:语法格式为(function 函数名(){})();自执行函数前的语句必须包含分号,通常用于隐藏作用域。
接下来通过一个综合示例进行展示:
function foo(sex){
console.log(sex);
}
var f=function(){
console.log('hello');
}
var height=180;
(
function fn(){
console.log(height);
}
)();
foo('female');
//依次打印:
//180
//female
//hello
分析如下:标识符包括foo、sex、height、fn;函数声明为function foo(sex){};函数表达式为var f=function(){};自执行函数为(function fn(){})()。需注意,自执行函数fn()前的语句var height=180;必须保留分号,否则可能导致语法错误。
二、预编译
虽然主题聚焦于作用域和链,但理解作用域链的必要前提是先了解预编译过程。此处将讨论预编译在不同执行环境下的具体行为。
- 发生在代码执行之前
(1)声明提升
console.log(b);
var b=123;//undefined
此处打印结果为undefined,这并非报错,而是预编译在代码执行前的结果,与ReferenceError: b is not defined不同。该过程等同于以下代码逻辑:
var b;//声明提升
console.log(b);//undefined
b=123;
(2)函数声明整体提升
test();//hello123 调用函数前并没有声明,但是任然打印,是因为函数声明整体提升了
function test(){
var a=123;
console.log('hello'+a);
}
2.发生在函数执行之前
理解此过程只需掌握四个步骤:
(1)创建一个AO(Activation Object,激活对象)
(2)查找形参和变量声明,将形参和变量名作为AO的属性名,属性值初始化为undefined
(3)将实参与形参的值统一
(4)在函数体内查找函数声明,将函数名作为AO对象的属性名,属性值赋为函数体。接下来通过具体案例进行演示:
var global='window';
function foo(name,sex){
console.log(name);
function name(){};
console.log(name);
var nums=123;
function nums(){};
console.log(nums);
var fn=function(){};
console.log(fn);
}
foo('html');
运行结果是什么?分析如下:
//从上到下
//1、创建一个AO(Activation Object)
AO:{
//2、找形参和变量声明,然后将形参和变量声明作为AO的属性名,属性值为undefined
name:undefined,
sex:undefined,
nums=undefined,
fn:undefined,
//3、将实参和形参统一
name:html,
sex:undefined,
nums=123,
fn:function(){},
//4、在函数体内找函数声明,将函数名作为AO对象的属性名,属性值予函数体
name:function(){},
sex:undefined,
fn:function(){},
nums:123//这里不仅存在nums变量声明,也存在nums函数声明,但是取前者的值
以上步骤得到的值,会按照后面步骤得到的值覆盖前面步骤得到的值
}
//依次打印
//[Function: name]
//[Function: name]
//123
//[Function: fn]
3.发生在全局(内层作用域可以访问外层作用域)
与函数执行前的预编译类似,全局预编译也遵循三个步骤:
(1)创建GO(Global Object,全局对象)
(2)查找全局变量声明,将变量名作为GO的属性名,属性值初始化为undefined
(3)在全局范围内查找函数声明,将函数名作为GO对象的属性名,属性值赋为函数体
参考示例:
var global='window';
function foo(a){
console.log(a);
console.log(global);
var b;
}
var fn=function(){};
console.log(fn);
foo(123);
console.log(b);
该示例逻辑较为简单,遵循相同的步骤和思路,此处不再赘述分析。打印结果依次为:
[Function: fn]
123
window
ReferenceError: b is not defined
至此,我们回到正轨,继续探讨作用域链。
三、作用域链
作用域链解释了为何内层可以访问外层,而外层无法访问内层。在深入理解作用域链之前,需先了解几个抽象概念。
执行期上下文:当函数执行时,会创建一个称为执行期上下文的对象(AO对象),它定义了函数执行时的环境。每次函数执行都会创建独一无二的执行上下文,多次调用函数会产生多个执行期上下文,函数执行完毕后,其对应的执行期上下文将被销毁。
查找变量:从作用域链的顶端开始,依次向下查找。
[[scope]]:作用域属性,也称为隐式属性,仅支持引擎自身访问。函数作用域不可直接访问,其中存储了运行期上下文的集合。
首先观察函数的自带属性:
function test(){//函数被创建的那一刻,就携带name,prototype属性
console.log(123);
}
console.log(test.name);//test
console.log(test.prototype);//{} 原型
// console.log(test[[scope]]);访问不到,作用域属性,也称为隐式属性
// test() --->AO:{}执行完毕会回收
// test() --->AO:{}执行完毕会回收
接下来展示作用域链的实现机制:
var global='window';
function foo(){
function fn(){
var fn=222;
}
var foo=111;
console.log(foo);
}
foo();
分析:
GO:{
foo:function(){}
}
fooAO:{
foo:111,
fn:function(){}
}
fnAO:{
fn:222
}
// foo定义时 foo.[[scope]]---->0:GO{}
// foo执行时 foo.[[scope]]---->0:AO{} 1:GO{} 后访问的在前面
//fn定义时 fn.[[scope]]---->0:fnAO{} 1:fooAO{} 2:GO{}
fnAO:fn的AO对象;fooAO:foo的AO对象

综上所述,作用域链即为[[scope]]中存储的执行期上下文对象的集合。这些对象以链式结构链接,这种链式链接被称为作用域链。
以上为JavaScript作用域及作用域链工作引擎的详细内容,更多关于JavaScript作用域和作用域链的资料请关注本站其他相关文章!
D3.js 比例尺与坐标轴的配置详解
2026-11-05 16:54:46
JavaScript怎么遍历对象的Symbol键和不可枚举属性
2026-11-05 13:13:39
封装一个健壮的JS字符串判空工具函数
2026-11-05 12:16:14
JavaScript合并数组的三种常用方法:concat、扩展运算符与push
2026-11-05 11:22:29
JavaScript立即执行函数(IIFE)是什么及基本语法
2026-11-05 10:51:19
