面试-手撕
参考链接
高频
1. 防抖
1.1 普通防抖
在规定时间内,如果事件被重复触发,只执行最后一次。比如搜索框输入。
function debounce(fn, delay) {
// 1. 创建一个闭包变量timer,用于存储定时器ID
let timer = null
// 2. 返回一个新的函数(闭包)
return function (...args) {
// 3. 如果已经有定时器,清除它(取消之前的调用)
if (timer) clearTimeout(timer)
// 4. 设置新的定时器
timer = setTimeout(() => {
// 5. 在延迟时间后执行原函数
// 使用apply确保正确的this上下文,并传递所有参数
fn.apply(this, args)
}, delay)
}
}防抖(Debounce)的核心思想是:在事件被触发n秒后再执行回调,如果在这n秒内又被触发,则重新计时。 可以把它想象成进电梯,只要一直有人进(触发事件),电梯门就不会关(不执行真正的函数),直到一定时间内没人进了才关门。
1. 整体实现逻辑
debounce函数接收两个参数:需要防抖的真实函数fn和延迟时间delay。它返回一个新的函数。我们绑定到DOM事件(如点击、滚动)上的,实际上是这个返回的新函数。
每次触发事件时,都会检查
timer是否存在。如果存在,说明上一次触发还在等待执行中,于是通过clearTimeout(timer)取消掉上一次的计时。然后开启一个新的定时器重新倒计时。只有在
delay期间没有新的触发,定时器里的fn.apply(this, args)才会被真正执行。
2. 闭包(Closure)的概念与应用
概念:闭包是指那些能够访问自由变量的函数。其实就是函数里面嵌套函数,内部函数可以访问外部函数的变量。
在这段代码中的体现:
function debounce(fn,delay){
let timer = null; // 外部函数的局部变量
return function(...args){
// 内部函数使用了外部的 timer
if(timer) clearTimeout(timer);
timer = setTimeout(...);
}
}内部返回的匿名函数访问并修改了外部 debounce 函数内的 timer 变量。当 debounce 执行完毕后,timer 并没有被垃圾回收机制销毁,因为它一直被返回的内部函数引用着。
3. 使用闭包的好处
- 维持状态(状态持久化):
防抖函数需要知道上一次触发的定时器是谁,以便在下一次触发时取消它。闭包让 timer 变量常驻内存,每次调用返回的函数时,都能操作同一个 timer。
- 避免全局变量污染(封装私有变量):
如果没有闭包,我们必须把 timer 定义在全局作用域中,才能让多次触发都访问到同一个定时器。但这会导致全局变量泛滥,并且如果页面上有多个按钮使用了防抖,它们的定时器状态会互相覆盖、互相干扰。通过闭包,每次调用 debounce() 生成的 timer 都是独立且私有的。
4. this 指向问题
这段代码在处理 this 时非常巧妙,主要解决的是确保真正的函数 fn 执行时,上下文能和绑定事件时的上下文保持一致。
拆解分析:
事件触发时的
this:JavaScriptreturn function(...args){ ... }这个返回的内部函数才是真正绑定给 DOM 元素的事件处理函数。当 DOM 触发事件(比如
button.onclick)时,这个内部函数的this会自动指向触发事件的 DOM 元素(即button)。setTimeout的this陷阱机制与箭头函数:JavaScripttimer = setTimeout(() => { ... }, delay);如果是普通的
function() {}传入setTimeout,在非严格模式下,它内部的this默认会指向window。这里使用了箭头函数
() => {}。箭头函数没有自己的this,它会捕获其所在上下文(也就是外层的返回函数function(...args))的this。这就成功地把 DOM 元素的this传递到了定时器内部。使用
apply绑定this和参数:JavaScriptfn.apply(this, args)如果不加
apply直接调用fn(...args),即使我们在箭头函数里,fn内部的this依然可能是全局对象(因为是直接调用)。通过
apply(this, args),我们显式地让开发者传入的原函数fn,使用前面捕获到的、正确的 DOM 元素this,同时把收集到的事件参数...args(如MouseEvent对象)完整地传递给原函数。1.2 加可立即执行及取消
function debounce(fn, delay, immediate = false) {
let timer = null
return function (...args) {
// 关键点1:每次调用时,先检查并清除之前的定时器
// 这是防抖的核心:取消之前的调用,重新计时
if (timer) clearTimeout(timer)
if (immediate) {
// 关键点2:判断是否可以立即执行
// callNow为true的条件:timer为null(即没有正在进行的定时器)
// 这意味着:
// - 第一次调用时,timer为null → callNow = true
// - 冷却期结束后,timer为null → callNow = true
// - 在冷却期内,timer不为null → callNow = false
const callNow = !timer
// 关键点3:设置定时器,用于重置timer(冷却期)
// 注意:这个定时器的作用不是执行函数,而是控制冷却期
// 在delay毫秒后,将timer设为null,表示冷却期结束
timer = setTimeout(() => {
timer = null
}, delay)
// 关键点4:在冷却期开始时就执行函数
// 只有满足callNow条件(即冷却期已结束)才执行
if (callNow) fn.apply(this, args)
} else {
timer = setTimeout(() => {
fn.apply(this, args)
}, delay)
}
}
}加取消和立即执行的完整实现:
/**
* 防抖函数(支持取消)
* @param {Function} fn 要执行的函数
* @param {number} delay 延迟时间
* @param {boolean} immediate 是否立即执行
*/
function debounce(fn, delay, immediate = false) {
let timer = null
// 创建要返回的函数
function debounced(...args) {
if (timer) clearTimeout(timer)
if (immediate) {
const callNow = !timer
timer = setTimeout(() => {
timer = null
}, delay)
if (callNow) fn.apply(this, args)
} else {
timer = setTimeout(() => {
fn.apply(this, args)
timer = null // 执行后清除timer
}, delay)
}
}
// 添加 cancel 方法,用于手动取消
debounced.cancel = function() {
if (timer) {
clearTimeout(timer)
timer = null
}
}
return debounced
}在“立即执行”模式下:
timer变量 == 冷却倒计时器。如果
timer是null,说明技能处于就绪状态(没有在冷却中)。如果
timer有值(非null),说明技能正在冷却中。
1. 立即执行的条件是什么?如果有 timer 代表什么?
// 看是否满足立即执行的条件
const callNow = !timer;立即执行的唯一条件:就是当前绝对没有定时器(
timer为null或undefined)。!null会变成true,所以callNow为true,允许放技能。如果有
timer代表什么?:代表你前不久刚刚按过了按钮,现在正处于"冷却期"内。这时候!timer就是false,所以本次触发被拦截,不能再次触发函数。
2. 为什么需要下面这个看似无用的定时器?
timer = setTimeout(() => {
timer = null
}, delay)这个定时器的唯一作用,就是“控制冷却什么时候结束”!它本身不负责执行原函数。
我们来看看你疯狂点击按钮时的完整运行流程:
第一下点击(第 0 秒):
此时
timer是null。遇到
if (timer),因为是null,跳过。callNow = !timer算出来是true(这就是初次执行的原因)。开启冷却倒计时:
timer = setTimeout(..., 2000)。(原本是 null 的 timer,现在变成了一个数字比如 1)if(callNow)成立,立即执行fn.apply(...)(技能放出来了!)。
第二下疯狂点击(第 0.5 秒时):
此时
timer是1。遇到
if (timer) clearTimeout(timer),把上一次的倒计时强行砸碎取消掉(但是timer变量里仍然存着1这个数字)!callNow = !timer算出来是false(因为 timer 是 1,说明技能还在冷却)。重新开启新的冷却倒计时:
timer = setTimeout(..., 2000)。(原本是 1 的 timer,变成了新定时器的 ID 比如 2。这意味着:只要你手欠又点了一次,2秒的冷却时间就要重新开始算!)if(callNow)不成立,不执行函数(技能放不出来)。
...只要一直以低于 2 秒的频率疯狂点击,步骤 2 到步骤 5 就会一直循环。虽然函数放不出来,但惩罚就是冷却时间不断被刷新。
终于,你停手了(等了完整的 2.001 秒):
就在这 2 秒内没有人去捣乱(没人触发 clearTimeout),那个最后创建的定时器终于走完了!
此时,它执行了内部唯一的代码:
timer = null;意味着:冷却彻底结束,系统回到初始状态。下一次点击,又可以立即执行了。
2. 节流
时间戳实现及定时器实现
/**
* 节流函数 - 时间戳实现
* 在延迟时间内只执行第一次调用
* @param {Function} fn 要执行的函数
* @param {number} delay 延迟时间(毫秒)
* @returns {Function} 节流后的函数
*/
function throttle(fn, delay) {
// 记录上一次执行的时间戳
let lastTime = 0
// 返回节流后的函数
return function (...args) {
// 获取当前时间戳
const now = Date.now()
// 判断是否达到了执行间隔
if (now - lastTime >= delay) {
// 更新上一次执行时间
lastTime = now
// 执行原函数,保持 this 上下文和参数
fn.apply(this, args)
}
// 如果没有达到间隔时间,则什么都不做
}
}
/**
* 节流函数 - 定时器实现
* 在延迟时间内只执行最后一次调用
* @param {Function} fn 要执行的函数
* @param {number} delay 延迟时间(毫秒)
* @returns {Function} 节流后的函数
*/
function throttle(fn, delay) {
// 定时器标识
let timer = null
// 返回节流后的函数
return function (...args) {
// 如果没有正在等待执行的定时器
if (!timer) {
// 设置定时器,延迟执行原函数
timer = setTimeout(() => {
// 执行原函数,保持 this 上下文和参数
fn.apply(this, args)
// 执行完毕后清除定时器,允许下一次调用
timer = null
}, delay)
}
// 如果有定时器在等待,则忽略本次调用
}
}https://juejin.cn/post/7492402669392773147 讲解防抖节流的文章
3.Promise四种静态方法
四个静态方法对比
all:全部成功 → 成功;有一个失败 → 失败
race:谁先完成 → 就跟谁(不管成功/失败)
any:谁先成功 → 跟谁;全部失败 → 才失败
allSettled:全部完成 → 返回结果(永不失败)
3.1实现Promise.all
Promise.all 的实现思路:
返回一个新的 Promise
遍历传入数组,用 Promise.resolve 统一处理每一项
每个 Promise 成功后,将结果按索引存入结果数组
使用计数器记录完成数量
当所有 Promise 都成功时,resolve 返回结果数组
如果有任意一个 Promise 失败,直接 reject
/**
* 实现 Promise.all 方法
* 接收一个 Promise 数组,返回一个新的 Promise
* 所有 Promise 成功时,返回结果数组
* 任何一个 Promise 失败时,立即 reject
* @param {Array} promises Promise 数组
* @returns {Promise} 新的 Promise 对象
*/
Promise.myAll = function (promises) {
// 返回一个新的 Promise 实例
return new Promise((resolve, reject) => {
// 存储每个 Promise 的结果,保持传入顺序
const result = []
// 计数器,记录已完成的 Promise 数量
let count = 0
// 遍历传入的 Promise 数组
promises.forEach((p, index) => {
// 使用 Promise.resolve 确保传入的值可以被正确处理
// 如果是普通值,会直接 resolve;如果是 Promise,则等待其完成
Promise.resolve(p).then(res => {
// 将结果存入对应位置,保证输出顺序与输入顺序一致
result[index] = res
// 完成计数加1
count++
// 当所有 Promise 都完成时
if (count === promises.length) {
// 返回结果数组
resolve(result)
}
}).catch( err=>{
reject(err)
}
) // 任何一个 Promise 失败,立即 reject
})
})
}3.2 实现Promise.race
/**
* 实现 Promise.race 方法
* 接收一个 Promise 数组,返回一个新的 Promise
* 哪个 Promise 最先改变状态(成功或失败),就采用它的结果
* @param {Array} promises Promise 数组
* @returns {Promise} 新的 Promise 对象
*/
Promise.myRace = function (promises) {
// 返回一个新的 Promise 实例
return new Promise((resolve, reject) => {
// 遍历传入的 Promise 数组
promises.forEach(p => {
// 使用 Promise.resolve 确保传入的值可以被正确处理
// 如果是普通值,会直接 resolve;如果是 Promise,则等待其完成
Promise.resolve(p)
.then(res => {
resolve(res)
) // 任何一个 Promise 成功,就用它的结果 resolve
.catch(err=>{
reject(err)
}) // 任何一个 Promise 失败,就用它的原因 reject
})
// 注意:这里没有使用计数器,因为 race 只关心第一个完成的结果
// 后续完成的 Promise 会被忽略(因为 Promise 状态一旦改变就不能再变)
})
}3.3 实现Promise.Any
Promise.myAny = function(promises) {
return new Promise((resolve, reject) => {
const errors = []; // 存储所有错误信息
let count = 0; // 记录失败的 Promise 数量
promises.forEach((p, index) => {
// 将每个值转为 Promise
Promise.resolve(p)
.then(resolve) // 任何一个成功,直接 resolve
.catch(err => { // 失败时收集错误
errors[index] = err; // 记录错误,保持顺序
count++; // 失败计数加1
// 如果所有 Promise 都失败了,才 reject
if (count === promises.length) {
reject(new AggregateError(errors, 'All promises were rejected'));
}
});
});
});
};3.4 Promise.myAllSettled
Promise.myAllSettled = function(promises) {
return new Promise(resolve => {
const results = []; // 存储所有结果
let count = 0; // 记录已完成的 Promise 数量
promises.forEach((p, index) => {
Promise.resolve(p)
.then(value => {
results[index] = { status: 'fulfilled', value };
count++;
if (count === promises.length) resolve(results);
})
.catch(reason => {
results[index] = { status: 'rejected', reason };
count++;
if (count === promises.length) resolve(results);
});
});
});
};4.深拷贝:实现支持对象、数组、循环引用的深拷贝函数。
/**
* 深拷贝函数
* 递归地拷贝一个对象的所有属性,处理循环引用问题
* @param {*} obj 要拷贝的对象
* @param {WeakMap} map 用于存储已拷贝对象的映射表,防止循环引用
* @returns {*} 拷贝后的新对象
*/
function deepClone(obj, map = new WeakMap()) {
// 1. 基础类型处理:如果是 null 或不是对象,直接返回
// 这包括:string、number、boolean、null、undefined、symbol、function 等
if (obj === null || typeof obj !== 'object') return obj
if(obj instanceof RegExp) return new RegExp(obj)
if(obj instanceof Date) return new Date(obj)
// 2. 循环引用检查:如果已经拷贝过这个对象,直接返回之前拷贝的结果
// 防止无限递归,例如:obj.self = obj
if (map.has(obj)) return map.get(obj)
// 3. 创建新对象:根据原对象是数组还是普通对象来创建
const clone = Array.isArray(obj) ? [] : {}
// 4. 将原对象和克隆对象的对应关系存入 map
// 必须在递归前存储,这样后续遇到循环引用时才能返回
map.set(obj, clone)
// 5. 遍历原对象的所有可枚举属性
// 注意:这里使用 for...in 会遍历原型链上的可枚举属性
for (let key in obj) {
// 只拷贝对象自身的属性,不拷贝原型链上的属性
if (obj.hasOwnProperty(key)) {
// 递归拷贝每个属性的值
clone[key] = deepClone(obj[key], map)
}
}
// 6. 返回克隆后的新对象
return clone
}JSON实现深拷贝的局限性
循环引用问题
WeakMap解释
WeakMap 是 JavaScript 中的一种特殊的数据结构,它的键必须是对象,并且是弱引用的。
弱引用的特点:
不会阻止垃圾回收
如果一个对象只有 WeakMap 在引用它,这个对象可以被垃圾回收
当对象被回收后,它在 WeakMap 中的键值对会自动被移除
注意:此处WeakMap可以直接先用Map,如果问到优化再说WeakMap,别给自己挖坑。
用 Map 会导致被拷贝的对象永远无法被垃圾回收 ,造成内存泄漏
5. 实现 sleep 函数
因为setTimeout是通过回调函数来实现定时任务的,所以在多任务的场景下就会出现回调嵌套:
console.time('runTime:');
setTimeout(() => {
console.log('1');
setTimeout(() => {
console.log('2')
setTimeout(() => {
console.log('3')
console.timeEnd('runTime:');
}, 2000);
}, 3000);
}, 2000);
// 结果:1 2 3 runTime:: 7017.87890625 ms上面的方式存在回调嵌套的问题,我们希望可以利用sleep函数更方便优雅地实现上面的例子。
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Document</title>
</head>
<body>
<script>
//使程序暂停一定时间,起到延时的效果
function sleep(time) {
return new Promise((resolve) =>
setTimeout(() => {
resolve();
}, time));
}
async function run() {
console.time("runTime:");
console.log("1");
await sleep(2000);
console.log("2");
await sleep(1000);
console.log("3");
console.timeEnd("runTime:");
}
run();
console.log("a");
// 1
// a
// 2
// 3
// runTime:: 3009.984ms
</script>
</body>
</html>6. setTimeout 实现 setInterval
setInterval 的作用是每隔一段指定时间执行一个函数,但是这个执行不是真的到了时间立即执行,它真正的作用是每隔一段时间将事件加入事件队列中去,只有当前的执行栈为空的时候,才能去从事件队列中取出事件执行。所以可能会出现这样的情况,就是当前执行栈执行的时间很长,导致事件队列里边积累多个定时器加入的事件,当执行栈结束的时候,这些事件会依次执行,因此就不能实现间隔一段时间执行的效果。
举个例子:
假设
delay是 1000ms,但callback执行时间很长(比如 2000ms)。第一次
callback执行时,第二次callback已经在 1000ms 时被加入队列了。等到第一次执行完(2000ms 时),发现队列里已经堆积了后续的
callback事件,它们会连续执行(每隔 0ms 就执行一个),而不是真正的“每隔 1000ms 执行一次”。
结果就是:执行时像连续触发,失去了间隔控制。
针对 setInterval 的这个缺点,我们可以使用 setTimeout 递归调用来模拟 setInterval,这样我们就确保了只有一个事件结束了,我们才会触发下一个定时器事件,这样解决了 setInterval 的问题。
为什么能解决?
因为 setTimeout 只在 上一次 callback 执行完成后 才开始计时 delay 毫秒,再安排下一次执行。
第一次执行:等待
delay毫秒 → 执行callback→callback执行完后才开始下一个setTimeout。如果
callback耗时 2000ms,那么下一次的定时器是从这个 2000ms 结束点再等delay毫秒才触发。两次
callback实际执行的间隔 =callback 运行时间 + delay,大于等于delay,永远不会出现堆积连续运行的情况。
实现思路是使用递归函数,不断地去执行 setTimeout 从而达到 setInterval 的效果
function mySetinterval(fn,delay,...args){
let timer = null
const settimeout = () =>{
timer = setTimeout(() => {
fn(...args)
settimeout()
},delay)
}
settimeout()
return function clear(){
clearTimeout(timer)
}
}
const fn = () => console.log(Date.now())
const clear = mySetinterval(fn,1000)
setTimeout(() => {
clear()
},10000)7. 异步循环打印 1,2,3
async function printAsync() {
console.log(1);
await new Promise(resolve => setTimeout(resolve, 1000));
console.log(2);
await new Promise(resolve => setTimeout(resolve, 1000));
console.log(3);
}
printAsync();
// 输出:1 (等待1秒) 2 (等待1秒) 3
function print(count = 1) {
console.log(count);
if (count < 3) {
setTimeout(() => print(count + 1), 1000);
}
}
print();8. 循环打印红、黄、绿
function printcolor(color, delay) {
return new Promise((resolve) => {
setTimeout(() => {
console.log(color);
resolve();
}, delay);
});
}
async function printcolors() {
while (true) {
await printcolor("red", 1000);
await printcolor("yellow", 2000);
await printcolor("green", 3000);
}
}
printcolors()9.数组扁平化:用递归或reduce实现多层级数组扁平化。
/**
* 数组扁平化函数 - forEach版本(支持depth深度控制)
* 将多维数组按指定深度扁平化
* @param {Array} arr - 需要扁平化的多维数组
* @param {number} depth - 扁平化深度,默认为1,Infinity表示无限深度
* @returns {Array} - 扁平化后的数组
*/
function flatten(arr, depth = 1) {
// 如果深度为0或负数,直接返回原数组的浅拷贝
if (depth <= 0) return [...arr]
const result = []
arr.forEach(item => {
if (Array.isArray(item) && depth > 0) {
// 递归扁平化子数组,深度减1
// 使用扩展运算符将递归结果展开后添加到result中
result.push(...flatten(item, depth - 1))
} else {
// 如果当前元素不是数组,或已达到指定深度,直接添加
result.push(item)
}
})
return result
}
/**
* 数组扁平化函数 - reduce版本(支持depth深度控制)
* 使用reduce方法实现更简洁的数组扁平化,支持指定深度
* @param {Array} arr - 需要扁平化的多维数组
* @param {number} depth - 扁平化深度,默认为1,Infinity表示无限深度
* @returns {Array} - 扁平化后的数组
*/
function flatten(arr, depth = 1) {
// 如果深度为0或负数,直接返回原数组的浅拷贝
if (depth <= 0) return [...arr]
return arr.reduce((acc, cur) => {
return acc.concat(
// 如果当前元素是数组且还有深度剩余,递归扁平化(深度减1)
// 否则直接使用当前元素
Array.isArray(cur) && depth > 0
? flatten(cur, depth - 1)
: cur
)
}, [])
}// 先转成字符串,再变回数组
function flat4(array){
//[1,[2,3]].toString() => 1,2,3
return array.toString().split(',').map(item => parseInt(item))
}
function flat5(array){
return array.join(',').split(',').map(item => Number(item))
}10.函数柯里化:实现 add(1)(2)(3) 的累加效果,理解闭包与参数收集。
1. 通俗理解:什么是柯里化?
你可以把普通函数想象成全款买房:
你要买套房(执行 sum(x, y, z)),你必须一次性凑齐首付 x、贷款 y、税费 z,把这笔钱一次性交出去,交易才能完成。
sum(10, 20, 30) 👉 一起交钱,拿房。
把这个过程柯里化,就变成了分期付款(或者攒首付):
你现在只有 10 万(只传一个参数 10),开发商说不行,钱不够不能给你房。但是他给你开了一张收据(返回了一个新函数)。
等你又攒了 20 万(传入 20),你拿着之前的收据去,开发商还是说不够,于是又给你换了一张新的收据(再返回一个新函数,并记住了你一共交了 30 万)。
最后你又拿了 30 万(传入 30),开发商一看,10+20+30=60,总共需要的钱凑齐了!于是当场交房(真正执行了原函数计算结果)。
总结一下概念:
柯里化就是一个“收集参数”的过程。只要参数没凑齐,它就一直返回新函数让你继续传参;一旦参数凑齐了,它就把之前收集到的所有参数一把塞给原函数去执行。
2. 代码
柯里化是把一个接收多个参数的函数,转换成一系列接收单个参数的函数的过程。
函数名.length 代表什么?当你定义一个函数时,它的 .length 属性会自动记录这个函数在定义时声明的参数个数(即形参的个数)。
function sum(x,y,z) {
return x + y + z
}
function myCurrying(fn) {
// 判断当前已经接收的参数的个数,和函数本身需要接收的参数是否一致
function curried(...args) {
// 1.当已经传入的参数 大于等于 需要的参数时,就执行函数
if(args.length >= fn.length){
// 如果调用函数时指定了this,要将其绑定上去
return fn.apply(this, args)
}
else{
// 没有达到个数时,需要返回一个新的函数,继续来接收参数
return function(...args2) {
//return curried.apply(this, [...args, ...args2])
// 接收到参数后,需要递归调用 curried 来检查函数的个数是否达到
return curried.apply(this, args.concat(args2))
}
}
}
return curried
}
var curryAdd = myCurrying(sum)
curryAdd(10,20,30)
curryAdd(10,20)(30)
curryAdd(10)(20)(30)
函数的调用顺序是:
myCurrying(sum)
↓
curried(10)
↓
匿名函数(20)
↓
匿名函数(30)
↓
curried(10,20,30)
↓
sum(...)核心逻辑就是:比较 “已经收到的参数个数” 和 “原函数需要的参数个数”。
curryAdd(10)(20)(30) 的调用过程:
初始状态:
当我们执行 var curryAdd = myCurrying(sum) 时,sum.length 是 3(它需要3个参数)。
返回了内部的 curried 函数。所以此时 curryAdd 就等于 curried。
第一步:传入 10 👉 curryAdd(10)
进入
curried函数,此时参数args = [10]。判断
args.length(1个)>= fn.length(3个) 吗?不成立。走进
else,返回了一个匿名函数。你可以把这个匿名函数看作是“带有记忆的收据”,它利用闭包记住了当前的args = [10]。
第二步:传入 20 👉 匿名函数(20)
上一步返回了匿名函数,紧接着后面有个
(20),说明马上调用了这个匿名函数。进入匿名函数,此时新传入的参数
args2 = [20]。执行
curried.apply(this, args.concat(args2))。这是什么意思?
就是把之前记住的 [10] 和新传入的 [20] 拼起来,变成了 [10, 20],然后再次调用 curried 函数去检查。
再次进入
curried,此时参数args = [10, 20]。判断长度
>= fn.length吗? 2 >= 3 不成立。再次走进
else,由于还没凑齐,又返回了一个新的匿名函数,并记住了现在的状态是[10, 20]。
第三步:传入 30 👉 新匿名函数(30)
接着调用新匿名函数,传入
(30)。进入匿名函数,新参数
args2 = [30]。再次拼接之前记住的参数:
[10, 20]加上[30],变成[10, 20, 30],再次调用curried。第三次进入
curried,此时参数args = [10, 20, 30]。判断
args.length(3个)>= fn.length(3个) 吗?成立!终于凑齐了!走进
if:直接执行return fn.apply(this, args)。也就是执行了
sum(10, 20, 30),算出了 60。整个过程结束。
3. 为什么代码里还要拼接 args 和 args2?
为什么 curryAdd(10, 20)(30) 也是可以工作的?
如果一开始传了 10, 20,那么第一次调用时 args 就是 [10, 20]。
返回匿名函数后,再传 (30) 也就是 args2 = [30]。
拼接时 args.concat(args2) 就直接变成了 [10, 20, 30]。再传给 curried 检查时,顺利凑齐,执行 sum。
所以 ...args2 这个匿名函数的唯一使命就是:接收新来的参数,把它和旧的参数拼到一起,然后交给 curried 这个黑脸长官去检查"人够了没"。够了就干活(if),不够就继续等下一批(else 返回新函数)。
11. 手写instanceof
instanceof 的核心作用是:判断某个对象是否是某个构造函数创建的实例。
比如 [] instanceof Array 看 [] 是不是构造函数Array实例化出来的。
它本质上做的事情只有一件:
看构造函数的
prototype是否存在于对象的原型链上。
也就是说,从实例化出来的对象出发,查找原型链,看看能不能找到构造函数的prototype,因为如果是这个构造函数实例化出来的,这个对象的原型链上就会有这个构造函数的prototye。
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<style>
/* 填写样式 */
</style>
</head>
<body>
<!-- 填写标签 -->
<script type="text/javascript">
// 填写JavaScript
const _instanceof = (target, Fn) => {
//先得到实例对象的__proto__
let __proto__ = Object.getPrototypeOf(target)
//得到构造函数的原型prototype
let prototype = Fn.prototype
while(true){
if(__proto__ == null) return false //原型链一直到末尾null都没找到
if(__proto__ == prototype) return true //匹配成果
//沿着原型链往上找,更新__proto__
__proto__ = Object.getPrototypeOf(__proto__)
}
}
</script>
</body>
</html>12. 手写new操作符
在 JavaScript 中,new 做了 4 件事:
- 创建一个新的空对象
- 让这个对象的
__proto__指向构造函数的prototype - 执行构造函数,并把 this 指向新对象
- 如果构造函数返回对象,则返回该对象,否则返回新对象
JS 的 new 有特殊规则:如果构造函数返回 对象 → 返回这个对象;返回 基本类型 → 忽略,返回实例。
<!DOCTYPE html>
<html>
<head>
<meta charset=utf-8>
</head>
<body>
<script type="text/javascript">
const _new = function(con, ...args) {
//new出来的是一个新的对象,所以先定义一个空对象
let obj = {}
//指定新对象的__proto__指向构造函数的原型
obj.__proto__ = con.prototype
//Object.setPrototypeOf(obj, con.prototype)
// 调用构造函数,修改this指向为新对象,给新对象加入属性和方法
//用result接收构造函数的返回值
let result = con.apply(obj,args)
//规则:如果构造函数有返回值,那么new返回这个返回值,否则返回新对象
return result instanceof Object ? result : obj
}
</script>
</body>
</html>13. 手写 call、apply、bind 函数
call(thisArg, ...args)
call 的本质:把函数变成对象的方法调用。
核心步骤:
- 创建临时属性
- 挂到对象上
- 通过对象调用函数
- 删除临时属性
伪代码:
obj.fn = fn
obj.fn()
delete obj.fn牛客已pass
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<style>
/* 填写样式 */
</style>
</head>
<body>
<!-- 填写标签 -->
<script type="text/javascript">
//函数本质也是对象,基础自Function.prototype
Function.prototype._call = function(thisArgs,...args){
//先获取要执行的函数,谁调用call,此时this就是谁
let func = this
//thisArgs决定函数调用时,this指向谁
//规则是null/undefined this指向全局对象 基本数据类型要转object
thisArgs = thisArgs ? Object(thisArgs) : window
//使用symbol创建独一无二的key,防止和原有普通属性冲突
let fn = Symbol()
thisArgs[fn] = this
//接收返回值
let result = thisArgs[fn](...args)
//最后删除属性值
delete thisArgs[fn]
//返回最终结果
return result
}
</script>
</body>
</html>apply(thisArg, argsArray)
Function.prototype._apply = function(thisArg,argArray){
// 1.获取需要执行的函数
let fn = this
// 2.将 thisArg 转成对象类型(防止它传入的是非对象类型,例如123数字)
thisArg = thisArg ? Object(thisArg) : window
// 判断一些边界情况
argArray = argArray || []
// 3.使用 thisArg 调用函数,绑定 this
//thisArg.fn = fn 使用symbol,防止属性覆盖
const key = Symbol()
thisArg[key] = fn
// 将传递过来的数组(可迭代对象)拆分,传给函数
let result = thisArg[key](...argArray)
delete thisArg[key]
// 4.返回结果
return result
}bind(thisArg, ...args)
<!DOCTYPE html>
<html>
<head>
<meta charset=utf-8>
</head>
<body>
<script type="text/javascript">
// 补全代码
Function.prototype._bind = function(target, ...arguments1) {
//保存调用bind的函数,以便后续调用
let fn = this
//返回一个新函数
return function(){
//用 call 执行原函数,并把 this 指向 target。
return fn.call(target,...arguments1)
}
}
</script>
</body>
</html>14. 手写Ajax请求
整个过程:
- 创建 Promise
- 创建 XMLHttpRequest 对象
- 配置请求(open)
- 监听请求状态变化(onreadystatechange)
- 发送请求(send)
- 请求成功 → resolve
- 请求失败 → reject
function myAjax(url,method = 'GET') {
//创建一个promise,因为ajax是异步的,函数无法直接return结果,promise可以在未来返回结果
return new Promise((resolve, reject) => {
//创建xhr对象
const xhr = new XMLHttpRequest();
//初始化请求,设置请求方式,url和异步
xhr.open(method, url, true);
//监听请求的状态变化
xhr.onreadystatechange = function () {
if (xhr.readyState === 4) {
if (xhr.status === 200) {
resolve(JSON.parse(xhr.responseText));
} else {
reject(xhr.status);
}
}
};
// 必须发送请求
xhr.send(null);
});
}15. 手写数组去重
必会标绿
// 原地去重:方法一 (双层 for 循环 + splice, O(n^2) 时间, O(1) 空间)
const uniqueInPlaceSplice = (arr) => {
for(let i = 0; i < arr.length; i++) {
for(let j = i + 1; j < arr.length; j++) {
if(arr[i] === arr[j]) {
arr.splice(j, 1);
j--; // 因为数组长度减少了,j 需要后退一步,否则会漏掉相邻的重复项
}
}
}
return arr; // 返回原数组的引用
}
// 1.Set + 数组复制
fuction unique1(array){
// Array.from(),对一个可迭代对象进行浅拷贝
return Array.from(new Set(array))
}
// 2.Set + 扩展运算符浅拷贝
function unique2(array){
// ... 扩展运算符
return [...new Set(array)]
}
// 3.filter,判断是不是首次出现,如果不是就过滤掉
function unique3(array){
return array.filter((item,index) => {
return array.indexOf(item) === index
//indexOf()返回 **这个元素第一次出现的位置**
})
}
// 4.创建一个新数组,如果之前没加入就加入
function unique4(array){
let res = []
array.forEach(item => {
if(!res.includes(item)){
res.push(item)
}
})
return res
}进阶:如果数组内有数组和对象,应该怎么去重(此时对象的地址不同,用Set去不了重)
function unique(arr){
const map = new Map()
return arr.filter(item=>{
const key = JSON.stringify(item)
if(map.has(key)) return false
map.set(key,true)
return true
})
}16. 手写数组乱序
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
</head>
<body>
<script>
function randomArray(arr){
// return arr.sort(() => Math.random() - 0.5)
// 洗牌算法,从后往前遍历,依次确定当前遍历i位置的值
for(let i = arr.length -1; i >0 ; i--){
const j = Math.floor(Math.random() * (i +1)) ; //注意这个分号
//random 范围是【0,1) 取i+1 再向下取整,是为了让i这个值也可以被选到
[arr[i],arr[j]] = [arr[j], arr[i]] //直接交换,时间复杂度o(1)
}
return arr
}
console.log(randomArray([1,2,3,4,5,6,7,8,9]))
</script>
</body>
</html>
真正的问题是 **JavaScript 自动分号插入(ASI)**。
如果前一行被解释器误解析,可能会导致下一行在解析时出现问题。
解决方法很简单:**在解构赋值前加分号。**
正确写法
function randomArray(arr){
for(let i = arr.length -1; i >0 ; i--){
const j = Math.floor(Math.random() * (i +1));
;[arr[i],arr[j]] = [arr[j], arr[i]]
}
return arr
}
关键是这一行:
;[arr[i],arr[j]] = [arr[j], arr[i]]
前面的 ; 是为了避免 JS 把它和上一行连在一起解析。
// 方法1: sort + Math.random()
function shuffle1(arr){
return arr.sort(() => Math.random() - 0.5);//
}
// 方法2:时间复杂度 O(n^2)
// 随机拿出一个数(并在原数组中删除),放到新数组中
function randomSortArray(arr) {
let backArr = [];
while (arr.length) {
let index = parseInt(Math.random() * arr.length);
backArr.push(arr[index]);
arr.splice(index, 1);
}
return backArr;
}
// 方法3:时间复杂度 O(n)
// 随机选一个放在最后,交换
function randomSortArray2(arr) {
let lenNum = arr.length - 1;
for (let i = 0; i < lenNum; i++) {
let index = parseInt(Math.random() * (lenNum + 1 - i));
[a[index],a[lenNum - i]] = [a[lenNum - i],a[index]]
}
return arr;
}17. 手撕快排
PS: 常见的排序算法,像冒泡,选择,插入排序这些最好也背一下,堆排序归并排序能写则写。万一考到了呢,要是写不出就直接回去等通知了
快速排序是一种基于分治思想的排序算法。
首先选择一个基准元素,然后通过分区操作,使得小于基准的元素放在左边,大于基准的元素放在右边。
接着递归地对左右两个子数组继续进行快速排序,直到数组长度为1。
平均时间复杂度是 O(n log n),最坏是 O(n²)。
选一个基准值(pivot),把数组分成
左边:比它小
右边:比它大
然后递归排序左右两边。
先把基准值保存下来,相当于留下一个坑,然后从右边找到小于基准的元素填到左坑,再从左边找到大于基准的元素填到右坑,不断重复,直到左右指针相遇,最后把基准值填回去,这样就完成了分区。
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<style>
/* 填写样式 */
</style>
</head>
<body>
<!-- 填写标签 -->
<script type="text/javascript">
// 填写JavaScript
const quicksort = (arr, start = 0, end = arr.length - 1) => {
if(start >= end) return arr // 务必注意是大于等于,返回 arr 方便第一次调用的返回值打印
let left = start
let right = end
const base = arr[left]
//务必注意外层还有一个循环
while(left<right){
//从右往左
while(left < right && arr[right] >= base) right--
arr[left] = arr[right] //base已保存,不用担心覆盖
//从左往右
while(left < right && arr[left] <= base) left++
arr[right] = arr[left]
}
arr[left] = base
quicksort(arr,start,left-1)
quicksort(arr,left+1,end)
return arr
}
console.log(quicksort([1,6,4,5,37,89,0]))
</script>
</body>
</html>18. 手写 JSONP
JSONP原理:
- 利用script标签可以跨域加载JS的特点
- 前端创建script请求接口
- 传递callback函数名
- 服务器返回 callback(data)
- 浏览器执行函数并拿到数据
特点:只支持GET,通过函数调用传递数据。
// 动态的加载js文件
function addScript(src) {
const script = document.createElement('script');
script.src = src;
script.type = "text/javascript";
document.body.appendChild(script);
}
addScript("http://xxx.xxx.com/xxx.js?callback=handleRes");
// 设置一个全局的callback函数来接收回调结果
function handleRes(res) {
console.log(res);
}
// 接口返回的数据格式,加载完js脚本后会自动执行回调函数
handleRes({a: 1, b: 2});19. 手写寄生组合继承
<!DOCTYPE html>
<html>
<head>
<meta charset=utf-8>
</head>
<body>
<script type="text/javascript">
// 补全代码
function Human(name) {
this.name = name
this.kingdom = 'animal'
this.color = ['yellow', 'white', 'brown', 'black']
}
//给human原型加getname方法
Human.prototype.getName = function(){
return this.name
}
function Chinese(name,age) {
// 构造函数继承父类实例方法和属性
Human.call(this,name)
//添加age属性
this.age = age
this.color = 'yellow'
}
//寄生组合继承,创建一个空对象并指定原型指向 这个新对象的 constructor 默认指向 Human
Chinese.prototype = Object.create(Human.prototype)
//每个函数的 原型对象(prototype)默认都有一个 constructor 属性,指向它自身:
Chinese.prototype.constructor = Chinese
//注意要在这里才添加子类方法,不然会被上面的继承覆盖
Chinese.prototype.getAge = function(){
return this.age
}
</script>
</body>
</html>20. 手写浅拷贝
<!DOCTYPE html>
<html>
<head>
<meta charset=utf-8>
</head>
<body>
<script type="text/javascript">
const _shallowClone = target => {
// 补全代码
//先判断边界,如果不是对象直接返回,注意null类型是对象
if(typeof target !== 'object' || target == null){
return target
}
//如果是数组,要开一个新数组
const newObj = Array.isArray(target) ? [] : {}
//得到target的每一个属性
const keys = Object.getOwnPropertyNames(target)
const symbols = Object.getOwnPropertySymbols(target);
//对属性进行遍历复制到新对象里
[...keys,...symbols].forEach(key => {
newObj[key] = target[key]
})
return newObj
}
</script>
</body>
</html>21. 数组/字符串操作题
可以自己找些基础的练一下,就不一一列举了
本人没练过这个
22. 手写二分查找
(这个感觉跟力扣差不多,本人没看过
// 迭代版
function search(nums, target) {
// write code here
if(nums.length === 0) return -1
let left = 0,right = nums.length - 1
// 注意这里的边界,有等号
while(left <= right){
let mid = Math.floor((left + right) / 2)
if(nums[mid] < target) left = mid + 1
else if(nums[mid] > target) right = mid - 1
else return mid
}
return -1
}
// 递归版
function binary_search(arr, low, high, key) {
if (low > high) {
return -1;
}
var mid = parseInt((high + low) / 2);
if (arr[mid] == key) {
return mid;
} else if (arr[mid] > key) {
high = mid - 1;
return binary_search(arr, low, high, key);
} else if (arr[mid] < key) {
low = mid + 1;
return binary_search(arr, low, high, key);
}
};23. CSS水平垂直居中
flex:
.container {
display: flex;
justify-content: center;
align-items: center;
}grid:
.container {
display: grid;
place-items: center;
}margin(只能水平居中):
.item {
margin: 0 auto;
}absolute+transform:
.parent {
position: relative;
}
.son {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
}文字居中:
.text {
height: 100px;
line-height: 100px;
text-align: center;
}vertical-align 常用值:top / middle / bottom / baseline。若想用 vertical-align: middle 实现单行文字垂直居中,需配合 line-height 等于容器高度,且元素为 inline 或 inline-block。
24. CSS画三角形
.div {
width: 0px;
height: 0px;
border: 5px solid transparent;
border-top: 5px solid red; /* 这里记得加宽度和实线 */
}25. CSS实现两栏和三栏布局
flex,grid,圣杯,双飞翼
26. 手写发布-订阅模式 || Event Bus
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
</head>
<body>
<script>
class EventEmitter{
constructor(){
this.events = {}
}
//订阅事件
on(eventName,fn){
if(!this.events[eventName]){
this.events[eventName] = []
}
this.events[eventName].push(fn)
}
//发布事件
emit(eventName,...args){
const fns = this.events[eventName]
if(!fns) return
// 【满分细节1】浅拷贝数组,防止在遍历过程中被 once 或 off 修改导致跳过后面的回调
const fnsToTigger = [...fns];
fnsToTigger.forEach(fn => {
fn.apply(this,args)
});
}
//取消订阅
off(eventName,fn){
const fns = this.events[eventName]
if(!fns) return
// 【满分细节2】兼容 once 的解绑(识别 originalFn)
const index = fns.findIndex(item => item === fn || item.originalFn === fn)
// 【满分细节3】找不到该函数时不误删(indexOf为-1时splice会删掉最后一位)
if(index !== -1){
fns.splice(index,1)
}
}
//一次订阅
once(eventName,fn){
const wrapper = (...args) => {
fn.apply(this,args)
this.off(eventName, wrapper)
}
// 【满分细节2配套】将旧函数挂在 wrapper 上
wrapper.originalFn = fn
this.on(eventName,wrapper)
}
}
</script>
</body>
</html>简单版
<!DOCTYPE html>
<html>
<head>
<meta charset=utf-8>
</head>
<body>
<script type="text/javascript">
class EventEmitter {
// 补全代码
constructor(){
//初始化对象,用于存储事件
this.events = {}
}
//订阅事件
on(eventName,fn){
if(!this.events[eventName]){
//如果事件中心对于某事件没有,那么开辟新数组进行存储
this.events[eventName] = []
}
this.events[eventName].push(fn)
}
//发布事件
emit(eventName,...args){
const fns = this.events[eventName]
if(!fns) return
//遍历每一个订阅者,进行函数调用
fns.forEach(fn =>{
fn(...args)
})
}
}
</script>
</body>
</html>中频
1. 手写事件委托(牛客⭐)
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<style>
/* 填写样式 */
</style>
</head>
<body>
<ul>
<li>.</li>
<li>.</li>
<li>.</li>
</ul>
<!-- 填写标签 -->
<script type="text/javascript">
// 事件委托就是利用事件冒泡,把子元素的事件交给父元素统一处理,通过 event.target 判断触发事件的具体元素。
// 填写JavaScript
document.querySelector('ul').onclick = event => {
// 获取真正被点击的元素
const target = event.target
// 判断是不是li
if(target.tagName === 'LI'){
target.innerHTML += '.'
}
}
</script>
</body>
</html>2. DOM操作相关题目
(这个不知道是啥
3. 手写对象扁平化
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Document</title>
</head>
<body>
<script>
/**
* 对象扁平化(使用 Object.keys)
* @param {Object} obj 需要扁平化的对象
* @param {String} prefix 前缀
* @param {Object} result 结果对象
* @returns {Object} 扁平化后的对象
*/
function flattenObject(obj, prefix = "", result = {}) {
// 处理基本类型和 null
if (obj === null || typeof obj !== "object") {
if (prefix) {
result[prefix] = obj;
}
return result;
}
// 使用 Object.keys 遍历对象自身可枚举属性
Object.keys(obj).forEach((key) => {
const newKey = prefix ? `${prefix}.${key}` : key;
const value = obj[key];
if (value !== null && typeof value === "object") {
flattenObject(value, newKey, result);
} else {
result[newKey] = value;
}
});
return result;
}
const nestedObject = {
name: "张三",
age: 25,
address: {
province: "广东省",
city: "深圳市",
district: "南山区",
detail: {
street: "科技园路",
number: 100,
},
},
hobbies: ["阅读", "编程", "运动"],
isActive: true,
};
const flattened = flattenObject(nestedObject);
console.log(flattened);
</script>
</body>
</html>4. 手写数组常见方法 Array.filter/map/fill/reduce
Array.map(牛客⭐)
遍历数组,对每个元素执行回调函数 Fn,把回调函数的返回值放入新数组,返回新数组。
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<style>
/* 填写样式 */
</style>
</head>
<body>
<!-- 填写标签 -->
<script type="text/javascript">
//
Array.prototype._map = function(Fn) {
const res = [] //map最终要返回一个新数组
//遍历数组每一项,执行回调函数
//this执行调用map的数组
for(let i = 0; i<this.length; i++){
//将回调函数对每一项执行的结果加入新数组,注意map回调有三个参数
res.push(Fn(this[i],i,this))
}
return res
}
</script>
</body>
</html>Array.filter(牛客⭐)
遍历数组,把每一项传给回调函数 Fn,如果返回 true 就把该元素放进新数组。
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<style>
/* 填写样式 */
</style>
</head>
<body>
<!-- 填写标签 -->
<script type="text/javascript">
// 填写JavaScript
Array.prototype._filter = function(Fn) {
const res = [] // 存放结果
for(let i = 0; i < this.length; i++){
if(Fn(this[i], i, this)){ // 执行回调
res.push(this[i]) // 满足条件加入新数组
}
}
return res
}
</script>
</body>
</html>Array.reduce(牛客⭐)
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<style>
/* 填写样式 */
</style>
</head>
<body>
<!-- 填写标签 -->
<script type="text/javascript">
// 填写JavaScript
Array.prototype._reduce = function(fn,initialvalue){
let arr = this //调用reduce的数组
let acc, startIndex
//判断初始值,这决定cur从数组哪一项开始
if(initialvalue !== undefined){
acc = initialvalue // 初始累计值为传入初始值
startIndex = 0
}else{
acc = arr[0] //如果没有初始值,那么累加值从数组第一项开始
startIndex = 1 //注意
}
for(let i = startIndex; i<arr.length; i++){
// 把这次计算的结果重新赋值给 acc,作为下一次循环的累计值。
acc = fn(acc, arr[i], i, arr)
}
return acc
}
</script>
</body>
</html>5. 手写列表转树
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Document</title>
</head>
<body>
<script>
function listToTree(list, parentId = null) {
const map = new Map();
const tree = [];
// 建立 id -> node 映射
for (const item of list) {
map.set(item.id, { ...item, children: [] });
}
// 构建树结构
for (const item of list) {
const node = map.get(item.id);
if (item.parentId === parentId) {
tree.push(node);
} else {
const parent = map.get(item.parentId);
parent?.children.push(node);
}
}
return tree;
}
const list = [
{ id: 1, name: "部门A", parentId: null },
{ id: 2, name: "部门B", parentId: 1 },
{ id: 3, name: "部门C", parentId: 1 },
{ id: 4, name: "部门D", parentId: 2 },
{ id: 5, name: "部门E", parentId: 3 },
];
console.log(listToTree(list));
</script>
</body>
</html>⭐ 日期时间格式化
https://juejin.cn/post/7238199999732711481
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
</head>
<body>
<script>
let date = new Date()
function formatDate(date,format){
const year = date.getFullYear()
const month = String(date.getMonth()+1).padStart(2,'0')
const day = String(date.getDate()).padStart(2,'0')
const hour = String(date.getHours()).padStart(2,'0')
const minute = String(date.getMinutes()).padStart(2,'0')
const second = String(date.getSeconds()).padStart(2,'0')
return format.replace('YYYY',year) //用第二个参数的结果替换第一个参数
.replace('MM',month)
.replace('DD',day)
.replace('hh',hour)
.replace('mm',minute)
.replace('ss',second)
}
console.log(formatDate(date,'YYYY-MM-DD hh:mm:ss'))
console.log(formatDate(date,'YYYY/MM/DD'))
console.log(formatDate(date,'DD-MM-YYYY'))
</script>
</body>
</html>7. 数字千分位
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Document</title>
</head>
<body>
<script>
function formatNumber(num) {
// 第1步:将数字转成字符串,方便遍历
// 例如:1234567.89 -> "1234567.89"
const str = num.toString();
// 第2步:用小数点分割,得到整数部分和小数部分
// 例如:["1234567", "89"]
const [integer, decimal] = str.split(".");
// 第3步:判断是否为负数(整数部分是否以"-"开头)
// 例如:-1234567 -> integer = "-1234567"
const isNegative = integer.startsWith("-");
// 第4步:如果是负数,去掉负号,只处理数字部分
// 例如:-1234567 -> digits = "1234567"
let digits = isNegative ? integer.slice(1) : integer;
// 第5步:result 用来存放最终结果,count 用来计数(每3位加逗号)
let result = "";
let count = 0;
// 第6步:从右向左遍历数字字符串(关键!)
// 例如:"1234567" 从 '7' 开始往前遍历
for (let i = digits.length - 1; i >= 0; i--) {
count++; // 计数器+1,表示已经处理了1位数字
// 把当前数字放到 result 的前面(因为是从右往左遍历)
// 例如:第一次循环 digits[i]='7', result='7'
result = digits[i] + result;
// 每3位加一个逗号,但注意:
// 1. count % 3 === 0:已经处理了3的倍数位
// 2. i !== 0:不是最后一位(避免在数字最前面加逗号)
if (count % 3 === 0 && i !== 0) {
result = "," + result; // 在结果前面加逗号
}
}
// 第7步:如果是负数,把负号拼回去
result = (isNegative ? "-" : "") + result;
// 第8步:如果有小数部分,就拼上小数点和小数
return decimal ? `${result}.${decimal}` : result;
}
console.log(formatNumber(1234567)); // "1,234,567"
console.log(formatNumber(-1234567.89)); // "-1,234,567.89"
console.log(formatNumber(123)); // "123"
console.log(formatNumber(1234)); // "1,234"
</script>
</body>
</html>8. URL参数解析 合法的url
(这个本人不会)
<!DOCTYPE html>
<html>
<head>
<meta charset=utf-8>
</head>
<body>
<script type="text/javascript">
// const _isUrl = url => {
// 补全代码
// const reg = /^https?:\/\/[^\s]+$/;
//return reg.test(url);
// return url.startsWith('https')
// }
const _isUrl = url => {
return /^((http|https):\/\/)?(([A-Za-z0-9]+-[A-Za-z0-9]+|[A-Za-z0-9]+)\.)+([A-Za-z]+)(:\d+)?(\/.*)?(\?.*)?(#.*)?$/.test(url)
}
</script>
</body>
</html>9. 手写观察者模式(牛客⭐)
<!DOCTYPE html>
<html>
<head>
<meta charset=utf-8>
</head>
<body>
<script type="text/javascript">
// 补全代码
class Observerd {
constructor(name){
this.name = name
this.state = '走路'
this.observers = [] //初始观察者为空
}
//保存观察者
setObserver(observer){
this.observers.push(observer)
}
//修改状态并且通知观察者
setState(state){
this.state = state
this.observers.forEach(observer =>{
observer.update(this)
//注意这里要传入this也就是被观察者给观察者
})
}
}
class Observer {
update(observerd){
console.log(`${observerd.name}正在${observerd.state}`)
}
}
</script>
</body>
</html>10. 手写 Promise(进阶)
本人不会
低频
1. 手写组合函数
compose 把多个函数组合成一个,从右向左执行,前一个函数的返回值是后一个函数的参数。
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Document</title>
</head>
<body>
<script>
function compose(...fns) {
return function (x) {
let result = x;
for (let i = fns.length - 1; i >= 0; i--) {
result = fns[i](result);
}
return result;
};
}
// 基础测试
const add1 = (x) => x + 1;
const double = (x) => x * 2;
const square = (x) => x * x;
const composed = compose(square, double, add1);
console.log(composed(5)); // 144
// 执行过程:
// 1. add1(5) = 6
// 2. double(6) = 12
// 3. square(12) = 144
</script>
</body>
</html>2. 手写Object.create()(牛客⭐)
Object.create(proto) 用来:
创建一个新对象,并指定它的原型。
实现 Object.create 本质三步:
- 创建空函数
- prototype 指向目标对象
- new 这个函数
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<style>
/* 填写样式 */
</style>
</head>
<body>
<!-- 填写标签 -->
<script type="text/javascript">
// 填写JavaScript
function _objectCreate(proto){
//先创建一个空构造函数
function F(){}
//指定F的prototype为传进来的proto
F.prototype = proto
//最后new这个构造函数,此时相当于创建了一个新的对象,并且对象的原型是传进来的proto
return new F()
}
</script>
</body>
</html>3. 手写Object.is()
用来解决 === 的两个特殊问题:NaN 和 +0/-0。
NaN === NaN→ false+0 === -0→ true
function objectIs(x, y) {
// 1. NaN 判断
if (x !== x && y !== y) return true;
// 2. +0 和 -0 判断
if (x === 0 && y === 0) return 1 / x === 1 / y;
// 3. 其他情况
return x === y;
}关键点:
NaN:利用 NaN 不等于自身的特性
+0/-0:利用 1/0 得到 Infinity 和 -Infinity 来区分
其他情况直接用
===
4. 手写Object.freeze()(牛客⭐)
<!DOCTYPE html>
<html>
<head>
<meta charset=utf-8>
</head>
<body>
<script type="text/javascript">
const _objectFreeze = object => {
// 补全代码
//得到string和symbol类型的key值
const keys = Object.getOwnPropertyNames(object);
const symbols = Object.getOwnPropertySymbols(object);
//遍历属性每一项
[...keys,...symbols].forEach(key => {
//定义每一个属性不可修改删除
Object.defineProperty(object,key,{
writable:false, //属性值不可改
configurable:false //属性不可删及改操作符
})
})
//并且要禁止属性扩展增加
Object.preventExtensions(object)
}
</script>
</body>
</html>5. 手写JSON.stringify()
这个太多了本人不会
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Document</title>
</head>
<body>
<script>
function jsonStringify(data) {
// ========== 1. 处理 null ==========
// null 在 JSON 中是 'null'
if (data === null) {
return "null";
}
// ========== 2. 处理布尔值 ==========
if (typeof data === "boolean") {
return String(data); // 'true' 或 'false'
}
// ========== 3. 处理数字 ==========
if (typeof data === "number") {
// NaN、Infinity、-Infinity 在 JSON 中转为 null
if (isNaN(data) || !isFinite(data)) {
return "null";
}
return String(data); // 普通数字转为字符串
}
// ========== 4. 处理字符串 ==========
if (typeof data === "string") {
return `"${data}"`; // 加上双引号
}
// ========== 5. 处理 undefined、函数、Symbol ==========
// 这些类型在 JSON.stringify 中返回 undefined
if (
typeof data === "undefined" ||
typeof data === "function" ||
typeof data === "symbol"
) {
return undefined;
}
// ========== 6. 处理数组 ==========
if (Array.isArray(data)) {
const result = [];
for (let i = 0; i < data.length; i++) {
const value = data[i];
// 数组中的 undefined、函数、Symbol 转为 null
if (
value === undefined ||
typeof value === "function" ||
typeof value === "symbol"
) {
result.push("null");
} else {
result.push(jsonStringify(value));
}
}
return `[${result.join(",")}]`;
}
// ========== 7. 处理普通对象 ==========
if (typeof data === "object") {
const result = [];
for (let key in data) {
// 只处理对象自身的属性(不处理原型链)
if (data.hasOwnProperty(key)) {
const value = data[key];
// 跳过值为 undefined、函数、Symbol 的属性
if (
value !== undefined &&
typeof value !== "function" &&
typeof value !== "symbol"
) {
result.push(`"${key}":${jsonStringify(value)}`);
}
}
}
return `{${result.join(",")}}`;
}
// 其他类型返回 undefined
return undefined;
}
// 基本类型
console.log(jsonStringify(null)); // "null"
console.log(jsonStringify(true)); // "true"
console.log(jsonStringify(123)); // "123"
console.log(jsonStringify("hello")); // '"hello"'
console.log(jsonStringify(NaN)); // "null"
console.log(jsonStringify(Infinity)); // "null"
// 数组
console.log(jsonStringify([1, 2, 3])); // "[1,2,3]"
console.log(jsonStringify([1, null, 3])); // "[1,null,3]"
console.log(jsonStringify([1, undefined, 3])); // "[1,null,3]"
console.log(jsonStringify([1, function () {}, 3])); // "[1,null,3]"
// 对象
console.log(jsonStringify({ a: 1, b: 2 })); // '{"a":1,"b":2}'
console.log(jsonStringify({ a: 1, b: undefined })); // '{"a":1}'
console.log(jsonStringify({ a: 1, b: function () {} })); // '{"a":1}'
// 嵌套结构
console.log(
jsonStringify({
name: "张三",
age: 25,
hobbies: ["篮球", "足球"],
address: {
city: "北京",
code: 100000,
},
}),
);
// '{"name":"张三","age":25,"hobbies":["篮球","足球"],"address":{"city":"北京","code":100000}}'
// 循环引用(需要额外处理)
const obj = { a: 1 };
obj.self = obj;
// jsonStringify(obj); // 会栈溢出,需要处理循环引用
</script>
</body>
</html>6. 大数相加
JS 中 Number 有精度限制,超过 2^53-1 会丢失精度,所以大数要用字符串处理 大概16位
function bigNumberAdd(a, b) {
// 1. 转为字符串,方便逐位处理
let str1 = a.toString();
let str2 = b.toString();
// 2. 初始化指针和进位
let i = str1.length - 1; // 指向 str1 的最后一位
let j = str2.length - 1; // 指向 str2 的最后一位
let carry = 0; // 进位,初始为0
let result = ''; // 存储结果
// 3. 从右向左逐位相加
// 循环条件:还有数字没处理,或者还有进位
while (i >= 0 || j >= 0 || carry > 0) {
// 获取当前位的数字(没有则取0)
const num1 = i >= 0 ? Number(str1[i]) : 0;
const num2 = j >= 0 ? Number(str2[j]) : 0;
// 当前位的和(包括进位)
const sum = num1 + num2 + carry;
// 当前位的结果:sum % 10
const currentDigit = sum % 10;
// 新的进位:Math.floor(sum / 10)
carry = Math.floor(sum / 10);
// 将当前位拼接到结果前面
result = currentDigit + result;
// 指针左移
i--;
j--;
}
// 4. 返回结果
return result;
}
// 测试
console.log(bigNumberAdd('123', '456')); // "579"
console.log(bigNumberAdd('999', '1')); // "1000"
console.log(bigNumberAdd('123456789', '987654321')); // "1111111110"
console.log(bigNumberAdd('9007199254740992', '1')); // "9007199254740993"7. ⭐下划线转驼峰
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
</head>
<body>
<script>
function toCamel(str){
return str.replace(/_([a-z])/g, (match,letter) => letter.toUpperCase())
//match是匹配到的完整内容,比如 _a
//letter是捕获组()中匹配到的内容,比如a
//直接把_a整体替换成a的大小写形式A
}
console.log(toCamel('hello_world')) // 输出: helloWorld
</script>
</body>
</html>8. CSS清除浮动
https://juejin.cn/post/7522686990870757386
进阶
Promise 并发控制、串行执行相关的题目
更多 Promise 的面试题在这里:要就来45道Promise面试题一次爽到底
