1.(重要)请解释宏任务和微任务,并说明事件循环机制
八股链接
事件循环:微任务和宏任务
JS可视化教程:事件循环、Web API 与(微)任务队列
事件循环的可视化图可以参考那个演讲里的动画,很形象生动。
根据这张图,区分清楚几个概念
- 执行栈(Call Stack):JavaScript 引擎维护一个调用栈(Call Stack),用于记录当前正在执行的函数调用。同步代码会依次入栈并执行,遇到函数调用时会在栈中压入一个新的调用帧(call frame)。调用栈遵循**后进先出(LIFO)**的原则。
- 事件循环(Event Loop):事件循环是 JavaScript 异步编程的核心机制。主线程会不断循环检查是否有任务需要执行。它的主要职责是协调调用栈、宏任务队列和微任务队列之间的执行顺序。

- 任务队列(Task Queue),这个是用于存放宏任务的回调函数的地方,比如在执行js脚本的过程中,遇到了setTimeout这样的异步函数,WebAPI就会创建一个定时器,在定时结束后将它的回调函数放到宏任务队列中,等待事件循环执行回调
宏任务队列用于存放宏任务(如 setTimeout、setInterval、I/O、UI 渲染、script 整体脚本等)的回调。当这些异步操作完成时,其回调会被放入宏任务队列中,等待事件循环调度执行。
- 微任务队列(Microtask Queue),这个是存放微任务回调函数的地方,比如执行js脚本时遇到promise这样的异步函数,WebAPI会等待当回调执行的时候将对应的回调函数存入微任务队列中,比如success的回调,然后等待事件循环执行回调
执行顺序如下:
- 先执行所有的同步脚本,当执行栈里没有同步脚本后,事件循环开始工作
- 检查微任务队列,执行完所有微任务后
- 检查宏任务队列,执行一个宏任务后,再次检查微任务队列
- 如果微任务队列有新的微任务,则执行微任务;如果没有,则执行下一个宏任务,以此类推
事件循环的机制主要源自于js语言是单线程的原因,一些耗时长的任务会阻塞js脚本继续运行
使用事件循环来负责调度同步和异步代码的执行顺序,是js实现异步操作的核心机制
JavaScript 最初设计为单线程语言,以避免多线程的复杂性(如死锁、资源竞争等)。为了在不阻塞主线程的前提下处理异步操作,JavaScript 引入了事件循环机制。
事件循环使得 JavaScript 能够通过非阻塞的方式处理异步操作(如网络请求、用户输入、定时器等),从而提高应用的响应性和性能。
2.JavaScript中有哪些数据类型
八股链接
【JavaScript】完全搞懂JavaScript数据类型!
截止ES2020,目前一共有8种数据类型,其中有7种原始类型,1种非原始类型
⚠️ 注意:虽然“引用类型”是常见术语,但它不是语言规范中定义的数据类型,而是对值传递方式的一种描述。因此,更准确的说法是“1 种非原始类型”。
原始类型
- Number:存储整形和浮点数,存在特殊值:
NaN、Infinity、-Infinity
- BigInt(ES2020新增):存储更大的整形,数字后紧跟一个n
- String:存储字符串类型,注意区别双引号、单引号和反引号在不同情况下的应用
- Boolean:存储布尔类型,true或false
- Undefined:只有一个undefined值,通常用于在定义原始类型但未赋值的情况下读取时返回undefined值,也可用于引用数据类型
- Null:表示“空值”或“无对象”。它是开发者显式赋给变量的一个原始值,表示“这里本应有值,但现在没有”。
⚠️ 注意:typeof null === 'object' 是 JS 的一个历史 bug。
- Symbol:表示唯一的、不可变的值,常用于对象属性键,防止命名冲突。
非原始类型(有时也可叫引用类型)
Object:对象,不同于原始类型直接在内存中存储数据,定义并给对象赋值后,会在内存中存储指向存储数据的堆的地址。普通对象使用花括号加键值对定义
数组、函数都是对象
| 类型 | 分类 | 说明 |
|---|
Number | 原始类型 | 数字,包括整数和浮点数 |
BigInt | 原始类型 | 大整数,后缀 n |
String | 原始类型 | 字符串 |
Boolean | 原始类型 | true / false |
undefined | 原始类型 | 未赋值的变量 |
null | 原始类型 | 表示“空值”(有意的无值) |
Symbol | 原始类型 | 唯一值,用于对象属性键 |
Object | 非原始类型 | 所有对象(包括数组、函数等) |
3.ES6新增了什么内容,简单说说
八股链接
ES6-阮一峰
3.1 块级作用域
let 和 const 命令
ES6新增了let和const用来定义变量(实际上还有import,class,加上ES5原有的var和function,一共有6种)
这里注意几个核心概念
- 变量提升:var可以在声明前访问(undefined),let不行,必须要声明后再访问
- 暂时性死区(TDZ):从进入作用域,到变量声明的期间,都叫做暂时性死区,在TDZ无法访问let和const变量
- 重复声明:在同一作用域内,let和const在声明后不允许重复声明
- 块级作用域:let和const定义的变量只在其所在的代码块内有效
const在定义时必须赋值,并且之后不允许改变其绑定的值,但是这里不能改变的只是其引用的地址,地址里存储的值还是可以改变的
3.2 箭头函数
函数的扩展-箭头函数
箭头函数是纯粹的函数,没有自己的this指向,没有arguments(因为可以使用rest参数),不能用new构造,没有原型和与面向对象的相关的任何特性;
this指向为向外部捕获最近的作用域的this
3.3模版字符串
使用反引号包裹字符串,可以实现嵌入变量${};使用模版字符串还可以实现多行字符串
3.4解构赋值
数组的解构赋值按照元素位置进行对应赋值
对象的解构赋值按照变量名进行对应的赋值
3.5类和继承
添加了面向对象的一些特性
3.6(重点)Promise对象
Promise 对象
MDN-Promise
可以看我博客手写的Promise
手写Promise及其实例方法和静态方法
3.6 模块化
MDN-JavaScript 模块
引入了export和import关键字
3.7 Spread语法(...)
JS-Info-Rest 参数与 Spread 语法
3.8 默认参数
函数参数可以设置默认值,当调用函数时没有传递该参数,就会使用默认值
3.9迭代器和生成器
先记住有这个东西,感觉用到的不多
4.简述ES6 Promise 的作用?
八股链接
Promise对象提供一个强大的用于异步编程的对象
参考我的博客
手写Promise及其实例方法和静态方法
5.JavaScript 闭包是什么,闭包形成的原因和闭包的用途?
八股链接
MDN-闭包(Closures)
JS-Info这篇文章里讲作用域的时候偏底层,讲到了词法环境的概念
JS-Info-变量作用域,闭包
了解闭包前,先了解一下执行上下文
深入理解JavaScript——执行上下文与调用栈
这篇文章很有意思,跟着他的案例走,会理解什么是闭包
我从来不理解JavaScript闭包,直到有人这样向我解释它
自古评论出大神
函数在定义时的词法作用域以外的地方被调用就会形成闭包。闭包使得函数可以继续访问定义时的词法作用域
其实很Easy, 闭包就是函数声明的作用域与执行的作用域是不同的就叫闭包
闭包的定义也很简单:
1.「函数」和「函数内部能访问到的变量」的总和,就是一个闭包 2. 函数和对其词法环境的引用捆绑在一起,这样的组合就是闭包
我来总结一下,就是在一个函数返回一个子函数时,子函数会记住定义期间作用域内的所有变量,在之后任何时机执行子函数的时候,都可以通过作用域链访问并使用这些变量
6. 什么是JavaScript原型,原型链 ? 有什么特点?
八股链接
彻底搞懂JS原型与原型链
【全网首发:已完结】快速搞懂『原型、原型链』【JavaScript基础专题】
这个视频的p1讲的挺清楚
首先这个知识点就涉及到两个名词,不要太纠结于中文的翻译
prototype : 原型
__proto__: (原型链上的一个节点) <-这样理解,不要纠结中文
然后搞清楚这两个名次的从属关系
prototype: 函数的一个属性:对象 {}
__proto__:是 Object.prototype 上的访问器属性,被所有对象继承:对象{}
__proto__: 是 Object.prototype 提供的一个访问器属性,所有普通对象都可以通过原型链访问到它,用于指向其构造函数的 prototype。
再最后理解一下这句话
对象的__proto__保存着该对象的构造函数的prototype
即a.__proto__ === A.prototype //输出true
这里A是a的构造函数
视频的p2用实际代码证明了一下原型链,然后引出了原型链的顶层
比如我现在写了一个构造函数(其实就是普通的函数,只是我们规定一下,当成构造函数的函数,你给我首字母大写)
function Test() {
this.a = 1;
}
然后我使用这个构造函数new一个实例
如果我们比较这个实例对象的__proto__属性和构造函数的prototype属性
console.log(test.__proto__ === Test.prototype) //true
会发现是true,也就验证了p1的结论
同时如果我们追溯一下test的__proto__属性
console.log(test.__proto__) //与Test.prototype相同
console.log(Test.prototype.__proto__) //与Object.prototype相同
console.log(Object.prototype.__proto__) //null
所以我们就能得知,Object.prototype 是原型链的顶层
同时也搞清楚了原型链到底是个什么东西,就是由__proto__属性值作为节点连接起来的一条链
p3主要是实际展示了一下一条原型链上的值是如何进行访问的
//复用上面的
console.log(test.a); //1
console.log(test.b) //找不到
//如果我在Test.prototype上增加一个属性
Test.prototype.b = 2;
//再执行
console.log(test.b) //先找test里有没有,没有,那就往它的上一层原型链找,ok,找到,打印2
//同理的
Object.prototype.c = 3;
console.log(test.c) // //先找test里有没有,没有,那就往它的上一层原型链找,还没有,再往上一层找,找到,打印3
//如果都没有,那就返回undefined;
整体就是
先在 test 自身查找,没有的话,就沿着 test.__proto__ 找到 Test.prototype → 再没有 → 沿着 Test.prototype.__proto__ 找到 Object.prototype,中间找到了就停止,找不到就返回undefined
p4讲解了 Function 和 Object
他们既是函数也是对象
因为Test是一个构造函数,它也是个对象,那么它肯定是由另一个构造函数函数new出来的
也就是说Test 是一个函数,而所有函数都是 Function 构造出来的对象
consoloe.log(Test.__proto__) //ƒ () { [native code] }
这里就引出了一个Function
//测试一下
console.log(Test.__proto__ === Function.prototype) //true
那Function是一个构造函数,它本身也是一个对象,那它又是谁new出来的?
console.log(Function.__proto__); //ƒ () { [native code] }
(重要)震惊了,它是也是Function new出来的,这就证明了其特殊性
这个特殊性也是底层所决定的,因为这个函数的构造已经是顶层了,它只能由自己构造
Function 是唯一一个 __proto__ === __proto__ 的函数,也就是说:
Function.__proto__ === Function.prototype
这是 JavaScript 引擎的底层设计,使得 Function 可以“自我构造”。
console.log(Function.__proto__ === Function.prototype); //true
我们再来研究Object
console.log(typeof Object); //function
说明Object是一个函数类型
console.log(Object.__proto__ === Function.prototype); //true
Object也是由Function这个构造函数new出来的
那我们综合一下,就能得出一个结论
console.log(Function.__proto__ === Function.prototype); //true
p5主要讲了一下怎么判断一个属性到底是它自己这层代码块定义的,还是从它的原型链上找到的
里面涉及的两个方法(hasOwnProperty / in)
hasOwnProperty 是 Object.prototype 上的方法,test 通过原型链继承了它
in是一个运算符,用于判断一个属性在一个对象的整条原型链上有没有
test.hasOwnProperty('a');//true
test.hasOwnProperty('b');//false 因为这个属性它本身没有
Test.prototype.b = 1;
'b' in test ; //true 因为原型链上找的到
p6讲了一下constructor属性
console.log(test.constructor);//ƒ Test() {this.a = 1}
test.constructor 的值来自 Test.prototype.constructor
而 Test.prototype 是一个对象,它的 constructor 属性是 JS 引擎自动添加的,指向 Test
它返回我们自己定义的构造函数的代码
这里我们判断下
console.log(test.constructor === Test); //true
也就是说,我们可手动修改我们的构造函数
function Test1() {
this.a = 123;
}
Test.prototype.constructor = Test1;
console.log(test.constructor === Test1) //true
最后来一张广为流传的图

