常见前端面试题

228 阅读26分钟

过于基础的问题就不记了,以下是分类记录,大部分来自网络,问题和答案都有待完善……

JavaScript

什么是闭包?应用?

在计算机科学中,闭包(英语:Closure),又称词法闭包(Lexical Closure)或函数闭包(function closures),是引用了自由变量的函数。这个被引用的自由变量将和这个函数一同存在,即使已经离开了创造它的环境也不例外。所以,有另一种说法认为闭包是由函数和与其相关的引用环境组合而成的实体。闭包在运行时可以有多个实例,不同的引用环境和相同的函数组合可以产生不同的实例。

词法作用域(lexical scoping)是指,函数在执行时,使用的是它被定义时的作用域,而不是这个函数被调用时的作用域。

存在自由变量的函数就是闭包。

image.png

MDN:一个函数和对其周围状态(lexical environment,词法环境)的引用捆绑在一起(或者说函数被引用包围),这样的组合就是闭包(closure)。

在函数式编程中,闭包的逻辑就是:『让程序运行环境来管理状态』。命令式语言围绕状态来建模,每次指令操作都在控制状态的变化,需要人为管理;而闭包是对行为的控制,将运行逻辑和上下文绑定,执行闭包的时候,逻辑和上下文是关联的,一个十分简单的例子:

const foo = () => {
  let v = 1
  return () => {
    return v++
  }
}
const bar = foo()
bar() // 1
bar() // 2
bar() // 3

应用:

模拟私有变量/模块模式

高阶函数的应用:防抖、缓存函数、Vue reactive 的应用

抽象:闭包是数据和行为的组合,这使得闭包具有较好抽象能力。对象是在数据中以方法的形式内含了过程,闭包是在过程中以环境的形式内含了数据。所谓“闭包是穷人的对象”、“对象是穷人的闭包”,就是说使用其中的一种方式,就能实现另一种方式能够实现的功能。

介绍原型、原型链?继承?

image.png

参考:一张图搞定JS原型&原型链

  • __proto__constructor 属性是对象所独有的;
  • prototype 属性是函数独有的;
  • js 中函数也是对象的一种,那么函数同样也有属性 __proto__constructor

  • prototype(显式原型):表示为构造函数的原型,如 Parent.prototype;
  • __proto__(隐式原型):实例(或对象)通过 __proto__ 属性找到构造函数的原型;
  • constructor:指示对象的构造器(构造函数)。constructor 存在于原型(xx.prototype)上,指向构造函数。于是 xx 构造出来的实例的 constructor 和 xx.prototype.constructor 相同,因为实例可以获取到原型上的属性。
Parent.prototype.__proto__ === Object.prototype; // true
Function.prototype.__proto__ === Object.prototype; // true

万物继承自 Object.prototype

我们可以调用很多我们没有定义的方法,这些方法是哪来的呢?现在引出原型链的概念,当我们调用 p1.toString() 的时候,先在 p1 对象本身寻找,没有找到则通过 p1.__proto__ 找到了原型对象 Parent.prototype,也没有找到,又通过 Parent.prototype.__proto__ 找到了上一层原型对象 Object.prototype。在这一层找到了 toString 方法。返回该方法供 p1 使用。

当然如果找到 Object.prototype 上也没找到,就在 Object.prototype.__proto__ 中寻找,但是 Object.prototype.__proto__ === null 所以就返回 undefined。这就是为什么当访问对象中一个不存在的属性时,返回 undefined 了。

JavaScript 实现继承,class 方式?

function Person(name, age) {
  this.name = name
  this.age = age
}
Person.prototype.getName = function () {
  return this.name
}
Person.prototype.getAge = function () {
  return this.age
}
function Student(name, age, grade) {
  // 构造函数继承
  Person.call(this, name, age)
  this.grade = grade
}
// 原型继承
Student.prototype = Object.create(Person.prototype, {
  // 不要忘了重新指定构造函数
  constructor: {
    value: Student
  },
  getGrade: {
    value: function () {
      return this.grade
    }
  }
})
var s1 = new Student('ming', 22, 5)
console.log(s1.getName()) // ming
console.log(s1.getAge()) // 22
console.log(s1.getGrade()) // 5

什么是跨域?跨域的解决方案?

参考:segmentfault.com/a/119000002…

跨域解决方案

image.png

  • JSONP
  • CORS
  • Nginx 反向代理
  • Node 正向代理

JSONP 的原理?

通过 script 标签可以跨域的原理,前端写好要执行的函数(回调函数),后端响应 script src 的请求之后返回回调函数的执行(js 字符串),形如:callback({})

关键要理解,后端在处理请求后,返回了一个函数执行,前端也写好了一个同名的函数,这样就获取到后端传来的数据。

promise 封装:

function jsonp({url, params, callback}) {
    return new Promise((resolve, reject) => {
        //创建script标签
        let script = document.createElement('script');
        //将回调函数挂在 window 上
        window[callback] = function(data) {
            resolve(data);
            //代码执行后,删除插入的script标签
            document.body.removeChild(script);
        }
        //回调函数加在请求地址上
        params = {...params, callback} //wb=b&callback=show
        let arrs = [];
        for(let key in params) {
            arrs.push(`${key}=${params[key]}`);
        }
        script.src = `${url}?${arrs.join('&')}`;
        document.body.appendChild(script);
    });
}

jsonp({
    url: 'http://localhost:3000/show',
    params: {
        //code
    },
    callback: 'show'
}).then(data => {
    console.log(data);
});

//express启动一个后台服务
let express = require('express');
let app = express();
app.get('/show', (req, res) => {
    let {callback} = req.query; //获取传来的callback函数名,callback是key
    res.send(`${callback}('Hello!')`);
});
app.listen(3000);

Event Loop:介绍浏览器的事件循环

JavaScript 有一个 main thread 主线程和 call-stack 调用栈(执行栈),所有的任务都会被放到调用栈等待主线程执行。JS 调用栈后进先出。JavaScript 单线程任务被分为同步任务和异步任务,同步任务会在调用栈中按照顺序等待主线程依次执行,异步任务会在异步任务有了结果后,将注册的回调函数放入任务队列中等待主线程空闲的时候(调用栈被清空),被读取到栈内等待主线程的执行。

Promise 是什么?实现原理?实现 promise?实现 promise.all ?

一种异步解决方案,解决了回调地狱的问题。

参考:

// promise.all
Promise.all = function (promises) {
    /** promises 是一个可迭代对象,省略对参数类型的判断 */
    return new Promise((resolve, reject) => {
        if (promises.length === 0) {
            //如果传入的参数是空的可迭代对象
            return resolve([]);
        } else {
            let result = [];
            let index = 0;
            let iterator = promises[Symbol.iterator]();
            function next() {
                try {
                    var { value, done } = iterator.next();
                    if(!done) {
                        Promise.resolve(value).then(data => {
                            result[index] = data;
                            index++;
                            next();
                        }, err => {
                            //某个promise失败
                            reject(err);
                            return;
                        });
                    }else {
                        //迭代完成
                        resolve(result);
                    }
                } catch (e) {
                    return reject(e);
                }
            }
            next(); //执行一次next
        }
    });
}
/** 仅考虑 promises 传入的是数组的情况时 */
Promise.all = function (promises) {
    return new Promise((resolve, reject) => {
        if (promises.length === 0) {
            resolve([]);
        } else {
            let result = [];
            let index = 0;
            for (let i = 0;  i < promises.length; i++ ) {
                //考虑到 i 可能是 thenable 对象也可能是普通值
                Promise.resolve(promises[i]).then(data => {
                    result[i] = data;
                    if (++index === promises.length) {
                        //所有的 promises 状态都是 fulfilled,promise.all返回的实例才变成 fulfilled 态
                        resolve(result);
                    }
                }, err => {
                    reject(err);
                    return;
                });
            }
        }
    });
}

async/await, generator, promise这三者的关联和区别是什么?

sleep 函数

