zl程序教程

您现在的位置是:首页 >  其他

当前栏目

高级前端常考手写面试题合集5

面试题前端 高级 手写 合集 常考
2023-06-13 09:16:37 时间

解析 URL Params 为对象

let url = 'http://www.domain.com/?user=anonymous&id=123&id=456&city=%E5%8C%97%E4%BA%AC&enabled';
parseParam(url)
/* 结果
{ user: 'anonymous',
  id: [ 123, 456 ], // 重复出现的 key 要组装成数组,能被转成数字的就转成数字类型
  city: '北京', // 中文需解码
  enabled: true, // 未指定值得 key 约定为 true
}
*/
function parseParam(url) {
  const paramsStr = /.+\?(.+)$/.exec(url)[1]; // 将 ? 后面的字符串取出来
  const paramsArr = paramsStr.split('&'); // 将字符串以 & 分割后存到数组中
  let paramsObj = {};
  // 将 params 存到对象中
  paramsArr.forEach(param => {
    if (/=/.test(param)) { // 处理有 value 的参数
      let [key, val] = param.split('='); // 分割 key 和 value
      val = decodeURIComponent(val); // 解码
      val = /^\d+$/.test(val) ? parseFloat(val) : val; // 判断是否转为数字

      if (paramsObj.hasOwnProperty(key)) { // 如果对象有 key,则添加一个值
        paramsObj[key] = [].concat(paramsObj[key], val);
      } else { // 如果对象没有这个 key,创建 key 并设置值
        paramsObj[key] = val;
      }
    } else { // 处理没有 value 的参数
      paramsObj[param] = true;
    }
  })

  return paramsObj;
}

实现forEach方法

Array.prototype.myForEach = function(callback, context=window) {
  // this=>arr
  let self = this,  
      i = 0,
      len = self.length;

  for(;i<len;i++) {
    typeof callback == 'function' && callback.call(context,self[i], i)
   }
}

设计一个方法提取对象中所有value大于2的键值对并返回最新的对象

实现:

var obj = { a: 1, b: 3, c: 4 }
foo(obj) // { b: 3, c: 4 }

方法有很多种,这里提供一种比较简洁的写法,用到了ES10Object.fromEntries()

var obj = { a: 1, b: 3, c: 4 }
function foo (obj) {
  return Object.fromEntries(
    Object.entries(obj).filter(([key, value]) => value > 2)
  )
}
var obj2 = foo(obj) // { b: 3, c: 4 }
console.log(obj2)
// ES8中 Object.entries()的作用:
var obj = { a: 1, b: 2 }
var entries = Object.entries(obj); // [['a', 1], ['b', 2]]
// ES10中 Object.fromEntries()的作用:
Object.fromEntries(entries); // { a: 1, b: 2 }

实现map方法

  • 回调函数的参数有哪些,返回值如何处理
  • 不修改原来的数组
Array.prototype.myMap = function(callback, context){
  // 转换类数组
  var arr = Array.prototype.slice.call(this),//由于是ES5所以就不用...展开符了
      mappedArr = [], 
      i = 0;

  for (; i < arr.length; i++ ){
    // 把当前值、索引、当前数组返回去。调用的时候传到函数参数中 [1,2,3,4].map((curr,index,arr))
    mappedArr.push(callback.call(context, arr[i], i, this));
  }
  return mappedArr;
}

实现一个简易的MVVM

实现一个简易的MVVM我会分为这么几步来:

  1. 首先我会定义一个类Vue,这个类接收的是一个options,那么其中可能有需要挂载的根元素的id,也就是el属性;然后应该还有一个data属性,表示需要双向绑定的数据
  2. 其次我会定义一个Dep类,这个类产生的实例对象中会定义一个subs数组用来存放所依赖这个属性的依赖,已经添加依赖的方法addSub,删除方法removeSub,还有一个notify方法用来遍历更新它subs中的所有依赖,同时Dep类有一个静态属性target它用来表示当前的观察者,当后续进行依赖收集的时候可以将它添加到dep.subs中。
  3. 然后设计一个observe方法,这个方法接收的是传进来的data,也就是options.data,里面会遍历data中的每一个属性,并使用Object.defineProperty()来重写它的getset,那么这里面呢可以使用new Dep()实例化一个dep对象,在get的时候调用其addSub方法添加当前的观察者Dep.target完成依赖收集,并且在set的时候调用dep.notify方法来通知每一个依赖它的观察者进行更新
  4. 完成这些之后,我们还需要一个compile方法来将HTML模版和数据结合起来。在这个方法中首先传入的是一个node节点,然后遍历它的所有子级,判断是否有firstElmentChild,有的话则进行递归调用compile方法,没有firstElementChild的话且该child.innderHTML用正则匹配满足有/\{\{(.*)\}\}/项的话则表示有需要双向绑定的数据,那么就将用正则new Reg('\\{\\{\\s*' + key + '\\s*\\}\\}', 'gm')替换掉是其为msg变量。
  5. 完成变量替换的同时,还需要将Dep.target指向当前的这个child,且调用一下this.opt.data[key],也就是为了触发这个数据的get来对当前的child进行依赖收集,这样下次数据变化的时候就能通知child进行视图更新了,不过在最后要记得将Dep.target指为null哦(其实在Vue中是有一个targetStack栈用来存放target的指向的)
  6. 那么最后我们只需要监听documentDOMContentLoaded然后在回调函数中实例化这个Vue对象就可以了

