了解JavaScript的底层——内存机制

165 阅读7分钟

哈喽,好久不见,我哆啦美玲又来啦~(,,・∀・)ノ゛hello

今天的内容很简单哦~但也是很重要的,要认真对待!在学习JavaScript的过程中,少不了要搞清楚它的数据类型有哪些以及数据的存储到底是什么样的?所谓“万丈高楼平地起”,要学好JavaScript这门语言,我们肯定要打好“地基”,从底层开始。 20220727220117_7fa2b.jpeg

在进入学习之前,我还是得提一嘴,本文会提到一点点闭包的知识点,不明白的你可以看看我之前写的文章:你不知道的JavaScript——作用域链和闭包-掘金

语言的类型

现在的开发者可以使用各种各样的编程语言进行开发,主流的语言有Java、Python、C/C++和JS等,它们各有各的优点。

但是我们主要分以下四类:

  • 静态语言——在使用前就要确定其变量的数据类型
  • 动态语言——在运行过程中检查数据的类型
  • 弱类型语言——支持隐式类型转化的语言
  • 强类型语言 ——不支持隐式类型转化的语言(例如:java 、 python)

JavaScript在声明变量过程中使用的是var、let和const关键字,完全不需要确定数据的类型,V8会在运行过程中自动的判断数据类型是什么,同时也支持隐式类型转化,所以,JavaScript是一种弱类型的动态语言

JS内存空间

1.数据的类型

JavaScript中的数据类型主要分两种。

基础类型:number(数值)、string(字符串)、boolean(布尔值)、null(空值)、undefined(未定义)、Symbol(唯一值)、bigInt(大整型)。

引用类型:Object(对象)、Array(数组)、Function(函数)

在JavaScript中,我们可以使用官方打造的方法typeof来检验一个变量的类型,typeof会把变量的值转换成二进制然后进行判断。在计算机中,所有的引用类型的二进制前三位是0,typeof检验后会返回Object。但是null要除外,因为null转换成二进制后64位都是0,所以也被判定成为对象,而函数则可以准确的判断出是function。测试代码如下:

// 基础类型
let a = 1
console.log(typeof a); // number

a = 'hello'
console.log(typeof a); // string

a = true
console.log(typeof a); // boolean

// null转换成二进制全部都是0,所以也被判定成为对象
a = null
console.log(typeof a); // 输出object

a = undefined
console.log(typeof a); // undefined

a = Symbol(1)
console.log(typeof a); // Symbol

a = 123n
console.log(typeof a); // bigInt

// 引用类型
a = []
console.log(typeof a); // object

a = {}
console.log(typeof a); // object

a= function (){}
console.log(typeof a); // function
2. 数据的存储

在js中,数据又是怎么存储的呢?

首先,我们得知道js的内存分为三块:代码空间、栈空间和堆空间。如图:

03c46e4954d17f706ee4d07a9484d80.png

  • 代码空间就是你写的代码存储的位置;
  • 栈空间就是我们的调用栈,用来维护函数之间的调用关系,因为栈的内存空间很小,而基本类型的值一般占据的空间都很小,所以用来栈用来存储基础数据类型
  • 堆空间有点像对象一样,一个引用地址对应存储一个值,因为引用类型数据占据的空间大,所以堆用来存储引用类数据类型

有点抽象了啊哈哈哈哈,我们举个例子:

function foo(){
    var a = 1
    var b = a
    var c = {name:'美美'}
    var d = c
}
foo()

在上面我们不看它的执行结果,我直接画图解释它在内存中的存储是什么样:

image.png

如图,我们可以发现:如果一个变量是引用类型(如变量c和d),那么它在栈中存储的是一个引用地址。

这时候就有人要问了:为什么不可以把栈的空间设置的大一点,把引用类数据也存进去呢?

因为栈是用来维护函数之间的调用关系,如果栈的空间很大,当我们存储过多的函数引用关系后,作用域链牵扯过长,函数上下文的切换过多耗时很长,执行效率大大降低。就像是我们的裤子的口袋,如果设计的和腿一样长,那我们想要拿中间或者最底下的东西,我们就要掏半天然后再放进去,会耗时特别久。所以才会把栈设置的比较小。

变量的赋值

let a = 1
let b = a
a = 2
console.log(a);  // 输出 2
console.log(b);  // 输出 1   不受影响

我们看上面的代码,如果对基本类型的变量进行赋值,一个变量的值改变是不会影响另一个的。 那我们再看另一端段代码,分析结果:

function fn(person) {
    person.age = 19
    // 原始类型复制值 引用类型复制值得地址
    person = {
      name: '美美',
      age: 19
    }
    return person
  }
  const p1 = {
    name: '墩墩',
    age: 18
  }
  const p2 = fn(p1)
  
  console.log(p1); // 输出{ name: '墩墩', age: 19 }
  console.log(p2); // 输出{ name: '美美', age: 19 }

图解永远是最直观的过程分析,我们可以画出它的执行过程的执行上下文过程:

image.png

首先,如上图:

  • 在全局执行上下文编译过程中,变量p1和p2的初始值都是undefined;
  • 运行代码的过程中,JS判断出p1是引用类型会先在堆空间某个地址对应存储p1的值,我们假设地址是 #001 ,然后将引用地址赋值给p1,p1 = #001;
  • 执行第14行调用fn函数,创建fn执行上下文,首先编译过程中形参person的值与实参统一,所以person = #001

image.png

其次,我们执行fn函数中的代码:

  • 在第二行中,我们会修改p1的age属性的值,所以堆空间中#001存储的值改变;
  • 执行第四到七行其实是将一个新的对象赋值给person,所以JS也会在堆空间开辟一个新的地址,存储该值,我们假设是#003,将引用地址赋值给person;

image.png

最后我们的函数执行结束:

  • fn返回person的值赋值给p2,p2 = #003;
  • fn结束调用,fn执行上下文销毁;
  • 所以打印p1和p2的值其实是根据引用地址索引到相应的值。

由此,我们会发现,原始类型的赋值是值的复制,引用类型的赋值是引用地址的复制。所以在引用类型变量a赋值给b是地址的赋值,a的值发生变化,堆空间的存储地址是不会变的,所以b也发生变化了。

闭包的数据存储

在上一篇文章中,我们理解了什么是闭包,那结合JS的内存空间,我们再次看看一段存在闭包的代码的完整执行过程:

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()); // 输出 1 、美玲

你也可以像我一样多动手画图,就能更好的理解喔!

  • foo执行上下文中,innerBar是一个对象,在堆空间会开辟新的地址(#001)存储值,foo返回innerBar的值给变量bar(bar = #001) image.png

  • foo执行完毕foo函数执行上下文销毁,bar对象的setName属性的值是个函数,调用触发setName执行上下文入栈;

  • 编译过程形参和实参值统一:name = '美玲';

  • 自身作用域没有myname变量,访问词法作用域(foo函数作用域)中的myname,所以foo函数存在闭包,里面存有myname变量;

  • name的值赋值给myname,所以myname = 美玲;

image.png

  • setName函数执行完毕执行上下文销毁,getName函数执行其执行上下文入栈;
  • 访问自身作用域未声明的变量test1,访问词法作用域(foo函数作用域)的变量test1和myname,所以foo的闭包还存储着变量test1;
  • getName函数内部打印test1的值 1,返回一个myname的值,在全局打印,所以打印美玲。 image.png

ok~这就是我要分享的全部内容啦(^▽^),我相信你都懂了啊哈哈哈哈