深入理解JS| 青训营笔记

73 阅读10分钟

在本节中主要围绕JS的发展、执行特点、常见知识点等进行了讲述,主要分为以下四个部分:

  1. JS的基本概念
  2. JS是怎么执行的
  3. JS的进阶知识点

主要内容

1.JS的基本概念

1.1 JS的诞生和发展

诞生:1995年,Brendan Eich开发

  1. 借鉴了c语言的基本语法
  2. 借鉴Java语言的数据类型和内存管理
  3. 借鉴Scheme语言,将函数提升到第一等公民的地位
  4. 借鉴Self语言,使用基于原型的继承机制

发展:

  1. Mocha.>1995.9.LiveScript > 1995.12 JavaScript

2.1997年6月,第一版ECMAScript发布

3.1999年12月,第三版ECMAScript发布

4.2009年12月,第五版ECMAScript发布

5.2009 年,Ryan 创建了 Node.js

6.2010年,Isaac基于node.js写出了npm

7.2015年6月,第六版ECMAScript发布

1.2 浏览器进程模型

js是单线程的,浏览器是多进程的。通常打开浏览器,会首先开启Browser进程、GPU进程、渲染进程以及插件进程网络进程等。在渲染进程中,包含了多个线程,GUI线程、JS线程、事件触发线程、定时器触发线程、网络线程等。其中GUI线程和JS线程是互斥的,因为js线程对js代码进行解析时候,可能包含对DOM进行操作,这个时候GUI线程要想绘制这个正在被操作的元素是不合理的。

1.3 JS特点

  1. 单线程。js会操作DOM 如果是多线程,就会出现,一个线程要修改DOM而另一个线程要删除DOM,就会产生冲突。
  2. 动态、弱类型。在运行时才确定数据类型的语言,变量在使用之前无需声明类型。(与之相反的就是静态语言/强类型语言,比如c语言,编译时变量的数据类型就需要确定)
  3. 面向对象、函数式。参数->结果,纯函数、副作用、jQuery。原型、继承、封装
  4. 解释类语言、JIT。
  5. 安全、性能差

1.4 JS数据类型

JS基本数据类型:Undefined、Null、Boolean、Number、String、Symbol (new in ES6)、BigInt

JS引用类型:统称为 对象 类型( Function、Array、Date 、RegExp等)

基本数据类型和引用数据类型的区别

基本数据类型引用数据类型
内存位置在栈内保存数据值实际值放在堆中,栈内保存的是指针,指向存储对象的内存地址
访问方式直接访问按引用访问。先得到象在堆内存的地址,再按照地址去获取这个对象中的值
变量赋值 b=ab中的值是a的副本,互不影响,完全独立,a的值改变不会影响b的值b中的值是a对象在堆内存中的地址,a和b指向同一个对象,会相互影响
参数传递(实参、形参)将变量的值传递给参数,之后参数和变量彼此不影响变量里的值是对象在堆中的内存地址,在函数内部对参数进行丢该也会反映到外部,就实参形参相互影响
  1. typeof:经常用来测试一个变量是不是最基本的数据类型
    var a = 'yes'
    console.log( typeof a)  //string
    a = 1
    console.log( typeof a)  //number
    var b
    console.log( typeof b)  //undefine
    a = null
    console.log( typeof a)  //object
    a = Symbol()
    console.log( typeof a)  // symbol
    a = BigInt(10)
    console.log( typeof a)  // bigint
    a = function(){}
    console.log( typeof a)  //function
    a = {}
    console.log( typeof a)  //object
    b = new Date()
    console.log( typeof b)  //object
    var c = new RegExp()
    console.log( typeof c)  //object

值得注意的是 typeof null 的结果是object

  1. instanceof:用于检测构造函数的prototype属性是否出现在某个实例对象的原型链上,即用于检测某个引用类型的对象是不是某个类型对象的实例。比如{}是不是object,[0,1]是不是数组
    console.log( typeof c)
    console.log(({}) instanceof Object)              // true
    console.log(([]) instanceof Array );             // true 
    console.log((/(?:)/) instanceof RegExp )         // true
    console.log((function(){}) instanceof Function)  // true

1.5 作用域

1.6 变量提升

变量提升就是浏览器解析代码的一个过程,发生在js代码之前。

变量提升的含义:在当前作用域,代码执行之前。浏览器会从头到尾检测一遍所有变量,给带var和function进行提前声明和定义。

带var的会只声明(创建变量),不定义(赋值)

带function的既声明(创建变量),又定义(赋值)

根据ES6规范块级作用域的提升行为是会制造一个暂时性死区(temporal dead zone, TDZ)。 暂时性死区的现象就是在块级顶部到变量正式申明这块区域去访问这个变量的话,直接报错。

需要注意的情况:

  1. 自执行函数、匿名函数不进行变量提升
  2. let 和 const 声明的变量不会进行提升,提前访问会出错,暂时性死区。
  3. 访问变量时,如果当前作用域没有,会一级一级往上找,一直到全局作用域,这就是作用域链。
  4. let和const是块级作用域,有效范围是一对{}

2.JS是怎么执行的

2.1 js执行过程

