前端八股总结-JS(持续更新)

Last updated on November 4, 2024 pm

记录一下在笔试或面试中的八股知识。融合一下其他人总结的。

是谁还不知道在vscode安装Code Running拓展就可以无痛跑js、c++等代码🥹

JS

0、ES6新特性

⭕块级作用域
  • let:用于声明块级作用域的变量。
  • const:用于声明常量,一旦赋值后不能更改。
⭕箭头函数
⭕模板字符串
⭕解构赋值

解构赋值允许从数组或对象中提取值,并赋值给变量。

1
2
3
4
5
6
7
const arr = [1, 2, 3];
const [a, b] = arr;
console.log(a, b); // 输出: 1 2

const obj = { x: 1, y: 2 };
const { x, y } = obj;
console.log(x, y); // 输出: 1 2
⭕默认参数

如果没有值或者传入了未定义的值,默认参数允许形式参数使用默认值初始化

⭕剩余参数和扩展运算符

剩余参数可以将不确定数量的参数组合为数组:

1
2
3
4
function sum(...numbers) {
return numbers.reduce((acc, curr) => acc + curr, 0);
}
console.log(sum(1, 2, 3, 4)); // 输出: 10

拓展运算符可以展开数组或对象:

1
2
3
const nums = [1, 2, 3];
const newNums = [...nums, 4, 5];
console.log(newNums); // 输出: [1, 2, 3, 4, 5]
⭕Promise

处理异步操作的新方法,允许更清晰地处理成功和失败的结果。

⭕类(Class)

引入类的概念,使得创建对象的方式更加清晰。

⭕模块(Module)

ES6 提供了模块化的支持,可以使用 exportimport 语句导出和导入模块。

0、JS语言特性

动态类型

JS是一种弱类型的语言,变量可以在运行时动态改变类型。

原型继承

JS采用原型链的方式实现继承。

对象可以通过原型(__proto__)链接到其他对象,从而共享属性和方法

一等函数

函数在JS中是“一等公民”,可以被赋值给变量、作为参数传递和作为返回值

闭包

JS中可以实现闭包,指函数可以记住其外部作用域的变量,即使在外部函数已经返回后

异步编程

JS支持异步编程,通过回调函数、Promise和async/await处理异步操作

事件驱动

JS是事件驱动的,特别是在浏览器环境中,用户交互(如点击、键盘输入等)会触发事件处理程序。

1、console.info()

console.info() 是 JavaScript 中 console 对象的一个方法,用于在浏览器的开发者工具控制台中输出信息。它的功能类似于 console.log(),但通常用于记录信息级别的日志。

2、帮你彻底搞懂JS中的prototype、__proto__与constructor(图解)_js prototype constructor-CSDN博客

1
2
3
4
5
6
7
8
9
10
// 判断 p 是否为 Person 的实例
function Person(name) {
this.name = name
}
const p = new Person('sunshine')
p instanceof Person // true

// 这里的 p 是 Person 函数构造出来的,所以顺着 p 的原型链可以找到 Object 的构造函数
p.__proto__ === Person.prototype // true
p.__proto__.__proto__ === Object.prototype // true
_ _ proto _ _

是每个对象的内部属性,它指向对象的原型。(任何通过构造函数创建的对象都会有一个__ proto __属性,指向其构造函数的prototype属性)

当访问对象属性的时候,如果对象没有这个属性,js会查找其__ proto __指向的对象,依次类推,直到找到该属性或达到原型链的顶端(即Object.prototype)

现在js中,建议使用Object.getPrototypeOf(obj)来获取对象的原型

prototype

是函数对象的属性,用于定义实例化对象的原型。

当你向构造函数的prototype添加属性或者方法时,所有通过该构造函数创建的实例都可以访问这些属性或者方法。

constructor

是每个对象的属性,指向创建该对象的构造函数。当通过构造函数创建一个对象时,该对象的constructor指向该构造函数。

原型链

原型链解决的是继承的问题。

每个对象都有一个原型,通过dunder proto指向其原型对象,从中继承方法和属性,同时原型对象也可能拥有原型,这样一层一层,最终指向null。这种关系被称为原型链,通过原型链,一个对象可以拥有定义在其他对象中的属性和方法。

3、Object.assign详解-CSDN博客

Object.assign() 方法用于将所有可枚举属性的值从一个或多个源对象复制到目标对象。它将返回目标对象。

4、看这一篇就够了!-Ajax详解_ajax解析-CSDN博客

5、数据类型

js中判断数据类型的几种实用方法_js判断类型-CSDN博客

1
2
3
4
Boolean([]); //true
Number([]); //0
Number({}); // NaN
Number(false); //0

typeof NaN=Number

NaN!==NaN (Not a Number)

做减法运算时,有一个是NaN结果就是NaN。

isNaN()会先将参数转为Number 类型,再判断是否为NaN。

  • console.log(isNaN(''));//false

5.1、原始类型和引用类型在函数中的行为

(没见到有人总结,那我自己浅总结一下吧)

原始数据类型由于传入的是值的拷贝,所以不会影响外部变量,不详细说了

由于引用数据类型在函数中是按照引用传递的,所以在函数中直接修改是可能会影响到外部的值的,所以下面来具体情况具体分析一下。

😕直接修改内部属性的值
1
2
3
4
5
6
7
8
9
10
let arr = [1, 2, 3];
let obj = { key: "value" };

function modifyReference(array, object) {
array.push(4);
object.key = "new value";
}

modifyReference(arr, obj);
console.log(arr, obj); // 输出: [1, 2, 3, 4] { key: "new value" }

