2020-2021前端知识点总结

447 阅读24分钟

前言

每天学一点新知识以及复习一点旧知识,但是零零散散的不太好看,也容易漏,就变成了丢了芝麻捡西瓜,所以决定系统的整理一个学习大纲出来。

html5+css3

html5

html新增的常用的几个语义化标签
<header>
<footer>
<video>
<audio>
<aside>
其中<canvas> 是h5新增的一个类似画布的标签,d3库就是基于canvas来进行图形绘制的

语义化的好处
语义化的最大优点就是方便阅读代码理解代码,其二就是利于SEO

h5新增的两个本地存储对象是什么
localStorage和sessionStorage

两者有什么区别
相同的是两个都是持久化存储,存储空间大,相当于一个小型的数据库
不同在于localStorage需要手动清空才会删除数据,而sessionStorage的话关闭当前浏览器页面就会自动清除数据,而且sessionStorage有执行上下文

localStorage和sessionStorage与cookie的区别
首先cookie的话基于http无状态的特性,是携带一些用户敏感信息的与服务器进行信息的传递的一个工具,本身可以携带的信息空间不大,4KB左右,具有时效性以及安全性
两个本地存储对象的话是类似本地数据库的功能,不与服务器进行交互,存储空间大,5MB左右

css3

盒模型

IE模型盒子 box-sizing: content-box;
给盒子设置的宽度不包括padding和border的宽度
例如设置div的width为为100px,padding10px,border10px, 则最后页面显示的整个盒子的宽度是为140px

弹性模型盒子(怪异盒子) box-sizing: border-box;
给盒子设置的宽度包括padding和border的宽度
例如设置div的width为为100px,padding10px,border10px, 则最后页面显示的整个盒子的宽度是为100px

常用选择器

类选择器,子类选择,伪类选择器是日常开发用的比较多的,兄弟选择器,属性选择器我用的比较少
css选择器有全部的选择器,可以学习一下

权重
!important > 行内样式 > id选择器 > 类名选择器 > 标签选择器 > 通用选择器 > 属性选择器 > 伪类选择器 > 伪元素选择器
权重计算规则如下
每个级别占有一个位置,不可越级的增加 例如,我有3个类名选择器加1个标签选择器
假设id的权重是第一位 那么该选择器的权重是 0,3,1

  div.class1.class2.class3 { 
    background: blue;
  }

而我有一个id选择器加一个标签选择器时

  div.#class { 
    background: red;
  }

我的权重则为 1,0,1 两者比较的话权重是下面的大,所以div会显红色,因为我说过单个选择器的权重是无法跨级的,也就是说就算你有100个类选择器,你的权重也没有我1个id选择器高;插一嘴,继承的权重为0

重绘与回流

这里有篇关于重绘和回流的详解可以看看 重绘与回流
一句话理解:回流是浏览器计算好节点的信息(位置,大小),重绘就是将计算好的值转化成视口的像素值渲染在页面上;重绘和回流是浏览器渲染页面的一个必然的过程; 回流一定会引起重绘,而重绘不一定引起回流

什么操作会引起重绘和回流

  1. 页面首次渲染(不可避免)
  2. 窗口大小变化
  3. 改变元素的宽、高、文档流
  4. 改变元素的内容,如innerText,或者是图片的引用更换
  5. 对dom的增删操作

如何减少重绘与回流
首先我们要知道的是,重绘和回流是无法避免的,而且很消耗性能,那么我们需要做的就是,尽量的去避免此类操作,比如批量修改css属性之后,一次性加上去,或者用className来操作

书写顺序

书写的顺序是话是某天在翻找一些浏览器性能优化的点看到的,放在css这块来讲也是可以的
鉴于浏览器的回流与重绘机制,我们在书写css的时候,尽量以以下的顺序为准

  1. 位置属性(position, top, right, z-index, display, float等)
  2. 大小(width, height, padding, margin)
  3. 文字系列(font, line-height, letter-spacing, color- text-align等)
  4. 背景(background, border等)
  5. 其他(animation, transition等)
    这样做的目的就是尽可能就减少回流与重绘,参考改变文章 css书写顺序

移动端常见问题

1px
这个问题已经老生常谈了,原因以及解决方法网上一搜一大把,就不重复的废话了,归根到底就是一个句话,原因就是不同的设备像素比,使得1px在不同设备间拥有的pt数量不同,解决方案我这里列举两个

  // 伪元素
  div::after {
  	display: block;
  	content: '';
  	border: 1px solid #ccc;
  	transform: scaleY(0.5);
  } 
  // 利用 box-shadow
  div: {
  	box-shadow: 0 0.5px 0 0 #fff;
  }

