前端面试手撕题整理(自用)

高频

一、手写LRU缓存

leetcode 446

请你设计并实现一个满足 LRU (最近最少使用) 缓存 约束的数据结构。
实现 LRUCache 类:

  • LRUCache(int capacity) 以 正整数 作为容量 capacity 初始化 LRU 缓存
  • int get(int key) 如果关键字 key 存在于缓存中,则返回关键字的值,否则返回 -1 。
  • void put(int key, int value) 如果关键字 key 已经存在,则变更其数据值 value ;如果不存在,则向缓存中插入该组 key-value 。如果插入操作导致关键字数量超过 capacity ,则应该 逐出 最久未使用的关键字。

函数 get 和 put 必须以 O(1) 的平均时间复杂度运行。

示例:

输入
["LRUCache", "put", "put", "get", "put", "get", "put", "get", "get", "get"]
[[2], [1, 1], [2, 2], [1], [3, 3], [2], [4, 4], [1], [3], [4]]
输出
[null, null, null, 1, null, -1, null, -1, 3, 4]

解释
LRUCache lRUCache = new LRUCache(2);
lRUCache.put(1, 1); // 缓存是 {1=1}
lRUCache.put(2, 2); // 缓存是 {1=1, 2=2}
lRUCache.get(1); // 返回 1
lRUCache.put(3, 3); // 该操作会使得关键字 2 作废,缓存是 {1=1, 3=3}
lRUCache.get(2); // 返回 -1 (未找到)
lRUCache.put(4, 4); // 该操作会使得关键字 1 作废,缓存是 {4=4, 3=3}
lRUCache.get(1); // 返回 -1 (未找到)
lRUCache.get(3); // 返回 3
lRUCache.get(4); // 返回 4

a. LRU 缓存策略举例:
i. 假设缓存大小为 4,依次打开了 gitlab、力扣、微信、QQ,缓存链表为 QQ -> 微信 -> 力扣 -> gitlab;
ii. 若此时切换到了「微信」,则缓存链表更新为 微信 -> QQ -> 力扣 -> gitlab;
iii. 若此时打开了腾讯会议,因为缓存已经满 4 个 ,所以要进行缓存淘汰机制,删除链表的最后一位 「gitlab」,则缓存链表更新为 腾讯会议 -> 微信 -> QQ -> 力扣;
b. 本题用的是 map 迭代器,map 实现了 iterator,next 模拟链表的下一个指针,为了方便操作,这里将 map 第一个元素作为链表的最后一个元素;

let cache = new Map();
cache.set('a', 1);
cache.set('b', 2);
cache.set('c', 3);
cache.keys(); // MapIterator {'a', 'b', 'c'}
cache.keys().next().value; // a

a. 时间复杂度:对于 put 和 get 都是 O(1);

b. 空间复杂度:O(capacity);

class LRUCache {

    capacity:number;
    cache:Map<number,number> = new Map();
    //缓存的容量
    constructor(capacity: number) {
        this.capacity = capacity
    }

    get(key: number): number {
        if(this.cache.has(key)){

            let value = this.cache.get(key);
            //重新set,相当于更新到cache最后----可以保证删除时,最久未使用的在第一位
            //先删除
            this.cache.delete(key);
            this.cache.set(key,value);
            return value;
        }
        return -1;
    }

    put(key: number, value: number): void {

        if(this.cache.has(key)){
            this.cache.delete(key);
        }

        this.cache.set(key,value);

        if(this.cache.size>this.capacity){
            //利用next指针,反复进行put时,可以删除正确的缓存
            this.cache.delete(this.cache.keys().next().value);
        }

    }
}

二、手写深拷贝

前置知识

前面文章我们讲到,JavaScript中存在两大数据类型:

  • 基本类型
  • 引用类型

基本类型数据保存在在栈内存中

引用类型数据保存在堆内存中,引用数据类型的变量是一个指向堆内存中实际对象的引用,存在栈中

深拷贝

深拷贝开辟一个新的栈,两个对象属完成相同,但是对应两个不同的地址,修改一个对象的属性,不会改变另一个对象的属性

常见的深拷贝方式有:

  • _.cloneDeep()
  • jQuery.extend()
  • JSON.stringify()
  • 手写循环递归
console.log(obj1.b.f === obj2.b.f); // false

JSON.stringify()

const obj2=JSON.parse(JSON.stringify(obj1));

但是这种方式存在弊端,会忽略undefinedsymbol函数

const obj = {
    name: 'A',
    name1: undefined,
    name3: function() {},
    name4:  Symbol('A')
}
const obj2 = JSON.parse(JSON.stringify(obj));
console.log(obj2); // {name: "A"}
function deepCopy(object) {
  if (!object || typeof object !== "object") return object;

  let newObject = Array.isArray(object) ? [] : {};

  for (let key in object) {
    if (object.hasOwnProperty(key)) {
      newObject[key] = deepCopy(object[key]);
    }
  }

  return newObject;
}

循环递归------考点

function deepClone(obj, hash = new WeakMap()) {
  if (!object || typeof object !== "object") return object;

  // 是对象的话就要进行深拷贝,遇到循环引用,将引用存储起来,如果存在就不再拷贝
  if (hash.get(obj)) return hash.get(obj);
  let cloneObj = Array.isArray(object) ? [] : {};
  hash.set(obj, cloneObj);

  for (let key in obj) {
    if (obj.hasOwnProperty(key)) {
      // 实现一个递归拷贝
      cloneObj[key] = deepClone(obj[key], hash);
    }
  }
  return cloneObj;
}

下面首先借助两张图,可以更加清晰看到浅拷贝与深拷贝的区别

img

从上图发现,浅拷贝和深拷贝都创建出一个新的对象,但在复制对象属性的时候,行为就不一样

浅拷贝只复制属性指向某个对象的指针,而不复制对象本身,新旧对象还是共享同一块内存,修改对象属性会影响原对象

// 浅拷贝
const obj1 = {
    name : 'init',
    arr : [1,[2,3],4],
};
const obj3=shallowClone(obj1) // 一个浅拷贝方法
obj3.name = "update";
obj3.arr[1] = [5,6,7] ; // 新旧对象还是共享同一块内存

console.log('obj1',obj1) // obj1 { name: 'init',  arr: [ 1, [ 5, 6, 7 ], 4 ] }
console.log('obj3',obj3) // obj3 { name: 'update', arr: [ 1, [ 5, 6, 7 ], 4 ] }

但深拷贝会另外创造一个一模一样的对象,新对象跟原对象不共享内存,修改新对象不会改到原对象

// 深拷贝
const obj1 = {
    name : 'init',
    arr : [1,[2,3],4],
};
const obj4=deepClone(obj1) // 一个深拷贝方法
obj4.name = "update";
obj4.arr[1] = [5,6,7] ; // 新对象跟原对象不共享内存

console.log('obj1',obj1) // obj1 { name: 'init', arr: [ 1, [ 2, 3 ], 4 ] }
console.log('obj4',obj4) // obj4 { name: 'update', arr: [ 1, [ 5, 6, 7 ], 4 ] }

三、手写Array.filter/map/fill/reduce/forEach

/*
纯函数API : 
concat
filter
map
reduce
slice
非纯函数API
splice
shift(unshift同理)
reverse
*/

forEach返回undefined不属于纯函数,同样的由every、some

Array.prototype._forEach = function(fn,_this){
    if(typeof fn!='function') throw '需要传入一个函数';
    if(!this instanceof Array) throw '需要传入一个数组';
    let length = this.length;
    for(let i=0;i<length;i++)
    {
        fn.call(_this,this[i],i,this)
    }
}

纯函数API

concat() 方法用于合并两个或多个数组。此方法不会更改现有数组,而是返回一个新数组

concat方法不会改变this或任何作为参数提供的数组,而是返回一个浅拷贝.也就是说,如果数组里有引用对象的话,拷贝过来的是引用地址

//实现一个简易concat
Array.prototype._concat = function(...args){
    let context = this , result = [];
    //如果数组为空,直接返回当前数组对象
    if(!args.length){
      return context
    }
    //如果没有参数,直接返回this
    args.forEach( item => {
      if(Object.prototype.toString.call(item) !== '[object Array]'){
        result.push(item)
        //判断参数里面有没有数组
      }else{
        item.forEach( it => result.push(it))
      }
    })
    return result
}
console.log( []._concat(1,2,3,3,45))
console.log( []._concat(1,2,3,{name:'dd'},45))
console.log( []._concat([1,[2]],3,3,45))
console.log( []._concat() )
// [ 1, 2, 3, 3, 45 ]
// [ 1, 2, 3, { name: 'dd' }, 45 ]
// [ 1, [ 2 ], 3, 3, 45 ]
// []

filter() 方法创建一个新数组, 其包含通过所提供函数实现的测试的所有元素。

需要注意的点

  • filter方法不会改变原数组,它返回过滤后的新数组
  • 这个过程是一次性的,后面数组改变不会影响filter的值
//实现一个简易filter
Array.prototype._filter = function(cb,thisArg){
    let context = this
    let cbThis = thisArg ? thisArg : null
    let result = []
    //this默认指向this,提供了thisArg的情况下指向thisArgs
    context.forEach( (item,index,arr) => {
      if(cb.call(cbThis,item,index,arr)){
        result.push(item)
      }
    })
    return result
}
console.log( [1,2,3,43]._filter( item => item&1 == 1 ) )
console.log( []._filter(item => item&1 == 1 , [1,2,3,43]) )
//[ 1, 3, 43 ]
//[]

