2023 面试之手写题

89 阅读28分钟

设计模式分类

手写单例模式(创建模式)
测试结果
let Winner = new CreateSingleton('Winner');
let Looser = new CreateSingleton('Looser');

console.log(Winner === Looser); // true
console.log(Winner.getName());  // 'Winner'
console.log(Looser.getName());  // 'Winner'
实现:
let CreateSingleton = (function(){
    let instance;
    return function(name) {
        if (instance) {
            return instance;
        }
        this.name = name;
        return instance = this;
    }
})();
CreateSingleton.prototype.getName = function() {
    console.log(this.name);
}
手写观察者模式(行为模式)
测试结果:
obj = observable({
  name:'789'
})

observe(function test(){
  console.log('触发了')
})

obj.name ="前端柒八九"
// 触发了
// 前端柒八九

实现:
// 定义observe
const queuedObservers = new Set();
const observe = fn => queuedObservers.add(fn);


const observable = obj => new Proxy(obj, {
  set(target, key, value, receiver) {
    const result = Reflect.set(target, key, value, receiver);
    // notify
    queuedObservers.forEach(observer => observer());
    return result;
  }
});
手写发布订阅 (行为模式)
测试结果:
ob = new Observer();

l1 = (data) => console.log(`l1_${data}`)
l2 = (data) => console.log(`l2_${data}`)

ob.on('event1',l1)
ob.on('event1',l2)

//发布订阅
ob.emit('event1',789) 
// l1_789
// l2_789

// 取消,订阅l1
ob.off('event1',l1)

ob.emit('event1',567)
//l2_567

实现:
class Observer {
  caches = {}; // 事件中心
  
  // eventName事件名-独一无二, fn订阅后执行的自定义行为
  on (eventName, fn){ 
    this.caches[eventName] = this.caches[eventName] || [];
    this.caches[eventName].push(fn);
  }
  
  // 发布 => 将订阅的事件进行统一执行
  emit (eventName, data) { 
    if (this.caches[eventName]) {
      this.caches[eventName]
      .forEach(fn => fn(data));
    }
  }
  // 取消订阅 => 若fn不传, 直接取消该事件所有订阅信息
  off (eventName, fn) { 
    if (this.caches[eventName]) {
      const newCaches = fn 
        ? this.caches[eventName].filter(e => e !== fn) 
        : [];
      this.caches[eventName] = newCaches;
    }
  }
}

JS 继承与原型,原型链

juejin.cn/post/707535…

原型链继承

让构造函数的prototype指向另一个构造函数的实例。

function Person() {
  this.colors = ['white', 'yellow', 'black']
}

function YellowRace() { }
YellowRace.prototype = new Person()

const hjy = new YellowRace()
hjy.colors.push('green')
 console.log(hjy.colors) // ['white', 'yellow', 'black', 'green']

const laowang = new YellowRace()
console.log(laowang.colors) // ['white', 'yellow', 'black', 'green']
盗用构造函数

原理: 通过在子类中调用父类构造函数实现上下文的绑定。

function Person(eyes) {
  this.eyes = eyes
  this.getEyes = function () {
    return this.eyes
  }
}

function YellowRace() {
  Person.call(this, 'black')
}

const hjy = new YellowRace()
console.log(hjy.getEyes()) // black
console.log(hjy.ReturnEyes()) // TypeError: hjy.ReturnEyes is not a function
组合继承
function Person(eyes) {
  this.eyes = eyes
  this.colors = ['white', 'yellow', 'black']
}

Person.prototype.getEyes = function () {
  return this.eyes
}

function YellowRace() {
  Person.call(this, 'black') // 调用构造函数并传参
}
YellowRace.prototype = new Person() // 再次调用构造函数

const hjy = new YellowRace()
hjy.colors.push('green')

const laowang = new YellowRace()

console.log(hjy.colors) // ['white', 'yellow', 'black', 'green']
console.log(laowang.colors) // ['white', 'yellow', 'black']
console.log(hjy.getEyes()) // black
原型式继承

既然原型式继承和原型链继承的本质基本一致,那么原型式继承也有一样的缺点

const hjy = {
  eyes: 'black',
  colors: ['white', 'yellow', 'black']
}

const laowang = Object.create(hjy, {
  name: {
    value: '老王',
    writable: false,
    enumerable: true,
    configurable: true
  },
  age: {
    value: '32',
    writable: true,
    enumerable: true,
    configurable: false
  }
})
console.log(laowang.eyes) // black
console.log(laowang.colors) // ['white', 'yellow', 'black']
console.log(laowang.name) // 老王
console.log(laowang.age) // 32
寄生式继承

它的思想就是在原型式继承的基础上以某种方式增强对象,然后返回这个对象。

function inherit(o) {
  let clone = Object.create(o)
  clone.sayHi = function () { // 增强对象
    console.log('Hi')
  }
  return clone
}

const hjy = {
  eyes: 'black',
  colors: ['white', 'yellow', 'black']
}

const laowang = inherit(hjy)

console.log(laowang.eyes) // black
console.log(laowang.colors) // ['white', 'yellow', 'black']
laowang.sayHi() // Hi
寄生式组合继承

基本思路就是使用寄生式继承来继承父类的原型对象,然后将返回的新对象赋值给子类的原型对象。

首先实现寄生式继承的核心逻辑:

function inherit(Son, Father) {
  const prototype = Object.create(Father.prototype) // 获取父类原型对象副本
  prototype.constructor = Son // 将获取的副本的constructor指向子类,以此增强副本原型对象
  Son.prototype = prototype // 将子类的原型对象指向副本原型对象
}
function Person(eyes) {
  this.eyes = eyes
  this.colors = ['white', 'yellow', 'black']
}

Person.prototype.getEyes = function () {
  return this.eyes
}

function YellowRace() {
  Person.call(this, 'black') // 调用构造函数并传参
}

inherit(YellowRace, Person) // 寄生式继承,不用第二次调用构造函数

const hjy = new YellowRace()
hjy.colors.push('green')

const laowang = new YellowRace()

console.log(hjy.colors) // ['white', 'yellow', 'black', 'green']
console.log(laowang.colors) // ['white', 'yellow', 'black']
console.log(hjy.getEyes()) // black

只调用一次父类构造函数
Child可以向Parent传参
父类方法可以复用
父类的引用属性不会被共享
function objectCopy(obj) {
  function Fun() { };
  Fun.prototype = obj;
  return new Fun();
}

function inheritPrototype(child, parent) {
  let prototype = objectCopy(parent.prototype); // 创建对象
  prototype.constructor = child; // 增强对象
  Child.prototype = prototype; // 赋值对象
}

function Parent(name) {
  this.name = name;
  this.friends = ["rose", "lily", "tom"]
}

Parent.prototype.sayName = function () {
  console.log(this.name);
}

function Child(name, age) {
  Parent.call(this, name);
  this.age = age;
}

inheritPrototype(Child, Parent);
Child.prototype.sayAge = function () {
  console.log(this.age);
}

let child1 = new Child("yhd", 23);
child1.sayAge(); // 23
child1.sayName(); // yhd
child1.friends.push("jack");
console.log(child1.friends); // ["rose", "lily", "tom", "jack"]

let child2 = new Child("yl", 22)
child2.sayAge(); // 22
child2.sayName(); // yl
console.log(child2.friends); // ["rose", "lily", "tom"]

es5和es6的继承有什么区别

ES5的继承实质上是先创建子类的实例对象,然后再将父类的方法添加到this上(superClass.call(this)).

ES6的继承机制完全不同,实质上是先创建父类的实例对象this(所以必须先调用父类的super()方法),然后再用子类的构造函数修改this

ES5的继承时通过原型或构造函数机制来实现。

ES6通过class关键字定义类,里面有构造方法,类之间通过extends关键字实现继承。子类必须在constructor方法中调用super方法,否则新建实例报错。因为子类没有自己的this对象,而是继承了父类的this对象,然后对其进行加工。如果不调用super方法,子类得不到this对象。

