美文网首页工作生活
前端基础查漏补缺(一):内存空间

前端基础查漏补缺(一):内存空间

作者: AizawaSayo | 来源:发表于2019-07-02 13:08 被阅读0次

首先说一下这一系列基本上都是整理引用自大大这波能反杀的文章《前端基础进阶系列,中途会加上一些自己的扩展延伸,以便加深自己对底层js的理解。若有侵权请指正。

一、内存空间

JavaScript中并没有严格意义上区分栈内存与堆内存。因此我们可以简单粗暴的理解为JavaScript的所有数据都保存在堆内存中。但是在某些场景,我们仍然需要基于堆栈数据结构的思维来实现一些功能,因此理解栈数据结构的原理与特点十分重要。

三种数据结构:堆(heap),栈(stack)、队列(queue)。

堆数据结构:key-value,按值查找。就像图书馆按书名查。(如变量对象的存储)
栈数据结构:先进后出,后进先出,就像乒乓球盒子。(如执行上下文执行顺序,也就是函数调用栈)

栈数据结构图示
队列:先进先出(FIFO)。就像排队过安检
队列图示
变量对象与基础数据类型

JavaScript的执行上下文生成之后,会创建一个叫做变量对象的特殊对象,JavaScript的基础数据类型往往都会保存在变量对象中。

变量对象也是存放于堆内存中,但是由于变量对象的特殊职能,我们在理解时仍然需要将其于堆内存区分开来。

基础数据类型:Undefined、Null、Boolean、Number、String、Symbol(ES6)。按值访问,这些类型可以直接操作保存在变量中的实际的。
基础类型使用声明式赋值,永远都是去常量池中查找Number、null 、undefined、String、Boolean,没有再次声明就放在常量池中,若声明的变量名已存在就使用同一个值,不重复分配地址。

//虽然字面就很好理解,但从堆内存存储的角度重新理解下
var a=1;
var a;
console.log(a);//1 同名即是同一个地址,直接能访问其中的值。
var b=2;
var b=3
console.log(b);//3 因为访问的是同一个变量地址,改变就能直接改变这个值
引用数据类型与堆内存

Object(在JS中除了基本数据类型以外的都是对象,数据是对象,函数是对象,正则表达式也是对象)

var a1 = 0;   // 变量对象
var a2 = 'this is string'; // 变量对象
var a3 = null; // 变量对象

var b = { m: 20 }; // 变量b存在于变量对象中,{m: 20} 作为对象存在于堆内存中,b为存储{m:20}对象的地址(指针)
var c = [1, 2, 3]; // 变量c存在于变量对象中,[1, 2, 3] 作为对象存在于堆内存中
上例图解
// demo01
var a = 20;
var b = a;
b = 30;
// 这时a的值是多少?
console.log(a);//20
console.log(b);//30
// demo02
var m = { a: 10, b: 20 }
var n = m;
n.a = 15;
// 这时m.a的值是多少
console.log(m.a);//15

demo1的a是常量变量,b是另一个声明的变量,在执行阶段把a复制了一份给b,b就直接获得了a的值。而这两个变量的值相互独立互不影响。
而在demo02中,m变量存的指向堆内存中{a: 10, b: 20}的地址,因此var n = m执行的是一次复制引用类型的操作。同样也是为新的变量自动分配一个新的值保存在n中,不同的是,这个新的值只是引用类型的一个地址指针。尽管他们相互独立,但是在变量对象中访问到的具体对象实际上是同一个。如图所示。


demo02图解

基础数据类型的比较是值的比较

var a = 1;
var b = 1;
console.log(a === b);//true

引用类型的比较是引用的比较(堆的空间)

 var a = [1,2,3];
var b = [1,2,3];
console.log(a === b); // false
共享传递,深拷贝和浅拷贝

当数据作为实参传递函数的形参时,也是分数据类型的。
按值传递就是把实参在内存栈中的数据传递给形参, 然后在方法内部就可以使用形参的值了, 而引用传递(共享传递)是把实参的内存栈的地址编号传递给形参。

