JS常见八股总结(新手向)
1.闭包
1.1闭包的官方定义
闭包 = 函数 + 函数能够记住的周围变量
更直白地说:一个函数,即使在外层函数执行完毕后,仍然能访问外层函数的变量。
1.2闭包的三个条件
| 条件 | 说明 |
|---|---|
| 1. 函数嵌套 | 一个函数里面定义了另一个函数 |
| 2. 内部函数使用了外部函数的变量 | 比如 inner 用了 name |
| 3. 内部函数被返回(或传递到外部) | 这样外部才能调用它 |
1.3js计数器
function createCounter() {
let count = 0
return function() {
count++
return count
}
}
const counter = createCounter()
console.log(counter()) // 1
console.log(counter()) // 2
console.log(counter()) // 3
每次调用 counter(),count 都会增加。 count 被“关”在闭包里,外面无法直接访问,只能通过返回的函数操作它。
1.4闭包陷阱
for (var i = 0; i < 3; i++) {
setTimeout(() => {
console.log(i) // 打印 3, 3, 3,不是 0,1,2
}, 100)
}
JavaScript 是单线程的,等循环结束才开始定时器。 而且闭包让它记住了外层的 i = 两者共同导致了打印 3,3,3。

1.5闭包缺点:内存泄漏
通俗解释:用完了的内存没有被释放,程序占着不放,导致可用内存越来越少。
原因:闭包的特性是函数会一直持有它用到的外部变量,即使外层函数已经执行完了。
1.6如何避免闭包内存泄漏(口诀)
| 规则 | 说明 |
|---|---|
| 用完就置空 | 不需要的大对象手动设为 null |
| 及时解绑 | 不需要的事件、定时器要清理 |
| 注意循环引用 | DOM 和 JS 对象互相引用 |
| 用 let/const 而不是 var | 块级作用域更容易被回收 |
| 避免在闭包里存 DOM | 存了记得清 |
2.事件循环(Event Loop)
JavaScript 是单线程的,一次只能做一件事。遇到等一会儿的事(比如定时器、网络请求),就先放一边,等同步代码执行完,再回来做这些事。
Event Loop 就是 “干完手头的活,再去队列里拿下一个活” 这个循环过程。
两个关键词(先记这两个)
| 词 | 意思 | 例子 |
|---|---|---|
| 同步任务 | 立刻做的事 | console.log、for 循环、函数调用 |
| 异步任务 | 等一会儿做的事 | setTimeout、点击事件、网络请求 |
规则:永远先清空同步任务,再处理异步任务
2.1事件循环的异步微任务-----promise
事件循环(Event Loop)
├── 同步任务
└── 异步任务
├── 宏任务(setTimeout、事件回调)
└── 微任务(Promise.then) ← Promise 的 .then 属于这里
Promise 是什么(最简版)
Promise 是一个容器,里面装着一个“将来才会结束”的事情。
三种状态(记住名字就行)
-
pending:进行中
-
fulfilled:成功了
-
rejected:失败了
状态只能变一次,一旦改变就不会再变。
2.2promise基础语法
// 1. 创建 Promise
const myPromise = new Promise((resolve, reject) => {
// 做某件事
let success = true;
if (success) {
resolve('成功了'); // 成功
} else {
reject('失败了'); // 失败
}
});
// 2. 使用 Promise
myPromise
.then((result) => {
console.log(result);
})
.catch((error) => {
console.log(error);
});
执行流程(按顺序)
-
new Promise立即执行里面的函数 -
判断
success是true -
调用
resolve('成功了') -
跳过
reject -
走到
.then,把'成功了'传给result,打印 -
.catch不执行(因为没失败)
success = false:
走 reject('失败了')
.then 不执行
.catch 执行,打印 '失败了'
小练习
console.log('A'); // 同步
new Promise((resolve) => {
console.log('B'); // 同步!Promise 构造函数里的代码是立刻执行的
resolve('C');
}).then((res) => {
console.log(res); // 微任务,稍后执行
});
console.log('D'); // 同步
输出:A → B → D → C
为什么?
-
A同步,打印 -
new Promise里面的console.log('B')立即执行 -
resolve('C')只是标记成功,.then里的代码先不执行 -
D同步,打印 -
同步代码结束,执行微任务:打印
C
新手最常错的点
❌ 错误理解:Promise 里的所有代码都是异步的
javascript
// 错!这里面的代码是同步的
new Promise(() => {
console.log('我会立刻执行');
});
✅ 正确理解:只有 .then() .catch() .finally() 里面的才是异步(微任务)
结合事件循环的终极例子(面试常考)
console.log('1');
setTimeout(() => {
console.log('2');
}, 0);
Promise.resolve()
.then(() => {
console.log('3');
})
.then(() => {
console.log('4');
});
console.log('5');
输出:1 → 5 → 3 → 4 → 2
执行过程:
-
同步:1、5
-
清空微任务队列:3、4(按顺序)
-
执行一个宏任务:2
3.柯里化
// 正常写法:一次传两个参数
function add(a, b) {
return a + b;
}
add(3, 5); // 8
// 柯里化写法:一次传一个参数
function curriedAdd(a) {
return function(b) {
return a + b;
};
}
curriedAdd(3)(5); // 8
执行过程拆解
function curriedAdd(a) {
return function(b) {
return a + b;
};
}
const add5 = curriedAdd(5); // 先固定第一个参数是5
console.log(add5(3)); // 8
console.log(add5(10)); // 15
为什么要用柯里化?(实际用处)
用处1:固定某个参数,生成新函数
curriedAdd(3)(5)
// 第一步:curriedAdd(3) 执行
function curriedAdd(3) {
return function(b) {
return 3 + b;
};
}
// 返回的是:function(b) { return 3 + b; }
// 第二步:返回的函数再被调用,参数是 5
function(5) {
return 3 + 5; // 8
}
用处2:延迟执行
function multiply(a) {
return function(b) {
return a * b;
};
}
const double = multiply(2); // 还没算,只是存起来
const triple = multiply(3);
console.log(double(5)); // 10,现在才算
console.log(triple(5)); // 15
4.foreach和map的区别
const numbers = [1, 2, 3];
// forEach:不返回数组
const result1 = numbers.forEach((num) => {
return num * 2;
});
console.log(result1); // undefined
console.log(numbers); // [1, 2, 3](原数组没变)
// map:返回新数组
const result2 = numbers.map((num) => {
return num * 2;
});
console.log(result2); // [2, 4, 6]
console.log(numbers); // [1, 2, 3](原数组还是没变)
map返回新数组,forEach不返回东西(undefined)
5.原型链
JavaScript 里,几乎所有东西都是对象。对象之间通过"原型"连接起来,形成一条链。
5.1构造函数,实例对象,原型对象
5.11构造函数
就是一个普通函数,用来"生产对象"的。习惯首字母大写。
// 这是一个构造函数
function Person(name) {
this.name = name;
}
它和普通函数没区别,只是用来创建对象。
5.12实例对象
用
new关键字调用构造函数,生产出来的对象。
const xiaoming = new Person('小明');
const xiaohong = new Person('小红');
5.13原型对象
一个普通的对象,专门用来存放"所有实例共享的方法/属性"。
// 这个 {} 就是原型对象
Person.prototype = {
sayHi: function() {
console.log('你好');
}
};
5.2简单使用
const 公共方法 = {
sayHi: function() { console.log('你好'); }
};
const 小明 = { name: '小明' };
const 小红 = { name: '小红' };
// 把公共方法设为原型
Object.setPrototypeOf(小明, 公共方法);
Object.setPrototypeOf(小红, 公共方法);
小明.sayHi(); // 你好(小明自己没有 sayHi,去原型上找)
小红.sayHi(); // 你好
这里记住一些基础的概念:
Prototype:
1.Prototype他是{函数的}一个属性
2.Prototype是个对象
3.创建函数时默认添加Prototype属性
__proto__:
1.{对象}的属性
2.指向构造函数的prototype
3.obj.__proto__ ===test.prototype true
三个连接关系(背下来)
| 连接 | 怎么写 | 方向 |
|---|---|---|
| 实例对象 → 原型对象 | 实例.__proto__ | 实例找到自己的原型 |
| 构造函数 → 原型对象 | 构造函数.prototype | 构造函数找到自己的原型 |
| 原型对象 → 构造函数 | 原型对象.constructor | 原型找到自己的构造函数 |

