深入JS语言 | 青训营笔记

134 阅读9分钟

所有图片来自 字节前端训练营 - 深入理解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线程、网络线程、事件处理线程、定时器线程。

image.png

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数据类型

image.png

对于基础数据类型,其赋值后,存储在栈内存中

对于复杂数据类型,赋值后,在堆内存中存储指针(引用),指向一块堆内存,存储真正的值。

因此,复杂数据类型的直接拷贝是对引用的的赋值,即产生了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声明的变量,和词法环境存储letconst声明的变量)再顺序执行代码。

由此,JS并不是完全的解释型语言(一句一句解释和运行)。

JS是如何运行的

image.png

从整个过程来看,JS引擎依次对源代码进行了:

词法分析、语法分析、语义分析等,构建AST抽象语法树

根据抽象语法树生成字节码(直接生成汇编代码的内存太大)

执行时,逐句解释字节码,生成机器码并运行

V8引擎做出优化:对于重复出现多次的机器码,标记为“热代码”,对其直接解释为机器码,加快运行速度;同时,当此代码不再有较高的出现频次(如代码更改)时,去除其“热代码”标记,仍将其转化为字节码。即根据出现频率动态的标记“热代码”,直接解释为机器码。

执行上下文

JS引擎在解析可执行代码片段时(即编译过程中),会先做一些准备工作:即创建执行上下文(execution context,简称EC,又叫执行环境)。

image.png

包括:

  • 变量环境:存放函数、var变量的定义
    • outer:指向外层作用域的指针
  • 词法环境:存放 letconst变量的定义
  • this:确定此处this绑定的对象
  • 可执行代码:将其它代码编译为字节码

分类:

  • 全局上下文:在程序开始被执行时创建,一个生命周期只有一个(即一个执行过程中只有一个),处于执行栈的最下层。
  • 函数上下文:执行一个函数时,对函数内的代码进行分析、编译,生成对应的变量环境。词法环境,添加到栈顶;函数执行结束,从栈顶弹出。
  • eval上下文:使用eval函数时,创建的实质性上下文

创建执行上下文时做了什么:

创建词法环境、语法环境、确定this的指向、生成可执行代码片段。

关于变量环境与词法环境

词法环境:

在编译时,将此执行上下文中的letconst变量的定义提取出来

但是,对于letconst变量,在创建执行上下文时,仅仅声明了变量,并未赋值。此时的值被设置为 uninitialized未初始化的(仅预留空间),直到运行到letconst变量的赋值语句,此时变量才初始化和赋值。

因此,产生了所谓的 暂时性死区

即创建了执行上下文,但未运行至赋值代码时,此时调用letconst变量会报错。

变量环境:

在编译时,将此执行上下文中的var变量和函数的定义提取出来

即,函数var变量做出了变量提升

对于var变量,在创建执行上下文时,初始化赋值 undefined

letconst变量,在创建执行上下文时,初始化赋值是uninitialized未初始化的。

执行时,若原JS代码中对变量有赋初始值,则更改为初始值。

关于函数

直接声明的会被提取出来,实现变量提升。

但,如果将函数赋值给变量,通过变量调用函数,需要在变量赋值语句后调用。

执行上下文的实例

image.png

image.png

函数调用栈,函数执行完成后,函数上下文出栈的实现:

通过ESP指针移动实现。

指针下移时,顶部的执行上下文的空间就失效了,其它执行上下文再次入栈时,会覆盖。

JS的进阶知识

闭包

实际上是函数使用外部定义的变量。

即,函数在使用上层作用域中的变量时,对应的上层作用域会保留。

此时,函数及其使用的词法空间共同构成一个闭包。

因此,使用闭包会占用额外的内存空间,影响性能。

例:

image.png

this

this的4种绑定方式

  • 默认绑定:独立调用函数时,取决于其环境
  • 隐式绑定:通过对象调用函数时,如 obj.foo()
  • 显示绑定:通过 apply/call/bind
  • new绑定:通过 new 调用函数生成实例时

image.png

垃圾回收

对于栈内存:通过ESP指针向栈底移动,使得上方的内存失效。

堆内存:

image.png

2部分

新生代(约1-8MB):存储较小的变量。

老生代:存储较大的变量,和在新生代中2轮垃圾回收仍生效的变量。

  • 新生代的垃圾回收策略:

分为对象区域和空闲区域

对象区域:存储新加入的、当前活跃的变量。当达到一定条件(占满空间等)时,执行清理:

  1. 选中所有仍在使用的变量
  2. 复制到空闲区域
  3. 然后交换对象区域和空闲区域。
  • 老生代的垃圾回收策略:

同样是标记无用的对象,然后清除其占用的内存。

对于清除后的空缺,导致空闲空间不连续,需要主垃圾回收器主动整理。

但是,此时回收老生代和整理的过程中会停止JS线程的运行。

因此做出优化:分时复用,将垃圾回收的任务分为碎片与JS进程交替进行。

事件循环

原则:每次事件循环,先执行微任务,微任务队列空,再执行下一个宏任务。

总结

image.png