深入理解 JS| 青训营笔记

103 阅读7分钟

1 JS的基本概念

  • 单线程
  • 动态、弱类型
  • 面向对象、函数式
  • 解释类语言、JIT
  • 安全、性能差
  • GUI线程与JS线程互斥
    弱类型:运行时才确定数据类型的语言,变量在用之前无需申明类型

1.1 数据类型

  1. 对象
    • 数组
    • 函数
    • ……
  2. 基础类型
    • 字符串
    • undefined
    • 数字
    • symbol
    • bigInt
    • boolen
    • null

1.1.1 案例

案例1

const a = {
  name:'haha'
}
const b = a
b.name = '111'
console.log(a,b)
输出结果

{name:'111'} {name:'111'}

个人理解

b指向了和a同一个指向的对象(地址),然后修改了b的name属性,即其指向对象(地址)的值,所以a的值也被改了

案例2

const str = '11'
let newStr = str
newStr = '22'
console.log(str,newStr)
输出结果

11 22

个人理解

newStr先是指向了str指向的对象('11'),然后newStr = '22'使其指向新的对象('22'),因此str的值并没有被改变

案例3

const arr = [1,2]
arr.push(3)
const str = 'strstrstr'
str.slice(0,2)
console.log(arr,str)
输出结果

[1,2,3] strstrstr

知识点

在JS里,复杂的数据类型原始值可以被改变,基础数据类型原始值不可以被改变。

1.2 作用域

变量的可访问性和可见性。
静态作用域,通过他就能够预测代码在执行过程中如何查找标识符。

  • 全局作用域
  • 函数作用域
  • 块级作用域

1.3 变量提升

  • var有变量提升
  • let、const没有变量提升,提前访问会报错
  • function函数可以先调用再定义
  • 赋值给变量的函数无法提前调用
个人理解
  1. var有变量提升,因此在声明前就访问不会报错,但值为undefined。
  2. 优先级:函数提升>变量提升。
  3. 如果一个函数碰上有一个同名变量声明,该函数并不会被同名变量覆盖。但如果该同名变量被赋值了,函数声明就会被同名变量覆盖。

2 JS是怎么执行的

2.1 流程图

graph TD
源代码 --词法分析--> AST --> 字节码 --编译执行--> 机器码
源代码 --语法分析--> 执行上下文
字节码 --逐行解释执行--> 机器码

2.2 执行上下文

当JS引擎解析到可执行代码片段(通常是函数调用)的时候,就会先做一些执行前的准备工作,这个准备工作就叫做“执行上下文”,也叫执行环境

2.2.1 分类

  • 全局执行上下文:代码开始执行时就会创建,将它压进执行(调用)栈的栈底,每个生命周期内只有一份
  • 函数执行上下文:当执行一个函数时,这个函数内的代码会被编译,生成变量环境、词法环境等,当函数执行结束的时候该执行环境从调用栈栈顶弹出
  • Eval执行上下文

2.2.2 创建执行上下文的时候做了什么?

  • 绑定This
  • 创建词法环境
  • 创建变量环境

词法环境:基于ES代码的词法嵌套结构来定义标识符和具体变量和函数的关联。一个词法环境由环境记录器和一个可能的引用外部词法环境的空值组成
变量环境:变量环境和词法环境的不同点:前者被用来存储函数声明和变量(let & const)绑定,而后者只用来存储var变量绑定
Outer:指向外部变量环境的一个指针


作用域链

个人理解:当一个函数执行时,代码访问某个未在这个函数里声明的变量的时候,会往上一级的作用域去找,直到找到(或者找不到报错),这就是作用域链。

变量环境

基础类型数据存储的是值,复杂类型数据(对象)存储的是一个堆里的地址。在堆空间里,这个地址存放的是该对象里面的基础类型数据和其对应的值。

ESP

记录当前执行状态的指针。当某段执行上下文已执行完毕,会变无效内存。此时ESP指针会往下走指向另一个执行上下文。准备新的要执行的执行上下文时,会把旧的已无效内存覆盖,完成栈上面的垃圾回收。


3 JS的进阶知识点

3.1 闭包

例子
function showName(){
  const company = "Bytedance"
  const dep = "haha"
  const name = "a"
  console.log('company',company)
  return function(){
    console.log(dep)
    return name
  }
}
const getName = showName()
console.log(getName())

为什么代码const getName = showName()已经执行完了,showName理应已被弹出了执行栈,但是为什么console.log(getName())还能访问内部变量输出结果呢?

原因

返回function的时候产生了闭包。是一个地址,指向堆上的一块内存,这块内存里存储了该闭包访问的变量。该闭包一直跟随该function,被赋值给getName后,一直跟随geName直到其生命周期结束。
闭包不合理应用可能造成内存泄漏,本质是没有被回收的对象

3.2 this

情况1

function show(){
  console.log(this)
}
show()

输出:window对象
1.普通函数(无论嵌套与否)的this指向window

情况2

const name = 11
const obj = {
  name:'a',
  showName(){
    console.log(this.name)
  }
}
const obj1 = {
  name:'b'
}
obj.showName.apply(obj1)

输出:a b

  1. 对象调用this时,this指向对象
  2. 先赋值再调用,看调用的地方
  3. 对象调用时想改变this指向,可以使用apply、bind、call

情况3(构造函数的情况)

function ShowName(){
this.name = 'a'
  console.log(this.name)
}
const getName = new ShowName()
console.log(getName.name)

输出:a a

new的时候做了什么?
  1. 创建临时对象
  2. 将this指向临时对象
  3. 执行构造函数
  4. 返回临时对象

3.3 垃圾回收

堆分为两块:新生代空间、老生代空间,两者垃圾回收机制不同
新生代空间存放小的变量,一般给变量分配内存时都放这里,过大的会放进老生代空间。

新生代

将新生代划成两个区域:对象区域(存放活跃变量) 、空闲区域。当对象区域满了以后:

  1. 将垃圾标记
  2. 复制对象
  3. 两个区域反转,将对象区域清空,完成一次垃圾回收
老生代

数据来源:分配内存很大的变量、新生代里经历了两轮垃圾回收仍未被回收的变量。
会标记变量并清除掉被标记的变量,并且对清除后不连续的内存进行一次整理使其连续,整理出大的内存空间。清除时js引擎停顿,所以会将其分成碎片任务进行操作。

3.4 事件循环

graph TD
Function*n --> 微任务 --> 宏任务

4 总结和个人感想

4.1 总结

  1. JS是单线程的,但是Render进程里面有多个线程
  2. JS线程和GUI线程互斥,执行大的计算任务会导致页面卡顿
  3. 基础数据类型存在上,复杂数据类型存在
  4. const、let没有变量提升,提前使用会报错
  5. JS也有编译的过程,不是一个纯解释型语言,它执行之前会生成执行上下文
  6. 一个执行上下文包括变量环境、词法环境、this
  7. 变量环境里面有一个指向外部函数执行上下文的指针,形成了作用域链
  8. 全局执行上下文只有一份
  9. this和执行上下文绑定

4.2 个人感想

  • 老师讲得很好!完善和复习了之前学过的知识,并且扩展了一些,例如垃圾回收。
  • 老师会运用代码讲解,预习的时候好奇ppt相比之前的课页数很少,听了课发现老师结合代码讲的很清晰,数据、变量提升、执行上下文、闭包一下就讲明白了。
  • 我之前只知道函数里嵌套的return函数里有上一级的变量会产生闭包,但是不知道闭包的目的和闭包的本质,今天终于明白了(详见3.1例子)