常见的前端面试题

145 阅读20分钟

js 的数据类型

基本类型

  1. Boolean
  2. String
  3. Null
  4. BigInt
  5. Number
  6. Symbol
  7. Undefined

引用类型

  1. Object 对象

    1. 数组 函数 叫做 对象 类型

ES 和 js 区别

JS

  1. 包括了 es 和 webApi
  2. 两个组成了 js

ES

  1. 语法规范
  2. 标准库

webAPI

  1. DOM
  2. BOM

[] + {} 和 {} + []

案例

console.log([] + {})  // object Object
console.log({} + [])  // 0

结论

  1. 引用类型 作运算的时候 都会先调用他的 valueOf() 方法
  2. 如果返回的还是一个引用类型 在调用他的 toString() 方法
  3. 如果返回还是引用类型 就进行运算
  4. 为什 上面 {} + [] = 0 因为 浏览器会把 {} 解析成 代码块 [] 解析成 +'' 所以变成了 0 如果在 node中 还是 object Object

函数的执行上下文

案例

function methods (a, b, c) {
  console.log(a, b, c)  //  1 2 [Function: c]
  
  var a = 'a'
  var b = function b () { };
  (function a () { });  // 不是函数 只是表达式
  (function b () { });
  function c () {

  }
  console.log(a, b, c)// a [Function: b] [Function: c]
 
  
}
methods(1, 2, 3)

结论

  1. 在调用函数 运行函数之前 会先对 函数进行填充
  2. 第一步 确认形参 解析参数 如果传递了 先把形参的值确认下来
  3. 第二步 确认函数声明 把函数 提取到最上面 如果有同名的 声明 会覆盖
  4. 第三步 确认声明变量 提取到最上面 如果有同名的声明 会忽略

赋值运算的细节

案例

var a = {n:1}
var b = a
a.x = a
a = {n:2}
console.log(a.x)  undefined
console.log(b.x)  {n:2}

   b
{
  n: 1
  x: {n:2} 
}


a 
{
 n: 2
}

赋值的步骤

  1. 找到变量 的内存地址 准备赋值
  2. 运算右侧代码 得到要赋值的值
  3. 将右侧运算的结果放到之前的地址中
  4. 返回整个表达式的结果为 右侧表达式 运算的值

parseInt() 参数

const nums = ['1','2','3']
const newnums = nums.map(parseInt)

// 1 NAN  NAN

结论

  1. parseInt 第二个参数是 进制转换
  2. 非 0 的无效进制 都会转换成 NaN 有效进制是 2-36
  3. 3 为什么是 NaN 因为 2进制中 只有 0 1 没有 3 所以也是 NaN

script async defer 区别

正常

  1. 浏览器 碰到 script 标签 就会暂停解析 dom 元素 去加载 js 解析 js 执行js
  2. 等 js 执行完毕 才会继续解析 dom

defer

  1. 浏览器 碰到 script 标签 不会暂停解析 dom 元素
  2. 同时会去加载 js 文件
  3. 等 dom 全部解析完毕 即将 触发 DOMContentLoaded 事件等时候
  4. 才会去 解析 js 执行js

async

  1. 浏览器 碰到 script 标签 不会暂停解析 dom 元素
  2. 同时会去加载 js 文件
  3. 当 加载到 js 文件 暂停 解析 dom 开始解析 js 执行 js
  4. 等 js 解析 完毕 执行完毕 继续 加载 dom

Javascript 中的数字在计算机中占 多少个 Byte

8byte

结论

  1. 8个 2进制数字 标识一个字节
  2. js 使用 双精度浮点数
  3. 双精度 占 64 位
  4. 64 / 8 所以 得到 8 byte

短路规则

案例

console.log(2 && 4)  // 4
console.log(2 || 4)  // 2

短路运算的步骤

  1. 依次判定 布尔判定
  2. 返回结果: 能确认结果的最后一个值
  3. && 一假则假
  4. || 一真则真

画一个边框宽度为0.5px的正方形叭

解决方法

1.直接写0.5px,不兼容。
2.transform的scale。
3.linear-gradient,渐变。
4.SVG。
5.box-shadow。
6.meta中的viewport。

实现

