JavaScript高级程序设计 | Ch4变量、作用域与内存 读书笔记

《JavaScript高级程序设计(第4版)》

4 变量、作用域与内存

4.1 原始值与引用值

ECMAScript 变量可以包含两种不同类型的数据:

  • 原始值:最简单是数据
    -> 保存原始值的变量是按值(by value)访问的,操作单就是存储在变量的实际值
  • 引用值:是保存在内存中的对象
    -> 保存引用值的变量是按引用(by reference)访问的,因为JavaScript不允许直接访问内存位置,当我们操作对象时,实际上操作的是对该对象的引用、
4.1.1 动态属性
  • 对于引用值而言,可以随时添加、修改和删除其属性和方法
  • 原始值不能有属性,尽管尝试给原始值添加属性不会报错
  • 原始类型的初始化可以只使用原始字面量形式。如果使用的是 new 关键字,则 JavaScript 会 创建一个 Object类型的实例,但其行为类似原始值。
4.1.2 复制值

原始值和引用值在变量复制中的不同:

  • 原始值
    在通过变量把一个原始值赋值到另一个变量时,原始值会被复制到新变量的位置,这两个变量可以独立使用,互不干扰。
    《JavaScript高级程序设计(第4版)》
  • 引用值
    在把引用值从一个变量赋给另一个变量时,存储在变量中的值也会被复制到新变量所在的位置。
    区别在于,这里复制的值实际上是一个指针,它指向存储在堆内存中的对象。操作完成后,两个变量实际上指向同一个对象,因此一个对象上面的变化会在另一个对象上反映出来。
let obj1 = new Object(); 
let obj2 = obj1; 
obj1.name = "Nicholas"; 
console.log(obj2.name); // "Nicholas" 

《JavaScript高级程序设计(第4版)》

4.1.3 传递参数

ECMAScript 中所有函数的参数都是按值传递的。【但 不代表被当作参数的函数外部的变量/对象完全不变】
例子1:
原始值传参和原始值变量的复制一样

function addTen(num) { 
  num += 10; 
  return num; 
} 
let count = 20; 

let result = addTen(count); 
console.log(count); // 20,没有变化
console.log(result); // 30 

例子2:
引用值传参和引用值变量的复制一样
-> 在函数内部,obj 和 person 都指向同一个对象。结果就是,即使对象是按值传进函数的,obj 也会通过引用访问对象。当函数内部给 obj 设置了 name 属性时,函数外部的对象也会反映这个变化,因为 obj 指向的对象保存在全局作用域的堆内存上。

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

例子3:
很多开发者错误地认为,当在局部作用域中修改对象而变化反映到全局时,就意味着参数是按引用传递的。为证明对象是按值传递的,下例:

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的最好方式。
    -> typeof 虽然对原始值很有用,但它对引用值的用处不大。我们通常不关心一个值是不是对象,而是想知道它是什么类型的对象。
  • instanceof
    -> 如果变量是给定引用类型(由其原型链决定)的实例,则 instanceof 操作
    符返回 true。
    -> 如果用 instanceof 检测原始值,则始终会返回 false,因为原始值不是对象。
console.log(person instanceof Object); // 变量 person 是 Object 吗?
console.log(colors instanceof Array); // 变量 colors 是 Array 吗?
console.log(pattern instanceof RegExp); // 变量 pattern 是 RegExp 吗?

4.2 执行上下文与作用域?

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

4.2.1 作用域链增强?

虽然执行上下文主要有全局上下文和函数上下文两种(eval()调用内部存在第三种上下文),但有其他方式来增强作用域链。
以下两种情况,会导致在作用域链前端临时添加一个上下文,这个上下文在代码执行后会被删除

  • try/catch 语句的 catch 块
    -> 会创建一个新的变量对象,这个变量对象会包含要抛出的错误对象的声明。
  • with 语句
    -> 会向作用域链前端添加指定的对象。
function buildUrl() { 
  let qs = "?debug=true"; 
  with(location){ 
  let url = href + qs; 
 } 
  return url; 
} 
  • with 语句将 location 对象作为上下文,因此 location 会被添加到作用域链前端。
  • buildUrl()函数中定义了一个变量 qs。当 with 语句中的代码引用变量 href 时,实际上引用的是
    location.href,也就是自己变量对象的属性。
  • 在引用 qs 时,引用的则是定义在 buildUrl()中的那个变量,它定义在函数上下文的变量对象上。而在 with 语句中使用 var 声明的变量 url 会成为函数上下文的一部分,可以作为函数的值被返回。
  • 但由于是使用 let 声明的变量 url,因为被限制在块级作用域,所以在 with 块之外没有定义。
