eval 是做什么的?

  • eval 的功能是把对应的字符串解析成 JS 代码并运行
  • eval 不安全,若有用户输入会有被攻击风险
  • 非常耗性能(先解析成 js 语句,再执行)

严格模式的限制

  • 变量必须声明后再使用
  • 函数的参数不能有同名属性,否则报错
  • 不能使用 with 语句
  • 不能对只读属性赋值,否则报错
  • 不能使用前缀 0 表示八进制数,否则报错
  • 不能删除不可删除的属性,否则报错
  • 不能删除变量 delete prop,会报错,只能删除属性 delete global[prop]
  • eval 不会在它的外层作用域引入变量
  • eval 和 arguments 不能被重新赋值
  • arguments 不会自动反映函数参数的变化
  • 不能使用 arguments.callee
  • 不能使用 arguments.caller
  • 禁止 this 指向全局对象
  • 不能使用 fn.caller 和 fn.arguments 获取函数调用的堆栈
  • 增加了保留字(比如 protected、static 和 interface)

Javascript 垃圾回收方法

  • 标记清除(mark and sweep)
    这是 JavaScript 最常见的垃圾回收方式,当变量进入执行环境的时候,比如函数中声明一个变量,垃圾回收器将其标记为“进入环境”,当变量离开环境的时候(函数执行结束)将其标记为“离开环境”
    垃圾回收器会在运行的时候给存储在内存中的所有变量加上标记,然后去掉环境中的变量以及被环境中变量所引用的变量(闭包),在这些完成之后仍存在标记的就是要删除的变量了
  • 引用计数(reference counting)
    在低版本 IE 中经常会出现内存泄露,很多时候就是因为其采用引用计数方式进行垃圾回收。引用计数的策略是跟踪记录每个值被使用的次数,当声明了一个 变量并将一个引用类型赋值给该变量的时候这个值的引用次数就加 1,如果该变量的值变成了另外一个,则这个值得引用次数减 1,当这个值的引用次数变为 0 的时 候,说明没有变量在使用,这个值没法被访问了,因此可以将其占用的空间回收,这样垃圾回收器会在运行的时候清理掉引用次数为 0 的值占用的空间

哪些操作会造成内存泄漏?

JavaScript 内存泄露指对象在不需要使用它时仍然存在,导致占用的内存不能使用或回收,以下几种情况会导致内存泄漏:

  • 未使用 let 声明的全局变量
  • 闭包函数(Closures)
  • 循环引用(两个对象相互引用)
  • 控制台日志(console.log)
  • 移除存在绑定事件的 DOM 元素(IE)

为什么要使用模块化?都有哪几种方式可以实现模块化,各有什么特点?

模块化可以给我们带来以下好处

  • 解决命名冲突
  • 提供复用性
  • 提高代码可维护性

实现模块化方式:

  • 立即执行函数
  • AMD 和 CMD
  • CommonJS
  • ES Module

setTimeout、setInterval

常见的定时器函数有 setTimeoutsetIntervalrequestAnimationFrame,但 setTimeout、setInterval 并不是到了哪个时间就执行,而是到了那个时间把任务加入到异步事件队列中

因为 JS 是单线程执行的,如果某些同步代码影响了性能,就会导致 setTimeout 不会按期执行。而 setInterval 可能经过了很多同步代码的阻塞,导致不正确了,可以使用 setTimeout 每次获取 Date 值,计算距离下一次期望执行的时间还有多久来动态的调整。

cookie,localStorage,sessionStorage,indexDB

特性 cookie localStorage sessionStorage indexDB
数据生命周期 一般由服务器生成,可以设置过期时间 除非被清理,否则一直存在 页面关闭就清理 除非被清理,否则一直存在
数据存储大小 4K 5M 5M 无限
与服务端通信 每次都会携带在 header 中,对于请求性能影响 不参与 不参与 不参与

