重学JavaScript的第1天

137 阅读6分钟

Ch4 变量、作用域、内存

4.1 原始值以及引用值

  1. 通过值访问(call by value)访问原始属性(primitive value)
  2. 通过引用访问(call by reference)访问对象

4.1.1 动态属性

对于引用类型而言,可以随时添加、修改和删除其属性和方法,并且可以随时访问这个新的属性或者方法直到这个引用类型被销毁或者属性显式地被删除。

而对于值类型而言,即原始值,不能有属性,尽管给原始值添加属性并不会报错。但是有一个列外就是通过new关键字来初始化一个原始值,是可以动态添加属性并访问。

// 对于引用类型
let person = new Object(); 
person.name = "Nicholas"; 
console.log(person.name); // "Nicholas"
​
// 对于String的值类型
let name = "Nicholas"; 
name.age = 27; 
console.log(name.age); // undefined
​
// new来初始化一个原始数据类型实际上创建的是一个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 复制值

在通过变量把一个原始值赋值到另一个变量时,原始值会被复制到新变量的位置。但是这两个变量是相互独立的,互不干扰。

let num1 = 927;
let num2 = num1;
num2 = 555;         // 此时num2变成555 而num1还是927

但是对于引用数据类型而言,把一个引用数据变量赋值给另一个变量时,其实赋值的是一个指针,这个指针指向的是引用数据类型在堆中的地址,而赋值之后,这两个变量其实指向的时堆中的同一个地址,因此可以理解为这两个变量是相同的,修改一个变量会同时反应到另一个变量。

let obj1 = new Object();
let obj2 = obj1;
obj1.name = 'Garfield';
console.log(obj2.name);     //  Garfield

1.png

4.1.3 参数传递

首先明确两点:

  • ECMAScript中函数的参数是局部变量
  • ECMAScript中所有函数的参数都是按值传递!

说到这里,可能会有不解。不着急,先来看一个例子。

function setName(obj) { 
 obj.name = "Garfield"; 
} 
let person = new Object(); 
setName(person); 
console.log(person.name); // "Garfield" 

看到这里,你也许会大叫,这明明不是按值传递啊,函数中对obj的修改都反映到函数外了,怎么能叫全局呢?这不是误人子弟吗?不急,我们再来看一个例子:

function setName(obj) { 
 obj.name = "Garfield"; 
 obj = new Object(); 
 obj.name = "Greg"; 
 return obj;
} 
let person = new Object(); 
let person2 = setName(person); 
console.log(person.name); // "Garfield"
console.log(person2.name);// "Greg"
console.log(person === person2) // false

假设obj是按照引用类型传递的,那么应该会有以下几点:

  • person.name 和 person2.name 应该都是 "Greg"
  • person === person2 应该返回true

但是通过上面代码的结果,可以推翻obj是按照引用类型传递的假设。

4.1.4 确定类型

typeof 用于判断一个变量是哪一种原始数据类型,而对于引用数据类型,则它的用处并不大。

这里我们使用 instanceof 操作符来检测对象类型,它的返回值是一个布尔类型。用法如下:

console.log(person instanceof Object); // 变量 person 是 Object 吗?
console.log(colors instanceof Array); // 变量 colors 是 Array 吗?
console.log(pattern instanceof RegExp); // 变量 pattern 是 RegExp 吗?

4.2 执行上下文与作用域

执行上下文:每个执行上下文都有一个关联的变量对象,而这个上下文中定义的所有变量和函数都存在于这个对象上。对于全局上下文,在浏览器中是window对象,在nodejs中是global对象。上下文在其所有代码都执行完毕后会被销毁,包括定义在它上面的所有变量和函数(全局上下文在应用程序退出前才会被销毁,比如关闭网页或退出浏览器)。

作用域:上下文中的代码在执行的时候,会创建变量对象的一个作用域链(scope chain)。这个作用域链决定 了各级上下文中的代码在访问变量和函数时的顺序。

通过代码理解上下文和作用域链:

var color = "blue"; 
function changeColor() { 
 let anotherColor = "red"; 
 function swapColors() { 
 let tempColor = anotherColor; 
 anotherColor = color; 
 color = tempColor; 
 // 这里可以访问 color、anotherColor 和 tempColor 
 } 
 // 这里可以访问 color 和 anotherColor,但访问不到 tempColor 
 swapColors(); 
} 
// 这里只能访问 color 
changeColor();

以上代码涉及 3 个上下文:

  • 全局上下文:有一个变量 color 和一个函数 changeColor()
  • changeColor()的局部上下文: 有一个变量 anotherColor 和一个函数 swapColors()
  • swapColors()的局部 上下文:有一个变量 tempColor

访问一个变量的顺序就是按照作用域链一层一层往上找,直到全局作用域。位于上层的上下文不能访问位于下层上下文中的变量或者方法(不用闭包等技巧)。

下图展示了前面这个例子的作用域链。

2.png

4.3 垃圾回收

JavaScript 为开发者卸下了手动释放内存这个沉重的负担,通过自动内存管理实现内存分配和闲置资源回收。

基本思路:确定哪个变量不会再使用,然后释放它占用的内存。

垃圾回收的过程是周期性的,即垃圾回收程序每隔一定时间(或者说在代码执行过程中某个预定的收集时间)就会自动运行。

主要的两种标记策略:标记清理和引用技术。

4.3.1 标记清理

JS最常用的垃圾回收策略。当变量进入上下文时,会被加上存在于上下文中的标记,在离开上下文时,会被加上离开上下文的标记。

加标记的方法:维护两个变量列表;反转某一位等

垃圾回收程序运行的时候,会标记内存中存储的所有变量。然后,它会将所有在上下文中的变量,以及被在上下文中的变量引用的变量的标记去掉。在此之后再被加上标记的变量就是待删除的了,原因是任何在上下文中的变量都访问不到它们了。随后垃圾回收程序做一次内存清理,销毁带标记的所有值并收回它们的内存。

4.3.2 引用计数

不太常用的垃圾回收策略。思路是对每个值都记录它被引用的次数。声明变量并给它赋一个引用值时,这个值的引用数为 1。如果同一个值又被赋给另一个变 量,那么引用数加 1。类似地,如果保存对该值引用的变量被其他值给覆盖了,那么引用数减 1。当一 个值的引用数为 0 时,就说明没办法再访问到这个值了,因此可以安全地收回其内存了。垃圾回收程序 下次运行的时候就会释放引用数为 0 的值的内存。但是一大问题是循环引用的存在。