视频1 视频21 视频41 视频61 视频文章1 视频文章21 视频文章41 视频文章61 推荐1 推荐3 推荐5 推荐7 推荐9 推荐11 推荐13 推荐15 推荐17 推荐19 推荐21 推荐23 推荐25 推荐27 推荐29 推荐31 推荐33 推荐35 推荐37 推荐39 推荐41 推荐43 推荐45 推荐47 推荐49 关键词1 关键词101 关键词201 关键词301 关键词401 关键词501 关键词601 关键词701 关键词801 关键词901 关键词1001 关键词1101 关键词1201 关键词1301 关键词1401 关键词1501 关键词1601 关键词1701 关键词1801 关键词1901 视频扩展1 视频扩展6 视频扩展11 视频扩展16 文章1 文章201 文章401 文章601 文章801 文章1001 资讯1 资讯501 资讯1001 资讯1501 标签1 标签501 标签1001 关键词1 关键词501 关键词1001 关键词1501 专题2001
JavaScript也谈内存优化_javascript技巧
2020-11-27 21:24:01 责编:小采
文档


相对C/C++ 而言,我们所用的JavaScript 在内存这一方面的处理已经让我们在开发中更注重业务逻辑的编写。但是随着业务的不断复杂化,单页面应用、移动HTML5 应用和Node.js 程序等等的发展,JavaScript 中的内存问题所导致的卡顿、内存溢出等现象也变得不再陌生。

这篇文章将从JavaScript 的语言层面进行内存的使用和优化的探讨。从大家熟悉或略有耳闻的方面,到大家大多数时候不会注意到的地方,我们一一进行剖析。

1. 语言层面的内存管理

1.1 作用域

作用域(scope)是JavaScript 编程中一个非常重要的运行机制,在同步JavaScript 编程中它并不能充分引起初学者的注意,但在异步编程中,良好的作用域控制技能成为了JavaScript 开发者的必备技能。另外,作用域在JavaScript 内存管理中起着至关重要的作用。

在JavaScript中,能形成作用域的有函数的调用、with语句和全局作用域。

如以下代码为例:
代码如下:
var foo = function() {
var local = {};
};
foo();
console.log(local); //=> undefined

var bar = function() {
local = {};
};
bar();
console.log(local); //=> {}

这里我们定义了foo()函数和bar()函数,他们的意图都是为了定义一个名为local的变量。但最终的结果却截然不同。

在foo()函数中,我们使用var语句来声明定义了一个local变量,而因为函数体内部会形成一个作用域,所以这个变量便被定义到该作用域中。而且foo()函数体内并没有做任何作用域延伸的处理,所以在该函数执行完毕后,这个local变量也随之被销毁。而在外层作用域中则无法访问到该变量。

而在bar()函数内,local变量并没有使用var语句进行声明,取而代之的是直接把local作为全局变量来定义。故外层作用域可以访问到这个变量。
代码如下:
local = {};
// 这里的定义等效于
global.local = {};

1.2 作用域链

在JavaScript编程中,你一定会遇到多层函数嵌套的场景,这就是典型的作用域链的表示。

如以下代码所示:
代码如下:
function foo() {
var val = 'hello';

function bar() {
function baz() {
global.val = 'world;'
}
baz();
console.log(val); //=> hello
}
bar();
}
foo();

根据前面关于作用域的阐述,你可能会认为这里的代码所显示的结果是world,但实际的结果却是hello。很多初学者在这里就会开始感到困惑了,那么我们再来看看这段代码是怎么工作的。

由于JavaScript 中,变量标识符的查找是从当前作用域开始向外查找,直到全局作用域为止。所以JavaScript 代码中对变量的访问只能向外进行,而不能逆而行之。

baz()函数的执行在全局作用域中定义了一个全局变量val。而在bar()函数中,对val这一标识符进行访问时,按照从内到外厄德查找原则:在bar函数的作用域中没有找到,便到上一层,即foo()函数的作用域中查找。

然而,使大家产生疑惑的关键就在这里:本次标识符访问在foo()函数的作用域中找到了符合的变量,便不会继续向外查找,故在baz()函数中定义的全局变量val并没有在本次变量访问中产生影响。

1.3 闭包

我们知道JavaScript 中的标识符查找遵循从内到外的原则。但随着业务逻辑的复杂化,单一的传递顺序已经远远不能满足日益增多的新需求。

我们先来看看下面的代码:
代码如下:
function foo() {
var local = 'Hello';
return function() {
return local;
};
}
var bar = foo();
console.log(bar()); //=> Hello

这里所展示的让外层作用域访问内层作用域的技术便是闭包(Closure)。得益于高阶函数的应用,使foo()函数的作用域得到『延伸』。

