面试-常考经典1
> Last Format Time:6/12/2026 20:34:58
题目的质量很高,几乎覆盖率90%吧,都背会+理解,基本上可以秒了面试笔试 🔥为常考,⌚️为我面试的时候考过,❌️为没看或者看了没啥用
实现Object.create
创建一个新对象,使用现有的对象来提供新创建的对象的__proto__。
function create(obj) {
function Func(){}
Func.prototype = obj;
// Func.prototype -> obj
// 这里是在干什么呢???很多代码依赖 obj.constructor 来判断对象的类型
// 在 JavaScript 中,每个函数都有一个 prototype 属性,这个 prototype 对象默认会有一个 constructor 属性,现在使用obj来覆盖了Func的prototype,constructor就丢失了,该属性指向函数本身。如果你不写也没啥事,但是写上是会好
Func.prototype.constructor = Func;
return new Func();
}
但是,在上一步 Func.prototype = obj; 中,我们用 obj 完全替换了 Func 原本自带的 prototype 对象。如果 obj 对象上没有 constructor 属性,或者它的 constructor 指向别处,那么通过新对象访问 constructor 时就会出现意料之外的情况。
因此,显式地写上 Func.prototype.constructor = Func; 是一种良好的编程习惯,它确保了由 new Func() 创建出来的对象,其 constructor 属性能正确地指向 Func 函数。
实现instanceof方法
instanceof 运算符用于判断构造函数的 prototype 属性是否出现在对象的原型链中的任何位置。
function myInstanceof(obj, constructor) {
// 获取 obj 的原型
let proto = Object.getPrototypeOf(obj)
console.log(proto.__proto__ === Object.getPrototypeOf(proto))
// true 这里推荐使用 Object.getPrototypeOf(proto) 来获取原型,
// 而不是使用 proto.__proto__,虽然他们的值是一样的
// 获取包装器函数的原型,比如 Object的,Array的,Function的,Date的,
// RegExp的prototype
let prototype = constructor.prototype
while (true) {
if (!proto) return false
if (proto === prototype) return true
// 不断地沿着原型链获取原型,直到找到 null 或者找到 prototype
proto = Object.getPrototypeOf(proto)
}
}
myInstanceof({}, Object)
// true
实现new关键字
在调用new之后会发生这几个步骤:
- 创建一个空对象
- 设置原型:将空白对象的原型设置为函数的prototype对象
- 让函数的this指向这个对象,执行构造函数的代码(为空白对象添加属性)
- 判断函数的返回值
- 如果是引用类型,直接返回,比如构造函数主动返回了一个对象:function T(){return {x: 1}}
- 如果不是引用类型,返回空白对象; 比如构造函数返回一个数字:function T(){return 1}
function myNew(constructor, ...args) {
const obj = {}
// 使用 Object.setPrototypeOf() 方法设置原型链。
// 将obj的__proto__设置为constructor
Object.setPrototypeOf(obj, constructor.prototype)
// 将构造函数的 this 指向设置为 obj,等于obj.constructor(args)
// 也就是初始化一个空的对象
console.log(obj.__proto__.constructor === constructor)
// true
console.log(
obj.__proto__.constructor(args) === constructor.apply(obj, args),
)
// true
const result = constructor.apply(obj, args)
return result instanceof Object ? result : obj
}
关于打印的两个true,既然都一样,我为啥还要apply呢???
apply 是一种更底层的调用方式,它能统一处理普通构造函数和 ES6 Class,而直接调用 constructor() 在 Class 场景下会失效。
拦截构造函数调用
禁止new以外的方式去调用构造函数(考的可能性不大)
在 ES6 之前,JavaScript 并没有一个标准的属性来区分“构造函数调用”和“普通函数调用”。开发者往往只能通过 this instanceof Person 来猜测,但这并不总是可靠。
ES6 引入了 new.target 这个元属性(Meta Property)。
- 元属性的意思是:它不是对象的数据属性,而是用来提供关于语言本身底层细节信息的特殊属性。
new.target的值:- 如果函数是通过
new调用的(如new Person()),new.target指向该函数本身(即Person)。 - 如果函数是直接调用的(如
Person())或通过call/apply调用的,new.target的值为undefined。
- 如果函数是通过
function Person(name) {
// 判断是不是new出来的
if (new.target === Person) {
// 开始构造
this.name = name;
} else {
throw new Error('必须使用 new 命令生成实例');
}
}
var person = new Person('张三'); // 正确
var notAPerson = Person.call(person, '张三'); // 报错
实现继承
组合式继承
//1. 父类 实例属性放在构造函数中
function Father(name, age) {
this.name = name
this.age = age
this.hobby = ['敲代码', '解Bug', '睡觉']
}
// 父类方法放在原型上实现复用
Father.prototype.sayName = function () {
console.log(this.name, 666)
}
Father.prototype.x = 1
//2. 子类
function Child(name, age) {
Father.call(this, name, age) // 子类调用父类的构造函数 (继承父类的属性)
this.a = 1
}
// 将子类的原型的__proto__设置为父类的prototype,原型链就串上了
Child.prototype = Object.create(Father.prototype)
// 另一种写法,和上面没啥区别啊
function Super(foo) {
this.foo = foo
}
Super.prototype.printFoo = function() {
console.log(this.foo)
}
function Sub(bar) {
this.bar = bar
Super.call(this)
}
Sub.prototype = Object.create(Super.prototype)
Sub.prototype.constructor = Sub
ES6版本继承
这个就是一个extend的事,背后做的事情和组合式继承很像
class Super {
constructor(foo) {
this.foo = foo
}
printFoo() {
console.log(this.foo)
}
}
class Sub extends Super {
constructor(foo, bar) {
// 必须调用父类的构造函数
super(foo)
this.bar = bar
}
}
简单实现Promise
这里简单实现一下,可以参考一下其他的Promise A+规范的实现,主要包含then,all,race 这里可以看另一篇集合
const PENDING = 'pending';
const RESOLVED = 'resolved';
const REJECTED = 'rejected';
function MyPromise(fn) {
const self = this;
this.state = PENDING;
this.value = null;
this.reason = null;
this.resolvedCallbacks = [];
this.rejectedCallbacks = [];
function resolve(value) {
if (value instanceof MyPromise) {
value.then(resolve, reject)
}
// 保证代码执行顺序为本轮事件循环的末尾
setTimeout(() => {
if (self.state === PENDING) {
self.state = RESOLVED;
self.value = value;
self.resolvedCallbacks.forEach(cb => cb(value));
}
}, 0)
}
function reject(reason) {
setTimeout(() => {
if (self.state === PENDING) {
self.state = REJECTED;
self.reason = reason;
self.rejectedCallbacks.forEach(cb => cb(reason));
}
}, 0)
}
try {
fn(resolve, reject);
} catch (e) {
reject(e);
}
}
MyPromise.prototype.then = function (onFulfilled, onReject) {
const self = this;
return new MyPromise((resolve, reject) => {
let fulfilled = () => {
try {
const result = onFulfilled(self.value);
return result instanceof MyPromise ? result.then(result) : resolve(result);
} catch (e) {
reject(e);
}
};
let rejected = () => {
try {
const result = onReject(self.reason);
return result instanceof MyPromise ? result.then(resolve, reject) : reject(result);
} catch (e) {
reject(e);
}
}
switch (self.state) {
case PENDING:
case RESOLVED:
case RESOLVED:
}
})
}
all&race ⌚️
这下完了,不会了吧,就是在promises中的promise里去调用外层return的promise的resolve或者reject函数
MyPromise.all = (promises) => {
return new MyPromise((resolve, reject) => {
if (!Array.isArray(promises)) {
throw new TypeError('arguments must be array');
}
let resolvedCounter = 0;
let promiseNum = promises.length;
let resolvedResult = [];
for (let i = 0; i < promises.length; i++) {
MyPromise.resolve(promises[i]).then(value => {
resolvedCounter++;
resolvedResult[i] = value;
if (resolvedCounter === promiseNum) {
return resolve(resolvedResult);
}
}, error => {
return reject(error);
})
}
})
}
MyPromise.race = function(args) {
return new Promise((resolve, reject) => {
for(let i = 0; len = args.length; i++) {
args[i].then(resolve, reject);
}
})
}
防抖函数
防抖是n秒内会重新计时
function debounce(fn, wait) {
let timer = null;
return function() {
if(timer) {
clearTimeout(timer);
timer = null;
}
timer = setTimeout(() => {
fn.apply(this, arguments);
}, wait);
}
}
节流函数
n秒内不重新计时
function throttle(fn, delay) {
let timer = null;
return function () {
if (timer) return;
timer = setTimeout(() => {
timer = null;
return fn.apply(this, arguments);
}, delay)
}
}
function throttle(fn, time) {
let timer = null
return function () {
let _this = this
let arg = arguments
if (!timer) {
timer = setTimeout(() => {
fn.apply(_this, arg)
timer = null
}, time)
}
}
}
实现类型判断函数
这个主要是返回的类型的字符串。typeof返回的值,在参数是null与引用类型的时候,全是object,原始值与函数能够正确的返回对应的类型:
function getType(value) {
// null的类型是Object,需要单独判断
if (value === null) {
return value + ''
}
if (typeof value === 'object') {
return Object
.prototype.toString.call(value)
.slice(8, -1).toLowerCase()
} else {
// 剩下的原始值、函数等,就可以使用typeof了
return typeof value
}
}
console.log('基本类型:')
console.log('getType(1):', getType(1))
console.log("getType('hello'):", getType('hello'))
console.log('getType(true):', getType(true))
console.log('getType(undefined):', getType(undefined))
console.log('getType(null):', getType(null))
console.log('\n引用类型:')
console.log('getType([]):', getType([]))
console.log('getType({}):', getType({}))
console.log('getType(new Date()):', getType(new Date()))
console.log('getType(/regex/):', getType(/regex/))
console.log(
'getType(function(){}):',
getType(function () {}),
)
console.log("getType(Symbol('foo')):", getType(Symbol('foo')))
console.log('\ntypeof 对比:')
console.log('typeof null:', typeof null)
console.log('typeof []:', typeof [])
// 基本类型:
// getType(1): number
// getType('hello'): string
// getType(true): boolean
// getType(undefined): undefined
// getType(null): null
// 引用类型:
// getType([]): array
// getType({}): object
// getType(new Date()): date
// getType(/regex/): regexp
// getType(function(){}): function
// getType(Symbol('foo')): symbol
// typeof 对比:
// typeof null: object
// typeof []: object
浅拷贝
// es6的Object.assign
Object.assign(target, source1, source2);
// 扩展运算符
{...obj1, ...obj2}
// 数组的浅拷贝
Array.prototype.slice
Array.prototype.concat
// 手动实现
function shallowCopy(object) {
if(!object || typeof object !== 'object') return;
// 这是啥
let newObj = Array.isArray(object);
for(let key in object) {
if(object.hasOwnProperty(key)) {
newObj[key] = object(key);
}
}
return newObj;
}
实现Object.assign
就是实现一个浅拷贝,assign的特性是仅仅拷贝对象自己含有的属性
Object.myAssign = function (obj, ...source) {
if (obj === null) throw new Error('can not be null')
// 保证转换的内容是一个对象
let target = Object(obj)
source.forEach((item) => {
if (item !== null) {
for (let key in item) {
// in会访问到从原型链上继承下来的属性
if (item.hasOwnProperty(key)) {
target[key] = item[key]
}
}
}
})
return target
}
let obj = { a: '2', d: '342' }
let obj2 = { c: 'p' }
console.log(Object.myAssign(obj, obj2))
// { a: '2', d: '342', c: 'p' }
❌️简单实现async/await中的async函数
async/await语法糖就是使用Generator函数+自动执行器来运作的(这个就先不看了)
// 定义了一个promise,用来模拟异步请求,作用是传入参数++
function getNum(num){
return new Promise((resolve, reject) => {
setTimeout(() => {
resolve(num+1)
}, 1000)
})
}
//自动执行器,如果一个Generator函数没有执行完,则递归调用
function asyncFun(func){
var gen = func();
function next(data){
var result = gen.next(data);
if (result.done) return result.value;
result.value.then(function(data){
next(data);
});
}
next();
}
// 所需要执行的Generator函数,内部的数据在执行完成一步的promise之后,再调用下一步
var func = function* (){
var f1 = yield getNum(1);
var f2 = yield getNum(f1);
console.log(f2) ;
};
asyncFun(func);
实现一个Object.freeze
锁定对象的方法
Object.preventExtensions() 对象不可扩展, 即不可以新增属性或方法, 但可以修改/删除
Object.seal() 在上面的基础上,对象属性不可删除, 但可以修改
Object.freeze() 在上面的基础上,对象所有属性只读, 不可修改
以上三个方法分别可用Object.isExtensible(), Object.isSealed(), Object.isFrozen()来检测
深度冻结,递归的访问对象中的属性,如果是对象就冻结
var deepFreeze =function (obj) {
var allProps = Object.getOwnPropertyNames(obj);
// 同上:var allProps = Object.keys(obj);
allProps.forEach(item => {
if (typeof obj[item] === 'object') {
deepFreeze(obj[item]);
}
});
return Object.freeze(obj);
}
模拟实现一个Object.freeze,使用了Object.seal
function myFreeze(obj) {
if (obj instanceof Object) {
Object.seal(obj);
let p;
for (p in obj) {
if (obj.hasOwnProperty(p)) {
Object.defineProperty(obj, p, {
writable: false
});
myFreeze(obj[p]);// 递归,实现更深层次的冻结
}
}
}
}
用ES5实现一下map和reduce函数
Array.prototype.myMap = function (callback, thisArg) {
let arr = this
let res = []
arr.forEach((value, i) => {
res.push(callback.call(thisArg, arr[i], i, arr))
})
return res
}
arr = [1, 2, 3, 4]
console.log(arr.myMap((value, index) => value * 5 + index))
// [5, 11, 17, 23]
Array.prototype.myReduce = function (callback, init) {
// 获取当前的数组
let arr = this
let acc, startIndex
// 确定初始的值与初始index,如果没有init,
// 就使用数组的第一项作为初始值,从index=1的位置(数组第二项)开始
acc = init ? init : arr[0]
startIndex = init ? 0 : 1
for (let i = startIndex; i < arr.length; i++) {
// 这几项分别是,上一个,当前,index,被reduce的数组
acc = callback(acc, arr[i], i, arr)
}
return acc
}
let arr = [1, 2, 3, 4]
console.log(arr.myReduce((acc,cur)=>acc+cur));
// 10
>以下是场景题
0.1+0.2问题
转成整数处理
function add(num1, num2) {
let n1, n2, m
// 将两个数小数部分的长度取出来
try {
n1 = num1.toString().split('.')[1].length
} catch (error) {
n1 = 0
}
try {
n2 = num2.toString().split('.')[1].length
} catch (error) {
n2 = 0
}
// 计算这两个小数的最大位数
m = Math.pow(10, Math.max(n1, n2))
// 先扩大做整数运算,再缩小恢复为小数
return (num1 * m + num2 * m) / m
}
console.log(add(0.1, 0.2))
// 0.3
⌚️大数相加解决(字符串相加)
传两个字符串进来,返回一个字符串。 这里处理很妙,对于长度不一样的字符串,肯定有一个是加到最后会有剩余有的,这里我们直接==用0来填充不够长度的字符串的位==,保证逻辑可以顺利的运行:
function add(str1, str2) {
let res = ''
let index1 = str1.length - 1
let index2 = str2.length - 1
// 最开始没有进位
let carry = 0
while (index1 >= 0 || index2 >= 0) {
// js中数组越界返回的值是undefined,转换为0就可以了.
// 这里用的其实是通过index的范围来判断的
let temp1 = index1 >= 0 ? +str1[index1] : 0
let temp2 = index2 >= 0 ? +str2[index2] : 0
// 记得加上进位
let tempSum = temp1 + temp2 + carry
// 将进位化为一位数,不是0,就是1
carry = Math.floor(tempSum / 10) ? 1 : 0
// 将答案拼接在一起,取模为个位
res = `${tempSum % 10}${res}`
index1--
index2--
}
// 若果最后的进位是1,那么就在最前面加上一个1
if (carry === 1) {
res = `1${res}`
}
return res
}
console.log(add('1234', '113456'))
// 114690
大数相乘(字符串相乘)
传两个字符串进来,返回一个字符串
转成数字相加的问题 注意处理全零字符串的情况
var multiply = function (num1, num2) {
let result = '0';
let i = num1.length - 1;
while (i >= 0) {
// 确定当前位的数的权重(后面有几个零)
// 使用join将数组中的各个项串起来
let subfixZero = new Array(num1.length - 1 - i).fill('0').join('');
// num1当前的位的值
let sumCount = +num1[i];
let tempSum = '0';
// 乘法的本质是加n次
while (sumCount > 0) {
// 加n次num2
tempSum = bigSum(tempSum, num2);
sumCount--;
}
// 将零(权)拼上
tempSum = `${tempSum}${subfixZero}`;
// 将这个位求出的值加进res
result = bigSum(result, tempSum);
i--;
}
// 处理一下开头的零,找到最靠前的为零位的index,从这里切开
for (let i = 0; i < result.length; i++) {
if (result[i] !== '0') {
return result.slice(i);
}
}
return '0';
// 与上面的实现一样
function bigSum(n1, n2) {
let result = '';
let i = n1.length - 1, j = n2.length - 1, curry = 0;
while (i >= 0 || j >= 0) {
let l1 = i >= 0 ? +n1[i] : 0;
let l2 = j >= 0 ? + n2[j] : 0;
let sum = l1 + l2 + curry;
curry = sum / 10 | 0;
result = `${sum % 10}${result}`;
i--; j--;
}
if (curry === 1) result = `1${result}`;
return result;
}
};
数组乱序输出
Math.random输出的结果是0-1内的小数,可以直接通过length映射
const randomIndex = Math.round(Math.random()*(array.length - 1 -i) + 1);
数组去重复(7种方法)
1.利用Set()+Array.from() 方式对NaN和undefined类型去重也是有效的,是因为NaN和undefined都可以被存储在Set中, NaN之间被视为相同的值
2.利用两层循环+数组的splice方法 此方法对NaN是无法进行去重的,因为进行比较时NaN !== NaN
3.利用数组的indexOf方法 新建一个空数组,遍历需要去重的数组,将数组元素存入新数组中,存放前判断数组中是否已经含有当前元素,没有则存入。此方法也无法对NaN去重 indexOf() 方法:返回调用它的String对象中第一次出现的指定值的索引
4.利用数组的includes方法 此方法逻辑与indexOf方法去重异曲同工,只是用includes方法来判断是否包含重复元素。
5.利用数组的filter()+indexOf() 输出结果中不包含NaN,是因为indexOf()无法对NaN进行判断
6.利用Map() 使用Map()也可对NaN去重,原因是Map进行判断时认为NaN是与NaN相等的
7.利用对象 和Map()是差不多的,主要是利用了对象的属性名不可重复这一特性。
数组扁平化flatten(6种方法)
这个还是看自己的吧
递归 reduce 扩展运算符 toString,split es6 flat 正则和json,json.stringify
function flatten(arr) {
let result = [];
for (let i = 0; i < arr.length; i++) {
if (Array.isArray(arr[i])) {
result = result.concat(flatten(arr[i]));
} else {
result.push(arr[i]);
}
}
return result;
}
// 这个究极简单,但是是无限的扁平化了
function flatten(arr) {
return arr.reduce(
(p, c) => p.concat(Array.isArray(c) ? flatten(c) : c), []
)
}
// 不在原型上写也是一样的
Array.prototype.myFlat = function (depth = 1) {
// console.log(this) ← 普通函数的 this 指向调用者(即数组本身)我最开始是使用的箭头函数,由于箭头函数没有this,会有问题
if (depth === 0) return this
return this.reduce((acc, cur) => {
if (Array.isArray(cur)) {
return acc.concat(cur.myFlat(depth - 1))
} else {
return acc.concat(cur)
}
}, [])
}
console.log(arr.myFlat(1))
// [ 1, 3, 4, 6, 1, 3, [ 1, 2 ] ]
有的同学可能会想,这个使用concat是浅拷贝,会不会有什么问题呢。这个数组扁平化的核心任务是将一个嵌套的、多维的数组结构,转换成一个一维的、线性的数组结构。它关注的是元素的层级关系。
🔥对象扁平化flatObj
多次遇到,建议背诵。这道题是比较绕的,但是本身的逻辑并不难,反正和递归扯上就不太舒服了。这个实现是将数组也拆来来了
/* 题目*/
var entryObj = {
a: {
b: {
c: {
dd: 'abcdd',
},
},
d: {
xx: 'adxx',
},
e: [1, 2, 4, 'ee'],
},
f: 'f',
g: ['gg'],
}
// 仔细的分析g,最开始的调用flat函数,path为空,res为空对象,isArray为false
// 然后递归调用flat函数,v为['gg'],path为'[g]',res为空对象,isArray为true
// 然后递归调用flat函数,v为'gg',path为'[g][0]',res为空对象,isArray为false
// 然后将'gg'赋值给res['g[0]'],res为{'g[0]': 'gg'}
// 对象的分析过程是一样的
// 要求转换成如下对象
var outputObj = {
'a.b.c.dd': 'abcdd',
'a.d.xx': 'adxx',
'a.e[0]': 1,
'a.e[1]': 2,
'a.e[2]': 4,
'a.e[3]': 'ee',
f: 'f',
'g[0]': 'gg',
}
function flat(obj, path = '', res = {}, isArray = false) {
// 遍历对象的所有属性,将其中的键与值取出来,判断他们的类型
for (let [k, v] of Object.entries(obj)) {
// console.log('当前键值对:', k, v)
if (Array.isArray(v)) {
// 如果值是数组,递归调用flat函数
let pathOfCurrentValue = isArray ? `${path}[${k}]` : `${path}${k}`
flat(v, pathOfCurrentValue, res, true)
} else if (typeof v === 'object') {
// 如果值是对象,递归调用flat函数
let pathOfCurrentValue = isArray ? `${path}[${k}].` : `${path}${k}.`
flat(v, pathOfCurrentValue, res, false)
} else {
// 如果值是其他类型,直接赋值给结果对象
let pathOfCurrentValue = isArray ? `${path}[${k}]` : `${path}${k}`
res[pathOfCurrentValue] = v
}
}
return res
}
console.log(
flat({
a: {
aa: [{ aa1: 1 }],
},
}),
flat(entryObj),
)
// { 'a.aa[0].aa1': 1 } {
// 'a.b.c.dd': 'abcdd',
// 'a.d.xx': 'adxx',
// 'a.e[0]': 1,
// 'a.e[1]': 2,
// 'a.e[2]': 4,
// 'a.e[3]': 'ee',
// f: 'f',
// 'g[0]': 'gg'
// }
我自己认为更加现代的实现
const flat = (obj, path = '', res = {}, isArray) => {
Object.entries(obj).forEach(([key, value]) => {
console.log(key, value)
if (Array.isArray(value)) {
let currentValuePath = isArray ? `${path}[${key}]` : `${path}${key}`
flat(value, currentValuePath, res, true)
} else if (typeof value === 'object') {
let currentValuePath = isArray ? `${path}[${key}]` : `${path}${key}`
flat(value, currentValuePath, res, false)
} else {
let currentValuePath = isArray ? `${path}[${key}]` : `${path}${key}`
res[currentValuePath] = value
}
})
return res
}
这个实现就没有将数组拆开,这个是人类可以实现的:
// prefix为递归调用时访问的路径,result为最后返回的结果
function flattenObject(obj, prefix = '', result = {}) {
for (let key in obj) {
if (obj.hasOwnProperty(key)) {
const newKey = prefix ? `${prefix}.${key}` : key
if (
typeof obj[key] === 'object' &&
obj[key] !== null &&
!Array.isArray(obj[key])
) {
// 如果值是对象,递归处理
flattenObject(obj[key], newKey, result)
} else {
// 否则直接赋值
result[newKey] = obj[key]
}
}
}
return result
}
let entryObj = {
a: {
b: {
c: {
dd: 'abcdd',
},
},
d: {
xx: 'adxx',
},
e: [1, 2, 4, 'ee'],
},
f: 'f',
g: ['gg'],
}
console.log(flattenObject(entryObj))
// {
// 'a.b.c.dd': 'abcdd',
// 'a.d.xx': 'adxx',
// 'a.e': [ 1, 2, 4, 'ee' ],
// f: 'f',
// g: [ 'gg' ]
// }
数字千分位分割
注意可能有小数
function format(number) {
const [intPart, decPart = undefined] = String(number).split('.')
let result = ''
let count = 0
for (let i = intPart.length - 1; i >= 0; i--) {
// 字符串的加法,将最后一位从’前面(高位)‘的位置加入result
result = intPart[i] + result
count++
// 如果访问了3位并且不是第一位,就添加一个','
if (count % 3 === 0 && i !== 0) {
result = ',' + result
}
}
if (decPart !== undefined) {
result = result + '.' + decPart
}
return result
}
console.log(format(123456.789), format(123456))
法二:
function format(number) {
return number.toLocaleString()
}
// 示例
console.log(format(1234567.89)) // "1,234,567.89"
console.log(format(1234)) // "1,234"
console.log(format(-12345.6)) // "-12,345.6"
js下划线转驼峰处理「快手」
正则法,/_([a-z])/g表达的意思是在全局情况下,寻找在_后面的小写的字符。找到后,replace将match的内容替换为函数返回的值
function camelCase(str) {
// _s -> S
return str.replace(
/_([a-z])/g,
(match, group1) => group1.toUpperCase()
)
}
console.log(camelCase('some_string'))
// someString
补充
function camelCase(str) {
return str.replace(/([-_])([a-z])/g, function(match, group1, group2) {
return group2.toUpperCase();
});
}
console.log(camelCase("some-string_with-underscores"));
Hex转RGB的方法
function hexToRgb(val) {
//HEX十六进制颜色值转换为RGB(A)颜色值
// 16进制颜色值的正则
var reg = /^#([0-9a-fA-f]{3}|[0-9a-fA-f]{6})$/;
// 把颜色值变成小写
var color = val.toLowerCase();
var result = '';
if (reg.test(color)) {
// 如果只有三位的值,需变成六位,如:#fff => #ffffff
if (color.length === 4) {
var colorNew = '#';
for (var i = 1; i < 4; i += 1) {
colorNew += color.slice(i, i + 1).concat(color.slice(i, i + 1));
}
color = colorNew;
}
// 处理六位的颜色值,转为RGB
var colorChange = [];
for (var i = 1; i < 7; i += 2) {
colorChange.push(parseInt('0x' + color.slice(i, i + 2)));
}
result = 'rgb(' + colorChange.join(',') + ')';
return { rgb: result, r: colorChange[0], g: colorChange[1], b: colorChange[2] };
} else {
result = '无效';
return { rgb: result };
}
}
❌️实现模版字符串解析
不看了,这是Vue的实现
var template = `
<div>
<% if(name){ %>
<span>%= name =%</span>
<% } %>
%= age =%
<div>`
let str = rander(template, {name: '小明', age: 18})
// 解析完成 str <div> <span>小明</span>18<div>
function parseTemplateString (templateString, data) {
// 使用正则表达式在模板字符串中查找所有 ${...} 的实例
const regex = /${(.*?)}/g;
// 使用 replace() 方法将每个 ${...} 的实例替换为数据对象中相应的值
const parsedString = templateString.replace(regex, (match, key) => {
// 使用 eval() 函数来评估 ${...} 中的表达式,并从数据对象中返回相应的值
return eval(`data.${key}`);
});
return parsedString;
}
⌚️🔥数组转树形结构
递归解法非常好理解,代码量也很少,题目出现概率很高
{
"city": [
{ "id": 12, "parent_id": 1, "name": "朝阳区" },
{ "id": 241, "parent_id": 24, "name": "田林街道" },
{ "id": 31, "parent_id": 3, "name": "广州市" },
{ "id": 13, "parent_id": 1, "name": "昌平区" },
{ "id": 2421, "parent_id": 242, "name": "上海科技绿洲" },
{ "id": 21, "parent_id": 2, "name": "静安区" },
{ "id": 242, "parent_id": 24, "name": "漕河泾街道" },
{ "id": 22, "parent_id": 2, "name": "黄浦区" },
{ "id": 11, "parent_id": 1, "name": "顺义区" },
{ "id": 2, "parent_id": 0, "name": "上海市" },
{ "id": 24, "parent_id": 2, "name": "徐汇区" },
{ "id": 1, "parent_id": 0, "name": "北京市" },
{ "id": 2422, "parent_id": 242, "name": "漕河泾开发区" },
{ "id": 32, "parent_id": 3, "name": "深圳市" },
{ "id": 33, "parent_id": 3, "name": "东莞市" },
{ "id": 3, "parent_id": 0, "name": "广东省" }
]
}
将source转化为下面的形式(这是我的字节一面最后一道题)
我自己的实现方式是先遍历一次,将根节点插入到res数组中,然后将这些插入的项移除,然后遍历剩余的部分,将其中pid===id的项添加到父项的children数组中,之后,重复移除,添加的过程,直到res的长度为零(但这其实是我之后想到的,根本不是我面试的想到的,有很多细小的错误,导致我整体的答题效果不是很好,尤其是我一开始的递归思路,source最开是Array,其Item是对象,自然就不能使用forEach,我没有察觉到这一点。。。)
// 实现函数将source转换为result
const source = [
{ id: 0, name: "a" },
{ id: 1, name: "b", pid: 0 },
{ id: 2, name: "c", pid: 0 },
{ id: 3, name: "d", pid: 1 },
{ id: 4, name: "e", pid: 2 },
{ id: 5, name: "f" },
{ id: 6, name: "g", pid: 3 },
{ id: 7, name: "h", pid: 5 },
{ id: 8, name: "i", pid: 7 },
];
const result = [
{
id: 0,
name: "a",
children: [
{
id: 1,
name: "b",
pid: 0,
children: [
{
id: 3,
name: "d",
pid: 1,
children: [{ id: 6, name: "g", pid: 3 }],
},
],
},
{ id: 2, name: "c", pid: 0, children: [{ id: 4, name: "e", pid: 2 }] },
],
},
{
id: 5,
name: "f",
children: [
{ id: 7, name: "h", pid: 5, children: [{ id: 8, name: "i", pid: 7 }] },
],
},
];
法一:
function arrayToTree(source) {
const tree = []; // 存放最终的树形结构,存放树的根结点,就是没有pid的结点
const map = {}; // 哈希表,用于存储 id 到节点的映射
// 1. 第一次遍历:将所有节点存入 map,并初始化 children 数组
source.forEach(item => {
map[item.id] = { ...item, children: [] };
});
// 2. 第二次遍历:建立父子关系
source.forEach(item => {
const node = map[item.id]; // 当前节点
// 如果存在 pid,说明它不是根节点
if (item.pid !== undefined && map[item.pid]) {
// 找到父节点,将当前节点 push 进父节点的 children
map[item.pid].children.push(node);
} else {
// 如果不存在 pid 或找不到父节点,说明它是根节点
tree.push(node);
}
});
return tree;
}
// 测试数据
const source = [
{ id: 0, name: "a" },
{ id: 1, name: "b", pid: 0 },
{ id: 2, name: "c", pid: 0 },
{ id: 3, name: "d", pid: 1 },
{ id: 4, name: "e", pid: 2 },
{ id: 5, name: "f" },
{ id: 6, name: "g", pid: 3 },
{ id: 7, name: "h", pid: 5 },
{ id: 8, name: "i", pid: 7 },
];
const result = arrayToTree(source);
console.log(JSON.stringify(result, null, 2));
法一的Map实现,和上面的逻辑几乎一样的,可能性能会更优?这个又是set又是get的,太麻烦了:
const fn = (source) => {
let root = []
let map = new Map()
// 遍历source,将每个item添加到map中
source.forEach((item) => {
// map.set(item.pid, item) 这里将对象展开,添加了一个children属性
map.set(item.id, { ...item, children: [] })
})
map.forEach((item) => {
// 这里的item.pid === undefined 不能写成!item.pid,这会导致pid为0的项也算作了根
if (item.pid === undefined) {
// 如果是根节点,直接添加到root中
root.push(map.get(item.id))
} else {
// 如果不是根节点,将当前item添加到父节点的children中
map.get(item.pid).children.push(map.get(item.id))
}
})
return root
}
fn(source)
// root: [
// { id: 0, name: 'a', children: [[Object], [Object]] },
// { id: 5, name: 'f', children: Object },
// ]
法二:
function arrayToTreeRecursive(source, pid = undefined) {
const tree = [];
// 筛选出所有父级 ID 等于当前 pid 的节点
source.forEach(item => {
if (item.pid === pid) {
// 找到子节点后,递归查找该子节点的子节点
const children = arrayToTreeRecursive(source, item.id);
if (children.length > 0) {
tree.push({ ...item, children });
} else {
tree.push({ ...item });
}
}
});
return tree;
}
// 调用方式:默认从 pid 为 undefined 或 null 的根节点开始找
// 注意:你的数据中根节点没有 pid 字段,所以默认传 undefined 即可匹配
const result = arrayToTreeRecursive(source);
console.log(JSON.stringify(result, null, 2));
加分项:
// 在解法一返回前,增加这一步清洗(可选)
function cleanEmptyChildren(node) {
if (node.children && node.children.length === 0) {
delete node.children; // 删除空的 children 数组
} else if (node.children) {
node.children.forEach(cleanEmptyChildren); // 递归清洗子节点
}
}
// 使用示例:
// result.forEach(cleanEmptyChildren);
最新的优化后的写法:
// 实现函数将source转换为result
const source = [
{ id: 0, name: 'a' },
{ id: 1, name: 'b', pid: 0 },
{ id: 2, name: 'c', pid: 0 },
{ id: 3, name: 'd', pid: 1 },
{ id: 4, name: 'e', pid: 2 },
{ id: 5, name: 'f' },
{ id: 6, name: 'g', pid: 3 },
{ id: 7, name: 'h', pid: 5 },
{ id: 8, name: 'i', pid: 7 },
]
function arrayToTree(arr) {
let root = []
let map = new Map()
// 遍历,将每个item添加到map中
arr.forEach((obj) => {
map.set(obj.id, { ...obj, children: [] })
})
// 相比于原先的实现,直接使用了forEach中的key
map.forEach((obj, key) => {
if (obj.pid === undefined) {
root.push(map.get(key))
} else {
map.get(obj.pid).children.push(map.get(key))
}
})
return root
}
console.log(arrayToTree(source))
// [
// { id: 0, name: 'a', children: [[Object], [Object]] },
// { id: 5, name: 'f', children: Object },
// ]
获取URL中的参数
这里主要还是正则表达式的设计
?:关键点。当它紧跟在 + 后面时,它将“贪婪模式”转变为“非贪婪模式”(也叫懒惰模式)。
- 贪婪模式(默认):会尽可能多地匹配字符。
- 非贪婪模式:一旦遇到下一个满足条件的字符(这里是 =),就立即停止匹配。
[^&]:这是一个否定字符集。^ 在方括号内表示“非”或“除了”。所以这表示匹配除了 & 以外的任意字符。
function parseUrl(url) {
const _url = url || window.location.href
// 正则匹配
const _urlParams = _url.match(/[?&](.+?=[^&]*)/g) // 注意这里改为 * 以支持空值
console.log(_urlParams);
if (!_urlParams) return {}
return _urlParams.reduce((a, b) => {
// 去掉首位分隔符并分割
const pair = b.slice(1).split('=')
const key = decodeURIComponent(pair[0])
const value = decodeURIComponent(pair[1] || '') // 处理空值
// 处理重复参数:如果已存在,转为数组
if (a[key]) {
a[key] = Array.isArray(a[key])
? [...a[key], value]
: [a[key], value]
} else {
a[key] = value
}
return a
}, {})
}
console.log(
parseUrl(
'https://example.com?user[name]=John&user[age]=30&tags=js&tags=css&tags=666',
),
)
// [
// '?user[name]=John',
// '&user[age]=30',
// '&tags=js',
// '&tags=css',
// '&tags=666'
// ]
// {
// 'user[name]': 'John',
// 'user[age]': '30',
// tags: [ 'js', 'css', '666' ]
// }
> 以下为进阶题
⌚️🔥🔥请求并发控
多次遇到的题目,而且有很多变种。主要的思路就是有一组url,在控制并发数的情况下,在所有的结果均返回的时候resolve,resolve一个数组,其项为url的返回值,无论是否请求成功,这个逻辑和allSettled很像:
/**
* 并发请求控制函数
* @param {string[]} urls - 请求地址列表
* @param {number} maxCount - 最大并发请求数
* @returns {Promise<any[]>} 按照urls的原始顺序(不是返回结果的顺序)返回的结果数组
*/
function concurRequest(urls, maxCount) {
if (maxCount <= 0) {
return Promise.resolve([])
}
const len = urls.length
const results = new Array(len) // 存储结果,按顺序填充
let activeCount = 0 // 当前活跃的请求数
let currentIndex = 0 // 下一个要发起请求的索引
return new Promise((resolve) => {
// 启动第一个批次的请求
fetchNext()
// 定义一个辅助函数,用于发起请求
function fetchNext() {
// 如果所有请求都已发起,且没有活跃的请求,则结束
if (currentIndex >= len && activeCount === 0) {
resolve(results)
return
}
// 如果还有请求未发起,且当前活跃数小于最大并发数
while (currentIndex < len && activeCount < maxCount) {
const i = currentIndex
activeCount++
// 发起请求
fetch(urls[i])
// 假设响应是 JSON 格式
.then((response) => response.json())
.catch((error) => {
// 请求失败时,将其作为一个错误对象存入结果数组
// 不会直接reject
console.error(`请求 ${urls[i]} 失败:`, error)
// 可以自定义错误格式
return { error: true, message: error.message }
})
.then((data) => {
// 将转化后的json结果按原始顺序存入数组
results[i] = data
})
.finally(() => {
// 无论成功失败,都减少活跃计数,并尝试发起下一个请求
activeCount--
currentIndex++
fetchNext() // 递归调用,检查是否可以发起新的请求
})
}
}
})
}
/*
const urls = [
'https://jsonplaceholder.typicode.com/posts/1',
'https://jsonplaceholder.typicode.com/posts/2',
'https://jsonplaceholder.typicode.com/posts/invalid', // 这个会失败
'https://jsonplaceholder.typicode.com/posts/4',
];
concurRequest(urls, 2) // 最多同时发起 2 个请求
.then(results => {
console.log('所有请求结果(按顺序):', results);
// 结果数组中,第3个位置将是错误对象,其他为对应的 JSON 数据
});
*/
变种,这种写法将while循环中的创建过程转移到了外侧的for循环中:
// 假设 pics 是一个包含图片 URL 的数组
const pics = ['url1', 'url2', 'url3', 'url4', 'url5', 'url6', 'url7', 'url8', 'url9', 'url10'];
// 假设 maxLoad 是最大并发数
const maxLoad = 3;
function getUrlByFetch() {
// 1. 定义一个指针,指向下一个需要被调度的任务索引
// 初始值为 maxLoad,意味着前 maxLoad 个任务会立即启动
let idx = maxLoad;
// 2. 核心调度函数
function getContention(index) {
// 模拟异步请求
fetch(pics[index])
.then(() => {
// 3. 当前任务完成后的回调
// 指针后移,准备调度下一个任务
idx++;
// 4. 边界检查与递归调度
// 如果指针还没指到数组末尾,说明还有任务没开始跑
if (idx < pics.length) {
// 关键点:这里传入的是 idx,而不是 index + 1
// 这意味着:谁先完成,谁就负责去开启“队列中的下一个任务”
// 这样保证了始终有 maxLoad 个任务在跑(直到任务不够分)
getContention(idx);
}
})
.catch(err => console.error(err)); // 建议加上错误处理
}
// 5. 启动函数
function start() {
// 6. 填满第一波并发池
// 循环 maxLoad 次,启动前 maxLoad 个任务
// 注意:这里 i 从 0 开始,且 i < maxLoad,同时也需要确保不超过数组长度
for (let i = 0; i < maxLoad && i < pics.length; i++) {
getContention(i);
}
}
start();
}
带并发限制的promise异步调度器,上一题的其中一个变化:
function TaskPool() {
this.tasks = []; // 等待执行的任务队列
this.pool = []; // 正在执行的任务集合
this.max = 2; // 最大并发数
}
// 添加任务的方法
TaskPool.prototype.addTask = function(task) {
// task 是一个返回 Promise 的函数
this.tasks.push(task);
// 每次添加任务后,都尝试运行(检查池子有没有空位)
this.run();
}
// 核心调度方法
TaskPool.prototype.run = function() {
// 1. 边界检查:如果没有等待的任务,直接返回
if (this.tasks.length === 0) {
return;
}
// 2. 计算当前能启动多少个任务
// 取 "剩余任务数" 和 "池子剩余空位数" 中的较小值
// 例如:池子最大2,正在跑1个,那空位就是1,这次循环最多启动1个
let min = Math.min(this.tasks.length, this.max - this.pool.length);
// 3. 循环启动任务
for (let i = 0; i < min; i++) {
// 从等待队列头部取出一个任务
const currTask = this.tasks.shift();
// 将任务加入正在执行的池子
this.pool.push(currTask);
// 执行任务
currTask().finally(() => {
// 4. 任务完成后的清理工作
// 从正在执行的池子中移除当前任务
// 注意:使用 indexOf 查找并移除,保证池子状态正确
const index = this.pool.indexOf(currTask);
if (index > -1) {
this.pool.splice(index, 1);
}
// 5. 递归/循环检查
// 一个任务结束了,池子有了空位,立刻尝试去等待队列里拿新任务
this.run();
});
}
}
🔥实现lazy链式调用: person.eat().sleep(2).eat()
解法其实就是将所有的任务异步化,然后存到一个任务队列里,在启动run的时候,将队列里的任务一个一个执行:
function Person() {
this.queue = [];
this.lock = false;
}
Person.prototype.eat = function () {
this.queue.push(() => new Promise(resolve => { console.log('eat'); resolve(); }));
// this.run();
return this;
}
Person.prototype.sleep = function(time, flag) {
this.queue.push(() => new Promise(resolve => {
setTimeout(() => {
console.log('sleep', flag);
resolve();
}, time * 1000)
}));
// this.run();
return this;
}
Person.prototype.run = async function() {
if(this.queue.length > 0 && !this.lock) {
this.lock = true;
const task = this.queue.shift();
await task();
this.lock = false;
this.run();
}
}
const person = new Person();
person.eat().sleep(1, '1').eat().sleep(3, '2').eat().run();
方法二
class Lazy {
// 函数调用记录,私有属性
#cbs = [];
constructor(num) {
// 当前操作后的结果
this.res = num;
}
// output时,执行,私有属性
#add(num) {
this.res += num;
console.log(this.res);
}
// output时,执行,私有属性
#multipy(num) {
this.res *= num;
console.log(this.res)
}
add(num) {
// 往记录器里面添加一个add函数的操作记录
// 为了实现lazy的效果,所以没有直接记录操作后的结果,而是记录了一个函数
this.#cbs.push({
type: 'function',
params: num,
fn: this.#add
})
return this;
}
multipy(num) {
// 和add函数同理
this.#cbs.push({
type: 'function',
params: num,
fn: this.#multipy
})
return this;
}
top (fn) {
// 记录需要执行的回调
this.#cbs.push({
type: 'callback',
fn: fn
})
return this;
}
delay (time) {
// 增加delay的记录
this.#cbs.push({
type: 'delay',
// 因为需要在output调用是再做到延迟time的效果,利用了Promise来实现
fn: () => {
return new Promise(resolve => {
console.log(`等待${time}ms`);
setTimeout(() => {
resolve();
}, time);
})
}
})
return this;
}
// 关键性函数,区分#cbs中每项的类型,然后执行不同的操作
// 因为需要用到延迟的效果,使用了async/await,所以output的返回值会是promise对象,无法链式调用
// 如果需实现output的链式调用,把for里面函数的调用全部放到promise.then的方式
async output() {
let cbs = this.#cbs;
for(let i = 0, l = cbs.length; i < l; i++) {
const cb = cbs[i];
let type = cb.type;
if (type === 'function') {
cb.fn.call(this, cb.params);
}
else if(type === 'callback') {
cb.fn.call(this, this.res);
}
else if(type === 'delay') {
await cb.fn();
}
}
// 执行完成后清空 #cbs,下次再调用output的,只需再输出本轮的结果
this.#cbs = [];
}
}
function lazy(num) {
return new Lazy(num);
}
const lazyFun = lazy(2).add(2).top(console.log).delay(1000).multipy(3)
console.log('start');
console.log('等待1000ms');
setTimeout(() => {
lazyFun.output();
}, 1000);
❌️lazy-load实现
img标签默认支持懒加载只需要添加属性 loading="lazy",然后如果不用这个属性,想通过事件监听的方式来实现的话,也可以使用IntersectionObserver来实现,性能上会比监听scroll好很多。不看了
const imgs = document.getElementsByTagName('img');
const viewHeight = window.innerHeight || document.documentElement.clientHeight;
let num = 0;
function lazyLoad() {
for (let i = 0; i < imgs.length; i++) {
let distance = viewHeight - imgs[i].getBoundingClientRect().top;
if(distance >= 0) {
imgs[i].src = imgs[i].getAttribute('data-src');
num = i+1;
}
}
}
window.addEventListener('scroll', lazyLoad, false);
实现简单的虚拟dom
给出如下虚拟dom的数据结构,如何实现简单的虚拟dom,渲染到目标dom树。这东西的本质就是将树形结构的对象通过操作DOM的方法转化为真实的DOM
// 样例数据
let demoNode = ({
tagName: 'ul',
props: {'class': 'list'},
children: [
({tagName: 'li', children: ['douyin']}),
({tagName: 'li', children: ['toutiao']})
]
});
构建一个render函数,将demoNode对象渲染为以下dom
<ul class="list">
<li>douyin</li>
<li>toutiao</li>
</ul>
效果 
更加现代的实现
// 使用 class 声明
class Element {
// 构造函数:直接处理参数,不再需要 "容错/工厂" 逻辑
constructor({ tagName, props = {}, children = [] }) {
this.tagName = tagName
this.props = props
this.children = children
}
// 实例方法:直接定义在类中,无需 prototype
render() {
// 创建真实 DOM
const el = document.createElement(this.tagName)
// 设置属性:使用 Object.entries 和现代循环
for (const [key, value] of Object.entries(this.props)) {
el.setAttribute(key, value)
}
// 处理子节点:使用 forEach 和 箭头函数
this.children.forEach((child) => {
// 递归渲染:判断子节点是 Element 实例还是字符串
const childEl =
child instanceof Element
? child.render()
: document.createTextNode(child)
el.appendChild(childEl)
})
return el
}
}
// 必须使用 new,这是现代 class 的标准用法
const elem = new Element({
tagName: 'ul',
props: { class: 'list' },
children: [
new Element({ tagName: 'li', children: ['item1'] }),
new Element({ tagName: 'li', children: ['item2'] }),
],
})
document.querySelector('body').appendChild(elem.render())
通过遍历,逐个节点地创建真实DOM节点
function Element({tagName, props, children}){
// 判断必须使用构造函数
if(!(this instanceof Element)){
return new Element({tagName, props, children})
}
this.tagName = tagName;
this.props = props || {};
this.children = children || [];
}
Element.prototype.render = function(){
var el = document.createElement(this.tagName),
props = this.props,
propName,
propValue;
for(propName in props){
propValue = props[propName];
el.setAttribute(propName, propValue);
}
this.children.forEach(function(child){
var childEl = null;
if(child instanceof Element){
childEl = child.render();
}else{
childEl = document.createTextNode(child);
}
el.appendChild(childEl);
});
return el;
};
// 执行
var elem = Element({
tagName: 'ul',
props: {'class': 'list'},
children: [
Element({tagName: 'li', children: ['item1']}),
Element({tagName: 'li', children: ['item2']})
]
});
document.querySelector('body').appendChild(elem.render());
实现SWR 机制
SWR 这个名字来自于 stale-while-revalidate:一种由 HTTP RFC 5861 推广的 HTTP 缓存失效策略
const cache = new Map();
async function swr(cacheKey, fetcher, cacheTime) {
let data = cache.get(cacheKey) || { value: null, time: 0, promise: null };
cache.set(cacheKey, data);
// 是否过期
const isStaled = Date.now() - data.time > cacheTime;
if (isStaled && !data.promise) {
data.promise = fetcher()
.then((val) => {
data.value = val;
data.time = Date.now();
})
.catch((err) => {
console.log(err);
})
.finally(() => {
data.promise = null;
});
}
if (data.promise && !data.value) await data.promise;
return data.value;
}
const data = await fetcher();
const data = await swr('cache-key', fetcher, 3000);
实现一个只执行一次的函数
// 闭包
function once(fn) {
let called = false;
return function _once() {
if (called) {
return _once.value;
}
called = true;
_once.value = fn.apply(this, arguments);
}
}
//ES6 的元编程 Reflect API 将其定义为函数的行为
Reflect.defineProperty(Function.prototype, 'once', {
value () {
return once(this);
},
configurable: true,
})
LRU 算法实现
LRU(Least recently used,最近最少使用)算法根据数据的历史访问记录来进行淘汰数据,其核心思想是“如果数据最近被访问过,那么将来被访问的几率也更高”。
class LRUCahe {
constructor(capacity) {
this.cache = new Map();
this.capacity = capacity;
}
get(key) {
if (this.cache.has(key)) {
const temp = this.cache.get(key);
this.cache.delete(key);
this.cache.set(key, temp);
return temp;
}
return undefined;
}
set(key, value) {
if (this.cache.has(key)) {
this.cache.delete(key);
} else if (this.cache.size >= this.capacity) {
// map.keys() 会返回 Iterator 对象
this.cache.delete(this.cache.keys().next().value);
}
this.cache.set(key, value);
}
}
在 JavaScript 中,Map 对象中的键值对是按照它们被插入的顺序进行迭代的。这意味着:
- 最先进入
Map的键值对,会位于迭代顺序的最前面。 - 最后进入
Map的键值对,会位于迭代顺序的最后面。
LRU 缓存的策略是:当容量满了需要淘汰数据时,淘汰掉那个“最久没有被使用”的数据。在这个实现中,我们约定:
- 迭代顺序的头部(最前面) 代表 “最久未使用” (LRU) 的数据。
- 迭代顺序的尾部(最后面) 代表 “最近使用” (MRU) 的数据。
this.cache.keys()- 这个方法会返回一个新的
Iterator对象,这个迭代器包含了Map中所有的键,并且顺序与Map的插入顺序一致。
- 这个方法会返回一个新的
.next()- 调用迭代器的
next()方法,会返回一个包含value和done属性的对象。 - 第一次调用
next(),它会返回迭代器中的第一个值,也就是Map中最前面的那个键。
- 调用迭代器的
.value- 从上一步返回的对象中,取出
value属性,这个值就是我们想要的“最久未使用”的键。
- 从上一步返回的对象中,取出
所以,this.cache.keys().next().value 整体上就是为了拿到那个应该被淘汰的“老古董”键。
🔥发布-订阅(事件总线)
发布者不直接触及到订阅者、而是由统一的第三方来完成实际的通信的操作,叫做发布-订阅模式。这个在另一个文档中也有实现。
class EventEmitter {
constructor() {
// handlers是一个map,用于存储事件与回调之间的对应关系
this.handlers = {}
}
// on方法用于安装事件监听器,它接受目标事件名和回调函数作为参数
on(eventName, cb) {
// 先检查一下目标事件名有没有对应的监听函数队列
if (!this.handlers[eventName]) {
// 如果没有,那么首先初始化一个监听函数队列
this.handlers[eventName] = []
}
// 把回调函数推入目标事件的监听函数队列里去
this.handlers[eventName].push(cb)
}
// emit方法用于触发目标事件,它接受事件名和监听函数入参作为参数
emit(eventName, ...args) {
// 检查目标事件是否有监听函数队列
if (this.handlers[eventName]) {
// 这里需要对 this.handlers[eventName] 做一次浅拷贝,主要目的是为了避免通过 once 安装的监听器在移除的过程中出现顺序问题
const handlers = this.handlers[eventName].slice()
// 如果有,则逐个调用队列里的回调函数
handlers.forEach((callback) => {
callback(...args)
})
}
}
// 移除某个事件回调队列里的指定回调函数
off(eventName, cb) {
const callbacks = this.handlers[eventName]
const index = callbacks.indexOf(cb)
if (index !== -1) {
callbacks.splice(index, 1)
}
}
// 为事件注册单次监听器
once(eventName, cb) {
// 对回调函数进行包装,使其执行完毕自动被移除
const wrapper = (...args) => {
cb(...args)
this.off(eventName, wrapper)
}
this.on(eventName, wrapper)
}
}
观察者模式
Vue机制
const queuedObservers = new Set();
const observe = fn => queuedObservers.add(fn);
const observable = obj => new Proxy(obj, {set});
function set(target, key, value, receiver) {
const result = Reflect.set(target, key, value, receiver);
queuedObservers.forEach(observer => observer());
return result;
}
单例模式
核心要点: 用闭包和Proxy属性拦截
function getSingleInstance(func) {
let instance;
let handler = {
construct(target, args) {
if(!instance) instance = Reflect.construct(func, args);
return instance;
}
}
return new Proxy(func, handler);
}
洋葱圈模型compose函数
不看
function compose(middleware) {
return function(context, next) {
let index = -1;
return dispatch(0);
function dispatch(i) {
// 不允许执行多次中间件
if(i <= index) return Promise.reject(new Error('next() called multiple times'));
// 更新游标
index = i;
let fn = middle[i];
// 这个next是外部的回调
if(i === middle.length) fn = next;
if(!fn) return Promsie.resolve();
try{
return Promise.resove(fn(context, dispatch.bind(null, i+1)));
}catch(err){
return Promise.reject(err);
}
}
}
}
作者:秋染蒹葭 链接:https://juejin.cn/post/7299357176928354313 来源:稀土掘金 著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。