JavaScript变量、作用域与内存管理机制详解

作者:拾光时间:2026-10-28 14:19:06浏览:447

本文深入解析JavaScript中基本类型与引用类型的存储差异,阐明栈内存与堆内存的工作机制。详细讲解函数参数传递规则、全局与局部作用域的区别,以及ECMAScript中的作用域链原理。同时涵盖常见内存泄漏场景如循环引用、闭包及定时器泄露,并提供Chrome开发者工具进行内存调试的具体方法。

JavaScript变量类型与内存存储机制

JavaScript中的基本类型包括undefined、null、Boolean、Number和String。这些类型的值在内存中占据固定的空间大小,其具体数值直接存储在栈内存(Stack)中,访问方式也是按值进行。

JavaScript的数据类型主要分为两类:一是值类型,涵盖数值、布尔值、null和undefined;二是引用类型,包括对象、数组和函数。

当变量赋值为引用类型时,系统必须在堆内存(Heap)中为该值分配空间。由于引用类型的大小不固定(对象包含众多属性和方法),无法直接存入栈内存。但内存地址的大小是固定的,因此可以将该对象的内存地址存储在栈内存中,通过地址访问实际对象。


<script type="text/javascript”>
var box = new Object(); //创建一个引用类型
var box = "lee";  //基本类型值是字符串
box.age = 23;  //基本类型值添加属性很怪异,因为只有对象才可以添加属性。
alert(box.age); //不是引用类型,无法输出;
</script>

总结而言,堆内存用于存放引用类型的值,而栈内存用于存放基本类型的固定值。

JavaScript变量类型与内存存储机制


<script type="text/javascript">
  var man = new Object();//man指向了栈内存的空间地址
  man.name = "Jack";
  var man2 = man;//man2获得了man的指向地址

  alert(man2.name);//两个都弹出Jack
  alert(man.name);
</script>

变量复制与函数参数传递规则

变量复制机制

观察以下示例:


<script type="text/javascript">
  var man = new Object();//man指向了栈内存的空间地址
  man.name = "Jack";
  var man2 = man;//man2获得了man的指向地址

  man2.name = "ming";//因为他们都指向同一个object,同一个name,不管修改谁,大家都修改了
  alert(man2.name);//两个都弹出ming
  alert(man.name);
</script>

由此可知,在变量复制过程中,基本类型与引用类型的行为存在显著差异。基本类型的复制是值的完整拷贝,而引用类型的复制则是内存地址的拷贝。

参数传递方式

在ECMAScript规范中,所有函数的参数均按值传递。


<script type="text/javascript">
   function box(num){   //按值传递
     num+=10;
     return num;
   }

   var num = 10;
   var result = box(num);
   alert(result); //如果是按引用传递,那么函数里的num会成为类似全局变量,把外面的number替换掉
   alert(num);  //也就是说,最后应该输出20(这里输出10)
</script>

JavaScript不支持按引用传递参数。如果存在按引用传递,函数内部的变量将等同于全局变量,从而在外部可被访问,这显然与JavaScript的实际行为不符。

执行环境、作用域链与变量查询

执行环境与作用域

执行环境是JavaScript中至关重要的概念,它定义了变量或函数有权访问的其他数据集合。

全局执行环境是最外层的执行环境。在Web浏览器中,全局执行环境对应window对象,因此所有全局变量和函数均作为window对象的属性和方法创建。


<script type="text/javascript">
   var name = "Jack";      //定义全局变量
   function setName(){
     return "trigkit4";
   }

   alert(window.name);    //全局变量,最外围,属于window属性
   alert(window.setName()); //全局函数,最外围,属于window方法
</script>

当执行环境内的代码执行完毕后,该环境及其保存的变量和函数将被销毁。对于全局环境,只有当所有程序执行完毕或网页关闭时才会被销毁。

省略var声明的局部变量


<script type="text/javascript">
   var name = "Jack";
   function setName(){
     name = "trigkit4";  //去掉var变成了全局变量
   }

   setName();
   alert(name);//弹出trigkit4
</script>

通过函数参数传递也是创建局部变量的一种方式。