map() 方法创建一个新数组,其结果是该数组中的每个元素是调用一次提供的函数后的返回值。

需要注意的点

  • map方法不会改变原数组,它返回新数组
  • callback 函数会被自动传入三个参数:数组元素,元素索引,原数组本身
  • map函数的第二个参数是可选项,作为callback的this指向
//实现一个简易map
Array.prototype._map = function(cb,thisArg){
    let context = this
    let cbThis = thisArg ? thisArg : null
    let result = []
    //this默认指向this,提供了thisArg的情况下指向thisArgs
    context.forEach( (item,index,arr) => {
       result.push( cb.call(cbThis,item,index,arr) )
      }) 
      return result
    }
console.log( [1,2,3]._map(Math.sqrt) )
console.log( [1,2,3]._map((index,item)=>{ return index * 2 + item}) )
// [ 1, 1.4142135623730951, 1.7320508075688772 ]
// [ 2, 5, 8 ]

reduce() 方法对数组中的每个元素执行一个由您提供的reducer函数(升序执行),将其结果汇总为单个返回值。

需要注意的点

  • reduce方法不会改变原数组,它返回一个值
  • 提供初始值initialValue会更加安全(建议)
//实现一个简易reduce
Array.prototype._reduce = function(reducer,initialValue){
    let context = this , startIndex = 1
    debugger
    //this默认指向this,提供了thisArg的情况下指向thisArgs
    if(initialValue !== undefined){
        startIndex = 0
    }else{
        initialValue = context[0]
    }
    for(let i = startIndex ; i < context.length ; i ++){
        initialValue = reducer(initialValue,context[i],i,context)
    }
      return initialValue
    }
console.log([1,2,3,34]._reduce((a,b) => a+b , 12))
console.log([1,2,3,34]._reduce((a,b) => a.concat(b) , []))
// 52
// [ 1, 2, 3, 34 ]

slice() 方法返回一个新的数组对象,这一对象是一个由 begin 和 end 决定的原数组的浅拷贝(包括 begin,不包括end)。原始数组不会被改变。

需要注意的点

  • slice方法是对数组项的浅拷贝
  • slice方法不会对原始数组进行修改
//实现一个简易slice
Array.prototype._slice = function(...args){
    let len = this.length , result = []
    let startIndex = args[0] === undefined ? 0 : args[0]
    let endIndex = args[1] === undefined ? len : args[1] >= len?len:args[1]
    for(let i = startIndex ; i < endIndex ; i ++ ){
        result.push(this[i])
    }
    return result
}
console.log( [1,2,3,34,3,3,3,33,3]._slice() )
//[1, 2,  3, 34, 3,3, 3, 33,  3]

非纯函数API

splice()方法通过删除或替换现有元素或者原地添加新的元素来修改数组,并以数组形式返回被修改的内容。此方法会改变原数组。

需要注意的点

  • 指定修改的开始位置(从0计数)。如果超出了数组的长度,则从数组末尾开始添加内容;如果是负值,则表示从数组末位开始的第几位(从-1计数,这意味着-n是倒数第n个元素并且等价于array.length-n);如果负数的绝对值大于数组的长度,则表示开始位置为第0位。
  • 如果 deleteCount 大于 start 之后的元素的总数,则从 start 后面的元素都将被删除(含第 start 位)。 如果 deleteCount 被省略了,或者它的值大于等于array.length - start(也就是说,如果它大于或者等于start之后的所有元素的数量),那么start之后数组的所有元素都会被删除。 如果 deleteCount 是 0 或者负数,则不移除元素。这种情况下,至少应添加一个新元素
//实现一个简易splice
Array.prototype._splice = function(startIndex,count,...items){
    debugger
    let context = this , len = context.length;
    let temp = this.slice()
    startIndex = startIndex < 0 ? Math.abs(startIndex>=len)?0:len + startIndex : startIndex
    //这里是判断startIndex负值相关情况
    //这里是判断count值得情况
    count = count <=0 ? 0 : count//count可以是负值
    if( count >= len - startIndex ){
        for(let i = 0 ; i < len - startIndex ; i ++){
            context.pop()
        }
        items.forEach( item => {
            context.push(item)
        })
    }else{//正常情况下得
        context.length = len - count + items.length//数组原地操作
        for(let j = context.length - 1 ; j >= startIndex + count ; j--){
            context[j] = context[ j + count - items.length ]
        }
        for(let i = 0 ; i < items.length ; i ++){
            context[startIndex + i] = items[i]
        }
    }
    console.log(this)
    return count === undefined ? temp.slice(startIndex) : temp.slice(startIndex,startIndex + count)
}
console.log( [1,2,3,34]._splice(1,2,3,4,45) )
// [ 1, 3, 4, 45, 34 ]
// [ 2, 3 ]

拓展:手写push、pop、shift、unshift------均为非纯函数

push

向数组末尾添加一个或多个元素,并返回数组新的长度

//通过数组的arguments属性

function push(){
    for(let i=0;i<arguments.length;i++){
        this[this.length] = arguments[i];
    }
    return this.length
}
Array.prototype.push = push;
unshift

向数组开头添加一个或多个元素,并且返回数组新的长度

function unshift(){
    //创建一个新数组接收添加的元素
    let newAry = [];
    for(let i=0;i<arguments.length;i++){
        newAry[i] = arguments[i];
    }
    let len = newAry.length;
    for(let i=0;i<this.length;i++){
        newAry[i+len] = this[i];
    }
    for(let i=0;i<newAry.length;i++){
        this[i] = newAry[i];
    }
    return this.length;
}
Array.prototype.unshift = unshift;
pop

删除数组最后一项,并返回该删除项目

function pop(){
    let returnVal = this[this.length-1];
    this.length--;
    return returnVal
}
Array.prototype.pop = pop;
shift

删除数组第一项,并且返回该删除项目

function shift(){
    let newAry = [];
    let reVal = this[0];
    for(let i=0;i<this.length-1;i++){
        newAry[i] = this[i+1];
    }
    for(let i=0;i<newAry.length;i++){
        this[i] = newAry[i]
    }
    this.length--;
    return reVal;
}
Array.prototype.shift = shift;

四、手写new操作符

前置知识

从上面介绍中,我们可以看到new关键字主要做了以下的工作:

  • 创建一个新的对象obj
  • 将对象与构建函数通过原型链连接起来
  • 将构建函数中的this绑定到新建的对象obj
  • 根据构建函数返回类型作判断,如果是原始值则被忽略,如果是返回对象,需要正常处理

举个例子:

function Person(name, age){
    this.name = name;
    this.age = age;
}
const person1 = new Person('Tom', 20)
console.log(person1)  // Person {name: "Tom", age: 20}
t.sayName() // 'Tom'

流程图如下:

img

手写

现在我们已经清楚地掌握了new的执行过程

那么我们就动手来实现一下new

function mynew(Func, ...args) {
    // 1.创建一个新对象
    const obj = {}
    // 2.新对象原型指向构造函数原型对象
    obj.__proto__ = Func.prototype
    // 3.将构建函数的this指向新对象
    let result = Func.apply(obj, args)
    // 4.根据返回值判断
    return result instanceof Object ? result : obj
}

测试一下

function mynew(func, ...args) {
    const obj = {}
    obj.__proto__ = func.prototype
    let result = func.apply(obj, args)
    return result instanceof Object ? result : obj
}
function Person(name, age) {
    this.name = name;
    this.age = age;
}
Person.prototype.say = function () {
    console.log(this.name)
}

let p = mynew(Person, "huihui", 123)
console.log(p) // Person {name: "huihui", age: 123}
p.say() // huihui

五、手写bind

从上面可以看到,applycallbind三者的区别在于:

  • 三者都可以改变函数的this对象指向
  • 三者第一个参数都是this要指向的对象,如果如果没有这个参数或参数为undefinednull,则默认指向全局window
  • 三者都可以传参,但是apply是数组,而call是参数列表,且applycall是一次性传入参数,而bind可以分为多次传入
  • bind是返回绑定this之后的函数,applycall 则是立即执行

实现bind的步骤,我们可以分解成为三部分:

  • 修改this指向
  • 动态传递参数
// 方式一:只在bind中传递函数参数
fn.bind(obj,1,2)()

// 方式二:在bind中传递函数参数,也在返回函数中传递参数
fn.bind(obj,1)(2)
  • 兼容new关键字

整体实现代码如下:

Function.prototype.myBind = function (context) {
    // 判断调用对象是否为函数
    if (typeof this !== "function") {
        throw new TypeError("Error");
    }

    // 获取参数
    const args = [...arguments].slice(1),
          fn = this;

    return function Fn() {

        // 根据调用方式,传入不同绑定值
        return fn.apply(this instanceof Fn ? new fn(...arguments) : context, args.concat(...arguments)); 
    }
}

六、手写vue v-if/v-show

v-show原理

不管初始条件是什么,元素总是会被渲染

我们看一下在vue中是如何实现的

代码很好理解,有transition就执行transition,没有就直接设置display属性

