「前端开发」 -

JS-Variable

JavaScript —— 变量及作用域

Posted by eliochiu on October 31, 2022

基本类型与引用类型

ES5有五种基本类型,分别是Number、String、Boolean、Undefined、Null和一种引用类型Object。基本类型是简单的数据段,引用类型是那些由多个值构成的对象。

当给一个变量赋值时,解析器必须判断该值是基本类型还是引用类型,基本类型是可以按值操作的,而引用类型则是按引用操作的,我们将一个变量赋值成对象,实际上变量存储的是该对象的地址,而不是该对象的值。

基本类型存储在栈内存空间中,可直接对基本类型进行修改;引用类型的所有值均存储在堆内存中,同时在栈内存中存储对象在堆内存中的地址,使用时再根据栈内存的地址来访问堆内存中的值。因此,操作对象时,我们操作的并不是对象本身,而是对象的一个引用,引用类型的值是按引用访问的。

基本类型和引用类型在许多方面上都有着较大不同。

动态属性

引用类型拥有动态属性,而基本类型则没有。

1
2
3
4
5
6
7
var obj = new Object();
obj.name = "Bob"
console.log(obj) // { name: "Bob"}

var str = "obj";
str.name = "Bob";
console.log(str.name) // undefined

尽管上述代码不会出现错误,但是可以看出,基本类型是没有动态属性的。

值的复制

如果从一个变量向另一个变量复制基本类型的值,会在变量对象上创建一个新值,然后将新值赋值给为新变量分配的位置上:

新的变量是原变量的一个副本,对新变量的修改不会影响原值。

1
2
3
4
5
var num = 5;
var numCopy = num;
numCopy = 3;
console.log(num) // 5
console.log(numCopy) // 3

如果从一个变量向另一个变量复制引用类型的值,则情况会有所不同。

我们知道,引用类型的值存储在堆内存中,使用时需要从栈内存中获得地址,因此引用类型的变量中实际上存储的是堆内存中值的地址,复制时,会在变量对象上创建一个新的引用,然后将该引用存储在分配的位置上。两个变量使用同一个引用,就会导致对其中一个修改,会导致所有变量都发生变化,即引用传递。

1
2
3
4
5
6
var obj1 = new Object();
obj1.name = "Alice";
var obj2 = obj;
obj2.name = "Bob";
console.log(obj1.name) // "Bob"
console.log(obj2.name) // "Bob"

参数传递

ES中所有类型的参数都是按值传递的。

1
2
3
4
5
6
7
var obj = new Object();
obj.name = "Alice";
function changeName(obj) {
    obj.name = "Bob";
}
changeName(obj);
console.log(obj.name); // "Bob" 

很多人认为在函数内部修改了obj的值导致函数外obj值的改变,因此得出按引用传递的结论,这是错误的。

1
2
3
4
5
6
7
var obj = new Object();
obj.name = "Alice";
function changeName(obj) {
    obj = new Object();
    obj.name = "Bob";
}
console.log(obj.name) // "Alice"

如果是按引用传递的,那么obj应该指向新的名为Bob的对象,然而最终结果是Alice,说明函数的参数是按照值传递的。

执行环境与作用域

执行环境(也称环境),它定义了函数或变量有权使用的数据,每个执行环境都有一个变量对象,用于保存执行环境可访问的所有变量,我们无法使用它,但是解析器会使用。

每个函数都有自己的执行环境,当语句运行到函数时,会将函数的执行环境推入栈中,函数执行结束后,再弹出栈,交由此前的执行环境。

当代码在一个函数内执行时,会创建变量对象的一个作用域链,作用域链的前端,始终都是当前执行环境的变量,然后从内到外,一层一层的添加,形成一条作用域链。在执行函数时,先在当前环境中寻找变量,找到则继续执行,未找到就在外层环境中寻找,直至全局变量。

上述代码主要涉及三个环境:swapColors中可以访问三个变量、changeColor中可以访问两个变量、全局只能访问一个变量。

全局作用域

函数外定义的变量均属于全局环境,在函数内可以访问全局作用域中的所有变量。

函数作用域

函数内定义的变量属于函数作用域,函数作用域的变量只能在函数内使用,函数流程结束会自动被销毁。

函数内不使用var声明的变量将会获得全局作用域,在函数外也可以访问。

块级作用域

ES5没有块级作用域,花括号封闭起来的区域不属于块级作用域:

1
2
3
4
5
for (var i = 0; i < 10; i++) {
    // ....
}

alert(i); // 10

垃圾收集

JavaScript具有自动垃圾收集的功能,即会自动管理内存。局部变量只在函数执行时使用,函数结束后就会被销毁,垃圾收集机制主要包括:标记清除、引用计数。

标记清除

JavaScript中最常用的垃圾收集方式是标记清除(mark-and-sweep)。当变量进入环境(例如,在函数中声明一个变量)时,就将这个变量标记为“进入环境”。从逻辑上讲,永远不能释放进入环境的变量所占有的内存,因为一旦进入环境,就有可能被使用,而当变量离开环境时,则将其 标记为“离开环境”。

引用计数

引用计数的含义是跟踪记录每个值被引用的次数。当声明了一个变量并将一个引用类型值赋给该变量时,则这个值的引用次数就是1。如果同一个值又被赋给另一个变量,则该值的引用次数加1。相反,如果包含对这个值引用的变量又取得了另外一个值,则这个值的引用次数减1。当这个值的引用次数变成0时,则说明没有办法再访问这个值了,因而就可以将其占用的内存空间回收回来。这样,当垃圾收集器下次再运行时,它就会释放那些引用次数为零的值所占用的内存。