JavaScript面试总结二:进阶知识点

116 阅读12分钟

变量提升、函数提升

JS引擎执行

JS引擎读取JS代码时分为两步:

  • JS代码的解析读取
  • JS代码执行

变量提升

变量提升就是指 JavaScript 代码在执行过程中, JavaScript 引擎把变量和函数的声明部分提升到代码开头的”行为“。变量提升后,会给变量设置默认值,这个默认值就是undefined 

浏览器会把带'var'或'function'关键字进行提前的声明或定义

⚠️:let和const定义的变量被提升却无法正常访问,因为存在暂时性死区

  • 变量提升指的是使用var声明的变量提升到它所在的作用域的最顶端,值停留在原地
console.log(a) // undefined
var a = 1
console.log(a) // 1

上述代码执行过程

var a 
console.log(a)
a = 1
console.log(a)

函数提升

函数提升只针对具名函数,对于赋值的匿名函数,不会存在函数提升

console.log(a) // f a()
console.log(b)  // undefined
// 具名函数
function a(){console.log('hello')}
// 匿名函数
var b = function(){console.log('world')}

上述代码执行过程

var a = function(){consle.log('hello')}
var b
console.log(a)
console.log(b)

变量提升与函数提升的优先级

具名函数提升优先级高于变量提升,且不会被同名变量声明覆盖,但是会被变量赋值后覆盖。而且存在同名函数与同名变量时,优先执行函数。

console.log(a)  // f a()
console.log(a()) // 1
var a = 1
function a(){console.log(1)}
console.log(a) // 1
a= 2
console.log(a()) // a not a function

上述执行过程为

var a = function(){console.log(1)}
var a 
console.log(a)
console.log(a())
a = 1
console.log(a)
a = 2
console.log(a())

JS中的作用域

定义:

JS中的作用域是指变量和函数在代码中可访问的范围。

作用域的类型

  • 全局作用域:在代码的最外层定义的变量和函数,可以在代码的任何位置被访问。
  • 局部作用域:在函数内部定义的变量和函数,只能在函数内部被访问
  • 块级作用域:通过新增命令 let 和 const 声明,所声明的变量在指定块的作用域外无法被访问。

作用域的作用

  • 隔离变量:作用域可以将变量限制在特定的代码块或函数内部,防止变量之间发生命名冲突
  • 封装和信息隐藏:可以将一些变量隐藏在函数内部,避免外部代码直接访问和修改
  • 变量的生命周期:变量在进入作用域时被创建,离开作用域时被销毁,有助于内存管理和垃圾回收

作用域链

当引擎从当前的作用域开始查找变量,如果找不到,就会向上一级继续查找,直到找到变量或者已经到达全局作用域仍然找不到就会停止,这就是作用域链。

  • 1.查看当前作用域,如果当前作用域声明了这个变量,可以直接访问

  • 2.如果没有就查找当前作用域的上级作用域,也就是当前函数的上级函数,看看上级函数中有没有声明,有就返回变量,没有继续下一步

  • 3.再查找上级函数的上级函数,直到全局作用域为止,有则返回,无则继续

  • 4.如果全局作用域中也没有,我们就认为这个变量未声明(xxx is not defined)

原型、原型链

原型

每个JavaScript对象都有一个关联的原型对象,它是一个普通对象,包含一些共享的属性和方法。当访问对象的属性或方法时,如果对象本身没有这个属性或方法,JS会沿着对象的原型链去查找。对象可以通过prototype属性来访问它的原型对象。

  • prototype : js通过构造函数来创建对象,每个构造函数内部都会一个原型prototype属性,它指向另外一个对象,这个对象包含了可以由该构造函数的所有实例共享的属性和方法。

  • proto: 当使用构造函数创建一个实例对象后,可以通过__proto__访问到prototype属性。

  • constructor:实例对象通过这个属性可以访问到构造函数

原型链

每个实例对象都有一个proto属性指向它的构造函数的原型对象,而这个原型对象也会有自己的原型对象,一层一层向上,直到顶级原型对象null,这样就形成了一个原型链。

原型链的顶层原型是Object.prototype,如果这里没有就只指向null