promise all

  static all(promiseList) {
    return new MyPromise((resolve, reject) => {
      const result = [];
      const length = promiseList.length;
      let count = 0;

      if (length === 0) {
        return resolve(result);
      }

      promiseList.forEach((promise, index) => {
        MyPromise.resolve(promise).then((value) => {
          count++;
          result[index] = value;
          if (count === length) {
            resolve(result);
          }
        }, (reason) => {
          reject(reason);
        });
      });
    });

allSettled

static allSettled = (promiseList) => {
    return new MyPromise((resolve) => {
      const length = promiseList.length;
      const result = [];
      let count = 0;

      if (length === 0) {
        return resolve(result);
      }
      for (let i = 0; i < length; i++) {
        const currentPromise = MyPromise.resolve(promiseList[i]);
        currentPromise.then((value) => {
          count++;
          result[i] = {
            status: 'fulfilled',
            value: value
          }
          if (count === length) {
            resolve(result);
          }
        }, (reason) => {
          count++;
          result[i] = {
            status: 'rejected',
            reason: reason
          }
          if (count === length) {
            resolve(result);
          }
        });
      }
    });
  }

race

  static race(promiseList) {
    return new MyPromise((resolve, reject) => {
      const length = promiseList.length;
      if (length === 0) {
        return resolve();
      }
      for (let i = 0; i < length; i++) {
        MyPromise.resolve(promiseList[i]).then((value) => {
          resolve(value);
        }, (reason) => {
          reject(reason);
        });
      }
    });
  }

reject

  // reject 静态方法
  static reject(reason) {
    return new MyPromise((resolve, reject) => {
      reject(reason);
    });
  }

resolve

 // resolve 静态方法 构造函数方法
  static resolve(parameter) {
    // 如果传入 MyPromise 就直接返回
    if (parameter instanceof MyPromise) {
      return parameter;
    }
    // 转成常规方式
    return new MyPromise(resolve => {
      resolve(parameter);
    });
  }

promise

//Promise/A+规范的三种状态
const PENDING = 'pending'
const FULFILLED = 'fulfilled'
const REJECTED = 'rejected'class MyPromise {
  // 构造方法接收一个回调
  constructor(executor) {
    this._status = PENDING     // Promise状态
    this._resolveQueue = []    // 成功队列, resolve时触发
    this._rejectQueue = []     // 失败队列, reject时触发
​
    // 由于resolve/reject是在executor内部被调用, 因此需要使用箭头函数固定this指向, 否则找不到this._resolveQueue
    let _resolve = (val) => {
      if(this._status !== PENDING) return// 对应规范中的"状态只能由pending到fulfilled或rejected"
      this._status = FULFILLED              // 变更状态
​
      // 这里之所以使用一个队列来储存回调,是为了实现规范要求的 "then 方法可以被同一个 promise 调用多次"
      // 如果使用一个变量而非队列来储存回调,那么即使多次p1.then()也只会执行一次回调
      while(this._resolveQueue.length) {
        const callback = this._resolveQueue.shift()
        callback(val)
      }
    }
    // 实现同resolve
    let _reject = (val) => {
      if(this._status !== PENDING) return// 对应规范中的"状态只能由pending到fulfilled或rejected"
      this._status = REJECTED               // 变更状态
      while(this._rejectQueue.length) {
        const callback = this._rejectQueue.shift()
        callback(val)
      }
    }
    // new Promise()时立即执行executor,并传入resolve和reject
    executor(_resolve, _reject)
  }
​
  // then方法,接收一个成功的回调和一个失败的回调
  then(resolveFn, rejectFn) {
    this._resolveQueue.push(resolveFn)
    this._rejectQueue.push(rejectFn)
  }
}

PromiseQueue

/*
存在如下的场景:
针对一批异步任务,由于某些特殊的原因,只能在某一时间内,并发执行concurrentCount个异步任务。只有在前面的N个任务处于落定状态,剩余的任务才会被执行。
换句话说,手动实现一个能够控制异步触发的数据结构。(如果大家对网络有了解的h话,这个结构是不是很像滑动窗口)
​
接收一个promise数组,定义窗口大小为3
const taskQueue = new PromiseQueue(tasks, 3); 
taskQueue.run();
*/
class PromiseQueue{
    constructor(tasks,concurrentCount=1){
        this.totals = tasks.length;
        this.todo =tasks;
        this.count = concurrentCount;
        this.running =[];
        this.complete =[];
        
    }
​
    runNext(){
        return (
            this.running.length < this.count
            && this.todo.length
        )
    }
​
    run(){
        while(this.runNext()){
            let promise = this.todo.shift();
            promise.then(()=>{
                this.complete.push(this.running.shift());
                this.run();
            })
​
            this.running.push(promise)
        }
    }
}

宏微任务

setTimeout(() => {
 console.log('start');
 Promise.resolve().then(() => {
   console.log('Promise 1');
   setTimeout(() => {
     console.log('setTimeout 2');
   })
 });
 setTimeout(() => {
   console.log('setTimeout 1');
   Promise.resolve().then(() => {
     console.log('Promise 2');
   });
 });
}, 0)
console.log('end');
​
end
start
Promise 1
setTimeout 1
Promise 2
setTimeout 2

闭包

var s = 0;
var i = 1;
var funcs = [];
var n = 3;
function x(n) {
 for (i = 0; i < 3; i++) { 
   funcs[i] = () => {
       console.log(i) 
     s = s + i * n; 
     console.log(s);
   };
 }
}
​
x(1);
funcs[0](); 3
funcs[1](); 6
funcs[2](); 9
​
3
3
3
6
3
9

var 变量运行

for(var i=0;i<3;i++){
  setTimeout(()=>console.log(i),1)
}
console.log(i) // 输出3
​
解决一:使用let
for(let i=0;i<3;i++){
  setTimeout(()=>console.log(i),i)  // 输出0,1,2
}
console.log(i) //报错
​
解决二:使用IIFE
for(var i=0;i<3;i++){
  setTimeout(
      ((i)=>console.log(i))(i), // 输出0,1,2
      1
    )
}
​
console.log(i) // 输出3

深浅拷贝

负责类型数据指针放在栈中,数据放在堆中

object 浅拷贝:for in,Object.assign({},obj),Object.getOwnPropertyDescriptors()Object.defineProperties()

Array 浅拷贝:slice,concat。

Object 深拷贝:JSON.stringify,JSON.parse(json反序列号成js对象)

展开运算符:如果只是一层数组或对象可以理解为深拷贝,如果数组或对象中的元素是引用类型元素是浅拷贝。

手写递归深拷贝

实现逻辑就是(FHT) 链接:juejin.cn/post/720290…

  1. 利用 for-in对对象的属性进行遍历(自身属性+继承属性)
  2. source.hasOwnProperty(i)判断是否是非继承可枚举属性
  3. typeof source[i] === 'object'判断值的类型,如果是对象,递归处理
function clone(source) {
    let target = {};
    for(let i in source) {
        if (source.hasOwnProperty(i)) { // hasOwnProperty() 只会检查对象的自有属性,对象原型上的属性其不会检测
            if (typeof source[i] === 'object') {
                target[i] = clone(source[i]); // 递归处理
            } else {
                target[i] = source[i];
            }
        }
    }
​
    return target;
}
深拷贝(考虑到复制 Symbol 类型)
function isObject(val) {
  return typeof val === "object" && val !== null;
}
​
function deepClone(obj, hash = new WeakMap()) {
  if (!isObject(obj)) return obj;
  if (hash.has(obj)) {
    return hash.get(obj);
  }
  let target = Array.isArray(obj) ? [] : {};
  hash.set(obj, target);
  Reflect.ownKeys(obj).forEach((item) => {
    if (isObject(obj[item])) {
      target[item] = deepClone(obj[item], hash);
    } else {
      target[item] = obj[item];
    }
  });
​
  return target;
}
​
// var obj1 = {
// a:1,
// b:{a:2}
// };
// var obj2 = deepClone(obj1);
// console.log(obj1);
通过嵌套扩展运算符实现深复制
const original = {name: '789', work: {address: 'BeiJing'}};
const copy = {name: original.name, work: {...original.work}};
​
original.work !== copy.work // 指向不同的引用地址
Object.getOwnPropertyDescriptors()Object.defineProperties() 浅拷贝
function copyAllOwnProperties(original) {
  return Object.defineProperties(
    {}, Object.getOwnPropertyDescriptors(original));
}

手写apply/call/bind

call() apply() bind() 区别

call 和 apply作用一模一样,区别仅在于传入参数形式的不同。

apply

apply 接受两个参数,第一个参数指定了函数体内 this 对象的指向,第二个参数为一个带下标的集合,这个集合可以为数组,也可以为类数组

类数组:

  1. 对象本身要可以存取属性;
  2. 对象的 length 属性可读写。

call

call 传入的参数数量不固定,第一个参数也是代表函数体内的 this 指向,从第二个参数开始往后,每个参数被依次传入函数

bind

参数类型和 call 相同,不过它不会执行函数,而是修改 this 后返回一个新的函数

apply:
/** 手写 apply
 * 用法:apply 方法用于调用一个函数,并指定函数内部 this 的指向,传入一个数组
 * 思路:
 *  1、判断 this 是否指向一个函数  只有函数才可以执行
 *  2、获取传入的 context 上下文 也就是我们要指向的 如果不存在就指向 window
 *  3、将当前 this 也就是外部需要执行的函数 绑定到 context 上的一个 fn 属性上
 *  4、执行 fn 函数 判断 args 是否有 如果没有参数就直接执行 如果有参数 将参数展开传入 fn
 *  5、删除 context 对象的 fn 属性 并将 result 返回
 */
链接:https://juejin.cn/post/7272737742307065914
Function.prototype.myApply = function (context, args) {
  if (typeof this !== 'function') {
    return new TypeError('type error')
  }
​
  context = context || window;
  context.fn = this; //this指向调用myApply 的 函数
  let result = context.fn(...args);
  delete context.fn
  return result;
}
​
call:
Function.prototype.myCall = function (context, ...args) {
  if (typeof this !== 'function') {
    return new TypeError('type error')
  }
  context = context || window;
  context.fn = this;
  let result = context.fn(...args);
  delete context.fn
  return result;
}
​
bind:
Function.prototype.myBind = function (context,...arg1) {
    let that = this //将原函数存起来
    return function (...arg2){ //返回新的函数
      return that.apply(context, [...arg1, ...arg2])
    }
}
​

compose 函数 科里化

概念:可以将嵌套函数平铺,嵌套函数执行就是将一个函数的返回值作为另一个函数的参数

要点:1.组合函数后形成新的函数 2.从右往左求值,每一个结果作为下次参数

应用:redux 中间件,webpack的loader 也是从右往左的

调用:
const add1 = (x) => x + 1;
const mul3 = (x) => x * 3;
const div2 = (x) => x / 2;
div2(mul3(add1(5))); //=> 9
​
let result = compose(div2, mul3, add1)(5);
console.log(result); // =>9
​
实现:
const compose = (...fns) => (...arg) => fns.reduceRight((pre, fn) => fn(pre), ...arg)

参考:www.bilibili.com/video/BV1ZL…

pipe 函数

是compose一样功能,只是执行顺序是从左往右

const compose = (...fns) => (...arg) => fns.reduce((pre, fn) => fn(pre), ...arg) // 上边调用 8.5

一维数组处理转成多维数组父子层级

运行结果:
[{
    id:1,
    name: 'a',
    parentId: 0,
    children:[
     { id: 3, name: 'c', parentId: 1 },
     { id: 4, name: 'd', parentId: 1 }
    ]
},{
    id: 2,
    name: 'b',
    parentId: 0,
    children:[
     { id: 7, name: 'e', parentId: 2 },
     { id: 6, name: 'f', parentId: 2 }
    ]
}]
实现:
const roles = [
  { id: 1, name: 'a', parentId: 0 },
  { id: 2, name: 'b', parentId: 0 },
  { id: 3, name: 'c', parentId: 1 },
  { id: 4, name: 'd', parentId: 1 },
  { id: 7, name: 'e', parentId: 2 },
  { id: 6, name: 'f', parentId: 2 }
]

function convert(roles) {
  const root = []
  const dict = {}

  roles.forEach(item => {
    if (item.parentId === 0) {
      root.push(item)
    }
    item.children = []
    dict[item.id] = item
  })
  roles.forEach(item => {
    if (item.parentId !== 0) {
      dict[item.parentId].children.push(item)
    }
  })
  return root
}

console.log(convert(roles));

一维数组处理转成多维父子层级对象

运行结果:
{
    女装:{
        A字裙:{},
        半身裙: {},
        连衣裙: {}
    },
    数码:{
        电脑配件:{内存: {}}
    }
}
实现:
let industry_list = [    {        "parent_ind": "女装",        "name": "连衣裙"    },    {        "name": "女装"    },    {        "parent_ind": "女装",        "name": "半身裙"    },    {        "parent_ind": "女装",        "name": "A字裙"    },    {        "name": "数码"    },    {        "parent_ind": "数码",        "name": "电脑配件"    },    {        "parent_ind": "电脑配件",        "name": "内存"    },]
​
function toDo(list) {
    function foo(list, name) {
        let obj = Object.create(null)
        for (let item of list) {
            if (item.parent_ind === name) {
                obj[item.name] = Object.create(null)
            }
        }
        for (let _name in obj) {
            obj[_name] = foo(list, _name)
        }
        return obj
    }
    return foo(list, undefined)
}
​
console.log(toDo(industry_list));

手写Object.create

/** 手写 Object.create
 * 用法:创建一个新的对象,将传入的对象原型指向新对象并返回
 * 思路:
 *  1、将原型写入到一个函数里面,然后将函数返回
 * @param {*} obj
 * @return {*} 
 */
function myCreate(obj) {
  function F() {}
  F.prototype = obj
​
  return new F()
}
使用案例:
// Object.create使用
const person = {
  showName () {
    console.log(this.name)
  }
}
const me = Object.create(person)
​
me.name = '前端胖头鱼' 
me.showName() // 前端胖头鱼

实现instanceof

调用:
myInstanceof(B,A) //B = new A()
实现:
/** 手写 instanceof 方法
 * 用法:instanceof 运算符用于检测构造函数的 prototype 属性是否出现在某个实例对象的原型链上。
 * 思路:
 *  1、通过 Object.getPrototypeOf 获取 obj 的原型
 *  2、循环判断 objProtoType 是否和 constructor 的原型相等
 *    2.1、如果相等就返回 true
 *    2.2、如果不相等 就重新赋值一下 obj 的原型 进入下一次循环
 *  3、判断是 objProtoType 是否为空 如果为空就说明不存在 返回 false
 * @param {Object} obj 需要判断的数据
 * @param {Object} constructor
 * @return {*} 
 Object.getPrototypeOf() 静态方法返回指定对象的原型
 */
function myInstanceof(obj, type) {
  let objPrototype = Object.getPrototypeOf(obj)
​
  while (true) {
    if (!objPrototype) return false
    if (objPrototype === type.prototype) return true
​
    objPrototype = Object.getPrototypeOf(objPrototype)
  }
}
​

new

使用 new 调用类的构造函数会执行如下操作(三步)

  1. 在内存中创建一个

    新对象

    • 这个新对象内部的[[Prototype]]指针被赋值为构造函数的 prototype 属性
    • context = Object.create(constructor.prototype);
  2. 构造函数

    内部的 this被赋值为这个新对象(即 this指向新对象)

    • 执行构造函数内部的代码(给新对象添加属性)
    • result = constructor.apply(context, params);
  3. 判断这个返回值。返回的是 Object || Function 类型 就返回该对象 否则返回创建的对象;

    • return (typeof result === 'object' && result != null) ? result : context;
function _new(
  /* 构造函数 */ constructor,
  /* 构造函数参数 */ ...params
) {
  // 创建一个空对象,继承构造函数的 prototype 属性
  let context = Object.create(constructor.prototype);
  // 执行构造函数
  let result = constructor.apply(context, params);
  // 如果返回结果是对象,就直接返回,否则返回 context 对象
  // 判断这个返回值 如果返回的是 Object || Function 类型 就返回该对象 否则返回创建的对象
  const flag = result && (typeof result === 'object' || typeof result === 'function')
  return flag ? result : context
}

链接:juejin.cn/post/720691…

对象拍平问题 (数组/对象)

数组扁平化 flat
/**
 * 
 * @param {*} array 深层嵌套的数据
 * @returns array 新数组
 */
const flat1 = (array) => {
  return array.reduce((result, it) => {
    return result.concat(Array.isArray(it) ? flat1(it) : it)
  }, [])
}
​
let arr1 = [
  1,
  [ 2, 3, 4 ],
  [ 5, [ 6, [ 7, [ 8 ] ] ] ]
]
console.log(flat1(arr1))
​
// js原生的flat方法
/**
 * 
 * @param {*} array 深层嵌套的数据
 * @returns 新数组
 */
const flat2 = (array) => {
  return array.flat(Infinity)
}
​
let arr2 = [
  1,
  [ 2, 3, 4 ],
  [ 5, [ 6, [ 7, [ 8 ] ] ] ]
]
​
console.log(flat2(arr2))
// 3
const flattenArr = arr => {
    let result = [];
    (function helper(arr) {
        arr.forEach(item=>{
            if(Array.isArray(item)){
                helper(item)
            }else{
                result.push(item)
            }
        })
    })(arr)
    return result;
}

指定展开N层-- 不用看

const flattenArrN = (arr,depth=1) => {
    let result = [];
    (function helper(arr,depth) {
        arr.forEach(item=>{
            if(Array.isArray(item)&&depth>0){
                helper(item,depth-1)
            }else{
                result.push(item)
            }
        })
    })(arr,depth)
    return result;
}
对象扁平化
const obj = {
  a: 1,
  b: [1, 2, { c: true }],
  c: { e: 2, f: 3 },
  g: null,
};
flattenObj(obj)
运行结果:
{
  a: 1,
  'b[0]': 1,
  'b[1]': 2,
  'b[2].c': true,
  'c.e': 2,
  'c.f': 3,
  g: null
}
实现:
const flattenObj = obj => {
  let result = {};
  (function helper(obj, preKey) { 
    if (!obj) return
    Object.entries(obj).forEach(([key, value]) => {
      let isArray = Array.isArray(obj);
      let keyStr = isArray
        ? `${preKey}[${key}]`
        : `${preKey}${key}`;
      if (Array.isArray(value)) {
        helper(value, keyStr)
      } else if (value && typeof value === 'object') {
        helper(value, `${keyStr}.`)
      } else {
        result[keyStr] = value;
      }
    })
  })(obj, '')
  return result;
}

树转数组(含数组对象)

测试:
let source1 = [
  {
    id: 1,
    pid: 0,
    name: 'body',
    children: [
      {
        id: 2,
        pid: 1,
        name: 'title',
        children: [{ id: 3, pid: 2, name: 'div' }],
      },
    ],
  },
  {
    id: 4,
    pid: 0,
    name: 'html',
    children: [
      {
        id: 5,
        pid: 4,
        name: 'div',
        children: [{ id: 7, pid: 5, name: 'img' }],
      },
    ],
  },
][
  // 转为
  ({ id: 1, pid: 0, name: 'body' },
  { id: 4, pid: 0, name: 'html' },
  { id: 2, pid: 1, name: 'title' },
  { id: 5, pid: 4, name: 'div' },
  { id: 3, pid: 2, name: 'div' },
  { id: 7, pid: 5, name: 'img' })
]

实现:
function treeToArr(arr) {
  let stack = [...arr]
  const result = []

  while (stack.length) {
    // 从数组中获取第一个
    const first = stack.shift()

    // 判断它有没有children
    if (first['children'] && first.children.length) {
      //  children 将它展开再放入到栈中
      stack.push(...first.children)

      // 删除 children 属性
      delete first.children
    }

    result.push(first)
  }

  return result
}

防抖和截流

  • debounce(防抖):一个连续操作中的处理,只触发一次,从而实现防抖动。代码实现重在清零 clearTimeout.。一定时间内只执行一次
  • throttle(节流):一个连续操作中的处理,按照阀值时间间隔进行触发,从而实现节流。代码实现重在开锁关锁 timer=timeout; timer=null 。 多次点击只执行最后一次
防抖:
function debounce(fn, delay) {
  let timer
  return function (...args) {
    if (timer) {
      clearTimeout(timer)
    }
    timer = setTimeout(() => {
      fn.apply(this, args)
    }, delay)
  }
}
​
节流:
function throttle(fn, delay) {
  let last = 0 // 上次触发时间
  return (...args) => {
    const now = Date.now()
    if (now - last > delay) {
      last = now
      fn.apply(this, args)
    }
  }
}

业务场景应用:抢票

三点是否共线

/*
三点是否共线可以通过判断斜率来判断:
设有 p1,p2,q三点,判断三点是否共线:
公式:
k1 = (p2.y - p1.y)/(p2.x - p1.x)
k2 = (q.y - p1.y)/(q.x - p1.x)
如果k1 === k2就表示三点共线
链接:https://juejin.cn/post/7206912311562174523
*/
function isOnLine(p1,p2,q){
  return (p2.y - p1.y)/(p2.x - p1.x) === (q.y - p1.y)/(q.x - p1.x)
}

fibonic(斐波那契数列)

1, 1, 2, 3, 5, 8, 13, ....

也就是,第 n 个数由数列的前两个相加而来:f(n) = f(n - 1) + f(n -2)

测试结果:
fibonacci(1000) //  浏览器卡死
实现:
一:常规方式:
function fibonacci(n){
  if(n<=1) return 1;
  return fibonacci(n - 1) + fibonacci(n - 2)
}
二:

实现每隔一秒打印 1,2,3,4

// 使用 let 块级作用域
for (let i = 0; i < 5; i++) {
  setTimeout(function() {
    console.log(i);
  }, i * 1000);
}

排序

链接:juejin.cn/post/720463…

常规算法:
const swap = (arr,i,j) => [arr[i],arr[j]] = [arr[j],arr[i]];
交换排序(SBQ)
冒泡排序(BubbleSort)
/*
冒泡排序的原理如下,从第一个元素开始,把当前元素和下一个索引元素进行比较。如果当前元素大,那么就交换位置,重复操作直到比较到最后一个元素,那么此时最后一个元素就是该数组中最大的数。下一轮重复以上操作,但是此时最后一个元素已经是最大数了,所以不需要再比较最后一个元素,只需要比较到 length - 1 的位置。*/
function bubbleSort(list) {
  var n = list.length;
  if (!n) return [];
​
  for (var i = 0; i < n; i++) {
    // 注意这里需要 n - i - 1
    for (var j = 0; j < n - i - 1; j++) {
      if (list[j] > list[j + 1]) {
        var temp = list[j + 1];
        list[j + 1] = list[j];
        list[j] = temp;
      }
    }
  }
  return list;
}
快排
/*快排的原理如下。随机选取一个数组中的值作为基准值,从左至右取值与基准值对比大小。比基准值小的放数组左边,大的放右边,对比完成后将基准值和第一个比基准值大的值交换位置。然后将数组以基准值的位置分为两部分,继续递归以上操作
*/
function quickSort(arr) {
  if (arr.length<=1){
    return arr;
  }
  var baseIndex = Math.floor(arr.length/2);//向下取整,选取基准点
  var base = arr.splice(baseIndex,1)[0];//取出基准点的值,
  // splice 通过删除或替换现有元素或者原地添加新的元素来修改数组,并以数组形式返回被修改的内容。此方法会改变原数组。
  // slice方法返回一个新的数组对象,不会更改原数组
  //这里不能直接base=arr[baseIndex],因为base代表的每次都删除的那个数
  var left=[];
  var right=[];
  for (var i = 0; i<arr.length; i++){
    // 这里的length是变化的,因为splice会改变原数组。
    if (arr[i] < base){
      left.push(arr[i]);//比基准点小的放在左边数组,
    }else{
      right.push(arr[i]);//比基准点大的放在右边数组,
    }
  }
  return quickSort(left).concat([base],quickSort(right));
}
插入排序(IIS)
插入排序(InsertSort)
function insertSort(arr) {
  for (let i = 1; i < arr.length; i++) {
    let j = i;
    let target = arr[j];
    while (j > 0 && arr[j - 1] > target) {
      arr[j] = arr[j - 1];
      j--;
    }
    arr[j] = target;
  }
  return arr;
}
// console.log(insertSort([3, 6, 2, 4, 1]));
希尔排序(ShellSort)
// t = len>>1
// while
// arr[j+t] = arr[j]
const ShellSort = arr => {
    let len = arr.length;
    let i,j,sential;
    let t = len>>1;
    while(t>=1){
        for(let i=t;i<len;i++){
            sential = arr[i];
            for(let j=i-t;j>=0&&arr[j]>sential;j=j-t){
                arr[j+t] = arr[j]
            }
            arr[j+t]= sential;
        }
        t = t>>1;
    }
    return arr;
}
选择排序
function selectSort(arr) {
  // 缓存数组长度
  const len = arr.length;
  // 定义 minIndex,缓存当前区间最小值的索引,注意是索引
  let minIndex;
  // i 是当前排序区间的起点
  for (let i = 0; i < len - 1; i++) {
    // 初始化 minIndex 为当前区间第一个元素
    minIndex = i;
    // i、j分别定义当前区间的上下界,i是左边界,j是右边界
    for (let j = i; j < len; j++) {
      // 若 j 处的数据项比当前最小值还要小,则更新最小值索引为 j
      if (arr[j] < arr[minIndex]) {
        minIndex = j;
      }
    }
    // 如果 minIndex 对应元素不是目前的头部元素,则交换两者
    if (minIndex !== i) {
      [arr[i], arr[minIndex]] = [arr[minIndex], arr[i]];
    }
  }
  return arr;
}
// console.log(selectSort([3, 6, 2, 4, 1]));
二分查找--时间复杂度 log2(n)
题目描述:如何确定一个数在一个有序数组中的位置
取中间值
function search(arr, target, start, end) {
  let targetIndex = -1;
​
  let mid = Math.floor((start + end) / 2);
​
  if (arr[mid] === target) {
    targetIndex = mid;
    return targetIndex;
  }
​
  if (start >= end) {
    return targetIndex;
  }
​
  if (arr[mid] < target) {
    return search(arr, target, mid + 1, end);
  } else {
    return search(arr, target, start, mid - 1);
  }
}
// const dataArr = [1, 2, 3, 4, 5, 6, 7, 8, 9];
// const position = search(dataArr, 6, 0, dataArr.length - 1);
// if (position !== -1) {
//   console.log(`目标元素在数组中的位置:${position}`);
// } else {
//   console.log("目标元素不在数组中");
// }
// console.log(Math.floor(5.95));
// Expected output: 5

版本号排序的方法

/*题目描述:有一组版本号如下 ['0.1.1', '2.3.3', '0.302.1', '4.2', '4.3.5', '4.3.4.5']。现在需要对其进行排序,排序的结果为 ['4.3.5','4.3.4.5','2.3.3','0.302.1','0.1.1']
链接:https://juejin.cn/post/7204635326559600698
*/
arr.sort((a, b) => {
  let i = 0;
  const arr1 = a.split(".");
  const arr2 = b.split(".");
​
  while (true) {
    const s1 = arr1[i];
    const s2 = arr2[i];
    i++;
    if (s1 === undefined || s2 === undefined) {
      return arr2.length - arr1.length;
    }
​
    if (s1 === s2) continue;
​
    return s2 - s1;
  }
});
console.log(arr);

分片思想解决大数据量渲染问题

题目描述:渲染百万条结构简单的大数据时 怎么使用分片思想优化渲染
应用:虚拟列表
let ul = document.getElementById("container");
// 插入十万条数据
let total = 100000;
// 一次插入 20 条
let once = 20;
//总页数
let page = total / once;
//每条记录的索引
let index = 0;
//循环加载数据
function loop(curTotal, curIndex) {
  if (curTotal <= 0) {
    return false;
  }
  //每页多少条
  let pageCount = Math.min(curTotal, once);
  window.requestAnimationFrame(function () {
    for (let i = 0; i < pageCount; i++) {
      let li = document.createElement("li");
      li.innerText = curIndex + i + " : " + ~~(Math.random() * total);
      ul.appendChild(li);
    }
    loop(curTotal - pageCount, curIndex + pageCount);
  });
}
loop(total, index);

LUR算法

juejin.cn/post/696871…

LRU

//  一个Map对象在迭代时会根据对象中元素的插入顺序来进行
// 新添加的元素会被插入到map的末尾,整个栈倒序查看
class LRUCache {
  constructor(capacity) {
    this.secretKey = new Map();
    this.capacity = capacity;
  }
  get(key) {
    if (this.secretKey.has(key)) {
      let tempValue = this.secretKey.get(key);
      this.secretKey.delete(key);
      this.secretKey.set(key, tempValue);
      return tempValue;
    } else return -1;
  }
  put(key, value) {
    // key存在,仅修改值
    if (this.secretKey.has(key)) {
      this.secretKey.delete(key);
      this.secretKey.set(key, value);
    }
    // key不存在,cache未满
    else if (this.secretKey.size < this.capacity) {
      this.secretKey.set(key, value);
    }
    // 添加新key,删除旧key
    else {
      this.secretKey.set(key, value);
      // 删除map的第一个元素,即为最长未使用的
      this.secretKey.delete(this.secretKey.keys().next().value);
    }
  }
}
// let cache = new LRUCache(2);
// cache.put(1, 1);
// cache.put(2, 2);
// console.log("cache.get(1)", cache.get(1))// 返回  1
// cache.put(3, 3);// 该操作会使得密钥 2 作废
// console.log("cache.get(2)", cache.get(2))// 返回 -1 (未找到)
// cache.put(4, 4);// 该操作会使得密钥 1 作废
// console.log("cache.get(1)", cache.get(1))// 返回 -1 (未找到)
// console.log("cache.get(3)", cache.get(3))// 返回  3
// console.log("cache.get(4)", cache.get(4))// 返回  4

动态规划求解硬币找零问题

题目描述:给定不同面额的硬币 coins 和一个总金额 amount。编写一个函数来计算可以凑成总金额所需的最少的硬币个数。如果没有任何一种硬币组合能组成总金额,返回 -1
示例1:
输入: coins = [1, 2, 5], amount = 11
输出: 3
解释: 11 = 5 + 5 + 1
​
示例2:
输入: coins = [2], amount = 3
输出: -1
​
const coinChange = function (coins, amount) {
  // 用于保存每个目标总额对应的最小硬币个数
  const f = [];
  // 提前定义已知情况
  f[0] = 0;
  // 遍历 [1, amount] 这个区间的硬币总额
  for (let i = 1; i <= amount; i++) {
    // 求的是最小值,因此我们预设为无穷大,确保它一定会被更小的数更新
    f[i] = Infinity;
    // 循环遍历每个可用硬币的面额
    for (let j = 0; j < coins.length; j++) {
      // 若硬币面额小于目标总额,则问题成立
      if (i - coins[j] >= 0) {
        // 状态转移方程
        f[i] = Math.min(f[i], f[i - coins[j]] + 1);
      }
    }
  }
  // 若目标总额对应的解为无穷大,则意味着没有一个符合条件的硬币总数来更新它,本题无解,返回-1
  if (f[amount] === Infinity) {
    return -1;
  }
  // 若有解,直接返回解的内容
  return f[amount];
};

请实现 DOM2JSON 一个函数,可以把一个 DOM 节点输出 JSON 的格式

题目描述:
<div>
  <span>
    <a></a>
  </span>
  <span>
    <a></a>
    <a></a>
  </span>
</div>
​
把上诉dom结构转成下面的JSON格式
​
{
  tag: 'DIV',
  children: [
    {
      tag: 'SPAN',
      children: [
        { tag: 'A', children: [] }
      ]
    },
    {
      tag: 'SPAN',
      children: [
        { tag: 'A', children: [] },
        { tag: 'A', children: [] }
      ]
    }
  ]
}
实现:
function dom2Json(domtree) {
  let obj = {};
  obj.name = domtree.tagName;
  obj.children = [];
  domtree.childNodes.forEach((child) => obj.children.push(dom2Json(child)));
  return obj;
}

手机号 3-3-4分割

// 适合纯11位手机
const splitMobile = (mobile, format = '-') => {
  return String(mobile).replace(/(?=(\d{4})+$)/g, format)
}
// 适合11位以内的分割
const splitMobile2 = (mobile, format = '-') => {
  return String(mobile).replace(/(?<=(\d{3}))/, format).replace(/(?<=([\d-]{8}))/, format)
}
​
console.log(splitMobile(18379802267))
console.log(splitMobile2(18379876545))

setTimeout模拟setInterval

const simulateSetInterval = (func, timeout) => {
  let timer = null
  const interval = () => {
    timer = setTimeout(() => {
      func()
      interval()
    }, timeout)
  }
​
  interval()
​
  return () => clearTimeout(timer)
}
​
const cancel = simulateSetInterval(() => {
  console.log(1)
}, 300)
​
setTimeout(() => {
  cancel()
}, 1000)

setInterval模拟setTimeout

const simulateSetTimeout = (fn, timeout) => {
  let timer = null
​
  timer = setInterval(() => {
    clearInterval(timer)
    fn()
  }, timeout)
​
  return () => clearInterval(timer)
}
​
const cancel = simulateSetTimeout(() => {
  console.log(1)
}, 1000)
​
setTimeout(() => {
  cancel()
}, 1100)

正则模拟实现trim方法

String.prototype.strim1 = function () {
  return this.replace(/^\s+|\s+$/g, '')
}
String.prototype.strim2 = function () {
  return this.replace(/^\s+(.*?)\s+$/, '$1')
}
​
let str = '    aaaa   'console.log(str.length)
​
console.log(str)
console.log(str.strim1().length)
console.log(str.strim2().length)

sleep

const sleep = (func, delay) => {
  return new Promise((resolve) => {
    setTimeout(() => {
      resolve(func())
    }, delay)
  })
}
​
const consoleStr = (str) => {
  return () => {
    console.log(str)
    return str
  }
}
​
const doFns = async () => {
  const name = await sleep(consoleStr('前端胖头鱼'), 1000)
  const sex = await sleep(consoleStr('boy'), 1000)
  const age = await sleep(consoleStr(100), 1000)
​
  console.log(name, sex, age)
}
​
doFns()

AJAX

const getJSON = function (url) {
  return new Promise((resolve, reject) => {
    const xhr = new XMLHttpRequest();
    xhr.open("GET", url, false);
    xhr.setRequestHeader("Content-Type", "application/json");
    xhr.onreadystatechange = function () {
      if (xhr.readyState !== 4) return;
      if (xhr.status === 200 || xhr.status === 304) {
        resolve(xhr.responseText);
      } else {
        reject(new Error(xhr.responseText));
      }
    };
    xhr.send();
  });
};
​

实现 async

// 定义了一个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);
​

算法

数组

map forEach 区别
  • map 会返回一个新数组,forEach不会。
  • Map 和 forEach 对于基础数据类型数组不会改变原始数组,对于引用类型都会改变原始数组。对引用类型的元素的修改会直接反映在原始数组中。因为引用类型存储的是引用-内存地址,而非值的本身。
  • forEach 可以通过 return 跳出循环,而map 不行
  • Map 支持链式调用,而forEach不行

这里注意:forEach 方法无法通过使用 break 语句来中断循环。 break 语句用于中断循环的功能只适用于 for 循环或 while 循环

操作数组的方法有哪些?

push() pop() sort() splice() unshift() shift() reverse() concat() join() map() filter() ervery() some() reduce() isArray() findIndex()

改变原数组:

  1. push
  2. pop
  3. shift
  4. unshift
  5. reverse
  6. sort
  7. splice

不改变原数组:

  1. concat
  2. join
  3. slice
  4. filter
  5. reduce
  6. find
  7. findIndex

实现数组去重

一:Set结构
const arr = [1, 2, 3, 5, 1, 5, 9, 1, 2, 8]
const uniqueArr = [...new Set(arr)]
二:使用 filter 和 indexOf
const uniqueArr = arr.filter((item, index) => {
  return arr.indexOf(item) === index
})
三:使用Map存储
function uniqueArray(array) {
  let map = {}
  let res = []
  for (var i = 0; i < array.length; i++) {
    if (!map.hasOwnProperty([array[i]])) {
      map[array[i]] = 1
      res.push(array[i])
    }
  }
  return res
}

输入为两个一维数组,将这两个数组合并,去重,不要求排序,返回一维数组

function dealArr(arr1: any[], arr2: any[]): any[] {
  return Array.from(new Set([...arr1.flat(), ...arr2.flat()]));
}
​
const arr1 = ["a", 1, 2, 3, ["b", "c", 5, 6]];
const arr2 = [1, 2, 4, "d", ["e", "f", "5", 6, 7]];
​
console.log("dealArr(arr1, arr2 ); :>> ", dealArr(arr1, arr2)); // dealArr(arr1, arr2 ); :>>  [ 'a', 1, 2, 3,'b', 'c', 5,6, 4, 'd', 'e', 'f','5', 7]

如何找到数组中出现次数最多的字符串

const findCountMax = arr => {
    let obj = {};
    arr.forEach(item=>{
        if(obj[item]) {
            obj[item]++
        }else {
            obj[item] =1
        }
    })
    let result = {count:0,str:''};
    for(let item in obj){
        if(obj[item]>result.count) {
            result.count = obj[item];
            result.str = item;
        }
    }
    return result.str;
}

实现reduce方法

/*reduce函数于累积数组元素并返回一个最终的累积结果,接受一个回调函数和一个初始值作为参数。回调函数将接受四个参数:累积结果(上一次的回调返回值或初始值)、当前元素、当前索引和原始数组。
*/
function myReduce(arr, callback, initialValue) {
  let accumulator = initialValue !== undefined ? initialValue : arr[0];
  const startIndex = initialValue !== undefined ? 0 : 1;
​
  for (let i = startIndex; i < arr.length; i++) {
    accumulator = callback(accumulator, arr[i], i, arr);
  }
​
  return accumulator;
}
实现数组 push 方法
Array.prototype.myPush = function () {
  // 循环遍历 arguments.length 也就是传入的参数个数
  for (let index = 0; index < arguments.length; index++) {
    // this.length 指向调用这个方法的数组 获取数组的长度 将当前元素放入最后一个
    this[this.length] = arguments[index]
  }
​
  return this.length
}
// let arr = [1,2,3]
// arr.myPush(6, 4, 5)
实现数组 filter 方法
/*filter方法创建给定数组一部分的浅拷贝,其包含通过所提供函数实现的测试的所有元素。*/
Array.prototype.myFilter = function (callback) {
  if (!callback || typeof callback !== 'function') {
    throw Error('callback must be a function ')
  }
​
  const res = []
  // this.length 指向调用方法的数组
  for (let index = 0; index < this.length; index++) {
    // 执行 callback 函数传入数据 如果函数返回 true 就将当前数据放入 res 中
    callback(this[index], index) && res.push(this[index])
  }
  return res
}
​
// let arr = [1, 2, 3]
// console.log(
//   arr.myFilter((item, index) => {
//     console.log('item', item)
//     console.log('index', index)
//     return item > 2
//   })
// )
实现数组 map 方法
/*map创建一个新数组,这个新数组由原数组中的每个元素都调用一次提供的函数后的返回值组成*/
Array.prototype.myMap = function (callback) {
  if (!callback || typeof callback !== 'function') {
    throw Error('callback must be a function ')
  }
  const result = []
​
  // this.length 指向调用方法的数组
  for (let index = 0; index < this.length; index++) {
    result.push(callback(this[index], index))
  }
  return result
}
​
// const map1 = array1.map((x) => x * 2)
// console.log(map1)
两数之和
// https://leetcode-cn.com/problems/two-sum/
​
/**
 * 
​
给定一个整数数组 nums 和一个整数目标值 target,请你在该数组中找出 和为目标值 target  的那 两个 整数,并返回它们的数组下标。
你可以假设每种输入只会对应一个答案。但是,数组中同一个元素在答案里不能重复出现。
你可以按任意顺序返回答案。
示例 1:
输入:nums = [2,7,11,15], target = 9
输出:[0,1]
解释:因为 nums[0] + nums[1] == 9 ,返回 [0, 1] 。
示例 2:
输入:nums = [3,2,4], target = 6
输出:[1,2]
 * 
 */
const twoSum = (nums, target) => {
  const cacheMap = {}
​
  for (let i = 0, len = nums.length; i < len; i++) {
    const diff = target - nums[ i ]
    
    if (cacheMap[ diff ] !== undefined) {
      return [ cacheMap[ diff ], i ]
    } else {
      cacheMap[ nums[ i ] ] = i
    }
  }
}
​
console.log(twoSum([ 2, 7, 11, 15 ], 22))
合并两个有序数组
// https://leetcode-cn.com/problems/merge-sorted-array/
/*** 给你两个有序整数数组 nums1 和 nums2,请你将 nums2 合并到 nums1 中,使 nums1 成为一个有序数组。
​
初始化 nums1 和 nums2 的元素数量分别为 m 和 n 。你可以假设 nums1 的空间大小等于 m + n,这样它就有足够的空间保存来自 nums2 的元素。
示例 1:
输入:nums1 = [1,2,3,0,0,0], m = 3, nums2 = [2,5,6], n = 3
输出:[1,2,2,3,5,6]
示例 2:
输入:nums1 = [1], m = 1, nums2 = [], n = 0
输出:[1]
 * 
 */
const nums1 = [1,2,3,0,0,0]
const m = 3
const nums2 = [2,5,6]
const n = 3
​
const merge = (num1, m, num2, n) => {
  let i = m - 1
  let j = n - 1
  let k = m + n  -1
​
  while (i >= 0 && j >= 0) {
    if (num1[ i ] > num2[ j ]) {
      num1[ k ] = num1[ i ]
      i--
      k--
    } else {
      num1[ k ] = num2[ j ]
      j--
      k--
    }
  }
​
  while (j >= 0) {
    num1[ j ] = num2[ j ]
    j--
  }
​
  return num1
}
​
console.log(merge(nums1, m, nums2, n))
两个数组的交集
// https://leetcode-cn.com/problems/intersection-of-two-arrays/solution/liang-ge-shu-zu-de-jiao-ji-by-user7746o/
/**
 * 给定两个数组,编写一个函数来计算它们的交集。
示例 1:
输入:nums1 = [1,2,2,1], nums2 = [2,2]
输出:[2]
示例 2:
输入:nums1 = [4,9,5], nums2 = [9,4,9,8,4]
输出:[9,4]
说明:
输出结果中的每个元素一定是唯一的。
我们可以不考虑输出结果的顺序。
 * 
 */
// 直觉做法
const intersection1 = (nums1, nums2) => {
  return [ ...new Set(nums1.filter((it) => nums2.includes(it))) ]
};
// 排序 + 双指针 
const intersection2 = (nums1, nums2) => {
  nums1.sort((a, b) => a - b)
  nums2.sort((a, b) => a - b)
​
  const length1 = nums1.length
  const length2 = nums2.length
​
  let index1 = 0
  let index2 = 0
​
  const intersection = []
​
  while (index1 < length1 && index2 < length2) {
    const num1 = nums1[ index1 ]
    const num2 = nums2[ index2 ]
​
    if (num1 === num2) {
      if (!intersection.length || num1 !== intersection[ intersection.length - 1 ]) {
        intersection.push(num1)
      }
      index1++
      index2++
    } else if (num1 < num2) {
      index1++
    } else {
      index2++
    }
  }
​
  return intersection
}
​
// hash map
const intersection3 = (nums1, nums2) => {
  // const isNums1Min = nums1.length < nums2
  // const nums1Set = new Set(isNums1Min ? nums1 : nums2)
  // const nums2Set = new Set(isNums1Min ? nums2: nums1)
​
  if (nums1.length > nums2.length) {
    return intersection3(nums2, nums1)
  }
​
  const nums1Set = new Set(nums1)
  const nums2Set = new Set(nums2)
  const intersection = new Set()
​
  for (let num of nums1Set) {
    if (nums2Set.has(num)) {
      intersection.add(num)
    }
  }
​
  return [ ... intersection ]
}
​
console.log(intersection1([1,2,2,1], [2,2]))
console.log(intersection1([4,9,5], [9,4,9,8,4]))
​
console.log(intersection2([1,2,2,1], [2,2]))
console.log(intersection2([4,9,5], [9,4,9,8,4]))
​
console.log(intersection3([1,2,2,1], [2,2]))
console.log(intersection3([4,9,5], [9,4,9,8,4]))

删除有序数组的重复项

/*给你一个有序数组 nums ,请你 原地 删除重复出现的元素,使每个元素 只出现一次 ,返回删除后数组的新长度。
输入:nums = [1,1,2]
输出:2, nums = [1,2]
解释:函数应该返回新的长度 2 ,并且原数组 nums 的前两个元素被修改为 1, 2 。不需要考虑数组中超出新长度后面的元素。
*/
​
const removeDuplicates = (nums) => {
  if (nums.length === 0 || nums.length === 1) {
    return nums.length
  }
  const len = nums.length
  let slow = 0
  let fast = 1
​
  while (fast < len) {
    if (nums[ slow ] !== nums[ fast ]) {
      nums[ ++slow ] = nums[ fast ]
    }
    fast++
  }
​
  return slow + 1
}
​
// let nums = [1, 2, 1]
let nums = [0,0,1,1,1,2,2,3,3,4]
​
console.log(removeDuplicates(nums), nums)
判断数组中是否有重复项
/* 给定一个整数数组,判断是否存在重复元素。
输入: [1,1,1,3,3,4,3,2,4,2]
输出: true*/
const containsDuplicate1 = (nums) => {
  const cacheMap = {}
​
  for (const num of nums) {
    if (cacheMap[ num ]) {
      delete cacheMap[ num ]
    } else {
      cacheMap[ num ] = 1
    }
  }
​
  return Object.keys(cacheMap).length < nums.length
}
移动零
输入: [0,1,0,3,12]
输出: [1,3,12,0,0]
// 思路就是将非零的不断往前移动,零的不断往后移动
const moveZeroes = (nums) => {
  let len = nums.length  
  let j = 0
  
  for (let i = 0; i < len; i++) {
    if (nums[ i ] !== 0) {
      let temp = nums[ i ]
​
      nums[ i ] = nums[ j ]
      nums[ j ] = temp
​
      j++
    }
  }
​
  return nums
}
​
moveZeroes([0,1,0,3,12])
搜索插入位置
​
const searchInsert = (nums, target) => {
  let i = 0
  let j = nums.length - 1
  let midIndex = 0
​
  while (i <= j) {
    midIndex = Math.floor((i + j) / 2)
    const midValue = nums[ midIndex ]
​
    if (midValue === target) {
      return midIndex
    } else if (midValue < target) {
      i = midIndex + 1
    } else {
      j = midIndex - 1
    }
  }
​
  return i
}
​
console.log(searchInsert([1,3,5,6], 7))
只出现一次的数字
/* 给定一个非空整数数组,除了某个元素只出现一次以外,其余每个元素均出现两次。找出那个只出现了一次的元素。*/
输入: [4,1,2,1,2]
输出: 4
const singleNumber = (nums) => {
  let ans = 0
​
  for (const num of nums) {
    ans = ans ^ num
  }
​
  return ans
}
​
// console.log(singleNumber([2,2,1]))
console.log(singleNumber([4,1,2,1,2]))
菲波那切数列
const fib = (n) => {
  if (typeof fib[ n ] !== 'undefined') {
    return fib[ n ]
  }
​
  if (n === 0) {
    return 0
  }
​
  if (n === 1 || n === 2) {
    return 1
  }
​
  const res = fib(n -2) + fib(n - 1)
​
  fib[ n ] = res
​
  return res
}
​
console.log(fib(1))
console.log(fib(2))
​
const t1 = Date.now()
console.log(fib(44))
console.log(Date.now() - t1)
有效的括号
/*
输入:s = "()[]{}"
输出:true*/
const isValid = (s) => {
  // 空字符串符合条件
  if (!s) {
    return true
  }
​
  const leftToRight = {
    '(': ')',
    '[': ']',
    '{': '}'
  }
  const stack = []
​
  for (let i = 0, len = s.length; i < len; i++) {
    const ch = s[i]
    const metchCh = leftToRight[ch]
    // 左括号
    if (metchCh) {
      stack.push(metchCh)
    } else {
      // 右括号开始匹配
      // 1. 如果栈内没有左括号,直接false
      // 2. 有数据但是栈顶元素不是当前的右括号
      if (!stack.length || stack.pop() !== ch) {
        return false
      }
    }
  }
​
  // 最后检查栈内还有没有元素,有说明还有未匹配则不符合
  return !stack.length
}
​
const isValid2 = (s) => {
  while (true) {
    let len = s.length
​
    s = s.replace('{}', '').replace('[]', '').replace('()', '')
​
    if (s.length == len) {
      return len === 0
    }
  }
}
​
// ({}})
// (})console.log(isValid('({[]})'))
console.log('2', isValid('({[]})'))

字符串

转化为驼峰命名

var s1 = "get-element-by-id"// 转化为 getElementById
// \w:表示一个字 [0-9a-zA-Z_]var f = function(s) {
    return s.replace(/-\w/g, function(x) {
        return x.slice(1).toUpperCase();
    })
}

