「查缺补漏」我的2020前端面试秘籍,为你秋招保驾护航

25,716 阅读57分钟

前言

开门见山,这篇文章,适合初级前端,如果你还在校招的话,或者还在求职的话,可以看看本文,找一找灵感,希望对你们有帮助呀。

先说一下最近个人情况:2020年8月底已经拿到网易有道offer, 这算是我的第一份web前端工作吧,一直以来都是自学前端的,走过很多的弯路,之前的技术栈是Vue.js,目前往react方向走。

这是我的网易面经👉「面经」你可能需要的三轮网易面经

我的感受就是,自己一边梳理知识点,一边总结归纳,收获可能更大,所以打算把我梳理的部分分享出来,篇幅有点长,大家见谅呀。

覆盖的点不是很全,分享给你们,希望你们秋招一切顺利,offer收割机❤️❤️❤️

HTML系列

你是如何理解 HTML 语义化的?

让页面内容结构化,它有如下优点

1、易于用户阅读,样式丢失的时候能让页面呈现清晰的结构。
2、有利于SEO,搜索引擎根据标签来确定上下文和各个关键字的权重。
3、方便其他设备解析,如盲人阅读器根据语义渲染网页
4、有利于开发和维护,语义化更具可读性,代码更好维护,与CSS3关系更和谐

如:

<header>代表头部
<nav>代表超链接区域
<main>定义文档主要内容
<article>可以表示文章、博客等内容
<aside>通常表示侧边栏或嵌入内容
<footer>代表尾部

可以跟面试官讲的更具体一点👇

  • 第一个是荒野阶段,那时候前端的代码主要是后台来写的,所以那个时候写的代码主要是用table来布局的。

  • 第二阶段---美工阶段,这个时候就有专门的人来前端代码了,这个阶段的布局主要是DIV+CSS布局,但是呢有一个问题,就是不够语义化。

  • 第三个阶段-->> 前端阶段,也就是利用具有语义的标签,比如p,h1,h2,article,header,nav,main,aside,footer这些标签,使用这些正确的标签,可以表达正确的内容,也利于开发和维护。

meta viewport 是做什么用的,怎么写?

通常viewport是指视窗、视口。浏览器上(也可能是一个app中的webview)用来显示网页的那部分区域。在移动端和pc端视口是不同的,pc端的视口是浏览器窗口区域,而在移动端有三个不同的视口概念:布局视口、视觉视口、理想视口

meta有两个属性name 和 http-equiv

name

  • keywords(关键字) 告诉搜索引擎,你网页的关键字

  • description(网站内容描述) 用于告诉搜索引擎,你网站的主要内容。

  • viewport(移动端的窗口) 后面介绍

  • robots(定义搜索引擎爬虫的索引方式) robots用来告诉爬虫哪些页面需要索引,哪些页面不需要索引

  • author(作者)

  • generator(网页制作软件)

  • copyright(版权)

http-equiv

http-equiv顾名思义,相当于http的文件头作用。

有以下参数:

  • content-Type 设定网页字符集

    //旧的HTML,不推荐

    //HTML5设定网页字符集的方式,推荐使用UTF-8

  • X-UA-Compatible(浏览器采用哪种版本来渲染页面)

    //指定IE和Chrome使用最新版本渲染当前页面

  • cache-control(请求和响应遵循的缓存机制)

  • expires(网页到期时间)

你用过哪些 HTML 5 标签?

有<header>、<footer>、<aside>、<nav>、<video>、<audio>、<canvas>等...

canvas画布

const ctx = canvas.getContext('2d');  // 获取它的2d上下文
ctx.fillStyle = 'green';    // 设置笔刷的填充色
ctx.fillRect(10, 10, 100, 100);  //  利用画笔范围,矩形,比如圆

video

autoplay 布尔属性;视频马上自动开始播放,不会停下来等着数据载入结束。

controls 提供用户控制,允许用户控制视频的播放,包括音量,跨帧,暂停/恢复播放。

loop 布尔属性;指定后,会在视频结尾的地方,自动返回视频开始的地方。

track标签表示的是字幕

poster 表示的是封面

<track kind="subtitles" src="foo.en.vtt" srclang="en" label="English">

H5 是什么?-->>移动端页面

h5一般指的是开一个WebView来加载页面吧,

WebView是一种控件,它基于webkit引擎,因此具备渲染Web页面的功能。

基于Webview的混合开发,就是在 Anddroid (安卓)/(苹果)原生APP里,通过WebView控件嵌入Web页面。

很多APP都是外边套原生APP的壳,内容是H5页面(基于html+css+js的Web页面)。现在的移动端混合开发软件,如果对于交互渲染要求不是特别高的项目,基本都是这么玩的。

WebView作用

  • 显示和渲染Web页面
  • 直接使用html文件(网络上或本地assets中)作布局
  • 可和JavaScript交互调用

HTML5新特性:

  1. 本地存储特性
  2. 设备兼容特性 HTML5提供了前所未有的数据与应用接入开放接口
  3. 连接特性 WebSockets
  4. 网页多媒体特性 支持Audio Video SVG Canvas WebGL CSS3
  5. CSS3特性

增加拖放API地理定位SVG绘图canvas绘图Web WorkerWebSocket

区分普通显示屏和高清屏

  • 当devicePixelRatio值等于1时(也就是最小值),那么它普通显示屏。
  • 当devicePixelRatio值大于1(通常是1.5、2.0),那么它就是高清显示屏。
  • 不同像素的图利用媒体查询结合 devicePixelRatio 可以区分普通显示屏和高清显示屏

并给出了如下CSS设计方案:

.css{/* 普通显示屏(设备像素比例小于等于1.3)使用1倍的图 */ 
    background-image: url(img_1x.png);
}
@media only screen and (-webkit-min-device-pixel-ratio:1.5){
.css{/* 高清显示屏(设备像素比例大于等于1.5)使用2倍图  */
    background-image: url(img_2x.png);
  }
}

服务端用nginx对图片进行处理

想要什么样尺寸的图片自己裁切,我们提供了按比例缩放和自定尺寸的裁切方法,地址后拼接字符串就行。

使用更小更快更强,新一代图片格式 WebP

在实测中,webp 格式比 jpg 格式减小约 20%。这对优化用户体验,减少CDN带宽消耗有很好的效果。

如何判断呢

我想到一个解决的方案,就是通过User-Agent信息,可以拿到你的浏览器信息,通过对你的浏览器分类,支持webp放在白名单里,不支持的则为黑名单。判断为白名单,则直接调用,返回webp格式图片;反之,则显示原图。


DOM

事件冒泡

事件会从最内层的元素开始发生,一直向上传播,直到document对象。

<div id="outer">
    <p id="inner">Click me!</p>
</div>

因此上面的例子在事件冒泡的概念下发生click事件的顺序应该是

p -> div -> body -> html -> document

事件捕获

与事件冒泡相反,事件会从最外层开始发生,直到最具体的元素。

上面的例子在事件捕获的概念下发生click事件的顺序应该是

document -> html -> body -> div -> p

所以从上面的图片来看👉1-5是捕获过程,5-6是目标阶段,6-10是冒泡阶段

addEventListener

addEventListener方法用来为一个特定的元素绑定一个事件处理函数,是JavaScript中的常用方法。

 element.addEventListener(event, function, useCapture)

重点来看看第三个参数useCapture

  • true - 事件句柄在捕获阶段执行(即在事件捕获阶段调用处理函数)
  • false- false- 默认。事件句柄在冒泡阶段执行(即表示在事件冒泡的阶段调用事件处理函数)

所以我们通常来说,默认第三个参数不写的话,是按照事件句柄在冒泡执行的。

attachEvent

兼容IE的写法,默认是事件冒泡阶段调用处理函数,写事件名时候要加上"on"前缀("onload"、"onclick"等)。

object.attachEvent(event, function)

事件代理

利用事件流的特性,我们可以使用一种叫做事件代理的方法,其实利用的就是事件冒泡的机制。

<ul id="xxx">下面的内容是子元素1
        <li>li内容>>> <span> 这是span内容123</span></li>
        下面的内容是子元素2
        <li>li内容>>> <span> 这是span内容123</span></li>
        下面的内容是子元素3
        <li>li内容>>> <span> 这是span内容123</span></li>
</ul>

js代码

xxx.addEventListener('click', function (e) {
            console.log(e,e.target)
            if (e.target.tagName.toLowerCase() === 'li') {
                console.log('打印')
            }
})

更加规范的写法👇

		function delegate(element, eventType, selector, fn) {
            element.addEventListener(eventType, e => {
                let el = e.target
                while (!el.matches(selector)) {
                    if (element === el) {
                        el = null
                        break
                    }
                    el = el.parentNode
                }
                el && fn.call(el, e, el)
            },true)
            return element
        }