5.3什么是链
function Person(name) {
this.name = name;
}
const p1 = new Person('小明');
// p1 的原型是 Person.prototype
console.log(p1.__proto__ === Person.prototype); // true
// Person.prototype 的原型是 Object.prototype
console.log(Person.prototype.__proto__ === Object.prototype); // true
// Object.prototype 的原型是 null(终点)
console.log(Object.prototype.__proto__); // null

这里贴一张黑马的图帮助理解
可以说这张图是最清晰好理解的了

5.4原型链的查找规则(核心)
当访问对象的一个属性/方法时,如果对象本身没有,就顺着原型链往上找,直到找到为止。找不到就返回 undefined。
6.深拷贝与浅拷贝
6.1简单说明
浅拷贝:只复制第一层,里面的东西还是共享的
深拷贝:全部复制,彻底分开,互不影响
const original = {
name: '小明',
age: 18,
address: {
city: '北京',
street: '长安街'
}
};
// 浅拷贝
const shallow = { ...original };
// 深拷贝(先记住这个简单方法)
const deep = JSON.parse(JSON.stringify(original));
// 修改原对象的第一层
original.name = '小红';
console.log(shallow.name); // '小明'(没变,第一层分开了)
console.log(deep.name); // '小明'(没变)
// 修改原对象的第二层(嵌套对象)
original.address.city = '上海';
console.log(shallow.address.city); // '上海'(变了!浅拷贝没分开)
console.log(deep.address.city); // '北京'(没变,深拷贝分开了)
关键点:
-
改第一层
name:浅拷贝不受影响 -
改第二层
address.city:浅拷贝跟着变了(因为是同一个对象)
6.2浅拷贝常见方法
const obj = { a: 1, b: { c: 2 } };
// 方法1:展开运算符
const copy1 = { ...obj };
// 方法2:Object.assign
const copy2 = Object.assign({}, obj);
// 方法3:数组的 slice(数组专用)
const arr = [1, 2, { a: 3 }];
const copy3 = arr.slice();
6.3深拷贝方法
6.3.1方法1:JSON.parse(JSON.stringify())(最常用,但有坑)
const obj = { a: 1, b: { c: 2 } };
const deep = JSON.parse(JSON.stringify(obj));
缺点(重要!):
const obj = {
fn: function() {}, // ❌ 函数会丢失
date: new Date(), // ❌ 日期会变成字符串
undef: undefined, // ❌ undefined 会丢失
nan: NaN, // ❌ NaN 变成 null
inf: Infinity, // ❌ Infinity 变成 null
reg: /abc/, // ❌ 正则变成空对象 {}
};
6.3.2structuredClone()(现代浏览器,推荐)
const obj = { a: 1, b: { c: 2 }, date: new Date() };
const deep = structuredClone(obj);
// 支持 Date、RegExp、Map、Set,比 JSON 方法强
6.3.3手写递归(面试常考)
function deepClone(obj) {
// 处理 null 或不是对象
if (obj === null || typeof obj !== 'object') {
return obj;
}
// 处理数组
if (Array.isArray(obj)) {
const arrCopy = [];
for (let i = 0; i < obj.length; i++) {
arrCopy[i] = deepClone(obj[i]);
}
return arrCopy;
}
// 处理普通对象
const objCopy = {};
for (let key in obj) {
if (obj.hasOwnProperty(key)) {
objCopy[key] = deepClone(obj[key]);
}
}
return objCopy;
}