可以看出这样是可以修改外部变量的。

🙂重新赋值
1
2
3
4
5
6
7
8
9
10
let arr = [1, 2, 3];
let obj = { key: "value" };

function reassignReference(array, object) {
array = [4, 5, 6]; // 重新赋值为新数组
object = { key: "new value" }; // 重新赋值为新对象
}

reassignReference(arr, obj);
console.log(arr, obj); // 输出: [1, 2, 3] { key: "value" }

重新赋值后产生的新变量只在函数作用域中有效,不会影响到外部的arr和obj

🙂Object.freeze

可以冻结对象或数组,使其属性不可更改,但是其子属性仍旧可以更改。也就是说,如果一个对象里包含另一个对象,还是可以修改那个对象的值的。

1
2
3
4
5
6
7
8
9
10
11
12
var obj = {
name: '张三',
info: {
a: 1,
b: 2
}
}
Object.freeze(obj)
obj.name = '李四'
console.log(obj) // {info: {a: 1, b: 2},name: "张三"}
obj.info.a = 66
console.log(obj.info) // {a: 66, b: 2}
🙂深拷贝

深拷贝的对象被修改不会影响原对象,而浅拷贝会。

5.2、数组类型详解

  • JavaScript 数组是可调整大小的,并且可以**包含不同的数据类型**。(当不需要这些特征时,可以使用类型化数组。)
  • JavaScript 数组不是关联数组,因此,不能使用任意字符串作为索引访问数组元素,但必须使用非负整数(或它们各自的字符串形式)作为索引访问。
  • JavaScript 数组的索引从 0 开始:数组的第一个元素在索引 0 处,第二个在索引 1 处,以此类推,最后一个元素是数组的 length 属性减去 1 的值。
  • JavaScript 数组复制操作创建浅拷贝。(所有 JavaScript 对象的标准内置复制操作都会创建浅拷贝,而不是深拷贝)。
定义和创建
  • 数组字面量方法:const fruits = ['apple', 'banana', 'cherry'];

  • Array构造函数:

    • new Array(1, 2, 3)( [ 1 , 2 , 3 ] )

    • new Array(5)(长度为5的空数组)

  • String.prototype.split()const fruits3 = "Apple, Banana".split(", ");

5.2、String类型

6、JSON.stringfy和JSON.parse

🐼JSON.stringfy

JSON.stringify(value[, replacer[, space]])

  • 用途:将 JavaScript 对象转换为 JSON 字符串,方便存储和传输。
1
2
3
4
const obj = { name: "Alice", age: null, job: undefined, hobbies: ["reading", "traveling"] };
const jsonString = JSON.stringify(obj);
console.log(jsonString);
// 输出: {"name":"Alice","age":null,"hobbies":["reading","traveling"]}
  • 特殊行为

    • undefined:在对象中,值为 undefined 的属性会被忽略。

    • 函数:如果对象包含函数,函数会被忽略。

    • symbol:如果对象包含 symbol 类型的属性,属性会被忽略。

    • null:会被保留,转换为 JSON 字符串中的 null

    • 数组:会被正常序列化。

🐻‍❄️JSON.parse

JSON.parse(text[, reviver])

  • 用途:将 JSON 字符串解析为 JavaScript 对象,方便在程序中使用。

  • 特殊行为:

    • null:解析字符串 "null" 会返回 null

    • 不支持 undefined:如果 JSON 字符串包含 undefined,解析会失败。

    • 错误处理:如果传入的字符串不是有效的 JSON,JSON.parse 会抛出错误。

7、V8垃圾回收

V8 是 Google 开发的高性能 JavaScript 引擎,广泛应用于 Chrome 浏览器和 Node.js。V8 的垃圾回收机制(Garbage Collection, GC)旨在自动管理内存,以确保不再使用的对象能够被清除,从而释放内存空间。

🕊️基本概念
  • 垃圾回收:指的是自动释放不再使用的内存空间,防止内存泄漏和提高内存使用效率。
  • 根对象:包括全局对象、活动函数的局部变量等,垃圾回收的起点。
🕊️回收机制
  • 标记-清除

    • 标记阶段:从根对象开始遍历所有可达对象,并标记它们为“存活”

    • 清除阶段:扫描堆内存,将未被标记的对象(不可达对象)清除,释放内存

  • 分代收集

    • V8 将内存分为两个主要区域:新生代(Young Generation)和老生代(Old Generation)。
    • 新生代中的对象是短暂的,频繁进行垃圾回收;老生代中的对象则相对长寿,收集频率较低。
  • 新生代垃圾回收

    • Scavenge 算法:使用复制算法,在新生代中,活着的对象会从一个内存区域复制到另一个区域,释放原来的区域内存。可以快速清理不再使用的对象。
  • 老生代垃圾回收

    • Mark-Sweep 和 Mark-Compact:在老生代中,V8 会使用标记-清除和标记-压缩算法。标记-压缩会整理存活对象,以消除内存碎片。
🕊️垃圾回收的触发
  • 内存压力:当内存使用达到一定阈值时,触发垃圾回收。
  • 手动触发:在 Node.js 中可以通过 global.gc() 手动触发垃圾回收(需启动时加上 --expose-gc 标志)。
🕊️优化

减少停顿时间和内存使用

  • 增量回收:将回收过程分成多个小步骤,减少长时间的停顿。
  • 并行回收:在多线程环境中,部分垃圾回收过程可以并行进行,以提高效率。

8、防抖和节流