阻止事件冒泡和默认事件

event.preventDefault()   // 阻止默认事件
event.stopPropagation() //阻止冒泡

实现一个可以拖拽的DIV

<div id="xxx"></div>

分割线-—---

var dragging = false
var position = null

xxx.addEventListener('mousedown',function(e){
  dragging = true
  position = [e.clientX, e.clientY]
})


document.addEventListener('mousemove', function(e){
  if(dragging === false) return null
  console.log('hi')
  const x = e.clientX
  const y = e.clientY
  const deltaX = x - position[0]
  const deltaY = y - position[1]
  const left = parseInt(xxx.style.left || 0)
  const top = parseInt(xxx.style.top || 0)
  xxx.style.left = left + deltaX + 'px'
  xxx.style.top = top + deltaY + 'px'
  position = [x, y]
})
document.addEventListener('mouseup', function(e){
  dragging = false
})

CSS系列

两种盒模型分别说一下

也就是标准盒模型写起来更方便,也更规范吧。

盒模型分为标准盒模型和怪异盒模型(IE模型)

box-sizing:content-box   //标准盒模型
box-sizing:border-box    //怪异盒模型

content-box

默认值,标准盒子模型。 widthheight 只包括内容的宽和高, 不包括边框(border),内边距(padding),外边距(margin)。注意: 内边距、边框和外边距都在这个盒子的外部。 比如说,.box {width: 350px; border: 10px solid black;} 在浏览器中的渲染的实际宽度将是 370px。

尺寸计算公式:

width = 内容的宽度

height = 内容的高度

宽度和高度的计算值都不包含内容的边框(border)和内边距(padding)。

border-box

widthheight 属性包括内容,内边距和边框,但不包括外边距。这是当文档处于 Quirks模式 时Internet Explorer使用的盒模型。注意,填充和边框将在盒子内 , 例如, .box {width: 350px; border: 10px solid black;} 导致在浏览器中呈现的宽度为350px的盒子。内容框不能为负,并且被分配到0,使得不可能使用border-box使元素消失。

尺寸计算公式:

width = border + padding + 内容的宽度

height = border + padding + 内容的高度

注意:如果你在设计页面中,发现内容区被撑爆了,那么就先检查一下border-sizing是什么,最好在引用reset.css的时候,就对border-sizing进行统一设置,方便管理

如何垂直居中?

16种方法实现水平居中垂直居中

水平局中

内联元素宽度默认就是内容的宽度,只需要给父级添加text-align

.wrapper{text-align: center;}

块级元素,将它的margin-left和margin-right设置为auto,并且块级元素一定要设置宽度,否则元素默认为100%宽度,不需要居中。

.inner{
            display: block;
            width: 150px;
            margin: 0 auto;
        }
// 一定要设置宽度,不然就不需要局中了

两个以上的水平局中,可以将其设置为display:inline-block,在设置父级text-align

垂直局中

内联元素,第一种实用的是flex布局,这里局中的值得是相对于父盒子

.wrapper{
            display: flex;
            align-items: center;
        }

第二种,这里面指的局中是相对于自身而言的

.inner{
            height:100px;
            line-height:100px;
}

块级元素

宽高确定情况下,实用 position absolute + 负margin

宽高不确定的情况下,实用position absolute + transform

垂直水平局中

子元素宽高确定的情况下,使用position absolute + 负margin

子元素宽高不确定的,使用position absolute + transform

.inner{
            position: absolute;
            top: 50%;
            left: 50%;
            transform: translate(-50%,-50%);
            background: blue;
        }

当然了flex布局也是可以解决问题的,下面就介绍👇


两列布局

左列定宽,右列自适应

float+margin

.left{
            float: left;
            width: 100px;
            height: 100%;
            background: rebeccapurple;
        }
        .rigth{
            height: 100%;
            margin-left: 100px; /*大于等于#left的宽度*/
            background: blue;
        }

左列自适应,右列定宽

float+overflow

<div class="wrapper">
        <div class="rigth"></div>
        <div class="left"></div>
</div>

css代码👇

.rigth {
            margin-left: 10px;
            /*margin需要定义在#right中*/
            float: right;
            width: 100px;
            height: 100%;
            background-color: #0f0;
        }

        .left {
            overflow: hidden;
            /*触发bfc*/
            height: 100%;
            background-color: blue;
        }

三列布局

两列定宽,一列自适应

使用float+margin实现
<div class="wrapper">
        <div class="left"></div>
        <div class="main"></div>
        <div class="rigth"></div>
    </div>

css代码

.wrapper {
            height: 400px;
            background: red;
            min-width: 500px;
        }

        .left {
            margin-left: 10px;
            float: left;
            width: 100px;
            height: 100%;
            background-color: #0f0;
        }
        .main{
            float: left;
            width: 100px;
            height: 100%;
            margin-left: 20px;
            background: brown;
        }
        .rigth {

            margin-left: 230px;  /*等于#left和#center的宽度之和加上间隔,多出来的就是#right和#center的间隔*/
            height: 100%;
            background-color: blue;
        }

间列自适应宽度,旁边两侧固定宽度

双飞翼布局

实现步骤

  • 三个部分都设定为左浮动,然后设置center的宽度为100%,此时,left和right部分会跳到下一行;
  • 通过设置margin-left为负值让left和right部分回到与center部分同一行;
  • center部分增加一个内层div,并设margin: 0 200px;

html部分

<div class="wrapper">
        
        <div class="main">
            <div class="inner"></div>
        </div>
        <div class="left"></div>
        
        <div class="right"></div>
    </div>

css部分

.wrapper {
            /* //确保中间内容可以显示出来,两倍left宽+right宽 */
            min-width: 600px; 
        }

        .left {
            float: left;
            width: 200px;
            height: 400px;
            background: red;
            margin-left: -100%;
        }

        .main {
            float: left;
            width: 100%;
            height: 500px;
            background: yellow;
        }

        .main .inner {
            /* margin水平方向要是左右两者的宽度 */
            margin: 0 200px;    
            height: 100%;
            border: 2px solid brown;
        }
        .right {
            float: left;
            width: 200px;
            height: 400px;
            background: blue;
            margin-left: -200px;
        }

flex 怎么用,常用属性有哪些?

flex 的核心的概念就是 容器

父容器

justify-content 项目在主轴上的对齐方式

  • flex-start | flex-end | center | space-between | space-around
  • space-between 子容器沿主轴均匀分布,位于首尾两端的子容器与父容器相切。
  • space-around 子容器沿主轴均匀分布,位于首尾两端的子容器到父容器的距离是子容器间距的一半。

align-items 定义项目在侧轴上如何对齐

  • flex-start | flex-end | center | baseline | stretch;
  • baseline: 项目的第一行文字的基线对齐。
  • stretch(默认值):如果项目未设置高度或设为auto,将占满整个容器的高度。

子容器

align-self 单个项目对齐方式

  • align-self: auto | flex-start | flex-end | center | baseline | stretch;

flex:前面三个属性的简写 是flex-grow flex-shrink flex-basis的简写

  • flex-grow 放大比例 根据所设置的比例分配盒子所剩余的空间
  • flex-shrink 缩小比例 设置元素的收缩比例 多出盒子的部分,按照比例的大小砍掉相应的大小,即比例越大,被砍的越大,默认值是1
  • flex-basis 伸缩基准值 项目占据主轴的空间
  • flex-basis 该属性设置元素的宽度或高度,当然width也可以用来设置元素宽度,如果元素上同时出现了width 和flex-basis那么flex-basis会覆盖width的值

flex: 0 1 auto; 默认主轴是row,那么不会去放大比例,如果所有的子元素宽度和大于父元素宽度时,就会按照比例的大小去砍掉相应的大小。

flex-direction 决定主轴的方向 即项目的排列方向

row | row-reverse | column | column-reverse

BFC 是什么?

深入理解BFC和外边距合并(Margin Collapse)

BFC全称是Block Formatting Context,即块格式化上下文。

BFC就是页面上的一个隔离的独立容器,容器里面的子元素不会影响到外面的元素。反之也如此。

下列方式会创建块格式化上下文

  1. 根元素
  2. float属性不为none
  3. position为absolute或fixed
  4. display为inline-block, table-cell, table-caption, flex, inline-flex
  5. overflow不为visible

需要背的条件👇

  1. 浮动元素(元素的 float 不是 none)
  2. 绝对定位元素(元素的 position 为 absolute 或 fixed)
  3. 行内块元素
  4. overflow 值不为 visible 的块元素
  5. 弹性元素(display为 flex 或 inline-flex元素的直接子元素)