foo()函数返回了一个匿名函数,该函数存在于foo()函数的作用域内,所以可以访问到foo()函数作用域内的local变量,并保存其引用。而因这个函数直接返回了local变量,所以在外层作用域中便可直接执行bar()函数以获得local变量。

闭包是JavaScript 的高级特性,我们可以借助它来实现更多更复杂的效果来满足不同的需求。但是要注意的是因为把带有​​内部变量引用的函数带出了函数外部,所以该作用域内的变量在函数执行完毕后的并不一定会被销毁,直到内部变量的引用被全部解除。所以闭包的应用很容易造成内存无法释放的情况。

2. JavaScript 的内存回收机制

这里我将以Chrome 和Node.js 所使用的,由Google 推出的V8 引擎为例,简要介绍一下JavaScript 的内存回收机制,更详尽的内容可以购买我的好朋友朴灵的书《深入浅出Node.js 》进行学习,其中『内存控制』一章中有相当详细的介绍。

在V8 中,所有的JavaScript 对象都是通过『堆』来进行内存分配的。

当我们在代码中声明变量并赋值时,V8 就会在堆内存中分配一部分给这个变量。如果已申请的内存不足以存储这个变量时,V8 就会继续申请内存,直到堆的大小达到了V8 的内存上限为止。默认情况下,V8 的堆内存的大小上限在位系统中为14MB,在32位系统中则为732MB,即约1.4GB 和0.7GB。

另外,V8 对堆内存中的JavaScript 对象进行分代管理:新生代和老生代。新生代即存活周期较短的JavaScript 对象,如临时变量、字符串等;而老生代则为经过多次垃圾回收仍然存活,存活周期较长的对象,如主控制器、服务器对象等。

垃圾回收算法一直是编程语言的研发中是否重要的​​一环,而V8 中所使用的垃圾回收算法主要有以下几种:

1.Scavange 算法:通过复制的方式进行内存空间管理,主要用于新生代的内存空间;
2.Mark-Sweep 算法和Mark-Compact 算法:通过标记来对堆内存进行整理和回收,主要用于老生代对象的检查和回收。


PS: 更详细的V8 垃圾回收实现可以通过阅读相关书籍、文档和源代码进行学习。

我们再来看看JavaScript 引擎在什么情况下会对哪些对象进行回收。

2.1 作用域与引用

初学者常常会误认为当函数执行完毕时,在函数内部所声明的对象就会被销毁。但实际上这样理解并不严谨和全面,很容易被其导致混淆。

引用(Reference)是JavaScript 编程中十分重要的一个机制,但奇怪的是一般的开发者都不会刻意注意它、甚至不了解它。引用是指『代码对对象的访问』这一抽象关系,它与C/C++ 的指针有点相似,但并非同物。引用同时也是JavaScript 引擎在进行垃圾回收中最关键的一个机制。

以下面代码为例:
代码如下:
// ......
var val = 'hello world';
function foo() {
return function() {
return val;
};
}
global.bar = foo();
// ......

阅读完这段代码,你能否说出这部分代码在执行过后,有哪些对象是依然存活的么?

根据相关原则,这段代码中没有被回收释放的对象有val和bar(),究竟是什么原因使他们无法被回收?

JavaScript 引擎是如何进行垃圾回收的?前面说到的垃圾回收算法只是用在回收时的,那么它是如何知道哪些对象可以被回收,哪些对象需要继续生存呢?答案就是JavaScript 对象的引用。

JavaScript 代码中,哪怕是简单的写下一个变量名称作为单独一行而不做任何操作,JavaScript 引擎都会认为这是对对象的访问行为,存在了对对象的引用。为了保证垃圾回收的行为不影响程序逻辑的运行,JavaScript 引擎就决不能把正在使用的对象进行回收,不然就乱套了。所以判断对象是否正在使用中的标准,就是是否仍然存在对该对象的引用。但事实上,这是一种妥协的做法,因为JavaScript 的引用是可以进行转移的,那么就有可能出现某些引用被带到了全局作用域,但事实上在业务逻辑里已经不需要对其进行访问了,应该被回收,但是JavaScript 引擎仍会死板地认为程序仍然需要它。

如何用正确的姿势使用变量、引用,正是从语言层面优化JavaScript 的关键所在。

3. 优化你的JavaScript

终于进入正题了,非常感谢你秉着耐心看到了这里,经过上面这么多介绍,相信你已经对JavaScript 的内存管理机制有了不错的理解,那么下面的技巧将会让你如虎添翼。

3.1 善用函数

如果你有阅读优秀JavaScript 项目的习惯的话,你会发现,很多大牛在开发前端JavaScript 代码的时候,常常会使用一个匿名函数在代码的最外层进行包裹。
代码如下:
(function() {
// 主业务代码
})();
有的甚至更高级一点:
代码如下:
;(function(win, doc, $, undefined) {
// 主业务代码
})(window, document, jQuery);
甚至连如RequireJS, SeaJS, OzJS 等前端模块化加载解决方案,都是采用类似的形式:
代码如下:
// RequireJS
define(['jquery'], function($) {
// 主业务代码
});

