笔记:原始值与引用值执行上下文与作用域垃圾回收

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 中的内存泄漏大部分是由不合理的引用导致的。

  1. 意外声明全局变量
  2. 定时器
  3. 闭包
  4. 静态分配与对象池

意外声明全局变量

解释器会把变量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 高级程序设计》第四章:变量作用域和内存问题

Think in java 笔记之 finalize() 函数

JavaScript的垃圾回收机制

垃圾收集器什么时候回收垃圾?