一个前端的2020面试分享(所遇题目+答案) | 掘金技术征文

1,157 阅读29分钟

背景介绍

作为一个毕业快两年但前端等级仍然是Lv1的小菜鸟,本该在前公司兢兢业业的做着自己的本分活,奈何公司以受疫情影响一而再再而三的降薪克扣着我本就不多的薪水,于是笔者毅然决定在这个“寒冷”的初春踏上求职之旅,经过二十余天十来场左右的厮杀,以还算不错的结果结束了这次的战斗,下面则是我对这一次所遇面试问题涵盖知识的一个总结,希望能给大家带来一些小小的帮助,文中若有不对之处,欢迎大家指出。

css

1.说说box-sizing的属性以及其区别

  • content-box:宽度和高度的计算值不包含内容的边框(border)和内边距(padding)。
  • border-box:宽度和高度属性包括内容,内边距和边框,但不包括外边距。

2.visibility: hidden、display: none的区别

  • DOM 结构:前者会被渲染只是被隐藏,占据空间;后者不会被渲染,不占据空间;
  • 性能:改变前者时会引起重绘,性能较高;改变后者会引起回流,性能较差;
  • 继承:前者会被子元素继承,子元素可以通过设置 visibility: visible; 来取消隐藏;后者不会被子元素继承,子类无法显示出来;
  • 参考至分析比较 opacity: 0、visibility: hidden、display: none 优劣和适用场景

3.怎么让一个 div 水平垂直居中

  • 弹性布局
  • 绝对定位+ translate
  • 绝对定位+ margin
  • 更多方式

4.描述一下弹性布局及其常用属性

5.你知道BFC吗?它有什么作用

  • BFC就是块级格式上下文,相当于一块隔离的容器,和外部元素互不影响。

  • 作用

    • 清除浮动
    • 防止同一个 BFC 下外边距会发生折叠的问题
  • 触发条件

    • body根元素
    • float浮动
    • 绝对定位
    • display为弹性布局和表格布局
    • overflow 不为 visiable

js

1.typeof和instanceof的区别

  • typeof:判断值的类型;
  • intanceof: 检测构造函数的prototype是否在对象的原型链上;

2.判断一个值的类型

  • typeof + intanceof;
  • Object.prototype.toString.call

3.防抖和节流的区别是什么?怎么实现?

  • 防抖:触发事件的n秒后执行对应函数,若n秒内事件再次被触发,那么时间将重新计算。
  function debounce(fn, time) {
    let timer = null; // 定时器ID
    return function() {
      clearTimeout(timer); // 触发事件时将上一次的定时器清除掉
      timer = setTimeout(() => { // 创建一个新的定时器,只有在一定时间后会执行
        fn.apply(this, arguments);
      }, time);
    }
  }
  • 节流:触发事件后,会先判断n秒内是否执行了函数,未执行时才会去执行。
  function throttle(fn, time) {
    let isRun = false; // 定义一个当前事件内是否执行了函数的变量
    return function () {
      if (isRun) return; // 如果当前时间内函数执行了,则直接return
      isRun = true; // 没执行,则将其设置为true
      setTimeout(() => { // 创建一个定时器在n秒后执行函数,并把isRun赋值为false
        fn.apply(this, arguments);
        isRun = false;
      }, time);
    }
  }

4.深拷贝和浅拷贝有什么区别?你能实现一个深拷贝吗?

  • 浅拷贝只能复制对象的第一层,当对象的属性还是引用类型的时候,则只会复制其引用地址,当值被修改以后所有的引用的值都会改变。深拷贝则是解决浅拷贝存在的问题。
  // 没有考虑函数拷贝的情况
  function deepClone(obj) {
    if (obj === null || typeof obj !== 'object') return obj;
    const constructor = obj.constructor;
    let cloneObj;
    switch (constructor) {
      case Boolean:
      case Date:
      case Number:
      case String:
      case RegExp:
        return new constructor(obj);
      default:
        cloneObj = new constructor();
    }
    // 该方法能返回目标值所有的属性(包括symbol)
    Reflect.ownKeys(obj).forEach(key => {
      cloneObj[key] = deepClone(obj[key])
    })
    return cloneObj;
  }    