// https://github.com/vuejs/vue-next/blob/3cd30c5245da0733f9eb6f29d220f39c46518162/packages/runtime-dom/src/directives/vShow.ts
export const vShow: ObjectDirective<VShowElement> = {
  beforeMount(el, { value }, { transition }) {
    el._vod = el.style.display === 'none' ? '' : el.style.display
    if (transition && value) {
      transition.beforeEnter(el)
    } else {
      setDisplay(el, value)
    }
  },
  mounted(el, { value }, { transition }) {
    if (transition && value) {
      transition.enter(el)
    }
  },
  updated(el, { value, oldValue }, { transition }) {
    // ...
  },
  beforeUnmount(el, { value }) {
    setDisplay(el, value)
  }
}

v-if原理

v-if在实现上比v-show要复杂的多,因为还有else else-if 等条件需要处理,这里我们也只摘抄源码中处理 v-if 的一小部分

返回一个node节点,render函数通过表达式的值来决定是否生成DOM

// https://github.com/vuejs/vue-next/blob/cdc9f336fd/packages/compiler-core/src/transforms/vIf.ts
export const transformIf = createStructuralDirectiveTransform(
  /^(if|else|else-if)$/,
  (node, dir, context) => {
    return processIf(node, dir, context, (ifNode, branch, isRoot) => {
      // ...
      return () => {
        if (isRoot) {
          ifNode.codegenNode = createCodegenNodeForBranch(
            branch,
            key,
            context
          ) as IfConditionalExpression
        } else {
          // attach this branch's codegen node to the v-if root.
          const parentCondition = getParentCondition(ifNode.codegenNode!)
          parentCondition.alternate = createCodegenNodeForBranch(
            branch,
            key + ifNode.branches.length - 1,
            context
          )
        }
      }
    })
  }
)

七、防抖与节流

//防抖函数
function debounce(func, delay) {

  // 这里使用了闭包,所以 timer 不会轻易被销毁
  let timer = null

  // 生成一个新的函数并返回
  return function (...args) {
    // 清空定时器
    if (timer) {
      clearTimeout(timer)
    }
    // 重新启动定时器
    timer = setTimeout(() => {
      func.call(this, ...args)
    }, delay)
  }
    
}


//节流函数
function throttle(func, delay) {
    
  let timer = null
  // 在 delay 时间内,最多执行一次 func
  return function (...args) {
    if (!timer) {
      timer = setTimeout(() => {
        func.call(this, ...args)
        // 完成一次计时,清空,待下一次触发
        timer = null
      }, delay)
    }
  }
    
}

八、手写 instanceof

// 原理:验证当前类的原型prototype是否会出现在实例的原型链proto上,只要在它的原型链上,则结果都为true
function myinstanceOf_(obj, class_name) {
      // let proto = obj.__proto__;
    let proto = Object.getPrototypeOf(obj)
    let prototype = class_name.prototype
    while (true) {
        if (proto == null) return false
        if (proto == prototype) return true
          // proto = proto.__proto__;
        proto = Object.getPrototypeOf(proto)
    }
}

九、手写 call、apply、bind 函数

// 给函数的原型添加 _call 方法,使得所有函数都能调用 _call
// thisArg 就是要绑定的那个this;...args 扩展操作符传参,适合不定长参数,args是一个数组
Function.prototype._call = function(thisArg,...args){
    // 1.获取需要执行的函数
    let func = this

    // 2.将 thisArg 转成对象类型(防止它传入的是非对象类型,例如123数字)
    thisArg = (thisArg !== null && thisArg !== undefined) ? Object(thisArg) : window

    // 3.使用 thisArg 调用函数,绑定 this
    // 评论区大佬提出的改进方法,用 Symbol 创建一个独一无二的属性,避免属性覆盖或冲突的情况
    let fn = Symbol()
    thisArg[fn] = this
    // thisArg.fn = fn
    let result = thisArg[fn](...args)
    delete thisArg[fn]

    // 4.返回结果
    return result
}


Function.prototype._apply = function(thisArg,argArray){
    // 1.获取需要执行的函数
    let fn = this

    // 2.将 thisArg 转成对象类型(防止它传入的是非对象类型,例如123数字)
    thisArg = (thisArg !== null && thisArg !== undefined) ? Object(thisArg) : window
    // 判断一些边界情况
    argArray = argArray || []

    // 3.使用 thisArg 调用函数,绑定 this
    thisArg.fn = fn
    // 将传递过来的数组(可迭代对象)拆分,传给函数
    let result = thisArg.fn(...argArray)
    delete thisArg.fn

    // 4.返回结果
    return result
}


Function.prototype._call = function(thisArg,...args){
    let fn = this
    thisArg = (thisArg !== null && thisArg !== undefined) ? Object(thisArg) : window

    thisArg.fn = fn
    let result = thisArg.fn(...args)
    delete thisArg.fn

    return result
}

// 利用 call 模拟 bind
Function.prototype._bind = function(thisArg,...args){
    let fn = this // 需要调用的那个函数的引用
    // bind 需要返回一个函数
    return function(){
        return fn._call(thisArg, ...args)
    }
}

十、手写AJAX请求

function ajax(url) {
    // 创建一个 XHR 对象
    return new Promise((resolve,reject) => {
        const xhr = new XMLHttpRequest()
        // 指定请求类型,请求URL,和是否异步
        xhr.open('GET', url, true)
        xhr.onreadystatechange = funtion() {
          // 表明数据已就绪
            if(xhr.readyState === 4) {
                if(xhr.status === 200){
                    // 回调
                    resolve(JSON.stringify(xhr.responseText))
                }
                else{
                    reject('error')
                }
            }
        }
        // 发送定义好的请求
        xhr.send(null)
    })
}

十一、手写数组去重

// 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
    })
}

// 4.创建一个新数组,如果之前没加入就加入
function unique4(array){
    let res = []
    array.forEach(item => {
        if(res.indexOf(item) === -1){
            res.push(item)
        }
    })
    return res
}


进阶:如果数组内有数组和对象,应该怎么去重(此时对象的地址不同,用Set去不了重)

方法:

十二、手写数组扁平

// 方法1-3:递归
function flat1(array){
    // reduce(): 对数组的每一项执行归并函数,这个归并函数的返回值会作为下一次调用时的参数,即 preValue
    // concat(): 合并两个数组,并返回一个新数组
    return array.reduce((preValue,curItem) => {
        return preValue.concat(Array.isArray(curItem) ? flat1(curItem) : curItem)
    },[])
}

function flat2(array){
    let res = []
    array.forEach(item => {
        if(Array.isArray(item)){
            // res.push(...flat2(item))
        // 如果遇到一个数组,递归
            res = res.concat(flat2(item))
        }
        else{
            res.push(item)
        }
    })
    return res
}

function flat3(array){
    // some(): 对数组的每一项都运行传入的函数,如果有一项返回 TRUE,则这个方法返回 TRUE
    while(array.some(item => Array.isArray(item))){
    // ES6 增加了扩展运算符,用于取出参数对象的所有可遍历属性,拷贝到当前对象之中:
        array = [].concat(...array)
        console.log(...array)
    }
    return array
}

// 方法4、5:先转成字符串,再变回数组
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))
}
另附:手写对象扁平化

十三、手写数组乱序

// 方法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;
}

十四、手写 Promise.all()、Promise.race()、Promise.on() 等其他promise方法

function myAll(promises){
    // 问题关键:什么时候要执行resolve,什么时候要执行 reject
    return new Promise((resolve,reject) => {
        results = []
        // 迭代数组中的 Promise,将每个 promise 的结果保存到一个数组里
        let counter = 0
        for (let i = 0; i < promises.length; i++) {
            // 如果不是 Promise 类型要先包装一下
            // 调用 then 得到结果
            Promise.resolve(promises[i]).then(res => {
                // 这里不能用 push(),因为需要保证结果的顺序。感谢评论区大佬的批评指正
                results[i] = res
                counter++
                // 如果全部成功,状态变为 fulfilled
                if(counter === promises.length){
                    resolve(results)
                }
                },err => { // 如果出现了 rejected 状态,则调用 reject() 返回结果
                reject(err)
            })
        }
    }
)
}

// test
let p1 = new Promise(function (resolve, reject) {
    setTimeout(function () {
        resolve(1)
    }, 1000)
})
let p2 = new Promise(function (resolve, reject) {
    setTimeout(function () {
        resolve(2)
    }, 2000)
})
let p3 = new Promise(function (resolve, reject) {
    setTimeout(function () {
        resolve(3)
    }, 3000)
})
myAll([p3, p1, p2]).then(res => {
    console.log(res) // [3, 1, 2]
})
// 哪个 promise 状态先确定,就返回它的结果
function myRace(promises) {
    return new Promise((resolve, reject) => {
        promises.forEach(promise => {
            Promise.resolve(promise).then(res => {
                resolve(res)
            }, err => {
                reject(err)
            })
        })
    })
}
//
function myOn(promises){

}
//
function myAllsetter(promises){

}

十五、手撕快排

PS: 常见的排序算法,像冒泡,选择,插入排序这些最好也背一下,堆排序归并排序能写则写。万一考到了呢,要是写不出就直接回去等通知了

