JS入门必读:JavaScript的内存机制

124 阅读5分钟

内存机制

前言

前面我们谈到了 JS 的执行机制和闭包,今天我们来了解一下 JS 的内存机制。内存机制就像是 JavaScript 代码背后的大总管,它精心安排着数据的存放、调配以及清理工作。这直接关系到我们写的程序跑起来快不快、稳不稳,还有对电脑资源的利用好不好。话不多说,开始学习。

语言的类型

先来了解一下语言的类型,来看一个 c 语言的例子:

int main(){
    int a = 1;
    char* b = "hello";
    bool c = true;
    c = a;            //隐式类型转换
}
//静态语言:在使用前就需要确定其变量的数据类型
  1. 在使用前就需要确定其变量的数据类型---静态语言(编译时检查数据类型)
  2. 在运行时检查数据类型---动态语言
  3. 支持隐式类型转换的语言 ---弱类型语言
  4. 不支持隐式类型转换的语言 ---强类型语言

js 是动态弱类型语言,python 是动态强类型语言,java 是静态强类型语言

内存空间

我们的内存空间大致分为:

  1. 代码空间:存放代码
  2. 栈:原始类型(原始类型的值一般占用的空间都很小)
  3. 堆:引用类型(占据的内存很大)

这里我们定义一个原始类型 alet b = a,把 a 赋值为 2 ,输出 b 为多少?

let a = 1
let b = a
a = 2
console.log(b);

来看一下输出:

image.png

那我们这里来定义一个引用类型 alet b = a ,这里来修改一下 a 的属性 ,那会输出什么?

let a = {name: '卢卡',age: 20} //对象
let b = a
a.age = 21
console.log(b.age);

输出是这样子的:

image.png

那为什么同样是 let b = a ,但是第一个 b 的值不会被改变,第二个会随着 a 的改变而改变。

因为是引用类型,所以 ab 指向同一个对象,所以修改 a.ageb.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()

这是编译执行完之后的情况:

image.png

  1. v8 不会直接把对象放到变量环境中,而是会放到堆中,生成一个引用地址,然后把堆的地址放到变量环境。
  2. 原始类型的值占用的内存不大,可以直接放入调用栈中;但是引用类型的值,会占用大量内存,所以会放到堆中。
  3. 原始类型的赋值是值的复制,引用类型的赋值时地址的复制。

这里 foo 执行时,识别到 c 是一个引用类型,把对象放入堆中,把引用地址放入变量环境中(也就是 foo 执行上下文左边的框);而 let d = c ,会直接把 c 的引用地址赋给 d

========================================

所以之前这个代码输出 21 .是因为 ba 共用一个引用地址。

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);

结果是这样子的:

image.png

不理解这个输出?没关系,我们画出调用栈来:

image.png

我们来着重讲一下这个 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

我们来看看这份代码的调用栈:

bad125df9c2f560d73d50a58f82cfcb.png

  1. 全局编译完成,开始执行,foo 被调用,执行上下文入栈。变量环境:mynameinnerbar ,词法环境:test1test2

  2. foo 开始执行 test1 = 1test2 = 2myname = "杰克"innerbar = #001(指向堆里放的对象)。foo 执行结束,bar = innerbar,留下闭包(myname="杰克"test1=1),全局继续执行。

  3. 调用 setName 函数,setName 执行上下文入栈。执行时形参实参统一为 "Jack",把 foo 的闭包中的 myname 改为 "Jack"setName 执行结束,执行上下文被销毁。

  4. 图中省略这些步骤: 调用 getName 函数,执行,销毁执行上下文;全局执行结束,销毁全局执行上下文;堆中内容被销毁。

======================

接下来还有一个小知识:

const a = 1
a = 2
console.log(a);// 报错,const定义的原始类型不能改变

const b = {
    a: 1
}
b.a = 2
console.log(b);// 2,const定义的引用类型可以改变其属性的值

面试

面试官会问:为什么栈不能分配的大一点?

栈是维持函数之间的调用关系的,闭包如果很多,函数之间的调用关系会很多,作用域链会很长,那么执行上下文会很多,那么执行效率就会大大降低。所以回答:

---栈的设计本身就很小:因为栈如果设计的很大的话,那么栈中函数的上下文切换效率就会大大降低(就像人的裤兜设计的很长,放很多东西,拿东西就会很慢)