手撕代码: 数组去重及优化,检查运行结果,说思路,再实现下对象数组去重

利用对象属性去重

创建空对象,遍历数组,将数组中的值设为对象的属性,并给该属性赋初始值1,每出现一次,对应的属性值增加1,这样,属性值对应的就是该元素出现的次数了

function unique(arr){
  const obj = {},
        res = [];
  for(let i = 0; i < arr.length; i++){
    if(obj[arr[i]]){
      obj[arr[i]]++
    }else{
      obj[arr[i]] = 1;
      res.push(arr[i]);
    }
  }
  return res
}

双循环去重

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

function unique(arr){
  const result = [arr[0]];
  for(let i = 1; i < arr.length; i++){
    let flag = false;
    for(let j = 0; j < result.length; j++){
      if(arr[i] === result[j]){
        flag = true;
        break;
      }
    }
    if(!flag){
      result.push(arr[i])
    }
  }
  return result
}

数组对象去重:使用filter和Map

function uniqueFunc(arr, uniId){ // arr要去重的数组,uniId数组对象中的id标识
  const res = new Map();
  return arr.filter((item) => !res.has(item[uniId]) && res.set(item[uniId], 1));
}

也可以利用对象的属性进行去重;

手撕代码: 大数相加及优化,检查运行结果,说思路,还有其他实现方式吗?

两种实现方式:1、使用字符串的形式;2、使用bigInt对象;

实现1

function bigIntDataAdd(str1, str2){
    const maxLength = Math.max(str1.length, str2.length); // 寻找两个字符串中最长的串的长度
    str1 = str1.padStart(maxLength, 0); // 并将两个串的长度用0补齐
    str2 = str2.padStart(maxLength, 0);
    let flag = 0, result = ''; // 设置两个变量 flag用来记录两个值的加和是否超过10 result记录最后的结果
    for(let i = maxLength - 1; i >= 0; i--){ // 循环遍历从字符串的末尾开始处理
        const temp = parseInt(str1[i]) + parseInt(str2[i]) + flag; //  
        flag = Math.floor(temp/10); // 判断和是否满10 满10进1 该变量为 1 || 0
        result = temp%10 + result; // 对结果取模并拼接结果上
    }

    if(flag === 1){
        result = flag + result; // 如果最后的flag为 则证明 最后一次的加和满10 需在字符串的最前面拼接上该值
    }
    return result
}

输入一个url到页面渲染发生了什么?

阶段一:用户输入阶段

用户在地址栏输入内容之后,浏览器会首先判断用户输入的是合法的URL还是搜索内容,如果是搜索内容就合成URL,如果是合法的URL就开始进行加载

阶段二:发起URL请求阶段

  1. 构建请求行:浏览器进程首先会构建请求行信息,然后通过进程间通信IPCURL请求发送给网络进程。
  2. 查找缓存:网络进程获取到URL之后,会先去本地缓存中查找是否有缓存资源,如果有则直接将缓存资源返回给浏览器进程,否则进入网络请求阶段。
  3. DNS解析:网络进程请求首先会从DNS数据缓存服务器中查找是否缓存过当前域名的信息,有则直接返回,否则,会进行DNS解析域名对应的IP和端口号。解析IP过程浏览器 -> 系统(本机host) -> 路由器 -> ispDNS 查找请求);
  4. 等待TCP队列:chrome有个机制,同一个域名同时最多只能建立6个TCP连接,如果超过这个数量的连接必须要进入排队等待状态。
  5. 与服务器建立TCP连接
    1. 首先在应用层建立一个http请求的数据包;
    2. 其次会在传输层封装上请求的标识及端口号,会在该层建立连接(三次握手);
      1. 首先客户端发送SYN 报文段,状态设置为 SYN_SEND;
      2. 服务端返回SYN+ACK 报文段给客户端,状态设置为 SYN_RECV
      3. 客户端收到服务器的 SYN+ACK 报文段,向服务器发送 ACK 报文段表示确认,此时客户端和服务器都设置为 ESTABLISHED 状态;
    3. 在网络层再封装上目标机器的mac地址;
    4. 数据连路层;
  6. 发起HTTP请求:浏览器首先会向服务器发送请求行,请求行中包含了请求方法、请求URI和HTTP版本,还会发送请求头,告诉服务器一些浏览器的相关信息,比如浏览器内核、请求域名、Cookie等信息。
  7. 服务器处理请求:服务器首先返回相应行,包括协议版本和状态码,然后会返回响应头包含返回的数据类型,服务器要在客户端保存的Cookie等。
  8. 断开TCP连接:数据传输完成后,通过四次挥手来断开连接
    1. 挥手请求可以是Client端,也可以是Server端发起的,我们假设是Client端发起:
      • 第一次挥手: Client端发起挥手请求,向Server端发送标志位是FIN报文段,设置序列号seq,此时,Client端进入FIN_WAIT_1状态,这表示Client端没有数据要发送给Server端了。
      • 第二次分手:Server端收到了Client端发送的FIN报文段,向Client端返回一个标志位是ACK的报文段,ack设为seq加1,Client端进入FIN_WAIT_2状态,Server端告诉Client端,我确认并同意你的关闭请求。
      • 第三次分手: Server端向Client端发送标志位是FIN的报文段,请求关闭连接,同时Client端进入LAST_ACK状态。
      • 第四次分手 : Client端收到Server端发送的FIN报文段,向Server端发送标志位是ACK的报文段,然后Client端进入TIME_WAIT状态。Server端收到Client端的ACK报文段以后,就关闭连接。此时,Client端等待2MSL的时间后依然没有收到回复,则证明Server端已正常关闭,那好,Client端也可以关闭连接了

阶段三:准备渲染进程阶段

  1. 网络进程将获取的数据进行解析,根据响应头中的Content-type来判断响应数据的类型,如果是字节流类型,就将该请求交给下载管理器去下载,如果是text/html类型,就通知浏览器进程获取到的是HTML,准备渲染进程。
  2. 一般情况下浏览器的一个tab页面对应一个渲染进程,如果从当前页面打开的新页面并且属于同一站点,这种情况会复用渲染进程,其他情况则需要创建新的渲染进程。

阶段四:提交文档阶段

  1. 渲染进程准备好之后,浏览器会发出提交文档的消息给渲染进程,渲染进程收到消息后,会和网络进程建立数据传输的管道,文档数据传输完成后,渲染进程会返回确认提交的消息给浏览器进程。
  2. 浏览器收到确认提交的消息后,会更新浏览器的页面状态,包括了安全状态,地址栏的URL,前进后退的历史状态,并更新web页面为空白。

阶段五:页面渲染阶段

  1. 文档提交之后,渲染进程将开始页面解析并加载子资源。
  2. 构建DOM树:HTML经过解析后输出的是一个以document为顶层节点的树状结构的DOM。
  3. 样式计算:将从link标签引入的外部样式,style标签里的样式和元素身上的样式转换成浏览器能够理解的样式表,然后将样式表中的属性值进行标准化,例如color:red转换为colorrgb形式,然后根据CSS的继承和层叠规则计算出DOM树种每个节点的具体样式。
  4. 布局阶段:会生成一棵只包含可见元素的布局树,然后根据布局树的每个节点计算出其具体位置和大小。
  5. 分层:对页面种的复杂效果例如3D转换,页面滚动或者z轴排序等生成图层树。
  6. 绘制:为每个图层生成绘制列表,并将其提交到合成线程中。
  7. 光栅化:优先选择可视窗口内的图块来生成位图数据。
  8. 合成:所有图块都被光栅话之后开始显示页面

问题汇总

RQ1:浏览器解析HTML过程

针对这个问题,我们可以从阶段五:页面渲染阶段来回答。

RQ2:强缓存和协商缓存发生在那个阶段?

强缓存和协商缓存发生在发起URL请求阶段,在这个阶段构建请求行之后会查找缓存。

RQ3:DNS解析中端口需要DNS解析吗?

不需要,因为HTTP默认的是80端口,HTTPS默认的是443端口,如果要指定端口可以直接在URL里面添加。

RQ4:哪些阶段可以优化?

  1. 优化DNS查询:DNS预解析
  2. 优化TCP连接:可以通过请求头keep-alive来优化。
  3. 优化HTTP响应报文:通过CDN和Gzip压缩。

js会阻塞页面的渲染吗?为什么?

情形一:页面中引入的script脚本会阻塞浏览器解析渲染文档。

