美文网首页
04|变量,作用域和内存问题

04|变量,作用域和内存问题

作者: 井润 | 来源:发表于2019-11-20 14:08 被阅读0次

可能看到这里就会有人问我,为什么没有介绍到JavaScript中的基本概念啊? 其实对应的没有对基本概念的介绍并不是因为我不对基本概念重视,而是我本身写速度系列就是快速并且巩固JavaScript的内容,主要是针对重点知识! 基本概念相关的笔记虽然写了,但是由于对应的知识点比较散,就不准备介绍了,如果在学前端的小伙伴可以针对性的去看JavaScript高级程序设计!

01|基本类型和引用类型的值

  • 基本类型:是简单的数据段
  • 引用类型:那些可能有多个值构成的对象
01|动态的属性只能够应用在引用类型中
let person = new Object();
person.name = 'Juli';
console.log(person.name); //Juli

如果说是普通的基本类型呢? 是不能够添加动态的数据类型的

let name = 'Jake';
name.age = 27;
console.log(name.age);//undefined
02|变量复制值

其实是分为两种情况的,一种是 基本类型,直接复制值到另外的对象上(此时的对象为变量对象 也就是变量本身)

如果说是引用数据类型的话,就是 变量对象的引用地址指向同一块内存(Memory)

let a = new Object();
let b = a;
a.name = "ProbeDream";
console.log(b.name);//ProbeDream

其实就是堆内存的内存指向问题,new Object() 产生新的对向(此时就开辟了一块内存空间 变量a为内存空间具体地址的引用!) 并且变量b也指向了变量a所指向的内存地址,一旦内存空间的属性发生变化,对应的b也可以拿到发生变化之后的值!

03|传递参数

ECMAScript中所有函数的参数都是按值传递的,对应的意思也就是 函数外部的值复制给函数内部的参数,就和把一个变量复制到另一个变量一样!

  • 基本类型值传递和基本类型变量复制一样!
  • 引用类型值传递和引用类型变量的赋值一样!
function add(num){
    num += 10;
    return num;
}
let count = 20;
let result = add(count);//基本数据类型的值传递
console.log(count); //20
console.log(result); //30

之所以打印出来的count的值是20,而不是三十和之前讲到过的一样,基本类型的值传递其实就是基本类型的变量复制,这个20被传递给num,仅仅只是具有相同的值,并且函数返回的是num+10之后的值和count并没有关系! 如果对应的是引用类型的值传递的话,那么就不同了!

function setName(object){
    object.name = "ProbeDream";
}
let person = new Object();
setName(person);//引用类型的值传递
console.log(person.name);//ProbeDream

Person指向的对象在堆内存中只有一个,并且是全局对象! 在函数中的修改,在外部的Person上面也会有所反映!

但是局部作用域中修改的对象并不是按照引用传递的,而是按照值传递的!

function setName(Object){
    Object.name = "ProbeDream";;
    Object = new Object();
    Object.name = "xiaoHu";
}
let person = new Object();
setName(person);
console.log(person.name);//ProbeDream

如果说不告诉你答案,你固执的认为引用类型的值传递,是引用传递的话,person.name不应该是xiaoHu吗?

为什么是ProbeDream,是因为函数中的参数内部重写Object,该变量引用是一个局部对象! 函数执行完毕之后就会销毁!

其实通过对应的输出,就能够知道对应的原始的引用没有变化!

04|类型的检测

其实对应的类型的检测在JavaScript中来讲还是一个比较重要的点,如何确定变量是所属哪种类型呢? 对应的关键的操作符 typeof是比较重要的工具,typeof是确定一个变量是字符串,数值,布尔值,还是undefined的最佳工具! 如果对应的变量的值为null或者说会null的话,typeof对应的结果为 "object"

  • 如何将检测变量为引用类型? 使用 instanceof关键字检测引用类型

所有引用类型都是object实例的对象,因此在检测一个引用类型的值和object构建函数的时候instanceof会返回true,如果说是检测最基本的类型的时候,对应的返回值为false! 显然,基本类型不是对象!

02|执行环境和作用域

执行环境/环境是JavaScript最为重要的一个概念,执行环境定义了变量或者函数有权访问其他数据! 并且决定了它们各自的行为 每个环境/执行环境都有其对应的变量对象,环境中所对应的变量以及函数都保存在对象中! 对应的编写代码的时候无法访问该对象,但是解析器在处理数据的时候会在后台使用到它!

全局执行环境是最外围的执行环境,Web浏览器中全局执行环境为window,node中的话全局执行环境则为global,对应的全局变量和函数都是作为全局执行环境对象的变量和方法(对象中的函数)存在! 某个执行环境中的所有代码执行完毕之后,该环境则会被销毁,对应的变量和函数定义也会随即被销毁! 全局环境的销毁其实是在我们关闭浏览器的时候销毁的!

其中对应的函数的执行流程是让人非常想了解的,其实对应的,每个函数都有自己的执行环境,当执行流进入到一个函数的时候,函数的环境就会被推入一个环境栈(Environment Stack),对应的函数执行完毕之后,环境栈弹出对应的环境,将控制权返回给之前的执行环境!