5.call和apply的区别,你能实现吗?

  • call和apply的功能都是改变函数的执行作用域,具体的区别在于传参的方式,call方法接受的是参数列表,而apply方法接受的是一个数组或类数组。
  Function.prototype._call = function (thisArg, ...arg) {
    thisArg = thisArg || window || global; // thisArg为null或undefined时,自动指向全局对象
    thisArg.thisFn = this; // 给执行作用域绑定需要执行的函数
    const result = thisArg.thisFn(...arg);
    delete thisArg.thisFn;
    return result;
  }
  
  Function.prototype._apply = function (thisArg, argsArray) {
    thisArg = thisArg || window || global; // thisArg为null或undefined时,自动指向全局对象;
    thisArg.thisFn = this; // 给执行作用域绑定需要执行的函数
    let result;
    if (argsArray === null || argsArray === undefined) { // argsArray值为 null 或 undefined,则表示不需要传入任何参数
      result = thisArg.thisFn();
    } else if (typeof argsArray === 'object') {
      result = thisArg.thisFn(...argsArray);
    } 
    delete thisArg.thisFn;
    return result;
  }

6.说说原型和原型链

  • 原型:js中的每个对象都有一个__proto__的属性,被称作隐式原型,指向该对象构造函数的prototype,这个prototype被称作显示原型,也就是我们常说的原型对象。
  • 原型链:当我们调用对象上的一个方法或者属性的时候,该对象首先会在其自身的当前作用域里查找,如果没找到,就会根据__proto__去其构造函数的prototype上进行查找,如果扔为找到,会通过其构造函数的prototype的__proto__属性去其构造函数的prototype的构造函数的prototype上查找...null(终点),找到就会返回对应方法或属性,没找到就报错或返回undefined,这个查找的过程就是原型链。

7.在ES5中比较常见的继承方式有哪些?

  • 原型链继承:直接将子类的原型赋值指向父类的实例,即Child.prototype = new Parent();

    • 缺点

      • 因为所有子类实例的隐式原型都指向Child.prototype,即相同的父类实例,所以会共用父类实例上的引用类型属性
      • 创建子类实例的时候不能向父类构造函数传参
  • 借用构造函数继承:在子类构造函数中通过call或者apply将父构造函数的执行作用域绑定到子类构造函数上,该方法解决了原型链继承中的两个问题。

    • 缺点
      • 在父类构造函数原型上的方法和属性无法被子类实例访问到
    ....
     function Child() {
          // 继承了Parent
          Parent.call(this);
    }
    ...
  • 组合继承:将原型链继承和构造函数继承组合,发挥二者的长处,即使用原型链实现对原型属性和方法的继承,通过构造函数继承实现对实例属性的继承。
    • 缺点
      • 每次创建子类实例时都会调用两次Parent构造函数,这样Child.prototype会生成两次name属性,而后者会覆盖前者,虽然并不影响继承功能,但是不太优雅。
     function Parent(name) {
       this.name = name;
     }
     Parent.prototype.getName = function () {
       return this.name;
     }
     function Child(name, age) {
        // 通过构造函数继承属性
        Parent.call(this, name); // 第二次调用Parent()
        this.age = age;
     }
     // 通过原型链继承方法
     Child.prototype = new Parent(); // 第一次调用Parent()
     Child.prototype.constructor = Child; // 让其constructor指向自身
  • 寄生组合式继承:当我们想要继承父类原型上的方法属性时,不必为了子类的原型调用父类构造函数,直接创建一个隐式原型指向父类构造函数原型的对象就行,即newObj.__proto__ = Parent.prototype,那么,Object.create()方法刚刚好。
    function Parent(name) {
        this.name = name;
      }
      Parent.prototype.getName = function () {
        return this.name;
      }
      function Child(name, age) {
        // 通过构造函数继承属性
        Parent.call(this, name);
        this.age = age;
      }
      Child.prototype = Object.create(Parent.prototype); //通过create方法创建
      Child.prototype.constructor = Child; // 让其constructor指向自身

参考文章:JavaScript高级程序设计(第3版)