//demo3
var value = 1;
function foo(v) {
    v = 2;
    console.log(v); //2
}
foo(value);
console.log(value) // 1

demo3 foo(value)执行时,v得到value的拷贝,在内存中为形参v分配了一个地址, 其中保存了1这个值, 这时给v赋值,是直接修改形参v的值,而value的值不会变。

//demo4
var obj = {
    value: 1
};
function foo(o) {
    console.log(o);//{value:1}
    o.value = 2;
    console.log(o.value); //2
}
foo(obj);
console.log(obj.value) // 2

demo4中形参o拷贝得到是obj的指针地址,指向堆内存中的同一个对象,因此改变o的属性时,obj的属性也会改变。

//demo5
var obj = {
    value: 1
};
function foo(o) {
    o = 2;
    console.log(o); //2
}
foo(obj);
console.log(obj) // {value: 1}

共享传递: 在传递对象的时候,拷贝了一份对象引用地址的副本,然后对这个引用进行操作。demo4也是共享传递(引用传递)
demo5直接改变了形参o的数据类型,让它不再指向obj所指向的地址,因此obj地址中的对象内容自然不会变。
浅拷贝
前面已经提到,在定义一个对象或数组时,变量存放的往往只是一个地址。当我们使用对象拷贝时,如果属性是对象或数组时,这时候我们传递的也只是一个地址。因此子对象在访问该属性时,会根据地址回溯到父对象指向的堆内存中,即父子对象发生了关联,两者的属性值会指向同一内存空间。

var a={key1:"11111"}
function Copy(p){
   var c ={};
   for (var i in p){
      c[i]=p[i]
   }    
   return c;
}
a.key2 = ["小辉","小辉"]
var b = Copy(a);
b.key3 = "33333"
alert(b.key1)//11111
alert(b.key3)//33333
alert(a.key3);//undefined
b.key2.push("大辉")
alert(a.key2);//小辉,小辉,大辉

本来b只是一个新的对象,return的c得到的形参p的每一个属性,和a指向的地址毫无关联,但是当修改的属性变为对象或数组时,那么父子对象之间就发生关联,原因是key1的值属于基本类型,所以拷贝的时候传递的就是该数据段;但是key2的值是堆内存中的对象,所以key2在拷贝的时候传递的是指向key2对象的地址,无论复制多少个key2,其值始终是指向父对象的key2对象的内存空间。

//ES6实现浅拷贝的方法
var a = {name:"暖风"} var b= Object.assign({},a);
b.age = 18;
console.log(a.age);//undefined
----------------------------------
//数组
var a = [1,2,3]; var b = a.slice();
b.push(4);
b//1,2,3,4
a//1,2,4
----------------------------------
var a = [1,2,3]; var b = a.concat();
b.push(4);
b//1,2,3,4
a//1,2,4
----------------------------------
var a = [1,2,3]; var b = [...a]
b//1,2,3,4
a//1,2,4</pre>

深拷贝
或许浅拷贝并不是我们在实际编码中想要的结果,我们不希望父子对象之间产生关联并且父对象的内容被改变,那么这时候可以用到深拷贝。既然属性值类型是数组和或象时只会传址,那么我们就用递归来解决这个问题,把父对象中所有属于对象的属性类型都遍历赋给子对象即可。测试代码如下:

var a={key1:"11111"}
function Copy(p,c){
   var c =c||{};
   for (var i in p){
    if(typeof p[i]==="object"){
      c[i]=(p[i].constructor ===Array)?[]:{}
      Copy(p[i],c[i]);
    }else{
       c[i]=p[i]
    }
   }    
   return c;
}
a.key2 = ["小辉","小辉"]
var b = {}
b = Copy(a,b); 
b.key2.push("大辉");
b.key2//小辉,小辉,大辉
a.key2//小辉,小辉   
内存空间管理

JavaScript的内存生命周期

  1. 分配你所需要的内存
  2. 使用分配到的内存(读、写)
  3. 不需要时将其释放、归还