coding :

需要注意的点:

  • childNodes会获取到所有的子节点以及文本节点(包括元素标签中的空白节点)
  • firstElementChild表示获取元素的第一个字元素节点,以此来区分是不是元素节点,如果是的话则调用compile进行递归调用,否则用正则匹配
  • 这里面的正则真的不难,大家可以看一下

完整代码如下:

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <meta http-equiv="X-UA-Compatible" content="ie=edge" />
    <title>MVVM</title>
  </head>
  <body>
    <div id="app">
      <h3>姓名</h3>
      <p>{{name}}</p>
      <h3>年龄</h3>
      <p>{{age}}</p>
    </div>
  </body>
</html>
<script>
  document.addEventListener(
    "DOMContentLoaded",
    function () {
      let opt = { el: "#app", data: { name: "等待修改...", age: 20 } };
      let vm = new Vue(opt);
      setTimeout(() => {
        opt.data.name = "jing";
      }, 2000);
    },
    false
  );
  class Vue {
    constructor(opt) {
      this.opt = opt;
      this.observer(opt.data);
      let root = document.querySelector(opt.el);
      this.compile(root);
    }
    observer(data) {
      Object.keys(data).forEach((key) => {
        let obv = new Dep();
        data["_" + key] = data[key];

        Object.defineProperty(data, key, {
          get() {
            Dep.target && obv.addSubNode(Dep.target);
            return data["_" + key];
          },
          set(newVal) {
            obv.update(newVal);
            data["_" + key] = newVal;
          },
        });
      });
    }
    compile(node) {
      [].forEach.call(node.childNodes, (child) => {
        if (!child.firstElementChild && /\{\{(.*)\}\}/.test(child.innerHTML)) {
          let key = RegExp.$1.trim();
          child.innerHTML = child.innerHTML.replace(
            new RegExp("\\{\\{\\s*" + key + "\\s*\\}\\}", "gm"),
            this.opt.data[key]
          );
          Dep.target = child;
          this.opt.data[key];
          Dep.target = null;
        } else if (child.firstElementChild) this.compile(child);
      });
    }
  }

  class Dep {
    constructor() {
      this.subNode = [];
    }
    addSubNode(node) {
      this.subNode.push(node);
    }
    update(newVal) {
      this.subNode.forEach((node) => {
        node.innerHTML = newVal;
      });
    }
  }
</script>

简化版2

function update(){
  console.log('数据变化~~~ mock update view')
}
let obj = [1,2,3]
// 变异方法 push shift unshfit reverse sort splice pop
// Object.defineProperty
let oldProto = Array.prototype;
let proto = Object.create(oldProto); // 克隆了一分
['push','shift'].forEach(item=>{
  proto[item] = function(){
    update();
    oldProto[item].apply(this,arguments);
  }
})
function observer(value){ // proxy reflect
  if(Array.isArray(value)){
    // AOP
    return value.__proto__ = proto;
    // 重写 这个数组里的push shift unshfit reverse sort splice pop
  }
  if(typeof value !== 'object'){
    return value;
  }
  for(let key in value){
    defineReactive(value,key,value[key]);
  }
}
function defineReactive(obj,key,value){
  observer(value); // 如果是对象 继续增加getter和setter
  Object.defineProperty(obj,key,{
    get(){
        return value;
    },
    set(newValue){
        if(newValue !== value){
            observer(newValue);
            value = newValue;
            update();
        }
    }
  })
}
observer(obj); 
// AOP
// obj.name = {n:200}; // 数据变了 需要更新视图 深度监控
// obj.name.n = 100;
obj.push(123);
obj.push(456);
console.log(obj);

手写常见排序

冒泡排序

冒泡排序的原理如下,从第一个元素开始,把当前元素和下一个索引元素进行比较。如果当前元素大,那么就交换位置,重复操作直到比较到最后一个元素,那么此时最后一个元素就是该数组中最大的数。下一轮重复以上操作,但是此时最后一个元素已经是最大数了,所以不需要再比较最后一个元素,只需要比较到 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;
}

快速排序

快排的原理如下。随机选取一个数组中的值作为基准值,从左至右取值与基准值对比大小。比基准值小的放数组左边,大的放右边,对比完成后将基准值和第一个比基准值大的值交换位置。然后将数组以基准值的位置分为两部分,继续递归以上操作

ffunction 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));
}

选择排序

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]));

插入排序

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]));

参考 前端进阶面试题详细解答

请实现一个 add 函数,满足以下功能

add(1);             // 1
add(1)(2);      // 3
add(1)(2)(3);// 6
add(1)(2, 3); // 6
add(1, 2)(3); // 6
add(1, 2, 3); // 6
function add(...args) {
  // 在内部声明一个函数,利用闭包的特性保存并收集所有的参数值
  let fn = function(...newArgs) {
   return add.apply(null, args.concat(newArgs))
  }

  // 利用toString隐式转换的特性,当最后执行时隐式转换,并计算最终的值返回
  fn.toString = function() {
    return args.reduce((total,curr)=> total + curr)
  }

  return fn
}

