《JavaScript高级程序设计》__ 作用域&内存

简介: 前言大家好,我是HoMeTown,web领域有一本神书大家应该都有看过,这本书我看过两遍,但是每次看都是粗粗的略过一些重要的知识点,甚至一些面试过程中的问题,在这本书里都能找到答案。

前言

大家好,我是HoMeTown,web领域有一本神书大家应该都有看过,这本书我看过两遍,但是每次看都是粗粗的略过一些重要的知识点,甚至一些面试过程中的问题,在这本书里都能找到答案。

工作这么多年,到现在为止对这本书都没有一个系统的知识点记录,这次想从头读一遍这一本JavaScript高级程序设计【第4版】,并把重要的知识点记录下来,同时加上自己的见解,这也是我第一次在掘金上记录分享读书笔记,共勉之!

关注专栏,一起学习吧~

原始值与引用值

原始值:最简单的数据,按值访问;比如:Undefined, Null, Boolean, Number, String, Symbol.

引用值:多个值构成的对象,按引用访问,实际操作的是对象的引用,而非值本身。

动态属性

引用值可以随时添加、修改、删除其属性和方法:

let person = new Object();
person.name = "HoMeTown"
console.log(person.name) // HoMeTown
复制代码

原始值不能有属性:

let name = "HoMeTown"
name.age = 22
console.log(name.age) // undefined
复制代码

但是如果使用new关键字创建string,可以有属性:

let name = new String("HoMeTown")
name.age = 22
console.log(name.age) // 22
复制代码

复制值

原始值:

let num1 = 5
let num2 = num1
复制代码

网络异常,图片无法展示
|

引用值:

let obj1 = new Object()
obbj2 = obj1
复制代码

网络异常,图片无法展示
|

参数传递

参数传递与复制值的原理一样。

// 原始值
function add(num) {
  num+=10
  return num
}
let count = 10
let res = add(count)
console.log(count) // 10
console.log(res) // 20
复制代码
// 引用值
function updateName(obj) {
  obj.name = "HoMeTown"
}
const person = { name: '' }
updateName(person) 
console.log(person.name)// HoMeTown
复制代码

obj 与 persion指向同一个内存地址,因此persion也受到反应,但是并不意味着这个参数是按照引用传递的:

function updateName(obj) {
  obj.name = "HoMeTown"
  obj = {name: ''};
  obj.name = "小红"
}
let person = {name: ''}
updateName(person)
console.log(person.name) // HoMeTown
复制代码

如果是按照引用传递,那么当obj={name:''}; obj.name = "小红"时,应该person的引用也会变,但是事实并没有,person.name依然是HoMeTown,由此看见,函数内部的obj重新指向了一个新的引用地址,然后随着updateName函数执行完毕,一起销毁了。

类型检测

typeof 可以完美的用来判断原始值的类型,但是对于引用值却不是很友好。

使用instance来判断:

let a = {}, b = []
console.log(b instanceof Array) // true
console.log(a instanceof Object) // true
console.log(a instanceof Array) // false
console.log(b instanceof Object) // true
复制代码

所以,一旦需要判断完美对象的时候,instanceof还是会出问题。

// 推荐使用
function getType(val) {
  return Object.prototype.toString.call(val)
}
console.log(getType({})) // [object Object]
console.log(getType([])) // [object Array]
复制代码

执行上下文与作用域

上下文

每个上下文都有一个关联的变量对象(VO/variable object) ,这个上下文中定义的所有变量和函数都挂载在这个对象上。

全局上下文是最外层的上下文,在浏览器中,通常指的就是window对象。

所有使用var定义的变量都会在window(全局上下文)上挂载,let和const则不会,但是在作用域链解析上效果是一样的。

上下文在其代码都执行完毕后,会被销毁,包括定义在它上面的所有变量和函数(全局上下文window在关闭网页或者退出浏览器时销毁)。

每个函数都有自己的上下文。代码执行流进入函数时,函数的上下文被推到一个上下文栈上,执行完毕后,上下文栈会弹出该函数上下文。

