JS 的特点

  • 单线程
  • JS 和 DOM 渲染共用同一个线程,因为 JS 可修改 DOM 结构
  • 同一时间只能做一件事,DOM 渲染会阻塞 JS 的执行
  • 浏览器和 nodeis 已支持 JS 启动进程,如 Web Worker (只能执行 js 计算,无法渲染 DOM)
  • 弱类型
  • 跨平台
  • 解释型语言
  • 以事件驱动为核心
  • 遵循 ES 标准

严格模式 的特点

  • 全局变量必须先声明
  • 禁止使用 with
  • 创建 eval 作用域
  • 禁止 this 指向 window
  • 函数参数不能重名
  • 禁止使用0开头的数字

数据类型有哪些

  • 值类型( 6 种):Number、String、Boolean、Null、Undefined、Symbol(ES6新增)
  • 引用类型(1 种):Object

解构赋值

【实战】互换赋值

let a=1, b=2;
[b,a] = [a,b]
console.log(a,b) //2 1

作用域

即变量的合法使用范围

  • 全局作用域——全局可使用
  • 函数作用域——函数内可使用
  • 块级作用域(ES6 新增)——代码块内能使用 (如 if、for 语句的 {} 内)

自由变量

  • 一个变量在当前作用域没有定义,但被使用了
  • 在变量定义的地方(不是执行的地方)向上级作用域一层一层寻找,直至找到为止
  • 如果到全局作用域都没找到,则报错 xx is not defined

函数

函数声明和函数表达式的区别

  • 函数声明 function fn() {…}
  • 函数表达式 const fn= function() {…}
  • 函数声明会在代码执行前预加载,而函数表达式不会
    函数在声明前调用,可以正常执行,改用函数表达式就不行,调用时变量 fn 还不是函数,会报错。

普通函数和箭头函数的区别

普通函数

箭头函数

声明方式

function , 可以是具名/ 匿名函数

=> ,只能为匿名函数

this指向

运行时确定

可以根据调用方式不同而改变

定义时确定

总是指向定义时的上层作用域中的this

(call , bind , apply 无法改变 this 指向)

构造函数

可以用作构造函数

不能用作构造函数

arguments 对象

有自己的 arguments 对象

没有自己的 arguments 对象,继承父级作用域中的 arguments 对象

箭头函数不适用的场景

  • 构造函数不能使用箭头函数(箭头函数没有自己的this,所以不能作为构造函数使用,也不能通过new操作符调用)
  • 事件监听的回调函数不能使用箭头函数(事件监听的回调函数中需通过 this 读取元素本身,但箭头函数内的 this 为外层的 window)
  • 对象的方法不适合使用箭头函数(对象方法需要通过 this 获取对象自己,但箭头函数内的 this 为外层的 window)
  • 原型链的方法不适合使用箭头函数(原因与对象的方法相同)
  • Vue 的生命周期和方法不能使用箭头函数(原因与对象的方法相同)

回调函数

被作为另一个函数的参数传入的函数

闭包

访问了外部变量的函数

this关键字是如何工作的?(this 的指向)

  • 全局上下文中,this 指向全局对象(在浏览器中是window)
  • 事件监听器中,this 指向触发事件的元素
  • 类的静态方法中,this 指向类
  • 普通函数中,this 指向全局对象(在浏览器中是window)
  • 构造函数中,this 指向新创建的对象实例
  • 箭头函数中,this 指向外层作用域的 this
  • 对象的方法用普通函数书写,被调用时 this 指向对象,用箭头函数书写时,this 指向外层的 this (在浏览器中是window)
  • bind、call、apply 方法创建的新函数中 ,this 指向方法的第一个参数

详见

原型 vs 原型链

原型链是 JavaScript 中实现对象继承的一种机制

  • 声明构造函数/类时,不仅会创建该构造函数/类,还会创建相应的原型对象,通过构造函数/类的 protoType 属性可以访问,所有可以被继承的属性和方法都存储在该原型对象中
  • 使用构造函数/类创建对象时,对象实例的 __proto__ 属性存储了原型对象的引用
  • 通过 __proto__ 属性,便将对象继承的所有原型串联了起来,构成了原型链
  • 当访问对象的属性/方法时,若对象中没有找到,就会由近及远去原型链中找,直到原型链的顶端 Object.prototype

异步

