https://jinxi1334640772.github.io/
Javascript 介绍
javascript 是一个基于原型、多范式、单线程的动态语言,并且支持面向对象、命令式编程和声明式编程的风格
动态特性的体现有:
运行时对象构造:可以在运行时创建和修改对象
变量参数列表:函数是可以接受任意数量的参数的讷
函数变量:函数可以作为变量传递和赋值
动态脚本创建:可以通
eval函数进行动态的执行代码对象内可枚举:通过
for...in和Object工具方法遍历对象源代码可恢复:函数会进行存储源代码文本,可以使
toString进行检索吧
const foo = function () {
console.log('hello world');
}
const ENUM = {
'function': 'function',
'variable': 'var|const|let'
}
function judgeCodeType(code) {
const codeType = code.match(ENUM.variable) ? ENUM.variable : ENUM.function;
return codeType;
}
console.log(judgeCodeType('const a = 1;'));
JavaScript 的核心部分可以通过添加对象来扩展语言以适应不同用途:
客户端 JavaScript
提供对象控制浏览器及其文档对象模型(DOM)
支持响应用户事件(鼠标点击、表单提交、页面导航)
允许应用程序将元素放在HTML表单中
服务端 JavaScript
提供有关在服务器上运行 JavaScript 的对象
支持应用和数据库通信
提供应用不同调用间的信息连续性
在服务器上执行文件操作
Javascript Proxy和Reflect
JavaScript 的元编程功能允许开发者拦截并自定义基本语言操作。Proxy 和 Reflect 对象是实现元编程的核心工具,它们提供了强大的拦截和反射能力
元编程是指编写能够操作其他程序(或自身)的程序。在 JavaScript 中,元编程主要通过 Proxy 和 Reflect 对象实现,允许拦截并自定义基本语言操作,例如:
属性查找和赋值
枚举操作
函数调用
对象构造
原型操作
Javascript Proxy
Proxy 进行实现的是对于我们的对象创建出一个新的代理对象的操作实现吧,从而实现基本操作的拦截和自定义实现
Proxy是「拦截层」,负责拦截对象 / 函数的底层操作(如get/set/apply);Reflect是「标准化执行层」,负责正确执行被拦截的原操作(解决this绑定、返回值不统一等问题)
const proxy = new Proxy(target, handler);其中进行设置拦截器的具体操作就是在 handler 中进行实现吧
const target = {
"name": "76433",
id: 123456,
phone: '13800000000',
infomations: {
school: '清华大学',
major: '计算机科学与技术',
job: '软件工程师'
}
}
// 定义一下现需要对于我们的数据进行拦截的处理函数吧
const handler = {
/**
* 拦截获取属性操作
* @param {Object} target 目标对象
* @param {String|Symbol} property 目标对象的属性名
* @param {Proxy} receiver 代理对象
*/
get(target, property, receiver) {
console.log('正在操作的属性是:' + property);
// 进行对属性的具体操作实现吧
// 这里就不进行额外的操作了
return Reflect.get(target, property, receiver);
},
/**
* 拦截设置属性操作
* @param {Object} target 目标对象
* @param {String|Symbol} property 目标对象的属性名
* @param {any} value 要设置的新值
* @param {Proxy} receiver 代理对象
*/
set(target, property, value, receiver) {
console.log('正在设置的属性是:' + property);
return Reflect.set(target, property, value, receiver);
},
/**
* 拦截属性存在性检查操作
* @param {Object} target 目标对象
* @param {String|Symbol} property 目标对象的属性名
* @param {Proxy} receiver 代理对象
*/
has(target, property, receiver) {
console.log('正在检查的属性是:' + property);
// 进行对属性的具体操作实现吧
// 这里就不进行额外的操作了
return Reflect.has(target, property, receiver);
}
}
const target_proxy = new Proxy(target, handler);
// 此时我们需要进行给外部操作的数据就是我们的 target_proxy;
/**
* 此时的访问流程就是:
* 1. 向handler配置中传入 target 为target,property 为 id
* 2. 调用 get 方法,将 target、id、target_proxy 传入
* 3. get 方法中调用 Reflect.get(target, property, receiver),将 target、id、target_proxy 传入
* 4. 返回 123456
*/
console.log(target_proxy.id);
// 代理对象修改了属性,原对象的属性也会被修改吧
target_proxy.id = 654321;
console.log(target.id)
console.log('id' in target_proxy)
/**
正在操作的属性是:id
123456
正在设置的属性是:id
654321
正在检查的属性是:id
true
*/createValidateProxy 工具
/**
* 创建一个代理对象,这个代理对象会对对象的属性进行自定义规则的校验吧
* 核心思路:用Proxy拦截属性的读取/赋值,递归处理嵌套对象,保证深层属性也能校验吧
* @param {Object} target 需要进行代理的对象吧
* @param {Object} rules 校验的时候具体的规则定义吧
* @returns {Proxy} 带校验逻辑的代理对象吧
*/
function createValifateProxy(target, rules = {}) {
// 使用WeakMap来跟踪已经包装过的对象,避免无限递归和重复包装
const wrappedObjects = new WeakMap();
/**
* 递归包装嵌套对象的内部函数吧
* 目的:让嵌套的子对象(比如 target.user.address)也能被Proxy拦截,实现深层校验吧
* @param {Object} obj 要包装的对象(可能是顶层/嵌套的对象)吧
* @returns {Proxy|any} 包装后的Proxy对象(非对象类型直接返回原值)吧
*/
function wrapNested(obj) {
// 只处理对象类型、非null
if (typeof obj !== 'object' || obj === null) {
return obj;
}
// 如果已经包装过,直接返回已有的Proxy对象
if (wrappedObjects.has(obj)) {
return wrappedObjects.get(obj);
}
// 遍历对象的每个属性,递归包装嵌套的子对象吧
for (const key in obj) {
// 如果子属性也是对象,就递归调用wrapNested包装吧
if (typeof obj[key] === 'object' && obj[key] !== null) {
obj[key] = wrapNested(obj[key]);
}
}
// 给当前对象创建Proxy
const proxyObj = new Proxy(obj, handler);
// 将原始对象和代理对象的映射关系存储在WeakMap中
wrappedObjects.set(obj, proxyObj);
return proxyObj;
}
/**
* Proxy的拦截器配置吧
* 核心:拦截get(读取属性)和set(赋值属性)操作,加入业务规则校验吧
*/
const handler = {
/**
* 拦截属性读取操作的钩子吧
* @param {Object} target 目标对象(原始对象,非Proxy)吧
* @param {String|Symbol} property 要读取的属性名(比如name/age/_id)吧
* @param {Proxy} receiver 代理对象本身吧
* @returns {any} 读取到的属性值(嵌套对象会自动包装成Proxy)吧
*/
get(target, property, receiver) {
/**
* 具体的业务需求:用元数据编程限制私有属性访问吧
* 禁止访问以 _、__、# 开头的属性(符合ECMAScript私有属性规范)吧
* 例外:
* 1. 允许访问内部使用的__proxy__属性(用于避免重复包装对象)
* 2. 允许访问Vue 3响应式系统内部使用的属性(以__v_开头)
* 3. 允许访问Symbol类型的属性
*/
// 如果属性是Symbol类型,直接允许访问
if (typeof property === 'symbol') {
const value = Reflect.get(target, property, receiver);
return wrapNested(value);
}
const validateRegexp = /^(_|__|#)/;
// 如果属性名匹配私有规则且不是内部使用的例外属性,直接抛出错误拦截访问吧
if (validateRegexp.test(property) && property !== '__proxy__' && !/^__v_/.test(property)) {
throw new Error('不允许访问以 _ __ # 开头的属性');
}
// 用Reflect读取原始属性值(保证this指向和原生行为一致)吧
const value = Reflect.get(target, property, receiver);
// 访问到的如果是嵌套对象,自动包装成Proxy(实现深层拦截)吧
return wrapNested(value);
},
/**
* 拦截属性赋值操作的钩子吧
* @param {Object} target 目标对象吧
* @param {String|Symbol} property 要赋值的属性名吧
* @param {any} value 要赋的新值吧
* @param {Proxy} receiver 代理对象本身吧
* @returns {Boolean} 赋值是否成功(符合Proxy规范必须返回布尔值)吧
*/
set(target, property, value, receiver) {
/**
* 赋值时的校验逻辑:
* 1. 私有属性已经被get拦截了,这里不用处理私有属性赋值吧
* 2. 先取当前属性的校验规则,有规则才校验,无规则直接赋值吧
*/
// 先把当前属性的校验规则取出来,后续频繁使用吧
const validateRule = rules[property];
// 如果规则存在,才执行校验逻辑吧
if (validateRule) {
// 解构规则里的类型和自定义校验器吧
const { type, validator } = validateRule;
// 1. 基础类型校验:必须符合javascript的原生类型规范吧
if (type && typeof value !== type.toLowerCase()) {
throw new Error(`属性 ${property} 类型必须为 ${type}`);
}
// 2. 自定义校验器:支持两种格式(函数/对象)吧
// 第一种:校验器是函数形式(简单场景)吧
if (validator && typeof validator === 'function') {
// 取规则里的错误提示,没有就用默认提示吧
const { message } = validateRule || {};
// 调用校验函数,返回false则抛出错误吧
if (!validator(value)) {
throw new Error(message || `属性 ${property} 校验失败`);
}
}
// 第二种:校验器是对象形式(复杂场景,带自定义方法和提示)吧
else if (validator && typeof validator === 'object') {
const { message } = validator || {};
// 调用对象里的validate方法进行校验吧
if (!validator.validate(value)) {
throw new Error(message || `属性 ${property} 校验失败`);
}
}
}
// 3. 校验通过后:如果赋值的是嵌套对象,先包装成Proxy再赋值吧
const wrappedValue = wrapNested(value);
// 用Reflect执行原生赋值操作(保证行为一致)吧
Reflect.set(target, property, wrappedValue, receiver);
// 返回true表示赋值成功(符合Proxy的规范要求)吧
return true;
}
};
// 初始化:把顶层对象包装成Proxy并返回吧
return wrapNested(target);
}
export {
createValifateProxy
};
这里需要注意一点的是:proxy 默认只对对象浅层,也就是第一层加上代理拦截,需要额外注意一下吧
Object.defineProperties 只是 Object.defineProperty 的 “批量优化版”,核心仍停留在 “属性级拦截”,没解决 ODP 的核心痛点(嵌套繁琐、新增属性无拦截、仅支持 get/set);而 Proxy 是从 “对象级” 层面设计的拦截方案,覆盖了更全的操作、更简洁的嵌套处理、更安全的原对象隔离—— 这也是 Vue3 放弃 ODP/ODPs、拥抱 Proxy 的核心原因
前端手写题
new 的实现
new 操作符是用来进行创建对象实例用的讷,理解内部的机制对于掌握 javascript 的面向对象编程十分的重要吧
实现原理总结
1. 创建一个空对象,设置原型为构造函数的原型
2. 将构造函数的 this 指向新创建的对象
3. 执行构造函数,为新对象添加属性
4. 如果构造函数返回对象,则返回该对象,否则返回新创建的对象吧
// 我们先看看 new 的具体使用吧
/**
* 先定义一个我们的构造函数吧,可以是 class 也可以是 function
* 那就先来 function 吧
* 原型链的知识先总结一下
* 1. 普通对象而言的话
* 1.1 每个对象是具备隐式原型的讷:obj.__proto__
* 2. 对于函数而言的话
* 2.1 函数具备隐式原型和显式原型
* 直接进行设置即可吧
*/
// 构造函数的具备的特性有
// 1. 进行对应的操作:Constructor.prototype.constructor = 'Constructor'
function Person(name, age) {
this.name = name;
this.age = age;
}
const person = new Person("luhanxin", 18);
// 这一步说明的是:构造函数 new 出来的实例指向的是构造函数的显式原型吧
console.log(Object.getPrototypeOf(person) === Person.prototype)
/**
* Object.create() 函数的使用吧
*/
const obj = {
name: "hello",
age: 16
}
const _obj = Object.create(obj);
console.log(_obj.__proto__ === obj)
const __obj = Object.create(Person.constructor);
console.log(__obj.__proto__ === Person.constructor)
function myNew(constructor, ...args) {
// 边界 case 的校验吧
if (Object.prototype.toString.call(constructor) !== "[object Function]") {
throw new TypeError("构造函数必须是一个函数吧");
}
// 1. 根据构造函数的原型创建一个新的对象出来吧,新的对象指向构造函数的原型吧
const obj = Object.create(constructor.prototype);
// 2. 执行一下构造函数获取得到函数执行结果吧以及进行绑定一下 this 讷
const result = constructor.apply(obj, args);
// 3. 进行对结果进行校验吧
return result instanceof Object ? result : obj;
}
const self_obj = myNew(Person, 'luhanxin', 18);
console.log(self_obj.__proto__ === Person.prototype);
1. 进行边界判断一下吧,因为我们的构造函数不是函数就是类 class 吧
2. 根据构造函数进行创建出新对象出来一下吧,该新对象的原型是指向的是我们的构造函数吧
const _obj = Object.create(proto)创建出来的新对象的原型指向的是我们设置的这个原型吧
由于是实例化的操作,我们的
_obj是一个对象吧,这里的特点是_obj.__proto__ = proto该方法也是可以进行的是设置一下配置的讷,具体查看一下 MDN 文档一下吧
call 函数实现
/**
* call 的使用实现吧
*/
function func(name, age) {
console.log(this, name, age);
}
func(); // undefined | window undefined undefined
func.call({}, 'luhanxin', 18) // {} luhanxin 18
/**
* 分析实现
* 1. call 的调用的具体形式:
* 1.1 需要一个执行的函数,func
* 1.2 需要传递函数调用的参数列表
* 2. 内部机制
* 2.1 绑定上下文的 this
* 2.1 执行函数
* 2.2 得到函数执行的结果吧
*/
/**
* context 需要绑定的上下文吧
* args 函数执行的时候的上下文列表吧
*/
Function.prototype.myCall = function (context, ...args) {
// 1. 进行边界 case 的校验吧
if (typeof this !== 'function') {
throw new TypeError("this 必须是 function type");
}
// 设置为安全的 上下文:window 和 nodejs 环境下的通用环境都是 globalThis 吧
context = context || globalThis;
// 创建唯一的属性名,避免覆盖原有属性
const fnSymbol = Symbol('fn');
// 在上下文中添加 this 吧
context[fnSymbol] = this;
// 调用函数,并且获取得到函数结果
const result = context[fnSymbol](...args);
// 清理一下空间吧,避免导致内存的浪费吧
delete context[fnSymbol];
// 返回结果即可吧
return result;
}原理解析:
call 函数的调用形式是
func.call(context, args1, args2, args3)也就是说 func 函数调用的时候,将 this 绑定在了我们的 context 上了
但是 this 是如何进行绑定的讷?有没有什么办法进行实现讷?那我也不知道呀!!呜呜!
此时可以做的就是:让该函数成为上下文的属性即可,以及通过链式调用来进行默认绑定 this 即可
在这个过程中如何避免一下额外的操作讷?就是如何避免向上下文添加属性的时候,属性 key 不会覆盖讷
此时就是利用我们的一个技术是:Symbol 实现创建全局唯一的 key 即可吧
以及进行函数的调用获取得到结果,返回回去即可
上下文的安全性如何保证讷??
不管是 nodejs 的全局上下文的 global 还是 浏览器的 window 环境,根环境都是我们的 globalThis 吧
特点是:call 绑定 this,显式修改 this ,以及绑定后函数进行立即调用
apply 函数实现
核心机制和 call 函数是十分类似的讷!不同的是参数的传递不同而已,传递的是数组吧,其他都是一样的讷
Function.prototype.myApply = function(context, argsArray) {
// 边界 case 的校验吧
if (typeof this !== 'function') {
throw new TypeError("调用对象必须是函数吧,,,");
}
// argsArray 可能是没有的,有了再判断是不是数组吧
if (argsArray && !Array.isArray(argsArray)) {
throw new TypeError("参数必须是数组吧");
}
// 安全的设置上下文讷
context = context || globalThis;
const args = argsArray ?? [];
// 安全的向上下文中添加属性吧
const fnSymbol = Symbol('fn');
context[fnSymbol] = this;
// 调用函数
const result = context[fnSymbol](...args);
// 清理内存
delete context[fnSymbol];
// 返回结果吧
return result;
}bind 函数实现
// 给函数原型添加自定义bind方法
Function.prototype.myBind = function (context) {
// 1. 校验调用者必须是函数
if (typeof this !== 'function') {
throw new TypeError('调用者必须是函数');
}
// 2. 保存原函数
const fn = this;
// 3. 提取bind的预设参数
const bindArgs = Array.prototype.slice.call(arguments, 1);
// 4. 定义返回的新函数
const boundFn = function () {
// 5. 提取新函数执行时传入的参数
const callArgs = Array.prototype.slice.call(arguments);
// 6. 关键:判断新函数是否被new调用
// - 如果是new调用:this指向boundFn的实例,此时原函数的this应该指向这个实例
// - 如果不是new调用:this指向bind传入的context
const finalContext = this instanceof boundFn ? this : context;
// 7. 执行原函数,绑定最终的this,合并预设参数和调用参数
return fn.apply(finalContext, bindArgs.concat(callArgs));
};
// 8. 关键:让新函数的原型继承原函数的原型(保证new调用时的原型链正确)
boundFn.prototype = Object.create(fn.prototype);
boundFn.prototype.constructor = fn;
// 9. 返回新函数
return boundFn;
};返回新函数:不立即执行,而是返回一个待执行的 “绑定函数”;
双场景 this 绑定:普通调用时 this 指向 bind 传入的上下文,new 调用时 this 指向实例;
参数柯里化:合并 bind 预设参数和新函数调用参数,传递给原函数
instanceOf 操作符手写
function myInstanceOf(left, right) {
// 处理一下 null 的情况吧
if (typeof right !== 'function') {
throw new TypeError('Right-hand side of \'instanceof\' is not an object');
}
if (right.prototype === null || right.prototype === undefined) {
throw new TypeError('Function has non-object prototype \'null\' in instanceof check');
}
if (typeof left !== 'object' || left === null) {
return false;
}
// 获取得到构造函数和实例对象的原型吧
const prototype = right.prototype;
let proto = Object.getPrototypeOf(left);
while (proto !== null) {
if (proto === prototype) {
return true;
}
proto = Object.getPrototypeOf(proto);
}
return false;
}
console.log(myInstanceOf({}, Object))instanceof 原理是只要构造函数的原型链上具备实例,那就是 true 吧
这里只是简单手写了一下,核心来说的话,ES6 还有一定的差异性吧
Object.create 函数
function myObjectCreate(proto, propertyOptions) {
// 原生规则:proto 必须是对象/函数 或 null,否则抛错
if ((typeof proto !== 'object' && typeof proto !== 'function') && proto !== null) {
throw new TypeError('Object prototype may only be an Object or null');
}
function Temp() {};
Temp.prototype = proto;
const obj = new Temp();
Temp.prototype = null;
if (propertyOptions !== undefined && propertyOptions !== null) {
if (typeof propertyOptions !== 'object') {
throw new TypeError('Property description must be an object');
}
Object.defineProperties(obj, propertyOptions);
}
return obj;
}会进行创建一个对象出来吧,并且返回,以及进行设置原型讷
通过临时构造函数
Temp绑定原型;new Temp()创建新对象,保证原型链指向proto;清空
Temp.prototype避免原型污染;可选通过
Object.defineProperties定义属性
数组的手写题
foreach 循环遍历
// 1. 定义一个数组吧
const arr = [1, 2, 3, 4, 5, 5];
arr.forEach((item, index, array) => {
/**
1 0 [ 1, 2, 3, 4, 5, 5 ]
2 1 [ 1, 2, 3, 4, 5, 5 ]
3 2 [ 1, 2, 3, 4, 5, 5 ]
4 3 [ 1, 2, 3, 4, 5, 5 ]
5 4 [ 1, 2, 3, 4, 5, 5 ]
5 5 [ 1, 2, 3, 4, 5, 5 ]
*/
console.log(item, index, array)
})原本的执行是:传递一个回调函数,然后回调函数的参数是
itemindexarray
// 1. 定义一个数组吧
const arr = [1, 2, 3, 4, 5, 5];
arr.forEach((item, index, array) => {
/**
1 0 [ 1, 2, 3, 4, 5, 5 ]
2 1 [ 1, 2, 3, 4, 5, 5 ]
3 2 [ 1, 2, 3, 4, 5, 5 ]
4 3 [ 1, 2, 3, 4, 5, 5 ]
5 4 [ 1, 2, 3, 4, 5, 5 ]
5 5 [ 1, 2, 3, 4, 5, 5 ]
*/
console.log(item, index, array)
}, [])
Array.prototype.myForEach = function(callback, thisArgs) {
if (!Array.isArray(this)) {
throw new TypeError("必须是数组吧");
}
const arr = Object(this);
const length = arr.length >>> 0;
for (let i = 0; i < length; i++) {
// 这里需要注意一下,和上面的 Object 操作是对应上的讷
// 只有真真有值的,不会输出空槽讷
if (i in arr) {
callback.call(thisArgs, arr[i], i, arr)
}
}
}
arr.myForEach((item, index, array) => {
console.log(item, index, array)
})filter 函数的实现
核心是进行的是对数组元素进行过滤用的讷
// 1. 定义一个数组吧
const arr = [1, 2, 3, 4, 5, 5];
// 2. 开始自定义函数吧
Array.prototype.myFilter = function(callback, thisArgs) {
if (!Array.isArray(this)) {
throw new TypeError("必须是数组吧");
}
const arr = Object(this);
const length = arr.length >>> 0;
const result = []
for (let i = 0; i < length; i++) {
// 这里需要注意一下,和上面的 Object 操作是对应上的讷
// 只有真真有值的,不会输出空槽讷
if (i in arr) {
if (callback.call(thisArgs, arr[i], i, arr)) {
result.push(arr[i])
}
}
}
return result
}
const newArr = arr.myFilter((item, index, array) => {
return true
})
console.log(newArr)map 函数实现
就是进行数组中的元素进行对应的操作是:每个元素进行映射一次把
整体上比 filter 还简单许多的讷
// 1. 定义一个数组吧
const arr = [1, 2, 3, 4, 5, 5];
Array.prototype.myMap = function(callback, thisArgs) {
if (!Array.isArray(this)) {
throw new TypeError("必须是数组吧");
}
const arr = Object(this);
const length = arr.length >>> 0;
const result = []
for (let i = 0; i < length; i++) {
// 这里需要注意一下,和上面的 Object 操作是对应上的讷
// 只有真真有值的,不会输出空槽讷
if (i in arr) {
result.push(callback.call(thisArgs, arr[i], i, arr))
}
}
return result
}
const newArr = arr.myMap((item, index, array) => {
return item * index
})
console.log(newArr)reduce 函数实现
/**
* 手写 reduce 方法
* @param {Function} callback - 回调函数
* @param {any} initialValue - 初始值
* @returns {any} 累计结果
*/
Array.prototype.myReduce = function(callback, initialValue) {
if (typeof callback !== 'function') {
throw new TypeError('callback must be a function');
}
const array = Object(this);
const length = parseInt(array.length) || 0;
if (length === 0 && arguments.length < 2) {
throw new TypeError('Reduce of empty array with no initial value');
}
let accumulator;
let startIndex = 0;
// 判断是否提供了初始值
if (arguments.length >= 2) {
accumulator = initialValue;
} else {
// 找到第一个有效的数组元素作为初始值
while (startIndex < length && !(startIndex in array)) {
startIndex++;
}
if (startIndex >= length) {
throw new TypeError('Reduce of empty array with no initial value');
}
accumulator = array[startIndex];
startIndex++;
}
// 遍历数组执行回调
for (let i = startIndex; i < length; i++) {
if (i in array) {
accumulator = callback(accumulator, array[i], i, array);
}
}
return accumulator;
};
// 测试
const numbers = [1, 2, 3, 4, 5];
const sum = numbers.myReduce((acc, cur) => acc + cur, 0);
console.log(sum); // 15
const product = numbers.myReduce((acc, cur) => acc * cur);
console.log(product); // 120find 函数实现
/**
* 手写 find 方法
* @param {Function} callback - 回调函数
* @param {any} thisArg - this 指向
* @returns {any} 找到的元素或 undefined
*/
Array.prototype.myFind = function(callback, thisArg) {
if (typeof callback !== 'function') {
throw new TypeError('callback must be a function');
}
const array = Object(this);
const length = parseInt(array.length) || 0;
for (let i = 0; i < length; i++) {
if (i in array) {
const value = array[i];
if (callback.call(thisArg, value, i, array)) {
return value;
}
}
}
return undefined;
};
// 测试
const users = [
{ id: 1, name: '张三', age: 25 },
{ id: 2, name: '李四', age: 30 },
{ id: 3, name: '王五', age: 35 }
];
const user = users.myFind(u => u.age > 28);
console.log(user); // { id: 2, name: '李四', age: 30 }数组扁平化
/**
* 数组扁平化 - 递归实现
* @param {Array} arr - 多维数组
* @param {number} depth - 扁平化深度,默认为 1
* @returns {Array} 扁平化后的数组
*/
function flattenRecursive(arr, depth = 1) {
const result = [];
for (const item of arr) {
if (Array.isArray(item) && depth > 0) {
result.push(...flattenRecursive(item, depth - 1));
} else {
result.push(item);
}
}
return result;
}
const nestedArray = [1, [2, 3], [4, [5, 6]], 7];
function flatCompletely(arr) {
return arr.reduce((prev, cur) => {
return Array.isArray(cur)
? prev.concat(flatCompletely(cur))
: prev.concat(cur)
}, [])
}
console.log(flatCompletely(nestedArray))
// 测试
const nestedArray = [1, [2, 3], [4, [5, 6]], 7];
console.log(flattenRecursive(nestedArray, 1)); // [1, 2, 3, 4, [5, 6], 7]
console.log(flattenDeep(nestedArray)); // [1, 2, 3, 4, 5, 6, 7]核心是使用我们的 reduce 来实现操作吧,这个函数是十分还用的讷
数组去重
/**
* 数组去重 - Set 方法(推荐)
* @param {Array} arr - 待去重数组
* @returns {Array} 去重后的数组
*/
function uniqueWithSet(arr) {
return [...new Set(arr)];
}
/**
* 对象数组去重 - 根据指定属性
* @param {Array} arr - 对象数组
* @param {string} key - 去重依据的属性名
* @returns {Array} 去重后的数组
*/
function uniqueByProperty(arr, key) {
const seen = new Map();
return arr.filter(item => {
const value = item[key];
if (seen.has(value)) {
return false;
}
seen.set(value, true);
return true;
});
}
function uniqueArray(arr) {
const map = {};
return arr.filter((item) => {
if (map[item]) {
return false;
}
map[item] = true;
return true
})
}
// 测试
const duplicateArray = [1, 2, 2, 3, 4, 4, 5];
console.log(uniqueWithSet(duplicateArray)); // [1, 2, 3, 4, 5]
const users = [
{ id: 1, name: '张三' },
{ id: 2, name: '李四' },
{ id: 1, name: '张三' },
{ id: 3, name: '王五' }
];
console.log(uniqueByProperty(users, 'id'));异步编程
Promise
/**
* 手写 Promise 实现
*/
class MyPromise {
constructor(executor) {
// Promise 状态
this.state = 'pending';
this.value = undefined;
this.reason = undefined;
// 回调函数队列
this.onFulfilledCallbacks = [];
this.onRejectedCallbacks = [];
// resolve 函数
const resolve = (value) => {
if (this.state === 'pending') {
this.state = 'fulfilled';
this.value = value;
this.onFulfilledCallbacks.forEach(fn => fn());
}
};
// reject 函数
const reject = (reason) => {
if (this.state === 'pending') {
this.state = 'rejected';
this.reason = reason;
this.onRejectedCallbacks.forEach(fn => fn());
}
};
// 执行 executor
try {
executor(resolve, reject);
} catch (error) {
reject(error);
}
}
then(onFulfilled, onRejected) {
// 参数处理
onFulfilled = typeof onFulfilled === 'function' ? onFulfilled : value => value;
onRejected = typeof onRejected === 'function' ? onRejected : reason => { throw reason; };
// 返回新的 Promise
return new MyPromise((resolve, reject) => {
const handleFulfilled = () => {
try {
const result = onFulfilled(this.value);
if (result instanceof MyPromise) {
result.then(resolve, reject);
} else {
resolve(result);
}
} catch (error) {
reject(error);
}
};
const handleRejected = () => {
try {
const result = onRejected(this.reason);
if (result instanceof MyPromise) {
result.then(resolve, reject);
} else {
resolve(result);
}
} catch (error) {
reject(error);
}
};
if (this.state === 'fulfilled') {
setTimeout(handleFulfilled, 0);
} else if (this.state === 'rejected') {
setTimeout(handleRejected, 0);
} else if (this.state === 'pending') {
this.onFulfilledCallbacks.push(() => setTimeout(handleFulfilled, 0));
this.onRejectedCallbacks.push(() => setTimeout(handleRejected, 0));
}
});
}
catch(onRejected) {
return this.then(null, onRejected);
}
finally(onFinally) {
return this.then(
value => MyPromise.resolve(onFinally()).then(() => value),
reason => MyPromise.resolve(onFinally()).then(() => { throw reason; })
);
}
// 静态方法
static resolve(value) {
if (value instanceof MyPromise) {
return value;
}
return new MyPromise((resolve) => resolve(value));
}
static reject(reason) {
return new MyPromise((resolve, reject) => reject(reason));
}
static all(promises) {
return new MyPromise((resolve, reject) => {
const results = [];
let completedCount = 0;
if (promises.length === 0) {
resolve(results);
return;
}
promises.forEach((promise, index) => {
MyPromise.resolve(promise).then(
value => {
results[index] = value;
completedCount++;
if (completedCount === promises.length) {
resolve(results);
}
},
reject
);
});
});
}
static race(promises) {
return new MyPromise((resolve, reject) => {
promises.forEach(promise => {
MyPromise.resolve(promise).then(resolve, reject);
});
});
}
static allSettled(promises) {
return new MyPromise((resolve) => {
const results = [];
let completedCount = 0;
if (promises.length === 0) {
resolve(results);
return;
}
promises.forEach((promise, index) => {
MyPromise.resolve(promise).then(
value => {
results[index] = { status: 'fulfilled', value };
completedCount++;
if (completedCount === promises.length) {
resolve(results);
}
},
reason => {
results[index] = { status: 'rejected', reason };
completedCount++;
if (completedCount === promises.length) {
resolve(results);
}
}
);
});
});
}
}
// 🧪 使用示例
const myPromise = new MyPromise((resolve, reject) => {
setTimeout(() => resolve('Hello World'), 1000);
});
myPromise
.then(value => {
console.log(value); // Hello World
return 'Next step';
})
.then(value => {
console.log(value); // Next step
})
.catch(error => {
console.error(error);
});防抖/节流
防抖
为什么需要做防抖节流讷?
1. 防止用户的频繁点击
2. javascript 语言的特性吧,javascript 本身是一个事件驱动的语言,每次事件的触发都会触发事件进入事件队列中,导致一定的性能损耗吧,性能浪费吧
防抖函数概念
限制事件触发的频率吧
控制事件触发频率的一种编程技术
应用场景
输入框实时搜索 / 联想(比如百度输入时,停止输入后才发请求);
窗口 resize/scroll 事件(比如调整窗口大小,停止调整后才计算布局);
按钮点击防重复提交(比如支付按钮,避免快速点击多次提交);
鼠标移动 / 拖拽事件(比如拖拽元素,停止拖拽后才更新位置)
/**
* @param {Function} callback 回调函数
* @param {Number} delay 触发频率事件
* @param {Boolean} immediate 是否立即调用触发
* @returns
*/
function debounce(callback, delay, immediate = false) {
let timer = null;
let isExecuted = false;
const _debounce = function(...args) {
return new Promise((resolve, reject) => {
try {
if (timer) {
clearTimeout(timer);
}
let res = undefined;
// 如果是需要立即调用并且没有执行过的话,那就执行一下防抖函数吧
if (immediate && !isExecuted) {
res = callback.apply(this, args);
isExecuted = true;
resolve(res);
return;
}
timer = setTimeout(() => {
// 绑定 this 并且进行调用函数吧
res = callback.apply(this, args);
timer = null;
isExecuted = true;
resolve(res);
}, delay);
} catch (e) {
reject(e);
}
})
}
// 设置一下防抖函数取消操作吧
_debounce.cancel = function() {
if (timer) {
timer = null;
}
}
return _debounce;
}1. 核心的功能是防抖定时器的限制部分吧
2. promise 来获取得到防抖函数调用的结果吧
3. immediate 和 isExecuted 来控制是否立即调用吧
节流
function My_throttle(callback, interval = 3000, immediate = true) {
let start_time = 0
const _throttle = function (...args) {
const now_time = new Date().getTime()
// 开始决定是否需要进行立即执行
if (immediate === false && start_time === 0) {
start_time = now_time
}
const wait_time = interval - (now_time - start_time)
if (wait_time <= 0) {
callback.apply(this, args)
start_time = now_time
}
}
return _throttle
}工具函数
深拷贝
Javascript 中的浅拷贝
1.
Object.create(proto)2.
Object.assign({}, obj)3. 数组的 concat 函数
4. 扩展运算符
...5. 数组的
slice浅拷贝的问题是如果深层的数据还具备引用数据类型的话,此时就会导致复制后的数据引发原本数据的变动吧
深拷贝的实现方式
1.
JSON.parse(JSON.stringfy(obj))不能处理 undefined、function 和 Symbol 等特殊数据类型,因为它们在 JSON 中没有对应的表示方式。
无法处理循环引用,即当一个对象的属性指向自身或形成循环引用关系时,深拷贝会陷入无限递归
2. 自定义实现一下
先回顾一下 javascript 本身具备那些数据类型,然后来探究如何对这些数据类型进行处理
基本数据类型:boolean string null undefined number Symbol
由于基本数据类型的引用模型是在栈中进行处理,此时就是值赋值即可
引用数据类型:Array Set Map Object Function RegExp
对于引用数据类型的话是在堆上进行赋值的讷
简单版本实现
function deepClone(obj) {
// 先判断是不是引用数据类型吧
if (obj instanceof object) {
return obj;
}
let objCopy = Array.isArray(obj) ? [] : {};
// 对象数组都有的操作吧 for...in...
for (key in obj) {
// 此时是对象
if (key instanceof Object) {
objCopy[key] = deepClone(obj[key]);
} else {
if (obj.hasOwnProperty(key)) {
objCopy[key] = obj[key];
}
}
}
return objCopy;
}复杂版本实现
function deepCopy(obj, cache = new WeakMap()) {
if (obj === null || typeof obj !== 'object' || typeof obj === 'function') {
return obj;
}
if (cache.has(obj)) {
return cache.get(obj);
}
// ========== 3. 精准判断引用类型,创建对应实例 ==========
let objCopy;
if (obj instanceof Date) {
objCopy = new Date(obj);
cache.set(obj, objCopy);
return objCopy;
}
if (obj instanceof RegExp) {
objCopy = new RegExp(obj.source, obj.flags);
cache.set(obj, objCopy);
return objCopy;
}
if (obj instanceof Map) {
objCopy = new Map();
cache.set(obj, objCopy);
obj.forEach((value, key) => {
objCopy.set(deepCopy(key, cache), deepCopy(value, cache));
});
return objCopy;
}
if (obj instanceof Set) {
objCopy = new Set();
cache.set(obj, objCopy);
obj.forEach(value => {
objCopy.add(deepCopy(value, cache));
});
return objCopy;
}
objCopy = Array.isArray(obj) ? [] : {};
cache.set(obj, objCopy);
const keys = Reflect.ownKeys(obj);
for (const key of keys) {
// 递归拷贝属性值(排除原型链属性)
objCopy[key] = deepCopy(obj[key], cache);
}
return objCopy;
}深比较函数
我们平时进行比较两个引用数据类型的时候,他们数据结构一样,数据也都一样,但是这个时候我们的操作就是得不到 true
/**
* 进阶版深比较函数(支持循环引用、内置类型、所有引用类型)
* @param {any} a 要比较的第一个值
* @param {any} b 要比较的第二个值
* @param {WeakMap} cache 缓存已比较的对象(处理循环引用)
* @returns {boolean} 是否深度相等
*/
function deepEqual(a, b, cache = new WeakMap()) {
// 步骤1:基础类型 + 引用地址相同
if (a === b) return true;
// 步骤2:特殊值处理(NaN、null/undefined)
if (a === null || b === null) return a === b;
if (a !== a && b !== b) return true; // 都是NaN
// 步骤3:类型不同 → 不相等
if (typeof a !== 'object' || typeof b !== 'object') return false;
// 步骤4:处理循环引用(避免无限递归)
if (cache.has(a) && cache.get(a) === b) return true;
cache.set(a, b);
cache.set(b, a); // 双向缓存,避免反向比较重复处理
// 步骤5:内置引用类型特殊处理
// 5.1 Date:比较时间戳
if (a instanceof Date && b instanceof Date) {
return a.getTime() === b.getTime();
}
// 5.2 RegExp:比较源字符串 + 修饰符
if (a instanceof RegExp && b instanceof RegExp) {
return a.source === b.source && a.flags === b.flags;
}
// 5.3 Map:比较键值对数量 + 每个键值对
if (a instanceof Map && b instanceof Map) {
if (a.size !== b.size) return false;
for (const [key, val] of a) {
if (!b.has(key) || !deepEqual(val, b.get(key), cache)) {
return false;
}
}
return true;
}
// 5.4 Set:比较元素数量 + 每个元素
if (a instanceof Set && b instanceof Set) {
if (a.size !== b.size) return false;
const arrA = Array.from(a);
const arrB = Array.from(b);
return deepEqual(arrA, arrB, cache);
}
// 步骤6:数组/普通对象比较(基础版逻辑)
const isArrA = Array.isArray(a);
const isArrB = Array.isArray(b);
if (isArrA !== isArrB) return false;
const keysA = Reflect.ownKeys(a);
const keysB = Reflect.ownKeys(b);
if (keysA.length !== keysB.length) return false;
for (const key of keysA) {
if (!keysB.includes(key) || !deepEqual(a[key], b[key], cache)) {
return false;
}
}
return true;
}判断是否含有循环引用
/**
* 判断一个值是否存在循环引用
* @param {any} obj 要检测的值(对象/数组/基础类型等)
* @param {WeakMap} cache 缓存已遍历对象的容器(内部自动创建,外部无需传)
* @returns {boolean} 是否存在循环引用
*/
function isCircular(obj, cache = new WeakMap()) {
// 规则1:非引用类型(基础类型/函数/null)→ 无循环引用
if (
obj === null ||
typeof obj !== 'object' ||
typeof obj === 'function' ||
obj instanceof Date || // 内置对象(Date/RegExp)无循环引用,直接跳过
obj instanceof RegExp
) {
return false;
}
// 规则2:当前对象已在缓存中 → 存在循环引用
if (cache.has(obj)) {
return true;
}
// 规则3:未命中缓存 → 加入缓存,继续递归遍历属性
cache.set(obj, true); // 标记为已遍历
// 遍历对象的所有自有属性(包括Symbol键、不可枚举属性)
const keys = Reflect.ownKeys(obj);
for (const key of keys) {
const value = obj[key];
// 递归检查属性值:只要有一个属性值存在循环引用,整体就存在
if (isCircular(value, cache)) {
return true;
}
}
// 规则4:遍历完所有属性,未发现循环引用 → 清除当前对象的缓存(避免影响后续检测)
cache.delete(obj);
// 最终判定:无循环引用
return false;
}URL 处理工具
/**
* 解析URL中的查询参数为对象(支持多值、自动解码)
* @param {string} [url=window.location.href] - 要解析的URL,默认当前页面URL
* @returns {Object} 解析后的参数对象(多值参数为数组)
* 示例:parseUrlParams('https://test.com?name=张三&age=20&hobby=code&hobby=game')
* → { name: '张三', age: '20', hobby: ['code', 'game'] }
*/
function parseUrlParams(url = window.location.href) {
// 校验参数:非字符串转空
if (typeof url !== 'string') return {};
// 提取?后的查询字符串(排除哈希#部分)
const searchStr = url.split('?')[1]?.split('#')[0] || '';
if (!searchStr) return {};
const params = {};
// 分割参数并遍历
searchStr.split('&').forEach(item => {
if (!item) return; // 过滤空值(如&a=1&&b=2中的空)
const [key, value = ''] = item.split('=');
// 解码(处理中文/特殊字符)
const decodeKey = decodeURIComponent(key.trim());
const decodeValue = decodeURIComponent(value.trim());
// 处理多值参数(如hobby=code&hobby=game)
if (params[decodeKey]) {
params[decodeKey] = Array.isArray(params[decodeKey])
? [...params[decodeKey], decodeValue]
: [params[decodeKey], decodeValue];
} else {
params[decodeKey] = decodeValue;
}
});
return params;
}
/**
* 将对象拼接为URL查询参数字符串(支持多值、自动编码)
* @param {Object} params - 要拼接的参数对象(多值传数组)
* @returns {string} 拼接后的查询字符串(开头无?)
* 示例:stringifyUrlParams({ name: '张三', age: 20, hobby: ['code', 'game'] })
* → "name=%E5%BC%A0%E4%B8%89&age=20&hobby=code&hobby=game"
*/
function stringifyUrlParams(params = {}) {
if (typeof params !== 'object' || params === null) return '';
const parts = [];
Object.entries(params).forEach(([key, value]) => {
if (value === undefined || value === null) return; // 过滤null/undefined
const encodeKey = encodeURIComponent(key);
// 处理多值参数
if (Array.isArray(value)) {
value.forEach(v => {
parts.push(`${encodeKey}=${encodeURIComponent(v)}`);
});
} else {
parts.push(`${encodeKey}=${encodeURIComponent(value)}`);
}
});
return parts.join('&');
}
/**
* 修改URL中的指定查询参数(不修改原URL,返回新URL)
* @param {string} [url=window.location.href] - 原URL
* @param {Object} updateParams - 要修改/新增的参数({key: value})
* @returns {string} 修改后的新URL
* 示例:updateUrlParams('https://test.com?age=20', { age: 21, name: '张三' })
* → "https://test.com?age=21&name=%E5%BC%A0%E4%B8%89"
*/
function updateUrlParams(url = window.location.href, updateParams = {}) {
// 解析原参数
const originParams = parseUrlParams(url);
// 合并新参数(覆盖原参数)
const newParams = { ...originParams, ...updateParams };
// 拼接新参数
const newSearch = stringifyUrlParams(newParams);
// 拆分原URL的协议+域名+路径、哈希部分
const [originBase, originHash = ''] = url.split('#');
const [base] = originBase.split('?');
// 拼接新URL
return newSearch
? `${base}?${newSearch}${originHash ? '#' + originHash : ''}`
: `${base}${originHash ? '#' + originHash : ''}`;
}
/**
* 获取URL中的哈希值(去除#,自动解码)
* @param {string} [url=window.location.href] - 要解析的URL
* @returns {string} 哈希值(无则返回空)
* 示例:getUrlHash('https://test.com#/user/123') → "/user/123"
*/
function getUrlHash(url = window.location.href) {
if (typeof url !== 'string') return '';
const hashStr = url.split('#')[1] || '';
return decodeURIComponent(hashStr);
}
/**
* 移除URL中的哈希值(返回新URL)
* @param {string} [url=window.location.href] - 原URL
* @returns {string} 移除哈希后的新URL
* 示例:removeUrlHash('https://test.com#/user/123') → "https://test.com"
*/
function removeUrlHash(url = window.location.href) {
return typeof url === 'string' ? url.split('#')[0] : url;
}
/**
* 进阶模板渲染(支持{{变量}} + {{each 数组}}循环)
* @param {string} template - 模板字符串
* @param {Object} data - 渲染数据
* @returns {string} 渲染后的字符串
* 示例:
* const tpl = `
* <ul>
* {{each list}}
* <li>{{item.name}} - {{item.age}}</li>
* {{/each}}
* </ul>
* `;
* renderAdvancedTemplate(tpl, { list: [{name: '张三', age:20}, {name: '李四', age:21}] })
* → 渲染出包含两个li的ul
*/
function renderAdvancedTemplate(template, data = {}) {
let tpl = String(template || '');
// 第一步:处理{{each 数组}}循环
tpl = tpl.replace(/{{each\s+([^}]+)}}([\s\S]*?){{\/each}}/g, (match, listKey, itemTpl) => {
const list = data[trimSpace(listKey)] || [];
if (!Array.isArray(list)) return '';
// 遍历数组,渲染每个item
return list.map(item => {
// 替换item变量(如{{item.name}})
return itemTpl.replace(/{{item\.([^}]+)}}/g, (m, k) => {
const keys = trimSpace(k).split('.');
let value = item;
for (const key of keys) {
value = value?.[key];
if (value === undefined || value === null) break;
}
return value === undefined || value === null ? '' : String(value);
});
}).join('');
});
// 第二步:处理普通{{变量}}
return renderTemplate(tpl, data);
}arrToTree | treeToArr
const flatList = [
{ id: 1, parentId: 0, name: '一级菜单A', sort: 2 },
{ id: 2, parentId: 0, name: '一级菜单B', sort: 1 },
{ id: 3, parentId: 1, name: '二级菜单A-1', sort: 1 },
{ id: 4, parentId: 1, name: '二级菜单A-2', sort: 2 },
{ id: 5, parentId: 2, name: '三级菜单A-1-1', sort: 1 },
{ id: 6, parentId: 2, name: '二级菜单B-1', sort: 1 }
];
const arrToTree = (list, id = 0) => {
// 进行边界 case
if (!Array.isArray(list)) {
return;
}
// 1. 构建出 list 得map映射出来吧
const map = {};
list.forEach((item) => {
map[item.id] = {
...item,
children: []
}
})
const result = [];
Object.keys(map).forEach(([key, value]) => {
const currentNode = map[key];
if (currentNode.parentId === 0) {
result.push(currentNode);
return;
};
if (currentNode.parentId in map) {
const parentNode = map[currentNode.parentId];
parentNode.children.push(currentNode);
}
})
// console.log(result)
// 开始进行排序实现吧
const sortedChildren = (nodes) => {
nodes.forEach(node => {
node.children.sort((a, b) => a.sort - b.sort);
sortedChildren(node.children);
})
}
sortedChildren(result)
return result;
}
console.log(arrToTree(flatList));
1. 需要深度理解的是:javascript 的操作的时候是基于内存的操作,节点的 push 和其他的操作都是基本原本的内存实现,这样才能快速的实现对应的元素的快速定位和动态的加入元素吧
2. 提前预构建出对应的map 表,便于后续的操作吧
const treeToArr = (tree) => {
const result = [];
const stack = [...tree]; // 获取得到对应的树吧
while (stack.length > 0) {
const node = stack.pop(); // 获取得到一个栈中的元素吧
// 由于转换的时候是需要对应的 children
// 以及需要记录一下node.children 后续还需要操作讷
let children;
if (node.children || node.children.length) {
children = node.children;
delete node.children;
}
result.push(node);
if (Array.isArray(children) && children.length > 0) {
stack.push(...children.reverse());
}
}
return result.sort((a, b) => {
return a.id - b.id
});
}核心就是使用栈的能力实现对应的操作实现吧和 DFS 的深度遍历来实现吧,还是十分简单的整体的思路的话
但是这里需要核心处理就是新增的 children 吧