考点:

  • 使用闭包, 同时要对JavaScript 的作用域链(原型链)有深入的理解
  • 重写函数的 toSting()方法
// 测试,调用toString方法触发求值

add(1).toString();             // 1
add(1)(2).toString();      // 3
add(1)(2)(3).toString();// 6
add(1)(2, 3).toString(); // 6
add(1, 2)(3).toString(); // 6
add(1, 2, 3).toString(); // 6

数组去重方法汇总

首先:我知道多少种去重方式

1. 双层 for 循环

function distinct(arr) {
    for (let i=0, len=arr.length; i<len; i++) {
        for (let j=i+1; j<len; j++) {
            if (arr[i] == arr[j]) {
                arr.splice(j, 1);
                // splice 会改变数组长度,所以要将数组长度 len 和下标 j 减一
                len--;
                j--;
            }
        }
    }
    return arr;
}

思想: 双重 for 循环是比较笨拙的方法,它实现的原理很简单:先定义一个包含原始数组第一个元素的数组,然后遍历原始数组,将原始数组中的每个元素与新数组中的每个元素进行比对,如果不重复则添加到新数组中,最后返回新数组;因为它的时间复杂度是O(n^2),如果数组长度很大,效率会很低

2. Array.filter() 加 indexOf/includes

function distinct(a, b) {
    let arr = a.concat(b);
    return arr.filter((item, index)=> {
        //return arr.indexOf(item) === index
        return arr.includes(item)
    })
}

思想: 利用indexOf检测元素在数组中第一次出现的位置是否和元素现在的位置相等,如果不等则说明该元素是重复元素

3. ES6 中的 Set 去重

function distinct(array) {
   return Array.from(new Set(array));
}

思想: ES6 提供了新的数据结构 Set,Set 结构的一个特性就是成员值都是唯一的,没有重复的值。

4. reduce 实现对象数组去重复

var resources = [
    { name: "张三", age: "18" },
    { name: "张三", age: "19" },
    { name: "张三", age: "20" },
    { name: "李四", age: "19" },
    { name: "王五", age: "20" },
    { name: "赵六", age: "21" }
]
var temp = {};
resources = resources.reduce((prev, curv) => {
 // 如果临时对象中有这个名字,什么都不做
 if (temp[curv.name]) {

 }else {
    // 如果临时对象没有就把这个名字加进去,同时把当前的这个对象加入到prev中
    temp[curv.name] = true;
    prev.push(curv);
 }
 return prev
}, []);
console.log("结果", resources);

这种方法是利用高阶函数 reduce 进行去重, 这里只需要注意initialValue得放一个空数组[],不然没法push

实现 getValue/setValue 函数来获取path对应的值

// 示例
var object = { a: [{ b: { c: 3 } }] }; // path: 'a[0].b.c'
var array = [{ a: { b: [1] } }]; // path: '[0].a.b[0]'

function getValue(target, valuePath, defaultValue) {}

console.log(getValue(object, "a[0].b.c", 0)); // 输出3
console.log(getValue(array, "[0].a.b[0]", 12)); // 输出 1
console.log(getValue(array, "[0].a.b[0].c", 12)); // 输出 12

实现

/**
 * 测试属性是否匹配
 */
export function testPropTypes(value, type, dev) {
  const sEnums = ['number', 'string', 'boolean', 'undefined', 'function']; // NaN
  const oEnums = ['Null', 'Object', 'Array', 'Date', 'RegExp', 'Error'];
  const nEnums = [
    '[object Number]',
    '[object String]',
    '[object Boolean]',
    '[object Undefined]',
    '[object Function]',
    '[object Null]',
    '[object Object]',
    '[object Array]',
    '[object Date]',
    '[object RegExp]',
    '[object Error]',
  ];
  const reg = new RegExp('\\[object (.*?)\\]');

  // 完全匹配模式,type应该传递类似格式[object Window] [object HTMLDocument] ...
  if (reg.test(type)) {
    // 排除nEnums的12种
    if (~nEnums.indexOf(type)) {
      if (dev === true) {
        console.warn(value, 'The parameter type belongs to one of 12 types:number string boolean undefined Null Object Array Date RegExp function Error NaN');
      }
    }

    if (Object.prototype.toString.call(value) === type) {
      return true;
    }

    return false;
  }
}
const syncVarIterator = {
  getter: function (obj, key, defaultValue) {
    // 结果变量
    const defaultResult = defaultValue === undefined ? undefined : defaultValue;

    if (testPropTypes(obj, 'Object') === false && testPropTypes(obj, 'Array') === false) {
      return defaultResult;
    }

    // 结果变量,暂时指向obj持有的引用,后续将可能被不断的修改
    let result = obj;

    // 得到知道值
    try {
      // 解析属性层次序列
      const keyArr = key.split('.');

      // 迭代obj对象属性
      for (let i = 0; i < keyArr.length; i++) {
        // 如果第 i 层属性存在对应的值则迭代该属性值
        if (result[keyArr[i]] !== undefined) {
          result = result[keyArr[i]];

          // 如果不存在则返回未定义
        } else {
          return defaultResult;
        }
      }
    } catch (e) {
      return defaultResult;
    }

    // 返回获取的结果
    return result;
  },
  setter: function (obj, key, val) {
    // 如果不存在obj则返回未定义
    if (testPropTypes(obj, 'Object') === false) {
      return false;
    }

    // 结果变量,暂时指向obj持有的引用,后续将可能被不断的修改
    let result = obj;

    try {
      // 解析属性层次序列
      const keyArr = key.split('.');

      let i = 0;

      // 迭代obj对象属性
      for (; i < keyArr.length - 1; i++) {
        // 如果第 i 层属性对应的值不存在,则定义为对象
        if (result[keyArr[i]] === undefined) {
          result[keyArr[i]] = {};
        }

        // 如果第 i 层属性对应的值不是对象(Object)的一个实例,则抛出错误
        if (!(result[keyArr[i]] instanceof Object)) {
          throw new Error('obj.' + keyArr.splice(0, i + 1).join('.') + 'is not Object');
        }

        // 迭代该层属性值
        result = result[keyArr[i]];
      }

      // 设置属性值
      result[keyArr[i]] = val;

      return true;
    } catch (e) {
      return false;
    }
  },
};