再详细一点可以看看这个大佬samul的文章,写的还是蛮通俗易懂的

动画

动画这块比较薄弱。。待补充QAQ

ECMAScript

声明关键字

ES5

var 关键字
作用:声明一个变量或者函数表达式
特点:全局变量,没有块级作用域,可以变量提升,同个变量可以重复定义

function 关键字
作用:声明一个函数
特点:变量提升

ES6
let 关键字
作用:声明一个变量或者函数表达式
特点:拥有块级作用域,不可以变量提升,同个变量不可以重复定义

const 关键字
作用:声明一个变量或者函数表达式
特点:拥有块级作用域,不可以变量提升,同个变量不可以重复定义,定义了该变量之后不可以重新赋值

class 关键字
作用:声明一个类
特点:拥有块级作用域,不可以变量提升,同个变量不可以重复定义,实例化后自动执行构造器里面的代码块,function关键字的语法糖

数据类型

基本数据类型

Number String Boolean undefined Null
以及es6新增的一个Symbol 还有一个少人用的BigInt

引用类型

Object Function
其中Object里面有Array Date RegExp等内置函数

新增数据结构

Map

一个与object存储方式都是key-value的数据结构,区别在于Map是真正的可以以任何类型作为键
比如在object里面你用Number类型的0去做键,在控制台打印你会发现,object把Numner类型的0转化成了String类型 使用它很简单,new一个对应的构造函数,也可以在构造器添加可迭代的[key, value]形式的数据类型

let map = new Map()
//也可以接受一个可迭代的参数
let map2 = new Map([[0, 'val1'], ['1', 'val2']])

Map的实例常见的操作方法和属性 2. map.size:返回Map结构的成员总数。

  1. set(key, value):添加某个值,返回Map结构本身。
  2. get(key):读取key对应的键值,如果找不到key,返回undefined
  3. delete(key):删除某个值,返回一个布尔值,表示删除是否成功。
  4. has(key):返回一个布尔值,表示该值是否为Map的成员。
  5. clear():清除所有成员,没有返回值。 常见的遍历方法
  6. Map.prototype.keys():返回键名的遍历器。
  7. Map.prototype.values():返回键值的遍历器。
  8. Map.prototype.entries():返回所有成员的遍历器。
  9. Map.prototype.forEach():遍历 Map 的所有成员。

weekMap

和map一样,但是他的key是一个引用值,如果不是引用值的话会报错。可以实现私有变量以及应对一些内存泄漏的场景

Set

一个无重复子集的一个集合,通常来做数组去重用的,构建他的实例也很简单

let set = new Set()
//也可以接受一个可迭代的参数
let set2 = new Set([1, 2, 3])

Set的实例常见的操作方法

  1. add(value):添加某个值,返回 Set 结构本身。
  2. delete(value):删除某个值,返回一个布尔值,表示删除是否成功。
  3. has(value):返回一个布尔值,表示该值是否为Set的成员。
  4. clear():清除所有成员,没有返回值。 常见的遍历方法
  5. Set.prototype.keys():返回键名的遍历器
  6. Set.prototype.values():返回键值的遍历器
  7. Set.prototype.entries():返回键值对的遍历器
  8. Set.prototype.forEach():使用回调函数遍历每个成员

weekSet

key也是引用值,不然会报错

es6新增的内置函数

Proxy
Reflect
Generator

类型判断

typeof

只能判断Number String Boolean undefined Object Function
其中typeof null 的话是会返回Object的,远古bug。感兴趣的同学自行百度一波,和js当时的底层存储位数有关

instanceof

instanceof的原理是,在当前作用域下,判断关键字左边这个对象的原型在不在右边这个构造器的原型的原型链上
[] instanceof Array 输出的是true 可以判断数组,当然是用Array的isArray方法也可以判断是否为数组。甚至还可以判断自定义的构造器类型 如

// 自定义构造函数
function Person() {
  this.name = 'young'
}
const person = new Person()
console.log(person instanceof Person) // ==>true

Object.prototype.toString.call

这个方法属于万能型判断,但是判断自定义构造函数的时候会输出[object Object]
其工作原理就是,因为Object对象的原型是所有对象的原型链的终点,并且所有对象都重写了toString方法,所以每一个不同类型的对象调用出来的结果是不一样的,比如Function类型返回内容为函数体的字符串,Array类型返回元素组成的字符串以此类推

