查漏补缺

284 阅读12分钟

本文主要针对23年初本人面试过程中出现的问题进行查漏补缺

1.Vue的双向数据绑定和响应式原理

双向数据绑定

v-model作为一个语法糖,它做了:

  • 绑定数据value
  • 触发事件input
  • data更新触发渲染

响应式原理

2.如何判断一个空对象

1. for...in...遍历属性

let judgeObj = function(obj) {
    for (let key in obj) {
        return true;
    }
    return false;
}

2. Object.keys()方法

let judgeObj = function(obj) {
    return Object.prototype.isPrototypeOf(obj) && Object.keys(obj).length === 0;
};
judgeObj(1) // false
judgeObj([]) // false
judgeObj({}) // true

3.JSON.stringfy()转化

let judgeObj = function(obj) {
    return JSON.stringfy(obj) === '{}';
}

3.如何获取Vue的data中数据的初始值

this.$options.data().xxx

注意:data()中若使用了this来访问props或者methods,用this.$options.data()重置组件data时,data()里使用this来获取props或者methods都为undefined,注意this.$options.data()中的this的指向,最好使用this.$options.data().call(this);

4.如何使Vue中data数据不再具有响应式

将Object.defineProperty()方法中的configurable设置为false

5.keep-alive的实现原理

keep-alive组件接收三个参数:include、exclude、max

  • include指定需要缓存的组件name集合,参数格式支持String/RegExp/Array。当为字符串的时候,多个组件名以逗号隔开。
  • exclude指定不需要缓存的组件name的集合,参数格式和include一样。
  • max指定最多可缓存组件的数量,超过数量删除一个。参数格式支持String/Number

原理

keep-alive实例会缓存对应组件的VNode,如果命中缓存,直接从缓存对象返回对应的VNode LRU(Last recently used)算法根据数据的历史访问记录来进行淘汰数据,其核心思想是如果数据最近被访问过,将来被访问的几率更高

组合函数

compose函数可以接受多个独立的函数做参数,然后将这些函数进行组合串联,最终返回一个组合函数

组合函数特点

  • 参数是多个函数,返回值是组合函数。
  • 组合函数内所有的函数自右向左逐个执行。
  • 组合函数除了第一个执行函数的参数是多元的,其他函数的参数都是接受上一个函数的返回值。

使用形式

let sayHello = (...str) => 'Hello, `${str.join("And")}`';
let toUpper = str => str.toUpperCase();
let combin = compose(sayHello, toUpper);

combin('jack', 'Bob'); // HELLO, JACK AND BOB;

管道

管道(pipeline)实现同compomse的实现方式很类似,因为二者的区别仅仅是数据流的方向不同而已。pipe的数据流处理是按照自左向右的方向。

100的阶乘

由于100的阶乘会导致数据溢出,所以需要使用基本数据类型BigInt来解决

BigInt 是一种内置对象,它提供了一种方法来表示大于 2^53 - 1 的整数。这原本是 Javascript 中可以用 Number 表示的最大数字。BigInt 可以表示任意大的整数。

function factorial_recursion(n) {
    if (n <= 1) {
        return 1;
    }
    return BigInt(n) * BigInt(factorial_recursion(n - 1));
}

位置调换

let a = 'aa';
let b = 'bb';
[a, b] = [b, a];
console.log(a); // 'bb'
console.log(b); // 'aa'

HTTP2和HTTP1有什么区别

HTTP1.0和HTTP1.1对比

  • 缓存处理:多了ETagLast-Modified等缓存信息
  • 带宽优化及网络连接的使用
  • 错误通知的管理
  • Host头的处理
  • 长连接:HTTP1.1默认采用开启Connection:keep-alive,一定程度上弥补了1.0每次都要创建连接的缺点

HTTP2和HTTP1.1的对比

  • HTTP2支持二进制传送(实现方便且健壮),HTTP1.1是字符串传送
  • HTTP2支持多路复用
  • HTTP2采用HPACK压缩算法压缩头部,减小了传输的体积
  • HTTP2支持服务端推送

Vuex的单向数据流

概念:单向数据流指只能从一个方向来修改状态。

下面是Vuex的状态管理流程图: image.png

可以看出,Vuex的完整流程是:Vue Components中触发ActionAction提交MutationsMutations修改StateVue Components根据State或者Getters来渲染页面。

store是数据存储的地方,是一个单一的数据源,需要挂载到根组件上,可以通过this.$store在任何组件中访问。

唯一改变状态的方式,就是显式的提交mutation,如果你需要进行异步操作,使用action中的context.commit改变mutation。如果你要请求数据,只需要dispatch一个对应的action即可。

Vue2和Vue3diff算法的不同

Vue2采用的是双端比较

Vue3采用的是去除两端的最长递增子序列,而且最长递增子序列中采用的是贪心算法+二分查找

同步异步问题细化

