深入JS内存模型

前言

什么是JS的内存模型,看过了很多资料,都是千篇一律的栈内存,堆内存,基本数据类型存储在栈内存,引用数据类型存储在堆内存等等之类的,看过这些之后,对于我们去理解整个JS内存之中复杂的关系。本文会从JavaScript语法之中的各个语言特性由浅入深分析JavaScript的内存。

JavaScript规定的数据类型

JavaScript有这最基本的七大数据类型:Number、Boolean、String、undefined、null、Symbol、Object

基本数据类型

基本数据Number、Boolean、String、undefined、null、Symbol往往都是以直接量存储在内存之中,每种类型的数据占用的内存空间的大小是确定的,这里我说的是内存,之前我们在前言之中也提到过栈内存和堆内存,这里我没说基本数据类型是存储在栈内存之中,划重点。

说说直接量存储

在这里我们得说一说直接量存储,了解内存机制的话,整个内存都是又bit组成byte,然后再组成更大的空间,然后以64位的机器举🌰🌰🌰,我们可以把内存理解为64位(bit)为一个基本单位来存储数据的,包括基本类型,寻址等等。

基本数据类型的直接量存储就是直接将这一个个基本单位组合或者直接以一个基本单位将数据存储起来,直接量的基本操作就是直接基于内存中的数据进行操作,所以基本数据类型的赋值等操作都是会直接的复制或者改变内存中的数据的。

假设我们在内存中声明并赋值了a=1,b=2就是直接在内存中,当我们执行c = a;a = a+b;这样的语句时,内存中的结果就是下图3这样的结果。

说说null和undefined

null和undefined同样都是所属基础类型中的唯一成员,在内存上又有什么样的区别呢?

 	static const int kUndefinedValueRootIndex = 4;
  static const int kTheHoleValueRootIndex = 5;
  static const int kNullValueRootIndex = 6;
  static const int kTrueValueRootIndex = 7;
  static const int kFalseValueRootIndex = 8;
  static const int kEmptyStringRootIndex = 9;

template<typename T>
void ReturnValue<T>::SetNull() {
  TYPE_CHECK(T, Primitive);
  typedef internal::Internals I;
  *value_ = *I::GetRoot(GetIsolate(), I::kNullValueRootIndex);
}

template<typename T>
void ReturnValue<T>::SetUndefined() {
  TYPE_CHECK(T, Primitive);
  typedef internal::Internals I;
  *value_ = *I::GetRoot(GetIsolate(), I::kUndefinedValueRootIndex);
}

由图上源码可见,null和undefined都是存储在根内存中的直接量。内存上的存储有这本质的区别。

引用数据类型

在JS中除了基本数据类型以外的都是对象,数据是对象,函数是对象,正则表达式是对象,即JavaScript 的所有其他对象都继承自Object对象,即那些对象都是Object的实例。

说到引用数据类型,大家都可以想得到措辞就是,对象的引用是存在栈内存中的,对象内容是存储在新开辟的堆内存中的,言论大概是如此,在此不做过多的定论。

栈内存与堆内存

先上一段千篇一律的基本概念

js基本类型数据都是直接按值存储在栈中的(Undefined、Null、不是new出来的布尔、数字和字符串),每种类型的数据占用的内存空间的大小是确定的,并由系统自动分配和自动释放。这样带来的好处就是,内存可以及时得到回收,相对于堆来说   ,更加容易管理内存空间。

js引用类型数据被存储于堆中 (如对象、数组、函数等,它们是通过拷贝和new出来的)。其实,说存储于堆中,也不太准确,因为,引用类型的数据的地址指针是存储于栈中的,当我们想要访问引用类型的值的时候,需要先从栈中获得对象的地址指针,然后,在通过地址指针找到堆中的所需要的数据。

堆栈数据概念中的存储悖论

当然这个玩意是我自己琢磨,其实有时候大家仔细想想就会觉得,不是这么回事啊~

接下来举个存储悖论的🌰:

var obj = {
	id: 123,
  name: 'foo'
};

先来有这样一个对象obj,作为引用类型它的地址指针必然是存储在所谓的栈内存中的,那么问题来了,对象obj的属性id的数据类型为基本数据类型,那么这个123是存储在堆中还是栈中呢?

这一刻堆内存和栈内存的界限就不那么清晰了。

通过这个悖论我们应该了解到,基本数据类型和引用数据类型的本质区别本不是什么存储位置的区别,而是基本数据类型的存储是内存的直接量存储,引用数据类型的存储是引用的直接量存储以及对应对象的随机存储。

Object

