1. js数据类型和存储差别
-
基本类型-【Number,String,Undefined,Boolean,Null,Symbol】
- Number
- String
- Undefined
- let 和 var 声明变量后未赋值,相当于给变量赋予了 undefined 值
let a ; console.log(a == undefined) // true // const a; 这样会报错;const 必须在声明的时候赋值且不能修改;- Boolean
- null
console.log(typeof null) // 'object' // (这是 JavaScript 的一个已知bug,null 实际上是基本数据类型)- Symbol (每次调用symbol都返回一个全新的symbol类型数据,不可枚举,用作对向属性键避免属性名冲突)
const sym2 = Symbol('desc'); // 可选的描述 -
引用类型- Object
- Array
- Function
- 除了上述说的三种之外,还包括 Date 、 RegExp 、 Map 、 Set 等......
-
存储的区别
- 基本数据类型存储在栈中
let a = 10; let b =a; b = 20; console.log(a) // a= 10 // 基本数据类型存储在不同的栈内存中,值不会互相影响- 引用类型的对象存储在堆中
var obj = {} var obj1 = obj; obj1.name="xxx" console.log(obj.name) // "xxx"- 引用数据类型存放在堆中,每个堆内存对象都有对应的引用地址,引用地址存放在栈中。上面的例子是将堆内存对象在栈内存对象的引用地址复制了一份,两者指向同一个堆内存对象,改变属性值会影响
2. js数据结构
✅ 基本类型(存储在栈内存中)
- Number:整数、小数、
NaN - String:字符串(不可变)
- Boolean:布尔值
- Null:空对象指针
- Undefined:未定义
- Symbol:唯一值
- BigInt:大整数
📌 特点:不可变,比较时是值的比较。
✅ 引用类型(存储在堆内存中,栈里存地址引用)
- Object:最常见的复杂数据结构
- Array:有序集合
- Function:函数对象
- Date:日期对象
- RegExp:正则对象
- Map / Set / WeakMap / WeakSet:ES6 新增的集合结构
📌 特点:存放在堆中,变量保存的是引用地址,比较时是地址比较。
3. DOM常见操作【Document Object Model】- 操作页面内容的API
✅ 节点获取
* `document.getElementById("id")`
* `document.getElementsByClassName("class")`
* `document.getElementsByTagName("div")`
* `document.querySelector(".class")`
* `document.querySelectorAll(".class")`
✅ 节点创建 / 插入 / 删除
let div = document.createElement("div");
div.innerText = "Hello DOM";
document.body.appendChild(div); // 插入
document.body.removeChild(div); // 删除
✅ 修改内容 / 属性 / 样式
let el = document.getElementById("title");
el.innerText = "新标题"; // 修改文本
el.setAttribute("class", "highlight"); // 设置属性
el.style.color = "red"; // 修改样式
✅ 事件绑定
let btn = document.getElementById("btn");
btn.addEventListener("click", () => {
alert("按钮被点击了!");
});
4. 你对BOM的理解,常⻅的BOM对象你了解哪些?
-
Browser Object Model,浏览器对象模型。是浏览器提供的与窗口交互的接口,主要是操作 浏览器窗口和外层环境 的对象集合。
-
📌 区别:
- DOM 关注文档内容(HTML、XML)
- BOM 关注浏览器本身
-
window
- 浏览器的全局对象(JS 顶层对象),DOM 和 BOM 的顶级对象
window.alert("Hi");
console.log(window.innerWidth); // 窗口宽度
-
navigator
- 浏览器信息(类型、版本、语言、用户代理等)
console.log(navigator.userAgent); // 获取浏览器引擎!!!
-
location
- 当前页面的 URL 信息,可用来跳转
console.log(location.href); // 当前网址 location.href = "https://juejin.cn"; // 页面跳转 -
history
- 访问历史记录(前进/后退)
history.back(); // 后退
history.forward(); // 前进
-
screen
- 用户屏幕的信息
console.log(screen.width, screen.height);
5. == 和 ===区别, 分别的使用场景
-
== 会做类型转换
-
但在⽐较 null 的情况的时候,我们⼀般使⽤相等操作符 ==
- 因为 null 和 undefined在宽松情况下是相等的 null==undefined
- 在比较
null时,null只和undefined相等,不会与其他类型相等。 - 所以
if (value == null)这个判断,等价于: value === null || value === undefined
👉 一条语句就能同时判断 值为 null 或 undefined 的情况。
6.typeof 与 instanceof 区别
- typeof 返回一个字符串,注意typeof null 为 'object'(这是 JavaScript 的一个已知bug,null 实际上是基本数据类型)
- instanceof(是否实例?) 用于检测构造函数的 prototype 属性是否出现在某个实例对象的原型上
- object instanceof Constructor
- 使用:【对象 instanceof 构造函数】。 obj instanceof Function
- 构造函数可以通过new实例对象,insatnceof判断这个对象是不是这个构造函数的实例
function Foo() {}
const obj = new Foo();
console.log(obj instanceof Foo); // true
console.log(obj instanceof Object);
// true,因为 Foo.prototype.__proto__ === Object.prototype
console.log(obj instanceof Array); // false
👆关键点:
- JS内部会做:
- 取出Foo.prototype
- 沿着obj的原型链(
__proto__)往上找 - 如果在某一层原型上找到
Foo.prototype就返回true;否则返回false
- 手写instanceof
function myInstanceof(left,right){
// 先判断是不是基础数据类型,如果是,返回false【是不是引用类型,且不为null】
if(left === null || typeof left !== 'object') return false;
// getPrototypeOf 是Object构造函数上帝的方法, 返回对象的原型(即 [[Prototype]])内部的属性
// `Object.getPrototypeOf(obj)` 是获取【指定对象】的【原型】(即 `__proto__` 或 `[[Prototype]]` 内部槽)。
let proto = Object.getPrototypeOf(left);//本质上就是 `obj.__proto__`
if(proto === null) {
return false;
}
while(proto) {
if(proto === right.prototype) { // 判断right.prototype是否出现在left的原型练上
return true;
}
proto = Object.getPrototypeOf(proto);
}
return false;
}
- 区别
-
typeof 返回字符串;instanceof返回布尔值
-
instanceof可以准确判断复杂引用数据类型,但是不能正确判断基础数据类型
-
typeof 不能判断null,也不能判断Function
-
各有利弊:最优解: Object.property.toString,调用该方法统一返回 “[object xxx] ”的字符串
-
-
为什么要用call?
1. 先看
Object.prototype.toString -
这是所有对象都继承的一个方法,作用是 返回对象的内部
[[Class]]标记,结果类似:Object.prototype.toString.call([]); // "[object Array]" Object.prototype.toString.call({}); // "[object Object]" Object.prototype.toString.call(123); // "[object Number]" Object.prototype.toString.call(null); // "[object Null]" Object.prototype.toString.call(undefined); // "[object Undefined]" -
它比
typeof更强大,因为typeof对数组、对象、null都会返回"object",而toString能精确区分。
2. 为什么要用 call
- 如果你直接调用
.toString(),情况会不一样:
const arr = [];
console.log(arr.toString()); // "" (空字符串)
👉 这里返回的并不是 [object Array],而是数组自己重写的 toString 方法(把元素用逗号拼接)。
再比如:
console.log((123).toString()); // "123"
console.log((true).toString()); // "true"
👉 基本类型和数组、函数等,都有自己覆盖过的 toString。
-
为了强制调用最原始的
Object.prototype.toString方法,必须写成:Object.prototype.toString.call(value)这样可以绕过对象本身的
toString,保证用的就是Object.prototype上的那个版本。
3. call 的作用
call的语法:func.call(thisArg, arg1, arg2, ...)- 在这里就是把
thisArg设置为我们要检测的值,让Object.prototype.toString的this指向那个值。
Object.prototype.toString.call([]);
// 等价于:Object.prototype.toString.apply([], []);
// 内部 this → []
Object.prototype.toString.call(123);
// 内部 this → 123(会被装箱成 new Number(123))
如果不用 call,你就没法让 Object.prototype.toString 的 this 指向其他对象。
function sayHi(){
console.log(this.name)
}
const tom = {name:'tom'};
const jerry = {name:'jerry'};
// 默认调用sayHi() ; 结果是undefined或者null
// 如果想让它代表tom去执行
sayHi.call(tom) // 让sayHi的this指向tom
- 回到 Object.prototype.toString(value) , 是想让toString中的this指向value,让value成为toString中的this。
- 总结:.call() 是 为了明确的告诉Object.prototype.toString() “请用我传进来的值作为this来判断类型”
7. 原型,原型链 ? 有什么特点?
-
一句话记住:javascript所有对象都有“祖先”(原型),如果找不到东西就会一层层往祖先(原型)身上找。这就是原型链
-
通俗类比
- 小明要找钥匙,在自己口袋(对象本身)找
- 找打不到,就去找爸爸(__proto__指向的原型)
- 没有,再往爷爷身上找,爸爸__proto__指向的原型,
- 最后实在没有,就返回undefined
-
重点概念解释
-
什么是原型prototype
- 每个函数function都有一个.prototype属性,他是一个对象,用来给它创建的实例当作原型
function Person(){} Person.prototype.sayHi = function (){ console.log('Hi') } const p = new Person() p.sayHi(); // p 找不到sayHi就去p.__proto__上去找,也就是Person.prototype- 可以理解为,构造函数.prototype属性是所有“儿子”(实例)的共同祖先
-
什么是__proto__
- 每个对象都有一个隐藏属性__proto__。他指向他的原型(构造函数的.prototype)
console.log(p.__proto__ === Person.prototype) // true // 也就是__proto__是指向爸爸的手指 -
原型链是什么?
- 就是一条由__proto__串起来的链条
- p.proto ---> Person.prototype ---> Object.prototype ---> null
p.__proto__ === Person.prototype Person.prototype.__proto__ === Object.prototype Object.prototype.__proto__ === null -
记忆口诀
- 构造函数有.prototype
- 实例对象有__proto__
- 查属性:先自己,再爸爸,再爷爷……
- 顶头祖宗是
Object.prototype
核心口诀
- 对象有
__proto__,指向构造函数的prototype - 函数既是对象,也有
__proto__,而且还有prototype属性 - 所有
prototype的尽头都是Object.prototype - 所有
__proto__的尽头都是null
-
8. 说说你对【作⽤域链】的理解
- 作用域链就是“找变量”的路线图, 优先级:inner->outer->global
let a = 1; // 全局作用域
function outer() {
let b = 2; // 外层函数作用域
function inner() {
let c = 3; // 内层函数作用域
console.log(a); // 往上找到了 1
console.log(b); // 往上找到了 2
console.log(c); // 自己作用域里就有 3
}
inner();
}
outer();
- 全局作⽤域、函数作⽤域、块级作⽤域
-
全局作⽤域
- 最外层,不在任何代码块和函数内
- 一旦创建,在整个页面/程序中都能访问
- 过多全局变量会造成变量命名冲突
-
函数作用域
- 函数内部声明,函数内部访问
- 每次调用函数都会创建一个新的作用域环境。
- 函数作用域变量不会污染全局
- 函数
内部变量可以访问函数外部变量,通过作用域链
-
块级作用域
- 用花括号{}括起来的区域,比如if,else,while,代码块,使用let/const 声明的变量只在这个块中有效
var不受块作用域限制!- 类比:像一个临时会议室,讨论完就走,东西也不能带出去
{ let c = 300; console.log(c); // ✅ OK } console.log(c); // ❌ 报错:c is not defined -
词法作用域:-
又叫静态作用域。就是
变量被创建的时候就确定好了作用域,而不是执行阶段。javascript遵循的就是词法作用域var a = 2; function foo(){ console.log(a) // 输出: 2 } function bar(){ var a = 3; foo() } bar()
-
-
9. 谈谈this的理解
-
一句话记住:普通函数中,this是谁在调用函数,就指向谁
-
五种常见情况+通俗解释
- 1、普通函数调用(谁也没喊)
function show(){ console.log(this) } show() // this是window // 类比:你一个人自言自语:“我是谁?” ——没人回应,所以默认是 **全局对象**(浏览器中是 `window`)。 // 在 **严格模式下(`'use strict'`)** ,`this` 是 `undefined`- 2、对象调用自身的方法(谁喊听谁的)
const person = { name: 'Tom', sayHi() { console.log(this.name); } }; person.sayHi(); // 👉 输出 Tom- 3、构造函数中(new)
function Person(name) { this.name = name; } const p = new Person('Alice'); console.log(p.name); // 👉 Alice- 类比:你去开了一家“人”工厂,每次
new出来一个人,this就是新出生的那个对象。
-
箭头你函数(不听调用者,只认生父)
const obj ={ name:'Lucy', sayHi:()=>{ console.log(this.name) } } obj.sayHi() // undefined-
类比:箭头函数里的
this就像“遗传基因”,永远指向它“出生的地方”的this,不会变。function outer(){ const inner = ()=>{ console.log(this) // 箭头函数没有自己的this,定义时捕获它所在作用域的this } inner() } outer() // 普通函数在调用时动态绑定 `this`。this 是 window(或 undefined,严格模式下)
步骤追踪:
-
你调用
outer()时:- 在 非严格模式下,普通函数
outer的this默认是window; - 在 严格模式下,
outer的this是undefined。
- 在 非严格模式下,普通函数
-
outer内部定义了inner箭头函数:- 箭头函数不会生成自己的
this,它的this直接继承自outer的this。
- 箭头函数不会生成自己的
-
因此:
-
如果非严格模式:
outer的this = window→inner的this也是window。 -
如果严格模式:
outer的this = undefined→inner的this也是undefined。
-
-
记忆小口诀
- 普通函数 this:运行时决定
- 箭头函数 this:定义时决定(继承外层作用域)
- 手动指定this,call、bind、apply
- call是立即调用,参数用逗号隔开
- apply也是立即调用,参数是数组
- bind 不是立即执行,返回一个新的函数,稍后执行
10. new操作符具体干了什么
- 一句话记住:“造人”,用构造函数作为蓝图,
生成一个全新的对象,并把“我是谁”(this)指定给他 - new 做了四件事:(超级重要!一定要记住)
const obj = new Constructor()
// 上面👆实际上相当于:
function newOperator(Constructor,...args){
// 1. 新建一个对象
const obj ={};
// 2.将新对象的原型指向构造函数的prototype
obj.__proto__ = Constructor.prototypr;
// 3.执行构造函数,把this指向新的对象
const result = Constructor.apply(obj,args);
// 4. 如果构造函数返回一个对象,就返回它;否则返回新对象
return typeof result === 'object' && result !== null ? result : obj;
}
- 一句话:“造对象、改原型、执行函数、决定返回谁”。
function Animal(type) {
this.type = type;
}
const dog = new Animal('dog');
// 相当于:
// 1. 创建新对象
const dog = {};
// 2. 设置原型指向 Animal.prototype
dog.__proto__ = Animal.prototype;
// 3. 执行 Animal,把 this 指向 dog
Animal.call(dog, 'dog'); // dog.type = 'dog'
// 4. 构造函数没有手动 return 对象 => 返回 dog。返回值看构造函数有没有返回对象。
-
小细节:
- 如果构造函数里写了
return { xx },就会返回那个对象,this就被覆盖了; - 如果
return返回基本类型(如return 123),new会无视,还是返回this。
- 如果构造函数里写了
11. bind、call、apply
除了箭头函数外,普通函数的this不是在定义的时候决定的,而是在调用的时候决定的。call/apply/bind 只是显式改变调用方式(也就是改变调用时的“接收者”),从而改变 this。
四种调用方式:
-
默认/函数调用(Default binding)
-
写法:
fn() -
this:非严格模式下为全局对象(浏览器是window/globalThis),严格模式下为undefined。 -
例:
function f(){ console.log(this); } f(); // window(非严格) 或 undefined(严格)
-
-
隐式绑定 / 对象方法调用(Implicit binding)
-
写法:
obj.method()—— 关键是“点左边的那个值(base)”决定了this。 -
this:指向调用点的对象obj。 -
例:
const o = { x:1, getX(){ return this.x; }}; o.getX(); // this 指向 o;输出:1- 注意:把方法赋给变量再调用会丢掉隐式绑定(
const g = o.getX; g()-> 丢失o)。
- 注意:把方法赋给变量再调用会丢掉隐式绑定(
-
-
显式绑定(Explicit binding):
call/apply/bind- 写法:
fn.call(ctx, a, b)fn.apply(ctx, [a,b])const g = fn.bind(ctx, preArg1)(返回一个新函数g)
this:由第一个参数ctx决定
- 写法:
-
构造调用
- 写法
new Fn() this:指向新创建的实例(由引擎创建),并且该实例继承Fn.prototype。- 与显式绑定的关系:构造调用(
new)优先于显式绑定 —— 也就是说new (fn.bind(obj))()时,this是新实例,不是obj(但预设参数仍然生效)。
- 写法
this 如何被决定 —— 引擎的心理模型(更接近规范)
JS 引擎内部在遇到一次函数调用时,大体做这件事:
- 判断调用形式(是作属性调用
obj.fn()还是普通函数fn()还是用call/apply/bound/ new)。 - 如果是属性调用:取那个属性访问的“base value”作为
this(obj)。 - 如果是
call/apply:把你传的thisArg作为this(非严格模式下若传null/undefined则替换为全局对象;非对象原始值会被装箱成对应对象)。 - 如果是
new:新对象成为this(即使原来函数是通过bind得到的,new也会优先产生新this)。
箭头函数的特别点
- 箭头函数没有自己的
this,它在定义时就从外层词法环境捕获this(类似闭包)。 - 因此对箭头函数调用
call/apply/bind不会改变它的this。
例:
const obj = {x: 1};
const arrow = () => console.log(this);
arrow.call(obj); // this 仍然是定义时的 this(不会被 call 改变)
call/apply 的原理(为什么能改变 this)
把函数作为对象属性去调用:obj.fn(),此时 fn 的 this 就是 obj。
call/apply 的 trick 就是临时把函数挂到目标对象上,然后以 obj.fn() 的形式调用它,
fn.call(obj) 样函数内部的 this 就是 obj!fn.call(obj) 样函数内部的 this 就是 obj!fn.call(obj) 样函数内部的 this 就是 obj!
- 实现步骤(心理模型):
- 接收
thisArg(目标对象)和参数; - 如果
thisArg是null/undefined,在非严格模式下替换为全局对象(浏览器:window/globalThis);如果是原始值(number/string/boolean),将其包装成对象(Object(thisArg)); - 在
thisArg上创建一个临时唯一属性(通常用Symbol),把要调用的函数赋给它; - 执行
thisArg[临时属性](...args); - 删除临时属性并返回结果。
这就是你在 polyfill 中看到“把函数挂到上下文对象上然后删掉”的原因 —— 它模拟了属性调用的 this 语义。
bind 的更多细节(为什么更复杂)
bind返回的不是原函数本身,而是一个新的函数对象(内部记录了:要调用的原函数、绑定的this、以及预置的参数)。- 当调用这个“bound 函数”时,运行时会把绑定的
this和预置参数与调用时的参数合并再去调用目标函数。 - 但如果用
new boundFn(),引擎需要当作构造来处理:这时候bind绑定的this被忽略,this指向新实例(这是规范要求),但预设参数仍然被传入。 - 标准里
bind返回的是一个 “Bound Function exotic object”,它内部有[[BoundTargetFunction]]、[[BoundThis]]、[[BoundArguments]]等槽。
严格模式 vs 非严格模式(与 this 相关)
-
非严格模式:
fn.call(null)或fn.call(undefined)->this会被替换成全局对象(浏览器是window)。fn.call(123)->this被装箱为new Number(123)(也就是一个对象)。
-
严格模式:
fn.call(null)->this就是null(不会替换为全局对象)。- 原始值也不会被强制装箱(
this按原样传入)。
♻️ 注:在实际 polyfill 中无法判断目标函数是否以严格模式定义,所以 polyfill 通常采用兼容性做法(把 null/undefined 替换成全局对象),这是为什么实现上会看到 context == null ? globalThis : Object(context) 的写法。
常见面试/实战例子(加深理解)
- 方法丢失上下文:
const o = {
x: 1,
getX(){
return this.x
}
};
const g = o.getX;// 只是复制了函数的引用,没有绑定上下文,导致this丢失
g(); // this 丢失 -> undefined/global
// 解决:const g = o.getX.bind(o);
bind与new:
function Person(name) {
this.name = name;
}
const B = Person.bind({x:1}, 'Alice');
const p = new B(); // p.name === 'Alice',this 指向新实例,不是 {x:1}
- 箭头函数不受
bind影响:
const arrow = () => this;
const b = arrow.bind({x:1});
b() === this; // true
最简单的 call 方法的完整实现(示意,不含严格模式分支)
Function.prototype._call = function(ctx, ...args) {
// 1. 处理上下文
// - `null/undefined` → 全局对象
// - 原始值(`string`, `number`等) → 包装对象 `Object(1)` → `Number(1)`
ctx = ctx == null ? globalThis : Object(ctx);
// 2. 创建唯一键避免属性冲突
const key = Symbol();
// 3. 关键步骤:将当前函数绑定到上下文对象
ctx[key] = this;
// 4. 通过对象方法调用方式触发 this 绑定
const res = ctx[key](...args);
// 5. 清理临时属性,避免内存泄漏和属性污染
delete ctx[key];
return res;
};
// 使用验证
function introduce() {
return `我是${this.name},今年${this.age}岁`;
}
const person = {name: "小明", age: 20};
// 正常调用
introduce._call(person); // "我是小明,今年20岁"
这段代码就是把“把函数挂到对象上再调用”的思想浓缩为最小示例。
小结(你该记住的关键点)
-
this是调用时决定的(箭头函数例外)。 -
四种常见调用规则:默认、隐式(对象方法)、显式(call/apply/bind)、构造(new)。
-
call/apply的实现思路:临时把函数挂到目标对象上并作为其属性调用。 -
bind返回一个带内部槽的“已绑定函数”,并处理部分应用与new的特殊行为。 -
严格模式会改变
null/undefined与原始值如何成为this。 -
call apply 立即来,参数格式别弄歪;
-
call 参数用逗号,apply 括号装起来;
-
bind 返回新函数,执行时才把它拽。
- 实现一个bind
// 目标: const boundFn = originalFn.bind(obj, arg1, arg2); boundFn(arg3, arg4); // 会以 obj 作为 this 执行 originalFn(arg1, arg2, arg3, arg4) // 给 Function.prototype 添加一个 myBind 方法 Function.prototype.myBind = function (context, ...bindArgs) { // 保存原始函数(也就是被绑定的函数) const originalFn = this; // 返回一个新函数,用户调用这个新函数时才会执行原始函数 function boundFn(...callArgs) { // 判断是否通过 new 调用这个 boundFn // 如果是,this 应该是 boundFn 的实例,而不是 context const isNewCall = this instanceof boundFn; // 如果是通过 new 调用,就使用 new 出来的 this; // 否则使用 bind 传入的 context const finalThis = isNewCall ? this : context; // 调用原始函数,合并 bind 时的参数和调用时的参数 return originalFn.apply(finalThis, [...bindArgs, ...callArgs]); } // 为了让通过 new 创建的对象继承原始函数的原型,需要设置 boundFn 的原型 // Object.create 创建一个空对象,并将其原型设置为 originalFn.prototype boundFn.prototype = Object.create(originalFn.prototype); // 返回绑定后的函数 return boundFn; };
12. JavaScript中【执⾏上下⽂】和【执⾏栈】是什么?
-
一句话理解:
执行上下文是js执行一段代码时的“环境信息打包箱子”,执行栈是JS 管理这些箱子的“堆叠货架”。 -
什么是
执行上下文?- 每段js代码在执行前,js引擎都会创建一个上下文对象,里面装着:
- 变量环境(变量、函数声明等)
- 作用域链 (作用域信息)
- 带上记忆(this的指向)
- 三种执行上下文类型
- 全局执行上下文:页面加载时最先创建,全局作用域
- 函数执行上下文:每次函数调用时生成新的上下文
- eval执行上下文:eval 执行代码也有自己的上下文(很少用)
- 每段js代码在执行前,js引擎都会创建一个上下文对象,里面装着:
-
什么是
执行栈- 后进先出
function a() { b(); console.log('a done'); } function b() { console.log('b done'); } a(); // 执行结果:先 b done,然后 a done // 执行过程: // - 创建全局上下文(`global`),**压栈** // - 调用 `a()`,创建 `a` 上下文,**压栈** // - `a` 里调用 `b()`,创建 `b` 上下文,**压栈** // - `b` 执行完,**弹出 b 的上下文** // - `a` 执行完,**弹出 a 的上下文** // - 最后只剩下全局上下文 [全局上下文] // 页面开始执行 [全局上下文] [a上下文] // 调用了 a() [全局上下文] [a上下文] [b上下文] // a() 里面调用了 b() [全局上下文] [a上下文] // b() 结束,弹出 b [全局上下文] // a() 结束,弹出 a执行栈(从底到顶):
- 🔽
- [ 全局执行上下文 ]
- [ 函数a的执行上下文 ]
- [ 函数b的执行上下文 ]
13. 说说JavaScript中的事件模型
-
捕获 → 目标 → 冒泡 三个阶段。
- 衍生概念:
- 事件冒泡(默认机制)
- 默认事件是从内往外传播的
- 事件委托
- 利用冒泡,在父元素上监听子元素的事件。性能好,不用给每个子元素都绑定事件
- 阻止冒泡&默认行为
- 阻止冒泡:event.stopPropagation();
- 阻止默认行为(比如 a 标签跳转) :event.preventDefault();
- 事件冒泡(默认机制)
14. 解释下什么是事件代理(事件委托)?应⽤场景?
- 就是把子元素的事件,交给父元素来处理,利用事件冒泡来实现。
- 事件触发后,会冒泡到父元素,你只要在父元素上监听,在事件对象中判断是谁触发的(通过
e.target) ,就能知道具体哪个子元素点击了。
15. 闭包
-
通俗说:就是让函数
记住它**出生**时候的作用域,即使搬出去了也不会忘记 -
闭包是一个函数,它可以访问
定义它时的外部函数作用域的变量,即使外部函数已经执行完毕 -
最常见的闭包例子
function outer(){ let count = 0 ;// 外部变量 return function inner(){ count++; console.log(count) } } const fn = outer() // outer 执行完毕,但是inner记住了count fn() // 输出 1 fn() // 输出 2 // inner是闭包,它访问了outer中的count。即使outer已经执行完了,但是count没有被释放
思考:
- 按理说
outer()执行完后,count应该被销毁对吧? - 但
inner还能访问它!为什么?- 原因:
- inner 是一个闭包
- 它保存着一个作用域链:inner->outer->global
- 所以即使outer执行完毕了,count这个变量仍然被inner的作用域引用着,不会被当垃圾回收掉
- 所以可以理解为:闭包是作用域链的快照,它把当时的作用域链保存了下来
- 原因:
-
三个特点
- 函数嵌套函数:闭包必须是函数里定义另一个函数
- 内部函数访问外部变量:内层函数可以使用外层函数的变量
- 外部函数执行后变量不释放:变量会被记住,不会被销毁
-
常见使用场景:
- 数据私有化(模拟私有变量)
function createrCounter(){ let count = 0; return { inc(){ count ++; }, get(){ return count; } } } const counter = createCounter(); counter.inc(); console.log(counter.get()); // 1 // 外部无法直接访问count,只能通过丰富控制 -- 实现封装- 循环中创建函数(防止变量被覆盖)
for (var i = 0; i < 3; i++) { setTimeout(function () { console.log(i); // 输出 3,3,3 ❌ }, 100); } // 使用闭包修改 for (var i = 0; i < 3; i++) { (function (j) { setTimeout(function () { console.log(j); // ✅ 0,1,2 }, 100); })(i); }- 惰性计算/缓存结果
function memoizedAdd() { const cache = {}; return function (n) { if (cache[n]) { return cache[n]; } else { cache[n] = n + 10; return cache[n]; } }; } const add = memoizedAdd(); add(5); // 计算并缓存 add(5); // 直接返回缓存- 生成函数工厂(自定义函数)
function makeMultiplier(x) { return function (y) { return x * y; }; } const double = makeMultiplier(2); console.log(double(5)); // 10 -
闭包的优缺点
- 优点:
- 模拟私有变量,封装性好
- 变量不会被提前销毁
- 工厂函数、延迟执行等场景好
- 缺点:
- 容易造成内存泄露
- 作用域链变长,调试麻烦
- 滥用会导致性能问题
- 优点:
-
结合之前说的作用域链:inner->outer->global作用域链的知识
- 闭包=函数+它的作用域链
- 也就是说闭包能够
记住外部变量是因为保存了作用域链
-
怎么解决闭包可能会导致内存泄露的问题?
- 可能会导致内存泄漏的场景:
- DOM 事件绑定里使用闭包但没移除
function bind() { const bigData = new Array(1000000).fill('x'); // 模拟大内存 document.getElementById('btn').addEventListener('click', function () { console.log(bigData[0]); // 闭包引用 bigData }); } - 解决:
- 1.及时移除事件监听器
function bind() { const bigData = new Array(1000000).fill('x'); const handler = function () { console.log(bigData[0]); }; const btn = document.getElementById('btn'); btn.addEventListener('click', handler); // ❗ 不再需要时移除 btn.removeEventListener('click', handler); }- 避免不必要的闭包,尤其是大数据量
// ❌ 不要这样保留没必要的数据 function create() { const large = new Array(1000000).fill('x'); return () => console.log('do something'); } // 改写成、 function create() { return () => console.log('do something'); } // 不用的数据别放在闭包了- 将闭包变量置为 null(断引用)
let closure; function create() { let temp = 'xxx'; closure = function () { console.log(temp); }; } // 使用完 closure 后清理 closure = null;- 模块化隔离数据,不暴露不必要的闭包
const Counter = (function () { let count = 0; function inc() { count++; console.log(count); } return { inc }; // ❗ 只暴露 inc,不暴露 count })();
- 可能会导致内存泄漏的场景:
16. 谈谈 JavaScript 中的类型转换机制
- 分为自动转换、手动转换
- 隐式转换
-
- “+” : 字符串有限;只要一边是字符串,就会把另一边也转成字符串
-
- 减法
-、乘法*、除法/:都转成数字
- 减法
-
- 比较
==会自动转类型
- 比较
-
- 显式转换
-
数值转换规则:
- Number('123') // 👉 123
- Number('') // 👉 0
- Number(null) // 👉 0
- Number(undefined) // 👉 NaN
- Number(true) // 👉 1
- Number(false) // 👉 0
-
字符串转换:
- String(123) // 👉 '123'
- String(null) // 👉 'null'
- String(undefined) // 👉 'undefined'
- String(true) // 👉 'true'
-
布尔值转换:
|原始值 转为 Boolean 的结果 false、0、NaN、''、null、undefined👉 false其他值(包括对象、数组、非零数) 👉 true- Boolean('hello') // 👉 true
- Boolean(0) // 👉 false
Boolean([]) // 👉 true(数组是对象)**
-
- 隐式转换
17. 深拷⻉浅拷⻉的区别?如何实现⼀个深拷⻉?
- 一句话理解:
- 浅拷贝:只复制第一层,内部引用类型共享地址。
- 深拷贝:复制所有层级,彻底断开引用关系。
-
如何实现一个深拷贝?
- 方法 1:JSON 方式(最简单)
- const obj2 = JSON.parse(JSON.stringify(obj1));
- 优点:简单粗暴
缺点 ❌:- 会丢失:
undefined、函数、symbol - 无法处理循环引用(自己引用自己)
- 会丢失:
- 方法 2:手写递归版深拷贝
// obj 是要拷贝的目标对象,hash是用来记住已经拷贝过的内容 function deepClone(obj, hash = new WeakMap()) { // 判断是否为基本数据类型,基本数据类型不需要深拷贝,直接返回 if (obj === null || typeof obj !== 'object') return obj; // 如果 `hash` 里已经存在这个对象,说明它之前被拷贝过。 // 直接返回之前存好的拷贝,防止 **循环引用导致的无限递归**。 if (hash.has(obj)) return hash.get(obj); // 处理循环引用 const clone = Array.isArray(obj) ? [] : {}; hash.set(obj, clone);//**我刚开始克隆这个对象,就先登记一下,这样后续如果又遇到这个对象,就知道直接返回正在构建的克隆版本** for (let key in obj) { if (obj.hasOwnProperty(key)) {// `hasOwnProperty` 确保只拷贝对象自身的属性 clone[key] = deepClone(obj[key], hash);//**继续深入克隆每个属性,并把同一个 hash 传递下去,让所有递归调用共享这个记忆** } } return clone; } ✅ 优点:能复制数组、对象、嵌套结构 ✅ 支持循环引用! ❌ 不支持函数、DOM、Map/Set 等特殊对象(可以扩展) * 扩展下WeakMap * WeakMap的key只能是对象,基本数据类型不能作为键 * - **值可以是任意类型** * **弱引用**:键是对象的弱引用,不会阻止垃圾回收 * 当没有其他引用指向这个对象时,它会被垃圾回收 * 对应的 WeakMap 键值对也会自动被清理 * 如果 `obj` 本身没有其他引用了,它和对应的 `clone` 可以被垃圾回收,不会造成内存泄漏。这是使用 `WeakMap` 而不是普通 `Map` 的关键优势之一。 const obj1 = { a: 0, c: 1, b: { d: { e: 9999 } } }; const result = deepClone(obj1); console.log(result); 运行时 `console.log(hash)` 会在每一层递归打印 `WeakMap` 的内容: - 第一次: `WeakMap { obj1 → {} }` - 第二次: `WeakMap { obj1 → {}, obj1.b → {} }` - 第三次: `WeakMap { obj1 → {}, obj1.b → {}, obj1.b.d → {} }` - 方法 1:JSON 方式(最简单)
- 常见的浅拷贝:
| 方法 | 是否浅拷贝 | 说明 |
|---|---|---|
Object.assign() | ✅ 是 | 后者覆盖前者 |
展开运算符 {...} | ✅ 是 | 推荐语法简洁 |
Array.slice() | ✅ 是 | 拷贝数组第一层 |
Array.concat() | ✅ 是 | 拼接也能产生浅拷贝 |
* 还会用 loadsh库 最全面
18. Javascript中如何实现【函数缓存】?函数缓存有哪些应用场景?
- 一句话理解: 函数缓存就是把函数运行的结果保存起来,下次遇到一样的参数就直接返回结果,不再重新计算。
function square(n) {
console.log('计算了!');
return n * n;
}
// 你每次都传 `5`,它每次都“重新算一遍”。
// 我们想: **“如果5已经算过了,别再算了!”**
// 实现函数缓存:记住算过的
function cachedSquare(){
const cache ={}; //创建一个缓存对象
return function (n){
if(cache[n] !== undefined){
console.log('从缓存取结果!');
return cache[n];
}
console.log('计算了!');
const result = n * n;
cache[n] = result; // 把结果缓存起来
return result;
}
}
const square = cachedSquare();
square(5); // 计算了!25
square(5); // 从缓存取结果!25
square(6); // 计算了!36
square(5); // 从缓存取结果!25
- 应用场景
| 场景 | 解释 |
|---|---|
| 🔄 递归函数优化(如斐波那契) | 减少重复计算,大幅提升性能 |
| 📊 数据处理(如 filter/sort/map) | 某些函数会被频繁调用 |
| 🧠 AI 计算(如路径规划、搜索) | 缓存中间结果避免爆炸计算 |
⚙️ 后端缓存 API 调用(SSR 场景) | 相同输入避免反复请求接口 |
| 🎮 游戏中的物理/碰撞计算 | 相同位置避免重复运算 |
| 🖼️ 图片/组件渲染缓存 | Vue/React 中也常用 memoization 优化渲染性能 |
通用的“记忆化工具函数”
function memoize(fn){
const cache = new Map();
return function(...args){
const key = JSON.stringify(args); // 多参数支持
if(cache.has(key)){
return cache.get(key)
}
const result = fn.apply(this,args);
cache.set(key, result);
return result;
}
}
// 使用
const slowAdd = (a, b) => {
console.log('计算了!');
return a + b;
};
const fastAdd = memoize(slowAdd);
fastAdd(1, 2); // 计算了!3
fastAdd(1, 2); // 缓存!3
19. JavaScript字符串的常⽤⽅法有哪些?
- 一句话:改、查、切、拼、删、补、转
-
改
方法 作用 例子 replace(old, new)替换字符串内容 'apple'.replace('p', 'b')→'abple'replaceAll(old, new)替换所有匹配 'aaa'.replaceAll('a', 'b')→'bbb'toUpperCase()转大写 'hi'.toUpperCase()→'HI'toLowerCase()转小写 'HI'.toLowerCase()→'hi' -
查
方法 作用 例子 includes(sub)是否包含子串 'hello'.includes('ll')→trueindexOf(sub)第一次出现的位置 'hello'.indexOf('l')→2lastIndexOf(sub)最后一次出现的位置 'hello'.lastIndexOf('l')→3startsWith(sub)是否以子串开头 'hello'.startsWith('he')→trueendsWith(sub)是否以子串结尾 'hello'.endsWith('lo')→true -
切
方法 作用 例子 slice(start, end)截取 [start, end)'abcdef'.slice(1, 4)→'bcd'substring(start, end)同上,但不支持负数 'abcdef'.substring(1, 4)→'bcd'substr(start, length)从 start 开始,取 length 个 'abcdef'.substr(2, 3)→'cde' -
拼
方法 作用 例子 concat(str1, str2)拼接字符串 'hello'.concat(' ', 'world')→'hello world'+拼接运算符 'hi' + '!'→'hi!'join()(数组方法)把数组变字符串 ['a', 'b'].join('-')→'a-b' -
删
方法 作用 例子 trim()去掉头尾空格 ' hi '.trim()→'hi'trimStart()/trimEnd()去头或尾空格 ' hi '.trimStart()→'hi ' -
补
方法 作用 例子 padStart(len, fill)左边补到指定长度 '5'.padStart(3, '0')→'005'padEnd(len, fill)右边补到指定长度 '5'.padEnd(3, '*')→'5**' -
转
方法 作用 例子 split(delimiter)字符串 → 数组 'a,b,c'.split(',')→['a', 'b', 'c']parseInt(str)字符串 → 整数 parseInt('123')→123Number(str)字符串 → 数字 Number('12.3')→12.3
-
20. 数组的常⽤⽅法有哪些?
🧩 一句话记忆法:
增删改查、遍历排序、转化过滤、判断查找
也可以记成:
增删改查 → 排序遍历 → 判断过滤 → 转换拍平
🍱 一类一类带你理解 + 举例 + 好记逻辑
✅ 一、增:往数组里添加元素
| 方法 | 作用 | 示例 |
|---|---|---|
push() | 尾部加元素 | [1, 2].push(3) → [1, 2, 3] |
unshift() | 头部加元素 | [1, 2].unshift(0) → [0, 1, 2] |
splice(index, 0, item) | 指定位置插入 | [1, 2].splice(1, 0, 99) → [1, 99, 2] |
✅ 二、删:删除数组中的元素
| 方法 | 作用 | 示例 |
|---|---|---|
pop() | 删除最后一个 | [1,2,3].pop() → [1,2] |
shift() | 删除第一个 | [1,2,3].shift() → [2,3] |
splice(index, n) | 删掉从 index 起的 n 个 | [1,2,3,4].splice(1, 2) → [1,4] |
✅ 三、改:修改数组
| 方法 | 作用 | 示例 |
|---|---|---|
splice() | 可删除 + 替换 | [1,2,3].splice(1, 1, 9) → [1,9,3] |
| 直接赋值 | 指定位置修改 | arr[0] = 99 |
✅ 四、查:访问数组信息
| 方法 | 作用 | 示例 |
|---|---|---|
includes(val) | 是否包含 | [1,2,3].includes(2) → true |
indexOf(val) | 找第一个匹配的下标 | [1,2,3].indexOf(3) → 2 |
lastIndexOf(val) | 找最后匹配的下标 | [1,2,3,2].lastIndexOf(2) → 3 |
✅ 五、遍历(非常常用)
| 方法 | 作用 | 示例 |
|---|---|---|
forEach(fn) | 每项执行一次函数(无返回) | arr.forEach(x => console.log(x)) |
map(fn) | 每项变换,返回新数组 | [1,2].map(x => x*2) → [2,4] |
✅ 六、过滤 & 查找
| 方法 | 作用 | 示例 |
|---|---|---|
filter(fn) | 过滤符合条件的项 | [1,2,3].filter(x => x > 1) → [2,3] |
find(fn) | 找到第一个符合条件的值 | [1,2,3].find(x => x > 1) → 2 |
findIndex(fn) | 找到第一个符合条件的下标 | [1,2,3].findIndex(x => x > 1) → 1 |
✅ 七、判断数组结构
| 方法 | 作用 | 示例 |
|---|---|---|
Array.isArray(x) | 是否为数组 | Array.isArray([1,2]) → true |
every(fn) | 是否所有项都满足 | [1,2,3].every(x => x > 0) → true |
some(fn) | 是否有任意一项满足 | [1,2,3].some(x => x > 2) → true |
✅ 八、排序 & 反转
| 方法 | 作用 | 示例 |
|---|---|---|
sort() | 排序(默认按字符串) | [3, 1, 2].sort() → [1,2,3] |
sort((a,b) => a-b) | 数字排序 | [3,1,2].sort((a,b)=>a-b) → [1,2,3] |
reverse() | 倒序 | [1,2,3].reverse() → [3,2,1] |
✅ 九、连接与拍平
| 方法 | 作用 | 示例 |
|---|---|---|
join(',') | 数组 → 字符串 | [1,2,3].join('-') → '1-2-3' |
concat(arr2) | 两数组拼接 | [1].concat([2,3]) → [1,2,3] |
flat() | 拍平多维数组 | [1,[2,3]].flat() → [1,2,3] |
✅ 十、聚合:reduce
array.reduce(callback(accumulator, currentValue[, currentIndex[, array]]), initialValue)
reduce 方法接收两个参数:
-
一个回调函数(
callback),该回调函数接收四个参数:accumulator:累积器,保存每次回调计算的结果。currentValue:当前正在处理的元素。currentIndex(可选):当前元素的索引。array(可选):原始数组。
-
一个可选的
initialValue(初始化值),如果提供,则会作为累积器的初始值。如果没有提供,则默认使用数组的第一个元素。
| 方法 | 作用 | 示例 |
|---|---|---|
reduce(fn, init) | 将数组变成单个值 | [1,2,3].reduce((sum, x) => sum + x, 0) → 6 |
21. 说说你对事件循环的理解
- 事件循环是为了处理一步操作,安排代码执行顺序的机制
- 事件循环的本质:
同步做完,异步排队等候 - 关键术语:
| 名称 | 含义 |
|---|---|
| 调用栈(Call Stack) | 当前执行的代码(同步任务) |
| 任务队列(Task Queue) | 异步任务的回调函数排队等待执行(比如 setTimeout) |
| 事件循环(Event Loop) | 不断检查“调用栈是否空”,有空就执行队列任务 |
重点:
- 执行同步代码,调用栈
- 遇到异步任务(定时器,Promise等),把异步回调丢到“任务队列”
- 当前调用栈清空后,事件循环开始
- 任务队列的函数进入调用栈继续执行
宏任务 vs 微任务
| 类型 | 举例 | 执行优先级 |
|---|---|---|
| 微任务 | Promise.then、queueMicrotask | 高(先执行) |
| 宏任务 | setTimeout、setInterval、DOM事件 | 低(后执行) |
-
经典题:
console.log('1'); setTimeout(() => { console.log('2'); }, 0); Promise.resolve().then(() => { console.log('3'); }); console.log('4'); // 1,4,3,2 因为: - `console.log('1')` 是同步 → 立即执行 - `setTimeout(...)` 是宏任务 → 放入任务队列 - `Promise.then(...)` 是微任务 → 微任务队列 - `console.log('4')` 是同步 → 立即执行 - 微任务先于宏任务 → 打印 `3` - 最后执行宏任务 → 打印 `2` -
思考:async/await 也是基于事件循环
async function test() { console.log('A'); await Promise.resolve(); console.log('B'); } test(); console.log('C'); // A C B // await 后面的代码会变成微任务,等待同步代码执行完毕后执行
22. Javascript本地存储的⽅式有哪些?区别及应⽤场景
- 一句话: localStorage、sessionStorage、cookie、IndexedDB 是最常见的本地存储方式。
- 对比:
| 名称 | 存储大小 | 生命周期 | 是否自动发送给服务器 | 适合场景 |
|---|---|---|---|---|
| localStorage | 约 5MB | 永久,除非手动清除 | ❌ 不会 | 长期存储,如用户设置、token、本地缓存数据 |
| sessionStorage | 约 5MB | 会话结束就清除 | ❌ 不会 | 短期存储,如页面跳转传值、登录中转状态 |
| cookie | ~4KB | 可设置过期时间 | ✅ 会随请求发送给服务器 | 登录状态保持、跨页面识别用户等 |
| IndexedDB | 大于 50MB(按浏览器) | 永久 | ❌ 不会 | 存储结构化数据、大文件,如离线应用、本地数据库缓存 |
✅ 1. localStorage
📌 特点:
- 键值对存储(字符串格式)
- 数据长期保留,除非主动清理
- 页面刷新、浏览器关闭也不会丢
📦 示例代码:
localStorage.setItem('name', 'Tom');
let name = localStorage.getItem('name'); // 'Tom'
localStorage.removeItem('name');
📍 应用场景:
- 用户设置(主题色、语言)
- 本地 token(登录后身份校验)
- 页面缓存、偏好选项
✅ 2. sessionStorage
📌 特点:
- 和
localStorage类似,但只在当前标签页/会话中生效 - 关闭页面就没了
📦 示例代码:
sessionStorage.setItem('step', '2');
let step = sessionStorage.getItem('step');
sessionStorage.clear();
📍 应用场景:
- 页面跳转中的临时状态
- 表单填写缓存(防止刷新丢失)
- 登录页面中间步骤记录
✅ 3. cookie
📌 特点:
- 会自动随请求发送给服务端(可选)
- 数据量小(4KB 左右)
- 支持设置过期时间、安全选项(如 HttpOnly)
📦 示例代码(JS 设置 cookie):
document.cookie = 'token=abc123; expires=Fri, 01 Jan 2026 12:00:00 UTC';
📍 应用场景:
- 跨页面登录状态识别
- 与后端配合身份认证
- 追踪用户行为(常见于广告系统)
✅ 4. IndexedDB
📌 特点:
- 支持结构化数据(对象、数组等)
- 支持事务、大容量存储
- 异步操作
📦 示例代码:
let request = indexedDB.open('MyDB', 1);
request.onsuccess = function(event) {
const db = event.target.result;
// 使用 db 来增删改查
};
📍 应用场景:
- 离线网页应用(PWA)
- 缓存大量数据(如地图、图片、视频)
- 本地小型数据库
🧠 应用场景快速对照
| 需求 | 用什么? | 为什么? |
|---|---|---|
| 保存登录 token | localStorage | 页面刷新后也保留 |
| 跨页面传参数但不持久保存 | sessionStorage | 关闭就清除 |
| 后端要识别用户身份 | cookie | 自动带到服务器 |
| 离线存储用户资料 | IndexedDB | 数据大且复杂 |
🎯 小技巧建议
- 登录状态建议用:
localStorage + cookie(HttpOnly)组合 - 多页面共享数据建议用:
localStorage - 页面临时数据:
sessionStorage - 大数据离线缓存:
IndexedDB
23. ⼤⽂件上传如何做断点续传
- 一句话:切片-上传每一片-服务端整合-断点续传(遇到失败,只补传失败的切片)
- 步骤:
- 把大文件切成小块(比如每块 1MB)
// 前端 JS 例子 const chunkSize = 1 * 1024 * 1024; // 1MB const chunks = []; for (let i = 0; i < file.size; i += chunkSize) { chunks.push(file.slice(i, i + chunkSize)); }- 把每一片加上编号,然后逐个上传
- 上传时告诉服务器:第几块、第几号文件、总共有几块
- 失败重试,断点续传
- 上传前问服务器,你已经收到几块了?
- 然后跳过已经上传的,只传剩下的
- 上传完成后,告诉服务器可以整合了
- 后端把文件合起来,恢复成原始文件。
-
💡 通俗记忆口诀
🎬 "切块上传,失败重传,上传完后,后端合拢"
- 切块:用
slice()把文件分片 - 上传:逐个 POST 到服务器
- 失败重传:用记录(比如后端或本地)判断哪些还没传
- 合并:后端收到所有片后再拼成完整文件
✅ 补充知识点
| 概念 | 说明 |
|---|---|
slice() | JS 中切文件的方法 |
| MD5 | 可以给整个文件生成唯一标识,便于断点记录 |
| 并发上传 | 多个切片可以同时上传,提速!Promise.all |
| FormData | 用于前端上传文件数据 |
如果你用 Vue + Vite,上传流程可以这样配合:
js
复制编辑
const uploadChunk = async (chunk, index) => {
const form = new FormData()
form.append('file', chunk)
form.append('index', index)
form.append('fileId', 'xxxx') // 文件唯一标识
await axios.post('/api/upload-chunk', form)
}
- 拓展: 如何控制最大并发数量?下面封装一个并发函数
/**
* 并发控制器:限制同时运行的异步任务数量
* @param tasks 所有任务函数组成的数组,每个函数返回 Promise(如上传某个切片)
* @param limit 最大并发数(同时运行的任务上限)
*/
async function runWithConcurrencyLimit<T>(
tasks: (() => Promise<T>)[], // 每个任务是一个返回 Promise 的函数
limit: number // 最大同时执行的任务数量
): Promise<T[]> {
const results: T[] = [] // 用于收集所有任务的结果
let index = 0 // 当前准备执行的任务索引
let active = 0 // 当前正在执行的任务数量
return new Promise((resolve, reject) => {
const next = () => {
// 所有任务完成后,且没有正在执行的任务,返回最终结果
if (index >= tasks.length && active === 0) {
return resolve(results)
}
// 当有可用并发槽位且还有未执行的任务时,继续调度
while (active < limit && index < tasks.length) {
const currentTask = tasks[index++] // 取出当前任务函数并递增索引
active++ // 标记有一个任务开始执行
currentTask()
.then(res => {
results.push(res) // 成功后存储结果
})
.catch(reject) // 任何任务失败则直接中止并抛出错误
.finally(() => {
active-- // 一个任务执行完成,释放一个并发槽
next() // 尝试继续执行下一个任务
})
}
}
next() // 初始化调度
})
}
假设我们有 5 个任务:
tasks = [task1, task2, task3, task4, task5]
并发限制是:
limit = 2
意味着“同一时间最多执行 2 个任务”。
🧠 关键变量说明
| 变量名 | 含义 |
|---|---|
index | 下一个要启动的任务下标 |
active | 当前正在执行的任务数量 |
results | 保存已完成任务的结果 |
limit | 最大并发数 |
🧩 执行流程(以 limit=2 举例)
第 1 轮:开始调度
index = 0active = 0next()被第一次调用
while (active < limit && index < tasks.length) 成立
→ 进入循环两次(因为 limit=2)
currentTask = tasks[0]
active++ // active = 1
currentTask().then(...).finally(() => { active--; next() })
currentTask = tasks[1]
active++ // active = 2
currentTask().then(...).finally(() => { active--; next() })
此时:
正在执行:task1, task2
active = 2
index = 2
当 task1 完成时(执行 .finally()):
active-- // 从 2 变成 1
next() // 再次调度
执行 next() 内的 while:
此时 active=1, limit=2,所以还有空位
→ 再启动下一个任务 task3。
active++ // 从 1 -> 2
index++ // 从 2 -> 3
现在:
正在执行:task2, task3
active = 2
index = 3
当 task2 完成时:
同理:
active-- // 从 2 -> 1
next() // 调度
→ 启动 task4。
此时:
正在执行:task3, task4
active = 2
index = 4
当 task3 完成时:
active-- // 2 -> 1
next()
→ 启动 task5。
正在执行:task4, task5
active = 2
index = 5
当 task4 完成时:
active-- // 2 -> 1
next()
但此时 index = 5 === tasks.length,所以不会再启动新任务。
→ 继续等待最后一个任务 task5。
当 task5 完成时:
active-- // 1 -> 0
next()
再执行到这行判断:
if (index >= tasks.length && active === 0) {
resolve(results)
}
→ 所有任务完成,返回结果 ✅
🔁 总结逻辑
| 事件 | active 变化 | 含义 |
|---|---|---|
| 启动任务时 | active++ | 占用一个并发槽 |
| 任务结束时 | active-- | 释放一个并发槽 |
释放后调用 next() | 尝试继续分配新任务 |
24. ajax原理是什么?如何实现
- 原理
// 创建XMLHttpRequest
const xhr = new XHLHttpRequest()
// 2. 配置请求:method, url, async(是否异步)
xhr.open('GET', 'https://api.example.com/data', true);
// 3. 监听状态变化(当 readyState 和 status 满足条件,说明请求完成)
xhr.onreadystatechange = function () {
if (xhr.readyState === 4 && xhr.status === 200) {
// 4. 处理响应数据(通常是 JSON)
const data = JSON.parse(xhr.responseText);
console.log(data);
// 可以更新 DOM 内容
document.getElementById('result').innerText = data.message;
}
};
// 5. 发送请求
xhr.send();
- xhr.readyState 状态码
| 值 | 意义 |
|---|---|
| 0 | 未初始化 |
| 1 | 正在建立连接 |
| 2 | 请求已发送 |
| 3 | 正在接收响应 |
| 4 | 响应已完成 |
🚀 AJAX 的核心优势:
- 页面不刷新,用户体验好
- 提高前后端交互效率
- 可局部刷新、按需加载内容(如表格分页、搜索建议等)
25. 什么是防抖和节流?有什么区别?如何实现
- 一句话:
防抖是“等你不动了我再做”,节流是“每隔一段时间我就做”。
- 防抖(Debounce)
- 事件触发后等待一段时间,如果这段时间内再次触发,就再重新计时。只有最后一次触发会被执行。
-
应用场景:
- 搜索框输入(input)
- resize 事件
- 表单验证
- 什么是节流(Throttle)
- 限制事件在一定时间间隔内只能触发一次。
- 无论你触发多少次,只会隔一段时间执行一次。
-
应用场景:
- 页面滚动监听(scroll)
- 页面窗口缩放(resize)
- 拖拽监听(mousemove)
| 特性 | 防抖(Debounce) | 节流(Throttle) |
|---|---|---|
| 执行频率 | 停止一段时间才执行一次(最后一次) | 每隔一段时间执行一次 |
| 应用场景 | 搜索框、输入校验 | 滚动、拖动、节省资源 |
| 典型例子 | 停止输入时搜索 | 页面滚动优化 |
- 如何实现?(代码示例)
- 防抖函数
// fn每次执行都会先清除定时器,导致fn延迟被执行 function debounce(fn, delay) { let timer = null; return function (...args) { // 每次调用包装函数都会先清除上一次未执行的定时器, // 这样做的效果是:只要事件不断触发,定时器就一直被重置,`fn` 就不会执行。 clearTimeout(timer); timer = setTimeout(() => { fn.apply(this, args); }, delay); }; }
-
fn.apply(this, args):使用apply把原始调用时的this(即包装函数被调用时的this)和参数args传给fn。 -
这里非常重要:
setTimeout的回调是一个箭头函数() => { ... },箭头函数不会改变this,它会“词法捕获”外层函数的this。因此this在fn.apply(this, args)中是包装函数被调用的上下文(通常是你期望的那个this)。 -
如果你把
setTimeout回调写成普通函数function() { fn.apply(this, args) },回调里的this会不一样(通常是window或undefined),所以要小心。 -
this与apply为什么要这样用-
apply(this, args)的目的就是正确地把包装函数被调用时的this绑定给被包装的fn,并传入参数数组args。 -
示例:
obj.method = debounce(fn),obj.method()希望fn内部的this指向obj。如果不apply、或this被 setTimeout 改掉了,就会出错。
- 节流函数
function throttle(fn, interval) { let lastTime = 0; return function (...args) { const now = Date.now(); if (now - lastTime >= interval) { lastTime = now; fn.apply(this, args); } }; } -
关于防抖函数中的apply
假设我们要给一个按钮绑定防抖点击事件:
const btn = document.querySelector("button");
btn.onclick = debounce(function() {
console.log(this); // 我希望这里 this -> btn
}, 500);
但是如果你直接写:
setTimeout(fn, delay);
那么执行的时候,fn 是 普通函数调用,this 不会再是 btn,而是变成 window 或 undefined。
3. 用 apply(this, args) 保留 this
当我们在 debounce 返回的函数里写:
fn.apply(this, args);
这里的 this 指的就是 调用 debounce 返回函数时的 this。
- 在
btn.onclick = debounce(...);的场景下:
this是btn。
于是 fn.apply(this, args) → 等价于
fn.call(btn, ...args)
这样就能保证:fn 执行时,里面的 this 还是 btn,没有丢失。
4. 改写版示例
function debounce(fn, delay) {
let timer = null;
return function(...args) {
clearTimeout(timer);
// 保存当前 this
const context = this;
timer = setTimeout(() => {
fn.apply(context, args); // 用 context 保留原来的 this
}, delay);
};
}
const btn = document.querySelector("button");
btn.onclick = debounce(function() {
console.log(this); // this 仍然是 <button>
}, 500);
输出结果:点击按钮时,this 正确打印 button 元素。
26. 如何判断⼀个元素是否在可视区域中
- 两种方法:
-
方法一:使用getBoundingClientRect() (最常用,兼容性好,语义直观)
- 核心原理:
const rect = element.getBoundingClientRect();它会返回元素相对于视口的大小和位置,如:
{ top: 120, left: 0, bottom: 300, right: 500, width: 500, height: 180 }// 判断是否在可是区域 function isInViewport(el){ const rect = el.getBoundingClientRect() return { rect.top >= 0 && rect.left >= 0 && rect.bottom <= window.innerHeight && rect.right <= window.innerWidth } } -
方法二:使用IntersectionObserver(更现代)
- 浏览器提供的api,可以自动监听元素离开还是进入视口
const observer = new IntersectionObserver((entries) => { entries.forEach(entry => { if (entry.isIntersecting) { console.log('元素进入可视区域'); } else { console.log('元素离开可视区域'); } }); }); // 监听某个元素 const el = document.getElementById('target'); observer.observe(el);
-
- 总结
| 方法 | 是否推荐 | 场景 |
|---|---|---|
getBoundingClientRect() | ✅经典通用 | 简单判断,配合滚动事件 |
IntersectionObserver | ✅现代推荐 | 监听进入/离开视口,懒加载 |
27. 什么是单点登录SSO (Single Sign-on)?如何实现
- 登录一次,多个系统(网站)都能自动登录,不用每次都输入账号密码
- 常用于:
- 一个公司有多个后台系统(OA、人事系统、财务系统)
- 一个用户只登录一次,其他系统自动登录,无需重复输入密码
- 工作原理:
✅ SSO 的核心是:
- 统一认证中心(统一登录系统)
- 登录状态共享(跨系统识别你已登录)
- 跳转机制(未登录就跳转去认证中心)
🔧 常见的实现方式:
🥇 方式一:基于 Cookie + 同域名
- 多个子系统使用相同主域(如
a.test.com、b.test.com) - 在主域下设置 cookie(如设置在
.test.com),所有子系统都能读到 - 用户登录一次后,每个子系统访问时都能识别 cookie
适用场景:同域名的多个子系统
🥈 方式二:基于 Token(JWT)+ 跳转
适用于不同域名的系统,比如:
系统A: a.com
系统B: b.com
认证中心: sso.com
实现流程:
- 你访问 a.com,没有登录
- 被跳转到 sso.com 登录中心
- 登录成功后,SSO 中心生成 token,并带回 a.com
- a.com 保存 token,标记你已登录
- 你访问 b.com,b.com 检查没有登录
- 又跳转到 sso.com,SSO 中心检测你已登录
- SSO 中心再次生成 token 返回 b.com,完成登录
✅ Token 可用方式:
- 放在 URL 参数中:
b.com?token=xxxxx - 放在 cookie 或 localStorage 中
- 可使用 JWT(JSON Web Token)进行加密传输
28. 如何实现上拉加载,下拉刷新
29. 说说你对正则表达式的理解?应⽤场景?
30. 说说你对函数式编程的理解?优缺点
- 函数式编程是一种“把计算过程看成函数的组合”的编程方式,强调无副作用、不可变数据**,核心思想是:函数就是值,代码像数学公式一样干净可预测。**
- 函数式编程就像开汉堡连锁店:
- 你每次传入相同的原料(输入)
- 都做出一模一样的汉堡(输出)
- 中间不污染环境,不偷拿材料,不动刀换配方(无副作用)
31. web常见的攻击⽅式有哪些?如何防御
1. 🦠 XSS(跨站脚本攻击)
Cross Site Scripting
📌 原理:
攻击者在网页中注入恶意脚本,当用户访问页面时被执行,可能盗取 Cookie、劫持登录、跳转钓鱼站等。
⚠️ 示例:
html
<input value="<script>alert('中招')</script>" />
用户打开网页后,JS 代码就会执行。
✅ 防御方法:
| 手段 | 说明 |
|---|---|
| ✅ 对用户输入进行转义 | < → <,> → > |
| ✅ 使用 CSP(内容安全策略) | 禁止内联脚本,限制资源来源 |
✅ DOM 插值时用 textContent | 不用 innerHTML |
| ✅ 后端过滤危险标签 | 如 <script>、<iframe> 等 |
2. 🧬 CSRF(跨站请求伪造)
Cross Site Request Forgery
📌 原理:
用户登录了网站 A,但打开了攻击者的 B 网站,B 利用浏览器带的 Cookie 向 A 发起请求,伪造用户操作。
⚠️ 示例:
你登录了银行网站没退出,然后访问了一个恶意页面:
html
<img src="https://bank.com/transfer?to=attacker&money=10000" />
浏览器会自动带 cookie,把钱转走了。
✅ 防御方法:
| 手段 | 说明 |
|---|---|
| ✅ 加 CSRF Token | 后端发一个随机 token,提交表单时必须带上 |
| ✅ 检查 Referer / Origin | 拒绝非本站域名的请求 |
| ✅ 使用 SameSite Cookie | 设置 Cookie 的 SameSite 属性,阻止跨站发送 |
| ✅ 不用 GET 做敏感操作 | 比如改密码、转账等必须用 POST/PUT |
3. 🐟 钓鱼攻击(Fake Login / Clickjacking)
📌 原理:
做一个和真实网站一模一样的登录页,引导你输入账号密码,然后窃取。
也可能用 iframe + CSS 把你引导去点击“看不见的按钮”。
✅ 防御方法:
- 限制 iframe 加载:
X-Frame-Options: DENY - 真实登录页使用 HTTPS + 域名校验
- 启用双因子验证(2FA)
4. 🔐 SQL 注入
📌 原理:
用户在输入框输入 SQL 代码,拼接到 SQL 语句里执行,导致查询泄露或修改数据。
⚠️ 示例:
sql
SELECT * FROM users WHERE username = 'admin' OR 1=1
✅ 防御方法:
| 手段 | 说明 |
|---|---|
| ✅ 使用预处理语句 / 参数化查询 | 如 prepareStatement 或 ORM 自动转义 |
| ✅ 后端校验字段类型 | 防止拼接 SQL |
| ✅ 不拼接 SQL 字符串 | 严禁使用字符串拼接动态 SQL |
5. 💣 DOS/DDOS 攻击(拒绝服务)
📌 原理:
攻击者用大量请求压垮服务器,使其无法正常服务。
✅ 防御方法:
| 手段 | 说明 |
|---|---|
| ✅ 加入限流机制 | 如 Nginx 的 limit_req、后端中间件限流 |
| ✅ 使用 CDN + 防火墙 | 阿里云、Cloudflare 可做抗 DDoS |
| ✅ 设置超时时间 / 黑名单 IP |
6. 📂 文件上传漏洞
📌 原理:
用户上传恶意文件(如 .php 脚本),被服务器执行,控制系统。
✅ 防御方法:
- 限制文件类型(MIME 类型 + 后缀双验证)
- 文件重命名 + 隔离存储(如非公开路径)
- 设置执行权限(上传目录不允许执行)
- 检查文件内容魔数,不仅靠扩展名
🧩 其他常见攻击方式(了解即可):
| 攻击名 | 简要说明 |
|---|---|
| 📊 中间人攻击(MITM) | 网络被监听,未加密数据被窃取 |
| 🧱 开放重定向 | URL 参数可被修改,诱导跳转到恶意网站 |
| 💥 SSRF(服务器请求伪造) | 后端服务被利用去访问内网资源(如 127.0.0.1) |
| 📤 信息泄露 | 错误堆栈、API 接口返回敏感字段 |
✅ 总结口诀(面试速记):
XSS 注脚本、CSRF 伪请求、SQL 拼注入、上传防执行、DOS 要限流、敏感别暴露
✅ Web 安全攻击方式与防御速查表(完整版)
| 编号 | 攻击方式 | 原理简述 | 常见场景 | 防御手段 |
|---|---|---|---|---|
| 1 | XSS 跨站脚本 | 注入 JS 代码,被浏览器执行 | 评论区、搜索框、富文本等 | ✅ 输入/输出转义 ✅ 不用 innerHTML ✅ 设置 CSP |
| 2 | CSRF 跨站请求伪造 | 利用用户 Cookie 向目标站伪造请求 | 登录用户未退出账号的操作(如转账) | ✅ CSRF Token ✅ 检查 Referer/Origin ✅ SameSite Cookie |
| 3 | SQL 注入 | 拼接 SQL 时注入恶意语句 | 登录、搜索、查询接口 | ✅ 使用预处理语句 ✅ 参数化查询 ✅ 严格校验输入 |
| 4 | 文件上传漏洞 | 上传恶意脚本被执行 | 上传头像、附件等接口 | ✅ 限制文件类型 ✅ 不保存原始文件名 ✅ 关闭执行权限 |
| 5 | DOS/DDOS | 大量请求压垮服务器 | 公开接口被刷、爬虫攻击 | ✅ 限流(Nginx/中间件) ✅ CDN防护 ✅ 黑名单/验证 |
| 6 | 钓鱼攻击 / ClickJacking | 伪造登录页面或诱导点击 | 登录页、支付页等 | ✅ X-Frame-Options ✅ 域名验证 ✅ 二次确认操作 |
| 7 | 信息泄露 | 返回调试信息、堆栈、敏感字段 | 接口报错/返回值未脱敏 | ✅ 统一错误处理 ✅ 删除敏感字段 ✅ 关闭调试模式 |
| 8 | 中间人攻击 MITM | 网络被劫持,篡改请求或读取数据 | 公共 Wi-Fi / HTTP 请求 | ✅ 全站 HTTPS ✅ TLS 加密传输 ✅ HSTS 策略 |
| 9 | SSRF 服务端请求伪造 | 后端被诱导访问内部资源 | 图片地址、Webhook 回调 | ✅ 限制请求 IP 域名 ✅ 禁用本地回环地址 ✅ URL 白名单 |
| 10 | 开放重定向 | 参数可被修改导致跳转到恶意站点 | 登录后跳转、分享链接 | ✅ 验证跳转 URL 是否可信 ✅ 使用 URL 白名单 |
🚨 前端项目中常用的防御建议
| 场景 | 防御方案 |
|---|---|
| 表单提交 | 添加 csrf-token,使用 POST 请求 |
| 显示用户数据 | 所有动态内容用 textContent 而非 innerHTML |
| 显示 HTML 内容 | 使用 DOMPurify 等库做 HTML 清洗 |
| 登录状态管理 | 使用 HttpOnly + SameSite Cookie |
| 上传文件 | 限制类型、校验魔数、文件重命名、服务器隔离存储 |
| 接口设计 | 永远不信前端传参,后端强校验 |
| 控制台日志 | 不打印敏感信息(如 token、手机号) |
🧰 推荐工具与库
| 类型 | 工具 |
|---|---|
| XSS 过滤 | DOMPurify(前端) / 后端模板转义 |
| CSRF 防御 | csrf 中间件(如 Express/Koa 插件) |
| 限流 | express-rate-limit / nginx limit_req |
| Token 加密 | jsonwebtoken / crypto |
| 安全扫描 | npm audit / OWASP ZAP / SonarQube |
| 监控告警 | 阿里云安骑士 / 腾讯云云镜 / 自建日志系统 |
32. 说说 JavaScript 中内存泄漏的⼏种情况
- 内存泄漏:指的是程序中不再使用的数据依旧占用内存,无法被垃圾回收器回收,导致内存持续增加
-
总结口诀(记忆用):
闭包没清、定时忘停、事件不解绑、DOM 被引用、缓存没清理。
✅ 如何定位内存泄漏?
- 浏览器 DevTools → Memory → Heap Snapshot → 看未释放对象
- Performance → Timeline → Watch memory trend
- 使用
WeakMap/WeakSet存储临时对象(自动释放)
33. Javascript如何实现继承
🧩 常见继承方式:
| 方式 | 简述 |
|---|---|
| 原型链继承 | 子类原型指向父类实例 |
| 借用构造函数 | 在子类构造函数中调用父类构造函数 |
| 组合继承(最常用) | 原型链 + 构造函数的结合 |
ES6 class 继承 | 语法糖,底层依然是原型链 |
| ✅ 示例:组合继承(经典) |
function Parent(name){
this.name = name;
}
Parent.prototype.say = function () {
console.log('hi', this.name);
};
function Child(name, age) {
Parent.call(this, name); // 借用构造函数
this.age = age;
}
Child.prototype = Object.create(Parent.prototype); // 原型链继承
Child.prototype.constructor = Child;
const child = new Child('Tom', 18);
child.say(); // hi Tom
✅ ES6 class 实现继承(推荐写法)
js
复制编辑
class Parent {
constructor(name) {
this.name = name;
}
say() {
console.log('hi', this.name);
}
}
class Child extends Parent {
constructor(name, age) {
super(name); // 等价于 Parent.call(this, name)
this.age = age;
}
}
🧠 面试这样说:
JS 是基于原型的语言,常见继承方式有原型链、借用构造函数、组合继承和 ES6 的 class 继承。最推荐的是组合继承和 ES6 的
extends,它既能继承属性,也能继承方法,避免原型共享带来的副作用。
✅ 34. JavaScript 数字精度丢失问题,如何解决?
🎯 一句话理解:
JS 使用 **64位双精度浮点数(IEEE 754)**表示数字,无法精确表示小数或大整数,会出现精度丢失问题。
🔥 经典例子:
js
复制编辑
0.1 + 0.2 === 0.3 // ❌ false
console.log(0.1 + 0.2); // 0.30000000000000004
✅ 原因:
JS 把数字转成二进制时,某些小数变成无限循环二进制,舍入误差导致精度问题。
🛠️ 解决方案:
| 方法 | 示例 |
|---|---|
| ✅ 手动放大缩小 | (0.1 * 10 + 0.2 * 10) / 10 |
✅ 使用 toFixed | Number((0.1 + 0.2).toFixed(2)) |
✅ 使用 BigInt | 处理大整数:BigInt(9007199254740991) |
| ✅ 使用专门库 | 如 decimal.js、bignumber.js |
✅ decimal.js 示例:
js
复制编辑
import Decimal from 'decimal.js';
const sum = new Decimal(0.1).plus(0.2);
console.log(sum.toString()); // "0.3"
🧠 面试这样说:
JS 使用 IEEE754 标准,导致某些浮点数运算不精确,如
0.1 + 0.2 != 0.3。常用解决方式是放大缩小处理,或使用 BigInt / decimal.js 等库来处理高精度运算。
✅ 35. 举例说明你对尾递归的理解,有哪些应用场景?
🎯 一句话理解:
**尾递归(Tail Recursion)**是递归中的一种优化形式,函数调用自身时,返回值是直接返回,不做额外运算,可被引擎优化为循环,节省内存。
✅ 普通递归(非尾递归):
js
复制编辑
function sum(n) {
if (n === 1) return 1;
return n + sum(n - 1); // ⛔️ 返回后还有加法,不能优化
}
每次递归都要等前一个返回后再加法,容易栈溢出。
✅ 尾递归(返回的就是递归结果):
js
复制编辑
function tailSum(n, acc = 0) {
if (n === 0) return acc;
return tailSum(n - 1, acc + n); // ✅ 直接返回递归结果
}
尾递归优化后相当于循环:
js
复制编辑
let acc = 0;
for (let i = n; i > 0; i--) {
acc += i;
}
⚠️ 注意:
- JavaScript 引擎(如 V8)并未默认开启尾递归优化(即使写对也可能没优化)
- ES6 标准支持尾调用优化,但实际浏览器不普遍支持
✅ 应用场景:
| 场景 | 原因 |
|---|---|
| ✅ 递归计算 | sum、factorial、斐波那契等 |
| ✅ 大数据遍历 | 用递归避免循环回调嵌套 |
| ✅ 减少栈溢出 | 比普通递归节省内存,适合大规模递归调用场景 |
🧠 面试这样说:
尾递归是指函数调用自身时是“最后一步”,不会做额外运算,理论上可被优化为迭代,减少调用栈。虽然目前大部分 JS 引擎未开启尾调优化,但代码写成尾递归形式依然有助于理解递归的本质,并在某些语言中获得性能提升。