整合营销服务商

电脑端+手机端+微信端=数据同步管理

免费咨询热线:

JavaScript深入 闭包

、理论上的闭包

从技术理论的角度讲,所有的JavaScript函数都是闭包。

闭包定义:闭包是指那些能够访问自由变量的函数。

自由变量:是指在函数中使用的,但既不是函数参数也不是函数的局部变量的变量。

由此,可以看出闭包共有两部分组成:闭包 = 函数 + 函数能够访问的自由变量

举个例子:

var a = 1;

function foo() {
    console.log(a);
}
foo();

foo 函数可以访问变量 a,但是 a 既不是 foo 函数的局部变量,也不是 foo 函数的参数,所以 a 就是自由变量。 那么,函数 foo + foo 函数访问的自由变量 a 就构成了一个闭包……

从技术理论的角度讲,所有的JavaScript函数都是闭包。

显然上面讲述的并不是我们实践中用的闭包,我们再接着往下看。


二、实践上的闭包

上面是理论上的闭包,其实还有一个实践角度上的闭包。

先举个栗子:

function checkscope(){
    var scope = "local scope";
    function f(){
        return scope;
    }
    return f;
}

var foo = checkscope();
console.log(foo());  					//'local scope'

我们先分析一下这段代码中执行上下文栈和执行上下文的变化情况。 注:如果看不懂以下的执行过程,建议先阅读《JavaScript深入 执行上下文(五):整个过程》。

这里直接给出简要的执行过程:

  • (1)进入全局代码,创建全局执行上下文,全局执行上下文压入执行上下文栈
  • (2)全局执行上下文初始化
  • (3)执行 checkscope 函数,创建 checkscope 函数执行上下文,checkscope 执行上下文被压入执行上下文栈
  • (4)checkscope 执行上下文初始化,创建活动对象、作用域链、this等
  • (5)checkscope 函数执行完毕,checkscope 执行上下文从执行上下文栈中弹出
  • (6)执行 f 函数,创建 f 函数执行上下文,f 执行上下文被压入执行上下文栈
  • (7)f 执行上下文初始化,创建变量对象、作用域链、this等
  • (8)f 函数执行完毕,f 函数上下文从执行上下文栈中弹出

了解到这个过程,我们应该思考一个问题,那就是: 当 f 函数执行的时候,checkscope 函数上下文已经被销毁了啊(即从执行上下文栈中被弹出),怎么还会读取到 checkscope 作用域下的 scope 值呢?

1. [[scope]]属性

每个函数都有一个内部属性[[scope]](即作用域链) 现在我们根据上面谈的程序具体执行过程,来看下f函数的内部属性[[scope]],即f 执行上下文维护的作用域链:

fContext = {
    Scope: [AO, checkscopeContext.AO, globalContext.VO],
}

没错,, 就是因为上面这个作用域链,f 函数依然可以读取到 checkscopeContext.AO 的值(即 变量scope)。 说明当 f 函数引用了 checkscopeContext.AO 中的值的时候,即使 checkscopeContext 被销毁了,但是 JavaScript 依然会让 checkscopeContext.AO 活在内存中,f 函数依然可以通过 f 函数的作用域链找到它。从而实现了闭包这个概念。

那问题又来了:checkscopeContext都被销毁了,为什么checkscopeContext.AO 还能活在内存中呢?

如果你知道 JavaScript的垃圾回收机理,你就懂了。


2. JavaScript垃圾回收

JavaScript垃圾回收的机理:垃圾收集器 会跟踪找出不再使用的变量,然后 每隔固定时间间隔 释放掉其内存。

再看上面的例子:

  • ① checkscope函数没被谁引用或使用着,说明它执行完后会被垃圾收集器销毁;checkscopeContext也没被谁引用或使用着,所以在checkscope函数执行完毕后,它也会被一同销毁。
  • ② checkscope返回的f函数被foo所引用着,说明我们还会使用f函数,所以f函数不被销毁。且f函数执行上下文的[[scope]]属性(即 作用域链)还引用着 checkscopeContext.AO,说明我们还会使用 checkscopeContext.AO,所以不被销毁。