防抖(Debounce)节流(Throttle)是两种常用的性能优化技术,主要用于控制函数执行的频率,尤其是在处理高频事件(如滚动、输入、窗口大小变化等)时。

🐻‍❄️防抖:

防抖是一种延迟执行的技术,用于确保事件处理函数在一段时间内不会被频繁调用,只有在事件停止触发后,防抖函数才会执行一次。(如用户输入时只有停止输入后再进行搜索)

工作原理:

  • 当事件被触发时,防抖函数会启动一个计数器。
  • 如果在计数器到期之前事件被再次触发,计数器将被重置。
  • 只有在事件停止触发超过设定时间后,事件处理函数才会被调用。

应用场景:

  • 输入框的变化;
  • 窗口调整大小;

代码示例:

1
2
3
4
5
6
7
8
9
function debounce(fn,delay) {
let timer;
return function(...args){
clearTimeout(timer);
timer=setTimeout(()=>{
fn.apply(this,args);
},delay);
}
}
🐨节流:

节流是一种控制函数执行频率的技术,用于确保事件处理函数在一定时间间隔内只被调用一次。(如用户滚动界面时控制频率,API请求时限制请求频率)。

工作原理:

  • 节流函数在时间触发时会立即执行,并且在设定的时间间隔内,不管事件触发多少次都不会再执行。在时间间隔结束后,下一次事件触发时函数会再次执行。

代码示例:

1
2
3
4
5
6
7
8
9
10
function throttle(fn,interval){
let lastTime=0;
return function(...args) {
const now=Date.now();
if(now-lastTime>=interval){
lastTime=now;
fn.apply(this,args);
}
}
}

9、call、apply、bind

🦔this

this 指的是当前执行上下文的对象。它的值取决于函数是如何调用的

例如:

1
2
3
4
5
6
7
8
9
10
11
const person = {
name: 'Alice',
greet: function() {
console.log('Hello, ' + this.name);
}
};
//this指向person对象
person.greet(); // 输出 "Hello, Alice"
//this指向默认的全局对象。在浏览器中是window对象,在Node.js中是global对象
const greetFn=person.greet;
greetFn(); //输出 "Hello, undefined" 或 "Hello, [global object]"
🦔call、apply、bind的作用

它们的作用是显式地设置函数执行时的this值。简单来说:

  • call:立即调用函数,参数一个个传递
  • apply:立即调用函数,参数以数组的形式传递
  • bind:不立即调用函数,而是返回一个可以稍后再调用的新函数,可以固定this和部分参数
🦔call

允许调用一个函数并明确指定函数内部this的值。

1
fn.call(thisArg, arg1, arg2, ...);
  • thisArg:指定 this 的值。
  • arg1, arg2, ...:传给函数的参数,一个一个传递。
1
2
3
4
5
6
7
8
9
10
11
const person1 = {
name: 'Alice',
greet: function(greeting) {
console.log(greeting + ', ' + this.name);
}
};

const person2 = { name: 'Bob' };

// 使用 call 方法来调用 greet 并改变 this 指向
person1.greet.call(person2, 'Hi'); // 输出 "Hi, Bob"
🦔apply

和call的唯一区别是传递参数的方式不同,apply使用数组来传递参数。

1
fn.apply(thisArg, [argsArray]);
🦔bind

bind不会立即调用函数,而是返回一个新的函数,并且这个新的函数中的 this 已经被固定了

1
const newFn=fn.bind(thisArg,arg1,arg2,...)
1
2
3
4
5
6
7
// 使用 bind 方法
const greetBob = person1.greet.bind(person2, 'Hi');

// greetBob 现在是一个新的函数,并且 this 始终指向 person2
greetBob(); // 输出 "Hi, Bob"

setTimeout(greetBob, 1000); // 一秒后输出 "Hi, Bob"

即使在异步调用中,this也指向person2。

10、闭包

简单来说,闭包就是一个函数以及他对外部变量的引用,这个函数能够”记住“它创建时的环境(作用域)。即使这个函数执行完成,或者它被传递到其他地方执行,他仍然可以访问在定义时的作用域中的变量。

🕸️特性:
  • 函数嵌套函数
  • 函数内部可以引用函数外部的参数和变量
  • 参数和变量不会被垃圾回收机制回收
🕸️优点
  • 保护函数内的变量安全,实现封装,防止变量流入其他环境发生命名冲突。
  • 在回调函数和事件处理函数中非常常用,它可以保留对当前作用域的访问
  • 可以访问和保留外部函数作用域中的变量
  • 可以帮助解决异步回调中变量值的保存问题
  • 在早期 JavaScript 版本(ES6 之前)中没有 letconst 来创建块级作用域,但闭包可以用来模拟这种行为。
🕸️缺点
  • 如果不小心滥用闭包,可能会导致内存泄漏,因为闭包会持有对外部变量的引用,使这些变量不会被垃圾回收。
  • 对于闭包的滥用,代码的可读性可能会下降,维护起来更复杂。

11、setTimeout 和 setInterval

🍁setTimeout

在一定时间之后执行一次代码,接收两个参数:回调函数和delay(等待时间)

  • 取消:
1
2
3
4
5
const timeoutId = setTimeout(() => {
console.log("This will not be shown");
}, 3000);

clearTimeout(timeoutId); // 在计时结束前取消
🍁setInterval

每隔一段时间重复执行代码。

  • 取消:
1
2
3
4
5
6
7
const intervalId = setInterval(() => {
console.log("This will be shown repeatedly every 2 seconds");
}, 2000);