使用promise来实现

创建 enhancedObject 函数

const enhancedObject = (target) =>
  new Proxy(target, {
    get(target, property) {
      if (property in target) {
        return target[property];
      } else {
        return searchFor(property, target); //实际使用时要对value值进行复位
      }
    },
  });

let value = null;
function searchFor(property, target) {
  for (const key of Object.keys(target)) {
    if (typeof target[key] === "object") {
      searchFor(property, target[key]);
    } else if (typeof target[property] !== "undefined") {
      value = target[property];
      break;
    }
  }
  return value;
}

使用 enhancedObject 函数

const data = enhancedObject({
  user: {
    name: "test",
    settings: {
      theme: "dark",
    },
  },
});

console.log(data.user.settings.theme); // dark
console.log(data.theme); // dark

以上代码运行后,控制台会输出以下代码:

dark
dark

通过观察以上的输出结果可知,使用 enhancedObject 函数处理过的对象,我们就可以方便地访问普通对象内部的深层属性。

怎么在制定数据源里面生成一个长度为 n 的不重复随机数组 能有几种方法 时间复杂度多少(字节)

第一版 时间复杂度为 O(n^2)

function getTenNum(testArray, n) {
  let result = [];
  for (let i = 0; i < n; ++i) {
    const random = Math.floor(Math.random() * testArray.length);
    const cur = testArray[random];
    if (result.includes(cur)) {
      i--;
      break;
    }
    result.push(cur);
  }
  return result;
}
const testArray = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14];
const resArr = getTenNum(testArray, 10);

第二版 标记法 / 自定义属性法 时间复杂度为 O(n)

function getTenNum(testArray, n) {
  let hash = {};
  let result = [];
  let ranNum = n;
  while (ranNum > 0) {
    const ran = Math.floor(Math.random() * testArray.length);
    if (!hash[ran]) {
      hash[ran] = true;
      result.push(ran);
      ranNum--;
    }
  }
  return result;
}
const testArray = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14];
const resArr = getTenNum(testArray, 10);

第三版 交换法 时间复杂度为 O(n)

function getTenNum(testArray, n) {
  const cloneArr = [...testArray];
  let result = [];
  for (let i = 0; i < n; i++) {
    debugger;
    const ran = Math.floor(Math.random() * (cloneArr.length - i));
    result.push(cloneArr[ran]);
    cloneArr[ran] = cloneArr[cloneArr.length - i - 1];
  }
  return result;
}
const testArray = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14];
const resArr = getTenNum(testArray, 14);

值得一提的是操作数组的时候使用交换法 这种思路在算法里面很常见

最终版 边遍历边删除 时间复杂度为 O(n)

function getTenNum(testArray, n) {
  const cloneArr = [...testArray];
  let result = [];
  for (let i = 0; i < n; ++i) {
    const random = Math.floor(Math.random() * cloneArr.length);
    const cur = cloneArr[random];
    result.push(cur);
    cloneArr.splice(random, 1);
  }
  return result;
}
const testArray = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14];
const resArr = getTenNum(testArray, 14);

基于Generator函数实现async/await原理

核心:传递给我一个Generator函数,把函数中的内容基于Iterator迭代器的特点一步步的执行

function readFile(file) {
    return new Promise(resolve => {
        setTimeout(() => {
            resolve(file);
    }, 1000);
    })
};

function asyncFunc(generator) {
    const iterator = generator(); // 接下来要执行next
  // data为第一次执行之后的返回结果,用于传给第二次执行
  const next = (data) => {
        let { value, done } = iterator.next(data); // 第二次执行,并接收第一次的请求结果 data

    if (done) return; // 执行完毕(到第三次)直接返回
    // 第一次执行next时,yield返回的 promise实例 赋值给了 value
    value.then(data => {
      next(data); // 当第一次value 执行完毕且成功时,执行下一步(并把第一次的结果传递下一步)
    });
  }
  next();
};