上下文的代码再执行的时候,会创建变量对象VO的一个作用域链(SC/scope chain) 。这个链决定了各级上下文中的代码在访问变量和函数时的顺序。代码正在执行的上下文变量对象始终位于作用域链的最前端。

如果上下文是函数,则其活动对象(AO/activation object) 用作变量对象。活动对象最初只有一个定义变量:arguments

作用域链中的下一个变量对象来自包含上下文(包含当前上下文的上下文) ,以此类推,直到全局上下文。

全局上下文一定是最后一个变量对象。

举个栗子:

var name = "HoMeTown"
function updateName() {
  if(name === "HoMeTown") {
    name = "HMT"
  } else {
    name = "HoMeTown"
  }
}
updateName()
复制代码

函数updateName的作用域链包含两个对象:

  1. 函数自己的变量对象(AO)
  2. 全局上下文的变量对象(Global VO)

所以能在updateName中找到name

局部作用域中定义的变量,可以在局部上下文中替换全局变量:

var name = "HoMeTown"
function updateName() {
  let anotherName = "HMT"
  function innerFunc() {
    let tempName = anotherName
    anotherName = name
    name = tempName
    // 这里可以访问到:name、anotherName、tempName
  }
  // 这里可以访问到:name、anotherName
  innerFunc()
}
// 这里可以访问到:name
updateName()
复制代码

上面的例子中出现了3个上下文:

  1. 全局上下文 __ GVO
  1. 变量name
  2. 函数updateName
  1. updateName的局部上下文
  1. 变量anotherName
  2. 函数innerFunc
  1. innerFunc的局部上下文
  1. 变量tempName

作用域链中的上下文只能往上查找,不可向下查找。

变量声明

ECMAScript5.1之前,var是声明变量的唯一关键字。ES6出现后,let与const成为声明变量的首选。

参考

垃圾回收

基本思路:确定哪个变量不会再使用,然后释放它占用的内存,这个过程是周期性的,也就是说垃圾回收程序每隔一段时间(或者说是在代码执行过程中某个预定的收集时间)就会自动执行。

标记清理 mark-and-sweep

变量进入上下文,会给变量加上存在于上下文中的标记,当变量离开上下文时,也会被加上离开上下文的标记。

这种标记的手段有很多,比如进入下上文时,反转二进制位的某一位;比如维护两个list。

垃圾回收程序运行时,会标记内存中存储的所有变量,然后将所有在上下文中的变量,以及被 在上下文中的变量 引用的变量 的标记去掉。剩下的其实就是待删除的了。

这块可能有点绕,其实大概是这么一个流程:

  • 垃圾回收程序在运行时会给内存中的所有变量都加上一个标记,假设内存中所有对象都是垃圾,全标记为0
  • 然后从各个根对象开始遍历,把不是垃圾(上下文中的变量&被上下文中的某一个变量引用的变量)的节点改回来成1
  • 清理所有标记为0的垃圾,销毁并回收它们所占用的内存空间
  • 最后,把所有内存中对象标记修改为0,等待下一轮垃圾回收。

引用计数 reference counting

思路是:对每一个值都记录它被引用次数,每引用一次,加一次,反之减一次。当为0时,程序回收内存。

有一个问题是:

对象A有一个引用指向对象B,对象B也有一个引用,指向A,就出现了循环引用

function problem() {
  let objA = new Object()
  let objB = new Object()
  objA.obj = objB
  objB.obj = objA
}
复制代码

在这个例子中,objAobjB通过各自的属性相互引用,也就意味着引用数都是2,他们的引用数永远不会变成0,如果函数被执行多次,那么会导致大量的内存不会被释放。

性能

垃圾回收程序是周期性运行的,开发者并不知道什么时候会执行,所以最好的方案是,不要管它什么时候收集,写代码的时候最好手动避免出现垃圾回收程序无法回收的变量。