console.log(Object.prototype.toString.call("jerry")) // [object String]
console.log(Object.prototype.toString.call(12)) // [object Number]
console.log(Object.prototype.toString.call(true)) // [object Boolean]
console.log(Object.prototype.toString.call(undefined)) // [object Undefined]
console.log(Object.prototype.toString.call(null)) // [object Null]
console.log(Object.prototype.toString.call({ name: "jerry" })) // [object Object]
console.log(Object.prototype.toString.call(function () { })) // [object Function]
console.log(Object.prototype.toString.call([])) // [object Array]
console.log(Object.prototype.toString.call(new Date)) // [object Date]
console.log(Object.prototype.toString.call(/\d/)) // [object RegExp]
function Person () {
    this.name = 'young'
}
console.log(Object.prototype.toString.call(new Person)) // [object Object]

原型/原型链与继承

原型
我对于原型的理解就是,每个对象都有自己的原型,每个对象如果没有某个方法的话,例如toString方法,可能会在该对象的原型上挂载着,原型的存在我认为有两个作用,一是节省空间,二是实现继承

原型链
原型链文字一句话描述就是每个对象会有一个__proto__的属性,链着该对象的构造函数的prototype原型对象
图解:

万物到头皆为空,指的就是原型链指到最后就是null
值得一提的是Function的构造函数是他本身 自己搞自己

继承
面向对象语言三大核心之一,继承。es5的话基于原型链来完成的
原型链继承
缺点1. 如果父类有有引用类型的数据,那么子类继承后new出来的所有对象都有这个引用类型的地址,也就是会互相影响
缺点2. 创建子类实例时,无法向父类的构造函数传递参数

function Father (name) {
    this.name = name
}
Father.prototype.eat = function () {
    console.log('eat')
}
function Son (name) {
    this.name = name
}
// 继承
Son.prototype = new Father()

借用构造器继承
优点: 可以向父类的构造器传递参数 缺点 无复用

function Father (name) {
    this.colors = ['red', 'black', 'blue']
    this.name = name
}
function Son (name) {
    // 借助父类的构造函数
    Father.call(this, name)
}
const son1 = new Son()
son1.colors.push('yellow') // =>['red', 'black', 'blue', 'yellow']
const son2 = new Son()
console.log(son2.colors) // =>['red', 'black', 'blue']

组合继承
比较完美,但是调用了两次父类的构造函数

function Father (name) {
    this.colors = ['red', 'black', 'blue']
    this.name = name
}
Father.prototype.sayHi = function () {
    console.log(this.name)
}
function Son (name, age) {
    // 借助父类的构造函数
    Father.call(this, name)
    this.age = age
}
// 继承方法
Son.prototype = new Father()
Son.prototype.constructor = Son
Son.prototype.sayAge = function () {
    console.log(this.age)
}
const son1 = new Son('young', 18)
son1.colors.push('yellow') // =>['red', 'black', 'blue', 'yellow']
son1.sayHi() // => young
son1.sayAge() // => 18
const son2 = new Son('阳', 25)
son2.sayHi() // => 阳
son2.sayAge() // => 25
console.log(son2.colors) // =>['red', 'black', 'blue']

寄生组合继承
解决了调用两次父类构造函数

function inherit (son, father) {
    const property = Object.create(father)
    property.constructor = son
    son.prototype = property
}
function Father (name) {
    this.colors = ['red', 'black', 'blue']
    this.name = name
}
Father.prototype.sayHi = function () {
    console.log(this.name)
}
function Son (name, age) {
    // 借助父类的构造函数
    Father.call(this, name)
    this.age = age
}
// 改造继承方法
inherit(Son, Father)
Son.prototype.sayAge = function () {
    console.log(this.age)
}

ES6继承
es6的继承写法就比较简单了,继承使用extends关键字,注意继承的时候要用super调用父类的构造函数

class Father {
    constructor(name) {
        this.name = name
        this.colors = ['red', 'black', 'blue']
    }
    hi () {
        console.log('你好,我是', this.name)
    }
}
class Son extends Father {
    constructor(name) {
        super(name)
    }
}
const son = new Son('young')
son.colors.push('yellow')
console.log(son.colors) // =>[ 'red', 'black', 'blue', 'yellow' ]
son.hi() // =>你好,我是 young
const son1 = new Son('阳')
console.log(son1.colors) // =>[ 'red', 'black', 'blue' ]

this指向

this指向一句话就是谁调用this指向谁,但是需要注意的一点是全局条件下,非严格模式的this指向window,而严格模式下this指向的是undefined