实现字符串翻转

String.prototype._reverse = function(a){
    return a.split("").reverse().join("");
}
var obj = new String();
var res = obj._reverse ('hello');
console.log(res);    // olleh

金钱格式化

'123456789.123'格式化为'123,456,789.12'
一:常规:
function formatPrice(price) {
  let priceStr = String(price);
  debugger
  let front = [];
  let back = []
  if (priceStr.includes('.')) {
    front = priceStr.split('.')[0].split('');
    back = priceStr.split('.')[1].slice(0, 2);
  } else {
    front = priceStr.split('')
  }
​
  let result = 0;
  result = front
    .reverse()
    .reduce(
      (prev, cur, index) => {
        console.log(prev, cur, index, index % 3)
        return (index % 3 ? cur : cur + ',') + prev
      }
    )
  if (back.length) {
    result = result + '.' + back.toString();
  }
  return result;
}
二:使用JS内置 API (toLocaleString),实现金额的格式化
const options = {
  style: 'currency',
  currency: 'CNY',
};
(999999.1212).toLocaleString('zh-CN', options); // ¥999,999.12
三:
// 金额转千分位
const formatPrice = (number) => {
  number = '' + number
​
  const [ integer, decimal = '' ] = number.split('.')
​
  return integer.replace(/\B(?=(\d{3})+$)/g, ',') + (decimal ? '.' + decimal : '')
}
​
console.log(formatPrice(123456789.3343))

