所有图片来自 字节前端训练营 - 深入理解JS.pptx - 飞书云文档 (feishu.cn)
此文档是对对应课程 深入理解 JS - 掘金 (juejin.cn) 的笔记
JS 的诞生和发展史
1995年诞生,由Brendan Eich开发
借鉴了
- C语言的基本语法(变量声明、执行语句、函数等)
- JAVA语言的数据类型和内存管理
- Scheme语言,将函数作为一等公民(可以创建、赋值、传递)(即函数可以赋值给变量、可以作为参数被传递、可以作为另一个函数的返回值)
- Self语言,基于原型prototype的继承
2009年发布 ECMAScript5,Nodejs
2015年发布 ECMAScript6
此后一年一版
JS基础概念
JS的特点
- 单线程
在浏览器运行时,生成了多个进程(浏览器/主进程、渲染进程、网络进程、插件进程、GPU进程等),其中渲染进程负责获取到相关资源后页面的展示。
渲染进程中,包含GUI线程、JS线程、网络线程、事件处理线程、定时器线程。
GUI线程与JS线程是互斥的,不能同时修改DOM和渲染DOM
类似的原因:JS语言是用于处理用户和浏览器交互的脚本语言,因此JS用于处理DOM的变化与渲染。当存在多个线程同时处理DOM元素时,很容易产生冲突和锁的问题,因此,JS设计为单线程的。
注意:此处的JS单线程是指,JS引擎同一时刻只运行一个(操作DOM的)JS线程。
为了利用多核CPU的计算能力,HTML5提出Web Worker标准,允许JavaScript脚本创建多个线程,但是子线程完全受主线程控制,且不得操作DOM。所以,这个新标准并没有改变JavaScript单线程的本质。
- JS是动态、弱类型的
对于一个变量,可以在初次赋值和修改时赋予任意类型的变量,不必明确指出其变量类型,不必进行显示类型转换。
由此也带来一些问题,TS的出现等提供了变量的类型限制和检测机制。
- 面向对象、函数式
可以使用函数和类的方式。
- 解释类语言、JIT
- 有的编程语言要求必须提前将所有源代码一次性转换成二进制指令,也就是生成一个可执行程序(Windows 下的 .exe),比如C语言、C++、Golang、Pascal(Delphi)、汇编等,这种编程语言称为编译型语言,使用的转换工具称为编译器。
- 有的编程语言可以一边执行一边转换,需要哪些源代码就转换哪些源代码,不会生成可执行程序,比如 Python、JavaScript、PHP、Shell、MATLAB 等,这种编程语言称为解释型语言,使用的转换工具称为解释器。
- 安全、性能差
出于JS在浏览器的环境下运行,不会影响本机的内存空间。
性能相较于编译型语言差。
JS数据类型
对于基础数据类型,其赋值后,存储在栈内存中
对于复杂数据类型,赋值后,在堆内存中存储指针(引用),指向一块堆内存,存储真正的值。
因此,复杂数据类型的直接拷贝是对引用的的赋值,即产生了2各指向同一块堆内存的指针(引用),即浅拷贝。对应的深拷贝,是再创建另一个指向新的堆内存的复杂变量。
注意:对于JS创建的基础类型变量,不能直接改变其值。
let a = 2
a = 3
console.log(a) // 3
let str1 = "123"
str1 = "234"
console.log(str1) // {"234"
let str2 = "123"
str2[0] = '4'
console.log(str2) // "123"
即,可以对变量再次赋值(创建了新的值,并销毁旧的值、将变量指向新的值),但不能直接更改变量的内容。
注意:栈的内存管理由操作系统实现,堆的内存管理可以由用户实现。
JS作用域
函数作用域:在函数中定义或传入的的变量,仅在函数中使用。
全局作用域:在全局定义的变量,任何位置都可以 最终 访问到。
块级作用域:ES6提出,仅在 {} 中有效。
JS通过静态作用域实现,使用变量时,控制变量的可见性和可访问性。
实际使用:JS在运行时的 调用栈。
在当前执行上下文访问变量,通过作用域判断能否获取变量的值。
JS变量提升
使用 var 声明的变量、当前上下文定义的函数会得到变量提升,即可以先访问后声明。
实际上是,JS执行到当前上下文时,先创建了当前上下文的变量环境(存储函数和var声明的变量,和词法环境存储let,const声明的变量)再顺序执行代码。
由此,JS并不是完全的解释型语言(一句一句解释和运行)。
JS是如何运行的
从整个过程来看,JS引擎依次对源代码进行了:
词法分析、语法分析、语义分析等,构建AST抽象语法树
根据抽象语法树生成字节码(直接生成汇编代码的内存太大)
执行时,逐句解释字节码,生成机器码并运行
V8引擎做出优化:对于重复出现多次的机器码,标记为“热代码”,对其直接解释为机器码,加快运行速度;同时,当此代码不再有较高的出现频次(如代码更改)时,去除其“热代码”标记,仍将其转化为字节码。即根据出现频率动态的标记“热代码”,直接解释为机器码。
执行上下文
JS引擎在解析可执行代码片段时(即编译过程中),会先做一些准备工作:即创建执行上下文(execution context,简称EC,又叫执行环境)。
包括:
- 变量环境:存放函数、
var变量的定义- outer:指向外层作用域的指针
- 词法环境:存放
let、const变量的定义 - this:确定此处
this绑定的对象 - 可执行代码:将其它代码编译为字节码
分类:
- 全局上下文:在程序开始被执行时创建,一个生命周期只有一个(即一个执行过程中只有一个),处于执行栈的最下层。
- 函数上下文:执行一个函数时,对函数内的代码进行分析、编译,生成对应的变量环境。词法环境,添加到栈顶;函数执行结束,从栈顶弹出。
- eval上下文:使用eval函数时,创建的实质性上下文
创建执行上下文时做了什么:
创建词法环境、语法环境、确定this的指向、生成可执行代码片段。
关于变量环境与词法环境
词法环境:
在编译时,将此执行上下文中的let、const变量的定义提取出来
但是,对于let、const变量,在创建执行上下文时,仅仅声明了变量,并未赋值。此时的值被设置为 uninitialized未初始化的(仅预留空间),直到运行到let、const变量的赋值语句,此时变量才初始化和赋值。
因此,产生了所谓的 暂时性死区
即创建了执行上下文,但未运行至赋值代码时,此时调用let、const变量会报错。
变量环境:
在编译时,将此执行上下文中的var变量和函数的定义提取出来
即,函数和var变量做出了变量提升。
对于var变量,在创建执行上下文时,初始化赋值 undefined。
而let、const变量,在创建执行上下文时,初始化赋值是uninitialized未初始化的。
执行时,若原JS代码中对变量有赋初始值,则更改为初始值。
关于函数
直接声明的会被提取出来,实现变量提升。
但,如果将函数赋值给变量,通过变量调用函数,需要在变量赋值语句后调用。
执行上下文的实例
函数调用栈,函数执行完成后,函数上下文出栈的实现:
通过ESP指针移动实现。
指针下移时,顶部的执行上下文的空间就失效了,其它执行上下文再次入栈时,会覆盖。
JS的进阶知识
闭包
实际上是函数使用外部定义的变量。
即,函数在使用上层作用域中的变量时,对应的上层作用域会保留。
此时,函数及其使用的词法空间共同构成一个闭包。
因此,使用闭包会占用额外的内存空间,影响性能。
例:
this
this的4种绑定方式
- 默认绑定:独立调用函数时,取决于其环境
- 隐式绑定:通过对象调用函数时,如 obj.foo()
- 显示绑定:通过 apply/call/bind
- new绑定:通过 new 调用函数生成实例时
垃圾回收
对于栈内存:通过ESP指针向栈底移动,使得上方的内存失效。
堆内存:
2部分
新生代(约1-8MB):存储较小的变量。
老生代:存储较大的变量,和在新生代中2轮垃圾回收仍生效的变量。
- 新生代的垃圾回收策略:
分为对象区域和空闲区域
对象区域:存储新加入的、当前活跃的变量。当达到一定条件(占满空间等)时,执行清理:
- 选中所有仍在使用的变量
- 复制到空闲区域
- 然后交换对象区域和空闲区域。
- 老生代的垃圾回收策略:
同样是标记无用的对象,然后清除其占用的内存。
对于清除后的空缺,导致空闲空间不连续,需要主垃圾回收器主动整理。
但是,此时回收老生代和整理的过程中会停止JS线程的运行。
因此做出优化:分时复用,将垃圾回收的任务分为碎片与JS进程交替进行。
事件循环
原则:每次事件循环,先执行微任务,微任务队列空,再执行下一个宏任务。