《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中的垃圾回收程序不需要开发者进行分配与回收:

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


目录
相关文章
|
18天前
|
JavaScript 前端开发 开发者
JavaScript的变量提升是一种编译阶段的行为,它将`var`声明的变量和函数声明移至作用域顶部。
【6月更文挑战第27天】JavaScript的变量提升是一种编译阶段的行为,它将`var`声明的变量和函数声明移至作用域顶部。变量默认值为`undefined`,函数则整体提升。`let`和`const`不在提升范围内,存在暂时性死区。现代实践推荐明确声明位置以减少误解。
20 2
|
18天前
|
JavaScript 前端开发
事件委托是JS技巧,通过绑定事件到父元素利用事件冒泡,减少事件处理器数量,提高性能和节省内存。
【6月更文挑战第27天】事件委托是JS技巧,通过绑定事件到父元素利用事件冒泡,减少事件处理器数量,提高性能和节省内存。例如,动态列表可共享一个`click`事件处理器,通过`event.target`识别触发事件的子元素,简化管理和响应动态内容变化。
16 0
|
20天前
|
自然语言处理 JavaScript 前端开发
JavaScript闭包是函数访问外部作用域变量的能力体现,它用于封装私有变量、持久化状态、避免全局污染和处理异步操作。
【6月更文挑战第25天】JavaScript闭包是函数访问外部作用域变量的能力体现,它用于封装私有变量、持久化状态、避免全局污染和处理异步操作。闭包基于作用域链和垃圾回收机制,允许函数记住其定义时的环境。例如,`createCounter`函数返回的内部函数能访问并更新`count`,每次调用`counter()`计数器递增,展示了闭包维持状态的特性。
30 5
|
18天前
|
JavaScript 前端开发
JavaScript作用域关乎变量和函数的可见范围。
【6月更文挑战第27天】JavaScript作用域关乎变量和函数的可见范围。全局作用域适用于整个脚本,局部作用域限于函数内部,而ES6引入的`let`和`const`实现了块级作用域。全局变量易引发冲突和内存占用,局部作用域在函数执行后消失,块级作用域提高了变量管理的灵活性。作用域关键在于组织代码和管理变量生命周期。
20 1
|
5天前
|
测试技术 API Android开发
autox.js如何监听异常情况,比如网络中断、内存慢、应用死机或者页面无响应
autox.js如何监听异常情况,比如网络中断、内存慢、应用死机或者页面无响应
|
12天前
|
JavaScript
JS 【详解】作用域
JS 【详解】作用域
7 0
|
13天前
|
缓存 自然语言处理 JavaScript
JavaScript作用域详解
JavaScript作用域详解
10 0
|
13天前
|
存储 JavaScript 前端开发
面试官:JS中变量定义时内存有什么变化?
面试官:JS中变量定义时内存有什么变化?
13 0
|
13天前
|
自然语言处理 前端开发 JavaScript
前端 JS 经典:闭包与内存泄漏、垃圾回收
前端 JS 经典:闭包与内存泄漏、垃圾回收
11 0
|
14天前
|
存储 JavaScript 前端开发
javascript的栈内存 VS 堆内存(浅拷贝 VS 深拷贝)
javascript的栈内存 VS 堆内存(浅拷贝 VS 深拷贝)
8 0