const _quickSort = array => {
    // 补全代码
    quickSort(array, 0, array.length - 1)
    // 别忘了返回数组
    return array
}

const quickSort = (array, start, end) => {
    // 注意递归边界条件
    if(end - start < 1)    return
    // 取第一个数作为基准
    const base = array[start]
    let left = start
    let right = end
    while(left < right){
        // 从右往左找小于基准元素的数,并赋值给右指针 array[right]
        while(left < right &&  array[right] >= base)    right--
        array[left] = array[right]
        // 从左往右找大于基准元素的数,并赋值给左指针 array[left]
        while(left < right && array[left] <= base)    left++
        array[right] = array[left]
    }
    // 双指针重合处,将基准元素填到这个位置。基准元素已经事先保存下来了,因此不用担心上面的赋值操作会覆盖掉基准元素的值
    // array[left] 位置已经确定,左边的都比它小,右边的都比它大
    array[left] = base
    quickSort(array, start, left - 1)
    quickSort(array, left + 1, end)
    return array
}

十六、手写 JSONP

// 动态的加载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});

十七、手写寄生组合继承

function Parent(name) {
  this.name = name;
  this.say = () => {
    console.log(111);
  };
}
Parent.prototype.play = () => {
  console.log(222);
};
function Children(name,age) {
  Parent.call(this,name);
  this.age = age
}
Children.prototype = Object.create(Parent.prototype);
Children.prototype.constructor = Children;

十八、手写二分查找

//  迭代版
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);
    }
};

十九、手写函数柯里化

function sum(x,y,z) {
    return x + y + z
}

function hyCurrying(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 = hyCurry(sum)

curryAdd(10,20,30)
curryAdd(10,20)(30)
curryAdd(10)(20)(30)

二十、CSS水平垂直居中

  • 不定宽高div,实现子元素垂直、水平居中,三种以上3
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Document</title>
</head>
<body>
    <div class="father">
        <div class="son"></div>
    </div>
</body>

<style>
/* 居中元素(子元素)的宽高已知 */

/* 利用定位+margin:负值 */




/* ------------------------------------------------------------------------------------------------------------------ */
/* 居中元素宽高未知     */

/* 利用定位+margin:auto   父级设置为相对定位,子级绝对定位 四个定位属性的值都设置了0,margin:auto*/
    /* .father{
            width:500px;
            height:300px;
            border:1px solid #0a3b98;
            position: relative;
        }
    .son{
        width:300px;
        height:40px;
        background: #f0a238;
        position: absolute;
        top:0;
        left:0;
        right:0;
        bottom:0;
        margin:auto;
    } */
/* ------------------------------------------------------------------------------------------------------------------ */
/* 利用定位+transform */
    /* 需要知道自身宽度移动,可以使用transform */
    /* .father{
        position: relative;
        width:500px;
        height:300px;
        border: 1px solid #0a3b98;
    }
    .son{
        position: absolute;
        top:50%;
        left:50%;
        
        transform:translate(-50%,-50%);
        width:100px;
        height:100px;
        background: red;
    } */
/* ------------------------------------------------------------------------------------------------------------------ */
/* table布局 */

/* 设置父元素为display:table-cell,
子元素设置 display: inline-block。
利用vertical和text-align可以让所有的行内块级元素水平垂直居中 */
    /* .father{
        width:500px;
        height:300px;
        border:1px solid #0a3b98;
        display: table-cell;

        text-align: center;
        vertical-align: middle;
    }
    .son{
        display: inline-block;
        width: 300px;
        height: 100px;
        background: red;
    } */
/* ------------------------------------------------------------------------------------------------------------------ */
/* flex布局 */
/* display: flex时,表示该容器内部的元素将按照flex进行布局

align-items: center表示这些元素将相对于本容器水平居中

justify-content: center也是同样的道理垂直居中 */

    /* .father{
        display: flex;
        justify-content: center;
        align-items: center;
        width:500px;
        height:300px;
        border:1px solid #0a3b98;
    }
    .son{
        width: 300px;
        height: 100px;
        background: red;        
    } */
/* ------------------------------------------------------------------------------------------------------------------ */
/* grid布局 */
    .father{
        display: grid;
        justify-content: center;
        align-items: center;
        width: 500px;
        height: 300px;
        border: 1px solid #0a3b98;
    }
    .son{
        width: 300px;
        height: 100px;
        background: red;   
    }


</style>



</html>

二十一、CSS画三角形

<!DOCTYPE html>
<html lang="en">
<head>
    <!-- Use correct character set. -->
    <meta charset="utf-8" />
    <!-- Tell IE to use the latest, best version. -->
    <meta http-equiv="X-UA-Compatible" content="IE=edge" />
    <!-- Make the application on mobile take up the full browser screen and disable user scaling. -->
    <meta
            name="viewport"
            content="width=device-width, initial-scale=1, maximum-scale=1, minimum-scale=1, user-scalable=no"
    />
    <title>三角形</title>
    <style>
        .border {
            width:0px;
            height:0px;
            border:50px solid;
            border-color: #96ceb4 #ffeead #d9534f #ffad60;
        }

        /* 三角形 */
        .border1 {
            width:0px;
            height:0px;
            border-style:solid;
            border-width:0 50px 50px;
            border-color: transparent transparent #d9534f;
        }


        /* 利用伪类定位一个小一点的三角形 */
        .border2 {
            width: 0;
            height: 0;
            border-style:solid;
            border-width: 0 50px 50px;
            border-color: transparent transparent #d9534f;
            position: relative;
        }
        /* 微调一下位置top、left */
        .border2:after{
            content: '';
            border-style:solid;
            border-width: 0 40px 40px;
            border-color: transparent transparent #ffffff;
            position: absolute;
            top: 6px;    
            left: -40px;
        }     

    </style>
</head>



<body>
<div class="border">
</div>
<div class="border1">
</div>
<div class="border2">
</div>
</body>



<script type="module">

</script>
</html>

二十二、CSS实现两栏和三栏布局

两栏布局
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Document</title>
</head>

<body>
    <div class="box">
        <div class="left">左边</div>
        <div class="right">右边</div>
    </div>    
</body>


<style>
/* 使用 float 左浮左边栏
右边模块使用 margin-left 撑出内容块做内容展示
为父级元素添加BFC,防止下方元素飞到上方内容 */
    /* .box{
        overflow: hidden;
    }
    .left{
        float: left;
        width: 200px;
        background-color: gray;
        height: 400px;
    }
    .right{
        margin-left: 210px;
        background-color: lightgray;
        height: 400px;
    } */
/* ------------------------------------------------------------------------------------------------------- */
/* flex弹性布局 */
/* flex容器的一个默认属性值:align-items: stretch;列等高 */
/* flex属性是flex-grow, flex-shrink 和 flex-basis的简写,默认值为0 1 auto,也是比较难懂的一个复合属性 */
    .box{
        display: flex;
    }
    .left{
        width: 200px;
        background-color: gray;
        height: 400px;
    }
    .right{
        flex:1;
        background-color: lightgray;
    }
</style>

</html>
三栏布局
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Document</title>
</head>
<body>
    <div class="box">
        <div class="left">左</div>
        <div class="middle">中</div>
        <div class="right">右</div>
    </div>
</body>

<style>
/* 两边使用 float,中间使用 margin */
/* 两边固定宽度,中间宽度自适应。
利用中间元素的margin值控制两边的间距
宽度小于左右部分宽度之和时,右侧部分会被挤下去 */


/* .box{
    background-color: #eee;
    overflow: hidden;  
    padding: 20px;
    height: 400px;
}
.left{
    width: 200px;
    height: 400px;
    float: left;
    background: coral;
}
.right{
    width: 120px;
    height: 400px;
    float: right;
    background: rgb(10, 148, 136);
}
.middle{
    margin-left: 220px;
    height: 400px;
    background: rgb(26, 132, 168);
    margin-right: 140px;
} */



/* 两边使用 absolute,中间使用 margin */
/* 基于绝对定位的三栏布局:注意绝对定位的元素脱离文档流,相对于最近的已经定位的祖先元素进行定位。无需考虑HTML中结构的顺序 */
/* .box{
    position: relative;
} */

/* 设置字体居中 */
/* .left,
.right,
.middle{
    height:200px;
    line-height:200px;
    text-align:center;
}

.left {
    position: absolute;
    top: 0;
    left: 0;
    width: 100px;
    background: green;
  }

.right {
    position: absolute;
    top: 0;
    right: 0;
    width: 100px;
    background: green;
}
.middle{
    margin: 0 110px;
    background:black;
    color:white

} */


/* 两边使用 float 和负 margin */




/* display: table 实现 */
/* <table> 标签用于展示行列数据,不适合用于布局。但是可以使用 display: table 来实现布局的效果 */
/* .box{
    height: 200px;
    line-height: 200px;
    text-align: center;
    display: table;
    table-layout: fixed;
    width: 100%;
}

.left,
.right,
.middle{
    display: table-cell;
}

.left,
.right{
    width:100px;
    background: green;
}

.middle{
    background: black;
    color: white;
    width: 100%;
} */
/* 
层通过 display: table设置为表格,设置 table-layout: fixed`表示列宽自身宽度决定,而不是自动计算。
内层的左中右通过 display: table-cell设置为表格单元。
左右设置固定宽度,中间设置 width: 100% 填充剩下的宽度 
*/



/* flex实现 */
/* .box{
    display: flex;
    justify-content: space-between;
    line-height: 100px;
    text-align: center;
}

.left,
.right,
.middle{
    height:100px
}

.left{
    width: 200px;
    background: coral;
}

.right{
    width: 120px;
    background: lightblue;
}

.middle{
    background: black;
    width: 100%;
    margin: 0 20px
} */



/* grid网格布局 */
/* grid-template-columns: 300px auto 300px;来设置每一列的宽度 */
.box{
    display: grid;
    text-align: center;
    line-height: 200px;
    width: 100%;
    grid-template-columns: 200px auto 120px;
}

.left,
.right,
.middle{
    height: 200px;
}

.left{
    background: coral;
}
.right{

    background: lightblue;
}

.middle{
    background: lightgray;
}

</style>



</html>

二十三、手写发布-订阅模式 || EventEmitter

on

发布和订阅
发布和订阅。在type上添加订阅者

emit

执行该订阅下的所有函数
执行该订阅下的所有函数。遍历type下的订阅者,执行。

off

取消某个函数的订阅
取消某个函数的订阅。在订阅者中找到fn然后删除

once
只执行一次订阅事件
once方法将handler函数挂载了type这个发布者上,如果执行emit就会执行handler函数中的内容,会先删除type上的所有的函数,然后执行fn。

class EventBus {
  constructor() {
    this.tasks = {}; // 按事件名称创建任务队列
  }

  /**
   * 注册事件(订阅)
   * @param {String} type  事件名称
   * @param {Function} fn  回调函数
   */
  on(type, fn) {
    // 如果还没有注册过该事件,则创建对应事件的队列
    if (!this.tasks[type]) {
      this.tasks[type] = [];
    }
    // 将回调函数加入队列
    this.tasks[type].push(fn);
  }
  
  /**
   * 注册一个只能执行一次的事件
   * @params type[String] 事件类型
   * @params fn[Function] 回调函数
   */
  once(type, fn) {
    if (!this.tasks[type]) {
      this.tasks[type] = [];
    }

    const that = this;
    // 注意该函数必须是具名函数,因为需要删除,但该名称只在函数内部有效
    function _once(...args) {
      fn(...args);
      that.off(type, _once); // 执行一次后注销
    }

    this.tasks[type].push(_once);
  }

  /**
   * 触发事件(发布)
   * @param {String} type  事件名称
   * @param {...any} args  传入的参数,不限个数
   */
  emit(type, ...args) {
    // 如果该事件没有被注册,则返回
    if (!this.tasks[type]) {
      return;
    }
    // 遍历执行对应的回调数组,并传入参数
    this.tasks[type].forEach((fn) => fn(...args));
  }

  /**
   * 移除指定回调(取消订阅)
   * @param {String} type  事件名称
   * @param {Function} fn  回调函数
   */
  off(type, fn) {
    const tasks = this.tasks[type];
    // 校验事件队列是否存在
    if (!Array.isArray(tasks)) {
      return;
    }

    // 利用 filter 删除队列中的指定函数
    this.tasks[type] = tasks.filter((cb) => fn !== cb);
  }
}


另附:手写观察者

Subject是构造函数,new Subject() 创建一个主题对象,该对象内部维护订阅当前主题的观察者数组。主题对象上有一些方法,如添加观察者(addObserver)、删除观察者(removeObserver)、通知观察者更新(notify)。 当notify 时实际上调用全部观察者 observer 自身的 update 方法。

Observer 是构造函数,new Observer() 创建一个观察者对象,该对象有一个 update 方法,观察者可以订阅主题,实际上是把自己加入到主题的订阅者列表里。

class Subject{
    
    observers = [];
    
    addObserver(observer){
        this.observers.push(observer)
    }
    removeObserver(observer){
        let index = this.observers.indexOf(observer)
        if(index>-1){
            this.observers.splice(index,1)
        }
    }
    notify(){
        this.observers.forEach(observer=>{
            observer.update()
        })
    }

}

class Observer{
    update(){
        subscribeTo(subject){
            dubject.addObserver(this)
        }
    }
}


//测试代码
let subject = new Subject()
let observer = new Observer()
observer.update = function(){
    console.log('observer update')
}
observer.subscribeTo(subject)  //观察者订阅主题
subject.notify()

二十四、手写轮播图

1、初级轮播图功能介绍:①左右两端有左右按钮;②下方有小球指示当前是第几张图片;③无切换效果;④如果两秒中用户没有点击轮播图,则从左到右自动播放。

HTML中需要包括一个大盒子class=wrap,为轮播图的盒子。一张一张的图片可以用无序列表存储,左右按钮使用button,下方圆点也用无序列表,并为每一个圆点设置计数器data-index。HTML的代码如下:

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <script src="animate.js"></script>
    <title>Document</title>
    <style>
        //CSS中,给wrap盒子一个宽高。list盒子和它同宽同高。每一张图片充满盒子,并且都用绝对定位固定在wrap盒子里,让他们有不同的颜色,初始透明度都是0即全透明,并且,哪个需要展示,哪个的z-index就变大,并且透明度改为1。左右按钮直接使用定位固定在左右两端,小圆点内部使用浮动,再用定位固定在下端。
        * {
            margin: 0;
            padding: 0;
            list-style: none;
        }
        .wrap {
            width: 800px;
            height: 400px;
            background-color: pink;
            position: relative;
            overflow: hidden;
        }
        .list{
            width: 600%;
            height: 400px;
            position: absolute;
            left:0;
        }
        .item {
            width: 800px;
            height: 100%;
            float: left;
        }
        .item:nth-child(1){
            background-color: skyblue;
        }
        .item:nth-child(2){
            background-color: yellowgreen
        }
        .item:nth-child(3){
            background-color: rebeccapurple;
        }
        .item:nth-child(4){
            background-color: pink;
        }
        .item:nth-child(5){
            background-color: orange;
        }
        .item:nth-child(6){
            background-color: skyblue;
        }
        /* .item.active {
            opacity: 1;
            z-index: 20;
        } */
        .btn {
            width: 50px;
            height: 100px;
            position: absolute;
            top: 50%;
            transform:translate(0,-50%);
            z-index: 200;
            display: none;
        }
        #leftBtn {
            left: 0;
        }
        #rightBtn {
            right: 0;
        }

        .pointList {
            height: 10px;
            position: absolute;
            bottom: 20px;
            right: 20px;
            z-index: 200;
        }
        .point {
            width: 10px;
            height: 10px;
            background-color: antiquewhite;
            float: left;
            margin-left: 8px;
            border-style: solid;
            border-radius: 100%;
            border-width: 2px;
            border-color: slategray;
        }
        .point.active {
            background-color: cadetblue;
        }
    </style>