结果完全是 ok 的,哈哈哈,那这里就写完了,但是我记得是可以利用 reduce 来更加方便的进行书写的,思路忘记了,但是可以后续帮大家加一份进来的讷
树转数组的操作的话核心需要大家掌握的是 DFS 和 BFS 两种算法以及对应的递归的处理实现吧
DFS 深度优先遍历
递归实现深度优先遍历
先父节点 → 递归遍历第一个子节点到最深 → 回溯遍历兄弟节点 → 再回溯遍历上级兄弟节点
const deptTree = [
{ id: 10, parentId: 0, deptName: '研发中心', sort: 2 },
{
id: 9,
parentId: 0,
deptName: '运营中心',
sort: 1,
children: [
{ id: 11, parentId: 9, deptName: '内容运营', sort: 1 },
{ id: 12, parentId: 9, deptName: '用户运营', sort: 2, children: [{ id: 13, parentId: 12, deptName: '社群运营', sort: 1 }] }
]
}
];
// 递归版DFS树转数组
function dfsTreeToArr(tree) {
const result = [];
// 定义递归函数:处理单个节点
function dfs(node) {
// 浅拷贝节点,删除children,避免污染原数据
const { children, ...newNode } = node;
result.push(newNode);
// 递归遍历子节点
if (Array.isArray(children) && children.length > 0) {
// 递归默认按子节点原顺序遍历,无需reverse
children.forEach(child => dfs(child));
}
}
// 遍历根节点,触发递归
tree.forEach(root => dfs(root));
// 按id升序排序
return result.sort((a, b) => a.id - b.id);
}
console.log(dfsTreeToArr(deptTree));栈实现深度优先遍历
function stackDfsTreeToArr(tree) {
if (!Array.isArray(tree)) return [];
const result = [];
const stack = [...tree]; // 初始栈:所有根节点
while (stack.length > 0) {
const node = stack.pop(); // 弹出栈顶(最后一个)节点
const { children, ...newNode } = node;
result.push(newNode);
// 子节点倒序入栈(保证遍历顺序和递归一致)
if (Array.isArray(children) && children.length > 0) {
stack.push(...children.reverse());
}
}
return result.sort((a, b) => a.id - b.id);
}
console.log(stackDfsTreeToArr(deptTree));BFS 广度优先遍历
先遍历所有根节点 → 再遍历所有根节点的子节点 → 再遍历子节点的子节点,层层推进
1. 使用队列来实现吧,唯一的实现方式吧
function bfsTreeToArr(tree) {
if (!Array.isArray(tree)) return [];
const result = [];
const queue = [...tree]; // 初始队列:所有根节点(先进先出)
while (queue.length > 0) {
const node = queue.shift(); // 弹出队列头部(第一个)节点
const { children, ...newNode } = node;
result.push(newNode);
// 子节点直接入队(无需reverse,保证层级顺序)
if (Array.isArray(children) && children.length > 0) {
queue.push(...children);
}
}
// 按id升序排序(可选,按业务需求)
return result.sort((a, b) => a.id - b.id);
}
console.log(bfsTreeToArr(deptTree));
数据结构实现
通过上面的很多的手写题的书写,以及算法的总结,我们也大致体会了很多的算法的利用吧
1. 数组
2. 哈希表
3. 树结构
4. 对象结构体字典结构
其他的数据结构可能会在开发中利用到的
1. 链表数据结构
2. 图数据结构
对于学习者的建议
核心掌握对于 数组 | 对象 | 字符串的相关操作,就可以解决平时业务逻辑中的大多数问题了
以及深度学习一下排序算法,掌握几个自己比较喜欢顺得清思路的即可
LRU 缓存
/**
* LRU (Least Recently Used) 缓存实现
*/
class LRUCache {
constructor(capacity) {
this.capacity = capacity;
this.cache = new Map();
}
get(key) {
if (this.cache.has(key)) {
// 移动到最新位置
const value = this.cache.get(key);
this.cache.delete(key);
this.cache.set(key, value);
return value;
}
return -1;
}
put(key, value) {
if (this.cache.has(key)) {
// 更新现有键值
this.cache.delete(key);
} else if (this.cache.size >= this.capacity) {
// 删除最久未使用的项(Map 的第一个项)
const firstKey = this.cache.keys().next().value;
this.cache.delete(firstKey);
}
this.cache.set(key, value);
}
// 获取所有键值对
getAll() {
return Array.from(this.cache.entries());
}
// 获取缓存大小
size() {
return this.cache.size;
}
// 清空缓存
clear() {
this.cache.clear();
}
}
// 🧪 使用示例
const lru = new LRUCache(3);
lru.put(1, 'one');
lru.put(2, 'two');
lru.put(3, 'three');
console.log(lru.get(2)); // 'two'
lru.put(4, 'four'); // 会移除键 1
console.log(lru.get(1)); // -1 (未找到)
console.log(lru.getAll()); // [[3, 'three'], [2, 'two'], [4, 'four']]为什么这里选择 map 来进行处理和实现讷??
键的类型无限制:Map 的键可以是任意类型(对象、数组、函数、null、undefined、NaN 等),不会像普通对象那样自动将非字符串 / Symbol 键转为字符串(比如用 DOM 元素、自定义对象做键都可行);
严格保持插入顺序:无论增删多少次键值对,Map 遍历(forEach/for...of)时都会按「键的插入顺序」返回,这是实现 LRU、有序配置表的核心依赖;
原生 size 属性:可直接通过
map.size获取键值对数量,无需像普通对象那样用Object.keys(obj).length遍历计算,性能是 O (1);原生可迭代:Map 本身实现了迭代器(Iterable),可以直接用
for...of遍历,也能解构为数组([...map]),无需额外转换;无原型污染风险:Map 的所有操作仅针对自身存储的键值对,不会访问原型链(比如
map.get('toString')不会拿到原型上的方法,而obj.toString会);键的唯一性更合理:基于「SameValueZero」算法判断键是否重复(和
===逻辑一致,但 NaN 被视为相等),比如map.set(NaN, 1)后,map.get(NaN)能正确匹配,而普通对象会把 NaN 转为字符串 "NaN",导致和字符串 "NaN" 冲突;高频增删更高效:针对 “频繁添加 / 删除键值对” 的场景(比如缓存、临时存储),Map 的底层实现做了优化,性能远优于普通对象
map 和 object 对比
1. 键的处理逻辑不同
普通对象
{}:键只能是「字符串」或「Symbol」类型,非该类型的键会被强制字符串化(比如数字 1 → "1",对象 → "[object Object]"),导致「数字 1」和「字符串 "1"」被视为同一个键;Map:键可以是任意类型,且不会做类型转换,键的唯一性基于 “引用 / 值本身”(比如两个不同的空对象
{}是两个独立的键)。
2. 顺序性完全不同
普通对象
{}:ES6 之前完全无序;ES6 后仅 “部分有序”(数字键升序 → 字符串键插入顺序 → Symbol 键插入顺序),并非严格的插入顺序(比如先插 "b"、再插 "a"、再插 1,遍历顺序是 1、"b"、"a"),完全不可靠;Map:遍历顺序和插入顺序完全一致,增删键后顺序仍可追溯,这是 LRU、有序队列等场景的核心需求。
3. 大小获取与遍历方式不同
普通对象
{}:无原生 size 属性,需通过Object.keys(obj).length遍历所有键计算,时间复杂度 O (n);且不能直接迭代,需先转成Object.entries(obj)数组才能遍历;Map:
map.size直接返回键值对数量(O (1)),本身是可迭代对象,支持for...of、forEach直接遍历,遍历逻辑更简洁。
4. 原型链与污染风险不同
普通对象
{}:继承自Object.prototype,键名可能和原型属性冲突(比如obj.hasOwnProperty会拿到原型方法,而非自己的键;甚至obj.__proto__会修改原型);Map:无原型链依赖,所有操作仅作用于自身存储的键值对,完全避免原型污染问题。
5. 性能适配场景不同
普通对象
{}:适合「键值对数量固定、增删少、以属性访问为主」的场景(比如固定配置),简单属性访问(obj.key)略快;Map:适合「高频增删、键类型多样、需要保持插入顺序」的场景(LRU 缓存、临时状态存储、高频更新的字典),整体性能更稳定。
字符串处理
核心分类为:
url-utils.js、regex-utils.js、template-utils.js
排序算法
快速排序
/**
* 快速排序实现
* @param {number[]} arr - 待排序数组
* @returns {number[]} 排序后的数组
*/
function quickSort(arr) {
// 递归终止条件
if (!Array.isArray(arr) || arr.length <= 1) {
return arr;
}
// 选择基准点(中间位置)
const pivotIndex = Math.floor(arr.length / 2);
const pivot = arr.splice(pivotIndex, 1)[0];
// 分割数组
const left = [];
const right = [];
// 遍历数组,小于等于基准值的放左边,大于的放右边
for (const item of arr) {
if (item <= pivot) {
left.push(item);
} else {
right.push(item);
}
}
// 递归处理左右子数组,然后合并结果
return [...quickSort(left), pivot, ...quickSort(right)];
}
// 🧪 测试示例
const testArray = [64, 34, 25, 12, 22, 11, 90];
console.log(`原始数组: ${testArray}`);
console.log(`快排结果: ${quickSort([...testArray])}`);二分搜索
/**
* 二分查找 - 递归实现
* @param {number[]} arr - 有序数组
* @param {number} target - 目标值
* @param {number} left - 左边界
* @param {number} right - 右边界
* @returns {number} 目标值的索引,不存在返回 -1
*/
function binarySearchRecursive(arr, target, left = 0, right = arr.length - 1) {
// 递归终止条件
if (left > right) {
return -1;
}
const mid = Math.floor((left + right) / 2);
if (arr[mid] === target) {
return mid; // 找到目标值
} else if (arr[mid] > target) {
return binarySearchRecursive(arr, target, left, mid - 1); // 在左半部分搜索
} else {
return binarySearchRecursive(arr, target, mid + 1, right); // 在右半部分搜索
}
}
/**
* 二分查找 - 迭代实现
* @param {number[]} arr - 有序数组
* @param {number} target - 目标值
* @returns {number} 目标值的索引,不存在返回 -1
*/
function binarySearchIterative(arr, target) {
let left = 0;
let right = arr.length - 1;
while (left <= right) {
const mid = Math.floor((left + right) / 2);
if (arr[mid] === target) {
return mid;
} else if (arr[mid] > target) {
right = mid - 1;
} else {
left = mid + 1;
}
}
return -1;
}
// 🧪 测试示例
const sortedArray = [1, 3, 5, 7, 9, 11, 13, 15, 17, 19];
console.log('数组:', sortedArray);
console.log('查找 7:', binarySearchIterative(sortedArray, 7)); // 输出: 3
console.log('查找 16:', binarySearchIterative(sortedArray, 16)); // 输出: -1双指针思想
对撞指针
/**
* 验证回文字符串
* @param {string} s - 输入字符串
* @returns {boolean} 是否为回文
*/
function isPalindrome(s) {
// 预处理:转换为小写并移除非字母数字字符
const cleaned = s.toLowerCase().replace(/[^a-z0-9]/g, '');
let left = 0;
let right = cleaned.length - 1;
while (left < right) {
if (cleaned[left] !== cleaned[right]) {
return false;
}
left++;
right--;
}
return true;
}
// 🧪 测试
console.log(isPalindrome("A man, a plan, a canal: Panama")); // true
console.log(isPalindrome("race a car")); // false快慢指针
/**
* 检测链表是否有环(快慢指针)
* @param {ListNode} head - 链表头节点
* @returns {boolean} 是否有环
*/
function hasCycle(head) {
if (!head || !head.next) return false;
let slow = head; // 慢指针,每次移动一步
let fast = head; // 快指针,每次移动两步
while (fast && fast.next) {
slow = slow.next;
fast = fast.next.next;
// 如果有环,快慢指针最终会相遇
if (slow === fast) {
return true;
}
}
return false;
}滑动窗口
/**
* 无重复字符的最长子串
* @param {string} s - 输入字符串
* @returns {number} 最长子串长度
*/
function lengthOfLongestSubstring(s) {
const charSet = new Set();
let left = 0;
let maxLength = 0;
for (let right = 0; right < s.length; right++) {
// 如果字符重复,收缩左边界
while (charSet.has(s[right])) {
charSet.delete(s[left]);
left++;
}
charSet.add(s[right]);
maxLength = Math.max(maxLength, right - left + 1);
}
return maxLength;
}
// 🧪 测试
console.log(lengthOfLongestSubstring("abcabcbb")); // 3 ("abc")
console.log(lengthOfLongestSubstring("bbbbb")); // 1 ("b")
console.log(lengthOfLongestSubstring("pwwkew")); // 3 ("wke")