浏览器解析文档时,默认是按照排列顺序向下解析的,当遇到script标签时,和其他标签元素一样(例如一个div),会先解析该元素(脚本),解析完成后再继续向下走完成剩余文档的解析和渲染。也就是说默认情况下,script脚本会阻塞文档的解析渲染

注意,如果我们的script脚本是放在页面底部的内联脚本,那么它对文档的解析渲染,在结果上影响不大。

但如果script脚本是外部脚本(通过网址引入的那种),那么这个脚本需要下载和解析执行,这期间会阻塞浏览器对文档的向下解析渲染,直至脚本下载执行完成,才会继续向下解析渲染。如果这个脚本出错了,可能还会导致整个页面永远无法正常渲染呈现。

注意,DOM树的生成是受JavaScript代码执行影响的,JavaScript代码会“阻塞”页面UI的渲染

情形二:页面中引入的script脚本不会阻塞浏览器解析渲染文档。

情形一中提到的脚本执行的同步和阻塞的情形,是指默认情况下的script脚本加载方式。script标签有两个属性,一个是defer(翻译为延迟),二是asyn(翻译为异步,没错,和我们的常见的ajaxaxios的异步是一个意思),这两个属性都可以改变脚本的加载执行方式(在浏览器支持的情况下,这两的兼容性,后面会补充)。

延迟:延迟是指当浏览器解析到script脚本时,会继续向下载入和解析文档,等到文档载入解析完成且可以操作文档时,才开始执行脚本。

异步:异步是指当浏览器解析到script脚本时,会立刻下载和执行脚本,但同时浏览器解析器也会继续向下解析渲染文档,不会造成阻塞的现象。这使得脚本可以尽快的被载入执行,这很想我们前端调用post异步接口,并不影响文档的正常解析渲染。

ps:deferasync属性像是在告诉浏览器,链接进来的脚本不会使用document.write(),也不会生成文档内容,因此浏览器在下载脚本时可以继续解析和渲染文档。

值得注意的是,使用defer属性的脚本,当有多个的时候,这些脚本会按照他们在文档中排列的顺序载入执行。而使用asyn属性的脚本,当有多个的时候,会顺序开始触发载入,但谁先完成载入就谁先执行,这就是说,他们的执行顺序可能是无序不确定的。在有的时候知道这点很重要,因为这可能涉及一些有强制顺序的逻辑处理。

注意事项

仅仅IE浏览器支持defer属性,而async属性则被ie10+和其他现代浏览器支持。

async属性是 HTML5中的新属性,仅适用于外部脚本(即是在script标签使用src属性时)。

一个script标签同时使用了async和defer,则执行async,忽略defer。

一个script标签只使用defer,且 defer="defer",则脚本将在页面完成解析时执行。

async和defer都不使用,遇到script脚本即会马上载入和执行脚本,此时会阻塞页面继续向下解析渲染。

只使用async且async="async",则脚本相对于页面的其余部分异步地执行。

css会阻塞js的加载吗?为什么?

不会影响加载,但会影响js的执行

  1. css加载不会阻塞DOM树的解析
  2. css加载会阻塞DOM树的渲染
  3. css加载会阻塞后面js语句的执行

只有在css加载完成后,才会触发DOMContentLoaded事件。因此,我们可以得出结论:

  1. 如果页面中同时存在css和js,并且存在js在css后面,则DOMContentLoaded事件会在css加载完后才执行。
  2. 其他情况下,DOMContentLoaded都不会等待css加载,并且DOMContentLoaded事件也不会等待图片、视频等其他资源加载。

说一下原型和原型链

引用类型的四个规则:

  1. 引用类型,都具有对象特性,即可自由扩展属性。
  2. 引用类型,都有一个隐式原型 __proto__ 属性,属性值是一个普通的对象。
  3. 引用类型,隐式原型 __proto__ 的属性值指向它的构造函数的显式原型 prototype 属性值。
  4. 当你试图得到一个对象的某个属性时,如果这个对象本身没有这个属性,那么它会去它的隐式原型 __proto__(也就是它的构造函数的显式原型 prototype)中寻找。

说一下JS继承的几种方式

一、原型链继承

关键核心:让父类的实例作为子类的原型

function Person(name, age) {
  this.name = name;
  this.age = age;
}
Person.prototype.running = function () {
  console.log('在跑步~');
};
function Student() {
  this.score = 99;
}
Student.prototype = new Person();
Student.prototype.goToSchool = function () {
  console.log('去上学~');
};
let s1 = new Student();
console.log(s1);
console.log(s1.running());

缺点:不能传递参数,且引用值类型的数据会被实例共享。

例子:

function Person(name, age) {
  this.name = name;
  this.age = age;
  this.friends = ['Yasuo', 'Zed', 'Yi'];
}
Person.prototype.running = function () {
  console.log('在跑步~');
};
function Student() {
  this.score = 99;
}
Student.prototype = new Person();
Student.prototype.goToSchool = function () {
  console.log('去上学~');
};
let s1 = new Student();
let s2 = new Student();
s1.friends.push('LeeSin');
console.log(s1.friends); // ['Yasuo', 'Zed', 'Yi', 'LeeSin']
console.log(s2.friends); // ['Yasuo', 'Zed', 'Yi', 'LeeSin']

可以看到,明明我只往s1friends中 push 了LeeSin,但是s2friends中也有LeeSin了,这就是原型链继承的缺点之一:引用值类型的数据会被实例共享

二、盗用构造函数继承

关键核心:在子类构造函数中使用 call() 调用父类构造函数

function Person(name, age) {
  this.name = name;
  this.age = age;
  this.friends = ['Yasuo', 'Zed', 'Yi'];
}
Person.prototype.running = function () {
  console.log('在跑步~');
};
function Student(name, age) {
  Person.call(this, name, age);
  this.score = 99;
}
// Student.prototype = new Person();
Student.prototype.goToSchool = function () {
  console.log('去上学~');
};
let s1 = new Student('Cyan', 18);
let s2 = new Student('Csy', 22);
console.log(s1.name); // Cyan
console.log(s2.name); // Csy
s1.friends.push('LeeSin');
console.log(s1.friends); // ['Yasuo', 'Zed', 'Yi', 'LeeSin'];
console.log(s2.friends); // ['Yasuo', 'Zed', 'Yi']
console.log(s1.running()); // 报错,s1.running is not a function

优点:解决了原型链继承中不能传参且引用值共享问题。
缺点:由上图可知,这个方式不能调用父类原型上的方法,因为 Student.prototypePerson.prototype 根本没有任何关系

三、组合继承(原型链 + 构造函数)

关键核心:使用 call() 调用父类的属性,使用 new 获取父类原型上的方法

function Person(name, age) {
  this.name = name;
  this.age = age;
  this.friends = ['Yasuo', 'Zed', 'Yi'];
}
Person.prototype.running = function () {
  console.log('在跑步~');
};
function Student(name, age) {
  Person.call(this, name, age);
  this.score = 99;
}
Student.prototype = new Person();
Student.prototype.goToSchool = function () {
  console.log('去上学~');
};
let s1 = new Student('Cyan', 18);
let s2 = new Student('Csy', 22);
console.log(s1.name); // Cyan
console.log(s2.name); // Csy
s1.friends.push('LeeSin');
console.log(s1.friends); // ['Yasuo', 'Zed', 'Yi', 'LeeSin']
console.log(s2.friends); // ['Yasuo', 'Zed', 'Yi']
console.log(s1.running()); // 在跑步~

优点:解决了不能调用父类原型上的方法的问题。
缺点:多次调用了父类构造函数

实际上,父类构造函数中的this.namethis.age在子类中是取不到的。因为通过子类 new 出来的对象在访问属性或者方法时,会先看 new 出自己的类上面有没有这个属性和方法,没有才去原型上找,原型上没有再去父类找,父类找不到再去父类的原型上找。显然,你都已经调用 call 了,那么父类(Person)中的属性和方法子类(Student)中必然也有

四、寄生式组合继承

关键核心:使用 Object.create() 来解决多次调用父类构造函数问题

function Person(name, age) {
  this.name = name;
  this.age = age;
  this.friends = ['Yasuo', 'Zed', 'Yi'];
}
Person.prototype.running = function () {
  console.log('在跑步~');
};
function Student(name, age) {
  Person.call(this, name, age);
  this.score = 99;
}
Student.prototype = Object.create(Person.prototype);
Student.prototype.goToSchool = function () {
  console.log('去上学~');
};
let s1 = new Student('Cyan', 18);
let s2 = new Student('Csy', 22);
console.log(s1.name); // Cyan
console.log(s2.name); // Csy
s1.friends.push('LeeSin');
console.log(s1.friends); // ['Yasuo', 'Zed', 'Yi', 'LeeSin']
console.log(s2.friends); // ['Yasuo', 'Zed', 'Yi']
console.log(s1.running()); // 在跑步~