asyncFunc(function* () {
    // 生成器函数:控制代码一步步执行 
  let data = yield readFile('a.js'); // 等这一步骤执行执行成功之后,再往下走,没执行完的时候,直接返回
  data = yield readFile(data + 'b.js');
  return data;
})

给定两个数组,写一个方法来计算它们的交集

例如:给定 nums1 = 1, 2, 2, 1,nums2 = 2, 2,返回 2, 2。

function union (arr1, arr2) {
  return arr1.filter(item => {
      return arr2.indexOf(item) > - 1;
  })
}
 const a = [1, 2, 2, 1];
 const b = [2, 3, 2];
 console.log(union(a, b)); // [2, 2]

实现一个管理本地缓存过期的函数

封装一个可以设置过期时间的localStorage存储函数

class Storage{
  constructor(name){
      this.name = 'storage';
  }
  //设置缓存
  setItem(params){
      let obj = {
          name:'', // 存入数据  属性
          value:'',// 属性值
          expires:"", // 过期时间
          startTime:new Date().getTime()//记录何时将值存入缓存,毫秒级
      }
      let options = {};
      //将obj和传进来的params合并
      Object.assign(options,obj,params);
      if(options.expires){
      //如果options.expires设置了的话
      //以options.name为key,options为值放进去
          localStorage.setItem(options.name,JSON.stringify(options));
      }else{
      //如果options.expires没有设置,就判断一下value的类型
          let type = Object.prototype.toString.call(options.value);
          //如果value是对象或者数组对象的类型,就先用JSON.stringify转一下,再存进去
          if(Object.prototype.toString.call(options.value) == '[object Object]'){
              options.value = JSON.stringify(options.value);
          }
          if(Object.prototype.toString.call(options.value) == '[object Array]'){
              options.value = JSON.stringify(options.value);
          }
          localStorage.setItem(options.name,options.value);
      }
  }
  //拿到缓存
  getItem(name){
      let item = localStorage.getItem(name);
      //先将拿到的试着进行json转为对象的形式
      try{
          item = JSON.parse(item);
      }catch(error){
      //如果不行就不是json的字符串,就直接返回
          item = item;
      }
      //如果有startTime的值,说明设置了失效时间
      if(item.startTime){
          let date = new Date().getTime();
          //何时将值取出减去刚存入的时间,与item.expires比较,如果大于就是过期了,如果小于或等于就还没过期
          if(date - item.startTime > item.expires){
          //缓存过期,清除缓存,返回false
              localStorage.removeItem(name);
              return false;
          }else{
          //缓存未过期,返回值
              return item.value;
          }
      }else{
      //如果没有设置失效时间,直接返回值
          return item;
      }
  }
  //移出缓存
  removeItem(name){
      localStorage.removeItem(name);
  }
  //移出全部缓存
  clear(){
      localStorage.clear();
  }
}

用法

let storage = new Storage();
storage.setItem({
  name:"name",
  value:"ppp"
})

下面我把值取出来

let value = storage.getItem('name');
console.log('我是value',value);

设置5秒过期

let storage = new Storage();
storage.setItem({
  name:"name",
  value:"ppp",
  expires: 5000
})
// 过期后再取出来会变为 false
let value = storage.getItem('name');
console.log('我是value',value);

字符串最长的不重复子串

题目描述

给定一个字符串 s ,请你找出其中不含有重复字符的 最长子串 的长度。


示例 1:

输入: s = "abcabcbb"
输出: 3
解释: 因为无重复字符的最长子串是 "abc",所以其长度为 3。

示例 2:

输入: s = "bbbbb"
输出: 1
解释: 因为无重复字符的最长子串是 "b",所以其长度为 1。

示例 3:

输入: s = "pwwkew"
输出: 3
解释: 因为无重复字符的最长子串是 "wke",所以其长度为 3。
     请注意,你的答案必须是 子串 的长度,"pwke" 是一个子序列,不是子串。

示例 4:

输入: s = ""
输出: 0

答案

const lengthOfLongestSubstring = function (s) {
  if (s.length === 0) {
    return 0;
  }

  let left = 0;
  let right = 1;
  let max = 0;
  while (right <= s.length) {
    let lr = s.slice(left, right);
    const index = lr.indexOf(s[right]);

    if (index > -1) {
      left = index + left + 1;
    } else {
      lr = s.slice(left, right + 1);
      max = Math.max(max, lr.length);
    }
    right++;
  }
  return max;
};

实现一个函数判断数据类型

function getType(obj) {
   if (obj === null) return String(obj);
   return typeof obj === 'object' 
   ? Object.prototype.toString.call(obj).replace('[object ', '').replace(']', '').toLowerCase()
   : typeof obj;
}

// 调用
getType(null); // -> null
getType(undefined); // -> undefined
getType({}); // -> object
getType([]); // -> array
getType(123); // -> number
getType(true); // -> boolean
getType('123'); // -> string
getType(/123/); // -> regexp
getType(new Date()); // -> date

异步串行 | 异步并行

// 字节面试题,实现一个异步加法
function asyncAdd(a, b, callback) {
  setTimeout(function () {
    callback(null, a + b);
  }, 500);
}