</head>
<body>
    <div class="wrap">
        <ul class="list">
            <li class="item">0</li>
            <li class="item">1</li>
            <li class="item">2</li>
            <li class="item">3</li>
            <li class="item">4</li>
        </ul>
        <ul class="pointList">
            <li class="point active" data-index="0"></li>
            <li class="point" data-index="1"></li>
            <li class="point" data-index="2"></li>
            <li class="point" data-index="3"></li>
            <li class="point" data-index="4"></li>
        </ul>
        <button class="btn" id="leftBtn"><</button>
        <button class="btn" id="rightBtn">></button>
    </div>
    <script>
        /* 1.获取元素 */
        // 整个轮播图范围
        let wrap = document.querySelector('.wrap')
        let ul = document.querySelector('.list')
        // 轮播图图片
        let items = document.querySelectorAll('.item')
        // 下方圆点
        let points = document.querySelectorAll('.point')
        // 左右按钮
        let left = document.querySelector('#leftBtn')
        let right = document.querySelector('#rightBtn')
        var focusWidth = wrap.offsetWidth;
        /* 2.鼠标经过轮播图,左右按钮出现,离开则按钮消失 */
        wrap.addEventListener('mouseenter',function(){
            left.style.display = 'block'
            right.style.display = 'block'
        });
        wrap.addEventListener('mouseleave',function(){
            left.style.display = 'none'
            right.style.display = 'none'
        })
         /* 3.克隆第一张图片放到ul最后面 */
         var first = ul.children[0].cloneNode(true)
         ul.appendChild(first)
         items = document.querySelectorAll('.item')

        /* 4. 记录当前展示的是第几张图片 */
        var index = 0;
        /* 5.移除所有的active */
        var removeActive = function(){
            for(var i=0;i<items.length;i++){
                items[i].className = "item"
            }
            for(var i=0;i<points.length;i++){
                points[i].className = "point"
            }
        }
        /* 6.为当前index加入active */
        var setActive = function(){
            removeActive();
            // ul.style.left = -(index*focusWidth) + 'px'
            animate(ul, -index * focusWidth);
            // console.log(index);
            // console.log(ul.style.left);
            if(index==5) {
                points[0].className = "point active";
            }else{
                points[index].className = "point active";
            }
        }
        /* 7.点击左右按钮触发修改index的事件 */
        var goleft = function(){
            if(index==0){
                index = 5;
                ul.style.left = "-4000px";
            }
            index--;
            setActive();
        }
        var goright = function(){
            if(index==5){
                index = 0;
                ul.style.left = 0;
            }
            index++;
            setActive();
        }        

        left.addEventListener('click',function(){
            goleft();
        })
        right.addEventListener('click',function(){
            goright();
        })
        /* 8.点击圆点更改轮播图 */
        for(var i=0;i<points.length;i++){
            points[i].addEventListener('click',function(){
                var pointIndex = this.getAttribute('data-index')
                index = pointIndex;
                setActive();
            })
        }
        /* 9.计时器 */
        var timer
        function play() {
            timer = setInterval(() => {
                goright()
            }, 2000)
        }
        play()
        //移入清除计时器r
        wrap.onmouseover = function () {
            clearInterval(timer)
        }
        //移出启动计时器
        wrap.onmouseleave = function () {
            play()
        }
        
    </script>