PS:由于使用Object.create()将student的prototype属性指向了一个新对象,那么该构造函数也会丢失他自己的构造函数指向

// 承接上面的代码
console.log(s1.constructor === Student) // false
console.log(s1.constructor === Person) // true

所以在修改构造函数的原型之后,需要将原型函数的constructor函数重新指向它自身

Student.prototype.constructo = Student

Student.prototype = Object.create(Person.prototype)这段代码的意思是先用 Object.create() 创建一个新对象,然后让这个新对象的__proto__ 指向Person.prototype,最后再让Student.prototype.__proto__指向这个新对象。

使用Object.create(),当修改Student.prototype上的方法时不会对Teacher.prototype造成影响。实际上,他们两个指向的不是同一个对象。

注意!不能用Student.prototype = Person.prototype,因为如果这时一个 Teacher 子类也想继承 Person 类并且如果 Student 或者 Teacher 修改了他们对应的原型上的方法的话,另外一个类的实例也会受到影响,如下面代码:

function Person(name, age) {
  this.name = name;
  this.age = age;
}
Person.prototype.running = function () {
  console.log('在跑步~');
};
function Student(name, age) {
  Person.call(this, name, age);
  this.score = 99;
}
function Teacher(name, age) {
  Person.call(this, name, age);
  this.salary = 9999;
}
Student.prototype = Person.prototype;
Teacher.prototype = Person.prototype;

let s1 = new Student('Cyan', 18);
let t1 = new Teacher('Csy', 22);

Student.prototype.running = function () {
  console.log('学生在跑步~');
};
console.log(s1.running()); // 学生在跑步~
console.log(t1.running()); // 学生在跑步~
console.log(Student.prototype === Teacher.prototype); // true

说一下闭包?应用场景

闭包:闭包是指有权访问另一个函数作用域中变量的函数

原因:内部的函数存在外部作用域的引用就会导致闭包

记住闭包的方法是通过背包的类比。当一个函数被创建并传递或从另一个函数返回时,它会携带一个背包。背包中是函数声明时作用域内的所有变量。

任何函数都携带一个闭包,包括全局作用域声明的函数

闭包中的变量是存在堆内存中的

  • 闭包保护对象内的属性不被外部修改
  • 保护函数的私有变量不受外部的干扰。形成不销毁的栈内存。
  • 保存,把一些函数内的值保存下来。闭包可以实现方法和属性的私有化

使用场景

  1. return 一个函数
  2. 函数作为参数
  3. IIFE(自执行函数)
  4. 循环赋值
  5. 使用回调函数就是在使用闭包
  6. 节流防抖
  7. 柯里化实现

实现一个防抖节流函数

防抖:在事件被触发n秒后再执行回调,如果在这n秒内又被触发,则重新计时。

类似于玩游戏时法师施放技能有读条,重新按了之后会触发再次读条

// 普通函数 apply
function debounce(fn, timeout) {
    var timer = null;
    return function () {
        clearTimeout(timer)
        var that = this;
        var args = arguments;
        timer = setTimeout(function () {
            fn.apply(that, args)
        }, timeout)
    }
}

// 普通函数 call 解构
function debounce(fn, timeout) {
    var timer = null;
    return function () {
        clearTimeout(timer)
        var that = this;
        var args = arguments;
        timer = setTimeout(function () {
            fn.call(that, ...args)
        }, timeout)
    }
}

// 箭头函数 结构赋值
function debounce(fn, timeout) {
    let timer = null;
    return function (...args) {
        clearTimeout(timer)
        timer = setTimeout(()=> {
            fn.apply(this, args)
        }, timeout)
    }
}

节流:规定在一个单位时间内,只能触发一次函数。如果这个单位时间内触发多次函数,只有一次生效。

理解 函数节流就是fps游戏的射速,就算一直按着鼠标射击,也只会在规定射速内射出子弹。

function throttle(fn, timeout) {
    let timer = null;
    return function (...args) {
        if (timer) return
        timer = setTimeout(() => {
            fn.apply(this, args)
            timer = null
        }, timeout)
    }
}

说一下DOM事件机制?

  • dom的等级有四个级别 dom0 dom1 dom2 dom3
  • dom的事件级别只有三级 dom0级事件 dom2级 dom3级
  • dom0级事件只能绑定一次,重复绑定会覆盖之前的事件
  • dom2级事件可以绑定多次,dom2级事件区分事件捕获和事件冒泡,默认是事件冒泡

如果对一个容器同时绑定捕获和冒泡事件,先触发哪个?为什么?

结论:先触发捕获事件

一个事件发生后,会在子元素和父元素之间传播(propagation)。这种传播分成三个阶段。

  • 第一阶段:从window对象传导到目标节点(上层传到底层),称为“捕获阶段”(capture phase)。

  • 第二阶段:在目标节点上触发,称为“目标阶段”(target phase)。

  • 第三阶段:从目标节点传导回window对象(从底层传回上层),称为“冒泡阶段”(bubbling phase)。

说一下事件循环?

JavaScript代码的执行过程中,除了依靠函数调用栈来搞定函数的执行顺序外,还依靠任务队列(task queue)来搞定另外一些代码的执行。整个执行过程,我们称为事件循环过程。一个线程中,事件循环是唯一的,但是任务队列可以拥有多个。任务队列又分为macro-task(宏任务)与micro-task(微任务),在最新标准中,它们被分别称为task与jobs。

macro-task大概包括:

  • script(整体代码)
  • setTimeout
  • setInterval
  • setImmediate
  • I/O
  • UI render

micro-task大概包括:

  • process.nextTick
  • Promise.then()
  • Async/Await(实际就是promise)
  • MutationObserver(html5新特性)

总的结论就是,执行宏任务,然后执行该宏任务产生的微任务,若微任务在执行过程中产生了新的微任务,则继续执行微任务,微任务执行完毕后,再回到宏任务中进行下一轮循环。

易错点

  1. promise本身是一个同步的代码(只是容器),只有它后面调用的then()方法里面的回调才是微任务;

  2. await右边的表达式还是会立即执行,表达式之后的代码才是微任务, await微任务可以转换成等价的promise微任务分析;

  3. 如果await 后面直接跟的为一个变量,比如:await 1;这种情况的话相当于直接把await后面的代码注册为一个微任务,可以简单理解为promise.then(await下面的代码),如果await后面若是一个微任务的话 会将这个微任务推到微任务队列中去,则不会立即将await后边的代码立即注册至微任务队列中;

  4. script标签本身是一个宏任务, 当页面出现多个script标签的时候,浏览器会把script标签作为宏任务来解析;