V8里面所有的引用数据类型的根父类都是Object,Object派生HeapObject,提供存储基本功能,往下的JSReceiver用于原型查找,再往下的JSObject就是JS里面的Object,Array/Function/Date等继承于JSObject。左边的FixedArray是实际存储数据的地方。

在创建一个JSObject之前,会先把读到的Object的文本属性序列化成constant_properties,如下的data:

var data = {
	name: "yin",
	age: 18,
	"-school-": "high school"
};

会被序列成:

../../v8/src/runtime/runtime-literals.cc 72 constant_properties:
0xdf9ed2aed19: [FixedArray]
– length: 6
[0]: 0x1b5ec69833d1 <String[4]: name>
[1]: 0xdf9ed2aec51 <String[3]: yin>
[2]: 0xdf9ed2aec71 <String[3]: age>
[3]: 18
[4]: 0xdf9ed2aec91 <String[8]: -school->
[5]: 0xdf9ed2aecb1 <String[11]: high school>

参考文献:从Chrome源码看JS Object的实现

从chrome源码来看,对象中的直接量也是存储在对象中的。

作用域

作用域是指程序源代码中定义变量的区域。作用域规定了如何查找变量,也就是确定当前执行代码对变量的访问权限。JavaScript 采用词法作用域(lexical scoping),也就是静态作用域。这样函数的作用域在函数定义时就已经决定。

这么一来,在函数对象在内存中产生之时,函数内部就已经挂载了函数自己所属的作用域引用。

举个🌰:

var a = "global";
function foo(){
	var a = "foo";
    console.log(a)
	function baz(){
        var b = "baz";
        console.log(a,b);
    }
    baz();
}
foo();

执行栈

说道执行栈,一般而论就是浏览器执行JS代码的运行栈内存,js运行时会将执行表达式以及执行方法等进行压栈处理。

这里我们就需要思考一个东西了,全局中执行的表达式,以及全局变量以及全局的属性之间的关系。

首先,全局变量的定义以及全局属性的赋值,其实都是在全局对象上进行操作。

其次,全局执行的表达式运算语句等执行时需要使用所谓栈内存中的变量。

这样就涉及到了另一个概念执行上下文,代码经编译器解释之后会将相应的上下文压入执行栈执行,那么JavaScript引擎解释到全局JavaScript代码后就会将这部分构成或者是重新解释构成全局执行上下文。

这样就有了这样的结论,就是全局上下文在解析压入执行栈之后,直到页面结束之前是不会被销毁的,全局上下文中的变量即是全局变量。

var bar = 1;
function foo(){
  var bar = 2;
  console.log(bar);
}
foo();

如图所示,在执行foo()之前,浏览器直接生成了全局上下文压入栈中并生成全局对象存储相应的变量与引用等。

当foo执行时,生成对应的函数上下文对象,并将其压入执行栈中,这个时候再内存中也就产生了相应的函数的变量对象已存储函数的变量以及引用等。

当foo执行完毕之后foo上下文从执行栈中弹出,变量对象失去引用,被浏览器销毁。

这样我们就完成了一个完整的执行栈中的执行调用过程,这样对全局变量也好还是函数变量也好都用了一个明确的界定。

闭包

从上边的执行栈中我们了解到了执行栈中的甘薯调用,全局对象的生成,函数变量对象的生成,而这一切对我们理解JavaScript的一些特性有什么帮助呢,接下来让我们想想闭包。

function foo(){
  var bar = 2;
  return function(){
    bar++;
  }
}
var baz = foo();
baz();

在执行foo()函数之前,浏览器内存中形成了这样的一个引用

在执行foo()后,全局对象中baz获得foo内闭包函数的句柄。并根据作用域链引用了foo的变量对象。

在执行baz后,内存中出现了引用foo变量对象的baz的变量对象,在foo执行完毕弹出执行栈后,由于闭包函数的作用域引用foo变量对象并未被回收。

总结

  1. 这样我们就可以清楚的了解到,JavaScript的数据的储存,并不是明确的存储在堆或者是栈某种数据结构中,我们以前理解的基本数据类型以直接量存储在栈中其实是,全局上下文中的基本类型都是直接存储在全局对象这个连续内存之内,又系统去管理和回收,引用类型则是随机的在内存中分配内存块来使用。
  2. 将作用链的查找放到具体执行栈中执行才会有具体的意义,外部函数定义之后没有压栈执行,内部函数就无法得到解释从而生成函数相应的函数对象,这样作用域链仅仅是纸面上的,在JavaScript引擎中是不存在链引用的。
  • 2
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值