01|作用域链

对应的当代码在一个环境中执行的时候,会创建变量对象的作用域链,作用域链的用途时保证对执行环境有权访问的所有变量和函数的有序访问!

作用域链的前端,始终都是当前执行代码所在的环境的变量对象,如果该环境为函数的话,则将其活动对象作为变量对象! 活动对象在最开始的时候只包含一个变量,即arguments对象 (该对象在全局环境中并不存在!) 作用域链中的下一个对象来自包含环境,而下一个变量则是下一个包含环境!

我们可以以一个形象的例子来说明作用域链:

let color = "blue";
function changeColor(){
    let anotherColor = "red";
    function swapColors(){
        let tempColor = anotherColor;
        anotherColor = color
        color = tempColor;
        //该作用域中可以访问color,anotherColor,tempColor
    }
    //该作用域中可以访问color和anotherColor但是并不能够访问tempColor
    swapColors();
}
//这里只能够访问 color
changeColor();

为什么会这样呢?

  • swapColors函数内部可以访问其他两个环境中的所有变量! 对应的两个环境(changeColor和全局环境)都是它的父执行环境!
window
    |
    |
    ----color
    |
    |
    ----changeColor()
        |
        |--anotherColor
        |
        |--swapColors()
            |
            |--tempColor

对应的上面的示例图,应该能够很清楚的解释所谓的 作用域链

02|没有块级作用域

如果说你学过类C语言风格的编程语言的话,那么你就会明显的感觉到JavaScript的设计确实有那么一点点不合理!

没有块级作用域在ES5的时候!

if(true){
    var color = "blue";
}
console.log(color);//blue

for(var i=0;i<10;i++){
    doSomeThing(i);
}
console.log(i);//10

是不是非常的不合理,if与语句中的变量声明会将变量添加到当前的执行环境,当前的是全局环境!

for循环的话,语句中创建的i即使在循环执行结束之后,依旧会存在于循环外部的执行环境之中!

  1. 声明变量
  • 使用var声明的变量会被自动添加到最接近的环境中,函数内部的话,那么最接近环境就是函数的局部环境
  • 在with语句中,最接近的环境就是函数环境!
function add(n1,n2){
    var sum = n1+n2;
    return sum;
}
var result = add(10,20);
console.log(sum);//ReferenceError: sum is not defined

对应的报错就是 引用错误,对应的sum在全局环境中没有被定义!

function add(n1,n2){
    sum = n1+n2;
    return sum;
}
var result = add(10,20);
console.log(sum);//30
console.log(window.sum);

之所以sum能够被打印出来是因为没有在函数中使用var因此不是函数作用域中的变量,sum在调用add完成之后被添加到了全局环境中,函数执行完成对应的变量sum依然存在!

03|垃圾收集

其实在JavaScript中,JavaScript石油气自动垃圾回收机制的,也就是说,环境执行会负责管理代码执行过程中使用的内存的!

在C和C++之类的语言当中,关于内存这块是需要手动进行管理的,开发人员最基本的一项任务就是,手工跟踪内存的使用情况! JavaScript则是实现了自动管理!

其实函数中局部变量的正常生命周期中,局部变量只在函数执行的过程中存在,并且在这些过程中,会为局部变量在栈(或者说是堆)内存上分配对应的内存地址!来存储他们的值,对应的等到函数执行完毕之后,局部变量就没有存在的必要了,因此可以释放对应的内存以供将来使用,虽然在这种情况下,很容易判断变量是否还有存在的必要,但并不是所有的应用场景下都是这种结论!

但是一般情的情况下是这样的,垃圾回收器会根据对应的变量是否有用还是没有,之类的做对应的标识,为以后准备收回占用的内存! 但是具体到浏览器的实现上面,对应的有两种回收策略:

01|标记清除
  1. 变量进入环境(函数中定义变量),该变量就会被标记为,进入环境!
    1. 该情况下不能够回收进入环境的变量,因为随时可能会被用到!
    2. 一旦变量离开了环境的时候,将会将其标记为 离开环境!
  2. 垃圾回收器会在运行的时候给存储在内存中的所有变量,打上标记,然后去掉环境中的变量以及被环境中的变量引用的变量的标记! 之后再被标记的变量将被视为准备删除的变量,原因是环境中的变量已经无法访问到这些值了! 对应的垃圾回收器完成内存清除的工作,销毁那些标记的值并且回收他们所占用的内存空间!
  3. 2008年止,IE,FireFox,Opera,Chrome和对应的Safari实现的都是采用标记清除的垃圾回收机制策略! 只不过对应的垃圾回收时间间隔不同!
02|引用计数
  1. 这是一种不太常见的垃圾回收策略
    1. 跟踪记录每个值被引用的次数,将一个引用类型的值赋值给对应的变量的时候,引用次数+1 如果说对应的变量换了另外一个引用值的话,对应的引用次数-1
    2. 当对应的值的引用次数为0的时候,说明没有办法再使用该值了!

我们通过简单的例子,分别用 两种不同的策略来验证具体的回收过程,看看两者之间的区别:

function examle01(){
    let objectA = new Object();
    let objectB = new Object();
    objectA.someOtherObject = objectB;
    objectB.anotherObject = objectA;
}
example01();
  • 如果为标记清除法

对应的因为函数执行完毕之后,函数作用域里面的变量的相互引用已经完全不是问题了,垃圾回收器直接对其进行回售操作!

  • 如果为引用计数

对应的引用计数的话,objectA和objectB通过各自的属性相互引用,因此不可能存在引用次数为0的情况下,因此很难被回收,假设该函数被多次调用,会出现大量的内存无法被回收的情况!

后面NetScape Navigator4.0中放弃了引用计数的方式,转而使用标记清除的垃圾回收策略! 但是引用计数的策略带来的问题仍然没有得到解决:

  • IE中一部分对象并非原生对象
    • BOM和DOM对象就是使用C++以COM(Component Object Model)对象的形式实现的! COM对象的垃圾回收机制就是使用的引用计数,对应的无论是在回收策略为标记清除的浏览器上面,一旦使用到了相关的COM对象,就无法避免的会出现循环引用的问题!
    • 我们可以通过DOM中简单的例子来说明这一切
<!DOCTYPE html>
<html>
<head>
  <meta charset="utf-8">
  <title>JS Bin</title>
</head>
<body>
    <div class="example">
        This is example code!
    </div>
</body>
        <script>
            let element = document.querySelector(".example");
            ket myObject = new Object();
            myObject.element = element;
            element.someObject = myObject;
        </script>
</html>

其实很好理解的是关于DOM的操作,DOM在前文中提到过,是COM对象,回收策略是引用计数回收策略,现在dom对象和普通的对象形成了循环应用,因此DOM从页面上被移除了的话,对应的也不会被回收!

  • 难道就没有好的解决方法了吗?

我们可以手动的断开他们之间的连接

myObject.element = null;
element.someObject = null;

需要注意的是,该问题的解决在IE9的时候完成的,IE9把BOM和DOM对象都转换为了真正的JavaScript对象,就避免了两种垃圾回收算法并存导致的问题,也清除了常见的内存泄漏的问题!

03|性能问题

我们要知道垃圾回收器是周期性运行的,也就是说以周期的形式来回收垃圾,如果说变量分配的内存数量非常可观的话,那么对应的回收工作量也是相当大的,对应的垃圾回收器的间隔是一个非常重要的问题,就比如说之前的IE因为设计上的失误导致声名狼藉的内存回收机制的性能问题!

IE浏览器之前的垃圾回收策略是按照内存分配量运行的,也就是说对应的垃圾到达到了一个临界值的时候就会启用回收回收器进行垃圾回收!

这里所说的临界值如果说具体一点的话是这样的:

  • 256个变量
  • 4096个对象/数组字面量和数组元素
  • 64KB的字符串

其实这样设计如果说遇到一种对应的场景的时候就有问题了,比如说:一个脚本中包含了很多变量,那么对应的脚本在生命周期中一直抱有这么多的变量的话,对应的垃圾回收器就会频繁的运行,一直忙个不停,最后引发了眼中的性能问题!

后面垃圾回收器改变了对应的工作方式:

  • 动态的调整临界值
  • 如果说垃圾回收的内存分配量低于15%的时候,临界值就会加倍
  • 如果说回收了85%的时候,对应的临界值则重置为默认的临界值!

这样一来IE上面运行大量JavaScript也面试的性能就大大提升了!

05|管理内存

使用具备垃圾回收机制的语言编写程序,开发人员一般不必操心内存管理的问题,但是JavaScript在进行内存管理及垃圾收集的时候面临的问题其实是有些不同的!

  • 分配给Web浏览器的可用内存数量和桌面应用程序的少

    • 主要目的是为了防止运行JavaScript的网页耗尽全部系统内存导致系统崩溃!
  • 内存限制问题于此同时还会限制给变量分配内存! 还会影响调用栈在一个线程中能够同时执行语句的数量!

问题如何解决?

  • 为执行中的代码只保存必要的数据.如果说对应的数据不再有用的话,手动设置为null来释放对应的引用! 这种做法被别人称之为 解除引用!
    • 解除引用使用与大多数全局变量和全局对象的属性
    • 当我们的局部变量在离开执行环境的时候回自动解除引用!

我们通过对应的代码来演示:

function createPerson(name){
    let localPerson = new Object();
    localPerson.name = name;
    return localPerson;
}
let globalPerson = createPerson("ProbeDream");
globalPerson = null;//手动解除引用

其实对应的还是比较好理解的:

  • 函数本身的目的就是返回一个对象,但是localPerson在createPerson函数执行完毕之后就离开了其执行环境,因此我们无需显示的为它解除引用
  • 但是对应的函数返回的对象的变量globalPerson而言我们就需要手动解除引用了

其实对应的手动解除引用并不是意味着自动回收该值所占用的内存,解除引用的真正作用是让值脱离执行环境,以便垃圾回收器下次运行时将其回收!

如果您有好的建议,欢迎在评论区留言,谢谢!

相关文章

网友评论

      本文标题:04|变量,作用域和内存问题

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