</body>
</html>

二十五、实现一个圆绕着中心一直转

<!doctype html>
<html lang="en">
<head>
    <meta charset="UTF-8" />
    <meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1" />
    <title> 页面名称 </title>
    <style type="text/css">
        @keyframes animX{
            0% {left: 0px;}
            100% {left: 500px;}
        }
        @keyframes animY{
            0% {top: 0px;}
            100% {top: 500px;}
        }

        #ball {
            width: 100px;
            height: 100px;
            background-color: #f99;
            border-radius: 50%;
            position: absolute;
            animation:animX 4s ease-in-out -2s infinite alternate, animY 4s ease-in-out infinite alternate;
        }
    </style>
</head>
<body>
<div id="ball"></div>
</body>
</html>

二十六、手写简单轮询

轮询请求就是间隔相同的时间(如5s)后不断地向服务端发起同一个接口的请求,当然不能无限次去请求,所以轮询必须要有个停止轮询的机制。

首先能想到的就是用setInterval去实现,但是setInterval做轮询的话,会有以下严重的问题

1. 即使调用的代码报错了,setInterval会持续的调用
2. setInterval不会等到上一次的请求响应后再执行,只要到了指定的时间间隔就会执行,哪怕有报错也会执行
3. setInterval定时不准确,这个跟事件循环有关,这里不展开啦~
function setTimer () {
         let timer
        axios.post(url, params)
        .then(function (res) {
            if(res){
                console.log(res);
                timer = setTimeout(() => {
                    this.setTimer()
                }, 5000)       
            }else {
                clearTimeout(timer) //清理定时任务
            }
            
        })
        .catch(function (error) {
                console.log(error);
        });
},           

二十七、使用闭包实现数据缓存

/*
* 使用立即执行函数
* */
var cacheConfig = (function(){
    var obj={};
    return {
        setCache:function(k,v){
            obj[k]=v;
        },
        getCache:function(k){
            return obj[k];
        }
    }
})();

场景题、

1.实现sleep休眠函数

function Sleep(time){
	return new Promise(resolve=>{
		setTimeout(resolve, time);
	})
}

(async function(){
	console.log('start sleep in '+ new Date());
	await Sleep(7000);
	console.log('end sleep in ' + new Date());
})()

2.setTimeout 实现 setInterval

function setInterval(fn, time){
    var interval = function(){
    // time时间过去,这个异步被执行,而内部执行的函数正是interval,就相当于进了一个循环
        setTimeout(interval, time);
    // 同步代码
        fn();
    }
    //interval被延迟time时间执行
    setTimeout(interval,time); 
}

3.异步循环打印 1,2,3

var sleep = function (time, i) {
  return new Promise(function (resolve, reject) {
    setTimeout(function () {
      resolve(i);
    }, time);
  })
};


var start = async function () {
  for (let i = 1; i <= 3; i++) {
    let result = await sleep(1000, i);
    console.log(result);
  }
};

start();

4.循环打印红、黄、绿

function red() {
    console.log('red');
}
function green() {
    console.log('green');
}
function yellow() {
    console.log('yellow');
}

const task = (timer, light) => {
    new Promise((resolve, reject) => {
        setTimeout(() => {
            if (light === 'red') {
                red()
            }
            else if (light === 'green') {
                green()
            }
            else if (light === 'yellow') {
                yellow()
            }
            resolve()
        }, timer)
    })
}
const taskRunner =  async () => {
    await task(3000, 'red')
    await task(2000, 'green')
    await task(2100, 'yellow')
    taskRunner()
}
taskRunner()

中频

1.手写事件委托

function delegateEvent(parent,selector,type,fn){}

parent:事件绑定的父级

selector:选择器

type:事件类型

handle:事件处理函数

写之前先了解一下事件委托的概念:
它是通过事件冒泡机制,
即子元素上触发的事件会冒泡到父级上,即父级也会触发该类型的事件,
通过父级触发的事件拿到事件源对象e.target就可以知道真正触发事件的元素是什么。

举个例子,一个ul下面有1000个li,我们要给li绑定点击事件,如果给每个li都绑定一个点击事件会大量占用内存,不利于性能优化,这种情况下我们只需要在ul上绑定一个点击事件,通过class或者id来识别每个li,这样就大大减少了事件注册,而且再有新增的li时我们也无需再去注册点击事件

功能:点击li,哪个变成绿色,再次点击取消

HTML

<ul id="parent">
	  <li>1</li>
	  <li>1</li>
	  <li>1</li>
</ul>

CSS

.active{
	background-color:green;
}

JS

const parent = document.getElementById('parent')
function changeColor(){
	if(this.classList.contains('active')){
		this.classList.remove('active')
	}else{
		this.classList.add('active');
	}
}
delegateEvent(parent,'li','click',changeColor)

事件委托函数

// ============ 简单的事件委托
function  delegateEvent(parent, selector, type, fn) {
	 
     
     if (parent.addEventListener){
     parent.addEventListener(type, eventfn,false);
     } else {//兼容老IE
     parent.attachEvent( "on" + type, eventfn);
     }
     
     function  eventfn(e){
         //兼容老IE
         const  e = e || window.event;    
         //兼容老IE
         const  target = e.target || e.srcElement;
        //如果目标元素与选择器匹配则执行函数
         if  (matchSelector(target, selector)) {
                 if (fn) {//将fn内部的this指向target(在此之前this都是指向的绑定事件的元素即interfaceEle)
                     fn.call(target, e); 
                 }
         }
     }
    
    
}


/**
  * only support #id, tagName, .className
  * and it's simple single, no combination
  */
//比较函数:判断事件的作用目标是否与选择器匹配;匹配则返回true
function  matchSelector(ele, selector) {
     // 如果选择器为ID
     if  (selector.charAt(0) ===  "#" ) {            
         return  ele.id === selector.slice(1);   
     }
      //charAt(0),返回索引为0的字符
    //slice(a,b),从已有的数组或字符串返回从索引从a处开始,截取到索引b之前的子数组或子字符串;
    
     //如果选择器为Class
     if  (selector.charAt(0) ===  "." ) {
         return  ( " "  + ele.className +  " " ).indexOf( " "  + selector.slice(1) +  " " ) != -1;
     }
     // 如果选择器为tagName
     return  ele.tagName.toLowerCase() === selector.toLowerCase();
}
//toLowerCase()将字符串转换成小写

2.DOM操作相关题目

Q1:有以下页面内容,请实现点击任一LI标签时在控制台输出或alert其index (仅限原生API,不考虑兼容性问题),并使性能最优

<ul id="list">
  <li>点击我弹出0</li>
  <li>点击我弹出1</li>
  <li>点击我弹出2</li>
  ...(此处省略N个相同的LI)
</ul>

解析

暴力方法:给每个li元素注册点击事件,打印遍历的索引

  • 使用for循环声明索引时需注意它的使用方式 var or let

优化性能:使用事件委托方式,只给ul注册点击事件,判断点击li元素时,打印索引

  • 事件委托参考 事件阶段和委托代理

  • document APIs获取 dom 元素对象,存储的是 DOM 的指向,所以可以直接匹配DOM对象是否相同,或用indexOf 获取索引。

// 遍历注册事件 - var
var lis = document.querySelectorAll('#list li')

for (var i = 0; i < lis.length; i++) {
  // var 声明的变量没有块作用域,所以不能直接打印
  (function(i) {
    lis[i].addEventListener('click', () => {
      console.log(i)
    })
  })(i)
}

// 遍历注册事件 - let
const lis = document.querySelectorAll('#list li')

for (let i = 0; i < lis.length; i++) {
  // let 声明的变量有块作用域,可以直接打印
  lis[i].addEventListener('click', () => {
    console.log(i)
  })
}
// 事件委托
const list = document.querySelector('#list')

list.addEventListener('click', (e) => {
  let index = Array.from(list.children).indexOf(e.target)
  console.log(index)
})

3.手写列表转树

用一个递归方法

function arrayToTree(arr) {
	let nodeparent = arr[0]
	let tree = {
		id : nodeparent.id,
		val : nodeparent.val,
		children : arr.length > 0 ?  totree(nodeparent.id, arr) : []
	}
	return tree
}

function totree(parentid, arr) {
	let children = []
	for (let i=0; i < arr.length; i++) {
		let node = arr[i]
		if (node.parentId === parentid) {
			children.push({
				id: node.id,
				val: node.val,
				children: totree(node.id, arr)
			})
		}
	}
	return children
}