<script type="text/javascript">
   var name = "Jack";
   function setName(name){  //通过传参,也是局部变量
     alert(name);
   }

   setName("trigkit4");//弹出trigkit4
   alert(name);//弹出Jack
</script>

若函数体内包含另一个函数,则只有外层函数可以访问内层函数的变量。


<script type="text/javascript">
   var name = "Jack";
   function setName(){
     function setYear(){  //setYear()方法的作用域在setName()内
       return 21;
     }
   }
   alert(setYear());//无法访问,出错 
</script>

可通过以下方法实现访问:


<script type="text/javascript">
   var name = "Jack";
   function setName(){
     function setYear(){  //setYear()方法的作用域在setName()内
       return 21;
     }
     return setYear();
   }
   alert(setName()); //弹出21
</script>

另一个作用域示例:


<script type="text/javascript">
   var name = "Jack";
   function setName(){
     function setYear(){  //setYear()方法的作用域在setName()内
       var b = "hi";   //变量b的作用域在setYear()内
       return 21;
     }
     alert(b);//无法访问 
   }
</script>

当代码在特定环境中执行时,会形成作用域链。其作用是确保对执行环境中具有访问权限的变量和函数进行有序访问(即按规则层次访问)。作用域链的前端始终是当前执行环境的变量对象。

作用域规则

若变量未在函数内声明,或声明时未使用var关键字,则该变量为全局变量,拥有全局作用域,window对象的所有属性也具备全局作用域,可在代码任何位置访问。在函数内部使用var声明的变量为局部变量,仅能在函数体内使用。需注意,即使函数参数未使用var声明,它仍然是局部变量。

块级作用域缺失

JavaScript传统上缺乏块级作用域。


// if语句:

<script type="text/javascript">
if(true){            //if语句的花括号没有作用域的功能。

var box = "trigkit4";
}
alert(box);//弹出 trigkit4
</script>

for循环语句同样不具备块级作用域。

变量查询机制

在变量查询过程中,访问局部变量的速度比访问全局变量更快,因为无需向上搜索作用域链。示例如下:


<script type="text/javascript">
   var name = "Jack";
   function setName(){
      var name = "trigkit4";
      return name; //从底层向上搜索变量
  }
  alert(setName());   
</script>

内存管理机制与常见泄漏排查

内存相关问题

JavaScript具备自动垃圾回收机制。一旦数据不再被使用,可将其设置为null以释放引用,从而帮助垃圾回收器回收内存。

循环引用问题

一个简单的例子是:当一个DOM对象被JavaScript对象引用,同时该DOM对象又引用同一个或其他JavaScript对象时,可能会引发内存泄漏。此时,该DOM对象的引用不会在脚本停止时被垃圾回收器回收。要破坏这种循环引用,需将引用DOM元素的对象或DOM对象的引用赋值为null。

闭包与内存

在闭包中引入闭包外部的变量时,当闭包执行结束,该外部对象无法被垃圾回收(GC)机制回收。


var a = function() {
 var largeStr = new Array(1000000).join('x');
 return function() {
  return largeStr;
 }
}();

DOM泄露

当原有的COM对象被移除时,若其子节点的引用未被移除,则无法被回收。


var select = document.querySelector;
var treeRef = select('#tree');

//在COM树中leafRef是treeFre的一个子结点
var leafRef = select('#leaf'); 
var body = select('body');

body.removeChild(treeRef);

//#tree不能被回收入,因为treeRef还在
//解决方法:
treeRef = null;

//tree还不能被回收,因为叶子结果leafRef还在
leafRef = null;

//现在#tree可以被释放了。

定时器泄露

定时器也是常见的内存泄漏来源:


for (var i = 0; i < 90000; i++) {
 var buggyObject = {
  callAgain: function() {
   var ref = this;
   var val = setTimeout(function() {
    ref.callAgain();
   }, 90000);
  }
 }

 buggyObject.callAgain();
 //虽然你想回收但是timer还在
 buggyObject = null;
}

内存调试方法

Chrome浏览器自带的内存调试工具可以方便地查看内存使用情况和检测内存泄漏。在Timeline -> Memory面板中点击record即可开始记录。

作者最新文章