综述: 每个函数都有一个内部属性[[scope]](即作用域链),而正因为f函数没被销毁,所以该属性也被保留着;又因为作用域链的本质是一个指向 变量对象/活动对象 的指针列表(它只是引用 不包含实际对象),所以作用域链上的这些对象不会被垃圾收集器销毁,所以我们可以通过f函数的作用域链找到 它的父级乃至父父级的变量。

让我们再看一遍实践角度上闭包的定义:

  • (1)即使创建它的上下文已经销毁,它仍然存在(比如,内部函数从父函数中返回)
  • (2)在代码中引用了自由变量

再总结一遍~

ECMAScript中,闭包指的是:

  • 从理论角度:闭包指所有的函数。因为它们都在创建的时候就将上层上下文的数据保存起来了。哪怕是简单的全局变量也是如此,因为函数中访问全局变量就相当于是在访问自由变量,这个时候使用最外层的作用域。
  • 从实践角度:以下函数才算是闭包:即使创建它的上下文已经销毁,它仍然存在(比如,内部函数从父函数中返回)在代码中引用了自由变量


三、必刷题

例1:

var data = [];

for (var i = 0; i < 3; i++) {
  data[i] = function () {
    console.log(i);
  };
}

data[0]();
data[1]();
data[2]();

输出: 3 3 3

让我们分析一下原因: 当执行到 data[0] 函数之前,此时全局上下文的 VO 为:

globalContext = {
   VO: { data: [...],   i: 3 }
}

当执行 data[0] 函数的时候,data[0] 函数的作用域链为:

data[0]Context = {
   Scope: [AO, globalContext.VO]
}

data[0]Context 的 AO 并没有 i 值,所以会从 globalContext.VO 中查找,i 为 3,所以打印的结果就是 3。

data[1] 和 data[2] 是一样的道理。

所以让我们改成闭包看看:

var data = [];

for (var i = 0; i < 3; i++) {
  data[i] = (function (i) {
     return function(){
        console.log(i);
     }
  })(i);
}

data[0]();
data[1]();
data[2]();

当执行到 data[0] 函数之前,此时全局上下文的 VO 仍为:

globalContext = {
   VO: { data: [...],   i: 3 }
}

跟没改之前一模一样。

但当执行 data[0] 函数(即 return的函数)的时候,其作用域链为:

data[0]Context = {
   Scope: [AO, 匿名函数Context.AO,globalContext.VO]
}

匿名函数执行上下文的 AO 为:

匿名函数Context = {
  AO: {
    arguments: {
      0: 0,
      length: 1
    },
    i: 0
  }
}

data[0]Context 的 AO 并没有 i 值,所以会沿着作用域链从匿名函数Context.AO 中查找,这时候就会找 i 为 0,找到了就不会往 globalContext.VO 中查找了,即使 globalContext.VO 也有 i 的值(值为3),所以打印的结果就是 0。 data[1] 和 data[2] 是一样的道理。


其实我们要想输出0 1 2,可以直接将上面代码改为:

var data = [];

for (let i = 0; i < 3; i++) {
  data[i] = function () {
    console.log(i);
  };
}

data[0]();
data[1]();
data[2]();

ES6中加入了块级作用域。 我们每创建一个函数会创建一个作用域。同理上面代码,我们用let声明i后,相当于每进行一次for循环就创建了一个(块级)作用域。每个作用域的AO都保存了一个不同的i值。

执行 data[0] 函数时,由于我们要打印i值,所以会沿着作用域链回溯寻找:首先会在当前匿名函数的作用域寻找i值,发现没有;再到块级作用域中找,发现有且为0,所以打印0;之后同理,输出:0 1 2


例2:

var globals = 0;
function test(parameter){
   var outerVal = 0;
   var outerVal2 = 0;
   console.log('outerVal2:',++outerVal2);
   return function(){
      var innerVal = 0;
      console.log('globals:',++globals);
      console.log('outerVal:',++outerVal);
      console.log('innerVal:',++innerVal);
      console.log('parameter:',++parameter);
   }
}

var a = test(0);
a();
a();

输出:

(1) 在执行到 a() 函数(即return的匿名函数)之前,此时全局上下文的 VO 为:

globalContext = {
   VO: { 
     globals: 0, 
     test:ƒ test(parameter),
     a:ƒ() 
   }
}

而test函数执行上下文的 AO 为:

testContext = {
    AO: {
        arguments: {
            0: 0,
            length: 1
        },
        parameter: 0,
        outerVal: 0,
        outerVal2: 0,
    }
}

(2) 当执行 a() 函数的时候,a() 函数的作用域链为:

aContext = {
    Scope: [AO, testContext.AO, globalContext.VO]
}

用上面的理论,解释这个例子,输出同理。

这里需要注意的是:第一次执行a()时,创建了a函数的执行上下文(aContext),执行完后销毁;第二次执行a()时,再次创建aContext,然后再次销毁。虽然aContext两次被销毁,但a函数的[[scope]]属性一直都留在内存里。


下面我们看一个误用闭包的例子~

例3:

function test(){
    var outerVal = 0;
    return function(){
        console.log(++outerVal);
    }
}
test()();
test()();

输出:1 1

为什么不是输出 1 2 呢? 首先你要知道,test()即返回的匿名函数,test()()即执行匿名函数。

再根据 JS垃圾回收的机理: 因为test函数返回的匿名函数没有被其他变量引用或使用着,说明我们不再继续使用该匿名函数,所以垃圾收集器会将其销毁。所以每次执行test()()后,变量outerVal都会被销毁。


为了方便闭包的分析,我们可以认为每个函数的[[scope]]属性(即作用域链),一直活在内存里,无论这个函数是否执行完毕。

些人会说语言学到最后不都差不多吗?其实可以这样讲,也可以不这样讲。虽然每种语言的表达能力大部分是重合的,只是语法表现形式不一样,但是由于历史发展的原因,每种语言形成了自己的支撑环境,所以都有其主要的适用范围。

C、C++、Python和Java四种是通用编程语言,JavaScript和PHP算是Web环境的专用编程语言。

C(令人崇拜的语言)

由于其底层操作特性和历史的积累,在嵌入式领域是当之无愧的王者。

C++(神秘莫测的语言)

是一种支持最广泛编程范式的复杂语言,在高级语言当中,处理运行速度是最快的,大部分的游戏软件,系统都是由C++来编写的。

Python(高端大气上档次的语言)

作为一种灵活的轻便的通用型脚本语言,使用范围比较广,从应用软件到Web开发都有它的身影,由于其解释语言的特点,比较适合轻量级或原型开发;

Java(有噱头的语言)

Java由于其跨平台可移植性,在Web开发领域大放异彩,特别是在企业级Web开发,同时由于Android系统采用Java来开发应用程序,所以也随着Android的发展而应用越发广泛;

JavaScript(有潜力的语言)

JavaScript语言由于其是浏览器内置的脚本语言,是Web前端开发的主流,近年来由于google的V8引擎开源,出现了Node.js之类JavaScript后台开发框架,把JavaScript的应用领域扩展到了Web后台。

PHP(低调奢华的语言)

独特的语法混合了C、Java、Perl以及PHP自创的语法。它可以比CGI或者Perl更快速地执行动态网页;还可以执行编译后代码,编译可以达到加密和优化代码运行,使代码运行更快。

理清不同语言间主要语法特性的差异,才能更好的在合适的领域或场景下去应用合适的编程语言,以满足我们所面对的需求。这六种语言都是从C语言发展而来,所以它们的语法都比较像C语言,下面我就主要语法特性对各个语言做一个对比。

1、常量定义

C:#define TEST 0

C++:#define TEST 0

或者

const test = 0;

Python:test = 0

C#:不支持

PHP:define('test', 1);

Java:final int test = 0;

分析:JavaScript不支持常量,C、C++都用特有的预定义宏,PHP用特殊的define语法,其它的都用定义不变变量的方式。

2、变量定义

C:int test = 0;

C++:int test = 0;

Python:test = 0

JavaScript:val test = 0;

PHP:$test = 0;

Java:int test = 0;

分析:这个最基本的都支持了。

3、函数定义

C:int test(int param){}

C++:int test(int param){}

Python:def test(param):

JavaScript:function test(param){}