JavaScript是单线程的,但是浏览器内核是多线程的。

浏览器会执行主线程的同步任务,遇到异步任务会放到EventLoop中,等到处理完成,通过callback通知主线程,主线程再执行相应的操作。

JS执行是同步的,但是可以通过事件循环实现异步。执行机制是:

  • 首先判断JS代码是同步还是异步,同步就进入主线程,异步就进入事件表
  • 异步任务在事件表中注册函数,当满足触发条件后,会被推入事件队列
  • 同步任务进入主线程后一直执行,直到主线程空闲时,才会去事件队列中查看是否有可执行的异步任务,如果有就推入到主线程中

以上三步循环执行,这就是事件循环。

Ajax默认是异步任务。

vue中的$nextTick的源码

Vue2

  • 优先使用微任务。
  • 宏任务执行优先级:setImmediate > MutationObserverMessageChanel(2.6版本由MessageChanel改为了MutationObserver) > setTimeout
  • 接受两个参数:cb(回调函数)ctx(上下文对象),都是非必选。
  • 如果不传参数,则会在有Promise的环境返回一个Promise,无Promise的环境需要自己写polyfill

问:为啥DOM渲染在微任务之后,为什么在$nextTick中可以拿到渲染后的DOM呢? 答:在$nextTick中拿到的是DOM对象,而非渲染后的DOM

Vue3

抛弃了兼容性,直接采用了Promise来实现了nextTick

结论就是nextTick的本质就是创建了一个微任务,将其回调推入微任务队列。

CSS3开启GPU加速

四个属性:transformopacityfilterwill-change

深拷贝

function deepClone(target, map = new WakeMap()) {
  if (typeof target === 'object') {
    const cloneTarget = Array.isArray(target) ? [] : {};
    if (map.get(target)) {
      return map.get(target);
    }
    map.set(target, cloneTarget);
    for (const key in target) {
      cloneTarget[key] = deepClone(target[key], map);
    }
    return cloneTarget;
  } else {
    return target;
  }
}

为啥0.1+0.2!==0.3

原因

  • 进制转换:JS在做数字计算的时候,0.1和0.2都会被转成二进制然后无线循环,但是JavaScript采用的是IEEE-754二进制浮点计算,最大可存储53位有效数字,于是大于53位的会全部被截掉,将导致精度丢失;
  • 对阶运算:由于指数位数不相同,需要对阶运算,阶数小的尾数要根据阶差来右移,尾数位移时可能会发生数丢失的的情况,影响精度。

解决方法

1.转为整数(大数)运算

function isEqual(a, b) {
  return Math.abs(a - b) < Number.EPSILON;
}
console.log(isEqual(0.1 + 0.2, 0.3)); // true

2.toFixed

function fn(a, b) {
  return (a + b).toFixed(1);
}
fn(0.1, 0.2); // 0.3

3.*10

参考资料:0.1+0.2为什么不等于0.3,以及怎么等于0.3

数组和链表的区别

数组定义:数组是有序的元素的集合,在内存上会分配一段连续的内存地址,扩容需要重新分配内存。 链表:是一种物理存储单元上非连续、非顺序的存储结构,数据元素的逻辑顺序是通过链表中的指针链接次序实现的。内存分配不要连续的内存,用多少分配多少。

  • 由于数组是有序的,所以可以通过下标维护查询,支持随机查找,时间复杂度为O(1),但是新增和修改都需要关联前后元素,所以时间复杂度为O(n)
  • 由于链表(默认是单链表)在内存中分配不连续,所以需要根据next指针去寻找,时间复杂度为O(n),但是增删由于不需要移动元素位置,只需要改变一下next指针,所以十分简单,时间复杂度则为O(1)

应用reduce的业务场景

语法

arr.reduce(cb, initialValue); // cb:回调函数,initialValue:初始值
/**
 * previousValue:上一次调用回调的返回值,或者提供的初始值initialValue
 * currentValue:数组当前被处理的元素
 * index:当前元素的下标
 * arr:当前数组
 */
cb(previousValue, currentValue, index, arr)

业务场景中的实际应用

用reduce过滤指定需要的字段

let sourceArr = [
    {id: 1, name: 'Web技术学苑', age: 18},
    {id: 2, name: 'Maic', age: 20},
    {id: 3, name: 'Tom', age: 16},
  ]
const ret = sourceArr.reduce((prev, cur) => {
  const {id, age} = cur;
  return prev.concat({id, age})
}, [])
console.log(ret);
// [ { id: 1, age: 18 }, { id: 2, age: 20 }, { id: 3, age: 16 } ]

统计一个字符出现的次数

const strCount2 = (arr) => {
  return arr.reduce((prev, cur) => {
    if (cur in prev) {
      prev[cur]+=1;
    } else {
      prev[cur] = 1;
    }
    return prev
  }, {})
}
console.log('ret6', strCount2(['a', 'a', 'b', 'c', 'd']))