setTimeout(() => {
clearInterval(intervalId); // 在 6 秒后取消 `setInterval`
}, 6000);
🍁注意
  • 精度问题setTimeoutsetInterval 的精度不高。尽管你设定了一个 1000 毫秒的延迟,实际执行时间可能会有一些误差,尤其在 JavaScript 的事件循环中,如果前面的任务阻塞了主线程,定时器的执行会被延后。
  • setInterval 的问题:如果某次定时任务的执行时间超过了间隔时间,可能会导致下一个任务的执行时间被推迟,从而出现定时不准的情况。因此在有些情况下(例如要控制任务的精度),递归使用 setTimeout 是更好的选择。
  • 递归 setTimeout 优于 setInterval:若需要在每次定时任务完成之后再进行下一次任务时,使用递归 setTimeout 更好,因为它可以确保上一次任务完成后再开始下一次。
1
2
3
4
5
6
7
function repeatTask() {
setTimeout(()=>{
console.log("Task executed");
repeatTask(); //再次调用
},1000);
}
repeatTask()

12、Promise

Promise 是 JavaScript 中处理异步操作的一种方式。它提供了一种更清晰、结构化的方式来处理异步任务的结果,避免了回调地狱问题,并且让代码更易读易维护。

☀️什么是Promise

Promise是一个代表异步操作的对象。有三种状态:

  • Pending(待定):初始状态,操作未完成
  • Fulfilled(已兑现):操作成功完成,并返回了一个值
  • Rejected(已拒绝):操作失败,返回了一个拒绝(错误)原因

Promise 不可变:Promise 的状态一旦从 Pending 转为 Fulfilled 或 Rejected 就不会再改变

🌤️基本用法

通过Promise构造函数来创建一个Promise实例。构造函数接受一个执行器函数(executor),该函数有两个参数:

  • resolve:当异步操作成功时调用,将 Promise 状态改为 Fulfilled。
  • reject:当异步操作失败时调用,将 Promise 状态改为 Rejected。
1
2
3
4
5
6
7
8
9
const promise = new Promise((resolve,reject)=>{
let success=true;

if(success){
resolve("Task succeeded");
}else{
reject("Task failed");
}
});
🌥️方法
  • .then:用于指定成功时的回调函数。
  • .catch:用于指定失败时的回调函数
  • finally:无论成功失败都执行;主要用于执行一些清理操作。
1
2
3
4
5
6
7
8
9
10
promise
.then((value)=>{
console.log("Success": value);
})//可以添加处理失败的情况,但是不常用
.catch((error)=>{
console.log("Error": error);
})
.finally(()=>{
console.log("Operation complete")
})
☁️链式调用

自动封装:如果在.then()中返回一个普通值,它会被自动封装成一个新的Promise

当一个then返回一个新的promise的时候,可以继续在后面调用then进行处理。后续的.then()会等待这个promise解决后再执行。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
const promise = new Promise((resolve) => {
setTimeout(() => {
resolve(1);
}, 1000);
});

promise
.then((value) => {
console.log(value); // 输出 1
return value + 1; // 返回 2
})
.then((newValue) => {
console.log(newValue); // 输出 2
return newValue + 1; // 返回 3
})
.then((finalValue) => {
console.log(finalValue); // 输出 3
});
🌧️常用静态方法
  • Promise.all()

接收一个包含多个Promise的数组(或可迭代对象),并返回一个新的Promise。当所有Promise都成功时返回的状态才会是Fulfilled,并且所有Promise的结果会以数组形式返回。如果有一个Promise失败,返回的状态就会立即变成Rejected,并返回失败的原因。

1
2
3
4
5
6
7
const promise1=Promise.resolve(1);
const promise2=new Promise((resolve)=>setTimeout(resolve,1000,2));
const promise3=new Promise((resolve)=>setTimeout(resolve,2000,3));

Promise.all([promise1,promise2,promise3]).then((values)=>{
console.log(values); //[1,2,3]
});
  • Promise.any()

任何一个promise兑现时兑现,全部被拒绝时才会拒绝

  • Promise.race()

也接收一个包含多个Promise的数组,但只要其中一个Promise解决或拒绝,就返回结果

1
2
3
4
5
6
const promise1 = new Promise((resolve) => setTimeout(resolve,1000,"first"));
const promise2 = new Promise((resolve) => setTimeout(resolve,500,"second"));

Promise.race([promise1, promise2]).then((value) => {
console.log(value); // "second" (因为 promise2 更快解决)
});
  • Promise.allSettled()

当所有 Promise 都完成(无论是成功还是失败)时,返回的 Promise 状态才会是Fulfilled。与 Promise.all() 不同的是,它不会在有一个 Promise 失败时立即失败,而是等到所有 Promise 都完成之后,返回每个 Promise 的状态和结果。

1
2
3
4
5
6
7
8
9
10
11
12
const promise1 = Promise.resolve(1);
const promise2 = Promise.reject("error");
const promise3 = new Promise((resolve) => setTimeout(resolve, 1000, 3));

Promise.allSettled([promise1, promise2, promise3]).then((results) => {
console.log(results);
// [
// { status: 'fulfilled', value: 1 },
// { status: 'rejected', reason: 'error' },
// { status: 'fulfilled', value: 3 }
// ]
});
🌈其他(有些不解)
  • promise.then().then().then() 结构使得错误能够被捕获并集中处理,任何一个 then 中的错误都会被链中的 catch 捕获。
  • 使用 new Promise((resolve, reject) => { ... }) 允许你在定义异步操作的过程中控制 Promise 的状态,成功和失败的状态可以直接通过调用 resolvereject 来管理,从而实现代码的截断。

13、深拷贝和浅拷贝

