变量作用域与内存
1 原始值与引用值
变量包含两种不同类型的数据:原始值和引用值。
原始值:是最简单的数据( 6 种原始值:Undefined、Null、Boolean、Number、String 和 Symbol)。保存原始值的变量是按值访问的,因为我们操作的就是存储在变量中的实际值。
引用值:是由多个值构成的保存在内存中的对象。JavaScript 不允许直接访问内存位置,所以在操作对象时,实际上操作的是对该对象的引用而非实际的对象本身。保存引用值的变量是按引用访问的。
1.1 动态属性
引用值:可以随时添加、修改和删除其属性和方法。比如:
let person = new Object();
person.name = "Nicholas";
console.log(person.name); // "Nicholas"
原始值:原始值不能有属性,但是给原始值添加属性不会报错。原始类型的初始化可以只使用原始字面量形式。比如:
let name = "Nicholas";
name.age = 27;
console.log(name.age); // undefined
1.2 复制值
原始值复制:在通过变量把一个原始值赋值到另一个变量时,原始值会被复制到新变量的位置:
let num1 = 5;
let num2 = num1;
num1 包含数值 5,当把 num2 初始化为 num1 时,num2 也会得到数值 5。这个值跟存储在num1 中的 5 是完全独立的,因为它是那个值的副本。这两个变量可以独立使用,互不干扰。
引用值复制:把引用值从一个变量赋给另一个变量时,存储在变量中的值也会被复制到新变量所在的位置。这里复制的值实际上是一个指针,它指向存储在堆内存中的对象。操作完成后,两个变量实际上指向同一个对象,因此一个对象上面的变化会在另一个对象上反映出来,如下面的例子所示:
let obj1 = new Object();
let obj2 = obj1;
obj1.name = "Nicholas";
console.log(obj2.name); // "Nicholas"
在这个例子中,变量 obj1 保存了一个新对象的实例。然后,这个值被复制到 obj2,此时两个变量都指向了同一个对象。在给 obj1 创建属性 name 并赋值后,通过 obj2 也可以访问这个属性,因为它们都指向同一个对象。
1.3 传递参数
ECMAScript 中所有函数的参数都是按值传递的。
1.4 确定类型
typeof 操作符:typeof 操作符最适合用来判断一个变量是否为原始类型。更确切地说,它是判断一个变量是否为字符串、数值、布尔值或 undefined 的最好方式,它对原始值很有用,但对引用值的用处不大。
instanceof 操作符:想要判断一个值是什么类型的对象,则使用instanceof 操作符:
console.log(person instanceof Object); // 变量 person 是 Object 吗?
console.log(colors instanceof Array); // 变量 colors 是 Array 吗?
console.log(pattern instanceof RegExp); // 变量 pattern 是 RegExp 吗?
2. 执行上下文与作用域
将上下文与作用域放在一起理解,可以对上下文的意思有更清晰的理解。
上下文的原意是content,而作用域的原意是scope。
scope指的是:函数被调用的时候, 各个变量的作用区域。
content指的是:函数被调用的时候, 查看 this指向哪个object, 那么那个object 就是当前的 “上下文”。
个人觉得可以理解为:当前执行环境的作用域。
看到的一个比较形象的例子:
如果我们想煮一碗泡面,应该怎么样?
肯定是提前准备好泡面,鸡蛋,青菜,等想吃的时候直接下锅煮就行.
而这里的执行上下文就相当于准备好煮泡面的材料,只为煮的时候更加方便。
在代码中看:
var a = "blue";
function changeColor() {
let b = "red";
function swapColors() {
let temp = b;
b = a;
a = temp;
// 这里可以访问 a、b 和 temp
}
// 这里可以访问 a 和 b,但访问不到 temp
swapColors();
}
// 这里只能访问 a
changeColor();
以上代码涉及 3 个上下文:全局上下文、changeColor()的局部上下文和 swapColors()的局部上下文。
全局上下文中有一个变量 a 和一个函数 changeColor()。
changeColor()的局部上下文中有一个变量 b 和一个函数 swapColors(),但在这里可以访问全局上下文中的变量 a。
swapColors()的局部上下文中有一个变量 tempr,只能在这个上下文中访问到。全局上下文和changeColor()的局部上下文都无法访问到 temp。而在 swapColors()中则可以访问另外两个上下文中的变量,因为它们都是父上下文(内部上下文可以通过作用域链访问外部上下文中的一切,但外部上下文无法访问内部上下文中的任何东西)。
上下文之间的连接是线性的、有序的。每个上下文都可以到上一级上下文中去搜索变量和函数,但任何上下文都不能到下一级上下文中去搜索。
(uu们通过代码体会一下作用域链)
swapColors()局部上下文的作用域链中有 3 个对象:swapColors()的变量对象、changeColor()的变量对象和全局变量对象。swapColors()的局部上下文首先从自己的变量对象开始搜索变量和函数,搜不到就去搜索上一级变量对象。changeColor()上下文的作用域链中只有 2 个对象:它自己的变量对象和全局变量对象。因此,它不能访问 swapColors()的上下文。
总结:
1.上下文表示的代码执行的环境,如函数上下文,提前确定好函数的变量对象以及this指向等。
2.在函数代码真正执行的时候,不用再去处理这些问题,加快运行的速度。
2.1 作用域链增强
执行上下文主要有全局上下文和函数上下文两种,但有其他方式来增强作用域链。某些语句会导致在作用域链前端临时添加一个上下文,这个上下文在代码执行后会被删除。通常在两种情况下会出现这个现象,即代码执行到下面任意一种情况时:
try/catch 语句的 catch 块
with 语句
这两种情况下,都会在作用域链前端添加一个变量对象。对 with 语句来说,会向作用域链前端添加指定的对象;对 catch 语句而言,则会创建一个新的变量对象,这个变量对象会包含要抛出的错误。
2.2 变量声明
2.2.1 使用 var 的函数作用域声明
在使用 var 声明变量时,变量会被自动添加到最接近的上下文。在函数中,最接近的上下文就是函数的局部上下文。如果变量未经声明就被初始化了,
那么它就会自动被添加到全局上下文(注意:未经声明而初始化变量是非常常见的错误,会导致很多问题。所以在初始化变量之前一定要先声明量)。
如下面的例子所示:
function add(num1, num2) {
var sum = num1 + num2;
return sum;
}
let result = add(10, 20); // 30
console.log(sum); // 报错:sum 在这里不是有效变量
函数 add()定义了一个局部变量 sum,保存加法操作的结果。这个值作为函数的值被返回,但变量 sum 在函数外部是访问不到的。如果省略上面例子中的关键字 var,那么 sum 在 add()被调用之后就变成可以访问的了,如下所示:
function add(num1, num2) {
sum = num1 + num2;
return sum;
}
let result = add(10, 20); // 30
console.log(sum); // 30
由于变量 sum没有使用 var 声明,在调用 add()之后,sum被添加到了全局上下文,在函数退出之后依然存在,从而在后面可以访问到。
变量提升:var 声明会被拿到函数或全局作用域的顶部,位于作用域中所有代码之前,这叫变量的“提升”。“提升”可以让同一作用域下的代码不考虑变量是否已经声明就可以直接使用。下面的两段代码是等价的:
var name = "Jake";
// 等价于:
name = 'Jake';
var name;
function fn1() {
var name = 'Jake';
}
// 等价于:
function fn2() {
var name;
name = 'Jake';
}
2.2.2 使用 let 的块级作用域声明(ES6新增)
let 关键字跟 var 很相似,但它的作用域是块级的,这也是 JavaScript 中的新概念。块级作用域由最近的一对包含花括号{}界定。换句话说,if 块、while 块、function 块,甚至连单独的块也是 let 声明变量的作用域。let 与 var 的另一个不同之处是在同一作用域内不能声明两次。重复的 var 声明会被忽略,而重复的 let 声明会抛出 SyntaxError。
let 的行为非常适合在循环中声明迭代变量。使用 var 声明的迭代变量会泄漏到循环外部。如下:
for (var i = 0; i < 10; ++i) {}
console.log(i); // 10
for (let j = 0; j < 10; ++j) {}
console.log(j); // ReferenceError: j 没有定义
严格来讲,let 在 JavaScript 运行时中也会被提升,但由于“暂时性死区”的缘故,实际上不能在声明之前使用 let 变量。因此,从写 JavaScript 代码的角度说,let 的提升跟 var是不一样的。
2.2.3 使用 const 的常量声明(ES6新增)
使用 const 声明的变量必须同时初始化为某个值。一经声明,在其生命周期的任何时候都不能再重新赋予新值。
const a; // SyntaxError: 常量声明时没有初始化
const b = 3;
console.log(b); // 3
b = 4; // TypeError: 给常量赋值
赋值为对象的 const 变量不能再被重新赋值为其他引用值,但对象的键则不受限制。
const o1 = {};
o1 = {}; // TypeError: 给常量赋值
const o2 = {};
o2.name = 'Jake';
console.log(o2.name); // 'Jake'
如果想让整个对象都不能修改,可以使用 Object.freeze(),这样再给属性赋值时虽然不会报错,但会静默失败:
const o3 = Object.freeze({});
o3.name = 'Jake';
console.log(o3.name); // undefined
2.2.4 标识符查找
当在特定上下文中为读取或写入而引用一个标识符时,必须通过搜索确定这个标识符表示什么。
搜索过程:搜索开始于作用域链前端,以给定的名称搜索对应的标识符。如果在局部上下文中找到该标识符,则搜索停止,变量确定;如果没有找到变量名,则继续沿作用域链搜索。这个过程一直持续到搜索至全局上下文的变量对象。如果仍然没有找到标识符,则说明其未声明。
为更好地说明标识符查找,我们来看一个例子:
var color = 'blue';
function getColor() {
return color;
}
console.log(getColor()); // 'blue'
对这个搜索过程而言,引用局部变量会让搜索自动停止,而不继续搜索下一级变量对象。也就是说,如果局部上下文中有一个同名的标识符,那就不能在该上下文中引用父上下文中的同名标识符,如下面的例子所示:
var color = 'blue';
function getColor() {
let color = 'red';
return color;
}
console.log(getColor()); // 'red'
3. 小结
JavaScript 变量可以保存两种类型的值:原始值和引用值。原始值可能是以下 6 种原始数据类型之一:Undefined、Null、Boolean、Number、String 和 Symbol。
原始值和引用值有以下特点:
原始值大小固定,因此保存在栈内存上。
从一个变量到另一个变量复制原始值会创建该值的第二个副本。
引用值是对象,存储在堆内存上。
包含引用值的变量实际上只包含指向相应对象的一个指针,而不是对象本身。
从一个变量到另一个变量复制引用值只会复制指针,因此结果是两个变量都指向同一个对象。
typeof 操作符可以确定值的原始类型,而 instanceof 操作符用于确保值的引用类型。
任何变量(不管包含的是原始值还是引用值)都存在于某个执行上下文中(也称为作用域)。这个上下文(作用域)决定了变量的生命周期,以及它们可以访问代码的哪些部分。
执行上下文可以总结如下:
执行上下文分全局上下文、函数上下文和块级上下文。
代码执行流每进入一个新上下文,都会创建一个作用域链,用于搜索变量和函数。
函数或块的局部上下文不仅可以访问自己作用域内的变量,而且也可以访问任何包含上下文乃至全局上下文中的变量。
全局上下文只能访问全局上下文中的变量和函数,不能直接访问局部上下文中的任何数据。
变量的执行上下文用于确定什么时候释放内存。
JavaScript 是使用垃圾回收的编程语言,开发者不需要操心内存分配和回收。
JavaScript 的垃圾回收程序可以总结如下:
离开作用域的值会被自动标记为可回收,然后在垃圾回收期间被删除。
主流的垃圾回收算法是标记清理,即先给当前不使用的值加上标记,再回来回收它们的内存。
引用计数是另一种垃圾回收策略,需要记录值被引用了多少次。注意:JavaScript 引擎不再使用这种算法,但某些旧版本的 IE 仍然会受这种算法的影响,原因是 JavaScript 会访问非原生 JavaScript 对象(如 DOM 元素)。
引用计数在代码中存在循环引用时会出现问题。
解除变量的引用不仅可以消除循环引用,而且对垃圾回收也有帮助。为促进内存回收,全局对象、全局对象的属性和循环引用都应该在不需要时解除引用。