事件流

事件流分为三个阶段:捕获阶段、目标阶段和冒泡阶段

  • 捕获阶段:事件从window上往事件触发处传播,遇到注册的捕获事件会触发(捕获过程)。在捕获阶段中,事件首先被顶层元素捕获,然后逐级向下传播,直到达到事件的目标元素。

  • 目标阶段:事件传播到目标处时的阶段。在目标阶段中,事件被目标元素上绑定的事件处理函数捕获并执行。

  • 冒泡阶段:从事件触发处往window上传播, 遇到注册的冒泡事件就会触发。在冒泡阶段中,事件会从目标元素开始逐级向上冒泡,直到达到顶层元素。

阻止默认行为的方法

  • event.stopPropagation()——可以阻止事件流的传播
  • event.stopImmediatePropagation()——可以阻止事件流的传播的同时可以阻止同一个容器上绑定其他相同事件

事件委托

原理:利用事件冒泡机制。当子元素上的事件被触发时,该事件会向上冒泡的父元素,父元素可以捕获并处理事件,通过在父元素上监听事件,可以捕获到子元素触发的事件,实现对子元素的事件处理。

JS的运行机制(事件循环)

JS是一门单线程语言。事件循环中,主线程执行完当前的同步任务后,会检查事件队列中是否有待处理的事件,有,主线程会取出事件并执行对应的回调函数。循环的过程被称为事件循环。主要是由主线程和任务队列组成,主线程负责执行同步任务,异步任务通过任务队列进行处理。

进程和线程的区别

进程是操作系统分配资源的最小单位,线程是程序执行的最小单位

事件循环(Event Loop)

1.先执行同步代码,所有同步代码都在主线程上执行,形成一个执行栈。

2.当遇到异步任务时,会将其挂起并添加到任务队列中,宏任务放入宏任务队列,微任务放进微任务队列。

3.当执行栈为空时,事件循环从任务队列中取出一个任务,加入到执行栈中执行。先执行微任务(先进先出),后执行宏任务(先进后出)

4.重复上述步骤,直到任务队列为空。

执行顺序

  • 同步代码先执行:console.log()、new.Promise()、
  • 异步代码---微任务:Promise.then()、async/await
  • 异步任务---宏任务:setTimeout、I/O

⚠️点

async function async1() {
  console.log(111); //1
  await async2(); //await会阻塞它下一行的代码
  await async3(); //异步代码中的微任务1,先挂起
  console.log('async111 end');//7 微任务3 等async3执行完毕后进入任务队列
}
async function async2() {
  console.log('async222 end'); //2
}
async function async3() {
  console.log('async333 end'); //5
}
async1();
setTimeout(() => { //异步代码中的宏任务1,先挂起
  console.log('setTimeout'); //9
}, 0)
new Promise(resolve => {
  console.log('Promise'); //3
  resolve()
})
.then(() => { //异步代码中的微任务2,先挂起
  console.log('Promise111'); //6
})
.then(() => { //异步代码中的微任务4,先挂起  [前一个.then的回调函数执行后被进入任务队列]
  console.log('Promise222'); //8
})
console.log('end'); //4

闭包

定义

闭包是指有权访问另一个函数作用域中变量的函数。创建闭包的方式就是在一个函数内创建另一个函数,创建的函数可以访问到当前函数的局部变量。

作用

  • 创建私有变量:通过闭包可以模拟私有变量,使得只有内部函数能够访问和修改某些数据
  • 实现回调和异步操作:JS中的回调函数和异步操作通常使用闭包捕获外部函数的状态和数据
  • 实现模块化:通过闭包,实现类似于模块的封装,避免命名冲突和变量泄漏
  • 保护数据:通过闭包,可以将一些数据限制在函数的作用域内,避免全局污染

应用场景

  • 构造函数的私有属性
  • 计算缓存
  • 函数节流、防抖

new操作符实际做了那些事情

new操作符用于创建一个实例对象,步骤如下:

  • 创建一个新的空对象:new操作符首先会创建一个新的空对象
  • 设置对象的原型链:新创建的对象被设置为构造函数的prototype属性
  • 将构造函数的上下文设置为新对象:调用构造函数时,将this关键字绑定到新创建的对象,使构造函数内部的代码可以操作新对象
  • 执行构造函数的代码:构造函数内部的代码被执行,可能会在新对象上设置属性和方法

