笔记:原始值与引用值执行上下文与作用域垃圾回收
Posted karshey
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了笔记:原始值与引用值执行上下文与作用域垃圾回收相关的知识,希望对你有一定的参考价值。
文章目录
第四章是重点。
1.原始值与引用值
变量有两种类型:原始值、引用值。
原始值:Undefined、Null、Boolean、Number、String 和 Symbol。
引用值:保存在内存中的对象。(访问内存中的对象其实是访问引用值,因为javascript 不允许直接访问内存位置)
1.1 动态属性
对于引用值而言,可以随时添加、修改和删除其属性和方法。
只有引用值可以动态添加后面可以使用的属性,原始值动态添加的属性会是undefined。
原始类型的初始化可以只使用原始字面量形式,如:
1.2 复制值
原始值:
let num1=1
let num2=num1
num1++
此时num1是2,num2是1。两个变量可以独立使用,互不干扰。
引用值:
let obj1 = new Object();
let obj2 = obj1;
obj1.name = "Nicholas";
console.log(obj2.name); // "Nicholas"
这里复制的是指针。obj1和obj2指向同一个对象。
1.3 传递参数
ECMAScript 中所有函数的参数都是按值传递的。
在按值传递参数时,值会被复制到一个局部变量。在按引用传递参数时,值在内存中的位置会被保存在一个局部变量(传的是指针),这意味着对本地变量的修改会反映到函数外部。
如:函数中的num是局部变量,所以不会影响到count。
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";
obj = new Object();
obj.name = "Greg";
let person = new Object();
setName(person);
console.log(person.name); // "Nicholas"
- person先指向一个对象(我们称他为A)
- obj是一个局部变量,它和person指向同一个对象
- obj动态添加属性,于是A上有了一个属性(Nicholas)
- obj指向新的对象(我们称它为B)
- obj动态添加属性,于是B上有了一个属性(Greg)
- 因此person.name是Nicholas
1.4 确定类型
typeof
操作符:判断一个变量是否为字符串、数值、布尔值或 undefined。如果是对象或null,返回Object。
instanceof
:
- 如果变量是给定引用类型的实例,则 instanceof 操作符返回 true。
- 所有引用值都是 Object 的实例。
- 用 instanceof 检测原始值,则返回 false,因为原始值不是对象。
result = variable instanceof constructor
2.执行上下文与作用域
执行上下文包括:全局上下文、函数上下文。
变量对象(variable object):执行上下文中定义的所有变量和函数都存在于这个对象上。
在浏览器中,全局上下文是window 对象:
- 通过 var 定义的全局变量和函数会成为 window 对象的属性和方法
- 使用 let 和 const 的顶级声明不会定义在全局上下文中,但在作用域链解析上效果一样的
上下文在其所有代码都执行完毕后会被销毁,包括定义在它上面的所有变量和函数(全局上下文在应用程序退出前才会被销毁,比如关闭网页或退出浏览器)。
如果上下文是函数,则其活动对象activation object用作变量对象。
作用域链:决定了各级上下文中的代码在访问变量和函数时的顺序。
内部上下文可以通过作用域链访问外部上下文中的一切。
一个例子:
这里的上下文:
- 全局上下文
- changeColor()的局部上下文
- swapColors()的局部上下文
作用域链如图:
2.1 作用域链增强
某些语句会导致在作用域链前端临时添加一个上下文,这个上下文在代码执
行后会被删除。即:
- try/catch 语句的 catch 块:创建新的变量对象,它会包含要抛出的错误对象的声明。
- with语句:向作用域链前端添加指定的对象
2.2 变量声明
增加了 let 和 const 两个关键字。
2.2.1 使用 var 的函数作用域声明
使用 var 声明变量时,变量会被自动添加到最接近的上下文:
- 在函数和with语句中,最接近的上下文是函数上下文
- 如果变量未经声明就被初始化了,那么它就会自动被添加到全局上下文
function add(num1, num2)
sum = num1 + num2; //没有声明,自动添加到全局上下文
return sum;
let result = add(10, 20); // 30
console.log(sum); // 30
var 声明会被拿到函数或全局作用域的顶部,位于作用域中所有代码之前,即提升:
console.log(name); // undefined
var name = 'Jake';
function()
console.log(name); // undefined
var name = 'Jake';
相当于:
var name;
console.log(name); // undefined
name = 'Jake';
function()
var name;
console.log(name); // undefined
name = 'Jake';
2.2.2 使用 let 的块级作用域声明
let:
- 作用域是块级,由最近的一对包含花括号界定。
- 在同一作用域内不能声明两次(var可以,var会忽略多余的声明)
- 适合在循环中声明迭代变量(使用 var 声明的迭代变量会泄漏到循环外部,因为使用 var 声明变量时,变量会被自动添加到最接近的上下文)
2.2.3 使用 const 的常量声明
- 使用 const 声明的变量必须同时初始化为某个值
- 一经声明,在其生命周期的任何时候都不能再重新赋予新值
- 其他方面与let相同
- const 声明只应用到顶级原语或者对象:赋值为对象的 const 变量不能再被重新赋值为其他引用值,但对象的键则不受限制(可以随意添加属性方法),如:
const o1 = ;
o1 = ; // TypeError: 给常量赋值
const o2 = ;
o2.name = 'Jake';
console.log(o2.name); // 'Jake'
3.垃圾回收
垃圾回收:执行环境负责在代码执行时管理内存。
基本思路:确定哪个变量不会再使用,然后释放它占用的内存。周期性的。
两种标记策略:标记清理和引用计数。
3.1 标记清理(常用)
- 垃圾回收程序运行的时候,会标记内存中存储的所有变量
- 将所有在上下文中的变量,以及被在上下文中的变量引用的变量的标记去掉
- 有标记的变量是待删除的
3.2 引用计数
思路:每个值都记录它被引用的次数。
- 声明变量并给它赋一个引用值时,这个值的引用数为 1
- 同一个值又被赋给另一个变量,引用数加 1
- 保存对该值引用的变量被其他值覆盖了,引用数减 1
- 引用数为 0 时,可以安全地收回其内存
遇到的问题:循环引用
- 是对象 A 有一个指针指向对象 B,而对象 B 也引用了对象 A,如:
objectA、objectB引用值都是2.
function problem()
let objectA = new Object();
let objectB = new Object();
objectA.someOtherObject = objectB;
objectB.anotherObject = objectA;
解决方案:把变量设置为 null
会切断变量与其之前引用值之间的关系。当下次垃圾回收程序运行时,这些值就会被删除,内存也会被回收。
3.3 性能
如果内存中分配了很多变量,则可能造成性能损失,因此垃圾回收的时间调度很重要。
IE7 发布后,JavaScript 引擎的垃圾回收程序被调优为动态改变分配变量、字面量或数组槽位等会触 发垃圾回收的阈值。IE7的起始阈值都与 IE6 的相同。如果垃圾回收程序回收的内存不到已分配的 15%,这些变量、字面量或数组槽位的阈值就会翻倍。如果有一次回收的内存达到已分配的 85%,则阈值重置为默认值。这么一个简单的修改,极大地提升了重度依赖 JavaScript 的网页在浏览器中的性能。
3.4 内存管理
分配给浏览器的内存通常比分配给桌面软件的要少,这是出于安全考虑:避免运行大量 JavaScript 的网页耗尽系统内存而导致操作系统崩溃。
- 将内存占用量保持在一个较小的值可以让页面性能更好。
- 优化内存占用的最佳手段:保证在执行代码时只保存必要的数据。
- 若数据不再必要,把它设置为
null
,从而释放其引用。 - 这也可以叫作解除引用。
- 这个建议最适合全局变量和全局对象的属性。局部变量在超出作用域后会被自动解除引用。
- 解除引用的关键:确保相关的值已经不在上下文里
3.4.1 通过 const 和 let 声明提升性能
const和let都以块为作用域,可能会更早地让垃圾回收程序介入,尽早回收应该回收的内存。
3.4.2 隐藏类和删除操作
如代码:V8 会在后台配置,让这两个类实例共享相同的隐藏类,因为这两个实例共享同一个构造函数和原型。
function Article()
this.title = 'Inauguration Ceremony Features Kazoo Band';
let a1 = new Article();
let a2 = new Article();
如果又添加了一句:它们就不共享同一个隐藏类。
a2.author = 'Jake';
如果:这样避免了“先创建后补充”的动态属性赋值,两个实例又共享同一个隐藏类(不考虑 hasOwnProperty 的返回值)
function Article(opt_author)
this.title = 'Inauguration Ceremony Features Kazoo Band';
this.author = opt_author;
let a1 = new Article();
let a2 = new Article('Jake');
如果又添加了一句:它们又不共享同一个隐藏类。
动态删除属性与动态添加属性导致的后果一样。
delete a1.author;
最好的方法:把不想要的属性设置为null,这样又可以保持隐藏类,又可以达到删除引用值供垃圾回收程序回收的效果。
3.5 内存泄漏
JavaScript 中的内存泄漏大部分是由不合理的引用导致的。
- 意外声明全局变量
- 定时器
- 闭包
- 静态分配与对象池
意外声明全局变量
解释器会把变量name当作window的属性来创建。
function setName()
name = 'Jake';
解决方法:在变量声明前头加上 var、let 或 const 关键字。
定时器
只要定时器还在运行,回调函数中的name就不会被回收。
let name = 'Jake';
setInterval(() =>
console.log(name);
, 100);
闭包
调用 outer()会导致分配给 name 的内存被泄漏。
以上代码执行后创建了一个内部闭包,只要返回的函数存在就不能清理 name,因为闭包一直在引用着它。
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;
在哪里创建矢量可以不让垃圾回收调度程序盯上呢?对象池
在初始化的某一时刻,可以创建一个对象池,用来管理一组可回收的对象。应用程序可以向这个对象池请求一个对象、设置其属性、使用它,然后在操作完成后再把它还给对象池。由于没发生对象初始化,垃圾回收探测就不会发现有对象更替,因此垃圾回收程序就不会那么频繁地运行。
注意:静态分配是优化的一种极端形式。大多数情况下不考虑。
小结
以上是关于笔记:原始值与引用值执行上下文与作用域垃圾回收的主要内容,如果未能解决你的问题,请参考以下文章
《javascript高级程序设计》学习笔记 | 4.1.原始值与引用值
《JavaScript 高级程序设计》第四章:变量作用域和内存问题