使用伪元素给box2添加边框。注意这里要添加transform-origin: left top;不然盒子会偏移中心。

        .box1 {
            background: none;
            margin-top: 10px;
            margin-left: 200px;
            height: 100px;
            width: 100px;
            border: 0.5px solid #000;
        }

        .box2 {
            position: relative;
            margin: 10px 0 0 200px;
            border: none;
            background: none;
            height: 100px;
            width: 100px;
        }

        .box2::after {
            content: '';
            position: absolute;
            border: 1px solid #000;
            top: 0;
            left: 0;
            box-sizing: border-box;
            width: 200%;
            height: 200%;
            transform: scale(0.5);
            transform-origin: left top;
        }

    <div class="box"></div>
    <div class="box1"></div>
    <div class='box2'></div>


这是在pc端的效果,是不是感觉直接设置为0.5px,反而会更细呢。但是我们再看看移动端的情况。


在移动端只有缩放0.5的方案才符合效果。

如何让localStorage支持过期时间设置?

问题描述

在实际的应用场景中, 我们往往需要让 localStorage 设置的某个 「key」 能在指定时间内自动失效, 所以基于这种场景, 我们如何去解决呢?

1. 初级解法

缺点


「维护成本极高, 且不利于工程化复用」。

实现

localStorage.setItem('dooring''1.0.0')
// 设置一小时的有效期
const expire = 1000 * 60 * 60;
setTimeout(() => {
  localStorage.setItem('dooring''')
}, expire)

2. 中级解法

  1. 用**「localStorage」**存一份{key(键): expire(过期时间)}的映射表
  2. 重写**「localStorage API」**, 对方法进行二次封装

缺点

  • 对 store 操作时需要维护2份数据, 并且占用缓存空间
  • 如果 EXPIRE_MAP 误删除将会导致所有过期时间失效
  • 对操作过程缺少更灵活的控制(比如操作状态, 操作回调等)

实现

const store = {
  // 存储过期时间映射
  setExpireMap(key, expire) => {
    const expireMap = localStorage.getItem('EXPIRE_MAP') || "{}"
    localStorage.setItem(
      'EXPIRE_MAP', 
      JSON.stringify({
      ...JSON.parse(expireMap),
      key: expire
    }))
  },
  setItem(key, value, expire) => {
    store.setExpireMap(key, expire)
    localStorage.setItem(key, value)
  },
  getItem(key) => {
    // 在取值之前先判断是否过期
    const expireMap = JSON.parse(
      localStorage.getItem('EXPIRE_MAP') || "{}"
    )
    if(expireMap[key] && expireMap[key] < Date.now()) {
      return localStorage.getItem(key)
    }else {
      localStorage.removeItem(key)
      return null
    }
  }
  // ...
}

3. 高级解法

  1. 将过期时间存到 key 中, 如 dooring|6000, 每次取值时通过分隔符“|”来将 key 和 expire 取出, 进行判断
  2. 将过期时间存到 value 中, 如 1.0.0|6000, 剩下的同1

实现

const store = {
  preId: 'xi-',
  timeSign: '|-door-|',
  status: {
    SUCCESS: 0,
    FAILURE: 1,
    OVERFLOW: 2,
    TIMEOUT: 3,
  },
  storage: localStorage || window.localStorage,
  getKey: function (key: string) {
    return this.preId + key;
  },
  set: function (
    key: string,
    value: string | number,
    time?: Date & number,
    cb?: (status: number, key: string, value: string | number) => void,
  ) {
    let _status = this.status.SUCCESS,
      _key = this.getKey(key),
      _time;
    // 设置失效时间,未设置时间默认为一个月
    try {
      _time = time
        ? new Date(time).getTime() || time.getTime()
        : new Date().getTime() + 1000 * 60 * 60 * 24 * 31;
    } catch (e) {
      _time = new Date().getTime() + 1000 * 60 * 60 * 24 * 31;
    }
    try {
      this.storage.setItem(_key, _time + this.timeSign + value);
    } catch (e) {
      _status = this.status.OVERFLOW;
    }
    cb && cb.call(this, _status, _key, value);
  },
  get: function (
    key: string,
    cb?: (status: number, value: string | number | null) => void,
  ) {
    let status = this.status.SUCCESS,
      _key = this.getKey(key),
      value = null,
      timeSignLen = this.timeSign.length,
      that = this,
      index,
      time,
      result;
    try {
      value = that.storage.getItem(_key);
    } catch (e) {
      result = {
        status: that.status.FAILURE,
        value: null,
      };
      cb && cb.call(this, result.status, result.value);
      return result;
    }
    if (value) {
      index = value.indexOf(that.timeSign);
      time = +value.slice(0, index);
      if (time > new Date().getTime() || time == 0) {
        value = value.slice(index + timeSignLen);
      } else {
        (value = null), (status = that.status.TIMEOUT);
        that.remove(_key);
      }
    } else {
      status = that.status.FAILURE;
    }
    result = {
      status: status,
      value: value,
    };
    cb && cb.call(this, result.status, result.value);
    return result;
  },
  // ...
};

