前端开发 JS 02 变量、作用域和内存问题

作者 柚爸

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

JS里基本类型值指的是undefined,null,Boolean,Number 和 String的值。引用类型的值指的是内存中的对象。

变量赋值

从一个变量向另一个变量赋值的时候,如果是基本类型,会新建一个同样的值,然后将这个值赋给新变量。新旧变量指向内存中不同地址的两个相同的值;如果是引用类型的变量,则会将变量对应的指针复制一份给新变量,新旧变量都指向同一个对象。

参数传递

参数就是当前函数的局部变量,在整个函数作用域内生效。JS里所有的参数都是按值传递的。也就是说遵循和变量赋值一样的规则,所以基本类型是复制了一份新值,而引用类型依然传递了指针。

所以在函数内部操作引用类型的变量,直接会导致原来的对象发生变化;操作基本类型的变量,则不会导致原来的变量产生变化。

之前用typeof检测基本类型的值。如果要检测引用类型的值,需要用到另外一个内置操作符instanceof,如果用其检测基本类型的值,结果总是false。

执行环境

每一个执行环境,都有一个与之关联的变量对象。环境中定义的所有变量和函数都保存在这个对象里。这个对象虽然代码无法处理,但解释器会处理到。

全局执行环境是最外边的一个执行环境,由于JS并没有单独的解释器,而是依赖于宿主环境。在Web开发中,一般认全局执行环境被认为是window对象。所以所有的全局变量和函数,就是window对象的属性和方法。

每个函数都有自己的执行环境。如果在全局环境内定义了一个函数然后立刻执行,这个函数执行的时候,函数的环境会被推入一个环境栈中,执行完毕后,栈将环境弹出,环境此时返回到之前的执行环境(也就是window对象)。

当代码在一个环境中执行的时候,会创建变量对象的一个作用域链,用途是保证代码可以有序的访问所有可以访问的变量和函数。这个作用域链的最前端(最优先被代码使用的地方)是代码所在环境的变量对象,如果代码是一个函数,那就是函数的AO对象(AO对象即活动对象activation object,其最开始的作用域链只有一个,就是arguments对象);作用域链的下一个变量对象来自包含上一个环境的环境,再下一个来自再外层的包含环境,最后一链就是全局环境。如果需要查找一个变量,就解析标识符,然后按照作用域链从最前边查找到最后边。

简单的说,就像是一层一层的同心球,最里边的是代码本身的局部作用域,然后一层一层逐渐扩大到全局变量。如果当前环境查找到了变量,就会停止查找。

看两段代码:

    var color = 'blue';

    function changecolor() {
        if (color === 'blue') {
            color = 'red';
        } else {
            color = 'blue';
        }
    }

    changecolor(color);
    console.log("color is now " + color)

这段代码的结果是外层变量color 被修改为red。虽然传了color进来,但内层标识符并没有color,所以函数通过作用域链找到了外层的color。

    var color = 'blue';

    function changecolor(color) {
        if (color === 'blue') {
            color = 'red';
        }
        else {
            color = 'blue';
        }
    }

    changecolor(color);
    console.log("color is now " + color)

这段代码的结果是,将外层变量color的值复制给了函数的color参数,函数的参数存在于AO对象上,函数内部在操作color的时候,始终操作的是内部的color变量,外部的color不受影响。

从上边两个例子还能够得出一个结果,就是函数在定义的时候指定的参数名称,其标识符就存在于函数环境里。函数如果没有定义某个参数的标识符,即使传入与函数体内某个标识符相同的参数,也不会让对应的标识符存在于函数环境中,函数还是要到外层去找标识符。

作用域链还可以被延长,当代码延伸出一个新的局部环境的时候,这个作用域链就会在最前端加出来一个变量对象,用于保存延伸出来的新环境。这种情况发生在try-catch语句和with语句中,不过这些语句都不推荐使用。

块级作用域

高程三专门讲了JS没有块级作用域,但是在目前ES6已经发布的情况下,新浏览器已经对ES6有了支持,在今后,赋值变量除了特别的全局变量之外,一律使用let来赋值。

用var 赋值会将变量自动添加到最接近的环境中。

垃圾回收机制

主要做到如下两点:

  1. 合理分配代码块和局部变量,减少使用全局变量,能用let赋值就用let赋值,确保函数执行后销毁相关内容
  2. 全局变量和对象再不使用的时候,手工设置其值为null