// 解决方案
// 1. promisify
const promiseAdd = (a, b) => new Promise((resolve, reject) => {
  asyncAdd(a, b, (err, res) => {
    if (err) {
      reject(err)
    } else {
      resolve(res)
    }
  })
})

// 2. 串行处理
async function serialSum(...args) {
  return args.reduce((task, now) => task.then(res => promiseAdd(res, now)), Promise.resolve(0))
}

// 3. 并行处理
async function parallelSum(...args) {
  if (args.length === 1) return args[0]
  const tasks = []
  for (let i = 0; i < args.length; i += 2) {
    tasks.push(promiseAdd(args[i], args[i + 1] || 0))
  }
  const results = await Promise.all(tasks)
  return parallelSum(...results)
}

// 测试
(async () => {
  console.log('Running...');
  const res1 = await serialSum(1, 2, 3, 4, 5, 8, 9, 10, 11, 12)
  console.log(res1)
  const res2 = await parallelSum(1, 2, 3, 4, 5, 8, 9, 10, 11, 12)
  console.log(res2)
  console.log('Done');
})()

实现深拷贝

简洁版本

简单版:

const newObj = JSON.parse(JSON.stringify(oldObj));

局限性:

  • 他无法实现对函数 、RegExp等特殊对象的克隆
  • 会抛弃对象的constructor,所有的构造函数会指向Object
  • 对象有循环引用,会报错

面试简版

function deepClone(obj) {
    // 如果是 值类型 或 null,则直接return
    if(typeof obj !== 'object' || obj === null) {
      return obj
    }

    // 定义结果对象
    let copy = {}

    // 如果对象是数组,则定义结果数组
    if(obj.constructor === Array) {
      copy = []
    }

    // 遍历对象的key
    for(let key in obj) {
        // 如果key是对象的自有属性
        if(obj.hasOwnProperty(key)) {
          // 递归调用深拷贝方法
          copy[key] = deepClone(obj[key])
        }
    }

    return copy
} 

调用深拷贝方法,若属性为值类型,则直接返回;若属性为引用类型,则递归遍历。这就是我们在解这一类题时的核心的方法。

进阶版

  • 解决拷贝循环引用问题
  • 解决拷贝对应原型问题
// 递归拷贝 (类型判断)
function deepClone(value,hash = new WeakMap){ // 弱引用,不用map,weakMap更合适一点
  // null 和 undefiend 是不需要拷贝的
  if(value == null){ return value;}
  if(value instanceof RegExp) { return new RegExp(value) }
  if(value instanceof Date) { return new Date(value) }
  // 函数是不需要拷贝
  if(typeof value != 'object') return value;
  let obj = new value.constructor(); // [] {}
  // 说明是一个对象类型
  if(hash.get(value)){
    return hash.get(value)
  }
  hash.set(value,obj);
  for(let key in value){ // in 会遍历当前对象上的属性 和 __proto__指代的属性
    // 补拷贝 对象的__proto__上的属性
    if(value.hasOwnProperty(key)){
      // 如果值还有可能是对象 就继续拷贝
      obj[key] = deepClone(value[key],hash);
    }
  }
  return obj
  // 区分对象和数组 Object.prototype.toString.call
}
// test

var o = {};
o.x = o;
var o1 = deepClone(o); // 如果这个对象拷贝过了 就返回那个拷贝的结果就可以了
console.log(o1);

实现完整的深拷贝

1. 简易版及问题

JSON.parse(JSON.stringify());

估计这个api能覆盖大多数的应用场景,没错,谈到深拷贝,我第一个想到的也是它。但是实际上,对于某些严格的场景来说,这个方法是有巨大的坑的。问题如下:

  1. 无法解决循环引用的问题。举个例子:
const a = {val:2};
a.target = a;

拷贝a会出现系统栈溢出,因为出现了无限递归的情况。

  1. 无法拷贝一些特殊的对象,诸如 RegExp, Date, Set, Map
  2. 无法拷贝函数(划重点)。

因此这个api先pass掉,我们重新写一个深拷贝,简易版如下:

const deepClone = (target) => {
  if (typeof target === 'object' && target !== null) {
    const cloneTarget = Array.isArray(target) ? []: {};
    for (let prop in target) {
      if (target.hasOwnProperty(prop)) {
          cloneTarget[prop] = deepClone(target[prop]);
      }
    }
    return cloneTarget;
  } else {
    return target;
  }
}

现在,我们以刚刚发现的三个问题为导向,一步步来完善、优化我们的深拷贝代码。

2. 解决循环引用

现在问题如下:

let obj = {val : 100};
obj.target = obj;

deepClone(obj);//报错: RangeError: Maximum call stack size exceeded

这就是循环引用。我们怎么来解决这个问题呢?

创建一个Map。记录下已经拷贝过的对象,如果说已经拷贝过,那直接返回它行了。

const isObject = (target) => (typeof target === 'object' || typeof target === 'function') && target !== null;

const deepClone = (target, map = new Map()) => { 
  if(map.get(target))  
    return target; 


  if (isObject(target)) { 
    map.set(target, true); 
    const cloneTarget = Array.isArray(target) ? []: {}; 
    for (let prop in target) { 
      if (target.hasOwnProperty(prop)) { 
          cloneTarget[prop] = deepClone(target[prop],map); 
      } 
    } 
    return cloneTarget; 
  } else { 
    return target; 
  } 
}

