Coder Social home page Coder Social logo

interviewsummary's People

Contributors

guyuezhai avatar

Watchers

 avatar  avatar

interviewsummary's Issues

git reset、git revert 和 git checkout 有什么区别

这个问题同样也需要先了解一下git仓库的三个组成部分:

  1. 工作区(workspace): 在git管理下的正常目录都算是工作区,我们平时的编辑工作都是在工作区完成的
  2. 暂存区(stage):临时区域。里面存放将要提交文件的快照
  3. 历史记录区(history):git commit 后的记录区域

三个区的转换关系以及转换所使用的命令:

gitresetrevert

git resetgit revertgit checkout的共同点:用来撤销代码仓库中的某些更改。

不同点:

从commit层面说起:

  • git reset 可以将一个分支的末端指向之前的一个commit。然后下次git执行垃圾回收的时候,会把这个commit之后的commit都扔掉。git reset 还支持三种标记,用来标记reset指令影响的范围:

    • --mixed: 会影响到暂存区和历史记录区。也是默认选项
    • --soft:只影响历史记录区
    • --hard:影响工作区、暂存区和历史记录区

注意: 因为git reset是直接删除commit记录,从而会影响到其它开发人员的分支,所以不要在公共分支比如(develop)做这个操作

  • git checkout 可以将HEAD移到一个新的分支,并更新工作目录。因为可能会覆盖本地的修改,所以执行这个指令前,你需要stash或者commit暂存区和工作区的更改。
  • git revertgit reset 的目的是一样,但做法不同,他会以创建新的commit的方式来撤销commit,这样能保留之前的commit历史,比较安全。另外,同样因为可能会覆盖本地的修改,所以执行这个指令前,你需要stash或者commit暂存区和工作区的更改。

然后,从文件层面来说:

  • git reset 只是把文件从历史记录区拿到暂存区,不影响工作区的内容,而且不支持 --mixed、--soft、--hard

  • git checkout 则是把文件从暂存区拿到工作区,不影响暂存区的内容。

    • 当执行git rm --cached命令时,会直接从暂存区删除文件,工作区则不会做出改变
    • 当执行 git checkout . 或者 git checkout -- 命令时,会用暂存区全部指定的文件替换工作区的文件。这个操作很危险,会清除工作区中未添加到暂存区的改动
    • 当执行git checkout HEAD . 或者git checkout HEAD 命令时,会用HEAD指向的master分支中的全部或者部分文件替换暂存区以及工作区的文件。这个命令也是具有危险性的,因为不但会清除工作区中未提交的改动,也会清除暂存区中未提交的改动
  • git revert 不支持文件层面的操作

合并两个有序数组

给你两个有序整数数组 nums1 和 nums2,请你将 nums2 合并到 nums1 中,使 nums1 成为一个有序数组。
说明: 初始化 nums1 和 nums2 的元素数量分别为 m 和 n 。 你可以假设 nums1 有足够的空间(空间大小大于或等于 m + n)来保存 nums2 中的元素。
示例: 输入:
nums1 = [1,3,5,7], m = 4
nums2 =[2,4,6,7,12,16], n = 6
输出: [ 1, 2, 3, 4, 5, 6, 7, 7, 12, 16 ]

解题思路,双指针法

初始化nums1的空间为m+n-1
nums1的初始索引为m-1
nums2的初始索引为n-1
分别比较指针所指向的数值大小,并将大的数值放到nums1的尾部,指针做对应的-1

/**
 * 
 * @param {number[]} arr1 
 * @param {number} m 
 * @param {number[]} arr2 
 * @param {number} n 
 */
function merge(arr1,m,arr2,n) {
    let i=m-1,j=n-1,k=m+n-1;
    while (i>=0 && j>=0) {
        if(arr1[i]>arr2[j]){
            arr1[k]=arr1[i]
            i--
            k--
        }else{
            arr1[k]=arr2[j]
            j--
            k--
        }
    }
    while(j>=0){
        arr1[k]=arr2[j]
        j--
        k--
    }
}
let nums1=[1,3,5,7]
let nums2=[2,4,6,7,12,16]
merge(nums1,4,nums2,6)

请设计一个懒人类,具有吃、喝、睡功能,具体信息请看以下描述!

LazyMan('Tom');
// Hi I am Tom

LazyMan('Tom').sleep(10).eat('lunch');
// Hi I am Tom
// 等待了10秒...
// I am eating lunch

LazyMan('Tom').eat('lunch').drink('500ml牛奶').sleep(10).eat('dinner')
// Hi I am Tom
// I am eating lunch
//喝了500ml牛奶...
// 等待了10秒...
// I am eating diner

LazyMan('Tom').eat('lunch').eat('dinner').sleepFirst(5).sleep(10).eat('junk food').drink('10000ml奶茶');
// Hi I am Tom
// 等待了5秒...
// I am eating lunch
// I am eating dinner
// 等待了10秒...
// I am eating junk food
//喝了10000ml奶茶...

输出以下打印结果

function Foo() {
    Foo.a = function() {
        console.log(1)
    }
    this.a = function() {
        console.log(2)
    }
}
Foo.prototype.a = function() {
    console.log(3)
}
Foo.a = function() {
    console.log(4)
}
Foo.a();
let obj = new Foo();
obj.a();
Foo.a();

解析

function Foo() {
    Foo.a = function() {
        console.log(1)
    }
    this.a = function() {
        console.log(2)
    }
}
// 以上只是 Foo 的构建方法,没有产生实例,此刻也没有执行
Foo.prototype.a = function() {
    console.log(3)
}
// 现在在 Foo 上挂载了原型方法 a ,方法输出值为 3
Foo.a = function() {
    console.log(4)
}
// 现在在 Foo 上挂载了直接方法 a ,输出值为 4

Foo.a();
// 立刻执行了 Foo 上的 a 方法,也就是刚刚定义的,所以
// # 输出 4

let obj = new Foo();
/* 这里调用了 Foo 的构建方法。Foo 的构建方法主要做了两件事:
1. 将全局的 Foo 上的直接方法 a 替换为一个输出 1 的方法。
2. 在新对象上挂载直接方法 a ,输出值为 2。
*/

obj.a();
// 因为有直接方法 a ,不需要去访问原型链,所以使用的是构建方法里所定义的 this.a,
// # 输出 2

Foo.a();
// 构建方法里已经替换了全局 Foo 上的 a 方法,所以
// # 输出 1

不用加减乘除运算符,求整数的7倍

可以使用三类方式:位运算加法、JS hack、进制转换。实现方式分别如下:

这道题的关键在于不能使用运算符号,那么一个直接的思路就是能不能不用加减乘除实现整数的加减法呢?其实不难,复习一下大学课本里面计算机组成原理,应该能想起来如何实现基本的加减乘除法。这里,我们其实只需要实现一个基本的加法:

a b a+b 进位
0 0 0
1 0 1
0 1 1
1 1 0

从上面的表可以看出一种实现简单的多位二进制整数加法的算法如下

m 和 n 是两个二进制整数,求 m+n:

  1. 用与运算求m和n共同为"1"的位:m' = m & n
  2. 用异或运算求m和n其中一个为"1"的位: n' = m ^ n
  3. 如果m'不为0,那么将m'左移一位(进位),即 m = m' << 1,即 n = n',跳回到步骤 1
  4. 如果 m'为0,那么n'就是我们要求的结果
/* -- 位运算 -- */
// 先定义位运算加法
function bitAdd(m, n){
    while(m){
        [m, n] = [(m & n) << 1, m ^ n];
    }
    return n;
}

// 位运算实现方式 1 - 循环累加7次
let multiply7_bo_1 = (num)=>
{
  let sum = 0,counter = new Array(7); // 得到 [empty × 7]
  while(counter.length){
    sum = bitAdd(sum, num);
    counter.shift();
  }
  return sum;
}

// 位运算实现方式 2 - 二进制进3位(乘以8)后,加自己的补码(乘以-1)
let multiply7_bo_2 = (num) => bitAdd(num << 3, -num) ;

/* -- JS hack -- */

// hack 方式 1 - 利用 Function 的构造器 & 乘号的字节码
let multiply7_hack_1 = (num) => 
    new Function(["return ",num,String.fromCharCode(42),"7"].join(""))();

// hack 方式 2 - 利用 eval 执行器 & 乘号的字节码
let multiply7_hack_2 = (num) => 
		eval([num,String.fromCharCode(42),"7"].join(""));

// hack 方式 3 - 利用 SetTimeout 的参数 & 乘号的字节码
setTimeout(["window.multiply7_hack_3=(num)=>(7",String.fromCharCode(42),"num)"].join(""))

/* -- 进制转换 -- */

// 进制转换方式 - 利用 toString 转为七进制整数;然后末尾补0(左移一位)后通过 parseInt 转回十进制
let multiply7_base7 = 
    (num)=>parseInt([num.toString(7),'0'].join(''),7);

修改以下 print 函数,使之输出 0 到 99,或者 99 到 0

要求:

1、只能修改 setTimeout 到 Math.floor(Math.random() * 1000 的代码

2、不能修改 Math.floor(Math.random() * 1000

3、不能使用全局变量

function print(n){
  setTimeout(() => {
    console.log(n);
  }, Math.floor(Math.random() * 1000));
}
for(var i = 0; i < 100; i++){
  print(i);
}

修改后

function print(n){
  setTimeout((() => {
    console.log(n)
    return ()=>{}
  }).call(n,[]), Math.floor(Math.random() * 1000));
}
for(var i = 0; i < 100; i++){
  print(i);
}

栈的实现

实现栈,具有以下功能

  • push : 添加一个(或几个)新元素到栈顶
  • pop : 移除栈顶的元素,同时返回被移除的元素
  • peek : 返回栈顶元素,不对栈做任何修改
  • isEmpty : 如果栈里没有任何元素返回true,否则返回false
  • clear : 清空栈中元素
  • size: 返回栈中元素个数
  • print : 把栈中存放的元素打印出来

ES5实现方式

function Stack(){
    let items=[]
    this.push=function(item){
        items.push(item)
    }
    this.pop=function () {
        return items.pop()
    }
    this.peek=function () {
        return items[items.length-1]
    }
    this.clear=function(){
        items=[]
    }
    this.print=function () {
        console.log(items.toString())
    }
    this.size=function(){
        return items.length()
    }
    this.isEmpty=function(){
        return items.length==0
    }

}

ES6 实现

class Stack{
    constructor(){
        this.items=[]
    }
    push(item){
        this.items.push(item)
    }
    pop(){
        return this.items.pop()
    }
    peek(){
        let length=this.items.length
        return this.items[length-1]
    }
    isEmpty(){
        return this.items.length==0
    }
    clear(){
        this.items=[]
    }
    size(){
        return this.items.length
    }
    print(){
        console.log(this.items.toString())
    }
}

队列实现

实现队列,具有以下功能

  • enqueue : 向队尾添加一个(或几个)新元素
  • dequeue : 移除队列中第一个的元素,同时返回被移除的元素
  • front : 返回队列中第一个元素,不对队列做任何修改
  • isEmpty : 如果队列里没有任何元素返回true,否则返回false
  • size: 返回队列中元素个数
  • print : 把队列中存放的元素打印出来

ES5 实现队列

function Queue() {
    let items=[]
    this.enqueue=function (item) {
        items.push(item)
    }
    this.dequeue=function () {
        return items.shift()
    }
    this.front=function(){
        return items[0]
    }
    this.isEmpty=function(){
        return items.length==0
    }
    this.size=function(){
        return items.length
    }
    this.print=function(){
        console.log(items.toString())
    }
}

ES6 实现

let Queue=(function () {
    let items=new WeakMap()
    class Queue{
        constructor(){
            items.set(this,[])
        }
        enqueue(item){
            let arr=items.get(this)
            arr.push(item)
        }
        dequeue(){
            let arr=items.get(this)
            return arr.shift()
        }
        front(){
            let arr=items.get(this)
            return arr[0]
        }
        isEmpty(){
            let arr=items.get(this)
            return arr.length==0
        }
        size(){
            let arr=items.get(this)
            return arr.length
        }
        print(){
            let arr=items.get(this)
            console.log(arr.toString())
        }
    }
    return Queue
})()

你的git工作流是怎样的?

GitFlow是由Vincent Driessen提出的一个Git操作流程规范,包含以下几个关键分支

名称 说明
master 主分支
develop 主开发分支,包含确定即将发布的代码
feature 新功能分支,一般一个新功能对应一个分支,对于功能的拆分需要比较合理,以避免一些后面不必要的冲突
release 发布分支,发布时候用的分支,一般测试时候发现的bug在这个分支就行修复
hotfix hotfix分支,紧急修复bug的时候用

GitFlow的优势有如下几点:

  • 并行开发:GitFlow可以很方便的实现并行开发,每个新功能都会建立一个新的feature分支,从而和已经完成的功能隔离开来,而且只有在新功能完成开发的情况下,其对应的feature分支才会合并到主分支上(也就是我们经常说的develop分支)。另外,如果你正在开发某个功能,同时又有一个新的功能需要开发,你只需要提交当前的feature代码,然后创建另外一个feature分支并完成新的功能开发,然后再切回之前的feature分支即可继续完成之前的功能开发。
  • 协作开发:GitFlow还支持多人协同开发,因为每个feature分支上改动的代码都只是为了让某个新的feature可以独立运行。同时我们也很容易知道每个人在干啥
  • 发布阶段:当一个新的feature开发完成的时候,他会被合并到develop分支,这个分支主要用来暂时保存那些还没有发布的内容,所以如果需要开发新的feature,我们只需要从develop分支创建新分支,即可包含所有已经完成的feature。
  • 支持紧急修复:GitFlow还包含了hotfix分支。这种类型的分支是从某个已经发布的tag上创建出来并做一个紧急的修复,而且这个紧急修复只影响这个已经发布的tag,而不会影响到你正在开发的新feature

然后就是GitFlow最经典的几张流程图

feature分支都是从develop分支创建,完成后再合并到develop分支,等待发布。

当需要发布时,我们从develop分支创建一个release分支

然后这个release分支会发布到测试环境进行测试,如果发现问题就在这个分支上直接进行修复。在所有问题修复之前,我们会不停的重复发布->测试->修复->重新发布->重新测试这个流程。
发布结束后,这个release分支会合并到develop分支和master分支,从而保证不会有代码丢失。

master分支值只能跟踪已经发布的代码,合并到master分支上的commit只能来自release分支和hotfix分支

hotfix分支的作用是紧急修改一些bug。
他们都是从master分支上的某个tag建立,修复结束后再合并到developmaster分支上

更多工作流参考阮老师的Git工流程

React 和 Vue 的 diff 时间复杂度从 O(n^3) 优化到 O(n) ,那么 O(n^3) 和 O(n) 是如何计算出来的?

原问题标题“React 和 Vue 的 diff 时间复杂度从 O(n^3) 优化到 O(n) ,那么 O(n^3) 和 O(n) 是如何计算出来的? ”

这里的n指的是页面的VDOM节点数,这个不太严谨。如果更严谨一点,我们应该应该假设 变化之前的节点数为m,变化之后的节点数为n。

React 和 Vue 做优化的前提是“放弃了最优解“,本质上是一种权衡,有利有弊。

倘若这个算法用到别的行业,比如医药行业,肯定是不行的,为什么?

React 和 Vue 做的假设是:

检测VDOM的变化只发生在同一层
检测VDOM的变化依赖于用户指定的key
如果变化发生在不同层或者同样的元素用户指定了不同的key或者不同元素用户指定同样的key, React 和 Vue都不会检测到,就会发生莫名其妙的问题。

但是React 认为, 前端碰到上面的第一种情况概率很小,第二种情况又可以通过提示用户,让用户去解决,因此 这个取舍是值得的。 没有牺牲空间复杂度,却换来了在大多数情况下时间上的巨大提升。 明智的选择!

基本概念

首先大家要有个基本概念。

其实这是一个典型的最小编辑距离的问题,相关算法有很多,比如Git中 ,提交之前会进行一次对象的diff操作,就是用的这个最小距离编辑算法。

leetcode 有原题目, 如果想明白这个O(n^3), 可以先看下这个。

对于树,我们也是一样的,我们定义三种操作,用来将一棵树转化为另外一棵树:

删除 删除一个节点,将它的children交给它的父节点

插入 在children中 插入一个节点

修改 修改节点的值

事实上,从一棵树转化为另外一棵树,我们有很多方式,我们要找到最少的。

直观的方式是用动态规划,通过这种记忆化搜索减少时间复杂度。

算法

由于树是一种递归的数据结构,因此最简单的树的比较算法是递归处理。

详细描述这个算法可以写一篇很长的论文,这里不赘述。 大家想看代码的,这里有一份 我希望没有吓到你。

确切地说,树的最小距离编辑算法的时间复杂度是O(n^2m(1+logmn)), 我们假设m 与 n 同阶, 就会变成 O(n^3)。

opacity: 0、visibility: hidden、display: none 优劣和适用场景

  1. display:none(不占空间,不能点击)
  2. visibility:hidden(占据空间,不能点击)隐藏后不会引起页面结构发生改变,不会撑开
  3. opacity:0 (占据空间,可以点击)

其中opacity:0 和display:none,具有株连特性,如果祖先元素设有此属性,无论子元素怎么样都不会出现在DOM中。

如果父节点元素为visibility:hidden,当子元素属性为visibility:visible 那么子元素就会显现出来。

babel原理

babel的转译过程分为三个阶段

  • 解析Parse:将代码解析生成抽象语法树(AST),即词法分析、语法分析的过程
  • 转换Transform:对于AST进行变换一系列的操作,babel接受得到AST并通过babel-traverse对其进行遍历,在此过程中进行添加、更新及移除等操作
  • 生成Generate:将变换后的AST再转换成JS代码,使用到的模块是babel-generator

babel

介绍 HTTPS 握手过程中,客户端如何验证证书的合法性

1、首先什么是HTTP协议?
http协议是超文本传输协议,位于tcp/ip四层模型中的应用层;通过请求/响应的方式在客户端和服务器之间进行通信;但是缺少安全性,http协议信息传输是通过明文的方式传输,不做任何加密,相当于在网络上裸奔;容易被中间人恶意篡改,这种行为叫做中间人攻击;
2、加密通信:
为了安全性,双方可以使用对称加密的方式key进行信息交流,但是这种方式对称加密秘钥也会被拦截,也不够安全,进而还是存在被中间人攻击风险;
于是人们又想出来另外一种方式,使用非对称加密的方式;使用公钥/私钥加解密;通信方A发起通信并携带自己的公钥,接收方B通过公钥来加密对称秘钥;然后发送给发起方A;A通过私钥解密;双发接下来通过对称秘钥来进行加密通信;但是这种方式还是会存在一种安全性;中间人虽然不知道发起方A的私钥,但是可以做到偷天换日,将拦截发起方的公钥key;并将自己生成的一对公/私钥的公钥发送给B;接收方B并不知道公钥已经被偷偷换过;按照之前的流程,B通过公钥加密自己生成的对称加密秘钥key2;发送给A;
这次通信再次被中间人拦截,尽管后面的通信,两者还是用key2通信,但是中间人已经掌握了Key2;可以进行轻松的加解密;还是存在被中间人攻击风险;

3、解决困境:权威的证书颁发机构CA来解决;
3.1制作证书:作为服务端的A,首先把自己的公钥key1发给证书颁发机构,向证书颁发机构进行申请证书;证书颁发机构有一套自己的公私钥,CA通过自己的私钥来加密key1,并且通过服务端网址等信息生成一个证书签名,证书签名同样使用机构的私钥进行加密;制作完成后,机构将证书发给A;
3.2校验证书真伪:当B向服务端A发起请求通信的时候,A不再直接返回自己的公钥,而是返回一个证书;
说明:各大浏览器和操作系统已经维护了所有的权威证书机构的名称和公钥。B只需要知道是哪个权威机构发的证书,使用对应的机构公钥,就可以解密出证书签名;接下来,B使用同样的规则,生成自己的证书签名,如果两个签名是一致的,说明证书是有效的;
签名验证成功后,B就可以再次利用机构的公钥,解密出A的公钥key1;接下来的操作,就是和之前一样的流程了;
3.3:中间人是否会拦截发送假证书到B呢?
因为证书的签名是由服务器端网址等信息生成的,并且通过第三方机构的私钥加密中间人无法篡改; 所以最关键的问题是证书签名的真伪;

4、https主要的**是在http基础上增加了ssl安全层,即以上认证过程;:

Originally posted by @GeekQiaQia in Advanced-Frontend/Daily-Interview-Question#74 (comment)

Virtual DOM 真的比操作原生 DOM 快吗?谈谈你的想法。

采用尤大大的回答:

1. 原生 DOM 操作 vs. 通过框架封装操作。

这是一个性能 vs. 可维护性的取舍。框架的意义在于为你掩盖底层的 DOM 操作,让你用更声明式的方式来描述你的目的,从而让你的代码更容易维护。没有任何框架可以比纯手动的优化 DOM 操作更快,因为框架的 DOM 操作层需要应对任何上层 API 可能产生的操作,它的实现必须是普适的。针对任何一个 benchmark,我都可以写出比任何框架更快的手动优化,但是那有什么意义呢?在构建一个实际应用的时候,你难道为每一个地方都去做手动优化吗?出于可维护性的考虑,这显然不可能。框架给你的保证是,你在不需要手动优化的情况下,我依然可以给你提供过得去的性能。

2. 对 React 的 Virtual DOM 的误解。

React 从来没有说过 “React 比原生操作 DOM 快”。React 的基本思维模式是每次有变动就整个重新渲染整个应用。如果没有 Virtual DOM,简单来想就是直接重置 innerHTML。很多人都没有意识到,在一个大型列表所有数据都变了的情况下,重置 innerHTML 其实是一个还算合理的操作... 真正的问题是在 “全部重新渲染” 的思维模式下,即使只有一行数据变了,它也需要重置整个 innerHTML,这时候显然就有大量的浪费。

我们可以比较一下 innerHTML vs. Virtual DOM 的重绘性能消耗:

  • innerHTML: render html string O(template size) + 重新创建所有 DOM 元素 O(DOM size)
  • Virtual DOM: render Virtual DOM + diff O(template size) + 必要的 DOM 更新 O(DOM change)

Virtual DOM render + diff 显然比渲染 html 字符串要慢,但是!它依然是纯 js 层面的计算,比起后面的 DOM 操作来说,依然便宜了太多。可以看到,innerHTML 的总计算量不管是 js 计算还是 DOM 操作都是和整个界面的大小相关,但 Virtual DOM 的计算量里面,只有 js 计算和界面大小相关,DOM 操作是和数据的变动量相关的。前面说了,和 DOM 操作比起来,js 计算是极其便宜的。这才是为什么要有 Virtual DOM:它保证了 1)不管你的数据变化多少,每次重绘的性能都可以接受;2) 你依然可以用类似 innerHTML 的思路去写你的应用。

3. MVVM vs. Virtual DOM

相比起 React,其他 MVVM 系框架比如 Angular, Knockout 以及 Vue、Avalon 采用的都是数据绑定:通过 Directive/Binding 对象,观察数据变化并保留对实际 DOM 元素的引用,当有数据变化时进行对应的操作。MVVM 的变化检查是数据层面的,而 React 的检查是 DOM 结构层面的。MVVM 的性能也根据变动检测的实现原理有所不同:Angular 的脏检查使得任何变动都有固定的
O(watcher count) 的代价;Knockout/Vue/Avalon 都采用了依赖收集,在 js 和 DOM 层面都是 O(change):

  • 脏检查:scope digest O(watcher count) + 必要 DOM 更新 O(DOM change)
  • 依赖收集:重新收集依赖 O(data change) + 必要 DOM 更新 O(DOM change)可以看到,Angular 最不效率的地方在于任何小变动都有的和 watcher 数量相关的性能代价。但是!当所有数据都变了的时候,Angular 其实并不吃亏。依赖收集在初始化和数据变化的时候都需要重新收集依赖,这个代价在小量更新的时候几乎可以忽略,但在数据量庞大的时候也会产生一定的消耗。

MVVM 渲染列表的时候,由于每一行都有自己的数据作用域,所以通常都是每一行有一个对应的 ViewModel 实例,或者是一个稍微轻量一些的利用原型继承的 "scope" 对象,但也有一定的代价。所以,MVVM 列表渲染的初始化几乎一定比 React 慢,因为创建 ViewModel / scope 实例比起 Virtual DOM 来说要昂贵很多。这里所有 MVVM 实现的一个共同问题就是在列表渲染的数据源变动时,尤其是当数据是全新的对象时,如何有效地复用已经创建的 ViewModel 实例和 DOM 元素。假如没有任何复用方面的优化,由于数据是 “全新” 的,MVVM 实际上需要销毁之前的所有实例,重新创建所有实例,最后再进行一次渲染!这就是为什么题目里链接的 angular/knockout 实现都相对比较慢。相比之下,React 的变动检查由于是 DOM 结构层面的,即使是全新的数据,只要最后渲染结果没变,那么就不需要做无用功。