数组去重

const ret9 = sourceData.reduce((prev, cur) => {
    if (prev.indexOf(cur) === -1) {
      prev.push(cur)
    }
    return prev
}, [])

代替filter和map

// 用filter和map在原数据中进行过滤
var publicInfo = [
    {
        id: '1',
        name: 'Web技术学苑',
        age: 10
    },
    {
        id: '2',
        name: '前端从进阶到入院',
        age: 10
    },
    {
        id: '3',
        name: '前端之神',
        age: 12
    },
    {
        id: '3',
        name: '前端之巅',
        age: 12
    }
]
const ret11 = publicInfo.filter(v => v.age >10).map(v => v.name);
console.log(ret11); // ['前端之神', '前端之巅']
// 用reduce一次搞定
...
publicInfo.reduce((prev, cur) => {
  if (cur.age > 10) {
      prev.push(cur.name)
    }
    return prev
}, [])

强制缓存和协商缓存

强制缓存

Expires:时间戳,返回具体过期时间,http1.0的产物,存在兼容性问题。 Cache-Control:

  • max-age(常用)缓存的内容将在max-age秒后失效。
  • no-cache(常用)不要本地强制缓存,正常向服务端请求(只要服务端最新的内容)。需要使用协商缓存来验证缓存数据(Etag Last-Modified)。
  • no-store 不要本地强制缓存,也不要服务端做缓存,所有内容都不会缓存,强制缓存和协商缓存都不会触发。
  • public 所有内容都将被缓存(客户端和代理服务器都可缓存)。
  • private 所有内容只有客户端可以缓存。

强制缓存的优先级高于协商缓存

强制缓存流程

  • 浏览器第一次请求资源,服务器返回资源和Cache-Control Expires
  • 浏览器第二次请求资源,会带上Cache-Control Expires,服务器根据这两个值判断是否命中强制缓存
  • 命中强制缓存,直接从缓存中读取资源,返回给浏览器
  • 未命中强制缓存,会带上If-Modified-Since If-None-Match,服务器根据这两个值判断是否命中协商缓存
  • 命中协商缓存,返回304,浏览器直接从缓存中读取资源
  • 未命中协商缓存,返回200,浏览器重新请求资源

强制缓存流程图 流程图1 流程图2

协商缓存

协商缓存是服务端缓存策略,服务端判断客户端资源,是否和服务端资源一样。如果判断一致则返回304(不再返回js、图片内容等资源),否则返回200和最新资源。

Response Headers有两种资源标识:Last-ModifiedEtag。会优先使用EtagLast-Modified只能精确到秒级,如果资源被重复生成而内容不变,则Etag更准确。

Last-Modified 服务端返回的资源的最后修改时间

If-Modified-Since: 客户端请求时,携带的资源的最后修改时间(即Last-Modified的值) Last-Modified

Etag服务端返回的资源的唯一标识(一个字符串,类似指纹)

If-None-Match 客户端请求时,携带的资源的唯一标识(即Etag的值) Etag

HTTP缓存总结 HTTP缓存总结

刷新方式对缓存的影响:

  • 正常操作(地址栏输入url,跳转链接,前进后退):强缓存有效,协商缓存有效
  • 手动操作(F5,点击刷新,右键菜单刷新):强缓存失效,协商缓存有效
  • 强制刷新(ctrl + F5 或 command + r):强缓存失效,协商缓存失效

强制缓存是根据过期时间来使用的,协商缓存是根据文件有没有修改来使用的,如果过期了就需要使用协商缓存来确定文件有没有修改,如果修改了就需要服务器返回修改后的资源,如果没有修改就还是可以使用缓存的资源。

属性透传

由于开发中会有将第三方组件库封装的情况,例如封装ElementUI,会有将属性传给框架中的组件,例如el-input的情况,推荐使用$attrs。 让我们先看一下官方对$attrs的定义:

包含了父作用域中不作为 prop 被识别 (且获取) 的 attribute 绑定 (classstyle 除外)。当一个组件没有声明任何 prop 时,这里会包含所有父作用域的绑定 (classstyle 除外),并且可以通过 v-bind="$attrs" 传入内部组件——在创建高级别的组件时非常有用。

// 组件parent
<template>
  <child-component :propertyA="propertyA" :propertyB="propertyB" @clickA="clickA" @clickB="clickB"></child-component>
</template>
<script>
import ChildComponent from '@/component/ChildComponent.vue';
export default {
  name: 'Parent',
  data() {
    return {
      propertyA: 'propertyA',
      propertyB: 'propertyB'
    }
  },
  methods: {
    clickA(msg = '') {
      console.log(`clickAGet${msg}`);
    },
    clickB(msg = '') {
      console.log(`clickBGet${msg}`);
    }
  }
}
</script>
// 组件child
<template>
  <el-input v-bind="$attrs" v-bind="$listeners"></el-input>