返回 { a: 1, b: { c: 2 } }(新对象)
6.3.3手写递归(面试常考)6.4使用场景区分
什么时候用浅拷贝?什么时候用深拷贝?
| 场景 | 用什么 | 原因 |
|---|---|---|
| 对象只有一层(没有嵌套) | 浅拷贝就够了 | 没有深层共享的问题 |
| 对象有嵌套,且需要完全独立 | 深拷贝 | 改深层数据不影响原对象 |
| React state 更新 | 浅拷贝(配合展开) | React 只比较引用,但嵌套对象要小心 |
7.Json.stringfy的弊端
1.无法处理循环引用
2.不支持特定的数据类型
3.丢失对象的原型链和方法
4.性能问题:在处理大量对象或者深层嵌套对象的时候,可能会消耗大量的时间和内存
8.数据类型
8.1总览
JavaScript 有 8 种数据类型:
| 分类 | 类型 | 英文 | 例子 |
|---|---|---|---|
| 基本类型(7种) | 数字 | number | 1, 3.14, NaN |
| 字符串 | string | 'hello', "world" | |
| 布尔 | boolean | true, false | |
| 未定义 | undefined | undefined | |
| 空 | null | null | |
| 符号 | symbol | Symbol('id') | |
| 大整数 | bigint | 9007199254740991n | |
| 引用类型(1种) | 对象 | object | {}, [], function(){} |
记忆口诀: 数、字、布、未、空、符、大 + 对象
8.2基本类型vs引用类型
区别1:存储方式不同
// 基本类型:值存在变量里
let a = 10;
let b = a;
b = 20;
console.log(a); // 10(没变)
console.log(b); // 20
// 引用类型:地址存在变量里,值存在别处
let obj1 = { name: '小明' };
let obj2 = obj1;
obj2.name = '小红';
console.log(obj1.name); // '小红'(变了!)
console.log(obj2.name); // '小红'
区别2:比较方式不同
// 基本类型:比较值
console.log(10 === 10); // true
console.log('a' === 'a'); // true
// 引用类型:比较地址(是不是同一个对象)
const objA = { name: '小明' };
const objB = { name: '小明' };
console.log(objA === objB); // false(两个不同的对象)
console.log(objA === objA); // true(同一个对象)
8.3堆和栈的区别
什么是栈(Stack)和堆(Heap)?
| 对比项 | 栈(Stack) | 堆(Heap) |
|---|---|---|
| 存储内容 | 基本类型值、引用类型的地址 | 引用类型的实际数据 |
| 空间大小 | 小,固定 | 大,不固定 |
| 分配方式 | 自动分配,顺序存储 | 动态分配,随机存储 |
| 访问速度 | 快 | 慢 |
| 内存管理 | 自动释放(函数结束就清空) | 垃圾回收(GC)自动回收,但时间不确定 |
快速理解
let name = '小明'; // 基本类型
let age = 18; // 基本类型
let person = { // 引用类型
city: '北京',
job: '前端'
};

