JS
原型和原型链
- 原型:在js内部是使用构造函数来创建对象的,每个构造函数内部都有一个prototype属性,这个属性是一个对象,此对象包含了实例共享的属性和方法,实际上这个对象就是由调用构造函数创建的对象的原型。在现代浏览器中,实例可以调用
__proto__或 Object.getPrototypeOf()获取原型 - 原型链:当访问一个对象的属性的时候,如果该对象不存在这个属性,那么就会去该对象的原型对象上去找,该原型对象又有自己的原型对象,就一直找下去,这就是原型链。原型链的终点是
Object.prototype.__proto__ === null。
- 函数对象的 prototype 指向的对象便是原型对象,该原型对象的 constructor 属性又指向该函数
- 只有函数对象和Object才有原型,实例化后的对象都是没有原型对象的
- 所有的对象实例都有一个私有属性
__proto__ - 由函数对象生成的实例的
__proto__属性指向函数对象的原型对象 - 由Object对象生成的实例的
__proto__属性指向父对象 - 最终所有的对象
__proto__属性都指向 Object.prototype - Object.prototype.
__proto__= null
new
- 先创建一个空对象 {}
- 将空对象的
__proto__指向函数对象的原型,并执行函数对象的构造函数 - 若函数对象的构造函数返回了一个新的对象,那么将 this 指向这个新的对象并返回
- 若构造函数没有返回值,那么将 this 指向第一步创建的空对象
面向对象
枚举对象的属性
- for...in用来遍历对象及其原型链上的所有可枚举属性
- Object.keys可以用来遍历对象上所有可枚举的属性,但不包含原型链上的
- Object.getOwnPropertyNames返回对象自身包含的所有属性(包含不可枚举的)名称数组,但不包含原型链上的
创建对象的方式
- 直接使用大括号创建对象
- 使用构造函数创建对象
- 使用new Object创建对象
- 使用Object.create创建对象
维度 直接用大括号(对象字面量) 使用构造函数 使用 new Object 使用 Object.create 语法简洁性 最高(一行完成定义) 中等(需先定义构造函数) 较低(需分步添加属性) 中等(需指定原型) 原型指向 自动指向 Object.prototype 指向构造函数的 prototype 自动指向 Object.prototype 显式指定原型(可传 null) 属性共享能力 无(属性均为实例自身) 可通过原型共享方法 无(属性均为实例自身) 可继承原型对象的属性 / 方法 适用场景 单例对象、简单数据结构 批量创建同类型对象 动态添加属性的场景 原型继承、定制原型链 内存效率 一般(无共享方法) 较高(原型方法共享) 一般(无共享方法) 较高(继承原型属性) 对象的继承
- 原型链继承:重写子对象的原型对象,将其指向父对象实例,缺点是子对象实例修改原型链方法时会影响所有子对象
- 构造函数继承:在子对象的构造函数中调用父对象的构造函数,并将this传入父对象,缺点是无法继承父对象原型上的属性和方法
- 组合继承:即使用原型链去继承父对象原型上的属性方法,构造函数继承父对象自己的属性和方法,本质上是在原型链继承的基础上将父对象属性方法复制了一份到子对象实例中
- ES6 extends继承:目前最常用的继承方式,原理在ES6 Class中介绍
变量作用域
- JS是静态作用域,即函数中变量作用域在程序运行前便已经确定
- 函数和变量的声明都会被提前,当同名时函数的优先级更高
- 块级作用域内声明的变量只能在块内生效,即 let 和 const 的声明
- 块级作用域内不允许重复声明变量,但也会存在声明提前,形成暂时性死区
- 全局声明的var会影响全局变量,但是let 和 const不会
- 一个块内调用的变量如果不存在,便会往上一个块中去找,从而形成作用域链
作用域,作用域链
- 作用域分为:全局作用域,函数作用域,块级作用域。
- 全局作用域:最外层函数或最外层函数外面定义的变量;未定义就赋值的变量;window对象。全局作用域容易导致命名冲突。
- var 申明的变量会同一作用域内提升,让调用者不必考虑是否申明了,但是仍在当前作用域内。
- let const 都是块级作用域内,由花括号界定。
- const不能限制对象属性值的修改,使用Object.freeze()可以限制
- 在当前域内没有所查找的变量,就去上一级查找,最终会查找到全局作用域,这一层层的关系就是作用链。
闭包
- 闭包是由一个函数和它内部所有能访问的变量组成
- 闭包的作用主要有两点:隐藏变量到函数内部,避免全局污染和缓存函数内部变量,以在函数之外访问
事件循环
- JS是单线程的,JS中的异步实际上为JS给浏览器或者V8引擎发送命令,由其他线程去执行对应操作
- JS在执行代码时,会将同步代码依次放入到执行栈中,当遇到异步任务时,则会通知其他线程去执行异步任务,并将异步任务的回调放入到一个队列中,当栈中的同步代码执行完成后,便会从异步队列中依次取出异步任务执行
- 异步队列中异步任务执行顺序为,始终优先执行微任务,只有队列中没有微任务时才会执行宏任务
- 异步队列中没有新的任务时一次事件循环结束,下一次会继续从同步代码开始执行
- 宏任务和微任务
- 任务队列中的任务包含了宏任务和微任务,微任务会比宏任务先执行
- 常见的宏任务包含:setTimeout、setInterval、setImmediate
- 常见的微任务有:promise.then、promise.catch、process.nextTick、new MutationObserver()
- 宏任务需要外部线程的支持,微任务不需要,只是单纯的晚执行
- 浏览器的渲染视图是在微任务执行完成之后,宏任务执行之前
- NodeJS中的事件队列执行顺序为:timer阶段 -> pending阶段 ->poll阶段 -> check阶段 -> close阶段,因为定时器会比I/O请求等先执行
NodeJS事件循环
- NodeJS的事件循环分为6个阶段,分别是:timers、I/O callbacks、idle、poll、check、close callbacks
- timers 阶段:执行 setTimeout 和 setInterval 的回调函数
- I/O callbacks 阶段:执行一些 I/O 操作的回调函数
- idle 阶段:空闲阶段,等待新的事件
- poll 阶段:执行 I/O 事件的回调函数,处理一些 I/O 操作
- check 阶段:执行 setImmediate 的回调函数
- close callbacks 阶段:执行一些关闭事件的回调函数,如 socket 的 close 事件
- 宏任务队列按 事件循环的阶段 依次执行,每个阶段处理特定类型的宏任务
- 微任务队列在以下时机执行:
- 每个宏任务结束后(注意:是单个宏任务,不是整个阶段)。
- 每个事件循环阶段结束后(阶段切换前)。
数据类型
- JS中变量的数据类型可以随时转换,在变量比较时进行隐式类型装换
- 6种基本类型:string、number、null、undefined、symbol、boolean
- 1种引用类型:object,其他的如function、Array、Date等其实都是object
- typeof可以判断是否基本类型或者引用类型,instanceof可以判断数据的真正类型
- instanceof 原理为判断右侧是否在左侧的原型链上
- 在执行 +、-、== 运算符的时候都会进行强制类型转换,但是===运算不会转换
- 判断数组的方法
- instanceof
- Array.isArray
- [].constructor ===Array
- Array.prototype.isPrototypeOf([])
- Object.prototype.toString.call([]) === "[object Array]"
- Object.getPrototypeOf([]) === Array.prototype
GC
- 浏览器的垃圾回收机制主要有两种:引用计数法和标记清除法
- 引用计数法:当变量被引用的时候计数 +1,当被覆盖的时候则计数 -1,当计数为0的时候,就可以进行释放
- 但是当出现循环引用的时候,会导致内存泄露,所以引用计数法后来被淘汰
- 标记清除法:当变量进入到块级上下文的时候标记使用,当离开该上下文时则标记结束,可进行释放
- 使用 let 和 const 可以将变量限制在较小的块级作用域中,有利于垃圾回收
- NodeJS中的GC在NodeJS模块中进行阐述
- 检查内存泄露的方式:查看任务管理器、使用开发者工具中的 Performance 模块
JS垃圾回收机制
就是不需要开发人员手动释放内存,根据一套机制自动释放内存。
- 计数法:计算对象引用次数,缺陷就是循环引用导致计数永远大于0
- 标记清除法:用于标记永远达不到的对象。
- 分代回收:将内存的对象分为新生代和老生代。两种处理方式不一样。
- 新生代:新生代内存分为from和to部分,当from快满后,将新生代内存进行标记,存活的对象复制后放入to区中。然后再将to区设置from,循环往复。
- 老生代是新生代存活两次之后晋升为老生代。老生代采用标记清除法,未标记的删除。
- 频繁的垃圾回收后,会产生内存碎片,如果此时需要分配较大连续内存时,就可能内存不足,所以会适时进行内存整理。
内存泄露方式
本质是垃圾回收机制无法收集的对象,即无论标记清除法还是计数法均无法清理的对象
- 循环引用的对象(计数法会泄漏)
- 意外的全局变量:全局变量可以从代码任何位置访问,导致GC不会清除清除全局变量,如过循环过程中不断的创建全局变量,就可能导致内存溢出。可以使用函数作用域的变量代替全局变量。避免使用var变量。
- 被遗忘的计时器:如果循环函数内有对外部变量引用的话,这个变量会一直在内存中
- 脱离Dom的变量:如果变量对一个dom进行引用。dom被删除,这个引用也无法被回收
- 不合理的闭包,导致某些变量一直在内存中:闭包持有外部函数作用域中的变量的引用,当外部执行完后,若闭包未被释放,则闭包仍持有这个变量,导致无法被清理。
如何检测内存泄漏
- 浏览器
- Chrome DevTools,使用Memory面板录制堆快照
- Chrome DevTools,只有Performance面板
- 静态代码分析:
- eslint会提示一些不合理的变量
- 使用单元测试,断言对象是否被销毁
- 一些第三方app或插件检测
- 如小程序本身提供内存监控
- 有些专业app可以对应用内存占用进行监控
- 如果是web页面,也可以使用浏览器插件监控,如lighthouse
- 如nodejs可以使用压测进行检测
- 使用API
- 在浏览器中可以使用
performance.memory(非标准api)来获取内存使用情况
- 在浏览器中可以使用
- 在nodejs中可以使用
PerformanceObserver来获取资源加载情况,结合页面间接卡顿判断内存问题
- 在nodejs中可以使用
- 在nodejs中可以使用process.memoryUsage()来获取内存使用情况
this
- 在标准函数中,this指向的函数调用的上下文对象。
- 在箭头函数中,this指向的定义箭头函数的上下文。
- 日常开发中,回调函数中,this指向不正确,改为箭头函数即可。
- 可以通过apply,call,bind修改this指向。
线程vs进程
- 线程包含在进程中,线程是实际的运作单元。
- 线程是运算调度的最小单元
- 进程是资源分配的最小单元
- 线程共享进程中的资源,线程简可以自由通信,导致耦合性较高
- 进程相对独立,通过显式方式进行通信。
- 线程具有原子性,其他线程修改了变量,线程能立马感知
- 进程具有并发性,能独立进行,进程可以重复利用CPU多核特性。
渲染进程中的线程
- 主线程: 负责页面渲染、事件处理、脚本执行等
- 合成线程:负责将渲染树转换为屏幕上的像素,由GPU执行
- Worker线程
- 光栅化线程池
浏览进程中的线程
- 浏览器UI 线程:绘制浏览器的UI界面
- 网络线程
- 存储线程
网络进程
- 负责处理网络请求和响应
为什么js会阻塞页面渲染
- js是单线程,js执行过程中可能修改dom树,或者读取Dom信息。导致渲染必须暂停。
- 若JS执行时候不阻塞渲染,可能导致DOM处于不一致状态
- 单线程模型避免了并发修改DOM带来的复杂同步问题
- 而script的defer是在页面渲染完后执行。
- async是js资源下载完后执行。
map vs 对象
- map在大量数据类型的时候,性能更好
- map没有原型链
- map的值的顺序是固定的,对象不一定
- map的键可以为任意类型,对象只能是字符串或Symbol
错误收集
全局JS错误
- 通过window.onerror捕获全局JS错误
- 通过window.addEventListener('error', callback)捕获全局错误,如图片加载失败等
- 通过window.addEventListener('unhandledrejection', callback)捕获未处理的Promise错误2
React捕获错误
- 通过ErrorBoundary组件捕获渲染期间、生命周期方法和构造函数中的错误,防止整个应用崩溃。
- 通过componentDidCatch方法捕获React组件中的错误
- React18 的 新特性:useErrorBoundary hook捕获错误
Vue捕获错误
- 通过Vue.config.errorHandler捕获Vue组件中的错误
- 组件基本:errorCaptured hook捕获