在上篇中,我们总结了 JavaScript
这门语言的基础知识,而这篇则是讲述这门语言的特色,也是它的核心知识、面试重点。
函数可谓是JavaScript中的一等公民,与函数涉及的也有相当多的概念,当初笔者刚学的时候被绕的云里雾里,下面先以一个问题,开始函数的总结与学习。
JavaScript
代码的执行主要分为以下三步:
而 JavaScript
比较有特色的就是在预编译阶段,我们重点看看预编译阶段做了什么事情。
引擎一开始会创建执行上下文(也叫Activation Object
、AO
对象),执行上下文主要有如下三种类型:
Eval
执行上下文:eval
中运行的函数代码,很少用执行上下文的创建主要分为创建阶段和执行阶段
1.绑定 this
指向
2.创建词法环境
3.生成变量环境
这里解释一下词法环境和变量环境,其实他们两个是差不多相同的组件。词法环境中包含两个部分,一个是存储变量与函数声明的位置,另一个是对外部环境的引用。
伪代码如下:
GlobalExectionContext = {
// 全局执行上下文
LexicalEnvironment: {
// 词法环境
EnvironmentRecord: {
// 环境记录
Type: "Object", // 全局环境
// 标识符绑定在这里
outer: <null>, // 对外部环境的引用
}
}
}
FunctionExectionContext = {
// 函数执行上下文
LexicalEnvironment: {
// 词法环境
EnvironmentRecord: {
// 环境记录
Type: "Declarative", // 函数环境
// 标识符绑定在这里 // 对外部环境的引用
outer: <Global or outer function environment reference>
}
}
变量环境也是一个词法环境,词法环境和变量环境的区别在于:
let
和 const
变量var
变量完成对所有变量的分配,最后执行代码
每个 JavaScript
函数都是一个对象,对象中有的属性可以访问,有的不能,这些属性仅供 JavaScript
引擎存取,如[[scope]]
。
[[scope]]就是函数的作用域,其中存储了执行上下文的集合。
[[scope]]
中所存储的执行上下文对象的集合,这个集合呈链式链接,我们称这种链式链接为作用域链。查找变量时,要从作用域链的顶部开始查找。在当前执行上下文中找不到变量时,则到对外部环境的引用中向上查找,故呈现一个链式结构。
JavaScript
中,函数声明与变量声明会被 JavaScript
引擎隐式地提升到当前作用域的顶部当内部函数被保存到外部时,将会生成闭包。生成闭包后,内部函数依旧可以访问其所在的外部函数的变量。
当函数执行时,会创建执行上下文,获取作用域链(存储了函数能够访问的所有执行上下文)。函数每次执行时对应的执行上下文都是独一无二的,当函数执行完毕,函数都会失去对这个作用域链的引用, JS
的垃圾回收机制是采用引用计数策略,如果一块内存不再被引用了那么这块内存就会被释放。
但是,当闭包存在时,即内部函数保留了对外部变量的引用时,这个作用域链就不会被销毁,此时内部函数依旧可以访问其所在的外部函数的变量,这就是闭包。
即闭包逃过了 GC
策略,故滥用会导致内存泄漏,其实本身就是一种内存泄漏?
for (var i = 0; i < 5; i++) {
setTimeout(function timer() {
console.log(i)
}, i * 100)
}
function test() {
var a = [];
for (var i = 0; i < 5; i++) {
a[i] = function () {
console.log(i);
}
}
return a;
}
var myArr = test();
for(var j=0;j<5;j++)
{
myArr[j]();
}
以上两个例子都打印5个5,简单解释就是变量 i
记录的是最终跳出循环的值,即5,可以通过立即执行函数或者 let
来解决。因为立即执行函数创建了一个新的执行上下文,可以保存当前循环 i
的值,而let则构建了块级作用域,也可以保存当前循环 i
的值。
for (var i = 0; i < 5; i++) {
;(function(i) {
setTimeout(function timer() {
console.log(i)
}, i * 100)
})(i)
}
function test(){
var arr=[];
for(i=0;i<10;i++)
{
(function(j){
arr[j]=function(){
console.log(j);
}
})(i)
}
return arr;
}
var myArr=test();
for(j=0;j<10;j++)
{
myArr[j]();
}
function Counter() {
let count = 0;
this.plus = function () {
return ++count;
}
this.minus = function () {
return --count;
}
this.getCount = function () {
return count;
}
}
const counter = new Counter();
counter.puls();
counter.puls();
console.log(counter.getCount())
实现一个foo函数 可以这么使用:
a = foo();
b = foo();
c = foo();
// a === 1;b === 2;c === 3;
foo.clear();d = foo(); //d === 1;
function myIndex() {
var index = 1;
function foo(){
return index++;
}
foo.clear = function() {
index = 1;
}
return foo;
}
var foo = myIndex();
在 JavaScript
中,调用函数的方式主要有如下数种
Foo.foo(arg1, arg2)
;foo(arg1, arg2)
;(new Foo())(arg1, arg2)
;call
/ apply
调用模式 Foo.foo.call(that, arg1, arg2)
;bind
调用模式 Foo.foo.bind(that)(arg1, arg2)()
;无论是面试还是业务开发,这都是经常接触到的知识,我们一起来看看
函数防抖就是在函数需要频繁触发的情况下,只有足够的空闲时间,才执行一次。
典型应用
function debounce(handler, delay = 300){
var timer = null;
return function(){
var _self = this,
_args = arguments;
clearTimeout(timer);
timer = setTimeout(function(){
handler.apply(_self, _args);
}, delay);
}
// 频繁触发时,清除对应的定时器,然后再开一个定时器,delay秒后执行
function debounce(handler, delay){
delay = delay || 300;
var timer = null;
return function(){
var _self = this,
_args = arguments;
clearTimeout(timer);
timer = setTimeout(function(){
handler.apply(_self, _args);
}, delay);
}
}
// 不希望被频繁调用的函数
function add(counterName) {
console.log(counterName + ": " + this.index ++);
}
// 需要的上下文对象
let counter = {
index: 0
}
// 防抖的自增函数,绑定上下文对象counter
let db_add = debounce(add, 10).bind(counter)
// 每隔500ms频繁调用3次自增函数,但因为防抖的存在,这3次内只调用一次
setInterval(function() {
db_add("someCounter1");
db_add("someCounter2");
db_add("someCounter3");
}, 500)
/**
* 预期效果:
*
* 每隔500ms,输出一个自增的数
* 即打印:
someCounter3: 0
someCounter3: 1
someCounter3: 2
someCounter3: 3
*/
一个函数只有在大于执行周期时才执行,周期内调用不执行。好像水滴积攒到一定程度才会触发一次下落一样。
典型应用:
function throttle(fn,wait=300){
var lastTime = 0
return function(){
var that = this,args=arguments
var nowTime = new Date().getTime()
if((nowTime-lastTime)>wait){
fn.apply(that,args)
lastTime = nowTime
}
}
}
在上面说函数的时候,我们也提到了一下 this
,即函数创建执行上下文的时候第一步就是绑定 this
的指向,也对应了那句话-- JavaScript
中 this
的指向是当函数执行的时候才确定的。
this
的指向主要有如下数种:
this
指向window
,严格模式下,this
指向undefined
this
通常指向调用的对象apply
、call
、bind
可以绑定this
指向this
指向新创建的对象this
值,this
在箭头创建时绑定,它与声明所在的上下文相同首先, new
的方式优先级最高,接下来是 bind
这些函数,然后是 obj.foo()
这种调用方式,最后是 foo
这种调用方式,同时,箭头函数的 this
一旦被绑定,就不会再被任何方式所改变。
new
绑定bind
、apply
、call
obj.foo()
window
function foo() {
// 运行在严格模式下,this会绑定到undefined
"use strict";
console.log( this.a );
}
var a = 2;
// 调用
foo(); // TypeError: Cannot read property 'a' of undefined
// --------------------------------------
function foo() {
// 运行
console.log( this.a );
}
var a = 2;
foo()//2
function foo() {
console.log( this.a );
}
var obj = {
a: 2,
foo: foo
};
obj.foo(); // 2
注意下面这种情况,称为隐式丢失。
function foo() {
console.log( this.a );
}
var obj = {
a: 2,
foo: foo
};
var bar = obj.foo; // 函数别名
var a = "global"; // a是全局对象的属性
bar(); // "global"
function foo() {
console.log( this.a );
}
var obj = {
a: 2
};
foo.call( obj ); // 2 调用foo时强制把foo的this绑定到obj上
function foo(a) {
this.a = a;
}
var bar = new foo(2); // bar和foo(..)调用中的this进行绑定
console.log( bar.a ); // 2
请分别写出下面题目的答案。
function Foo() {
getName = function() {
console.log(1);
};
return this;
}
Foo.getName = function() {
console.log(2);
};
Foo.prototype.getName = function() {
console.log(3);
};
var getName = function() {
console.log(4);
};
function getName() {
console.log(5);
}
//请写出以下输出结果:
Foo.getName(); //-> 2 Foo对象上的getName() ,这里不会是3,因为只有Foo的实例对象才会是3,Foo上面是没有3的
getName(); //-> 4 window上的getName,console.log(5)的那个函数提升后,在console.log(4)的那里被重新赋值
Foo().getName(); //-> 1 在Foo函数中,getName是全局的getName,覆盖后输出 1
getName(); //-> 1 window中getName();
new Foo.getName(); //-> 2 Foo后面不带括号而直接 '.',那么点的优先级会比new的高,所以把 Foo.getName 作为构造函数
new Foo().getName();//-> 3 此时是Foo的实例,原型上会有输出3这个方法
箭头函数里面的 this
是继承它作用域父级的 this
, 即声明箭头函数处的 this
let a = {
b: function() {
console.log(this)
},
c: () => {
console.log(this)
}
}
a.b() // a
a.c() // window
let d = a.b
d() // window
bind
方法bind
的使用方法是 某函数.bind(某对象,…剩余参数)
apply
的方式在一个回调函数中执行即可this
,因为要拿到那个函数用 apply
/**
* 简单版本
*/
Function.prototype.myBind = (that, ...args) => {
const funcThis = this;
return function(..._args) {
return funcThis.apply(that, args.concat(_args));
}
}
Function.prototype.mybind = function(ctx) {
var _this = this;
var args = Array.prototype.slice.call(arguments, 1);
return function() {
return _this.apply(ctx, args.concat(args, Array.prototype.slice.call(arguments)))
}
}
/**
* 自封装bind方法
* @param {对象} target [被绑定的this对象, 之后的arguments就是被绑定传入参数]
* @return {[function]} [返回一个新函数,这个函数就是被绑定了this的新函数]
*/
Function.prototype.myBind = function (target){
target = target || window;
var self = this;
var args = [].slice.call(arguments, 1);
var temp = function(){
};
var F = function() {
var _args = [].slice.call(arguments, 0);
return self.apply(this instanceof temp ? this: target, args.concat(_args));
}
temp.prototype = this.prototype; //当函数是构造函数时,维护原型关系
F.prototype = new temp();
return F;
}
Function.prototype
上编程this
this
Function.prototype.myApply = function(context) {
if (typeof this !== 'function') {
throw new TypeError('Error')
}
context = context || window
context.fn = this
let result
// 处理参数和 call 有区别
if (arguments[1]) {
result = context.fn(...arguments[1])
} else {
result = context.fn()
}
delete context.fn
return result
}
new
的过程
this
function create() {
let obj = {
}
obj.__proto__ = con.prototype
con.call(this)
return obj
};
JavaScript
的所有对象都有一个__proto__
属性,这个属性所对应的就是该对象的原型JavaScript
的函数对象除了原型__proto__
之外,还预置了prototype
属性(Function.prototype.bind
没有)prototype
属性值将被作为实例对象的原型__proto__
constructor 返回创建实例对象时构造函数的引用
function Parent(age) {
this.age = age;
}
var p = new Parent(50);
p.constructor === Parent; // true
p.constructor === Object; // false
这是一个显式原型属性,只有函数才拥有该属性。基本所有的函数都有这个属性,除了Function.prototype.bind()
let fun = Function.prototype.bind()
如果你以上述方法创建一个函数,那么可以发现这个函数是不具有 prototype
属性的。
当我们声明一个函数时,这个属性就被自动创建了
function Foo(){}
并且这个属性的值是一个对象(也就是原型),只有一个属性constructor
constructor
对应着构造函数,也就是Foo
这是每个对象都有的隐式原型属性,指向了创建该对象的构造函数的原型。其实这个属性指向了 [[prototype]]
,但是 [[prototype]]
是内部属性,我们并不能访问到,所以使用__proto__
来访问。
在 new
的过程中,新对象被添加了 __proto__
并且链接到构造函数的原型上。
对于对象来说,obj.__proto__.constructor
是该对象的构造函数,但是上图标明Function.__proto__=== Function.prototype
。从图中可以发现,所有对象都可以通过原型链最终找到Object.prototype
,虽然Object.prototype
也是一个对象,但是这个对象却不是Object
创造的,而是引擎创建的。
引擎创建了 Object.prototype
,然后创建了 Function.prototype
,并且通过 __proto__
将两者联系了起来。这里也很好的解释了上面的一个问题,为什么 let fun = Function.prototype.bind()
没有 prototype
属性。因为 Function.prototype
是引擎创建出来的对象,引擎认为不需要给这个对象添加 prototype
属性。
所以我们又可以得出一个结论,不是所有函数都是 new Function()
产生的。
有了 Function.prototype
以后才有了 function Function()
,然后其他的构造函数都是 function Function()
生成的。
现在可以来解释 Function.__proto__ === Function.prototype
这个问题了。因为先有的 Function.prototype
以后才有的 function Function()
,所以也就不存在鸡生蛋蛋生鸡的悖论问题了。对于为什么 Function.__proto__
会等于 Function.prototype
,个人的理解是:其他所有的构造函数都可以通过原型链找到 Function.prototype
,并且 function Function()
本质也是一个函数,为了不产生混乱就将 function Function()
的 __proto__
联系到了 Function.prototype
上。
Object
是所有对象的爸爸,所有对象都可以通过__proto__
找到它Function
是所有函数的爸爸,所有函数都可以通过__proto__
找到它Function.prototype
和Object.prototype
是两个特殊的对象,由引擎创建new
创建出来的prototype
是一个对象,也就是原型__proto__
指向原型, __proto__
将对象和原型连接起来组成了原型链判断实例对象的__proto__
属性与构造函数的prototype
是不是用一个引用。如果不是,他会沿着对象的__proto__
向上查找的,直到为null
。
const Person = function(){
}
const p1 = new Person()
p1 instanceof Person//true
var str = 'hello world'
str instanceof String // true
var str1 = new String('hello world')
str1 instanceof String // true
function instance_of(L, R) {
var O = R.prototype;
L = L.__proto__;
while (true) {
if (L === null)
return false;//最终没找到,返回false
if (O === L)
return true;//相等则返回true
L = L.__proto__;//继续沿着__proto__向上找
}
}
//ES5
function Animal(){
this.name = 'Animal'
}
//ES6
class Animal2{
constructor () {
this.name = 'Animal';
}
}
function Cat(){
}
Cat.prototype=new Animal()
var cat = new Cat()
原理:把子类的prototype
(原型对象)直接设置为父类的实例
缺点:因为子类只进行一次原型更改,所以子类的所有实例保存的是同一个父类的值。
当子类对象上进行值修改时,如果是修改的原始类型的值,那么会在实例上新建这样一个值;
但如果是引用类型的话,他就会去修改子类上唯一一个父类实例里面的这个引用类型,这会影响所有子类实例
function Cat(){
Animal.call(this)
}
原理: 将子类的this
使用父类的构造函数跑一遍
缺点: Parent原型链上的属性和方法并不会被子类继承
function Cat(name){
var instance = new Animal()
instance.name = name || 'cat'
return instance
}
function Cat(){
Animal.call(this)
}
Cat.prototype = new Animal()
Cat.prototype.constructor = Cat
特点:
function Cat(name){
var animal = new Animal();
for(var p in animal){
Cat.prototype[p] = animal[p];
}
Cat.prototype.name = name || 'Tom';
}
var cat = new Cat()
特点:
function Cat(){
Animal.call(this)
}
(function(){
var Super = function(){
}
Super.prototype = Animal.prototype
Cat.prototype = new Super()
})()
Cat.prototype.constructor = Cat
var cat = new Cat()
JavaScript
语言的一大特点就是单线程,也就是说,同一个时间只能做一件事。那么,为什么JavaScript
不能有多个线程呢?这样能提高效率啊。
JavaScript
的单线程,与它的用途有关。作为浏览器脚本语言,JavaScript
的主要用途是与用户互动,以及操作DOM
。这决定了它只能是单线程,否则会带来很复杂的同步问题。比如,假定JavaScript
同时有两个线程,一个线程在某个DOM
节点上添加内容,另一个线程删除了这个节点,这时浏览器应该以哪个线程为准?
所以,为了避免复杂性,从一诞生,JavaScript
就是单线程,这已经成了这门语言的核心特征,将来也不会改变。
为了利用多核CPU
的计算能力,HTML5
提出Web Worker标准,允许JavaScript
脚本创建多个线程,但是子线程完全受主线程控制,且不得操作DOM
。所以,这个新标准并没有改变JavaScript
单线程的本质。
参考地址:Event Loop 这个循环你晓得么?(附 GIF 详解)-饿了么前端
setTimeOut
、setInterval
DOM
事件Promise
可分为宏任务和微任务,分类如下:
前面我们介绍,事件循环会将其中的异步任务按照执行顺序排列到事件队列中。然而,根据异步事件的不同分类,这个事件实际上会被排列到对应的宏任务队列或者微任务队列当中去。
当执行栈中的任务清空,主线程会先检查微任务队列中是否有任务,如果有,就将微任务队列中的任务依次执行,直到微任务队列为空,之后再检查宏任务队列中是否有任务,如果有,则每次取出第一个宏任务加入到执行栈中,之后再清空执行栈,检查微任务,以此循环… …
将事件插入到了事件队列,必须等到当前代码(执行栈)执行完,主线程才会去执行它指定的回调函数。
当主线程时间执行过长,无法保证回调会在事件指定的时间执行。
浏览器端每次setTimeout
会有4ms
的延迟,当连续执行多个setTimeout
,有可能会阻塞进程,造成性能问题。
事件插入到事件队列尾部,主线程和事件队列的函数执行完成之后立即执行。和setTimeout(fn,0)
的效果差不多。
服务端 node
提供的方法。浏览器端最新的 api
也有类似实现:window.setImmediate
,但支持的浏览器很少。
插入到事件队列尾部,但在下次事件队列之前会执行。也就是说,它指定的任务总是发生在所有异步任务之前,当前主线程的末尾。
大致流程:当前”执行栈”的尾部–>下一次Event Loop
(主线程读取”任务队列”)之前–>触发 process
指定的回调函数。
服务器端 node
提供的办法。用此方法可以用于处于异步延迟的问题。
可以理解为:此次不行,预约下次优先执行。
Promise
本身是同步的立即执行函数, 当在 executor
中执行 resolve
或者 reject
的时候, 此时是异步操作, 会先执行 then/catch
等,当主栈完成后,才会去调用 resolve/reject
中存放的方法执行,打印 p
的时候,是打印的返回结果,一个 Promise
实例。
Async/Await
就是一个自执行的 generate
函数。利用generate
函数的特性把异步的代码写成“同步”的形式。
async
函数返回一个 Promise
对象,当函数执行的时候,一旦遇到 await
就会先返回,等到触发的异步操作完成,再执行函数体内后面的语句。可以理解为,是让出了线程,跳出了 async
函数体。
/\d+$/g
字符串内如有空格,但是空格的数量可能不一致,通过正则将空格的个数统一变为一个。
let reg = /\s+/g
str.replace(reg, " ");
str.test(/^\d+$/);
/^\d{3,4}-\d{7,8}(-\d{3,4})?$/
正则验证手机号,忽略前面的0,支持130-139,150-159。忽略前面0之后判断它是11位的。
/^0*1(3|5)\d{9}$/
function trim(str) {
let reg = /^\s+|\s+$/g
return str.replace(reg, '');
}
/^\d*\.\d{0,2}$/
/^[a-z\.:\/\\]*$/
例如:infomarket.php?id=197 替换为 test.php?id=197
var reg = /^[^\.]+/;
var target = '---------';
str = str.replace(reg, target)
/[\u4E00-\u9FA5\uf900-\ufa2d]/ig
先去掉非中文字符,再返回length属性。
function cLength(str){
var reg = /[^\u4E00-\u9FA5\uf900-\ufa2d]/g;
//匹配非中文的正则表达式
var temp = str.replace(reg,'');
return temp.length;
}
只要匹配掉最后一段并且替换为空字符串就行了
function getPreThrstr(str) {
let reg = /\.\d{1,3}$/;
return str.replace(reg,'');
}
/<ul>[\s\S]+?</ul>/i
c:\images\tupian\006.jpg
可能是直接在盘符根目录下,也可能在好几层目录下,要求替换到只剩文件名。
首先匹配非左右斜线字符0或多个,然后是左右斜线一个或者多个。
function getFileName(str){
var reg = /[^\\\/]*[\\\/]+/g;
// xxx\ 或是 xxx/
str = str.replace(reg,'');
return str;
}
“http://23.123.22.12/image/somepic.gif"转换为:”/image/somepic.gif"
var reg = /http:\/\/[^\/]+/;
str = str.replace(reg,"");
用于用户名注册,,用户名只 能用 中文、英文、数字、下划线、4-16个字符。
/^[\u4E00-\u9FA5\uf900-\ufa2d\w]{4,16}$/
规则如下:
包含 “点”, “字母”,“空格”,“逗号”,“数字”,但开头和结尾不能是除字母外任何字符。
/^[a-zA-Z][\.a-zA-Z,0-9]*[a-zA-Z]$/
开头数字若干位,可能有一个小数点,小数点后面可以有两位数字。
/^\d+(\.\d{2})?$/
身份证号码可以是15位或者是18位,其中最后一位可以是X。其它全是数字
/^(\d{14}|\d{17})(X|x)$/
每单词首字大写,其他小写。如blue idea转换为Blue Idea,BLUE IDEA也转换为Blue Idea
function firstCharUpper(str) {
str = str.toLowerCase();
let reg = /\b(\w)/g;
return str.replace(reg, m => m.toUpperCase());
}
yyyy-mm-dd格式
4位数字,横线,1或者2位数字,再横线,最后又是1或者2位数字。
/^\d{4}-\d{1,2}-\d{1,2}$/
www.abc.com/dc/fda.asp 变为 www.abc.com/dc/fda
function removeExp(str) {
return str.replace(/\.\w$/,'')
}
开始必须是一个或者多个单词字符或者是-,加上@,然后又是一个或者多个单词字符或者是-。然后是点“.”和单词字符和-的组合,可以有一个或者
多个组合。
/^[\w-]+@\w+\.\w+$/
例如:
p的内容,同样也是没闭合的标签。
标签可能有两种方式闭合, 或者是
xxx
。/<([a-z]+)(\s*\w*?\s*=\s*".+?")*(\s*?>[\s\S]*?(<\/\1>)+|\s*\/>)/i
不能小于12位,且必须为字母和数字的混
/^(([a-z]+[0-9]+)|([0-9]+[a-z]+))[a-z0-9]*$/i
function replaceReg(reg,str){
let arr=["零","壹","贰","叁","肆","伍","陆","柒","捌","玖"];
let reg = /\d/g;
return str.replace(reg,function(m){
return arr[m];})
}
***td>
变成没有任何属性的
<td>***td>
思路:非捕获匹配属性,捕获匹配标签,使用捕获结果替换掉字符串。正则如下:
/()/
垃圾回收
JavaScript垃圾回收
标记清除(mark and sweep)
- 这是
JavaScript
最常见的垃圾回收方式,当变量进入执行环境的时候,比如函数中声明一个变量,垃圾回收器将其标记为“进入环境”,当变量离开环境时(函数执行结束)将其标记为“离开环境”
- 垃圾回收器会在运行的时候给存储在内存中的所有变量加上标记,然后去掉环境中的变量以及被环境中的变量所引用的变量(闭包),在这些完成后仍存在标记的就是要删除的变量了。
引用计数(reference counting)
- 在低版本
IE
中经常会出现内存泄漏,很多时候就是因为采取引用计数的方式进行垃圾回收。引用计数的策略是跟踪记录每个值被使用的次数,当声明了一个变量并将一个引用类型赋值给改变量的时候这个值的引用次数加1,如果该变量的值变成了另外一个,则这个值的引用次数减1,当这个值的引用次数变为0的时候,说明没有变量在引用,这个值没法被访问了,因此可以将其占用的空间回收,这样垃圾回收器会在运行的时候清理掉引用次数为0的值占用的空间
参考链接 内存管理-MDN
V8下的垃圾回收机制
V8
实现了准确式GC,GC
算法采用了分代式垃圾回收机制。因此,V8
将内存(堆)分为新生代和老生代
-
新生代算法
- 新生代中对象一般存活时间较短,使用
Scavenge GC
算法。
- 在新生代空间中,内存空间分成两块,分别为
From
空间和To
空间。在这两个空间中,必定有一个空间是使用的,另一个空间是空闲的。新分配的对象会被放入From
空间中,当From
空间被沾满时,新生代GC
就会启动了。算法会检测From
空间中存活的对象并复制到To
空间中,如果有失活的对象就会销毁。当复制完成将From
空间和To
空间互换,这样GC
就结束了
-
老生代算法
- 老生代中的对象一般存活时间比较长且数量也多,使用了两个算法,分别是标记清除算法(Mark-Sweep)和标记压缩算法(Mark-Compact)。对象出现在老生代空间的情况如下
- 新生代中的对象是否已经经历过一次
Scavenge
算法,如果经历过的话,会将对象从新生代空间移到老生代空间中。
To
空间的对象占比大小超过25%
。在这种情况下为了不影响到内存分配,会将对象从新生代空间移动到老生代空间。
- 标记清除(Mark-Sweep)
- Mark-Sweep在标记阶段遍历堆中的所有对象,并标记活着的对象,在随后的清除阶段中,只清楚没有标记的对象。Mark-Sweep最大的问题是在进行一次标记清除回收后,内存空间出现不连续的状态,为了解决这个内存碎片的问题,Mark-Compact被提出。
- Mark-Compact在整理的过程中,将活着的对象往一端移动,移动完成后,直接清除掉边界外的内存。
内存泄漏
申请的内存没有及时回收掉,则导致内存泄漏
为什么会发生内存泄漏?
虽然前端有垃圾回收机制,但当某块无用的内存,却无法被垃圾回收机制认为是垃圾时,也就发生内存泄漏了
而垃圾回收机制通常是使用标志清除策略,简单说,也就是引用从根节点开始是否可达来判定是否是垃圾
上面是发生内存泄漏的根本原因,直接原因则是,当不同生命周期的两个东西相互通信时,一方生命到期该回收了,却被另一方还持有时,也就发生内存泄漏了
哪些情况会引起内存泄漏
- 意外的全局变量
- 遗忘的定时器
- 使用不当的闭包
- 遗漏的DOM元素
- 网络回调
如何监控内存泄漏
内存泄漏是可以分成两类的,一种是比较严重的,泄漏的就一直回收不回来了,另一种严重程度稍微轻点,就是没有及时清理导致的内存泄漏,一段时间后还是可以被清理掉
不管哪一种,利用开发者工具抓到的内存图,应该都会看到一段时间内,内存占用不断的直线式下降,这是因为不断发生 GC
,也就是垃圾回收导致的
针对第一种比较严重的,会发现,内存图里即使不断发生 GC
后,所使用的内存总量仍旧在不断增长
另外,内存不足会造成不断 GC
,而 GC
时是会阻塞主线程的,所以会影响到页面性能,造成卡顿,所以内存泄漏问题还是需要关注的
举例场景
在某个函数内申请一块内存,然后该函数在短时间内不断被调用
// 点击按钮,就执行一次函数,申请一块内存
startBtn.addEventListener("click", function() {
var a = newArray(100000).fill(1);
var b = newArray(20000).fill(1);
});
一个页面能够使用的内存是有限的,当内存不足时,就会触发垃圾回收机制去回收没用的内存
而在函数内部使用的变量都是局部变量,函数执行完毕,这块内存就没用可以被回收了
所以当我们短时间内不断调用该函数时,可以发现,函数执行时,发现内存不足,垃圾回收机制工作,回收上一个函数申请的内存,因为上个函数已经执行结束了,内存无用可被回收了
如何分析内存泄漏,找出有问题的代码
借助开发者工具的memory
功能
最后
行文至此,感谢阅读,一键三连是对我最大的支持。
你可能感兴趣的:(javascript,node.js,es6,html,css)