</template>
<script>
import ElInput from 'ElementUI';
export default {
  name: 'child',
  data() {
    return {
      propertyA: 'propertyA',
      propertyB: 'propertyB'
    }
  },
  props: {
    propertyA: { // 会消费掉propertyA属性,不会透传给el-input
      type: String,
    }
  },
  mount() {
    this.$emit('clickA', 'child here'); // 由于绑定了$listeners,这里会触发
  }
}
</script>
// 组件-input
<template>
  <div>$attrs: {{$attrs}}</div>
</template>
<script>
export default {
  name: 'el-input',
  mount() {
    console.log(this.$attrs); // { propertyB: "propertyB" }
    this.$emit('clickB', 'el-input here'); // 由于绑定了$listeners,这里会触发
  }
}
</script>

参考文章:Vue中$attrs$listeners 详解及使用

$listeners$attrs在透传时和新增事件和属性的合并规则: 同名事件会做类似于数据的concat操作,属性则会覆盖。 详见掘金的此篇文章

git merge 和 git rebase

rebase作为merge的替代方法,可以将master分支变成你自己分支的其中一部分且不产生merge记录。

下面三幅图更好的解释了什么叫rebase(变基)

图一: 图一 图二: 图二 图三: 图三 通过图二和图三的对比,我们可以清楚地看出变基和合并的区别:

  • 合并会将两个分支的历史联系在一起,不会改变两个分支的commit hash
  • 变基则是将当前更改分支移动到master分支的顶端,整合了所有来自master的新提交,但是,rebase不是使用merge commit,而是通过为原始分支中的每个提交创建全新的提交来重写项目历史记录。简单点说,就是当前更改分支的commit hash全都更改为新提交的commit hash了,但是commit message不变

基本原则

  1. 下游分支更新上游分支内容的时候使用 rebase
  2. 上游分支合并下游分支内容的时候使用 merge
  3. 更新当前分支的内容时一定要使用 --rebase 参数

例如:现有上游分支 master,基于 master 分支拉出来一个开发分支 dev,在 dev 上开发了一段时间后要把 master 分支提交的新内容更新到 dev 分支,此时切换到 dev 分支,使用 git rebase master 等 dev 分支开发完成了之后,要合并到上游分支 master 上的时候,切换到 master 分支,使用 git merge dev

==的问题

均以a == b来举例

同类型比较时

  1. abundefinednull 时,结果为 true
  2. ab均为 string 时,字符串内容相同时为 true
  3. abnumber 类型时, NaN 与谁都不相等
  4. abboolean 时,同值相等
  5. abobject 时,只有当两者指向同一个内存地址才相等
{} == {} // false
[] == [] // false
new String('a') == new String('a') // false
// 指向同一个内存地址
const obj = {};
const a = obj;
const b = obj;
a == b // true

不同类型比较

  1. a、b中有一个是 undefined 或者 null 时,为 true
  2. a、b中有一个是 number ,一个是 string ,那么会将 string 转化为 number 进行比较
1 == '1' // true,转换成1 == 1进行比较
  1. ab 存在 boolean 时,会将 boolean 转为 number 再进行比较
1 == true // true,转化成 1 == 1进行比较;
'1' == true // true 转化成 '1' == 1 => 1 == 1
'2' == true // false
'0' == false // true
  1. ab 中有一个是object(非null),且另一个为基本数据类型 ,则先调用 objectvalueOf方法,若结果为基本数据类型,则返回结果进行比较;否则调用objecttoString方法,若结果为基本数据类型,则返回结果进行比较,否则抛出错误
// 优先调用valueOf
"a" == new String("a"); // true, new String('a').valueOf() === 'a'

let obj = {
    valueOf(){
        return 2
    }
}
2 == obj // true

// valueOf没有返回基本类型则调用toString
let obj1 = {
    toString(){
        return '3'
    }
}
3 == obj1 // true

// toString没有返回基本类型,报错
let obj2 = {
    toString(){
        return {}
    }
}
3 == obj2 // 报错 Cannot convert object to primitive value

0 == [] // true 转化为 0 == '' => 0 == 0

需要注意的是,引用数据类型转化为boolean会被转为true,所以以下例子可能会有些出乎意料

true == [] // false,[] 被转化成'' => 0;当引用类型和基本类型相比较时,先调用引用类型的valueOf方法,[]的valueOf为'',再进行字符串和boolean比较,会将它们都转化为数字进行比较,而true -> 1,1和0不等,所以比较结果为false
true == ![] // false, 转化成 true == !true;这里存在!运算符,则先进行取反,上面所说的引用类型被转化为boolean类型时,会被转化为true,则![]会被转化为!true,该等式结果为false
true == !![] // true, 转化成 true == !!true;
// 同理
false == [] // true
false == ![] // true
false == !![] // false
[] == ![] // true 本质上是[] == false; 转换为 '' == false  =>  0 == 0  =>  true
{} == !{} // false 本质上是NaN == 0;!{}根据上面规则,转化为!true -> 0,{}和基本类型比较时,先调用valueOf,没有得到基本类型数据,再调用toString,会被转化为NaN,而NaN和任意值都不等,所以等式结果为false。