var a = 20;  // 在内存中给数值变量分配空间
alert(a + 100);  // 使用内存
a = null; // 使用完毕之后,释放内存空间

JavaScript的自动垃圾收集机制就是找出那些不再继续使用的值,然后释放其占用的内存。垃圾收集器会每隔固定的时间段就执行一次释放操作。
在JavaScript中,最常用的是通过标记清除的算法来找到哪些对象是不再继续使用的,因此a = null其实仅仅只是做了一个释放引用的操作,让 a 原本对应的值失去引用,脱离执行环境,这个值会在下一次垃圾收集器执行操作时被找到并释放。而在适当的时候解除引用,是为页面获得更好性能的一个重要方式。

扩展一下null数据类型
null值表示一个空对象指针(引用),undefined 值是派生自 null 值的,所以typeof null ==> object(特殊)
undefined 是表示已声明但没有被赋值(未经初始化)的变量.
利用null和undefined就可以区分对象空指针和的变量
要定义的变量是将来保存对象用的,就应该明确地给该变量初始化为null。这样做不仅可以体现null作为空对象指针的惯例,而且也有助于进一步区分null和undefined。只要直接检查null值就可以知道相应的变量是否已经保存了一个对象的引用。

//准确判断
var exp = null; 
!exp && typeof exp != "undefined" && exp != 0  ==>true
exp === null  ==>true

//有误区的判断
!exp  ==>true
//如果 exp 为 undefined,或数字零,或 false,也会得到与 null 相同的结果。
//同时判断 null、undefined、数字零、false 时可使用本法。
exp == null  ==>true
//exp为 undefined 时,也会得到与 null 相同的结果。
//要同时判断 null 和 undefined 时可使用本法。
typeof exp == "null"
//为了向下兼容,exp 为 null 时,typeof null 返回 object,所以不能这样判断。

在局部作用域中,当函数执行完毕,局部变量也就没有存在的必要了,因此垃圾收集器很容易做出判断并回收。但是全局变量什么时候需要自动释放内存空间则很难判断,因此在我们的开发中,需要尽量避免使用全局变量。

参考文章之一:前端基础进阶(一):内存空间详细图解

相关文章

  • 前端基础查漏补缺(一):内存空间

    首先说一下这一系列基本上都是整理引用自大大这波能反杀的文章《前端基础进阶系列,中途会加上一些自己的扩展延伸,以便加...

  • 从新开始

    安卓、前端。从新开始,查漏补缺。

  • 【Android面试查漏补缺】之事件分发机制详解

    前言 查漏补缺,查漏补缺,你不知道哪里漏了,怎么补缺呢?本文属于【Android面试查漏补缺】系列文章第一篇,持续...

  • PHP 基础查漏补缺

    1、注释的第三种写法 使用#,这是shell风格的写法。 2、PHP 不像许多其他的编程语言,它不支持全局变量(除...

  • 基础英语-查漏补缺

    关于Hope的用法的易错点 1. hope的被动语态中多用it作为形式主语 eg. It is hoped tha...

  • python查漏补缺-基础

    最近刷题感觉一些简单的概念看似很熟悉,实际上还有很多旮旯需要掌握,本篇不做笼统的汇总,仅针对一些易混淆概念之间的区...

  • iOS知识体系

    iOS 的知识体系,包括了基础、原理、应用开发、原生与前端四大模块。 根据下面的体系框架图,慢慢查漏补缺吧

  • 查漏补缺

    如果想让HTML5标签兼容低版本浏览器的话,可以使用 html5shiv js来实现。注意:一定要把它引入到前面。...

  • 查漏补缺

    图文环绕和浮动 最初的CSS只是用来写文章,熟练使用float和clear两个属性来布局: float属性:指定一...

  • 查漏补缺

    1.js字符串转换成数字与数字转换成字符串的实现方法https://www.2cto.com/kf/201612/...

网友评论

    本文标题:前端基础查漏补缺(一):内存空间

    本文链接:https://www.haomeiwen.com/subject/upuacctx.html