call、apply、bind

作用:三个都有可以改变this指向的作用
区别:call接收的参数是分开的,apply接受的是一个数组参数,而且这两个api会自动执行改变后的函数,而bind则需要手动执行,bind的参数列表也是一个数组

手写apply

Function.prototype.myApply = function (context, arg) {
    if (!context) {
        context = window
    }
    const fn = Symbol()
    context[fn] = this
    let res = context[fn](...arg)
    delete context[fn]
    return res
}

手写call

Function.prototype.myCall = function (context) {
    if (!context) {
        context = window
    }
    const fn = Symbol()
    context[fn] = this
    const args = Array.from(arguments).slice(1);
    context[fn](...args)
    delete context[fn]
}

手写bind

Function.prototype.myBind = function (context) {
    if (!context) {
        context = window
    }
    const that = this
    const args = Array.from(arguments).slice(1)
    return function () {
        that.apply(context, args)
    }
}

箭头函数

普通函数声明格式

const fn = function () {
  console.log(123)
}

箭头函数声明格式

const fn = () => {
    console.log(123)
}

!注意
使用箭头函数的时候慎用this,因为箭头函数是在定义的时候就已经确定好this指向了,而不是调用时候才去确定this指向,也就是说箭头函数不能使用call,bind,apply来改变this指向的

作用域/作用域链/闭包

作用域与作用域链

es6之前是没有块级作用域的,所以可以产生作用域的只有函数functionif条件判断的{}内,作用域一般分全局作用域局部作用域
用红宝书的一个例子来解释作用域链

var color = 'blue'
function changeColor () {
    var anotherColor = 'red'
    function swapColors () {
        var tempColor = anotherColor
        anotherColor = color
        color = tempColor
        // 这里可以访问 tempColor anotherColor color
    }
    // 这里可以访问 anotherColor color
    swapColors()
}
// 这里可以访问 color
changeColor()

上述有三个作用域,全局作用域changeColor()局部作用域与swapColors()局部作用域,作用域链如下图展示

闭包

闭包我个人的理解是'逆'作用域,外部作用域访问内部作用域的变量,使用方法就是一个函数的返回是一个函数
闭包我用的不是很多,所以这块写的会很少,举两个常见的例子吧。

如何是一个函数执行三次之后,再执行就失效了?

function countFn (fn) {
    let _count = 0
    return function () {
        if (_count++ < 3) {
            fn()
        }
    }
}
function log () {
    console.log('执行')
}
const test = countFn(log)
test() // =>执行
test() // =>执行
test() // =>执行
test() // =>无输出
test() // =>无输出
test() // =>无输出

经典使用闭包改造定时输出函数

一道很经典的题目,1秒后输出的是什么?

for (var i = 0; i < 6; i++) {
    setTimeout(function () {
        console.log(i)
    }, 1000)
}

答案是6个6
为什么呢?因为for循环比setTimeout快,而导致for循环已经结束了,i已经变成6了,setTimeout里面的代码还没执行,等到1秒后,才输出i的值,那么自然而然的就全都是6了。那么有什么办法可以让它输出我们想要的答案呢,是有的,那就是我们的闭包,当然我们也可以使用let关键字和自执行函数的写法来开辟新的作用域,这里就不作讨论了

//改写
for (var i = 0; i < 6; i++) {
    setTimeout((function (_i) {
        return function () {
            console.log(_i)
        }
    })(i), 1000)
}

这样就可以输出0 1 2 3 4 5了

内存泄漏

说到闭包那么就免不了内存泄漏这个话题了,内存泄漏的原因就是因为一直有引用依赖,导致js垃圾回收机制无法清除,导致内存一直占用着。
解决方法

  1. 不使用闭包
  2. 在使用闭包后,将引用的对象的指针设置为null 例子
function handler () {
    const el = document.getElementById('#app')
    el.onclick = function () {
        console.log(el.innerText)
    }
}

这个函数给#app这个元素绑定着点击事件,但是el的引用一直都在,所以导致el这个不会被回收

正确写法

function handler () {
    const el = document.getElementById('#app')
    const content = el.innerText
    el.onclick = function () {
        console.log(content)
    }
    el = null
    
}

将el元素的引用设置为null,没有引用的话改变量就会被标记,然后回收

浅拷贝与深拷贝

深拷贝与浅拷贝出现的原因
由于js存在着两中数据类型,一种是值类型,也是基本数据类型,而另外一种则是引用类型,对象和数组就是引用类型,他们保存的方式有所不同,值类型的保存在栈中,引用类型的话则是存一个指针在栈中,指针指向堆中的真实地址