问题延伸

所以,如何使(a == 1 && a == 2 && a == 3)呢? 答案是:

const a = { value : 0 };
a.valueOf = function() {
    return this.value += 1;
};

console.log(a==1 && a==2 && a==3); //true

那么,有办法使(a === 1 && a === 2 && a === 3)成立吗? 还真有:

var value = 0; // window.value
Object.defineProperty(window, 'a', {
    get: function() {
        return this.value += 1;
    }
});

console.log(a===1 && a===2 && a===3); // true

详见此篇文章

JS实现随机背景色

方案一:采用十六进制法则

function randomColor() {
    const color = `#${parseInt(Math.random() * 0xFFFFFF).toString(16)}`
    return color
}

方案二:采用RGB

function randomParams() {
    return Math.floor(Math.random() * 256)
}
function randomColor() {
    return `rgb(${randomParams}, ${randomParams}, ${randomParams})`
}

一个数组中连续出现次数最多的数和对应的次数

function mostTimesNum(arr) {
    if (arr.length === 0) { // 非空判断
        return '数组为空'
    }
    let num = 0 // 记录重复次数
    let max = 0 // 记录最大重复次数
    let maxItem = null // 数字的值,如果均不重复,则取第一个值
    for (let i = 0; i < arr.length; i++) {
        if (arr[i] === arr[i+1]) { // 当出现连续重复的元素时
            num++
            if (max < num) { // 当超过最大次数时,将值和次数更新
                max = num
                maxItem = arr[i]
            }
        } else { 
            num = 1
        }
    }
    if (maxItem) {
        console.log(`出现次数最多的数字为${maxItem},连续出现了${max}次`)
    } else {
        console.log('数组无重复数字')
    }
}
mostTimesNum([]) // 数组为空
mostTimesNum([1, 2, 3]) // 数组无重复数字
mostTimesNum([1,2,3,3,2,2,2,3,3,3,3,5,3,3,3,3,3]) // 出现次数最多的数字为3,连续出现了5次

求两个数组的交、并、差集

const arr1 = [1, 2, 3]
const arr2 = [2, 3, 4, 5]

const union = arr1.concat(arr2.filter(item => !arr1.includes(item))) // 并集

const intention = arr1.filter(item => arr2.includes(item)) // 交集

const difference = arr1.concat(arr2).filter(item => !(arr1.includes(item) && arr2.includes(item))) // 差集

函数柯里化

柯里化,即Currying的音译,是编译原理层面实现多参函数的一个技术。 柯里化为实现多参函数提供了一个递归降解的实现思路——把接受多个参数的函数变成接受一个单一参数的函数,并且返回一个新的函数,这个函数可以接受余下的参数并继续返回一个函数。

使用场景:

  • 减少重复传递不变的部分参数
  • 将柯里化的callback参数传递给map,filter等函数

手写:

// ES5
function curry(fn, args) {
    const len = fn.length;
    args = args || [];
    return function() {
        const subArgs = args.slice(0)
        for (let i = 0; i < arguments.length; i++) {
            subArgs.push(arguments[i])
        }
        return subArgs.length >= len ? fn.apply(this, subArgs) : curry.call(this, fn, subArgs)
  }
}
// ES6
function curry(fn, ...args) {
  return fn.length <= args.length ? fn(...args) : curry.bind(null, fn, ...args);
}

二分查找

function search(arr, target) {
  const len = arr.length;
  if (len === 0) {
    return -1;
  }
  let startIndex = 0;
  let endIndex = len - 1;
  while (startIndex <= endIndex) {
    const midIndex = Math.floor((startIndex + endIndex) / 2);
    const midValue = arr[midIndex];
    if (target > midValue) {
      startIndex = midIndex + 1;
    } else if (target < midValue) {
      endIndex = midIndex - 1;
    } else {
      return midIndex;
    }
  }
  return -1;
}

Vue.use的原理及作用

Vue.use是什么

Vue.use()是Vue提供的一种全局注册插件的方法。 Vue.use()会自动阻止多次注册相同的插件,需要你在调用new Vue()启动应用之前完成。 Vue.use()方法至少传入一个参数,该参数的类型必须为Object或Function,如果是Object,那么这个Object需要定义一个install方法,在Vue.use()调用的时候会默认执行。 所以,Vue.use()的使用时机是在当引入的插件中有install方法的时候。

源码