说一下Promise,简单实现一下Promise

  1. Promise 是一个类,在执行这个类的时候会传入一个执行器,这个执行器会立即执行

  2. Promise 会有三种状态

    • Pending 等待
    • Fulfilled 完成
    • Rejected 失败
  3. 状态只能由 Pending --> Fulfilled 或者 Pending --> Rejected,且一但发生改变便不可二次修改;

  4. Promise 中使用 resolve 和 reject 两个函数来更改状态;

  5. then 方法内部做但事情就是状态判断

    • 如果状态是成功,调用成功回调函数
    • 如果状态是失败,调用失败回调函数
    • 若是executor函数报错 直接执行reject();
    class MyPromise {
       constructor(executor) {
           this.state = 'pending'
           this.value = undefined
           this.reason = undefined
           // 成功存放的数组
           this.onResolvedCallbacks = [];
           // 失败存放法数组
           this.onRejectedCallbacks = [];
    
           let resolve = (value) => {
               if (this.state === 'pending') {
                   this.state = 'fulfilled'
                   this.value = value
                   // this.onResolvedCallbacks.forEach(fn => fn())
                   while (this.onResolvedCallbacks.length > 0) {
                       const fn = this.onResolvedCallbacks.shift()
                       fn()
                   }
               }
           }
    
           let reject = (reason) => {
               if (this.state === 'pending') {
                   this.state = 'rejected '
                   this.reason = reason
                   // this.onRejectedCallbacks.forEach(fn => fn())
                   while (this.onRejectedCallbacks.length > 0) {
                       const fn = this.onRejectedCallbacks.shift()
                       fn()
                   }
               }
           }
    
           try {
               executor(resolve, reject);
           } catch (e) {
               reject(e)
           }
       }
    
       then(onFulfilled, onRejected) {
    
           onFulfilled = typeof onFulfilled === 'function' ? onFulfilled : (value) => value;
           onRejected = typeof onRejected === 'function' ? onRejected : (reason) => { throw reason };
    
           const promise2 = new MyPromise((resolve, reject) => {
               if (this.state === 'fulfilled') {
                   try {
                       queueMicrotask(() => {
                           const x = onFulfilled(this.value)
                           resolvePromise(promise2, x, resolve, reject)
                       })
                   } catch (e) {
                       reject(e)
                   }
               }
    
               if (this.state === 'rejected') {
                   try {
                       queueMicrotask(() => {
                           const x = onRejected(this.reason)
                           resolvePromise(promise2, x, resolve, reject)
                       })
                   } catch (e) {
                       reject(e)
                   }
               }
    
               if (this.state === 'pending') {
                   this.onResolvedCallbacks.push(() => {
                       // onFulfilled(this.value)
                       try {
                           queueMicrotask(() => {
                               const x = onFulfilled(this.value)
                               resolvePromise(promise2, x, resolve, reject)
                           })
                       } catch (e) {
                           reject(e)
                       }
                   })
    
                   this.onRejectedCallbacks.push(() => {
                       // onRejected(this.reason)
                       try {
                           queueMicrotask(() => {
                               const x = onRejected(this.reason)
                               resolvePromise(promise2, x, resolve, reject)
                           })
                       } catch (e) {
                           reject(e)
                       }
    
                   })
               }
           })
    
           return promise2
       }
    
       static resolve(parameter) {
           if (parameter instanceof MyPromise) {
               return parameter
           }
    
           return new MyPromise((resolve, reject) => {
               resolve(parameter)
           })
       }
    
       static reject(reason) {
           // if (parameter instanceof MyPromise) {
           //     return parameter
           // }
    
           return new MyPromise((resolve, reject) => {
               reject(reason)
           })
       }
    }
    
    function resolvePromise(promise2, x, resolve, reject) {
       if (x === promise2) {
           return reject(new TypeError('Chaining cycle detected for promise #'))
       }
       if (x instanceof MyPromise) {
           x.then(resolve, reject)
       } else {
           resolve(x)
       }
    }
    

async/await是怎么实现的?await怎么做到的阻塞?

结论:async/await是基于Generator对Promise进行封装后的函数,返回一个Promise对象,并await暂停等待Promise执行resolve的结果。也说async/await实际上是Generator的一个语法糖。那么这里的暂停的效果就是通过generator来完成的

H5语义化标签用过吗?为什么使用语义化标签?

  • 对人类友好之外,语义类标签也十分适宜机器阅读。它的文字表现力丰富,更适合 搜索引擎检索(SEO),也可以让搜索引擎爬虫更好地获取到更多有效信息,有效提升网页的搜索量。
  • 在页面没有加载CSS的情况下,开发者也能够清晰地看出网页的结构,也更为便于团队的开发和维护。

代码结构清晰,可读性高,减少差异化,便于团队开发和维护。

  • 网语义类可以支持读屏软件,根据文章可以自动生成目录等等
1. <small>:呈现小号字体效果,指定细则,输入免责声明、注解、署名、版权。

2. <strong>:和 em 标签一样,用于强调文本,但它强调的程度更强一些。 

3. <em>:将其中的文本表示为强调的内容,表现为斜体。 

4. <mark>:使用黄色突出显示部分文本。 

5. <figure>:规定独立的流内容(图像、图表、照片、代码等等)(默认有40px左右margin)。 

6. <figcaption>:定义 figure 元素的标题,应该被置于 figure 元素的第一个或最后一个子元素的位置。 

7. <cite>:表示所包含的文本对某个参考文献的引用,比如书籍或者杂志的标题。 

8. <blockquoto>:定义块引用,块引用拥有它们自己的空间。 

9. <q>:短的引述(跨浏览器问题,尽量避免使用)。 

10. <time>:datetime属性遵循特定格式,如果忽略此属性,文本内容必须是合法的日期或者时间格式。 

11. <abbr>:简称或缩写。 

12. <dfn>:定义术语元素,与定义必须紧挨着,可以在描述列表dl元素中使用。 

13. <address>:作者、相关人士或组织的联系信息(电子邮件地址、指向联系信息页的链接)。 

14. <del>:移除的内容。 

15. <ins>:添加的内容。 

16. <code>:标记代码。 

17. <meter>:定义已知范围或分数值内的标量测量。(Internet Explorer 不支持 meter 标签) 

18. <progress>:定义运行中的进度(进程)。

居中有多少种实现方式,越多越好。

  1. 使用flex布局设置居中。
  2. 使用flex 时也能通过给子项设置margin: auto实现居中。
  3. 使用绝对定位的方式实现水平垂直居中。
  4. 使用grid设置居中。给容器设置 display: grid; align-items: center; justify-content: center;
  5. 使用grid时还能通过给子项设置margin: auto实现居中。给容器设置 display: grid; 子项设置 margin: auto;
  6. 使用tabel-cell实现垂直居中。
  7. 还有一种不常用的方法实现垂直居中,给容器加给伪元素,设置line-height等于容器的高度。给孩子设置display: inline-block此种方式适合给文本设置水平垂直居中;
  8. 最后还有一种奇葩的方法。容器设置position: relative。子元素设置 top、left、bottom、right都设置为0

跨域解决方案有哪些,详细讲讲jsonp,同源策略又是什么?

「同源策略」是一个重要的安全策略,它用于限制一个origin的文档或者它加载的脚本如何能与另一个源的资源进行交互。它能帮助阻隔恶意文档,减少可能被攻击的媒介。

写过博客吗?怎么部署?

地址:jacklon.online

cookie和session的区别

cookie

Cookie是访问某些网站以后在本地存储的一些网站相关的信息,下次再访问的时候减少一些步骤。

Cookie中一般包括如下主要内容:

  1. key:设置的cookie的key。
  2. value:key对应的value。
  3. max_age/expire_time:设置cookie的过期时间。
  4. domain:该cookie在哪个域名中有效。一般设置子域名,比如cms.example.com。
  5. path:该cookie在哪个路径下有效。

例如,我们登录某一个网站时需要输入用户名及密码,如果用户名和密码保存为cookie,则下次我们登录该网站的时候就不需要再输入用户密码了

session

session是存在服务器的一种用来存放用户数据的类HashTable结构。

浏览器第一次发送请求时,服务器自动生成了一HashTable和一SessionID来唯一标识这个HashTable,并将其通过响应发送到浏览器。浏览器第二次发送请求会将前一次服务器响应中的SessionID放在请求中一并发送到服务器上,服务器从请求中提取出Session ID,并和保存的所有Session ID进行对比,找到这个用户对应的HashTable。

例如,我们浏览一个购物网站,用户将部分商品添加到购物车中,许久以前许多网站都是用服务端session存储购物车内容(现在基本都是用数据库了),就用到了session存储这部分信息。

区别

1.存储位置不同

cookie的数据信息存放在本地。
session的数据信息存放在服务器上。

2.存储容量大小不同

cookie存储的容量较小,一般 小于等于 4KB。
session存储容量大小没有限制(但是为了服务器性能考虑,一般不能存放太多数据)。

3.存储有效期不同

cookie可以长期存储,只要不超过设置的过期时间,可以一直存储。
session在超过一定的操作时间(通常为30分钟)后会失效,但是当关闭浏览器时,为了保护用户信息,会自动调用session.invalidate()方法,该方法会清除掉session中的信息。

4.安全性不同

cookie存储在客户端,所以可以分析存放在本地的cookie并进行cookie欺骗,安全性较低。
session存储在服务器上,不存在敏感信息泄漏的风险,安全性较高。

5.域支持范围不同

cookie支持跨域名访问。例如,所有a.com的cookie在a.com下都能用。
session不支持跨域名访问。例如,www.a.com的session在api.a.com下不能用。

6.对服务器压力不同

cookie保存在客户端,不占用服务器资源。
session是保存在服务器端,每个用户都会产生一个session,session过多的时候会消耗服务器资源,所以大型网站会有专门的session服务器。

7.存储的数据类型不同