8.实现一个new函数

  function _new(Fn,...arg) {
    // 必须为一个函数且不能为箭头函数
    if(typeof Fn !== "function" || !Fn.prototype) throw Error(`Fn is not a constructor`);

    // 将新对象的隐式原型赋值为构造函数的prototype,obj.__proto__ = Fn.prototype
    const obj = Object.create(Fn.prototype);

    // 改变Fn的执行作用域,并传入相应的参数
    const isRet = Fn.call(obj, ...arg);

    // 判断构造函数本身是否返回一个object
    return isRet instanceof Object ? isRet : obj;
  }

9.箭头函数和普通函数的区别是什么?

  • 自身无this,其this继承上一级,所以无法使用call、apply、bind绑定this
  • 自身无 arguments 对象,会通过作用域链向上查找
  • 不能用作 Generator 函数。
  • 没有prototype属性,所以无法用new关键字调用

10.实现一个promise

我当时实现的是非常低配的promise,代码就不贴了,推荐坤坤大佬的promise实现

11.promise、async/await、setTimeout的区别是什么?

首先,js的事件循环分为宏任务和微任务,而当前执行栈的执行顺序为同步代码 -> 微任务中的代码 -> 宏任务中的代码。

  • promise:promise函数本身是同步执行的,只有其then或者catch等方法是异步执行的并且其回调函数会被放在事件循环中的微任务队列,让同步代码先执行。
  • async/await:当一个函数被添加async关键字的时候表明当前函数中可能会有异步方法,await关键字只能在async函数中使用并且后面跟一个表达式,在async函数中遇见await关键字时会同步执行后面的表达式并将表达式后面的代码放入微任务队列,让同步代码先执行。
  • setTimeout/setInterval:定时器中的回调函数会被放在宏任务队列,等同步代码和微任务队列中的代码执行完毕后执行

浏览器

1.浏览器的渲染流程是怎样的?

主要流程为:

  1. 浏览器解析HTML生成DOM树
  2. 浏览器解析CSS生成CSSOM树
  3. 将DOM树和CSSOM树合并生成渲染树
  4. 根据渲染树进行布局,计算每个节点的位置大小信息
  5. 最后将每个节点绘制到页面上 参考十分钟读懂浏览器渲染流程

2.重绘和回流的区别是什么?

  • 重绘:节点的属性或者样式的改变不会影响布局,如color,background-color,visibility,性能相对较好
  • 回流:布局或者几何属性被改变,如display:none,width,height,性能较差
  • 详细可见

3.浏览器的缓存机制

深入理解浏览器的缓存机制,这篇文章很详细的讲解了浏览器缓存。

React

1.react在列表中加key的作用是什么?

  • 提升diff同级对比的效率,因为只有给循环生成的列表添加了唯一索引那么在更新之后才能直接通过该索引对比出谁发生了改变,否则就只能一一去进行对比

2.你认为虚拟dom的优势是什么?

3.setState何时异步,何时同步?

  • 在react的合成事件以及生命周期中是异步,在原生事件以及定时器中是同步,并且这个"异步"也并不是使用异步代码实现的,其本身还是同步代码,只不过会把多个state合成到一起进行批量更新,这样实现的原因如下

4.react组建通信方式有哪些?

  • 父子组件通信:父组件通过props属性给子组件传值
  • 子父组件通信:子组件将所传值当作参数传入父组件通过props传入子组件的函数
  • 兄弟组件通信:找到两个组件的公共父组件,结合前两种方式传值
  • 跨层级组件通信:Context,基础使用方法如下 详细可看
     // 创建context  ./src/context.js
     import { createContext } from "react"
     
     // 只有当组件所处的树中没有匹配到 Provider 时,这个默认值才会生效
     const { Provider, Consumer } = createContext("默认值")
    
     export {
       Provider,
       Consumer
     }
    
     // 在需要使用context值的公共祖先组件中使用Provider包裹组件  ./src/App.js
     import { Provider } from "./context"
     import Child from "./child"
     
     function App() {
       return (
         <Provider value={"需要传递的值"}>
           <Child />
         </Provider>
       );
     }
    
     // 子组件  ./src/child.js
     import Grandson from "./Grandson"
     
     function Child() {
       return <Grandson />
     }
     
     // 后代组件中可以通过Consumer来获得Provider中的value值,value为 需要传递的值  ./src/grandson.js
     import { Consumer } from "./context"
     
     function Grandson() {
       return (
         <Consumer>
           {value => <div>{value}</div>}
         </Consumer>
       )
     }
    
  • 状态管理库通信:使用第三方状态管理库如Redux或者Mobx等来进行通信