区别
首先我们要知道深拷贝和浅拷贝是基于引用类型的对象来说的,值类型的对象没有这一说法。
那么从这幅图就可以很清楚的知道浅拷贝与深拷贝的区别了,浅拷贝就是在栈中开辟一块新的空间,指的还是之前的那个对象。而深拷贝就是开辟新的一块空间之后,在堆中也要开辟一块新的空间来存储一个对象,并且新的指针指向新的对象,与之前的对象没有任何瓜葛了。
还不能理解的同学可以这么想,浅拷贝等于我和你住同一个屋子,同一串钥匙,我把你的其中一个房间拆了,你回到家发现你的家的一个房间没了,而深拷贝就是,咱俩屋子的格局是一样的,但是我是我的,你是你的,毫无关系了。

实现
这里主要说一下实现几种深拷贝的方式吧

let obj = {
    a: 1,
    b: 'b',
    c: {
        d: '1',
        e: 2,
        f: [1, 2, 3]
    },
    g: [{ a: 1, b: 2 }, '2', '3']
}

方法1 Object.assign

const copy1 = Object.assign({}, obj)

方法2 ... 拓展运算法

const copy2 = {...obj}

方法3 JSON.stringify
缺点是如果要拷贝的对象有函数的话无法拷贝

const copy 3 = JSON.stringify(obj)

方法4 自定义方法
缺点,层次比较深的话会栈溢出

function deepClone (target) {
    let result
    if (typeof target === 'object') {
        if (target instanceof Array) {
            result = []
            for (const value of target) {
                result.push(deepClone(value))
            }
        } else if (target === null) {
            result = null
        } else if (target instanceof RegExp) {
            result = target
        } else {
            result = {}
            for (const key in target) {
                result[key] = deepClone(target[key])
            }
        }
    } else {
        result = target
    }
    return result
}

方法5 loadsh库的cloneDeep方法

import { cloneDeep }  from 'lodash'
let copy5 = cloneDeep(objects)

异步系列

Event Loop

Event Loop(事件循环) 先放一道烂大街的题目

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

答案自己放到node或者浏览器跑一跑就出来了,如果你做对了,那么这个篇幅可以不用看了,如果不会,或者有疑问,那我们来详细说说

事件循环的由来

基于js是单线程的语言,这是设计之初就定下来的了
为啥是单线程?你说我有条线程修改这个div背景色为红色,另一个线程修改它为蓝色,你说我听谁的? 这就注定了js只能是单线程的语言。而单线程就是同一时间只能做一件事情,你要做就得排队,但是用户等不得啊,特别是ajax请求。那么,Event Loop就出现了

执行

首先我们得有栈,队列,执行栈,同步异步这些概念,我们普通的函数,比如 console.log(123) 就是经典的同步任务,代码块执行到它这里直接压栈出栈,而setTimeout函数则是经典的异步函数,会进入任务队列里面等待,等到结果返回之后通过回调函数将数值拿到
图解 ok,那么异步任务中还分宏任务微任务
常见的宏任务有 setTimeout setInterval I/O操作函数 Node环境下的setImmediate
微任务有Promise.then Node环境下的process.nextTick
在当前微任务还没有执行完之前宏任务是不会执行的,另外process.nextTick是有插队的功能

解题

回到题目来,我们一行代码一行代码走

  1. async1async2两个函数的定义不多说
  2. 代码执行到console.log('script start')这条输出script start
  3. 遇到setTimeout,丢入宏任务队列
  4. 执行async1()
  5. console.log('async1 start')输出 async1 start
  6. await async2() 等待async2函数的执行,并且把后面的代码放入微任务队列中,也就是console.log('async1 end')
  7. 执行async2函数,输出async2
  8. 继续往下走,遇到promise函数,记住promise函数虽然是来处理异步的,但是它本身是同步的,所以输出promise1
  9. 碰到resolve()promise的状态改成resolve 此时有个.then,把console.log('promise2')放入微任务队列中
  10. 再往下走,遇到一个process.nextTick,于是将console.log('nextTick') 放入微任务队列中,注意的是我们说过process.nextTick会插队,所以他是在微任务的第一个,如果下面还有第二个process.nextTick的话,那么第二个process.nextTick是在第二个,而不是把第一个往后挤
  11. ok 遇到了熟悉的console.log('script end') 直接输出
  12. 现在同步任务已经执行完毕了,栈为空,是时候去执行我们任务队列里面的回调了
  13. 还记得我们插队的process.nextTick吗,此时任务队列第一个就是它,直接把他压栈执行,输出nextTick1
  14. 继续执行我们的微任务队列 依次输出async1 endpromise2
  15. 此时我们的微任务队列已经空了,剩下一个宏任务队列里面的定时器任务,于是执行输出setTimeout