cookie中只能保管ASCII字符串,并需要通过编码方式存储为Unicode字符或者二进制数据。
session中能够存储任何类型的数据,包括且不限于string,integer,list,map等。

cookie的属性有哪些

Expires

Expires属性指定一个具体的到期时间,到了指定时间以后,浏览器就不再保留这个 Cookie。

Max-Age

属性指定从现在开始 Cookie 存在的秒数,比如60 * 60 * 24 * 365(即一年)。过了这个时间以后,浏览器就不再保留这个 Cookie。

Domain

Domain属性指定 Cookie 属于哪个域名,以后浏览器向服务器发送 HTTP 请求时,通过这个属性判断是否要附带某个 Cookie。

服务器设定 Cookie 时,如果没有指定 Domain 属性,浏览器会默认将其设为浏览器的当前域名。如果当前域名是一个 IP 地址,则不得设置 Domain 属性。

Path

Path属性指定浏览器发出 HTTP 请求时,哪些路径要附带这个 Cookie。只要浏览器发现,Path属性是 HTTP 请求路径的开头一部分,就会在头信息里面带上这个 Cookie。比如,Path属性是/,那么请求/docs路径也会包含该 Cookie

Secure

Secure属性指定浏览器只有在加密协议 HTTPS 下,才能将这个 Cookie 发送到服务器。另一方面,如果当前协议是 HTTP,浏览器会自动忽略服务器发来的Secure属性。该属性只是一个开关,不需要指定值。如果通信是 HTTPS 协议,该开关自动打开

HttpOnly

HttpOnly属性指定该 Cookie 无法通过 JavaScript 脚本拿到,主要是document.cookie属性、XMLHttpRequest对象和 Request API 都拿不到该属性。这样就防止了该 Cookie 被脚本读到,只有浏览器发出 HTTP 请求时,才会带上该 Cookie。

SameSite

Chrome 51 开始,浏览器的 Cookie 新增加了一个SameSite属性,用来防止 CSRF 攻击和用户追踪。

StrictStrict最为严格,完全禁止第三方 Cookie,跨站点时,任何情况下都不会发送 Cookie

LaxLax规则稍稍放宽,大多数情况也是不发送第三方 Cookie,但是导航到目标网址的 Get 请求除外。

如果cookie不设置有效期,cookie什么时候删除

结论:关闭浏览器的时候

如果Set-Cookie字段没有指定ExpiresMax-Age属性,那么这个 Cookie 就是 Session Cookie,即它只在本次对话存在,一旦用户关闭浏览器,浏览器就不会再保留这个 Cookie。

如何加快页面首屏渲染?

  1. 更加具体的标记要求浏览器处理的工作更多,实际编写中应该尽可能避免编写过于具体的选择器;
  2. 合并浏览器请求,因为浏览器针对同一个域名同时只能发起六个请求;
  3. 由于请求限制对于项目使用到的第三方依赖可以使用cdn加速;
  4. webpack打包优化,例如减少css、js、注释等无用代码打包至生产环境;
  5. 组件动态化引入,使用预加载等技术;
  6. 在服务端配置gzip压缩,开启浏览器缓存等策略

详细说一下浏览器缓存

浏览器的缓存策略

浏览器对于缓存的处理是根据第一次请求资源时返回的响应头来确定的。

根据响应头,浏览器缓存策略一般分为三种:强缓存,协商缓存启发式缓存

浏览器常见字段和指令

在讲强缓存和协商缓存之前先提前了解以下这几个字段和指令,便于后面理解:

  1. expires: 告知客户端资源缓存失效的绝对时间
  2. last-modified: 资源最后一次修改的时间
  3. Etag: 文件的特殊标识
  4. cache-control:告诉客户端或是服务器如何处理缓存。
  5. private: cache-control里的响应指令.表示客户端可以缓存
  6. public: cache-control里的响应指令.表示客户端和代理服务器都可缓存.如果没有明确指定private,则默认为public。
  7. no-cache: cache-control里的指令.表示需要可以缓存,但每次用应该去向服务器验证缓存是否可用
  8. no-store: cache-control字段里的指令.表示所有内容都不会缓存,强制缓存,对比缓存都不会触发.
  9. max-age=xxx: cache-control字段里的指令.表示缓存的内容将在 xxx 秒后失效

强缓存

强缓存简单理解就是:给浏览器缓存设置过期时间,超过这个时间之后缓存就是过期,浏览器需要重新请求。

强缓存主要是通过http请求头中的Cache-Control和Expires两个字段控制

expires是一个HTTP/1.0的字段,它给浏览器设置了一个绝对时间,当浏览器时间超过这个绝对时间之后,重新向服务器发送请求。

为了解决expires存在的问题,Http1.1版本中提出了cache-control:max-age,该字段与expires的缓存思路相同,都是设置了一个过期时间,不同的是max-age设置的是相对缓存时间开始往后的多少秒,因此不再受日期不准确情况的影响。

优先级:在优先级上:max-age>Expires。当两者同时出现在响应头时,Expires将被max-age覆盖

协商缓存

协商缓存解决了无法及时获取更新资源的问题。它利用下面会讲到的两组字段,对资源做标识.然后由服务器做分析,如果资源未更新,则返回304状态码.那么浏览器则会从缓存中读取资源,否则重新请求资源。

协商缓存是利用的是【Last-Modified,If-Modified-Since】和【ETag、If-None-Match】这两对Header来管理的。

优先级: ETag与If-None-Match > Last-Modified与If-Modified-Since, 同时存在时, 前者覆盖后者。

对于大部分的场景都可以使用强缓存配合协商缓存解决,但是在一些特殊的地方可能需要选择特殊的缓存策略

  • 对于某些不需要缓存的资源,可以使用 Cache-control: no-store ,表示该资源不需要缓存

  • 对于频繁变动的资源,可以使用 Cache-Control: no-cache 并配合 ETag 使用,表示该资源已被缓存,但是每次都会发送请求询问资源是否更新。

  • 对于代码文件来说,通常使用 Cache-Control: max-age=31536000 并配合策略缓存使用,然后对文件进行指纹处理,一旦文件名变动就会立刻下载新的文件。

    304?协商缓存时浏览器会发请求给服务器吗?

  • 触发了协商缓存策略,服务器资源无变化返回状态304,这时候浏览器在从内存或者硬盘中提取缓存资源加载

  • 会发出请求

如何实现DNS预解析?DNS预解析在什么时候执行?

打开和关闭DNS预解析

希望在HTTPS页面开启自动解析功能时,添加如下标记

<meta http-equiv="x-dns-prefetch-control" content="on">
// off 则是关闭

也可以通过在服务器端发送 X-DNS-Prefetch-Control 报头

详细说一下CDN?它是怎么找到离它地理位置最近的服务器的而不是其他的服务器?

关键在于三点:

  • 利用 CNAME 的能力,把对 www.test.com 域名的解析变成对特定 CDN 域名的解析,例如下图中的 www.test.com.cdn.dnsv1.com
  • CDN 服务商维护着一个巨大而精确的 IP 地址数据库,能根据用户客户端的 IP 判断出客户端所在的地区、网络运营商等信息。
  • 当解析 www.test.com.cdn.dnsv1.com 域名 IP 时,会向 CDN 服务的 DNS 域名发起请求(也就是下图中的第 2 步),此时 CDN 服务的 DNS 域名可以根据用户客户端的 IP 返回一个距离它最近的缓存服务器 IP

http2.0了解多少?http3.0呢?

二进制分帧层,多路复用,头部压缩,服务器端推送

webpack

简单说一下 webpack 的构建流程

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

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

Loader 和 Plugin 的区别

Loader 本质就是一个函数,在该函数对接收到的内容进行转换,返回转换后的结果。你可以理解为是一个“管道”,在外部接收到的内容通过这个“管道”进行转换,然后再将转换后的结果输出。因为 webpack 只认识 js,所以,你也可以将 Loader 称之为“翻译官”,对其他类型的资源进行转译的预处理工作。

Plugin 直译为插件,基于事件流框架 Tapable。Plugin 可以扩展 webpack 的功能,让 webpack 具有更多的灵活性。在 Webpack 运行的生命周期中会广播出许多事件,Plugin可以监听这些事件,在合适的时机通过 webpack 提供的 API 改变输出结果。

说一下 webpack 的热更新原理

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

