前言
大家好,我是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
的作用域链包含两个对象:
- 函数自己的变量对象(AO)
- 全局上下文的变量对象(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个上下文:
- 全局上下文 __ GVO
- 变量name
- 函数updateName
- updateName的局部上下文
- 变量anotherName
- 函数innerFunc
- innerFunc的局部上下文
- 变量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 } 复制代码
在这个例子中,objA
与objB
通过各自的属性相互引用,也就意味着引用数都是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中的垃圾回收程序不需要开发者进行分配与回收:
- 离开上下文的值会被标记解除引用,然后再回收期间被删除。
- 主流的垃圾回收算法是标记清理。
- 引用计数算法会出现循环引用的问题。
- 解除变量引用不仅可以消除循环引用,也对垃圾回收有帮助。