Angular 和 Vue 都提供了列表重绘的优化机制,也就是 “提示” 框架如何有效地复用实例和 DOM 元素。比如数据库里的同一个对象,在两次前端 API 调用里面会成为不同的对象,但是它们依然有一样的 uid。这时候你就可以提示 track by uid 来让 Angular 知道,这两个对象其实是同一份数据。那么原来这份数据对应的实例和 DOM 元素都可以复用,只需要更新变动了的部分。或者,你也可以直接 track by $index 来进行 “原地复用”:直接根据在数组里的位置进行复用。在题目给出的例子里,如果 angular 实现加上 track by $index 的话,后续重绘是不会比 React 慢多少的。甚至在 dbmonster 测试中,Angular 和 Vue 用了 track by $index 以后都比 React 快: dbmon (注意 Angular 默认版本无优化,优化过的在下面)

顺道说一句,React 渲染列表的时候也需要提供 key 这个特殊 prop,本质上和 track-by 是一回事。

4. 性能比较也要看场合

在比较性能的时候,要分清楚初始渲染、小量数据更新、大量数据更新这些不同的场合。Virtual DOM、脏检查 MVVM、数据收集 MVVM 在不同场合各有不同的表现和不同的优化需求。Virtual DOM 为了提升小量数据更新时的性能,也需要针对性的优化,比如 shouldComponentUpdate 或是 immutable data。

  • 初始渲染:Virtual DOM > 脏检查 >= 依赖收集
  • 小量数据更新:依赖收集 >> Virtual DOM + 优化 > 脏检查(无法优化) > Virtual DOM 无优化
  • 大量数据更新:脏检查 + 优化 >= 依赖收集 + 优化 > Virtual DOM(无法/无需优化)>> MVVM 无优化

不要天真地以为 Virtual DOM 就是快,diff 不是免费的,batching 么 MVVM 也能做,而且最终 patch 的时候还不是要用原生 API。在我看来 Virtual DOM 真正的价值从来都不是性能,而是它 1) 为函数式的 UI 编程方式打开了大门;2) 可以渲染到 DOM 以外的 backend,比如 ReactNative。

5. 总结

以上这些比较,更多的是对于框架开发研究者提供一些参考。主流的框架 + 合理的优化,足以应对绝大部分应用的性能需求。如果是对性能有极致需求的特殊情况,其实应该牺牲一些可维护性采取手动优化:比如 Atom 编辑器在文件渲染的实现上放弃了 React 而采用了自己实现的 tile-based rendering;又比如在移动端需要 DOM-pooling 的虚拟滚动,不需要考虑顺序变化,可以绕过框架的内置实现自己搞一个。

附上尤大的回答链接
链接:https://www.zhihu.com/question/31809713/answer/53544875

写出以下代码打印结果

function changeObjProperty(o) {
  o.siteUrl = "http://www.baidu.com"
  o = new Object()
  o.siteUrl = "http://www.google.com"
} 
let webSite = new Object();
changeObjProperty(webSite);
console.log(webSite.siteUrl);

找出字符串中连续出现最多的字符和个数

  • 'abcaakjbb' => {'a':2,'b':2}
  • 'abbkejsbcccwqaa' => {'c':3}
    注意:是连续字符
function getmaxlen(str){
   if(str){
       let matchStr=str.match(/(\w)\1*/g);
       let lenArr=matchStr.map(item=>item.length);
       let maxLen=Math.max(...lenArr);
       return matchStr.reduce((res,cur)=>{
                if(cur.length===maxLen){
                  res[cur]=maxLen
                }
                return res
              },{})
   }
   return {}
}
getmaxlen('abbkejsbcccwqaa')
// => {ccc: 3}

vue 计算属性 vs 侦听属性

计算属性 VS 侦听属性

Vue 的组件对象支持了计算属性 computed 和侦听属性 watch 2 个选项,很多同学不了解什么时候该用 computed 什么时候该用 watch。先不回答这个问题,我们接下来从源码实现的角度来分析它们两者有什么区别。

computed


计算属性的初始化是发生在 Vue 实例初始化阶段的 initState 函数中,执行了 if (opts.computed) initComputed(vm, opts.computed),initComputed 的定义在 src/core/instance/state.js 中:

const computedWatcherOptions = { computed: true }
function initComputed (vm: Component, computed: Object) {
  // $flow-disable-line
  const watchers = vm._computedWatchers = Object.create(null)
  // computed properties are just getters during SSR
  const isSSR = isServerRendering()

  for (const key in computed) {
    const userDef = computed[key]
    const getter = typeof userDef === 'function' ? userDef : userDef.get
    if (process.env.NODE_ENV !== 'production' && getter == null) {
      warn(
        `Getter is missing for computed property "${key}".`,
        vm
      )
    }

    if (!isSSR) {
      // create internal watcher for the computed property.
      watchers[key] = new Watcher(
        vm,
        getter || noop,
        noop,
        computedWatcherOptions
      )
    }

    // component-defined computed properties are already defined on the
    // component prototype. We only need to define computed properties defined
    // at instantiation here.
    if (!(key in vm)) {
      defineComputed(vm, key, userDef)
    } else if (process.env.NODE_ENV !== 'production') {
      if (key in vm.$data) {
        warn(`The computed property "${key}" is already defined in data.`, vm)
      } else if (vm.$options.props && key in vm.$options.props) {
        warn(`The computed property "${key}" is already defined as a prop.`, vm)
      }
    }
  }
}

函数首先创建 vm._computedWatchers 为一个空对象,接着对 computed 对象做遍历,拿到计算属性的每一个 userDef,然后尝试获取这个 userDef 对应的 getter 函数,拿不到则在开发环境下报警告。接下来为每一个 getter 创建一个 watcher,这个 watcher 和渲染 watcher 有一点很大的不同,它是一个 computed watcher,因为 const computedWatcherOptions = { computed: true }。computed watcher 和普通 watcher 的差别我稍后会介绍。最后对判断如果 key 不是 vm 的属性,则调用 defineComputed(vm, key, userDef),否则判断计算属性对于的 key 是否已经被 data 或者 prop 所占用,如果是的话则在开发环境报相应的警告。

那么接下来需要重点关注 defineComputed 的实现:

export function defineComputed (
  target: any,
  key: string,
  userDef: Object | Function
) {
  const shouldCache = !isServerRendering()
  if (typeof userDef === 'function') {
    sharedPropertyDefinition.get = shouldCache
      ? createComputedGetter(key)
      : userDef
    sharedPropertyDefinition.set = noop
  } else {
    sharedPropertyDefinition.get = userDef.get
      ? shouldCache && userDef.cache !== false
        ? createComputedGetter(key)
        : userDef.get
      : noop
    sharedPropertyDefinition.set = userDef.set
      ? userDef.set
      : noop
  }
  if (process.env.NODE_ENV !== 'production' &&
      sharedPropertyDefinition.set === noop) {
    sharedPropertyDefinition.set = function () {
      warn(
        `Computed property "${key}" was assigned to but it has no setter.`,
        this
      )
    }
  }
  Object.defineProperty(target, key, sharedPropertyDefinition)
}

这段逻辑很简单,其实就是利用 Object.defineProperty 给计算属性对应的 key 值添加 getter 和 setter,setter 通常是计算属性是一个对象,并且拥有 set 方法的时候才有,否则是一个空函数。在平时的开发场景中,计算属性有 setter 的情况比较少,我们重点关注一下 getter 部分,缓存的配置也先忽略,最终 getter 对应的是 createComputedGetter(key) 的返回值,来看一下它的定义:

function createComputedGetter (key) {
  return function computedGetter () {
    const watcher = this._computedWatchers && this._computedWatchers[key]
    if (watcher) {
      watcher.depend()
      return watcher.evaluate()
    }
  }
}

createComputedGetter 返回一个函数 computedGetter,它就是计算属性对应的 getter。

整个计算属性的初始化过程到此结束,我们知道计算属性是一个 computed watcher,它和普通的 watcher 有什么区别呢,为了更加直观,接下来来我们来通过一个例子来分析 computed watcher 的实现。

var vm = new Vue({
  data: {
    firstName: 'Foo',
    lastName: 'Bar'
  },
  computed: {
    fullName: function () {
      return this.firstName + ' ' + this.lastName
    }
  }
})

当初始化这个 computed watcher 实例的时候,构造函数部分逻辑稍有不同:

constructor (
  vm: Component,
  expOrFn: string | Function,
  cb: Function,
  options?: ?Object,
  isRenderWatcher?: boolean
) {
  // ...
  if (this.computed) {
    this.value = undefined
    this.dep = new Dep()
  } else {
    this.value = this.get()
  }
}  

可以发现 computed watcher 会并不会立刻求值,同时持有一个 dep 实例。

然后当我们的 render 函数执行访问到 this.fullName 的时候,就触发了计算属性的 getter,它会拿到计算属性对应的 watcher,然后执行 watcher.depend(),来看一下它的定义:

/**
  * Depend on this watcher. Only for computed property watchers.
  */
depend () {
  if (this.dep && Dep.target) {
    this.dep.depend()
  }
}

注意,这时候的 Dep.target 是渲染 watcher,所以 this.dep.depend() 相当于渲染 watcher 订阅了这个 computed watcher 的变化。

然后再执行 watcher.evaluate() 去求值,来看一下它的定义:

/**
  * Evaluate and return the value of the watcher.
  * This only gets called for computed property watchers.
  */
evaluate () {
  if (this.dirty) {
    this.value = this.get()
    this.dirty = false
  }
  return this.value
}

evaluate 的逻辑非常简单,判断 this.dirty,如果为 true 则通过 this.get() 求值,然后把 this.dirty 设置为 false。在求值过程中,会执行 value = this.getter.call(vm, vm),这实际上就是执行了计算属性定义的 getter 函数,在我们这个例子就是执行了 return this.firstName + ' ' + this.lastName。

这里需要特别注意的是,由于 this.firstName 和 this.lastName 都是响应式对象,这里会触发它们的 getter,根据我们之前的分析,它们会把自身持有的 dep 添加到当前正在计算的 watcher 中,这个时候 Dep.target 就是这个 computed watcher。

最后通过 return this.value 拿到计算属性对应的值。我们知道了计算属性的求值过程,那么接下来看一下它依赖的数据变化后的逻辑。

一旦我们对计算属性依赖的数据做修改,则会触发 setter 过程,通知所有订阅它变化的 watcher 更新,执行 watcher.update() 方法:

/* istanbul ignore else */
if (this.computed) {
  // A computed property watcher has two modes: lazy and activated.
  // It initializes as lazy by default, and only becomes activated when
  // it is depended on by at least one subscriber, which is typically
  // another computed property or a component's render function.
  if (this.dep.subs.length === 0) {
    // In lazy mode, we don't want to perform computations until necessary,
    // so we simply mark the watcher as dirty. The actual computation is
    // performed just-in-time in this.evaluate() when the computed property
    // is accessed.
    this.dirty = true
  } else {
    // In activated mode, we want to proactively perform the computation
    // but only notify our subscribers when the value has indeed changed.
    this.getAndInvoke(() => {
      this.dep.notify()
    })
  }
} else if (this.sync) {
  this.run()
} else {
  queueWatcher(this)
}

那么对于计算属性这样的 computed watcher,它实际上是有 2 种模式,lazy 和 active。如果 this.dep.subs.length === 0 成立,则说明没有人去订阅这个 computed watcher 的变化,仅仅把 this.dirty = true,只有当下次再访问这个计算属性的时候才会重新求值。在我们的场景下,渲染 watcher 订阅了这个 computed watcher 的变化,那么它会执行:

this.getAndInvoke(() => {
  this.dep.notify()
})

getAndInvoke (cb: Function) {
  const value = this.get()
  if (
    value !== this.value ||
    // Deep watchers and watchers on Object/Arrays should fire even
    // when the value is the same, because the value may
    // have mutated.
    isObject(value) ||
    this.deep
  ) {
    // set new value
    const oldValue = this.value
    this.value = value
    this.dirty = false
    if (this.user) {
      try {
        cb.call(this.vm, value, oldValue)
      } catch (e) {
        handleError(e, this.vm, `callback for watcher "${this.expression}"`)
      }
    } else {
      cb.call(this.vm, value, oldValue)
    }
  }
}

getAndInvoke 函数会重新计算,然后对比新旧值,如果变化了则执行回调函数,那么这里这个回调函数是 this.dep.notify(),在我们这个场景下就是触发了渲染 watcher 重新渲染。

通过以上的分析,我们知道计算属性本质上就是一个 computed watcher,也了解了它的创建过程和被访问触发 getter 以及依赖更新的过程,其实这是最新的计算属性的实现,之所以这么设计是因为 Vue 想确保不仅仅是计算属性依赖的值发生变化,而是当计算属性最终计算的值发生变化才会触发渲染 watcher 重新渲染,本质上是一种优化。

接下来我们来分析一下侦听属性 watch 是怎么实现的。

watch


侦听属性的初始化也是发生在 Vue 的实例初始化阶段的 initState 函数中,在 computed 初始化之后,执行了:

if (opts.watch && opts.watch !== nativeWatch) {
  initWatch(vm, opts.watch)
}

来看一下 initWatch 的实现,它的定义在 src/core/instance/state.js 中:

function initWatch (vm: Component, watch: Object) {
  for (const key in watch) {
    const handler = watch[key]
    if (Array.isArray(handler)) {
      for (let i = 0; i < handler.length; i++) {
        createWatcher(vm, key, handler[i])
      }
    } else {
      createWatcher(vm, key, handler)
    }
  }
}

这里就是对 watch 对象做遍历,拿到每一个 handler,因为 Vue 是支持 watch 的同一个 key 对应多个 handler,所以如果 handler 是一个数组,则遍历这个数组,调用 createWatcher 方法,否则直接调用 createWatcher:

function createWatcher (
  vm: Component,
  expOrFn: string | Function,
  handler: any,
  options?: Object
) {
  if (isPlainObject(handler)) {
    options = handler
    handler = handler.handler
  }
  if (typeof handler === 'string') {
    handler = vm[handler]
  }
  return vm.$watch(expOrFn, handler, options)
}

这里的逻辑也很简单,首先对 hanlder 的类型做判断,拿到它最终的回调函数,最后调用 vm.$watch(keyOrFn, handler, options) 函数,$watch 是 Vue 原型上的方法,它是在执行 stateMixin 的时候定义的:

Vue.prototype.$watch = function (
  expOrFn: string | Function,
  cb: any,
  options?: Object
): Function {
  const vm: Component = this
  if (isPlainObject(cb)) {
    return createWatcher(vm, expOrFn, cb, options)
  }
  options = options || {}
  options.user = true
  const watcher = new Watcher(vm, expOrFn, cb, options)
  if (options.immediate) {
    cb.call(vm, watcher.value)
  }
  return function unwatchFn () {
    watcher.teardown()
  }

也就是说,侦听属性 watch 最终会调用 $watch 方法,这个方法首先判断 cb 如果是一个对象,则调用 createWatcher 方法,这是因为 $watch 方法是用户可以直接调用的,它可以传递一个对象,也可以传递函数。接着执行 const watcher = new Watcher(vm, expOrFn, cb, options) 实例化了一个 watcher,这里需要注意一点这是一个 user watcher,因为 options.user = true。通过实例化 watcher 的方式,一旦我们 watch 的数据发送变化,它最终会执行 watcher 的 run 方法,执行回调函数 cb,并且如果我们设置了 immediate 为 true,则直接会执行回调函数 cb。最后返回了一个 unwatchFn 方法,它会调用 teardown 方法去移除这个 watcher。

所以本质上侦听属性也是基于 Watcher 实现的,它是一个 user watcher。其实 Watcher 支持了不同的类型,下面我们梳理一下它有哪些类型以及它们的作用。

watcher options


Watcher 的构造函数对 options 做的了处理,代码如下:

if (options) {
  this.deep = !!options.deep
  this.user = !!options.user
  this.computed = !!options.computed
  this.sync = !!options.sync
  // ...
} else {
  this.deep = this.user = this.computed = this.sync = false
}

所以 watcher 总共有 4 种类型,我们来一一分析它们,看看不同的类型执行的逻辑有哪些差别。

deep watcher

通常,如果我们想对一下对象做深度观测的时候,需要设置这个属性为 true,考虑到这种情况:

var vm = new Vue({
  data() {
    a: {
      b: 1
    }
  },
  watch: {
    a: {
      handler(newVal) {
        console.log(newVal)
      }
    }
  }
})
vm.a.b = 2

这个时候是不会 log 任何数据的,因为我们是 watch 了 a 对象,只触发了 a 的 getter,并没有触发 a.b 的 getter,所以并没有订阅它的变化,导致我们对 vm.a.b = 2 赋值的时候,虽然触发了 setter,但没有可通知的对象,所以也并不会触发 watch 的回调函数了。

而我们只需要对代码做稍稍修改,就可以观测到这个变化了

watch: {
  a: {
    deep: true,
    handler(newVal) {
      console.log(newVal)
    }
  }
}

这样就创建了一个 deep watcher 了,在 watcher 执行 get 求值的过程中有一段逻辑:

get() {
  let value = this.getter.call(vm, vm)
  // ...
  if (this.deep) {
    traverse(value)
  }
}

在对 watch 的表达式或者函数求值后,会调用 traverse 函数,它的定义在 src/core/observer/traverse.js 中:

import { _Set as Set, isObject } from '../util/index'
import type { SimpleSet } from '../util/index'
import VNode from '../vdom/vnode'

const seenObjects = new Set()

/**
 * Recursively traverse an object to evoke all converted
 * getters, so that every nested property inside the object
 * is collected as a "deep" dependency.
 */
export function traverse (val: any) {
  _traverse(val, seenObjects)
  seenObjects.clear()
}

function _traverse (val: any, seen: SimpleSet) {
  let i, keys
  const isA = Array.isArray(val)
  if ((!isA && !isObject(val)) || Object.isFrozen(val) || val instanceof VNode) {
    return
  }
  if (val.__ob__) {
    const depId = val.__ob__.dep.id
    if (seen.has(depId)) {
      return
    }
    seen.add(depId)
  }
  if (isA) {
    i = val.length
    while (i--) _traverse(val[i], seen)
  } else {
    keys = Object.keys(val)
    i = keys.length
    while (i--) _traverse(val[keys[i]], seen)
  }
}

traverse 的逻辑也很简单,它实际上就是对一个对象做深层递归遍历,因为遍历过程中就是对一个子对象的访问,会触发它们的 getter 过程,这样就可以收集到依赖,也就是订阅它们变化的 watcher,这个函数实现还有一个小的优化,遍历过程中会把子响应式对象通过它们的 dep id 记录到 seenObjects,避免以后重复访问。

那么在执行了 traverse 后,我们再对 watch 的对象内部任何一个值做修改,也会调用 watcher 的回调函数了。

对 deep watcher 的理解非常重要,今后工作中如果大家观测了一个复杂对象,并且会改变对象内部深层某个值的时候也希望触发回调,一定要设置 deep 为 true,但是因为设置了 deep 后会执行 traverse 函数,会有一定的性能开销,所以一定要根据应用场景权衡是否要开启这个配置。

user watcher

前面我们分析过,通过 vm.$watch 创建的 watcher 是一个 user watcher,其实它的功能很简单,在对 watcher 求值以及在执行回调函数的时候,会处理一下错误,如下:

get() {
  if (this.user) {
    handleError(e, vm, `getter for watcher "${this.expression}"`)
  } else {
    throw e
  }
},
getAndInvoke() {
  // ...
  if (this.user) {
    try {
      this.cb.call(this.vm, value, oldValue)
    } catch (e) {
      handleError(e, this.vm, `callback for watcher "${this.expression}"`)
    }
  } else {
    this.cb.call(this.vm, value, oldValue)
  }
}

handleError 在 Vue 中是一个错误捕获并且暴露给用户的一个利器

computed watcher

computed watcher 几乎就是为计算属性量身定制的,我们刚才已经对它做了详细的分析,这里不再赘述了。

sync watcher

在我们之前对 setter 的分析过程知道,当响应式数据发送变化后,触发了 watcher.update(),只是把这个 watcher 推送到一个队列中,在 nextTick 后才会真正执行 watcher 的回调函数。而一旦我们设置了 sync,就可以在当前 Tick 中同步执行 watcher 的回调函数。

update () {
  if (this.computed) {
    // ...
  } else if (this.sync) {
    this.run()
  } else {
    queueWatcher(this)
  }
}

只有当我们需要 watch 的值的变化到执行 watcher 的回调函数是一个同步过程的时候才会去设置该属性为 true。

总结

通过这一小节的分析我们对计算属性和侦听属性的实现有了深入的了解,计算属性本质上是 computed watcher,而侦听属性本质上是 user watcher。就应用场景而言,计算属性适合用在模板渲染中,某个值是依赖了其它的响应式对象甚至是计算属性计算而来;而侦听属性适用于观测某个值的变化去完成一段复杂的业务逻辑。

同时我们又了解了 watcher 的 4 个 options,通常我们会在创建 user watcher 的时候配置 deep 和 sync,可以根据不同的场景做相应的配置。

介绍 HTTPS 握手过程

  • 客户端使用https的url访问web服务器,要求与服务器建立ssl连接
  • web服务器收到客户端请求后, 会将网站的证书(包含公钥)传送一份给客户端
  • 客户端收到网站证书后会检查证书的颁发机构以及过期时间, 如果没有问题就随机产生一个秘钥
  • 客户端利用公钥将会话秘钥加密, 并传送给服务端, 服务端利用自己的私钥解密出会话秘钥
  • 之后服务器与客户端使用秘钥加密传输

链表

链表与数组的区别

  • 数组的大小是固定的,从数组的起点或者中间插入、移除项的成本很高(需要移动其他元素);链表在添加、移除元素的时候不需要移动其他元素。
  • 链表存储有序的元素集合,元素在内存中并不是连续放置的,每个元素都由一个存储本身的节点和指向下一个 节点元素的引用;数组在内存中元素是连续放置的。
  • 数组可以直接访问任何位置的元素,数组元素的访问时间复杂度为O(1);链表需要从头(表头)开始迭代列表直到找到所需的元素,链表元素的访问时间复杂度为O(N)

正则字符匹配

正则字符匹配

惰性匹配 就是尽可能少的匹配

let regex = /\d{2,5}?/g;
let string = "123 1234 12345 123456";
console.log( string.match(regex) ); 
// => ["12", "12", "34", "12", "34", "12", "34", "56"]

贪婪的,它会尽可能多的匹配

let regex = /\d{2,5}/g;
let string = "123 1234 12345 123456";
console.log( string.match(regex) ); 
// => ["123", "1234", "12345", "12345"]

多选分支

// 例如要匹配"good"和"nice"可以使用/good|nice/。
// 测试如下:
let regex = /good|nice/g;
let string = "good idea, nice try.";
console.log( string.match(regex) ); 
// => ["good", "nice"]
//但有个事实我们应该注意,比如我用/good|goodbye/,去匹配"goodbye"字符串时,
//结果是"good":
let regex = /good|goodbye/g;
let string = "goodbye";
console.log( string.match(regex) ); 
// => ["good"]
//把正则改成/goodbye|good/,结果是:
let regex = /goodbye|good/g;
let string = "goodbye";
console.log( string.match(regex) ); 
// => ["goodbye"]
//也就是说,分支结构也是惰性的,即当前面的匹配上了,后面的就不再尝试了。

匹配id

贪婪匹配的问题

//要求从 <div id="container" class="main"></div> 提取出id="container"
// 最开始想到的正则可能是
let regex=/id=".*"/
let str="<div id="container" class="main"></div>"
console.log(str.match(regex))
//=> id="container" class="main"
// 因为.是通配符,本身就匹配双引号的,而量词*又是贪婪的,当遇到container后面的双引号时,又不会停下来,就会继续匹配,直到遇到最后一个双引号为止

//

惰性匹配解决问题

let regex=/id=".*?"/
let str="<div id="container" class="main"></div>"
console.log(str.match(regex))
//=> id="container"

两数之和

给定一个整数数组 nums 和一个目标值 target,请你在该数组中找出和为目标值的那 两个 整数,并返回他们的数组下标。

你可以假设每种输入只会对应一个答案。但是,数组中同一个元素不能使用两遍。

示例:

给定 nums = [2, 7, 11, 15], target = 9
因为 nums[0] + nums[1] = 2 + 7 = 9
所以返回 [0, 1]

介绍下 Set、Map、WeakSet 和 WeakMap 的区别?

Set 和 Map 主要应用场景在于 数据重组和数据存储
Set 是一种集合的数据结构,Map 是一种字典的数据结构

1. 集合 Set

ES6 新增的一种数据结构,类似于数组,数据成员唯一且无序、不重复

Set 本身是一种构造函数,用来生成Set数据结构

const s = new Set([1,2,3,4,1,2,3,4,2,2,2,3,3,4])
for(let i of s){
  console.log(i) // 1、2、3、4
}
// 去除重复的数组对象
[...s] // [1, 2, 3, 4]  Set 转 Array

Set 对象允许存储任何类型的唯一值,无论是原始值或者是对象引用

向Set添加值时不会发生类型转换,所以number 3 和 string '3' 是两个不同的值,Set 内部会判断是否两个值相同,使用"Same-value-zero equality"的算法,类似于精确相等运算符(===), 主要的区别是在Set中会把两个为NaN的值当做相等来处理,而在js中会认为NaN与任何值都是不相等的

let s = new Set();
let a = NaN;
let b= NaN;
s.add(a);
s.add(b);
console.log(s,a==b) // Set(1) {NaN} false

let s1= new Set();
s1.add(3);
s1.add('3');
console.log([...s1]) // [3, "3"]
  • Set 实例属性
    constructor: 构造函数
    size: 成员数量
let s = new Set([1,2,3,2,1,3,4,5,3,2,1,2,3,5]);
console.log(s.size) // 5
  • Set 实例属性
    操作方法
    add: 新增,相当于array里的push
    delete: 删除集合中的value
    has: 判断集合中是否存在某一元素
    clear: 清空集合中的元素
let s = new Set()
s.add(1).add(2).add(3).add(1)
s.has(1) // true
s.has(0) // false
s.delete(1)
s.has(1) //false
console.log(s) //Set(2) {2, 3}
s.clear()
console.log(s) //Set(0) {}

将 Set 转换成 Array数组

// 利用ES6扩展**...*** 运算符
let set = new Set([1,2,3,4,5])
console.log([...set]) //[1, 2, 3, 4, 5]

// 利用 Array.from()
console.log(Array.from(set)) // [1, 2, 3, 4, 5]

遍历的方法(遍历的顺序为插入的顺序)

  • keys(): 返回一个包含集合中所有键的迭代器
  • values(): 返回一个包含集合中所有值的迭代器
  • entries(): 返回一个包含Set对象中所有元素键值对的迭代器
  • forEach(callbackFn,thisArg): 用于对集合成员执行callbackFn操作,如果有thisArg参数,回调函数中的this会是这个参数,没有返回值
let set = new Set([1,2,3,4,5])
let keys=set.keys();
let entries=set.entries();
console.log(keys) //SetIterator {1, 2, 3, 4, 5}
console.log(set.values()) //SetIterator {1, 2, 3, 4, 5}
console.log(entries) //SetIterator {1 => 1, 2 => 2, 3 => 3, 4 => 4, 5 => 5}

for(let item of keys){
   console.log(item) // 1  2  3  4  5
}
for(let item of entries){
   console.log(item) // [1,1]  [2,2]  [3,3]  [4,4]  [5,5]  
}
set.forEach((value,key)=>{
  console.log(key+":"+value) // 1:1  2:2  3:3  4:4  5:5  
})

Set 可默认遍历,默认迭代器生成函数是 values()

Set.prototype[Symbol.iterator]===Set.prototype.values // true

所以 Set 可以使用map,filter方法

let set = new Set([1,2,3,4,5])
set=new Set([...set].map(item=>item*item))
console.log([...set]) //[1, 4, 9, 16, 25]

set=new Set([...set].filter(item=>item>10))
console.log([...set]) //[16, 25]

因此使用 Set 很容易实现交集(Intersect)、并集(Union)、差集(Difference)

let set1 = new Set([1,2,3]);
let set2 = new Set([0,2,4]);
let intersect= new Set([...set1].filter(item=>set2.has(item)))
let union= new Set([...set1,...set2])
let difference= new Set([...set1].filter(item=>!set2.has(item)))

console.log(intersect) //Set(1) {2}
console.log(union) //Set(5) {1,2,3,0,4}
console.log(difference) //Set(2) {1,3}

隐式类型转换计算

# 求以下运算结果

1 + "1"

2 * "2"

[1, 2] + [2, 1]

"a" + + "b"

  • 1 + "1"

加性操作符:如果只有一个操作数是字符串,则将另一个操作数转换为字符串,然后再将两个字符串拼接起来

所以值为:“11”

  • 2 * "2"

乘性操作符:如果有一个操作数不是数值,则在后台调用 Number()将其转换为数值

  • [1, 2] + [2, 1]

Javascript中所有对象基本都是先调用valueOf方法,如果不是数值,再调用toString方法。

所以两个数组对象的toString方法相加,值为:"1,22,1"

  • "a" + + "b"

后边的“+”将作为一元操作符,如果操作数是字符串,将调用Number方法将该操作数转为数值,如果操作数无法转为数值,则为NaN。

所以值为:"aNaN"

模拟实现一个深拷贝,并考虑对象相互引用以及 Symbol 拷贝的情况

var testobj={
    num: 0,
    str: '',
    boolean: true,
    unf: undefined,
    nul: null,
    obj: {
        name: '我是一个对象',
        id: 1,
        qwe: {
            a: 1
        }
    },
    arr: [0, 1, 2, {b: 2}],
    date: new Date(0),
    reg: /我是一个正则/ig,
    [Symbol('1')]: 1,
    func() {
        console.log(123)
    }
}

function deepclone(obj,map=new WeakMap()){
    if(obj instanceof Date) return new Date(obj);
    if(obj instanceof RegExp) return new RegExp(obj)
    if(obj==null || typeof obj !='object'){
        return obj
    }
    if(map.has(obj)){
        return map.get(obj)
    }
    let t = new obj.constructor()
    map.set(obj,t)
    let keys=[...Object.getOwnPropertyNames(obj),...Object.getOwnPropertySymbols(obj)]
    for(let key of keys){
        t[key]=deepclone(obj[key],map)
    }

    return t
}

let result=deepclone(testobj)
result.data=new Date()
console.log(result,testobj)

运行结果

{
  num: 0,
  str: '',
  boolean: true,
  unf: undefined,
  nul: null,
  obj: { name: '我是一个对象', id: 1, qwe: { a: 1 } },
  arr: [ 0, 1, 2, { b: 2 } ],
  date: 1970-01-01T00:00:00.000Z,
  reg: /我是一个正则/gi,
  func: [Function: func],
  data: 2020-08-21T11:30:05.269Z,
  [Symbol(1)]: 1
} {
  num: 0,
  str: '',
  boolean: true,
  unf: undefined,
  nul: null,
  obj: { name: '我是一个对象', id: 1, qwe: { a: 1 } },
  arr: [ 0, 1, 2, { b: 2 } ],
  date: 1970-01-01T00:00:00.000Z,
  reg: /我是一个正则/gi,
  func: [Function: func],
  [Symbol(1)]: 1
}

正则位置匹配

正则位置匹配

相邻是字符之间的位置,比如,下图中箭头所指的地方
图片

文章来源 https://juejin.im/post/6844903487155732494

如何匹配字符位置

 ^ $ \b \B (?=p) (?!p)

^ 与 $ 匹配 开头 与 结尾

例如把字符串的开头和结尾用"#"替换

let res="hello".replace(/^|$/,'#');
console.log(res) 
// => "#hello#"

多行匹配

let res="I\nlove\njavascript".replace(/^|$/gm,"#")
console.log(result)
/*
#I#
#love#
#javascript#
*/

\b 与 \B

\b是单词的边界,具体就是\w和\W之间的位置,也包括\w和^之间的位置,也包括\w和$之间的位置

比如一个文件名是"[js] lesson_01.mp4"中的\b 如下

let res="[js] lesson_01.mp4".replace(/\b/g,"#")
console.log(res)
//=> "[#js#] #lesson_01#.#mp4#"

为什么会这样呢,仔细看
首先,\w是字符数组[0-9a-zA-Z_]的简写,即\w是字母数字或者下划线中的任一个字符。
而\W是排除[^0-9a-zA-Z_]的简写,即\W是\w之外的任一个字符
那上面代码的执行结果 "[#js#] #lesson_01#.#mp4#" 是怎么来的呢

  • 第一个"#",两边是"["与"j", 是\W与\w之间的位置
  • 第二个"#",两边是"s"与"]", 是\w与\W之间的位置
  • 第三个"#",两边是空格与"l", 是\W与\w之间的位置
  • 第四个"#",两边是"1"与".", 是\w与\W之间的位置
  • 同理其它"#" 以此类推...

依据上面的例子如果把所有的 \B替换成"#"

let res="[js] lesson_01.mp4".replace(/\B/g,"#")
console.log(res)
//=> "#[j#s]# l#e#s#s#o#n#_#0#1.m#p#4"

(?=p) 与 (?!p)

(?=p), p 是一个子模式,即p前面的位置

比如(?=l) 表示'l'字符前面的位置,例如

let res="hello".replace(/(?=l)/g,"#")
// => "he#l#lo"

而 (?!p) 就是 (?=p) 的反义词 例如

let res="hello".replace(/(?!l)/g,"#")
// => "#h#ell#o#"

二者的学名分别是positive lookahead和negative lookahead。

中文翻译分别是正向先行断言和负向先行断言。

ES6中,还支持positive lookbehind和negative lookbehind。

具体是(?<=p)和(?<!p)。也有书上把这四个东西,翻译成环视,

即看看右边或看看左边。但一般书上,没有很好强调这四者是个位置。

比如(?=p),一般都理解成:要求接下来的字符与p匹配,但不能包括p的那些字符。而在本人看来(?=p)就与^一样好理解,就是p前面的那个位置。

位置特性

对于位置的理解,我们可以理解成空字符""。

比如"hello"字符串等价于如下的形式:

"hello" == "" + "h" + "" + "e" + "" + "l" + "" + "l" + "o" + "";

因此,把/^hello$/写成/^^hello?$/,是没有任何问题的:

var result = /^^hello?$/.test("hello");
console.log(result); 
// => true

甚至可以写成更复杂的:

var result = /(?=he)^^he(?=\w)llo$\b\b$/.test("hello");
console.log(result); 
// => true

也就是说字符之间的位置,可以写成多个。

把位置理解空字符,是对位置非常有效的理解方式。

相关案例

  • 不匹配任何东西的正则

/.^/ 此正则要求只有一个字符 该字符后面是开头

  • 数字千分位分隔符表示法

比如 12345678 -> 12,345,678

需要把相应的位置替换成 ","

弄出最后一个逗号 (?=\d{3}$) 就可以做到

let res="12345678".replace(/(?=\d{3}$)/g,',')
// => "12345,678"

弄出所有的逗号 要求后面3个数字一组,也就是\d{3} 至少出现一次 因此可以使用量词 '+'

let res='12345678'.replace(/(?=(\d{3})+$)/g,',')
// => "12,345,678"

其余匹配案例

//以上匹配存在问题
let res='123456789'.replace(/(?=(\d{3})+$)/g,',')
// => ",123,456,789"

// 因为以上正则仅仅把从结尾向前数,一旦是3的倍数,就把其前面的位置换成 ',' 因此才出现这个问题
// 解决 要求匹配这个位置不能是开头 


// 要求这个位置不能是开头怎么办 (?!^)

let res='123456789'.replace(/(?!^)(?=(\d{3})+$)/g,',')
// => "123,456,789"

支持其他转换实现 比如把 "12345678 123456789"替换成"12,345,678 123,456,789"

此时需要修改正则 把里面的开头 ^ 和 $ 结尾 替换成 \b

let res="12345678 123456789".replace(/(?!\b)(?=(\d{3})+\b)/g,',')
// => "12,345,678 123,456,789"

其中(?!\b) 要求当前一个位置,但不是\b前面的位置,其实(?!\b) 即 \B

因此最终正则变成了 /\B(?=(\d{3})+\b)/g

let res="12345678 123456789".replace(/\B(?=(\d{3})+\b)/g,',')
// => "12,345,678 123,456,789"
  • 验证密码长度问题

密码长度6-12位,由数字、小写字符和大写字母组成,但必须至少包括2种字符。

此题,如果写成多个正则来判断,比较容易。但要写成一个正则就比较困难。

那么,我们就来挑战一下。看看我们对位置的理解是否深刻。

不考虑"但必须至少包含2种字符"这一条件

 let reg=/[0-9a-zA-Z]{6,12}$/

判断是否包含有某一种字符

假设,要求的必须是数字,此时可以使用 (?=.*[0-9])

let reg=/(?=.*[0-9])^[0-9a-zA-Z]{6-12}$/

同时包含具体的两种字符

比如同时包含数字和小写字母 可以使用 (?=.[0-9])(?=.[a-z])

let reg=/(?=.*[0-9])(?=.*[a-z])^[0-9a-zA-Z]{6,12}$/

解答

我们可以把原题变换成下列几种情况之一

  1. 同时包含数字和小写字母
  2. 同时包含数字和大写字母
  3. 同时包含小写字母和大写字母
  4. 同时包含数字、小写字母和大写字母

以上的4种情况是或的关系(实际上,可以不用第四条)

先看一下例子

'study'.replace(/(?=s)/, '#') // #study
'study'.replace(/(?=d)/, '#') // stu#dy
'study'.replace(/(?=^)/, '#') // #study

我们从而得知,是找到匹配的pattern后之前的位置,重点强调匹配到的是位置。
明白了这个,我们来理解下/(?=pattern)^/,及找到符合pattern的开始位置,并且该位置要在^之前,能在^之前的是什么?思考下。

/^^study/.test('study') // true

看到这个应该就明白 ^之前的就只能是^,^是个锚点。

/(?=\d)^/.test('9') // true
/(?=\d)^/.test('9l') // true
/(?=\d)^/.test('l9') // false

我们现在明白 /(?=\d)^/的意思是以数字开头的字符串。

到这里我们应该明白 /(?=.*[0-9])^/ 的意思是以任意字符(可以无)加数字开头。说白了就是必须包含数字

/(?=.*[0-9])^/.test('9') // true
/(?=.*[0-9])^/.test('9l') // true
/(?=.*[0-9])^/.test('l9') // true

扩展

1
`study`.replace(/$/, '#') // study#
`study`.replace(/$(?<=y)/, '#') //study#
`study`.replace(/(?<=d)/, '#') // stud#y

2
`study1`.replace(/(?![a-z])/g, '#') //study#1#
`study`.replace(/(?![a-z])/g, '#') //study#

最终答案

let reg=/((?=.*[0-9])(?=.*[a-z])|(?=.*[0-9])(?=.*[A-Z]|(?=.*[a-z])(?=.*[A-Z])))^[0-9A-Za-z]{6,12}$/;
console.log( reg.test("1234567") ); // false 全是数字
console.log( reg.test("abcdef") ); // false 全是小写字母
console.log( reg.test("ABCDEFGH") ); // false 全是大写字母
console.log( reg.test("ab23C") ); // false 不足6位
console.log( reg.test("ABCDEF234") ); // true 大写字母和数字
console.log( reg.test("abcdEF234") ); // true 三者都有

另一种解法

“至少包含两种字符”的意思就是说,不能全部都是数字,也不能全部都是小写字母,也不能全部都是大写字母。

那么要求“不能全部都是数字”,怎么做呢?(?!p)出马!

(?!p)

let  reg = /(?!^[0-9]{6,12}$)^[0-9A-Za-z]{6,12}$/;

三种都不能呢

var reg = /(?!^[0-9]{6,12}$)(?!^[a-z]{6,12}$)(?!^[A-Z]{6,12}$)^[0-9A-Za-z]{6,12}$/;
console.log( reg.test("1234567") ); // false 全是数字
console.log( reg.test("abcdef") ); // false 全是小写字母
console.log( reg.test("ABCDEFGH") ); // false 全是大写字母
console.log( reg.test("ab23C") ); // false 不足6位
console.log( reg.test("ABCDEF234") ); // true 大写字母和数字
console.log( reg.test("abcdEF234") ); // true 三者都有

react-router 的 <Link> 标签和 <a> 标签有什么区别

从最终渲染的DOM来看,这两者都是<a>标签,
在react-router中<Link>标签需要配合<Route>标签做路由跳转,react-router接管了其默认的跳转行为,
有别于传统的页面跳转,且<Link>的跳转只触发相匹配的<Route>对应页面内容更新,不会刷新整个页面。
而<a>标签是普通的超链接,用于从当前页面跳转到href指向的另一个页面(非锚点情况)

正则表达式 括号

正则表达式 括号

文本参考 https://juejin.im/post/6844903487155732494

内容包括:

  1. 分组和分支结构
  2. 捕获分组
  3. 反向引用
  4. 非捕获分组
  5. 相关案例

分组

/a+/ 匹配连续出现的a ,要匹配连续出现的"ab"时,需要使用/(ab)+/

var regex = /(ab)+/g;
var string = "ababa abbb ababab";
console.log( string.match(regex) ); 
// => ["abab", "ab", "ababab"]

分支结构

在多选分支结构(p1|p2)

var regex = /^I love (JavaScript|Regular Expression)$/;
console.log( regex.test("I love JavaScript") );
console.log( regex.test("I love Regular Expression") );
// => true
// => true

如果去掉正则中的括号,即/^I love JavaScript|Regular Expression$/,匹配字符串是"I love JavaScript"和"Regular Expression",当然这不是我们想要的。

引用分组

以日期为例。假设格式是yyyy-mm-dd的,我们可以先写一个简单的正则:

var regex = /\d{4}-\d{2}-\d{2}/;

带括号版 可以提取年、月、日

let regex=/(\d{4})-(\d{2})-(\d{2})/;
let str="2020-08-11"
console.log(str.match(regex))
// => ["2020-08-11", "2020", "08", "11", index: 0, input: "2020-08-11", groups: undefined]
regex=/(\d{4})-(\d{2})-(\d{2})/g;
console.log(str.match(regex))
// => ["2020-08-11"]

match 返回一个数组,第一个元素是整体匹配结果,然后是各个分组(括号里)匹配的内容,然后是下标,最后是输入的文本

如果正则是否有修饰符g,match 返回的数组格式是不一样的

使用正则对象的exec方法

let regex=/(\d{4})-(\d{2})-(\d{2})/;
let str="2020-08-11"
console.log(regex.exec(str))
// => ["2020-08-11", "2020", "08", "11", index: 0, input: "2020-08-11", groups: undefined]

也可以使用构造函数的全局属性$1-$9来获取

var regex = /(\d{4})-(\d{2})-(\d{2})/;
var string = "2017-06-12";

regex.test(string); // 正则操作即可,例如
//regex.exec(string);
//string.match(regex);

console.log(RegExp.$1); // "2017"
console.log(RegExp.$2); // "06"
console.log(RegExp.$3); // "12"

替换

比如,想把yyyy-mm-dd格式,替换成mm/dd/yyyy怎么做

var regex = /(\d{4})-(\d{2})-(\d{2})/;
var string = "2017-06-12";
var result = string.replace(regex, "$2/$3/$1");
console.log(result); 
// => "06/12/2017"

其中replace中的,第二个参数里用$1、$2、$3指代相应的分组。等价于如下的形式:

var regex = /(\d{4})-(\d{2})-(\d{2})/;
var string = "2017-06-12";
var result = string.replace(regex, function(){
    return RegExp.$2+"/"+RegExp.$3+"/"+RegExp.$1;
});
console.log(result); 
// => "06/12/2017"

也可以写成

var regex = /(\d{4})-(\d{2})-(\d{2})/;
var string = "2017-06-12";
var result = string.replace(regex, function(match,year,month,day){
    return month+"/"+day+"/"+year;
});
console.log(result); 
// => "06/12/2017"

反向引用

除了使用相应API来引用分组,也可以在正则本身里引用分组。但只能引用之前出现的分组,即反向引用。

还是以日期为例。

比如要写一个正则支持匹配如下三种格式:

  • 2016-06-12
  • 2016/06/12
  • 2016.06.12

可能会想到这个正则

var regex = /\d{4}(-|\/|\.)\d{2}(-|\/|\.)\d{2}/;
var string1 = "2017-06-12";
var string2 = "2017/06/12";
var string3 = "2017.06.12";
var string4 = "2016-06/12";
console.log( regex.test(string1) ); // true
console.log( regex.test(string2) ); // true
console.log( regex.test(string3) ); // true
console.log( regex.test(string4) ); // true

其中/和.需要转义。虽然匹配了要求的情况,但也匹配"2016-06/12"这样的数据。

假设我们想要求分割符前后一致怎么办?此时需要使用反向引用:

var regex = /\d{4}(-|\/|\.)\d{2}\1\d{2}/;
var string1 = "2017-06-12";
var string2 = "2017/06/12";
var string3 = "2017.06.12";
var string4 = "2016-06/12";
console.log( regex.test(string1) ); // true
console.log( regex.test(string2) ); // true
console.log( regex.test(string3) ); // true
console.log( regex.test(string4) ); // false

注意里面的\1,表示的引用之前的那个分组(-|/|.)。不管它匹配到什么(比如-),\1都匹配那个同样的具体某个字符。

我们知道了\1的含义后,那么\2和\3的概念也就理解了,即分别指代第二个和第三个分组。

看到这里,此时,恐怕你会有三个问题。

括号嵌套怎么办

以左括号(开括号)为准。比如:

var regex = /^((\d)(\d(\d)))\1\2\3\4$/;
var string = "1231231233";
console.log( regex.test(string) ); // true
console.log( RegExp.$1 ); // 123
console.log( RegExp.$2 ); // 1
console.log( RegExp.$3 ); // 23
console.log( RegExp.$4 ); // 3

我们可以看看这个正则匹配模式:

  1. 第一个字符是数字,比如说1,
  2. 第二个字符是数字,比如说2,
  3. 第三个字符是数字,比如说3,
  4. 接下来的是\1,是第一个分组内容,那么看第一个开括号对应的分组是什么,是123,
  5. 接下来的是\2,找到第2个开括号,对应的分组,匹配的内容是1,
  6. 接下来的是\3,找到第3个开括号,对应的分组,匹配的内容是23,
  7. 最后的是\4,找到第3个开括号,对应的分组,匹配的内容是3。

\10 表示什么呢 表示第10个分组 还是\1 和 0 呢 当然是第十个分组了

var regex = /(1)(2)(3)(4)(5)(6)(7)(8)(9)(#) \10+/;
var string = "123456789# ######"
console.log( regex.test(string) );
// => true

引用不存在的分组会怎样呢
因为反向引用,是引用前面的分组,但我们在正则里引用了不存在的分组时,此时正则不会报错只是匹配反向引用字符本身,例如\2,就匹配'\2'。

注意'\2' 此时表示对"2" 进行了转意

ar regex = /\1\2\3\4\5\6\7\8\9/;
console.log( regex.test("\1\2\3\4\5\6\7\8\9") ); 
console.log( "\1\2\3\4\5\6\7\8\9".split("") );
// =>  ["�", "�", "�", "�", "�", "�", "�", "8", "9"]

非捕获分组

之前文章中出现的分组,都会捕获他们匹配到的数据,以便后续引用,因此也称他们是捕获型分组

如果只想要括号最原始的功能,但不会引用它,即不在API里引用,也不在正则里反向引用,此时可以使用非捕获分组(?:p)

var regex =/(?:ab)+/g;
var str='ababa abbb ababab';
console.log(str.match(regex));
// => ["abab", "ab", "ababab"]

相关案例

字符串trim方法模拟

去掉字符串开头和结尾的空白符

  • 匹配到开头和结尾的空白符,然后替换成空字符
function trim(str){
    return str.replace(/^\s+|\s+$/g,'')
}
console.log(trim('  hello world  '))
  • 匹配整个字符串,然后用引用来提取出相应的数据
function trim(str){
    return str.replace(/^\s*(.*?)\s*$/g,'$1')
}
console.log(trim('  hello world  '))

这里使用了惰性匹配 *? 不然也会匹配最后一个空格之前所有的空格

前者效率更高一些

将每个单词的首字母转换成大写

function titleize(str){
    return str.toLowerCase().replace(/(?:^|\s)\w/g,function(c){
        console.log(c)
        return c.toUpperCase()
    })
}
console.log(titleize('hello everyone'))
// => Hello Everyone

思路是找到每个单词的首个字母,当然这里不使用非捕获匹配也是可以的。

驼峰化

function camelize(str){
    return str.replace(/[-_\s]+(.)?/g,function(match,c){
        console.log(c)
        return c?c.toUpperCase():'';
    })
} 
console.log(camelize('-moz-transform'))
// => MozTransform

其中分组(.)表示首字母,单词的界定是,前面的字符可以是多个连字符、下划线以及空白符,正则后面的?的目的,是为了应对str尾部的字符可能不是单词字符,比如str是'-moz-transform '。

中划线化

function dasherize(str){
    return str.replace(/([A-Z])/g,'-$1').replace(/[-_\s]+/g,'-').toLowerCase()
}
console.log( dasherize('MozTransform') ); 

驼峰化的逆过程

HTML转义和反转义

// 将HTML特殊字符转换成等值的实体
function escapeHTML(str) {
	var escapeChars = {
	  '¢' : 'cent',
	  '£' : 'pound',
	  '¥' : 'yen',
	  '€': 'euro',
	  '©' :'copy',
	  '®' : 'reg',
	  '<' : 'lt',
	  '>' : 'gt',
	  '"' : 'quot',
	  '&' : 'amp',
	  '\'' : '#39'
	};
	return str.replace(new RegExp('[' + Object.keys(escapeChars).join('') +']', 'g'), function(match) {
        console.log(match)
		return '&' + escapeChars[match] + ';';
	});
}
console.log( escapeHTML('<div>Blah blah blah</div>') );
// => "&lt;div&gt;Blah blah blah&lt;/div&gt";

其中使用了构造函数生成的正则,然后替换相应的格式就行了

它的逆过程,使用了括号,以便提供引用,也很简单

/ 实体字符转换为等值的HTML。
function unescapeHTML(str) {
	var htmlEntities = {
	  nbsp: ' ',
	  cent: '¢',
	  pound: '£',
	  yen: '¥',
	  euro: '€',
	  copy: '©',
	  reg: '®',
	  lt: '<',
	  gt: '>',
	  quot: '"',
	  amp: '&',
	  apos: '\''
	};
	return str.replace(/\&([^;]+);/g, function(match, key) {
		if (key in htmlEntities) {
			return htmlEntities[key];
		}
		return match;
	});
}
console.log( unescapeHTML('&lt;div&gt;Blah blah blah&lt;/div&gt;') );
// => "<div>Blah blah blah</div>"

通过key获取相应的分组引用,然后作为对象的键

匹配成对标签

要求匹配

  • <title>regular expression</title>
  • <p>laoyao bye bye</p>

不匹配

<title>wrong!</p>

匹配一个开标签,可以使用正则<[^>]+>

匹配一个闭标签,可以使用</[^>]+>

但是要求匹配成对标签,那就是要使用反向引用

var regex=/<([^>]+)>[\d\D]*<\/\1>/
var string1 = "<title>regular expression</title>";
var string2 = "<p>laoyao bye bye</p>";
var string3 = "<title>wrong!</p>";
console.log( regex.test(string1) ); // true
console.log( regex.test(string2) ); // true
console.log( regex.test(string3) ); // false

其中开标签 <[^>]+> 改成 <([^>]+)> 使用括号的目的是为了后面的反向引用,而提供分组,闭标签使用了反向引用</\1>

另外 [\d\D] 的意思是,这个字符是数字或者不是数字, 因此,也就是匹配任意字符的意思

rebase 与 merge的区别?

git rebase 和 git merge一样都是用于从一个分支获取并合并到当前分支

假设一个场景,就是我们开发的feature/todo分支要合并到master主分支,那么用rebase或者merge有什么不同呢

git merge

  • merge特点:自动创建一个新的commit如果合并的时候遇到冲突,仅需要修改后重新commit
  • 优点:记录了真实的commit情况,包括每个分支的详情
  • 缺点:因为每次的merge会自动产生一个merge commit,所以在使用一些git的GUI tools,特别是commit比较频繁的时候,看到分支很杂乱。

gitrebase

  • rebase特点:会合并之前的commit历史
  • 优点:得到更简洁的项目历史,去掉了merge commit
  • 缺点:如果合并出现代码问题不容易定位,因为re-write了history

因此,当需要保留详细的合并信息的时候建议使用git merge,特别是需要将分支合并进入master分支是;当发现自己修改某个功能时,频繁进行了git commit提交时,发现其实过多的提交信息没有必要时,可以选择git rebase

实现 convert 方法,把原始 list 转换成树形结构,要求尽可能降低时间复杂度

以下数据结构中,id 代表部门编号,name 是部门名称,parentId 是父部门编号,为 0 代表一级部门,现在要求实现一个 convert 方法,把原始 list 转换成树形结构,parentId 为多少就挂载在该 id 的属性 children 数组下,结构如下:

// 原始 list 如下
let list =[
    {id:1,name:'部门A',parentId:0},
    {id:2,name:'部门B',parentId:0},
    {id:3,name:'部门C',parentId:1},
    {id:4,name:'部门D',parentId:1},
    {id:5,name:'部门E',parentId:2},
    {id:6,name:'部门F',parentId:3},
    {id:7,name:'部门G',parentId:2},
    {id:8,name:'部门H',parentId:4}
];
const result = convert(list, ...);

// 转换后的结果如下
let result = [
    {
      id: 1,
      name: '部门A',
      parentId: 0,
      children: [
        {
          id: 3,
          name: '部门C',
          parentId: 1,
          children: [
            {
              id: 6,
              name: '部门F',
              parentId: 3
            }, {
              id: 16,
              name: '部门L',
              parentId: 3
            }
          ]
        },
        {
          id: 4,
          name: '部门D',
          parentId: 1,
          children: [
            {
              id: 8,
              name: '部门H',
              parentId: 4
            }
          ]
        }
      ]
    },
  ···
];

三数求和

真题描述:给你一个包含 n 个整数的数组 nums,判断 nums 中是否存在三个元素 a,b,c ,使得 a + b + c = 0 ?请你找出所有满足条件且不重复的三元组。
注意:答案中不可以包含重复的三元组。

示例: 给定数组 nums = [-1, 0, 1, 2, -1, -4], 满足要求的三元组集合为: [ [-1, 0, 1], [-1, -1, 2] ]

思路分析

三数之和延续两数之和的思路,我们可以把求和问题变成求差问题——固定其中一个数,在剩下的数中寻找是否有两个数和这个固定数相加是等于0的。
双指针法用在涉及求和、比大小类的数组题目里时,大前提往往是:该数组必须有序。否则双指针根本无法帮助我们缩小定位的范围,压根没有意义。因此这道题的第一步是将数组排序
然后,对数组进行遍历,每次遍历到哪个数字,就固定哪个数字。然后把左指针指向该数字后面一个坑里的数字,把右指针指向数组末尾,让左右指针从起点开始,向中间前进:

image
每次指针移动一次位置,就计算一下两个指针指向数字之和加上固定的那个数之后,是否等于0。如果是,那么我们就得到了一个目标组合;否则,分两种情况来看:
相加之和大于0,说明右侧的数偏大了,右指针左移
相加之和小于0,说明左侧的数偏小了,左指针右移
tips:这个数组在题目中要求了“不重复的三元组”,因此我们还需要做一个重复元素的跳过处理。这一点在编码实现环节大家会注意到。

编码实现

let numSum=[-1, 0, 1, 2, -1, -4]
let sum=0

function numSumArr(arr,sum){
    let result=[],len=arr.length;
    arr=arr.sort((a,b)=>a-b)
    for(let i=0;i<len-2;i++){
        let j=i+1,k=len-1;
        if(i>0 && arr[i]==arr[i-1]){
            continue
        }
        while (j<k) {
            if(arr[i]+arr[j]+arr[k]<sum){
                j++
                while (j<k && arr[j]==arr[j-1]) {
                    j++
                }
            }else if (arr[i]+arr[j]+arr[k]>sum) {
                k--
                while(j<k && arr[k]==arr[k+1]){
                    k--
                }
            }else{
                result.push([arr[i],arr[j],arr[k]])
                j++
                k--
                while (j<k && arr[j]==arr[j-1]) {
                    j++
                }
                while (j<k && arr[k]==arr[k+1]) {
                    k--
                }
            }
        }
    }
    console.log('result',result)
    return result
}

numSumArr(numSum,0)

Webpack

webpack与grunt、gulp的不同?

Grunt、Gulp是基于任务运行的工具:

它们会自动执行指定的任务,就像流水线,把资源放上去然后通过不同插件进行加工,它们包含活跃的社区,丰富的插件,能方便的打造各种工作流。

Webpack是基于模块化打包的工具:

自动化处理模块,webpack把一切当成模块,当 webpack 处理应用程序时,它会递归地构建一个依赖关系图(dependency graph),其中包含应用程序需要的每个模块,然后将所有这些模块打包成一个或多个 bundle。

因此这是完全不同的两类工具,而现在主流的方式是用npm script代替Grunt、Gulp,npm script同样可以打造任务流。

webpack、rollup、parcel优劣?

  • webpack适用于大型复杂的前端站点构建: webpack有强大的loader和插件生态,打包后的文件实际上就是一个立即执行函数,这个立即执行函数接收一个参数,这个参数是模块对象,键为各个模块的路径,值为模块内容。立即执行函数内部则处理模块之间的引用,执行模块等,这种情况更适合文件依赖复杂的应用开发.

  • rollup适用于基础库的打包,如vue、d3等: Rollup 就是将各个模块打包进一个文件中,并且通过 Tree-shaking 来删除无用的代码,可以最大程度上降低代码体积,但是rollup没有webpack如此多的的如代码分割、按需加载等高级功能,其更聚焦于库的打包,因此更适合库的开发.

  • parcel适用于简单的实验性项目: 他可以满足低门槛的快速看到效果,但是生态差、报错信息不够全面都是他的硬伤,除了一些玩具项目或者实验项目不建议使用

有哪些常见的Loader?

  • file-loader:把文件输出到一个文件夹中,在代码中通过相对 URL 去引用输出的文件
  • url-loader:和 file-loader 类似,但是能在文件很小的情况下以 base64 的方式把文件内容注入到代码中去
  • source-map-loader:加载额外的 Source Map 文件,以方便断点调试
  • image-loader:加载并且压缩图片文件
  • babel-loader:把 ES6 转换成 ES5
  • css-loader:加载 CSS,支持模块化、压缩、文件导入等特性
  • style-loader:把 CSS 代码注入到 JavaScript 中,通过 DOM 操作去加载 CSS。
  • eslint-loader:通过 ESLint 检查 JavaScript 代码

有哪些常见的Plugin?

  • define-plugin:定义环境变量
  • html-webpack-plugin:简化html文件创建
  • uglifyjs-webpack-plugin:通过UglifyJS压缩ES6代码
  • webpack-parallel-uglify-plugin: 多核压缩,提高压缩速度, node 是单线程的,但node能够fork子进程,基于此,webpack-parallel-uglify-plugin 能够把任务分解给多个子进程去并发的执行,子进程处理完后再把结果发送给主进程,从而实现并发编译,进而大幅提升js压缩速度
  • webpack-bundle-analyzer: 可视化webpack输出文件的体积
  • mini-css-extract-plugin: CSS提取到单独的文件中,支持按需加载
const ParallelUglifyPlugin = require('webpack-parallel-uglify-plugin');

// ...
optimization: {
    minimizer: [
        new ParallelUglifyPlugin({ // 多进程压缩
            cacheDir: '.cache/',
            uglifyJS: {
                output: {
                    comments: false,
                    beautify: false
                },
                compress: {
                    warnings: false,
                    drop_console: true,
                    collapse_vars: true,
                    reduce_vars: true
                }
            }
        }),
    ]
}

分别介绍bundle,chunk,module是什么

  • bundle:是由webpack打包出来的文件
  • chunk:代码块,一个chunk由多个模块组合而成,用于代码的合并和分割
  • module:是开发中的单个模块,在webpack的世界,一切皆模块,一个模块对应一个文件,webpack会从配置的entry中递归开始找出所有依赖的模块

Loader和Plugin的不同?

不同的作用:

  • Loader直译为"加载器"。Webpack将一切文件视为模块,但是webpack原生是只能解析js文件,如果想将其他文件也打包的话,就会用到loader。 所以Loader的作用是让webpack拥有了加载和解析_非JavaScript文件_的能力。
  • Plugin直译为"插件"。Plugin可以扩展webpack的功能,让webpack具有更多的灵活性。 在 Webpack 运行的生命周期中会广播出许多事件,Plugin 可以监听这些事件,在合适的时机通过 Webpack 提供的 API 改变输出结果。

不同的用法:

  • Loader在module.rules中配置,也就是说他作为模块的解析规则而存在。 类型为数组,每一项都是一个Object,里面描述了对于什么类型的文件(test),使用什么加载(loader)和使用的参数(options)
  • Plugin在plugins中单独配置。 类型为数组,每一项是一个plugin的实例,参数都通过构造函数传入。

webpack的构建流程是什么?

Webpack 的运行流程是一个串行的过程,从启动到结束会依次执行以下流程:

  1. 初始化参数:从配置文件和 Shell 语句中读取与合并参数,得出最终的参数;
  2. 开始编译:用上一步得到的参数初始化 Compiler 对象,加载所有配置的插件,执行对象的 run 方法开始执行编译;
  3. 确定入口:根据配置中的 entry 找出所有的入口文件;
  4. 编译模块:从入口文件出发,调用所有配置的 Loader 对模块进行翻译,再找出该模块依赖的模块,再递归本步骤直到所有入口依赖的文件都经过了本步骤的处理;
  5. 完成模块编译:在经过第4步使用 Loader 翻译完所有模块后,得到了每个模块被翻译后的最终内容以及它们之间的依赖关系;
  6. 输出资源:根据入口和模块之间的依赖关系,组装成一个个包含多个模块的 Chunk,再把每个 Chunk 转换成一个单独的文件加入到输出列表,这步是可以修改输出内容的最后机会;
  7. 输出完成:在确定好输出内容后,根据配置确定输出的路径和文件名,把文件内容写入到文件系统。

在以上过程中,Webpack 会在特定的时间点广播出特定的事件,插件在监听到感兴趣的事件后会执行特定的逻辑,并且插件可以调用 Webpack 提供的 API 改变 Webpack 的运行结果

是否写过Loader和Plugin?描述一下编写loader或plugin的思路?

Loader像一个"翻译官"把读到的源文件内容转义成新的文件内容,并且每个Loader通过链式操作,将源文件一步步翻译成想要的样子。

编写Loader时要遵循单一原则,每个Loader只做一种"转义"工作。 每个Loader的拿到的是源文件内容(source),可以通过返回值的方式将处理后的内容输出,也可以调用this.callback()方法,将内容返回给webpack。 还可以通过 this.async()生成一个callback函数,再用这个callback将处理后的内容输出出去。 此外webpack还为开发者准备了开发loader的工具函数集——loader-utils。

相对于Loader而言,Plugin的编写就灵活了许多。 webpack在运行的生命周期中会广播出许多事件,Plugin 可以监听这些事件,在合适的时机通过 Webpack 提供的 API 改变输出结果。

webpack的热更新是如何做到的?说明其原理?

webpack的热更新又称热替换(Hot Module Replacement),缩写为HMR。 这个机制可以做到不用刷新浏览器而将新变更的模块替换掉旧的模块。

原理:

webpack热更新原理图

首先要知道server端和client端都做了处理工作

  • 第一步,在 webpack 的 watch 模式下,文件系统中某一个文件发生修改,webpack 监听到文件变化,根据配置文件对模块重新编译打包,并将打包后的代码通过简单的 JavaScript 对象保存在内存中。
  • 第二步是 webpack-dev-server 和 webpack 之间的接口交互,而在这一步,主要是 dev-server 的中间件 webpack-dev-middleware 和 webpack 之间的交互,webpack-dev-middleware 调用 webpack 暴露的 API对代码变化进行监控,并且告诉 webpack,将代码打包到内存中。
  • 第三步是 webpack-dev-server 对文件变化的一个监控,这一步不同于第一步,并不是监控代码变化重新打包。当我们在配置文件中配置了devServer.watchContentBase 为 true 的时候,Server 会监听这些配置文件夹中静态文件的变化,变化后会通知浏览器端对应用进行 live reload。注意,这儿是浏览器刷新,和 HMR 是两个概念。
  • 第四步也是 webpack-dev-server 代码的工作,该步骤主要是通过 sockjs(webpack-dev-server 的依赖)在浏览器端和服务端之间建立一个 websocket 长连接,将 webpack 编译打包的各个阶段的状态信息告知浏览器端,同时也包括第三步中 Server 监听静态文件变化的信息。浏览器端根据这些 socket 消息进行不同的操作。当然服务端传递的最主要信息还是新模块的 hash 值,后面的步骤根据这一 hash 值来进行模块热替换。
  • webpack-dev-server/client 端并不能够请求更新的代码,也不会执行热更模块操作,而把这些工作又交回给了 webpack,webpack/hot/dev-server 的工作就是根据 webpack-dev-server/client 传给它的信息以及 dev-server 的配置决定是刷新浏览器呢还是进行模块热更新。当然如果仅仅是刷新浏览器,也就没有后面那些步骤了。
  • HotModuleReplacement.runtime 是客户端 HMR 的中枢,它接收到上一步传递给他的新模块的 hash 值,它通过 JsonpMainTemplate.runtime 向 server 端发送 Ajax 请求,服务端返回一个 json,该 json 包含了所有要更新的模块的 hash 值,获取到更新列表后,该模块再次通过 jsonp 请求,获取到最新的模块代码。这就是上图中 7、8、9 步骤。
  • 而第 10 步是决定 HMR 成功与否的关键步骤,在该步骤中,HotModulePlugin 将会对新旧模块进行对比,决定是否更新模块,在决定更新模块后,检查模块之间的依赖关系,更新模块的同时更新模块间的依赖引用。
  • 最后一步,当 HMR 失败后,回退到 live reload 操作,也就是进行浏览器刷新来获取最新打包代码。

详细原理解析来源于知乎饿了么前端Webpack HMR 原理解析

如何用webpack来优化前端性能?

用webpack优化前端性能是指优化webpack的输出结果,让打包的最终结果在浏览器运行快速高效

  • 压缩代码:删除多余的代码、注释、简化代码的写法等等方式。可以利用webpack的UglifyJsPluginParallelUglifyPlugin来压缩JS文件,利用cssnano(css-loader?minimize)来压缩css
  • 利用CDN加速:在构建过程中,将引用的静态资源路径修改CDN上对应的路径。可以利用webpack的output参数和各loader的publicPath参数来修改资源路径
  • Tree Shaking:将代码中永远不会走到的片段删除掉,可以通过在启动webpack时追加参数 --optimize-minimize来实现
  • Code Splitting:将代码按路由维度或者组件分块(chunk),这样做到按需加载,同时可以充分利用浏览器缓存
  • 提取公共第三方库:SpliitChunksPlugin插件来进行公共模块抽离,利用浏览器缓存可以长期缓存这些无需频繁变动的公共代码

如何提高webpack的打包速度

  • HappyPack:利用进程并行编译loader,利用缓存来使得rebuild更快,遗憾的是作者表示已经不会再继续开发此项目,类似的替代者是thread-loader
  • 外部扩展(externals): 将不怎么需要更新的第三方库脱离webpack打包,不被打入bundle中,从而减轻少打包时间,比如jQuery用script标签引入
  • dll: 采用webpack的DllPlugin和DllReferencePlugin引入dll,让一些基本不会变动的代码打包成静态资源,避免反复编译浪费时间
  • 利用缓存:webpack.cachebable-loader.cacheDirectoryHappyPack.cache都可以利用缓存提高rebuild效率
  • 缩小文件搜索的范围:比如babel-loader插件,如果你的文件仅存在与src中,那么可以使用include:path.resolve(__dirname,'src'),当然绝大多数情况下这种操作的提升有限,除非不小心build了node_modules文件

实战文章推荐使用webpack4提升180%编译速度Tool

如何提高webpack的构建速度

  • 多入口情况下,使用CommonsChunkPlugin(webpack4中已废弃)来提取公共代码,在webpack4中由optimization.splitChunksoptimization.runtimeChunk替代,前者拆分代码,后者提取runtime代码。原来的CommonsChunkPlugin产出模块时,会包含重复的代码,并且无法优化异步模块,minchunks的配置也较复杂,splitChunks解决了这个问题;另外将optimization.runtimeChunk设置为true或{name:"manifest"},便能将入口模块中的runtime部分提取出来
  • 通过externals配置来提取常用库
  • 利用DllPluginDllReferencePlugin预编译资源模块,通过DllPlugin来对那些我们引用引用绝对不会修改的npm包来进行预编译,再使用DllReferencePlugin将预编译的模块加载进来
  • 使用HappyPack或者thread-loader来实现多线程加速编译
  • 使用webpack-uglify-parallel来提升uglifyPlugin的压缩速度。原理上webpack-uglify-parallel采用多核并行压缩来提升压缩的速度
  • 使用Tree-shakingScope Hoisting来剔除多余的代码

webpack4 的默认配置

optimization: {
    minimize: env === 'production' ? true : false, // 开发环境不压缩
    splitChunks: {
        chunks: "async", // 共有三个值可选:initial(初始模块)、async(按需加载模块)和all(全部模块)
        minSize: 30000, // 模块超过30k自动被抽离成公共模块
        minChunks: 1, // 模块被引用>=1次,便分割
        maxAsyncRequests: 5,  // 异步加载chunk的并发请求数量<=5
        maxInitialRequests: 3, // 一个入口并发加载的chunk数量<=3
        name: true, // 默认由模块名+hash命名,名称相同时多个模块将合并为1个,可以设置为function
        automaticNameDelimiter: '~', // 命名分隔符
        cacheGroups: { // 缓存组,会继承和覆盖splitChunks的配置
            default: { // 模块缓存规则,设置为false,默认缓存组将禁用
                minChunks: 2, // 模块被引用>=2次,拆分至vendors公共模块
                priority: -20, // 优先级
                reuseExistingChunk: true, // 默认使用已有的模块
            },
            vendors: {
                test: /[\\/]node_modules[\\/]/, // 表示默认拆分node_modules中的模块
                priority: -10
            }
        }
    }
}

splitChunks是拆包优化的重点,如果你的项目中包含element-ui等第三方组件(组件比较大),建议单独拆包,如下所示

splitChunks: {
    // ...
    cacheGroups: {    
        elementUI: {
            name: "chunk-elementUI", // 单独将 elementUI 拆包
            priority: 15, // 权重需大于其它缓存组
            test: /[\/]node_modules[\/]element-ui[\/]/
        }
    }
}

loader 解析速度如何提升。同 webpack-parallel-uglify-plugin 插件一样,HappyPack 也能实现并发编译,从而可以大幅提升 loader 的解析速度, 如下是部分配置。

const HappyPack = require('happypack');
const happyThreadPool = HappyPack.ThreadPool({ size: os.cpus().length });
const createHappyPlugin = (id, loaders) => new HappyPack({
    id: id,
    loaders: loaders,
    threadPool: happyThreadPool,
    verbose: process.env.HAPPY_VERBOSE === '1' // make happy more verbose with HAPPY_VERBOSE=1
});

对于前面 loader: "happypack/loader?id=happy-babel" 这句,便需要在 plugins 中创建一个 happy-babel 的插件实例。

plugins: [
    createHappyPlugin('happy-babel', [{
        loader: 'babel-loader',
        options: {
            babelrc: true,
            cacheDirectory: true // 启用缓存
        }
    }])
]

另外,像 vue-loader、css-loader 都支持 happyPack 加速,如下所示。

plugins: [
    createHappyPlugin('happy-css', ['css-loader', 'vue-style-loader']),
    new HappyPack({
        loaders: [{
            path: 'vue-loader',
            query: {
                loaders: {
                    scss: 'vue-style-loader!css-loader!postcss-loader!sass-loader?indentedSyntax'
                }
            }
        }]
    })
]
  1. 我们都知道,webpack打包时,有一些框架代码是基本不变的,比如说 babel-polyfill、vue、vue-router、vuex、axios、element-ui、fastclick 等,这些模块也有不小的 size,每次编译都要加载一遍,比较费时费力。使用 DLLPlugin 和 DLLReferencePlugin 插件,便可以将这些模块提前打包。
//webpack.dll.config.js
const webpack = require("webpack");
const path = require('path');
const CleanWebpackPlugin = require("clean-webpack-plugin");
const dllPath = path.resolve(__dirname, "../src/assets/dll"); // dll文件存放的目录

module.exports = {
    entry: {
        // 把 vue 相关模块的放到一个单独的动态链接库
        vue: ["babel-polyfill", "fastclick", "vue", "vue-router", "vuex", "axios", "element-ui"]
    },
    output: {
        filename: "[name]-[hash].dll.js", // 生成vue.dll.js
        path: dllPath,
        library: "_dll_[name]"
    },
    plugins: [
        new CleanWebpackPlugin(["*.js"], { // 清除之前的dll文件
            root: dllPath,
        }),
        new webpack.DllPlugin({
            name: "_dll_[name]",
            // manifest.json 描述动态链接库包含了哪些内容
            path: path.join(__dirname, "./", "[name].dll.manifest.json")
        }),
    ],
};

接着, 需要在 package.json 中新增 dll 命令。

"scripts": {
    "dll": "webpack --mode production --config build/webpack.dll.config.js"
}

运行 npm run dll 后,会生成 ./src/assets/dll/vue.dll-[hash].js 公共js 和 ./build/vue.dll.manifest.json 资源说明文件,至此 dll 准备工作完成,接下来在 webpack 中引用即可。

externals: {
    'vue': 'Vue',
    'vue-router': 'VueRouter',
    'vuex': 'vuex',
    'elemenct-ui': 'ELEMENT',
    'axios': 'axios',
    'fastclick': 'FastClick'
},
plugins: [
    ...(config.common.needDll ? [
        new webpack.DllReferencePlugin({
            manifest: require("./vue.dll.manifest.json")
        })
    ] : [])
]

dll 公共js轻易不会变化,假如在将来真的发生了更新,那么新的dll文件名便需要加上新的hash,从而避免浏览器缓存老的文件,造成执行出错。由于 hash 的不确定性,我们在 html 入口文件中没办法指定一个固定链接的 script 脚本,刚好,add-asset-html-webpack-plugin 插件可以帮我们自动引入 dll 文件。

const autoAddDllRes = () => {
    const AddAssetHtmlPlugin = require('add-asset-html-webpack-plugin');
    return new AddAssetHtmlPlugin([{ // 往html中注入dll js
        publicPath: config.common.publicPath + "dll/",  // 注入到html中的路径
        outputPath: "dll", // 最终输出的目录
        filepath: resolve("src/assets/dll/*.js"),
        includeSourcemap: false,
        typeOfAsset: "js" // options js、css; default js
    }]);
};

// ...
plugins: [
    ...(config.common.needDll ? [autoAddDllRes()] : [])
]

怎么配置单页面应用、多页面应用

单页面应用可以理解为webpack的标准模式,直接在entry中指定单页面应用的入口即可

多页面应用的话,可以使用webpack的AutoWebPlugin来完成简单自动化的构建,但是前提是项目的目录结构也必须遵守他预设的规范。

多页面应用注意事项:

  • 每个页面都有公共的代码,可以将这些代码抽离出来,避免重复的加载。
  • 随着业务的不断扩展,页面可能会不断的追加,所以一定要让入口的配置足够灵活,避免每次添加新页面还需要修改构建配置

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.