调度垃圾回收程序的时机是一个大问题。

内存管理

分配给浏览器要比桌面软件的内存少得多,移动端浏览器更少!

写代码的时候,如果数据不再必要,那么请把它手动设置为null,从而释放引用,也叫解除引用,这个操作最适合全局变量和全局对象的属性,因为局部变量在函数执行完毕后会自动解除引用。比如:

function createPerson(name) {
  // localPerson在createPerson执行完毕出栈后,会自动解除引用
  let localPerson = new Object()
  localPerson.name = name
  return localPerson
}
let globalPerson = createPerson("HoMeTown")
// 这个操作不代表被回收了,而是解除引用,解除引用的关键在于保证它不再上下文里了,下次才能被回收。
globalPerson = null
复制代码

隐藏类 & 删除操作

高性能操作,如果你的代码对性能要求超级高,那这个对你会有很大的帮助: 代码运行期间,V8引擎会将创建的对象与隐藏类关联起来,以跟踪他们的属性特征。能够共享相同隐藏类的对象性能会更好,比如:

function Article() {
  this.title = "JavaScript高级程序设计"
}
let a1 = new Article()
let a2 = new Article()
复制代码

上面的例子中,a1和a2两个实例共享相同的隐藏类,但如果又添加了下面这行代码:

a2.size = "2000"
复制代码

此时a1和a2就会对应两个不同的隐藏类。

解决方案就是避免先创建再补充,最好一次性声明所有属性:

function Article() {
  this.title = "JavaScript高级程序设计"
  this.size = "2000"
}
复制代码

delete同样会使a1和a2不共享同一个隐藏类,比如加上下面这行代码:

delete a1.size
复制代码

最佳实践是不要delete,而是把你不想要的属性,设置成null

a1.size = null
复制代码

这样就可以保证还继续共享。

内存泄露

意外声明全局变量
function setName() {
  name="HoMeTown"
}
setName()
复制代码

解释器会把变量name当做window的属性来创建,window只有关闭浏览器的时候才会clear。

定时器
let name = 'HoMeTown'
setInterval(() => {
  console.log(name)
}, 100)
复制代码

只要定时器一直运行,name就一直会占用内存。

闭包
let outer = function() {
  let name = "HoMeTown"
  return function() {
    return name
  }
}
复制代码

调用outer会导致分配给name的内存被泄露。

总结

Js变量可以保存两种类型的值:原始值和引用值:

  • 原始值大小固定,因此保存在栈内存中。
  • 复制原始值会创建第二个副本。
  • 引用值是对象,存储在堆内存中。
  • 包含引用值的变量实际上只包含相应对象的指针,而不是对象本身。
  • 复制引用值,两个变量都指向同一个对象。
  • typeof 可以确定引用值的类型,instanceof可以用于确定值的引用类型。

任何变量(不管是原始值还是引用值)都存在于某个执行上下文中(也就是作用域)。这个上下文决定了变量的生命周期,以及他们可以访问代码的哪些部分:

  • 执行上下文包括:全局上下文、函数上下文、块级上下文。
  • 代码执行流 每进入一个新的上下文,都会创建一个作用域链,用于搜索变量和函数。
  • 函数或块的局部上下文不仅可以访问自己作用域内的变量,还可以访问上级-全局上下文中的变量。
  • 全局上下文中的变量不能访问局部上下文中的变量。
  • 变量的执行上下文用于确定什么时候释放内存。

JS中的垃圾回收程序不需要开发者进行分配与回收:

  • 离开上下文的值会被标记解除引用,然后再回收期间被删除。
  • 主流的垃圾回收算法是标记清理。
  • 引用计数算法会出现循环引用的问题。
  • 解除变量引用不仅可以消除循环引用,也对垃圾回收有帮助。