🦛浅拷贝

对象的浅拷贝是属性与拷贝的源对象属性共享相同的引用(指向相同的底层值)的副本。因此,当你更改源对象或副本时,也可能导致另一个对象发生更改。与之相比,在深拷贝中,源对象和副本是完全独立的。

对于浅拷贝,只有顶层属性被复制,而不是嵌套对象的值。因此:

  • 对副本的顶层属性的重新赋值不会影响源对象。
  • 对副本的嵌套对象属性的重新赋值会影响源对象。

在 JavaScript 中,所有标准内置对象复制操作(扩展语法Array.prototype.concat()Array.prototype.slice()Array.from()Object.assign())都创建浅拷贝,而不是深拷贝。

除此之外,这些也是浅拷贝

  • 使用拓展运算符:const shallowCopy = { …obj1 }
  • 直接相等:obj1=obj2
🦛深拷贝

定义深拷贝:

  1. 它们不是同一个对象(o1 !== o2)。
  2. o1o2 的属性具有相同的名称且顺序相同。
  3. 它们的属性的值是彼此的深拷贝。
  4. 它们的原型链是结构等价的。
  • 方法一:JSON.parse(JSON.stringify())
    • 对象的属性是函数时无法拷贝
    • 原型链上的属性无法拷贝
    • 无法处理Date/RegExp这种复杂类型
    • 会忽略Symbol和undefined
1
2
let ingredients_list = ["noodles", { list: ["eggs", "flour", "water"] }];
let ingredients_list_deepcopy = JSON.parse(JSON.stringify(ingredients_list));
  • 方法二:structuredClone()(支持Date等复杂类型)
  • 方法三:Lodash的_.cloneDeep(不支持DOM节点等特定对象)

14、箭头函数

以下三种情况访问this不会输出undefined:

🦤在类中使用箭头函数

在类的方法中定义箭头函数,箭头函数会继承类实例的 this

1
2
3
4
5
6
7
8
9
10
class MyClass {
constructor(value) {
this.value=value;
this.func=()=>{
console.log(this.value);
}
}
}
const instance=new MyClass(42);
instance.func(); //输出42
🦤在另一个函数的作用域中定义箭头函数

如果在一个函数内定义箭头函数,this 将指向该函数的上下文。

1
2
3
4
5
6
7
function func(value) {
return ()=>{
console.log(value);
};
}
const fun=func(42);
fun(); //输出42
🦤在 setTimeout 中使用箭头函数

如果在 setTimeout 等异步函数中使用箭头函数,它会继承 this

1
2
3
4
5
6
7
8
9
const obj={
value: 42,
func: function() {
setTimeout(()=>{
console.log(this.value);
},1000);
}
}
obj.func(); //一秒后输出42

15、如何判断是数组

  • Array.isArray() (最推荐的方法,返回true/false)
  • instanceof(arr instanceof Array)(返回true/false)
  • Object.prototype.toString.call()
  • constructor(arr.constructor===Array)(返回true/false)

16、for in和for of和forEach

🐥for in

for...in 用于遍历对象的可枚举属性,包括对象自身的属性和继承自原型链的属性。

  • for...in 循环会遍历对象的所有可枚举属性,包括继承的属性。
  • 使用 hasOwnProperty 方法可以避免遍历原型链上的属性。
  • 不建议用 for...in 遍历数组,因为它会遍历数组的索引,且不保证顺序。
🐥for of

for...of 用于遍历可迭代对象(如数组、字符串、Map、Set 等),获取其值。

  • for...of 只会遍历可迭代对象的值,不会遍历对象的属性。
  • 适合用于数组、字符串等数据结构的遍历。
🐥forEach

是一种迭代方法,按照索引升序为数组中的每个元素调用一次提供的callbackFn函数。它总是返回undefined,而不能链式调用。(不会在空槽上调用)

除非抛出异常,否则没有办法停止或中断 forEach() 循环。如果有这样的需求,则不应该使用 forEach() 方法。

forEach() 期望的是一个同步函数,它不会等待 Promise 兑现。

1
2
3
4
5
6
7
8
9
10
11
12
const ratings = [5, 4, 5];
let sum = 0;

const sumFunction = async (a, b) => a + b;

ratings.forEach(async (rating) => {
sum = await sumFunction(sum, rating);
});

console.log(sum);
// 期望的输出:14
// 实际的输出:0

17、async/await

async:用于声明一个函数为异步函数。异步函数会返回一个 Promise,即使函数内部没有明确返回 Promise,JavaScript 也会自动将其转换为 Promise。(不一定要和await一起)

await:用于等待一个 Promise 被解决(fulfilled)或被拒绝(rejected)。**await 只能在 async 函数内部使用**。

错误处理
1
2
3
4
5
6
7
8
9
10
async function getData() {
try {
const data = await fetchData();
console.log(data);
} catch (error) {
console.error("错误:", error);
}
}

getData();
结合promise
1
2
3
4
5
6
7
8
9
10
11
12
13
async function fetchAllData() {
try {
const [data1, data2] = await Promise.all([
fetch('https://api.example.com/data1'),
fetch('https://api.example.com/data2')
]);
console.log(data1, data2);
} catch (error) {
console.error("错误:", error);
}
}

fetchAllData();

18、arguments

arguments 是一个类数组对象,代表传递给函数的所有参数。它在函数内部可用,用于访问函数的参数,无论参数是如何传递的

🦩基本用法

在函数内部,可以使用 arguments 关键字来访问传入的参数。arguments 是一个类似数组的对象,但不是数组,因此没有数组的方法,比如 pushpop 等。