HMR 的核心就是客户端从服务端拉取更新后的文件,准确的说是 chunk diff(chunk需要更新的部分),实际上 webpack-dev-server 与浏览器之间维护了一个 WebSocket,当本地资源发生变化时,webpack-dev-server会向浏览器推送更新,并带上构建时的 hash,让客户端与上一次资源进行对比。客户端对比出差异后会向 webpack-dev-server 发起 ajax 请求来获取更改内容(文件列表、hash),这样客户端就可以再借助这些信息继续向 webpack-dev-server 发起 jsonp 请求获取该 chunk 的增量更新。后续的部分由 HotModulePlugin 来完成,提供了相关的 API 以供开发真针对自身场景进行处理,像 react-hot-loadervue-loader 都是借助这些 API 实现 HMR。

如何提高 webpack 的打包速度?

  1. 多入口情况下,使用 optimization.splitChunks 来提取公共代码。
  2. 通过 externals 配置来提取常用库。
  3. 利用 DllPlugin DllReferencePlugin 预编译资源模块,通过 DllPlugin 来对那些我们引用但是绝对不会修改的npm包来进行预编译,再通过DllReferencePlugin 将编译编译的模块加载进来。
  4. 使用 thread-loader 实现多进程加速编译。
  5. 使用 terser-webpack-plugin 对js进行代码压缩。
  6. 优化 resolve 配置缩小范围。
  7. 使用 tree-shakingScope hoisting 来剔除多余代码。

如何减少 webpack 打包体积?

  1. 使用 externals 配置来提取常用库。
  2. 使用 tree-shakingscope hoisting 来剔除多余代码。
  3. 使用 optimize-css-assets-webpack-plugin 压缩css。
  4. 使用 terser-webpack-plugin 对 js 进行代码压缩。

webpack 有哪些常见的 loader?你用过哪些 loader?

  • cache-loader:可以在一些性能开销较大的 Loader 之前添加,目的是将结果缓存到磁盘里。
  • file-loader:把文件输出到一个文件夹中,在代码中通过相对 URL 去引用输出的文件(处理图片、字体、图标)。
  • url-loader:与file-loader 类似,区别是用户可以设置一个阈值,大于阈值会交给 file-loader,小于阈值时返回文件 base64 形式编码(处理图片)。
  • image-loader:加载并且压缩图片文件。
  • babel-loader:把 ES6 转换成 ES5。
  • ts-loader:将 typescript 转换成 JavaScript。
  • svg-inline-loader:将压缩后的SVG内容注入代码中。
  • raw-loader:加载文件原始内容(utf-8)。
  • sass-loader:将 scss/sass 代码转换成 css。
  • css-loader:加载 css,支持模块化、压缩、文件导入等特性。
  • less-loader:将 less 代码转换成 css。
  • style-loader:生成 style 标签,将 js 中的样式资源插入,并添加到 header 中生效。
  • postcss-loader:扩展 css 语法,使用下一代 css,可以配合 autoprefixer 插件自动补齐 css3 前缀。
  • eslint-loader:通过 eslint 检查 JavaScript 代码。
  • tslint-loader:通过tslint 检查 typesc 代码。
  • vue-loader:加载 vue.js 单文件组件。
  • awesome-typescript-loader:将 typescript 转换成 JavaScript,性能优于 ts-loader。

webpack 有哪些常见的 plugin?你用过哪些 plugin?

  • ignore-plugin:忽略部分文件。
  • html-webpack-plugin:简化 html 文件创建。
  • web-webpack-plugin:可以方便地为单页应用输出 html,比 html-webpack-plugin 好用。
  • terser-webpack-plugin:支持压缩ES6。
  • optimize-css-assets-webpack-plugin:压缩css代码。
  • mini-css-extract-plugin:分离样式文件,css 提取为单独文件,支持按需加载。
  • werviceworker-webpack-plugin:为网页应用追加离线缓存功能。
  • clean-webpack-plugin:目录清理。
  • ModuleconcatenationPlugin:开启 Scope Hoisting。
  • webpack-bundle-analyzer:可视化 webpack 输出文件的体积(业务组件、依赖第三方模块)。
  • speed-measure-webpack-plugin:可以看到每个 loader 和 plugin 执行耗时(这个打包耗时、plugin 和 loader 耗时)。 - HotModuleReplacementPlugin:模块热替换。

在使用 webpack 开发时,你用过哪些可以提供效率的插件?

  • webpack-dashboard:可以更有好的展示相关打包信息。
  • webpack-merge:提取公共配置,减少重复配置代码。
  • speed-measure-webpack-plugin:简称SMP,分析出 webpack 打包过程中 loader 和 plugin的耗时,有助于找到构建过程中的性能瓶颈。
  • HotModuleReplacementPlugin:模块热替换。
  • size-plugin:监控资源体积变化,尽早发现问题。

source map 是什么?生产环境怎么用?

source map 是将编译、打包、压缩后的代码映射会源代码的过程。打包压缩后的代码不具备良好的可读性,想要调试源码就需要 source map。
map 文件只要不打开开发者工具,浏览器是不会加载的。
显示环境一般有三种处理方案:

  • hidden-source-map:借助第三方错误监控平台 Sentry 使用。
  • nosource-source-map:只会显示具体行数以及查看源代码的错误栈。安全性比 source-map 高
  • source-map:通过 nginx 设置将.map文件指对白名单开发。 注意:在生产环境中避免使用 inline-eval-,因为它们会增加 bundle 体积大小,并降低整体性能。

文件指纹是什么?

文件指纹是指打包后输出文件的名的后缀。

  • hash:和整个项目的构建相关,只要项目文件有修改,整个项目构建的 hash 就会变化。
  • chunkhash:和webpack打包的 chunk 有关,不同的 chunk、不同的 entry 会生成不同的 chunkhash。
  • contenthash:根据文件内容来定义 hash,文件内容不发生变化,则contenthash就不会变化。 直接在输出文件名添加对应的 hash值即可。

tree shaking 原理是什么?

webpack中,tree-shaking 的实现 一是先标记出模块导出值中哪些没有被动用过,二是 Terser 使用删除掉这些没被用到的导出语句。

标记功能需要配置 optimization.usedExports = true 开启   
复制代码

标记过程大致可划分为三个步骤:

  • Make 阶段:收集模块导出变量并记录到模块依赖关系图 ModuleGraph 变量中。
  • Seal 阶段:遍历 ModuleGraph 标记模块导出变量有没有被使用
  • 生成产物时:若变量没有被其他模块使用则删除对应的导出语句。 Webpack 中 Tree Shaking 的实现分为如下步骤:
  • FlagDependencyExportsPlugin 插件中根据模块的 dependencies 列表收集模块导出值,并记录到 ModuleGraph 体系的 exportsInfo
  • FlagDependencyUsagePlugin 插件中收集模块的导出值的使用情况,并记录到 exportInfo._usedInRuntime 集合中
  • HarmonyExportXXXDependency.Template.apply 方法中根据导出值的使用情况生成不同的导出语句
  • 使用 DCE 工具删除 Dead Code,实现完整的树摇效果 详细tree-shaking原理请参考这里

说一下 Babel 原理

大多数JavaScript Parser遵循 estree 规范,Babel 最初基于 acorn 项目(轻量级现代 JavaScript 解析器) Babel大概分为三大部分:

  • 解析:将代码转换成 AST
    • 词法分析:将代码(字符串)分割为token流,即语法单元成的数组
    • 语法分析:分析token流(上面生成的数组)并生成 AST
  • 转换:访问 AST 的节点进行变换操作生产新的 AST
    • Taro就是利用 babel 完成的小程序语法转换
  • 生成:以新的 AST 为基础生成代码 详细参考 深入理解babel

有写过 loader 吗?简单描述一下思路

没有。但是我知道其开发的基本思路:因为 loader 支持链式调用,所以开发上需要严格遵循“单一职责”,每个 loader 只负责自己需要负责的事情。loader 拿到的是源文件的内容(content),通过this.getOptions() 拿到传入的参数,可以通过返回值的方式将处理后的内容输出或者通过 this.callback() 同步方式将内容返回出去,也可以调用 this.async() 生成一个异步的函数callback 来处理传入的内容,再通过调用 cabllback()将处理后的内容返回出去。开发的过程中尽量使用异步 loader。使用 schema-utils 来检验的我们的参数。然后再利用第三方提供的模块进行 loader 的开发。

有写过 plugin 吗?简答描述一下思路