从上表可以看到,cookie 已经不建议用于存储。如果没有大量数据存储需求的话,可以使用 localStoragesessionStorage 。对于不怎么改变的数据尽量使用 localStorage 存储,否则可以用 sessionStorage 存储。

对于 cookie,我们还需要注意安全性。

属性 作用
value 如果用于保存用户登录态,应该将该值加密,不能使用明文的用户标识
http-only 不能通过 JS 访问 Cookie,减少 XSS 攻击
secure 只能在协议为 HTTPS 的请求中携带
same-site 规定浏览器不能在跨域请求中携带 Cookie,减少 CSRF 攻击

var、let 及 const 区别?

  • 全局申明的 var 变量会挂载在 window 上,而 let 和 const 不会
  • var 声明变量存在变量提升,let 和 const 不会
  • let、const 的作用范围是块级作用域,而 var 的作用范围是函数作用域
  • 同一作用域下 let 和 const 不能声明同名变量,而 var 可以
  • 同一作用域下在 let 和 const 声明前使用会存在暂时性死区
  • const一旦声明必须赋值,不能使用 null 占位,声明后不能再修改,如果声明的是复合类型数据,可以修改其属性。

##Proxy

Proxy 是 ES6 中新增的功能,它可以用来自定义对象中的操作。 Vue3.0 中将会通过 Proxy 来替换原本的 Object.defineProperty 来实现数据响应式。

let p = new Proxy(target, handler);

target 代表需要添加代理的对象,handler 用来自定义对象中的操作,比如可以用来自定义 set 或者 get 函数。

let onWatch = (obj, setBind, getLogger) => {
let handler = {
set(target, property, value, receiver) {
setBind(value, property);
return Reflect.set(target, property, value);
},
get(target, property, receiver) {
getLogger(target, property);
return Reflect.get(target, property, receiver);
},
};
return new Proxy(obj, handler);
};

let obj = { a: 1 };
let p = onWatch(
obj,
(v, property) => {
console.log(`监听到属性${property}改变为${v}`);
},
(target, property) => {
console.log(`'${property}' = ${target[property]}`);
}
);
p.a = 2; // 控制台输出:监听到属性a改变
p.a; // 'a' = 2

自定义 set 和 get 函数的方式,在原本的逻辑中插入了我们的函数逻辑,实现了在对对象任何属性进行读写时发出通知。

当然这是简单版的响应式实现,如果需要实现一个 Vue 中的响应式,需要我们在 get 中收集依赖,在 set 派发更新,之所以 Vue3.0 要使用 Proxy 替换原本的 API 原因在于 Proxy 无需一层层递归为每个属性添加代理,一次即可完成以上操作,性能上更好,并且原本的实现有一些数据更新不能监听到,但是 Proxy 可以完美监听到任何方式的数据改变,唯一缺陷可能就是浏览器的兼容性不好了。

map

map 作用是生成一个新数组,遍历原数组,将每个元素拿出来做一些变换然后返回一个新数组,原数组不发生改变。

map 的回调函数接受三个参数,分别是当前索引元素,索引,原数组

var arr = [1, 2, 3];
var arr2 = arr.map((item) => item + 1);
arr; //[ 1, 2, 3 ]
arr2; // [ 2, 3, 4 ]

一个典型面试题

['1', '2', '3'].map(parseInt);
// -> [ 1, NaN, NaN ]
  • 第一个 parseInt(‘1’, 0) -> 1
  • 第二个 parseInt(‘2’, 1) -> NaN
  • 第三个 parseInt(‘3’, 2) -> NaN

parseInt传入一个字符串和一个进制,比如’100’用2计算完即是1*4+0*2+0*1,得4;根据进制规则字符串中每个数字一定是小于进制的,问题中的’2’,’3’都大于进制了,所以返回NaN

filter

filter 的作用也是生成一个新数组,在遍历数组的时候将返回值为 true 的元素放入新数组,我们可以利用这个函数删除一些不需要的元素;filter 的回调函数接受三个参数,分别是当前索引元素,索引,原数组

