前端JavaScript面试重难点: 闭包+内存泄漏+垃圾回收机制

Luckyfif 2024-10-03 13:33:06 阅读 51

前置知识!!!

闭包 是Javascript语言的一个重难点, 也是它的特色, 很多高级应用都要依靠闭包来实现。在各种专业文献上学习"闭包"的时候, 就一个感觉 – “抽象” !

特别是学习内存泄漏的时候, 没想明白为什么使用闭包的时候 不及时清除函数中的元素会导致内存泄漏, 直到我的第一次面试结束之后, 回顾的时候把这几个知识串联了起来, 一切都明朗了。

这里先给大家看个 解除闭包的引用, 释放数据内存 的例子(看不懂没关系, 后面才是正文开始)

<code>function createClosure() {

let externalData = { /* 一些数据 */ };

return function innerFunction() {

// 使用externalData做一些事情

};

}

let myClosure = createClosure();

// ... 使用myClosure做一些事情

// 当你不再需要myClosure时

myClosure = null; // 移除对闭包的引用

// 注意:如果externalData仍然被其他闭包或外部代码引用,则它不会被回收

// 你需要确保没有其他引用指向它,或者它本身也应该被设置为null

这里还要补充一点 垃圾回收机制 的知识, 方便后续理解:

前端中的垃圾回收机制(Garbage Collection, 简称GC)是一种 自动内存管理机制 ,它负责找出并释放那些不再被使用的内存空间,以防止内存泄漏,从而优化程序的性能。在JavaScript等前端技术中,垃圾回收机制扮演着至关重要的角色。

好了, 前面看不懂没关系, 现在正文开始:

一、变量的作用域:

想要理解闭包, 首先必须理解 Javascript 特殊的变量作用域。

变量的作用域分为两种: 全局变量 和 局部变量。

函数内部可以直接读取全局变量, 而函数外部自然无法读取函数内的局部变量

var n=999;

function f1(){

alert(n);

}

f1(); // 999

function f1(){

var n=999;

}

alert(n); // error

这里有一个地方需要注意,函数内部声明变量的时候,一定要使用 var 命令。如果不用的话,你实际上声明了一个全局变量!

function f1(){

n=999;

}

f1();

alert(n); // 999

二、如何从外部读取局部变量?

由于业务场景的不同,我们有时候需要得到函数内的局部变量, 由于该局部变量需要收到保护, 不能让外部环境直接更改。但是,前面已经说过了,正常情况下,这是办不到的,只有通过变通方法才能实现。那就是在函数的内部,再定义一个函数。 如:

function f1(){

var n=999;

function f2(){

alert(n); // 999

}

}

在上面的代码中,函数 f2 就被包括在函数 f1 内部,这时 f1 内部的所有局部变量,对 f2 都是可见的。但是反过来就不行,f2 内部的局部变量,对 f1 就是不可见的。这就是 Javascript 语言特有的"链式作用域"结构(chain scope),子对象会一级一级地向上寻找所有父对象的变量。所以,父对象的所有变量,对子对象都是可见的,反之则不成立。

既然f2可以读取f1中的局部变量,那么只要把f2作为返回值,我们就可以在f1外部读取它的内部变量了!

function f1(){

var n=999;

function f2(){

alert(n);

}

return f2;

}

var result=f1();

result(); // 999

三、闭包的概念

上一节代码中的f2函数, 就是闭包

各种专业文献上的"闭包"(closure)定义非常抽象,很难看懂。我的理解总结成一句话就是:

闭包就是在全局环境中, 通过新建对象, 调用函数的方式 已达到读取父对象变量的目的, 而这个调用的函数就是闭包。

由于在Javascript语言中,只有函数内部的子函数才能读取局部变量,因此可以把闭包简单理解成"定义在一个函数内部的函数"。

四、闭包的用途

可能这个时候我们就会想到, f1 函数里面的不是局部变量吗? 为什么他在全局环境中, 还能够一直存在?

要彻底理解, 很关键的点的是在 "var result=f1(); "这里!!!

这里的 f1() return返回的是 f2 函数! 而函数是引用数据类型, 内存是放在堆里面的, 而 result 其实只是一个地址, 指向 f2 函数内存存放的地址, 这个时候 f2 函数就成了全局变量 result 的依赖性, f2 函数就必须保存在内存当中, 不被 垃圾回收 清理掉。

而 f1 函数是 f2函数的父对象, f2 存在, f1 就也要存在, 所以 f1 函数的内部变量就不会被 垃圾回收 清理掉!!

所以说, 刚才 “解除闭包的引用, 释放数据内存” 例子中的 "myClosure = null; // 移除对闭包的引用 "就是用来释放 f1 函数的内部变量。提一个极端的例子就可以理解为什么内存泄漏需要被重视:

在一个网页应用中,你可能有一个循环,该循环不断创建新的闭包,而这些闭包都引用了外部作用域中的大型数据结构(比如大型数组或对象)。如果这些闭包被不当地存储(例如,作为全局变量或DOM元素的事件处理器),那么它们将阻止垃圾回收器回收那些大型数据结构的内存,从而导致内存泄漏。

五、使用闭包的注意点

1)由于闭包会使得函数中的变量都被保存在内存中,内存消耗很大,所以不能滥用闭包,否则会造成网页的性能问题,在IE中可能导致内存泄露。解决方法是,在退出函数之前,将不使用的局部变量全部删除。

2)闭包会在父函数外部,改变父函数内部变量的值。所以,如果你把父函数当作对象(object)使用,把闭包当作它的公用方法(Public Method),把内部变量当作它的私有属性(private value),这时一定要小心,不要随便改变父函数内部变量的值。

最后, 这里放我面试中考到的一道闭包代码题给大家练练:

/**

* 不能改动原代码

* f 函数里面的console.log只能执行一次

* 第二次调用开始就只能返回undefined

*/

function once(fn) {

}

var f = function() {

console.log('被执行了')

}

var onceF = once(f)

onceF() // 被执行了

onceF() // undefined



声明

本文内容仅代表作者观点,或转载于其他网站,本站不以此文作为商业用途
如有涉及侵权,请联系本站进行删除
转载本站原创文章,请注明来源及作者。