V8 works: 12 | stack and heap space: how the data is stored?

Preface: This article Description: See  description - the browser works with the practice directory

 

  For front-end developers, JavaScript memory mechanism is a concept not often mentioned, it can easily be overlooked. In particular, some non-computer science students, may not have a very clear understanding of the mechanisms of memory, and even some students simply do not know what memory mechanism of JavaScript Yes.

 

  But if you want to become industry experts, and build a high-performance front-end application, then you have to figure out the JavaScript memory mechanism of.

 

  In fact, the memory mechanisms to figure out JavaScript is not a very difficult thing, in the next three articles (the data stored in memory, garbage collection and processing V8 JavaScript code execution), we will by the memory mechanism introduction, step by step take you into the world of JavaScript memory.

 

  Today we explain the data content --JavaScript first part is how to store in memory. Although JavaScript does not need to directly manage memory, but the actual project in order to avoid unnecessary pit, you still need to understand the method of storing data in memory of.

 

Puzzling code

  First, we look at the following two paragraphs of code:

function foo(){
    var a = 1
    var b = a
    a = 2
    console.log(a)
    console.log(b)
}
foo()
function foo () {
     var A = {name: "extreme off time" }
     var B = A
    a.name = "Geek Nation" 
    console.log(a)
    console.log(b)
}
foo()

 

  If these two perform the above code, you know what the results of their output is not it? Here we have to analyze one by one under.

 

  Execute first code value is printed a 2, b is a value of 1, this is not difficult to understand.

 

  Then, in the implementation of the second paragraph of the code, you will find that merely changing the name of a property value in, but in the end a and b values ​​are printed out {name: "Geek Nation"}. This is inconsistent with our expectations, because we want to change only a content, but also b content was changed.

 

  To completely clarify this issue, we have to start with the "JavaScript is what type of language" talk.

 

What kind of language is JavaScript

  Each programming language has built-in data types, but they often have different types of data, the use is also very different, such as C language before defining variables, you need to determine the type of a variable, you can see it below section C code:

int main ()
{
   int A = . 1 ;
    char * B = " very time off " ;
    BOOL C = to true ;
    return  0 ;
}

 

  Features above code declares variables: the need to define the variable type before you declare a variable. We call this before using the need to confirm their variable data type called a static language .

 

  On the contrary, we need to check the data type of language called dynamic languages during operation . For example, we are talking about is the dynamic JavaScript language, because it does not need to confirm its data type before you declare a variable.

 

  Although the C language is static, but the C language, we can put other types of data given to a good variable declarations, such as:

c = a

 

  The previous code, we assign a variable of type int to a bool type variable c, this code can also compile and run, because in the course of the assignment, C compiler will quietly convert a variable of type int to bool variable, we usually secretly converted this operation is called implicit type conversion . The support implicit type conversion language called weakly typed language, the language does not support implicit type conversion is called a strongly typed language . At this point, C, and JavaScript is weakly typed language.

 

  For various types of language, you can refer to the following chart:

 

Language type map

 

JavaScript data types

  Now we know, JavaScript is a weakly typed, dynamic language. What these features mean it?

  • Weakly typed , the JavaScript engine means you do not need to tell this or that variable is what type of data, the JavaScript engine when running the code will be calculated out on his own.
  • Dynamic , which means you can use the same variables to keep different types of data.

 

  So then, let us look at the JavaScript data types, you can see the following code:

the bar
bar = 12 is  
bar = "Geek Time" 
bar = to true 
bar = null 
bar = {name: "Geek Time"}

  From the above you can see the code, we declare a variable bar, then use various types of data values ​​assigned to the variable.

 

  In JavaScript, if you want to see in the end is what type of a variable, you can use the "typeof" operator. DETAILED use as follows:

the bar
console.log(typeof bar)  //undefined
bar = 12 
console.log(typeof bar) //number
bar = " 极客时间 "
console.log(typeof bar)//string
bar = true
console.log(typeof bar) //boolean
bar = null
console.log(typeof bar) //object
bar = {name:" 极客时间 "}
console.log(typeof bar) //object

  Execute this code, you can see the print out different data types, there are undefined, number, boolean, object and so on. So then we talk about JavaScript in the end how many kinds of data types.

 

  In fact, the data type of JavaScript in a total of eight, they are:

 

  Knowing these types, there are three things you look at.

 

  First, upon detection of typeof Null type, returns Object. It was originally a Bug JavaScript language has been preserved, the reason has not been modified over, mainly for compatibility with old code.

 

  Second, Object special type, which is composed by the seven types of data comprising a type of key-value pairs. As follows:

let myObj = {
        name: 'Time Geeks' ,
        update:function(){....}
}

 

  From which you can see, Object is composed of a key-value, which value can be any type that contains functions, which means you can store Object function, the function of Object, also known methods such as the above update method code.

 

  Third, we put in front of seven kinds of data types called primitive type , the last object type called a reference type , reason why they are divided into two different types, because of the position they are stored in memory is not the same . In the end how a law is not the same then? "Next, we'll explain JavaScript primitive types and reference types in the end is how to save.

 

Memory space

  To understand the process of running JavaScript in the data is how to store, you have to first find out which types of storage space. Here is the JavaScript memory model I paint, you can refer to the following:

JavaScript memory model

 

  从图中可以看出,在 JavaScript 的执行过程中,主要有三种类型内存空间,分别是代码空间栈空间堆空间

 

  其中的代码空间主要是存储可执行代码的,这个我们后面在做介绍,今天主要来说说栈空间和堆空间。

 

栈空间和堆空间

  这里的栈空间就是我们之前反复提及的调用栈,是用来存储执行上下文的。为了搞清楚栈空间是如何存储数据的,我们还是先看下下面这段代码:

function foo(){
    var a = "极客时间"
    var b = a
    var c = {name:"极客时间"}
    var d = c
}
foo()

 

  前面文章我们已经讲解过了,当执行一段代码时,需要先编译,并创建执行上下文,然后再按照顺序执行代码。那么下面我们来看看,当执行到第 3 行代码时,其调用栈是状态,你可以参考下面这张调用栈状态图:

执行到第 3 行时的调用栈状态图

 

  从图中可以看出来,当执行到第 3 行时,变量 a 和 变量 b 的值都被保存在执行上下文中,而执行上下文又被压入到栈中,所以你也可以认为变量 a 和 变量 b 的值都是存放在栈中的。

 

  接下来继续执行第 4 行代码,由于 JavaScript 引擎判断右边的值是一个引用类型,这时候处理的情况就不一样了,JavaScript 引擎并不是直接将该对象存放到变量环境中,而是将它分配到堆空间里面,分配后该对象会有一个在 ”堆“ 中的地址,然后再将该数据的地址写进 c 的变量值,最终分配好内存的示意图如下所示:

对象类型是 ”堆“ 来存储

 

  从上图你可以清晰地观察到,对象类型是存放在堆空间的,在栈空间中只是保留了对象的引用地址,当 JavaScript 需要访问该数据的时候,是通过栈中的引用地址来访问的,相当于多了一道转手流程。

 

  好了,现在你应该知道了原始类型的数据值都是直接保存在 ”栈“ 中的,引用类型的值是存放在 ”堆“ 中的。不过你也许会好奇,为什么一定要分 ”堆“ 和 ”栈“ 两个存储空间呢?所有数据直接存放在 ”栈“ 中不就可以了吗?

 

  答案是不可以的。这是因为 JavaScript 引擎需要用栈来维护程序执行期间上下文的状态,如果栈空间大了话,所有的数据都存放在栈空间里面,那么会影响到上下文切换的效率,进而又影响到整个程序的执行效率。比如文中的 foo 函数执行结束了,JavaScript 引擎需要离开当前的执行上下文,只需要将指针下移到上个执行上下文的地址就可以了,foo 函数执行上下文栈区空间全部回收,具体过程你可以参考下图:

调用栈中切换执行上下文状态

 

  所以通常情况下,栈空间都不会设置太大,主要用来存放一些原始类型的小数据。而引用类型的数据占用的空间都比较大,所以这一类数据会被存放到堆中,堆空间很大,能存放很多大的数据,不过缺点是分配内存和回收内存都会占用一定的时间。

 

  解释了程序在执行过程中为什么需要堆和栈两种数据结构后,我们还是回到示例代码那里,看看它最后一步将变量 c 赋值给 变量 d 是怎么执行的?

 

  在 JavaScript 中,赋值操作和其他语言有很大的不同,原始类型的赋值会完整复制变量值,而引用类型的赋值是赋值引用地址

 

  所以 d = c 的操作就是把 c 的引用地址赋值给 d,你可以参考下图:

引用赋值

 

  从图中你可以看到,变量 c 和 变量 d 都指向了同一个堆中的对象,所以这就很好地解释了文章开头的那个问题,通过 c 修改 name 的值,变量 d 的值也跟着改变,归根结底它们是同一个对象。

 

再谈闭包

  现在你知道了作用域内的原始类型数据都被存储到栈空间,引用类型会被存储到堆空间,基于这两个点的认知,我们再深入一步,探讨下闭包的内存模型。

 

  这里以《10 | 作用域链和闭包:代码中出现相同的变量,JavaScript 引擎是如何选择的?》中关于闭包的一段代码为例:

function foo() {
    var myName = "极客时间"
    let test1 = 1
    const test2 = 2
    var innerBar = { 
        setName:function(newName){
            myName = newName
        },
        getName:function(){
            console.log(test1)
            return myName
        }
    }
    return innerBar
}
var bar = foo()
bar.setName("极客邦")
bar.getName()
console.log(bar.getName())

  当执行这段代码的时候,你应该有过这样的分析:由于变量 myName、test1、test2 都是原始类型数据,所以在执行 foo 函数的时候,它们会被压入到调用栈中;当 foo 函数执行结束之后,调用栈中 foo 函数的执行上下文会被销毁,其内部变量 myName、test1、test2 也应该一同被销毁。

 

  但是在 那篇文章中,我们介绍了当 foo 函数的执行上下文销毁时,由于 foo 函数产生了闭包,所以变量 myName 和 test1 并没有被销毁,而是保存在内存中,那么应该如何解释这个现象呢?

 

  要解释这个现象,我们就得站在内存模型的角度来分析这段代码的执行流程。

1、当 JavaScript 引擎执行到 foo 函数时,首先会编译,并创建一个空执行上下文。

2、在编译过程中,遇到内部函数 setName ,JavaScript 引擎还要对内部函数做一次快速的词法扫描,发现该内部函数引用了 foo 函数中的 myName 变量,由于是内部函数引用了外部函数的变量,所以 JavaScript 引擎判断这是一个闭包,于是在堆空间创建换一个 ”closure(foo)“ 的对象(这是一个内部对象,JavaScript 是无法访问的),用来保存 myName 变量。

3、接着继续扫描到 getName 方法时,发现该函数内部还引用变量 test1,于是 JavaScript 引擎又将 test1 添加到 ”closure(foo)“ 对象中。这时候堆中的 ”closure(foo)“ 对象中就包含了 myName 和 test1 两个变量了。

4、由于 test2 并没有被内部函数引用,所以 test2 依然保存着调用栈中。

 

 

通过上面的分析,我们可以画出执行到 foo 函数中的 ”return innerBar“ 语句时的调用栈状态,如下图所示:

闭包的产生过程

 

  从上图你可以清晰地看出,当执行到 foo 函数时,闭包就产生了;当 foo 函数执行结束之后,返回的 getName 和 setName 方法都引用 ”clourse(foo)“  对象,所以即使 foo 函数退出了, ”clourse(foo)“ 依然被其内部的 getName 和 setName 方法引用。所以在下次调用 bar.setName 或者 bar.getName 时,创建的执行上下文中就包含了 "clourse(foo)"。

 

  总的来说,产生闭包的核心有两步:第一步是需要预扫描内部函数;第二部是把内部函数引用的外部变量保存到堆中。

 

总结

  好了,今天就讲到这里,下面我来简单总结下今天的要点。

 

  我们介绍了 JavaScript 中的 8 种数据类型,它们可以分为两大类——原始类型和引用类型

 

  其中,原始类型的数据是存放在中,引用类型的数据是存放在中的。堆中的数据是通过引用和变量关联起来的。也就是说,JavaScript 的变量是没有数据类型的,值才有数据类型,变量可以随时持有任何类型的数据。

 

  然后我们分析了,在 JavaScript 中将一个原始类型的变量 a 赋值给 b,那么 a 和 b 会相互独立、互不影响;但是将引用类型的变量 a 赋值给变量 b,那会导致 a、b 两个变量都同时指向了堆中的同一块数据。

 

  最后,我们还站在内存模型的视角分析了闭包的产生过程。

 

