JavaScript - 高级 面试题总结
1. JS中的作用域、预解析、变量声明提升
- 作用域 :
- 简单来说,就是变量的有效范围,在一定的空间里可以对数据进行读写操作,这个空间就是数据的作用域;
- 全局作用域 :
- 最外层函数定义的变量用于全局作用域,即对任何内部函数来说,都是可以访问的;
- 局部作用域 :
- 一般只在固定的的代码片段内部可以访问到,而对于函数外部都是无法访问的;
- 块级作用域 :
- 凡是代码块就可以划分变量的作用域,这种作用域的规则就叫块级作用域;
- 块级作用域包含函数作用域;
- 预解析 :
- JS代码的执行是由浏览器中的JS解析器来执行的,JS解析器执行代码的时候,分为两个过程:预解析过程、代码执行过程;
- 预解析过程:
- 把用var声明的变量提升到当前作用域的最前面,只提升声明部分,不提升赋值;
- 把函数的声明提升到当前作用域的最前面,只提升声明部分,不提升调用;
- 先提升
function
,再提升var
;
- 变量提升 :
- 变量提升:
- 定义变量的时候,变量的声明会被提升到当前作用域的最上面,变量的赋值不会提升;
- 函数提升:
- JS解析器首先会把当前作用域的函数声明提升到整个作用的最前面;
- 函数声明提升的特点,在函数声明的前面,可以调用这个函数;
- 变量提升:
2. 变量提升 和 函数提升 的区别?
- 变量提升 :
- 简单来说就是在JS代码执行前解析引擎先进行预编译,预编译期间会将变量声明和函数声明提升到对应作用域的最前面,函数内声明的变量只会提升至该函数作用域最顶层,当函数内部定义了一个变量与外部相同时,那么函数内部的这个变量就会被上升的最顶端;
- 函数提升 :
- 函数提升只会提升函数声明式的写法,函数表达式不存在函数提升这一说法;
- 函数提升的优先级大与变量提升的优先级,即函数提升在变量提升之上;
3. 解释一下作用域链
- 作用域:变量起作用的范围,在一定空间内,可以对数据进行读写操作,这个空间就是数据的作用域;
- 嵌套关系的作用域串联起来就形成了作用域链;
- 本质 :
- 底层变量的访问机制;
- 在函数执行时,会优先查找当前作用域下的变量,如果当前作用域查找不到,就会依次逐级向上查找,直到全局作用域;
4. 如何延长作用域链?
- 执行环境 :
- 也叫做上下文(
context
),在全局中也就是window,在函数中执行环境就是函数体;
- 也叫做上下文(
- 变量对象 :
- 在每个执行环境中,都会存在一个变量对象,环境中的所有变量和函数都保存在这个对象中。虽然我们编写代码的时候不能访问它,但是内部解析器在处理的时候回在后台使用它;
- 执行环境的类型有两种,全局和局部(函数),但是有些语句可以在作用域链的前端临时增加一个变量对象,该变量对象会在代码执行后被移除;
- 具体来说,就是执行这两个语句时,作用域链都会得到加强:
try-catch
语句的catch
块:会创建一个新的变量对象,包含的是被抛出异常的错误对象的声明;with
语句:with
语句会将指定的对象添加到作用域链中;
5. 什么是闭包
- 一个作用域可以访问另一个函数内部的局部变量,或者说一个函数(子函数)访问另一个函数(父函数)中的变量,此时就会有闭包产生,那么这个变量所在的函数我们就成为闭包函数;
- 简单来说,就是一个内部函数访问了外部函数的变量,此时就会有闭包产生;
- 优点 :
- 延伸了变量的作用范围;
- 因为闭包函数中的局部变量不会等着闭包函数执行完就销毁,因为还有别的函数要调用它,只有等着所有的函数都调用完了他才会销毁闭包造成的内存泄漏,如何解决:用完之后手动释放;
- 实现数据的私有化;
- 延伸了变量的作用范围;
- 注意 :
- 由于闭包会使得函数中的变量都被保存在内存中,内存消耗大,所以不能滥用闭包,否则会造成网页的性能问题,解决方法是,在退出函数之前,将不使用的局部变量全部删除;
- 闭包会在父函数外部,改变父函数内部变量的值;
6. 什么是内存泄漏?
- 内存泄漏也称作"存储泄露",用动态存储分配函数动态开辟的空间,在使用完毕后未释放,结果导致一致占据该内存单元,直到程序结束。
- 说白了,就是该内存空间使用完毕之后未回收;
7. 哪些操作会导致内存泄漏?
setTimeout
的第一个参数使用字符串而非函数的话,会引发内存泄漏;- 闭包、控制台日志、循环(两个对象彼此引用且彼此保留时,就会产生一个循环);
8. 内存泄漏的处理方式?
- 意外的全局变量:
- 可以在JS文件开头添加
use strict
,使用严格模式,这样在严格模式下解析JS可以防止意外的全局变量,在使用完毕之后,对其赋值为null
或重新分配;
- 可以在JS文件开头添加
- 被遗忘的计时器或回调函数;
- 使用完毕之后需要清除定时器
9. 箭头函数 和 普通函数 的区别?
- 动态参数(
arguments
):- 普通函数有;
- 箭头函数没有;
this
:- 普通函数有;
- 箭头函数没有
this
;
- 箭头函数不能是构造函数;
- 因为构造函数内部需要
this
;
- 因为构造函数内部需要
- 箭头函数是匿名函数;
- 箭头函数和普通函数有些语法不同(箭头函数可以简写);
10. new操作符干了什么?
- 隐式的创建一个对象;
- 将构造函数的this指向该对象;
- 执行构造函数函数体,修改this,给对象添加属性和方法;
- 返回新对象;
11. ES6新增特性
- 展开运算符;
- 解构赋值;
- 箭头函数;
- async/await;
- Map和Set;
- 模板字符串;
- 数组新方法、字符串新方法;
- 特殊的运算符;
- let、const;
- 函数参数默认值;
- Promise;
12. 说说你对原型的理解
- JS规定,每一个构造函数内部都有一个
prototype
属性,指向另一个对象,我们把该对象称为原型对象; - 在JS中,原型也是一个对象,通过原型可以实现对象的属性继承,JS的对象中包含了一个
prototype
内部属性,这个属性所对应的就是该对象的原型; prototype
属性作为对象的内部属性,是不能直接访问的,所以为了方便查看一个对象的原型,Firefox和Chrome内核的JS引擎提供了一个proto
这个非标准的访问器;- 原型的主要作用就是为了实现继承与扩展对象;
13. 介绍下原型链
- JS原型:每个对象都会在其内部初始化一个属性,就是
prototype
(原型); - 原型链:
- 当访问对象的某个属性的时候,会先在该对象的本身属性上查找,如果没有找到,就会去该对象的原型上查找(
实例.__proto__
)。如果还没有找到,就会去原型的原型上查找(构造函数的prototype
的__proto__
指向的原型),一直查找到null
(object.prototype.__ proto __
)为止,如果都没有查找到,得到的就是undefined
,将这种链式的查找机制称为原型链;
- 当访问对象的某个属性的时候,会先在该对象的本身属性上查找,如果没有找到,就会去该对象的原型上查找(
14. 继承的实现
- 原型链的继承 :
- 优点:可以访问父类和原型上的的属性和方法;
- 缺点:如果继承的是引用数据类型,其中某一个子类对数据进行了修改,全部的都会收到影响,造成实例共享;
- 构造函数继承 :
- 优点:可以保证每个子类维护自己的属性和方法;
- 缺点:无法访问原型上的属性和方法;
- 组合式继承 :
- 将原型链继承和构造函数继承进行组合;
- 优点:即可以访问原型上的属性和方法,又可以保证每个子类维护好自己的数据;
- 缺点:每次创建一个实例,父类都会被执行一次;
15. 介绍this的各种情况
- 普通函数:
- 谁调用,this指向谁;
- 以函数调用时,this永远都是window;
- 以方法调用时,this是调用方法的对象;
- 当调用者不明确的时候,this指向window;
- 构造函数和原型中的this,都指向实例对象;
- 箭头函数没有this,沿用的是它创建环境所在的this;
- 特殊情况:
- 通常意义上的this指向为最后调用它的对象,这里需要注意的一点就是:
- 如果返回值是一个对象,那么this指向的就是那个返回的对象;
- 如果返回值不是一个对象,那么this还是指向函数的实例;
- 通常意义上的this指向为最后调用它的对象,这里需要注意的一点就是:
16. 数组中的 forEach 和 map 有什么区别?
- 相同点 :
- 都是循环遍历数组中的每一项;
- 都是接收一个回调函数,回调函数有三个参数:
item
、index
、arr
; - 匿名函数中的
this
都是指向window
,只能遍历数组,都不会改变原数组;
- 区别 :
map
:map
方法返回一个新数组,数组中的元素为原始数组调用回调函数处理之后的值;map
方法不会对空数组进行检测;
forEach
:forEach
方法用来调用数组中的每个元素,将元素传给回调函数;forEach
对于空数组是不会调用回调函数的,无论arr
是不是数组,forEach
返回的都是undefined
,这个方法只是将数组中的每一项作为callback
的参数执行一次;
17. split() 和 join() 的区别?
split()
是把一字符串(根据某个分隔符)分割成若干个元素存放在一个数组里面,即切割成数组的形式;join()
是把数组中的元素挨个拼接成一个字符串;
18. call 和 apply、bind的区别
- 共同点 :
- 都是用来改变函数的this的指向;
- 第一个参数都是tis要指向的对象;
- 都可以利用后续参数进行传参;
- 不同点 :
call
和apply
都是调用函数,只是传参形式不同,call
接收的是参数列表 ,而apply
接收的是参数数组,返回值都是函数的返回值;bind
返回一个新函数(一个已经改变好this指向的函数),参数和call相同,接收的是参数列表;
19. 数组常用方法有哪些?
- 改变原始数组 :
arr.push(参数)
--- 向数组末尾追加一些元素- 返回值:追加元素之后数组的长度
arr.unshift(参数)
--- 向数组起始位置插入一些元素- 返回值:插入元素之后数组的长度
arr.pop()
--- 删除数组最后一个元素- 返回值:被删除的元素
arr.shift()
--- 删除数组第一个元素- 返回值:被删除的元素
arr.splice(起始元素索引, 删除几个, 新增/替换的元素)
--- 删除/替换/添加元素- 返回值:新数组
arr.sort()
--- 数组排序- 语法:
- 降序:
arr.sort((a, b) => a - b)
- 升序:
arr.sort((a, b) => b - a)
- 降序:
- 返回值:新数组(排序好的数组)
- 语法:
arr.reverse()
--- 反转数组- 返回值:新数组
- 不改变原始数组 :
arr.forEach(function(item, index, arr) { 函数体 })
➡ 循环遍历数组- 没有返回值
arr.map(function(item, index, arr) { 函数体 })
➡ 迭代数组(映射数组)- 返回值:处理之后的新数组
arr.filter(function(item, index, arr) { 函数体 })
➡ 筛选数组- 返回值:新数组(将满足条件(条件为true)的元素筛选出来放心一个新数组并返回)
arr.reduce(function(prev(累计值), item, index, arr) { 函数体 }, 0(起始值))
- 返回值:返回函数累计的处理结果
arr.join('连接符号')
arr.some(function(item, index, arr) { 函数体 })
➡ 判断数组中是否有满足条件的元素- 返回值:布尔值(true:只要有一个满足就是true,false:都不满足)
arr.every(fucntion(item, index, arr) { 函数体 })
➡ 判断数组中的元素是否都满足条件- 返回值:布尔值(true:都满足,false:只要有一个不满足就是false)
arr.concat(多个数组)
➡ 合并数组- 返回值:新数组(合并之后的数组)
arr.slice(开始索引[, 结束索引 ])
➡ 提取数组元素- 返回值:新数组(存放的是提取出来的元素)
arr.find(function(item, index, arr) { 函数体 })
➡ 返回数组中满足条件的第一个元素- 返回值:有满足条件的元素就返回该元素,否则就是undefined
arr.indexOf(元素[, 开始索引])
➡ 找到指定元素的索引- 返回值:有该元素,就是第一个满足条件元素的索引;没有该元素就是 -1
arr.includes(元素)
➡ 判断数组中是否有某个元素- 返回值:布尔值(true:有这个元素;false:没有该元素)
arr.flat(数组维数 / Infinity)
➡ 数组扁平化- 返回值:降维之后的数组
20. 字符串常用方法有哪些?
str.substring(开始索引[, 结束索引])
➡ 截取字符串- 返回值:新字符产(指定部分的字符产)
str.split('分隔符')
➡ 将字符串拆分成数组- 返回值:数组
str.replace(旧字符,新字符)
➡ 替换字符串中指定的字符- 返回值:替换之后的字符串
str.startsWith(目标字符[, 检测位置索引号])
➡ 检测是否以某段字符开头- 返回值:布尔值
str.endsWith(目标字符[, 检测位置索引号])
➡ 检测是否以某段字符结尾- 返回值:布尔值
str.includes(目标字符[, 检测位置索引号])
➡ 判断一个字符串是否包含在另一个字符串中- 返回值:布尔值
str.toUpperCase()
➡ 将字符串全部转为大写字母str.toLowerCase()
➡ 将字符串全部转为小写字母str.indexOf(目标字符)
➡ 得到目标字符在字符串中的索引- 返回值:有该字符:得到该字符的索引值;没有该字符:-1
21. EventLoop事件循环机制
- JS在执行代码的时候,将任务分为同步任务和异步任务,将同步任务放在主线程执行栈中执行,异步任务在异步队列中排队等候;
- 事件循环是一种轮询机制,先执行主线程里面的同步任务,待所有的同步任务执行完毕,系统就会依次读取任务队列中的异步任务;
- 异步任务分为宏任务和微任务,宏任务在宏任务队列中,微任务在微任务队列中,宏任务和微任务是交替执行的,在执行宏任务之前,先检查微任务队列中是否有微任务要执行,如果要,就先执行完所有的微任务,再去执行宏任务,每执行完一个宏任务都会区微任务队列中检查是否有微任务要执行,如果有,就先执行完所有的微任务再去执行下一个宏任务,如果没有,就继续执行宏任务,将这种循环不断的机制称为事件循环;
22. 常见的宏任务和微任务
- 宏任务 :
- 异步Ajax请求;
- 定时器;
setInterval()
;setTimeout()
;
- 相关的文件操作;
- 微任务 :
Promise.then()、Promise.catch()、Promise.finally()
;- ❗ 注意: 非
new Promise
;
- ❗ 注意: 非
process.nextTick()
(node中);
23. 防抖和节流
-
防抖 (
debounce
):- 是在事件发生一段时间后再执行,如果这段时间内继续触发新的事件,那么取消之前的事件,只执行最新的事件;
- 原理:
- 维护一个计时器,规定在延迟时间后触发函数,但是在延迟时间内再次触发的话,就会取消之前的计时器而重新设置,只有最后一次操作才能触发;
jsconst div = document.querySelector('div'); let i = 0; div.addEventListener('mousemove', debounce(mouseMove, 200)); function mouseMove() { div.innerHTML = ++i; } function debounce(fn, t) { let timerId = null; return function () { if (timerId) clearTimeout(timerId); timerId = setTimeout(fn, t); } }
-
节流 (
throttle
):- 一段时间内只执行一次事件,执行结束后才能继续执行新的事件;
- 原理:
- 通过判断是否到达一定时间来触发函数;
jslet i = 0; const div = document.querySelector('div'); div.addEventListener('mousemove', throttle(mouseMove, 200)); function mouseMove() { div.innerHTML = ++i; } function throttle(fn, t) { let timeStart = 0; return function () { const timeEnd = new Date().getTime(); if (timeEnd - timeStart > t) { fn(); timeStart = timeEnd; } } }
-
应用场景:
- 防抖:
- 搜索联想;
- window触发resize的时候,不断的调整浏览器窗口大小会不断触发这个事件,用防抖来让其只触发一次;
- 节流:
- 鼠标不断点击触发;
- 监听滚动事件;
- 防抖:
-
区别 :
- 节流:不管事件触发多频繁,都会保证在规定时间内一定会执行一次真正的事件处理函数;
- 防抖:只是在最后一次事件后才会触发一次函数;
24. 深拷贝和浅拷贝
- 深拷贝 :
- 是将一个对象从内存中取出一份,从内存中开辟一块新的空间存放该对象,且修改新对象不会对原对象造成影响;
- 简单来说,深拷贝,拷贝的就是堆里面的数据;
- 实现方式:
-
递归;
jsconst obj = { name: '邵秋华', age: 23, city: ['海口', '武汉', '上海', '北京'], xiXi: { a: 1, b: 2, c: { d: 3 } }, sayHi: () => { }, girl: undefined } // const newObj = JSON.parse(JSON.stringify(obj)) function cloneDeep(newObj, oldObj) { Object.keys(oldObj).forEach(item => { if (Array.isArray(oldObj[item])) { newObj[item] = [] cloneDeep(newObj[item], oldObj[item]) } else if (oldObj[item] instanceof Object) { newObj[item] = {} cloneDeep(newObj[item], oldObj[item]) } else { newObj[item] = oldObj[item] } }) } const newObj = {} cloneDeep(newObj, obj) newObj.xiXi.c.d = 18 console.log(obj, newObj);
-
JSON(序列化与反序列化);
- 原理:先转换成字符串(基本数据类型),再转为引用数据类型(再堆里面重新开辟一块空间)
- ❗ 注意:不能识别 函数 和
undefined
;
-
_.cloneDeep()
;
-
- 浅拷贝 :
- 创建一个新对象,这个新对象里面有着原对象数据的一份精确拷贝,如果属性值是基本数据类型,拷贝的就是数据的值,如果数据是引用数据类型,拷贝的就是该数据的地址,所以修改新对象的属性值会对就对象造成影响;
25. 浅拷贝和直接赋值的区别
- 直接赋值 :
- 当我们直接把一个对象赋值给另一个对象的时候,赋的值其实是该对象在栈中的地址,而不是堆中的数据;
- 也就是说两个对象用的是同一个地址,指向堆中的同一块存储空间,无论哪个对象发生改变,其实改变的是存储空间里面的数据,因此两个对象是联动的;
- 浅拷贝 :
- 重新在堆中创建一块新的存储空间存放数据,拷贝前后对象的基本数据类型互不影响,但拷贝前后对象的引用类型的属性值还是用的同一个地址,会相互影响;
26. 数组去重
-
Set + Array.from
- 文字描述 :
- 使用 Set()方法,Set中的元素只允许出现一次;
- 单纯使用Set方法得到的是一个伪数组
- 代码实现:
jsconst arr = [11, 22, 23, 11, 67, 23, 5, 6, 5, 90, 78, 67]; console.log(Array.from(new Set(arr)));
- 文字描述 :
-
Map + filter
jsconst arr = [1,2,3,4,3,2,3,4,6,7,6]; let unique = (arr) => { let seen = new Map(); return arr.filter((item) => !seen.has(item) && seen.set(item, 1)); } console.log(unique(arr));
-
forEach + includes + push
- 文字描述 :
- 声明一个空数组,使用forEach循环遍历数组,判断新数组里面有没有我要push的这个元素,如果有,就不追加,如果没有就追加
- 代码展示:
jsconst arr = [11, 22, 23, 11, 67, 23, 5, 6, 5, 90, 78, 67]; const newArr = []; arr.forEach(item => { if (!newArr.includes(item)) newArr.push(item); }); console.log(newArr);
- 文字描述 :
-
forEach + indexOf + push
-
文字描述 :
- 声明一个空数组,遍历数组,判断新数组里面有没有要追加的元素,如果有就不追加,否则就追加
-
代码展示:
jsconst arr = [11, 22, 23, 11, 67, 23, 5, 6, 5, 90, 78, 67]; const newArr = []; arr.forEach(item => { if (newArr.indexOf(item) === -1) newArr.push(item); }); console.log(newArr);
-
-
sort + forEach+ splice
-
文字描述 :
- 使用sort()方法先对数组进行排序,然后判断当前元素和下一个元素是否相等,如果相等就删除当前元素或下一个元素
-
代码展示:
jsconst arr = [11, 22, 23, 11, 67, 23, 5, 6, 5, 90, 78, 67]; arr.sort((a, b) => a - b); arr.forEach((item, index) => { if (item === arr[index + 1]) arr.splice(index, 1); }); console.log(arr);
-
-
forEach + find
-
文字描述 :
- 声明一个空数组,循环遍历数组,在新数组里面能不能找到当前要追加的元素,如果能找到就不追加,找不到就追加
-
代码展示:
jsconst arr = [11, 22, 23, 11, 67, 23, 5, 6, 5, 90, 78, 67]; const newArr = []; arr.forEach(item => { if (!(newArr.find(item1 => item === item1))) newArr.push(item); }); console.log(newArr)
-
-
双层for循环
-
代码展示:
jsconst arr = [11, 22, 23, 11, 67, 23, 5, 6, 5, 90, 78, 67]; const newArr = []; for (let i = 0; i < arr.length; i++) { newArr.push(arr[i]); for (let j = 0; j < newArr.length; j++) { if (newArr.includes(arr[i])) newArr.splice(i, 1); } } console.log(newArr);
-
-
filter + indexOf
-
文字描述 :
- 筛选出满足条件的元素,条件:用indexOf方法获取原数组的索引值,如果原数组的索引值和当前元素的索引值相等,就返回到一个新的数组
-
代码展示:
jsconst arr = [11, 22, 23, 11, 67, 23, 5, 6, 5, 90, 78, 67]; const newArr = arr.filter((item, index) => { return arr.indexOf(item) === index; }); console.log(newArr);
-
27. 数组的随机排序
js
// 声明两个空数组
// 第一个空数组:存放从0到arr.length - 1的随机数
// 第二个空数组:用来接收按照第一个空数组里面的元素取原数组里面的元素
const arr = [1, 2, 3, 4, 5];
const a = [];
const newArr = [];
for (let i = 0; i >= 0; i++) {
let b = Math.floor(Math.random() * arr.length);
if (!a.includes(b)) a.push(b);
if (a.length === arr.length) break;
}
a.forEach(index => newArr.push(arr[index]));
console.log(newArr);
arr.sort(() => Math.random() - 0.5);
console.log(arr);
28. cookie、sessionStorage、localStorage的区别
- cookie :
- 是网站为了标识用户身份而存储在本地终端上的数据(通常是经过加密的);
- 如果不给
cookie
设置过期时间,则表示这个cookie
的生命周期为浏览器会话期间,只要关闭浏览器窗口,cookie
就消失了;
cookie
数据始终在同源http
请求中携带(也就是说cookie
在浏览器和服务器之间来回传递),而sessionStorage
和localStorage
不会主动把数据发送给服务器;- 存储大小限制 :
cookie
:不能超过4KB
;sessionStorage
和localStorage
:虽然也有存储限制,但相比cookie
要大得多,可达到5M
甚至更大;
- 数据的有效期不同 :
cookie
:只在设置的过期时间之前有效,即使关闭页面或浏览器也依然有效;sessionStorage
:只在当前页面没有关闭之前有效;localStorage
:始终有效,即使关闭窗口和浏览器也依然有效,除非手动清除;
- 作用域不同 :
cookie
:在所有同源窗口中都是共享的;sessionStorage
:只在当前窗口中共享;localStorage
:在所有同源窗口中共享;
cookie
并不一定都能通过js获取到,如果设置了httponly
,是获取不到的;
29. 多窗口之间sessionStorage可以共享吗?
- 多窗口之间
sessionStorage
不可以共享状态,但是在某些特定场景下新开的页面恢复至之前页面的sessionStorage
; - 有两种新开的页面会复制之前的
sessionStorage
:window.open('同源页面');
a标签
;
30. 什么是任务队列?(了解)
- 任务队列(task queue)主要分为两种:
- 宏任务 (macrotask):
- 在新标准中叫 task;
- 主要包括:
script(整体代码)、setTimeout、setInterval、setImmediate、I/O、ui、rendering
;
- 微任务 (microtask):
- 在新标准中叫 jobs;
- 主要包括:
process.nextTick()、Promise.then()、Promise.catch()、Promise.finally()、MutationObserver(H5新特性)
;
- 拓展:
- 同步任务 :
- 在主线程上,排队执行的任务,只有前一个任务执行完毕,才能执行下一个任务;
- 异步任务 :
- 不进入主线程,而进入"任务队列"的任务,只有"任务队列"通知主线程,某个异步任务可以执行了,该任务才会进入主线程执行;
- 同步任务 :
31. 栈和队列的区别?(了解)
- 栈的插入和删除操作都是在一端进行的,而队列的操作却是在两端进行的;
- 列队先进先出,栈先进后出;
- 栈只允许在一端进行插入和删除,而队列允许在一段进行插入,在另一端进行删除;
32. 栈 和 堆 的区别?(了解)
- 栈区 (stack):
- 由编译器自动分配释放,存放函数的参数,局部变量的值等;
- 堆区 (heap):
- 一般由程序员分配释放,若程序员不释放,程序结束时可能由OS回收;
- 栈 (数据结构):
- 一种先进后出的数据结构;
- 堆 (数据结构):
- 堆可以被看成是一棵树,如堆排序;
33. 什么是进程、什么是线程,它们之间是什么关系(了解)
- 进程 :
- 程序执行时的一个实例;
- 每个进程都有独立的内存地址空间;
- 系统进资源分配和调度的基本单位;
- 进程里的堆,是一个进程中最大一块内存,被进程的所有线程共享,进程创建时分配,主要存放 new 创建的对象实例;
- 进程里的方法区,是用来存放进程中的代码片段,是线程共享的;
- 在多线程OS中,进程不是一个可执行的实体,即一个进程至少创建一个线程去执行代码;
- 线程 :
- 进程中的一个实体;
- 进程的一个执行路径;
- CPU调度的和分派的基本单位;
- 线程本身是不会独立存在的;
- 当前线程CPU时间片用完后,会让出 CPU 等下次轮到自己的时候再执行;
- 系统不会为线程分配内存,线程组之间只能共享所属进程的资源;
- 线程只拥有在运行中必不可少的资源(如程序计数器、栈);
- 线程里的程序计数器就是为了记录该线程让出CPU时候的执行地址,待再次分配到时间片的时候就可以从自己私有的计数器指定地址继续执行;
- 每个线程有自己的栈资源,用于存储该线程的局部变量和调用栈帧,其他线程无权访问;
- 关系 :
- 一个程序至少有一个进程,一个进程至少有一个线程,进程中的多个线程共享进程的资源;
- Java中当我们启动
main
函数的时候就启动了一个JVM进程,而main
函数所在的线程就是这个进程中的一个线程,也叫做主线程; - 一个进程中有多个线程,多个线程共享进程的堆和方法区资源,但是每个线程有自己的程序计数器、栈区域;
34. 谈谈你对JS垃圾回收机制的理解(了解)
- 标记清除法 :
- 这是JS最常见的垃圾回收方式,当变量进入执行环境的时候,比如函数中声明了一个变量,垃圾回收器将其标记为"进入环境",当变量离开环境的时候(函数执行结束),将其标记为"离开环境";
- 垃圾回收器会在运行的时候给存储在内存中的所有变量加上标记,然后去掉环境中的变量以及被环境中变量所引用的变量(闭包),在这些完成之后仍存在标记的就是要删除的变量了;
- 引用计数法 :
- 在低版本IE中经常会出现内存泄漏,很多时候就是因为采用引用计数法进行垃圾回收,引用计数的策略是跟踪记录每个值被使用的次数,当声明了一个变量并将一个引用类型赋值给该变量的时候这个值的引用次数就加1,如果该变量的值变成了另外一个,则这个值的引用次数就减1,当这个值得引用次数为0的时候,说明没有变量在使用,这个值没法被访问了,因此可以将其占用的内存空间回收,这样垃圾回收器会在运行的时候清理引用次数为0的值占用的空间;
- 在IE中虽然JS对象通过标记清除的方式进行垃圾回收,但BOM与DOM对象却是通过引用计数回收垃圾的,也就是说只要涉及BOM及DOM就会出现循环引用的问题;
35. JS垃圾回收机制是什么,常用的是哪种,怎么处理的?(了解)
- JS垃圾回收机制是为了以防内存泄漏,内存泄漏的含义就是当已经不需要某块内存时这块内存还存在着,垃圾回收机制就是间歇的不定期的寻找不再使用的变量,并释放掉它们所指向的内存;
- JS中最常见的垃圾回收方式是:
- 标记清除法;
- 工作原理:
- 当变量进入环境的饿时候,将这个变量标为"进入环境",当变量离开环境时,则将其标记为"离开环境",标记"离开环境"的就回收内存;
- 工作流程:
- 垃圾回收器,在运行的时候会给存储在内存中的所有变量都加上标记;
- 去掉环境中的变量以及被环境中的变量引用的变量的标记;
- 再被加上标记的会被视为准备删除的变量;
- 垃圾回收器完成内存清除工作,销毁那些带标记的值并回收它们所占用的内存空间;