1
2
3
4
5
function exampleFunction() {
console.log(arguments); // 打印所有传入的参数
}

exampleFunction(1, 2, 3, "hello"); // 输出: [1, 2, 3, "hello"]
🦩转换为数组
  • 使用Array.from:
1
2
3
4
5
function exampleFunction() {
const argsArray = Array.from(arguments);
console.log(argsArray); // [1,2,3]
}
exampleFunction(1, 2, 3);
  • 使用拓展运算符:
1
2
3
4
function exampleFunction(...args) {
console.log(args);
}
exampleFunction(1, 2, 3);

19、this的指向和绑定规则

🐥全局上下文

在全局作用域中,this 指向全局对象:

  • 在浏览器中,this 指向 window 对象。
  • 在 Node.js 中,this 指向 global 对象。
🐥函数调用

在普通函数中调用 this,它指向调用该函数的对象。如果函数是通过对象调用的,this 指向该对象;如果是全局调用,this 指向全局对象(在严格模式下为 undefined)。

🐥对象方法

当一个方法被调用时,this 指向调用该方法的对象。

1
2
3
4
5
6
7
8
const person = {
name: 'Alice',
greet() {
console.log(`Hello, ${this.name}`);
}
};

person.greet(); // 输出 "Hello, Alice"
🐥构造函数

当通过new关键字调用构造函数的时候,this指向新创建的实例对象

🐥箭头函数

箭头函数没有自己的this,他从外部上下文(定义时的上下文)继承this的值

🐥call、apply、bind

callapplybind 方法来显式地绑定 this 的指向。

🐥事件处理程序

在事件处理程序中,this 通常指向触发事件的元素。

1
2
3
4
const button=document.getElementById('myButton');
button.addEventListener('click',function(){
console.log(this); //指向button元素
})
🐥严格模式
  • 未绑定的 this 在函数中会是 undefined,而不是全局对象。
  • 如果希望在函数中访问 this,应该将其作为对象的方法调用,或者使用 bind 进行绑定。局部变量不会影响 this 的上下文。

20、如何解决script标签阻碍DOM的解析

🐵使用defer属性

当在 <script> 标签中添加 defer 属性时,浏览器会在 HTML 解析完成后再执行脚本。此时,DOM 已经构建完成,因此可以安全地操作 DOM。

🐵将 <script> 标签放在 <body> 的底部

将脚本放在 <body> 标签的结束标签之前,这样可以确保在执行脚本时,DOM 已经构建完成。

21、变量提升

指的是变量和函数声明在代码执行之前被提升到其所在的作用域的顶部。这意味着无论你在代码的哪个位置声明变量,JavaScript 在执行代码时都会将其提升到该作用域的顶部。

🐎基本原理
  • 变量声明:在 JavaScript 中,只有变量的声明会被提升,而赋值不会被提升。
  • 函数提升:函数声明也会被提升,允许在函数声明之前调用该函数。
🐎变量提升

由于只有声明被提升而赋值没有,所以输出的是undefined

1
2
3
console.log(a); // 输出: undefined
var a = 10;
console.log(a); // 输出: 10

使用let和const声明的变量不会被提升到顶部,访问他们会导致暂时性死区,即在生命之前访问变量会抛出错误。

🐎函数提升
1
2
3
4
5
greet(); // 输出: "Hello, world!"

function greet() {
console.log("Hello, world!");
}

如果使用函数表达式(如将函数赋值给一个变量),则不会提升该变量。

1
2
3
4
5
foo(); // 报错: TypeError: foo is not a function

var foo = function() {
console.log("Function expression");
};

22、模板字符串

  • 模板字符串允许在 ${} 中插入变量或表达式,构成动态内容(会自动运算)
  • 可以直接编写多行字符串,无需添加换行符 \n 或使用 + 拼接
  • 在模板字符串中可以直接调用函数,将返回值嵌入字符串
  • 模板字符串中可以嵌套其他模板字符串,形成动态构建内容

23、getter和setter

🐣getter

get语法将对象属性绑定到查询该属性时将被调用的函数。(有时需要动态访问计算属性的值,或者需要反应内部变量的状态,而不需要显示方法调用,可以用getter实现)

  • 它不能与另一个 get 或具有相同属性的数据条目同时出现在一个对象字面量中(不允许使用 { get x() { }, get x() { } }{ x: ..., get x() { } })。
  • getter 延迟计算值的成本,直到需要此值,如果不需要,你就不用支付成本。该值是在第一次调用 getter 时计算的,然后被缓存,因此后续访问返回缓存值而不重新计算它。这意味着你不应该为你希望更改其值的属性使用懒 getter,因为 getter 不会重新计算该值。
  • 可以使用delete操作符删除一个getter:delete obj.getter
1
2
3
4
5
6
7
const obj={
log: ['a','b','c'],
get latest() {
return this.log[this.log.length-1];
},
}
console.log(obj.latest); //'c'
🐣setter

当尝试设置属性时,**set** 语法将对象属性绑定到要调用的函数。

在 javascript 中,如果试着改变一个属性的值,那么对应的 setter 将被执行。setter 经常和 getter 连用以创建一个伪属性。不可能在具有真实值的属性上同时拥有一个 setter 器。

1
2
3
4
5
6
7
8
const lan={
set current(name){
this.log.push(name);
},
log: [],
}
lan.current="EN";
console.log(lan.log);

24、Generator和yield

生成器和yield是用于处理异步编程和迭代的工具。生成器是一种特殊类型的函数,可以在执行过程中暂停,并在稍后继续执行。

