内存机制
前言
前面我们谈到了 JS 的执行机制和闭包,今天我们来了解一下 JS 的内存机制。内存机制就像是 JavaScript 代码背后的大总管,它精心安排着数据的存放、调配以及清理工作。这直接关系到我们写的程序跑起来快不快、稳不稳,还有对电脑资源的利用好不好。话不多说,开始学习。
语言的类型
先来了解一下语言的类型,来看一个 c 语言的例子:
int main(){
int a = 1;
char* b = "hello";
bool c = true;
c = a; //隐式类型转换
}
//静态语言:在使用前就需要确定其变量的数据类型
- 在使用前就需要确定其变量的数据类型---静态语言(编译时检查数据类型)
- 在运行时检查数据类型---动态语言
- 支持隐式类型转换的语言 ---弱类型语言
- 不支持隐式类型转换的语言 ---强类型语言
js 是动态弱类型语言,python 是动态强类型语言,java 是静态强类型语言
内存空间
我们的内存空间大致分为:
- 代码空间:存放代码
- 栈:原始类型(原始类型的值一般占用的空间都很小)
- 堆:引用类型(占据的内存很大)
这里我们定义一个原始类型 a , let b = a,把 a 赋值为 2 ,输出 b 为多少?
let a = 1
let b = a
a = 2
console.log(b);
来看一下输出:
那我们这里来定义一个引用类型 a ,let b = a ,这里来修改一下 a 的属性 ,那会输出什么?
let a = {name: '卢卡',age: 20} //对象
let b = a
a.age = 21
console.log(b.age);
输出是这样子的:
那为什么同样是 let b = a ,但是第一个 b 的值不会被改变,第二个会随着 a 的改变而改变。
因为是引用类型,所以 a 和 b 指向同一个对象,所以修改 a.age ,b.age 也会变。我们要理解这个,首先要对 JS 的内存空间有一定的了解。
let a = 1
a = 'hello'
a = true
a = null //64个0,object
a = undefined
a = Symbol(1)
a = 123n //原始类型
a = []
a = {}//引用类型
a = function () {}
console.log(typeof a);//typeof(a)判断数据类型
//通过判断二进制,000就是object
举个例子,我们来画出这个代码的调用栈情况:
function foo() {
var a = 1;
var b = a;
var c = {name: 'Luka'}
var d = c;
}
foo()
这是编译执行完之后的情况:
- v8 不会直接把对象放到变量环境中,而是会放到堆中,生成一个引用地址,然后把堆的地址放到变量环境。
- 原始类型的值占用的内存不大,可以直接放入调用栈中;但是引用类型的值,会占用大量内存,所以会放到堆中。
- 原始类型的赋值是值的复制,引用类型的赋值时地址的复制。
这里 foo 执行时,识别到 c 是一个引用类型,把对象放入堆中,把引用地址放入变量环境中(也就是 foo 执行上下文左边的框);而 let d = c ,会直接把 c 的引用地址赋给 d。
========================================
所以之前这个代码输出 21 .是因为 b 和 a 共用一个引用地址。
let a = {name: '卢卡',age: 20} //对象
let b = a
a.age = 21 //共用一个引用地址
console.log(b.age); //21
学到了我们再来提升一下,我们来思考一下这个代码的输出是什么?
function fn(person) {
person.age = 19
person = {
name: '美智子',
age: 19
}
return person
}
const p1 = {
name: '伊莱',
age: 18
}
const p2 = fn(p1)
console.log(p1);
console.log(p2);
结果是这样子的:
不理解这个输出?没关系,我们画出调用栈来:
我们来着重讲一下这个 fn 的执行上下文,执行过程中形参实参统一,person = #001,接着重新给 person 赋值,把 person 这个引用对象的内容放入堆中,地址 #003 ,把地址放到变量环境中,#001被替代,所以 p2 也就是 foo 中的 person。
让我们来看看更复杂一点的:
function foo() {
var myname = '杰克'
let test1 = 1
const test2 = 2
var innerBar = {
setName: function (name) {
myname = name
},
getName: function () {
console.log(test1);
return myname
}
}
return innerBar
}
var bar = foo() //innerbar
bar.setName('Jack')
console.log(bar.getName()); // 1 Jack
我们来看看这份代码的调用栈:
-
全局编译完成,开始执行,
foo被调用,执行上下文入栈。变量环境:myname、innerbar,词法环境:test1、test2。 -
foo开始执行test1 = 1,test2 = 2,myname = "杰克",innerbar = #001(指向堆里放的对象)。foo执行结束,bar = innerbar,留下闭包(myname="杰克",test1=1),全局继续执行。 -
调用
setName函数,setName执行上下文入栈。执行时形参实参统一为"Jack",把foo的闭包中的myname改为"Jack",setName执行结束,执行上下文被销毁。 -
图中省略这些步骤: 调用
getName函数,执行,销毁执行上下文;全局执行结束,销毁全局执行上下文;堆中内容被销毁。
======================
接下来还有一个小知识:
const a = 1
a = 2
console.log(a);// 报错,const定义的原始类型不能改变
const b = {
a: 1
}
b.a = 2
console.log(b);// 2,const定义的引用类型可以改变其属性的值
面试
面试官会问:为什么栈不能分配的大一点?
栈是维持函数之间的调用关系的,闭包如果很多,函数之间的调用关系会很多,作用域链会很长,那么执行上下文会很多,那么执行效率就会大大降低。所以回答:
---栈的设计本身就很小:因为栈如果设计的很大的话,那么栈中函数的上下文切换效率就会大大降低(就像人的裤兜设计的很长,放很多东西,拿东西就会很慢)