没有。但是我知道其开发的基本思路: webpack 在运行生命周期中会广播出许多事件,PLugin 可以监听这些事件,在特定的阶段写入想要添加的自定义功能。webpack 的 tapable 事件流机制保证了插件的有序性,使得整个系统扩展性良好。 通过 consturctor 获取传入的配置参数,apply() 方法得到 compiler,compiler 暴露了和 webpack 整个生命周期相关的钩子,通过 conpiler.hooks.thiscompilation 初始 compilationcompilation 暴露了与模块和依赖有关的粒度更小的事件钩子,再使用相关的 hooks 对资源进行添加或者修改。emit 事件发生时,可以读取到最终输出的资源、代码块、模块及其依赖,并进行修改(emit 事件是修改 webpack 输出资源的最后时机)。

异步的事件需要再插件处理完任务时调用回调函数通知 webpack 进入下一个流程,不然会卡住。

require和import的区别

node编程中最重要的思想就是模块化,import 和 require 都是被模块化所使用。在 ES6 当中,用 export 导出接口,用 import 引入模块。但是在 node 模块中,使用module.exports导出接口,使用 require 引入模块。

两者的区别如下:

遵循规范:

  • require 是 AMD 规范引入方式
  • import是 ES6 的一个语法标准,如果要兼容浏览器的话必须转化成 ES5 的语法

调用时间:

  • require是运行时调用,所以require理论上可以运用在代码的任何地方
  • import是编译时调用,所以必须放在文件开头

本质:

  • require 是赋值过程。module.exports后面的内容是什么,require的结果就是什么,比如对象、数字、字符串、函数等,然后再把require的结果赋值给某个变量,它相当于module.exports的传送门
  • import 是解构过程,但是目前所有的引擎都还没有实现import,我们在node中使用babel支持ES6,也仅仅是将ES6转码为ES5再执行,import语法会被转码为require

import 虽然是 es6 中的语法,但就目前来说,所有的引擎都还没有实现import。

我们在 node 中使用 babel 支持ES6(在 node 当中,比如 node.js 代码,也不能直接使用 import 来导入,必须使用 babel 支持才能使用 import 语法),实际上也是将 ES6 转码为 ES5 再执行,import 语法实际上会被转码为 require。这也是为什么在模块导出时使 module.exports,在引入模块时使用 import 仍然起效,因为本质上,import 会被转码为 require 去执行。

vue响应式数据原理

在vue2中实现响应式原理的方式为Object. defineProperty+发布订阅模式

  1. 首先定义defineReactive方法,给对象属性都加上数据劫持
  2. 如果对象上有多个属性,创建一个Observer类来遍历该对象
  3. 如果obj内有嵌套的属性,可以使用递归来完成嵌套属性的数据劫持,即创建一个observe在此函数中完成Observer类的实例


创建一个Watcher类,在Watcher类中的get方法中返回模版表达式中所需的值,在这个求值的过程触发了劫持对象的getter,在劫持对象的getter中实现对自己属性的依赖的收集。
创建一个dep数组,在数据劫持的getter中将watcher push到这个数组中,在触发数据劫持的setter时遍历该数组,并调用watcher的update方法

简述vue虚拟dom

虚拟dom更新流程图

diff算法逻辑

diff 算法只会精细化比较两个相同的节点,且同时具备children

  1. 创建新前、新后、旧前、旧后四个指针,并获取对应的节点,并同时创建一个keyMap对象用来缓存所有老节点的key
  2. 创建一个循环,循环中断的条件是新前索引大于新后索引或者旧前索引大于旧后索引
  3. 首先不应该判断四种命中,而是略过已经加了undefined标记的项
  4. 旧前虚拟节点为null或者undefined,旧前指针下移
  5. 旧后虚拟节点为null或者undefined,旧后指针上移
  6. 新前虚拟节点为null或者undefined,新前指针下移
  7. 新后虚拟节点为null或者undefined,新后指针上移
  8. 命中新前与旧前,新前与旧前指针需要同时往下移(证明无变化,指针移动)
  9. 命中新后与旧后,新后与旧后指针需要同时往上移动(证明无变化,指针移动)
  10. 命中新后与旧前,需要将旧前指向的节点 插入到旧后之后,利用insertBefore进行节点操作
  11. 命中新前与旧后,需要将旧后指向的节点插入到旧前之前
  12. 四种命中都没有命中
  13. 先判断keyMap是否为null,如果是迭代所有的旧节点,以key为索引
  14. 寻找新前的key在keyMap中映射的序号,如果没有找到,说明这是一个新的节点,创建该节点并插入到旧前之前
  15. 寻找到对应的映射,说明不是新的节点,需要进行移动操作,此时重新启动一轮patch过程,结束后将节点中的该节点设置为undefined
  16. 判断结束后,需要将新前指针下移
  17. 循环结束后需要进行新前指针是否小于等于新后指针旧前指针是否小于等于旧后指针的判断
  18. 新的头小于新的尾,证明新节点中还有未处理完的节点,创建before变量,before的目的是为了判断是在旧前还是旧后插入,如果为null则是在旧后追加,如果不为null则在旧前增加
  19. 旧的头小于旧的尾,证明旧节点中还有未处理完的节点,循环遍历旧前和旧后之间的节点,并将他们移除

vue的生命周期

  1. beforeCreate( 创建前 ):在实例初始化之后,数据观测和事件配置之前被调用,此时组件的选项对象还未创建,el 和 data 并未初始化,因此无法访问methods, data, computed等上的方法和数据。
  2. created ( 创建后 ):实例已经创建完成之后被调用,在这一步,实例已完成以下配置:数据观测、属性和方法的运算,watch/event事件回调,完成了data 数据的初始化,el没有。 然而,挂在阶段还没有开始, $el属性目前不可见,这是一个常用的生命周期,因为你可以调用methods中的方法,改变data中的数据,并且修改可以通过vue的响应式绑定体现在页面上,获取computed中的计算属性等等,通常我们可以在这里对实例进行预处理,也有一些童鞋喜欢在这里发ajax请求,值得注意的是,这个周期中是没有什么方法来对实例化过程进行拦截的,因此假如有某些数据必须获取才允许进入页面的话,并不适合在这个方法发请求,建议在组件路由钩子beforeRouteEnter中完成
  3. beforeMount:挂在开始之前被调用,相关的render函数首次被调用(虚拟DOM),实例已完成以下的配置: 编译模板,把data里面的数据和模板生成html,完成了el和data 初始化,注意此时还没有挂在html到页面上
  4. mounted:挂在完成,也就是模板中的HTML渲染到HTML页面中,此时一般可以做一些ajax操作,mounted只会执行一次
  5. beforeUpdate:在数据更新之前被调用,发生在虚拟DOM重新渲染和打补丁之前,可以在该钩子中进一步地更改状态,不会触发附加地重渲染过程
  6. updated(更新后):在由于数据更改导致地虚拟DOM重新渲染和打补丁只会调用,调用时,组件DOM已经更新,所以可以执行依赖于DOM的操作,然后在大多是情况下,应该避免在此期间更改状态,因为这可能会导致更新无限循环,该钩子在服务器端渲染期间不被调用
  7. beforeDestroy(销毁前):在实例销毁之前调用,实例仍然完全可用,
    1. 这一步还可以用this来获取实例
    2. 一般在这一步做一些重置的操作,比如清除掉组件中的定时器 和 监听的dom事件
  8. destroyed(销毁后):在实例销毁之后调用,调用后,所以的事件监听器会被移出,所有的子实例也会被销毁,该钩子在服务器端渲染期间不被调用

vue指令了解多少?v-if和v-show的区别?两者的应用场景

vue3.0和2.0有什么区别,说下vue3.0的新特性