JS中的面向对象

  • 类:类描述对象的特征和行为
  • 对象:对象是类的实例,具有类定义的属性和方法。
  • 封装:封装时将数据和操作封装在一个对象中,隐藏对象的内部细节,只暴露必要的接口供外部使用
  • 继承:通过创建一个新的类来扩展已有的类,子类继承父类的属性和方法,也可以添加自己的属性和方法
  • 多态:多态允许不同的类实现相同的接口或方法,并可以以相同的方式进行调用。

JS中的垃圾回收机制

JS中的/垃圾回收是一种自动管理内存的机制,用于检测和回收不再使用的内存,以防止内存泄漏和资源浪费。

引用计数垃圾回收

1:通过维护每个对象的引用计数来判断 对象是否可达 2:当一个对象被引用时,它的引用计数加一;当一个对象的引用被移除时,它的引用计数减一 3:当一个对象的引用计数为零时,垃圾回收机制会将其回收

标记-清除垃圾回收

1:通过判断对象是否可达来确定对象是否应该回收 2:首先从根对象(全局对象、活动函数的局部变量等)开始,标记所有可达对象 3:之后,垃圾回收器会遍历所有对象,清除没有被标记为可达的对象,将其内存回收

JS内存泄漏

如何检测是否有内存泄漏(利用浏览器Chrome devtools查找问题

  • 打开开发者工具,选择 Memory 面板

  • 点击左上角的录制按钮

  • 在页面上进行各种操作,模拟用户的使用情况。也可以叫做我们在开发测试时的“问题复现”

  • 一段时间后,点击对话框的 stop 按钮,面板上就会显示这段时间的内存占用情况

  • 如果内存占用基本平稳,则说明不存在内存泄漏,相反,就是存在内存泄漏。

  • 录制中选择Snapshot,查看对应的内存记录最大的

JS内存泄漏的方式有哪些

  • ES6中的set、map
  • 循环引用
    • 当两个或多个对象彼此相互引用,而且没有其他对象引用它们,就会形成循环引用,导致内存泄漏
未清理的定时器和事件监听

使用定时器或事件监听器,不在需要时未清理

  • 解决方法:
    • window.removeEventListener()移除事件监听
闭包

闭包可以使函数内部的变量在函数执行结束后仍然被引用,导致这些变量的内存无法释放 解决方法: 解除闭包,将事件处理函数定义在外部。或者在定义事件处理函数的外部函数中

未使用的全局变量:

浏览器中,全局对象就是window对象,变量在窗口关闭或重新刷新页面之前都不会释放,如果有大量数据就会存在内存泄漏.

  • 解决办法:
    • 避免创建全局变量
    • 使用严格模式,在JS文件头部或函数的顶部加上use strict
DOM元素引用

JS中保留对DOM元素的引用,即使这些元素从页面中删除,也会导致内存泄漏

  • 解决方法:
    • 手动删除,设置为null
  • 大量缓存
  • 忘记释放资源
  • 循环引用的事件处理器

开发中涉及到哪些设计模式

工厂模式:一个工厂对象负责创建其他对象,具体的创建逻辑由子类决定(应用:根据参数,调用对应的插件)

单例模式:一个类只有一个实例(应用:全局状态管理vuex,单例弹窗)

原型模式:通过复制现有对象来创建新对象 (应用:JS的原型)

适配器模式:将一个类的接口转换成客户端所期望的另一个接口(应用:vue中的computed)

装饰器模式:动态地给对象添加一些额外的功能(应用:react的高阶组件HOC)

代理模式:创建一个代理对象控制另一个对象的访问(应用:ES6中的proxy)

观察者模式:一个目标对象管理所有相依于它的观察者对象,并且在它本身的状态改变时主动发出通知(应用:跨组件通信)

策略模式:根据不同参数可以命中不同的策略(应用:表单校验)

中介者模式:对象和对象之间借助第三方中介者进行通信(应用:聊天室)