reduce

reduce 可以将数组中的元素通过回调函数最终转换为一个值。如果我们想实现一个功能将函数里的元素全部相加得到一个值,可能会这样写代码:

const arr = [1, 2, 3];
let total = 0;
for (let i = 0; i < arr.length; i++) {
total += arr[i];
}
console.log(total); //6

但是如果我们使用 reduce 的话就可以将遍历部分的代码优化为一行代码

const arr = [1, 2, 3];
const sum = arr.reduce((acc, current) => acc + current, 0);
console.log(sum);

对于 reduce 来说,它接受两个参数,分别是回调函数和初始值,接下来我们来分解上述代码中 reduce 的过程

  • 首先初始值为 0,该值会在执行第一次回调函数时作为第一个参数传入
  • 回调函数接受四个参数,分别为累计值、当前元素、当前索引、原数组,后三者想必大家都可以明白作用,这里着重分析第一个参数
  • 在一次执行回调函数时,当前值和初始值相加得出结果 1,该结果会在第二次执行回调函数时当做第一个参数传入
  • 所以在第二次执行回调函数时,相加的值就分别是 1 和 2,以此类推,循环结束后得到结果 6。

Es6 中箭头函数与普通函数的区别?

  • 普通 function 的声明在变量提升中是最高的,箭头函数没有函数提升
  • 箭头函数没有属于自己的thisarguments
  • 箭头函数不能作为构造函数,不能被 new,没有 property
  • 不可以使用 yield 命令,因此箭头函数不能用作 Generator 函数
  • 不可以使用 new 命令,因为没有自己的 this,无法调用 call,apply;没有 prototype 属性 ,而 new 命令在执行时需要将构造函数的 prototype 赋值给新的对象的 __proto__

Promise

Promise 翻译过来就是承诺的意思,这个承诺会在未来有一个确切的答复,并且该承诺有三种状态,这个承诺一旦从等待状态变成为其他状态就永远不能更改状态了。

  • 等待中(pending)
  • 完成了(resolved)
  • 拒绝了(rejected)

当我们在构造 Promise 的时候,构造函数内部的代码是立即执行的。

new Promise((resolve, reject) => {
console.log('new Promise');
resolve('success');
});
console.log('finifsh');

// 先打印new Promise, 再打印 finifsh

Promise 实现了链式调用,也就是说每次调用 then 之后返回的都是一个 Promise,并且是一个全新的 Promise,原因也是因为状态不可变。如果你在 then 中 使用了 return,那么 return 的值会被 Promise.resolve() 包装。

Promise.resolve(1)
.then((res) => {
console.log(res); // => 1
return 2; // 包装成 Promise.resolve(2)
})
.then((res) => {
console.log(res); // => 2
});

当然了,Promise 也很好地解决了回调地狱的问题

ajax(url)
.then((res) => {
console.log(res);
return ajax(url1);
})
.then((res) => {
console.log(res);
return ajax(url2);
})
.then((res) => console.log(res));

其实它也是存在一些缺点的,比如无法取消 Promise,错误需要通过回调函数捕获。

async 和 await

一个函数如果加上 async ,那么该函数就会返回一个 Promise

async function test() {
return '1';
}
console.log(test());
// -> Promise {<resolved>: "1"}

async 就是将函数返回值使用 Promise.resolve() 包裹了下,和 then 中处理返回值一样,并且 await 只能配套 async 使用。

async function test() {
let value = await sleep();
}

async 和 await 可以说是异步终极解决方案了,相比直接使用 Promise 来说,优势在于处理 then 的调用链,能够更清晰准确的写出代码,毕竟写一大堆 then 也很恶心,并且也能优雅地解决回调地狱问题。

当然也存在一些缺点,因为 await 将异步代码改造成了同步代码,如果多个异步代码没有依赖性却使用了 await 会导致性能上的降低。