PHP:function test($param){}

Java:public class test{

public int test(int param){} }

分析:这个也是最基本的了,只是Java比较特殊,不支持定义类之外的函数。

4、类定义(含继承)

C:不支持

C++:class test2: public test1{}

Python:class test2(test1):

JavaScript:function test2(){}

test2.prototype =inherit(test1.prototype){}

PHP:class test2 extend test1{}

Java:class test2 extends test1{}

分析:C由于是传统面向过程的语言不支持类,其他的都支持了,只是JavaScript的类模型比较特殊,把函数作为类来使用。

5、对象定义

C:不支持

C++:test2 obj = new test2();

Python:obj = test2()

JavaScript:var obj = new test2();

PHP:$obj = new test2();

Java:test2 obj = new test2();

分析:除了C外其它语言都是通过new一个对象。

6、数组定义

C:int a[] = {1, 2, 3};

C++:int a[] = {1, 2, 3};

Python:a = [1, 2, 3]

JavaScript:var a = [1, 2, 3];

PHP:$a = array("1", "2", "3");

Java:int a[] = {1, 2, 3};

分析:数组是语言的基本特性,都支持了,只是PHP通过类似函数调用的语法来完成。

7、条件语句

C:if (test > 0){}

else if (test < 0){}

else{}

C++:if (test > 0){}

else if (test < 0){}

else{}

Python:if test > 0:

elif test < 0:

else:

JavaScript:if (test > 0){}

else if (test < 0){}

else{}

PHP:if ($test > 0){}

elseif ($test < 0){}

else{}

Java:if (test > 0){}

else if (test < 0){}

else{}

分析:这是最基本的语句,都支持了。

8、循环语句

C:for (idx=0; idx<num; idx++){}

C++:for (idx=0; idx<num; idx++){}

Python:for idx in range(1,10):

JavaScript:for (var idx=0; idx<num; idx++){}

PHP:for ($idx=0; $idx<$num; $idx++){}

Java:for (idx=0; idx<num; idx++){}

分析:这个也是基本的语句,都支持了。

9、foreach语句

C:不支持

C++:不支持

Python:for i in a:

或者

for key in d:

d[key]

JavaScript:for(i in a){}

PHP:foreach($a as $i){}

Java:for(int i : a){}

分析:foreach算是循环语句的一个变种,在操作顺序容器的时候非常有用,可以看到C和C++不支持,其它的都语言内置支持了。

10、打印语句

C:printf("test: %d", val);

C++:cout<<"test: "<<val<<endl;

Python:print "test: "+val

JavaScript:不支持

PHP:echo "test: $val";

Java:System.out.println("test :"+val);

分析:打印算是语言所运行环境的支持库功能,除了JavaScript外都支持了,因为JavaScript主要使用来操控DOM树的,没有自己的输出窗口所以也没必要支持。

11、字符串定义

C:char test[] = {"helloworld"};

C++:String test = "helloworld";

Python:test = "helloworld"

JavaScript:var test = "helloworld";

PHP:$test = "helloworld";

Java:String test = "helloworld";

分析:这个都支持了,其中C++、Java都是用标准库来现实的。

12、字符串串接

C:test = strcat(test1, test2);

C++:test = test1 + test2;(STL库)

Python:test = test1 + test2

JavaScript:var test = test1 + test2;

PHP:$test = $test1 .= $test2;

Java:test = test1 + test2;

分析:很有用的功能,除了C是用标准库函数来实现,其它都是语言内置支持了。

13、字符串分割

C:不支持

C++:test.substr(3, 8);

Python:test[3:8]

JavaScript:test.slice(3, 5);

PHP:substr($test, 3, 5);

Java:test.substring(3, 8);

分析:常用的功能,C不支持,Python是语言内置支持,其他的都依靠库来完成。

14、字符串正则表达式

C:不支持

C++:不支持

Python:test.replace("test1", "test2")

JavaScript:test.replace(/test1/gi, "test2");

PHP:str_replace($test, "test1", "test2");

Java:test.replaceAll("test1", "test2");

分析:常用的功能,可惜C、C++不支持,其他都有标准库来支持。

15、内置容器类型

C:数组