7.箭头函数与普通函数的区别
八股链接
在回答这个问题之前,我们来彻底解决一下this有关的问题
嗨,你真的懂this吗?
读完这篇文章,然后做一下开头给出的样例,应该就能明白了
MDN-this
所以箭头函数与普通函数在this上有不同
箭头函数的this是在定义时就被确定了,它没有自己的this,指向的是外部作用域的this
所以由此引申出来,箭头函数无法使用bind,call,apply
同时因为箭头函数的this是在定义时被确定的,new关键字创建时需要把this指向新对象,这出现了矛盾,所以箭头函数不能作为构造函数
再记住一点:箭头函数没有argument
其实说来说去,箭头函数创建的初衷就是希望箭头函数是一个纯净的函数,不要有那么多复杂的功能
记住这一点,在编码设计的时候就知道怎么用箭头函数了
8.简述ES var、let、const之间的区别?
八股链接
可以参考一下[[JS高频八股]]题3里的一些知识
let和const是ES6新增的定义变量的关键字
区分var和新增的两个关键字可以从以下角度出发
1. 作用域(Scope)
var:全局作用域下挂载到全局对象上(如 window),函数作用域内仅限于该函数。无块级作用域。
let 和 const:具有块级作用域,仅在其所在的 {} 内有效。
2. 变量提升(Hoisting)
var:声明会被提升并初始化为 undefined,因此可以在声明前访问。
let 和 const:存在暂时性死区(TDZ),在声明之前访问会导致错误。
3. 重新定义和重新赋值
var:允许在同一作用域内重复声明,支持重新赋值。
let:不允许重复声明,支持重新赋值。
const:
- 不允许重复声明。
- 不可重新赋值:对于原始类型,值不可变;对于引用类型,引用地址不可变,但内部数据可修改。
注意事项
- 使用
const 定义对象或数组时,虽然不能重新赋值,但可以修改对象内部的属性或数组元素。
- 尽量使用
let 和 const 替代 var,以避免潜在的作用域问题。
9.JavaScript实现深拷贝
八股链接
主要有四种实现方式,我把手写的放在最后
- 使用JSON的序列化和反序列化实现,但是只适用于可序列化的对象
- 使用浏览器的
structuredClone方法
const clone = structuredClone(obj);
- 使用第三方库,比如Lodash提供的deepClone方法
- 手写一个深拷贝
手写一个深拷贝,需要考虑的因素还是蛮多的
/**
* 深拷贝函数,支持对象、数组、Date、RegExp,并能处理循环引用
* @param {any} target - 需要深拷贝的目标对象
* @param {WeakMap} map - 记录已经拷贝过的对象,防止循环引用导致死循环
* @returns {any} - 返回深拷贝过的新对象
*/
function deepClone(target, map = new WeakMap()) {
//如果target不是对象或者null,直接返回(即基本类型直接返回)
if (typeof target !== 'object' || target === null) {
return target;
}
//处理循环引用
if (map.has(target)) {
return map.get(target);
}
//创建新对象进行存储
let clone;
//针对不同类型进行判断
if (target instanceof Array) {
//数组
clone = [];
} else if (target instanceof Date) {
//Date类型
clone = new Date(target.getTime());
} else if (target instanceof RegExp) {
//正则表达式类型
clone = new RegExp(target);
} else {
//对象
clone = {};
}
//记录当前的对象,防止循环引用
map.set(target, clone);
//遍历target,递归拷贝
for (let key in target) {
//判断是否为自身属性,排除原型链属性
if (target.hasOwnProperty(key)) {
//递归调用
clone[key] = deepClone(target[key], map);
}
}
//返回克隆完的对象
return clone;
}
这个深拷贝是怎么实现的呢?
假如我们要拷贝这样一个嵌套对象
const original = {
a: 1,
b: [2, 3],
c: { d: 4 }
};
const cloned = deepClone(original);
首先我们将original传入函数,函数在调用时第二个参数为默认参数,创建一个WeakMap用于解决后续可能的循环引用的问题
函数开始执行时,先判断我们传进来的这个target是不是基本类型,如果是基本类型就直接返回了,
这里返回同样也用于后续我们递归的时候,递归到最深层的基本类型时能正常返回。
下一步我们要判断我们当前想要深拷贝的这个target,在这次之前是不是已经被拷贝过一次了,如果是的话我们就直接使用WeakMap里的缓存并直接return。如果没有这一步,我们继续往下处理,在不久的将来就会再循环往复的不断重复一套操作,造成死循环
这些都判断完之后,我们可以创建一个cloned变量来存储我们的新拷贝结果了
但是我们还要判断,我们拿到的这个target是一个什么样的类型,不然如果对象里嵌套个数组,或者数组里套个对象,又或者是时间类型,正则表达式类型,我们都要考虑进去
进行完判断后我们确定了cloned变量到底是什么了
此时我们要处理关键的一步来完善我们对循环引用的处理
我们要把当前正在被拷贝的target作为key,新对象cloned作为value保存到map中
注意这里的cloned虽然目前还是上面判断时的赋值,可能并没有什么数据在里面
但是我们这里是引用它,所以后续我们遍历递归时cloned值会改变
然后当我们以后遇到循环引用,就是又处理到同样的target时,就可以用WeakMap缓存,获取到我们的cloned值
下一步我们就要对我们传入的target进行遍历然后递归了
首先我们遍历target,拿到它里面的key,这里我们按例子讲,比如第一层循环我们拿到了key='a',然后我们要排除原型链上的属性对我们深拷贝的影响,所以要用hasOwnProperty方法来判断,OK,如果通过了,我们就要递归的去拷贝,先把本次循环的值拷贝给我们的cloned变量,然后我们并不知道target[key]是一个基本类型,还是仍有嵌套,所以我们继续我们的递归调用,同时我们把WeakMap的值传入这个递归函数,防止它重新创建一个默认值
下一步递归就简单了,我们先看看是不是递归到最深层了,也就是递归到基本类型,是的话就返回,不是的话我们把它(新的target)按照与之前所描述的步骤继续进行下去,直到我们递归栈被清空,最后我们返回一个完整的cloned
10. (暂放)JavaScript中常用的数组方法?
八股链接
这里我暂时不记录笔记,之后有空回来看,因为这个涉及到数据结构的知识了
11. 手写防抖、节流函数,此外防抖和节流的区别是什么 ?
八股链接
MDN-防抖(Debounce)
MDN-节流(Throttle)
手写防抖函数
/**
* 防抖函数:返回一个新的函数,只有在指定延迟的时间内没有再次调用,才会执行原函数
*
* @param {Function} func - 需要防抖处理的原函数
* @param {number} delay - 延迟时间(ms),规定了在最后一次调用后过多长时间后执行func
* @returns {Function} - 返回处理过的原函数
*/
export function debounce(func, delay) {
//保存一个计时器的引用,利用闭包来实现未来不同作用域的调用
let timer = null;
//返回一个新函数代替原函数
return function () {
//保存当前函数的this上下文
const context = this;
//保存当前函数的参数
const args = arguments;
//如果timer已经存在,则说明在延迟时间内又重复调用了,所以要清除之前的timer,然后重新设置一个timer
if (timer) {
clearTimeout(timer);
}
timer = setTimeout(() => {
//使用apply保证func内部的this和参数不变
func.apply(context, args);
}, delay)
}
}
手写节流函数
/**
* 节流函数:限制func在delay(ms)内只能执行一次
* @param {Function} func - 需要被节流的原函数
* @param {number} delay - 节流的时间间隔(ms)
* @returns {Function} - 返回一个新的节流函数
*/
export function throttle(func, delay) {
//保存一个标志变量,控制func是否能够执行,初始值为true,利用闭包来实现未来不同作用的调用
let canRun = true;
//返回新的节流函数
return function () {
//保存当前的this上下文
const context = this;
//保存传入的参数
const args = arguments;
//判断是否能执行
if (canRun) {
//执行原函数,并绑定上下文和参数
func.apply(context, args);
//设置canRun为false防止在delay(ms)内重复调用
canRun = false;
//设置定时器,delay(ms)后重置canRun为true
setTimeout(() => {
canRun = true;
}, delay)
}
}
}
12. apply、call 和 bind 有什么区别
八股链接
this、apply、call、bind
好文,结合一下之前学的this,不会了就回来看看
这里再摘录一个很凝练的结论
this 永远指向最后调用它的那个对象
13. 手写 Promise.all
八股链接
参考我的博客手写Promise及其实例方法和静态方法
14. (重复)解释节流、防抖函数的概念,并说明在项目中是否使用过
八股链接
参考[[JS高频八股]]题11
15. 如何判断JavaScript数据类型
八股链接
主流的有四种方式来判断,其中第四种是比较全面的
- 使用typeof来判断
//可靠识别
console.log(typeof 'hello'); // "string"
console.log(typeof 123); // "number"
console.log(typeof BigInt(10)); // "bigint"
console.log(typeof true); // "boolean"
console.log(typeof undefined); // "undefined"
console.log(typeof function() {}); // "function"
console.log(typeof Symbol()); // "symbol"
//不可靠识别
console.log(typeof null); // "object"(这是 JavaScript 语言的一个历史遗留问题)
console.log(typeof {}); // "object"
console.log(typeof []); // "object"
console.log(typeof new Date()) // "object"
可以看出,typeof可以比较好的判断基本数据类型,对于引用类型,只能判断出function,其他都是object,还要注意typeof对null的判断会返回object
在 JavaScript 最初的实现中,JavaScript 中的值是由一个表示类型的标签和实际数据值表示的。对象的类型标签是 0。由于 null 代表的是空指针(大多数平台下值为 0x00),因此,null 的类型标签是 0,typeof null 也因此返回 "object"。
曾有一个 ECMAScript 的修复提案(通过选择性加入的方式),但被拒绝了。该提案会导致 typeof null === 'null'。
- 使用instanceof判断
function Person() {}
const person = new Person();
console.log(person instanceof Person); // true
const arr = [];
console.log(arr instanceof Array); // true
console.log(arr instanceof Object); // true
const date = new Date();
console.log(date instanceof Date); // true
instanceOf就是判断左侧的对象的整条原型链上,是否存在右侧的原型,有则为true,遍历完原型链都不存在则返回false
手写实现instanceof逻辑
function myInstanceof(Fn, obj) {
// 获取该函数显示原型
const prototype = Fn.prototype;
// 获取obj的隐式原型
let proto = obj.__proto__;
// 遍历原型链
while (proto) {
// 检测原型是否相等
if (proto === prototype) {
return true;
}
// 如果不等于则继续往深处查找
proto = proto.__proto__;
}
return false;
}
这里有个有意思的例子
var simpleStr = "This is a simple string";
var newStr = new String("String created with constructor");
simpleStr instanceof String; // 返回 false, 非对象实例,因此返回 false
newStr instanceof String; // 返回 true
我们发现用字面量形式创建的变量不能正常用instanceof来判断
同时null和undefined也不能通过这个来判断
所以它也有缺点
- 使用constructor判断
console.log(("1").constructor === String);
console.log((1).constructor === Number);
console.log((true).constructor === Boolean);
// console.log((null).constructor === Null);
// console.log((undefined).constructor === Undefined);
console.log(([]).constructor === Array);
console.log((function() {}).constructor === Function);
console.log(({}).constructor === Object);
输出结果均为true
但是它也有缺点,我们手动改一改它的constructor,它就没法正常判断了
function Fn() {};
Fn.prototype = new Array();
var f = new Fn();
console.log(f.constructor === Fn);
console.log(f.constructor === Array);
所以我们一般选择使用第四种方法
- 使用Object.prototype.toString().call()
function type(obj) {
return Object.prototype.toString.call(obj);
}
console.log(type("aaa")); [object String]
console.log(type(1)); [object Number]
console.log(type(true)); [object Boolean]
console.log(type(null)); [object Null]
console.log(type(undefined)); [object Undefined]
console.log(type([])); [object Array]
console.log(type(function () {})); [object Function]
console.log(type({})); [object Object]
就算改动原型,也不会影响结果,酷~
为了获得最精准的类型判断,我们不能依赖对象自身可能被重写的 toString 方法(因为它可能返回内容而非类型)。我们必须强制调用那个最原始、最顶层的 Object.prototype.toString 方法。通过使用 call(),我们可以让这个“原始方法”直接检查目标对象内部的 [[Class]] 属性,并返回一个标准化的类型字符串(如 [object Array]),从而实现跨所有内置类型的精准识别。
这个的原理是怎么实现的呢?
我们用一个生动的比喻来解释 Object.prototype.toString.call() 的原理。
想象一下,JavaScript 世界就像一个庞大的国际机场。
-
Object.prototype.toString:这就像机场里一个万能的、官方的“身份核查员”。他的工作不是看你行李里有什么(内容),也不是听你自我介绍(你自己的 toString 方法),而是检查你护照上的“国籍”或“签证类型”(相当于 [[Class]] 内部属性)。他有一套标准的报告格式:“[object 国籍]”。
-
call():这就像你直接把某个人(this 值)带到这个身份核查员面前,并说:“请核查这位先生的身份!”。call() 的作用就是确保这个万能的核查员亲自处理你指定的对象。
-
对象 (arr, date, null 等):这些就是机场里的旅客。每个旅客都有一本由 JavaScript 引擎(相当于“世界公民管理局”)颁发的、内置的“护照”,护照上有一个关键的“国籍/身份”字段([[Class]])。
场景重现:核查员的精准判断
-
场景一:检查数组 arr (一位“数组国”公民)
- 你(代码)对身份核查员说:
call(arr) —— “请核查这位先生的身份!”
- 核查员 (
Object.prototype.toString) 接过这位旅客的护照。
- 他查看护照上的“国籍”字段,上面写着
Array。
- 他严格按照标准格式报告:
"[object Array]"。
- 精准! 他知道这是“数组国”的公民。
-
场景二:检查 null
- 你对核查员说:
call(null) —— “请核查这位先生的身份!”
- 核查员接过护照(
null 虽然特殊,但它也有一个内置的“身份标签”)。
- 他查看“国籍”字段,上面写着
Null。
- 他报告:
"[object Null]"。
- 精准! 他知道这是一个特殊身份的“空值公民”。
-
场景三:检查日期对象 date
- 你对核查员说:
call(date)。
- 核查员查看护照,“国籍”是
Date。
- 他报告:
"[object Date]"。
- 精准!
为什么直接问旅客不行?(对比 arr.toString())
- 现在,想象你不找官方核查员,而是直接去问那位“数组国”旅客自己:“你是谁?”(相当于
arr.toString())。
- 这位旅客很友好,但他可能根据自己的习惯回答。比如,他可能会说:“哦,我是
[1, 2, 3]”,因为他想介绍自己的家庭成员(数组元素)。他没有说“我是数组国公民”。
- 同样,一位“日期国”旅客可能会说:“今天是 2024年7月31日星期三...”,介绍的是他的“行程”(日期值),而不是他的“国籍”。
- 这就是为什么直接调用对象自己的
toString() 方法不可靠,因为它可能被“旅客”自己(对象的构造函数原型)重写了,用来描述内容,而不是身份。
总结:万能核查员的秘诀
Object.prototype.toString.call() 之所以精准,是因为:
- 找到了对的人:它找到了那个万能的、官方的“身份核查员” (
Object.prototype.toString)。
- 用了正确的方式:它用
call() 把目标旅客直接带到核查员面前,确保核查员亲自处理。
- 查了最根本的信息:核查员只看护照上的**“国籍/身份”字段** (
[[Class]]),这是由“世界公民管理局”(JavaScript引擎)在创建对象时就盖章确定的,不会改变。
- 输出标准化:核查员总是用
[object 国籍] 的格式报告,结果统一且可预测。
所以,Object.prototype.toString.call() 就像一个不会被花言巧语迷惑、只认官方证件的超级核查员,能精准地告诉你任何JavaScript对象的“真实国籍”(类型)。
16. 闭包有哪些使用场景
八股链接
这道题相当于对之前记录的闭包的实战丰富了
- 数据隐藏与封装
// createCounter 利用闭包实现counter对外部隐藏,并提供封装的对外接口
function createCounter() {
// count 是 createCounter 的局部变量,只能在 createCounter 内部访问
// 它不会暴露到外部,实现了数据的隐藏
let count = 0;
return {
// increment 方法通过闭包实现在不同的执行上下文调用时访问并修改 count 变量,实现自增
increment: function () {
count++;
},
// decrement 方法通过闭包实现在不同的执行上下文调用时访问并修改 count 变量,实现自减
decrement: function () {
count--;
},
// getCount 方法通过闭包实现在不同的执行上下文调用时能访问 count 变量
getCount: function () {
return count;
}
};
}
const counter = createCounter();
counter.increment();
counter.decrement();
console.log(counter.getCount()); // 输出: 0
// 无法直接访问 count 变量
// console.log(counter.count); // 输出: undefined
- 函数柯里化
这里用一个制作咖啡的过程,介绍一个标准的柯里化转换和可以扩展的柯里化转换过程
function makeCoffee(bean, sugar, milk) {
return `您选择了${bean}咖啡豆,加入${sugar}克糖和${milk}毫升牛奶。`;
}
//标准的
function curryMakeCoffee(fn) {
return function (bean) { // 第一步:选择咖啡豆
return function (sugar) { // 第二步:决定糖的数量
return function (milk) { // 第三步:加入牛奶
return fn(bean, sugar, milk); // 执行原始函数
};
};
};
}
const curriedMakeCoffee = curryMakeCoffee(makeCoffee);
// const myCoffee = curriedMakeCoffee('哥伦比亚')(10)(50);
// console.log(myCoffee); // 输出: 您选择了哥伦比亚咖啡豆,加入10克糖和50毫升牛奶。
const chooseBean = curriedMakeCoffee('哥伦比亚');
// 稍后再决定糖的数量
const addSugar = chooseBean(10);
// 最后决定牛奶的量
const finalCoffee = addSugar(50);
console.log(finalCoffee); // 同样的输出
// smartCurry 函数实现了通用的函数柯里化(Curry)功能,支持多参数一次性传递
// 柯里化:把接受多个参数的函数,转化为一系列只接受一个或部分参数的函数,并且每次调用后返回一个新函数,直到参数足够执行原始函数
// 这里用到了闭包:每次调用 curried 或返回的新函数时,都会“记住”之前传入的参数 args,形成闭包环境,直到参数收集齐全
function smartCurry(fn) {
// curried 是递归函数,每次调用都保存当前已收集的参数 args
return function curried(...args) {
// fn.length 是原始函数参数个数,判断当前收集的参数是否足够
if (args.length >= fn.length) {
// 参数足够,直接调用原始函数 fn,并传入所有参数
//使用apply 保证 this 始终指向调用者
return fn.apply(this, args);
} else {
// 参数不够,返回一个新函数,继续收集剩余参数
// 这里用到了闭包,args 会被“记住”,下次调用时会和新参数合并
return function (...moreArgs) {
// 递归调用 curried,合并之前的参数和新参数
//使用apply 保证 this 始终指向调用者
return curried.apply(this, [...args, ...moreArgs]);
};
}
};
}
// 以 makeCoffee 为例,原始函数有3个参数
const smartCurriedMakeCoffee = smartCurry(makeCoffee);
//同时决定咖啡类型和糖的数量
// 第一次调用传入2个参数,返回一个新函数,等待最后一个参数
const chooseBeanAndAddSugar = smartCurriedMakeCoffee('哥伦比亚', 10);
//最后决定牛奶的量
// 第二次调用传入剩下的参数,参数收集齐全,执行原始函数
const finalCoffee2 = chooseBeanAndAddSugar(50);
console.log(finalCoffee2);
我们以这个smartCurry为例子来梳理这中间发生了什么,以及哪里用到了闭包
首先我们定义了一个smartCurry函数,它接受一个fn并返回一个函数
这里就触发闭包关键词了,函数返回函数,里面又有值
所以第一次涉及到闭包就是我在将普通函数转化为柯里化函数时,我的fn参数被保存了

