文章目录
- 函数提升+上下文
- 函数释放
- 拓展-垃圾回收机制
- 垃圾回收之触发应用
函数提升+上下文
-
函数提升(Hoisting)
- 概念:在JavaScript中,函数声明会被提升到当前作用域的顶部。这意味着可以在函数声明之前调用函数。例如:
sayHello(); function sayHello() { console.log("Hello!"); }
- 原理:当JavaScript引擎在执行代码时,会首先扫描整个作用域(例如全局作用域或者函数作用域),找到所有的函数声明,并将它们“提升”到作用域的顶部。但需要注意的是,函数表达式不会提升。例如:
// 这会报错,因为函数表达式不会提升 sayHi(); var sayHi = function() { console.log("Hi!"); };
- 在上面的代码中,
var sayHi
这个变量声明会被提升,但是赋值(也就是函数表达式部分)不会提升。所以在调用sayHi
的时候,它的值是undefined
,调用就会出错。
-
函数执行上下文(Execution Context)
- 概念:执行上下文是JavaScript中一个非常重要的概念,它定义了函数执行时的环境。当一个函数被调用时,就会创建一个新的执行上下文。这个执行上下文包括变量对象(VO)、作用域链和
this
的值。 - 创建阶段:
- 变量对象(VO):在执行上下文的创建阶段,会创建变量对象。对于函数执行上下文,参数会作为变量对象的属性,函数内部声明的变量也会添加到变量对象中。例如:
在function add(num1, num2) { var result; result = num1 + num2; return result; } add(3, 5);
add
函数的执行上下文中,变量对象在创建阶段会有num1
、num2
和result
这几个属性,其中num1
和num2
会被初始化为传入的参数值(3和5),result
会被初始化为undefined
。- 作用域链(Scope Chain):作用域链是由当前执行上下文的变量对象和它外层执行上下文的变量对象组成的一个链表。它用于查找变量的值。例如,在一个嵌套函数中:
在var globalVar = 10; function outer() { var outerVar = 20; function inner() { var innerVar = 30; console.log(globalVar + outerVar + innerVar); } inner(); } outer();
inner
函数的执行上下文中,作用域链首先会查找自己的变量对象中的innerVar
,然后查找outer
函数执行上下文的变量对象中的outerVar
,最后查找全局变量对象中的globalVar
。- this值:
this
的值取决于函数的调用方式。在全局环境中,this
指向window
(在浏览器环境下)。在对象方法中,this
指向调用该方法的对象。例如:
var obj = { name: "John", sayName: function() { console.log(this.name); } }; obj.sayName(); // 输出 "John"
- 执行阶段:在执行阶段,JavaScript引擎会逐行执行函数中的代码,对变量进行赋值操作,执行函数调用等。
- 概念:执行上下文是JavaScript中一个非常重要的概念,它定义了函数执行时的环境。当一个函数被调用时,就会创建一个新的执行上下文。这个执行上下文包括变量对象(VO)、作用域链和
-
函数内部使用的对象
- 函数参数作为对象:函数可以接收对象作为参数,然后在函数内部对这个对象进行操作。例如:
function updateObject(obj) { obj.property = "new value"; return obj; } var myObject = { property: "original value" }; var updatedObject = updateObject(myObject); console.log(updatedObject.property); // 输出 "new value"
- 在函数内部创建对象:可以在函数内部使用
new
关键字创建对象(如果是构造函数的话),或者使用对象字面量来创建对象。例如:
function createObject() { var newObject = { name: "New Object", description: "This is a newly created object." }; return newObject; } var createdObject = createObject(); console.log(createdObject.name); // 输出 "New Object"
-
监听事件清除释放
- 在DOM中清除事件监听器:在浏览器环境下,当给DOM元素添加事件监听器后,需要在适当的时候清除它,以避免内存泄漏等问题。如果使用
addEventListener
添加事件监听器,可以使用removeEventListener
来清除。例如:
var button = document.getElementById("myButton"); function handleClick() { console.log("Button clicked!"); } button.addEventListener("click", handleClick); // 之后想要清除事件监听器 button.removeEventListener("click", handleClick);
- 注意事项:在使用
removeEventListener
时,传递的函数必须是和添加监听器时完全相同的函数引用。如果是使用匿名函数添加的监听器,想要清除就会比较复杂。例如,下面这种情况就很难正确地清除监听器:
button.addEventListener("click", function() { console.log("Anonymous function click!"); });
- 一种解决方法是将匿名函数赋值给一个变量,然后在
removeEventListener
中使用这个变量:
var anonymousClickHandler = function() { console.log("Anonymous function click!"); }; button.addEventListener("click", anonymousClickHandler); // 清除监听器 button.removeEventListener("click", anonymousClickHandler);
- 在DOM中清除事件监听器:在浏览器环境下,当给DOM元素添加事件监听器后,需要在适当的时候清除它,以避免内存泄漏等问题。如果使用
函数释放
-
JavaScript的内存管理基础
- 在JavaScript中,内存管理主要是由垃圾回收器(Garbage Collector,GC)来自动完成的。垃圾回收器会定期扫描内存,找出那些不再被使用的对象,并释放它们所占用的内存空间。
- 当一个对象没有任何引用指向它时,就会被垃圾回收器认为是“垃圾”,从而被回收。例如,在下面的代码中:
function createObject() { let myObject = { name: "Example" }; return myObject; } let newObject = createObject(); // 此时myObject对象仍然被newObject引用,不会被回收 newObject = null; // 现在没有引用指向myObject对象了,它会在下次垃圾回收时被回收
-
函数内部局部变量的释放
- 手动设置为
null
:- 在函数内部,如果有一些比较占用内存的对象,如大型数组、复杂的对象等,可以在函数执行结束前手动将它们设置为
null
。例如:
function processData() { let largeArray = new Array(1000000).fill(0); // 对大型数组进行一些操作... // 操作完成后,手动将其设置为null largeArray = null; }
- 当把
largeArray
设置为null
后,就切断了对这个大型数组对象的引用。这样,在下一次垃圾回收时,这个数组对象占用的内存就有更大的机会被回收。不过,需要注意的是,设置为null
并不意味着立即释放内存,只是告诉垃圾回收器这个对象可以被回收了。
- 在函数内部,如果有一些比较占用内存的对象,如大型数组、复杂的对象等,可以在函数执行结束前手动将它们设置为
- 让变量超出作用域:
- 函数内部的局部变量在函数执行结束后会自动超出作用域。例如:
function limitedScope() { let localVariable = "This is a local variable"; console.log(localVariable); } limitedScope(); // 在这里,localVariable已经超出了作用域,它所占用的内存会由垃圾回收器来管理
- 当函数
limitedScope
执行完毕后,localVariable
就不再存在于当前的执行上下文中,它所占用的内存会在适当的时候被垃圾回收器回收。但是,如果这个变量所引用的对象还被其他地方(如全局变量或者闭包)引用,那么它不会被回收。
- 手动设置为
-
闭包中的内存释放
- 理解闭包对内存的影响:
- 闭包是指有权访问另一个函数内部变量的函数。当一个函数返回一个闭包时,这个闭包会保留对其外部函数的变量的引用,即使外部函数已经执行完毕。例如:
function outerFunction() { let outerVariable = "I'm from outer function"; return function innerFunction() { console.log(outerVariable); }; } let closureFunction = outerFunction(); closureFunction(); // 此时,即使outerFunction已经执行完毕, // outerVariable仍然被closureFunction引用,不会被回收
- 释放闭包中的内存:
- 要释放闭包中引用的内存,可以通过将闭包函数设置为
null
来切断引用。例如:
closureFunction = null; // 现在没有引用指向outerVariable了,它会在下次垃圾回收时被回收
- 另外,如果闭包中的变量是一个比较复杂的对象,也可以在闭包内部手动将其设置为
null
来帮助垃圾回收。
- 要释放闭包中引用的内存,可以通过将闭包函数设置为
- 理解闭包对内存的影响:
-
处理事件监听器和定时器对内存的影响
- 事件监听器:
- 在函数内部添加的事件监听器,如果没有正确移除,会导致内存泄漏。例如,在一个函数中给DOM元素添加了一个点击事件监听器:
function addEventListenerFunction() { let button = document.getElementById("myButton"); button.addEventListener("click", function() { console.log("Button clicked"); }); } addEventListenerFunction();
- 每次调用这个函数,都会添加一个新的点击事件监听器,但是这些监听器不会自动被移除。要释放内存,需要在合适的时候(如组件卸载或者不再需要监听事件时)使用
removeEventListener
来移除事件监听器。
- 定时器:
- 类似地,在函数内部设置的定时器(如
setTimeout
或setInterval
)也可能会导致内存问题。例如:
function setTimerFunction() { let timerId = setTimeout(function() { console.log("Timer expired"); }, 1000); } setTimerFunction();
- 要释放定时器占用的资源,可以使用
clearTimeout
(对于setTimeout
)或者clearInterval
(对于setInterval
)来取消定时器。例如:
function cancelTimerFunction() { let timerId = setTimeout(function() { console.log("Timer expired"); }, 1000); clearTimeout(timerId); } cancelTimerFunction();
- 类似地,在函数内部设置的定时器(如
- 事件监听器:
拓展-垃圾回收机制
-
垃圾回收的概念和重要性
- 概念:垃圾回收(Garbage Collection,GC)是一种自动内存管理机制,用于回收程序中不再使用的内存。在JavaScript等高级编程语言中,开发人员不需要手动分配和释放内存来存储对象和数据,垃圾回收器会自动处理这些事情。这大大简化了编程工作,但也需要开发人员理解其基本原理,以避免潜在的内存泄漏等问题。
- 重要性:如果没有垃圾回收机制,随着程序的运行,内存中会积累大量不再被使用的对象,导致内存泄漏。内存泄漏会逐渐耗尽系统的内存资源,最终可能使程序崩溃或者系统运行缓慢。例如,在一个长期运行的Web应用程序中,如果存在内存泄漏,用户在浏览页面时会发现页面越来越卡顿,甚至浏览器可能会因为内存耗尽而无响应。
-
引用计数垃圾回收算法
- 原理:
- 引用计数算法是一种比较简单的垃圾回收算法。它的基本思想是为每个对象维护一个引用计数。当一个对象被创建并赋值给一个变量时,它的引用计数为1;当有新的变量引用这个对象时,引用计数加1;当一个引用该对象的变量不再使用(例如,变量被重新赋值或者超出了作用域),引用计数减1。当一个对象的引用计数为0时,就表示这个对象不再被使用,可以被回收。
- 例如,在下面的代码中:
let a = { name: 'objectA' }; let b = a; // 此时对象a的引用计数为2 a = null; // 引用计数减1,变为1 b = null; // 引用计数变为0,对象a可以被垃圾回收
- 局限性:
- 循环引用问题是引用计数算法的主要缺陷。当两个或多个对象相互引用形成一个循环时,它们的引用计数永远不会为0,即使这些对象从程序的其他部分无法访问。例如:
function createCycle() { let obj1 = {}; let obj2 = {}; obj1.other = obj2; obj2.other = obj1; } createCycle();
- 在这个例子中,
obj1
和obj2
相互引用,它们的引用计数都为2(自身的引用和对方的引用)。即使createCycle
函数执行完毕,这两个对象在引用计数算法下也不会被回收,从而导致内存泄漏。
- 原理:
-
标记 - 清除垃圾回收算法
- 原理:
- 标记 - 清除算法是现代JavaScript引擎中常用的垃圾回收算法之一。它的基本过程包括两个阶段:标记阶段和清除阶段。
- 在标记阶段,垃圾回收器从一组被称为“根”(roots)的对象开始,这些根对象通常包括全局对象(在浏览器环境中是
window
对象)、当前执行栈中的变量等。然后,沿着对象之间的引用关系进行遍历,标记所有从根对象可达的对象。 - 在清除阶段,垃圾回收器遍历整个堆内存,回收那些没有被标记的对象,即将它们所占用的内存释放掉。例如,在一个简单的JavaScript程序中,假设全局变量
globalObj
引用了一个对象,这个对象又引用了其他对象,垃圾回收器会从globalObj
开始标记所有可达的对象,然后清除那些不可达的对象。
- 与引用计数算法对比:
- 标记 - 清除算法能够很好地解决循环引用的问题。在上面的循环引用示例中,虽然
obj1
和obj2
相互引用,但如果它们无法从根对象到达,那么在标记 - 清除算法下,它们会在清除阶段被回收。 - 不过,标记 - 清除算法也有一些缺点。它在标记和清除过程中需要暂停程序的执行(称为“STW”,Stop - The - World),这可能会导致短暂的性能卡顿。而且,清除后的内存空间是不连续的,可能会产生内存碎片,影响后续内存分配的效率。
- 标记 - 清除算法能够很好地解决循环引用的问题。在上面的循环引用示例中,虽然
- 原理:
-
标记 - 整理垃圾回收算法
- 原理:
- 标记 - 整理算法是在标记 - 清除算法的基础上发展而来的。它同样包括标记阶段,标记从根对象可达的对象。在清除阶段,它不是简单地回收未标记的对象,而是将所有存活的(被标记的)对象向一端移动,然后将剩余的内存空间一次性清理掉。这样就解决了标记 - 清除算法产生内存碎片的问题。
- 性能考虑:
- 标记 - 整理算法虽然解决了内存碎片问题,但在移动对象的过程中也需要消耗更多的时间和资源。因此,不同的JavaScript引擎会根据具体的应用场景和性能需求,选择合适的垃圾回收算法或者结合使用多种算法。例如,V8引擎在新生代(对象刚创建时所处的内存区域)主要使用复制算法(一种特殊的高效的内存回收算法,通过将存活对象复制到新的内存空间来实现回收),在老生代(对象经过一段时间后,从新生代晋升到的内存区域)可能会结合使用标记 - 清除和标记 - 整理算法。
- 原理:
-
分代垃圾回收(以V8引擎为例)
- 新生代和老生代的划分:
- V8引擎将内存分为新生代和老生代两个区域。新生代主要用于存储新创建的对象,通常这些对象的生命周期较短。老生代用于存储经过多次垃圾回收后仍然存活的对象,这些对象的生命周期较长。
- 新生代内存空间相对较小,一般采用更高效的复制算法进行垃圾回收。在复制算法中,新生代内存被划分为两个等大小的区域,称为From空间和To空间。当进行垃圾回收时,将From空间中存活的对象复制到To空间,然后清空From空间,最后将From空间和To空间的角色互换。
- 对象晋升:
- 当一个对象在新生代中经过多次垃圾回收后仍然存活,它会被晋升到老生代。晋升的条件可能包括对象的存活时间达到一定阈值、对象的大小超过一定限制等。老生代的垃圾回收相对复杂,因为其中的对象数量较多且生命周期较长,通常会结合使用标记 - 清除和标记 - 整理算法。
- 新生代和老生代的划分:
垃圾回收之触发应用
-
垃圾回收器运行的情况
- 内存达到一定阈值:
- 不同的JavaScript引擎(如V8引擎等)有自己的内存管理策略。当内存占用达到一定的阈值时,垃圾回收器就会自动启动。这个阈值是由JavaScript引擎内部设定的,目的是平衡性能和内存使用。例如,在浏览器环境中,V8引擎会监控堆内存(用于存储对象等数据)的使用情况。当堆内存的占用接近其容量上限时,垃圾回收器就会被触发,开始清理那些不可达的对象,释放内存空间。
- 程序空闲时段:
- 为了尽量减少垃圾回收对程序性能的影响,JavaScript引擎通常会选择在程序相对空闲的时段运行垃圾回收。比如,当事件循环(Event Loop)中的任务队列暂时为空,没有正在执行的JavaScript代码,且浏览器没有其他高优先级的任务(如页面渲染)时,垃圾回收器就可能会启动。这就像是在打扫房间,选择在房间没人活动的时候进行打扫,以避免干扰正常的活动。
- 全局变量或对象的生命周期结束(理论情况):
- 在一个理想的模型中,如果一个全局变量所引用的对象不再被需要,且这个全局变量被重新赋值或者删除,那么垃圾回收器应该回收这个对象。例如,在一个简单的脚本中:
let globalObject = { property: 'value' }; // 使用globalObject进行一些操作... globalObject = null; // 理论上,此时这个对象应该在之后被垃圾回收
- 不过,在实际情况中,由于JavaScript引擎的复杂性和各种优化策略,即使这样设置为
null
,垃圾回收器也不一定会立即运行,而是会根据上述提到的内存阈值和空闲时间等因素来决定何时运行。
- 内存达到一定阈值:
-
无法主动触发垃圾回收机制(在标准JavaScript中):
- 在标准的JavaScript规范中,没有提供直接触发垃圾回收的方法。这是因为垃圾回收是一个复杂的过程,由JavaScript引擎自动管理,目的是确保内存的高效利用和程序的性能稳定。如果允许随意触发垃圾回收,可能会导致性能问题,例如,频繁地触发垃圾回收可能会打断程序的正常执行,导致程序卡顿。
- 虽然不能直接触发,但可以通过优化代码来间接地影响垃圾回收的效果。例如,及时释放不再使用的对象引用(如将变量设置为
null
),避免创建不必要的全局变量,以及正确地管理闭包等,这些做法可以让垃圾回收器更容易识别出哪些对象是不可达的,从而更高效地回收内存。