Chapter4 变量、作用域与内存
本章内容:
- 通过变量使用原始值与引用值
- 理解执行上下文
- 理解垃圾回收
JavaScript的变量是松散类型,变量是特定时间点一个特定值的名称而已。
4.1 原始值与引用值
原始值:最简单的数据。
6种原始值:Undefined、Null、Boolean、Number、String和Symbol。
引用值:由多个值构成的对象。
引用值是保存在内存中的对象,操作对象实际上操作的是对该对象的引用而非对象本身。
4.1.1 动态属性
原始值不能有属性,尽管尝试给原始值添加属性不会报错。
let name = "Nicholas";
name.age = 27;
console.log(name.age); // undefined
使用new关键字,Js会创建一个Object类型的实例,但其行为类似原始值。
let name1 = "Nicholas";
let name2 = new String("Matt");
name1.age = 27;
name2.age = 26;
console.log(name1.age); // undefined
console.log(name2.age); // 26
console.log(typeof name1); // string
console.log(typeof name2); // object
4.1.2 复制值
原始值的复制是将一个原始值复制到新变量的位置(地址),是一个全新的变量,互不干扰。
引用值的复制是将一个引用值的地址复制到新变量的地址上,实际复制的值是一个指针,两个变量指向的是同一个对象,因此一个对象上面的变化会再另一个对象上反映出来。
4.1.3 传递参数
ECMAScript中所有函数的参数都是按值传递的。这意味着函数外的值会被复制到函数内部的参数中,会遵循原始值或引用值的复制规则进行。
函数中的原始值传递,不会影响到外部变量;
function addTen(num) { num += 10; return num; } let count = 20; let result = addTen(count); console.log(count); // 20,没有变化 console.log(result); // 30
函数中的引用值传递,也是按值传递的,会影响到外部变量
function setName(obj) { obj.name = "Nicholas"; } let person = new Object(); setName(person); console.log(person.name); // "Nicholas"
ES中函数的参数是局部变量(本地对象),在函数执行结束时就被销毁。
function setName(obj) { obj.name = "Nicholas"; obj = new Object(); obj.name = "Greg"; } let person = new Object(); setName(person); console.log(person.name); // "Nicholas"
4.1.4 确定类型
typeof
可以用来判断原始值:字符串、数值、布尔值或undefined。
如果是null或对象,typeof返回object。
如果是函数,typeof返回function
instanceof
- 可以判断一个对象是什么类型的对象。
- 检测原始值,始终返回false;
- 检查引用值,始终返回true;
4.2 执行上下文与作用域
每个上下文都有一个关联的变量对象,这个上下文中定义的所有变量和函数都会存在这个对象上,但无法通过代码访问变量对象。
上下文在其所有的代码都执行完毕后会被销毁。
全局上下文:
- 最外层的上下文,不同宿主环境中,全局上下文的对象可能不一样。
- 在浏览器中,window对象就是全局上下文,因此所有通过var定义的全局变量和函数都会成为window对象的属性和方法。
- let和const的顶级声明不会定义在全局上下文中,但在作用域链解析上效果是一样的。
- 全局上下文在应用退出前才会被销毁,比如关闭网页或浏览器
每个上下文都可以到上一级上下文中去搜索变量和函数,但任何上下文都不能到下一级上下文中去搜索。
4.2.1 作用域增强
虽然执行上下文主要有全局上下文和函数上下文两种(eval()调用内部存在第三种上下文,但有其他方式来增强作用域链。
function buildUrl() {
let qs = "?debug=true";
with(location){
let url = href + qs;
}
return url;
}
这里的location对象作为上下文,会添加到作用域链前端。引用的href其实是location.href。with语句中使用let声明的变量url,会被限制在块级作用域。
4.2.2 变量声明
使用var的函数作用域声明
使用var声明变量时,变量会被自动添加到最接近的上下文。
var声明会被拿到函数或全局作用域的顶部,位于作用域中所有代码之前,这个现象叫作“提升“。==需要注意的是,提升的是声明而不是赋值。==
console.log(name); // undefined var name = 'Jake'; function() { console.log(name); // undefined var name = 'Jake'; }
使用let的块级作用域声明
let关键字和var很相似,但是它的作用域是块级的{}。
let和var另一个不同之处在于,在同一个作用域内不能声明两次,重复的var声明会被忽略,而重复的let声明会抛出SyntaxError。
var a; var a; // 不会报错 { let b; let b; } // SyntaxError: 标识符 b 已经声明过了
let的行为非常适合在循环中声明迭代变量,使用var声明会泄漏到循环外部。
严格来讲,let 在 JavaScript 运行时中也会被提升,但由于“暂时性死区”(temporal dead zone)的 缘故,实际上不能在声明之前使用 let 变量。因此,从写 JavaScript 代码的角度说,let 的提升跟 var 是不一样的。创建过程提升,但是初始化没有提升,所以不能被使用,因为var提升之后也初始化了undefined,但是let没有赋值,暂时还不能被访问。
使用const的变量声明
使用const声明的变量必须同时初始化为某个值。且一经声明,在其生命周期的任何时候都不能再重新赋予新值。其他方面跟let一样。
const声明只应用到顶级原语或对象,赋值为对象的congst变量不能被重新赋值为其他引用值,但是对象的键值不受限制。
如果想要让整个对象不能修改,可以使用
Object.freeze()
。const o3 = Object.freeze({}); o3.name = 'Jake';// 不会报错,但静默失败 console.log(o3.name); // undefined
标识符查找
讲述变量的查找过程。
4.3 垃圾回收
- 周期性,每隔一定时间或在代码执行过程中的某个预定的收集时间。
- 浏览器发展史上,主要有两种标记策略:标记清理和引用计数
4.3.1 标记清理
当变量进入上下文时,变量会被加上一个【存在于上下文中的标记】,而在上下文中的变量,逻辑上讲,永远不应该释放它们的内存,因为只要上下文中的代码在运行,就可能用到它们。当变量离开上下文时,也会被加上【离开上下文的标记】。
给变量加标记的方法有多种:
- 当变量进入上下文时,反转某一位;
- 可以维护“在上下 文中”和“不在上下文中”两个变量列表,可以把变量从一个列表转移到另一个列表。
关键在于策略,各家的浏览器都采用标记清理(或变体),只是在运行垃圾回收的频率上有所差异。
4.3.2 引用计数
其思路是对每个值都记录它被 引用的次数。声明变量并给它赋一个引用值时,这个值的引用数为 1。如果同一个值又被赋给另一个变 量,那么引用数加 1。类似地,如果保存对该值引用的变量被其他值给覆盖了,那么引用数减 1。当一 个值的引用数为 0 时,就说明没办法再访问到这个值了,因此可以安全地收回其内存了。垃圾回收程序 下次运行的时候就会释放引用数为 0 的值的内存。
存在循环引用的问题,永远都无法清除内存。
function problem() { let objectA = new Object(); let objectB = new Object(); objectA.someOtherObject = objectB; objectB.anotherObject = objectA; }
4.3.3 性能
- 如果数据不再必要,那么把它设置为null,从而释放其引用,这也叫作解除引用,最适合全局变量和全局变量属性。
- 解除一个值的引用并不会自动导致相关内存被回收,解除引用的关键在于确保相关的值已经不在上下文里,因此下次垃圾回收时会被回收。
提升性能的方法
通过const和let声明提升性能
const和let都是以块而非函数为作用域,相比var会更早的让垃圾回收程序介入,今早回收应该回收的内存。
隐藏类和删除操作
v8引擎中有使用到隐藏类的概念。
避免 JavaScript 的“先创建再补充”(ready-fire-aim)式的动态属性赋值,并在 构造函数中一次性声明所有属性。
function Article(opt_author) { this.title = 'Inauguration Ceremony Features Kazoo Band'; this.author = opt_author; } //错误示范 let a1 = new Article(); let a2 = new Article(); a2.author = 'Jake'; //这样两个实例会共用一个隐藏类,带来潜在性能提升。 let a1 = new Article(); let a2 = new Article('Jake'); //使用结束后 let a1 = new Article(); let a2 = new Article(); a1.author = null;
内存泄漏
未使用关键字的变量声明:
相当于在window对象上创建属性,window本身不被清理就不会消失。
function setName() { name = 'Jake'; }
定时器的回调函数中引用的变量会一直占用内存。
let outer = function() { let name = 'Jake'; return function() { return name; }; };
静态分配与对象池
合理使用分配的内存,同时避免多余的垃圾回收,可以保住因释放内存而损失的性能。
// 会频繁的进行垃圾回收 function addVector(a, b) { let resultant = new Vector(); resultant.x = a.x + b.x; resultant.y = a.y + b.y; return resultant; } // 解决方案,不动态创建对象,使用一个已有的对象 function addVector(a, b, resultant) { resultant.x = a.x + b.x; resultant.y = a.y + b.y; return resultant; }
- 使用对象池,先创建并管理一组可回收的对象。程序可以向对象池请求一个对象,并在操作后还给对象池。因为没有发生对象初始化,垃圾回收探测不到对象的更替。
- 本质上是一个贪婪算法,前期定义一个够大的数组,避免先删除后创建的操作。(静态分配,大多数情况下,都属于过早优化,不用考虑)
4.4 小结
- 原始值大小固定,因此保存在栈内存上。
- 从一个变量到另一个变量复制原始值会创建该值的第二个副本。
- 引用值是对象,存储在堆内存上。
- 包含引用值的变量实际上只包含指向相应对象的一个指针,而不是对象本身。
- 从一个变量到另一个变量复制引用值只会复制指针,因此结果是两个变量都指向同一个对象。
- typeof 操作符可以确定值的原始类型,而 instanceof 操作符用于确保值的引用类型。
任何变量(不管包含的是原始值还是引用值)都存在于某个执行上下文中(也称为作用域)。这个 上下文(作用域)决定了变量的生命周期,以及它们可以访问代码的哪些部分。执行上下文可以总结 如下。
- 执行上下文分全局上下文、函数上下文和块级上下文。
- 代码执行流每进入一个新上下文,都会创建一个作用域链,用于搜索变量和函数。
- 函数或块的局部上下文不仅可以访问自己作用域内的变量,而且也可以访问任何包含上下文乃 至全局上下文中的变量。
- 全局上下文只能访问全局上下文中的变量和函数,不能直接访问局部上下文中的任何数据。
- 变量的执行上下文用于确定什么时候释放内存。
JavaScript 是使用垃圾回收的编程语言,开发者不需要操心内存分配和回收。JavaScript 的垃圾回收 程序可以总结如下。
- 离开作用域的值会被自动标记为可回收,然后在垃圾回收期间被删除。
- 主流的垃圾回收算法是标记清理,即先给当前不使用的值加上标记,再回来回收它们的内存。
- 引用计数是另一种垃圾回收策略,需要记录值被引用了多少次。JavaScript 引擎不再使用这种算 法,但某些旧版本的 IE 仍然会受这种算法的影响,原因是 JavaScript 会访问非原生 JavaScript 对 象(如 DOM 元素)。
- 引用计数在代码中存在循环引用时会出现问题。
- 解除变量的引用不仅可以消除循环引用,而且对垃圾回收也有帮助。为促进内存回收,全局对 象、全局对象的属性和循环引用都应该在不需要时解除引用。