export default store;

4. 骨灰级解法

骨灰级解法是直接使用 xijs 这个 javascript 工具库

h5.dooring.cn/xijs/lib/uu…

实现

//  先安装 yarn add xijs
import { store } from 'xijs';
// 设置带有过期时间的key
store.set('name''dooring'Date.now() + 1000);
console.log(store.get('name'));
setTimeout(() => {
  console.log(store.get('name'));
}, 1000);

// 设置成功后的回调
store.set('dooring''xuxiaoxi'Date.now() + 1000(status, key, value) => {
  console.log('success');
});

Virtual DOM的优势在哪里

  1. 操作真实DOM 性能会差一点
  2. 虚拟DOM不会立马进行排版与重绘操作
  3. 虚拟DOM进行频繁修改,然后一次性比较并修改真实DOM中需要改的部分,最后在真实DOM中进行排版与重绘,减少过多DOM节点排版与重绘损耗
  4. 虚拟DOM有效降低大面积真实DOM的重绘与排版,因为最终与真实DOM比较差异,可以只渲染局部
  5. 当热最大的一个优点 解决了 跨平台的能力

简易版 vdom 的diff 算法的简易实现

vdom转化为真实dom

const createElement = (vnode) => {
  let tag = vnode.tag;
  let attrs = vnode.attrs || {};
  let children = vnode.children || [];
  if(!tag) {
    return null;
  }
  //创建元素
  let elem = document.createElement(tag);
  //属性
  let attrName;
  for (attrName in attrs) {
    if(attrs.hasOwnProperty(attrName)) {
      elem.setAttribute(attrName, attrs[attrName]);
    }
  }
  //子元素
  children.forEach(childVnode => {
    //给elem添加子元素
    elem.appendChild(createElement(childVnode));
  })

  //返回真实的dom元素
  return elem;
}

简易diff算法做更新操作

function updateChildren(vnode, newVnode) {
  let children = vnode.children || [];
  let newChildren = newVnode.children || [];

  children.forEach((childVnode, index) => {
    let newChildVNode = newChildren[index];
    if(childVnode.tag === newChildVNode.tag) {
      //深层次对比, 递归过程
      updateChildren(childVnode, newChildVNode);
    } else {
      //替换
      replaceNode(childVnode, newChildVNode);
    }
  })
}

JS中的数据类型检测方案

typeof

console.log(typeof 1);               // number
console.log(typeof true);            // boolean
console.log(typeof 'mc');            // string
console.log(typeof Symbol)           // function
console.log(typeof function(){});    // function
console.log(typeof console.log());   // function
console.log(typeof []);              // object 
console.log(typeof {});              // object
console.log(typeof null);            // object
console.log(typeof undefined);       // undefined

优点

能够快速区分基本数据类型

缺点

不能将Object、Array和Null区分,都返回object

instanceof

console.log(1 instanceof Number);                    // false
console.log(true instanceof Boolean);                // false 
console.log('str' instanceof String);                // false  
console.log([] instanceof Array);                    // true
console.log(function(){} instanceof Function);       // true
console.log({} instanceof Object);                   // true

优点

能够区分Array、Object和Function,适合用于判断自定义的类实例对象

缺点

Number,Boolean,String基本数据类型不能判断

Object.prototype.toString.call()