C++:数组

顺序容器 Vector

关联容器 Pair MapSet

Python:列表/元组

字典

JavaScript:数组

对象

PHP:数组(含关联数组)

Java:数组

序列 Collection

映射表 Map

分析:C最简单只支持数组,其他都支持容器,不过主要还是顺序容器和关联容器两大类。

16、注释方式

C:/* */

C++://

Python:#

JavaScript:/* */

//

PHP:/* */

//

#

Java:/* */

//

分析:大概就/**/、//、#三种方式,各自支持情况不一。

17、多线程支持

C:支持

C++:支持

Python:支持

JavaScript:不支持

PHP:不支持

Java:支持

分析:四种通用编程语言都支持了,两种专用编程语言都不支持。

18、socket支持

C:支持

C++:支持

Python:支持

JavaScript:不支持

PHP:支持

Java:支持

分析:除了JavaScript以外都支持,这也是JavaScript的应用领域限制所决定的。

19、垃圾回收机制

C:不支持

C++:不支持

Python:支持

JavaScript:支持

PHP:支持

Java:支持

分析:这是现代语言的重要机制,C和C++不支持,其他的都支持了。

20、引入其他文件中的函数

C:export int test();

C++:export int test();

Python:from test import *

JavaScript:<script language='javascript' src="test.js"charset="utf-8"></script>

PHP:require_once('test.php');

或者

include_once('test.php');

Java:import java.util.test.*;

分析:都支持,C和C++用export,Python和Java用import,JavaScript依靠HTML脚本,PHP用自己的函数调用。

21、将字符串作为指令执行

C:不支持

C++:不支持

Python:eval("port=5060")

JavaScript:eval("port=5060;");

PHP:eval("port=5060;");

Java:Porcess proc = new ProcessBuilder(“test”).start();

分析:很有用的一个动态语言特性,C和C++都不支持,Java要类库来支持,其它的语言内置eval关键字.

C/C++资料分享:

需要的小伙伴们可以【点击下方】链接哦~

递归(英语:Recursion)

程序调用自身的编程技巧称为递归( recursion)。递归作为一种算法程序设计语言中广泛应用。 一个过程或函数在其定义或说明中有直接或间接调用自身的一种方法,它通常把一个大型复杂的问题层层转化为一个与原问题相似的规模较小的问题来求解,递归策略只需少量的程序就可描述出解题过程所需要的多次重复计算,大大地减少了程序的代码量。递归的能力在于用有限的语句来定义对象的无限集合。一般来说,递归需要有边界条件、递归前进段和递归返回段。当边界条件不满足时,递归前进;当边界条件满足时,递归返回。递归返回阶段。当边界条件不满足时,递归前进;当边界条件满足时,递归返回

下面实现一个函数 pow(x, n),它可以计算 xn 次方

使用迭代的方式,如下:

function pow(x, n) {
  let result = 1;

  // 再循环中,用 x 乘以 result n 次
  for (let i = 0; i < n; i++) {
    result *= x;
  }
  return result;
}

使用递归的方式,如下:

function pow(x, n) {
  if (n == 1) {
    return x;
  } else {
    return x * pow(x, n - 1);
  }
}

pow(x, n) 被调用时,执行分为两个分支:

             if n==1  = x
             /
pow(x, n) =
             \
              else     = x * pow(x, n - 1)

也就是说pow 递归地调用自身 直到 n == 1

为了计算 pow(2, 4),递归变体经过了下面几个步骤:

  1. pow(2, 4) = 2 * pow(2, 3)
  2. pow(2, 3) = 2 * pow(2, 2)
  3. pow(2, 2) = 2 * pow(2, 1)
  4. pow(2, 1) = 2

因此,递归将函数调用简化为一个更简单的函数调用,然后再将其简化为一个更简单的函数,以此类推,直到结果

尾递归

尾递归,即在函数尾位置调用自身(或是一个尾调用本身的其他函数等等)。尾递归也是递归的一种特殊情形。尾递归是一种特殊的尾调用,即在尾部直接调用自身的递归函数

尾递归在普通尾调用的基础上,多出了2个特征:

  • 在尾部调用的是函数自身
  • 可通过优化,使得计算仅占用常量栈空间