export function initUse (Vue: GlobalAPI) {
 Vue.use = function (plugin: Function | Object) { // 规定传入的plugin只能是Function或Object
  const installedPlugins = (this._installedPlugins || (this._installedPlugins = []))
  if (installedPlugins.indexOf(plugin) > -1) { // 如果已经注册过,则不再注册
   return this
  }
  const args = toArray(arguments, 1)
  args.unshift(this)
  if (typeof plugin.install === 'function') { // 有install方法则直接调用install
   plugin.install.apply(plugin, args)
  } else if (typeof plugin === 'function') {
   plugin.apply(null, args)
  }
  installedPlugins.push(plugin)
  return this
 }
}

npm-link

是什么

npm-link称为软链,简单来说就是为开发的模块创建一个全局的链接,在主项目里链接这个依赖的模块,进行测试。

怎么用

如想在B项目中使用A项目的npm包,可以这样做:

# 先在A项目中创建link软连接
cd ~/projectA
npm link

# 在B项目中使用这个链接
cd ~/projectB
npm link packageName

使用完成之后,如何解除改软连接呢?

# 先在B项目中解除
cd ~/projectB
npm unlink packageName

# 再全局解除
npm unlink

Webpack VS Vite

ElementUI设计思想

  1. 丰富的feature 包含丰富的组件、主题和国际化

    • 在线换肤:ElementUI组件库中的所有组件的颜色和样式都是可以通过var.scss中定义的变量修改的,想要实现在线换肤,还需要底层server服务的帮助,需要通过接口将配置条件传到server端,接口返回一段css文本,替换掉head标签底部的样式
    • 国际化:使用i18nHandler函数,针对如vue-i18n等外部国际化文件进行兼容
  2. 文档&demo 对使用方面有详尽介绍的demo

  3. 安装&引入指导 CDN/npm的引入方式,可以全量引入或者按需引入

    • 按需引入原理:借助babel-plugin-component,配置.babelrc,实际上是将import { Button } from 'element-ui'转化成

          let button = require('element-ui/lib/button')
          require('element-ui/lib/theme-chalk/button.css')
      
  4. 工程化 自动打包部署构建集成

两数之和

// HashMap写法
let twoSum = (arr, target) => {
    const map = new Map()
    for (let i = 0; i < arr.length; i++) {
        if (map.has(target - arr[i])) {
            return map.get([target - arr[i], i])
        }
        map.set(arr[i], i)
    }
    return []
}

router

编程式导航

可以通过$router访问路由实力,可以调用this.router.push 在vue3中需要从vue-router中引用

import { useRoute, useRouter } from 'vue-router'

const route = useRoute()
const router = useRouter() // 路由实例
router.replace({path: '/home'}) // replace不会在history产生一条新纪录,而是取代当前记录
router.go(1) // 向前移动一条记录
router.forward(1) // 同router.go

有时,你可能希望将任意信息附加到路由上,如过渡名称、谁可以访问路由等。这些事情可以通过接收属性对象的meta属性来实现,并且它可以在路由地址和导航守卫上都被访问到。 定义路由的时候你可以这样配置meta字段:

const routes = [
  {
    path: '/posts',
    component: PostsLayout,
    children: [
      {
        path: 'new',
        component: PostsNew,
        // 只有经过身份验证的用户才能创建帖子
        meta: { requiresAuth: true }
      },
      {
        path: ':id',
        component: PostsDetail
        // 任何人都可以阅读文章
        meta: { requiresAuth: false }
      }
    ]
  }
]

字符串和数组互转

字符串转数组

  1. split()方法
  2. 展开运算符 [...]
  3. Array.from()
  • ...Array.from()可以将可迭代对象转化为数组,split方法不行
  • Array.from()适用于类数组对象和可迭代对象,而...只能适用于可迭代对象,所以对于转换为数组,Array.from()会更通用一些

数组转字符串

  1. join()方法
  2. toString()方法
  3. toLocalString()方法
  • join()方法可以指定分隔符,toString()方法无法指定分隔符,可以结合replace()方法使用
  • toLocalString()更适用于日期对象转换成字符串,因其提供了locale参数

Javascript的包装类型

JavaScript中,基本类型是没有属性和方法的,但是为了便于操作基本类型的值,在调用基本类型的属性或方法时,JavaScript会在后台隐式的将基本类型的值转化为对象,如:

  const a = 'abc'
  a.length // 3
  a.toUpperCase() // 'ABC'

包装对象只存在与当前行的代码命令中,如:

const a = 'abc'
a.test = 123
console.log(a.test) // undefined

上面第二行代码给自动创建的 String 实例对象添加了 test 属性,虽然此刻代码执行时他是生效的,但是在这行代码执行完毕后该 String 实例就会立刻被销毁,String 实例的 test 属性也就不存在了。 当执行第三行代码时,由于是读取模式,又重新创建了新的 String 实例,而这个新创建的 String 实例没有 test 属性,结果也就是 undefined。

用代码的解释方式如下