一种避免耗时较长的任务阻塞代码执行的 JS 代码运行机制 ( 详解下文 JS 代码在浏览器中的执行顺序

最初使用回调函数实现,但因代码层层嵌套,形成了回调地域 难以维护,ES6 新增了 Promise 实现异步,通过 Promise 可链式调用的特征,解决了回调函数的回调地域 问题。

Promise

Promise 是构造函数,专为异步设计,可通过 new 创建未来会执行的对象实例(一个状态为 pending 的 Promise 实例 )

let p1 = new Promise((resolve, reject) => {});

pending 状态的 Promise 实例执行 resolve() 后,Promise 实例的状态会变为 resolved / fulfilled,并立即触发后续的 then 函数

let p1 = new Promise((resolve, reject) => {
  resolve();
});

let p2 = p1.then(function () {
  console.log("打印 p1 --", p1); // p1 状态为 fulfilled
});

// 若then函数内无报错,p2 状态为 fulfilled,若报错,则 p2 状态为 rejected
console.log("打印 p2 --", p2);

pending 状态的 Promise 实例执行 reject() 后,Promise 实例的状态会变为 rejected ,并立即触发后续的 catch 函数

let p1 = new Promise((resolve, reject) => {
  reject();
});

let p2 = p1.catch(function () {
  console.log("打印 p1 --", p1); // p1 状态为 rejected 
});

// 若catch 函数内无报错,p2 状态为 fulfilled,若报错,则 p2 状态为 rejected
console.log("打印 p2 --", p2);

Promise 的串行执行

async function promise_serial() {
  let result = [];

  result.push((await promise_list[2]).data);
  result.push((await promise_list[1]).data);
  result.push((await promise_list[0]).data);

  console.log(result);
}

promise_serial();

Promise 的并行执行

Promise.all() 用于将多个 Promise 实例,包装成一个新的 Promise 实例,实现等待多个 Promise 实例全部变为 fulfilled 状态才执行目标操作。

import axios from 'axios'

let infoList = []

let id_list = ['1', '2', '3']

let promise_list = []

for (let id of id_list) {
  promise_list.push(axios.get(`http://jsonplaceholder.typicode.com/users/${id}`))
}

Promise.all(promise_list).then((res) => {
  infoList = res.map((item) => item.data)
  console.log(infoList) // 得到预期结果
})

async await

async await 是基于Promise的语法糖,用于实现使用同步的语法实现异步

  • async 函数返回的都是 Promise 对象
  • await 只能在 async 函数中使用
  • await 后跟 Promise 对象:会阻塞代码的运行,需等待 Promise 变为 resolved 状态返回结果后,才继续执行后续代码
  • await 后跟非 Promise 对象:会原样返回
  • await 相当于 Promise 的 then
  • await 中的异常需用 try…catch 捕获
try {
    const res = await p4;
     console.log('await后的Promise执行成功后的返回值res:',res);
  } catch (err) {
    console.error('await后的Promise执行失败后的报错err:',err); 
  }

JS 代码在浏览器中的执行顺序

异步任务分为 微任务宏任务

JS 【精华速查版】2024最新版_原型模式

  • 同步任务放入调用栈 Call Stack
  • 微任务(Promise,async、await 等) 放入微任务队列 micro task queue
  • 宏任务(setTimeout ,setInterval、ajax、Dom事件等) 放入 Web APIs

添加 async 的函数本身不是异步,内部代码会同步执行,遇到 await 时,await 后紧跟的函数会立即执行,是同步任务,await 语句后的代码才是异步微任务

async function async1() {
  console.log("async1 start"); // 第 1 个打印
  await async2(); // 先执行 async2() , 进入 async2 函数内

  // await 后的代码都是微任务,将其放入微任务队列 --- 微任务 1
  console.log("async1 end"); // 第 3 个打印
}

// 定义函数,先跳过
async function async2() {
  console.log("async2"); // 第 2 个打印
}

// 代码从此开始执行,进入 async1 函数内
async1();

执行结果

async1 start
async2
async1 end

Promise 函数内的代码是同步执行,待 Promise 执行 resolve() 后会立即触发 then 函数,then 函数内的代码才是异步微任务

new Promise(function (resolve) {
  console.log("promise 函数内"); // 第1个打印
  resolve(); // Promise 状态变为 resolved , 立即触发了 then 函数
}).then(function () {
  // then 函数是个微任务,将其放入微任务队列
  console.log("then 函数内"); // 第3个打印
});

console.log("promise 函数外"); // 第2个打印

执行结果

promise 函数内
promise 函数外
then 函数内

setTimeout 和 setInterval 从被放入 Web APIs 开始计时,计时结束后,会放入回调队列(Callback Queue),等待 event loop 触发执行

对象

创建对象的方法

  • {}
  • 构造函数,含 new Object()
  • Object.create()
  • 工厂函数

{}、Object()、new Object()、Object.create() 的区别

  • {} 是通过字面量的方式创建空对象,原型为 Object.prototype,语法最简洁,也最常用。
  • Object() 与 new Object() 功能相同,无论传入什么参数,其创建的对象的原型都是 Object.prototype,但 new Object() 创建对象的意图更明确
  • Object.create() 创建的对象的原型由其传入的第一个参数决定。

【实战】判断变量是否为空对象

if(JSON.stringify(obj)==="{}"){
  // 是空对象
}

数组

【实战】判断是否为数组

Array.isArray(val)

数组常用的 API

数组的API

功能

入参

返回值

是否改变原数组

unshift

在数组头部追加元素

新元素

数组的新长度

改变

push

在数组尾部追加元素

新元素

数组的新长度

改变

shift

移除数组的第一项


被移除的元素

改变

pop

移除数组最后一项


被移除的元素

改变

sort

数组排序

排序规则函数

排序后的数组

改变

reverse

数组反转


反转后的数组

改变

fill

数组覆写

新元素,起始下标,终点下标

覆写后的数组

改变

splice

拼接数组

下标,删除数量,新元素

移除元素的数组

改变

slice

数组截取

起始下标,终点下标

截取的数组

不改变

filter

数组过滤

过滤规则函数

过滤后的数组

不改变

concat

数组合并

被合并的数组/元素

合并后的数组

不改变

map

数组格式化

被合并的数组/元素

格式化后的数组

不改变

reduce

数组缩减

缩减规则函数

计算结果

不改变

toString

数组转字符串


字符串

不改变

join

拼接元素

拼接符

字符串

不改变

数组的 API,有哪些是纯函数 ?

  • concat
  • map
  • filter
  • slice

纯函数:1.不改变源数组 2.返回一个数组

数组遍历

遍历方法

返回值

使用场景

备注

副作用

for 循环

——

遍历数组

通用

可以改变原数组

forEach 循环

——

遍历数组

ES5 新增,不支持中断和异步

可以改变原数组

for of 循环

——

遍历数组

ES6 新增

可以改变原数组

map

格式化后的数组

格式化

数组的API

不会改变原数组

filter

过滤后的数组

过滤

数组的API

不会改变原数组

reduce

最终计算结果

累计

数组的API

不会改变原数组

every

匹配结果

全部匹配

数组的API

不会改变原数组

some

匹配结果

部分匹配

数组的API

不会改变原数组

数组去重

最简单的是使用 Set

let oldList = [1, 2, 3, 3];
let newList = Array.from(new Set(oldList)); // 得到 [1, 2, 3]

其他思路: 创建一个新数组,循环遍历目标数组,若新数组中不存在则添加,否则不添加,用 reduce 可便捷实现

let arr = [1,2,3,4,4,1]
let newArr = arr.reduce((pre,cur)=>{
    if(!pre.includes(cur)){
      return pre.concat(cur)
    }else{
      return pre
    }
},[])
console.log(newArr);// [1, 2, 3, 4]

事件

事件冒泡 vs 事件捕获

  • 事件冒泡指事件从触发事件的元素开始,逐级向外传播至 html 节点
  • 事件捕获指事件从 html 节点,逐级向内传播到触发事件的元素

异常

throw 语句手动抛出异常

// 用户定义的 throw 语句 --- 通用错误
throw new Error('The number is low');

try catch 语句手动捕获并处理异常

try {
  // 执行目标代码
} catch (err) {
  // 控制台打印报错信息
  console.log(err);
} finally {
  // 无论是否报错都会执行的代码
}

常见的异常:

  • 通用错误 Error
  • 语法错误 SyntaxError
  • 类型错误 TypeError
  • 引用错误 ReferenceError
  • 范围错误 RangeError

垃圾回收 GC

导致内存泄漏的场景

  • 不断创建全局变量
  • 未及时清理的闭包
  • DOM元素的引用
  • 事件监听器

避免内存泄漏的方法

  • 避免创建全局变量。
  • 使用严格模式或者let/const声明变量。
  • 及时解除DOM元素的引用。
  • 在移除DOM元素之前,移除相关的事件监听器。
  • 使用弱引用或者引用计数来处理循环引用的问题。(如使用 WeakMap 和 WeakSet)
  • 使用try/catch/finally来确保资源在异常发生时也能被正确释放。

JS 的垃圾回收机制 GC

  • 引用计数(之前)
  • 标记清除(现代)
  • 标记整理(优化)
  • 分代式垃圾回收(V8引擎)

其他

延迟加载


监控白屏、首屏

// 监听DOMContentLoaded事件来计算首屏时间和白屏时间
document.addEventListener('DOMContentLoaded', (event) => {
  const perf = performance.timing;
  // 首屏时间(First Paint)
  const firstPaint = event.timeStamp - perf.navigationStart;
  console.log(`首屏时间(First Paint): ${firstPaint} ms`);
 
  // 通过DOMContentLoaded事件的延迟来估算白屏时间
  const whiteScreenTime = event.timeStamp - perf.fetchStart;
  console.log(`估算的白屏时间: ${whiteScreenTime} ms`);
});