在递归调用的过程当中系统为每一层的返回点、局部量等开辟了栈来存储,递归次数过多容易造成栈溢出

这时候,我们就可以使用尾递归,即一个函数中所有递归形式的调用都出现在函数的末尾,对于尾递归来说,由于只存在一个调用记录,所以永远不会发生"栈溢出"错误

实现一下阶乘,如果用普通的递归,如下:

function factorial(n) {
  if (n === 1) return 1;
  return n * factorial(n - 1);
}

factorial(5) // 120

如果n等于5,这个方法要执行5次,才返回最终的计算表达式,这样每次都要保存这个方法,就容易造成栈溢出,复杂度为O(n)

如果我们使用尾递归,则如下:

function factorial(n, total) {
  if (n === 1) return total;
  return factorial(n - 1, n * total);
}

factorial(5, 1) // 120

可以看到,每一次返回的就是一个新的函数,不带上一个函数的参数,也就不需要储存上一个函数了。尾递归只需要保存一个调用栈,复杂度 O(1)

应用场景

数组求和

function sumArray(arr, total) {
    if(arr.length === 1) {
        return total
    }
    return sum(arr, total + arr.pop())
}

使用尾递归优化求斐波那契数列

function factorial2 (n, start = 1, total = 1) {
    if(n <= 2){
        return total
    }
    return factorial2 (n -1, total, total + start)
}

数组扁平化

let a = [1,2,3, [1,2,3, [1,2,3]]]
// 变成
let a = [1,2,3,1,2,3,1,2,3]
// 具体实现
function flat(arr = [], result = []) {
    arr.forEach(v => {
        if(Array.isArray(v)) {
            result = result.concat(flat(v, []))
        }else {
            result.push(v)
        }
    })
    return result
}

数组对象格式化

let obj = {
    a: '1',
    b: {
        c: '2',
        D: {
            E: '3'
        }
    }
}
// 转化为如下:
let obj = {
    a: '1',
    b: {
        c: '2',
        d: {
            e: '3'
        }
    }
}

// 代码实现
function keysLower(obj) {
    let reg = new RegExp("([A-Z]+)", "g");
    for (let key in obj) {
        if (obj.hasOwnProperty(key)) {
            let temp = obj[key];
            if (reg.test(key.toString())) {
                // 将修改后的属性名重新赋值给temp,并在对象obj内添加一个转换后的属性
                temp = obj[key.replace(reg, function (result) {
                    return result.toLowerCase()
                })] = obj[key];
                // 将之前大写的键属性删除
                delete obj[key];
            }
            // 如果属性是对象或者数组,重新执行函数
            if (typeof temp === 'object' || Object.prototype.toString.call(temp) === '[object Array]') {
                keysLower(temp);
            }
        }
    }
    return obj;
};
//案例1:求和,1-100
    function sun(n){
        if(n==1) return 1
        
    }
//案例2:递归方法1,1,2,3,5,8,13,21,34,55,89…求第 n 项
   function fib(n) {
        if (n === 1 || n === 2) return 1
        return fib(n - 2) + fib(n - 1)
    }
    console.log(fib(3)) 
//案例3:深拷贝
    function clone(o) {
        var temp = {}
        for (var key in o) {
            if (typeof o[key] == 'object') {
                temp[key] = clone(o[key])
            } else {
                temp[key] = o[key]
            }
        }
        return temp
    }
//案例4:递归组件
//组件在它的模板内可以递归的调用自己,只要给组件设置 name 组件就可以了。
//不过需要注意的是,必须给一个条件来限制数量,否则会抛出错误: max stack size exceeded
//组件递归用来开发一些具体有未知层级关系的独立组件。比如:联级选择器和树形控件
    function clone(o) {
        var temp = {}
        for (var key in o) {
            if (typeof o[key] == 'object') {
                temp[key] = clone(o[key])
            } else {
                temp[key] = o[key]
            }
        }
        return temp
    }