async function test() {
// 以下代码没有依赖性的话,完全可以使用 Promise.all 的方式
// 如果有依赖性的话,其实就是解决回调地狱的例子了
await fetch(url);
await fetch(url1);
await fetch(url2);
}

看一个使用 await 的例子:

let a = 0;
let b = async () => {
a = a + (await 10);
console.log('2', a);
};
b();
a++;
console.log('1', a);

//先输出 ‘1’, 1
//在输出 ‘2’, 10
  • 首先函数 b 先执行,在执行到 await 10 之前变量 a 还是 0,因为 await 内部实现了 generator ,generator 会保留堆栈中东西,所以这时候 a = 0 被保存了下来
  • 因为 await 是异步操作,后来的表达式不返回 Promise 的话,就会包装成 Promise.reslove(返回值),然后会去执行函数外的同步代码
  • 同步代码 a++ 与打印 a 执行完毕后开始执行异步代码,将保存下来的值拿出来使用,这时候 a = 0 + 10

上述解释中提到了 await 内部实现了 generator,其实 await 就是 generator 加上 Promise 的语法糖,且内部实现了自动执行 generator

ES Module

ES Module 是原生实现的模块化方案,与 CommonJS 有以下几个区别

  • CommonJS 支持动态导入,也就是 require(${path}/xx.js),后者目前不支持,但是已有提案
  • CommonJS 是同步导入,因为用于服务端,文件都在本地,同步导入即使卡住主线程影响也不大。而后者是异步导入,因为用于浏览器,需要下载文件,如果也采用同步导入会对渲染有很大影响
  • CommonJS 在导出时都是值拷贝,就算导出的值变了,导入的值也不会改变,所以如果想更新值,必须重新导入一次。但是 ES Module 采用实时绑定的方式,导入导出的值都指向同一个内存地址,所以导入值会跟随导出值变化
  • ES Module 会编译成 require/exports 来执行的
// 引入模块 API
import XXX from './a.js';
import { XXX } from './a.js';
// 导出模块 API
export function a() {}
export default function () {}

为什么 ES 模块比 CommonJS 更好?

ES 模块是官方标准,也是 JavaScript 语言明确的发展方向,而 CommonJS 模块是一种特殊的传统格式,在 ES 模块被提出之前做为暂时的解决方案。
ES 模块允许进行静态分析,从而实现像 tree-shaking 的优化,并提供诸如循环引用和动态绑定等高级功能。

Proxy

Proxy 可以理解成,在目标对象之前架设一层“拦截”,外界对该对象的访问,都必须先通过这层拦截,因此提供了一种机制,可以对外界的访问进行过滤和改写。Proxy 这个词的原意是代理,用在这里表示由它来“代理”某些操作,可以译为“代理器”。

var obj = new Proxy(
{},
{
get: function (target, key, receiver) {
console.log(`getting ${key}!`);
return Reflect.get(target, key, receiver);
},
set: function (target, key, value, receiver) {
console.log(`setting ${key}!`);
return Reflect.set(target, key, value, receiver);
},
}
);

