前端面试-基础

857 阅读1小时+

HTML

如何理解 HTML 语义化

通俗地讲,语义化就是让正确的标签做正确的事情,比如:段落用 p 标签,标题用 h1~h6 标签。
这样做有两个好处:

  1. 让人更容易读懂(增加代码可读性)
  2. 让搜索引擎和浏览器更容易读懂(便于浏览器和搜索引擎解析)

默认情况下,哪些 HTML 标签是块级元素、哪些是内联元素

display: block/table 的是块状元素,有 div h1 table ul ol p 等
display: inline/inline-block 的是内联元素,有 span img input button 等

DOCTYPE 有什么作用?区分严格模式和混杂模式有何意义?

<!DOCTYPE> 文档声明位于文档最前面,位于 html 标签之前,用于告诉浏览器的解析器,用什么文档类型规范来解析这个文档。 DOCTYPE 不存在或者格式不正确都会导致文档以混杂模式程序。

区分严格模式和混杂模式的意义在于:
严格模式的排版和 JavaScript 的运行模式都会以该浏览器支持的最高标准运行。
在混杂模式中,页面以宽松的向后兼容的方式显示,模拟老式浏览器的行为以防止站点无法工作。

iframe 的优缺点

优点:

  1. 可以解决加载缓慢的第三方内容的加载问题,如图标和广告等。
  2. 可以跨域请求其他网站,并将网站完整展示出来。
  3. 可以实现安全沙箱
  4. 可以并行加载脚本

缺点:

  1. iframe 会阻塞主页面的 onload 事件
  2. iframe 内容即使为空,加载也需要时间、
  3. iframe 没有语义
  4. 不利于 SEO,搜索引擎的爬虫无法解读 iframe 的页面。

CSS

布局

说一下 CSS 选择器优先级

image.png

盒模型的宽度如何计算

如下代码,请问 div1 的 offsetWidth 是多大?

<style> 
#div1 {
    width: 100px;
    padding: 10px;
    border: 1px solid #ccc;
    margin: 10px;
} 
</style>
<div id="div1"></div>

offsetWidth = (内容宽度 + 内边距 + 边框),无外边距
所以 div1 的 offsetWidth = 110 + (10 + 1) * 2 = 112 px

补充:如果让 offsetWidth 等于 100px,该如何做?
设置 box-sizing: border-box;

margin 纵向重叠问题

如下代码,AAA 和 BBB 之间的距离是多少?

<style> 
p {
    font-size: 16px;
    line-height: 1;
    margin-top: 10px;
    margin-bottom: 15px;
} 
</style>

<p>AAA</p>
<p></p>
<p></p>
<p></p>
<p>BBB</p>

相邻元素的 margin-top 和 margin-bottom 会发生重叠
空白内容的 <p></p> 也会重叠
答案:15px

margin 负值的问题

对 margin 的 top left right bottom 设置负值,有何效果?

margin-top 和 margin-left 负值,元素向上、向左移动
margin-right 负值,右侧元素左移,自身不受影响
margin-bottom 负值,下方元素上移,自身不受影响

BFC 理解和应用

什么是 BFC?如何应用?

Block format contex,块级格式化上下文
一块独立的渲染区域,内部元素的渲染不会影响到边界以外的元素

形成 BFC 的常见条件
float 不是 none (left、right)
position 是 absolute 或 fixed
overflow 不是 visible (hidden、auto、scroll)
display 是 flex inline-block 等

BFC 常见应用

  1. 清除浮动
    通过改变浮动元素的父元素的属性值,触发 BFC,以此来清除浮动。

  2. 阻止元素被浮动元素覆盖
    一个正常文档流的块级元素可能被一个浮动元素覆盖,因此可以设置一个元素的 float、position、overflow 或者 display 值等方式触发 BFC,以阻止被浮动盒子覆盖。

  3. 阻止相邻元素的 margin 合并
    属于同一个 BFC 的两个相邻块级子元素的上下 margin 会发生重叠,所以当两个相邻块级子元素分属于不同的 BFC 时可以阻止 margin 重叠。

float 布局

如何实现圣杯布局和双飞翼布局

圣杯布局和双飞翼布局的目的:

  1. 三栏布局,中间一栏最先加载和渲染(内容最重要)
  2. 两侧内容固定,中间内容随着宽度自适应
  3. 一般用于 PC 网页

圣杯布局

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta http-equiv="X-UA-Compatible" content="IE=edge" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>圣杯布局</title>
    <style> 
      body {
        min-width: 550px;
      }

      .header,
      .footer {
        text-align: center;
        background: #f1f1f1;
      }

      .container {
        padding-left: 200px;
        padding-right: 150px;
      }

      .container .col {
        float: left;
      }

      .center {
        width: 100%;
        background-color: skyblue;
      }

      .left {
        width: 200px;
        margin-left: -100%;
        position: relative;
        right: 200px;
        background-color: yellow;
      }

      .right {
        width: 150px;
        margin-right: -150px;
        background-color: red;
      }

      .clearfix:after {
        content: "";
        height: 0;
        visibility: hidden;
        display: block;
        clear: both;
      }
      .clearfix {
        *zoom: 1;
      } </style>
  </head>
  <body>
    <div class="header">header</div>
    <div class="container clearfix">
      <div class="center col">center</div>
      <div class="left col">left</div>
      <div class="right col">right</div>
    </div>
    <div class="footer">footer</div>
  </body>
</html>

双飞翼布局

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta http-equiv="X-UA-Compatible" content="IE=edge" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>双飞翼布局</title>
    <style> 
      body {
        min-width: 550px;
      }

      .col {
        float: left;
      }

      .main {
        width: 100%;
        background-color: skyblue;
      }

      .main .main-wrapper {
        margin-left: 200px;
        margin-right: 150px;
      }

      .left {
        width: 200px;
        margin-left: -100%;
        background-color: yellow;
      }

      .right {
        width: 150px;
        margin-left: -150px;
        background-color: red;
      } </style>
  </head>
  <body>
    <div class="main col">
      <div class="main-wrapper">main</div>
    </div>
    <div class="left col">left</div>
    <div class="right col">right</div>
  </body>
</html>

圣杯布局和双飞翼布局的技术总结:
使用 float 布局
两侧使用 margin 负值,以便和中间内容横向重叠
防止中间内容被两侧覆盖,一个用 padding 一个用 margin

手写 clearfix

.clearfix:after {
    content: "";
    height: 0;
    visibility: hidden;
    display: block;
    clear: both;
}
.clearfix {
    *zoom: 1;
}

伪类和伪元素的区别
css 引入伪类和伪元素概念是为了格式化文档树以外的信息。
伪类用于当已有元素处于的某个状态时,为其添加对应的样式。
伪元素用于创建一些不在文档树中的元素,并为其添加样式。

flex 布局

flex 1 是什么

flex 实现一个三点的色子

常用语法:
flex-direction 主轴方向
justify-content 主轴对齐方式
align-items 交叉轴对齐方式
flex-wrap 换行
align-self 子元素在交叉轴的对齐方式

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta http-equiv="X-UA-Compatible" content="IE=edge">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>flex 画色子</title>
  <style> 
    .box {
      width: 200px;
      height: 200px;
      border: 2px solid #ccc;
      border-radius: 10px;
      padding: 20px;
      display: flex;
      justify-content: space-between;
    }

    .item {
      width: 40px;
      height: 40px;
      border-radius: 50%;
      background-color: #666;
    }

    .item:nth-child(2) {
      align-self: center;
    }

    .item:nth-child(3) {
      align-self: flex-end;
    } </style>
</head>
<body>
  <div class="box">
    <div class="item"></div>
    <div class="item"></div>
    <div class="item"></div>
  </div>
</body>
</html>

flex 实现左边固定,右边自适应

<style>
  body {
    display: flex;
    height: 200px;
  }
  .left {
    width: 200px;
    background-color: blue;
  }
  .right {
    flex: 1;
    background-color: red;
  }
</style>

<body>
  <div class="left"></div>
  <div class="right"></div>
</body>

定位

absolute 和 relative 分别依据什么定位

relative 依据自身定位
absolute 依据最近一层的定位元素定位
找父元素或者祖先元素中最近的定位元素(absolute、relative、fixed),最终没有找到则依据 body 定位

居中对齐的实现方式

水平居中
inline 元素:text-align:center
block 元素:margin:auto
absolute 元素:left 50% + margin-left 负值

垂直居中
inline 元素:line-height 的值等于 height 的值
absolute 元素:top:50% + margin-top 负值

水平垂直居中
absolute 元素:left:50% + top:50% + transform:translate(-50%,-50%)
absolute 元素:top,left,bottom,right = 0 + margin: auto
flex 元素:display:flex; justify-content:center; align-items: center;

图文样式

line-height 继承问题

如下代码,p 标签的行高会是多少

<style> 
.body {
    font-size: 20px;
    line-height: 200%;
}
p {
    font-size: 16px;
} </style>

<body>
    <p>AAA</p>
</body>

line-height 取值
写具体数值,如 30 px,则继承该值
写比例,如 2 或 1.5,则继承该比例
写百分比,如 200%,则继承计算出来的值
答案:40px

单行和多行文本溢出截断
单行

div {
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}

多行

div { 
    display: -webkit-box;
    overflow: hidden;
    -webkit-line-clamp: 2;// webkit 内核浏览器支持的行数
    -webkit-box-orient: vertical;
    text-overflow: ellipsis;// clip|ellipsis|string 截断|省略号|自定义字符串 
}

动画

CSS animation 与 CSS transition 有何区别? 其实整体来看,animation 和transition想做的事情都是一样,通过控制属性变化的过程,也实现动画。都是立足于控制本身dom的css属性变化过程,来实现动画的视觉效果。

区别就是,两者的控制粒度不一样。某种程度上,transition更加粗一点,比如过渡的速度进行了封装,可以控制是匀速改变还是贝塞尔曲线之类的。而animation提供的keyframe方法,可以让你手动去指定每个阶段的属性。此外animation还封装了循环次数,动画延迟等功能,根据自由和强大。

简单的情况选用transition,自由一点或者复杂的可以用animation

响应式

rem 是什么

px,绝对长度单位,最常用
em,相对长度单位,相当于父元素的 font-size,不常用
rem,相对长度单位,相当于根元素的 font-size,常用于响应式布局
vw/vm,vw 网页视口宽度的 1/100,vh 网页视口高度的 1/100
vmax:取 vw/vh 中最大值
vmin:取 vw/vh 中最小值

响应式布局的常见方案

  1. media-query 媒体查询,根据不同屏幕宽度设置根元素 font-size
  2. rem,基于根元素的相对单位

JavaScript

JavaScript 基础知识

变量类型和计算

介绍 js 的数据类型

