js 是什么类型的语言
我们把这种在使用之前就需要确认其变量数据类型的称为静态语言。
我们把在运行过程中需要检查数据类型的语言称为动态语言
JavaScript 就是动态语言,因为在声明变量之前并不需要确认其数据类型
JavaScript 是一种弱类型的、动态的语言
- 弱类型,意味着你不需要告诉 JavaScript 引擎这个或那个变量是什么数据类型,JavaScript 引擎在运行代码的时候自己会计算出来
- 动态,意味着你可以使用同一个变量保存不同类型的数据。
补充:
JS 基本类型:
- 在ES5的时候,我们认知的数据类型确实是 6种:Number、String、Boolean、undefined、object、Null。
- ES6 中新增了一种 Symbol 。
- 谷歌67版本中还出现了一种 bigInt。是指安全存储、操作大整数。
JS 的数据类型有几种?
8种:Number、String、Boolean、Null、undefined、object、symbol、bigInt。
Object 中包含了哪几种类型?
包含了Data、function、Array等。这三种是常规用的。
JS的基本类型和引用类型有哪些呢?
基本类型(单类型):除Object。 String、Number、boolean、null、undefined
引用类型:object。里面包含的 function、Array、Date。
那么讲一下 基本数据类型和引用类型 存储数据有什么不同?
堆栈空间
栈空间就是我们之前反复提及的调用栈,是用来存储执行上下文的
function foo(){
var a = 1
var b = a
a = 2
console.log(a) // 2
console.log(b) // 1
}
foo()
function foo(){
var a = {name:"极客时间"}
var b = a
a.name = "极客邦"
console.log(a) // {name:"极客邦"}
console.log(b) // {name:"极客邦"}
}
foo()
从上面代码中我们可以看到第一段代码 a 改变 b 不会改变,第二段代码 a 改变 b 也改变了, 我们前面介绍了 js 的数据类型。
可以知道,第一段代码中a 和 b 都是 基本的数据类型。
第二段中a 和b 是引用类型
那么我们下面看下 基本类型和引用类型是如何存储的数据
function foo(){
var a = "极客时间"
var b = a
var c = {name:"极客时间"}
var d = c
}
foo()
- 当执行一段代码时,需要先编译,并创建执行上下文
- 执行foo 时,变量 a 和变量 b 的值都被保存在执行上下文中,你也可以认为变量 a 和变量 b 的值都是存放在栈中的。
- JavaScript 引擎判断c的值是一个引用类型,JavaScript 引擎并不是直接将该对象存放到变量环境中,而是将它分配到堆空间里面,分配后该对象会有一个在“堆”中的地址,然后再将该数据的地址写进 c 的变量值。具体如下图:
对象类型是存放在堆空间的,在栈空间中只是保留了对象的引用地址,
原始类型的数据值都是直接保存在“栈”中的,引用类型的值是存放在“堆”中的
那么为什么要分两个空间来存储呢?
- JavaScript 引擎需要用栈来维护程序执行期间上下文的状态,如果栈空间大了话,所有的数据都存放在栈空间里面,那么会影响到上下文切换的效率,进而又影响到整个程序的执行效率
- foo 函数执行结束,javaScript 引擎需要离开当前的执行上下文,只需要将指针下移到上个执行上下文的地址就可以了,foo 函数执行上下文栈区空间全部回收;
栈空间都不会设置太大,主要用来存放一些原始类型的小数据
堆空间很大,能存放很多大的数据,不过缺点是分配内存和回收内存都会占用一定的时间。
所以回到前面的题,原始类型的赋值会完整复制变量值,而引用类型的赋值是复制引用地址。
d=c的操作就是把 c 的引用地址赋值给 d
- 变量 c 和变量 d 都指向了同一个堆中的对象,所以修改对象,变量都会变化,因为指向的是同一个对象。
闭包如何产生?
function foo() {
var myName = "极客时间"
let test1 = 1
const test2 = 2
var innerBar = {
setName:function(newName){
myName = newName
},
getName:function(){
console.log(test1)
return myName
}
}
return innerBar
}
var bar = foo()
bar.setName("极客邦")
bar.getName()
console.log(bar.getName())
当 JavaScript 引擎执行到 foo 函数时,首先会编译,并创建一个空执行上下文
在编译过程中,遇到内部函数 setName,JavaScript 引擎还要对内部函数做一次快速的词法扫描,发现该内部函数引用了 foo 函数中的 myName 变量,由于是内部函数引用了外部函数的变量,所以 JavaScript 引擎判断这是一个闭包。
于是在堆空间创建换一个“closure(foo)”的对象(这是一个内部对象,JavaScript 是无法访问的),用来保存 myName 变量。
接着继续扫描到 getName 方法时,发现该函数内部还引用变量 test1,于是 JavaScript 引擎又将 test1 添加到“closure(foo)”对象中。
这时候堆中的“closure(foo)”对象中就包含了 myName 和 test1 两个变量了。
由于 test2 并没有被内部函数引用,所以 test2 依然保存在调用栈中。
调用栈中的数据是如何回收的
function foo(){
var a = 1
var b = {name:"极客邦"}
function showName(){
var c = "极客时间"
var d = {name:"极客时间"}
}
showName()
}
foo()
一个记录
当前执行状态的指针(称为 ESP)
,指向调用栈中 showName 函数的执行上下文,表示当前正在执行 showName 函数。当 showName 函数执行完成之后,函数执行流程就进入了 foo 函数,那这时就需要销毁 showName 函数的执行上下文了
JavaScript 会将 ESP 下移到 foo 函数的执行上下文,这个下移操作就是销毁 showName 函数执行上下文的过程
上面 showName 的执行上下文虽然保存在栈内存中,但是已经是无效内存了
当 foo 函数再次调用另外一个函数时,这块内容会被直接覆盖掉,用来存放另外一个函数的执行上下文。
JavaScript 引擎会通过向下移动 ESP 来销毁该函数保存在栈中的执行上下文。
堆中的数据是如何回收的
上面那段代码的 foo 函数执行结束之后,ESP 应该是指向全局执行上下文的,那这样的话,showName 函数和 foo 函数的执行上下文就处于无效状态了,不过保存在堆中的两个对象依然占用着空间
要回收堆中的垃圾数据,就需要用到 JavaScript 中的垃圾回收器了。
在 V8 中会把堆分为新生代和老生代两个区域,
新生代中存放的是生存时间短的对象,
老生代中存放的生存时间久的对象。
副垃圾回收器,主要负责新生代的垃圾回收
主垃圾回收器,主要负责老生代的垃圾回收。
回收流程:
标记空间中活动对象和非活动对象 ---> 回收非活动对象所占据的内存 ---->做内存整理。
副垃圾回收器:
副垃圾回收器主要负责新生区的垃圾回收。而通常情况下,大多数小的对象都会被分配到新生区,所以说这个区域虽然不大,但是垃圾回收还是比较频繁的
- 通过
Scavenge 算法
来处理 ,把新生区分为对象和空闲区域; - 新加入的对象,先进入对象区域,因为不大,满的时候要进行垃圾回收
- 回收的时候,会对对象区域标记
- 标记完成之后,把存货的对象复制到空闲区域;相当于完成了内存整理操作
同时这种角色翻转的操作还能让新生代中的这两块区域无限重复使用下去。
为了执行效率,一般新生区的空间会被设置得比较小
所以很容易被存活的对象装满整个区域。所以JavaScript 引擎采用了对象晋升策略,也就是经过两次垃圾回收依然还存活的对象,会被移动到老生区中。
主垃圾回收器:
主垃圾回收器是采用标记 - 清除(Mark-Sweep)的算法进行垃圾回收的
- 标记阶段就是从一组根元素开始,递归遍历这组根元素,在这个遍历过程中,能到达的元素称为活动对象,没有到达的元素就可以判断为垃圾数据。
- 上图可知:
showName执行结束之后,这个时候如果遍历调用栈。是不会找到引用 1003 地址的变量,也就意味着 1003 这块数据为垃圾数据 - 由于 1050 这块数据被变量 b 引用了,所以这块数据会被标记为活动对象
- 最后整理,排序