生成器

是用function*定义的函数,返回一个生成器对象,该对象用于控制函数的执行

yield

yield关键字用于暂停函数的执行并返回一个值。每当生成器遇到yield1语句,都会返回yield后面的值,并暂停执行,直到生成器的next()方法被调用。

25、async/await

是generator的语法糖。WHY?

  • async函数内置执行器,函数调用后会立即执行;Generator里需要配合next模块使用
  • 更好的语义,async表示有异步操作,await表示紧跟在后面的表达式需要等待结果
  • 返回值是promise。generator返回的是迭代器

26、requestAnimationFrame

在它之前,我们使用setTimeout/setInterval来编写JS动画。

编写动画的关键是循环间隔的设置,一方面循环间隔足够段,动画效果才能显得平滑流畅;另一方面,循环间隔要足够长,才能保证浏览器有能力渲染产生的变化。1000ms/60

setTimeout/setInterval有一个显著的缺点在于,时间是不精确的,只能保证延时或者间隔不小于设定的时间。因为它们只是把任务添加到了任务队列中,但是如果前面的任务还没有执行完成,就必须要等待。

requestAnimationFrame采用的是系统时间间隔,可以保持最佳绘制效率,不会因为间隔过短导致过度绘制,增加开销,也不会因为间隔过长导致动画卡顿不流畅。可以让各种网页动画有一个统一的刷新机制,从而节省系统资源,提高系统性能,改善视觉效果。

优点:

  • 不需要设置时间,采用系统时间间隔,达到最佳动画效果

  • 会把每一帧所有的DOM操作集中起来,再一次重绘或回流中完成

  • 如果运行在后台或隐藏的<iframe>里,会被暂停调用以提升性能

27、ES6和CommonJS的差异

🐄导入导出方式
  • ES6使用import/export关键字处理导入导出,default关键字处理默认导出
  • CommonJS使用require/module.exports用于导入和导出
🐄加载机制
  • ES6基于静态加载,在编译时就确定依赖关系,import会被提升,在文件顶部执行
  • CommonJS基于动态加载,只有代码执行到require时才会加载模块(同步加载)
🐄执行顺序和作用域
  • ES6:每个模块都有独立的顶层作用域,不会污染全局作用域
    • 自动启动严格模式,顶层this指向undefined
    • 如果多个模块引用同一个模块,会被缓存且加载一次
  • CommonJSthis指向当前模块,也有上述缓存机制
🐄使用场景
  • ES6更适合在现代浏览器中使用
  • CommonJS主要用于Node.js环境,适合服务器端开发
🐄兼容性
  • ES6如果在Node.js中使用需要开启“type:module”或者将文件扩展名设置为.mjs
  • CommonJS:Node.js默认支持CommonJS模块

28、事件代理

通过将事件处理程序添加到父级元素来管理多个子元素的事件,而不是给每个子元素分别绑定事件。可以提高性能,尤其是在动态添加或删除子元素的情况下。

💦原理

利用的是事件冒泡。当一个元素上的事件触发时,事件会从目标元素逐级向上冒泡到父级元素,直到document或者window为止。事件代理的机制是将事件处理程序绑定在一个父级容器上,事件触发时,通过父容器捕获事件,并通过判断事件目标(event.target)来确定触发的实际子元素。

💦实例

假设有一个列表,每个列表项都需要响应点击事件。

1
2
3
4
5
<ul id="list">
<li>Item1</li>
<li>Item2</li>
<li>Item3</li>
</ul>

可以将事件监听器放在列表的父容器,而不是单独给每个li单独绑定事件。

1
2
3
4
5
6
const list=document.getElementById('list');
list.addEventListener('click',function(event){
if(event.target.tagName==="li"){
console.log("Click");
}
});

如果动态添加元素到列表,新的子元素也会响应同样的实践

1
2
3
const newItem=document.createElement('li');
newItem.textContent='Item4';
list.appendChild(newItem);

29、代码分析

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
function Foo() {
getName = function() {console.log(1)};
return this;
}
Foo.getName = function() {console.log(2)};
Foo.prototype.getName = function() {console.log(3)};
var getName = function() {console.log(4)};
function getName() {console.log(5)};

Foo.getName();
getName();
Foo().getName();
getName();
new Foo.getName();
new Foo().getName();
new new Foo().getName();

30、JS执行上下文栈和作用域链

执行上下文

是当前JS代码被解析和执行时所在的环境的抽象概念,JS中运行任何代码都是在执行上下文中运行。分为:全局执行上下文和函数执行上下文。

执行上下文创建过程中,需要做:

  • 创建变量对象:初始化函数的参数args,提升函数声明和变量声明
  • 创建作用域链
  • 确定this的值
作用域

作用域负责收集和维护由所有声明的标识符(变量)组成的一系列查询,并实施一套严格的规则确定当前执行的代码对这些标识符的访问权限。

分为:

  • 全局作用域:代码中任何地方都可以访问(最外层的变量);在整个程序中始终存在直到页面或者脚本关闭;浏览器中是window对象的属性,node环境中是global对象的
  • 函数作用域:在函数中定义的变量只在函数内部有效
  • 块级作用域:在{}大括号内声明的变量和常量(使用let、const声明)仅在快内部有效,外部无法访问。(避免变量在循环或条件语句中意外污染全局作用域)
执行上下文栈(即执行栈)