Proxy 支持的拦截操作一览,一共 13 种。

  • get(target, propKey, receiver)
    • 拦截对象属性的读取,比如 proxy.foo 和 proxy[‘foo’]。
  • set(target, propKey, value, receiver)
    • 拦截对象属性的设置,比如 proxy.foo = v 或 proxy[‘foo’] = v,返回一个布尔值。
  • has(target, propKey)
    • 拦截 propKey in proxy 的操作,返回一个布尔值。
  • deleteProperty(target, propKey)
    • 拦截 delete proxy[propKey]的操作,返回一个布尔值。
  • ownKeys(target)
    • 拦截 Object.getOwnPropertyNames(proxy)、Object.getOwnPropertySymbols(proxy)、Object.keys(proxy)、for…in 循环,返回一个数组。该方法返回目标对象所有自身的属性的属性名,而 Object.keys()的返回结果仅包括目标对象自身的可遍历属性。
  • getOwnPropertyDescriptor(target, propKey)
    • 拦截 Object.getOwnPropertyDescriptor(proxy, propKey),返回属性的描述对象。
  • defineProperty(target, propKey, propDesc)
    • 拦截 Object.defineProperty(proxy, propKey, propDesc)、Object.defineProperties(proxy, propDescs),返回一个布尔值。
  • preventExtensions(target)
    • 拦截 Object.preventExtensions(proxy),返回一个布尔值。
  • getPrototypeOf(target)
    • 拦截 Object.getPrototypeOf(proxy),返回一个对象。
  • isExtensible(target)
    • 拦截 Object.isExtensible(proxy),返回一个布尔值。
  • setPrototypeOf(target, proto)
    • 拦截 Object.setPrototypeOf(proxy, proto),返回一个布尔值。如果目标对象是函数,那么还有两种额外操作可以拦截。
  • apply(target, object, args)
    • 拦截 Proxy 实例作为函数调用的操作,比如 proxy(…args)、proxy.call(object, …args)、proxy.apply(…)。
  • construct(target, args)
    • 拦截 Proxy 实例作为构造函数调用的操作,比如 new proxy(…args)。

this 的指向有哪几种情况?

this 代表函数调用相关联的对象,通常页称之为执行上下文。

  1. 作为函数直接调用,非严格模式下,this 指向 window,严格模式下,this 指向 undefined;
  2. 作为某对象的方法调用,this 通常指向调用的对象。
  3. 使用 apply、call、bind 可以绑定 this 的指向。
  4. 在构造函数中,this 指向新创建的对象
  5. 箭头函数没有单独的 this 值,this 在箭头函数创建时确定,它与声明所在的上下文相同。

如果对一个函数进行多次 bind,那么上下文会是什么呢?

let a = {};
let fn = function () {
console.log(this);
};
fn.bind().bind(a)(); // => ?

不管我们给函数 bind 几次,fn 中的 this 永远由第一次 bind 决定,所以结果永远是 window。

// fn.bind().bind(a) 等于
let fn2 = function fn1() {
return function () {
return fn.apply();
}.apply(a);
};
fn2();

多个 this 规则出现时,this 最终指向哪里?

首先,new 的方式优先级最高,接下来是 bind 这些函数,然后是 obj.foo() 这种调用方式,最后是 foo 这种调用方式,同时,箭头函数的 this 一旦被绑定,就不会再被任何方式所改变。

事件循环

彻底搞懂JavaScript事件循环

变量提升

彻底解决 JS 变量提升的面试题 | 一题一图,超详细包教包会

创建对象的方法

var o1 = { name: "value" };
var o2 = new Object({ name: "value" });

var M = function () {
this.name = "o3";
};
var o3 = new M();

var P = { name: "o4" };
var o4 = Object.create(P);

原型

  • JavaScript 的所有对象中都包含了一个 __proto__ 内部属性,这个属性所对应的就是该对象的原型
  • JavaScript 的函数对象,除了原型 __proto__ 之外,还预置了 prototype 属性
  • 当函数对象作为构造函数创建实例时,该 prototype 属性值将被作为实例对象的原型 __proto__

原型链

任何一个实例对象通过原型链可以找到它对应的原型对象,原型对象上面的实例和方法都是实例所共享的。

一个对象在查找以一个方法或属性时,他会先在自己的对象上去找,找不到时,他会沿着原型链依次向上查找。

注意: 函数才有 prototype,实例对象只有__proto__, 而函数有的__proto__是因为函数是 Function 的实例对象

instanceof 原理

判断实例对象的__proto__属性与构造函数的 prototype 是不是用一个引用。如果不是,他会沿着对象的__proto__向上查找的,直到顶端 Object。

判断对象是哪个类的直接实例

使用对象.construcor直接可判断

构造函数,new 时发生了什么?