var toString = Object.prototype.toString;
console.log(toString.call(1));                      //[object Number]
console.log(toString.call(true));                   //[object Boolean]
console.log(toString.call('mc'));                   //[object String]
console.log(toString.call([]));                     //[object Array]
console.log(toString.call({}));                     //[object Object]
console.log(toString.call(function(){}));           //[object Function]
console.log(toString.call(undefined));              //[object Undefined]
console.log(toString.call(null));                   //[object Null]

优点

精准判断数据类型

缺点

写法繁琐不容易记,推荐进行封装后使用

数组和链表区别

不同

  1. 链表是链式的存储结构;数组是顺序的存储结构。
  2. 链表通过指针来连接元素与元素,数组则是把所有元素按次序依次存储。
  3. 链表的插入删除元素相对数组较为简单,不需要移动元素,且较为容易实现长度扩充,但是寻找某个元素较为困难;
  4. 数组寻找某个元素较为简单,但插入与删除比较复杂,由于最大长度需要再编程一开始时指定,故当达到最大长度时,扩充长度不如链表方便。

相同

两种结构均可实现数据的顺序存储,构造出来的模型呈线性结构

数组

一、数组的特点

1.在内存中,数组是一块连续的区域
2.数组需要预留空间

在使用前需要提前申请所占内存的大小,这样不知道需要多大的空间,就预先申请可能会浪费内存空间,即数组空间利用率低 ps:数组的空间在编译阶段就需要进行确定,所以需要提前给出数组空间的大小(在运行阶段是不允许改变的)

3.在数组起始位置处,插入数据和删除数据效率低。

插入数据时,待插入位置的的元素和它后面的所有元素都需要向后搬移
删除数据时,待删除位置后面的所有元素都需要向前搬移

4.随机访问效率很高,时间复杂度可以达到O(1)

因为数组的内存是连续的,想要访问那个元素,直接从数组的首地址处向后偏移就可以访问到了

5.数组开辟的空间,在不够使用的时候需要扩容,扩容的话,就会涉及到需要把旧数组中的所有元素向新数组中搬移
6.数组的空间是从栈分配的

二、数组的优点

随机访问性强,查找速度快,时间复杂度为O(1)

三、数组的缺点


1.头插和头删的效率低,时间复杂度为O(N)
2.空间利用率不高
3.内存空间要求高,必须有足够的连续的内存空间
4.数组空间的大小固定,不能动态拓展

链表

一、链表的特点


1.在内存中,元素的空间可以在任意地方,空间是分散的,不需要连续
2.链表中的元素都会两个属性,一个是元素的值,另一个是指针,此指针标记了下一个元素的地址

每一个数据都会保存下一个数据的内存的地址,通过此地址可以找到下一个数据

3.查找数据时效率低,时间复杂度为O(N)

因为链表的空间是分散的,所以不具有随机访问性,如要需要访问某个位置的数据,需要从第一个数据开始找起,依次往后遍历,直到找到待查询的位置,故可能在查找某个元素时,时间复杂度达到O(N)

4.空间不需要提前指定大小,是动态申请的,根据需求动态的申请和删除内存空间,扩展方便,故空间的利用率较高
5.任意位置插入元素和删除元素效率较高,时间复杂度为O(1)
6.链表的空间是从堆中分配的

二、链表的优点


1.任意位置插入元素和删除元素的速度快,时间复杂度为O(1)
2.内存利用率高,不会浪费内存
3.链表的空间大小不固定,可以动态拓展

三、链表的缺点


随机访问效率低,时间复杂度为0(N)

promise all allSettled any race

Promise.all()

promise.all() 如果其中一个 promise reject() 了 就会返回 该 reject()信息

接受一个由promise任务组成的数组,可以同时处理多个promise任务,当所有的任务都执行完成时Promise.all()返回resolve,但当有一个失败(reject),则返回失败的信息,即使其他promise执行成功,也会返回失败。可以用一句话来说Promise.all(),要么全有要么全无。

但话又说回来,有时候我们使用Promise.all()执行很多个网络请求,可能有一个请求出错,但我们并不希望其他的网络请求也返回reject,要错都错,这样显然是不合理的。

手写