目录
相关文章
|
17天前
|
Web App开发 监控 JavaScript
监控和分析 JavaScript 内存使用情况
【10月更文挑战第30天】通过使用上述的浏览器开发者工具、性能分析工具和内存泄漏检测工具,可以有效地监控和分析JavaScript内存使用情况,及时发现和解决内存泄漏、过度内存消耗等问题,从而提高JavaScript应用程序的性能和稳定性。在实际开发中,可以根据具体的需求和场景选择合适的工具和方法来进行内存监控和分析。
|
17天前
|
JavaScript 前端开发 Java
避免 JavaScript 中的内存泄漏
【10月更文挑战第30天】避免JavaScript中的内存泄漏问题需要开发者对变量引用、事件监听器管理、DOM元素操作以及异步操作等方面有深入的理解和注意。通过遵循良好的编程实践和及时清理不再使用的资源,可以有效地减少内存泄漏的风险,提高JavaScript应用程序的性能和稳定性。
|
16天前
|
JavaScript 前端开发
js的作用域作用域链
【10月更文挑战第29天】理解JavaScript的作用域和作用域链对于正确理解变量的访问和生命周期、避免变量命名冲突以及编写高质量的JavaScript代码都具有重要意义。在实际开发中,需要合理地利用作用域和作用域链来组织代码结构,提高代码的可读性和可维护性。
|
30天前
|
存储 JavaScript 前端开发
JS 中的内存管理
【10月更文挑战第17天】了解和掌握 JavaScript 中的内存管理是非常重要的。通过合理的内存分配、及时的垃圾回收以及避免内存泄漏等措施,可以确保代码的高效运行和稳定性。同时,不断关注内存管理的最新发展动态,以便更好地应对各种挑战。在实际开发中要时刻关注内存使用情况,以提升应用的性能和质量。
27 1
|
16天前
|
自然语言处理 JavaScript 前端开发
[JS]作用域的“生产者”——词法作用域
本文介绍了JavaScript中的作用域模型与作用域,包括词法作用域和动态作用域的区别,以及全局作用域、函数作用域和块级作用域的特点。通过具体示例详细解析了变量提升、块级作用域中的暂时性死区等问题,并探讨了如何在循环中使用`var`和`let`的不同效果。最后,介绍了两种可以“欺骗”词法作用域的方法:`eval(str)`和`with(obj)`。文章结合了多位博主的总结,帮助读者更快速、便捷地掌握这些知识点。
29 2
[JS]作用域的“生产者”——词法作用域
|
17天前
|
前端开发 JavaScript 数据处理
CSS 变量的作用域和 JavaScript 变量的作用域有什么不同?
【10月更文挑战第28天】CSS变量和JavaScript变量虽然都有各自的作用域概念,但由于它们所属的语言和应用场景不同,其作用域的定义、范围、覆盖规则以及与其他语言特性的交互方式等方面都存在明显的差异。理解这些差异有助于更好地在Web开发中分别运用它们来实现预期的页面效果和功能逻辑。
|
16天前
|
JavaScript 前端开发
如何在 JavaScript 中实现块级作用域?
【10月更文挑战第29天】通过使用 `let`、`const` 关键字、立即执行函数表达式以及模块模式等方法,可以在JavaScript中有效地实现块级作用域,更好地控制变量的生命周期和访问权限,提高代码的可维护性和可读性。
|
24天前
|
JavaScript 前端开发
javascript的作用域
【10月更文挑战第19天javascript的作用域
|
21天前
|
Web App开发 JavaScript 前端开发
使用 Chrome 浏览器的内存分析工具来检测 JavaScript 中的内存泄漏
【10月更文挑战第25天】利用 Chrome 浏览器的内存分析工具,可以较为准确地检测 JavaScript 中的内存泄漏问题,并帮助我们找出潜在的泄漏点,以便采取相应的解决措施。
138 9
|
21天前
|
监控 JavaScript 前端开发
如何检测和解决 JavaScript 中内存泄漏问题
【10月更文挑战第25天】解决内存泄漏问题需要对代码有深入的理解和细致的排查。同时,不断优化和改进代码的结构和逻辑也是预防内存泄漏的重要措施。
35 6