基本数据类型:Number、String、Boolean、Undefined、Null,ES6 新增了 Symbol 类型。
引用类型:Object、Function、Array、Date、RegExp、Math

undefined 和 null 的区别
undefined 表示一个变量没有被声明,或者被声明了但没有被赋值(未初始化),一个没有传入实参的形参变量的值为undefined,如果一个函数什么都不返回,则该函数默认返回 undefined。null 则表示“什么都没有”,即“空值”。

介绍下 Symbol
Symbol 表示独一无二的值,Symbol 最大的用途是用来定义对象的唯一属性名。
比如我们要给一个已有属性的对象添加一个新的属性,新的属性可能和旧的属性名称冲突,这个时候采用 Symbol 是最好的。

值类型和引用类型的区别
基本数据类型的值存在栈中,复杂数据类型在栈中存的是地址,其真正的值存在堆中。
因为考虑到性能和存储的问题,基本数据类型占用空间小,复制的时候也不会对性能造成太大的影响,所以栈基本可以满足基本数据类型;而引用类型占用的空间可能非常大,不好管理,在复制的时候会导致复制过程非常慢。

const obj1 = {
    x: 100,
    y: 200
}
const obj2 = obj1
let x1 = obj1.x
obj2.x = 101
x1 = 102
console.log(obj1.x)  // 101

堆和栈的区别
栈:栈会自动分配内存空间,会自动释放,存放基本类型,简单的数据段,占据固定大小的空间。 优点:存取速度比堆快,仅次于直接位于CPU中的寄存器,数据可以共享;
缺点:存在栈中的数据大小,缺乏灵活性。

堆:动态分配的内存,大小不定也不会自动释放,存放引用类型。堆内存中的对象不会随方法的结束而销毁,即使方法结束后,这个对象还可能被另一个引用变量所引用(参数传递)。创建对象是为了反复利用,这个对象将被保存到运行时数据区。

typeof 能判断哪些类型

能识别所有的值类型(null 除外)undefined、string、number、boolean、symbol
能识别函数 function
判断是否是引用类型(不可再细分)object

null 本身不是对象,typeof null === 'object’, 是语言本身的一个 bug。其原理是不同的对象在底层都是用二进制来表示的,在 js 中二进制前 3 位是 0 即判断是为对象,null 的二进制表示是全 0,即前三位都是 0,所以执行 typeof 返回的是 object,实际上 null 为基本数据类型。

深拷贝与浅拷贝的区别
浅拷贝是只拷贝一层,深层次的对象级别就只拷贝引用,修改新的值会影响旧的。 深拷贝是拷贝多层,每一级别的数据都拷贝出来,修改新的值不会影响旧的。

手写深拷贝

/**
 * 深拷贝
 * @param {Object} obj
 * @return {Object}
 */
function deepClone(obj) {
  if (typeof obj !== "object" || obj === null) return obj;
  if (obj instanceof RegExp || obj instanceof Date || typeof obj === "function")
    return new obj.constructor(obj);
  let res = new obj.constructor();
  for (let i in obj) {
    if (obj.hasOwnProperty(i)) {
      res[i] = deepClone(obj[i]);
    }
  }
  return res;
}

手写深度比较,模拟 lodash isEqual

function isObject(obj) {
  return typeof obj === "object" && obj !== null;
}
function isEqual(obj1, obj2) {
  if (!isObject(obj1) || !isObject(obj2)) return obj1 === obj2;
  if (obj1 === obj2) return true;
  const keys1 = Object.keys(obj1);
  const keys2 = Object.keys(obj2);
  if (keys1.length !== keys2.length) return false;
  for (let k of keys1) {
    const res = isEqual(obj1[k], obj2[k]);
    if (!res) return false;
  }
  return true;
}

何时使用 === 何时使用 ==
除了判断 == null 之外,其他都一律用 ===,因为 == 会进行隐式类型转换

== 和 === 的区别
== 会尝试类型转换
=== 严格相等

列举强制类型转换和隐式类型转换
强制:parseInt、parseFloat、toString 等
隐式:if、逻辑运算、==、+ 拼接字符串

js 的 0.1 + 0.2 为什么不等于 0.3
在 JavaScript 中的二进制的浮点数 0.1 和 0.2 并不是十分精确,在他们相加的结果并非正好等于 0.3,而是一个比较接近的数字 0.30000000000000004 ,所以条件判断结果为 false。

这是因为 JavaScript 存储数值采用的是双精度浮点数,但是 JS 采用的浮点数标准会裁剪我们的数字,这就出现了精度丢失的问题。

怎么解决

parseFloat((0.1 + 0.2).toFixed(10)) === 0.3 //  true

(0.1*1000 + 0.2*1000) / 1000 // true

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

大数相加

const checkNum = (num) => typeof num === "string" && !isNaN(Number(num));

// 格式化函数,主要针对两个大数长度不一致时,超长的数字的格式化为0
const format = (val) => {
  if (typeof val === "number") return val;
  if (!isNaN(Number(val))) return Number(val);
  return 0;
};

function bigNumAdd(num1, num2) {
  if (checkNum(num1) && checkNum(num2)) {
    const tmp1 = num1.split("").reverse();
    const tmp2 = num2.split("").reverse();
    const result = [];

    let temp = 0;
    for (let i = 0; i <= Math.max(tmp1.length, tmp2.length); i++) {
      const addTmp = format(tmp1[i]) + format(tmp2[i]) + temp;
      result[i] = addTmp % 10;
      temp = addTmp > 9 ? 1 : 0; // 进位
    }
    result.reverse();

    // 将数组for中多加的一位进行处理
    const resultNum = result[0] > 0 ? result.join("") : result.join("").slice(1);
    return resultNum;
  } else {
    return "big number type error";
  }
}

数组

知道哪些数组 api
添加/删除元素:

  • push(...items) —— 向尾端添加元素,
  • pop() —— 从尾端提取一个元素,
  • shift() —— 从首端提取一个元素,
  • unshift(...items) —— 向首端添加元素,
  • splice(pos, deleteCount, ...items) —— 从 pos 开始删除 deleteCount 个元素,并插入 items。
  • slice(start, end) —— 创建一个新数组,将从索引 start 到索引 end(但不包括 end)的元素复制进去。
  • concat(...items) —— 返回一个新数组:复制当前数组的所有元素,并向其中添加 items。如果 items 中的任意一项是一个数组,那么就取其元素。

搜索元素:

  • indexOf/lastIndexOf(item, pos) —— 从索引 pos 开始搜索 item,搜索到则返回该项的索引,否则返回 -1。
  • includes(value) —— 如果数组有 value,则返回 true,否则返回 false。
  • find/filter(func) —— 通过 func 过滤元素,返回使 func 返回 true 的第一个值/所有值。
  • findIndex 和 find 类似,但返回索引而不是值。

遍历元素:

  • forEach(func) —— 对每个元素都调用 func,不返回任何内容。

转换数组:

  • map(func) —— 根据对每个元素调用 func 的结果创建一个新数组。
  • sort(func) —— 对数组进行原位(in-place)排序,然后返回它。
  • reverse() —— 原位(in-place)反转数组,然后返回它。
  • split/join —— 将字符串转换为数组并返回。
  • reduce/reduceRight(func, initial) —— 通过对每个元素调用 func 计算数组上的单个值,并在调用之间传递中间结果。

其他:

  • Array.isArray(arr) 检查 arr 是否是一个数组。

sort,reverse 和 splice 方法修改的是数组本身。

split() 和 join() 的区别
split():把字符串根据切割符切割,返回数组。
join():把数组根据分隔符拼接成字符串。

数组的 pop push unshift shift 分别做什么
pop():移除数组的最后一项,并返回移除项
push(value):在数组末尾添加一项并返回新数组长度,value可以是任何类型
shift():移除数组首项,并返回移除项
unshift(value):在数组首项插入一项并返回新数组长度,value可以是任何类型

数组的 api 有哪些是纯函数
纯函数:不改变原数组(没有副作用),返回一个数组
concat、map、filter、slice

数组 slice 和 splice 的区别
slice(start,end)
从 start 开始截取到 end 但是不包括 end
返回值为截取出来的元素的集合
原始的数组不会发生变化,是纯函数

splice(start,deleteCount,item1,item2…..); start 参数 开始的位置
deleteCount 要截取的个数
后面的 items 为要添加的元素
如果 deleteCount 为 0,则表示不删除元素,从 start 位置开始添加后面的几个元素到原始的数组里面
返回值为由被删除的元素组成的一个数组。如果只删除了一个元素,则返回只包含一个元素的数组。如果没有删除元素,则返回空数组
这个方法会改变原始数组,不是纯函数

[10, 20, 30].map(parseInt)

// 完整写法
[10, 20, 30].map((num, index) => {
  return parseInt(num, index);
});

parseInt(string, radix) 可解析一个字符串, 并返回一个整数。
string 要被解析的字符串。
radix 表示要解析的数字的基数,该值介于 2~36 之间。
如果省略该参数或其值为 0, 则数字将以 10 为基础来解析。 如果它以“ 0x” 或“ 0X” 开头, 将以 16 为基数。 如果该参数小于 2 或者大于 36, 则 parseInt() 将返回 NaN

parseInt(10,0):数字基数为0,数字以 10进制解析,故结果为 10;
parseInt(20,1):数字基数为1,数字以 1进制解析,1进制出现了2,1进制无法解析,结果返回NaN;
parseInt(30,2):数字基数为2,数字以 2进制解析,2进制出现了3,3进制无法解析,结果返回NaN;

反转数组

// 方法一
function reverseArray(arr) {
  return arr.reverse();
}

// 方法二
function reverseArray(arr) {
  const res = [];
  const last = arr.length - 1;
  for (let i = 0; i < arr.length; i += 1) {
    res[i] = arr[last - i];
  }
  return res;
}

手写数组 flatten,考虑多层级(数组扁平化)

// 方法一 ES6
let a = [1,[2,3,[4,[5]]]];  
a.flat(Infinity); // [1,2,3,4,5]  a是4维数组

// 方法二
function flatten(arr) {
  while (arr.some((item) => Array.isArray(item))) {
    arr = [].concat(...arr);
  }
  return arr;
}

// 方法三
function flatten(arr) {
  return arr.reduce(function (prev, next) {
    return prev.concat(Array.isArray(next) ? flatten(next) : next);
  }, []);
}

数组去重

// 方法一
function unique(arr) {
  const res = [];
  arr.forEach((item) => {
    if (res.indexOf(item) === -1) {
      res.push(item);
    }
  });
  return res;
}

// 方法二 Set
function unique(arr) {
  return [...new Set(arr)];
}
function unique(arr) {
  return Array.from(new Set(arr));
}