也叫调用栈,用于存储在代码执行期间创建的所有执行上下文

  • 首次运行JS代码的时候,会创建一个全局的执行上下文并Push到当前的执行栈中,每当发生函数调用,引擎都会为该函数创建一个新的函数执行上下文并Push到当前执行栈的栈顶
  • 当栈顶的函数运行完成后,对应的函数执行上下文会从执行栈Pop出,上下文的控制权将移动到当前执行栈的下一个执行上下文。
作用域链

从当前作用域开始一层一层向上寻找某个变量,直到找到全局作用域还是没找到就宣布放弃。这种一层一层的关系就是作用域链。

31、WeakMap、WeakSet

用于存储对象的引用,同时不防止其被垃圾回收。

  • 键必须是对象,键是弱引用,意味着如果没有其他引用指向该对象,垃圾回收器可以自动回收这个对象,减少内存泄露的风险。
  • 不支持遍历。意味着不能获取其键或值的列表。

假设你在开发一个单页应用(SPA),在其中频繁创建和销毁 DOM 元素。你可能希望将某些数据与这些元素关联,但又不想让它们持续占用内存。

在某些情况下,你需要确保某个对象只能被存储一次。例如,在某些应用中,可能需要跟踪已处理的 DOM 元素或事件处理程序。

、事件循环

JavaScript 的事件循环(Event Loop)是理解其异步行为和非阻塞特性的核心概念。事件循环使得 JavaScript 能够处理事件和异步操作,尽管它是单线程的。

事件循环机制确保了 JavaScript 的异步执行能力,使其能够在单线程环境下有效地处理事件和异步任务。

JavaScript 有一个基于事件循环的运行时模型,事件循环负责执行代码、收集和处理事件以及执行队列中的子任务。这个模型与其他语言中的模型截然不同,比如 C 和 Java。

image-20241029125906648
🔺执行栈

JS使用执行栈来管理执行上下文;

当函数被调用时,会被推入栈顶;当函数执行完成后,会从栈中弹出。

🔺任务队列

当异步操作完成(如setTimeout、事件监听等),相关的回调函数会被放入任务队列;

任务队列是用于存放等待执行的任务的队列。

🔺微任务队列

微任务队列用于处理更高优先级的任务,通常包括Promise的回调函数。

微任务队列的执行优先级高于任务队列。

🐡事件循环的工作原理
  • 执行栈检查:

    • 如果执行栈为空,事件循环会检查微任务队列
  • 处理微任务:

    • 如果微任务队列不为空,事件循环会从微任务队列中取出任务并执行,直到微任务队列为空
  • 处理任务:

    • 之后,事件循环会从任务队列中取出一个任务并执行
  • 重复循环:

    • 重复以上步骤,直到所有任务和微任务都被执行完毕
1
2
3
4
5
6
7
8
9
10
11
12
13
setTimeout(()=>{
console.log(1)
},0);
Promise.resolve().then(()=>{
console.log(4);
})
async function (){
console.log(3);
await Promise.resolve(){
console.log(5);
}
}
console.log(2);

输出: 2 4 3 5 1

1
2
3
4
5
6
7
8
try{
setTimeout(function(){
console.log(2);
throw(1);
},0)
}catch(e){
console.log(e);
}

输出:2 undifined(由于异步,没catch到错误)

队列:

  • JS运行时包含一个待处理消息的消息队列。每一个消息都关联着用以处理这个消息的回调函数。在事件循环期间的某个时刻,运行时会开始处理最先进入队列的消息。被处理的消息会移出队列,并作为输入参数来调用与之关联的函数。正如前面提到的,调用一个函数总是会为其创造一个新的栈帧。
  • 函数的处理会一直到执行栈再次为空为止,然后事件循环将会处理队列中的下一个消息(如果还有的话)。

每一个消息完整地执行后,其他消息才会被执行。

当一个函数执行时,它不会被抢占,只有在它运行完毕之后才会去运行任何其他的代码,才能修改这个函数操作的数据。

在浏览器里,每当一个事件发生并且有一个事件监听器绑定在该事件上时,一个消息就会被添加进消息队列。如果没有事件监听器,这个事件将会丢失。所以当一个带有点击事件处理器的元素被点击时,就会像其他事件一样产生一个类似的消息。然而,一些事件同步发生,没有产生消息——例如,通过 click 方法模拟的点击。

函数 setTimeout() 接受的前两个参数:一个是待加入队列的消息,一个是时间值(可选,默认为 0)。这个时间值代表了消息被实际加入到队列的最小延迟时间。如果队列中没有其他消息并且栈为空,在这段延迟时间过去之后,消息会被马上处理。然而,如果有其他消息,setTimeout() 消息必须等待其他消息处理完。因此第二个参数仅仅表示最少延迟时间,而非确切的等待时间。

以 0 为第二参数调用 setTimeout() 并不表示在 0 毫秒后就调用回调函数。

其等待的时间取决于队列里待处理的消息数量。在下面的例子中,"这是一条消息" 将会在回调获得处理之前输出到控制台,这是因为延迟参数是运行时处理请求所需的最小等待时间,但并不保证是准确的等待时间。

基本上,setTimeout() 需要等待当前队列中所有的消息都处理完毕之后才能执行,即使已经超出了由第二参数所指定的时间

JavaScript 的事件循环模型与许多其他语言不同的一个非常有趣的特性是,它永不阻塞。处理 I/O 通常通过事件和回调来执行,所以当应用程序正等待一个 IndexedDB 查询返回或者一个 fetch() 请求返回时,它仍然可以处理其他事情,比如用户输入。


前端八股总结-JS(持续更新)
http://example.com/2024/10/23/前端八股总结-JS/
Author
Yaodeer
Posted on
October 23, 2024
Licensed under