JavaScript词法环境与闭包机制深度解析

作者:小括号时间:2026-10-22 21:37:10浏览:233

本文深入解析JavaScript执行机制,详细阐述词法环境对象的创建过程、函数声明与变量声明的提升差异及覆盖规则。通过实例剖析闭包的形成条件、作用域链本质及内存保留机制,并介绍其在封装、缓存及解决循环引用中的实际应用。

本文通过具体实例深入解析JavaScript的底层解析机制与闭包原理,旨在帮助开发者理解相关概念并提供参考。

JavaScript词法环境解析机制

首先探讨JavaScript的解析机制。

在代码正式解析执行前,JavaScript引擎会预先创建一个词法环境对象(LexicalEnvironment),初始状态为空对象。

在扫描JavaScript代码的过程中,引擎会将以下元素存入该词法环境:

1、通过函数声明方式创建的函数名称;

2、通过var关键字定义的变量名称;

3、当存在同名标识符时,函数声明会覆盖变量声明,且后续的函数声明会覆盖之前的同名函数声明;

4、存入词法环境的函数值为对该函数的引用,而变量的值则为undefined;

5、若采用函数表达式方式创建函数:

var fn = function(){ } 此时词法环境中存储的是变量名fn,其初始赋值为undefined;

若在函数定义之前调用该函数,其行为与变量类似,会抛出undefined相关的错误;

这体现了两种函数创建方式在提升行为上的区别;


LexicalEnvironment(这个词法环境===window)
{
fn: 对函数的一个引用;
b:undefined;
}

通过函数声明方式创建的函数示例:


function fn(){ };

通过var定义的变量示例:


var b=5;

每次调用函数时,都会创建一个新的词法环境对象(LexicalEnvironment)。解析函数内部的变量和函数声明时,规则与全局词法环境相同,但需注意以下两点:


LexicalEnvironment(这个词法环境===fn)
{
a:对函数的一个引用;(解析的时候函数声明把变量覆盖了,尽管变量已经被赋值为1)
b:2;(解析的时候把变量存在了词法环境里,同时赋值为2)
}

 


function fn (a,b){
  alert(a)// function a(){ }
  alert(b)//2
  var b= 100;
  function a(){ }
}
fn(1,2);

调用函数并传递参数时,词法环境中会存储形参名并赋值。若函数内部存在同名函数声明,传入的参数值会覆盖该函数声明;若形参数量少于实参,多余的实参将被赋值为undefined;

闭包原理与形成条件

接下来分析闭包的概念。

关于闭包的定义有多种表述:

1、通俗定义:函数嵌套函数,内部函数可以访问外部函数的参数和变量,这些被引用的参数和变量不会被垃圾回收机制回收;

2、计算机科学定义:闭包是词法闭包的简称,指引用了自由变量的函数。该自由变量与函数一同存在,即使离开创建它的环境也不会被销毁;

3、实体定义:闭包是由函数及其相关的引用环境组合而成的实体(隐含意思是函数必须与引用环境共存,且存在引用关系);

综合来看,所有定义均围绕两个核心本质:函数引用变量,且该变量不会被销毁。

所谓“被引用的自由变量离开了创造它的环境”,具体表现如下:


function fn(){
  var a = 10;
  var b = 20;
  return function fn2(){
    alert(a);
  }
}
var result = fn();
result();//10;

上述代码中,fn2在被return出去后,虽然离开了fn函数的作用域环境,但在外部调用时仍能访问到fn内部的变量;

这证明了被引用的变量不会被销毁。同理,在自执行函数构成的闭包中,尽管自执行函数执行完毕后其内部变量本应被垃圾回收,但由于内部存在引用它的子函数(构成闭包),其变量依然不会被销毁;

由此可见,闭包的一个主要作用是允许在外部访问内部函数的变量;

许多人在解释闭包时,常强调子函数被return出去后在外部调用的情况。实际上,无论在哪里调用,只要函数嵌套且子函数引用了父函数的变量,闭包即已形成(可通过在子函数内部打断点,在f12中观察闭包内容来证明,此时引用函数已出现,即便调用尚未执行);

闭包的本质在于形成了作用域链。

注意:形成闭包需满足两个条件:1、函数必须嵌套;2、子函数必须引用父函数的变量(若未引用则不形成闭包;若引用的是父函数的父函数的变量,同样会形成闭包);

父函数每调用一次,就会形成一个新的闭包(即复制一份),从而形成一个新的词法作用域,并重新引用父函数的变量;

以下代码说明了这一过程:


function fn() {
  var num = 1;
  return function() {
    num++;
    alert(num);
  }
}
var result1 = fn();
result1();//2;
result1();//3;
var result2 = fn();
result2();//2;
result2();//3;

当fn被调用两次时,分别形成了新的闭包及各自的词法作用域,因此result2的输出结果不受result1的影响;

函数每调用一次都会复制一份新的实例,这可以解释for循环中i变量相关的问题;


for (var i = 0; i < 5; i++) {
  (function(index){
    oDiv.onclick = function() {
      alert(index)
    }
  })(i)
}

自执行函数每调用一次都会复制一份新的实例,传入的i值随之变化。由于在静态作用域中,预解析阶段已确定变量的作用域,子函数引用的父函数变量index只能是每次复制的那个父函数变量,从而实现了获取不同i值的目的;

闭包的实际应用场景

闭包的主要用途包括:

1、匿名自执行函数;

避免污染全局变量(若声明为全局变量,其他函数可能误用这些变量,导致全局对象过于庞大,影响访问速度,因为变量取值需从原型链上遍历),从而提高效率;

2、结果缓存;

开发中常遇到处理过程耗时的函数对象,每次调用都花费大量时间。利用闭包,由于它不会释放外部引用,函数内部的值得以保留。这样在第二次调用时,即可从缓存中读取资源;

3、实现数据封装;

4、实现类和继承(通过构造函数);

更多关于JavaScript相关内容感兴趣的读者可查看本站专题:《javascript面向对象入门教程》、《JavaScript错误与调试技巧总结》、《JavaScript数据结构与算法技巧总结》、《JavaScript遍历算法与技巧总结》及《JavaScript数学运算用法总结》

希望本文所述内容对大家理解JavaScript程序设计有所帮助。

作者最新文章