function sleep(time) {
  return new Promise(resolve => setTimeout(resolve, time))
}

输出顺序题

async function async1() {
  console.log('async1 start');
  await async2();
  console.log('async1 end');
}
async function async2() {
  console.log('async2');
}
console.log('script start');
setTimeout(function () {
  console.log('setTimeout');
}, 0)
async1();
new Promise(function (resolve) {
  console.log('promise1');
  resolve();
  console.log('promise2')
}).then(function () {
  console.log('promise3');
});
console.log('script end');

// script start
// async1 start
// async 2
// promise1
// promise2
// script end
// async1 end
// promise3
// setTimeout

容易错的一个位置:await 让它之后的代码变为异步微任务

实现一个带并发限制的异步调度器

class Scheduler {
  constructor(limit) {
    this.limit = limit
    this.num = 0
    this.list = []
  }

  add(promiseCreator) {
    return new Promise((resolve, reject) => {
      promiseCreator.resolve = resolve
      promiseCreator.reject = reject
      this.list.push(promiseCreator)
      this.executor()
    })
  }

  executor() {
    if (this.num < this.limit && this.list.length > 0) {
      this.num += 1
      const promise = this.list.shift()
      promise().then(res => {
        promise.resolve(res)
      }).catch(err => {
        promise.reject(err)
      }).finally(() => {
        this.num -= 1
        this.executor()
      })
    }
    
  }
}

const timeout = (time) => new Promise(resolve => {
  setTimeout(resolve, time)
})

const scheduler = new Scheduler(2)
const addTask = (time, order) => {
  scheduler.add(() => timeout(time)).then(() => console.log(order))
}

addTask(1000, '1')
addTask(500, '2')
addTask(300, '3')
addTask(400, '4')

// 2 3 1 4

ES6 模块和 CommonJS 模块有什么区别?

CommonJS:输出值的复制,运行时加载 ES6:输出值的引用,编译时输出接口

实现防抖和节流?

// 防抖
window.onload = function () {
  input.addEventListener(
    'input',
    debounce(function () {
      console.log(123)
    })
  )
}

function debounce(fn, delay = 300) {
  let timer = null
  return function () {
    let context = this;
    let args = arguments;
    clearTimeout(timer)
    timer = setTimeout(() => {
      fn.apply(context, args)
    }, delay)
  }
}

// 节流
window.onload = function () {
  window.onresize = throttle(function () {
    console.log(123)
  }, 1000)
}

function throttle(fn, delay = 300) {
  let time = 0
  return function () {
    let context = this;
    let args = arguments;
    if (Date.now() - time > delay) {
      time = Date.now()
      fn.apply(context, args)
    }
  }
}

缓存函数

function memoize(fn) {
  if (typeof fn !== 'function') {
    throw new TypeError('Expected a function')
  }
  const memoized = function(...args) {
    const key = args[0]
    const cache = memoized.cache
    if (cache.has(key)) {
      return cache.get(key)
    }
    const res = fn.apply(this, args)
    memoized.cache = cache.set(key, res)
    return res
  }
  memoized.cache = new Map()
  return memoized
}
// 平方计算
var square = (function () {
    var cache = {};
    return function(n) {
        if (!cache[n]) {
            cache[n] = n * n;
        }
        return cache[n];
    }
})();

函数 curry

function curry(fn) {
    let judge = (...args) => {
        if (args.length == fn.length) return fn(...args)
        return (...arg) => judge(...args, ...arg)
    }
    return judge
}

参考:死磕 36 个 JS 手写题(搞懂后,提升真的大)

深拷贝

考虑正则、日期:

function cloneDeep(obj) {
  if(obj === null) return null;
  if (typeof obj !== 'object') return obj;
  if (obj instanceof Date) return new Date(obj);
  if (obj instanceof RegExp) return new RegExp(obj);
  // 获取obj的构造函数并实例化一个新的
  const cloneObj = new obj.constructor();
  Object.keys(obj).forEach(key => {
      // 递归拷贝属性
      cloneObj[key] = cloneDeep(obj[key]);
  });
  return cloneObj;
}

考虑循环引用:

function deepClone(obj, cache = new WeakMap()) {
  if (obj === null || typeof obj !== 'object') return obj
  if (obj instanceof Date) return new Date(obj)
  if (obj instanceof RegExp) return new RegExp(obj)
  
  if (cache.get(obj)) return cache.get(obj) // 如果出现循环引用,则返回缓存的对象,防止递归进入死循环
  let cloneObj = new obj.constructor() // 使用对象所属的构造函数创建一个新对象
  cache.set(obj, cloneObj) // 缓存对象,用于循环引用的情况

  for (let key in obj) {
    if (obj.hasOwnProperty(key)) {
      cloneObj[key] = deepClone(obj[key], cache) // 递归拷贝
    }
  }
  return cloneObj
}

// 测试
const obj = { name: 'Jack', address: { x: 100, y: 200 } }
obj.a = obj // 循环引用
const newObj = deepClone(obj)
console.log(newObj.address === obj.address) // false

获取 URL 参数?

function getURLParameters(url) {
  return url
    .match(/([^?=&]+)(=([^&]*))/g)
    .reduce(
      (a, v) => (
        (a[v.slice(0, v.indexOf("="))] = v.slice(v.indexOf("=") + 1)), a
      ),
      {}
    );
}

事件代理:点击列表获取对应索引

window.onload = function () {
  let list = document.getElementById('test')
  let items = list.getElementsByTagName('li')
  list.addEventListener('click', function (event) {
    if (event.target.nodeName === 'LI') {
      console.log([...items].indexOf(event.target))
    }
  })
}

字符串解析方法

var str = '您好,<%=name%>。欢迎来到<%=location%>';
// 填充代码实现template方法
function template(str) {
  return data => str.replace(/<%=(\w+)%>/g, (match, p) => data[p] || '')
}
var compiled = template(str);
console.log(compiled({name: '张三', location: '网易游戏'}))

正则校验

网址(URL):[a-zA-z]+://[^\s]*
电子邮件(Email):\w+([-+.]\w+)*@\w+([-.]\w+)*\.\w+([-.]\w+)*
QQ号码:[1-9]\d{4,}
HTML标记(包含内容或自闭合):<(.*)(.*)>.*<\/\1>|<(.*) \/>
日期(年-月-日):(\d{4}|\d{2})-((1[0-2])|(0?[1-9]))-(([12][0-9])|(3[01])|(0?[1-9]))
中国大陆手机号码:1\d{10}

for in、hasOwnProperty、for of、 in、Object.keys()

  • for in:以任意顺序遍历一个对象的除Symbol以外的可枚举属性,包括继承的可枚举属性
  • hasOwnProperty():方法会返回一个布尔值,指示对象自身属性中是否具有指定的属性(也就是,是否有指定的键)。
  • in:如果指定的属性在指定的对象或其原型链中,则in 运算符返回true
  • for...of语句在可迭代对象(包括 ArrayMapSetStringTypedArrayarguments 对象等等)上创建一个迭代循环,调用自定义迭代钩子,并为每个不同属性的值执行语句
  • Object.keys()  方法会返回一个由一个给定对象的自身可枚举属性组成的数组,数组中属性名的排列顺序和正常循环遍历该对象时返回的顺序一致 。

apply、call、bind 实现:

apply:

// apply 函数实现
Function.prototype.myApply = function (context) {
  // 判断调用对象是否为函数
  if (typeof this !== "function") {
    throw new TypeError("Error");
  }
  // 判断 context 是否存在,如果未传入则为 window
  context = context || window;
  // 获取参数
  let args = arguments[1] || [];
  // 将函数设为对象的方法
  context.fn = this;
  // 调用方法
  let result = context.fn(...args);
  // 将属性删除
  delete context.fn;
  return result;
};

call:

// call 函数实现
Function.prototype.myCall = function (context) {
  // 判断调用对象
  if (typeof this !== "function") {
    console.error("type error");
  }
  // 获取参数
  let args = [...arguments].slice(1);
  // 判断 context 是否传入,如果未传入则设置为 window
  context = context || window;
  // 将调用函数设为对象的方法
  context.fn = this;
  // 调用函数
  let result = context.fn(...args);
  // 将属性删除
  delete context.fn;
  return result;
};

bind:

Function.prototype.bind = function( context ){ 
    var self = this; // 保存原函数
    // 返回一个新的函数
    return function(){ 
        // 执行新的函数的时候,会把之前传入的 context // 当作新函数体内的 this
        return self.apply( context, arguments );
    }
};

图片懒加载

let imgList = [...document.querySelectorAll('img')]
let length = imgList.length

// 修正错误,需要加上自执行
- const imgLazyLoad = function() {
+ const imgLazyLoad = (function() {
    let count = 0
    
   return function() {
        let deleteIndexList = []
        imgList.forEach((img, index) => {
            let rect = img.getBoundingClientRect()
            if (rect.top < window.innerHeight) {
                img.src = img.dataset.src
                deleteIndexList.push(index)
                count++
                if (count === length) {
                    document.removeEventListener('scroll', imgLazyLoad)
                }
            }
        })
        imgList = imgList.filter((img, index) => !deleteIndexList.includes(index))
   }
- }
+ })()

// 这里最好加上防抖处理
document.addEventListener('scroll', imgLazyLoad)

参考:死磕 36 个 JS 手写题(搞懂后,提升真的大)

虚拟列表

虚拟列表的含义、为什么需要虚拟列表?

虚拟列表的实现:

参考:zhuanlan.zhihu.com/p/34585166

理解:

  • 一种针对长列表的优化方案
  • 列表中的数据量很大,一次性加载
  • 一个可视区域,一个包含所有元素的列表高度区域
  • 一个 startIndex、一个 endIndex,可是数据为 data.slice(startIndex, endIndex)
  • 设置内容区 style="transform: translate3d(0px, 150px, 0px);",让可见数据显示在可视区域
  • 滚动事件

CSS

BFC是什么?有什么作用?

块级格式化上下文, 一旦形成一个块级格式化上下文,这个上下文中的元素的布局不会影响外部元素的布局。

简单来说,BFC 实际上是一块区域,在这块区域中遵循一定的规则,有一套独特的渲染规则。

文档流其实分为普通流定位流浮动流三种,普通流其实就是指 BFC 中的 FC,也即格式化上下文

普通流:元素按照其在 HTML 中的先后位置从上到下、从左到右布局,在这个过程中,行内元素水平排列,直到当行被占满然后换行,块级元素则会被渲染为完整的一个新行。

格式化上下文:页面中的一块渲染区域,有一套渲染规则,决定了其子元素如何布局,以及和其他元素之间的关系和作用

BFC 的触发条件:

  • 根元素
  • float:不为 none
  • display:inline-block、table-cell、table-caption、flow-root、flex、grid
  • position: fixed/absolute
  • overflow:不为 visible

BFC 的规则:

  • BFC 区域内的元素 margin 会发生重叠。
  • BFC 的区域不会与 float box 重叠。
  • BFC 就是页面上的一个隔离的独立容器,内部的元素不会影响到外部,同样外部的元素也不会影响到内部。
  • 计算 BFC 的高度时,浮动元素也参与计算。

应用:

  • 阻止 margin 重叠
  • 清除内部浮动
  • 自适应两栏布局

参考:前端面经 - 看这篇就够了(帮你拿到大厂offer)

flex:1 2 300px?

flex 缩写的意义

基本概念:

  1. 容器&项目:采用 Flex 布局的元素,称为 Flex 容器(flex container),简称"容器"。它的所有子元素自动成为容器成员,称为 Flex 项目(flex item),简称"项目"。
  2. 主轴&交叉轴:堆叠的方向,默认主轴是水平方向,交叉轴是垂直方向。可通过flex-derection: column设置主轴为垂直方向。

容器属性:

  • display: flex
  • flex-direction:主轴的方向(即项目的排列方向),row | row-reverse | column | column-reverse;
  • flex-wrap:是否换行,nowrap | wrap | wrap-reverse;
  • flex-flow:direction 和 wrap简写
  • justify-content:主轴对齐方式,flex-start | flex-end | center | space-between | space-around;
  • align-items:交叉轴对齐方式,flex-start | flex-end | center | baseline | stretch;
  • align-content: 多根轴线的对齐方式。如果项目只有一根轴线,该属性不起作用。flex-start | flex-end | center | space-between | space-around | stretch;

项目的属性:

  • order:项目的排列顺序,数值越小,排列越靠前,默认为0。
  • flex-grow:放大比例,默认为0,指定元素分到的剩余空间的比例。
  • flex-shrink:缩小比例,默认为1,指定元素分到的缩减空间的比例。
  • flex-basis:分配多余空间之前,项目占据的主轴空间,默认值为auto
  • flex:grow, shrink 和 basis的简写,默认值为0 1 auto
  • align-self:单个项目不一样的对齐方式,默认值为auto,auto | flex-start | flex-end | center | baseline | stretch;

参考:前端面经 - 看这篇就够了(帮你拿到大厂offer)

水平垂直居中?

左定宽、右不定宽布局?

left { width: 200px }
right { flex: 1 }

布局:左右定宽、中间自适应

1. float:

<style>
.left{
    float: left;
    width: 300px;
    height: 100px;
    background: #631D9F;
}
.right{
    float: right;
    width: 300px;
    height: 100px;
    background: red;
}
.center{
    margin-left: 300px;
    margin-right: 300px;
    background-color: #4990E2;
}
</style>
<body>
    <div class="left"></div>
    <div class="right"></div>
    <div class="center"></div>  
    <!-- center如果在中间,则right会被中间block的元素挤到下一行 -->
</body>

2. position:

<style>
.left{
    position: absolute;
    left: 0;
    width: 300px;
    background-color: red;
}
.center{
    position: absolute;
    left: 300px;
    right: 300px;
    background-color: blue;
}
.right{
    position: absolute;
    right: 0;
    width: 300px;
    background-color: #3A2CAC;
}
</style>
<body>
    <div class="left"></div>
    <div class="center"></div>  
    <div class="right"></div>
</body>

3. flex:

<style>
.main {
    display: flex;
}
.left{
    width: 300px;
}
.center{
    flex-grow: 1;
    flex-shrink: 1;
}
.right{
    width: 300px;
}
</style>
<body class="main">
    <div class="left"></div>
    <div class="center"></div>  
    <div class="right"></div>
</body>

参考:前端面经 - 看这篇就够了(帮你拿到大厂offer)

清除浮动

由于浮动元素脱离了文档流,所以父元素的高度无法被撑开,影响了与父元素同级的元素。

清除浮动的方法:

  1. 使用clear: both清除浮动

浮动元素后面放一个空的div标签,div设置样式clear:both来清除浮动。它的优点是简单,方便兼容性好,但是一般情况下不建议使用该方法,因为会造成结构混乱,不利于后期维护。

  1. 利用伪元素after来清除浮动

父元素添加了:after伪元素,通过清除伪元素的浮动,达到撑起父元素高度的目的

.clearfix:after{
    content: "";
    display: block;
    visibility: hidden;
    clear: both;
}
  1. 使用CSS的overflow属性

当给父元素设置了overflow样式,不管是overflow:hidden或overflow:auto都可以清除浮动只要它的值不为visible就可以了,它的本质就是建构了一个BFC,这样使得达到撑起父元素高度的效果

移动端适配?


Vue

Vue 的响应式原理?

Vue3:

Vue 遍历 data 的属性,使用 Object.defineProperty 把这些 property 全部转为 getter/setter。

先是将数据转换成响应式数据,通过 proxy 拦截对象的 get 和 set,再加上发布订阅模式追踪触发依赖,get 时收集依赖,set 时触发依赖;观察副作用,将副作用函数和相关的响应式数据联系起来,副作用函数是相关响应式数据的依赖,首次执行一次,以及读取的时候进行依赖收集,以后每次数据变化set,都会触发依赖。