4.2.2 变量声明

使用var的函数作用域声明
如果变量未经声明就被初始化了,那么它就会自动被添加到全局上下文

function add(num1, num2) { 
  var sum = num1 + num2; 
  return sum; 
} 
let result = add(10, 20); // 30 
console.log(sum); // 报错:sum 在这里不是有效变量
function add(num1, num2) { 
  sum = num1 + num2; 
  return sum; 
} 
let result = add(10, 20); // 30 
console.log(sum); // 30 

通过在声明之前打印变量,可以验证变量会被提升。声明的提升意味着会输出 undefined 而不是Reference Error:

console.log(name); // undefined 
var name = 'Jake'; 
function() { 
  console.log(name); // undefined 
  var name = 'Jake'; 
} 

使用let的块级作用域声明
块级作用域由最近的一对包含花括号{}界定。

if (true) { 
  let a; 
} 
console.log(a); // ReferenceError: a 没有定义

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 没有定义

补充:

  • 当尝试引用一个未定义的变量/函数时,就会抛出一个ReferenceError。
  • 当一个变量声明后,没有被复制,那么它就是undefined类型。

使用const的常量声明
使用 const 声明的变量必须同时初始化为某个值。一经声明,在其生命周期的任何时候都不能再重新赋予新值。也是块作用域。

const a; // SyntaxError: 常量声明时没有初始化
const b = 3; 
console.log(b); // 3 
b = 4; // TypeError: 给常量赋值