现在来试一试:

const a = {val:2};
a.target = a;
let newA = deepClone(a);
console.log(newA)//{ val: 2, target: { val: 2, target: [Circular] } }

好像是没有问题了, 拷贝也完成了。但还是有一个潜在的坑, 就是map 上的 key 和 map 构成了强引用关系,这是相当危险的。我给你解释一下与之相对的弱引用的概念你就明白了

在计算机程序设计中,弱引用与强引用相对,

被弱引用的对象可以在任何时候被回收,而对于强引用来说,只要这个强引用还在,那么对象无法被回收。拿上面的例子说,map 和 a一直是强引用的关系, 在程序结束之前,a 所占的内存空间一直不会被释放。

怎么解决这个问题?

很简单,让 map 的 key 和 map 构成弱引用即可。ES6给我们提供了这样的数据结构,它的名字叫WeakMap,它是一种特殊的Map, 其中的键是弱引用的。其键必须是对象,而值可以是任意的

稍微改造一下即可:

const deepClone = (target, map = new WeakMap()) => {
  //...
}

3. 拷贝特殊对象

可继续遍历

对于特殊的对象,我们使用以下方式来鉴别:

Object.prototype.toString.call(obj);

梳理一下对于可遍历对象会有什么结果:

["object Map"]
["object Set"]
["object Array"]
["object Object"]
["object Arguments"]

以这些不同的字符串为依据,我们就可以成功地鉴别这些对象。

const getType = Object.prototype.toString.call(obj);

const canTraverse = {
  '[object Map]': true,
  '[object Set]': true,
  '[object Array]': true,
  '[object Object]': true,
  '[object Arguments]': true,
};

const deepClone = (target, map = new Map()) => {
  if(!isObject(target)) 
    return target;
  let type = getType(target);
  let cloneTarget;
  if(!canTraverse[type]) {
    // 处理不能遍历的对象
    return;
  }else {
    // 这波操作相当关键,可以保证对象的原型不丢失!
    let ctor = target.prototype;
    cloneTarget = new ctor();
  }

  if(map.get(target)) 
    return target;
  map.put(target, true);

  if(type === mapTag) {
    //处理Map
    target.forEach((item, key) => {
      cloneTarget.set(deepClone(key), deepClone(item));
    })
  }

  if(type === setTag) {
    //处理Set
    target.forEach(item => {
      target.add(deepClone(item));
    })
  }

  // 处理数组和对象
  for (let prop in target) {
    if (target.hasOwnProperty(prop)) {
        cloneTarget[prop] = deepClone(target[prop]);
    }
  }
  return cloneTarget;
}

不可遍历的对象

const boolTag = '[object Boolean]';
const numberTag = '[object Number]';
const stringTag = '[object String]';
const dateTag = '[object Date]';
const errorTag = '[object Error]';
const regexpTag = '[object RegExp]';
const funcTag = '[object Function]';

对于不可遍历的对象,不同的对象有不同的处理。

const handleRegExp = (target) => {
  const { source, flags } = target;
  return new target.constructor(source, flags);
}

const handleFunc = (target) => {
  // 待会的重点部分
}

const handleNotTraverse = (target, tag) => {
  const Ctor = targe.constructor;
  switch(tag) {
    case boolTag:
    case numberTag:
    case stringTag:
    case errorTag: 
    case dateTag:
      return new Ctor(target);
    case regexpTag:
      return handleRegExp(target);
    case funcTag:
      return handleFunc(target);
    default:
      return new Ctor(target);
  }
}

4. 拷贝函数

  • 虽然函数也是对象,但是它过于特殊,我们单独把它拿出来拆解。
  • 提到函数,在JS种有两种函数,一种是普通函数,另一种是箭头函数。每个普通函数都是
  • Function的实例,而箭头函数不是任何类的实例,每次调用都是不一样的引用。那我们只需要
  • 处理普通函数的情况,箭头函数直接返回它本身就好了。

那么如何来区分两者呢?

答案是: 利用原型。箭头函数是不存在原型的。