BFC布局规则

  1. 内部的Box会在垂直方向,一个接一个地放置。
  2. Box垂直方向的距离由margin决定。属于同一个BFC的两个相邻Box的margin会发生重叠
  3. 每个元素的margin box的左边, 与包含块border box的左边相接触(对于从左往右的格式化,否则相反)。即使存在浮动也是如此。
  4. BFC的区域不会与float box重叠。
  5. BFC就是页面上的一个隔离的独立容器,容器里面的子元素不会影响到外面的元素。反之也如此。
  6. 计算BFC的高度时,浮动元素也参与计算

选择器优先级

css常用选择器

通配符:*
ID选择器:#ID
类选择器:.class
元素选择器:p、a    等
后代选择器:p span、div a   等
伪类选择器:a:hover 等
属性选择器:input[type="text"]  等

css选择器权重

!important -> 行内样式 -> #id -> .class -> 元素和伪元素 -> * -> 继承 -> 默认

CSS新特性

transition:过渡
transform:旋转、缩放、移动或者倾斜
animation:动画
gradient:渐变
shadow:阴影
border-radius:圆角

transition

transition: property duration timing-function delay;
// css属性名称   过渡时间  过渡时间曲线  过渡延迟时间

transform

transform:rotate(30deg)  旋转
transform:translate(100px,20px)  移动
transform:scale(2,1.5);  缩放
transform:skew(30deg,10deg);  扭曲

animation

animation: move 1s linear forwards;
// 定义动画的时间  duration 
// 动画的名称
// 动画的贝塞尔曲线
// animation-fill-mode 属性规定动画在播放之前或之后,其动画效果是否可见。 
// forwards  当动画完成后,保持最后一个属性值	

清除浮动说一下

第一种用伪元素

.clearfix:after{
  content: "";
  display: block;
  clear: both; 
}

 .clearfix{
     zoom: 1; /* IE 兼容*/
 }

第二种给父容器添加 overflow:hidden 或者 auto 样式

overflow:hidden;

三种地位方案

在定位的时候,浏览器就会根据元素的盒类型和上下文对这些元素进行定位,可以说盒就是定位的基本单位。定位时,有三种定位方案,分别是常规流,浮动已经绝对定位。

常规流(Normal flow)

  • 在常规流中,盒一个接着一个排列;
  • 块级格式化上下文里面, 它们竖着排列;
  • 行内格式化上下文里面, 它们横着排列;
  • positionstaticrelative,并且floatnone时会触发常规流;
  • 对于静态定位(static positioning),position: static盒的位置是常规流布局里的位置
  • 对于相对定位(relative positioning),position: relative,盒偏移位置由这些属性定义topbottomleftandright即使有偏移,仍然保留原有的位置,其它常规流不能占用这个位置。

浮动(Floats)

  • 盒称为浮动盒(floating boxes);
  • 它位于当前行的开头或末尾;
  • 导致常规流环绕在它的周边,除非设置 clear 属性;

绝对定位(Absolute positioning)

  • 绝对定位方案,盒从常规流中被移除,不影响常规流的布局;
  • 它的定位相对于它的包含块,相关CSS属性:topbottomleftright
  • 如果元素的属性positionabsolutefixed,它是绝对定位元素;
  • 对于position: absolute,元素定位将相对于最近的一个relativefixedabsolute的父元素,如果没有则相对于body

获取DOM

<div id="css-cell">
        <div class="heart"></div>
        <div class="sun"></div>
</div>
    
 		let oDiv = document.getElementById('css-cell')
        let oDiv1 = document.getElementsByTagName('div')   //集合 根据标签
        let oDiv2 = document.querySelectorAll('div')     // 集合  标签       
        let oDiv3 = document.getElementsByClassName('heart')     // className    

Attribute与Property

attribute:是HTML标签上的某个属性,如id、class、value等以及自定义属性

property:是js获取的DOM对象上的属性值,比如a,你可以将它看作为一个基本的js对象。

let demo11 = oDiv.getAttribute('class');
let demo2 = oDiv.setAttribute('data-name','new-value')

路由规则

可以在不刷新页面的前提下动态改变浏览器地址栏中的URL地址,动态修改页面上所显示资源。

window.history的方法和属性

back() forward() go()

HTML5 新方法:添加和替换历史记录的条目

pushState()

history.pushState(state, title, url); 添加一条历史记录,不刷新页面
  • state : 一个于指定网址相关的状态对象,popstate事件触发时,该对象会传入回调函数中。如果不需要这个对象,此处可以填null。
  • title : 新页面的标题,但是所有浏览器目前都忽略这个值,因此这里可以填null。
  • url : 新的网址,必须与前页面处在同一个域。浏览器的地址栏将显示这个网址。
replaceState
  history.replaceState(state, title, url);  替换当前的历史记录,不刷新页面
  • 这两个API的相同之处是都会操作浏览器的历史记录,而不会引起页面的刷新。
  • 不同之处在于,pushState会增加一条新的历史记录,replaceState则会替换当前的历史记录。
  • 这两个api,加上state改变触发的popstate事件,提供了单页应该的另一种路由方式。
popstate 事件:历史记录发生改变时触发

基于hash(location.hash+hashchange事件)

我们知道location.hash的值就是url中#后面的内容,如http://www.163.com#something

此网址中,location.hash='#something'。

hash满足以下几个特性,才使得其可以实现前端路由:

  1. url中hash值的变化并不会重新加载页面,因为hash是用来指导浏览器行为的,对服务端是无用的,所以不会包括在http请求中。
  2. hash值的改变,都会在浏览器的访问历史中增加一个记录,也就是能通过浏览器的回退、前进按钮控制hash的切换
  3. 我们可以通过hashchange事件,监听到hash值的变化,从而响应不同路径的逻辑处理。
window.addEventListener("hashchange", funcRef, false)

如此一来,我们就可以在hashchange事件里,根据hash值来更新对应的视图,但不会去重新请求页面,同时呢,也在history里增加了一条访问记录,用户也仍然可以通过前进后退键实现UI的切换。

触发hash值的变化有2种方法👇

  • 一种是通过a标签,设置href属性,当标签点击之后,地址栏会改变,同时会触发hashchange事件
<a href="#TianTianUp">to somewhere</a>
  • 另一种是通过js直接赋值给location.hash,也会改变url,触发hashchange事件。
location.hash="#somewhere"

JS系列

JS基础是最重要的一个环节,所以这个专题,我也是梳理总结了很多,毕竟这个是灵魂嘛,那接下来我把我梳理的文章也总结一遍,然后我复习的部分内容也梳理出来了。

往期文章总结

介绍一下js数据类型

基本数据类型,Number、String、Boolean、Undefined、Null、Symbol ,BigInt。

比如Symbol提出是为了解决什么问题?可以往全局变量冲突讲。

比如BigInt,解决的问题是大数问题,超过了安全数,怎么办?

引用数据类型,数组,对象,函数。

可以试着往它们存储问题上面答,基本数据类型的值直接保存在栈中,而复杂数据类型的值保存在堆中,通过使用在栈中保存对应的指针来获取堆中的值。

Number.isFinite & isFinite区别

某种程度上,都是检测有限性的值。两者区别在于,isFinite函数强制将一个非数值的参数转换成数值,如果能转换成数值,然后再去判断是否是有限的

Number.isFinite()检测有穷性的值,这个方法不会强制将一个非数值的参数转换成数值,这就意味着,只有数值类型的值,且是有穷的(finite),才返回 true

Number.isFinite(0)    // true
Number.isFinite('0')  // false
Number.isFinite(Infinity) false
isFinite('0')   // true
isFinite('0')  // true
isFinite(Infinity)  // false

isNaN 和 Number.isNaN 函数的区别?

  • isNaN首先会接受一个参数,参数讲这个转换成数字,任何不能被转换成数值的都返回true,所以对于非数字的参数,也是true,会影响NaN判断
  • Number.isNaN首先判断是不是数字,是数字在去判断是不是NaN,这种方法更准确。
// isNaN('sdasd') true
// isNaN('21N') true
// isNaN(NaN)  true
// isNaN(123) false

我们来看看Number.isNaN

Number.isNaN('1232N')  // false
Number.isNaN('1232')    // false
Number.isNaN(21312)  // false
Number.isNaN('sadas')  // false
Number.isNaN(NaN)   // true

什么是可迭代对象

要成为可迭代对象, 一个对象必须实现 @@iterator 方法。这意味着对象(或者它原型链上的某个对象)必须有一个键为 @@iterator 的属性,可通过常量 Symbol.iterator 访问该属性:

如何判断一个类型是不是可迭代对象

let someString = "hi";
typeof someString[Symbol.iterator];          // "function"