Promise.MyAll = function (promises) {
  // 返回一个新的 promise list
  let arr = [],
    count = 0 // 用来计数 如果都 resolve 了 判断length 返回 resolve
  return new Promise((resolve, reject) => {
    // 循环 传入都promise 
    promises.forEach((item, i) => {
      Promise.resolve(item).then(res => {
        // 成功 就往数组里面赋值
        arr.push(res)
        // 记录 成功次数
        count += 1
        // 如果都成功  resolve 
        if (count === promises.length) resolve(arr)
      }).catch(reject)
    })
  })
}

案例

const p1 = Promise.resolve('p1')

const p2 = new Promise((resolve, reject) => {
  setTimeout(() => {
    resolve('p2 延时一秒')
  }, 1000)
})

const p3 = new Promise((resolve, reject) => {
  setTimeout(() => {
    resolve('p3 延时两秒')
  }, 2000)
})

const p4 = Promise.reject('p4 rejected')

const p5 = new Promise((resolve, reject) => {
  setTimeout(() => {
    reject('p5 rejected 延时1.5秒')
  }, 1500)
})

// 所有Promise实例都成功
Promise.all([p1, p2, p3])
  .then(res => {
    console.log(res)
  })
  .catch(err => console.log(err)) // 2秒后打印 [ 'p1', 'p2 延时一秒', 'p3 延时两秒' ]
  
// 一个Promise实例失败
Promise.all([p1, p2, p4])
  .then(res => {
    console.log(res)
  })
  .catch(err => console.log(err)) // p4 rejected
  
// 一个延时失败的Promise
 Promise.all([p1, p2, p5])
  .then(res => {
    console.log(res)
  })
  .catch(err => console.log(err)) // 1.5秒后打印 p5 rejected
  
// 两个Promise实例失败
Promise.all([p1, p4, p5])
  .then(res => {
    console.log(res)
  })
  .catch(err => console.log(err)) // p4 rejected

Promise.race()

最快的 Promise 成功 Promise.race 就成功,最快的 Promise 失败 Promise.race 就失败。

Promise.race 从字面意思理解就是赛跑,以状态变化最快的那个 Promise 实例为准

手写

Promise.MyRace = (promise) => {
    return new Promise((resolve,reject) => {
        promise.forEach((item) => {
           Promise.resolve(item).then(resolve,reject)
        })
    })
}

案例

Promise.MyRace = (promise) => {
    return new Promise((resolve,reject) => {
        promise.forEach((item) => {
           Promise.resolve(item).then(resolve,reject)
        })
    })
}


const p1 = Promise.resolve('p1')
const p2 = new Promise((resolve, reject) => {
  setTimeout(() => {
    resolve('p2 延时一秒')
  }, 1000)
})
const p3 = new Promise((resolve, reject) => {
  setTimeout(() => {
    resolve('p3 延时两秒')
  }, 2000)
})

const p4 = Promise.reject('p4 rejected')

const p5 = new Promise((resolve, reject) => {
  setTimeout(() => {
    reject('p5 rejected 延时1.5秒')
  }, 1500)
})

// p1无延时,p2延时1s,p3延时2s
Promise.MyRace([p1, p2, p3])
  .then(res => console.log(res))
  .catch(err => console.log(err)) // p1

// p4无延时reject
Promise.MyRace([p4, p2, p3])
  .then(res => console.log(res))
  .catch(err => console.log(err)) // p4 rejected
  
// p5 延时1.5秒reject,p2延时1s
Promise.MyRace([p5, p2, p3])
  .then(res => console.log(res))
  .catch(err => console.log(err)) // 1s后打印: p2 延时一秒

Promise.allSettled()

promise.allSettled() 一定是 所有 promise 的状态发生变更之后 才会返回结果 注意: reject() 不会中断后续执行

方法接受一个数组作为参数,数组的每个成员都是一个 Promise 对象,并返回一个新的 Promise 对象。只有等到参数数组的所有 Promise 对象都发生状态变更(不管是fulfilled还是rejected),返回的 Promise 对象才会发生状态变更,一旦发生状态变更,状态总是fulfilled,不会变成rejected

手写

Promise.MyAllSettled = function (promises) {
  let arr = [],
    count = 0
  return new Promise((resolve, reject) => {
    promises.forEach((item, i) => {
      Promise.resolve(item).then(res => {
        arr[i] = { status: 'fulfilled', val: res }
        count += 1
        if (count === promises.length) resolve(arr)
      }, (err) => {
        arr[i] = { status: 'rejected', val: err }
        count += 1
        if (count === promises.length) resolve(arr)
      })
    })
  })
}

