美文网首页
JS回顾变量、作用域、内存问题

JS回顾变量、作用域、内存问题

作者: 席坤 | 来源:发表于2019-12-10 09:16 被阅读0次

    ECMAScript变量包含两种不同数据类型的值:基本数据类型引用数据类型

    在将一个值赋值给变量时,解析器必须知道这个值是基本数据类型还是引用数据类型.基本数据类型: Undefine、Null、Boolean、Number和String,这五种基本数据类型是按值访问的,因为可以操作保存变量中的实际的值.

    引用数据类型的值是保存在内存中的对象. 在操作对象时,实际上是在操作对象的引用而不是实际的对象。为此,引用类型的值是按引用放完的.

    属性的动态

    创建一个变量并为该变量赋值. 对引用类型的值,我们可以为其添加属性和方法,也可以改变和删除其属性和方法 如下:

    var person = new Object();
    person.name = "xxx";
    alert(person.name) //"xxx"
    

    但是我们不能给基本数据类型的值添加属性,尽管这样做不会导致任何错误,如:

    var name = "aa";
    name.age=18;
    alert(name.age); //undefine 
    

    复制变量值

    如果从一个变量向另一个变量复制基本数据的值,会在变量对象上创建一个新值,然后把该值复制到为新变量分配的位置上。如:

    var num1 = 5;
    var num2 = num1;
    

    在使用num1的值来初始化num2时,num2中也保存了值5,但 num1 和 num2 中的5 时完全独立的, 该值只是 num1 的一个副本, 这两个变量可以参加任何操作而不会相互影响。

    当一个变量向另一个变量复制引用类型的值时,同样也会在变量对象中复制一份放到为新变量分配的空间中。不同的是,这个值的副本实际上是一个指针,而这个指针指向存储在堆中的一个对象.复制操作结束后,两个变量实际上将引用同一个对象. 改变一个变量,就会影响另一个变量,如下:

    var obj1 = new Object();
    var obj2 = obj1;
    obj2.name = "xxx";
    alert(obj1.name); //name
    

    传递参数

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

    再向参数传递基本数据的值时,被传递的值会被复制给一个局部变量. 再向参数传递引用类型的值时,会把这个值的在内存中的地址复制给一个局部变量,如下:

    • 基本类型传参
    function add(num){
        num+=10;
        return num;
    }
    
    var count=20;
    var result=add(10);
    alert(count); //20 没有变化
    alert(result) //30
    
    • 引用数据类型传参
    function setName(obj){
        obj.name="name";
    }
    
    var person= new Object();
    setName();
    alert(person.name); //name
    

    检测类型

    在检测基本数据类型的时 typeOf 是非常得力的帮手,但是在检测引用类型的值时,ECMAScript 为我们提供了 instanceof 操作符,语法如下:

    result = variable instanceof constructor
    
    • 检测变量是Object
    obj instanceof Object
    
    • 检测变量是Array
    obj instanceof Array
    
    • 检测变量是RegExp
    obj instanceof RegExp
    

    在检测一个引用类型值和Object构造函数时,instanceof 操作符始终会返回 true,如果检测基本数据类型,始终会返回false, 因为基本类型不是对象.

    执行环境与作用域

    执行环境决定了变量或者函数有权访问的其他数据,决定了它们各自的行为. 每个执行环境都有一个与之关联的变量对象. 在Web浏览器中, 全局执行环境被认为是 Window 对象。

    查找变量的过程中,先找自己局部环境有没有变量或者函数,如果有,则查看声明有无赋值或者是函数的内容,如果没有,向上提升.

    1.执行环境决定了变量的生命周期,以及哪部分代码可以访问其中变量

    2.执行环境有全局执行环境(全局环境)和局部执行环境之分。

    3.每次进入一个新的执行环境,都会创建一个用于搜索变量和函数的作用域链

    4.函数的局部环境可以访问函数作用域中的变量和函数,也可以访问其父环境,乃至全局环境中的变量和环境。

    5.全局环境只能访问全局环境中定义的变量和函数,不能直接访问局部环境中的任何数据。

    6.变量的执行环境有助于确定应该合适释放内存。

    延长作用域链

    执行环境的类型总共有两种-- 全局 和 局部 ,但是还有其他方法来延长作用域链 ,如下:

    • try-catch 语句的 catch 块

    • with 语句

    这两个语句都会在作用域链的前端添加一个变量对象。对 with 来说,会将执行的对象添加到作用域链中。 对 catch 来说,会创建一个新的变量对象,其中包含的是被抛出的错误对象的声明.

    function getUrl(){
        with(location){
           var url = href 
        }
        return url
    }
    

    在with 语句内部,定义了一个url 变量,因而url就成了执行函数执行环境的一部分,可以作为函数的值被返回.

    没有块级作用域

    在JavaScript 中,if 语句中的变量声明会将变量添加到当前的执行环境中.

    if(true){
        var age = 18;
    }
    alert(age); //18
    

    在JavaScript中, for 语句初始化变量的表达式所定义的变量,即使在for 循环执行结束后,也依旧会存在于循环外部的执行环境中,如下:

    for (var i = 0; i<10; i++){
        doSomething(i);
    }
    
    alert(i); //10
    

    声明变量

    使用 var 声明的变量会自动被添加到最接近的环境中. 在函数内部,最接近的环境就是函数的局部环境; 在with 语句中,最接近的环境就是函数环境. 如果 初始值变量没有使用var 声明 ,该变量会自动被添加到 全局环境中.

    • 全局变量
      在全局范围内声明的变量, 如 var a=1

    • 局部变量
      写入函数中的变量,叫做局部变量

    function add(num){
        var result = num+10;
        return result;
    }
    alert(add()) //20
    alert(result) // sum is not defined
    

    如果省去 var 关键字, sum 将可以访问到 :

    function add(num){
        result = num+10;
        return result;
    }
    alert(add()) //20
    alert(result) // 20
    

    提升

    提升有变量提升和函数提升之分:

    变量提升

    1 var a = 123;
    2 function fun() {
    3    console.log(a);
    4    var a = 456;
    5 }
    6 fun();
    7 console.log(a);
    

    上面输出结果是 undefine123,分析一下fun() 的作用域链 : 自己的变量对象---->全局的变量对象。解析在在函数执行环境中发现了 变量 a,因此不会向全局环境的变量中寻找,解析器在解析第三行的时候还不知道 a 的值 是多少,也就是说只知道有 a 这个值,但是并不知道它具体位置(还没执行到第四行), 因此输出 undefine. 第七行输出是因为作用域问题,当 局部作用域有a 属性时,不会去修改全局环境的变量 a

    我们把代码调整下

    1 var a = 123;
    2 function fun() {
    3     var a ;
    4    console.log(a);
    5   a = 456;
    6 }
    7 fun();
    8 console.log(a);
    

    这个现象就是 变量提升

    变量提升,就是把变量提升到函数的顶部,需要注意的是,变量提升只是提升变量的声明,不会把变量的值也提升上来

    函数提升

    函数提升就是把函数提升到前面。

    在JavaScript中函数的创建方式有三种:函数声明(静态的,如上)、函数表达式(函数字面量)、函数构造法(动态的,匿名的)。函数表达式的形式如下:

    var fun = function(){
        doSomething();
    }
    

    函数构造法构造函数的形式如下:

    var fun = new Function("para1","para2",...,"function body");    
    

    只有函数声明形式才能被提升

    //函数声明
    function myTest1(){ 
        func(); 
        function func(){ 
            console.log("我可以被提升"); 
        } 
    } 
    myTest1();
    
    //函数表达式
    function myTest2(){ 
        func(); 
        var func = function(){ 
            console.log("我不能被提升"); 
        } 
    } 
    myTest2();
    

    说了这么多,下面我们来点作用域的面试题:

    if (10) {
        var age = 28;
    }
    console.log(age);
    
    function add(num) {
        sum = num + 10;
        return sum
    }
    
    var result = add(10);
    console.log(sum);
    
    var a = 123;
    function fun() {
        console.log(a);
        var a = 456;
    }
    
    fun();
    console.log(a);
    
    var a = 123;
    function fun() {
        console.log(a);
        a = 456;
    }
    fun()
    console.log(a);
    
    var a = 123;
    function fun(a) {
        console.log(a);
        a = 456;
    }
    fun(489)
    console.log(a);
    
    var a = 12;
    function fun() {
        console.log(a);
        return 4;
        var a = 45;
    }
    fun()
    console.log(a);
    

    内存问题

    JavaScript,会在创建变量(对象,字符串等)时分配内存,并且在不再使用它们时自动释放内存,这个自动释放内存的过程称为垃圾回收。

    堆内存和栈内存

    栈内存主要用于存储各种基本类型的变量,包括Boolean、Number、String、Undefined、Null,**以及对象变量的指针

    堆内存主要负责像对象Object这种变量类型的存储

    内存生命周期

    JS 环境中分配的内存有如下声明周期:

    • 内存分配:当我们申明变量、函数、对象的时候,系统会自动为他们分配内存
    • 内存使用:即读写内存,也就是使用变量、函数等
    • 内存回收:使用完毕,由垃圾回收机制自动回收不再使用的内存

    内存回收

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

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

    引用计数算法

    引用计数算法定义"内存不再使用"的标准很简单,就是看一个对象是否有指向它的引用。如果没有其他对象指向它了,说明该对象已经不再需了.

    引用计数算法是个简单有效的算法。但它却存在一个致命的问题:循环引用。如果两个对象相互引用,尽管他们已不再使用,垃圾回收器不会进行回收,导致内存泄露。如下:

    function cycle() {
        var o1 = {};
        var o2 = {};
        o1.a = o2;
        o2.a = o1; 
        return "Cycle reference!"
    }
    
    cycle();
    

    标记清除算法

    标记清除算法将“不再使用的对象”定义为“无法达到的对象”。
    简单来说,就是从根部(在JS中就是全局对象)出发定时扫描内存中的对象。
    凡是能从根部到达的对象,都是还需要使用的。
    那些无法由根部出发触及到的对象被标记为不再使用,稍后进行回收。

    从这个概念可以看出,无法触及的对象包含了没有引用的对象这个概念(没有任何引用的对象也是无法触及的对象)。
    但反之未必成立。

    内存泄露

    内存泄漏就是由于疏忽或错误造成程序未能释放那些已经不再使用的内存,造成内存的浪费。

    不再用到的内存,没有及时释放,就叫做内存泄漏(memory leak)。

    • 内存泄漏的识别方法

    在 Chrome 浏览器中,我们可以这样查看内存占用情况

    1. 打开开发者工具,选择 Performance 面板
    2. 在顶部勾选 Memory
    3. 点击左上角的 record 按钮
    4. 在页面上进行各种操作,模拟用户的使用情况
    5. 一段时间后,点击对话框的 stop 按钮,面板上就会显示这段时间的内存占用情况

    我们有两种方式来判定当前是否有内存泄漏:

    1. 多次快照后,比较每次快照中内存的占用情况,如果呈上升趋势,那么可以认为存在内存泄漏
    2. 某次快照后,看当前内存占用的趋势图,如果走势不平稳,呈上升趋势,那么可以认为存在内存泄漏

    在服务器环境中使用 Node 提供的 process.memoryUsage 方法查看内存情况

    process.memoryUsage返回一个对象,包含了 Node 进程的内存占用信息。

    该对象包含四个字段,单位是字节,含义如下:

    • rss(resident set size):所有内存占用,包括指令区和堆栈。
    • heapTotal:"堆"占用的内存,包括用到的和没用到的。
    • heapUsed:用到的堆的部分。
    • external: V8 引擎内部的 C++ 对象占用的内存。
      判断内存泄漏,以heapUsed字段为准。

    常见的内存泄露案例

    意外的全局变量

    function foo() {
      bar1 = 'some text'; // 没有声明变量 实际上是全局变量 => window.bar1
      this.bar2 = 'some text' // 全局变量 => window.bar2
    }
    foo();
    

    被遗忘的定时器和回调函数

    var serverData = loadData();
    setInterval(function() {
      var renderer = document.getElementById('renderer');
      if(renderer) {
        renderer.innerHTML = JSON.stringify(serverData);
      }
    }, 5000); // 每 5 秒调用一次
    

    如果后续 renderer 元素被移除,整个定时器实际上没有任何作用。

    但如果你没有回收定时器,整个定时器依然有效, 不但定时器无法被内存回收,

    定时器函数中的依赖也无法回收。在这个案例中的 serverData 也无法被回收。

    闭包

    在 JS 开发中,我们会经常用到闭包,一个内部函数,有权访问包含其的外部函数中的变量。

    闭包很容易发生无意识的内存泄露。如下所示:

    function addHandler() {
    var el = document.getElementById('el');
        el.onclick = function() {
            el.style.backgroundColor = 'red';
        }
    }
    

    这段代码创建了一个元素,当它被点击的时候变红,但同时它也会发生内存泄露。为什么?因为对el 的引用不小心被放在一个匿名内部函数中。这就在 JavaScript 对象(这个内部函数)和本地对象之间(el)创建了一个循环引用。

    最简单的解决方案:不要使用 el 变量:

    function addHandler(){
        document.getElementById('el').onclick = function(){
            this.style.backgroundColor = 'red';
        };
     }
    

    DOM 引用

    var elements = {
      image: document.getElementById('image')
    };
    function doStuff() {
      elements.image.src = 'http://example.com/image_name.png';
    }
    function removeImage() {
      document.body.removeChild(document.getElementById('image'));
      // 这个时候我们对于 #image 仍然有一个引用, Image 元素, 仍然无法被内存回收.
    }
    

    即使我们对于 image 元素进行了移除,但是仍然有对 image 元素的引用,依然无法对齐进行内存回收。

    如何避免内存泄漏

    • 减少不必要的全局变量,使用严格模式避免意外创建全局变量。
    • 在你使用完数据后,及时解除引用(闭包中的变量,dom引用,定时器清除)。
    • 组织好你的逻辑,避免死循环等造成浏览器卡顿,崩溃的问题。

    相关文章

      网友评论

          本文标题:JS回顾变量、作用域、内存问题

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