5.react在挂载、更新、卸载阶段分别会执行哪些生命周期?

  • 挂载阶段:constructor -> static getDerivedStateFromProps(替代UNSAFE_componentWillMount) -> render -> componentDidMount

  • 更新阶段:static getDerivedStateFromProps(替代UNSAFE_componentWillReceiveProps) -> shouldComponentUpdate -> UNSAFE_componentWillUpdate (不推荐使用,并且使用了static getDerivedStateFromProps 或 getSnapshotBeforeUpdate后将不会被调用) -> render -> getSnapshotBeforeUpdate -> componentDidUpdate

  • 卸载阶段: componentWillUnmount

    详细使用方法

6.你知道哪些react优化方式?

  • shouldComponentUpdate:在class组件中添加该生命周期以返回布尔值来确定是否更新组件,true为更新,false为不更新。在PureComponent中默认以浅层对比 prop 和 state 的方式实现了该函数
  • memo:只适用于函数组件,默认和PureComponent一样只会进行浅比较,但是可以通过传递函数作为第二个参数来实现具体的更新逻辑,需要注意的是传递函数返回的布尔值更新与shouldComponentUpdate相反,即true为不更新,false为更新
  • useMemo:该函数为react hook中的API,可以记忆一个值,使用场景为需要通过大量计算得出的一个值的时候,为避免每次渲染都去计算,可以将值进行记忆,useCallback的功能也可以用该函数实现
     // 第二个参数为依赖项,即a,b发生改变后memoizedValue的值会重新计算
     const memoizedValue = useMemo(() => computeExpensiveValue(a, b), [a, b]);
     // 用useMemo记忆一个函数
     const memoizedFn = useMemo(() => fn, deps)
    
  • 动态加载组件:使用 React.lazy 配合 React.Suspense动态加载组件,并在加载过程中优雅降级 使用方法
  • 避免使用匿名函数:在给子组件传递函数时,若使用匿名函数返回传递函数的方式,那么每一次父组件重新渲染匿名函数都会重新生成,这样子组件可能会进行额外的重新渲染
  • Fragment:在一个组件需要返回多个元素的时候,通常是在最外层包裹一个div,而有时这个div实际没什么用处,那么这个时候可以使用Fragment将其包裹,而无需向 DOM 添加额外节点。

7.react如何进行逻辑复用

  • 高阶组件(HOC):本事是一个参数为组件,返回值为新组件的函数

    • 实现方法
      • 属性代理:即给传入的组件添加props
        // 一个简单的例子 
        function HOC(WrappedComponent) {
          return class newComponent extends React.Component {
              render() {
                  const newProps = {
                      username: "beadre"
                  };
                  // this.props为 HOC 接收到的 props
                  return <WrappedComponent {...this.props} {...newProps} />
              }
          }
        }
        
      • 反向继承:即返回一个继承于传入的组件的类
        // 一个简单的例子
        function HOC(WrappedComponent) {
          return class newComponent extends WrappedComponent {
            render() {
              // 可以通过this访问到 WrappedComponent的 state、props、组件生命周期方法和 render 方法。
              return super.render()
            }
          }
        }
        
    • 缺点
      • 不能直观的看出返回组件中被传入了哪些props
      • 如果传入的props与原有的props重名,原有属性会被覆盖

    参考React组件复用指南

  • render props:指一种在 React 组件之间使用一个值为函数的 prop 共享代码的简单技术

     // 一个简单的例子
     import React from "react"
    
     class RenderProps extends React.Component {
       state = {
         count: 0
       }
       render() {
         return (
           <div onClick={() => this.setState({count: this.state.count + 1})}>
             {this.props.render(this.state.count)}
           </div>
         )
       }
     }
     
     function App() {
       return <RenderProps render={count => <h1>这是第{count}次点击</h1>}/>
     }
    
    • 特点:render props规避了HOC所有的缺点,但是对比HOC复用起来稍嫌麻烦

    参考React组件复用指南

  • 自定义hook

8.假设页面中一个小组件因为某种原因出错了,怎么让其不对其它组件造成影响?