字符串出现的不重复最长长度-----

/*用一个滑动窗口装没有重复的字符,枚举字符记录最大值即可。用 map 维护字符的索引,遇到相同的字符,把左边界移动过去即可。挪动的过程中记录最大长度:
*/
var lengthOfLongestSubstring = function (s) {
  debugger
  let map = new Map();
  let i = -1
  let res = 0
  let n = s.length
  for (let j = 0; j < n; j++) {
    if (map.has(s[j])) {
      i = Math.max(i, map.get(s[j]))
    }
    res = Math.max(res, j - i)
    map.set(s[j], j)
  }
  return res
};

如何找到字符串中出现次数最多的字符串

const findCountMax = strs => {
  let obj = {};
  for (let item of strs) {
    if (obj[item]) {
      obj[item]++
    } else {
      obj[item] = 1
    }
  }
  let result = { count: 0, str: '' };
  for (let item in obj) {
    if (obj[item] > result.count) {
      result.count = obj[item];
      result.str = item;
    }
  }
  return result.str;
}

验证回文串

/* 给定一个字符串,验证它是否是回文串,只考虑字母和数字字符,可以忽略字母的大小写。
输入: "A man, a plan, a canal: Panama"
输出: true
解释:"amanaplanacanalpanama" 是回文串
*/
const isPalindrome = (str) => {
  str = str.replace(/[^a-zA-Z\d]/g, '').toLowerCase()
  const length = str.length
  let i = 0
  while (i < length / 2) {
    if (str[ i ] !== str[ str.length - 1 - i ]) {
      return false
    }
    i++
  }
  return true
}
console.log(isPalindrome('A man, a plan, a canal: Panama'))
​