案例

Promise.MyAllSettled = function (promises) {
  let arr = [],
    count = 0
  return new Promise((resolve, reject) => {
    promises.forEach((item, i) => {
      Promise.resolve(item).then(res => {
        arr[i] = { status: 'fulfilled', val: res }
        count += 1
        if (count === promises.length) resolve(arr)
      }, (err) => {
        arr[i] = { status: 'rejected', val: err }
        count += 1
        if (count === promises.length) resolve(arr)
      })
    })
  })
}


const p1 = Promise.resolve('p1')
const p2 = new Promise((resolve, reject) => {
  setTimeout(() => {
    resolve('p2 延时一秒')
  }, 1000)
})
const p3 = new Promise((resolve, reject) => {
  setTimeout(() => {
    resolve('p3 延时两秒')
  }, 2000)
})

const p4 = Promise.reject('p4 rejected')

const p5 = new Promise((resolve, reject) => {
  setTimeout(() => {
    reject('p5 rejected 延时1.5秒')
  }, 1500)
})

// 所有 Promise 实例都成功
Promise.MyAllSettled([p1, p2, p3])
  .then(res => console.log(res))
  .catch(err => console.log(err)) 
// [
//   { status: 'fulfilled', value: 'p1' },
//   { status: 'fulfilled', value: 'p2 延时一秒' },
//   { status: 'fulfilled', value: 'p3 延时两秒' }
// ]

// 有一个 Promise 失败
Promise.MyAllSettled([p1, p2, p4])
  .then(res => console.log(res))
  .catch(err => console.log(err))
// [
//   { status: 'fulfilled', value: 'p1' },
//   { status: 'fulfilled', value: 'p2 延时一秒' },
//   { status: 'rejected' , value: 'p4 rejected' }
// ]

// 所有 Promise 都失败
Promise.MyAllSettled([p4, p5])
  .then(res => console.log(res))
  .catch(err => console.log(err))
// [
//   { status: 'rejected', reason: 'p4 rejected' },
//   { status: 'rejected', reason: 'p5 rejected 延时1.5秒' }
// ]

Promise.any

Promise.any() 中只要有一个 Promise 实例成功就成功

与 Promise.all 可以看做是相反的。只有当所有的 Promise 实例失败时 Promise.any 才失败,此时Promise.any 会把所有的失败/错误集合在一起,返回一个失败的 promise 和AggregateError类型的实例。

手写

Promise.MyAny = function (promises) {
  let arr = [],
    count = 0
  return new Promise((resolve, reject) => {
    promises.forEach((item, i) => {
      Promise.resolve(item).then(resolve, err => {
        arr[i] = { status: 'rejected', val: err }
        count += 1
        if (count === promises.length) reject(new Error('没有promise成功'))
      })
    })
  })
}

案例

Promise.MyAny = function (promises) {
  let arr = [],
    count = 0
  return new Promise((resolve, reject) => {
    promises.forEach((item, i) => {
      Promise.resolve(item).then(resolve, err => {
        arr[i] = { status: 'rejected', val: err }
        count += 1
        if (count === promises.length) reject(new Error('没有promise成功'))
      })
    })
  })
}


const p1 = Promise.resolve('p1')
const p2 = new Promise((resolve, reject) => {
  setTimeout(() => {
    resolve('p2 延时一秒')
  }, 1000)
})
const p3 = new Promise((resolve, reject) => {
  setTimeout(() => {
    resolve('p3 延时两秒')
  }, 2000)
})

const p4 = Promise.reject('p4 rejected')

const p5 = new Promise((resolve, reject) => {
  setTimeout(() => {
    reject('p5 rejected 延时1.5秒')
  }, 1500)
})

// 所有 Promise 都成功
Promise.MyAny([p1, p2, p3])
  .then(res => console.log(res))
  .catch(err => console.log(err)) // p1
  
// 两个 Promise 成功
Promise.MyAny([p1, p2, p4])
  .then(res => console.log(res))
  .catch(err => console.log(err)) // p1