总结
  • 要先分清楚宏任务微任务,分别放入队列中,微任务执行完再执行宏任务
  • async表示函数里有异步操作,await之前的代码该怎么执行怎么执行,await右侧表达式照常执行,后面的代码被阻塞掉,等待await的返回。返回是非promise对象时,执行后面的代码;返回promise对象时,等promise对象resolved时再执行
  • process.nextTick优先于Promise.then

promise

DOM

待补充

BOM

待补充

Node

待补充 =====>Node篇幅<=====

前端框架(主要是Vue)

MVVM

双向绑定

生命周期

上表格,具体每个函数能做什么就不多说了

vue2.xvue3.x
beforCreatesetup
createdsetup
beforMountonBeforeMount
mountedonMounted
beforUpdateonBeforeUpdate
updatedonUpdated
beforDestroyonBeforeUnmount
destroyedonUnmounted

数据绑定

双向绑定

状态管理

Vue中提供了vuex基于我们状态管理,可以存放一些公用的数据

拆解
Vuex就相当于一个仓库,我们可以从他的结构上来分析


const modules = {
    store1: store1,
    store2: store2,
    store3: store3
}
const store = {
    state: {
        name: 'young',
        age: 18
    },
    mutations: {
        changeName (state, payload) {
            state.name = payload
        },
        changeAge (state, payload) {
            state.age = payload
        }
    },
    actions: {
        asyncChangeName (context) {
            setTimeout(() => {
                context.commit('changeName')
            }, 1000)
        },
        deferChangeAge (context) {
            context.commit('changeAge')

        }
    },
    modules: {
        ...modules
    }
}

state 存放状态/数据的一个对象,可以展示在视图上,只能通过mutations里面的方法改变
mutations 存放着修改state的同步函数
actions 存放着触发mutations里面的方法的方法 ,可以是异步函数
modules 当多人开发页面的时候,难免会使用多少vuex仓库,那么modules就可以帮我们进行模块化切割
如何触发我建议直接用各自的辅助函数,如mapState,mapMutation。这样就可以直接this.xxx拿到state里面的值了,或者this.xxx()调用mutations或者actions里面的方法,不过如果有命名空间的话需要注意加上

组件通信

  1. 父传子 props
  2. 子传父 emit
  3. 兄弟传 event bus
  4. 爷孙传 attrsattrs listeners
  5. inject与provide
  6. route的query与params
  7. Vuex
  8. localStorage与sessionStorage (多页面)
  9. indexedDB (多页面)
  10. cookie (页面跳转 传递身份)

侦听属性与计算属性

虚拟DOM

  1. 虚拟dom是一个根据真实dom节点的一些信息,例如类名,属性,节点名称等模拟出来的一个对象
  2. 为diff算法服务
  3. 真实的dom开销很大,以前页面上改动一个小小的div也会直接操作全部的dom,而且通常在页面渲染之前,我们会有一个初始值,计算值,和最终显示在页面的值,如果这三部操作都要操作dom,那么性能可想而知的查

diff算法

简易diff算法

前端工程化

rollup webpack

对于前端工程化的了解

计算机网络相关知识

什么是http协议

http协议即是超文本传送协议,它定义了浏览器如何向服务器获取资源,而服务器如何返回浏览器所要的资源的规则

http和https的区别

信息传输的安全性不同
http是明文传输,被拦截后可以直接读懂响应报文 https是使用ssl加密传输,保证数据的安全性

协议不同
会产生跨域

端口不同
http默认端口80 而https默认端口443

申请证书方式不同
http:免费申请
https:需要到ca申请证书,一般免费证书很少,需要交费

http method

GET

常见的请求方式,一般用于请求一些列表数据或者表格数据,发送搜索接口,传递一些不敏感的信息

POST

一般用于提交表单数据,较GET请求来说安全性高一些,也可以来获取一些列表的数据

http response报文结构是怎样的

  1. 请求行/响应行
  2. 请求头/响应头
  3. 空行
  4. 消息主体(响应体)

http常见的状态码以及意义

  • 200 正常请求
  • 301 永久重定向
  • 302 临时重定向
  • 304 协商缓存
  • 400 参数错误
  • 403 禁止访问
  • 404 资料消失了
  • 500 服务器错误