反转字符串

/*
*/
// 双指针
const reverseString = (s) => {
  let i = 0
  let j = s.length - 1
​
  while (i < j) {
    let temp = s[ i ]
    
    s[ i ] = s[ j ]
    console.log(i, j, temp, s[ j ], s[ i ])
    s[ j ] = temp
    
    i++
    j--
  }
​
  return s
}
​
console.log(reverseString(["h","e","l","l","o"]))//[ 'o', 'l', 'l', 'e', 'h' ]

字符串中的第一个唯一字符

/* 字符串中的第一个唯一字符
给定一个字符串,找到它的第一个不重复的字符,并返回它的索引。如果不存在,则返回 -1。
s = "leetcode"
提示:你可以假定该字符串只包含小写字母。
返回 0*/
// 利用hash表存储数量进行计数,然后再进行一次计算
const firstUniqChar = (s) => {
  let cacheMap = {}
​
  for (let i = 0, len = s.length; i < len; i++) {
    const value = s[ i ]
    
    cacheMap[ value ] = typeof cacheMap[ value ] !== 'undefined' ? cacheMap[ value ] +1 : 1
  }
​
  for (let i = 0, len = s.length; i < len; i++) {
    if (cacheMap[ s[ i ] ] === 1) {
      return i
    }
  }
​
  return -1
}
​
console.log(firstUniqChar("aadadaad"))

