注册
web

JavaScript内存管理机制解析

前言


内存,作为计算机系统中存储数据和指令的关键资源,其管理效率直接影响着程序的性能和稳定性。在JavaScript的世界里,理解内存机制并非遥不可及,每一位开发者必须面对并掌握的实用技能。无论是初涉开发的新手,还是经验丰富的老手,深入理解JavaScript的内存机制都是通往更高层次编程能力的必经之路。


语言类型


静态语言


静态语言是指在编译时变量的数据类型就已经确定的语言,比如java定义一个整数类型需要先用int去定义一个变量。这类语言在编写程序时,要求开发者明确地声明变量的类型,并且在程序的整个生命周期内,该变量的类型都不能改变。换句话说,静态语言的类型检查是在编译阶段完成的,而不是在运行时,常见的静态语言包括Java、C++、C#、Go等。


动态语言


动态语言(Dynamic Language),也称为动态编程语言或动态类型语言,与静态语言相反,是指在程序运行时可以改变其结构的语言。这种改变可能包括引进新的函数、删除已有的函数,或者在运行时确定变量的类型等。动态语言的特点使得它们通常具有更高的灵活性和表达能力。常见的动态语言有我们学的JavaScript,还有Python,PHP等。


弱类型语言


弱类型语言是指变量的类型检查和转换方式相对宽松的一种编程语言。在弱类型语言中,变量可以在不明确声明类型的情况下直接使用,并且在运行时可以自动改变类型,或者可以在不同类型之间自由进行操作和转换,常见的弱类型语言包括JavaScript、Python等。


强类型语言


强类型语言(Strongly Typed Language)是一种在编译时期就进行类型检查的编程语言。这类语言要求变量在使用前必须明确声明其类型,并且在使用过程中,变量的类型必须保持一致,不能随意更改,常见的强类型语言包括Java、C++、C#、Go等。


数据类型


在每种语言里面都会有一个方法去查看数据的类型,js也不例外,我们可以用typeof去查看一个数据的类型,那我们来看看js中所有的数据类型吧


let a = 1
// console.log(typeof a); //Number
a = 'hello'
// console.log(typeof a); //String
a = true
// console.log(typeof a); //boolean
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

我们可以看到所有判断类型的结果,大部分还正常,可是数组和null怎么也被判断成了object类型呢?


那我们要来了解一下typeof的判断原理,怎么给a判断出来它的数据类型的呢,其实是通过转换为计算机能看懂的二进制,然后通过二进制的数据进行的分析,所有的引用类型转换成二进制前三位一定是零,然后数组是引用类型,而typeof判断时如果前三位是零,那么就无脑认为它是object类型,但是函数是一个特例,在js中函数是一个对象,它做了一些特殊操作,所以能够判断出来,但是null是原始数据类型,为什么也能被判断为object类型呢,因为null在被读取成二进制时,它会被读取为全是零。而这个不是编程语言能够决定的,在计算机创建出来时就是这样设定的,因此这是一个bug,在设计这门语言的的bug,这个bug如果要修复并不困难,但是一旦修复,所有用js语言开发的项目都需要修复,影响太大,因此这个bug就被默认为js语言里面的规则。


内存空间


内存空间的分布


在v8引擎执行你写的代码时,会占用一部分的运行空间,而执行时占用的内存空间在v8的视角里会被分布成这个样子的


46.png


代码空间是专门存储你所写的代码,栈空间就是我们之前讲过的调用栈juejin.cn/post/743706…


用来存储函数被调用时,它的执行上下文,维护函数的调用关系,调用栈被分布的空间是比较小的。


堆空间(Heap Space)是内存管理的一个重要部分,它用于存储动态分配的对象和数据结构。


栈和堆之间的关系


让我们来看看栈和堆之间的关系


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

47.png


此时foo函数已经完成编译,且已经执行到了b=a这一行,然后将一个对象赋值给c的时候,并不会直接把这个对象存储在函数的执行上下文里面,而是会在旁边在创建一个堆空间,将对象存储在堆空间里面,而这个c存储的就是对象在堆空间的地址值


48.png
然后在执行将c的值赋给d其实就是将对象的地址值赋值给了d,因此cd的地址值指向的是同一个对象,并没有创建出一个新的对象,如果这个对象发生改变,那么cd所代表的对象都会发生改变。


那为什么原始数据类型可以直接存储在栈当中,而引用数据类型却要存储在堆空间里面,因为原始类型数据所占的空间小,而引用数据类型所占的空间较大,比如一个对象,它可以有无数个属性,而原始类型,它就只有一个固定的值,所占内存不大,而栈被分布的空间比较小,堆被分布的空间比较大,因此原始数据类型可以直接存储在栈当中,而引用数据类型要存储在堆当中。


栈设计为什么这么小


首先我们要明白栈是用来维护函数的调用关系,而如果将栈设计的很大,那么程序员就可以写很长作用域链,并且不考虑代码的执行效率,写出不断嵌套的屎山代码。举个例子,栈就好比在你身上的空间,比如你的衣服裤子口袋,而堆就相当于一个分层的柜子,你把衣服上的口袋设计的很大,不要柜子,把你的东西全部装在口袋里面,首先看起来就十分丑陋,其次,你如果想将你想要的东西拿出来就要在口袋里翻来覆去的找,那样的效率是很低的


成果检验


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

请分析上面的代码中的p1p2的输出结果


49.png
我们创建全局上下文进行编译执行,然后对函数fn进行编译,编译过程中形参和实参要进行统一,接下来,我们要开始执行函数fn了,首先它将p1所指向的对象age修改为了19,然后再函数中它将p1的地址值修改指向为了新对象,并将新对象返回,然后在全局接着执行,将返回的地址值赋给了p2,所以p2的值就是函数中新对象的地址值,接下来输出p1,此时函数已经执行完毕,在调用栈中被销毁了,那我们就在全局中查找,在全局中p1的指向就是#001,但是函数销毁前他将地址值为#001的对象age属性修改为19,所以p1打印出来的只有age改为了19,而p2就是返回的新对象的值,然我们看看结果是不是我们分析的那样


50.png


没错,p1name为'凤如',age19p2name为'庆玲',age为19


最后来一道添加闭包的内存管理机制代码分析,如果不熟悉闭包的概念,可以先看看这篇文章
](juejin.cn/post/743814…)


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

总结


本文探讨了JavaScript的内存机制,包括语言类型(静态与动态、强类型与弱类型)、数据类型及typeof的判断原理,并解析了内存空间的分布,特别是栈空间和堆空间的作用及它们之间的关系。通过示例代码,阐述了原始数据类型和引用数据类型在内存中的存储差异,以及栈为何设计得相对较小的原因。最后,通过实际代码演示和结果分析,检验了对JavaScript内存机制的理解。本文是掌握JavaScript编程能力的关键一步,适合各层次开发者阅读。


作者:竺梓君
来源:juejin.cn/post/7440717815709057050

0 个评论

要回复文章请先登录注册