type
status
date
slug
summary
tags
category
icon
password
1 数据类型与数值部分2 作用域1.变量作用域分类2.执行环境 Execution Environment3.作用域链4.闭包 closures5.原型链 prototype chains6.this指向问题3 事件循环event loop1.并发模型runtime module2.渲染主线程的工作方式3.异步4.任务队列的分类与优先级5.计时器4 浏览器的渲染原理1.渲染流程2.相关名词5 属性描述符6 函数1.构造函数和原型链2.惰性函数7 类 class1.类的声明和定义2.类上的访问器属性3.属性表达式4.静态成员4.私有成员5.继承与派生
1 数据类型与数值部分
JS对象中的每个属性,都可以按这三个因素进行分类:
1. 可枚举 和 不可枚举
2. string 或者是 symbol
3. 自有属性 或者是 从原型链中继承的属性
- Null和Undefined的区别:
转化为数值的时候,null ⇒ 0 ,而undefined ⇒ NaN
最初设计的时候只有null没有undefined,但是null属于
Object
,相当于是一个空对象,它的自动转化使得编程中的错误变的不太好识别,所以加入了新的原始值undefined- ⚠️
typeof NaN
为number
,NaN表示“非数字”(Not a Number),属于是一个特殊数值,而不是一个数据类型,不等于任何值,包括他本身。所以使用NaN !== NaN
这个特性来判断一个变量是否是NaN,而不是使用typeof会比较合适,判断一个值是否是NaN的方法总共有: x !== x
Number.isNaN()
仅当值为NaN的时候才会成立isNaN()
用于检测当前值为NaN,或者是一个值被转换为数字之后是NaN
2 作用域
什么是作用域?作用域(Scope)是一个范围、区域或者是空间
1.变量作用域分类
2.执行环境 Execution Environment
什么是执行环境:
执行环境定义了变量或函数有权访问的其他数据。存储在执行环境栈中,也叫做执行栈(Execution Context Stack)或者上下文环境栈或者执行上下文
- 在浏览器中,全局对象window所处的环境➡️全局环境是最外层的执行环境,最先入栈,只有在关闭浏览器的时候才会销毁
- 当函数被调用的时候,函数会被放入一个环境栈(也叫函数调用栈)入栈,指向完毕后弹出栈
- 该环境栈包含此函数自己的执行环境以及他的依赖成员
- 每个执行环境都有一个表示变量的对象 ➡️ 变量对象,这个对象里储存着在当前环境中所有的变量和函数。 全局执行上下文中的变量对象就是全局对象 详细的就不讲了,以后再更新。。。
3.作用域链
什么是作用域链:
当一个变量被调用,那么变量被调用时所处的局部作用域和全局作用域之间,由多个执行上下文中的变量对象构成的链表就叫做作用域链
- 在JS中,全局作用域是整个作用域的终点
- 注意,作用域链之间的链接都是引用关系,而不是包含关系
- 🔴 函数的作用域在函数定义的时候就决定了,这是因为函数有一个内部属性 [[scope]],当函数创建的时候,就会保存所有父变量对象到其中,你可以理解 [[scope]] 就是所有父变量对象的层级链,但是注意:[[scope]] 并不代表完整的作用域链
- 形式为:
⁉️ 作用域链的作用:
- 在函数执行时确定变量的作用域,它决定了在当前执行环境中变量的查找顺序 ➡️ 无论函数什么时候调用,函数内部查找变量的顺序都是不变的
⁉️ 什么是词法作用域(Lexical Scoping)?
- 解释:在JS引擎编译代码时,会根据变量声明的位置将变量绑定到相应的作用域中,这是一种在编译阶段确定变量作用域的规则,也可以认为词法作用域就是定义在词法阶段的作用域。词法作用域是一种静态的作用域,只由变量在代码中所处的位置来决定,而不由调用时的位置决定
- 词法作用域发生在编译阶段的第一个步骤➡️分词/词法分析(Tokenizing/Lexing)中,它有两种可能,
分词
和词法分析
,分词是无状态的,而词法分析是有状态的。一条不用被赋予语义的代码会进行分词,一条代码如果词法单元生成器无法判断是否是个独立的单元,就会进行词法分析,比如var a = 1
⬆️ myName变量的词法作用域由变量定义时候的作用域确定,因此是全局作用域,而不是
getName()
的局部作用域⬆️ 1处的变量定义在局部作用域displayName中,2处name的定义是在局部作用域makeFunc中,并且所处词法作用域不同,外部函数是不能访问内部函数中的变量的,所以输出不同
4.闭包 closures
看一个使用属性修饰符实现的闭包计时器的例子:
在控制台查看
a
的结构:(对象的原型上会自动使用getter和setter,如果我们有自己定义,就会执行自定义的部分)什么是闭包?
- 从书写形式上看可以理解为是:
一个函数可以访问其此法作用域之外的变量
➡️ 从中得到几个关键信息:1️⃣闭包的存在至少有两个函数 2️⃣其中一个函数所需要的变量,并不存在于自己本身的函数作用域中,而是对外部变量的引用
- 从原理上理解: 闭包是依据词法作用域产生的必然结果,根源上起源于词法阶段,是通过变相引用函数的活动对象,导致这个活动对象不能被回收,形成了即使这个引用函数已经执行结束,但是依然可以用【引用】来访问其作用域链的结果,也就是引用的活动对象并不会被销毁
⬆️ 这个函数并不完全是闭包的原因:
- 在函数
fun
中可以访问其外部函数foo
中的变量a,但是fun
并没有在其词法作用域之外被调用或者被返回(fun所处的词法作用域在于定义这个函数的地方,也就是foo函数的作用域之内),也就是说fun
函数的执行环境的释放会在foo
函数之前,所以这个活动对象a
其实并没有被缓存,foo
函数执行完之后就已经释放了,所以不符合严格的闭包定义。
⬇️ 以下是一个经典的闭包示例,
fun
函数被保存在变量funClosure
中,并在foo
函数之外被调用,所以即使foo
已经执行完毕,仍然可以访问foo
函数中的变量a
。为什么要使用闭包(GPT的回复)?
- 保存状态:闭包允许函数捕获并保存其所在作用域的状态。这意味着即使函数执行完成后,闭包仍然可以访问并修改其创建时所捕获的变量。这种机制使得闭包在需要记住某些状态或者在之后使用这些状态时非常有用
- 数据隐藏:通过使用闭包,可以隐藏函数内部的数据。只有在内部函数中才能访问这些数据,这样可以防止外部代码直接修改函数的状态,从而提高了代码的安全性和可靠性
- 简化代码:闭包可以帮助简化代码结构,特别是在涉及到回调函数或者事件处理器的情况下。通过使用闭包,可以避免传递大量的参数,并且可以更方便地管理函数之间的依赖关系
- 函数式编程:在函数式编程中,闭包是一种非常重要的概念。它允许函数作为一等公民进行操作,可以被传递给其他函数,或者从其他函数中返回。这种灵活性使得函数式编程风格更加强大和易于实现。
5.原型链 prototype chains
注意和作用域链用来确认变量的查找顺序不同,原型链是一个完全不同的概念,它的关注的是JS对象(广义的对象,JS中一切皆对象)上的属性或者方法
🔴 在某些情况下,两者会有使用上的关联:
例如在函数中访问某个对象的属性时,JS引擎会首先在当前函数的作用域链中查找该属性,如果找不到,它会沿着原型链向上查找
⁉️ 什么是原型链:
- 原型是一个对象,它包含了一些属性和方法,可以被其他对象继承
- 原型链是JS中实现继承的机制之一。每个对象都有一个指向其原型对象的内部链接,这个链接被称为原型链。如果在当前对象上找不到某个属性或方法,JS引擎会沿着原型链向上查找,直到找到该属性或方法或者到达原型链的顶端(Object.prototype),如果找不到,就会返回undefined
- 当创建一个新对象时,新对象会自动继承它的原型对象的属性和方法
- JS中
Function
和Object
是互指的关系,互为对方的实例,原型链的终点是Object.prototype.__proto__
指向null
⁉️ 原型链的作用:
- 原型链的作用是实现对象之间的继承关系,允许对象共享属性和方法,读取属性的时候会沿着对象的隐式原型链
__proto__
来进行查找,而存属性的时候会放到显式原型链上
🔴 原型链的隐患:
- 避免在原型对象中添加可变的数据类型,比如数组或者对象,一旦修改了原型对象中的可变数据类型,那么这个原型的所有实例都会受到影响
- 避免直接使用
__proto__
访问对象的原型,因为不同浏览器的实现可能会不同,使用Object.getPrototypeOf
方法来访问对象的原型对象
6.this指向问题
🔴 this的隐式丢失情况有四种,前提是函数在对象中属于引用调用(奇怪的写法):
- 函数名是别名
- 函数作为参数传递
- 出现在内置函数中 比如定时器中传入了这个函数
- 函数的赋值
🔴 词法绑定的经典例子就是箭头函数的this绑定
- 词法绑定是指在函数定义的时候就确定this的值,与函数的调用方式无关,是一种静态的绑定,绑定之后在整个生命周期中都不会再更改
- 隐式绑定是指在函数调用的时候,根据调用者确定this的值,是一种动态的绑定方式
- 在事件监听器、回调函数中,this通常指向监听该事件的DOM元素,属于是隐式绑定(如果内部包含普通函数,普通函数自执行之后,内部this还是指向window)
- 显示绑定中,
call
和apply
方法会立即执行函数,bind
方法不会立即执行,返回的是一个绑定了指定this值的新函数
🔴 优先级为:显式绑定 > 构造调用绑定 > 隐式绑定 > 默认绑定
🔴 在编码中改变this指向的方法:
- 箭头函数
- 在函数内部使用
_this = this
- 使用显式绑定
- 使用new实例化一个对象
3 事件循环event loop
JS作为浏览器脚本语言,因为浏览器的工作原理需要,设计为单线程语言,虽现今有多线程的出现,但是也不是真正实现的多线程。因而解析JS的核心原理也和浏览器(或node)有着密不可分的关系。
理解:事件循环是JS为了解决单线程代码执行不阻塞主进程而使用的机制,这种事件循环机制是由JS的宿主环境来实现的,宿主环境分为浏览器环境和node环境。
在浏览器环境中,事件循环也叫消息循环,是浏览器的渲染主线程的工作方式。
事件循环主要负责执行代码,收集和处理事件,执行任务队列(Task Queue)中的子任务。
运行机制:在执行事件循环的时候会开启一个死循环,每次循环从任务队列中取出第一个可执行的任务执行,其他的线程可以随时向任务队列添加任务,新任务会添加到任务队列的末尾
1.并发模型runtime module
程序运行的基础概念有:进程、线程、堆、栈、队列。
浏览器是一个多进程多线程的应用程序,其中比较常见的几个进程有:
- 浏览器进程:主要负责界面的显示、用户交互、子进程管理等
- CPU进程:处理网页中的图像和视频
- 插件进程:负责浏览器中的插件
- 🌟渲染进程:负责网页(具体界面)的呈现和交互
渲染进程启动之后,会开启一个渲染主线程和若干个其他线程,渲染主线程负责执行HTML、CSS、JS代码,默认情况下是一个页签会开启一个新的渲染进程,保证数据的独立性。
2.渲染主线程的工作方式
也就是上面的运行机制
3.异步
在代码执行的过程中,会遇到一些无法立刻处理的任务,比如
- 计时完成之后才能执行的任务 — setTimeout、setTnterval
- 网络通信完成后需要执行的任务 — XHR、Fetch
- 用户操作之后需要执行的问题 — addEventListener
渲染主线程要执行的任务非常多,为了不让渲染主线程因为等待这些任务的执行而进入阻塞状态,浏览器选择使用异步来解决这个问题,异步模式会使浏览器永不阻塞,最大限度的保证了单线程的流畅运行。
具体做法:
当某些需要等待的任务发生的时候,渲染主线程会将这些任务交给其他线程去处理,自身结束当前等待任务,继续执行之后的任务。当其他线程完成的时候,会将事先传递的回调函数包装成任务,加入到任务队列中,等待渲染主线程的调度。
🔴在渲染主线程上的同步任务全部执行完成之后,系统才会读取任务队列中的任务。
JS会阻碍渲染的原因:
JS代码的执行和浏览器的渲染都是在渲染主线程上进行的,而渲染主线程只有一个,只能一个个的执行任务。
4.任务队列的分类与优先级
任务没有优先级,在任务队列中按照队列的规则先进先出。
任务队列有优先级:
顺序:进入整体代码(宏任务main script)—> 开始首次事件循环 —>执行主线程上的同步任务 —>执行结束 —> 检测微任务队列 —>检测宏任务队列
按照以往说法,异步任务根据事件分为:
- 宏任务(MacroTask):
- 由浏览器发起的、在事件循环中执行的异步任务
- setTimeout
- setInterval
- setImmediate(Node.js)
- I/O(Mouse Events、Keyboard Events、Network Events)
- UI Rendering(HTML Parsing)、MessageChannel
- 微任务(MicroTask):
- 由 JavaScript 引擎发起、在当前宏任务执行结束后立即执行的异步任务
- Promise().then
- process.nextTick(Node.js)
- MutaionObserver()
当当前执行栈(Execution Context Stack)执行完毕之后,会优先处理所有微任务队列中的任务,然后再去宏任务中取出一个事件,同一次事件循环中,微任务永远在宏任务之前执行。
🔴 nextTick的优先级高于Promise.then
🔴 Promise的创建(非.then())属于同步代码,立即执行
⚠️ 主线程只取不放
⚠️ 主线程每执行完一个任务就会出栈一个任务,下一个任务再入栈
- 每一个任务都有一个任务类型,同一类型的任务必须在同一个队列,不同类型的任务可以处于不同的队列,在一次事件循环中,浏览器可以根据实际情况从不同的队列中取出任务执行。比如在有的浏览器中会认为用户的点击是最高优先级任务
- 浏览器必须准备好一个微队列,微队列中的任务优先其他任务执行(随着浏览器复杂度的急剧提升,不再使用宏队列的说法)
- 微队列中主要存放Promise和MutaionObserver,例如
- 在目前的Chrome的实现中,至少包含以下队列
- 延时队列:计时器的回调,优先级中
- 交互队列:用户的操作,优先级高
- 微队列:需要最快执行的任务,优先级最高
5.计时器
计时器的第二个参数延迟时间,表示的是将消息推入任务队列的最小延时时间,而不是保证执行时间,执行时间取决于任务队列中等待的任务数量
4 浏览器的渲染原理
什么是浏览器的渲染?
是浏览器将HTML字符串转换为界面上的像素信息。
整个渲染流程分为多个阶段,分别是解析HTML、样式计算、布局、分层、绘制、分块、光栅化、画
每个阶段都有明确的输入输出,上一个阶段的输入会成为下一个阶段的输出。
1.渲染流程
1.解析HTML
HTML字符串不适合浏览器进行后续复杂操作,所以浏览器会将其先解析为DOM树和CSSOM(CSS Object Module)树,将其变为对象结构之后更便于操作,并且会给JS开放操作这两个对象的入口。
- HTML的不同类型的样式表(内部样式表、外部样式表、内联样式表、浏览器默认样式表)会被解析成多个CSSStyleSheets对象,
- 通过以下代码可以在内部样式表,或外部样式表中添加新的CSS规则
解析流程图如下,解析完成后得到DOM树和CSSOM树:
CSS的渲染不会阻塞HTML的解析是因为: CSS的下载和解析式处于另一个线程,预解析线程上的
JS记载的时候会阻塞HTML解析是因为: JS可能会修改当前的DOM树
2 样式计算
解析BOM树,为它的每个节点计算最终样式
3 布局
遍历所有的BOM节点,计算出每个节点的几何信息,比如宽高、相对包含块的位置,生成layout tree布局树
4 分层
对布局树进行分层,可以提高渲染的效率,减小出错的影响范围
5 绘制
渲染主线程为每一个分层产生一个单独的绘制指令(Paint)集合
6 分块
7 光栅化
在代码转换为图像的过程(把每个像素点转换为位图)中,也会用到其他的进程,比如渲染主线程和合成线程(其他线程)并不负责光栅化的操作,而是由GPU进程来完成。
合成线程处于渲染进程中,是无法直接调用操作系统的接口的,所以需要交给GPU进行中转输出
8 画
在
画
阶段会处理一些图形的变换效果,比如transform,此过程是发生在合成线程的,所以不会影响到渲染主线程的解析效率2.相关名词
- reflow 回流:
- 当进行了影响布局树的操作之后,会引发layout树的重新计算,这个过程就叫回流。
- 当设置一系列回流指令的时候,这些任务是异步任务,当设置指令需要读取的时候,这个任务就会变成一个同步任务
- repanit 重绘:
- 除了布局之外的样式发生了变化,根据改动的分层信息重新计算了绘制指令,reflow 一定会引起repanit
- Critical Rendering Path 关键渲染路径:根据这些渲染的知识,可以设计方案,提升浏览器的加载性能
5 属性描述符
es5出的JS标准内置对象Object.defineProperty()可以方便用户自定义封装自己的功能代码,对象存在的属性描述符主要有两种(这两种都是对象):
- 访问器描述符: getter/setter
⚠️ 描述符只能是这两者之一,而不能同时为两者,他们共享以下可选键:
注意:
- 可以在getter/setter自定义属性的范围、类型、运算规则等,遇到不符合的可以自定义抛出错误提示
- 不能在getter/setter中调用自己,会引起无限递归
- 可以使用
(Object.freeze || Object)(Object.prototype)
来防止添加或者删除对象原型属性(如果freeze可用)
JS的内置对象中也有很多地方都用到了
defineProperty()
,比如Object.assign(),在第二个参数source上使用的是[[Get]],然后[[Set]]到第一个参数target上。🔴👉🏻 基于此知识点,可以实现vue2的响应式原理
6 函数
JS实现oop的方式主要是通过构造函数(constructor)和原型链(prototype chains)实现,ES6引入了类(class)的概念之后,使用class的写法会更佳的清晰,可以看做是构造函数的另一种写法
1.构造函数和原型链
构造函数的返回值的注意点:
- 默认的返回值是➡️新创建的实例对象
- 如果手动添加了返回值(
return
语句): - 返回值是基本数据类型的话,真正的返回值还是那个新创建的实例对象
- 返回值是复杂数据类型(对象)的话,真正的返回值是这个对象
示例:
2.惰性函数
7 类 class
ES6引入的新概念,使得JS中的OOP更加清晰,关键概念包含类的声明和定义,类的
constructor
方法,类的静态成员以及类的私有成员,类的继承方式1.类的声明和定义
ES5中使用构造函数创建类的方法:
ES6中使用class的方法:
- 私有属性是实例中的属性,不会出现在原型上(除非显示的定义在其本身
this
上,否则都是定义在原型上,即定义在class上,与ES5保持一致),且只能在类的构造函数或方法中创建,在类的构造函数中创建私有变量可以方便统一管理
- 因为类的方法都定义在
prototype
上面,所以可以通过Object.assign()
方法一次向类添加多个方法
- 生产环境中,也可以使用
Object.getPrototypeOf()
方法来获取实例对象的原型,然后再向其上添加方法/属性,注意不建议使用__proto__
属性改写原型
🔴 注意:
- 类声明不能提升
- 类声明中的代码自动强行运行在严格模式下
- 类中的所有方法都是不可枚举的(ES5中是可枚举的),类中的所有方法都定义在类的
prototype
属性上面
- 每个类都有一个
[[constructor]]
方法
- 只能使用
new
来调用类的构造函数,直接调用就会报错
- 不能在类中修改类名
表现形式为声明式和表达式
2.类上的访问器属性
类支持在原型上定义访问器属性,如果有自定义的访问器,运行时就会执行自定义的访问器
3.属性表达式
类的属性名可以用表达式
4.静态成员
使用
static
声明的成员就是静态成员,静态成员在OOP中通常是指定义在类本身上,与类的实例无关,包含:
静态属性
静态方法
这两者都是可以被子类继承的静态属性:
- 实例成员不能访问类上的静态属性,只能通过类名直接访问
- 不存在变量提升
- ES6规定
Class
内部只有静态方法,没有静态属性
- 静态属性是通过浅拷贝实现继承的
- 为了避免在构造函数中进行静态属性的初始化之后,导致每次新建实例都会运行一次的问题,ES2022引入了静态块static block,在类生成时运行且只运行一次,主要作用是对静态属性进行初始化,还有一个作用就是私有属性与类的外部代码分享
静态方法:
- 如果类的静态方法中包含
this
关键字,则this
指向这个类本身
4.私有成员
早期私有成员的定义实现:
在ES2022发布正式的标准之前,一般是使用
Symbol
、闭包、特殊命名例如下划线开头的命名的方式来实现类的私有成员的定义⬆️ 以上方法虽然可以修改
privateProperty
的值,但是因为privateProperty
是定义在类中的变量,会被所有的实例共享,会导致任何一个实例修改了值之后,其他的实例获取值都会出错的情况,所以这个修改方法应该是跟类的实例相关的方法,而不是仅仅为类本身相关的功能(静态方法)。使用以下方法来修改,在类的构造函数中,使用this
将这个方法绑定到类的实例上私有属性和私有方法
- ⚠️ 类的私有属性只能在类的内部使用,在外部使用会报错(使用
#xValue
和使用xValue
是两个不一样的变量,在示例上使用#xValue
会报错,使用xValue
则是找不到会打印undefined)。
- 在内外部访问一个不存在的私有属性会报错,而不是返回
undefined
,
- 私有成员不会被继承,如果在类中定义了对私有属性的读写方法,那么子类可以访问到
- 要么在定义的时候进行初始化,要么在
constructor
方法里进行初始化
使用
#
来定义私有属性的示例如下,以下例子中类有一个私有变量#xValue
和使用访问器定义读取的私有属性#x
,外部直接访问a.#xValue
和a.#x
这两个都会报错- 使用
in
运算符可以判断某个对象是否有含有某个私有属性#brand
,返回一个布尔值,只能在类的内部这样判断
5.继承与派生
在 JavaScript 中,如果一个子类继承了父类的各种属性,并且在子类中使用了
constructor
方法,通常来说是必须使用super
函数来执行父类的构造函数,以确保父类中的初始化工作得以完成关于
super
的几个注意点:super
作为函数调用的时候表示父类的构造函数,用来新建一个父类的实例对象- 在构造函数中访问
this
之前要先调用super()
,负责初始化this
,不调用就没有自己的this
super
代表父类,但是返回子类的this
,所以super
内部的this
表示子类的实例,super()
相当于A.prototype.constructor.call(this)
(在子类的this
上运行父类的构造函数)- 只能在子类的构造函数中使用
- super作为对象的时候:
- 在普通方法中,指向父类的原型对象 ➡️ 因为是指向父类原型,所以取不到
constructor
方法里的属性和方法(构造函数里的属性和方法是绑定在实例上的) - 在普通方法中,通过
super
调用父类的方法的时候,方法内部如果有this,指向的是当前所在子类的实例,同理,如果是通过super
进行赋值,也是给当前子类的实例属性赋值
- 在静态方法中,指向父类
- 只能在子类中使用,没有显示调用的时候,JS引擎是会默认给子类提供一个空的构造函数的,可以省略
在子类中可以通过重新定义父类中相同名字的方法,来重写父类的方法
- 作者:NotionNext
- 链接:https://mia.missz.top//article/0c67b255-53d2-42f5-b1e7-2ea4907e20ab
- 声明:本文采用 CC BY-NC-SA 4.0 许可协议,转载请注明出处。
相关文章