// SeaJS
define('m​​odule', ['dep', 'underscore'], function($, _) {
// 主业务代码
});
如果你说很多Node.js 开源项目的代码都没有这样处理的话,那你就错了。Node.js 在实际运行代码之前,会把每一个.js 文件进行包装,变成如下的形式:
代码如下:
(function(exports, require, module, __dirname, __filename) {
// 主业务代码
});

这样做有什么好处?我们都知道文章开始的时候就说了,JavaScript中能形成作用域的有函数的调用、with语句和全局作用域。而我们也知道,被定义在全局作用域的对象,很有可能是会一直存活到进程退出的,如果是一个很大的对象,那就麻烦了。比如有的人喜欢在JavaScript中做模版渲染:
代码如下:
$db = mysqli_connect(server, user, password, 'myapp');
$topics = mysqli_query($db, "SELECT * FROM topics;");
?>




你是猴子请来的逗比么?





  • <%=title%>


    <%=content%>




  • 输出相应的事件。


    代码如下:
    var btns = document.querySelectorAll('.btn'); // 6 elements
    var output = document.querySelector('#output');
    var events = [1, 2, 3, 4, 5, 6];

    // Case 1
    for (var i = 0; i < btns.length; i++) {
    btns[i].onclick = function(evt) {
    output.innerText += 'Clicked ' + events[i];
    };
    }

    // Case 2
    for (var i = 0; i < btns.length; i++) {
    btns[i].onclick = (function(index) {
    return function(evt) {
    output.innerText += 'Clicked ' + events[index];
    };
    })(i);
    }

    // Case 3
    for (var i = 0; i < btns.length; i++) {
    btns[i].onclick = (function(event) {
    return function(evt) {
    output.innerText += 'Clicked ' + event;
    };
    })(events[i]);
    }

    这里第一个解决方案显然是典型的循环绑定事件错误,这里不细说,详细可以参照我给一个网友的回答;而第二和第三个方案的区别就在于闭包传入的参数。

    第二个方案传入的参数是当前循环下标,而后者是直接传入相应的事件对象。事实上,后者更适合在大量数据应用的时候,因为在JavaScript的函数式编程中,函数调用时传入的参数是基本类型对象,那么在函数体内得到的形参会是一个复制值,这样这个值就被当作一个局部变量定义在函数体的作用域内,在完成事件绑定之后就可以对events变量进行手工解除引用,以减轻外层作用域中的内存占用了。而且当某个元素被删除时,相应的事件监听函数、事件对象、闭包函数也随之被销毁回收。

    3.6 内存不是缓存

    缓存在业务开发中的作用举足轻重,可以减轻时空资源的负担。但需要注意的是,不要轻易将内存当作缓存使用。内存对于任何程序开发来说都是寸土寸金的东西,如果不是很重要的资源,请不要直接放在内存中,或者制定过期机制,自动销毁过期缓存。

    4. 检查JavaScript 的内存使用情况

    在平时的开发中,我们也可以借助一些工具来对JavaScript 中内存使用情况进行分析和问题排查。

    4.1 Blink / Webkit 浏览器

    在Blink / Webkit 浏览器中(Chrome, Safari, Opera etc.),我们可以借助其中的Developer Tools 的Profiles 工具来对我们的程序进行内存检查。


    4.2 Node.js 中的内存检查

    在Node.js 中,我们可以使用node-heapdump 和node-memwatch 模块进​​行内存检查。
    代码如下:
    var heapdump = require('heapdump');
    var fs = require('fs');
    var path = require('path');
    fs.writeFileSync(path.join(__dirname, 'app.pid'), process.pid);
    // ...
    代码如下:在业务代码中引入node-heapdump 之后,我们需要在某个运行时期,向Node.js 进程发送SIGUSR2 信号,让node-heapdump 抓拍一份堆内存的快照。
    代码如下:$ kill -USR2 (cat app.pid)

    这样在文件目录下会有一个以heapdump-..heapsnapshot格式命名的快照文件,我们可以使用浏览器的Developer Tools中的Profiles工具将其打开,并进行检查。

    5. 小结

    很快又来到了文章的结束,这篇分享主要向大家展示了以下几点内容:

    1.JavaScript 在语言层面上,与内存使用息息相关的东西;
    2.JavaScript 中的内存管理、回收机制;
    3.如何更高效地使用内存,以至于让出产的JavaScript 能更有拓展的活力;
    4.如何在遇到内存问题的时候,进行内存检查。

    希望通过对这篇文章的学习,你能够出产更优秀的JavaScript 代码,让妈妈安心、让老板放心。

    下载本文
    显示全文
    专题