《JavaScript高级程序设计(第4版》

4.3 垃圾回收

在 C 和 C++等语言中,跟踪内存使用对开发者来说是个很大的负担,也是很多问题的来源。
JavaScript 为开发者卸下了这个负担,通过自动内存管理实现内存分配和闲置资源回收。基本思路很简单:确定哪个变量不会再使用,然后释放它占用的内存。这个过程是周期性的,即垃圾回收程序每隔一定时间(或者说在代码执行过程中某个预定的收集时间)就会自动运行。

4.3.1 标记清理

JavaScript 最常用的垃圾回收策略是标记清理(mark-and-sweep)。

  • 当变量进入上下文,比如在函数内部声明一个变量时,这个变量会被加上存在于上下文中的标记。而在上下文中的变量,逻辑上讲,永远不应该释放它们的内存,因为只要上下文中的代码在运行,就有可能用到它们。当变量离开上下文时,也会被加上离开上下文的标记。
  • 垃圾回收程序运行的时候,会标记内存中存储的所有变量(记住,标记方法有很多种)。然后,它会将所有在上下文中的变量,以及被在上下文中的变量引用的变量的标记去掉。在此之后再被加上标记的变量就是待删除的了,原因是任何在上下文中的变量都访问不到它们了。随后垃圾回收程序做一次内存清理,销毁带标记的所有值并收回它们的内存。
4.3.2 引用计数

引用计数(reference counting)不太常用。

  • 思路是对每个值都记录它被引用的次数。声明变量并给它赋一个引用值时,这个值的引用数为 1。如果同一个值又被赋给另一个变量,那么引用数加 1。类似地,如果保存对该值引用的变量被其他值给覆盖了,那么引用数减 1。当一个值的引用数为 0 时,就说明没办法再访问到这个值了,因此可以安全地收回其内存了。垃圾回收程序下次运行的时候就会释放引用数为 0 的值的内存。
  • 带来的问题:循环引用,是对象 A 有一个指针指向对象 B,而对象 B 也引用了对象 A。
function problem() { 
  let objectA = new Object(); 
  let objectB = new Object(); 
  objectA.someOtherObject = objectB; 
  objectB.anotherObject = objectA; 
} 

为避免类似的循环引用问题,应该在确保不使用的情况下切断原生 JavaScript 对象与 DOM 元素之间的连接。比如,通过以下代码可以清除前面的例子中建立的循环引用:

let element = document.getElementById("some_element"); 
let myObject = new Object(); 
myObject.element = element; 
element.someObject = myObject; 
myObject.element = null; 
element.someObject = null; 

把变量设置为 null 实际上会切断变量与其之前引用值之间的关系。当下次垃圾回收程序运行时,
这些值就会被删除,内存也会被回收。

4.3.3 性能
  • 垃圾回收程序会周期性运行,如果内存中分配了很多变量,则可能造成性能损失,因此垃圾回收的时间调度很重要。尤其是在内存有限的移动设备上,垃圾回收有可能会明显拖慢渲染的速度和帧速率。
  • IE7 发布后,JavaScript 引擎的垃圾回收程序被调优为动态改变分配变量、字面量或数组槽位等会触发垃圾回收的阈值。IE7 的起始阈值都与 IE6 的相同。如果垃圾回收程序回收的内存不到已分配的 15%,这些变量、字面量或数组槽位的阈值就会翻倍。如果有一次回收的内存达到已分配的 85%,则阈值重置为默认值。这么一个简单的修改,极大地提升了重度依赖 JavaScript 的网页在浏览器中的性能。
4.3.4 内存管理
  • JavaScript 运行在一个内存管理与垃圾回收都很特殊的环境。分配给浏览器的内存通常比分配给桌面软件的要少很多,分配给移动浏览器的就更少了。这更多出于安全考虑而不是别的,就是为了避免运行大量 JavaScript 的网页耗尽系统内存而导致操作系统崩溃。这个内存限制不仅影响变量分配,也影响调用栈以及能够同时在一个线程中执行的语句数量。
  • 。优化内存占用的最佳手段就是保证在执行代码时只保存必要的数据。如果数据不再必要,那么把它设置为 null,从而释放其引用。这也可以叫作解除引用。

这个建议最适合全局变量和全局对象的属性。局部变量在超出作用域后会被自动解除引用,
如下面的例子所示:

function createPerson(name){ 
  let localPerson = new Object(); 
  localPerson.name = name; 
  return localPerson; 
} 
let globalPerson = createPerson("Nicholas"); 
// 解除 globalPerson 对值的引用
globalPerson = null; 

在上面的代码中,变量 globalPerson 保存着 createPerson()函数调用返回的值。在createPerson()内部,localPerson 创建了一个对象并给它添加了一个 name 属性。然后,localPerson 作为函数值被返回,并被赋值给 globalPerson。localPerson 在createPerson()执行完成超出上下文后会自动被解除引用,不需要显式处理。但 globalPerson 是一个全局量,应该在不再需要时手动解除其引用,最后一行就是这么做的。
注意:解除对一个值的引用并不会自动导致相关内存被回收。解除引用的关键在于确保相关
的值已经不在上下文里了,因此它在下次垃圾回收时会被回收。
1.通过const和let声明提升性能
使用这两个新关键字可能会更早地让垃圾回收程序介入,尽早回收应该回收的内存。
2.隐藏类和删除操作
《JavaScript高级程序设计(第4版)》《JavaScript高级程序设计(第4版)》《JavaScript高级程序设计(第4版)》3.内存泄露
(1)意外声明全局变量
此时,解释器会把变量 name 当作 window 的属性来创建(相当于 window.name = ‘Jake’)。
可想而知,在 window 对象上创建的属性,只要 window 本身不被清理就不会消失。
解决办法:
在变量声明前面加上var、let或const关键字。

function setName() { 
 name = 'Jake'; 
} 

(2)定时器导致内存泄露
定时器的回调通过闭包引用了外部变量,只要定时器一直运行,回调函数中引用的 name 就会一直占用内存。垃圾回收程序当然知道这一点,因而就不会清理外部变量。

let name = 'Jake'; 
setInterval(() => { 
 console.log(name); 
}, 100); 

(3)使用JavaScript 闭包导致内存泄露
调用 outer()会导致分配给 name 的内存被泄漏。以上代码执行后创建了一个内部闭包,只要返回
的函数存在就不能清理 name,因为闭包一直在引用着它。假如 name 的内容很大(不止是一个小字符串),会造成较大内存泄露。

let outer = function() { 
  let name = 'Jake'; 
  return function() { 
    return name; 
 }; 
}; 

4.静态分配与对象池
为了提升 JavaScript 性能,最后要考虑的一点往往就是压榨浏览器了。此时,一个关键问题就是如何减少浏览器执行垃圾回收的次数。开发者无法直接控制什么时候开始收集垃圾,但可以间接控制触发垃圾回收的条件。
《JavaScript高级程序设计(第4版)》
《JavaScript高级程序设计(第4版)》

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值