let input = [
    {
        id: 1, val: '学校', parentId: null
    }, {
        id: 2, val: '班级1', parentId: 1
    }, {
        id: 3, val: '班级2', parentId: 1
    }, {
        id: 4, val: '学生1', parentId: 2
    }, {
        id: 5, val: '学生2', parentId: 2
    }, {
        id: 6, val: '学生3', parentId: 3
    },
]
console.log(arrayToTree(input))

4.日期时间格式化

方法

//格式化时间
function format(dat){
    //获取年月日,时间
    var year = dat.getFullYear();
    var mon = (dat.getMonth()+1) < 10 ? "0"+(dat.getMonth()+1) : dat.getMonth()+1;
    var data = dat.getDate()  < 10 ? "0"+(dat.getDate()) : dat.getDate();
    var hour = dat.getHours()  < 10 ? "0"+(dat.getHours()) : dat.getHours();
    var min =  dat.getMinutes()  < 10 ? "0"+(dat.getMinutes()) : dat.getMinutes();
    var seon = dat.getSeconds() < 10 ? "0"+(dat.getSeconds()) : dat.getSeconds();
				
    var newDate = year +"-"+ mon +"-"+ data +" "+ hour +":"+ min +":"+ seon;
    return newDate;
}

调用

//获取时间
var dat = new Date();	
//格式化时间
var newDate = format(dat);

5.数字千分位

1.最最便捷的实现方式:toLocaleString()

注:只针对数字格式有效!

let num = 1234567890;
num.toLocaleString(); //"1,234,567,890"

2.正则匹配

// 正则匹配方法一
let num = 1234567890;
let reg = /\d{1,3}(?=(\d{3})+$)/g;   
String(num).replace(reg, '$&,'); //"1,234,567,890"
// 正则匹配方法二
let num = 1234567890;
let reg = /\B(?=(\d{3})+$)/g;   
String(num).replace(reg, ','); //"1,234,567,890"

