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

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

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

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

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
  • 10
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值