var str = 'hello';
str.number = 10; //假设我们想给字符串添加一个属性 number ,后台会有如下步骤
(
    var _str = new String('hello'); // 1 找到对应的包装对象类型,然后通过包装对象创建出一个和基本类型值相同的对象
    _str.number = 10; // 2 通过这个对象调用包装对象下的方法 但结果并没有被任何东西保存
    _str =null; // 3 这个对象又被销毁
)
console.log(str.number); // undefined  当执行到这一句的时候,因为基本类型本来没有属性,后台又会重新重复上面的步骤
(
   var str = new String('hello');// 1 找到基本包装对象,然后又新开辟一个内存,创建一个值为 hello 对象
   str.number = undefined;// 2 因为包装对象下面没有 number 这个属性,所以又会重新添加,因为没有值,所以值是未定义;然后弹出结果
   str =null; // 3 这个对象又被销毁
)

在访问'abc'.length属性时,JavaScriptabc在后台转换成String('abc'),然后再访问其length属性。 JavaScript也可以使用Object函数显示的将基本类型转化成包装类型

const a = 'aaa'
const b = Object(a) // [String: 'abc']
console.log(typeof b) // Object

也可以使用valueOf将包装类型变成基本类型

const a = 'abc'
const b = Object(a)
const c = b.valueOf() // c: '123' typeof c : string

判断如下代码的打印

const a = new Boolean(false)

if (!a) { // a被上式转化为包装类型,包装类型取反时按照对象处理,对象取反为false
  console.log('aaa') // 不会打印
}

箭头函数

  1. 箭头函数更加简洁
  2. 箭头函数没有this且其this的指向永远不会变
  3. 箭头函数没有arguments
  4. 箭头函数不能作为构造函数
  5. 箭头函数不能使用yeild的关键字
  6. callapplybind方法不能改变其指向

Iterator(遍历器)

Javascript有四种表示“集合”的数据结构,包括数组(Array)、对象(Object)、Map和Set。这样就需要一种统一的接口机制,来处理不同的数据结构。

遍历器(Iterator)就是这样一种机制。它是一种接口,为各种不同的数据结构提供统一的访问机制。任何数据结构只要部署了Iterator接口,就可以完成遍历操作。

Iterator的作用有三个:

  1. 为各种数据结构提供一个统一的访问接口
  2. 使得数据结构的成员能够按某种次序排列
  3. 供for...of消费

Iterator的遍历过程是这样的: (1)创建一个指针对象,指向当前数据结构的起始位置。也就是说,遍历器对象本质上,就是一个指针对象。 (2)第一次调用指针对象的next方法,可以将指针指向数据结构的第一个成员。 (3)第二次调用指针对象的next方法,指针就指向数据结构的第二个成员。 (4)不断调用指针对象的next方法,直到它指向数据结构的结束位置。

每一次调用next方法,都会返回数据结构的当前成员的信息。具体来说,就是返回一个包含valuedone两个属性的对象。其中,value属性是当前成员的值,done属性是一个布尔值,表示遍历是否结束。

下面是一个模仿next方法返回值的例子:

function makeIterator(array) {
  let nextIndex = 0
  return {
    next: function() {
      return nextIndex < array.length ?
        {value: array[nextIndex++], done: false} :
        {value: undefined, done: true}
    }
  }
}

const it = makeIterator(['a', 'b', 'c'])

it.next() // { value: 'a', done: false }
it.next() // { value: 'b', done: false }
it.next() // { value: 'c', done: false }
it.next() // { value: undefined, done: true }

// 当然,done的false和value的undefined都是可以省略的,所以可以简写成:

function makeIteratorSimple(array) {
  let makeIndex = 0
  return {
    next: function() {
      return makeIndex < array.length ? {value: array[makeIndex++]} : {done: true}
    }
  }
}

BigInt类型

BigInt是一种内置对象,可以安全的表示表示-2^53 ~ 2^53 - 1之外的整数

可以用在一个整数字面量后面加n的方式定义一个BigInt,或者调用函数BigInt()(但不包含new运算符)并传递一个数字或者字符串类型的值

BigInt支持除单目(+)之外的运算符和无符号右移>>>之外的位操作,如:

+ 3n // Cannot convert a BigInt value to a number
3n >>> 1n // BigInts have no unsigned right shift, use >> instead

使用BigInt时,带小数的运算会被取向下整:

38n / 10n // 3n

BigIntNumber不是严格相等的,但宽松相等

1n === 1 // false
1n == 1 // true

this绑定的五种方式

1. 默认绑定

在非严格模式下,this指向的是全局对象window,在严格模式下,会绑定到undefined

2. 隐式绑定

普通隐式绑定

遵循谁最后调用的函数,this就指向谁

隐式绑定丢失问题

有两种情况容易出现隐式丢失问题:

  • 使用另一个变量来给函数取别名
  • 将函数作为参数传递时会被隐式赋值,回调函数丢失this绑定

