Pressidian
花园入口
笔记
项目
关于
实验室
GitHub
花园入口
笔记
项目
关于
实验室
GitHub

KNOWLEDGE PATHS

笔记库
当前位置
笔记库/前端/面试/代码

面试-常考经典1

10 分钟阅读 · Note

目录树 578 篇

          • 面试-常考经典1
          • 面试-常考经典2
          • 面试-数组专项
        • 可投递企业
      • 前端技术栈
    • 笔记目录
    • CLAUDE.md
    • Vue 组件与 Render 函数

关联笔记 6

↗面试-常考经典2同一路径↗面试-数组专项同一路径↗5 最长回文子串 ⌚️共同主题↗解析模板字符串共同主题↗牛客 101共同主题↗数组&字符串技巧共同主题
  • 面试-常考经典1

面试-常考经典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>

效果 ![Pasted image 20260416161011](../../../_assets/9faac1f-Pasted image 20260416161011.png)

更加现代的实现

// 使用 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) 的数据。
  1. this.cache.keys()

    • 这个方法会返回一个新的 Iterator 对象,这个迭代器包含了 Map 中所有的键,并且顺序与 Map 的插入顺序一致。
  2. .next()

    • 调用迭代器的 next() 方法,会返回一个包含 value 和 done 属性的对象。
    • 第一次调用 next(),它会返回迭代器中的第一个值,也就是 Map 中最前面的那个键。
  3. .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 来源:稀土掘金 著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。