// 只有一个延时成功的 Promise
Promise.MyAny([p2, p4, p5])
  .then(res => console.log(res))
  .catch(err => console.log(err)) // p2 延时1秒

// 所有 Promise 都失败
Promise.MyAny([p4, p5])
  .then(res => console.log(res))
  .catch(err => console.log(err)) // AggregateError: All promises were rejected

react router exact starict 区别

exact

exact默认为false,如果为true时,需要和路由相同时才能匹配,但是如果有斜杠也是可以匹配上的。
如果在父路由中加了exact,是不能匹配子路由的,建议在子路由中加exact,如下所示

//父路由
<Switch>
    <Route path="/a" component={ComponentA} />
</Switch>
//子路由,tuanDetail组件里
<Switch>
        <Route path="/a/b" exact component={ComponentB}/>
</Switch>

strict

<Route strict path="/one" component={About} />

strict默认为false,如果为true时,路由后面有斜杠而url中没有斜杠,是不匹配的

\

实现一个最大并发数的 批量请求 有顺序返回

利用 promise.allSettled 配合 数组切片 (maxNum)


function sliceArr (arr, count) {
  let result = [];
  //遍历输出成员
  arr.forEach((item, index) => {
    let temp = Math.floor(index / count);
    //检验数组是否初始化
    if (!(result[temp] instanceof Array)) {
      result[temp] = new Array;
    }
    result[temp].push(item);
  })
  return result;
}


const allRequest = async (urls, maxNum) => {
  //  切片
  const arr = sliceArr(urls, maxNum)
  const result = []

  for (let i = 0; i < arr.length; i++) {
    const data = await Promise.allSettled(arr[i])
    result.push(...data)
  }

  console.log(result)
  return result

}

const p1 = new Promise((resolve, reject) => {
  resolve('p1')
})

const p2 = new Promise((resolve, reject) => {
  resolve('p2')
})

const p3 = new Promise((resolve, reject) => {
  reject('p3')
})

const p4 = new Promise((resolve, reject) => {
  resolve('p4')
})

const p5 = new Promise((resolve, reject) => {
  resolve('p5')
})

const p6 = new Promise((resolve, reject) => {
  resolve('p6')
})

allRequest([p1, p2, p3, p4, p5, p6], 2)

Promise 执行顺序 (Js引擎)

Promise.resolve().then(() => {
    console.log(0);
    return Promise.resolve(4);
}).then((res) => {
    console.log(res)
    return 
})
.then((res) => {
    console.log(7)
})
.then((res) => {
    console.log(8)
})

Promise.resolve().then(() => {
    console.log(1);
}).then(() => {
    console.log(2);
}).then(() => {
    console.log(3);
}).then(() => {
    console.log(5);
}).then(() =>{
    console.log(6);
})

// 大家先思考一下

结论

Js引擎为了让microtask尽快的输出,做了一些优化,连续的多个then(3个) 如果没有reject或者resolve会交替执行then而不至于让一个堵太久完成用户无响应,不单单v8这样 其他引擎也是这样,因为其实promise内部状态已经结束了。这块在v8源码里有完整的体现

答案

通过结论 得出答案

0,1,2,3,4,5,7,6,8

运行 npm run xxx 的时候发生了什么?

解答

对象隐士转换

var a = { name: "Sam" };
var b = { name: "Tom" };
var o = {};
o[a] = 1;
o[b] = 2;
console.log(o[a]);

答案 : 2

原因

  1. 对象会隐士转换成 "[object Object]"
  2. 所以 o[a] = o[ "[object Object]" ] = 1 o[b] = o[ "[object Object]" ] =2
  3. 改变的都是o 对象里面的 "[object Object]" 这个key 的value 所以是2

js 为什么基本数据类型存到栈但是引用数据类型存到堆

原始值:

存储在栈(stack)中的简单数据段,也就是说,它们的值直接存储在变量访问的位置

这是因为这些原始类型占据的空间是固定的,所以可将他们存储在较小的内存区域 – 栈中。这样存储便于迅速查寻变量的值。

引用值:

存储在堆(heap)中的对象,也就是说,存储在变量处的值是一个指针(point),指向存储对象的内存地址。

这是因为:引用值的大小会改变,所以不能把它放在栈中,否则会降低变量查寻的速度。相反,放在变量的栈空间中的值是该对象存储在堆中的地址。 地址的大小是固定的,所以把它存储在栈中对变量性能无任何负面影响。