最长公共前缀

const longestCommonPrefix = (strs) => {
  if (strs.length === 0) {
    return ''
  }
​
  let prefix = strs[0]
  const len = strs.length
  const getPrevfix = (str1, str2) => {
    if (str1.length > str2.length) {
      return getPrevfix(str2, str1)
    }
​
    let i = 0
    let substrLen = 0
    const len = str1.length
​
    while (i < len) {
      if (str1[ i ] === str2[ i ]) {
        substrLen++
      } else {
        break
      }
      i++
    }
​
    return str1.substr(0, substrLen)
  }
​
  for (let i = 1; i < len; i++) {
    prefix = getPrevfix(prefix, strs[ i ])
​
    if (prefix === '') {
      return ''
    }
  }
​
  return prefix
}
​
console.log(longestCommonPrefix(["flower","flow","flight"]))//fl

实现模板字符串解析功能

/*let template = '我是{{name}},年龄{{age}},性别{{sex}}';
let data = {
  name: '姓名',
  age: 18
}
render(template, data); // 我是姓名,年龄18,性别undefined*/
​
​
function render(template, data) {
  let computed = template.replace(/{{(\w+)}}/g, function (match, key) {
    return data[key];
  });
  return computed;
}
​

链表

链表反转

function reverseList(head){
  // 初始化prev/cur指针
  let prev = null;
  let cur = head;
  // 开始遍历链表
  while(cur){
    // 暂存后继节点
    let next = cur.next;
    // 修改引用指向
    cur.next = prev;
    // 暂存当前节点
    prev = cur;
    // 移动指针
    cur = next;
  }
  return prev;
};