我们使用转化好的柯里化函数,然后一次性给它传两个参数
内部会判断我们这次传的值有没有满足我们原来的普通函数所需要的参数数量
满足的话就直接调用原函数(使用闭包获取),不满足的话我们就继续返回新的函数,接受更多的参数
这里又触发闭包的关键词了,我们又在函数里返回了函数,并且...args保存了我们传入的两个参数

我们继续传入剩下的第三个参数,然后调用上次返回的函数,这个函数里又是递归的调用了我们的curried函数,然后把我们新的参数和之前传的参数(使用闭包获取)合并起来作为args传入
然后内部判断我们已经满足了所有参数,OK,调用原函数吧!(从闭包获取)
这里的核心就是每次返回新函数时,都会形成新的闭包环境,保存当前参数链,直到最终调用原始函数
记得要处理this哦
- 事件处理
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
</head>
<body>
<button id="btn1">按钮 1</button>
<button id="btn2">按钮 2</button>
<script>
// createClickListener 是一个工厂函数,接收一个 message 参数
// 返回一个新的函数,这个新函数作为事件处理器使用
// 这里用到了闭包:返回的函数会“记住”message,即使外部 createClickListener 已经执行完毕
function createClickListener(message) {
// 返回的匿名函数作为事件处理函数
// 由于闭包,message 会被保存在函数的词法作用域中
return function () {
alert(message); // 这里访问的 message 就是闭包保存下来的
};
}
// 获取页面上的按钮元素
const btn1 = document.getElementById('btn1');
const btn2 = document.getElementById('btn2');
// 给按钮1添加点击事件,事件处理函数是 createClickListener 返回的闭包
// 当点击按钮1时,会弹出“你点击了按钮 1”
btn1.addEventListener('click', createClickListener('你点击了按钮 1'));
// 给按钮2添加点击事件,事件处理函数是 createClickListener 返回的闭包
// 当点击按钮2时,会弹出“你点击了按钮 2”
btn2.addEventListener('click', createClickListener('你点击了按钮 2'));
</script>
</body>
</html>
- 循环事件绑定
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
</head>
<body>
<button id="btn1">按钮 1</button>
<button id="btn2">按钮 2</button>
<script>
// createClickListener 是一个工厂函数,接收一个 message 参数
// 返回一个新的函数,这个新函数作为事件处理器使用
// 这里用到了闭包:返回的函数会“记住”message,即使外部 createClickListener 已经执行完毕
function createClickListener(message) {
// 返回的匿名函数作为事件处理函数
// 由于闭包,message 会被保存在函数的词法作用域中
return function () {
alert(message); // 这里访问的 message 就是闭包保存下来的
};
}
// 获取页面上的按钮元素
const btn1 = document.getElementById('btn1');
const btn2 = document.getElementById('btn2');
// 给按钮1添加点击事件,事件处理函数是 createClickListener 返回的闭包
// 当点击按钮1时,会弹出“你点击了按钮 1”
btn1.addEventListener('click', createClickListener('你点击了按钮 1'));
// 给按钮2添加点击事件,事件处理函数是 createClickListener 返回的闭包
// 当点击按钮2时,会弹出“你点击了按钮 2”
btn2.addEventListener('click', createClickListener('你点击了按钮 2'));
</script>
</body>
</html>
17. 简述JavaScript继承的原理
八股链接
搞清楚了原型和原型链其实很好理解
这里再弄清楚new的过程到底发生了什么?
MDN-new
-
创建一个空的简单 JavaScript 对象。为方便起见,我们称之为 newInstance。
-
如果构造函数的 prototype 属性是一个对象,则将 newInstance 的 [[Prototype]] 指向构造函数的 prototype 属性,否则 newInstance 将保持为一个普通对象,其 [[Prototype]] 为 Object.prototype。
备注: 因此,通过构造函数创建的所有实例都可以访问添加到构造函数 prototype 属性中的属性/对象。
( 通俗理解就是新对象隐式原型__proto__链接到构造函数显式原型prototype上。)
-
使用给定参数执行构造函数,并将 newInstance 绑定为 this 的上下文(换句话说,在构造函数中的所有 this 引用都指向 newInstance)。
-
如果构造函数返回非原始值,则该返回值成为整个 new 表达式的结果。否则,如果构造函数未返回任何值或返回了一个原始值,则返回 newInstance。(通常构造函数不返回值,但可以选择返回值,以覆盖正常的对象创建过程。)
代码模拟实现
/**
* 自定义模拟实现 new 操作符
* @param {Function} constructor 构造函数
* @param {...any} args 构造函数的参数
* @returns {Object} 新创建的对象
*/
function myNew(constructor, ...args) {
// 1. 创建一个空对象,该对象将作为实例返回
const newInstance = {};
// 2. 将新对象的原型(__proto__)指向构造函数的 prototype 属性
// 这样新对象就可以访问构造函数原型上的属性和方法
newInstance.__proto__ = constructor.prototype;
// 3. 执行构造函数,将 this 绑定到新对象,并传入参数
// 构造函数可能会有返回值,用 res 接收
const res = constructor.apply(newInstance, args);
// 4. 如果构造函数返回的是对象(非原始值),则返回该对象
// 否则返回新创建的对象 newInstance
return typeof res === 'object' && res !== null ? res : newInstance;
}
// 测试代码
/**
* 构造函数 Person
* @param {string} name 姓名
* @param {number|string} age 年龄
*/
function Person(name, age) {
this.name = name;
this.age = age;
this.info = function () {
console.log(`My name is ${this.name}, I' ${this.age} years old`);
}
}
const person = myNew(Person, "C", '21');
person.info();
继承的底层就是运用到了原型和原型链的知识
JavaScript继承的原理是:通过__proto__链将对象与原型连接,实现属性和方法的逐级委托查找
比如有一个子类Son,让Son.prototype.__proto__ = Father.prototype 就建立了一个原型链,通过委托查找就能实现宏观上的继承
这一步的Son.prototype.__proto__ = Father.prototype还可以使用更安全的写法Son.prototype = Object.create(Father.prototype)
在new 一个子类实例son的时候,内部new的时候将son.__proto__与Son.prototype建立链接,再加上之前Son.prototype.__proto__与Father.protortype建立链接,这样子类实例就能访问父类原型上的属性
ES6新增的class和继承其实就是语法糖,本质就是实现了上面的事情
18. 请讲述v8垃圾回收机制
八股链接
MDN-垃圾回收
JS-Info-垃圾回收
赠你13张图,助你20分钟打败了「V8垃圾回收机制」!!!

19. (重复)防抖、节流的原理是什么,在项目中如何使用?
参考题11