// 方法三 for...of + Object
function unique(arr) {
  const res = [];
  const obj = {};
  for (let i of arr) {
    if (!obj[i]) {
      res.push(i);
      obj[i] = 1;
    }
  }
  return res;
}

实现一个打乱数组的方法

// Fisher–Yates shuffle 洗牌算法
Array.prototype.shuffle = function() {
    var array = this;
    var m = array.length,
        t, i;
    while (m) {
        i = Math.floor(Math.random() * m--);
        t = array[m];
        array[m] = array[i];
        array[i] = t;
    }
    return array;
}

// 2
[12,4,16,3].sort(function() {
    return .5 - Math.random();
});

reduce 模拟 map

Array.prototype._map = function (fn, cbThis) {
  const res = [];
  const that = cbThis || null;
  this.reduce(function (total, item, index, arr) {
    res.push(fn.call(that, item, index, arr));
  }, null);
  return res;
};

reduce 实现累加器

var total = [ 0, 1, 2, 3 ].reduce(
  ( acc, cur ) => acc + cur,
  0
);

如何获取多个数字中的最大值

// 方法一
function max(...nums) {
  let max = 0;
  nums.forEach((n) => {
    if (n > max) max = n;
  });
  return max;
}

// 方法二
function max(...nums) {
  return Math.max(...nums);
}

其他 JS 基础面试题

函数柯里化
柯里化是把接受多个参数的函数变换成接受一个单一参数的函数,并且返回接受余下参数的新函数的技术。
柯里化的目的是,减少代码冗余,以及增加代码的可读性。
柯里化的好处在于,调用函数的时候,如果某一个参数在每次调用中都相同,可以避免重复传入这个参数。

写一个 sum 函数,让 sum(2)(3) 返回 5

function curry(fn, currArgs) {
  return function (...args) {
    if (currArgs !== undefined) args = args.concat(currArgs);
    if (args.length < fn.length) {
      return curry(fn, args);
    }
    return fn.apply(null, args);
  };
}
function sum(a, b) {
  return a + b;
}

const fn = curry(sum);
fn(2, 3); // 5
fn(2)(3); // 5

for in 和 for of 的区别
for in 循环返回的值都是数据结构的键名。
遍历对象返回的是对象的 key 值,遍历数组返回的是数组的下标。
还会遍历原型上的值和手动添加的值

for of 循环获取键值对中的键值。
一个数据结构只要部署了 Symbol.iterator 属性,就被视为具有 iterator 接口,可以使用 for of。
for of 不同于 forEach,for of 是可以 break,continue,return 配合使用,for of 循环可以随时退出循环。

以下数据结构的有 iterator 接口的: 数组 Array、Map、Set、String、arguments 对象、Nodelist 对象、类数组

为什么不推荐用 for...in 遍历数组
for-in 遍历的的属性值是字符串,而不是数字
for-in 遍历的是对象的枚举属性,包括自身属性以及原型链上的属性
for-in 遍历顺序是对象属性的枚举顺序,并不一定按数组的下标顺序遍历

判断字符串以字母开头,后面字母数字下划线,长度 6-30

const reg = /^[a-zA-Z]\w{5,29}$/;

手写字符串 trim 方法,保证浏览器兼容性

String.prototype.trim = function (str) {
  return str.replace(/^\s+|\s+$/gm, "");
};

如何捕获 JS 程序中的异常

// 手动捕获
try {
  // TODO
} catch (e) {
  console.error(e); // 手动捕获异常
} finally {
  // TODO
}

// 自动捕获
window.onerror = function (message, source, lineNum, colNum, error) {
  // 第一,对跨域的 js,如 CDN 的,不会有详细的报错信息
  // 第二,对压缩的 js,还要配合 sourcemap 反查到未压缩代码的行、列
};

什么是 JSON
json 是一种数据格式,本质是一段字符串
json 格式和 JS 对象结构基本一致,对 JS 语言更友好,JSON 的 key 和字符串必须用双引号
window.JSON 是一个全局对象:JSON.parse()、JSON.stringify()

use strict 有什么用
"use strict" 指令在 ES5 中新增。
它不是一条语句,但是是一个字面量表达式,在 JavaScript 旧版本中会被忽略。
"use strict" 的目的是指定代码在严格条件下执行。
严格模式下不能使用未声明的变量。

x = 3.14;       // 不报错
myFunction();

function myFunction() {
   "use strict";
    y = 3.14;   // 报错 (y 未定义)
}

为什么使用严格模式:

  • 消除 Javascript 语法的一些不合理、不严谨之处,减少一些怪异行为;
  • 消除代码运行的一些不安全之处,保证代码运行的安全;
  • 提高编译器效率,增加运行速度;
  • 为未来新版本的 Javascript 做好铺垫。

缺点:
现在网站的 JS 都会进行压缩,一些文件用了严格模式,而另一些没有。这时这些本来是严格模式的文件,被 merge 后,这个串就到了文件的中间,不仅没有指示严格模式,反而在压缩后浪费了字节。

原型和原型链

ES5 继承
在子类中通过 call / apply 方法借助父类的构造函数
将子类的原型函数设置为父类的实例对象

function Person(myName, myAge) {
    this.name = myName;
    this.age = myAge;
}
Person.prototype.say = function () {
    console.log(this.name, this.age);
}
function Student(myName, myAge, myScore) {
    // 1.在子类中通过call/apply方法借助父类的构造函数
    Person.call(this, myName, myAge);
    this.score = myScore;
    this.study = function () {
        console.log("day day up");
    }
}
// 2.将子类的原型对象设置为父类的实例对象
Student.prototype = new Person();
Student.prototype.constructor = Student;

let stu = new Student("zs", 18, 98);
stu.say();  // zs 18

ES6 继承
通过子类 extends 父类, 来告诉浏览器子类要继承父类
通过 super() 方法修改 this

class Person{
    constructor(myName, myAge){
        // this = stu;
        this.name = myName; // stu.name = myName;
        this.age = myAge;   // stu.age = myAge;
    }
    say(){
        console.log(this.name, this.age);
    }
}
// 以下代码的含义: 告诉浏览器将来Student这个类需要继承Person这个类
class Student extends Person{
    constructor(myName, myAge, myScore){
        super(myName, myAge);   // 这一行代码相当于在子类中通过call/apply方法借助父类的构造函数
        this.score = myScore;
    }
    study(){
        console.log("day day up");
    }
}

let stu = new Student("zs", 18, 98);
stu.say();  // zs 18

对原型对象的理解
我们创建的每一个普通函数都有一个 prototype 属性,这个属性是一个指针,指向一个对象。这个对象包含了该函数所有实例共享的属性和方法。并且,该函数实例化的所有对象的 __proto__ 的属性也指向这个对象,它是该函数所有实例化对象的原型。

对原型链的理解
原型链的本质是链表,原型链上的节点是各种原型对象,比如 Function.prototype, Object.prototype,原型链通过 __proto__ 属性连接各种原型对象。

instanceof 的原理, 并用代码实现
解法: 遍历 A 的原型对象, 如果能找到 B.prototype, 返回 true, 否则返回 false

/**
 * 实现 instanceOf
 * @param {*} a
 * @param {*} b
 * @returns {boolean}
 */
const instanceOf = (a, b) => {
  let p = a;

  while (p) {
    if (p === b.prototype) return true;
    p = p.__proto__;
  }
  return false;
};

new 一个函数的时候发生了什么(模拟 new 操作)

  • 创建一个空对象
  • 把该对象关联到构造函数
  • 把新创建的空对象作为构造函数的上下文
  • 如果构造函数有返回值且为对象,则返回该对象,否则返回新创建的对象
function _new(fn, ...args) {
  const obj = {};
  obj.__proto__ = fn.prototype;
  const res = fn.apply(obj, args);
  return res instanceof Object ? res : obj;
}

new Object() 和 Object.create()的区别
{} 等同于 new Object(),原型 Object.prototype
Object.create(obj) 创建一个新对象,使用现有的对象 obj 来提供新创建的对象的 __proto__

如何准确判断一个变量是否是数组

  1. 通过 Array.isArray() 判断
let a = [1,2,3]
Array.isArray(a);//true
  1. 通过 instanceof 判断
let a = [];
a instanceof Array; //true
  1. 通过 constructor 判断
let a = [1,3,4];
a.constructor === Array;//true
  1. 通过 Object.prototype.toString.call() 判断
let a = [1,2,3]
Object.prototype.toString.call(a) === '[object Array]';//true

作用域和闭包

什么是作用域
作用域代表某个变量的合法使用范围。作用域分为全局作用域、函数(局部)作用域、块级作用域(ES6新增)。 作用域最大的用处就是隔离变量,不同作用域下同名变量不会有冲突。

什么是自由变量
当前作用域没有定义但是在使用的变量,就是自由变量。比如在函数中使用的,但既不是函数参数也不是函数的局部变量的变量。

关于作用域和自由变量的场景题

let i;
for (i = 0; i <= 3; i++) {
  setTimeout(function () {
    console.log(i);
  }, 0);
}
/*
输出:
4
4
4
4
*/

let a = 100;
function test() {
  alert(a);
  a = 10;
  alert(a);
}
test();
alert(a);
/* 
输出:
100
10
10
*/

什么是作用域链
一个变量在当前作用域没有定义,但被使用了,就会向上级作用域,一层一层依次寻找,直到找到为止,如果到全局作用域都没有找到,则报错。这种一层一层的关系,就是作用域链 。

var let const 的区别
var 是 ES5 语法,let const 是 ES6 语法;var 有变量提升;
var 和 let 是定义变量,可修改;const 是定义常量,不可修改;
let const 有块级作用域,var 没有;

函数声明和函数表达式的区别
函数声明 function fn() {...}
函数表达式 const fn = function() {...}
函数声明会在代码执行前预加载,而函数表达式不会

什么是闭包
闭包是指那些能够访问自由变量的函数,闭包是作用域应用的特殊情况,有两种表现:

  1. 函数作为参数被传递
function fn1(fn) {
  let a = 20;
  fn();
}
let a = 10;
function fn() {
  console.log(a);
}
fn1(fn);  // 10
  1. 函数作为返回值被返回
function fn2() {
  let a = 10;
  return function () {
    console.log(a);
  };
}
let fn = fn2();
let a = 20;
fn(); // 10

注意:自由变量的查找,是在函数定义的地方,向上级作用域查找,不是在执行的地方!!!

实际开发中闭包的应用场景,举例说明

  1. 隐藏数据
function fnc() {
  const data = {};
  return {
    get(key) {
      return data[key];
    },
    set(key, value) {
      data[key] = value;
    },
  };
}
  1. 拿到正确的值
for (let i = 0; i < 10; i++) {
  setTimeout(() => {
    console.log(i);
  }, 0);
}