var obj = {};
obj.__proto__ = Base.prototype;
Base.call(obj);
  1. 创建一个新的对象 obj;
  2. 将这个空对象的__proto__成员指向了 Base 函数对象 prototype 成员对象
  3. Base 函数对象的 this 指针替换成 obj, 相当于执行了 Base.call(obj);
  4. 如果构造函数显示的返回一个对象,那么则这个实例为这个返回的对象。 否则返回这个新创建的对象

// 普通写法
function Animal() {
this.name = "name";
}

// ES6
class Animal2 {
constructor() {
this.name = "name";
}
}

继承

在构造函数中 使用Parent.call(this)的方法继承父类属性。

原理: 将子类的 this 使用父类的构造函数跑一遍

缺点: Parent 原型链上的属性和方法并不会被子类继承

function Parent() {
this.name = "parent";
}

function Child() {
Parent.call(this);
this.type = "child";
}

ES5/ES6 的继承除了写法以外还有什么区别?

  • class 声明会提升,但不会初始化赋值。Foo 进入暂时性死区,类似于 let、const 声明变量。
  • class 声明内部会启用严格模式。
  • class 的所有方法(包括静态方法和实例方法)都是不可枚举的。
  • class 的所有方法(包括静态方法和实例方法)都没有原型对象 prototype,所以也没有 construct,不能使用 new 来调用。
  • 必须使用 new 调用 class。
  • class 内部无法重写类名。

JS 变量类型

JS 中有 6 种原始值,分别是:

  1. boolean
  2. number
  3. string
  4. undefined
  5. symbol
  6. null

3 种引用类型:

  1. 对象
  2. 数组
  3. 函数

JS 中使用 typeof 能得到哪些类型?

关于null,虽然是基本变量,但是因为设计的时候null是全 0,而对象是000开头,所以有这个误判。

  1. boolean
  2. number
  3. string
  4. undefined
  5. symbol
  6. object
  7. function
  8. bigint

instanceof 能正确判断对象的原理是什么?

判断一个对象与构造函数是否在一个原型链上

const Person = function () {};
const p1 = new Person();
p1 instanceof Person; // true

var str = "hello world";
str instanceof String; // false

var str1 = new String("hello world");
str1 instanceof String; // true

实现一个类型判断函数

  1. 判断 null
  2. 判断基础类型
  3. 使用Object.prototype.toString.call(target)来判断引用类型

注意: 一定是使用call来调用,不然是判断的 Object.prototype 的类型
之所以要先判断是否为基本类型是因为:虽然Object.prototype.toString.call()能判断出某值是:number/string/boolean,但是其实在包装的时候是把他们先转成了对象然后再判断类型的。 但是 JS 中包装类型和原始类型还是有差别的,因为对一个包装类型来说,typeof 的值是 object

/**
* 类型判断
*/
function getType(target) {
//先处理最特殊的Null
if (target === null) {
return "null";
}
//判断是不是基础类型
const typeOfT = typeof target;
if (typeOfT !== "object") {
return typeOfT;
}
//肯定是引用类型了
const template = {
"[object Object]": "object",
"[object Array]": "array",
// 一些包装类型
"[object String]": "object - string",
"[object Number]": "object - number",
"[object Boolean]": "object - boolean",
};
const typeStr = Object.prototype.toString.call(target);
return template[typeStr];
}

如何判断一个数据是不是 Array

  • Array.isArray(obj)
    • ECMAScript 5 种的函数,当使用 ie8 的时候就会出现问题。
  • obj instanceof Array
    • 当用来检测在不同的 window 或 iframe 里构造的数组时会失败。这是因为每一个 iframe 都有它自己的执行环境,彼此之间并不共享原型链,所以此时的判断一个对象是否为数组就会失败。此时我们有一个更好的方式去判断一个对象是否为数组。
  • Object.prototype.toString.call(obj) == '[object Array]'
    • 这个方法比较靠谱
  • obj.constructor === Array
    • constructor 属性返回对创建此对象的函数的引用