什么是同源

一句话,同源就是相同协议相同端口相同主机下的访问
我们以http://www.testPage.com:8080/userPage?id=1来举例子

  • https://www.testPage.com:8080/userPage?id=1
    不同源,协议不同
  • http://www.test.com:8080/userPage?id=1
    不同源,主机不同
  • http://www.testPage.com:80/userPage?id=1
    不同源,端口不同
  • http://www.testPage.com:8080/userPage?id=3&pageNo=1
    同源

如何跨域

跨域的话一般找后台解决,已经会有很成熟的配置了;亦或者在vue.config.js里面的devServer配置一个proxy

devServer: {
  port: 80,
  host: '0.0.0.0',
  https: false,
  open: true,
  proxy: {
    '/': {
      target: 'http://xxxxx',
      ws: false,
      changeOrigin: true,
      timeout: 3 * 60 * 1000
    }
  }
}

其他的一些跨域方法比较有限制,常见的一个jsonP跨域,其原理就是利用script标签不会有浏览器的同源策略限制,在src里面请求一段接口且把回调函数的名字带上在url上传到后台,后台给予对应的数据。

// promise封装的jsonP
function jsonP ({ url, params, callbackName }) {
  const getUrl = () => {
    let dataStr = ''
    for (const key in params) {
      dataStr += `${key}=${params[key]}&`
    }
    dataStr += `callback=${callbackName}`
    return `${url}?${dataStr}`
  }
  return new Promise((reslove, reject) => {
    callbackName = callbackName || Math.random().toString.replace(',', '')
    let script = document.createElement('script')
    script.src = getUrl()
    document.body.appendChild(script)
    window[callbackName] = data => {
      reslove(data)
      document.body.removeChild(script)
    }
  })
}

安全防护

参考出处

XSS攻击

XSS 全称是 Cross Site Scripting(即跨站脚本),为了和 CSS 区分,故叫它XSS。XSS 攻击是指浏览器中执行恶意脚本(无论是跨域还是同域),从而拿到用户的信息并进行操作 三种实现方式

  • 存储型 顾名思义就是将恶意脚本存储了起来,确实,存储型的 XSS 将脚本存储到了服务端的数据库,然后在客户端执行这些脚本,从而达到攻击的效果。
    常见的场景是留言评论区提交一段脚本代码,如果前后端没有做好转义的工作,那评论内容存到了数据库,在页面渲染过程中直接执行, 相当于执行一段未知逻辑的 JS 代码,这就是存储型的 XSS 攻击
  • 反射型 反射型XSS指的是恶意脚本作为网络请求的一部分
    比如我输入:
http://sanyuan.com?q=<script>alert("你完蛋了")</script>

这样,在服务器端会拿到q参数,然后将内容返回给浏览器端,浏览器将这些内容作为HTML的一部分解析,发现是一个脚本,直接执行,这样就被攻击了。

之所以叫它反射型, 是因为恶意脚本是通过作为网络请求的参数,经过服务器,然后再反射到HTML文档中,执行解析。和存储型不一样的是,服务器并不会存储这些恶意脚本。

  • 文档型 文档型的 XSS 攻击并不会经过服务端,而是作为中间人的角色,在数据传输过程劫持到网络数据包,然后修改里面的 html 文档! 这样的劫持方式包括WIFI路由器劫持或者本地恶意软件等。
  • 如何防范 一个信念: 不要相信用户的输入,对输入内容转码或者过滤,让其不可执行
    两个利用: 利用 CSP,利用 Cookie 的 HttpOnly 属性。

CSRF

CSRF(Cross-site request forgery), 即跨站请求伪造,指的是黑客诱导用户点击链接,打开黑客的网站,然后黑客利用用户目前的登录状态发起跨站请求。 CSRF攻击一般会有三种方式:

  • 自动 GET 请求
  • 自动 POST 请求
  • 诱导点击发送 GET 请求。 防范措施: 利用CookieSameSite属性、验证来源站点CSRF Token

浏览器相关

浏览器输入URL之后 发生了什么事情

浏览器的工作原理

浏览器解析过程

浏览器的缓存策略

介绍一下浏览器内核

页面访问cookie的限制

如何实现多个标签页的通信

页面可见性(page visibility API)可以有哪些用途

为什么利用多个域名来存储网站资源

如何做好SEO

浏览器性能优化问题

一个页面上有大量图片,加载很慢,有什么优化方法

如何对网站的文件和资源进行优化

渐进增强和优雅降级

常见的设计模式

常见的设计模式