// 或者
for (let i = 0; i < 10; i++) {
  ((j) => {
    setTimeout(() => {
      console.log(j);
    }, 0);
  })(i);
}
  1. 防抖和节流

闭包是什么,有什么特性?有什么负面影响?
闭包是指那些能够访问自由变量的函数

闭包的特性:
封闭性:外界无法访问闭包内部的数据,如果在闭包内声明变量,外界是无法访问的,除非闭包主动向外界提供访问接口;
持久性:一般的函数,调用完毕之后,系统自动注销函数,而对于闭包来说,在外部函数被调用之后,闭包结构依然保存在

影响:变量会常驻内存,得不到释放。闭包不要乱用。

闭包会导致内存泄露吗?
不会。内存泄露是指你用不到的变量,依然占居着内存空间,不能被再次利用起来。
闭包里面的变量就是我们需要的变量,不能说是内存泄露。

this 的不同应用场景,如何取值 / 谈谈你对 this 的理解
this 的指向是根据调用的上下文来决定的,默认指向 window 对象

  1. 作为普通函数
function fn1() {
  console.log(this);
}
fn1();  // window
  1. 箭头函数
    箭头函数的 this 指向上级作用域的 this
const person = {
  name: "Jason_liang",
  say() {
    console.log(this); // 当前对象
  },
  wait() {
    setTimeout(() => {
      console.log(this); // 当前对象
    });
  },
};
  1. 作为对象方法被调用
const person = {
  name: "Jason_liang",
  say() {
    console.log(this); // 当前对象
  },
  wait() {
    setTimeout(function () {
      console.log(this); // window
    });
  },
};
  1. 在 class 方法中调用
class People {
  constructor(name) {
    this.name = name;
  }
  say() {
    console.log(this);  // 当前实例
  }
}
  1. 使用 call apply bind
function fn1() {
  console.log(this);
}
fn1.call({ x: 100 }); // {x: 100}

注意:this 的取值是在函数执行的时候决定的,不是在定义的时候决定的!!!

关于 this 的场景题

const User = {
  count: 1,
  getCount: function () {
    return this.count;
  },
};
console.log(User.getCount()); // 1
const func = User.getCount;
console.log(func()); // undefined

call apply bind 的区别
三者都可以改变函数的 this 指向。
三者第一个参数都是 this 要指向的对象,如果没有这个参数或参数为 undefined 或 null,则默认指向全局 window。
三者都可以传参,但是 apply 是数组,而 call 和 bind 是参数列表,且 apply 和 call 是一次性传入参数,而 bind 可以分为多次传入。
bind 是返回绑定 this 之后的函数,便于稍后调用;apply 、call 则是立即执行 。

手写 call

Function.prototype.myCall = function (ctx, ...args) {
  ctx = ctx || window;
  const key = Symbol();
  ctx[key] = this;
  const res = ctx[key](...args);
  delete ctx[key];
  return res;
};

手写 apply

Function.prototype.myApply = function (ctx, args) {
  ctx = ctx || window;
  const key = Symbol();
  ctx[key] = this;
  const res = ctx[key](...args);
  delete ctx[key];
  return res;
};

手写 bind

Function.prototype.myBind = function (ctx, ...args) {
  return (...newArgs) => {
    return this.apply(ctx, args.concat(newArgs));
  };
};

箭头函数和普通函数的区别

  • this 指向问题
    箭头函数不绑定 this,会捕获其所在的上下文的 this 值,作为自己的 this 值
    不可以用 call()、apply()、bind() 这些方法去改变 this 的指向

  • 不可以被当作构造函数
    箭头函数是匿名函数,不能作为构造函数,不能使用 new,不存在 prototype 属性,也不能通过 super 访问原型的属性

  • 不可以使用 arguments 对象,该对象在函数体内不存在,如果要用就用 rest 参数替代。

  • 不可以使用 yield 命令,因此箭头函数不能用作 Generator 函数。

异步

单线程和异步
JS 是单线程语言,只能同时做一件事。
浏览器和 nodejs 已经支持 JS 启动进程,如 Web Worker。
JS 和 DOM 渲染共用一个线程,因为 JS 可修改 DOM 结构。
遇到等待(网络请求,定时任务)不能卡住。
所以需要异步,异步基于回调(callback)函数形式调用

同步和异步的区别是什么
基于 JS 是单线程语言,异步不会阻塞代码执行,同步会阻塞代码执行

前端使用异步的场景有哪些

  1. 网络请求,如 ajax 图片加载
  2. 定时任务,如 setTimeout

以下代码的打印结果

console.log(1)
setTimeout(() => {
    console.log(2)
}, 1000)
console.log(3)
setTimeout(() => {
    console.log(4)
}, 0)
console.log(5)

// 1 3 5 4 2

为什么要有 Promise ?Promise 解决了什么问题
promise 解决了回调地狱 (callback hell) 的问题

Promise 有哪三种状态?如何变化?
三种状态:pending resolved rejected
状态的变化:pending —> resolved 或 pending —> rejected,一旦变化了就不可再改变
状态的表现

  • pending 状态,不会触发 then 和 catch
  • resolved 状态,会触发后续的 then 回调函数
  • rejected 状态,会触发后续的 catch 回调函数

then 和 catch 如何影响状态的变化
then 正常返回 resolved 状态的 Promise,里面有报错则返回 rejected 状态的 Promise
catch 正常返回 resolved 状态的 Promise,里面有报错则返回 rejected 状态的 Promise

场景题:promise then 和 catch 的链式调用

// 第一题
Promise.resolve()
  .then(() => {
    console.log(1);
  })
  .catch(() => {
    console.log(2);
  })
  .then(() => {
    console.log(3);
  });
//答案: 1 3 

// 第二题
Promise.resolve()
  .then(() => {
    console.log(1);
    throw new Error();
  })
  .catch(() => {
    console.log(2);
  })
  .then(() => {
    console.log(3);
  });
// 答案 1 2 3

// 第三题
Promise.resolve()
  .then(() => {
    console.log(1);
    throw new Error();
  })
  .catch(() => {
    console.log(2);
  })
  .catch(() => {
    console.log(3);
  });
// 答案: 1 2

手写 Promise.all()
Promise的all静态方法:
1、接收一个 Promise 实例的数组或具有 Iterator 接口的对象
2、如果元素不是 Promise 对象,则使用 Promise.resolve 转成 Promise 对象
3、如果全部成功,状态变为 resolved,返回值将组成一个数组传给回调
4、只要有一个失败,状态就变为 rejected,返回值将直接传递给回调 all() 的返回值也是新的 Promise 对象

Promise.all = function (iterator) {
  if (!Array.isArray(iterator)) iterator = Array.from(iterator);
  let count = 0;
  const res = [];

  return new Promise((resolve, reject) => {
    for (let i = 0; i < iterator.length; i += 1) {
      Promise.resolve(iterator[i])
        .then((data) => {
          res[i] = data;
          if (++count === iterator.length) resolve(res);
        })
        .catch((e) => {
          reject(e);
        });
    }
  });
};

介绍下 async/await
async/await 用同步的语法编写异步代码,彻底消灭回调函数,是异步操作的终极解决方案。
async 函数返回一个 Promise 对象,可以使用 then 方法添加回调函数。当函数执行的时候,一旦遇到 await 就会先返回,等到触发的异步操作完成,再接着执行函数体内后面的语句。

await 命令只能用在 async 函数之中,如果用在普通函数,就会报错。

参考文档:www.cnblogs.com/peiyu1988/p…

async/await 和 Promise 的关系
async/await 和 Promise 并不互斥,而是相辅相成的。
执行 async 函数,返回的是 Promise 对象
await 相当于 Promise 的 then
try...catch 可捕获异常,代替了 Promise 的 catch

场景题:async/await 语法

// 第一题
async function fn() {
  return 100;
}
(async function () {
  const a = fn();
  console.log(a); // Promise {<resolved>: 100}
  const b = await fn();
  console.log(b); // 100
})();

// 第二题
(async function () {
  console.log("start");
  const a = await 100;
  console.log("a", a);
  const b = await Promise.resolve(200);
  console.log("b", b);
  const c = await Promise.reject(300);
  console.log("c", c);
  console.log("end");
})();
/* 
答案: 
start
a 100
b 200
Uncaught (in promise) 300
*/

// 第三题
async function async1() {
  console.log("async1 start");
  await async2();
  // await 的后面,都可以做是 callback 里的内容,即异步
  console.log("async1 end");
}

async function async2() {
  console.log("async2");
}

console.log("script start");
async1();
console.log("script end");
/* 
答案: 
script start
async1 start
async2
script end
async1 end
*/

for...of 常用于异步遍历

function muti(num) {
  return new Promise((resolve) => {
    setTimeout(() => {
      resolve(num * num);
    }, 1000);
  });
}

const nums = [1, 2, 3];

nums.forEach(async (i) => {
  const res = await muti(i);
  console.log(res);
});
/* 
一秒后同时打印
1
4
9
*/

(async () => {
  for (let i of nums) {
    const res = await muti(i);
    console.log(res);
  }
})();
/* 
隔1秒打印
1
4
9
*/

请描述 event loop (事件循环/事件轮询)的机制,可画图
JS 是单线程的,异步要基于回调来实现,event loop 就是基于回调的实现原理。

版本1:

  1. 同步代码,一行一行放在执行栈执行
  2. 遇到异步,会先“记录”下,等待时机(定时、网络请求等)
  3. 时机到了,就移到任务队列
  4. 当执行栈为空(即同步代码执行完)Event Loop 开始工作
  5. 轮询查找任务队列,如有则移到执行栈执行
  6. 然后继续轮询查找

版本2:

  1. 同步执行代码,一行一行放在执行栈执行
  2. 遇到异步,微任务会放入微任务队列中,宏任务会会先“记录”下,等待时机(定时、网络请求等),时机到了,就移到任务队列
  3. 当执行栈为空 (即同步代码执行完),会先执行当前的微任务,然后再尝试 DOM 渲染,最后触发 event loop
  4. 轮询查找任务队列,如有则移到到执行栈执行
  5. 然后继续轮询查找

image.png

event loop 和 DOM 渲染
JS 是单线程的,而且和 DOM 渲染共用一个线程,JS 执行的时候,得留一些时机供 DOM 渲染。每次 call stack 清空,即同步代码执行完,都是 DOM 重新渲染的机会,DOM 结构如有改变则重新渲染,然后再去触发下一次 Event Loop

什么是宏任务和微任务
宏任务:setTimeout,setInterval,Ajax,DOM 事件
微任务:Promise,async/await
微任务执行时机比宏任务要早