undefined 可被赋值

在 ES3 中(Firefox4 之前),window.undefined 就是一个普通的属性,你完全可以把它的值改变成为任意的真值,但在 ES5 中((Firefox4 之后),window.undefined 成了一个不可写,不可配置的数据属性,它的值永远是 undefined。

var undefined = 1;
alert(undefined); // chrome: undefined, ie8: 1

不管是标准浏览器,还是老的 IE 浏览器,在函数内部 undefined 可作为局部变量重新赋值

function fn() {
var undefined = 100;
alert(undefined); //chrome: 100, ie8: 100
}
fn();

有时候我们需要判断一个变量是不是 undefined,会这样用

if (str === undefined) {
console.log("I am empty");
}

但假如 str 这个变量没声明就会出现报错,用下面的方式会更好一些

if (typeof str === "undefined") {
console.log("I am better");
}

有时候我们会看到这种写法

if (str === void 0) {
console.log("I am real undefind");
}

那是因为 「void 0」的执行结果永远是「undefined」, 即使在某些老旧浏览器 或者在某个函数中 undefined 被重新赋值,我们仍然可以通过void 0得到真正的 undefined

如何判断回收内容

如何确定哪些内存需要回收,哪些内存不需要回收,这是垃圾回收期需要解决的最基本问题。我们可以这样假定,一个对象为活对象当且仅当它被一个根对象 或另一个活对象指向。根对象永远是活对象,它是被浏览器或 V8 所引用的对象。被局部变量所指向的对象也属于根对象,因为它们所在的作用域对象被视为根对 象。全局对象(Node 中为 global,浏览器中为 window)自然是根对象。浏览器中的 DOM 元素也属于根对象。

V8 回收策略

新生代的对象为存活时间较短的对象,老生代中的对象为存活时间较长或常驻内存的对象。分别对新生代和老生代使用 不同的垃圾回收算法来提升垃圾回收的效率。对象起初都会被分配到新生代,当新生代中的对象满足某些条件(后面会有介绍)时,会被移动到老生代(晋升)。

新生代算法

在新生代空间中,内存空间分为两部分,分别为 From 空间和 To 空间。在这两个空间中,必定有一个空间是使用的,另一个空间是空闲的。新分配的对象会被放入 From 空间中,当 From 空间被占满时,新生代 GC 就会启动了。算法会检查 From 空间中存活的对象并复制到 To 空间中,如果有失活的对象就会销毁。当复制完成后将 From 空间和 To 空间互换,这样 GC 就结束了。

老生代算法

老生代中的对象一般存活时间较长且数量也多,使用了两个算法,分别是标记清除算法和标记压缩算法。

在讲算法前,先来说下什么情况下对象会出现在老生代空间中:

  1. 新生代中的对象是否已经经历过一次 Scavenge 算法,如果经历过的话,会将对象从新生代空间移到老生代空间中。
  2. To 空间的对象占比大小超过 25 %。在这种情况下,为了不影响到内存分配,会将对象从新生代空间移到老生代空间中。

什么是内存泄漏?

存泄漏是指程序中已分配的堆内存由于某种原因未释放或者无法释放,造成系统内存的浪费,导致程序运行速度减慢甚至系统奔溃等后果。

常见的内存泄漏的场景

  • 缓存
  • 作用域未释放(闭包)
  • 没有必要的全局变量
  • 无效的 DOM 引用
  • 定时器未清除
  • 事件监听为空白

内存泄漏优化

  • 在业务不需要的用到的内部函数,可以重构到函数外,实现解除闭包。
  • 避免创建过多的生命周期较长的对象,或者将对象分解成多个子对象。
  • 避免过多使用闭包。
  • 注意清除定时器和事件监听器。
  • nodejs 中使用 stream 或 buffer 来操作大文件,不会受 nodejs 内存限制。