Vue 1.0 实现响应式的核心方法是 Object.defineProperty,在 Vue 初始化时,进行拦截操作,分为对象和数组:

对象:考虑属性为对象的递归处理。在 mount 挂载时

  • 有一个对模板变量读取赋值的操作,为副作用函数 cb
  • 同时创建 watcher 实例,传入 cb 函数
  • 先将 Dep.target 赋值为当前 watcher 实例
  • cb 函数执行
  • 触发属性的读取操作 get(因为第一步 cb 函数中有对属性的读取操作)
  • 响应式:get: dep.depend(), set: dep.notify()
  • 全局有 Dep.target,执行 dep.depend() 进行依赖收集
  • 一个属性对应一个 dep 实例
  • 当属性写操作 set 被触发时,dep.notify() 进行依赖通知更新

数组:人为区分,性能考虑,数组数据量一般较大。只能通过 7 个方法操作。为什么是那 7 个可变异的方法?因为如果产生新的数组,新的数组要响应式需要 set 方法设置,和对象一样,不在这里的考虑范围。

做了 2 个操作:

  • 覆写增强数组的原型对象,改变 7 个变异方法,增加响应式处理。
  • observeArray: 遍历数组项,增加响应式(数组项为对象)。
observe(vm._data)
// observe.js
export default function observe(value) {
  // observe 后续还会递归调用
  // 避免无限递归,当 value 不是对象直接结束递归
  if (typeof value !== 'object') return
  // value.__ob__ 是 Observer 实例
  // 如果 value.__ob__ 属性已经存在,说明 value 对象已经具备响应式能力,直接返回已有的响应式对象
  if (value.__ob__) return value.__ob__
  // 返回 Observer 实例
  return new Observer(value)
}
// observer.js
export default function Observer(value) {
  // 为对象本身设置一个 dep,方便在更新对象本身时使用,比如 数组通知依赖更新时就会用到
  this.dep = new Dep()  
  // 为对象设置 __ob__ 属性,值为 this,标识当前对象已经是一个响应式对象了
  Object.defineProperty(value, '__ob__', {
    value: this,
    // 设置为 false,禁止被枚举
    // 1、可以在递归设置数据响应式的时候跳过 __ob__ 
    // 2、将响应式对象字符串化时也不限显示 __ob__ 对象
    enumerable: false,
    writable: true,
    configurable: true
  })

  if (Array.isArray(value)) {
    // 数组响应式
    protoArgument(value)
    this.observeArray(value)
  } else {
    // 对象响应式
    this.walk(value)
  }
}

/**
 * 遍历对象的每个属性,为这些属性设置 getter、setter 拦截
 */
Observer.prototype.walk = function (obj) {
  for (let key in obj) {
    defineReactive(obj, key, obj[key])
  }
}

// 遍历数组的每个元素,为每个元素设置响应式
// 其实这里是为了处理元素为对象的情况,以达到 this.arr[idx].xx 是响应式的目的
Observer.prototype.observeArray = function (arr) {
  for (let item of arr) {
    observe(item)
  }
}
// defineReactive.js
export default function defineReactive(obj, key, val) {
  // 递归调用 observe,处理 val 仍然为对象的情况
  const childOb = observe(val)
  const dep = new Dep()
  Object.defineProperty(obj, key, {
    // 当发现 obj.key 的读取行为时,会被 get 拦截
    get() {
      // 读取数据时 && Dep.target 不为 null,则进行依赖收集
      if (Dep.target) {
        dep.depend()
        // 如果存在子 ob,则顺道一块儿完成依赖收集
        if (childOb) {
          childOb.dep.depend()
        }
      }
      console.log(`getter: key = ${key}`)
      return val
    },
    // 当发生 obj.key = xx 的赋值行为时,会被 set 拦截
    set(newV) {
      console.log(`setter: ${key} = ${newV}`)
      if (newV === val) return
      val = newV
      // 对新值进行响应式处理,这里针对的是新值为非原始值的情况,比如 val 为对象、数组
      observe(val)
      // 数据更新,让 dep 通知自己收集的所有 watcher 执行 update 方法
      dep.notify()
    }
  })
}
// dep.js
export default function Dep() {
  // 存储当前 dep 实例收集的所有 watcher
  this.watchers = []
}

// Dep.target 是一个静态属性,值为 null 或者 watcher 实例
// 在实例化 Watcher 时进行赋值,待依赖收集完成后在 Watcher 中又重新赋值为 null
Dep.target = null

/**
 * 收集 watcher
 * 在发生读取操作时(vm.xx) && 并且 Dep.target 不为 null 时进行依赖收集
 */
Dep.prototype.depend = function () {
  // 防止 Watcher 实例被重复收集
  if (this.watchers.includes(Dep.target)) return
  // 收集 Watcher 实例
  this.watchers.push(Dep.target)
}

/**
 * dep 通知自己收集的所有 watcher 执行更新函数
 */
Dep.prototype.notify = function () {
  for (let watcher of this.watchers) {
    watcher.update()
  }
}
// watcher.js
/**
 * @param {*} cb 回调函数,负责更新 DOM 的回调函数
 */
export default function Watcher(cb) {
  console.log('cb: ', cb);
  // 备份 cb 函数
  this._cb = cb
  // 赋值 Dep.target
  Dep.target = this
  // 执行 cb 函数,cb 函数中会发生 vm.xx 的属性读取,进行依赖收集
  cb()
  // 依赖收集完成,Dep.target 重新赋值为 null,防止重复收集
  Dep.target = null
}

/**
 * 响应式数据更新时,dep 通知 watcher 执行 update 方法,
 * 让 update 方法执行 this._cb 函数更新 DOM
 */
Watcher.prototype.update = function () {
  this._cb()
}

mount 时执行

// compileTextNode.js
/**
 * 编译文本节点
 * @param {*} node 节点
 * @param {*} vm Vue 实例
 */
 export default function compileTextNode(node, vm) {
  // <span>{{ key }}</span>
  const key = RegExp.$1.trim()
  // 当响应式数据 key 更新时,dep 通知 watcher 执行 update 函数,cb 会被调用
  function cb() {
    node.textContent = JSON.stringify(vm[key])
  }
  // 实例化 Watcher,执行 cb,触发 getter,进行依赖收集
  new Watcher(cb)
}

Vue 对数组的监听?

Vue 不能检测以下数组的变动:

当你利用索引直接设置一个数组项时,例如:vm.items[indexOfItem] = newValue 当你修改数组的长度时,例如:vm.items.length = newLength 应使用以下两种方法:

Vue.set(vm.items, indexOfItem, newValue);
vm.items.splice(indexOfItem, 1, newValue);修改length:vm.items.splice(newLength);

Vue 对数组的监听:

用数组的原型创造一个新的对象 arrayMethods,将数组的方法重写在对象上,这些方法执行原来的原型方法,另外加入拦截操作、notify 依赖。

Vue 在对数据监听时,数据如果是数组,如果支持__proto__,target.proto 指向改写的对象 arrayMethods,如果不支持,定义对象,然后对数组中的每项添加监听器。

