深入解析JavaScript作用域与闭包机制
本文深入解析JavaScript的作用域与闭包机制。从ES5函数作用域出发,阐述栈内存与堆内存的区别,详解函数执行时的私有作用域创建、预解释及变量查找规则。重点分析闭包的形成原理、作用域链的查找逻辑,以及函数返回值对内存释放的影响,帮助开发者理解变量生命周期与内存管理。
深入解析JavaScript作用域与闭包机制
在ECMAScript5: JS 规范下,代码本身并不具备代码块级别的作用域;闭包是通过函数运行的机制来创建的;本质上,闭包体现的正是作用域的概念。
在ES5标准中,JavaScript仅允许通过函数来创建具有操作权限的作用域。
JavaScript的内存空间分为栈内存(Stack)和堆内存(Heap)。通常,栈内存存储对象的引用地址,而堆内存存储对象的具体内容。对于原始类型值,其地址和内容均存储在栈内存中;对于引用类型值,其地址存储在栈内存,具体数据存储在堆内存中。栈内存运行效率高于堆内存,但空间较小;反之,堆内存空间较大但效率较低。因此,简单的原始类型值适合存放在栈内存中,而复杂的引用类型值则存放在堆内存中,以保障栈内存的运行效率。
函数执行时形成私有作用域
当函数执行时(其直接目的是运行函数体内的代码),会创建一个新的私有作用域(位于栈内存中),供函数体内的代码执行使用。
- 对形参进行赋值
- 在私有作用域中进行预解释
- 执行私有作用域下的代码
新创建的私有作用域保护了其中的私有变量不受外界影响,这种保护机制被称为“闭包”。为何需要作用域?因为变量需要规定活动范围,便于管理不同范围的变量,因此必须为变量设置活动范围。
预解释过程也是在各自的作用域内独立进行的。
function fn(){
var a=1;
}
fn();
fn();
console.log(a);//Uncaught ReferenceError: a is not defined;
由于变量a既未声明也未定义,因此程序会报错。
上述fn函数运行了两次,从而产生了两个堆内存空间;同时形成了两个作用域(即两个闭包),每个作用域中各自拥有一个变量a。虽然两个a的值均为数字1,但这两个变量互不相等,且彼此之间没有任何关联。
这就像我们都属于人类,都继承了人类这个对象所具有的特征。我有一双手,你也有一双手,但我们两个人的手是相互独立的;我的手不等于你的手。
全局变量与私有变量
- 在全局作用域下声明的变量被称为全局变量。
- 在私有作用域中声明的变量被称为私有变量;此外,函数的形参也属于私有变量。
如何区分函数中出现的变量是私有的还是非私有的?
首先判断该变量是否为形参,其次判断其是否在私有作用域中声明过(即是否使用var声明)。只要满足其中任一条件,该变量在当前函数中无论出现在什么位置,均被视为私有变量,与全局变量无关。如果两者均不满足,则说明该变量不是私有的。若函数中出现的变量不是私有的,解释器会向其上级作用域查找;若上级作用域中不存在,则继续向上查找,直至找到window对象为止。如果window对象中也不存在该变量,会如何处理?
如果是获取该变量的值,程序会报错。
function fn() {
console.log(num);//Uncaught ReferenceError: num is not defined
}
fn();
如果是设置该变量的值,且该变量非私有,程序会查找全局作用域;若全局作用域中不存在该变量,则相当于给全局对象添加一个名为该变量的属性。
function fn() {
num = 13;//相当于给window增加了一个叫做num的属性名,属性值是13 window.num=13;
}
fn();
console.log(num);//13
如何查找当前作用域的上级作用域?
当前作用域的上级作用域是谁,与函数在哪里执行没有任何关系。我们只需要看当前函数是在哪个作用域下定义的,那么它的上级作用域就是谁。
以下是查找上一级作用域,直至找到window对象的案例。
var total = 100;
function fn() {
var total = 10;
return function () {
console.log(total);
}//如果返回的是一个引用数据类型的值(对象、数组、函数...),首先是把这个值开辟一个内存空间,有一个地址,然后把内存地址返回 ->例如在这里其实返回的就是 return xxxfff111;
}
var f = fn();//f=xxxfff111;
f();
// 10
//输出的结果是 10 还是 100 ? 为什么 ?
如果代码是在自执行函数中运行呢?
var total = 100;
function fn() {
var total = 10;
return function () {
console.log(total);
}
}
var f = fn();//f=xxxfff111;
~function () {
var total = 1200;
f();//->;里面变量的上级作用域是谁
}();
// 10
下面代码输出的是什么?
var a=0;
var b="0";
function fn(){
console.log(a);
console.log(b);
console.log(typeof a);
console.log(typeof b);
a="1";
var b=1;
console.log(a);
console.log(b);
console.log(typeof a);
console.log(typeof b);
}
fn();//运行后会输出什么?如果没有这行代码;函数fn的定义是没有意义的;函数只声明定义,而不运行是没有任何意义的;
//0,undefined,"number","undefined","1",1,"string","number"
预解释是在各自的作用域中进行的;JavaScript允许函数内部嵌套函数。当代码运行时,会在函数创建的作用域内再创建一个作用域。
以下是作用域范围的示意图。
var a="window";
console.log(a);//window
function father(){
console.log(a);//undefined
var a="father";
console.log(a);//father
function children(){
console.log(b);//undefined
console.log(a);//father
var b="flag";
a="children";
console.log(a);//children
}
children();
console.log(a);//children
}
father();
console.log(a);//window
JavaScript中的代码执行顺序是从上到下逐条运行的。遇到function定义函数的代码块时,直接跳过;遇到函数执行的代码时,找到引用的函数地址,开始跳到执行函数产生的作用域中执行代码;等函数执行完毕后,再回到当前作用域执行下面的代码。
上面的代码运行后,输出的是
“window”,undefined,“father”,undefined,“father”,“children”,“children”,“window”
作用域链查找:当作用域嵌套作用域时,若children内部找不到某个变量,会到children的父作用域father中查找;当father中也找不到时,会到father的父作用域中查找,直至找到window这个根作用域。这属于作用域链式查找。
函数运行产生的作用域
函数的运行是一个具有生命周期的内存地址。
函数运行时,会创建一个内存地址(产生堆内存,函数保存的就是这个堆内存的地址)。当此函数运行结束时,此内存地址又会销毁。这个地址我们无法保存,它是灵活的、活动的、有生命周期的。我们无法给这个作用域起一个变量名字,也无法保存这个作用域,JavaScript不提供这种机制。
也就是说:在作用域外面是没办法控制作用域内部的数据的;只能在作用域内部控制;而且作用域内部的代码可以控制外部的数据。这种机制就叫做闭包,闭包与作用域链和函数的运行有关系的。
函数里的变量,就在这个内存里创建;我们可以把这个内存当成一个对象;那函数里的变量就是这个内存对象的属性。
函数的定义和函数的执行是两码事(fn和fn()的区别);函数的执行与函数的定义地方无关;这个一定要理解!
闭包
作用域就是闭包;我理解的是相同的意思;只是不同人对这个机制的叫法不同;闭包是一种机制;并不是某种形式或者概念;最大的闭包就是window,我们可以把window当做一个闭包。
参考《权威指南》182页中对闭包的解释。
《权威指南》解释:函数的执行依赖于变量作用域,这个作用域是函数定义时决定的,而不是函数调用时决定的。
注意:函数对象可以通过作用域链相互关联起来;函数体内部的变量都可以保存在函数作用域内,这种特性叫闭包; 批注:这里和作用域链有关系的,和闭包没有关系的;函数的作用域是谁,和在哪运行没有关系;只和在哪儿定义有关系。
如下代码。
var a=0;
function fn(){
var a=1;
function fm(){
console.log(a);
}
return fm;
}
var testFn1=fn();//hanshu这个变量就相当于fm函数;
testFn1();//相当于fm函数运行;此时输出的是1;而不是0;虽然是在window中运行的;但是在fn中定义的;所以a找到的是fm上一级作用域fm的a;而不是window中的a;
作用域不销毁的情况|内存释放
作用域不销毁的总结:当函数内return一个引用数据类型;并且函数外面有一个变量接收这个引用数据类型;此时的作用域是不销毁的。
堆内存
对象数据类型或者函数数据类型在定义的时候首先都会开辟一个堆内存,堆内存有一个引用的地址,如果外面有变量等指到了这个地址,我们就说这个内存被占用了,就不能销毁了。
堆内存释放的问题->堆内存用来存储引用数据类型值的
[谷歌]:浏览器会每隔一段时间,看我们的堆内存是否还有其他的东西引用着,如果还在被占用着,浏览器不会进行处理;但是如果我们的堆内存已经没有任何的东西占用了,那么浏览器会把这个堆内存进行回收释放
[IE和火狐]:开辟了一个堆内存,我们有一个占用的时候浏览器记一个数(记录有多少个占用这个内存),当我们减少引用的时候,浏览器会把记数减1,当记的数字减为0的时候,浏览器会把我们的堆内存回收释放
var obj1 = {name: "张三"};
var obj2 = obj1;
//我们想要让堆内存释放/销毁,只需要把所有引用它的变量值赋值为null即可,如果当前的堆内存没有任何东西被占用了,那么浏览器会在空闲的时候把它销毁...
obj1 = null;
obj2 = null;
以上就是深入了解JS之作用域和闭包的详细内容,更多关于JavaScript 作用域和闭包的资料请关注本站其它相关文章!
什么时候该用JS构造器:与工厂模式、字面量的选择对比
2026-11-05 16:18:22
推荐几款轻量级JS照片墙开源库及对比
2026-11-05 15:56:19
JavaScript递归写法有哪些常见陷阱及避免方法
2026-11-05 14:53:38
JS数组浅拷贝与深拷贝怎么选?场景与方法总结
2026-11-05 14:41:02
p5.js 中 setup() 和 draw() 函数的执行机制详解
2026-11-05 12:32:38