注:如果你把一个函数当成参数传递给另一个函数时,会发生隐式丢失的问题,并且this的指向与调用它的函数无关。在非严格模式下,this会指向window,严格模式下,会指向undefined

3. 显示绑定

指使用call/apply/bind方法直接指定this的绑定对象

  • this永远指向最后调用它的那个对象
  • 匿名函数的this永远指向window
  • 使用call()或者apply()函数会直接执行,bind()则需要再主动执行一次
  • 如果call()/apply()/bind()接收的第一个参数为空/null/undefined的话,则会忽略这个参数
  • forEach/map/filter函数的第二个参数也是能绑定this

4. new绑定

使用new来调用一个函数,会构造一个新对象并把这个新对象绑定到调用函数的this

5. 箭头函数绑定

  • 箭头函数的this是由外层作用于来决定的,指向函数定义时的this而非执行时
  • 字面量创建的对象,作用域是window,如果里面有函数箭头属性的话,this指向的是window
  • 构造函数创建的对象,作用域是可以理解为是这个构造函数,且这个构造函数的this是指向新建的对象的,因此this指向这个对象
  • 箭头函数的this是无法通过bind、call、apply来直接修改,但是可以通过改变作用域中this的指向来间接修改

随机红包

const getBonus = (amount, count) => {
  if (isNaN(Number(amount)) || isNaN(Number(count))) {
    throw Error('请输入正确的金额或人数')
  }
  if (count * 0.01 > amount) return '每个红包最近金额不少于0.01元'
  // 用随机数作为切割点
  let randomList = []
  let res = []
  // 生成count-1份随机数来切割
  for (let i = 0; i < count - 1; i++) {
    res.push(Math.random() * amount / amount)
  }
  randomList = [...[0], ...res.sort((a, b) => a - b)] // 带顺序的随机数列表

  // 进行切割
  let result = [] // 切割结果
  let sum = 0 // 前count-1项的和,剩余的1项需要
  let floatArr = [] // 存储随机浮点数的数组
  // 浮点数组填充
  for (let i = 1; i < randomList.length; i++) {
    floatArr.push((randomList[i] - randomList[i - 1]))
  }
  // 将随机数处理为红包金额后进行修改
  floatArr.forEach(n => {
    const num = Number((n * amount).toFixed(2)) || 0.01
    sum += num
    result.push(num)
  })
  const lastNum = Number((amount - sum).toFixed(2)) // 最后一个数
  // 当最后一项为0或者负数时,对数组重新进行处理,反之则直接拼接结果
  return lastNum < 0.01 ? dealLastNum(result, lastNum) : result.concat(lastNum)
}
// 处理最后一项小于最低金额的情况
function dealLastNum(result, lastNum) {
  let temp = 0 // 余数和
  // 遍历count-1项,找出其中大于最低金额的项做处理的项进行处理
  for (let i = 0; i < result.length; i++) {
    if (result[i] > 0.01) {
      temp +=  Number((result[i] - 0.01).toFixed(2))
      result[i] = 0.01 // 处理完成后,当前项设置为最低金额
      // 当最后一项不小于0.01时,即可返回
      if (Number((temp + lastNum).toFixed(2)) >= 0.01) {
        break
      }
    }
  }
  return result.concat(0.01)
}
console.log(getBonus(0.08, 8));

简单请求和复杂请求

某些请求不会触发CORS预检请求,这样的请求一般会被称为“简单请求”,而会触发预检的请求则被称为“复杂请求”

简单请求

  • 请求方法为GET、HEAD、POST时发的请求
  • 人为设置了规范集合之内的首部字段,如Accept/Accept-Language/Content-Language/Content-Type/DPR/Downlink/Save-Data/Viewport-Width/Width
  • Content-Type 的值仅限于下列三者之一,即application/x-www-form-urlencoded、multipart/form-data、text/plain
  • 请求中的任意 XMLHttpRequestUpload 对象均没有注册任何事件监听器
  • 请求中没有使用 ReadableStream 对象

复杂请求

  • 使用了下面任一 HTTP 方法,PUT/DELETE/CONNECT/OPTIONS/TRACE/PATCH
  • 人为设置了以下集合之外首部字段,即简单请求外的字段
  • Content-Type 的值不属于下列之一,即application/x-www-form-urlencoded、multipart/form-data、text/plain

预检请求

OPTIONS请求,简单来说,可以用OPTIONS请求去嗅探某个请求在对应服务器都支持哪些请求方式 在前端我们一般不会主动发起,这种请求一般是在跨域的情况下,在浏览器发起“复杂请求”时主动发起的。 浏览器必须先使用OPTIONS方法去发起一个预检请求,在请求成功的情况下,再发起复杂请求。

预检请求优化

  • 转化为简单请求,如使用jsonp跨域
  • 对预检请求进行缓存,设置Access-Control-Max-Age字段