const handleFunc = (func) => {
  // 箭头函数直接返回自身
  if(!func.prototype) return func;
  const bodyReg = /(?<={)(.|\n)+(?=})/m;
  const paramReg = /(?<=\().+(?=\)\s+{)/;
  const funcString = func.toString();
  // 分别匹配 函数参数 和 函数体
  const param = paramReg.exec(funcString);
  const body = bodyReg.exec(funcString);
  if(!body) return null;
  if (param) {
    const paramArr = param[0].split(',');
    return new Function(...paramArr, body[0]);
  } else {
    return new Function(body[0]);
  }
}

5. 完整代码展示

const getType = obj => Object.prototype.toString.call(obj);

const isObject = (target) => (typeof target === 'object' || typeof target === 'function') && target !== null;

const canTraverse = {
  '[object Map]': true,
  '[object Set]': true,
  '[object Array]': true,
  '[object Object]': true,
  '[object Arguments]': true,
};
const mapTag = '[object Map]';
const setTag = '[object Set]';
const boolTag = '[object Boolean]';
const numberTag = '[object Number]';
const stringTag = '[object String]';
const symbolTag = '[object Symbol]';
const dateTag = '[object Date]';
const errorTag = '[object Error]';
const regexpTag = '[object RegExp]';
const funcTag = '[object Function]';

const handleRegExp = (target) => {
  const { source, flags } = target;
  return new target.constructor(source, flags);
}

const handleFunc = (func) => {
  // 箭头函数直接返回自身
  if(!func.prototype) return func;
  const bodyReg = /(?<={)(.|\n)+(?=})/m;
  const paramReg = /(?<=\().+(?=\)\s+{)/;
  const funcString = func.toString();
  // 分别匹配 函数参数 和 函数体
  const param = paramReg.exec(funcString);
  const body = bodyReg.exec(funcString);
  if(!body) return null;
  if (param) {
    const paramArr = param[0].split(',');
    return new Function(...paramArr, body[0]);
  } else {
    return new Function(body[0]);
  }
}

const handleNotTraverse = (target, tag) => {
  const Ctor = target.constructor;
  switch(tag) {
    case boolTag:
      return new Object(Boolean.prototype.valueOf.call(target));
    case numberTag:
      return new Object(Number.prototype.valueOf.call(target));
    case stringTag:
      return new Object(String.prototype.valueOf.call(target));
    case symbolTag:
      return new Object(Symbol.prototype.valueOf.call(target));
    case errorTag: 
    case dateTag:
      return new Ctor(target);
    case regexpTag:
      return handleRegExp(target);
    case funcTag:
      return handleFunc(target);
    default:
      return new Ctor(target);
  }
}

const deepClone = (target, map = new WeakMap()) => {
  if(!isObject(target)) 
    return target;
  let type = getType(target);
  let cloneTarget;
  if(!canTraverse[type]) {
    // 处理不能遍历的对象
    return handleNotTraverse(target, type);
  }else {
    // 这波操作相当关键,可以保证对象的原型不丢失!
    let ctor = target.constructor;
    cloneTarget = new ctor();
  }

  if(map.get(target)) 
    return target;
  map.set(target, true);

  if(type === mapTag) {
    //处理Map
    target.forEach((item, key) => {
      cloneTarget.set(deepClone(key, map), deepClone(item, map));
    })
  }

  if(type === setTag) {
    //处理Set
    target.forEach(item => {
      cloneTarget.add(deepClone(item, map));
    })
  }

  // 处理数组和对象
  for (let prop in target) {
    if (target.hasOwnProperty(prop)) {
        cloneTarget[prop] = deepClone(target[prop], map);
    }
  }
  return cloneTarget;
}

判断括号字符串是否有效(小米)

题目描述

给定一个只包括 '(',')','{','}','[',']' 的字符串 s ,判断字符串是否有效。

有效字符串需满足:
- 左括号必须用相同类型的右括号闭合。
- 左括号必须以正确的顺序闭合。

示例 1:

输入:s = "()"
输出:true

示例 2:

输入:s = "()[]{}"
输出:true

示例 3:

输入:s = "(]"
输出:false

答案

const isValid = function (s) {
  if (s.length % 2 === 1) {
    return false;
  }
  const regObj = {
    "{": "}",
    "(": ")",
    "[": "]",
  };
  let stack = [];
  for (let i = 0; i < s.length; i++) {
    if (s[i] === "{" || s[i] === "(" || s[i] === "[") {
      stack.push(s[i]);
    } else {
      const cur = stack.pop();
      if (s[i] !== regObj[cur]) {
        return false;
      }
    }
  }

  if (stack.length) {
    return false;
  }

  return true;
};

树形结构转成列表(处理菜单)

[
    {
        id: 1,
        text: '节点1',
        parentId: 0,
        children: [
            {
                id:2,
                text: '节点1_1',
                parentId:1
            }
        ]
    }
]
转成
[
    {
        id: 1,
        text: '节点1',
        parentId: 0 //这里用0表示为顶级节点
    },
    {
        id: 2,
        text: '节点1_1',
        parentId: 1 //通过这个字段来确定子父级
    }
    ...
]

实现代码如下:

function treeToList(data) {
  let res = [];
  const dfs = (tree) => {
    tree.forEach((item) => {
      if (item.children) {
        dfs(item.children);
        delete item.children;
      }
      res.push(item);
    });
  };
  dfs(data);
  return res;
}

实现find方法

  • find 接收一个方法作为参数,方法内部返回一个条件
  • find 会遍历所有的元素,执行你给定的带有条件返回值的函数
  • 符合该条件的元素会作为 find 方法的返回值
  • 如果遍历结束还没有符合该条件的元素,则返回 undefined
var users = [
  {id: 1, name: '张三'},
  {id: 2, name: '张三'},
  {id: 3, name: '张三'},
  {id: 4, name: '张三'}
]

Array.prototype.myFind = function (callback) {
  // var callback = function (item, index) { return item.id === 4 }
  for (var i = 0; i < this.length; i++) {
    if (callback(this[i], i)) {
      return this[i]
    }
  }
}

var ret = users.myFind(function (item, index) {
  return item.id === 2
})

console.log(ret)