结论

  • 常见的可迭代对象,有Array,Map, Set, String,TypeArray, arguments
  • 可以通过判断Symbol.iterator判断当前变量是否是可迭代对象

arguments对象了解吗

这个arguments有个易错点,容易忽略的点。

首先我们看下它的定义:arguments对象是所有(非箭头)函数中都可用的局部变量。此对象包含传递给函数的每个参数,第一个参数在索引0处。

arguments对象不是一个 Array 。它类似于Array,但除了length属性和索引元素之外没有任何Array属性。

转换成数组👇

let args = Array.prototype.slice.call(arguments)
let args1 = Array.from(arguments)
let args2 = [...arguments]

易错点👇

当非严格模式中的函数没有包含剩余参数、默认参数和解构赋值,那么arguments对象中的值跟踪参数的值(反之亦然),看几个题目懂了

function func(a) { 
  arguments[0] = 99;   // 更新了arguments[0] 同样更新了a
  console.log(a);
}
func(10); // 99

这里arguments就会跟踪a变量👇

function func(a) { 
  a = 99;              // 更新了a 同样更新了arguments[0] 
  console.log(arguments[0]);
}
func(10); // 99

当非严格模式中的函数包含剩余参数、默认参数和解构赋值,那么arguments对象中的值不会跟踪参数的值(反之亦然)。相反, arguments反映了调用时提供的参数:

function func(a = 55) { 
  arguments[0] = 99; // updating arguments[0] does not also update a
  console.log(a);
}
func(10); // 10

并且

function func(a = 55) { 
  a = 99; // updating a does not also update arguments[0]
  console.log(arguments[0]);
}
func(10); // 10

并且

function func(a = 55) { 
  console.log(arguments[0]);
}
func(); // undefined

原型

  • 在js中,我们通常会使用构造函数来创建一个对象,每一个构造函数的内部都有一个prototype属性,这个属性对应的值是一个对象,这个对象它包含了可以由该构造函数的所有实例都共享的属性和方法,我们把它称为原型。
  • 原型分为显示原型隐式原型,一般称prototype为显示原型,__proto__称为隐式原型。
  • 一般而言,__proto__这个指针我们应该获取这个值,但是浏览器中都实现了 __proto__ 属性来让我们访问这个属性,但是我们最好不要使用这个属性,因为它不是规范中规定的。
  • ES5 中新增了一个 Object.getPrototypeOf() 方法,我们可以通过这个方法来获取对象的原型。

举个例子👇

为什么我们新建的对象可以使用toString()方法,这是因为我们访问一个对象的属性时,首先会在这个对象身上找,如果没有的话,我们会通过这个对象的__proto__找到该对象的原型,然后在这个原型对象中找,这个原型对象又没有的话,就这样子通过一直找下去,这也就是原型链概念。直到找到原型链的尽头也就是Object.prototype。

js 获取原型的方法?

假设Demo是一个对象,那么有三种方式👇

  • Demo.constructor.prototype
  • Demo.__proto__
  • Object.getPrototypeOf(Demo)

获取对象属性的方法

  1. Object.keys(testObj) 返回的参数就是一个数组,数组内包括对象内可枚举属性和方法名
  2. for in 遍历的也可以,不过对于非继承的属性名称也会获取到,通过hasOwnproperty判断
  3. Object.getOwnPropertyNames(obj) 返回的参数就是一个数组,数组内包括自身拥有的枚举或不可枚举属性名称字符串,如果是数组的话,还有可能获取到length属性

for of 和 for in区别

for in

我们直接从一段代码来看

Array.prototype.method=function(){
  console.log(this.length);
}
var myArray=[1,2,4,5,6,7]
myArray.name="数组"
for (var index in myArray) {
  console.log(myArray[index]);
}

有哪些缺陷呢👇

  • index获取的是索引
  • 遍历的顺序可能不是按照顺序进行的
  • 使用for in 会遍历数组所有可枚举属性,包括原型。例如上面的method和name都会遍历
  • for in 更适合遍历对象,不要使用for in去遍历数组

for of

Array.prototype.method=function(){
  console.log(this.length);
}
var myArray=[1,2,4,5,6,7]
myArray.name="数组";
for (var value of myArray) {
  console.log(value);
}
  • for of语法遍历的是数组元素的值
  • for in 遍历的是索引
  • for of遍历的只是数组内的元素,而不包括数组的原型属性method和索引name

小结

  • for..of适用遍历数/数组对象/字符串/map/set等拥有迭代器对象的集合,不能遍历对象,因为没有迭代对象,与forEach()不同的是,它可以正确响应break、continue和return语句。
  • for in 可以遍历一个普通的对象,这样也是它的本质工作,for in会遍历原型以及可枚举属性,最好的情况下,使用hasOwnProperty判断是不是实例属性。

作用域链

作用域 规定了如何查找变量,也就是确定当前执行代码对变量的访问权限。当查找变量的时候,会先从当前上下文的变量对象中查找,如果没有找到,就会从父级(词法层面上的父级)执行上下文的变量对象中查找,一直找到全局上下文的变量对象,也就是全局对象。这样由多个执行上下文的变量对象构成的链表就叫做 作用域链

**函数的作用域在函数创建时就已经确定了。**当函数创建时,会有一个名为 [[scope]] 的内部属性保存所有父变量对象到其中。当函数执行时,会创建一个执行环境,然后通过复制函数的 [[scope]] 属性中的对象构建起执行环境的作用域链,然后,变量对象 VO 被激活生成 AO 并添加到作用域链的前端,完整作用域链创建完成:

Scope = [AO].concat([[Scope]]);

所以闭包,可以说是作用域链的另外一种表示形式。

闭包的应用

闭包的应用比较典型是定义模块,我们将操作函数暴露给外部,而细节隐藏在模块内部

闭包的第一个用途是使我们在函数外部能够访问到函数内部的变量。
通过使用闭包,我们可以通过在外部调用闭包函数,从而在外部访问到函数内部的变量,可以使用这种方法来创建私有变量。


函数的另一个用途是使已经运行结束的函数上下文中的变量对象继续留在内存中,因为闭包函数保留了这个变量对象的引用,所以这个变量对象不会被回收。

ES6 语法知道哪些,分别怎么用?

let const 块级作用域 箭头函数 词法this Class 解构,剩余运算符,Promise等,往这些方面展开。

手写函数防抖和函数节流

节流throttle

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

function throttle(fn, delay) {
            let flag = true,
                timer = null
            return function(...args) {
                let context = this
                if(!flag) return
                
                flag = false
                clearTimeout(timer)
                timer = setTimeout(function() {
                    fn.apply(context,args)
                    flag = true
                },delay)
            }
        }

防抖

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

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

手写AJAX

function ajax(url, method) {
  return new Promise((resolve, reject) => {
    const xhr = new XMLHttpRequest()
    xhr.open(url, method, true)
    xhr.onreadystatechange = function () {
      if (xhr.readyState === 4) {
        if (xhr.status === 200) {
          resolve(xhr.responseText)
        } else if (xhr.status === 404) {
          reject(new Error('404'))
        }
      } else {
        reject('请求数据失败')
      }
    }
    xhr.send(null)
  })
}

数组去重

function unique_3(array) {
    var obj = {};
    return array.filter(function (item, index, array) {
        return obj.hasOwnProperty(typeof item + item) ? false : (obj[typeof item + item] = true)
    })
}

手写bind函数

Function.prototype.mybind = function(context, ...args) {
    return (...newArgs) => {
        return this.call(context, ...args, ...newArgs)
    }
}

实现call

Function.prototype.mycall = function (context, ...args) {
    context = Object(context) || window
    let fn = Symbol(1)
    context[fn] = this
    let result = context[fn](...args)
    delete context[fn]
    return result
}

实现一个快排

function quickSort(arr){

    if (arr.length <= 1) return arr;
    let index = Math.floor(arr.length / 2)
    let pivot = arr.splice(index, 1)[0],
        left = [],
        right = [];
    for(let i = 0; i < arr.length; i++){
        if(pivot > arr[i]){
            left.push(arr[i])
        }else{
            right.push(arr[i])
        }
    }
    return quickSort(left).concat([pivot],quickSort(right))
}

数组的扁平化

function flatDeep(arr) {
    return arr.reduce((res, cur) => {
        if(Array.isArray(cur)){
            return [...res, ...flatDeep(cur)]
        }else{
            return [...res, cur]
        }
    },[])
}

深拷贝

function deepClone(obj, hash = new WeakMap()) {
    if (obj instanceof RegExp) return new RegExp(obj)
    if (obj instanceof Date) return new Date(obj)

    if (obj === null || typeof obj !== 'object') return obj

    if (hash.has(obj)) return obj

    let res = new obj.constructor();
    hash.set(obj, res)
    for (let key in obj) {
        if (obj.hasOwnProperty(key)) {
            res[key] = deepClone(obj[key],hash)
        }
    }
    return res
}