Vue 3 改动的地方
使用 Typescript
放弃 class 采用 function-based API
option API => Composition API
重构 complier
重构 virtual DOM
新的响应式机制

  1. 优化diff算法vue2.x中的虚拟dom是进行全量的对比,在运行时会对所有节点生成一个虚拟节点树,当页面数据发生变更时,会遍历判断virtual dom所有节点(包括一些不会变化的节点)有没有发生变化;虽然说diff算法确实减少了多DOM节点的直接操作,但是这个减少是有成本的,如果是复杂的大型项目,必然存在很复杂的父子关系的VNode,而Vue2.x的diff算法,会不断地递归调用 patchVNode,不断堆叠而成的几毫秒,最终就会造成 VNode 更新缓慢。在Vue3.0中,在这个模版编译时,编译器会在动态标签末尾加上 /* Text*/ PatchFlag。也就是在生成VNode的时候,同时打上标记,在这个基础上再进行核心的diff算法并且 PatchFlag 会标识动态的属性类型有哪些,比如这里 的TEXT 表示只有节点中的文字是动态的。而patchFlag的类型也很多。

    总结:Vue3.0对于不参与更新的元素,做静态标记并提示,只会被创建一次,在渲染时直接复用。其中还有cacheHandlers(事件侦听器缓存)
  2. 调整了数据的响应式原理 Object.defineProperty()的用法就是给一个对象定义一个属性(方法),并提供set和get两个内部实现,让我们可以获取或者设置这个属性(方法),vue2.x的响应式原理就是利用这个api对数据的读取和赋值进行监听和拦截。
    但是其中有个吐槽最多的地方就是:Vue2.x针对数组只实现了push,pop,shift,unshift,splice,sort,reverse 这七个方法的监听,以前通过数组下标改变值的时候,是不能触发视图更新的。
    但是并不是因为Object.defineProperty()监听不了数组下标,因为数组也是对象的一种,只不过每个key都是数字罢了。之所以出现以上的原因主要是因为:
  3. 数组的多变性。数组可以通过下标直接操控数组长度,比如Array.length = 0;瞬间就将数组清空,Array.length = 100,瞬间为数组填充了100个空元素。也就是说数组的key和value都可能发生变化。一旦数组的下标发生变化,就要重新对数组进行递归遍历,使用 Object.defineProperty 加上 setter 和 getter,这在性能上是非常不友好的。
  4. 数组的操作方法太多了。但是通常 push,pop,shift,unshift,splice,sort,reverse 这 7 种操作就能达到目的。因此,出于性能方面的考虑 Vue2.x 做出了一定的取舍。
  5. Vue3.0使用Proxy只要是对象就能代理直接监听数据的各种自定义行为,免去了Object.defineProperty需要遍历的操作。Vue3.0 响应式的优点,那么,除了以上说的能直接监听数组变化的优点以外,还有什么优点呢?
  6. 选择性监听,Vue3.0将响应式单独拆分出来,直接暴露给使用者使用,其中包括 ref、reactive
    setup(){
    const count = ref(0)
    const person = reactive({name:'小红'})
    }

    在Vue3.0中我们将通过ref和reactive生成响应式的数据,也就是说Vue将选择权交给了使用者。我们将有选择性的让需要的数据‘响应’。
    对比Vue2.x中,所有在模板中使用到的数据都需要在 data 中定义,组件实例在初始化的时候会将 data 整个对象变为可观察对象。这样的话,组件的实例化速度显而易见的会慢了一点,而且还需要开辟出部分运行内存,来保存这样多余的被‘响应化’的数据。
    所以Vue3.0的响应式不但「提高了组件实例初始化速度」,而且还「降低了运行内存的使用」

proxy能操作的陷进函数有哪些?

代理陷阱 被重写的行为 默认行为
get 读取一个属性的值 Reflect.get()
set 写入一个属性 Reflect.set()
has in 运算符 Reflect.has()
deleteProperty delete 运算符 Reflect.deleteProperty()
getPrototypeOf Object.getPrototypeOf() Reflect.getPrototypeOf()
setPrototypeOf Object.setPrototypeOf() Reflect.setPrototypeOf()
isExtensible Object.isExtensible() Reflect.isExtensible()
preventExtensions Object.preventExtensions() Reflect.preventExtensions()
getOwnPropertyDescriptor Object.getOwnPropertyDescriptor() Reflect.getOwnPropertyDescriptor
defineProperty Object.defineProperty() Reflect.defineProperty
ownKeys Object.keys,Object.getOwnPropertyNames()与Object.getOwnPropertySymbols() Reflect.ownKeys()
apply 调用一个函数 Reflect.apply()
construct 使用 new 调用一个函数 Reflect.construct()

vue和react的有什么相同点和不同点?

项目用的是vue-cli3.0吧?vue.config.js用过吗?拿来做什么?还配置过什么?

v-for中key的作用?

nextTick 什么时候用?

vue-router的原理?

Hash模式

vue-router 默认模式是 hash 模式 —— 使用 URL 的 hash 来模拟一个完整的 URL,当 URL 改变时,页面不会去重新加载。hash的改变会触发hashchange事件,通过监听hashchange事件来完成操作实现前端路由。hash值变化不会让浏览器向服务器请求。

hash(#)是URL 的锚点,代表的是网页中的一个位置,单单改变#后的部分(/#/..),浏览器只会加载相应位置的内容,不会重新加载网页,也就是说 #是用来指导浏览器动作的,对服务器端完全无用,HTTP请求中不包括#;同时每一次改变#后的部分,都会在浏览器的访问历史中增加一个记录,使用”后退”按钮,就可以回到上一个位置;所以说Hash模式通过锚点值的改变,根据不同的值,渲染指定DOM位置的不同数据

History模式

HTML5 History API提供了一种功能,能让开发人员在不刷新整个页面的情况下修改站点的URL,就是利用 history.pushState API 来完成 URL 跳转而无须重新加载页面;

由于hash模式会在url中自带#,如果不想要很丑的 hash,我们可以用路由的 history 模式,只需要在配置路由规则时,加入mode: 'history',这种模式充分利用history.pushState API 来完成 URL 跳转而无须重新加载页面。

简单实现一个发布订阅的功能

深拷贝、浅拷贝

XSS攻击

Cross-Site Scripting(跨站脚本攻击)简称 XSS,是一种代码注入攻击。攻击者通过在目标网站上注入恶意脚本,使之在用户的浏览器上运行。利用这些恶意脚本,攻击者可获取用户的敏感信息如 Cookie、SessionID 等,进而危害数据安全。

根据攻击的来源,XSS 攻击可分为存储型、反射型和 DOM 型三种。

存储型 XSS

存储型 XSS 的攻击步骤:

  1. 攻击者将恶意代码提交到目标网站的数据库中。
  2. 用户打开目标网站时,网站服务端将恶意代码从数据库取出,拼接在 HTML 中返回给浏览器。
  3. 用户浏览器接收到响应后解析执行,混在其中的恶意代码也被执行。
  4. 恶意代码窃取用户数据并发送到攻击者的网站,或者冒充用户的行为,调用目标网站接口执行攻击者指定的操作。

这种攻击常见于带有用户保存数据的网站功能,如论坛发帖、商品评论、用户私信等。

反射型 XSS

反射型 XSS 的攻击步骤:

  1. 攻击者构造出特殊的 URL,其中包含恶意代码。
  2. 用户打开带有恶意代码的 URL 时,网站服务端将恶意代码从 URL 中取出,拼接在 HTML 中返回给浏览器。
  3. 用户浏览器接收到响应后解析执行,混在其中的恶意代码也被执行。
  4. 恶意代码窃取用户数据并发送到攻击者的网站,或者冒充用户的行为,调用目标网站接口执行攻击者指定的操作。

反射型 XSS 跟存储型 XSS 的区别是:存储型 XSS 的恶意代码存在数据库里,反射型 XSS 的恶意代码存在 URL 里。

反射型 XSS 漏洞常见于通过 URL 传递参数的功能,如网站搜索、跳转等。

由于需要用户主动打开恶意的 URL 才能生效,攻击者往往会结合多种手段诱导用户点击。

POST 的内容也可以触发反射型 XSS,只不过其触发条件比较苛刻(需要构造表单提交页面,并引导用户点击),所以非常少见。

DOM 型 XSS

DOM 型 XSS 的攻击步骤:

  1. 攻击者构造出特殊的 URL,其中包含恶意代码。
  2. 用户打开带有恶意代码的 URL。
  3. 用户浏览器接收到响应后解析执行,前端 JavaScript 取出 URL 中的恶意代码并执行。
  4. 恶意代码窃取用户数据并发送到攻击者的网站,或者冒充用户的行为,调用目标网站接口执行攻击者指定的操作。

DOM 型 XSS 跟前两种 XSS 的区别:DOM 型 XSS 攻击中,取出和执行恶意代码由浏览器端完成,属于前端 JavaScript 自身的安全漏洞,而其他两种 XSS 都属于服务端的安全漏洞。

如何防范

输入测过滤

不可取对于用户输入的内容无法做出准确的判断

前端字符转译

例如将>之类的特殊字符转义成<字符,现有很多的模板框架都能实现特殊字符转义的工作

websocket

最后修改日期: 2024年 4月 1日

作者

留言

撰写回覆或留言

发布留言必须填写的电子邮件地址不会公开。