一篇文章带你深入了解js内存机制

331 阅读6分钟

一. 引言

这篇文章让我们来看看js中的内存是如何存放以及使用的,首先,js是一种动态的弱类型语言,就是在运行中检查数据类型(动态),且支持隐式转换的语言(弱类型);相反,在使用前就需要确定其变量的数据类型(比如int a = 1;)就是静态语言,不支持隐式转换的就是弱类型语言

二. 隐式转换

隐式转换:就是执行时一种类型自动转化为另一种数据类型;js中的隐式转换有:数字与字符串,数字与布尔,=====,if语句中的隐式转换,逻辑运算符&&||也会隐式转换。

  • 数字与字符串:
var a = 'abc'
var b = 1
console.log(a + b);   //abc1  1被转化成了字符串
  • 数字与布尔:
var bool = true;
var num = 5;
console.log(bool + num); // 6,因为bool被转换成了数字1
  • 松散相等(==)与严格相等(===):
var num = 0;
var str = "0";
console.log(num == str); // true,因为字符串"0"被隐式转换成了数字0
console.log(num === str); // false,因为类型不同,不进行隐式转换
  • if语句中的隐式转换:
var value = "";
if (value) { // value被隐式转换为false,因为空字符串是假值
    console.log("This will not be logged.");
}
  • 逻辑运算符&&||
var value = "hello";
if (value && value.length) { // value被隐式转换为true
    console.log("This will be logged.");
}

三. js中的数据类型

跟很多语言一样,js的数据类型分为基本数据类型(原始数据类型)和引用数据类型(对象数据类型)

基本数据类型有:

  1. number
  2. string
  3. undefined
  4. boolean (true或false)
  5. null (null)
  6. symbol (表示独一无二的标识符)
  7. bigint(123n)

引用数据类型有:Object:包含数组,对象,函数等。

js数据类型一共有八种,而有时numberbigint也可以合称为numeric,也可称七种。

我们可以使用typeof去查看js中的数据类型。null有点特殊,其用console.log(typeof null);检查输出是object,这是因为在计算机中数据都是以二进制存取的,typeof 判断原理就是判断如果二进制前三位为 0就是 object,(引用类型转换为二进制时前三位都是0),而null64位全为0,所以是object。

四. 内存空间

js运行会将内存空间划分为代码空间,栈空间和堆空间。代码空间主要用于存放编译的代码,栈空间主要用于存放上下文,基本数据类型,管理函数间调用关系,堆空间用来动态分配内存,存储引用数据类型的对象和数组的值。

image.png

例如下面代码存取;

image.png

再分析下面代码;

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

let c = { name: '庆玲', age: 18 }
let d = c
c.age = 19
console.log(d.age);  //19

基本数据类型存放在栈中,存储的是它的值,所以修改时直接修改它的值,而第二个是创建一个对象,栈中存储的是它的地址,对象中的数据是存放在堆中的,将c对象的地址赋值给d,再去c中改值,改的时候是通过访问同一个地址,然后修改其上面的值,所以再去访问d中的数据是跟c同一个地址。

为什么要将引用类型存放在堆里而不存放在栈里呢?

因为栈的空间小,只适合用于存放占空间比较小的基本类型,而引用类型其空间一般比较大,所以需另外开辟一个堆来存放。

为什么不将栈空间设置大点呢?

栈是用来存储上下文,是用来管理函数之间的调用关系的,如果栈空间过大,就会导致函数写很多也不会爆栈,而写过多函数会导致函数间层层调用效率降低,这样你打游戏就会很卡了,就相当于把裤子的口袋拉长,往里就会放很多东西,当拿的时候就会非常的慢。所以为了运行效率够快,不会影响你上大分,就不能将栈设置过大。

五. 闭包与调用栈、堆的结合

知道了引用类型是存放在堆里的,那么来看下面代码,看下是如何存储运行的;

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

如图; f54845015dc34de2567f472d9ab5c8c.jpg

在全局定义一个函数fn存放在变量环境,const声明对象p1,变量p2存放在词法环境,全局上下文入栈,执行,给p1赋一个对象,对象的存放在堆中,有一个对应的引用地址(假设为#001),在栈中的p1的值就是该地址,再给 p2赋值,调用fn函数,fn执行上下文入栈。

fn中编译执行时,先声明形参person为undefined,然后形参与实参相匹配,此时person就是传入的p1的地址#001,然后person.age,去堆中地址为#001上将age改为19,然后给person又赋了一个新对象,将对象存入堆中,其引用地址(假设为#003),此时person的值为#003,然后return出#003这个地址给p2,此时p2的地址就为#003,所以输出结果为:

image.png

让我们再看一段有闭包的代码;

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()
bar.setName('陈总')
console.log(bar.getName());

全局中定义了一个变量bar和一个函数foo,函数里面定义myname,test1,test2,对象innerBar(里面俩个key分别为函数体setName和getName),全局代码开始编译执行,foo函数上下文入栈,变量环境中有myname='子俊哥哥',词法环境有test1=1,test2=2,当执行到innerBar时发现是一个对象,然后将对象放入堆中,假设引用地址为#001,innerBar此时的值就是引用地址#001,然后返回inneBar,所以全局上下文此时bar的值为#001。,然后销毁foo函数执行上下文,但是会留下一个闭包,因为把里面的函数return出去执行,而函数里面又访问到了test1和myname,所以留下了一个闭包,里面存放test1和myname

然后执行bar.setName('陈总'),用bar(存放引用地址#001)调用了setName,setName函数执行上下文入栈,里面形参与实参匹配name='陈总'然后执行myname = name,找myName,自己域里找不到,就去它的词法作用域(foo函数)找,由于已被销毁,就去闭包中找,然后将闭包中的myName改为陈总。然后执行getName,输出test1同理,去闭包中找,输出1,此时myName为陈总,所以输出为1,陈总。

执行过程如下所示:

image.png

想了解执行上下文过程可以翻看文章:juejin.cn/post/743710…