实现高阶函数柯里化

function currying(fn, ...args) {
    if (fn.length > args.length) {
        return (...newArgs) => currying(fn, ...args, ...newArgs)
    } else {
        return fn(...args)
    }
}

寄生组合式继承

function inherit(Son, Father) {
    // 创建对象,创建父类原型的一个副本
    let prototype = Object.create(Father.prototype)
    // 增强对象,弥补因重写原型而失去的默认的constructor 属性
    prototype.construct = Son
     // 指定对象,将新创建的对象赋值给子类的原型
    Son.prototype = prototype
}

this

this 永远指向最后调用它的那个对象

主要有下面几个规则

  • 默认指向,作为普通函数调用,指向window,严格模式下指向undefined
  • 使用call/apply/bind 显示改变this指向
  • new对象,被实例调用,指向的就是实例对象
  • 箭头函数:this指向的是上级作用域中的this
  • class方法:该this指向的就是实例

ECMAScript6 怎么写 class,为什么会出现 class 这种东西?

在我看来 ES6 新添加的 class 只是为了补充 js 中缺少的一些面向对象语言的特性,但本质上来说它只是一种语法糖,不是一个新的东西,其背后还是原型继承的思想。通过加入 class 可以有利于我们更好的组织代码。在 class 中添加的方法,其实是添加在类的原型上的。

哪些操作会造成内存泄漏?

相关知识点:

  • 1.意外的全局变量
  • 2.被遗忘的计时器或回调函数
  • 3.脱离 DOM 的引用
  • 4.闭包
第一种情况是我们由于使用未声明的变量,而意外的创建了一个全局变量,而使这个变量一直留在内存中无法被回收。

第二种情况是我们设置了 setInterval 定时器,而忘记取消它,如果循环函数有对外部变量的引用的话,那么这个变量会被一直留
在内存中,而无法被回收。

第三种情况是我们获取一个 DOM 元素的引用,而后面这个元素被删除,由于我们一直保留了对这个元素的引用,所以它也无法被回
收。

第四种情况是不合理的使用闭包,从而导致某些变量一直被留在内存当中。

Object.is()使用过吗?跟 === 和 == 区别

  • 两等号判等,会在比较时进行类型转换。
  • 三等号判等(判断严格),比较时不进行隐式类型转换,(类型不同则会返回false)
  • 使用 Object.is 来进行相等判断时,一般情况下和三等号的判断相同,它处理了一些特殊的情况,比如 -0 和 +0 不再相等,两个 NaN 认定为是相等的。

JS事件循环机制了解吗

  • 因为 js 是单线程运行的,在代码执行的时候,通过将不同函数的执行上下文压入执行栈中来保证代码的有序执行。
  • 在执行同步代码的时候,如果遇到了异步事件,js 引擎并不会一直等待其返回结果,而是会将这个事件挂起,继续执行执行栈中的其他任务。
  • 当异步事件执行完毕后,再将异步事件对应的回调加入到与当前执行栈中不同的另一个任务队列中等待执行。
  • 任务队列可以分为宏任务对列和微任务对列,当前执行栈中的事件执行完毕后,js 引擎首先会判断微任务对列中是否有任务可以执行,如果有就将微任务队首的事件压入栈中执行。
  • 当微任务对列中的任务都执行完成后再去判断宏任务对列中的任务。

微任务包括了 promise 的回调、node 中的 process.nextTick 、对 Dom 变化监听的 MutationObserver。

宏任务包括了 script 脚本的执行、setTimeout ,setInterval ,setImmediate 一类的定时事件,还有如 I/O 操作、UI 渲染等。

立即执行函数是什么?

声明一个函数,并马上调用这个匿名函数就叫做立即执行函数;也可以说立即执行函数是一种语法,让你的函数在定义以后立即执行;

写法👇

(function () {alert("我是匿名函数")}())   //用括号把整个表达式包起来
(function () {alert("我是匿名函数")})()  //用括号把函数包起来
!function () {alert("我是匿名函数")}()  //求反,我们不在意值是多少,只想通过语法检查
+function () {alert("我是匿名函数")}() 
-function () {alert("我是匿名函数")}() 
~function () {alert("我是匿名函数")}() 
void function () {alert("我是匿名函数")}() 
new function () {alert("我是匿名函数")}() 

作用:

  1. 不必为函数命名,避免了污染全局变量
  2. 立即执行函数内部形成了一个单独的作用域,可以封装一些外部无法读取的私有变量
  3. 封装变量

什么是 JSONP,什么是 CORS,什么是跨域?

这个我有篇文章已经总结啦,所以这里就直接跳到对应文章吧,传送门

发布订阅者模式

class EventEmitter {
    constructor(){
        this.list = {}
    }
    on(key,fn){
        if(!this.list[key]){
            this.list[key] = []
        }
        this.list[key].push(fn)
        return this
    }
    once(key,fn) {
        if(!this.list[key]){
            this.list[key] = []
        }
        this.list[key].push(fn)
        this.list[key].flag = this.list[key].length;
        return this
    }
    emit(key, args){
        let that = this;
        let fns = this.list[key]
        if(!fns || fns.length === 0) return false
        for(let i = 0; i < fns.length; i++) {
            fns[i].apply(this, args)
            if(fns.flag === i){
                that.off(key,fns[i-1])
            }
        }
    }
    off(key,fn) {
        let fns = this.list[key];
        let len = fns.length,
            k = -1;
        for(let i = 0; i < len; i++) {
            if(fns[i].name === fn.name){ // 删除
                k = i;
                break;
            }
        }
        if(k !== -1) {
            this.list[key].splice(k,1)
        }
    }

    allOff(key) {
        if(key === undefined){
            this.list = {}
        }else{
            this.list[key] = []
        }
    }
}

下面是测试数据

var emitter = new EventEmitter();

function handleOne(a, b, c) {
    console.log('第一个监听函数', a, b, c)
}

function handleSecond(a, b, c) {
    console.log('第二个监听函数', a, b, c)
}

function handleThird(a, b, c) {
    console.log('第三个监听函数', a, b, c)
}

emitter.on("demo", handleOne)
    .once("demo", handleSecond)
    .on("demo", handleThird);

emitter.emit('demo', [1, 2, 3]);
// => 第一个监听函数 1 2 3
// => 第二个监听函数 1 2 3
// => 第三个监听函数 1 2 3

emitter.off('demo', handleThird);
emitter.emit('demo', [1, 2, 3]);
// => 第一个监听函数 1 2 3

emitter.allOff();
emitter.emit('demo', [1, 2, 3]);
// nothing

浏览器相关

这个浏览器专题的话,我之前也总结过啦,所以这里就贴出地址,有兴趣的可以去补一补基础知识,大部分的知识点下面也提及到了,就不单独拿出来梳理啦👇

往期文章总结

Cookie V.S. LocalStorage V.S. SessionStorage V.S. Session

其中的一个相同点,就是它们保存在浏览器端,且同源的。

那么不同点是哪些呢👇

异同点

分类生命周期存储容量存储位置
cookie默认保存在内存中,随浏览器关闭失效(如果设置过期时间,在到过期时间后失效)4KB保存在客户端,每次请求时都会带上
localStorage理论上永久有效的,除非主动清除。4.98MB(不同浏览器情况不同,safari 2.49M)保存在客户端,不与服务端交互。节省网络流量
sessionStorage仅在当前网页会话下有效,关闭页面或浏览器后会被清除。4.98MB(部分浏览器没有限制)同上

操作方式

接下来我们来具体看看如何来操作localStoragesessionStorage

let obj = { name: "TianTianUp", age: 18 };
localStorage.setItem("name", "TianTianUp"); 
localStorage.setItem("info", JSON.stringify(obj));
复制代码

接着进入相同的域名时就能拿到相应的值👇

let name = localStorage.getItem("name");
let info = JSON.parse(localStorage.getItem("info"));
复制代码

从这里可以看出,localStorage其实存储的都是字符串,如果是存储对象需要调用JSONstringify方法,并且用JSON.parse来解析成对象。

应用场景

  • localStorage 适合持久化缓存数据,比如页面的默认偏好配置,如官网的logo,存储Base64格式的图片资源等;
  • sessionStorage 适合一次性临时数据保存,存储本次浏览信息记录,这样子页面关闭的话,就不需要这些记录了,还有对表单信息进行维护,这样子页面刷新的话,也不会让表单信息丢失。

什么是 XSS?如何预防?

XSS 全称是 Cross Site Scripting ,为了与CSS区分开来,故简称 XSS,翻译过来就是“跨站脚本”。