判断链表是否有环

function hasCycle(head){
  let fast = head;
  let slow = head;
  
  while(fast && fast.next){
    fast = fast.next.next;
    slow = slow.next;
    if(fast == slow) return true;
  }
  return false;
}
​

找链表中点

function middleNode(head){
    let slow = head;
    let fast = head;
    // 遍历链表节点
    while (fast && fast.next) {
        slow = slow.next;
        fast = fast.next.next;
    }
    // 处理链表节点为偶数的情况
    if(fast){
      slow = slow.next;
    }
    return slow;
}
​

链表中环的入口节点

function detectCycle(head){
  let fast = head;
  let slow = head;
  while(fast && fast.next){
    fast = fast.next.next;
    slow = slow.next;
    if(fast ==slow){
      fast = head;
      while(fast!=slow){
        fast = fast.next;
        slow = slow.next;
      }
      return slow
    }
  }
  return null;
}
​

执行

Promise+setTimeout

const testPromise = new Promise((resolve) => {
    setTimeout(() => {
        console.log('准备第一次resolve');
        resolve(1000);
        setTimeout(() => {
            console.log('准备第二次resolve');
            resolve(2000);
        }, 1000);
    }, 1000);
});
​
testPromise.then(val => {
    console.log('first', val);
})
​
testPromise.then(val => {
    console.log('second', val);
})
​
setTimeout(() => {
    testPromise.then(val => {
        console.log('third', val);
    })
}, 5000);
----
输出结果为:
​
准备第一次resolve
first(1000)
second(1000)
准备第二次resolve 
third(1000)