//hash模式和history模式
//这里的hash指的就是url后的 # 号以及后面的支付,比如说:www.baidu.com/#hashhash,其中#hashhash 就是我们期望的 hash值,
//由于hash值的变化不会导致浏览器向服务器发送请求,而且在hash的改变会触发hashchange事件,浏览器的前进后退也能对其进行控制,所以在H5的history模式出现之前,基本都是使用hash模式来实现前端路由,代码如下
    window.addEventListener('hashchange',function(event){
        let newUrl=event.newURL;//hash改变后的新的URL
        let loadUrl=event.oldURL;//hash改变前的URL
    })

//history模式,以下是history的相关API:
    history.go(-1);       // 后退一页
    history.go(2);        // 前进两页
    history.forward();     // 前进一页
    history.back();      // 后退一页
    //规范新增
    history.pushState();         // 添加新的状态到历史状态栈
    history.replaceState();      // 用新的状态代替当前状态
    history.state                // 返回当前状态对象

内存泄漏是什么

内存泄漏(Memory leak)是在计算机科学中,由于疏忽或错误造成程序未能释放已经不再使用的内存

并非指内存在物理上的消失,而是应用程序分配某段内存后,由于设计错误,导致在释放该段内存之前就失去了对该段内存的控制,从而造成了内存的浪费

程序的运行需要内存。只要程序提出要求,操作系统或者运行时就必须供给内存

对于持续运行的服务进程,必须及时释放不再用到的内存。否则,内存占用越来越高,轻则影响系统性能,重则导致进程崩溃

C语言中,因为是手动管理内存,内存泄露是经常出现的事情。

char * buffer;
buffer = (char*) malloc(42);

// Do something with buffer

free(buffer);

上面是 C 语言代码,malloc方法用来申请内存,使用完毕之后,必须自己用free方法释放内存。

这很麻烦,所以大多数语言提供自动内存管理,减轻程序员的负担,这被称为"垃圾回收机制"

内存生命周期:


内存也是有生命周期的,一般可以按顺序分为三个周期:

  • 分配期(分配所需要的内存)
  • 使用期(使用分配到的内存(读、写))
  • 释放期(不需要时将其释放和归还)

内存分配 -> 内存使用 -> 内存释放。

内存管理机制:

JavaScript是在创建变量(对象,字符串等)时自动进行了分配内存,并且在不使用它们时“自动”释放。 释放的过程称为垃圾回收。

JavaScript 内存管理机制和内存的生命周期是一一对应的。首先需要分配内存,然后使用内存,最后释放内存。

其中 JavaScript 语言不需要程序员手动分配内存,绝大部分情况下也不需要手动释放内存,对 JavaScript 程序员来说通常就是使用内存(即使用变量、函数、对象等)。


垃圾回收机制

Javascript 具有自动垃圾回收机制(GC:Garbage Collecation),也就是说,执行环境会负责管理代码执行过程中使用的内存

原理:垃圾收集器会定期(周期性)找出那些不在继续使用的变量,然后释放其内存

通常情况下有两种实现方式:

  • 标记清除
  • 引用计数

标记清除

JavaScript最常用的垃圾收回机制

当变量进入执行环境是,就标记这个变量为“进入环境“。进入环境的变量所占用的内存就不能释放,当变量离开环境时,则将其标记为“离开环境“

垃圾回收程序运行的时候,会标记内存中存储的所有变量。然后,它会将所有在上下文中的变量,以及被在上下文中的变量引用的变量的标记去掉

在此之后再被加上标记的变量就是待删除的了,原因是任何在上下文中的变量都访问不到它们了

随后垃圾回收程序做一次内存清理,销毁带标记的所有值并收回它们的内存

举个例子:

var m = 0,n = 19 // 把 m,n,add() 标记为进入环境。
add(m, n) // 把 a, b, c标记为进入环境。
console.log(n) // a,b,c标记为离开环境,等待垃圾回收。
function add(a, b) {
  a++
  var c = a + b
  return c
}

引用计数

语言引擎有一张"引用表",保存了内存里面所有的资源(通常是各种值)的引用次数。如果一个值的引用次数是0,就表示这个值不再用到了,因此可以将这块内存释放

如果一个值不再需要了,引用数却不为0,垃圾回收机制无法释放这块内存,从而导致内存泄漏