宏任务和微任务和 DOM 渲染的区别
宏任务:DOM 渲染后触发,如 setTimeout
微任务:DOM 渲染前触发,如 Promise
微任务是 ES6 语法规定的,宏任务是由浏览器规定的,遇到微任务会将微任务放到微任务队列中,在 call stack 清空的时候会先执行当前的微任务,然后再尝试 DOM 渲染,最后触发 Event Loop 所以微任务执行时机比宏任务要早

场景题:promise 和 setTimeout 的顺序

console.log(100);
setTimeout(() => {
  console.log(200);
});
Promise.resolve().then(() => {
  console.log(300);
});
console.log(400);
/* 
答案:
100
400
300
200
*/

综合场景题:async/await 的顺序问题

async function async1() {
  console.log("async 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();
}).then(function () {
  console.log("promise2");
});

console.log("script end");

/* 
答案: 
script start
async start
async2
promise1
script end
async1 end
promise2
setTimeout
*/

JS-Web-API

DOM

DOM 是哪种数据结构
DOM 的本质是一颗树 (DOM树)

DOM 操作常用 API
DOM 节点操作
获取节点:getElementById()、getElementsByClassName()、getElementsByTagName()、querySelector()、querySelectorAll()
创建节点:createElement()
添加节点:appendChild()
删除节点:parentNode.removeChild()
插入节点:insertBefore()

元素属性操作
获取元素属性:getAttribute()
修改/新增元素属性:setAttribute()
删除元素属性:removeAttribute()

元素内容操作
获取/设置元素内容:innerHTML()、innerText()、textContent()

查找、添加、删除、移动 DOM 节点的方法
获取节点:getElementById()、getElementsByClassName()、getElementsByTagName()、querySelector()、querySelectorAll()
创建节点:createElement()
添加节点:appendChild()
删除节点:parentNode.removeChild()
移动节点:先获取节点然后通过 appendChild() 移动节点

property 和 attribute 的区别
property:是 DOM 中的属性,是 JavaScript 里的对象。修改对象属性,不会体现到 HTML 结构中。
attribute:是 HTML 标签上的特性,它的值只能够是字符串。修改 HTML 属性,会改变 HTML 结构。

两者都可能引起 DOM 重新渲染,尽量使用 property,如果必须要修改标签结构则使用 attribute。因为 property 会避免一些不必要的重复的渲染,而 attribute 改变标签结构会引起 DOM 重新渲染,比较消耗性能。

一次性插入多个 DOM 节点,考虑性能

<ul id="list"></ul>
const listNode = document.getElementById("list");

// 创建一个文档碎片
const frag = document.createDocumentFragment();

for (let i = 0; i < 10; i += 1) {
  const li = document.createElement("li");
  li.innerHTML = "List item" + i;
  frag.appendChild(li);
}

// 最后再插入 DOM 中
listNode.appendChild(frag);

如何减少 DOM 操作
缓存 DOM 查询结果
多次 DOM 操作,合并到一次插入

BOM

如何识别浏览器的类型
navigator.userAgent 可以识别

const ua = navigator.userAgent
const isChrome = ua.indexOf('Chrome')

分析拆解 url 的各个部分

// https://coding.imooc.com/class/chapter/400.html?a=100&b=200#Anchor

location.href // https://coding.imooc.com/class/chapter/400.html?a=100&b=200#Anchor
location.protocol  // "https:"
location.host  // "coding.imooc.com"
location.pathname  // "/class/chapter/400.html"
location.search  // "?a=100&b=200"
location.hash  // "#Anchor"

获取当前页面 url 参数

// URLSearchParams
function getQueryVariable(name) {
  const search = location.search;
  const p = new URLSearchParams(search);
  return p.get(name);
}

// 字符串方法
function getQueryVariable(name) {
  const s = location.search.substr(1).split("&");
  for (let i of s) {
    const [key, val] = i.split("=");
    if (key === name) return val;
  }
}

function getQueryVariable(name) {
  const s = location.search;
  let tmp = [];
  let value = "";
  if (s) tmp = s.substr(1).split("&");
  for (const i of tmp) {
    if (i.substring(0, i.indexOf("=")) === name) {
      value = i.substr(i.indexOf("=") + 1);
      break;
    }
  }
  return value;
}

// 正则
function getQueryVariable(name) {
  const search = location.search.substr(1);
  const reg = new RegExp(`(^|&)${name}=([^&]*)(&|$)`, "i");
  const res = search.match(reg);
  if (res === null) return null;
  return res[2];
}

将 url 参数解析为 JS 对象

// 使用 URLSearchParams
function query2obj() {
  const res = {};
  const s = new URLSearchParams(location.search);
  s.forEach((val, key) => {
    res[key] = val;
  });
  return res;
}

// 字符串方法
function query2obj() {
  const res = {};
  const search = location.search.substr(1);
  search.split("&").forEach((s) => {
    const [key, val] = s.split("=");
    res[key] = val;
  });
  return res;
}

事件

编写一个通用的事件监听函数

/**
 * 事件监听函数
 * @param {*} elem 需要监听的元素
 * @param {String} type 事件类型
 * @param {String} selector 子元素选择器
 * @param {Function} fn 回调函数
 */
function bindEvent(elem, type, selector, fn) {
  if (fn == null) {
    fn = selector;
    selector = null;
  }
  elem.addEventListener(type, (event) => {
    const target = event.target;
    if (selector) {
      // 代理绑定
      if (target.matches(selector)) {
        fn.call(target, event);
      }
    } else {
      // 普通绑定
      fn.call(target, event);
    }
  });
}

描述事件冒泡的流程
基于 DOM 树形结构,事件会顺着触发元素往上传递
应用场景:代理

描述一下事件传递的三个阶段
捕获阶段:事件(从 Window)向下走近元素。
目标阶段:事件到达目标元素。
冒泡阶段:事件从元素上开始冒泡。

事件代理(委托)是什么?
把原本需要绑定在子元素的响应事件委托给父元素,让父元素担当事件监听的职务。事件代理的原理是DOM元素的事件冒泡。

如何阻止事件冒泡和默认行为
event.stopPropagation()
event.preventDefault()

无限下拉的图片列表,如何监听每个图片的点击
使用事件代理,用 e.target 获取触发元素,通过 matches 来判断是否是触发元素

Ajax

手写一个简易的 ajax

const xhr = new XMLHttpRequest();
xhr.open("GET", "/js-web-api/data/test.json", true);
xhr.send();
xhr.onreadystatechange = function () {
  if (xhr.readyState === 4) {
    if ((xhr.status >= 200 && xhr.status < 300) || xhr.status == 304) {
      console.log("请求成功", xhr.responseText);
    }
  }
};

xhr.status
2xx 表示成功处理请求如 200
3xx 需要重定向,浏览器直接跳转,如 301 302 304
4xx 客户端请求错误,如 404 403
5xx 服务端错误

什么是跨域
同源策略
ajax 请求时,浏览器要求当前网页和 server 必须同源(安全)
同源:协议、域名、端口,三者必须一致
非同源即跨域

跨域的常用解决方案
所有的跨域,都必须经过 server 端允许和配合。
未经 server 端允许就实现跨域,说明浏览器有漏洞。
JSONP
<script> 可绕过跨域限制,服务端跨域任意动态拼接数据返回。所以,<script> 就可以获得跨域的数据,只要服务端愿意返回。
CORS
服务端设置 http header

解释 jsonp 原理,为何它不是真正的 ajax
jsonp的原理:利用浏览器可以动态地插入一段 js 并执行的特点完成的。

ajax 和 jsonp 本质上是不同的东西。ajax 的核心是通过 XmlHttpRequest 获取非本页内容,而 jsonp 的核心则是动态添加 <script> 标签来调用服务器提供的js脚本。

存储

描述 cookie localStorage sessionStorage 的区别
在同一浏览器下生命周期不同
Cookie 生命周期: 默认是关闭浏览器后失效, 但是也可以设置过期时间
SessionStorage 生命周期: 仅在当前会话(窗口)下有效,关闭窗口或浏览器后被清除, 不能设置过期时间
LocalStorage 生命周期: 除非被清除,否则永久保存

容量不同
Cookie 容量限制: 大小(4KB左右)和个数(20~50)
SessionStorage 和 LocalStorage 容量限制: 大小(5M左右)

网络请求不同
Cookie 网络请求: 每次都会携带在 HTTP 请求头中,如果使用 cookie 保存过多数据会带来性能问题 SessionStorage 和 LocalStorage 网络请求: 仅在浏览器中保存,不参与和服务器的通信

API 易用性不同
Cookie:需要程序员自己封装,源生的 Cookie 接口不友好
SessionStorage 和 LocalStorage:源生接口可以接受,亦可再次封装来对 Object 和 Array 有更好的支持

参考文档:jerryzou.com/posts/cooki…

HTTP

http 状态码

http 常见的状态码有哪些
1xx 服务器收到请求
2xx 请求成功,如 200
3xx 重定向,如 302
4xx 客户端错误,如 404
5xx 服务端错误,如 500

常见状态码:
200 请求成功
301 永久重定向(配合 location,浏览器自动处理)
302 临时重定向(配合 location,浏览器自动处理)
304 资源未被修改
404 资源未找到
403 没有权限
500 服务端错误
504 网关超时

Restful API

什么是 Restful API
一种 API 设计方法
传统 API 设计:把每一个 url 当做一个功能
Restful API 设计:把每一个 url 当做一个唯一的资源

如何设计成一个资源?
1.尽量不用 url 参数
2.用 method 表示操作类型

// 不用 url 参数
传统 API 设计:/api/list?pageIndex=2
Restful API 设计:/api/list/2

// 用 method 表示操作类型
传统:
post 请求: /api/create-blog
post 请求: /api/update-blog?id=100
get 请求: /api/get-blog?id=100
Restful API:
post 请求: /api/blog
patch 请求: /api/blog/100
get 请求: /api/blog/100

http headers

http 常见的 header 有哪些
常见的 Request Headers

Accept:浏览器可接收的数据格式
Accept-Encoding:浏览器可接收的压缩算法,如 gzip
Accept-Languange:浏览器可接收的语言,如 zh-CN
Connection:keep-alive 一次 TCP 连接重复使用
cookie
Content-type:发送的数据格式,如 application/json
Host:请求的域名
User-Agent(简称 UA )浏览器信息

常见的 Response Headers

Content-type:返回数据的格式,如 application/json
Content-length:返回的数据大小,多少字节
Content-Encoding:返回数据的压缩算法,如 gzip
Set-Cookie:服务端修改 cookie

缓存相关 headers

Cache-Control
Exprise
Last-Modified
If-Modified-Since
Etag
If-None-Match

http 的缓存机制