-
name和age的值直接存在栈里 -
person的栈里存的是一个地址(0x01),指向堆里的真实数据
8.4类型转换
1. 显式转换(手动)
// 转字符串
String(123); // '123'
(123).toString(); // '123'
// 转数字
Number('123'); // 123
Number('abc'); // NaN
parseInt('123px'); // 123
parseFloat('3.14'); // 3.14
// 转布尔
Boolean(0); // false
Boolean(1); // true
Boolean(''); // false
Boolean('hi'); // true
2. 隐式转换(自动,容易坑)
// 字符串 + 任何东西 → 字符串
console.log('1' + 2); // '12'
console.log('1' + true); // '1true'
// 数字 - * / → 转数字
console.log('6' - 2); // 4
console.log('6' * '2'); // 12
console.log('abc' - 1); // NaN
// 比较运算
console.log('5' > 3); // true('5' 转成 5)
console.log('5' > '10'); // true(字符串比较,比第一位 '5' > '1')
// == 会做类型转换
console.log(1 == '1'); // true(== 会转类型)
console.log(1 === '1'); // false(=== 不转类型,推荐用这个)
// 逻辑运算
console.log(1 && 2); // 2(返回最后一个真值)
console.log(0 || 'hi'); // 'hi'(返回第一个真值)
9.数据类型判断
判断数据类型:
1. typeof
- 优点:简单,判断基本类型
- 缺点:null 变 'object',无法区分对象/数组
2. instanceof
- 优点:判断引用类型的具体类型
- 缺点:不能判断基本类型
3. Array.isArray()
- 优点:准确判断数组
- 缺点:只能判断数组
4. Object.prototype.toString.call()(万能)
- 优点:能判断所有类型,包括 null
- 缺点:写法麻烦
实际开发推荐:
- 基本类型 → typeof
- 数组 → Array.isArray()
- 其他引用类型 → instanceof
- 需要完全准确 → Object.prototype.toString.call()
10.深拷贝如何解决循环引用
循环引用是什么?
对象里面的某个属性,又指回了自己(或间接指回自己),形成一个闭环。
const obj = { name: '小明' };
obj.self = obj; // 自己的 self 属性指向自己
// 现在 obj 变成了:
// obj {
// name: '小明',
// self: { name: '小明', self: { name: '小明', self: ... } } // 无限套娃
// }
问题1:JSON.stringify 会报错
因为 JSON 格式不支持循环引用(JSON 是树形结构,不能有环)。
问题2:普通递归深拷贝会死循环
怎么解决循环引用?
核心思想:记录已经拷贝过的对象,遇到重复的,直接返回之前拷贝的结果,不再递归。
Map做缓存(新手了解即可)
function deepClone(obj, cache = new Map()) {
// 处理基本类型
if (obj === null || typeof obj !== 'object') {
return obj;
}
// 检查是否已经拷贝过这个对象
if (cache.has(obj)) {
return cache.get(obj); // 直接返回之前拷贝的结果
}
// 处理数组
if (Array.isArray(obj)) {
const arrCopy = [];
cache.set(obj, arrCopy); // 先存起来
for (let i = 0; i < obj.length; i++) {
arrCopy[i] = deepClone(obj[i], cache);
}
return arrCopy;
}
// 处理普通对象
const objCopy = {};
cache.set(obj, objCopy); // 先存起来
for (let key in obj) {
if (obj.hasOwnProperty(key)) {
objCopy[key] = deepClone(obj[key], cache);
}
}
return objCopy;
}
现在只需要记住:在深拷贝过程中,每次遇到一个对象需要拷贝时,首先检查映射表中是否已经存在该对象的拷贝。如果存在,则直接返回拷贝副本,避免重复拷贝。如果不存在,则创建一个新的拷贝对象,并将原对象和拷贝对象记录到映射表中。
11.this指向和作用域
11.1常见
总结表(背下来)
| 调用方式 | this 指向 |
|---|---|
fn() | window(严格模式下是 undefined) |
obj.fn() | obj |
new fn() | 新创建的对象 |
| 箭头函数 | 定义时外面的 this |
这里特殊说明一下箭头函数:由于箭头函数呢没有this,那么基于闭包,就会从外层寻找this,而闭包属于词法作用域,词法作用域在编译时态下确定(js预编译),所以箭头函数this指向在编译时就确定,这也就是箭头函数this取决于定义位置而不是运行位置的原因
普通函数的 this 看怎么调用(运行位置决定)。
箭头函数的 this 看在哪里定义(定义位置决定)。
11.2易错例子(面试常考)
例子1:函数被拿出来单独调用
const obj = {
name: '小明',
sayHi: function() {
console.log(this.name);
}
};
const fn = obj.sayHi; // 把函数拿出来
fn(); // undefined(this 指向 window,window.name 不存在)
原因: fn() 前面没有对象,是普通函数调用。
例子2:定时器里的 this
const obj = {
name: '小明',
sayHi: function() {
setTimeout(function() {
console.log(this.name);
}, 1000);
}
};
obj.sayHi(); // undefined(定时器里的函数是普通调用,this 指向 window)
解决方法: 用箭头函数
setTimeout(() => {
console.log(this.name); // this 继承外面的 this,指向 obj
}, 1000);
例子3:事件监听里的 th
button.addEventListener('click', function() {
console.log(this); // 指向 button 元素
});
button.addEventListener('click', () => {
console.log(this); // 指向 window(箭头函数继承外面的 this)
});
11.3手动改变this(apply,call,apply)
三个的区别(一句话)
| 方法 | 是否立即调用 | 参数怎么传 |
|---|---|---|
call | ✅ 立即调用 | 一个一个传 fn.call(obj, 1, 2, 3) |
apply | ✅ 立即调用 | 数组传 fn.apply(obj, [1, 2, 3]) |
bind | ❌ 返回新函数,不立即调用 | 一个一个传 fn.bind(obj, 1, 2, 3) |
11.3.1call(立即调用,参数一个一个传)
function sayHi(age, city) {
console.log(this.name + ',' + age + ',' + city);
}
const obj = { name: '小明' };
// 正常调用:this 指向 window
sayHi(18, '北京'); // undefined,18,北京
// 用 call:手动把 this 指向 obj
sayHi.call(obj, 18, '北京'); // 小明,18,北京
语法: 函数.call(想要this指向谁, 参数1, 参数2, 参数3...)
11.3.2apply和call一样,只有传参方式不同
语法: 函数.apply(想要this指向谁, [参数1, 参数2, ...])
这里不过多赘述
11.3.3bind(不立即调用,返回新函数)
function sayHi(age, city) {
console.log(this.name + ',' + age + ',' + city);
}
const obj = { name: '小明' };
// 用 bind:返回一个新函数,不立即执行
const boundSayHi = sayHi.bind(obj, 18, '北京');
// 以后想调用的时候再调用
boundSayHi(); // 小明,18,北京
语法: const 新函数 = 原函数.bind(想要this指向谁, 参数1, 参数2, ...)
什么时候用 bind? 不着急调用,比如定时器、事件监听。
const obj = {
name: '小明',
sayHi: function() {
console.log(this.name);
}
};
// 定时器里 this 会丢失
setTimeout(obj.sayHi, 1000); // undefined(this 指向 window)
// 用 bind 绑定
setTimeout(obj.sayHi.bind(obj), 1000); // '小明'
这里this指向可能考最难的点就是bind,我们可以看一下bind伪代码来帮助理解

(来源:前端渡一大师)
这张图在说什么?
它说:bind 返回的新函数,在被调用时要做一个判断:
如果这个新函数是被
new调用的,那this就不再是绑定的那个ctx了,而是新创建的对象。否则,就用绑定的ctx。为什么要做这个判断?
因为
bind有一个特殊规则:绑定的函数,被new调用时,this绑定失效
更多推荐



所有评论(0)