const arrayProto = Array.prototype
export const arrayMethods = Object.create(arrayProto)
const methodsToPatch = [ 'push', 'pop', 'shift', 'unshift', 'splice', 'sort', 'reverse' ]
def(arrayMethods, method, function mutator (...args) {
const result = original.apply(this, args)
if (inserted) ob.observeArray(inserted)
ob.dep.notify()

if (Array.isArray(value)) {
if (hasProto) { protoAugment(value, arrayMethods) } 
else { copyAugment(value, arrayMethods, arrayKeys) }
this.observeArray(value)

function protoAugment (target, src: Object) 
target.__proto__ = src
function copyAugment (target: Object, src: Object, keys: Array<string>) {
for (let i = 0, l = keys.length; i < l; i++) {
const key = keys[i]
def(target, key, src[key])

Vue 对数组的处理,并不是因为JavaScript 的限制或者 Object.defineProperty 的原因(数组也是对象),而是性能的考虑。如修改一个索引可能导致整个数组的遍历更新。

数组在 JS 中常被当作栈,队列,集合等数据结构的实现方式,会储存批量的数据以待遍历。编译器对对象与数组的优化也有所不同。所以对数组的处理需要特化出来以提高性能。用户要修改数组只能使用这些方法,否则不会是响应式的。

filter() 、concat() 、slice(),它们不会改变原始数组,而是返回一个新的数组。当使用这些非变异方法时,可以使用新数组去替换原来的数组。

虚拟 DOM 是什么?

diff 算法?

参考:详解vue的diff算法

vue3.0 diff算法思想

  • 编译模版时进行静态分析标记动态节点,diff对比差异时仅对比动态节点(性能提升明显);
  • diff算法先去头去尾,借此缩短遍历对比数组长度(对数组插入和删除操作性能优化明显);
  • 通过对更新前后子节点数组建立映射表的方式,将O(n^2)复杂度的遍历降低到O(n);
  • 通过最长递增子序列方法了来diff前后的子节点数组,减少移动操作的次数;

参考:最长递增子序列 - 动态规划方法及打印

Vue 的生命周期、包含父子组件?

  • beforeCreate:init events & lifecycle。执行时组件实例还未创建,通常用于插件开发中执行一些初始化任务
  • created:组件初始化完毕,各种数据可以使用,常用于异步数据获取
  • beforeMount:组件挂载之前。相关的 render 函数首次被调用。在此阶段可获取到vm.el;此阶段vm.el 虽已完成 DOM 初始化,但并未挂载在 el 选项上。
  • mounted:组件挂载到实例上去之后。vm.el 已完成 DOM 的挂载与渲染,此刻打印 vm.$el,发现之前的挂载点及内容已被替换成新的 DOM。可用于获取访问数据和 DOM 元素
  • beforeUpdate:组件数据发生变化,更新之前。此时 view 层还未更新。
  • updated:数据数据更新之后。完成 view 层的更新。
  • beforeDestroy:组件实例销毁之前。实例被销毁前调用,此时实例属性与方法仍可访问。可用于一些定时器或订阅的取消
  • destroyed:组件实例销毁之后。
  • activated:keep-alive 缓存的组件激活时
  • deactivated:keep-alive 缓存的组件停用时调用
  • errorCaptured:捕获一个来自子孙组件的错误时被调用

Vue 的父组件和子组件生命周期钩子函数执行顺序可以归类为以下 4 部分:

  • 加载渲染过程:父 beforeCreate -> 父 created -> 父 beforeMount -> 子 beforeCreate -> 子 created -> 子 beforeMount -> 子 mounted -> 父 mounted
  • 更新过程:父 beforeUpdate -> 子 beforeUpdate -> 子 updated -> 父 updated
  • 销毁过程:父 beforeDestroy -> 子 beforeDestroy -> 子 destroyed -> 父 destroyed

computed 和 watch 的区别?

组件间通信的方式?

  • props、$emit
  • $attrs$listeners(Vue3 已废除)
  • $parent$children
  • ref
  • provide/inject
  • eventBus
  • vuex

nextTick 的原理?

Vue 在更新 DOM 时是异步执行的。只要侦听到数据变化,Vue 将开启一个队列,并缓冲在同一事件循环中发生的所有数据变更。如果同一个侦听器被多次触发,它只会被推入到队列中一次。这种在缓冲时去除重复数据对于避免不必要的计算和 DOM 操作是非常重要的。然后,在下一个的事件循环“tick”中,Vue 刷新队列并执行实际 (已去重的) 工作。Vue 在内部对异步队列尝试使用原生的 Promise.then、MutationObserver 和 setImmediate,如果执行环境不支持,则会采用 setTimeout(fn, 0) 代替。

例如,当你设置 vm.someData = 'new value',该组件不会立即重新渲染。当刷新队列时,组件会在下一个事件循环“tick”中更新。多数情况我们不需要关心这个过程,但是如果你想基于更新后的 DOM 状态来做点什么,这就可能会有些棘手。虽然 Vue.js 通常鼓励开发人员使用“数据驱动”的方式思考,避免直接操作 DOM,但是有时我们必须要这么做。为了在数据变化之后等待 Vue 完成更新 DOM,可以在数据变化之后立即使用 Vue.nextTick(callback)。

Vuex 的工作流程?

commit mutation dispatch action

State:

在 Vue 组件中获取 state

const Counter = {
  template: `<div>{{ count }}</div>`,
  computed: {
    count () {
      return this.$store.state.count
    }
  }
}

mapState:

computed: {
  localComputed () { /* ... */ },
  // 使用对象展开运算符将此对象混入到外部对象中
  ...mapState({
    // ...
    count: state => state.count,
  })
}

Getter:

getters: {
  doneTodos: (state) => {
    return state.todos.filter(todo => todo.done)
  }
}

Mutation:

更改 Vuex 的 store 中的状态的唯一方法是提交 mutation。mutations 相当于一个事件集合,放置着所有对 state 的操作,所有对 state 的操作都需要显式的调用 mutation:

mutations: {
  increment (state, payload) {
    state.count += payload.amount
  }
}

store.commit('increment', {
  amount: 10
})

mutation 必须是同步函数,目的是为了状态变化可追踪。

Action:

Action 类似于 mutation,不同在于:

Action 提交的是 mutation,而不是直接变更状态。

Action 可以包含任意异步操作。

actions: {
  increment (context) {
    context.commit('increment')
  }
}

actions: {
  increment ({ commit }) {
    commit('increment')
  }
}

store.dispatch('incrementAsync', {
  amount: 10
})

Vue Router 的实现原理?

Vue Router 完整的导航解析流程:

  • 导航被触发。
  • 在失活的组件里调用 beforeRouteLeave 守卫。
  • 调用全局的 beforeEach 守卫。
  • 在重用的组件里调用 beforeRouteUpdate 守卫(2.2+)。
  • 在路由配置里调用 beforeEnter。
  • 解析异步路由组件。
  • 在被激活的组件里调用 beforeRouteEnter。
  • 调用全局的 beforeResolve 守卫(2.5+)。
  • 导航被确认。
  • 调用全局的 afterEach 钩子。
  • 触发 DOM 更新。
  • 调用 beforeRouteEnter 守卫中传给 next 的回调函数,创建好的组件实例会作为回调函数的参数传入。

登录鉴权、路由守卫?

Vue3 有哪些变化?

对 Vue 项目进行过哪些优化?

编码阶段:

  • 尽量减少 data 中的数据,data 中的数据都会增加 getter 和 setter,会收集对应的 watcher
  • v-for 遍历必须为 item 添加 key,且避免同时使用 v-if
  • 如果需要使用 v-for 给每项元素绑定事件时使用事件代理
  • v-if 和 v-show 区分使用场景
  • 使用 keep-alive 缓存组件
  • 事件、定时器的销毁
  • 使用路由懒加载、异步组件
  • 第三方模块按需导入
  • 长列表性能优化
  • 图片懒加载
  • 防抖、节流

SEO优化:

  • 预渲染
  • 服务端渲染SSR

打包优化:

  • 压缩代码、图片
  • Tree Shaking/Scope Hoisting
  • splitChunks 抽离公共文件
  • 多线程打包 happypack
  • sourceMap 优化
  • 构建结果输出分析

基础的 Web 技术的优化:

  • 开启 gzip 压缩
  • 浏览器缓存
  • CDN 的使用
  • 使用 Chrome Performance 查找性能瓶颈

用户体验:

  • 骨架屏
  • PWA

针对 Vue-Cli 项目的优化?

  • 静态资源优化,如图片压缩
  • 图片懒加载
  • 路由懒加载
  • 打包体积优化,Webpack 分片调整
  • 服务端渲染

首屏优化?

Webpack、Vite

Webpack 的工作原理?

Webpack 的功能?

  • 代码转换
  • 文件优化
  • 代码分割
  • 模块合并
  • 自动刷新
  • 代码校验
  • 自动发布

loader 和 plugin 的区别?

Webpack 优化?

Vite 的原理

Webpack 等构建工具会提前把模块打包成浏览器可读取的 js bundle 文件, Vite 别出心裁的利用了浏览器原生的 ES Module 支持。本地启动服务器,当浏览器读取 HTML 文件,执行 import 模块时,向服务器发送读取模块的请求,Vite 内部解析成浏览器可执行的 js 文件返回浏览器端。保证了只有需要使用这个模块的时候,浏览器才会请求并解析这个模块,最大程度做到了按需加载

依赖预编译,其实是 Vite 2.0 在为用户启动开发服务器之前,先用 esbuild 把检测到的依赖预先构建了一遍。

参考:浅谈 Vite 2.0 原理,依赖预编译,插件机制是如何兼容 Rollup 的?

小程序

微信小程序生命周期?

  • 应用生命周期:onLaunch、onShow、onHide、onError、onPageNotFound
  • 页面生命周期:onLoad、onShow、onReady、onHide、onUnload、onPullDownRefresh、onReachBottom、onShareAppMessage、onPageScroll、onResize、onTabItemTap

小程序开发遇到的难点?

TypeScript

type 和 interface 的区别?

interface 和 type 很像,很多场景,两者都能使用。但也有细微的差别:

  • 类型:对象、函数两者都适用,但是 type 可以用于基础类型、联合类型、元祖。
  • 同名合并:interface 支持,type 不支持。
  • 计算属性:type 支持, interface 不支持。

总的来说,公共的用 interface 实现,不能用 interface 实现的再用 type 实现。是一对互帮互助的好兄弟。

js 和 TS typeof 的区别?

使用对象的 key 生成联合类型?

声明文件?

工程相关

前端性能优化?

参考:zhuanlan.zhihu.com/p/403169124

浏览器相关

从输入网址到页面打开发生了什么?

  • DNS 域名解析实际 IP 地址。
    • 缓存查找:浏览器 -> 系统 -> 路由器 -> ISP;如果在某一个缓存中找到的话,就直接跳到下一步。
    • 如果都没有找到的话,就会向 ISP 或者公共的域名解析服务发起 DNS 查找请求。这个查找的过程还是一个递归查询的过程。
  • 检查浏览器是否有缓存。
    • 通过Cache-ControlExpires来检查是否命中强缓存,命中则直接取本地磁盘的html(状态码为200 from disk(or memory) cache,内存or磁盘);
    • 如果没有命中强缓存,则会向服务器发起请求(先进行下一步的TCP连接),服务器通过EtagLast-Modify来与服务器确认返回的响应是否被更改(协商缓存),若无更改则返回状态码(304 Not Modified),浏览器取本地缓存;
    • 若强缓存和协商缓存都没有命中则返回请求结果。
  • 与 WEB 服务器建立 TCP 连接。

TCP 协议通过三次握手建立连接:

  1. 客户端:你能接收到我的消息吗?
  2. 服务端:可以的,那你能接收到我的回复吗?
  3. 客户端:可以,那我们开始聊正事吧。
  • 若协议是https则会做加密

  • 浏览器发送请求获取页面 HTML

浏览器向 WEB 服务器的 ip 地址发送相应的 http get 请求页面html。

通常的请求行是: 请求的方式(getpost) + 请求的资源的位置(url) + HTTP/[版本号](HTTP/1.1)

发起http请求的过程主要是组装http报文并将报文发向指定地址的过程。

  • 服务器响应 html

这里的服务器可能是server或者是cdn

服务器上可能会通过nginx等设置静态资源代理,将url对应的html等静态资源返回。

  • 浏览器解析 HTML

    • 浏览器下载 HTML 数据,将html文档解析成为一个个标签,这些标签组成了树状结构
    • 如果解析到style标签则开始解析css,如果解析到link标签则先异步下载,完成后解析css。
    • 如果遇到script标签,判断是行内写法则直接解析执行,如果是src引入则同步下载脚本文件,下载完成立即执行,注意这里下载过程是阻塞的,其他流程都会等下载完成后执行。
  • 浏览器渲染页面

    • 浏览器会将HTML解析成一个DOM树,DOM 树的构建过程是一个深度遍历过程:当前节点的所有子节点都构建好后才会去构建当前节点的下一个兄弟节点。
    • 将CSS解析成 CSS Rule Tree(css规则树) 。
    • 解析完成后,浏览器引擎会根据DOM树和CSS规则树来构造 Render Tree。注意:Render Tree 渲染树并不等同于 DOM 树,因为一些像Header或display:none的东西就没必要放在渲染树中了。
    • 有了Render Tree,浏览器已经能知道网页中有哪些节点、各个节点的CSS定义以及他们的从属关系。下一步进行layout,进入布局处理阶段,即计算出每个节点在屏幕中的位置。
    • 再下一步就是绘制,即遍历RenderTree,并使用用户界面后端层绘制每个节点。根据计算好的信息绘制整个页面。
  • 浏览器解析执行js脚本

这个过程中可能会有dom操作、ajax发起的http网络请求等。

  • 浏览器发起网络请求

web-socket、ajax等,这个过程通常是为了获取数据

  • 服务器响应 ajax 请求

    • ajax请求在到达真正的server之前,可能还会经过网关全线校验、消息队列或nginx等负载均衡处理
    • 到达server后,后端会解析http请求报文,得到url、请求参数、http头、cookie等等信息
    • 登录校验、权限校验(cookie校验、jwt权限校验等)
    • 可能会查询数据库,进行常用的CRUD(增删改查)等操作
    • 返回响应数据

参考:从输入URL到浏览器显示页面过程中都发生了什么?

浏览器渲染原理?

如何减少重绘回流?

  • 使用 visibility 代替 display: none。重排&重绘 -> 重绘
  • CSS3 硬件加速
  • 批量操作,使用 DocumentFragment
  • 操作 class
  • 避免频繁读取引起重排的属性,比如 offsetHeigt,修改 class 后立即读取
  • 使用 requestAnimationFrame
  • 防抖节流避免频繁操作 DOM
  • 避免使用 table 布局
  • 将动画应用在 absolute 元素上
  • 将频繁重绘或者回流的的节点设置为图层,比如 video

HTTP相关

HTTP 缓存介绍?

强缓存:

两个响应头 ExpiresCache-Control

  • Expires:http1.0,绝对时间,服务器返回
  • Cache-Control: HTTP / 1.1,优先级高于 Expires ,表示的是相对时间

Cache-Control 控制缓存,除了 max-age 控制过期时间外

  • Cache-Control: public 可以被所有用户缓存,包括终端和CDN等中间代理服务器
  • Cache-Control: private 只能被终端浏览器缓存,不允许中继缓存服务器进行缓存
  • Cache-Control: no-cache 先缓存本地,但是在命中缓存之后必须与服务器验证缓存的新鲜度才能使用
  • Cache-Control: no-store 不会产生任何缓存

协商缓存

当第一次请求时服务器返回的响应头中没有 Cache-Control 和 Expires 或者 Cache-Control 和 Expires 过期抑或它的属性设置为 no-cache 时,那么浏览器第二次请求时就会与服务器进行协商。

如果缓存和服务端资源的最新版本是一致的,那么就无需再次下载该资源,服务端直接返回 304 Not Modified 状态码,如果服务器发现浏览器中的缓存已经是旧版本了,那么服务器就会把最新资源的完整内容返回给浏览器,状态码就是 200 Ok。

服务器判断缓存是否是新鲜的方法就是依靠HTTP的另外两组信息

Last-Modified/If-Modified-Since

客户端首次请求资源时,服务器会把资源的最新修改时间 Last-Modified:Thu, 19 Feb 2019 08:20:55 GMT 通过响应部首发送给客户端,当再次发送请求是,客户端将服务器返回的修改时间放在请求头 If-Modified-Since:Thu, 19 Feb 2019 08:20:55 GMT 发送给服务器,服务器再跟服务器上的对应资源进行比对,如果服务器的资源更新,那么返回最新的资源,此时状态码 200,当服务器资源跟客户端的请求的部首时间一致,证明客户端的资源是最新的,返回 304状态码,表示客户端直接用缓存即可。

ETag/If-None-Match

ETag 的流程跟 Last-Modified 是类似的,区别就在于 ETag 是根据资源内容进行 hash,生成一个信息摘要,只要资源内容有变化,这个摘要就会发生巨变,通过这个摘要信息比对,即可确定客户端的缓存资源是否为最新,这比 Last-Modified 的精确度要更高。

介绍 HTTP2?

是 HTTP 网络协议的一个重要版本。 HTTP/2 的主要目标是通过启用完整的请求和响应多路复用来减少延迟,通过有效压缩 HTTP 标头字段来最小化协议开销,并增加对请求优先级和服务器推送的支持。HTTP/2 不会修改 HTTP 协议的语义。 HTTP 1.1 中的所有核心概念(例如 HTTP 方法,状态码,URI 和 headers)都得以保留。 而是修改了 HTTP/2 数据在客户端和服务器之间的格式(帧)和传输方式,这两者都管理整个过程,并在新的框架层内隐藏了应用程序的复杂性。 所以,所有现有的应用程序都可以不经修改地交付。

优势:

  • 采用二进制解析协议;
  • 头部压缩;
  • 服务端推送,比如一个 HTML 页面中还带着一个 CSS 和 js 资源请求,不用发送三次请求,服务器发现,就会将三个资源都返回,一次通信;
  • 安全;
  • 多路复用,在 HTTP 1.x 情况下,每个 HTTP 请求都会建立一个 TCP 连接,造成时间和资源的浪费,浏览器会限制并发请求数,HTTP2,所有的请求都会共用一个 TCP 连接,多路复用,一个 TCP 连接可以存在多个 stream,也就是多个请求,每个 stream 包含多个 frame

HTTP1.0 和 1.1 的区别?

  • 缓存处理;
  • 带宽优化及网络连接的使用;
  • 错误通知的管理;
  • Host 头处理;
  • 长连接;

介绍 HTTPS

(安全的HTTP)是 HTTP 协议的加密版本。它通常使用 SSL 或者 TLS 来加密客户端和服务器之间所有的通信 。这安全的链接允许客户端与服务器安全地交换敏感的数据,例如网上银行或者在线商城等涉及金钱的操作。

与 HTTP 的差异:

HTTP 的 URL 是由 “http://” 起始与默认使用端口 80,而 HTTPS 的 URL 则是由 “https://” 起始与默认使用端口 443。HTTP 不是安全的,而且攻击者可以通过监听和中间人攻击等手段,获取网站帐户和敏感信息等。HTTPS 的设计可以防止前述攻击,在正确配置时是安全的。

TCP 和 UDP?

301、302、304 的区别?

301 表示永久重定向,请求的资源分配了新 URL,302 表示临时重定向,请求的资源临时分配了新 URL,304 表示走缓存,访问服务器,发现数据没有更新,服务器返回此状态码,然后从缓存中读取数据,协商缓存:将缓存信息中的 Etag 和 Last-Modified 通过请求发送给服务器;

TCP 为什么需要三次握手和四次挥手?

参考:segmentfault.com/a/119000002…

三次握手(Three-way Handshake)其实就是指建立一个TCP连接时,需要客户端和服务器总共发送3个包。进行三次握手的主要作用就是为了确认双方的接收能力和发送能力是否正常、指定自己的初始化序列号为后面的可靠性传送做准备。

三次握手:

  • 第一次握手:客户端给服务端发一个 SYN 报文,并指明客户端的初始化序列号 ISN(c)。此时客户端处于 SYN_SENT 状态。首部的同步位SYN=1,初始序号seq=x,SYN=1的报文段不能携带数据,但要消耗掉一个序号。
  • 第二次握手:服务器收到客户端的 SYN 报文之后,会以自己的 SYN 报文作为应答,并且也是指定了自己的初始化序列号 ISN(s)。同时会把客户端的 ISN + 1 作为ACK 的值,表示自己已经收到了客户端的 SYN,此时服务器处于 SYN_RCVD 的状态。在确认报文段中SYN=1,ACK=1,确认号ack=x+1,初始序号seq=y。
  • 第三次握手:客户端收到 SYN 报文之后,会发送一个 ACK 报文,当然,也是一样把服务器的 ISN + 1 作为 ACK 的值,表示已经收到了服务端的 SYN 报文,此时客户端处于 ESTABLISHED 状态。服务器收到 ACK 报文之后,也处于 ESTABLISHED 状态,此时,双方已建立起了连接。确认报文段ACK=1,确认号ack=y+1,序号seq=x+1(初始为seq=x,第二个报文段所以要+1),ACK报文段可以携带数据,不携带数据则不消耗序号。

为什么不是两次握手?

如果是两次握手,发送端可以确定自己发送的信息能对方能收到,也能确定对方发的包自己能收到,但接收端只能确定对方发的包自己能收到 无法确定自己发的包对方能收到

并且两次握手的话, 客户端有可能因为网络阻塞等原因会发送多个请求报文,延时到达的请求又会与服务器建立连接,浪费掉许多服务器的资源

image.png

四次挥手:

  • 第一次挥手:客户端发送一个 FIN 报文,报文中会指定一个序列号。此时客户端处于 FIN_WAIT1 状态,停止发送数据,等待服务端的确认
  • 第二次挥手:服务端收到 FIN 之后,会发送 ACK 报文,且把客户端的序列号值 +1 作为 ACK 报文的序列号值,表明已经收到客户端的报文了,此时服务端处于 CLOSE_WAIT状态
  • 第三次挥手:如果服务端也想断开连接了,和客户端的第一次挥手一样,发给 FIN 报文,且指定一个序列号。此时服务端处于 LAST_ACK 的状态
  • 第四次挥手:客户端收到 FIN 之后,一样发送一个 ACK 报文作为应答,且把服务端的序列号值 +1 作为自己 ACK 报文的序列号值,此时客户端处于 TIME_WAIT状态。需要过一阵子以确保服务端收到自己的 ACK 报文之后才会进入 CLOSED 状态,服务端收到 ACK 报文之后,就处于关闭连接了,处于 CLOSED 状态

四次挥手原因

服务端在收到客户端断开连接Fin报文后,并不会立即关闭连接,而是先发送一个ACK包先告诉客户端收到关闭连接的请求,只有当服务器的所有报文发送完毕之后,才发送FIN报文断开连接,因此需要四次挥手

image.png

数据结构和算法

数组和链表的比较?

链表在前端中的应用?

数组去重?

删除有序数组中的重复项

给你一个有序数组 nums ,请你 原地 删除重复出现的元素,使每个元素 只出现一次 ,返回删除后数组的新长度。

不要使用额外的数组空间,你必须在 原地 修改输入数组 并在使用 O(1) 额外空间的条件下完成。

/**
 * @param {number[]} nums
 * @return {number}
 */
var removeDuplicates = function (nums) {
  if (!nums || !nums.length) {
    return 0;
  }
  // variable to keep track of the portion of list in which duplicates have
  // been removed
  let length = 1;
  for (let i = 1; i < nums.length; i++) {
    // if we encounter a element in the sorted array that is different from
    // the previous one, we update the start of the array
    if (nums[i] !== nums[i - 1]) {
      nums[length++] = nums[i];
    }
  }
  nums.length = length;
  return nums;
};

const arr = [1, 2, 3, 3, 3, 4, 4, 5, 6, 6];
console.log(removeDuplicates(arr));

数组中第 k 大的值?

查找字符串中出现次数最多的字符及次数?

二叉树遍历?

二分查找?

爬楼梯(LeetCode)

打家劫舍(LeetCode 198)

/**
 * @param {number[]} nums
 * @return {number}
 */
const rob = (nums) => {
  let len = nums.length
  if (len === 0) {
    return 0
  }
  if (len === 1) {
    return nums[0]
  }
  let max = Math.max(nums[0], nums[1])
  let res = [nums[0], max]
  for (let i = 2; i < len; i++) {
    res[i] = Math.max(res[i - 2] + nums[i], (res[i - 3] || 0) + nums[i - 1])
  }
  return res[len - 1]
}

LRU(LeetCode)

class Node {
  constructor(key, value) {
    this.key = key
    this.value = value
    this.prev = null
    this.next = null
  }
}

class DLinkedNode {
  constructor() {
    this.head = new Node()
    this.tail = new Node()
    this.head.next = this.tail
    this.tail.prev = this.head
  }
  addToHead(node) {
    node.prev = this.head
    node.next = this.head.next
    this.head.next.prev = node
    this.head.next = node
  }
  removeNode(node) {
    let prev = node.prev
    let next = node.next
    prev.next = next
    next.prev = prev
  }
  moveToHead(node) {
    this.removeNode(node)
    this.addToHead(node)
  }
  popTail() {
    let tail = this.tail.prev
    this.removeNode(tail)
    return tail
  }
}

class LRUCache {
  constructor(capacity) {
    this.capacity = capacity
    this.count = 0
    this.cache = new Map()
    this.dll = new DLinkedNode()
  }
  get(key) {
    let node = this.cache.get(key)
    if (!node) {
      return -1
    }
    this.dll.moveToHead(node)
    return node.value
  }
  put(key, value) {
    let node = this.cache.get(key)
    if (!node) {
      let newNode = new Node(key, value)
      this.cache.set(key, newNode)
      this.dll.addToHead(newNode)
      ++this.count
      if (this.count > this.capacity) {
        let tail = this.dll.popTail()
        this.cache.delete(tail.key)
        --this.count
      }
    } else {
      node.value = value
      this.dll.moveToHead(node)
    }
  }
}

设计模式

常见的设计模式有哪些?

  • 创建型:

    • 工厂模式(工厂方法、抽象工厂)
    • 原型模式:JavaScript 原型的概念、继承的实现
    • 单例模式
  • 结构型:

    • 适配器模式
    • 装饰器模式
    • 代理模式
    • 组合
  • 行为型:

    • 观察者模式(发布订阅)
    • 策略模式

实现观察者模式,有哪些应用场景?**

function Subject() { 
  this.subscribers = [];
}

Subject.prototype.subscribe = function(subscriber) {
  this.subscribers.push(subscriber);
}

Subject.prototype.unsubscribe = function (subscriber) {
  this.subscribers = this.subscribers.filter(function(sub) {
    return sub !== subscriber;
  });
}

Subject.prototype.notify = function () {
  this.subscribers.forEach(function (sub) {
    sub.call();
  });
}

const subject = new Subject();

function observer1() {
  console.log('observer1');
}

function observer2() {
  console.log('observer2');
}

subject.subscribe(observer1);
subject.subscribe(observer2);

subject.notify();

应用场景:onclick 事件绑定,vue中的 watch,Promise

实现单例模式,有哪些应用场景?

应用场景:弹框、全局缓存、Vuex store、全局登录框

工厂模式

应用场景:创建工具库,jQuery 可以使用 $;

策略模式

策略模式是一种行为软件设计模式,在运行时选择算法。代码接收运行时指令决定要使用一系列算法中的哪一个,而不是直接实现单个算法。

一个基于策略模式的程序至少由两部分组成。第一个部分是一组策略类,策略类封装了具体 的算法,并负责具体的计算过程。第二个部分是环境类 Context,Context 接受客户的请求,随后把请求委托给某一个策略类。要做到这点,说明 Context 中要维持对某个策略对象的引用。

function USPS() { 
  this.calculate = function (package) {
    return 4.5
  }
}

function FedEx() { 
  this.calculate = function (package) {
    return 3.5
  }
}

function UPS() { 
  this.calculate = function (package) {
    return 2.5
  }
}

function Shipping() { 
  this.company = null
  this.setStrategy = function (company) {
    this.company = company
  }
  this.calculate = function (package) {
    return this.company.calculate(package)
  } 
}

const fedex = new FedEx()
const ups = new UPS()
const usps = new USPS()
const package = {
  from: 'New York',
  to: 'San Francisco',
  weight: 2
}

const shipping = new Shipping()
shipping.setStrategy(fedex)
console.log(shipping.calculate(package))
shipping.setStrategy(ups)
console.log(shipping.calculate(package))
shipping.setStrategy(usps)
console.log(shipping.calculate(package))

参考:

安全

什么是 XSS 攻击?分为哪几类?如何防范?

参考:zhuanlan.zhihu.com/p/21308080

Cross Site Script(跨站脚本攻击)

JavaScript 有权访问一些用户的敏感信息,比如 Cookie JavaScript 能够通过 XMLHttpRequest 或者其他一些机制发送带有任何内容的 HTTP 请求到任何地址。 JavaScript 能够通过DOM操作方法对当前页面的HTML做任意修改。

恶意脚本的后果:

  • Cookie窃取:攻击者能够通过document.cookie访问受害者与网站关联的cookie,然后传送到攻击者自己的服务器,接着从这些cookie中提取敏感信息,如Session ID。
  • 记录用户行为(Keylogging):攻击者可以使用 addEventListener方法注册用于监听键盘事件的回调函数,并且把所有用户的敲击行为发送到他自己的服务器,这些敲击行为可能记录着用户的敏感信息,比如密码和信用卡号码。
  • 钓鱼网站(Phishing):攻击者可以通过修改DOM在页面上插入一个假的登陆框,也可以把表单的action属性指向他自己的服务器地址,然后欺骗用户提交自己的敏感信息。

分类:

存储型/持续型:

  • 攻击者利用提交网站表单将一段恶意文本插入网站的数据库中;
  • 受害者向网站请求页面;
  • 网站从数据库中取出恶意文本把它包含进返回给受害者的页面中;
  • 受害者的浏览器执行返回页面中的恶意脚本,把自己的cookie发送给攻击者的服务器。

image.png

反射型:

  • 攻击者构造了一个包含恶意文本的URL发送给受害者;
  • 受害者被攻击者欺骗,通过访问这个URL向网站发出请求;
  • 网站给受害者的返回中包含了来自URL的的恶意文本;
  • 受害者的浏览器执行了来自返回中的恶意脚本,把受害者的cookie发送给攻击者的服务器

image.png

Reflected XSS DOM型:

  • 攻击者构造一个包含恶意文本的URL发送受害者
  • 受害者被攻击者欺骗,通过访问这个URL向网站发出请求
  • 网站收到请求,但是恶意文本并没有包含在给受害者的返回页面中
  • 受害者的浏览器执行来自网站返回页面里的合法脚本,导致恶意脚本被插入进页面中
  • 受害者的浏览器执行插入进页面的恶意脚本,把自己的cookie发送到攻击者的服务器

image.png

预防:

预防存储型和反射型 XSS 攻击:

  • 改成纯前端渲染,把代码和数据分隔开。
  • 对 HTML 做充分转义。

预防 DOM 型 XSS 攻击:

  • 在使用 .innerHTML、.outerHTML、document.write() 时要特别小心,不要把不可信的数据作为 HTML 插到页面上,而应尽量使用 innerText、.textContent、.setAttribute() 等。
  • 如果用 Vue/React 技术栈,并且不使用 v-html/dangerouslySetInnerHTML 功能,就在前端 render 阶段避免 innerHTML、outerHTML 的 XSS 隐患。

其他XSS攻击防范:

  • Content Security Policy(CSP)
  • 输入内容长度控制,增加XSS攻击的难度。
  • HTTP-only Cookie: 禁止 JavaScript 读取某些敏感 Cookie,攻击者完成 XSS 注入后也无法窃取此 Cookie。
  • 验证码:防止脚本冒充用户提交危险操作。