首先对源代码进行词法分析和语法分析生成AST(abstract syntax tree\抽象语法树,抽象表示把js代码进行了结构化的转化,转化为一种类似树状数据结构的json对象),同时创造执行上下文。AST会进一步转换成字节码,字节码通过逐行解释执行生成机器码(类似于高级语言转换成汇编语言)。字节码编译执行生成机器码的过程js会对其进行优化(JIT)。过程如下图所示,相关细节可参考后续的链接。

2.2 执行上下文

当js引擎解析可执行代码片段(通常是函数调用)时,会仙做准备工作,这个准备工作就是执行上下文。执行上下文中包括变量环境、词法环境、this、可执行代码、Outer(指向外层执行上下文)。

在js中有三种执行上下文。即:全局执行上下文、函数执行上下文、Eval执行上下文。

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

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

  1. 绑定this
  2. 创建词法环境
  3. 创建变量环境

词法环境:基于ECMAScript代码的词法嵌套结构来定义标识符和具体变量和函数的关联。一个词法环境有环境记录器和一个可能的引用外部词法环境的空值组成

变量环境:变量环境和词法环境的一个不同就是前者被用来存储函数声明和变量(let和const)绑定,而后者用来存储var变量绑定

outer:指向外部变量环境的一个指针

3.JS的进阶知识点

3.1闭包

概念:红宝书:闭包指的是那些引用了另一个函数作用域中变量的函数,通常是在嵌套函数中实现的。

MDN:一个函数和对其周围状态(lexical environment,词法环境)的引用捆绑在一起(或者说函数被引用包围),这样的组合就是闭包(closure)。也就是说,闭包让你可以在一个内层函数中访问到其外层函数的作用域。在 JavaScript 中,每当创建一个函数,闭包就会在函数创建的同时被创建出来。

特点:

  1. 让外部访问函数内部变量成为可能
  2. 局部变量会常驻在内存中
  3. 可以避免使用全局变量,防止全局变量污染
  4. 会造成内存泄漏(有一块内存空间被长期占用,而不被释放)

应用场景:防抖、节流、函数柯里化

3.2 this

  1. 函数外面的this,即全局作用域的this指向window。
  2. 函数里面的this总是指向直接调用者。如果没有直接调用者,隐含的调用者是window。
  3. 使用new调用一个函数,这个函数即为构造函数。构造函数里面的this是和实例对象沟通的桥梁,他指向实例对象。
  4. 箭头函数里面的this在它申明时确定,跟他当前作用域的this一样。
  5. DOM事件回调里面,this指向绑定事件的对象(currentTarget),而不是触发事件的对象(target)。当然这两个可以是一样的。如果回调是箭头函数,请参考上一条,this是它申明时作用域的this。
  6. 严格模式下,函数里面的this指向undefined,函数外面(全局作用域)的this还是指向window。
  7. call和apply可以改变this,这两个方法会立即执行原方法,他们的区别是参数形式不一样。
  8. bind也可以修改this,但是他不会立即执行,而是返回一个修改了this的函数

new实例化对象的过程

1.创建一个新的空对象;
2.将构造函数的作用域赋给新对象,(因此this就指向了这个新对象)
3.执行构造函数中的代码(为这个新对象添加属性)
4.返回新对象

3.3 垃圾回收

标记清理

JavaScript 最常用的垃圾回收策略是标记清理(mark-and-sweep)。

当变量进入上下文,比如在函数内部声明一个变量时,这个变量会被加上存在于上下文中的标记。而在上下文中的变量,逻辑上讲,不应该释放它们的内存,因为只要上下文中的代码在运行,就有可能用到它们。当变量离开上下文时,也会被加上离开上下文的标记。

给变量加标记的方式有很多种。比如,当变量进入上下文时,反转某一位;或者可以维护“在上下文中”和“不在上下文中”两个变量列表,可以把变量从一个列表转移到另一个列表。标记过程的实现并不重要,关键是策略。

垃圾回收程序运行的时候,会标记内存中存储的所有变量(标记方法有很多种)。然后,它会将所有在上下文中的变量,以及被在上下文中的变量引用的变量的标记去掉。在此之后再被加上标记的变量就是待删除的了,原因是任何在上下文中的变量都访问不到它们了。随后垃圾回收程序做一次内存清理,销毁带标记的所有值并收回它们的内存。

引用计数

引用计数的思路是对每个值都记录它被引用的次数。声明变量并给它赋一个引用值时,这个值的引用数为 1。如果同一个值又被赋给另一个变量,那么引用数加 1。

类似地,如果保存对该值引用的变量被其他值给覆盖了,那么引用数减 1。当一个值的引用数为 0 时,就说明没办法再访问到这个值了,因此可以安全地收回其内存了。垃圾回收程序下次运行的时候就会释放引用数为 0 的值的内存。

不能滥用闭包,如果闭包中的对象没有正确清除,对象反复引用,将会导致内存泄漏

课程总结

参考链接

垃圾回收:blog.csdn.net/weixin_4707…

this:blog.csdn.net/qq_41880073…

闭包:://blog.csdn.net/Hunt_bo/article/details/107699137

闭包使用场景:juejin.cn/post/684490…