XSS是指黑客往 HTML 文件中或者 DOM 中注入恶意脚本,从而在用户浏览页面时利用注入的恶意脚本对用户实施攻击的一种手段。

最开始的时候,这种攻击是通过跨域来实现的,所以叫“跨域脚本”。发展到现在,往HTML文件中中插入恶意代码方式越来越多,所以是否跨域注入脚本已经不是唯一的注入手段了,但是 XSS 这个名字却一直保留至今。

注入恶意脚本可以完成这些事情:

  1. 窃取Cookie
  2. 监听用户行为,比如输入账号密码后之间发给黑客服务器
  3. 在网页中生成浮窗广告
  4. 修改DOM伪造登入表单

一般的情况下,XSS攻击有三种实现方式

  • 存储型 XSS 攻击
  • 反射型 XSS 攻击
  • 基于 DOM 的 XSS 攻击

存储型 XSS 攻击

存储型 XSS 攻击大致步骤如下:

  1. 首先黑客利用站点漏洞将一段恶意 JavaScript 代码提交到网站的数据库中;
  2. 然后用户向网站请求包含了恶意 JavaScript 脚本的页面;
  3. 当用户浏览该页面的时候,恶意脚本就会将用户的 Cookie 信息等数据上传到服务器。

比如常见的场景:

在评论区提交一份脚本代码,假设前后端没有做好转义工作,那内容上传到服务器,在页面渲染的时候就会直接执行,相当于执行一段未知的JS代码。这就是存储型 XSS 攻击。

反射型 XSS 攻击

反射型 XSS 攻击指的就是恶意脚本作为**「网络请求的一部分」**,随后网站又把恶意的JavaScript脚本返回给用户,当恶意 JavaScript 脚本在用户页面中被执行时,黑客就可以利用该脚本做一些恶意操作。

举个例子:

http://TianTianUp.com?query=<script>alert("你受到了XSS攻击")</script>
复制代码

如上,服务器拿到后解析参数query,最后将内容返回给浏览器,浏览器将这些内容作为HTML的一部分解析,发现是Javascript脚本,直接执行,这样子被XSS攻击了。

这也就是反射型名字的由来,将恶意脚本作为参数,通过网络请求,最后经过服务器,在反射到HTML文档中,执行解析。

主要注意的就是,「服务器不会存储这些恶意的脚本,这也算是和存储型XSS攻击的区别吧」

基于 DOM 的 XSS 攻击

基于 DOM 的 XSS 攻击是不牵涉到页面 Web 服务器的。具体来讲,黑客通过各种手段将恶意脚本注入用户的页面中,在数据传输的时候劫持网络数据包

常见的劫持手段有:

  • WIFI路由器劫持
  • 本地恶意软件

阻止 XSS 攻击的策略

以上讲述的XSS攻击原理,都有一个共同点:让恶意脚本直接在浏览器执行。

针对三种不同形式的XSS攻击,有以下三种解决办法

对输入脚本进行过滤或转码

对用户输入的信息过滤或者是转码

举个例子👇

转码后👇