数据结构与算法

常见的数据结构

js中常见的数据结构有队列链表哈希表二叉树集合等等,由于篇幅太大,我就不在这里一一描述了,附上我在github上的用es6实现的以上的数据结构的类,有兴趣的同学可以看一下
常见的数据结构

算法题

一般面试的话 小公司比较注重业务,你只要偏业务一点,技术栈比较吻合的话基本都没啥问题,但是如果要进一些中大型企业,特别是BAT,字节,pdd这些就别说了,算法没跑的了
算法题的话对于我这个菜鸡来说实在是太难了,所以只能慢慢来,leetcode当仁不让的第一个推荐,每日刷一题。推荐就是一个类型一个类型的来,然后难度也是由easy到hard,不要按照他上面的顺序来,个人觉得效果不太好

排序算法

这里列举几个常用的排序算法

冒泡排序

  • 思路:冒泡,一次遍历选一个最大/小的出来,直到排序完成
    基础版
function bubblerSort (array) {
    let len = array.length
    for (let i = 0; i < len; i++) {
        for (let j = 0; j < len; j++) {
            if (array[j] > array[j + 1]) {
                let temp = array[j]
                array[j] = array[j + 1]
                array[j + 1] = temp
            }
        }
    }
}	

优化版

function bubblerSort (array) {
    let len = array.length
    for (let i = 0; i < len; i++) {
        for (let j = 0; j < len - i -1; j++) {
            if (array[j] > array[j + 1]) {
                let temp = array[j]
                array[j] = array[j + 1]
                array[j + 1] = temp
            }
        }
    }
}	

选择排序

  • 思路:选择第一个元素为最小值,进行一轮对比,选择最小的放在第一个,第二次选择第二个元素为最小值,在进行一轮对比,选择最小的放在第二个,以此类推
function selectSort (array) {
    let len = array.length
    for (let i = 0; i < len - 1; i++) {
        let min = i //从0开始
        for (let j = min + 1; j < len; j++) {
            if (array[j] < array[min]) {
                min = j
            }
        }
        // 找到最小的
        let temp = array[min]
        array[min] = array[i]
        array[i] = temp
    }
}

插入排序

  • 核心思路:局部有序。依次将被标记的元素插入局部有序的队列
function insertSort (array) {
    let len = array.length
    // 外层循环,从第一个位置开始获取数据,向前面局部有序进行插入
    for (let i = 1; i < len; i++) {
        let current = array[i]
        let j = i
        // 内部循环,获取i的位置,和面前的局部有序数据进行比较
        while (current < array[j - 1] && j > 0) {
            array[j] = array[j - 1]
            j--
        }
        array[j] = current
    }
}

希尔排序

  • 历史意义:打破了O(N²)的效率
  • 核心思路:定义一个初始的增量,根据增量分组,分组内进行插入排序,可以很大程度上避免小数字出现在最右侧
function shellSort (array) {
    let len = array.length
    // 初始化增量,采用原稿的length/2
    let gap = Math.floor(len / 2)
    // 根据增量分组进行插入排序
    while (gap >= 1) {
        // 与插入排序一致
        for (let i = gap; i < len; i++) {
            let current = array[i]
            let j = i
            while (current < array[j - gap] && j > 0) {
                array[j] = array[j - gap]
                j -= gap
            }
            array[j] = current
        }
        // 改变增量值,直到为1
        gap = Math.floor(gap / 2)
    }
}

快速排序

  • 历史意义:冒泡排序进阶版,二十世纪十大算法之一
  • 核心思路:分而治之 二分法 选择一个合适的枢纽
// 交换函数
function change (array, left, right) {
    let temp = array[left]
    array[left] = array[right]
    array[right] = temp
}
// 获取合适的枢纽
function getMedium (array) {
    // 取出中间的位置
    let center = Math.floor(array.length / 2)
    let left = 0
    let right = array.length - 1
    // 判断后交换位置
    if (array[left] > array[center]) {
        change(array, left, center)
    }
    if (array[center] > array[right]) {
        change(array, center, right)
    }
    if (array[left] > array[right]) {
        change(array, left, right)
    }
    // 返回中间的index
    return center
}
// 递归调用
function quickSort (array) {
    if (array.length === 0) return []
    let left = []
    let right = []
    let cidx = getMedium(array)
    let base = array.splice(cidx, 1)
    array.forEach(num => {
        if (num < base) {
            left.push(num)
        } else {
            right.push(num)
        }
    })
    return quickSort(left).concat(base, quickSort(right))
}

计算机相关的知识