JavaScript 引擎、作用域与预编译机制深度解析

作者:异步漫步时间:2026-10-25 14:40:35浏览:171

本文深入解析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作用域和作用域链的资料请关注本站其他相关文章!

作者最新文章