SPA 和 MPA 的区别

什么是SPA

  1. SPA(single-page application),
  2. 翻译过来就是单页应用SPA是一种网络应用程序或网站的模型,它通过动态重写当前页面来与用户交互
  3. 这种方法避免了页面之间切换打断用户体验在单页应用中,所有必要的代码(HTML、JavaScript和CSS)都通过单个页面的加载而检索,
  4. 我们熟知的JS框架如react,vue,angular,ember都属于SPA

什么 是 MPA

  1. 多页应用MPA(MultiPage-page application)
  2. 翻译过来就是多页应用在MPA中,每个页面都是一个主页面,都是独立的当我们在访问另一个页面的时候,都需要重新加载html、css、js文件
  3. 公共文件则根据需求按需加载如下图

单页应用与多页应用的区别

单页面应用(SPA)多页面应用(MPA)
组成一个主页面和多个页面片段多个主页面
刷新方式局部刷新整页刷新
url模式哈希模式历史模式
SEO搜索引擎优化难实现,可使用SSR方式改善容易实现
数据传递容易通过url、cookie、localStorage等传递
页面切换速度快,用户体验良好切换加载资源,速度慢,用户体验差
维护成本相对容易相对复杂

单页应用优缺点

优点:

  • 具有桌面应用的即时性、网站的可移植性和可访问性
  • 用户体验好、快,内容的改变不需要重新加载整个页面
  • 良好的前后端分离,分工更明确

缺点:

  • 不利于搜索引擎的抓取
  • 首次渲染速度相对较慢

单页应用的首评优化

实现一个函数,每调用一次,函数输出值 +1

const add = (function () {
  let i = 0
  return function () {
    return ++i
  }
})()
console.log(add())// 1
console.log(add())// 2
function add () {
  let i = 0
  return function () {
    return ++i
  }
}

const temp = add() // 先用一个临时变量接收执行 add 的返回值,再调用 temp()

console.log(temp()) // 1
console.log(temp()) // 2

// 如果直接写 add()() 不会生效
function fn () {
  var i = 0

  return { // fn 函数返回一个对象,有 get、set 等方法,来操作私有变量 i
    get: function () {
      return i
    },
    set: function (val) {
      i = val
    },
    increment: function () {
      return ++i
    }
  }
}

const counter = fn() // 执行 fn 函数,拿到 counter 对象
counter.get() // 0
counter.set(3)
counter.increment() // 4
counter.increment() // 5

路由原理

两种模式

前端路由实现起来其实很简单,本质就是监听 URL 的变化,然后匹配路由规则,显示相应的页面,并且无须刷新页面。目前前端使用的路由就只有两种实现方式

  • Hash 模式
  • History 模式

Hash 模式

www.test.com/#/ 就是 Hash URL,当 # 后面的哈希值发生变化时,可以通过 hashchange 事件来监听到 URL 的变化,从而进行跳转页面,并且无论哈希值如何变化,服务端接收到的 URL 请求永远是 www.test.com。

window.addEventListener('hashchange', () => {
  // ... 具体逻辑
})

History 模式

History 模式是 HTML5 新推出的功能,主要使用 history.pushState 和 history.replaceState 改变 URL。

通过 History 模式改变 URL 同样不会引起页面的刷新,只会更新浏览器的历史记录。

// 新增历史记录
history.pushState(stateObject, title, URL)
// 替换当前历史记录
history.replaceState(stateObject, title, URL)

当用户做出浏览器动作时,比如点击后退按钮时会触发 popState 事件

window.addEventListener('popstate', e => {
  // e.state 就是 pushState(stateObject) 中的 stateObject
  console.log(e.state)
})

两种模式对比

  • Hash 模式只可以更改 # 后面的内容,History 模式可以通过 API 设置任意的同源 URL
  • History 模式可以通过 API 添加任意类型的数据到历史记录中,Hash 模式只能更改哈希值,也就是字符串
  • Hash 模式无需后端配置,并且兼容性好。History 模式在用户手动输入地址或者刷新页面的时候会发起 URL 请求,后端需要配置 index.html 页面用于匹配不到静态资源的时候