描述一下 http 的缓存机制(重要)
缓存是一种保存资源副本并在下次请求时直接使用该副本的技术,当浏览器发现请求的资源已经被存储,它会拦截请求,返回该资源的拷贝,而不会去服务器重新下载。
这样可以加快资源获取速度,提升用户体验,减少网络传输,缓解服务端的压力。

http 缓存策略
强制缓存
不需要发送请求到服务端,直接读取浏览器本地缓存,是否强制缓存由 Expires、Cache-Control 和 Pragma 3 个 Response Header 属性共同来控制。

Expires
Expires 的值为服务端返回的到期时间,即下一次请求时,请求时间小于服务端返回的到期时间,直接使用缓存数据。 到期时间是由服务端生成的,但是客户端时间可能跟服务端时间有误差,这就会导致缓存命中的误差。
Expires 的优先级在三个 Header 属性中是最低的。

Cache-Control
Cache-Control 是 HTTP/1.1 中新增的属性,在请求头和响应头中都可以使用,常用的属性值如有:

  • max-age: 缓存的内容将在 xxx 秒后失效
  • no-cache:不使用强制缓存,需要协商缓存
  • no-store:禁止使用缓存(包括协商缓存),每次都向服务器请求最新的资源
  • private:专用于个人的缓存,中间代理、CDN 等不能缓存此响应
  • public:响应可以被中间代理、CDN 等缓存

Pragma
Pragma 只有一个属性值,就是 no-cache ,效果和 Cache-Control 中的 no-cache 一致,不使用强缓存,需要协商缓存,在 3 个头部属性中的优先级最高。

协商缓存
协商缓存是一种服务端缓存策略,浏览器第一次请求数据时,服务器会将缓存标识与数据一起返回给客户端,客户端将二者备份至本地缓存中。
再次请求数据时,客户端将备份的缓存标识发送给服务器,服务器根据缓存标识进行判断,判断成功后,返回304状态码,通知客户端比较成功,可以使用缓存数据,否则返回 200 和最新的资源和资源标识。

一共分为两种标识标识
Last-Modified / If-Modified-Since
Last-Modified/If-Modified-Since 的值代表的是文件的最后修改时间,第一次请求服务端会把资源的最后修改时间放到 Last-Modified 响应头中,第二次发起请求的时候,请求头会带上上一次响应头中的 Last-Modified 的时间,并放到 If-Modified-Since 请求头属性中,服务端根据文件最后一次修改时间和 If-Modified-Since 的值进行比较,如果相等,返回 304 ,并加载浏览器缓存。否则返回新的资源和新的 Last-Modified。

Etag / If-None-Match(优先级高于 Last-Modified / If-Modified-Since)
ETag/If-None-Match 的值代表资源的唯一标识,初次请求时,服务端会返回资源和 Etag,再次请求的时会将 Etag 的值放到请求头的 if-None-Match 中。服务器收到请求后发现有头 If-None-Match 则与被请求资源的唯一标识进行比对,相同,说明资源无新修改,则返回 304,告知浏览器继续使用所保存的缓存;不同,说明资源又被改动过,则返回新的资源和新的 Etag。

Last-Modified 和 Etag
会优先使用 Etag
Last-Modified 只能精确到秒级
如果资源被重复生成,而内容不变,则 Etag 更精准

http 缓存-综述
对于强制缓存,服务器通知浏览器一个缓存时间,在缓存时间内,下次请求,直接用缓存,不在时间内,执行协商缓存策略。
对于协商缓存,浏览器会将缓存信息中的 Etag 和 Last-Modified 通过请求发送给服务器,由服务器校验,返回 304 状态码时,浏览器直接使用缓存。

image.png

image.png

刷新操作方式,对缓存的影响
正常操作:地址栏输入 url,跳转连接,前进后退等
强制缓存有效,协商缓存有效
手动刷新:F5,点击刷新按钮,右击菜单刷新
强制缓存失效,协商缓存有效
强制刷新:ctrl + F5
强制缓存和协商缓存都失效

http & tcp

http 和 https 的区别
HTTP 是明文传输,HTTPS 通过 SSL/TLS 进行了加密,HTTPS 要比 HTTP 安全。
HTTP 的端口号是 80,HTTPS 是 443
HTTPS 需要到 CA 申请证书,一般需要一定的费用
HTTP 页面响应比 HTTPS 快,主要因为 HTTP 使用 3 次握手建立连接,而 HTTPS 还需要经历一个 SSL 协商的过程

http1.0 1.1 2.0 的区别
http 1.0 和 http 1.1 的区别

  • 长连接
    http 1.0 打开一个 tcp/ip 连接后只用来发送一个 http 请求,发完后 tcp/ip 连接关闭。
    http 1.1 打开一个 tcp/ip 连接后可以发送多个 http 请求,发完后如果上一个 http 请求报文中有 connection: keep-alive,服务端在返回应答报文以后不把 tcp/ip 连接关闭,如果是 connection: close,那么 tcp/ip 连接关闭。
  • 节约带宽
    HTTP/1.0 一次只能请求一整个资源对象,而 HTTP/1.1 可以请求一个资源对象的一部分,因此在不需要得到整个资源对象时,可节约带宽
  • host 域
    由于一台物理服务器上可以存在多个虚拟主机,并且它们共享一个 IP 地址,因此 HTTP1.1 在 HTTP1.0 的基础上加了改进,加了一个 Host 域,用于指定共享同一个 IP 地址中的某一台主机,而 HTTP1.0 则默认一个 IP 地址只能属于一台主机,没有 Host 域
  • 缓存
    HTTP1.0 缓存的资源对象到了一定时间之后会失效,不能再次使用;而 HTTP1.1 缓存的资源对象失效后还能与源服务器进行重新激活。

http 1.1 和 http 2.0 的区别

  • 二进制分帧层
    就是在应用层与传输层之间添加一层二进制分帧层。HTTP2.0是二进制协议,他采用二进制格式传输数据而不是1.x的文本格式。文本格式对计算机解析不友好。
    HTTP1.x 的解析是基于文本。基于文本协议的格式解析存在天然缺陷,文本的表现形式有多样性,要做到健壮性考虑的场景必然很多,二进制则不同,只认 0 和 1 的组合。基于这种考虑 HTTP2.0 的协议解析决定采用二进制格式,实现方便且健壮。
  • 多路复用
    HTTP2.0 同一个连接可以并发处理多个请求,而且并发请求的数量比 HTTP1.1 大了好几个数量级,这意味着减少了建立连接所需要的开销
  • 数据压缩
    HTTP 的请求和响应包括三个部分,即状态行,头部信息,消息主体。HTTP1.1 只对消息主体进行压缩,而HTTP2.0 对状态行,头部信息,消息主体都进行压缩
  • 服务器推送
    在使用 HTTP1.1 时,客户端请求什么资源,服务器才给什么;而 HTTP2.0 服务器会自动把客户端一定需要的资源传输给客户端,比如一些必要的附加资源等等
    比方说,一个资源服务器上有 html,css,js,http 1.1 会在浏览器解析 html 的时候再次发送请求,请求 css 和 js 资源。但是 http 2.0 会在浏览器第一次请求 html 的时候,检查这个 html 引用的资源,如果被引用的资源刚好也在这台服务器上,那么服务器会自动地把被引用的资源,比如,css,js 推送给浏览器。

http1.1 怎么升级到 http2.0
使用 Nginx 作为前端页面与后端接口的反向代理服务器(Reverse Proxy),只需要修改一下 Nginx 配置文件就可以升级 HTTP/2了。

注意,在 Nginx 上 开启 HTTP/2 需要 Nginx 1.9.5 以上版本(包括1.9.5),并且需要 OpenSSL 1.0.2 以上版本(包括1.0.2)。
还有一点,虽然 HTTP/2 标准并没有要求加密,但是所有浏览器都要求 HTTP/2 必须加密,这样的话,只有 HTTPS才能升级 HTTP/2。

一切前提没问题的话 (Nginx>=1.9.5,OpenSSL>=1.0.2,HTTPS),只需要修改 1 行配置,在 listen 指令后面添加 http2 :

server {     
    listen 443 ssl http2;
}

重启 Nginx,升级 HTTP/2 就成功了

get 和 post 的区别
get 提交的数据会放在 URL 之后,而 post 参数放在请求主体中。
get 请求只支持 URL 编码,post 请求支持多种编码格式。
get 只支持 ASCII 字符格式的参数,而 post 方法没有限制。
get 提交的数据大小有限制,而 post 方法没有限制。

get 一般用于查询,post 一般用于用户提交操作
安全性:post 易于预防 CSRF

TCP 三次握手
发送端首先给接收端发送一个带 SYN 标志的数据包。接收端收到后,回传一个带有 SYN/ACK 标志的数据包以表示正确传达,并确认信息。最后,发送端再回传一个带 ACK 标志的数据包,代表 “握手” 结束。

为什么要握三次手? 握一次两次不行吗?
若采用两次握手,当第二次握手后就建立连接的话,此时客户端知道服务器能够正常接收到自己发送的数据,而服务器并不知道客户端是否能够收到自己发送的数据。

第 2 次握手传回了 ACK,为什么还要传回 SYN
ACK 是为了告诉客户端发来的数据已经接收无误,而传回 SYN 是为了告诉客户端,服务端收到的消息确实是客户端发送的消息。

TCP 四次挥手

  • 第一次挥手:客户端发送一个 FIN,用来关闭客户端到服务端的数据传送。
  • 第二次挥手:服务端收到 FIN 后,发送一个 ACK 给客户端,确认序号为收到序号 +1。
  • 第三次挥手:服务端发送一个 FIN,用来关闭服务端到客户端的数据传送,也就是告诉客户端,服务端的数据已经发送完毕。
  • 第四次挥手:客户端收到 FIN 后,发送一个 ACK 给服务端,确认序号为收到序号 +1,至此,完成四次挥手。

为什么要挥四次手?
因为 FIN 释放连接报文和 ACK 确认报文是分别在两次握手中传输的。当客户端发出断开连接的通知,服务端可能还用数据要处理,所以会先返回 ACK 确认收到报文。当服务端也没有数据再发送的时候,则发出连接释放的通知,对方确认后才完全关闭 TCP 连接。

TCP 和 UDP 的区别

image.png

TCP 是基于连接的协议,也就是说,在正式收发数据前,必须和对方建立可靠的连接。一个 TCP 连接必须要经过 3 次 “对话” 才能建立起来。
UDP 是与TCP相对应的协议。它是面向非连接的协议,它不与对方建立连接,而是直接就把数据包发送过去!UDP适用于一次只传送少量数据、对可靠性要求不高的应用环境。

介绍下 websocket
版本一:

websocket 是 HTML5 的一个新协议,它允许服务端向客户端传递信息,实现客户端和服务端双工通信