&lt;script&gt;alert(&#39;你受到XSS攻击了&#39;)&lt;/script&gt;

这样的代码在 html 解析的过程中是无法执行的。

当然了对于<script><img><a>等关键字标签也是可以过来的,效果如下👇

最后什么都没有剩下了

利用 CSP

该安全策略的实现基于一个称作 Content-Security-PolicyHTTP 首部。

可以移步MDN,有更加规范的解释。我在这里就是梳理一下吧。

CSP,即浏览器中的内容安全策略,它的核心思想大概就是服务器决定浏览器加载哪些资源,具体来说有几个功能👇

  • 限制加载其他域下的资源文件,这样即使黑客插入了一个 JavaScript 文件,这个 JavaScript 文件也是无法被加载的;
  • 禁止向第三方域提交数据,这样用户数据也不会外泄;
  • 提供上报机制,能帮助我们及时发现 XSS 攻击。
  • 禁止执行内联脚本和未授权的脚本;

利用 HttpOnly

由于很多 XSS 攻击都是来盗用 Cookie 的,因此还可以通过使用 HttpOnly 属性来保护我们 Cookie 的安全。这样子的话,JavaScript 便无法读取 Cookie 的值。这样也能很好的防范 XSS 攻击。

通常服务器可以将某些 Cookie 设置为 HttpOnly 标志,HttpOnly 是服务器通过 HTTP 响应头来设置的,下面是打开 Google 时,HTTP 响应头中的一段:

set-cookie: NID=189=M8l6-z41asXtm2uEwcOC5oh9djkffOMhWqQrlnCtOI; expires=Sat, 18-Apr-2020 06:52:22 GMT; path=/; domain=.google.com; HttpOnly

总结

XSS 攻击是指浏览器中执行恶意脚本, 然后拿到用户的信息进行操作。主要分为存储型反射型文档型。防范的措施包括:

  • 对输入内容过滤或者转码,尤其是类似于<script><img><a>标签
  • 利用CSP
  • 利用Cookie的HttpOnly属性

除了以上策略之外,我们还可以通过添加验证码防止脚本冒充用户提交危险操作。而对于一些不受信任的输入,还可以限制其输入长度,这样可以增大 XSS 攻击的难度。

什么是 CSRF?如何预防?

CSRF 英文全称是 Cross-site request forgery,所以又称为“跨站请求伪造”,是指黑客引诱用户打开黑客的网站,在黑客的网站中,利用用户的登录状态发起的跨站请求。简单来讲,CSRF 攻击就是黑客利用了用户的登录状态,并通过第三方的站点来做一些坏事。

一般的情况下,点开一个诱导你的链接,黑客会在你不知情的时候做哪些事情呢

1. 自动发起 Get 请求

黑客网页里面可能有一段这样的代码👇

 <img src="http://bank.example/withdraw?amount=10000&for=hacker" > 

在受害者访问含有这个img的页面后,浏览器会自动向http://bank.example/withdraw?account=xiaoming&amount=10000&for=hacker发出一次HTTP请求。

bank.example就会收到包含受害者登录信息的一次跨域请求。

2. 自动发起 POST 请求

黑客网页中有一个表单,自动提交的表单👇

 <form action="http://bank.example/withdraw" method=POST>
    <input type="hidden" name="account" value="xiaoming" />
    <input type="hidden" name="amount" value="10000" />
    <input type="hidden" name="for" value="hacker" />
</form>
<script> document.forms[0].submit(); </script> 

访问该页面后,表单会自动提交,相当于模拟用户完成了一次POST操作。

同样也会携带相应的用户 cookie 信息,让服务器误以为是一个正常的用户在操作,让各种恶意的操作变为可能。

3. 引诱用户点击链接

这种需要诱导用户去点击链接才会触发,这类的情况比如在论坛中发布照片,照片中嵌入了恶意链接,或者是以广告的形式去诱导,比如:

 <a href="http://test.com/csrf/withdraw.php?amount=1000&for=hacker" taget="_blank">
  重磅消息!!!
  <a/>

点击后,自动发送 get 请求,接下来和自动发 GET 请求部分同理。

以上三种情况,就是CSRF攻击原理,跟XSS对比的话,CSRF攻击并不需要将恶意代码注入HTML中,而是跳转新的页面,利用服务器的验证漏洞用户之前的登录状态来模拟用户进行操作

防护策略

其实我们可以想到,黑客只能借助受害者的**cookie**骗取服务器的信任,但是黑客并不能凭借拿到cookie,也看不到 cookie的内容。另外,对于服务器返回的结果,由于浏览器同源策略的限制,黑客也无法进行解析。

这就告诉我们,我们要保护的对象是那些可以直接产生数据改变的服务,而对于读取数据的服务,则不需要进行**CSRF**的保护。而保护的关键,是 在请求中放入黑客所不能伪造的信息

用户操作限制——验证码机制

方法:添加验证码来识别是不是用户主动去发起这个请求,由于一定强度的验证码机器无法识别,因此危险网站不能伪造一个完整的请求。

1. 验证来源站点

在服务器端验证请求来源的站点,由于大量的CSRF攻击来自第三方站点,因此服务器跨域禁止来自第三方站点的请求,主要通过HTTP请求头中的两个Header

  • Origin Header
  • Referer Header

这两个Header在浏览器发起请求时,大多数情况会自动带上,并且不能由前端自定义内容。

服务器可以通过解析这两个Header中的域名,确定请求的来源域。

其中,Origin只包含域名信息,而Referer包含了具体的 URL 路径。

在某些情况下,这两者都是可以伪造的,通过AJax中自定义请求头即可,安全性略差。

2. 利用Cookie的SameSite属性

可以看看MDN对此的解释

SameSite可以设置为三个值,StrictLaxNone

  1. Strict模式下,浏览器完全禁止第三方请求携带Cookie。比如请求sanyuan.com网站只能在sanyuan.com域名当中请求才能携带 Cookie,在其他网站请求都不能。
  2. Lax模式,就宽松一点了,但是只能在 get 方法提交表单况或者a 标签发送 get 请求的情况下可以携带 Cookie,其他情况均不能。
  3. 在None模式下,Cookie将在所有上下文中发送,即允许跨域发送。

3. CSRF Token

前面讲到CSRF的另一个特征是,攻击者无法直接窃取到用户的信息(Cookie,Header,网站内容等),仅仅是冒用Cookie中的信息。

那么我们可以使用Token,在不涉及XSS的前提下,一般黑客很难拿到Token。

可以看看这篇文章,将了Token是怎么操作的👉彻底理解cookie,session,token

Token(令牌)做为Web领域验证身份是一个不错的选择,当然了,JWT有兴趣的也可以去了解一下。

Token步骤如下:

第一步:将CSRF Token输出到页面中

首先,用户打开页面的时候,服务器需要给这个用户生成一个Token,该Token通过加密算法对数据进行加密,一般Token都包括随机字符串和时间戳的组合,显然在提交时Token不能再放在Cookie中了(XSS可能会获取Cookie),否则又会被攻击者冒用。因此,为了安全起见Token最好还是存在服务器的Session中,之后在每次页面加载时,使用JS遍历整个DOM树,对于DOM中所有的a和form标签后加入Token。这样可以解决大部分的请求,但是对于在页面加载之后动态生成的HTML代码,这种方法就没有作用,还需要程序员在编码时手动添加Token。

第二步:页面提交的请求携带这个Token

对于GET请求,Token将附在请求地址之后,这样URL 就变成 http://url?csrftoken=tokenvalue。 而对于 POST 请求来说,要在 form 的最后加上: <input type=”hidden” name=”csrftoken” value=”tokenvalue”/> 这样,就把Token以参数的形式加入请求了。

第三步:服务器验证Token是否正确

当用户从客户端得到了Token,再次提交给服务器的时候,服务器需要判断Token的有效性,验证过程是先解密Token,对比加密字符串以及时间戳,如果加密字符串一致且时间未过期,那么这个Token就是有效的。

非常感兴趣的,可以仔细去阅读一下相关的文章,Token是如何加密的,又是如何保证不被攻击者获取道。

总结

CSRF(Cross-site request forgery), 即跨站请求伪造,本质是冲着浏览器分不清发起请求是不是真正的用户本人,所以防范的关键在于在请求中放入黑客所不能伪造的信息。从而防止黑客伪造一个完整的请求欺骗服务器。

防范措施:验证码机制,验证来源站点,利用Cookie的SameSite属性,CSRF Token

JS 垃圾回收机制

这部分的知识点,基本上看别人写的翻译,然后按照别人的思路去完成的,所以这里就推荐一篇我看的文章吧,个人觉得写的还是挺好的,所以有兴趣的可以了解一下,下面的文章👇

简单了解JavaScript垃圾回收机制

计算机网络部分

这个专题也十分的重要,面试大厂的话,这个你得会,不问就不要紧,但是问到你的话,必须的会,我之前梳理过一篇文章,效果还不错,这里分享给大家👇

往期文章

HTTP 状态码知道哪些?分别什么意思?

状态码:由3位数字组成,第一个数字定义了响应的类别

1xx:指示信息,表示请求已接收,继续处理

2xx:成功,表示请求已被成功接受,处理。

200 OK:客户端请求成功
204 No Content:无内容。服务器成功处理,但未返回内容。一般用在只是客户端向服务器发送信息,而服务器不用向客户端返回什么信息的情况。不会刷新页面。
206 Partial Content:服务器已经完成了部分GET请求(客户端进行了范围请求)。响应报文中包含Content-Range指定范围的实体内容

3xx:重定向

301 Moved Permanently:永久重定向,表示请求的资源已经永久的搬到了其他位置。

302 Found:临时重定向,表示请求的资源临时搬到了其他位置

303 See Other:临时重定向,应使用GET定向获取请求资源。303功能与302一样,区别只是303明确客户端应该使用GET访问

307 Temporary Redirect:临时重定向,和302有着相同含义。POST不会变成GET

304 Not Modified:表示客户端发送附带条件的请求(GET方法请求报文中的IF…)时,条件不满足。返回304时,不包含任何响应主体。虽然304被划分在3XX,但和重定向一毛钱关系都没有

4xx:客户端错误

400 Bad Request:客户端请求有语法错误,服务器无法理解。
401 Unauthorized:请求未经授权,这个状态代码必须和WWW-Authenticate报头域一起使用。
403 Forbidden:服务器收到请求,但是拒绝提供服务
404 Not Found:请求资源不存在。比如,输入了错误的url
415 Unsupported media type:不支持的媒体类型

5xx:服务器端错误,服务器未能实现合法的请求。

500 Internal Server Error:服务器发生不可预期的错误。
503 Server Unavailable:服务器当前不能处理客户端的请求,一段时间后可能恢复正常

长轮询和短轮询

短轮询

短轮询(Polling)的实现思路就是浏览器端每隔几秒钟向服务器端**发送http请求,服务端在收到请求后,不论是否有数据更新,都直接进行响应。**在服务端响应完成,就会关闭这个Tcp连接。

  • 优点:就是兼容性比较好,只要支持http协议就可以实现该方式。
  • 缺点:很明显消耗资源,因为下一次的建立Tcp是非常消耗资源的,服务器端响应后就会关闭这个Tcp连接。
function LongAjax() {
    fetch(url).then(data => {
        // 数据正确拿到后,dosometing
        
    }).catch(err => {
        // 发现错误,比如返回的数据为空等。
        console.log(err);
    });
}
setInterval(LongAjax, 5000);

长轮询

客户端发送请求后服务器端不会立即返回数据,服务器端会阻塞请求连接不会立即断开,直到服务器端有数据更新或者是连接超时才返回,客户端才再次发出请求新建连接、如此反复从而获取最新数据。

function LongAjax() {
    fetch(url).then(data => {
    	// 数据正确拿到后,
        LongPolling();
    }).catch(err => {
    	// 出错或者就是超时间
        LongPolling();
        
    });
}
LongAjax()
  • 优点:长轮询与短轮询相比,明显减少了很多不必要的http请求,节约资源。
  • 节点:连接挂起也会导致资源的浪费,停留在服务器端。

HTTP 缓存有哪几种?

浏览器缓存是性能优化的一个重要手段,对于理解缓存机制而言也是很重要的,我们来梳理一下吧👇

强缓存

强缓存两个相关字段,「Expires」「Cache-Control」

「强缓存分为两种情况,一种是发送HTTP请求,一种不需要发送。」

首先检查强缓存,这个阶段**不需要发送HTTP请求。**通过查找不同的字段来进行,不同的HTTP版本所以不同。

  • HTTP1.0版本,使用的是Expires,HTTP1.1使用的是Cache-Control

Expires

Expires即过期时间,时间是相对于服务器的时间而言的,存在于服务端返回的响应头中,在这个过期时间之前可以直接从缓存里面获取数据,无需再次请求。比如下面这样:

Expires:Mon, 29 Jun 2020 11:10:23 GMT
复制代码

表示该资源在2020年7月29日11:10:23过期,过期时就会重新向服务器发起请求。

这个方式有一个问题:「服务器的时间和浏览器的时间可能并不一致」,所以HTTP1.1提出新的字段代替它。

Cache-Control

HTTP1.1版本中,使用的就是该字段,这个字段采用的时间是过期时长,对应的是max-age。

Cache-Control:max-age=6000
复制代码

上面代表该资源返回后6000秒,可以直接使用缓存。

当然了,它还有其他很多关键的指令,梳理了几个重要的👇

注意点:

  • 当Expires和Cache-Control同时存在时,优先考虑Cache-Control。
  • 当然了,当缓存资源失效了,也就是没有命中强缓存,接下来就进入协商缓存👇

协商缓存

强缓存失效后,浏览器在请求头中携带响应的缓存Tag来向服务器发送请求,服务器根据对应的tag,来决定是否使用缓存。

缓存分为两种,「Last-Modified」「ETag」。两者各有优势,并不存在谁对谁有绝对的优势,与上面所讲的强缓存两个Tag所不同。

Last-Modified

这个字段表示的是**「最后修改时间」**。在浏览器第一次给服务器发送请求后,服务器会在响应头中加上这个字段。

浏览器接收到后,「如果再次请求」,会在请求头中携带If-Modified-Since字段,这个字段的值也就是服务器传来的最后修改时间。

服务器拿到请求头中的If-Modified-Since的字段后,其实会和这个服务器中该资源的最后修改时间对比:

  • 如果请求头中的这个值小于最后修改时间,说明是时候更新了。返回新的资源,跟常规的HTTP请求响应的流程一样。
  • 否则返回304,告诉浏览器直接使用缓存。

ETag

ETag是服务器根据当前文件的内容,对文件生成唯一的标识,比如MD5算法,只要里面的内容有改动,这个值就会修改,服务器通过把响应头把该字段给浏览器。

浏览器接受到ETag值,会在下次请求的时候,将这个值作为**「If-None-Match」**这个字段的内容,发给服务器。

服务器接收到**「If-None-Match」后,会跟服务器上该资源的「ETag」**进行比对👇

  • 如果两者一样的话,直接返回304,告诉浏览器直接使用缓存
  • 如果不一样的话,说明内容更新了,返回新的资源,跟常规的HTTP请求响应的流程一样

两者对比

  • 性能上,Last-Modified优于ETagLast-Modified记录的是时间点,而Etag需要根据文件的MD5算法生成对应的hash值。
  • 精度上,ETag``Last-Modified``ETag``Last-Modified
    • 编辑了资源文件,但是文件内容并没有更改,这样也会造成缓存失效。
    • Last-Modified 能够感知的单位时间是秒,如果文件在 1 秒内改变了多次,那么这时候的 Last-Modified 并没有体现出修改了。

最后,「如果两种方式都支持的话,服务器会优先考虑ETag」

缓存位置

接下来我们考虑使用缓存的话,缓存的位置在哪里呢?

浏览器缓存的位置的话,可以分为四种,优先级从高到低排列分别👇

  • Service Worker
  • Memory Cache
  • Disk Cache
  • Push Cache

Service Worker

这个应用场景比如PWA,它借鉴了Web Worker思路,由于它脱离了浏览器的窗体,因此无法直接访问DOM。它能完成的功能比如:离线缓存消息推送网络代理,其中离线缓存就是**「Service Worker Cache」**。

Memory Cache

指的是内存缓存,从效率上讲它是最快的,从存活时间来讲又是最短的,当渲染进程结束后,内存缓存也就不存在了。

Disk Cache

存储在磁盘中的缓存,从存取效率上讲是比内存缓存慢的,优势在于存储容量和存储时长。

Disk Cache VS Memory Cache

两者对比,主要的策略👇

内容使用率高的话,文件优先进入磁盘

比较大的JS,CSS文件会直接放入磁盘,反之放入内存。

Push Cache

推送缓存,这算是浏览器中最后一道防线吧,它是HTTP/2的内容。具体我也不是很清楚,有兴趣的可以去了解。

总结

  • 首先检查Cache-Control, 尝鲜,看强缓存是否可用
  • 如果可用的话,直接使用
  • 否则进入协商缓存,发送HTTP请求,服务器通过请求头中的If-Modified-Since或者If-None-Match字段检查资源是否更新
  • 资源更新,返回资源和200状态码。
  • 否则,返回304,直接告诉浏览器直接从缓存中去资源。

GET 和 POST 的区别

首先,我们的知道区别只是语义上有区别而已,但是面试的时候,肯定不能这么回答的。

  • GET在浏览器回退时是无害的,而POST会再次提交请求。
  • GET请求会被浏览器主动cache,而POST不会,除非手动设置。
  • GET请求只能进行url编码,而POST支持多种编码方式。
  • GET请求参数会被完整保留在浏览器历史记录里,而POST中的参数不会被保留。
  • GET请求大小一般是(1024字节),http协议并没有限制,而与服务器,操作系统有关,POST理论上来说没有大小限制,http协议规范也没有进行大小限制,但实际上post所能传递的数据量根据取决于服务器的设置和内存大小。
  • 对参数的数据类型,GET只接受ASCII字符,而POST没有限制。
  • GET比POST更不安全,因为参数直接暴露在URL上,所以不能用来传递敏感信息。

Webpack

这个面试也是会经常考的一部分了,所以掌握它还是很有必要的,我是从0到1配过它的,所以这里我就没有梳理笔记了,嗯,下面就推荐两个文章,希望看完可以对你们有帮助。

实打实的从0到1配置webpack👇

「一劳永逸」由浅入深配置webpack4

针对面试的👇

关于webpack的面试题总结


算法与数据结构

这个专题,我目前总结了三个板块,速度有点慢,不过面试初级前端的话,应该是没有问题的,需要了解的小伙伴可以看看我梳理的三篇👇

往期文章

如果你跟我一样,对算法也有所热爱的话,我们可以互相讨论下算法,或者关注我哒,我会一直更新算法哒。


模块化

将一个复杂的程序依据特定的规则(规范)封装成几个文件,然后将其组合在一起,这些只是向外暴露一些接口,或者方法,与其他模块进行通信,这样子叫做是模块化的过程。

为什么要模块化,目的在于减少复杂性,减少它们相互之间的功能关系。使每个模块功能单一。

模块化好处

  • 避免了命名冲突(减少了命名空间污染)
  • 更好的分离,按需加载
  • 更高复用性
  • 高维护性

CommomJS

CommonJS定义了两个主要概念:

require函数,用于导入模块

module.exports变量,用于导出模块

require

导入,代码很简单,let {count,addCount}=require("./utils")就可以了。

require的第一步是解析路径获取到模块内容:

  • 如果是核心模块,比如fs,就直接返回模块
  • 如果是带有路径的如/,./等等,则拼接出一个绝对路径,然后先读取缓存require.cache再读取文件。如果没有加后缀,则自动加后缀然后一一识别。
    • .js 解析为JavaScript 文本文件
    • .json解析JSON对象
    • .node解析为二进制插件模块
  • 首次加载后的模块会缓存在require.cache之中,所以多次加载require,得到的对象是同一个。
  • 在执行模块代码的时候,会将模块包装成如下模式,以便于作用域在模块范围之内。

module

let count=0
function addCount(){
    count++
}
module.exports={count,addCount}

然后根据require执行代码时需要加上的,那么实际上我们的代码长成这样:

(function(exports, require, module, __filename, __dirname) {
    let count=0
    function addCount(){
        count++
    }
    module.exports={count,addCount}
});

ES6模块与CommonJS的区别

CommonJS 模块输出的是一个值的拷贝,ES6 模块输出的是值的引用。

  • CommonJS 模块输出的是值的拷贝,也就是说,一旦输出一个值,模块内部的变化就影响不到这个值。
  • ES6 模块的运行机制与 CommonJS 不一样。JS 引擎对脚本静态分析的时候,遇到模块加载命令import,就会生成一个只读引用。等到脚本真正执行时,再根据这个只读引用,到被加载的那个模块里面去取值。换句话说,ES6 的import有点像 Unix 系统的“符号连接”,原始值变了,import加载的值也会跟着变。因此,ES6 模块是动态引用,并且不会缓存值,模块里面的变量绑定其所在的模块。

CommonJS 模块是运行时加载,ES6 模块是编译时输出接口。

  • 运行时加载: CommonJS 模块就是对象;即在输入时是先加载整个模块,生成一个对象,然后再从这个对象上面读取方法,这种加载称为“运行时加载”。
  • 编译时加载: ES6 模块不是对象,而是通过 export 命令显式指定输出的代码,import时采用静态命令的形式。即在import时可以指定加载某个输出值,而不是加载整个模块,这种加载称为“编译时加载”。

模块化开发怎么做?

  • 我对模块的理解是,一个模块是实现一个特定功能的一组方法。在最开始的时候,js 只实现一些简单的功能,所以并没有模块的概念,但随着程序越来越复杂,代码的模块化开发变得越来越重要。
  • 由于函数具有独立作用域的特点,最原始的写法是使用函数来作为模块,几个函数作为一个模块,但是这种方式容易造成全局变量的污染,并且模块间没有联系。
  • 后面提出了对象写法,通过将函数作为一个对象的方法来实现,这样解决了直接使用函数作为模块的一些缺点,但是这种办法会暴露所有的所有的模块成员,外部代码可以修改内部属性的值。
  • 现在最常用的是立即执行函数的写法,通过利用闭包来实现模块私有作用域的建立,同时不会对全局作用域造成污染。

❤️ 感谢大家

如果你觉得这篇内容对你挺有有帮助的话:

  1. 点赞支持下吧,让更多的人也能看到这篇内容(收藏不点赞,都是耍流氓 -_-)
  2. 关注公众号前端UpUp,定期为你推送好文。
  3. 觉得不错的话,也可以阅读TianTian近期梳理的文章(感谢掘友的鼓励与支持🌹🌹🌹):