说明:如果想知道具体怎样的分组方式,可在 [https://regexper.com/](https://regexper.com/) 上测试
1. ?= 表示正向引用
2. $& 表示与正则表达式相匹配的内容,可查看replace()
3. \B 非单词边界

3.for循环

// for循环方法一
function format(num){  
  num = String(num);//数字转字符串  
  let str = '';//字符串累加  
  for (let i = num.length- 1, j = 1; i >= 0; i--, j++){  
      if (j%3 == 0 && i != 0){ //每隔三位加逗号,过滤正好在第一个数字的情况  
          str += num[i] + ','; //加千分位逗号  
          continue;  
      }  
      str += num[i]; //倒着累加数字
  }  
  return str.split('').reverse().join(""); //字符串=>数组=>反转=>字符串  
} 
let num = 1234567890;
format(num); //"1,234,567,890"
// for循环方法二
function format(num){  
  num = String(num);//数字转字符串
  let str = '';//字符串累加
  for (let i = num.length- 1, j = 1; i >= 0; i--, j++){  
      if (j%3 == 0 && i != 0){ //每隔三位加逗号,过滤正好在第一个数字的情况
          str = ',' + num[i] + str; //加千分位逗号
         continue; 
      }  
      str = num[i] + str; //累加数字
  }  
  return str;
}
let num = 1234567890; 
format(num); //"1,234,567,890"

4.slice+while循环

function format(num) {
  let arr = [],
      str = String(num),
      count = str.length;
  while (count >= 3) {
    arr.unshift(str.slice(count - 3, count));
    count -= 3;
  }
  // 如果是不是3的倍数就另外追加到上去
  if(str.length % 3) arr.unshift(str.slice(0, str.length % 3));
  return arr.toString();
}
let num = 1234567890; 
format(num); //"1,234,567,890"

5.reduce

function format(num) {
  var str = num+'';
  return str.split("").reverse().reduce((prev, next, index) => {
    return ((index % 3) ? next : (next + ',')) + prev;
  })
}
let num = 1234567890; 
format(num); //"1,234,567,890"
另附:金额千分位
/**
* 金额千分位分割格式函数
* @param {Number|String} vlaue 需要转化的金额字符串
*/
function formatAmount(value) {
	//传入值不是数字直接返回0
	if (!value) return '0.00'
    const values = value.toString().split('.')
    // 整数部分
    let integerNum = values[0]
    // 小数部分
    let decimalNum = values[1] ? values[1] : '00'
    decimalNum = decimalNum.length === 1 ? decimalNum + 0 : decimalNum
    //传入值小于1000不切割
    if (integerNum < 1000) {
      return `${integerNum}.${decimalNum}`
    }
    const list = []
    while (integerNum.length > 3) {
       // 倒序切割
      list.unshift(integerNum.slice(-3))
      integerNum = integerNum.slice(0, -3)
    }
    // 处理剩余长度
    list.unshift(integerNum)
    return `${list.join(',')}.${decimalNum}`
 }

6.URL参数解析

字符串 split 方法

let URL = "http://www.baidu.com?name=小宇&age=25&sex=男&wife=小君";

function getUrlParams(url) {
  // 通过 ? 分割获取后面的参数字符串
  let urlStr = url.split(``'?'``)[1];
  // 创建空对象存储参数
  let obj = {};
  // 再通过 & 将每一个参数单独分割出来
  let paramsArr = urlStr.split('&');
    
  for  (let i = 0,len = paramsArr.length;i < len;i++){
    // 再通过 = 将每一个参数分割为 key:value 的形式
    let arr = paramsArr[i].split('=');
    obj[arr[0]] = arr[1];
  }
    
  return obj;
    
}
console.log(getUrlParams(URL));

使用 URLSearchParams 方法

1、解析搜索字符串

let url = 'https://www.baidu.com/s?ie=UTF-8&wd=%E8%AE%B8%E5%96%84%E7%A5%A5&p=1';
let urlStr = url.split('?')[1];
const params = new URLSearchParams(urlStr);
console.log(params.get('k'));   // 返回字符串“许善祥”,支持自动 UTF-8 解码
console.log(params.get('p'));   // 返回字符串“1”
console.log(params.get('xxx')); // 如果没有 xxx 这个键,则返回 null
console.log(params.has('xxx')); // 当然也可以通过 has() 方法查询是否存在指定的键
console.log(params.keys());     // 返回一个 ES6 Iterator,内含 ['k', 'p']
console.log(params.values());   // 返一个 ES6 Iterator,内含 ['许善祥', '1']
console.log(params.entries());  // 与 Map.prototype.entries() 类似

2、生成搜索字符串

const params = new URLSearchParams();
params.set('k', '许善祥');       // 设置参数
params.set('p', 1);             // 支持 Boolean、Number 等丰富类型
console.log(params.toString()); // k=%E8%AE%B8%E5%96%84%E7%A5%A5&p=1

使用正则匹配方法

正则匹配功能强大,不仅可以实现在登录注册时的账号、密码、邮箱、手机号等验证(看这里),还可以非常方便的处理一些字符串(校验、替换、提取等操作)。

let URL = 'http://www.baidu.com?name=祥&friend=宇';
function getUrlParams3(url){
    // \w+ 表示匹配至少一个(数字、字母及下划线),
    // [\u4e00-\u9fa5]+ 表示匹配至少一个中文字符
    let pattern = /(\w+|[\u4e00-\u9fa5]+)=(\w+|[\u4e00-\u9fa5]+)/ig;
    let result = {};
    url.replace(pattern, ($, $1, $2)=>{
        result[$1] = $2;
    });
    return result;
}
console.log(getUrlParams3(URL));
// {name: '祥', friend: '宇'}

7.大数相加

将每一次运算的结果保存到一个数组 result 中去,最终用 Array.prototype.join() 方法还原成一个数组。

这里为了保持循环的正常进行,我们需要保证两个字符串位数相等,所以我们要用 String.prototpye.padStart() 方法将位数比较小的那一个字符串的前面用 '0' 补齐。

按位相加有个问题就是进位如何保存,我的思路是这样的。当我们相加 num1[i] 和 num2[i] 的时候,得到的最多是一个两位数,它将影响 result 的两位,即当前的 result[0] 位置和即将 unshift 到 result 中的一位。当前的 result[0] 位置的数就是计算 [i -1] 是得到的数的高位(即进位),我们将我们计算的值加上进位,得到的数在分成两位分别放到 result 中。

所以总结一下就是我们计算 num1[i] + num2[i] 得到一个两位数,这个两位数要先和 num1[i-1] + num2[i-1] 的结果的进位(即 result[0] 相加,然后在分成 high 和 low 两位,将 result[0] 的值用 low 位替换,然后将 high 位 unshift 到 result 最前面。可以参考下图理解。

img

所以我们每次计算都是确定一位和下一位的进位。最后代码如下:

let add = function (num1, num2) {
 if (isNaN(num1) || isNaN(num2)) return '';
 if (num1 === '0' || num2 === '0') return (num1 === '0' ? num2 : num1);

 let len = Math.max(num1.length, num2.length);
 num1 = num1.padStart(len, '0');
 num2 = num2.padStart(len, '0');

 let result = [];

 for (let i = len - 1; i >= 0; i--) {
  let sum = Number(num1[i]) + Number(num2[i]) + (result[0] || 0);
  let low = sum % 10;
  let high = Math.floor(sum / 10);

  result[0] = low;
  result.unshift(high);
 }
 return result.join('');
}

console.log(add('10', '9007199254740991')) //09007199254741001

代码中我们加了两个判断,判断两个参数是否是合法数字格式,以及如果一个数是 '0' 则直接返回另一个数

另附大数相乘:

相乘的逻辑要比相加复杂一点,但是总体思路还是根据竖式来实现算法,我画了一张图,我们借助图来说明。

img

相乘是一个两层循环,我们要循环一个数的位,每一位再与另一个数循环的每一位相乘。我们每次相乘的结果最多是一个两位数。但是与相加不同的是,相加的 high 每次都是 unshift 进去即可,而相乘的高位也要与 result 的位进行运算。

我们来看一看相乘的规律,当我们用 num1[i] * num2[j] 的时候,可能得到两位数,也可能得到一位数,我们都统一算作两位数,高位没有的就用 0 补齐,那么最后我们得到的结果将是一个 i + j 位的数(开头可能存在补齐的 0)。而我们每次计算 num1[i] * num2[j] 的结果影响到的都是 result 中的 i + j 和 i + j + 1 位。

和加法中逻辑一样,我们将 num1[i] * num2[j] 的结果和 result[i + j + 1] 相加,得到的结果分为 low 和 high 分别存入 reslut 的 [i + j +1] 和 [i +j] 中。但是这里要注意,和加法不同,加法的高位直接存入就可以,我们这里的 high 对应的 result[i + j] 可能已经有值了,我们需要将已经存在的值加上。

high 和 result[i +j] 的相加可能存在进位怎么办呢,看上图中右边的当前 result 值中我们可以看到有些位存了不止一位数,我们将 high + result[i +j] 的值直接连进位一起保存到 result[i + j] 中。为什么能这样做呢,因为下次计算 num1[i] * num2[j - 1] 的时候(注意我们是从后往前遍历),会把 result[i + j]和 low 相加,进位自然能被处理,这也是这个算法比较重要的地方。

let multiply = function (num1, num2) {
  if (isNaN(num1) || isNaN(num2)) return '';
  if (num1 === '0' || num2 === '0') return '0';

  let l1 = num1.length,
    l2 = num2.length;

  let result = [];

  for (let i = l1 -1; i >= 0; i--) {
    for (let j = l2 - 1; j >= 0; j--) {
      let index1 = i + j;
      let index2 = i + j + 1;

      let product = num1[i] * num2[j] + (result[index2] || 0);
      result[index2] = product % 10;
      result[index1] = Math.floor(product / 10) + (result[index1] || 0);
    }
  }
  return result.join('').replace(/^0+/, '');
}

console.log(multiply('123', '234')) //28782

代码中加了两个判断:是否是合法数字,如果有一个值为 0 则直接返回 0。注意最后要判断得到的结果是否开头有 0,如果有则要去掉,这里用的正则表达式。

8.手写 Promise(进阶)

const PENDING = 'PENDING';
const FULFILLED = 'FULFILLED';
const REJECTED = 'REJECTED';

const resolvePromise = (promise2, x, resolve, reject) => {
  // 自己等待自己完成是错误的实现,用一个类型错误,结束掉 promise  Promise/A+ 2.3.1
  if (promise2 === x) { 
    return reject(new TypeError('Chaining cycle detected for promise #<Promise>'))
  }
  // Promise/A+ 2.3.3.3.3 只能调用一次
  let called;
  // 后续的条件要严格判断 保证代码能和别的库一起使用
  if ((typeof x === 'object' && x != null) || typeof x === 'function') { 
    try {
      // 为了判断 resolve 过的就不用再 reject 了(比如 reject 和 resolve 同时调用的时候)  Promise/A+ 2.3.3.1
      let then = x.then;
      if (typeof then === 'function') { 
        // 不要写成 x.then,直接 then.call 就可以了 因为 x.then 会再次取值,Object.defineProperty  Promise/A+ 2.3.3.3
        then.call(x, y => { // 根据 promise 的状态决定是成功还是失败
          if (called) return;
          called = true;
          // 递归解析的过程(因为可能 promise 中还有 promise) Promise/A+ 2.3.3.3.1
          resolvePromise(promise2, y, resolve, reject); 
        }, r => {
          // 只要失败就失败 Promise/A+ 2.3.3.3.2
          if (called) return;
          called = true;
          reject(r);
        });
      } else {
        // 如果 x.then 是个普通值就直接返回 resolve 作为结果  Promise/A+ 2.3.3.4
        resolve(x);
      }
    } catch (e) {
      // Promise/A+ 2.3.3.2
      if (called) return;
      called = true;
      reject(e)
    }
  } else {
    // 如果 x 是个普通值就直接返回 resolve 作为结果  Promise/A+ 2.3.4  
    resolve(x)
  }
}

class Promise {
  constructor(executor) {
    this.status = PENDING;
    this.value = undefined;
    this.reason = undefined;
    this.onResolvedCallbacks = [];
    this.onRejectedCallbacks= [];

    let resolve = (value) => {
      if(this.status ===  PENDING) {
        this.status = FULFILLED;
        this.value = value;
        this.onResolvedCallbacks.forEach(fn=>fn());
      }
    } 

    let reject = (reason) => {
      if(this.status ===  PENDING) {
        this.status = REJECTED;
        this.reason = reason;
        this.onRejectedCallbacks.forEach(fn=>fn());
      }
    }

    try {
      executor(resolve,reject)
    } catch (error) {
      reject(error)
    }
  }

  then(onFulfilled, onRejected) {
    //解决 onFufilled,onRejected 没有传值的问题
    //Promise/A+ 2.2.1 / Promise/A+ 2.2.5 / Promise/A+ 2.2.7.3 / Promise/A+ 2.2.7.4
    onFulfilled = typeof onFulfilled === 'function' ? onFulfilled : v => v;
    //因为错误的值要让后面访问到,所以这里也要跑出个错误,不然会在之后 then 的 resolve 中捕获
    onRejected = typeof onRejected === 'function' ? onRejected : err => { throw err };
    // 每次调用 then 都返回一个新的 promise  Promise/A+ 2.2.7
    let promise2 = new Promise((resolve, reject) => {
      if (this.status === FULFILLED) {
        //Promise/A+ 2.2.2
        //Promise/A+ 2.2.4 --- setTimeout
        setTimeout(() => {
          try {
            //Promise/A+ 2.2.7.1
            let x = onFulfilled(this.value);
            // x可能是一个proimise
            resolvePromise(promise2, x, resolve, reject);
          } catch (e) {
            //Promise/A+ 2.2.7.2
            reject(e)
          }
        }, 0);
      }

      if (this.status === REJECTED) {
        //Promise/A+ 2.2.3
        setTimeout(() => {
          try {
            let x = onRejected(this.reason);
            resolvePromise(promise2, x, resolve, reject);
          } catch (e) {
            reject(e)
          }
        }, 0);
      }

      if (this.status === PENDING) {
        this.onResolvedCallbacks.push(() => {
          setTimeout(() => {
            try {
              let x = onFulfilled(this.value);
              resolvePromise(promise2, x, resolve, reject);
            } catch (e) {
              reject(e)
            }
          }, 0);
        });

        this.onRejectedCallbacks.push(()=> {
          setTimeout(() => {
            try {
              let x = onRejected(this.reason);
              resolvePromise(promise2, x, resolve, reject)
            } catch (e) {
              reject(e)
            }
          }, 0);
        });
      }
    });

    return promise2;
  }
}

9.手写虚拟DOM

class VNode {
    type;
    text;
    props = { children: [] }
    constructor(type, property, children, text) {
        this.type = type;
        this.props.children = children;
        if (type === "#text") this.text = text;
        for (let key in property) {
            this.props[key] = property[key]
        }
    }
    render() {
        if (this.type === "#text") return document.createTextNode(this.text);
        let el = document.createElement(this.type);
        for (let key in this.props) {
            if (key !== "children") {
                el[key] = this.props[key]
            }
        };
        this.props.children.forEach(VNode => {
            el.appendChild(VNode.render())
        })
        return el;
    }
}
const v = (type, property, children, text) => {
    return new VNode(type, property, children, text);
}

const test = () => {
    const el = v("div", { onclick: () => { console.log(1) } }, [
        v("span", {}, [
            v("#text", {}, [], "hello ")
        ]),
        v("#text", {}, [], "world")
    ]);
    document.getElementById("root").appendChild(el.render())
}
text()

domDiff

const patchesVNode = (parent, newVNode, oldVNode, index = 0) => {

    console.log(parent.childNodes)
    if (!oldVNode) {
        parent.appendChild(newVNode.render());
    } else if (!newVNode) {
        parent.removeChild(parent.childNodes[index]);
    } else if (newVNode.type !== oldVNode.type || newVNode.text !== oldVNode.text) {
        parent.replaceChild(newVNode.render(), parent.childNodes[index]);
    } else {
        for (let i = 0; i < newVNode.props.children.length || i < oldVNode.props.children.length; i++) {
            patchesVNode(parent.childNodes[index], newVNode.props.children[i], oldVNode.props.children[i], i)
        }
    }
}
posted @ 2022-11-08 17:20  青川薄  阅读(44)  评论(0编辑  收藏  举报