const arr = [1, 2, 3, 4];
console.log('hello world');

上面代码中,数组[1, 2, 3, 4]是一个值,会占用内存。变量arr是仅有的对这个值的引用,因此引用次数为1。尽管后面的代码没有用到arr,它还是会持续占用内存

如果需要这块内存被垃圾回收机制释放,只需要设置如下:

arr = null

通过设置arrnull,就解除了对数组[1,2,3,4]的引用,引用次数变为 0,就被垃圾回收了

小结

有了垃圾回收机制,不代表不用关注内存泄露。那些很占空间的值,一旦不再用到,需要检查是否还存在对它们的引用。如果是的话,就必须手动解除引用

常见内存泄露情况

意外的全局变量

一个未声明变量的引用会在全局对象中创建一个新的变量。在浏览器的环境下,全局对象就是window,也就是说:

function foo(arg) {
    bar = "this is a hidden global variable";
}

另一种意外的全局变量可能由 this 创建:

function foo() {
    this.variable = "potential accidental global";
}
// foo 调用自己,this 指向了全局对象(window)
foo();

上述使用严格模式,可以避免意外的全局变量

闭包引起的内存泄漏

闭包可以使变量常驻内存,但如果使用不当就会在成内存泄漏

var theThing = null;
var replaceThing = function () {
  var originalThing = theThing;
  var unused = function () {
    if (originalThing)
      console.log("hi");
  };
  theThing = {
    longStr: new Array(1000000).join('*'),
    someMethod: function () {
      console.log(someMessage);
    }
  };
};
setInterval(replaceThing, 1000);

上面代码中,每次调用 replaceThing 时,theThing 都会得到新的包含一个大数组和新的闭包(someMethod)的对象。

同时,没有用到的那个变量持有一个引用了 originalThingreplaceThing 调用之前的 theThing)闭包。

关键的问题是每当在同一个父作用域下创建闭包作用域的时候,这个作用域是被共享的。在这种情况下,someMethod 的闭包作用域和 unused 的作用域是共享的。

unused 持有一个 originalThing 的引用。尽管 unused 从来没有被使用过,someMethod 可以在 theThing 之外被访问。

而且 someMethodunused 共享了闭包作用域,即便 unused 从来都没有被使用过,它对 originalThing 的引用还是强制它保持活跃状态(阻止它被回收)。

当这段代码重复运行时,将可以观察到内存消耗稳定地上涨,并且不会因为 GC 的存在而下降。

本质上来讲,创建了一个闭包链表(根节点是 theThing 形式的变量),而且每个闭包作用域都持有一个对大数组的间接引用,这导致了一个巨大的内存泄露。

定时器也常会造成内存泄露

var someResource = getData();
setInterval(function() {
    var node = document.getElementById('Node');
    if(node) {
        // 处理 node 和 someResource
        node.innerHTML = JSON.stringify(someResource));
    }
}, 1000);

如果id为Node的元素从DOM中移除,该定时器仍会存在,同时,因为回调函数中包含对someResource的引用,定时器外面的someResource也不会被释放

包括我们之前所说的闭包,维持函数内局部变量,使其得不到释放

function bindEvent() {
  var obj = document.createElement('XXX');
  var unused = function () {
    console.log(obj, '闭包内引用obj obj不会被释放');
  };
  obj = null; // 解决方法
}

没有清理对DOM元素的引用同样造成内存泄露

const refA = document.getElementById('refA');
document.body.removeChild(refA); // dom删除了
console.log(refA, 'refA'); // 但是还存在引用能console出整个div 没有被回收
refA = null;
console.log(refA, 'refA'); // 解除引用

包括使用事件监听addEventListener监听的时候,在不监听的情况下使用removeEventListener取消对事件监听

怎样避免内存泄漏

1)减少不必要的全局变量,或者生命周期较长的对象,及时对无用的数据进行垃圾回收;

2)注意程序逻辑,避免“死循环”之类的 ;

3)避免创建过多的对象 原则:不用了的东西要及时归还。


给大家分享我收集整理的各种学习资料,前端小白交学习流程,入门教程等回答-下面是学习资料参考。

前端学习交流、自学、学习资料等推荐 - 知乎