特点: 服务器可以主动向客户端推送信息,客户端也可以主动向服务器发送信息,是真正的双向平等对话,属于服务器推送技术的一种。

  • 与 HTTP 协议有着良好的兼容性。默认端口也是 80 和 443 ,并且握手阶段采用 HTTP 协议,因此握手时不容易屏蔽,能通过各种 HTTP 代理服务器。 
  • 建立在 TCP 协议基础之上,和 http 协议同属于应用层
  • 数据格式比较轻量,性能开销小,通信高效。
  • 可以发送文本,也可以发送二进制数据。 
  • 没有同源限制,客户端可以与任意服务器通信
  • 协议标识符是 ws(如果加密,则为 wss),服务器网址就是 URL,如 ws://localhost:8023

参考文档:juejin.cn/post/684490…

版本二:
webSocket 和 http 一样,同属于应用层协议。它最重要的用途是实现了客户端与服务端之间的全双工通信,当服务端数据变化时,可以第一时间通知到客户端。

除此之外,它与 http 协议不同的地方还有:

http 只能由客户端发起,而 webSocket 是双向的。
webSocket 传输的数据包相对于 http 而言很小,很适合移动端使用
没有同源限制,可以跨域共享资源

参考文档:zhuanlan.zhihu.com/p/32565654

服务端如何知道这是 websocket 请求,而不是 http 请求
这是 websocket 的核心,通过 header 告诉服务器这是 websocket 请求

Upgrade: websocket
Connection: Upgrade

如何实现 websocket 心跳检测 和 断线重连
心跳机制是什么
心跳机制是每隔一段时间会向服务器发送一个数据包,告诉服务器自己还活着,服务器也会回传一个数据包给客户端来确定服务器端也还活着,否则的话,有可能是网络断开连接了。需要重连~

实现心跳检测的思路是:
每隔一段固定的时间,向服务器端发送一个数据包,如果在正常的情况下,服务器会返回一个数据包给客户端,如果客户端通过 onmessage 事件能监听到的话,说明请求正常,这里我们使用了一个定时器,每隔 3 秒的情况下,如果是网络断开的情况下,在指定的时间内服务器端并没有返回心跳响应消息,因此服务器端断开了,因此这个时候我们使用 ws.close 关闭连接,在一段时间后,可以通过 onclose事件监听到。因此在 onclose 事件内,我们可以调用 reconnect 事件进行重连操作。

参考文档:www.cnblogs.com/tugenhua070…

运行环境

页面加载过程

从输入 url 到渲染出整页面的整个过程

  • DNS 解析:域名 -> IP 地址
  • 浏览器根据 IP 地址向服务器发起 http 请求
  • 服务器处理 http 请求, 并返回数据给浏览器
  • 根据 HTML 代码生成 DOM 树
  • 根据 CSS 代码生成 CSSOM
  • 将 DOM 树和 CSSOM 整合形成 Render 树
  • 浏览器根据 Render 树渲染页面
  • 遇到 script 则暂停渲染,优先加载并执行 JS 代码,完成再继续
  • 直至把 Render 渲染完成

DNS 解析过程(原理)
DNS(域名服务器)是进行域名和与之相对应的IP地址转换的服务器。

  • 本地 DNS 服务器收到请求后会首先查询它的缓存记录,如果缓存有这条记录的话就直接返回。
  • 如果没有,本地 DNS 服务器还要向 DNS 根服务器进行查询
  • 根 DNS 服务器拿到这个请求后,告诉本地 DNS 服务器,你可以到域服务器上去继续查询,并给出域服务器的地址。
  • 然后本地 DNS 服务器继续向域服务器发出请求(迭代查询)
  • 最后,本地 DNS 服务器向域名的解析服务器发出请求,这时就能收到一个域名和 IP 地址对应关系,本地 DNS 服务器把 IP 地址返回给用户电脑,并把这个对应关系保存在缓存中,以备下次别的用户查询时,可以直接返回结果,加快网络访问。

网址的解析是一个从右向左的过程: . -> .com -> google.com. -> www.google.com.

为什么建议把 css 放在 head 中
放在 body 底部,在 DOM Tree 构建完成之后开始构建 render Tree ,计算布局然后绘制网页,等 css 文件加载后,开始构建 CSSOM Tree,并和 DOM Tree 一起构建 render Tree,再次计算布局重新绘制;

放在 head 中,先加载 css,构建 CSSOM,同时构建 DOM Tree,CSSOM 和 DOM Tree 构建完成后,构建 render Tree,进行计算布局绘制网页。

总体来看,放在 body 底部要比放在 head 中多一次构建 render Tree,多一次计算布局,多一次绘制,从性能方面来看,不如放在 head 中。再次,放在 body 底部网页会闪现默认样式的 DOM 结构,用户体验不好。

为什么建议把 js 代码放在 body 底部
因为当浏览器解析到 script 的时候,就会立即下载执行,中断 html 的解析过程,如果外部脚本加载时间很长(比如一直无法完成下载),就会造成网页长时间失去响应,浏览器就会呈现“假死”状态,这被称为“阻塞效应”。

window.onload 和 DOMContentLoaded 的区别
window.onload:资源全部加载完才能执行,包括图片、视频等
DOMContentLoaded: DOM 渲染完成即可,图片、视频可能还没下载

document load 和 ready 的区别
load:资源全部加载完才能执行,包括图片、视频等
ready: DOM 渲染完成即可,图片、视频可能还没下载

回流和重绘是什么
回流:回流又称之为 「重排」,因元素的规模,尺寸,布局等改变,而需要重新构建页面,就会触发回流

具体总结为:

  • 页面初始渲染
  • 添加、删除可见的 DOM 元素
  • 改变元素位置,尺寸,内容

触发回流的属性:
盒子模型相关属性:width、height、display、border、border-width…
定位及浮动:position、left、right、top、bottom、float、padding、margin…
文字相关:text-align、overflow、font-weight、font-family、line-height,vertical-align、font-size、white-space…

重绘:元素需要更新属性,而这些属性只是影响到元素的外观,风格而不影响布局,就会触发重绘

触发重绘的属性:
color、border-style、border-radius、outline、visibility、background-color、text-decoration、background、background-image、box-shadow…

回流一定重绘,但是重绘不一定回流

如何减少回流和重绘

  • 用 translate 代替 top
  • 用 opacity 代替 visibility
  • 预先定义好 className,然后统一修改 Dom 的 className
  • 不要把 Dom 结点的属性值放在一个循环里面变成循环变量
  • 让要操作的元素进行“离线处理”,处理完后一起更新
  • 通过 fragment 批量修改DOM
  • 不要使用 table 布局,可能很小的一个小改动会造成整个 table 的重新布局。

浏览器的渲染原理
首先,JavaScript引擎是基于事件驱动单线程执行的,渲染线程负责渲染浏览器界面,但是GUI渲染线程与JS引擎是互斥的,当JS引擎执行时GUI线程会被挂起,GUI的更新也会被保存在一个队列中,等到JS引擎空闲时才有机会被执行。

image.png

性能优化

性能优化原则:
多使用内存、缓存或其他方法
减少 CPU 计算量,减少网络加载耗时

性能优化方法:

  • 让加载更快
    减少资源体积:压缩代码
    减少访问次数:合并代码,SSR 服务端渲染,缓存
    使用更快的网络:CDN
  • 让渲染更快
    CSS 放在 head,JS 放在 body 最下面
    尽早开始执行 JS,用 DOMContentLoaded 触发
    懒加载(图片懒加载,上滑加载更多)
    对 DOM 查询进行缓存
    频繁 DOM 操作,合并到一起插入到 DOM 结构中
    节流 throttle ,防抖 debounce

前端性能如何优化,一般从几个方面考虑
原则:多使用内存、缓存,减少计算、减少网络请求
方向:加载页面,页面渲染,页面操作流畅度

性能监控一般看哪些核心指标

用户体验核心指标定义衡量指标
白屏时间页面开始有内容的时间,在没有内容之前是白屏FP 或 FCP
首屏时间可视区域内容已完全呈现的时间FSP
可交互时间用户第一次可以与页面交互的时间FCI
可流畅交互时间用户第一次可以持续与页面交互的时间TTI

SSR 是什么?SSR 的利弊?
SSR 服务端渲染是指服务器接到客户端请求之后,找到对应的数据并生成对应的视图,然后将包含数据的视图一次性发给客户端,客户端直接渲染即可

优点:
每次请求返回的都是一个独立的网页, 更利于 SEO
不同爬虫工作原理类似,只会爬取源码,不会执行网站的任何脚本。服务端渲染返回给客户端的是已经获取了异步数据并执行 JavaScript 脚本的最终 HTML,网络爬虫就可以抓取到完整页面的信息。

首屏加载快/白屏时间更短(因为服务器返回的网页已经包含数据, 所以下载完 JS/CSS 就可以直接渲染)
相对于客户端渲染,服务端渲染在浏览器请求 URL 之后已经得到了一个带有数据的 HTML 文本,浏览器只需要解析HTML,直接构建 DOM 树就可以。而客户端渲染,需要先得到一个空的 HTML 页面,这个时候页面已经进入白屏,之后还需要经过加载并执行 JavaScript、请求后端服务器获取数据、JavaScript 渲染页面几个过程才可以看到最后的页面。

缺点:
网络传输数据量大
服务端压力较大:本来是通过客户端完成渲染,现在统一到服务端去做。尤其是高并发访问的情况,会大量占用服务端 CPU 资源;
开发条件受限:在服务端渲染中,Vue 只会执行到 beforeMount 之前的生命周期钩子,因此项目引用的第三方的库也不可用其它生命周期钩子,这对引用库的选择产生了很大的限制;

CDN 是什么?加速的原理是什么?
CDN 内容分发网络。是构建在现有互联网基础之上的一层智能虚拟网络,通过在网络各处部署节点服务器,实现将源站内容分发至所有 CDN 节点,使用户可以就近获得所需的内容。CDN 服务缩短了用户查看内容的访问延迟,提高了用户访问网站的响应速度与网站的可用性,解决了网络带宽小、用户访问量大、网点分布不均等问题。

CDN 加速原理:当用户访问使用 CDN 服务的网站时,本地 DNS 服务器通过 CNAME 方式将最终域名请求重定向到 CDN 服务。CDN 通过一组预先定义好的策略(如内容类型、地理区域、网络负载状况等),将当时能够最快响应用户的 CDN 节点 IP 地址提供给用户,使用户可以以最快的速度获得网站内容。

简单的说,CDN 的工作原理就是将源站的资源缓存到位于全球各地的 CDN 节点上,用户请求资源时,就近返回节点上缓存的资源,而不需要每个用户的请求都回源站获取,避免网络拥塞、缓解源站压力,保证用户访问资源的速度和体验