9.在redux中是如何更改数据?

  1. 用户在界面(view)执行一个操作,触发事件之后通过dispatch方法发出一个action到store(如果是异步请求会发出action到中间件)
  2. 中间件收到action之后,执行相应的异步请求,然后拿到返回的数据之后会发送一个action到reducer(有中间件才会执行这一步
  3. store收到以后会把当前的state以及收到的action通过参数传给reducer
  4. reducer根据action执行相应的操作然后返回一个新的state到store
  5. 由于react组件接收的props(即state)改变,那么就会触发相应组件重渲染(这里是react-redux通过Provider包裹,connect函数将state传入组件的)

10.你刚刚提到了react-redux,那你说说吧

react-redux 具体的实现是通过react的context API,将redux中的store传给context中Provider的value属性,然后在后代组件中通过consumer获取。

其常用的API有两个:

  • Provider:使组件层级中的 connect 方法都能够获得 store,通常用来包裹根组件(希望获得store的组件的共有父组件就行),有一个store属性接收 Redux 的 store

  • connect:为一个HOC,用于连接 React 组件与 Redux store,可以接收4个参数

    • mapStateToProps:为一个接收两个参数(state,ownProps)的函数,返回一个对象用于跟组件的props合并,如果定义该参数,组件将会监听 Redux store 的变化
    • mapDispatchToProps:为一个函数或者对象,都用于 action creator 的描述,如果传递该参数,那么组件不会收到dispatch
    • mergeProps:为一个函数,用于接收 mapStateToProps 与 mapDispatchToProps 的执行结果和组件自身的 props,返回一个对象用于跟组件的props合并
    • options:一个可以定制 connector 的行为的对象

    更详细的API及使用方法

webpack

1.常见的loader及其作用

  • babel-loader:将高版本js转化为ES5
  • file-loader:将文件输出到文件夹,并返回(相对)URL
  • url-loader:和file-loader类似,区别是可以限制处理文件的大小,小于限制值直接返回base64
  • css-loader:解析CSS文件后,使用 import 加载,并且返回 CSS 代码
  • style-loader:将CSS注入到style标签中
  • postcss-loader:自动添加css3属性的浏览器前缀
  • less-loader:将less文件解析为css文件
  • sass-loader:将scss文件解析为css文件
  • ts-loader:将ts文件解析为js文件
  • eslint-loader:使用ESLint检查js代码
  • vue-loader:加载和转译 Vue 组件
  • cache-loader:将loader解析后的结果缓存在磁盘中

更多loader

2.常见的plugin及其作用

  • html-webpack-plugin:简单创建 HTML 文件,用于服务器访问
  • clean-webpack-plugin:清理文件夹
  • ignore-plugin:忽略模块包中的某些文件
  • terser-webpack-plugin:压缩js代码
  • HotModuleReplacementPlugin:启用模块热替换
  • DllPlugin:拆分bundles

更多plugin

3.loader和plugin的区别

  • loader:本身是一个函数,功能是对接收的文件进行预处理转换,因为webpack本身只能处理javascript,所以需要先用loader转换
  • plugin:丰富webpack自身功能,针对是loader结束后,webpack打包的整个过程,它并不直接操作文件,而是基于事件机制工作,会监听webpack打包过程中的某些节点,执行广泛的任务

4.如何打包多页应用

const HtmlWebpackPlugin = require('html-webpack-plugin');
module.exports = {
    entry: {
        page1: './src/page1.js',
        page2: './src/page2.js'
    },
    // ...
    plugins: [
        new HtmlWebpackPlugin({
            template: './public/page1.html',
            filename: 'page1.html', // 多个html-webpack-plugin时,filename必填,否则都是index.html
            chunks: ['page1'] // page2.html引入的js文件,当前只会引入page1.js
        }),
        new HtmlWebpackPlugin({
            template: './public/page2.html',
            filename: 'page2.html', // 多个html-webpack-plugin时,filename必填,否则都是index.html
            // chunks: ['page2']  默认会引入所有js文件
        }),
    ]
}

5.webpack热更新原理

  • 首先是webpack-dev-server(wds)跟浏览器之间建立了一个websocket的长连接并且通过webpack暴露的API对本地代码的变化进行一个监控,当本地文件改变之后,webpack会对其重新打包,由wds将新模块的 hash 值推送到浏览器,如下图
    浏览器收到之后,会带着该 hash 值通过ajax去wds请求对应更新的文件列表
    在根据对应的文件列表通过 jsonp 请求最新的模块代码
    之后会将新旧模块代码进行对比决定是否更新模块,在决定更新模块后,检查模块之间的依赖关系,更新模块的同时更新模块间的依赖引用。

更多细节

6.如何优化webpack构建速度

网络

1.什么是跨域? 跨域的解决方案有哪些?

跨域是指当前发起请求的域与该请求指向的资源所在的域不一样而触发了浏览器安全方面的限制,一般来说只要发起域跟请求域的域名、协议和端口号任一不同就会存在跨域。解决方案如下:

  • document.domain + iframe:该方法需要有相同的主域名,通过把需要通信的页面用document.domain设置为相同的主域来实现通信。
  • postMessage:主要为一个窗口通过调用该方法传递消息,接收的窗口通过监听message事件接受消息,在接受消息时一定要判断origin和source属性验证发件人的身份,否则可能会导致XSS攻击。
  • jsonp:主要是动态创建script标签,并把src赋值为 带上前端接收参数的函数名的请求地址(如src=http://xxx.com?cb=函数名),后端接收到请求之后获得我们的函数名,将数据当做参数传入函数,返回给前端执行调用。
    • 优点是该方法实现了跨域且兼容性比较好
    • 缺点
      • 只能发送get请求
      • jsonp调用失败时只能静默的失败,无错误处理
      • 安全性很低,可能会遭受XSS攻击
  • CORS:该方法跨域的实现关键在于后端,只要后端设置了Access-Control-Allow-Origin就行,前端不用做任何操作,只不过需要注意,使用cors的话请求会分为简单请求和复杂请求两种。
    • 简单请求:
      • 请求方法为head、get、post三者之一
      • 请求头中为以下几种字段:Accept、Accept-Language、Content-Language、Last-Event-ID、Content-Type(只有三个值application/x-www-form-urlencoded、multipart/form-data、text/plain)
    • 复杂请求:除了简单请求的都是复杂请求,复杂请求在正式通信之前,会先发一个option请求进行预检,只有服务端成功响应预检,浏览器才会发出我们对应的请求,否则就会报错。
  • websocket:websocket是HTML5实现的一个新API,其本身就支持跨域,使用该方法可以在浏览器和服务器之间打开交互式通信会话。websocket本身提供了onopen、onclose、onmessage、onerror、send等API,我们直接调用就好。
  • node代理跨域:通过搭建node服务器来转发我们的请求,大概以下三种情况
    • 使用了webpack:修改webpack.config.js中的devServer下的proxy字段 具体配置
    • 使用了create-react-app:
      • 在package.json 中添加 proxy 字段,注意该方法只能设置一个地址
      • 在src目录新建setupProxy.js文件 具体可见
      • eject然后自行修改webpack.config.js
    • 未使用webpack也未使用脚手架:通过node搭建一个服务器,使用cors让我们前端能成功访问该node服务,再通过http-proxy-middleware这个库配置代理信息实现请求转发 配置可见
  • nginx代理跨域:和node一样是搭建一个服务器来转发我们的请求,具体是修改nginx.conf文件,例如
   server {
       listen       启动服务的端口 如8081;
       server_name  启动的服务名 如localhost;
       root         代理的项目路径 如D:\\example\\project;
       location /api/{ 在代理项目中遇见可/api会将其替换为proxy_pass中的地址
           proxy_pass 反向代理的地址 如 http://x.x.x.x:x/;
       }
   }

2.http常见状态码有哪些?

  • 1xx:信息响应

  • 2xx:成功响应

    • 200 OK:请求成功
    • 204 No Content:服务器成功处理了请求,但没有返回任何内容
  • 3xx:重定向

    • 301 Moved Permanently:永久重定向,被请求的资源已永久移动到新位置
    • 302 Found:临时重定向,被请求的资源临时搬到了其他位置
  • 4xx:客户端错误

    • 400 Bad Request:
      • 语义有误,当前请求无法被服务器理解。除非进行修改,否则客户端不应该重复提交这个请求
      • 请求参数有误。
    • 401 Unauthorized :未授权,需要身份验证,通常是需要登录
    • 403 Forbidden:服务端拒绝客户端访问该资源
    • 404 Not Found:请求资源不存在或服务端拒绝请求且不想说明理由
  • 5xx:服务器错误

    • 500 Internal Server Error:服务器出错
    • 502 Bad Gateway:错误网关
    • 503 Service Unavailable:服务不可用
    • 504 Gateway Timeout:网关超时
  • 更多状态码

3.输入一个url到页面显示的过程

  • 输入url
  • DNS解析:将域名解析为IP地址
    • IP地址查找顺序:浏览器缓存 -> 操作系统缓存 -> host文件 -> 路由器缓存 -> 互联网服务提供商 -> 根DNS服务器;查找到以后不仅会把IP返回到用户电脑,还会把域名跟IP的映射关系进行缓存,以便下一次访问时能直接返回
  • TCP连接:拿到IP之后浏览器会发起TCP的连接请求,也就是常说的三次握手
    • 三次握手:首先浏览器会发送一个SYN包到服务器并进入SYN_SENT状态,服务器收到以后会返回一个SYN + ACK包并进入SYN_RECV状态,客户端收到后会向服务器发送一个ACK确认包,然后双方进入ESTABLISHED(TCP连接成功)状态,完成三次握手
  • HTTP请求:建立TCP连接之后,客户端会发起HTTP请求,一个HTTP请求包含请求行、请求头、请求体
    • 请求行:包含请求方法、协议版本等
    • 请求头:包含自定义的请求头、浏览器信息、请求源、语言等
    • 请求体:发送的数据,GET请求没有请求体
  • HTTP响应:服务端收到请求后对客户端的响应,分为响应行、响应头、响应体
    • 响应行:包含状态码、状态码说明、协议版本等
    • 响应头:包含服务器的信息、缓存规则、日期等
    • 请求体:请求的资源
  • 页面渲染:即我上面写的浏览器的渲染流程
  • 连接关闭:在我们请求头和响应头中有一个Connection的字段,默认值为keep-alive(持久连接),所以通常连接关闭发生当前标签页被关闭的时候,这时会进行四次挥手
    • 四次挥手:
      • 客户端先告知服务器要断开连接,并关闭输入通道
      • 服务端收到以后会回复客户端我也会关闭这条通道(接收通道)
      • 服务端再告诉客户端我要关闭输入通道
      • 客户端回复说你关吧,我也把接收通道关了
    • 上面有两条通道的原因是因为TCP是全双工连接,两端各有接收和发送的能力,所以每条通道的两端为接收和发送

4.http和https的区别

  • http的端口是80,而https为443
  • http通信使用明文,而https使用SSL/TSL加密
  • https需要申请CA证书,申请需要交费
  • http的传输效率优于https,因为https需要经过加密过程

5.https的加密方式

  • 对称加密:使用同一串秘钥进行加密和解密

    • 优点:加解密花费时间比较短、速度快
    • 缺点:不是很安全,因为秘钥分发的过程中有被拦截的风险
  • 非对称加密:使用不同的秘钥进行加密和解密,其中加密的秘钥和解密的秘钥是相对应的一组秘钥,被称为公钥和私钥,公钥加密的信息,只能用对应私钥解密,反之亦成立

    • 优点:安全性较高,因为只有公钥在网络中传输,私钥留在自己手里
    • 缺点:加解密花费时间比较长、速度慢
  • 对称加密+非对称加密:去除了两者的缺点只保留优点,使用非对称加密的方式传输对称加密的秘钥,然后后续使用对称加密的方式进行通信,https使用了这种方式

  • 中间人攻击:非对称加密的公钥是服务端发给客户端的,但是传输过程中可能会被中间人拦截,那么中间人可以用自己的公钥将其掉包然后把假的公钥发送给客户端,然后客户端用假的公钥加密对称加密使用的秘钥,那么中间人就能用自己对应的私钥解密然后得到对称加密的秘钥。

    • 解决办法:为了解决中间人攻击,我们需要一个办法来确定收到的公钥没有被改变,或者说被改变了我们能够知道,而这个办法就是CA证书
  • CA证书:首先将服务端的信息和公钥通过hash算法生成消息摘要,然后用第三方公证机构的私钥对消息摘要进行加密生成数字签名,然后把服务端信息和公钥以及数字签名合在一起就是CA证书了。在收到服务端CA证书后,我们会用CA机构的公钥对数字签名解密,得到传输过来的消息摘要,然后再用相同的hash算法对收到的公钥和服务端信息加密生成新的消息摘要,对比两个消息摘要就能知道信息是否被修改

安全

1.XSS攻击(跨站脚本攻击)

  • 定义:指攻击者往网页中注入恶意脚本代码,当用户访问时恶意代码会执行,从而攻击用户
  • 分类:
    • 存储型XSS:攻击者首先将恶意代码通过表单提交的方式提交到数据库,当用户访问网页获取对应的数据时,服务端会取出该代码返回给浏览器,浏览器将数据渲染到页面的过程中会执行该代码
    • 反射型XSS:攻击者将恶意代码拼接在URL中诱导用户点击,用户点击后,服务端会把恶意代码当作参数取出,最后返回给浏览器,当需要将其进行渲染时即会被执行。比如一个网站的搜索功能接口为xxxx.com/search?key=XX,当数据为空时前端页面提示暂无XX数据,那么攻击者把XX改为恶意代码让用户点击URL,服务端取出搜索值并未作处理最后将其返回给浏览器,浏览器渲染相关内容时,恶意代码会被执行
    • DOM型XSS:跟反射型XSS很相似,都是通过拼接URL诱导用户点击,区别是反射型是先发送到服务端然后再返回到浏览器被执行,而DOM型是直接在前端页面被执行。比如一个页面地址为xxxx.com?name=XX,js中要把name的值渲染到页面,如果XX为恶意代码,那么渲染的时候就会被执行
  • 预防:
    • 表单提交中对比较明确的输入如电话号码,url等做内容和长度的限制和过滤
    • 前端渲染不明确的内容用innerText
    • 设置HttpOnly禁止JS读取Cookie等

参考美团技术团队的前端安全系列(一)

2.CSRF攻击(跨站请求伪造)

  • 定义:挟制用户在当前已登录的Web应用程序上执行非本意的操作的攻击方法
  • 攻击流程:
    • 用户登录a网站,保留了cookie
    • 攻击者诱导用户点击b网站
    • b网站会向a网站发送请求,此时浏览器会带着用户的cookie
    • a网站接收到请求,检查登录凭证,误以为是用户自身的操作,然后执行请求,攻击完成
  • 特点:
    • 通常是在点击链接进入第三方网站后发起攻击
    • 利用用户在被攻击网站的登录凭证伪造用户操作
    • 不能获取用户凭证,仅仅是冒用
    • 跨站请求可以用各种方式:图片URL、超链接、CORS、Form提交等等。部分请求方式可以直接嵌入在第三方论坛、文章中,难以进行追踪。
    • 除了第三方网站跨域进行攻击,在本域下若有发图和链接的评论区,攻击也可在本域下进行
  • 预防
    • 同源检测:通过http请求头中的Origin和Referer两个字段确定请求的来源
      • origin缺陷:
        • IE 11 不会在跨站CORS请求上添加Origin
        • 在302重定向之后Origin不包含在重定向的请求中
      • Referer缺陷:
        • 不同浏览器的Referer实现有差别
        • 攻击者可以在自己的请求中隐藏Referer
        • HTTPS页面跳转到HTTP页面,所有浏览器Referer都丢失
    • Token:由于之前的登录凭证是cookie,虽然攻击者无法直接获取,但是浏览器会带上,那么可以通过在每次请求的请求头中加上一个攻击者无法获取且浏览器不会自动带上的自定义请求头来进行验证
    • 双重Cookie验证:在用户访问的时候主动向请求域下添加一个随机字符串的cookie,之后的每个请求都需要取出cookie添加到url参数中,后端收到请求后将参数和cookie进行对比
      • 缺陷:
        • Cookie中增加了额外的字段
        • 如果有其他漏洞(例如XSS),攻击者可以注入Cookie,那么该防御方式失效
        • 难以做到子域名的隔离

更多请参考美团技术团队的前端安全系列(二)

最后

以上内容即是我二十来天面试以来的被问问题所涵盖的一个知识点,在记录的过程中发现之前自己不少知识点都掌握的不是很全面,所以此次也是得到了蛮多的收获,也同样希望给每一个需要的小伙伴提供或多或少的帮助。^_^