面试第四篇

  

前端

js手撕代码

实现解析url

const url = "https://shanyue.tech?a=3&b=4&c=5"

//解析后得到对象
const qs = {
  a:3;
  b:4;
  c:5;
}

实例.用正则表达式获取query部分,然后用split分割&、=,获取key、value,同一个key出现多次则设为数组。

function parse(url){
  //夹在?和#之间的就是querystring,使用正则表达式匹配/\?([^/?#:]+)?/
  const queryString = url.match(/\?([^/?#:]+)?/)?.[1]
  
  if(!queryString) {
    return {}
  }
  
  queryObj = queryString.split('&').reduce((params,block)=>{
    const [_k,_v='']=block.split('=')
    
    const k = decodeURIComponent(_k);
    const v = decodeURIComponent(_v);
    if(params[k] !== undefined){
      //如果同一个key出现多次设为数组
      params[k] = [].concat(params[k],v);
    }else{
      params[k] = v
    }
    return params
  },{})
  return queryObj
}

手写evenbus

class EventEmeitter {
  constructor() {
    this._events = this._events || new Map(); // 储存事件/回调键值对
    this._maxListeners = this._maxListeners || 10; // 设立监听上限
  }
}

// 触发名为type的事件
EventEmeitter.prototype.emit = function(type, ...args) {
  let handler;
  // 从储存事件键值对的this._events中获取对应事件回调函数
  handler = this._events.get(type);
  if (args.length > 0) {
    handler.apply(this, args);
  } else {
    handler.call(this);
  }
  return true;
};

// 监听名为type的事件
EventEmeitter.prototype.addListener = function(type, fn) {
  // 将type事件以及对应的fn函数放入this._events中储存
  if (!this._events.get(type)) {
    this._events.set(type, fn);
  }
};

// 实例化
const emitter = new EventEmeitter();

// 监听一个名为arson的事件对应一个回调函数
emitter.addListener('arson', man => {
  console.log(`expel ${man}`);
});

// 我们触发arson事件,发现回调成功执行
emitter.emit('arson', 'low-end'); // expel low-end

https://juejin.cn/post/6844903587043082247

简易版

type Event = (data?: any) => any

class EventHub {
    // cache用于存储事件,存储中心
    cache: { [key: string]: Event[] } = {}

    // 接收事件名和事件,同时将其放入到存储中心
    on = (name: string, fn: Event) => {
        this.cache[name] = this.cache[name] || []
        this.cache[name].push(fn)
    }

    // 被订阅的时候,将存储中心对应事件名的所有事件拿出来执行
    emit = (name: string, data?: any) => {
        if(this.cache[name] === undefined) return
        this.cache[name].forEach(fn => {
            fn(data)
        })
    }

    // 取消事件的时候,将被取消的事件从对应事件名中剔除
    off = (name: string, fn: Event) => {
        if(this.cache[name] === undefined) return
        this.cache[name] = this.cache[name].filter(f => f !== fn)
    }
}

测试

import EventHub from './index'

const eventHub = new EventHub()

const f1=(s)=>{
  console.log("被调用",s)
}

eventHub.on('xxx',f1)
eventHub.emit('xxx',)
eventHub.off('xxx',f1)

js继承

原型式继承

实例

function Person(name){
  this.name = name;
  this.className = "person"
}
Person.prototype.getClassName = function(){
  console.log(this.className)
}
function Man(){
}
Man.prototype = new Person();
var man = new Man;

组合继承

实例

function Person(name){
  this.name = name || "default name";
  this.className = "person"
}
Person.prototype.getName = function(){
  console.log(this.name)
}
function Man(name){
  Person.apply(this,arguments)
}
Man.prototype = new Person();
var man1 = new Man("Davin")

寄生式继承

寄生式继承是指创建一个仅用于封装继承过程的函数,该函数在内部以某种形式来增强对象,最后再像真的做了所有事情一样返回对象

寄生组合(分离组合)式继承

通过借用构造函数来继承属性,通过原型链的混成形式继承方法

function Person(name){
  this.name = name;
  this.className = 'person'
}
Person.prototype.getName = function(){
  console.log(this.name)
}
function Man(name){
  Person.apply(this.arguments)
}
Man.prototype = Object.create(Person.prototype)
var man1 = new Man("Davin")

将二维数组转换为树结构

function arrToTreeArr(arr = []){
  let map = new Map();
  for(let i = 0;i<arr.length;i++){
    for(let j = 0;j<arr[i];j++){
      let name = arr[i][j]
      let isRoot = j == 0;
      let hasItem = map.has(name);
      
      if(!hasItem){
        map.set(name,{name,child:[],isRoot })
      }
      
      if(!isRoot && !hasItem){
        let item = map.get(name)
        let pName = arr[i][j-1]
        map.get(pName).child.push(item)
      }
     }
  }
  return [...map.values()].filter(item => item.isRoot)
}

arrToTreeArr(arr)

另一个方法

function toTree(arr){
  const obj = {};
  const res = [];
  for (let i = 0;i<arr.length;i++){
    for(let j = 0;j<arr[i].length;j++){
      const item = arr[i][j];
      if(!obj[item]){
         obj[item] = {
           name:item,
           child:[]
         }
      }
      if(j>0){
        const parent = obj[arr[i][j-1]];
        if(parent){
           if(parent.child.indexOf(obj[item])<0){
              parent.child.push(obj[item]);
           }
        }
      }else{
        if(res.indexOf(obj[item])<0){
           res.push(obj[item]);
        }
      }
    }
  }
  return res;
} 

修改树结构中的值

function treeSelectDataTransformer<T extends Record<string, unknown>>(
  list: null | undefined | T[],
): treeItemType<T>[] {
  return (list || []).map(function mapValueAndTitle(item) {
    const result = {
      title: item?.name || item?.groupName,
      label: item.name,
      id: item.id || item?.groupId,
      children: item.children,
      parentId: item?.parentId,
    } as any;
    if (Array.isArray(result.children)) {
      result.children = result.children.map(mapValueAndTitle);
    }
    return result;
  }) as treeItemType<T>[];
}

根据id返回树结构中的树

function findTree<T extends Record<string, unknown>>(
  treeData: T[] | null | undefined,
  id: ID,
): treeItemType<T> {
  const temp: any[] = [];
  function mapTree(data: any, id: ID) {
    for (let i = 0; i < data.length; i++) {
      const item = data[i];
      if (item.id === id) {
        temp.push(item);
        mapTree(treeData, item.parentId);
        break;
      } else {
        if (Array.isArray(item.children)) {
          mapTree(item.children, id);
        }
      }
    }
  }
  mapTree(treeData, id);
  return _.last(temp) as treeItemType<T>;
}

Promise、Promise.all实现原理、手写promise、promise.all

  • 适合多个异步调用函数,并且多个异步函数的调用的入参和结果都无必然联系,比如多个文件的上传或下载。
  • 多个异步函数的执行只关注成功或失败结果。
  • Promise.all是挂载到Promise类实例上
  • 返回的是一个Promise
  • 需要遍历入参数组中的每一项,判断传入的是不是promise,如果是promise则执行then方法,然后将then方法中的成功回调的data返回,失败则reject
  • 如果入参数组中有基本数值,则直接返回
  • 通过计数器,来判断函数的执行结果

手写promise

function Promise(fn){
  var state = PENDING;  //初始化state为pending,进行中
  
  //状态为成功时执行
  function fullfill(result){
    state = FuLFILLED; 
    value = result;
    handlers.forEach(handle);
    handlers = null;
  }
  //状态为失败时执行
  function reject(error){
    state = REJECTED;
    value = error;
    handlers.forEach(handle);
    handlers = null;
  }
  //resolve方法
  function resolve(result){
    try {
      var then = getThen(result);
      if(then){
        doResolve(then.bind(result),resolve,reject)
        return;
      }
      fulfill(result);
    } catch(e){
      reject(e);
    }
  }
  //.then方法
  this.then = function(onFulfilled,onRejected)z{
    var self = this;
    return new Promise(function(resolve,reject){
      self.done(function(result){
        if(typeof onFullfilled === 'function'){
          try{
            return resolve(onFulfilled(result));
          }catch (ex){
            return reject(ex);
          }
        }else{
            return resolve(result);
        }
      },function(error){
        if(typeof onRejected === 'function'){
          try{
            return resolve(onRejected(error));
          }catch (ex){
            return reject(ex);
          }
        } else{
          return reject(es);
        }
      });
    });
  }
  //.catch方法
  this.catch = function(errorHandle){
    return this.then(null,errorHandle);
  }
}

手写promise.all

Promise.all = function(promises){
  if (!Array.isArray(promises)) {
    return reject(new TypeError('arguments must be an array'));
  }
   let count = 0
   let len = promises.length
   let res = []
   //Promise.all 的参数是一个可迭代对象,如 Array 、 String、Map、Set、包含length属性的对象等,所以应该兼容一下参数。
   return new Promise(function(resolve,reject) {

     	// 这里用for...in,获取键名,不用for...of获取键值
      for(let i in promises){
        // 这里可以直接用promises[i],也可以用Promise.resolve(promises[i]),防止数组中不是promise的情况
        promises[i]
        .then((data)=>{
          // 注意:这里不能用push方法将结果推入原数组,因为promise的返回时间是不固定的,可能先resolve,因此不能使用res.push(data)
          res[i] = data;
          if(++count === len){
            resolve(res)
          }
        }, function(reason) {
          return reject(reason)
        })
      }
   })
}

手写promise.race

Promise.race = function(iterators){
  return new Promise((resolve,reject)=>{
    for (const p of iterators){
      Promise.resolve(p)
      .then((res)=>{
         resolve(res)
      })
      .catch(e=>{
         reject(e)
      })
    }
  })
}

手写promise.any

Promise.any = function(iterators) {
  const promises = Array.from(iterators);
  const num = promises.length;
  const rejectedList = new Array(num);
  let rejectedNum = 0;

  return new Promise((resolve, reject) => {
    promises.forEach((promise, index) => {
      Promise.resolve(promise)
        .then(value => resolve(value))
        .catch(error => {
          rejectedList[index] = error;
          if (++rejectedNum === num) {
            reject(rejectedList);
          }
        });
    });
  });
};

手写promise.allsettled

const formatSettledResult = (success, value) =>
  success
    ? { status: "fulfilled", value }
    : { status: "rejected", reason: value };

Promise.allSettled = function(iterators) {
  const promises = Array.from(iterators);
  const num = promises.length;
  const settledList = new Array(num);
  let settledNum = 0;

  return new Promise(resolve => {
    promises.forEach((promise, index) => {
      Promise.resolve(promise)
        .then(value => {
          settledList[index] = formatSettledResult(true, value);
          if (++settledNum === num) {
            resolve(settledList);
          }
        })
        .catch(error => {
          settledList[index] = formatSettledResult(false, error);
          if (++settledNum === num) {
            resolve(settledList);
          }
        });
    });
  });
};

Promise并发限制

使用promise.all能够确保所有调用的promise对象全部达到resolve状态执行then回调。

但是如果请求的promise对象太多,瞬间发出很多http请求,tcp连接数不足可能造成等待,或者堆积了无数调用栈导致内存溢出。

此时就对http连接数进行限制。

Promise.limitAll = function(promises, limit) {
    return new Promise(resolve => {
        let resolvedCount = 0;
        let count = 0;
        let res = [];
        const len = promises.length;

        function next(p, index) {
            p().then(r => {
                res[index] = r;
                // 记录请求成功的数量
                resolvedCount ++
                // 数组还存在为执行的promise
                if (promises.length) {
                    const p = promises.shift()
                    next(p, count)
                    count ++
                } else if(resolvedCount === len) {
                    resolve(res)
                }
            })
        }
        // 1. 设置最开始的并发请求为最大值或全部promise数组
        while (count < limit && promises.length) {
            const p = promises.shift()
            next(p, count)
            count ++
        }
    })
}

项目中也可以直接使用npm包,比如async-pool,es6-promise-pool,p-limit。

并发调用只执行一次

定义一个同步函数对传入的数组进行遍历乘二操作,同时每执行一次就会给 executeCount 累加。最终我们需要实现一个 batcher 函数,使用其对该同步函数包装后,实现每次调用依旧返回预期的二倍结果,同时还需要保证 executeCount 执行次数为1

let executeCount = 0
const fn = nums => {
  executeCount++
  return nums.map(x => x * 2)
}

const batcher = f => {
  // todo 实现 batcher 函数
  let nums = [];
  const p = Promise.resolve().then(_ => f(nums));

  return arr => {
    let start = nums.length;
    nums = nums.concat(arr);
    let end = nums.length;
    return p.then(ret => ret.slice(start, end));
  };
}

const batchedFn = batcher(fn);

const main = async () => {
  const [r1, r2, r3] = await Promise.all([
    batchedFn([1,2,3]),
    batchedFn([4,5]),
    batchedFn([7,8,9])
  ]);

  //满足以下 test case
  assert(r1).tobe([2, 4, 6])
  assert(r2).tobe([8, 10])
  assert(r3).tobe([14, 16, 18])
  assert(executeCount).tobe(1)
}

由于 Promise 的微任务队列效果将 _ => f(nums) 推入微任务队列,待主线程的三次 batcherFn() 调用都执行完成之后才会执行。之后 p 的状态变为 fulfilled 后继续完成最终 slice 的操作

最终分析下来,其实这道理的本质就是要通过某些方法将 fn() 函数的执行后置到主线程执行完毕,至于是使用宏任务还是微任务队列,就看具体的需求了。

https://segmentfault.com/a/1190000039406198?sort=newest

实现一个lazy_man

题目:

实现一个LazyMan,可以按照以下方式调用: LazyMan(“Hank”)输出: Hi! This is Hank!

LazyMan(“Hank”).sleep(10).eat(“dinner”)输出 Hi! This is Hank! //等待10秒.. Wake up after 10 Eat dinner~

LazyMan(“Hank”).eat(“dinner”).eat(“supper”)输出 Hi This is Hank! Eat dinner~ Eat supper~

LazyMan(“Hank”).sleepFirst(5).eat(“supper”)输出 //等待5秒 Wake up after 5 Hi This is Hank! Eat supper 以此类推。

考察知识点:闭包事件轮询机制链式调用队列,ES6实现方式

class _LazyMan {
  constructor(name) {
    this.tasks = [];
    const task = () => {
      console.log(`Hi! This is ${name}`);
      this.next();
    }
    this.tasks.push(task);
    setTimeout(() => {               // 把 this.next() 放到调用栈清空之后执行
      this.next();
    }, 0);
  }

  next() {
    const task = this.tasks.shift(); // 取第一个任务执行
    task && task();
  }

  sleep(time) {
    this._sleepWrapper(time, false);
    return this;                     // 链式调用
  }

  sleepFirst(time) {
    this._sleepWrapper(time, true);
    return this;
  }

  _sleepWrapper(time, first) {
    const task = () => {
      setTimeout(() => {
        console.log(`Wake up after ${time}`);
        this.next();
      }, time * 1000)
    }
    if (first) {
      this.tasks.unshift(task);     // 放到任务队列顶部
    } else {
      this.tasks.push(task);        // 放到任务队列尾部
    }
  }

  eat(name) {
    const task = () => {
      console.log(`Eat ${name}`);
      this.next();
    }
    this.tasks.push(task);
    return this;
  }
}

function LazyMan(name) {
  return new _LazyMan(name);
}

如果用 ES5 来写要在维护 this 方面多写一些代码。

js队列实现

基本队列、优先队列和循环队列

基本队列的六个方法:

①向队列(尾部)中添加元素(enqueue)、②(从队列头部)删除元素(dequeue)、③查看队列头部的元素(front)、④查看队列是否为空(isEmpty)、⑤查看队列的长度(size)、⑥查看队列(print) 等 6 个方法

function Queue() {
        //初始化队列(使用数组实现)
        var items = [];

        //向队列(尾部)中插入元素
        this.enqueue = function(element) {
             items.push(element);
        }

        //从队列(头部)中弹出一个元素,并返回该元素
        this.dequeue = function() {
            return items.shift();
        }

        //查看队列最前面的元素(数组中索引为0的元素)
        this.front = function() {
            return items[0];
        }

        //查看队列是否为空,如果为空,返回true;否则返回false
        this.isEmpty = function() {
            return items.length == 0;
        }

        //查看队列的长度
        this.size = function() {
            return items.length;
        }

        //查看队列
        this.print = function() {
            //以字符串形势返回
            return items.toString();
        }
    }

优先队列

在优先队列中,元素的添加或者删除是基于优先级的。实现优先队列有两种方式:①优先添加,正常出列;②正常添加,优先出列、

优先添加,正常出列的例子

function PriorityQueue() {
        var items = [];
        //需要插入队列的元素(该元素为对象,包括值和优先级)
        function QueueElement(element, priority) {
            this.element = element;
            this.priority = priority;
        }

        //插入元素到队列中的方法
        this.enqueue = function (element, priority) {
            //需要插入队列的元素
            var queueElement = new QueueElement(element, priority);

            if(this.isEmpty()) {
                //当队列为空时,直接往队列中添加元素
                items.push(queueElement);
            }else{
                //当队列不为空时,遍历队列中的元素,当需要添加的元素的优先级小于(队列中)当前元素的优先级,就把该元素插入到当前元素之前
                var added = false;
                for(var i = 0; i < items.length; i++){
                    if(queueElement.priority < items[i].priority) {
                        items.splice(i, 0, queueElement);
                        added = true;
                        break;//终止队列循环
                    }
                }
                //当需要添加的元素的优先级比队列中任何一个元素的优先级都要高时,把该元素插入到队列的末尾
                if(!added){
                    items.push(queueElement);
                }
            }
        }

        //查看队列是否为空,如果为空,返回true;否则返回false
        this.isEmpty = function() {
            return items.length == 0;
        }    

        //查看队列
        this.print = function() {
            return items;
        }        
    }

    var priorityQueue = new PriorityQueue();

循环队列的经典问题:

约瑟夫环问题:

一群孩子围成一圈,每次传递 n 个数,停下来时手里拿花的孩子被淘汰,直到队伍中只剩下一个孩子,即胜利者。

循环队列,每次循环的时候(从队列头部)弹出一个孩子,再把这个孩子加入到队列的尾部,循环 n 次,循环停止时弹出队列头部的孩子(被淘汰),直到队列中只剩下一个孩子。

基于基本队列实现循环队列

//循环队列
    //@param Obj nameList 名单
    //@param Int num 指定的传递次数
    function hotPotato(nameList, num) {

        var queue = new Queue();

        //把名单插入队列
        for(var i = 0; i < nameList.length; i++) {
            queue.enqueue(nameList[i]);
        }

        //淘汰者的名字初始值
        var eliminated = '';

        //当队列里的人数大于1人时,继续传递
        while(queue.size() > 1) {
            for(var i = 0; i < num; i++) {
                //每次把队列头部弹出的队员再次插入队列的尾部,行程一个循环队列
                queue.enqueue(queue.dequeue());
            }
            //当循环停止时,即到了指定的传递次数时,弹出队列头部的队员
            eliminated = queue.dequeue();
            console.log(eliminated + '被淘汰');
        }

        //当队列中只剩下一个队员时,即是胜利者
        return queue.dequeue();
    }

    var names = ['dee', 'death mask', 'saga', 'mu', 'alexis'];
    var winner = hotPotato(names, 7);
    console.log('胜利者是' + winner);

基于generator实现async/await

function asyncToGenerator(generatorFunc) {
    return function() {
      const gen = generatorFunc.apply(this, arguments)
      return new Promise((resolve, reject) => {
        function step(key, arg) {
          let generatorResult
          try {
            generatorResult = gen[key](arg)
          } catch (error) {
            return reject(error)
          }
          const { value, done } = generatorResult
          if (done) {
            return resolve(value)
          } else {
            return Promise.resolve(value).then(val => step('next', val), err => step('throw', err))
          }
        }
        step("next")
      })
    }
}

版本排序

function sortVersion(versions) {
	if (!versions || !versions.length) return [];
	const result = versions.sort((a, b) => {
		const arrA = a.split('.'), arrB = b.split('.');
		const length = Math.max(a.length, b.length);
		for (let i = 0; i < length; i++) {
			const x = Number(arrA[i] || 0);
			const y = Number(arrB[i] || 0);
			if (x - y !== 0) return x - y;
		}
	});
	return result;
}

大数相加

补0

function add(str1,str2){
  let maxLength = Math.max(str1.length,str2.length);
  var a = str1.padStart(maxLength,0);
  var b = str2.padStart(maxLength,0);
  
  let t = 0;
  let f = 0;
  let sum = '';
  for (let i = maxLength -1;i >= 0;i--){
    t = parseInt(a[i]) + parseInt(b[i]) + f;
    f = Math.floor(t / 10);
    sum = t % 10 + sum;
  }
  if(f == 1){
    sum = "1" + sum;
  }
  return sum
}

json下划线与驼峰格式切换格式

// 字符串的下划线格式转驼峰格式,eg:hello_world => helloWorld
function underline2Hump(s) {
  return s.replace(/_(\w)/g, function(all, letter) {
    return letter.toUpperCase()
  })
}

// 字符串的驼峰格式转下划线格式,eg:helloWorld => hello_world
function hump2Underline(s) {
  return s.replace(/([A-Z])/g, '_$1').toLowerCase()
}

// JSON对象的key值转换为驼峰式
obj = JSONparse(ojb)
function jsonToHump(obj) {
  if (obj instanceof Array) {
    obj.forEach(function(v, i) {
      jsonToHump(v)
    })
  } else if (obj instanceof Object) {
    Object.keys(obj).forEach(function(key) {
      var newKey = underline2Hump(key)
      if (newKey !== key) {
        obj[newKey] = obj[key]
        delete obj[key]
      }
      jsonToHump(obj[newKey])
    })
  }
}

// JSON对象的key值转换为下划线格式
function jsonToUnderline(obj) {
  if (obj instanceof Array) {
    obj.forEach(function(v, i) {
      jsonToUnderline(v)
    })
  } else if (obj instanceof Object) {
    Object.keys(obj).forEach(function(key) {
      var newKey = hump2Underline(key)
      if (newKey !== key) {
        obj[newKey] = obj[key]
        delete obj[key]
      }
      jsonToUnderline(obj[newKey])
    })
  }
}

预定问题

选择最低价格预定,如果价格相同,优先选择时间

const firstbook = [
  {
    name: 'a'
    time: '08:00'
  },
  {
    name: 'b',
    time: '12:15'
  }
]

const secondbook = [
  {
    name: 'c',
    time: '12:00'
  },
  {
    name: 'd',
    time: '15:25'
  }
]

const bookprice = {
  'man': {
    'normal':{
			'a': 1,
      'b': 2,
      'c': 3,
      'd': 4,
    },
    'vip': {
      'a': 2,
      'b': 1,
      'c': 3,
      'd': 4,
    }
  },
  'woman': {
    'normal':{
			'a': 1,
      'b': 2,
      'c': 3,
      'd': 4,
    },
    'vip': {
      'a': 2,
      'b': 1,
      'c': 3,
      'd': 4,
    }
  }
}

function judgeDatetype(date) {
  if(date == 'SUN' || date == 'SAT'){
    return 'WEEKENDS'
  }else {
    return 'WEEKDAYS'
  }
}

function durationTotime(clock,standard) {
  const clockarray = clock.split(':')
  const standardarray = standard.split(':')
  moment.duration('01:01:01').as('seconds')
  moment({h:hours, m:minutes, s:seconds}).format('HH:mm:ss')
  return Math.abs(new Date().setUTCHours(clockarray[0],clockarray[1]) - new Date().setUTCHours(standardarray[0],standardarray[1]))/1000;
}

function sorted(array) {
  return array.sort(function(a,b){
    if(a.price != b.price) {
      return a.price - b.price
    }else {
      return a.duration - b.duration
    }
  })
}

function bestbook(type,time,book) {
  const date = time.slice(0,8)
  const dateType = judgeDateType(time.replace(date,''))
  
  book.forEach(element => {
		element.duration = durationTotime(element.time,'12:00')
    element.price = bookprice[type][dateType][element.name]
  })
  
  return sorted(book)
}

function bookPlan(type,gotime,backtime) {
  const sortedgobook = bestbook(type,gotime,firstbook);
  const sortedbackbook = bestbook(type,backtime,secondbook);
  
  console.log(sortedgobook[0].name)
  console.log(sortedbackbook[0].name)
  return [sortedgobook[0].name,sortedbackbook[0].name]
}

function isDuplicate(array){
  const set = new Set(array)
  return set.size !== array.length
}

function findKey(obj,value, compare= (a,b) => a === b){
  return Object.keys(obj).find(k => compare(obj[k],value))
}

两数平均值(无穷大)

x&y+(x^y)>>1

数组根据某属性去重

function arrayUnique(arr,property) {
	let hash = {};
  return arr.reduce(function (item,next) {
		hash[next[property]]? '':hash[next[property]] = true && item.push(next);
    return item;
  },[]);
}

数组根据某属性去重,保留后面的

for循环从后往前

一维数组变二维数组

转为子数组长度为n的二维数组,push方法,

function conversion (arr,n){ 
    let len = arr.length
    let lineNum = len % n === 0 ? len / n : Math.floor(len / n + 1)
    let res = []
    for (let i = 0; i < lineNum; i++) {
      // slice() 方法返回一个从开始到结束(不包括结束)选择的数组的一部分浅拷贝到一个新数组对象。且原始数组不会被修改。
      let temp = baseArray.slice(i * n, i * n + n)
      res.push(temp)
    }
     return res
}

reduce方法

function oneTransTwo(source, num) {
      return source.reduce((v, item, index) => {
        let r = null;
        if (index % num === 0) {
          r = [...v, [JSON.parse(JSON.stringify(item))]];
        } else {
          v[v.length - 1].push(item);
          r = v;
        }
        return r;
      }, []);
};

object-fit各属性实现

object-fit总共有fill、contain、cover、none、scale-down五种方式

function cover(containerSize, elementSize) {
  const containerRatio = containerSize.width / containerSize.height;
  const elementRatio = elementSize.width / elementSize.height;
  
  let width, height 
  
  if(containerRatio > elementRatio) {
    width = containerSize.width;
    height = containerSize.width / elementRadio
  } else {
    width = containerSize.height * elementRadio;
    height = containerSize.height;
  }
  
  return { width, height }
}

function contain(containerSize, elementSize) {
  const containerRatio = containerSize.width / containerSize.height;
  const elementRatio = elementSize.width / elementSize.height;
  
  let width, height
  
  if(containerRatio > elementRatio) {
    width = containerSize.width;
    height = containerSize.width / elementRadio
  } else {
    width = containerSize.height * elementRadio;
    height = containerSize.height;
  }
  
  return { width, height }
}

function fill(containerSize) {
  return containerSize
}

function none(elementSize) {
  return elementSize
}

function scaleDown(containerSize, elementSize) {
  if(elementSize.width > containerSize.width || elementSize.height > containerSize.height) {
    return contain(containerSize, elementSize)
  } else {
    return none(containerSize)
  }
}

Https://xiaojun1994.top/posts/a97a42fc.html

js执行计算密集型任务

根据w3c性能小组,超过50ms的任务就是长任务

由于js是单线程运行,并且js引擎与UI渲染引擎互斥,所以在运行计算密集型任务时,容易造成页面假死的状态。虽然我们可以将任务放到任务队列中,通过异步的方式执行,但这并不能改变js执行时的本质

为了改变这种问题,可以使用一些手段避免

1.使用web-worker执行

2.使用时间分片进行任务分割

时间分片的核心思想是,如果任务不能在不影响用户体验的时间内执行完成,为了不阻塞主线程,这个任务应该让出主线程的控制权,以使浏览器可以处理其他任务,让出控制权意味着停止执行当前任务,让浏览器执行其他任务,随后再回来执行没有执行完的任务。

所以时间分片的目的是为了不阻塞主线程,而实现目的的技术手段是将任务拆分为很多个不超过50ms的小任务分散在宏任务队列中执行

使用requestAnimationFrame和requestIdleCallback。由系统本身去调度异步执行

3.大量渲染DOM使用documentFragments

图片懒加载

let imgList = [...document.querySelectorAll('img')];
let length = imgList.length

const imgLazyLoad = (function() {
	let count = 0;
  
  return 
})

js实现LRU

数组实现

let LRU = function(capacity){
  this.capacity = capacity;
  this.cache = [];
}

LRU.prototype.get = function(key) {
	let index = this.cache.findIndex((item) => item.key === key);
  if(key === -1){
    return -1;
  }
  let value = this.cache[index].value;
  this.cache.splice(index,1);
  this.cache.unshift({
    key,
    value,
  });
  return value;
}

LRU.prototype.put = function(key,value){
	let index = this.cache.findIndex((item) => item.key === key);
  if(index > -1){
    this.cache.splice(index,1)
  }else if(this.cache.length >= this.capacity){
    this.cache.pop()
  }
  this.cache.unshift({key, value})
}

map数据结构实现

// 上述代码来自 LRU 缓存机制-官方
// 时间复杂度 O(1),因为 Map 既能保持键值对,还能记住插入顺序。
var LRUCache = function (capacity) {
  this.cache = new Map();
  this.capacity = capacity;
};

LRUCache.prototype.get = function (key) {
  if (this.cache.has(key)) {
    // 存在即更新
    let temp = this.cache.get(key);
    this.cache.delete(key);
    this.cache.set(key, temp);
    return temp;
  }
  return -1;
};

LRUCache.prototype.put = function (key, value) {
  if (this.cache.has(key)) {
    // 存在即更新(删除后加入)
    this.cache.delete(key);
  } else if (this.cache.size >= this.capacity) {
    // 不存在即加入
    // 缓存超过最大值,则移除最近没有使用的
    // new Map().keys() 返回一个新的 Iterator 对象
    this.cache.delete(this.cache.keys().next().value);
  }
  this.cache.set(key, value);
};

js实现正则表达式通配符

get/post请求实现下载文件

下载文件可以通过get请求或者post请求

get请求直接创建一个a标签

var blob=new Blob([result]);
var link=document.createElement('a');
link.href=window.URL.createObjectURL(blob);
link.download="myFileName.txt";
link.click();

post请求的设置

1.后端设置响应头

Content-disposition: attachment; filename=数据报表.xlsx(表示会直接下载文件,文件名为‘数据报表’)

Content-Type:application/octet-stream

2.写好响应函数

 axios({
   method: 'post',
   url: '/api/main/manage/memberLogAttendance/findPage',
   responseType: 'arraybuffer',
   data: para,
   headers: {
     token: this.token
   }
 }).then(res => {
   console.log('111')
   this.download(res.data) // 注意,这里一定是res.data,不然导出的excel会乱码
   console.log(err)
 })

download(data) {
  if (!data) {
    return
  }
  const url = window.URL.createObjectURL(new Blob([data]))
  let link = document.createElement('a')
  link.style.display = 'none'
  link.href = url
  link.setAttribute('download', '考勤统计.xlsx')
  document.body.appendChild(link)
  link.click()
  URL.revokeObjectURL(link.href) // 释放URL 对象
  document.body.removeChild(link)
  link = null
},

Node

定时堵塞线程

Node中常常会遇到需要延迟的任务,Java中有类似Thread.sleep()实现,在Nodejs中有以下方式实现

1.循环空转(不建议使用)

const start = new Date();
while (new Date() - start < 2000){}

2.Promise+定时器实现sleep

通过Promise+定时器延时执行函数setTimeout的链式依赖实现,本质是创建一个新的Promise对象,待到定时器时间到了执行resolve函数这是then才会执行。这里Nodejs是没有睡眠的,事件循环和V8都是正常运行的,这也是目前比较常用的方式

const sleep = ms => new Promise(resolve => setTimeout(resolve,ms));

3.Node中还能通过util模块提供的promisify方法实现,一种快捷方式

const { promisify } = require("util");
const sleep = promisify(setTimeout);

libuv的执行流程

libuv是一个跨平台异步IO库。因为Nodejs是单线程的,作为服务器,他涉及到IO,而IO是会阻塞的,从而影响性能。所以Nodejs把IO操作交给libuv,保证主线程可以继续处理其他事情。Libuv主要是利用系统提供的事件驱动模块解决网络异步IO,利用线程池解决文件IO。另外还实现了定时器,对进程,线程等使用进行了封装。

libuv执行流程:

1.初始化libuv loop,新建一个uvloopt* loop。loop中保存了各个阶段对应的数据结构。

2.初始化tcp,建立、绑定、监听

3.设置客户端连接后的回调函数,再设置接收到客户端数据之后的回调函数

4.执行uv_run函数进入死循环。

5.libuv在每一轮循环里处理各个阶段。

Node的启动流程

1 注册内置c++模块(通过process.binding函数使用内置c++模块)。

2 新建process的c++对象,设置一系列属性(binding),然后挂载到全局。

3 执行bootstrap_node.js,初始化和挂载nextTick,setTimeout等函数,然后加载用户js,编译执行。

4 调用libuv开始事件循环。

setimmediately执行原理

1.nodejs启动的时候注册了check阶段的一个c++层回调是CheckImmediate,该函数再执行js回调processImmediate

2 用户调用setImmediate,生成一个节点插到双向链表。返回。

3 uv_run在check阶段。执行回调。setImmediate和setTimeout的关系这两个其实没什么关系,对应的阶段也不一样。

cluster模块原理

cluster可以以master-worker模式启动多个应用实例。

特点:端口只占用一个。

对于端口只用一个,cluster源码中有cluster.getServer方法

该方法向master进程注册worker。若master进程第一次接收到监听此端口下的worker,则起一个内部TCP服务器,来承担监听该端口的职责,并在master中记录worker

Hack掉worker进程中的listen方法中的监听端口的服务,使其不再承担该责任

cluster默认的复杂均衡策略为轮训

master是如何将接收的请求传递至worker中进行处理然后响应?

所有请求先统一经过内部TCP服务器

在内部TCP服务器的请求处理逻辑中,由负载均衡挑选出一个worker进程,将其发送一个newcon内部消息,随消息发送客户端句柄

Worker进程接收到此内部消息,根据客户端句柄创建socket实例,执行具体业务逻辑

Node高并发原理

Nodejs利用单线程模型省去了系统维护和切换多进程/线程的开销,同时多路复用的I/O模型可以让Nodejs的单线程不会堵塞在某一个连接上。在高并发场景下,nodejs应用只需要创建和管理多个客户端连接对应的socket描述符而不需要创建对应的线程/进程,系统开销大大减少,所以能同时处理更多的客户端连接

Nodejs并不能提升底层的真正I/O操作的效率,如果底层I/O成为系统的性能瓶颈,Nodejs依然无法解决。在这种情况下, Nodejs依然可以接收高并发请求,但如果需要处理大量慢IO操作,比如读写磁盘等,仍然可能造成系统过载,因此并不能简单地通过单线程+非阻塞IO模型来解决

CPU密集型应用可能会让nodejs的单线程模型成为性能瓶颈

Nodejs适合高并发处理少量业务逻辑或者快I/O

express与koa比较

koa使用async-await执行异步,并使用ctx上下文执行req和response

express使用传统的call back回调函数,和node原生的req和res

中间件响应机制

koa在中间件处理完成后的最外层响应,express是立即响应(中间件机制不同)

package-lock.json的作用

package-lock.json的官方定义:

package-lock.json它会在npm更改node_modules目录树或者package.json时自动生成的,它准确的描述了当前项目npm包的依赖树,并且在随后的安装中会根据package-lock.json来安装,保证是相同的一个依赖树,不考虑这个过程中是否有某个依赖有小版本的更新。

如果你有浏览它,会发现它长得类似package.json的依赖,但是比它复杂多了。package-lock.json是一个包含你所有依赖的巨大列表, 它包含明确的版本号, 依赖的获取地址, 一个用于验证完整性和正确性的哈希值, 以及这个依赖本身所需要的依赖

生成:

默认,当我们在一个项目中npm install时候(前提该项目有package.json文件),安装完成后,会自动生成一个package-lock.json文件(位置和之前的package.json文件同级)。该文件里面记录了package.json依赖的模块,以及依赖的依赖。并且给每个依赖标明了版本, 获取地址和哈希值, 使得每次安装都会出现相同的结果. 不管你在什么机器上面或什么时候安装。

当我们下次再npm install时候,npm发现如果项目中有package-lock.json文件,会根据package-lock.json里的内容来处理和安装依赖而不再根据package.json.

没有package-lock.json带来的问题

执行npm init后,安装express:npm install express — save。我撰文时express最高版本是4.15.4。所以“express”: “^4.15.4”被写进package.json里,依赖也已经装好了。不过第二天,可能express的维护者发布了一个bugfix版本,所以最新版本又变成了4.15.5。如果这时我同事clone了这个项目,并且执行npm install,因为4.15.5的大版本号没变,所以他装的版本就会是最新的4.15.5。我们都安装了express,不过是不同的版本。按道理讲他们是互相兼容的,但是可能好巧不巧,这个被修复的bugfix刚好就影响到我们使用的功能了,那么我们分别运行自己的项目时就会出现不同的结果。这是个问题!

npm5.x.x以前,package.json是项目的唯一真理来源。package.json说啥就是啥!npm使用者喜欢并习惯仅维护package.json。但是,当package-lock出现后,它和很多人期望的背道而驰了!如果有同一份package和package-lock,package.json的修改并不会影响到package-lock。

于是就出现了更改package.json后,安装未生效的情况,需要移除package-lock再生成一份,由此引发了很多争论。可以从这份有趣的reop时间线上看出端倪。有的人觉得还是得以package.json为准,有的人又觉得既然package-lock被创造出来了,那就以新的为准呗。最后这份争论以PR#17508划上句号。npm决定,如果package.json上发生过改动,安装时package.json的权重就会超过package-lock。这个决定随着npm v5.1.0发布,自2017年7月5日起即日执行~

如果你觉得我的文章对你有帮助的话,希望可以推荐和交流一下。欢迎關注和 Star 本博客或者关注我的 Github