9/12
JS的数据类型及判断方法
数据类型有基本数据类型和引用数据类型。
基本数据类型有Number、String、Boolean、Null、Undenfied,在ES6+新增了Symbol和BigInt
Symbol:表示独一无二的值,用于解决命名冲突等问题,使用Symbol()函数创建Symbol值,注意Symbol值作为对象属性名时不能使用点运算符
BigInt:解决超出最大整型数据范围 (253-1) 的问题,在数据末尾加n即可。
引用数据类型有对象、数组、函数、Date、RegExp
存储上的区别:基本数据类型存储在栈中;引用数据类型的对象存储在堆中,每个堆有一个引用地址,引用地址存储在栈中。复杂类型的赋值其实是将对象的内存地址赋给另一个变量(两个变量指向堆中的同一个对象)
判断方法
typeof:用于判断Number、String、Boolean、Undenfied、Function。其余均会判断为object。
另外,typeof函数判断用构造函数创建的对象均会返回object。
instanceof:用于判断给定数据是否是由某构造函数创建的,判断的原理是该构造函数的原型是否在该数据的原型链上。
基本数据类型的字面量不可以用其判断,Null和Undefined并不是Object创建出来的,因此返回false
constructor:prototype对象上的属性,指向构造函数,会沿着原型链寻找,因此可以使用num.constructor == Number来判断一个变量是否是Number类型
注:undefined和null会报错
Object.prototype.toString.call()检测对象类型
手写判断方法
function getType(obj){ let type = typeof obj if(type != 'object'){ return type }else{ return Object.prototype.toString.call(obj).slice() } }
宏任务和微任务,promise、async/await
进程与线程
进程:一个在内存中运行的应用程序。
线程:进程中的一个执行任务(控制单元),负责当前进程中程序的执行。
每个进程都有自己独立的一块内存空间,一个进程可以有多个线程,多个线程之间共享数据。
进程是操作系统资源分配的基本单位,而线程是处理器任务调度和执行的基本单位。
JS是单线程的
js是单线程的,这主要和js的用途有关,js是作为浏览器的脚本语言,主要是实现用户与浏览器的交互,以及操作dom;这决定了它只能是单线程,否则会带来很复杂的同步问题。
举个例子:如果js被设计了多线程,如果有一个线程要修改一个dom元素,另一个线程要删除这个dom元素,此时浏览器就会一脸茫然,不知所措。
JS通过任务队列来实现js代码的异步,js引擎会一直等待着任务队列中任务的到来,然后加以处理。
注意:JavaScript 只在一个线程上运行,不代表 JavaScript 引擎只有一个线程。事实上,JavaScript引擎有多个线程,单个脚本只能在一个线程上运行(称为主线程),其他线程都是在后台配合
同步任务和异步任务
同步任务:没有被JS引擎挂起的,在主线程上排队执行的任务。只有在前一个任务执行完才会被执行。
异步任务:被引擎挂起,不进入主线程而进入任务队列的任务。
异步实现
任务队列
JavaScript 运行时,除了一个正在运行的主线程,引擎还提供一个任务队列(task queue),里面是各种需要当前程序处理的异步任务。
首先,主线程会去执行所有的同步任务。等到同步任务全部执行完,就会去看任务队列里面的异步任务。如果满足条件,那么异步任务就重新进入主线程开始执行,这时它就变成同步任务了。等到执行完,下一个异步任务再进入主线程开始执行。一旦任务队列清空,程序就结束执行。
异步任务的写法通常是回调函数。一旦异步任务重新进入主线程,就会执行对应的回调函数。如果一个异步任务没有回调函数,就不会进入任务队列,也就是说,不会重新进入主线程,因为没有用回调函数指定下一步的操作。
事件循环
当同步任务执行完,JavaScript 引擎就会去检查那些挂起来的异步任务,是不是可以进入主线程了。这种循环检查的机制,就叫做事件循环(Event Loop)。
js引擎遇到一个异步事件后并不会一直等待其返回结果,而是会将这个事件挂起,继续执行执行栈中的其他任务。当一个异步事件返回结果后,js会将这个事件加入与当前执行栈不同的另一个队列,我们称之为事件队列。
被放入事件队列不会立刻执行其回调,而是等待当前执行栈中的所有任务都执行完毕, 主线程处于闲置状态时,主线程会去查找事件队列是否有任务。如果有,那么主线程会从中取出排在第一位的事件,并把这个事件对应的回调放入执行栈中,然后执行其中的同步代码…,如此反复,这样就形成了一个无限的循环。
宏任务和微任务
在异步模式下,创建异步任务主要分为宏任务与微任务两种。
ES6 规范中,宏任务(Macrotask) 称为 Task, 微任务(Microtask) 称为 Jobs。
宏任务是由宿主(浏览器、Node)发起的,而微任务由 JS 自身发起。
-
宏任务包括:setTimeout、setInterval、 Ajax 、DOM事件
-
微任务:Promise、 async/await
-
微任务比宏任务的执行时间要早
-
执行顺序:
- 主线程 >> 主线程上创建的微任务 >> 主线程上创建的宏任务
- 主线程 >> 主线程上的宏任务队列1 >> 宏任务队列1中创建的微任务
Promise
Promise是异步编程的一种解决方案,比传统的解决方案(回调函数)更加合理和更加强大
传统的回调函数如果多层嵌套,就会出现回调地狱,使代码的可读性大大降低
而promise可以通过链式操作避免回调地狱,代码的可读性明显增强
- promise有3种状态:进行中,成功,失败
- 对象的状态不受外界影响,只有异步操作的结果,可以决定当前是哪一种状态
- 一旦状态改变(从pending变为fulfilled和从pending变为rejected),就不会再变,任何时候都可以得到这个结果
通过Promise构造函数创建promise对象
Promise((resolve,reject)=>{})需要传递一个函数作为参数,而函数中有两个参数resolve,reject,它们也是函数
有异步操作时使用promise对这个异步操作进行封装
new Promise时会立刻执行传入的函数
promise有then(),catch(),finally()函数,当异步操作成功时执行then,异步操作失败时执行catch,
then(()=>{},()=>{})可以直接传入两个函数,分别对应成功时和失败时的回调函数
链式调用
return new Promise((resolve)=>{resolve(res)}) 简写return Promise.resolve(res) 简写为return res
return new Promise((reject)=>{reject(err)}) 简写return Promise.reject(err) 简写为throw err
all()
Promise.all()方法用于将多个 Promise实例,包装成一个新的 Promise实例
const p = Promise.all([p1, p2, p3]);
p1,p2,p3都fulfilled, 则p会fulfilled,p1p2p3的返回值组成一个数组传递给p的回调函数
只要有一个rejected,p就会rejected,率先rejected的 Promise 实例的返回值则传递给p的回调函数
手写实现
function promiseAll(promises) {
let len = promises.length
let index = 0
let data = []
return new Promise((resolve, reject) => {
for (let i in promises) {
promises[i].then(res => {
index++
data[i] = res
if (index == len) {
resolve(data)
}
}).catch(err => {
reject(err)
})
}
})
}
race()
Promise.race()方法也是用于将多个 Promise实例,包装成一个新的 Promise实例
const p = Promise.race([p1, p2, p3]);
只要有一个实例率先改变状态,p的状态就跟着改变
率先改变的 Promise 实例的返回值则传递给p的回调函数
手写实现
function PromiseRace(promises) {
return new Promise((resolve, reject) => {
for (let i in promises) {
promises[i].then(res => {
resolve(res)
}).catch(err => {
reject(err)
})
}
})
}
async
async是一个加在函数前的修饰符,被async定义的函数会默认返回一个Promise对象resolve的值。因此对async函数可以直接then,返回值就是then方法传入的函数。
await
await 也是一个修饰符,只能放在async定义的函数内。可以理解为等待。
await 修饰的如果是Promise对象:可以获取Promise中返回的内容(resolve或reject的参数),且取到值后语句才会往下执行;
如果不是Promise对象:把这个非promise的东西当做await表达式的结果。
Vue的生命周期
vue实例从创建到销毁的全过程,从开始创建、初始化数据、编译模版、挂载Dom -> 渲染、更新 -> 渲染、卸载 等⼀系列过程,称为Vue的⽣命周期。
钩子函数是生命周期中每个阶段对外开放让程序员操作vue的接口。
常见的钩子函数有8个
BeforeCreate
实例被完成创建出来,el和data都没有初始化,不能访问data、method,一般在这个阶段不进行操作。
Created
vue实例中的data、method已被初始化,属性也被绑定,但是此时还是虚拟dom,真是dom还没生成,$el 还不可用。这个时候可以调用data和method的数据及方法,created钩子函数是最早可以调用data和method的,故一般在此对数据进行初始化。
BeforeMount
此时模板已经编译完成,但还没有被渲染至页面中(即为虚拟dom加载为真实dom),此时el存在则会显示el。在这里可以在渲染前最后一次更改数据的机会,不会触发其他的钩子函数,一般可以在这里做初始数据的获取。
Mounted
此时模板已经被渲染成真实DOM,用户已经可以看到渲染完成的页面,页面的数据也是通过双向绑定显示data中的数据。 这实例创建期间的最后一个生命周期函数,当执行完 mounted 就表示,实例已经被完全创建好了。
BeforeUpdate
更新前状态(view层的数据变化前,不是data中的数据改变前),重新渲染之前触发,然后vue的虚拟dom机制会重新构建虚拟dom与上一次的虚拟dom树利用diff算法进行对比之后重新渲染。只有view上面的数据变化才会触发beforeUpdate和updated,仅属于data中的数据改变并不能触发。
Updated
数据已经更改完成,dom也重新render完成。
BeforeDestroy
销毁前执行($destroy方法被调用的时候就会执行),一般在这里善后:清除计时器、清除非指令绑定的事件等等…
Destroyed
销毁后 (Dom元素存在,只是不再受vue控制),卸载watcher,事件监听,子组件。
当keep-alive 缓存组件才会有的生命周期的钩子函数
activated
deactivated
9/13
Webpack、Rollup、Vite
Webpack
一个强力的模块化打包器,解决了构造复杂单页面应用的问题。
- 模块化:将实现不同功能的代码划分为不同的模块,以便代码的复用与管理。
- 打包:将各种资源合并在一起以便它们可以在同一个文件请求中发回给客户。
原理:根据文件间的依赖关系对其进行静态分析,然后将这些模块按指定规则生成静态资源,当 webpack 处理程序时,它会递归地构建一个依赖关系图,其中包含应用程序需要的每个模块,然后将所有这些模块打包成一个或多个 bundle。
webpack有两种组织模块的依赖方式——同步、异步。异步依赖将作为分割点,形成一个新的块;在优化了依赖树之后,每一个异步区块都将作为一个文件被打包。
特点:
- 代码分割:代码分割成模块,可按需加载(懒加载),减少初次加载时间。
- 插件(Plugin):有丰富的插件接口,可以对功能进行扩展。
- 加载器(Loader):通过加载器机制支持对文件的预处理。
- 智能解析:可以处理第三方库,无论它们的模块形式是 CommonJS、 AMD 还是普通的 JS 文件。
Webpack热更新HMR
在应用程序运行过程中,替换、添加、删除模块,而无需重新刷新整个应用
热更新主要可以通过以下几种方式来显著加快发速度:
- 保留在完全重新加载页面时丢失的应用程序的状态。
- 只更新改变的内容,以节省开发时间。
- 调整样式更加快速,几乎等同于就在浏览器调试器中更改样式。
原理:
webpack-dev-server 创建两个服务器:提供静态资源的服务(express)和Socket服务
- express server 负责直接提供静态资源的服务(打包后的资源直接被浏览器请求和解析)
- socket server 是一个 websocket 的长连接,双方可以通信
- 当 socket server 监听到对应的模块发生变化时,会生成.json和.js文件
- 通过长连接,socket server 可以直接将这两个文件主动发送给客户端(浏览器)
- 浏览器拿到两个新的文件后,通过HMR runtime机制,加载这两个文件,并且针对修改的模块进行更新
Rollup
开发者可以在你的应用或库中使用ES2015模块,然后高效地将它们打包成一个单一文件用于浏览器和Node.js使用。
Rollup最令人激动的地方,就是能让打包文件体积很小,相比其他JavaScript打包工具,Rollup总能打出更小,更快的包。
Rollup基于ES2015模块,比Webpack和Browserify使用的CommonJS模块机制更高效。
特点:
- Tree-shaking:Rollup通过对代码的静态分析,分析出冗余代码,在最终的打包文件中将这些冗余代码删除掉,进一步缩小代码体积。
- ES2015模块打包支持:直接不需要通过babel将import转化成Commonjs的require方式,极大地利用ES2015模块的优势。
Vite
两大组成部分:
- No-bundle开发服务,源文件无需打包
- 生产环境基于Rollup 的 Bundler
核心特征:
- 高性能,dev启动速度和热更新速度非常快
- 简单易用,开发者体验好
当下问题(Webpack、Rollup)
- 缓慢的启动 -> 项目编译等待成本高
- 缓慢的热更新 -> 修改代码后不能实时更新
瓶颈在哪里?
bundle带来的性能开销
JavaScript语言的性能瓶颈
两大行业趋势
-
全球浏览器对原生ESM的普遍支持(目前占比92%以上)
-
两大要素:
- script标签增加type = "module"属性
- 使用ESM模块导入导出语法
-
基于原生ESM的开发服务优势:
- 无需打包项目源代码
- 天然的按需加载
- 可以利用文件级的浏览器缓存
-
-
基于原生语言(Go、Rust)编写前端编译工具链
-
基于Esbuild的编译性能优化
-
Esbuild——基于Golang开发的前端工具,具备如下能力:
- 打包器Bundler
- 编译器Transformer
- 压缩器Minifier
-
-
Vite内置的Web构建能力:Vite开箱即用的功能等价于webpack、webpack-dev-server、css-loader、style-loader、less-loader、sass-loader、postcss-loader、file-loader、MiniCssExtractPlugin、HTMLWebpackPlugin......
var和const、let的区别
1.块级作用域
const和let具有块级作用域;而var没有
2.变量提升
var具有变量提升;而const和let没有
3.重复声明
var可以重复声明变量;而let和const不可以
4.初始值设置
const必须设置初始值;而let和var不用
5.全局属性
var声明的变量为全局变量,会将该变量添加为全局对象的属性;而let和const不会
变量提升与函数变量提升
1.变量提升:
- 使用var关键字声明的变量,会在所有代码执行前被声明(但不会赋值)
- 如果不使用var关键字,则变量不会被声明提前
2.函数提升:
- 使用函数声明function a(){} 创建的函数,可以在创建前使用(在所有代码创建前被创建)
- 使用函数表达式var a = function(){} 创建的函数会被变量提升而非函数提升,因此不能在声明前使用
先执行变量提升,再执行函数提升
9/18
对MVVM的理解
数据层——视图层——数据视图层的响应式框架:响应式+双向数据绑定
- 修改View层,Model层数据发生改变
- Model数据发生改变,不需要查找DOM,直接修改View
- M、V都与VM层双向数据绑定,而M层与V层没有直接的交互,实现view和model的解耦
CSS水平居中和垂直居中
水平居中
margin:auto
对于width不确定的元素也可用
position
子元素开启绝对定位,设置left为50%,marfin-left为自身width的一半或transform:translateX(50%)
text-align:center
需要在行内块元素上使用,而盒子是块级元素,所以需要将盒子转换为行内块元素
display:flex
父元素开启flex布局
display:table
配合margin:auto使用
垂直居中
position
子元素开启绝对定位,设置top为50%,marfin-top为自身width的一半或transform:translateY(50%)
vertical-align:center
需要在行内块元素上使用,而盒子是块级元素,所以需要将盒子转换为行内块元素
display:flex
父元素开启flex布局,子元素align-self:center
display:table
父元素display:table,子元素display:table-ceil,vertical-align:center
line-height
9/19
编程范式
面向过程——过程化/命令式编程
面向对象
函数式编程范式
- 函数是 “第一等公民”:函数与其他数据类型一样,可以赋值给其它变量,也可以作为参数,也可以作为返回值
- 函数式编程会使用较多的 闭包 和 高阶函数
高阶函数:函数作为参数或者返回值的函数
闭包
闭包的本质:函数执行时,会被放到执行栈上,函数执行完毕后,会从执行栈上删除,但是堆上作用域成员因为被外部引用而不能被释放,因此内部函数依然可以访问到作用域的成员。
柯里化——把多元函数转化成 一元函数
当一个函数有多个参数的时候,先传递一部分参数调用它(这部分参数以后永远不变),然后返回一个新的函数接受剩余的参数,直到参数接收完毕,才返回结果