一、数据类型
1、基础数据类型栈内存
Number、String、Boolean、Undefined、Null、Symbol(es6)、BigInt(es2020)
注意点:
- Symbol:用于创建唯一标识符,避免属性名冲突
let s1 = Symbol('foo'); - BigInt:表示超过 Number 安全范围的超大整数
100n, BigInt(9999999999999999)
2、引用数据类型堆内存
都属于 Object
-
对象:
{} -
数组:
[] -
函数:
function(){} -
日期:
new Date() -
正则:
/abc/
二、栈内存、堆内存
1、区别
-
栈(Stack) :放基本数据类型,存的是值本身
-
堆(Heap) :放引用数据类型,栈存的是地址,真正内容在堆里
2、特点
-
栈内存
-
空间小、速度快
-
自动分配、自动释放
-
按顺序存放,先进后出
-
存基本类型的值:Number、String、Boolean、Undefined、Null、Symbol、BigInt
-
-
堆内存
-
空间大、速度稍慢
-
无序存放
-
存对象、数组、函数等复杂数据
-
栈里只存堆的地址,真正内容在堆中
-
栈里存的是地址编号(#001、#002)
-
真正的数据放在堆里
-
栈通过地址指向堆
-
-
三、浅拷贝、深拷贝
1、浅拷贝
- 定义: 只拷贝第一层
- 浅拷贝方式:
- 展开运算符...
- Object.assign
- 数组slice
oldArr.slice() - 数组concat
oldArr.concat()
2、深拷贝
- 定义:所有层级都拷贝(完全独立)
- 深拷贝方式:
- JSON.parse(JSON.stringify())
不能拷贝函数、undefined、Symbol、循环引用; - 三方库Lodash
const newObj = _.cloneDeep(oldObj) - 结构化克隆
structuredClone()(ES 新 API):- 使用:structuredClone(oldObj)
- 优点:原生、支持大部分类型、无循环引用问题
- 缺点:不支持函数
- 手写:
function deepCopy(obj) { if(obj === null || typeof obj !== 'object') return obj; let newObj = Array.isArray(obj) ? [] : {}; for(let key in obj) { newObj[key] = deepCopy(obj[key]); } return newObj; }
- JSON.parse(JSON.stringify())
四、数据类型判断
1、typeof
作用:判断 基本类型 + 函数*
typeof 123 // 'number'
typeof 'abc' // 'string'
typeof true // 'boolean'
typeof undefined // 'undefined'
typeof function(){} // 'function'
缺点:
typeof null→'object'(坑!)- 无法区分 对象、数组,都返回
'object'
2、Array.isArray ()
Array.isArray([]) // true Array.isArray({}) // false
3、instanceof (判断引用类型)
作用:判断 对象是谁的实例
[] instanceof Array // true
{} instanceof Object // true
function(){} instanceof Function // true
缺点:不能判断基本类型
适合:判断引用类型(数组、对象、日期等)
4、Object.prototype.toString.call ()
作用:判断所有类型,包括内置对象
Object.prototype.toString.call(123) // "[object Number]"
Object.prototype.toString.call('abc') // "[object String]"
Object.prototype.toString.call([]) // "[object Array]"
Object.prototype.toString.call({}) // "[object Object]"
Object.prototype.toString.call(null) // "[object Null]"
Object.prototype.toString.call(undefined) // "[object Undefined]"
适合:需要精准判断所有类型时
总结:
-
- 判断基本类型、函数:👉 typeof
-
- 判断数组 👉 Array.isArray()
-
-
判断纯对象 {}
js
typeof obj === 'object' && obj !== null && !Array.isArray(obj)
-
-
- 最精准判断所有类型 👉 Object.prototype.toString.call()
五、数组、伪数组/类数组
-
伪数组/类数组 :
结构长得像数组的普通对象,有数字下标 + 有 length 属性
-
常见 :
arguments、document.querySelectorAll获取的 DOM 集合let fakeArray = { 0: 'a', 1: 'b', 2: 'c', length: 3 }
注意点:arguments类数组,数据格式是
function fn() {
console.log(arguments) // arguments函数自带的,存放参数
}
fn(1, 2, 3)
六、var、let、const 的区别
1、var:函数级作用域,可重复声明,存在变量提升
```js
1、函数作用域,,不认 if /for/ 大括号 {}
if (true) {
var a = 10
}
console.log(a) // 10 ✅ 能访问到!
2、只有函数能拦住
function fn() {
var a = 10
}
console.log(a) // 报错 ❌ 访问不到
3、可重复声明
var a = 1
var a = 2
var a = 3
console.log(a) // 3 ✅ // 重复声明,报错
```
变量提升:
2、let:块级作用域,不可重复声明,存在暂时性死区
// 直接报错,暂时性死区
console.log(a); // 报错:Cannot access 'a' before initialization
let a = 10;
3、const:块级作用域,声明必须赋值,不可修改引用(但对象可改属性)
七、闭包
函数嵌套函数,内部函数引用了外部函数的变量,就形成了闭包
1、作用:
- 让函数外部能访问函数内部的变量
- 让变量一直保存在内存中,不会被销毁
// 外部函数
function outer() {
// 外部函数的变量
let num = 100
// 内部函数(闭包核心)
function inner() {
// 内部函数 使用了 外部变量
console.log(num)
}
// 把内部函数 return 出去
return inner
}
// 接收返回的 inner 函数
const fn = outer()
fn() // 100 ✅ 能访问到 outer 里的 num
// 为什么能一直累加?因为闭包让 `count` **常驻内存**,不会消失
function add() {
let count = 0 // 这个变量不会被销毁!
return function() {
count++
console.log(count)
}
}
const fn = add()
fn() // 1
fn() // 2
fn() // 3
函数add执行的流程
-
const fn = add()
- 执行
add() - 创建
count = 0 - 返回内部函数
- add 执行完毕,再也不执行了!
- 执行
-
fn()
- 执行的是:return 出来的那个函数
- 根本不会再进 add 里面
- 更不会重新创建 count = 0
总结:
闭包会导致内存泄漏
八、数组常用的方法 (增删改排序)
- 1、push (末尾)
arr.push(3) - 2、unshift (开头)
arr.unshift(3) - 3、pop (删除最后一个)
arr.pop() - 4、shift (删除第一个)
arr.shift() - 5、splice (从哪删,删几个)
arr.splice(1,1) // 从下标1开始,删1个 → [1] - 6、concat (合并数组)
arr.concat([4,5]) - 7、join(数组转字符串)
arr.join('-') // "1-2-3" - 8、sort(排序)
arr.sort((a,b) => a-b) // 正序 - 9、reverse(反转)
arr.reverse() [2,1,3].reverse() =>[3,1,2] - 10、slice(截取)
arr.slice(1,3) // 开始,结束(不包含),截取一段,不影响原数组
注意点:不会改变原数组的是->concat、join、slice
九、遍历数组的方法
- forEach()
- map()
- filter()
- find()
- findIndex()
- some()
有一个满足条件 就返回 true - every()
全部满足条件 才返回 true - reduce()
reduce(回调函数,初始值);
// 回调函数
('上一次计算结果',item)=>{}
// 初始值
从什么值开始累加 ()
const sum = [0,1,2].reduce((total, item) => total + item, 3)
sum = 6
- for循环(
break、continue、return)
function testLoop() {
const arr = [1, 2, 3, 4, 5];
for (let i = 0; i < arr.length; i++) {
if (i === 2) {
// 在这里分别测试 break、continue、return
// break; 跳出整个循环
// continue; 跳出本次,进入下一次
// return; 结束整个函数
}
console.log(arr[i]);
}
console.log("循环结束,继续执行函数后面的代码");
}
testLoop();
10.for…in((key==下标,适合遍历对象)
for (let key in arr) {
console.log('值:', arr[key]);
}
11.for…of(break、continue)
| 方法 | 性能 | 能否提前退出 | 性能差异根本原因 |
|---|---|---|---|
| for 循环 | 最快 | 能 | 纯原生循环,无函数调用、无额外逻辑、无封装,CPU 执行成本最低 |
| for…of | 很快 | 能 | 遍历数组时会被引擎直接优化为接近原生 for 循环的执行逻辑,无明显额外开销(for of是语法,引擎能识别) |
| find / findIndex / some / every | 快 | 能 | 有回调函数调用开销,但满足条件提前终止遍历,实际执行次数少 |
| reduce | 中等 | 不能 | 有回调开销 + 累计值读写,必须完整遍历,无提前退出机会 |
| forEach / map / filter | 较慢 | 不能 | 每一项都产生函数调用 + 作用域切换开销,且必须完整遍历全部元素 |
| for…in | 最慢 | 能 | 会遍历原型链、key 转为字符串、做大量属性检查,冗余操作极多 |
十、switch(多条件判断===if else)
let score = 85;
switch (true) {
case score >= 90:
console.log('优秀');
break;
case score >= 80:
console.log('良好');
break;
case score >= 60:
console.log('及格');
break;
default:
console.log('不及格');
}
十一、js中假值
JS 规定:只有下面这 6 种值是假值(falsy) ,除此之外全是真值:
false0、-0''、""(空字符串)nullundefinedNaN
十二、运算符
1、==、===
-
==会转换数据类型 -
null/undefined特殊情况console.log(null == undefined); // true → == 认为它们相等 console.log(null === undefined); // false → === 类型不同,不相等 -
空字符串、0陷阱console.log('' == 0); // true console.log('' === 0); // false
2、!=、!==
差异:!=会先进行类型转换
3、!、!! (布尔判断、取反)
4、?.、??、??=、||、&&
-
?.:// 找不到user返回undefined,往下也如此 const name = user?.info?.name; -
??: 和||类似// 只有`null`、`undefined`时,`??`才取默认值 false ?? true // false null ?? '默认' // '默认' undefined ?? '默认' // '默认'
5、++、--、
-
i++// **返回原来的值**,再自身 +1 let i = 5 let a = i++ console.log(a) // 5(先赋值) console.log(i) // 6(后自增) -
++i// 先**自身 +1**,再返回新值 let i = 5 let a = ++i console.log(a) // 6(先自增,再赋值) console.log(i) // 6
十三、对象创建的方式
1. 字面量
const obj = {}
- 本质:
new Object()的语法糖 obj.__proto__ === Object.prototype
2. new Object()
const obj = new Object()
-
标准构造函数实例化
-
obj.__proto__ === Object.prototype
十四、原型、原型链
1、原型
简单:每个对象都有 __proto__,指向构造函数的 prototype
更准确的说:JS 里一切对象(实例、函数、数组、普通对象)都有 proto,都指向 “创建它的那个构造函数” 的 prototype
2、原型链
每个对象都有 __proto__,指向构造函数的 prototype,而prototype也是个对象也有 __proto__。一直往上找,直到 __proto__ === null,这条链路就是原型链
十五、构造函数
1、定义构造函数
// 约定:构造函数名首字母大写(只是规范,不是强制)
function Person(name, age) {
// this 指向即将创建的新对象
this.name = name;
this.age = age;
}
// 定义公有方法
Person.prototype.sayHi = function() {}
2、特点
-
首字母大写:只是约定,方便一眼认出是构造函数
-
内部用 this:给未来的对象添加属性 / 方法
-
不用 return:默认返回新对象
-
可以传参,实现批量创建同类对象
3、和普通函数区别
-
调用时前面加个
new -
构造函数优势:原型共享 + 统一类型识别。
// 普通函数
function createPerson(name) {
const obj = {}; // 手动创建
obj.name = name;
obj.say = function() {
console.log(this.name);
};
return obj; // 手动返回
}
const p1 = createPerson('张三');
const p2 = createPerson('李四');
缺点:每次调用都**新建一个 say 方法**,浪费内存
4、现在替代写法
// JS 现在更推荐用 class,本质还是构造函数,只是写法更优雅:
class Person {
constructor(name, age) {
this.name = name;
this.age = age;
}
sayHi() {
console.log(this.name);
}
}
const p = new Person('王五', 22);
为什么class更好,简要分析下,后面会细讲
-
写法清晰,语义更强
一眼看出非普通函数、constructor负责初始化、结构更清晰
-
继承简单,用
extends// Es5原型继承 = this指向+子赋值父prototype+子constructor重新赋值子构造器
function Person(name, age) { this.name = name this.age = age } Person.prototype.sayHi = function() { console.log(`Hi, I'm ${this.name}`) } // 1. 写子类构造函数 function Student(name, age, score) { // 2. 借用父类构造函数(手动绑定 this) Person.call(this, name, age) // 3. 加自己的属性 this.score = score } // 4. 手动绑定原型链(最容易写错的地方) Student.prototype = Object.create(Person.prototype) Student.prototype.constructor = Student // 5. 给子类加方法 Student.prototype.study = function() { console.log(`${this.name} 考了 ${this.score} 分`) } // 6.调用 const s = new Student('小明', 18, 100) s.sayHi() // 继承来的 s.study()// Class = extends+super
class Person { constructor(name, age) { this.name = name; this.age = age; } sayHi() { console.log(`Hi, I'm ${this.name}`); } } // 子类继承(一句话) class Student extends Person { constructor(name, age, score) { super(name, age); // 自动调用父类构造 this.score = score; } study() { console.log(`${this.name} 考了 ${this.score} 分`); } }
十六、new关键字
1、创建一个空的全新对象 const obj = {};
2、链接原型
把这个空对象的 __proto__ 指向构造函数的 prototype:
obj.__proto__ = Person.prototype;
3、绑定 this
执行构造函数,并把空对象绑定为构造函数里的 this
4、返回实例对象
-
如果构造函数没有手动返回对象 / 函数,就自动返回第 1 步创建的对象;
-
如果构造函数手动返回了对象 / 函数,则以手动返回的为准(基本类型无效,仍返回新对象)。
总结:
1.造空对象 → 2. 连原型 → 3. 绑 this → 4. 返对象
function myNew(constructor, ...args) {
// 1. 创建空对象
const obj = {};
// 2. 链接原型
obj.__proto__ = constructor.prototype;
// 3. 绑定 this 并执行构造函数
const res = constructor.apply(obj, args);
// 4. 返回:如果构造函数返回对象则用它,否则返回新对象
return res instanceof Object ? res : obj;
}
// 测试
const p2 = myNew(Person, "李四", 20);
p2.sayHi(); // 你好,我是李四
十七、this关键字
1、全局环境 / 普通函数直接调用 → 指向 window(浏览器)
// 全局 this
console.log(this); // window
// 普通函数
function fn() {
console.log(this);
}
fn(); // window
意思:没人指定调用者,默认就是全局对象 window
2、对象方法调用 → 指向调用该方法的对象
const obj = {
name: "小明",
say: function () {
console.log(this.name);
}
};
obj.say(); // 小明 → this 指向 obj
3、构造函数 + new → 指向 新创建的实例对象
function Person(name) {
this.name = name; // this 是新实例
}
const p = new Person("小红");
console.log(p.name); // 小红
4、call /apply/bind → 指向 你传入的第一个参数
function fn() {
console.log(this);
}
const obj = { a: 1 };
fn.call(obj); // this → obj
fn.apply(obj); // this → obj
fn.bind(obj)(); // this → obj
5、箭头函数 → 指向 外层作用域的 this
const person = {
name: '张三',
say: function () {
// 普通函数,this = person
setTimeout(() => {
// 箭头函数,this指向外层say的this
console.log(this.name); // 张三
}, 1000);
}
};
person.say()
// 箭头改成普通函数
const person = {
name: '张三',
say: function() {
setTimeout(function() {
console.log(this.name)
// undefined,因为 this 变成 window 了(无调用者)
}, 1000)
}
}
总结:
-
普通函数:谁调用我,我指向谁
-
构造函数:new 我,我指向新对象
-
call/apply/bind:你让我指向谁,我就指向谁
-
箭头函数:我没有 this,我继承外面的 this
-
全局 / 自己调用:指向 window
十八、call、apply、bind
1、call 立刻执行函数,手动改 this,参数一个个传
fn.call(this指向的对象, 参数1, 参数2, ...)
2、apply 立刻执行函数,手动改 this,参数用数组传
fn.apply(this指向的对象, [参数1, 参数2, ...])
3、bind 不立刻执行,返回一个永久绑定 this的新函数
const newFn = fn.bind(this指向的对象, 参数1, 参数2, ...)
总结:
const obj = { name: 'obj' }
function fn(a, b) {
console.log(this, a, b)
}
-
fn.call(obj, 1, 2)→ 立即执行,this=obj -
fn.apply(obj, [1,2])→ 立即执行,this=obj -
const newFn = fn.bind(obj,1,2)→ 不执行,返回新函数 -
newFn()→ 执行,this 永远是 obj
十九、什么是作用域?
-
全局作用域
-
函数作用域
-
块级作用域(let/const)- ES6 新增
{}包裹的代码块(if、for、while、switch 等)if (true) { // 块级作用域 let a = 10; const b = 20; var c = 30; // var 没有块级作用域 } console.log(a); // 报错 ❌ console.log(b); // 报错 ❌ console.log(c); // 30 ✔️ -
模块作用域-ES6 - 新增
使用
export/import的独立 JS 模块// a.js 模块 let msg = "模块私有"; export function say() { console.log(msg); } // b.js import { say } from './a.js'; say(); // 可以用 console.log(msg); // 报错 ❌
作用域链:
-
先找当前作用域
-
找不到 → 找外层作用域
-
一直找到全局作用域
-
都找不到 → 报错
is not definedlet global = "全局"; function outer() { let outerVal = "外层"; function inner() { let innerVal = "内层"; // 查找顺序:inner → outer → global console.log(innerVal); // 内层 console.log(outerVal); // 外层 console.log(global); // 全局 } inner(); } outer();
二十、什么是变量提升?
var 声明的变量会被提升到作用域顶部,但赋值不提升
let/const 不存在变量提升,存在暂时性死区
二十一、箭头函数和普通函数区别?
-
没有自己的 this
-
没有 arguments
-
不能当构造函数
-
没有 prototype
二十二、什么是原型继承?
// 通过原型链让子类拥有父类的属性和方法
Child.prototype = new Parent()
二十三、Promise 有哪些状态?
-
pending
-
fulfilled
-
rejected
二十四、Promise 常用方法?
-
.then() -
.catch() -
.finally() -
Promise.all()-
全部 fulfilled → 返回结果数组
-
任意一个 rejected → 立即失败,进入 catch
-
-
Promise.allSettled()-
所有 Promise 结束后才返回
-
返回数组,每个对象形如:
-
成功:
{ status: 'fulfilled', value: ... } -
失败:
{ status: 'rejected', reason: ... }
-
二十五、Es6新增属性、方法
1、变量声明
let / const
2、解构赋值
- 对象解构
const { name, age } = user; - 数组解构
const [a, b, c] = arr; - 函数参数解构
function fn({ name }) { console.log(name); }
3、字符串新增方法
-
includes(str):判断是否包含指定字符串,返回布尔值 -
startsWith(str):判断是否以指定字符串开头 -
endsWith(str):判断是否以指定字符串结尾 -
repeat(n):重复字符串 n 次 -
模板字符串
` `${变量}`:支持换行、直接拼接变量
4、数组新增方法
-
扩展运算符
...const arr = [1,2,3]; const newArr = [...arr]; // 拷贝 const arr2 = [...arr, 4,5]; // 合并 -
Array.from(类数组):把类数组 / 伪数组转成真正的数组// 可迭代对象- 能被for of遍历的 Array.from('hello') // ['h','e','l','l','o'] -
Array.of():创建数组(避免 new Array () 歧义)Array.of(1,2,3) // [1,2,3] -
find(callback):返回第一个符合条件的元素 -
findIndex(callback):返回第一个符合条件的索引 -
some(callback):只要一个满足就返回 true -
every(callback):全部满足才返回 true -
flat(n):数组扁平化(n 为深度,默认 1,Infinity全拍平)const arr = [1, [2, [3, 4]]]; arr.flat(); // 默认只拍平 1 层 // [1, 2, [3,4]] arr.flat(2); // 拍平 2 层 // [1,2,3,4]
5、函数新增
- 箭头函数
() => {} - 函数默认参数
function fn(a = 10) {} rest参数...args接收剩余参数:function fn(...args) {}
6、对象新增
-
简洁表示法
const name = "小明"; const obj = { name, fn(){} }; // 等同于 {name:name, fn:function(){}} -
属性名表达式
const key = "age"; const obj = { [key]: 20 }; -
对象方法
-
Object.assign(目标, 源1, 源2):合并 / 拷贝对象 -
Object.keys(obj):获取所有键 -
Object.values(obj):获取所有值 -
Object.entries(obj):转成[[key,val],...]const entries = Object.entries({ name: '张三', age: 20, gender: '男' }); console.log(entries); 打印: [ ['name', '张三'], ['age', 20], ['gender', '男'] ]
-
7、Set / Map 数据结构
-
Set
去重const arr = [1,2,2,3]; const s = new Set(arr); const newArr = [...s]; // [1,2,3]方法:
add()、delete()、has()、clear()s.add(2); s.delete(2); // 删除集合里的某个元素 console.log(s.has(1)); // true **判断是否包含**某个值 s.clear(); // 清空整个 Set -
Map
键可以是任意类型Map 的 key 可以是对象、函数、DOM 元素
const m = new Map(); m.set("name", "小明"); m.get("name");
8、Promise
```js
// 解决回调地狱,处理异步请求
new Promise((resolve,reject)=>{
setTimeout(()=>resolve(1))
}).then(res=>{})
```
9、class 类(面向对象)
class Person {
constructor(name){ this.name = name; }
say(){}
}
10、模块化 import /export
export default fn;
import fn from './a.js';
高频速记清单(面试 / 工作核心)
let/const- 解构赋值、模板字符串
- 扩展运算符
... - 数组:
find、flat、includes - 对象:
Object.assign、keys/values/entries - 箭头函数
Set数组去重Promiseclassimport/export
二十六、继承(实现继承的几种方式)
1、原型链继承:
function Parent() {
this.name = 'Parent';
}
function Child() {
this.age = 10;
}
Child.prototype = new Parent();
const child = new Child();
console.log(child.name); // 可以访问到父类的属性
2、构造函数继承:
function Parent(name) {
this.name = name;
}
function Child(name, age) {
Parent.call(this, name);
this.age = age;
}
const child = new Child('Child', 10);
console.log(child.name); // 可以访问到父类的属性
3、ES6 Class继承:
class Parent {
constructor(name) {
this.name = name;
}
}
class Child extends Parent {
constructor(name, age) {
super(name);
this.age = age;
}
}
const child = new Child('Child', 10);
console.log(child.name); // 可以访问到父类的属性
4、组合继承(原型链继承和构造函数继承的结合):
function Parent(name) {
this.name = name;
}
Parent.prototype.sayHello = function() {
console.log('Hello, ' + this.name);
}
function Child(name, age) {
Parent.call(this, name);
this.age = age;
}
Child.prototype = new Parent();
Child.prototype.constructor = Child;
const child = new Child('Child', 10);
console.log(child.name); // 可以访问到父类的属性
child.sayHello(); // 可以调用父类的方法
二十七、script标签中的async和defer的作用
这两个属性都是用来控制外部脚本(必须带 src)的加载和执行时机,解决JS 阻塞 HTML 解析和渲染的问题,让页面加载更流畅。
核心前提:
浏览器解析 HTML 时,默认遇到 <script src="..."> 会停止解析 HTML,先下载脚本 → 执行脚本 → 再继续解析 HTML,容易导致页面白屏。
1、默认情况(无 async/defer)
<script src="script.js"></script>
- 浏览器解析 HTML 到这里
- 暂停解析 HTML
- 立即下载 JS 文件
- 下载完成后立即执行 JS
- 执行完毕,才继续解析 HTML
❌ 缺点:阻塞页面渲染,大脚本会让页面长时间白屏。
2、defer 属性(延迟执行)
<script src="script.js" defer></script>
核心特点
- 异步下载:下载 JS 时,浏览器不停止解析 HTML
- 延迟执行:等整个 HTML 文档完全解析完成后,再执行 JS
- 按顺序执行:多个带 defer 的脚本,按照书写顺序依次执行
- 只对外部脚本生效(必须有 src)
执行顺序图解
HTML解析 → 并行下载JS → HTML解析完毕 → 执行JS
3、async 属性(异步执行)
<script src="script.js" async></script>
核心特点
- 异步下载:下载 JS 时,浏览器不停止解析 HTML
- 立即执行:下载完成后立刻执行,不管 HTML 是否解析完
- 乱序执行:多个带 async 的脚本,谁先下载完谁先执行
- 只对外部脚本生效
- 如果同时写
async defer,浏览器优先使用 async
执行顺序图解
HTML解析 → 并行下载JS → JS下载完成 → 暂停HTML,执行JS → 继续解析HTML
使用建议:
-
用 defer:脚本需要操作 DOM / 依赖其他脚本 / 必须按顺序执行(如框架、插件)
-
用 async:脚本完全独立,不依赖任何代码,也不操作页面 DOM(如统计、埋点)
-
都不用:小体积脚本,或脚本必须在某个元素前执行
总结:
-
defer:异步下载,HTML 解析完再执行,保顺序
-
async:异步下载,下载完立刻执行,乱序
-
核心作用:避免 JS 阻塞 HTML 解析,提升页面加载速度
二十八、 http和https的区别
http:浏览器和服务器之间约定好的 “说话规则”
-
HTTP:快、不安全、明文
-
HTTPS:加密、安全、需要证书、现在互联网主流
注意点:TCP/IP 负责通,HTTP 负责内容格式。
二十九、 Tcp/Ip三次握手,四次挥手
三次握手(建立连接)
- 客户端发:我要连
- 服务端回:可以,你确认下
- 客户端再回:收到,连接成功
四次挥手(断开连接)
- 客户端发:我发完了,要关
- 服务端回:知道了,我收尾
- 服务端发:我也发完了,可以关
- 客户端回:收到,断开连接
三十、 hash和history模式的区别
- url显示:hash携带“#”,history不携带
- 路由更新原理:hash是通过监听hashChange,history是监听popState事件
- 兼容性:hash支持在不同浏览器和服务器环境下,history不支持低版本的浏览器(能兼容到IE10) 注意点:
// hash window.location.hash+hashchange
window.addEventListener('hashchange', () => {
// 拿到最新的 hash
const path = window.location.hash.slice(1)
// 匹配路由 → 渲染组件
})
history是靠pushState +replaceState
history.pushState({}, '', '/home')
history.replaceState({}, '', '/login')
// pushState和replaceState类似,但replaceState不会新增记录,不能后退
- 服务器端配置支持:history需要服务器配置(在刷新页面时,它会向服务器发送 GET 请求,但此时服务器并没有配置相应的资源来匹配这个请求,因此返回 404 错误)---blog.csdn.net/qq_38290251…
三十一、同源策略
同源策略是浏览器的核心,如果没有同源策略会遭受网络攻击。
主要指的是 协议+域名+端口号 三者一致
备注:非同源会引起跨域。如何处理:
-
CORS(跨源资源共享)
- 服务器在响应头添加
Access-Control-Allow-Origin,明确允许哪些域访问。 - 最标准、最推荐。
- 服务器在响应头添加
-
JSONP
-
利用
<script>可跨域嵌入的特性,仅支持 GET 请求。<script> // 提前定义好一个函数 function getData(res) { console.log('拿到跨域数据:', res) } </script> <script src="http://localhost:3000/api/jsonp?callback=getData"></script>
-
-
代理服务器(Proxy)
- 前端 → 同源代理 → 转发到目标跨域服务器。
- 开发常用:Webpack Dev Server、Vite Proxy。
三十二、浏览器缓存策略
强缓存(本地缓存)/ 协商缓存(弱缓存)。
强缓:不发起请求,直接使用缓存里的内容,浏览器把js css image等存到内存中,下次用户访问直接从内存中取,提高性能。
协缓:需要向后台发送请求,通过判断来决定是否使用缓存,如果请求内容没有变化,则返回304,浏览器就用缓存里的内容。
三十三、事件机制
JavaScript 事件机制是浏览器与用户交互的核心原理,描述了事件在 DOM 元素之间的传递规则,是前端开发必须掌握的核心知识点。
事件流:事件(点击、输入、滚动、鼠标移动等)发生后,不会只作用于目标元素,而是会在DOM 树的元素之间传递,这个传递过程就是事件流
1、三个阶段(W3C 标准)
事件触发后,会完整经历 3 个阶段(捕获阶段 → 目标阶段 → 冒泡阶段):
- 捕获阶段(Capture Phase)事件从最外层根元素(window/document) 向内传递,直到触发事件的目标元素。
- **目标阶段(Target Phase)**事件到达用户实际操作的元素(点击 / 输入的元素),执行该元素的事件监听。
- 冒泡阶段(Bubbling Phase)事件从目标元素向外传递,回到最外层根元素。
2、两个核心传播模式
-
事件冒泡(默认模式)
- 特点:事件由内向外传递(子元素 → 父元素 → 祖先元素 → document → window)
- 默认行为:我们平时写的
addEventListener不写第三个参数,默认就是冒泡模式。
代码示例:
<div id="parent"> 父元素 <button id="child">子按钮</button> </div> <script> const parent = document.getElementById('parent'); const child = document.getElementById('child'); // 冒泡监听(第三个参数 false/不写) parent.addEventListener('click', () => { console.log('父元素 冒泡点击'); }); child.addEventListener('click', () => { console.log('子元素 冒泡点击'); }); // 点击按钮:先打印 子元素 → 再打印 父元素 </script> -
事件捕获
- 特点:事件由外向内传递(window → document → 祖先元素 → 父元素 → 子元素)
- 用法:
addEventListener第三个参数传true
代码示例:
js
// 捕获监听 parent.addEventListener('click', () => { console.log('父元素 捕获点击'); }, true); // 开启捕获 child.addEventListener('click', () => { console.log('子元素 捕获点击'); }, true); // 点击按钮:先打印 父元素 → 再打印 子元素
3、关键 API
-
阻止事件冒泡
event.stopPropagation():阻止事件继续向外 / 向内传播,只执行当前元素的事件。js
child.addEventListener('click', (e) => { e.stopPropagation(); // 阻止冒泡 console.log('子元素点击'); }); // 点击按钮:只打印子元素,父元素不会触发 -
阻止默认行为
event.preventDefault():阻止浏览器自带的默认事件(如表单提交、a 标签跳转、右键菜单)js
const link = document.querySelector('a'); link.addEventListener('click', (e) => { e.preventDefault(); // 阻止a标签跳转 console.log('阻止跳转'); }); -
事件委托(利用冒泡)
核心用途:给父元素绑定事件,利用冒泡机制监听子元素触发的事件。优点:
- 节省内存(不用给每个子元素绑事件)
- 支持动态生成的元素(新增子元素自动生效)
示例:
<ul id="list"> <li>选项1</li> <li>选项2</li> </ul> <script> // 给父ul绑定事件,委托给所有子li document.getElementById('list').addEventListener('click', (e) => { // 判断点击的是不是li if(e.target.tagName === 'LI'){ console.log('点击了:', e.target.textContent); } }); </script>
总结:同时绑定捕获 + 冒泡时,执行规则:
window捕获 → document捕获 → 父元素捕获 → 子元素(目标)捕获/冒泡 → 父元素冒泡 → document冒泡 → window冒泡
三十四、排队机制
所有任务都要排队执行,先同步、后异步,异步任务分优先级排队
1、3 个关键队列
JS 执行任务时,会维护 3 个「排队队列」,严格按顺序执行:
- 调用栈(同步队列) :执行同步代码(直接运行的代码,无延迟)
- 微任务队列(高优先级异步) :执行微任务(优先级最高,插队执行)
- 宏任务队列(低优先级异步) :执行宏任务(普通异步,最后执行)
2、执行规则
-
先清空 调用栈 所有同步任务
-
再清空 微任务队列 所有微任务
-
最后取 宏任务队列 中第一个宏任务执行
-
循环往复(执行完一个宏任务,立刻再检查微任务)
3、宏任务 vs 微任务
-
宏任务(普通异步,排队靠后)
setTimeout/setInterval(定时器)- 接口请求(
fetch/axios/ajax) - DOM 事件(点击、滚动)
- I/O 操作(文件读取)
-
微任务(高优先级异步,插队执行)
Promise.then()/Promise.catch()/Promise.finally()async/await(底层就是 Promise 微任务)queueMicrotask()
注意点:
- 微任务:完全由 JS 引擎自己 产生并管理
- 宏任务:由 浏览器宿主环境 产生(浏览器给 JS 塞进来的)
4、常见面试题
-
第 1 题(基础版,和你给的一模一样)
console.log('1'); setTimeout(() => { console.log('2'); }, 0); Promise.resolve().then(() => { console.log('3'); }); console.log('4');输出顺序
1 → 4 → 3 → 2
解析
- 先执行所有同步:1、4
- 再清空微任务:3
- 最后执行宏任务:2
-
第 2 题(多个微任务 + 多个宏任务)
console.log('1'); setTimeout(() => console.log('2'), 0); setTimeout(() => console.log('3'), 0); Promise.resolve().then(() => console.log('4')); Promise.resolve().then(() => console.log('5')); console.log('6');输出顺序
1 → 6 → 4 → 5 → 2 → 3
解析
- 同步先跑:1、6
- 微任务全部清空:4、5
- 宏任务按顺序执行:2、3
-
第 3 题(async /await 必考)
async function fn() { console.log('1'); await Promise.resolve(); console.log('2'); } console.log('3'); fn(); console.log('4');输出顺序
3 → 1 → 4 → 2
解析
- await 前面是同步:1
- await 后面是微任务:2
- 同步顺序:3 → 1 → 4
- 最后微任务:2
-
第 4 题(宏任务里产生微任务)
console.log('1'); setTimeout(() => { console.log('2'); Promise.resolve().then(() => console.log('3')); }, 0); Promise.resolve().then(() => console.log('4')); console.log('5');输出顺序
1 → 5 → 4 → 2 → 3
解析
- 同步:1、5
- 微任务:4
- 宏任务:2
- 宏任务执行完 → 立刻检查微任务 → 3
-
第 5 题(综合最难面试题)
console.log('1'); setTimeout(() => { console.log('2'); Promise.resolve().then(() => console.log('3')); }, 0); Promise.resolve().then(() => { console.log('4'); setTimeout(() => console.log('5'), 0); }); console.log('6');输出顺序
1 → 6 → 4 → 2 → 3 → 5
三十五、Promise
Promise 是 JS 用来处理异步操作的对象,让回调地狱(多层嵌套)变成链式调用,代码更干净、更好读。
1、回调地狱
// 第一层:请求用户
getUser(function(user) {
// 第二层:用 userId 查订单
getOrder(user.id, function(order) {
// 第三层:查商品
getGoods(order.goodsId, function(goods) {
console.log(goods)
})
})
})
// 每一步都要等上一步完成,所以每一步都要写在上一步的回调里,这就形成了**回调地狱**
2、Promise 三种状态
Promise 一生只有 3 种状态,且一旦改变就不能再变:
- pending(等待中)—— 刚开始,还没结果
- fulfilled(成功)—— 操作完成,调用 resolve ()
- rejected(失败)—— 操作出错,调用 reject ()
// 创建 Promise
const p = new Promise((resolve, reject) => {
// 这里做异步操作
setTimeout(() => {
const success = true
if (success) {
resolve('成功啦!') // 成功 → 把结果传给 then
} else {
reject('失败了~') // 失败 → 把错误传给 catch
}
}, 1000)
})
// 使用 Promise
p.then(res => {
console.log(res) // 成功结果
}).catch(err => {
console.log(err) // 失败错误
}).finally(() => {
console.log('无论成功失败都会执行')
})
3、链式调用(最强大的地方)
// Promise 最牛的就是**链式写法**,彻底告别回调地狱
// 所有异步任务按顺序执行,代码扁平、清晰
request1()
.then(res1 => request2(res1))
.then(res2 => request3(res2))
.then(res3 => console.log('全部完成'))
.catch(err => console.log('任何一步出错都会进 catch'))
4、常用静态方法(工作高频)
-
Promise.all()等待所有 Promise 成功,一个失败就全部失败
Promise.all([p1, p2, p3]).then(res => { // res 是 [p1结果, p2结果, p3结果] }) -
Promise.allSettled()不管成功失败,全部执行完再返回
5、async/await(Promise 的语法糖)
async function getData() {
try {
const res = await axios.get('/api')
console.log(res)
} catch (err) {
console.log(err)
}
}
三十六、try catch
1、能捕捉的错误
-
变量未定义:
a is not defined -
类型错误:
null.xxx、undefined.xxx -
调用不是函数的东西:
123() -
访问不存在的下标等
-
手动 throw 抛出的错误
throw new Error('出错了') -
eval 里的同步错误
try { eval('abc') } -
绝大多数同步逻辑异常
只要是这一行代码立刻、马上、同步抛出的错误,try 包着就能 catch 到。
2、不能捕捉的错误
-
setTimeout回调 -
setInterval回调 -
原生 AJAX
onload/onerror -
事件监听
click等 -
语法错误
语法错误:代码还没开始跑,浏览器一读就发现写得不对,直接拒绝执行
// 代码还没开始跑,解析就挂了,根本进不去 try try { const a = // 语法错误 } catch (e) {} -
全局 / 跨域脚本错误(被浏览器限制)
- 别的域名加载的 JS 报错
- 报错信息会被模糊化,catch 拿不到详细堆栈
-
Promise 内部 reject(不使用 await 时)
// Promise 错误属于**微任务异步错误**,不是同步抛出 try { Promise.reject('err') } catch (e) { // 抓不到 } -
堆栈溢出 :有时能抓到,有时直接崩,看浏览器实现,不依赖它捕获
三十七、堆栈溢出
堆栈溢出只在一种核心场景出现:函数调用层级太深,或无限递归,导致调用栈被塞满,放不下新的函数调用
1、递归没有中止条件
// 自己调用自己,无限套娃,永远停不下来
function fn() {
fn()
}
fn()
2、递归有终止条件,但数据太大也会溢出
function factorial(n) {
if (n === 1) return 1
return n * factorial(n - 1)
}
factorial(100000) // 爆栈
3、两个函数互相调用,形成死循环
function a() { b() }
function b() { a() }
a()