点击上方蓝字关注我们
js数据类型有哪些
JavaScript
共有八种数据类型,分别是 Undefined、Null、Boolean、Number、String、Object、Symbol、BigInt。
其中 Symbol 和 BigInt 是ES6 中新增的数据类型:
-
Symbol代表创建后独一无二且不可变的数据类型,它主要是为了解决可能出现的全局变量冲突的问题。
-
BigInt 是一种数字类型的数据,它可以表示任意精度格式的整数,使用 BigInt 可以安全地存储和操作大整数,即使这个数已经超出了 Number 能够表示的安全整数范围。
这些数据可以分为原始数据类型和引用数据类型(复杂数据类型),他们在内存中的存储方式不同。
-
堆: 存放引用数据类型,引用数据类型占据空间大、大小不固定。如果存储在栈中,将会影响程序运行的性能;引用数据类型在栈中存储了指针,该指针指向堆中该实体的起始地址,如
Object
、Array
、Function
。 -
栈: 存放原始数据类型,栈中的简单数据段,占据空间小,属于被频繁使用的数据,如
String
、Number
、Null
、Boolean
。
null和undefined区别
Undefined
和 Null
都是基本数据类型,这两个基本数据类型分别都只有一个值,就是 undefined
和 null
。
-
undefined 代表的含义是未定义,一般变量声明了但还没有定义的时候会返回
undefined
,typeof
为undefined
-
null 代表的含义是空对象,null主要用于赋值给一些可能会返回对象的变量,作为初始化,
typeof
为object
js
复制代码
null == undefined // true
null === undefined //false
instanceof 运算符的实现原理及实现
instanceof
运算符适用于检测构造函数的prototype
属性上是否出现在某个实例对象的原型链上
instanceof
运算符的原理是基于原型链的查找。当使用 obj instanceof Constructor
进行判断时,JavaScript
引擎会从 obj
的原型链上查找 Constructor.prototype
是否存在,如果存在则返回 true
,否则继续在原型链上查找。如果查找到原型链的顶端仍然没有找到,则返回 false
。
instanceof
运算符只能用于检查某个对象是否是某个构造函数的实例,不能用于基本类型的检查,如string
、number
等
typeof 和 instanceof 区别
typeof
与instanceof
都是判断数据类型的方法,区别如下:
-
typeof
会返回一个运算数的基本类型,instanceof
返回的是布尔值 -
instanceof
可以准确判断引用数据类型,但是不能正确判断原始数据类型 -
typeof
虽然可以判断原始数据类型(null
除外),但是无法判断引用数据类型(function
除外)
那为什么typeof判断null为object?
这是 JavaScript
语言的一个历史遗留问题,在第一版JS
代码中用32位比特来存储值,通过值的1-3
位来识别类型,前三位为000
表示对象类型。而null
是一个空值,二进制表示都为0,所以前三位也就是000
,所以导致 typeof null
返回 "object"
为什么0.1+0.2 ! == 0.3,如何让其相等
因为浮点数运算的精度问题。在计算机运行过程中,需要将数据转化成二进制,然后再进行计算。因为浮点数自身小数位数的限制而截断的二进制在转化为十进制,就变成0.30000000000000004,所以在计算时会产生误差。
解决方案
-
将其先转换成整数,再相加之后转回小数。具体做法为先乘10相加后除以10
-
let x=(0.1*10+0.2*10)/10;
console.log(x===0.3) -
使用
number
对象的toFixed
方法,只保留一位小数点。 -
(n1 + n2).toFixed(2)
判断数组的方式有哪些
-
通过
Object.prototype.toString.call()
做判断 -
Object.prototype.toString.call(obj).slice(8,-1) === 'Array';
-
通过原型链做判断
obj.__proto__ === Array.prototype;
-
通过ES6的
Array.isArray()
做判断Array.isArrray(obj);
-
通过
instanceof
做判断obj instanceof Array
对类数组对象的理解,如何转化为数组
类数组也叫伪数组,类数组和数组类似,但不能调用数组方法,常见的类数组有arguments、通过document.getElements
获取到的内容等,这些类数组具有length
属性。
转换方法
-
通过
call
调用数组的slice
方法来实现转换Array.prototype.slice.call(arrayLike)
-
通过
call
调用数组的splice
方法来实现转换Array.prototype.splice.call(arrayLike, 0)
-
通过
apply
调用数组的concat
方法来实现转换Array.prototype.concat.apply([], arrayLike)
-
通过
Array.from
方法来实现转换Array.from(arrayLike)
Array.propotype.slice.call()是什么比如Array.prototype.slice.call(arguments)
这句里,就是把 arguments
当做当前对象。
也就是说 要调用的是 arguments
的 slice
方法,而typeof arguments="Object"
而不是 Array
它没有slice
这个方法,通过这么Array.prototype.slice.call
调用,JS的内部机制应该是 把arguments
对象转化为Array
数组有哪些原生方法?
-
数组和字符串的转换方法:
toString()
、toLocalString()
、join()
其中join()
方法可以指定转换为字符串时的分隔符。 -
数组尾部操作的方法
pop()
和push()
,push
方法可以传入多个参数。 -
数组首部操作的方法
shift()
和unshift()
重排序的方法reverse()
和sort()
,sort()
方法可以传入一个函数来进行比较,传入前后两个值,如果返回值为正数,则交换两个参数的位置。 -
数组连接的方法
concat()
,返回的是拼接好的数组,不影响原数组。 -
数组截取办法
slice()
,用于截取数组中的一部分返回,不影响原数组。 -
数组插入方法
splice()
,影响原数组查找特定项的索引的方法,indexOf()
和lastIndexOf()
迭代方法every()
、some()
、filter()
、map()
和forEach()
方法 -
数组归并方法
reduce()
和reduceRight()
方法 -
改变原数组的方法:
fill()
、pop()
、push()
、shift()
、splice()
、unshift()
、reverse()
、sort()
; -
不改变原数组的方法:
concat()
、every()
、filter()
、find()
、findIndex()
、forEach()
、indexOf()
、join()
、lastIndexOf()
、map()
、reduce()
、reduceRight()
、slice()
、some()
。
substring和substr的区别
它们都是字符串方法,用于截取字符串的一部分,主要区别在于参数不同
-
substring(startIndex, endIndex)
:接收两个参数,一个起始索引和结束索引,来指定字符串范围,如果省略第二个参数,则截取到字符串末尾。 -
substr(startIndex, length)
:接收两个参数,并返回从startIndex
开始,长度为length
的子字符串。如果省略第二个参数,则截取到字符串末尾。
js
复制代码
const str = "Hello, World!";
console.log(str.substring(0, 5)); // 输出: "Hello"
console.log(str.substr(7, 5)); // 输出: "World"
object.assign和扩展运算法是深拷贝还是浅拷贝,两者区别
都是浅拷贝
-
Object.assign()
方法接收的第一个参数作为目标对象,后面的所有参数作为源对象。然后把所有的源对象合并到目标对象中。它会修改了一个对象,因此会触发 ES6 setter。 -
扩展操作符
(…)
使用它时,数组或对象中的每一个值都会被拷贝到一个新的数组或对象中。它不复制继承的属性或类的属性,但是它会复制ES6的symbols
属性。
new操作符的实现原理
new
操作符用来创建一个对象,并将该对象绑定到构造函数的this
上。
new操作符的执行过程:
-
创建一个空对象
-
设置原型,将构造函数的原型指向空对象的
prototype
属性。 -
将
this
指向这个对象,通过apply执行构造函数。 -
判断函数的返回值类型,如果是值类型,返回创建的对象。如果是引用类型,就返回这个引用类型的对象
for…in和for…of的区别
for...in
和for...of
都是JavaScript
中的循环语句,而for…of
是ES6新增的遍历方式,允许遍历一个含有iterator
接口的数据结构(数组、对象等)并且返回各项的值,和ES3
中的for…in
的区别如下
-
for…of
遍历获取的是对象的键值,for…in
获取的是对象的键名; -
for… in
会遍历对象的整个原型链,性能非常差不推荐使用,而for … of
只遍历当前对象不会遍历原型链; -
对于数组的遍历,
for…in
会返回数组中所有可枚举的属性(包括原型链上可枚举的属性),for…of
只返回数组的下标对应的属性值;
总结:for...in
循环主要是为了遍历对象而生,不适用于遍历数组;for...of
循环可以用来遍历数组、类数组对象,字符串、Set
、Map
以及 Generator
对象。
如何使用for…of遍历对象
为什么不能遍历对象
for…of
是作为ES6新增的遍历方式,能被其遍历的数据内部都有一个遍历器iterator接口,而数组、字符串、Map
、Set
内部已经实现,普通对象内部没有,所以在遍历的时候会报错。想要遍历对象,可以给对象添加一个Symbol.iterator
属性,并指向一个迭代器即可
在迭代器里面,通过Object.keys
获取对象所有的key
,然后遍历返回key 、value
。
js
复制代码
var obj = {
a:1,
b:2,
c:3
};
obj[Symbol.iterator] = function*(){
var keys = Object.keys(obj);
for(var k of keys){
yield [k,obj[k]]
}
};
for(var [k,v] of obj){
console.log(k,v);
}
对AJAX的理解,实现一个AJAX请求
AJAX
是 Asynchronous JavaScript and XML 的缩写,指的是通过 JavaScript 的 异步通信,从服务器获取 XML
文档从中提取数据,再更新当前网页的对应部分,而不用刷新整个网页。创建AJAX
请求的步骤:
-
创建一个
XMLHttpRequest
对象。 -
在这个对象上使用
open
方法创建一个HTTP
请求,open
方法所需要的参数是请求的方法、请求的地址、是否异步和用户的认证信息。 -
在发起请求前,可以为这个对象添加一些信息和监听函数。比如说可以通过
setRequestHeader
方法来为请求添加头信息。还可以为这个对象添加一个状态监听函数。一个XMLHttpRequest
对象一共有 5 个状态,当它的状态变化时会触发onreadystatechange
事件,可以通过设置监听函数,来处理请求成功后的结果。当对象的readyState
变为 4 的时候,代表服务器返回的数据接收完成,这个时候可以通过判断请求的状态,如果状态是 2xx 或者 304 的话则代表返回正常。这个时候就可以通过response
中的数据来对页面进行更新了。 -
当对象的属性和监听函数设置完成后,最后调用
send
方法来向服务器发起请求,可以传入参数作为发送的数据体。
js
复制代码
const SERVER_URL = "/server";
let xhr = new XMLHttpRequest();
// 创建 Http 请求
xhr.open("GET", url, true);
// 设置状态监听函数
xhr.onreadystatechange = function() {
if (this.readyState !== 4) return;
// 当请求成功时
if (this.status === 200) {
handle(this.response);
} else {
console.error(this.statusText);
}
};
// 设置请求失败时的监听函数
xhr.onerror = function() {
console.error(this.statusText);
};
// 设置请求头信息
xhr.responseType = "json";
xhr.setRequestHeader("Accept", "application/json");
// 发送 Http 请求
xhr.send(null);
ajax、axios、fetch的区别
ajax
axios
-
支持
Promise
API -
从浏览器中创建
XMLHttpRequest
-
从
node.js
创建http
请求 -
支持请求拦截和响应拦截
-
自动转换
JSON
数据 -
客服端支持防止
CSRF/XSRF
fetch
-
浏览器原生实现的请求方式,ajax的替代品
-
基于标准
Promise
实现,支持async/await
-
fetchtch
只对网络请求报错,对400,500都当做成功的请求,需要封装去处理 -
默认不会带
cookie
,需要添加配置项 -
fetch
没有办法原生监测请求的进度,而XHR
可以。
forEach和map方法有什么区别
两个方法都是用来遍历循环数组,区别如下:
-
forEach()
对数据的操作会改变原数组,该方法没有返回值; -
map()
方法不会改变原数组的值,返回一个新数组,新数组中的值为原数组调用函数处理之后的值;
什么是尾调用,使用尾调用有什么好处?
尾调用就是在函数的最后一步调用函数,在一个函数里调用另外一个函数会保留当前执行的上下文,如果在函数尾部调用,因为已经是函数最后一步,所以这时可以不用保留当前的执行上下文,从而节省内存。但是ES6的尾调用只能在严格模式下开启,正常模式是无效的。
你用过哪些设计模式
-
单例模式:保证类只有一个实例,并提供一个访问它的全局访问点。
-
工厂模式:用来创建对象,根据不同的参数返回不同的对象实例。
-
策略模式:定义一系列的算法,把它们一个个封装起来,并且使它们可以相互替换。
-
装饰器模式:在不改变对象原型的基础上,对其进行包装扩展。
-
观察者模式:定义了对象间一种一对多关系,当目标对象状态发生改变时,所有依赖它对对象都会得到通知。
-
发布订阅模式:基于一个主题/事件通道,希望接收通知的对象通过自定义事件订阅主题,被激活事件的对象(通过发布主题事件的方式被通知)。
如何实现深浅拷贝
深拷贝
-
JSON.stringify()
将js对象序列化,再通过JSON.parse
反序列 -
如果对象中有函数、
undefined
、symbol
时,都会丢失 -
如果有正则表达式、
Error
对象等,会得到空对象
浅拷贝
-
Objec.assign()
拷贝对象 -
扩展运算符
ES6
let、const、var的区别
-
块级作用域: 块作用域由
{ }
包裹,let
和const
具有块级作用域,var
不存在块级作用域。块级作用域解决了ES5中的两个问题: -
内层变量可能覆盖外层变量
-
用来计数的循环变量泄露为全局变量
-
变量提升:
var
存在变量提升,let
和const
不存在变量提升,即在变量只能在声明之后使用,否在会报错。 -
给全局添加属性: 浏览器的全局对象是
window
,Node
的全局对象是global
。var
声明的变量为全局变量,并且会将该变量添加为全局对象的属性,但是let
和const
不会。 -
重复声明:
var
声明变量时,可以重复声明变量,后声明的同名变量会覆盖之前声明的遍历。const
和let
不允许重复声明变量。 -
初始值设置: 在变量声明时,
var
和let
可以不用设置初始值。而const
声明变量必须设置初始值。 -
暂时性死区:在使用
let
、const
命令声明变量之前,该变量都是不可用的。这在语法上,称为暂时性死区。使用var
声明的变量不存在暂时性死区。
箭头函数与普通函数的区别
-
箭头函数是匿名函数,不能作为构造函数,使用
new
关键字。 -
箭头函数没有
arguments
-
箭头函数没有自己的
this
,会获取所在的上下文作为自己的this
-
call()
、applay()
、bind()
方法不能改变箭头函数中的this
指向 -
箭头函数没有
prototype
-
箭头函数不能用作
Generator
函数,不能使用yeild
关键字
Set、Map的区别
Set
-
创建:
new Set([1, 1, 2, 3, 3, 4, 2])
-
add(value)
:添加某个值,返回Set结构本身。 -
delete(value)
:删除某个值,返回一个布尔值,表示删除是否成功。 -
has(value)
:返回一个布尔值,表示该值是否为Set的成员。 -
clear()
:清除所有成员,没有返回值。
Map
-
set(key, val):
向Map
中添加新元素 -
get(key):
通过键值查找特定的数值并返回 -
has(key):
判断Map
对象中是否有Key
所对应的值,有返回true
,否则返回false
-
delete(key):
通过键值从Map
中移除对应的数据 -
clear():
将这个Map
中的所有元素删除
区别
-
Map
是一种键值对的集合,和对象不同的是,键可以是任意值 -
Map
可以遍历,可以和各种数据格式转换 -
Set
是类似数组的一种的数据结构,类似数组的一种集合,但在Set中没有重复的值
map和Object的区别
map
和Object
都是用键值对来存储数据,区别如下:
-
键的类型:
Map
的键可以是任意数据类型(包括对象、函数、NaN
等),而Object
的键只能是字符串或者Symbol
类型。 -
键值对的顺序:
Map
中的键值对是按照插入的顺序存储的,而对象中的键值对则没有顺序。 -
键值对的遍例:
Map
的键值对可以使用for...of
进行遍历,而Object
的键值对需要手动遍历键值对。 -
继承关系:
Map
没有继承关系,而Object
是所有对象的基类。
map和weakMap的区别
它们是 JavaScript
中的两种不同的键值对集合,主要区别如下:
-
map
的键可以是任意类型,weakMap
键只能是对象类型。 -
map
使用常规的引用来管理键和值之间的关系,因此即使键不再使用,map
仍然会保留该键的内存。weakMap
使用弱引用来管理键和值之间的关系,因此如果键不再有其他引用,垃圾回收机制可以自动回收键值对。
说说你对Promise的理解
Promise
是异步编程的一种解决方案,将异步操作以同步操作的流程表达出来,避免了地狱回调。
Promise
的实例有三个状态:
-
Pending
(初始状态) -
Fulfilled
(成功状态) -
Rejected
(失败状态)
Promise
的实例有两个过程:
-
pending
->fulfilled
: Resolved(已完成) -
pending
->rejected
:Rejected(已拒绝)注意:一旦从进行状态变成为其他状态就永远不能更改状态了,其过程是不可逆的。
Promise
构造函数接收一个带有resolve
和reject
参数的回调函数。
-
resolve
的作用是将Promise
状态从pending
变为fulfilled
,在异步操作成功时调用,并将异步结果返回,作为参数传递出去 -
reject
的作用是将Promise
状态从pending
变为rejected
,在异步操作失败后,将异步操作错误的结果,作为参数传递出去
Promise
的缺点:
-
无法取消
Promise
,一旦新建它就会立即执行,无法中途取消。 -
如果不设置回调函数,
Promise
内部抛出的错误,不会反应到外部。 -
当处于
pending
状态时,无法得知目前进展到哪一个阶段(刚刚开始还是即将完成)。
Promise方法
-
promise.then()
对应resolve
成功的处理 -
promise.catch()
对应reject
失败的处理 -
promise.all()
可以完成并行任务,将多个Promise
实例数组,包装成一个新的Promise
实例,返回的实例就是普通的Promise
。有一个失败,代表该Primise
失败。当所有的子Promise
完成,返回值时全部值的数组 -
promise.race()
类似promise.all()
,区别在于有任意一个完成就算完成 -
promise.allSettled()
返回一个在所有给定的promise
都已经fulfilled
或rejected
后的promise
,并带有一个对象数组,每个对象表示对应的promise
结果。
promise.all 和 promise.allsettled 区别
Promise.all()
和 Promise.allSettled()
都是用来处理多个 Promise
实例的方法,它们的区别在于以下几点:
-
all: 只有当所有
Promise
实例都resolve
后,才会resolve
返回一个由所有Promise
返回值组成的数组。如果有一个Promise
实例reject
,就会立即被拒绝,并返回拒绝原因。all
是团队的成功才算,如果有一个人失败就算失败。 -
allSettled: 等所有
Promise
执行完毕后,不管成功或失败, 都会吧每个Promise
状态信息放到一个数组里面返回。
对async/await 的理解
async/await
其实是Generator 的语法糖,它能实现的效果都能用then
链来实现,它是为优化then
链而开发出来的。通过async
关键字声明一个异步函数, await
用于等待一个异步方法执行完成,并且会阻塞执行。async
函数返回的是一个 Promise 对象,如果在函数中 return
一个变量,async
会把这个直接量通过 Promise.resolve()
封装成 Promise
对象。如果没有返回值,返回 Promise.resolve(undefined)
async/await对比Promise的优势
-
代码可读性高,
Promise
虽然摆脱了回掉地狱,但自身的链式调用会影响可读性。 -
相对
Promise
更优雅,传值更方便。 -
对错误处理友好,可以通过
try/catch
捕获,Promise
的错误捕获⾮常冗余
谈谈你对ES6的理解
-
解构赋值
-
扩展运算符
-
箭头函数
-
模版字符串
-
Set
、Map
集合 -
新增
class
类 -
Proxy
-
Promise
-
…
ES6模块和CommonJS模块有什么区别
-
语法不同:
ES6
模块使用import
和export
关键字来导入和导出模块,而CommonJS
模块使用require
和module.exports
或exports
来导入和导出模块。// ES6 模块
import { foo } from './module';
export const bar = 'bar';
// CommonJS 模块
const foo = require('./commonjs');
exports.bar = 'bar'; -
异步加载:
ES6
模块支持动态导入(dynamic import),可以异步加载模块。这使得在需要时按需加载模块成为可能,从而提高了性能。CommonJS
模块在设计时没有考虑异步加载的需求,通常在模块的顶部进行同步加载。
原型
-
prototype : js通过构造函数来创建对象,每个构造函数内部都会一个原型
prototype
属性,它指向另外一个对象,这个对象包含了可以由该构造函数的所有实例共享的属性和方法。 -
proto: 当使用构造函数创建一个实例对象后,可以通过
__proto__
访问到prototype
属性。 -
constructor:实例对象通过这个属性可以访问到构造函数
原型链
每个实例对象都有一个__proto__
属性指向它的构造函数的原型对象,而这个原型对象也会有自己的原型对象,一层一层向上,直到顶级原型对象null
,这样就形成了一个原型链。
当访问对象的一个属性或方法时,当对象身上不存在该属性方法时,就会沿着原型链向上查找,直到查找到该属性方法位置。
原型链的顶层原型是Object.prototype
,如果这里没有就只指向null
实现寄生组合继承
利用Object.create()
方法,将子类的原型指向父类,实现继承父类的方法属性,修改时也不影响父类。
js
复制代码
function Parent(name) {
this.name = name;
this.colors = ['red', 'green', 'blue'];
}
Parent.prototype.sayName = function() {
console.log(this.name);
};
function Child(name, age) {
// 执行父类构造函数
Parent.call(this, name);
this.age = age;
}
// 将子类的原型 指向父类
Child.prototype = Object.create(Parent.prototype);
// 此时的狗早函数为父类的 需要指回自己
Child.prototype.constructor = Child;
Child.prototype.sayAge = function() {
console.log(this.age);
};
var child1 = new Child('Tom', 18);
child1.sayName(); // 'Tom'
child1.sayAge(); // 18
对闭包的理解已经它的使用场景
闭包是指有权访问另一个函数作用域中变量的函数,创建闭包的最常见的方式就是在一个函数内创建另一个函数,创建的函数可以访问到当前函数的局部变量。
闭包优点:
-
创建全局私有变量,避免变量全局污染
-
可以实现封装、缓存等
闭包缺点:
-
创建的变量不能被回收,容易消耗内存,使用不当会导致内存溢出
-
解决: 在不需要使用的时候把变量设为
null
使用场景:
-
用于创建全局私有变量
-
封装类和模块
-
实现函数柯里化
闭包一定会造成内存泄漏吗?
闭包并不一定会造成内存泄漏,如果在使用闭包后变量没有及时销毁,可能会造成内存泄漏的风险。只要合理的使用闭包,就不会造成内存泄漏。
对作用域、作用域链的理解
作用域是一个变量或函数的可访问范围,作用域控制着变量或函数的可见性和生命周期。
-
全局作用域:可以全局访问
-
最外层函数和最外层定义的变量拥有全局作用域
-
window
上的对象属性方法拥有全局作用域 -
为定义直接复制的变量自动申明拥有全局作用域
-
过多的全局作用域变量会导致变量全局污染,命名冲突
-
函数作用域:只能在函数中访问使用哦
-
在函数中定义的变量,都只能在内部使用,外部无法访问
-
内层作用域可以访问外层,外层不能访问内存作用域
-
ES6中的块级作用域:只在代码块中访问使用
-
使用ES6中新增的
let
、const
什么的变量,具备块级作用域,块级作用域可以在函数中创建(由{}包裹的代码都是块级作用域) -
let
、const
申明的变量不会变量提升,const
也不能重复申明 -
块级作用域主要用来解决由变量提升导致的变量覆盖问题
作用域链:变量在指定的作用域中没有找到,会依次向一层作用域进行查找,直到全局作用域。这个查找的过程被称为作用域链。
call() 、bind()、 apply() 的区别?
-
都可以用作改变
this
指向 -
call
和apply
的区别在于传参,call
、bind
都是传入对象。apply
传入一个数组。 -
call
、apply
改变this
指向后会立即执行函数,bind
在改变this
后返回一个函数,不会立即执行函数,需要手动调用。
连续多个 bind,最后this指向是什么?
在 JavaScript
中,连续多次调用 bind
方法,最终函数的 this
上下文是由第一次调用 bind 方法的参数决定的
js
复制代码
const obj1 = { name: 'obj1' };
const obj2 = { name: 'obj2' };
const obj3 = { name: 'obj3' };
function getName() {
console.log(this.name);
}
const fn1 = getName.bind(obj1).bind(obj2).bind(obj3);
fn1(); // 输出 "obj1"
浏览器的垃圾回收机制
垃圾回收:JavaScript
代码运行时,需要分配内存空间来储存变量和值。当变量不再参与运行时,就需要系统收回被占用的内存空间。如果不及时清理,会造成系统卡顿、内存溢出,这就是垃圾回收。
在 V8 中,会把堆分为新生代和老生代两个区域,新生代中存放的是生存时间短的对象,老生代中存放生存时间久的对象:
-
Major GC(主垃圾回收器):主要负责老生代垃圾的回收
-
内存占用比较小
-
Minor GC(副垃圾回收器):主要负责新生代垃圾的回收
-
对象的占用空间大 对象存活时间长
新生代(副垃圾回收器)
副垃圾回收器主要负责新⽣代的垃圾回收。大多数的对象最开始都会被分配在新生代,该存储空间相对较小,分为两个空间:from 空间(对象区)和 to 空间(空闲区)。
-
新增变量会放到
To
空间,当空间满后需要执行一次垃圾清理操作 -
对垃圾数据进行标记,标记完成后将存活的数据复制到From空间中,有序排列
-
交换两个空间,原来的
To
变成From
,旧的From
变成To
老生代(主垃圾回收器)
主垃圾回收器主要负责⽼⽣代中的垃圾回收。存储一些占用空间大、存活时间长的数据,采用标记清除算法进行垃圾回收。
主要分为标记、清除两个阶段。
-
标记:将所有的变量打上标记0,然后从根节点(
window
对象、DOM树等)开始遍历,把存活的变量标记为1 -
清除:清除标记为0的对象,释放内存。清除后将1的变量改为0,方便下一轮回收。
对⼀块内存多次执⾏标记清除算法后,会产⽣⼤量不连续的内存碎⽚。⽽碎⽚过多会导致⼤对象⽆法分配到⾜够的连续内存,于是⼜引⼊了另外⼀种算法——标记整理。
标记整理的标记过程仍然与标记清除算法⾥的是⼀样的,先标记可回收对象,但后续步骤不是直接对可回收对象进⾏清理,⽽是让所有存活的对象都向⼀端移动,然后直接清理掉这⼀端之外的内存。
引用计数法
一个对象被引用一次,引用数就+1,反之就-1。当引用为0,就会出发垃圾回收。
这种方式会产生一个问题,在循环引用时,引用数永远不会为0,无法回收。
哪些情况会导致内存泄漏
-
意外的全局变量:由于使用未声明的变量,而意外的创建了一个全局变量,而使这个变量一直留在内存中无法被回收。
-
被遗忘的计时器或回调函数:设置了
setInterval
定时器,而忘记取消它,如果循环函数有对外部变量的引用的话,那么这个变量会被一直留在内存中,而无法被回收。 -
脱离
DOM
的引用:获取一个DOM
元素的引用,而后面这个元素被删除,由于一直保留了对这个元素的引用,所以它也无法被回收。 -
闭包:不合理的使用闭包,从而导致某些变量一直被留在内存当中。
原文始发于微信公众号(猿来是前端):前端面试必备八股文合集之JavaScript
版权声明:本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 举报,一经查实,本站将立刻删除。
文章由极客之音整理,本文链接:https://www.bmabk.com/index.php/post/250416.html