image.png

参考文档:
www.huaweicloud.com/zhishi/cdn0…
www.jianshu.com/p/1dae6e168…

手写节流和防抖
节流和防抖都是优化高频率执行代码的一种手段

定义:
节流:n 秒内只运行一次,若在 n 秒内重复触发,只有一次执行
防抖:n 秒后再执行该事件,若在 n 秒内被重复触发,则重新计时

区别:
节流不管事件触发有多频繁,都会在规定时间内执行一次,而防抖只是在最后一次事件才触发后才执行

应用场景:
节流:
监听滚动事件,比如是否滑到底部自动加载更多
高频点击提交,表单重复提交
防抖:
搜索框联想功能,用户在不断输入值时,用防抖来减少请求次数
调整浏览器窗口大小时,resize 次数过于频繁。只需窗口调整完成后,计算窗口大小,防止重复渲染
手机号邮箱等的输入规则校验

手写节流

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

手写防抖

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

了解过垃圾回收机制吗
垃圾回收就是就是清理掉内存中没用的值。
如果是局部变量,在函数调用结束后即是无用的,可以被回收掉;而全局变量在浏览器卸载页面的时候才会消失。

如果没有垃圾回收机制,适时清理不被引用的值并释放相应的内存空间,JavaScript 解释器将会消耗完系统中所有可用内存,造成系统崩溃。

垃圾回收的两种机制:

  • 标记清除
    1)给所有变量增加一个标记,如果是进入执行环境(比如声明变量),则标记为“进入环境”,如果是结束执行环境(比如执行完相关函数),则标记为“离开环境”;

    2)去掉“进入环境”的变量标记以及被该变量所引用的变量标记(比如闭包);

    3)还存在标记的变量即是需要被清理的变量。

  • 引用计数
    1)声明了一个变量,并且将一个引用类型的值赋值给这个变量,那么这变量的引用次数就加 1;

    2)如果这个变量的值又指向另外一个值,或者说这个变量被重新赋值了,那么以上的引用类型的值的引用次数就减 1;

    3)如此一来,该引用类型的值的引用次数即为 0,垃圾回收器会在运行的时候清理掉引用次数为 0 的值并释放相应的内存空间;

    4)特别注意:引用计数在代码中存在循环引用时会出现问题

内存泄漏和内存溢出是什么
不再用到的内存,没有及时释放,就叫做内存泄漏。
内存溢出就是程序运行所需的内存大于可用内存,就出现内存溢出错误。
内存泄漏的堆积最终会导致内存溢出。

内存泄露的常见原因及处理方式

  • 意外的全局变量
    下面代码中变量 bar 在 foo 函数内,但是 bar 并没有声明.JS 就会默认将它变为全局变量,这样在页面关闭之前都不会被释放.

    function foo(){
        bar = 2
        console.log('bar没有被声明!')
    }
    

    b 没被声明,会变成一个全局变量,在页面关闭之前不会被释放.使用严格模式可以避免.

  • dom 清空时,还存在引用
    很多时候, 为了方便存取, 经常会将 DOM 结点暂时存储到数据结构中.但是在不需要该DOM节点时, 忘记解除对它的引用, 则会造成内存泄露.

    var a = document.getElementById('id');
    document.body.removeChild(a);
    // 不能回收,因为存在变量a对它的引用。虽然我们用 removeChild 移除了,但是还在对象里保存着#的引用,即DOM元素还在内存里面。
    

    解决方法: a = null;
    与此类似情景还有: DOM 节点绑定了事件, 但是在移除的时候没有解除事件绑定,那么仅仅移除 DOM 节点也是没用的

  • 定时器中的内存泄漏
    定时器 setInterval 或者 setTimeout 在不需要使用的时候,没有被 clear,导致定时器的回调函数及其内部依赖的变量都不能被回收,这就会造成内存泄漏。
    解决方式:当不需要 interval 或者 timeout 的时候,调用 clearInterval 或者 clearTimeout

  • 不规范地使用闭包
    闭包本身不会造成内存泄漏,但闭包过多很容易导致内存泄漏。
    闭包如果使用不当,会导致相互循环引用.

    function foo() { 
      var a = {}; 
      function bar() { 
        console.log(a); 
      }; 
      a.fn = bar; 
      return bar; 
    };
    

    bar 和 a 形成了相互循环引用.即使 bar 内什么都没有还是造成了循环引用, 那真正的解决办法就是, 不要将 a.fn = bar.

介绍下 RAF requestAnimationFrame
想要动画流畅,更新频率要 60 帧/s,即 16.67 ms 更新一次视图
setTimeout 要手动控制频率,而 RAF 浏览器会自动控制
后台标签或隐藏 iframe 中,RAF 会暂停,而 setTimeout 依然执行

web 安全

常见的 web 前端攻击方式有哪些?如何预防?
XSS 跨站脚本攻击
是一种代码注入攻击,攻击者通过在目标网站上注入恶意脚本,使之在用户的浏览器上运行。利用这些恶意脚本,攻击者可获取用户的敏感信息如 Cookie、SessionID 等,进而危害数据安全。
XSS攻击可分为三类,分别为DOM 型、反射型、储存型。

  • DOM 型
    DOM即文本对象模型,使用DOM可以允许程序和脚本动态的访问和更新文档的内容、结构和样式。这种方式不需要服务器解析响应的直接参与,触发XSS靠的是浏览器端的DOM解析,可以认为完全是客户端的事情。
  • 反射型
    反射型XSS也被称为非持久性XSS,是现在最容易出现的一种XSS漏洞。发出请求时,XSS代码出现在URL中,最后输入提交到服务器,服务器解析后在响应内容中出现这段XSS代码,最后浏览器解析执行。
  • 储存型
    存储型XSS又被称为持久性XSS,它是最危险的一种跨站脚本,相比反射型XSS和DOM型XSS具有更高的隐蔽性,所以危害更大,它不需要用户手动触发。 当攻击者提交一段XSS代码后,被服务器端接收并存储,当所有浏览者访问某个页面时都会被XSS,其中最典型的例子就是留言板。

XSS 预防
输入检查:不要相信用户的任何输入,对输入内容中的 <script>、<iframe> 等标签进行转义或者过滤。
输出检查:不要完全信任服务端,对服务端输出内容有规则的过滤后再输出到页面中。
设置 httpOnly:很多 XSS 攻击目标都是窃取用户 cookie 伪造身份认证,设置此属性可防止 JS 获取 cookie。
开启 CSP:即开启白名单,可阻止白名单以外的资源加载和运行。

CSRF 跨站请求伪造
攻击者诱导受害者进入第三方网站,在第三方网站中,向被攻击网站发送跨站请求。利用受害者在被攻击网站已经获取的注册凭证,绕过后台的用户验证,达到冒充用户对被攻击的网站执行某项操作的目的。

可能造成危害:
利用已通过认证的用户权限更新设定信息等;
利用已通过认证的用户权限购买商品;
利用已通过的用户权限在留言板上发表言论。

防御:
验证码:强制用户必须与应用进行交互,才能完成最终请求。
尽量使用 post ,限制 get 使用;get 太容易被拿来做 csrf 攻击;
请求来源限制,此种方法成本最低,但是并不能保证 100% 有效,因为服务器并不是什么时候都能取到 Referer,而且低版本的浏览器存在伪造 Referer 的风险。
token 验证 CSRF 防御机制是公认最合适的方案。

XSS 和 CSRF 的区别
本质上讲,XSS 是代码注入问题,CSRF 是 HTTP 问题。 XSS 是内容没有过滤导致浏览器将攻击者的输入当代码执行。CSRF 则是因为浏览器在发送 HTTP 请求时候自动带上 cookie,而一般网站的 session 都存在 cookie里面。

关于 Web 密码学你了解哪些呢?
对称加密算法
对称加密算法就是加密和解密使用同一个密钥,简单粗暴
常见的经典对称加密算法有 DES、AES(AES-128)、IDEA、国密SM1、国密SM4

非对称加密算法
非对称加密就是加密和解密使用不同的密钥。发送方使用公钥对信息进行加密,接收方收到密文后,使用私钥进行解密。
主要解决了密钥分发的难题
我们常说的签名就是私钥加密
常见的经典非对称加密算法有 RSA、ECC和国密SM2

散列算法
不可逆性、鲁棒性、唯一性
MD5、SHA(SHA-256)、国密SM3
使用时记得加盐

https 为什么更安全
http 是 HTTP 协议运行在 TCP 之上。所有传输的内容都是明文,客户端和服务器端都无法验证对方的身份。

https 是在 HTTP(应用层) 和 TCP (传输层)之间插入一个 SSL / TSL 协议。所有传输的内容都经过加密,加密采用对称加密,但对称加密的密钥用服务器方的证书进行了非对称加密。此外客户端可以验证服务器端的身份,如果配置了客户端验证,服务器方也可以验证客户端的身份。。

HTTPS 传输数据的流程

  • 浏览器给服务器发送一个请求
  • 服务器将证书交给浏览器
  • 浏览器验证证书是否合法(不合法则自动弹窗提示用户)
  • 浏览器利用权威机构的公钥解密得到服务器的公钥
  • 浏览器随机生成一个密钥(用于对称加密)
  • 浏览器利用服务器的公钥加密随机生成的密钥
  • 浏览器将加密后的密钥发送给服务器
  • 服务器利用自己的私钥解密得到密钥
  • 浏览器和服务器可以开始建立 TCP 连接...

IMG_20210324_191402.jpg.jpg

由于 HTTPS 流程比 HTTP 复杂,所以传输效率会低于 HTTP,但是安全性高于 HTTP

参考文档:www.ershicimi.com/p/ce8469e8b…

八股文

进程和线程的区别是什么

  • 根本区别:进程是操作系统资源分配的基本单位,而线程是任务调度和执行的基本单位

  • 在开销方面:每个进程都有独立的代码和数据空间(程序上下文),程序之间的切换会有较大的开销;线程可以看做轻量级的进程,同一类线程共享代码和数据空间,每个线程都有自己独立的运行栈和程序计数器(PC),线程之间切换的开销小。

  • 所处环境:在操作系统中能同时运行多个进程(程序);而在同一个进程(程序)中有多个线程同时执行(通过CPU调度,在每个时间片中只有一个线程执行)

  • 内存分配方面:系统在运行的时候会为每个进程分配不同的内存空间;而对线程而言,除了CPU外,系统不会为线程分配内存(线程所使用的资源来自其所属进程的资源),线程组之间只能共享资源。

  • 包含关系:没有线程的进程可以看做是单线程的,如果一个进程内有多个线程,则执行过程不是一条线的,而是多条线(线程)共同完成的;线程是进程的一部分,所以线程也被称为轻权进程或者轻量级进程