这个前端学习笔记是学习gitchat上的一个课程,这个课程的质量非常好,价格也不贵,非常时候前端入门的小伙伴们进阶。
笔记不会涉及很多,主要是提取一些知识点,详细的大家最好去过一遍教程,相信你一定会有很大的收获
作用域
就近原则,通过作用域链,找到最近的一个变量
-
ES6的块级作用域
使用
let
和const
命名的变量,不会声明提升。在上述2个声明的变量前使用变量会报错,并且同一个作用域下只允许一个
let
声明同一个变量。 -
函数的参数也会出现死区TDZ,所以需要注意引用的顺序。
执行上下文和调用栈
- 执行上下文就是当前代码的执行环境/作用域
代码执行会经历2个阶段:
-
代码预编译(VO)
降JS代码编译成可执行的代码。javascript是解释型语言,编译一行执行一行。执行前,javascript引擎会提前做些准备工作。确认语法无误的时候,javascript代码在预编译的阶段对变量的内存空间进行分配,在这过程中会经历:
- 变量声明
- 变量声明提升,值为undefined
- 非表达式函数声明提升
-
代码执行阶段(AO)
此时作用域链已经确定,由当前作用域和外层所决定,保证了变量的有序访问。
调用栈
函数一个接着一个调用,形成的类似栈访问。符合先进后出的形式。
函数执行完之后,退出栈之后,函数的内部变量就会被垃圾回收器回收,这也是函数外部无法访问函数内部的原因。
正常的函数会经历3个阶段:
- 创建阶段
- 执行阶段
- 执行完毕,回收阶段
闭包
闭包就是为了解决无法访问函数内部的这个问题,函数执行完毕,空间不会马上销毁。
内层函数引用了外层函数作用域下的变量,并且内层函数在全局环境下可访问,就形成了闭包。
一个简单的闭包:
function numGenerator() {
let num = 1
num++
return () => {
console.log(num)
}
}
var getNum = numGenerator()// 函数执行之后,num不会马上消失
getNum()// 可以通过返回的函数进行访问
我们如果返回一个函数,而这个函数访问了上一个函数作用域下的变量,这样就能够让函数上下文在函数执行之后不会马上销毁。这也是闭包的基本原理。
内存管理
- 分配内存:声明的时候,会划分内存保存数据
- 使用内存:所有使用到变量的地方
- 销毁内存:手动等于null,或者执行完毕之后走出上下文环境等
堆内存和栈内存
javascript有2种数据类型:基本和引用
基本类型都保存在栈内存中,而引用类型保存在堆内存中,注意引用内存也会使用到栈内存,引用类型栈内存保存的是堆内存的地址。这里用一张图表示:
每一个语言都会有属于自己的垃圾回收器,在不用到的内存会进行释放,但是垃圾回收器也是不完美的,也会存在判断错误的情况,无法将垃圾内存进行回收。这样就会出现内存泄漏:指内存空间明明已经不再被使用,但由于某种原因并没有被释放的现象。
内存泄漏
这个是每一个程序员都需要关心的问题。我们来看看前端有那些常见的内存泄漏情形。
-
例子1:
var element = document.querySelectorAll("li")
// 移除 element 节点
function remove() {
document.querySelector(‘body’).removeChild(element1[0])
}
```
我们在操作DOM元素的时候,如果响应移除掉一个元素节点,一般使用removeChild
即可,但是上述情况中,element
还保存着对已经移除的元素引用,虽然视觉上这个节点已经移除了,但是在dom
对象上,还是会保存这个节点的信息。所以,我们需要手动的移除element
这个节点。
-
例子2
var element = document.getElementById('element') element.innerHTML = '<button id="button">点击</button>' var button = document.getElementById('button') button.addEventListener('click', function() { // ... }) element.innerHTML = ''
这里我们动态添加一个节点,但是我们给这个按钮添加了一个事件之后,想要清楚
element
节点的内容,虽然视觉上已经移除,但是在内存中,button
还保存着button
节点的信息和其事件处理句柄还在,垃圾回收器无法回收,需要手动清楚这些引用。 -
例子3
定时器,如果不需要了就及时停止。
function foo() { var name = 'lucas' window.setInterval(function() { console.log(name) }, 1000) } foo()
由于计时器的一直存在,
name
无法释放内存。如果业务不需要,就自己手动停止计时器。
clearInterval
-
意外的全局变量
function foo(arg) { bar = "this is a hidden global variable"; } // 或者不恰当使用this function foo() { this.variable = "potential accidental global"; }
函数执行后,并不会消除
bar
的内存
垃圾回收机制
当然,除了开发者主动保证以外,大部分的场景浏览器都会依靠:
-
引用计数
这是一个算法,能够追踪没有被引用的对象,进行回收。通过例子来理解这个引用的意思。
var obj1 = { property1: { subproperty1: 20 } };
obj1
引用了一个对象property1
,property1
也引用了一个对象。由于obj1
引用了一个对象,所以不会被回收。我们接下来继续进行操作:
var obj2 = obj1; obj1 = "some random text"
这时,
obj2
引用这obj1
所引用的同一个对象,后面obj1
不在引用对象,这时只存在一个obj2
引用着这个对象。垃圾回收器也不会回收。我们继续操作:
var obj2_pro = obj2.property1
obj2_pro
引用这obj2
属性。这个时候,对象就有2个引用,一个是obj2
,一个是obj2_pro
我们消除
obj2
的引用obj2 = "some random text"
但是还存在
obj2_pro
的引用,垃圾回收器无法回收。我们继续操作,消除obj2_pro
的引用。obj2_pro = null
这个时候,最初的对象没有任何引用了,这时引用垃圾回收器就可以进行回收了。
假如,2个对象互相引用,那么这个就永远无法回收了。这就会导致内存泄漏。
-
标记清除
为了解决引用计数的弊端,这里还存在一种回收机制。
我们从跟节点进行访问,把所有访问到的进行标记,所有可以遍历都遍历之后,存在没有被标记的对象,就可以进行回收。
自 2012 年以来,JavaScript 引擎已经使用此算法来代替引用计数垃圾回收。