思考时间

  在实际的项目中,经常需要完整地拷贝一个对象,也就是说拷贝完成之后两个对象之间就不能互相影响。那该如何实现呢?

 

  结合下面这段代码,你可以分析下它是如何将对象 jack 拷贝给 jack2,然后在完成拷贝操作时两个 jack 还互不影响的呢。

let jack = {
    name : "jack.ma",
    age:40,
    like:{
        dog:{
            color:'black',
            age:3,
        },
        cat:{
            color:'white',
            age:2
        }
    }
}
function copy(src){
    let dest
    //实现拷贝代码,将src的值完整地拷贝给dest
   //在这里实现
    return dest
}
let jack2 = copy(jack)

//比如修改jack2中的内容,不会影响到jack中的值
jack2.like.dog.color = 'green'
console.log(jack.like.dog.color) //打印出来的应该是 "black"

 

问题记录

1、从内存模型角度分析执行代码的执行流程第二步看,在堆空间创建closure(foo)对象,它是存储在foo函数的执行上下文中的。
那么closure(foo)创建开始时是空对象,执行第三步的时候,才会逐渐把变量添加到其中。
2、当foo函数执行结束后,foo的执行上下文是不是销毁了?如果销毁了,产生一下两个疑问:
    a、如果foo函数执行上下文销毁了,closure(foo)并没有销毁,那foo函数执行上下文是怎么销毁的呢?就比如销毁一个盒子,盒子毁里,里面的东西应该也是毁掉的
    b、既然closure(foo)既然没有销毁,那它存储在堆中的什么地方呢?必定它所依赖的foo执行上下文已经不存在了

作者回复: 关于foo函数执行上下文销毁过程:foo函数执行结束之后,当前执行状态的指针下移到栈中的全局执行上下文的位置,foo函数的执行上下文的那块数据就挪出来,这也就是foo函数执行上下文的销毁过程,这个文中有提到,你可以参考“调用栈中切换执行上下文状态“图。

第二个问题:innerBar返回后,含有setName和getName对象,这两个对象里面包含了堆中的closure(foo)的引用。虽然foo执行上下文销毁了,foo函数中的对closure(foo)的引用也断开了,但是setName和getName里面又重新建立起来了对closure(foo)引用。

你可以:
1:打开“开发者工具”
2:在控制台执行上述代码
3:然后选择“Memory”标签,点击"take snapshot" 获取V8的堆内存快照。
4:然后“command+f"(mac) 或者 "ctrl+f"(win),搜索“setName”,然后你就会发现setName对象下面包含了 raw_outer_scope_info_or_feedback_metadata,对闭包的引用数据就在这里面。

 

最近面试老问这个问题,什么是深拷贝和浅拷贝以及如何实现一个深拷贝?

1、JSON.parse(JSON.stringify(obj))
2、递归遍历对象
3、Object.assigin() 这种方法只能拷贝一层,有嵌套的情况就不适用了。

 

老师,我有几个疑问:
1、Function 函数类型也是继承于Object,声明函数后是不是也是存在堆空间中的,那么浏览器编译函数时是不是会同时创建执行上下文和向堆空间中压入一个值
2、function a(){
            var b = 1;
            var c = {
                    d: 2
            };
     }
     当 a 的执行上下文销毁后,c 对象在堆空间中的引用会跟着销毁么,将 c 返回出去或不返回,会不会是不一样的情况

作者回复: 函数就是一种特别的对象,所以会保存在堆上,编译函数时,这个函数的已经存在于堆中了!

第二个问题返回了c对象的话,那么说明全局环境对c对象有引用,既然有引用那么就不会被垃圾回收器标记出来,所以c对象也就不会回收!

 

“JavaScript 中的数据类型一共有 8 种。”

TypedArray,Blob,FIle,Promise这些呢?

作者回复: 这些都属于object类型

Guess you like

Origin www.cnblogs.com/bala/p/12188441.html