这篇文章记录了一些前端知识点的补充,其中包括ES6,ES5一些函数的实现原理,便于更好的理解JS,一些基础排序算法的JS实现,以及其他比较常见的知识点

数据响应式实现

Vue3.0中将会通过Proxy来替换原本的Object.defineProperty来实现数据双向绑定

Proxy可以用来自定义对象中的操作。

通过Object.defineProperty实现数据响应式

ES6中新加了Array.property.includes用于判断一个特定的值是否存在于数组中。ES5中的indexOf

不够语义化,而且对于NaN存在误判

let data = {
    price: 10,
    count: 5
}
let target = null
class Dep {
    constructor() {
        this.subscribers = []
    }
    depend() {
        if (target && !this.subscribers.includes(target)) {
            this.subscribers.push(target)
        }
    }
    notify() {
        this.subscribers.forEach(sub => sub())
    }
}
Object.keys(data).forEach(key => {
    let internalValue = data[key]
    const dep = new Dep()
    Object.defineProperty(data, key, {
        get() {
            dep.depend()
            return internalValue
        },
        set(newValue) {
            internalValue = newValue
            dep.notify()
        }
    })
})
target = () => {
    data.total = data.price * data.count
}
target()
console.log('data.total', data.total);
data.count = 10
console.log('data.total', data.total);

基于Proxy实现的数据响应式

let onWatch = (obj,setBind,getLogger)=>{
    let handler = {
		set(target,property,value,receiver){
            setBind(value,property)
            return Reflect.set(target,property,value)
           //如果property部署了set函数,则set函数的this绑定receiver
        }
        get(target,property,receiver){
            getLogger(target,property)
            return Reflect.get(target,property,receiver)
            //如果property部署了get函数,则set函数的this绑定receiver
        }
    }
    return new Proxy(obj,handler)
}
let obj = {a:1}
let watchObj = onWatch(
    obj,
    (value,property)=>{
        console.log(`监听到属性${property}改变为${value}`);
    },
    (target,property)=>{
        console.log(`'${property}' = ${target[property]}`);
    }
)

Promise的ES5实现

function MyPromise(executor) {
    let self = this
    self.status = 'pending'
    self.onResolvedCallback = [] //Promise resolve时的回调函数集
    self.onRejectedCallback = [] //Promise reject时的回调函数集
    function resolve(value) {
        if (value instanceof MyPromise) {
            return value.then(resolve, reject)
        }
        setTimeout(function() {
            if (self.status === 'pending') {
                self.status = 'resolved'
                self.data = value
                self.onResolvedCallback.forEach(cb => cb(self.data))
            }
        })
    }

    function reject(reason) {
        setTimeout(function() {
            if (self.status === 'pending') {
                self.status = 'rejected'
                self.data = reason
                self.onRejectedCallback.forEach(cb => cb(self.data))
            }
        })
    }
    try {
        executor(resolve, reject)
    } catch (reason) {
        reject(reason)
    }
}
MyPromise.prototype.then = function(onResolved, onRejected) {
    let self = this
    let promise2 //then必须返回一个新的promise
        //如果参数不是函数,那么则将其忽略。同时这里实现了值穿
    onResolved = typeof onResolved === 'function' ? onResolved : (v) => v
    onRejected = typeof onRejected === 'function' ? onRejected : (r) => { throw r }
    if (self.status === 'resolved') {
        //如果promise1(此处即为this)的状态已经确定并且是resolved,调用onResolved
        //考虑到可能会抛出错误,所以将其包在try/catch块中
        return promise2 = new MyPromise((resolve, reject) => {
            try {
                let x = onResolved(self.data)
                    // 如果onResolved的返回值是一个Promise对象,直接取它的结果做为promise2的结果
                if (x instanceof MyPromise) {
                    x.then(resolve, reject)
                }
                resolve(x)
            } catch (e) {
                reject(e)
            }
        })
    }
    if (self.status === 'rejected') {
        return promise2 = new MyPromise((resolve, reject) => {
            try {
                let x = onRejected(self.data)
                if (x instanceof MyPromise) {
                    x.then(resolve, reject)
                }
            } catch (e) {
                reject(e)
            }
        })
    }
    // 如果当前的Promise还处于pending状态,我们并不能确定调用onResolved还是onRejected,
    // 只能等到Promise的状态确定后,才能确实如何处理。
    // 所以我们需要把我们的**两种情况**的处理逻辑做为callback放入promise1(此处即this/self)的回调数组里
    if (self.status === 'pending') {
        return promise2 = new MyPromise((resolve, reject) => {
            self.onResolvedCallback.push((value) => {
                try {
                    let x = onResolved(self.data)
                    if (x instanceof MyPromise) {
                        x.then(resolve, reject)
                    }
                } catch (e) {
                    reject(e)
                }
            })
            self.onRejectedCallback.push((reason) => {
                try {
                    let x = onRejected(self.data)
                    if (x instanceof MyPromise) {
                        x.then(resolve, reject)
                    }
                } catch (e) {
                    reject(e)
                }
            })
        })
    }
}
MyPromise.prototype.catch = function(onRejected) { this.then(null, onRejected) }
//promise.then(onResolved, onRejected)里的这两相函数需要异步调用,关于这一点,标准里也有说明:
//In practice, this requirement ensures that onFulfilled and onRejected execute asynchronously, after the event loop turn in which then is called, and with a fresh stack.

call,apply,bind的原生实现

call

Function.prototype.call = function(context,...arg){
    context = context || window //不传入参数,默认this指向window
    const symbol = Symbol() //避免覆盖context的原始属性
    context[symbol] = this	//把要调用此方法的那个函数对象绑定在context上
    const val = context[symbol](...arg)	//在context上调用函数,那函数的this值就是context
    delete context[symbol] //删除context的fn属性,去除影响
    return val
}

apply

Function.prototype.myapply = function(context, arr) {
    context = context || window
    const symbol = Symbol()
    context[symbol] = this
    const result = context[symbol](...arr)
    delete context[symbol]
    return result
}
Function.prototype.myapply = function(context, arr) {
    return this.mycall(context, ...arr)
}

bind

Function.prototype.mybind = function(obj, ...arg) { //收集剩余参数
    return (...arg2) => this.myapply(obj, arg1.concat(arg2))
}
Function.prototype.mybind = function(obj, ...arg) {
    return (...arg2) => { //使用箭头函数绑定this在当前环境,否则this指向window
        let args = arg.concat(arg2)
        const symbol = Symbol()
        obj[symbol] = this;
        const val = obj[symbol](...args)
        delete obj[symbol]
        return val
    }
}
不使用箭头函数的话,只需要在return函数之前将this对象保存即可
let self = this

用于实现柯里化
function a(a,b,c){
    console.log(a+b+c)
}
let default = a.bind(undefined,1)
default(2,3) //6

New

function New(func, ...arg) {
    let MiddleObj = {}
    if (func.prototype !== null) {
        MiddleObj.__proto__ = func.prototype
    } 
    const value = func.call(MiddleObj, ...arg)  // this 绑定
    if ((typeof value === 'object' || typeof value === 'function') && value !== null) {
        return value
    }
    return MiddleObj
}

Object.create

Object.prototype.create = function(obj){
    if(Object.prototype.create){
        return Object.prototype.create
    }else{
        function F(){}
        F.prototype = obj
        return new F()
    }
}

Instanceof

function myInstanceof(left, right) {
    let prototype = right.prototype
    left = left.__proto__
    while (true) { //循环查询原型链  myInstanceof(实例, Object)
        if (left === null || left === undefined) { //原型链的终点是null
            return false
        }
        if (prototype === left) {
            return true
        }
        left = left.__proto__ 
    }
}

类型检测

typeof 对于原始类型来说,除了 null 都可以显示正确的类型

null会返回object因为在计算机中以32字节存储,对象的前三位为0,null的所有位都是0,就被错误判定为对象,这个bug不可修复。因为会对现有代码造成破坏

typeof对于对象,除了函数都会返回object

instanceof判断对象类型可以返回正确的结果,但无法判断原始类型

instanceof用于查看对象是否是特定构造函数的实例

如果想要instanceof可以判断原始类型

class PlusIns{
    static [Symbol.hasInstance](x){
        return typeof x === 'string'
    }
}
console.log('hey' instanceof PlusIns) //true

这是ES6的语法,当对象调用instanceof时会调用[Symbol.hasInstance]这个方法,通过改写这个方法达到判断原始类型的目的

Object.prototype.toString.call('') ;   // [object String]
Object.prototype.toString.call(1) ;    // [object Number]
Object.prototype.toString.call(true) ; // [object Boolean]
Object.prototype.toString.call(undefined) ; // [object Undefined]
Object.prototype.toString.call(null) ; // [object Null]
Object.prototype.toString.call(new Function()) ; // [object Function]
Object.prototype.toString.call(new Date()) ; // [object Date]
Object.prototype.toString.call([]) ; // [object Array]
Object.prototype.toString.call(new RegExp()) ; // [object RegExp]
Object.prototype.toString.call(new Error()) ; // [object Error]
Object.prototype.toString.call(document) ; // [object HTMLDocument]
Object.prototype.toString.call(window) ; //[object global] window是全局对象global的引用

类型转换

加法会进行隐式类型转换,如果一方为字符串,那么会把另外一方也转为字符串

如果一方不是字符串也不是数字,那么会将他转换为数字或者字符串

'a'++'b' //aNaN

因为+'b'等于NaN,也可以通过+'1'的形式来快速获取number类型

对于除了加法的运算符来说,只要其中一方是数字,那么另一方就会被转为数字

THIS

不管函数调用 bind 几次,fn 中的 this 永远由第一次 bind 决定也就是fn.bind().bind(a)() // window

bind没有传参,所以指向window

对于 == 来说,如果对比双方的类型不一样的话,就会进行隐式类型转换

[] == [] //false 因为是引用类型,两者的堆内存地址不同
[] == ![] //true 因为!运算符优先级较高,右边会变为false 其中一个为Boolean所以将两边转为数字,都为0,所以结果返回true

由于this在箭头函数中已经按照词法作用域绑定了,所以,用call()或者apply()调用箭头函数时,无法对this进行绑定,即传入的第一个参数被忽略

闭包

闭包存在的意义就是可以间接访问函数内部的变量。

闭包的作用

1.可以读取函数内部的变量

2.可以使变量的值长期保存在内存中,生命周期比较长。因此不能滥用闭包,否则会造成网页的性能问题

3.可以用来实现JS模块

for (var i = 1; i <= 5; i++) {
  setTimeout(function timer() {
    console.log(i)
  }, i * 1000)
}

setTimeout属于异步任务,在同步任务执行完毕后执行

setTimeout还有第三个参数,用于传参给函数

深度拷贝

Call By Sharing javascript中不存在按引用传递,传递方式都为值传递或者Call By Sharing 大意就是如果更改拷贝对象内部的值,会影响到原始对象,但如果直接更改了拷贝对象的引用地址,那么两者就相互独立,互不影响

浅拷贝只复制指向某个对象的指针,而不复制对象本身,新旧对象还是共享同一块内存。但深拷贝会另外创造一个一模一样的对象,新对象跟原对象不共享内存,修改新对象不会改到原对象。

  1. Object.assign

    let a = {
      age: 1
    }
    let b = Object.assign({}, a)
    a.age = 2
    console.log(b.age) // 1
    
  2. ...

    let a = {
      age: 1
    }
    let b = { ...a }
    a.age = 2
    console.log(b.age) // 1
    

浅拷贝在于如果对象内部得值还有对象的话,那么两个子对象之间还是会互相影响

let a = {
  age: 1,
  jobs: {
    first: 'FE'
  }
}
let b = { ...a }
a.jobs.first = 'native'
console.log(b.jobs.first) // native

通常可以通过 JSON.parse(JSON.stringify(object)) 来解决。

这种方法会忽略掉对象内部的Symbolundefinedfunction

实现深拷贝函数是很复杂的,推荐使用lodash提供的深拷贝函数

原型链

函数提升优于变量提升

JS中不存在类,CLASS只是语法糖,本质还是函数

Object与Function

Function instanceof Object;//true
Object instanceof Function;//true

首先ObjectFunction都是构造函数,而所有的构造函数都是Function的实例对象,因此ObjectFunction的实例对象

Function.prototypeObject的实例对象

实例对象的原型(__proto__)指向构造函数的prototype属性,因此Object.__proto__ === Function.prototype,Function.prototype.__proto__===Object.prototype

当访问一个属性的时候,它会沿着原型链向上查找,直到找到或者到Object.prototype.__proto__null)为止

匿名函数是浏览器中的native code ,由c或c++编写

寄生组合继承

function Parent(value) {
  this.val = value
}
Parent.prototype.getValue = function() {
  console.log(this.val)
}

function Child(value) {
  Parent.call(this, value)
}
Child.prototype = Object.create(Parent.prototype, {
  constructor: {
    value: Child,
    enumerable: false,
    writable: true,
    configurable: true
  }
})

const child = new Child(1)

child.getValue() // 1
child instanceof Parent // true



function create(proto,options){
    vat tmp = {};
    tmp.__proto__ = proto
    Object.defineProperties(tmp,options)
    return tmp
}

ES6实现继承必须调用super,否则子类无法正常使用,因为子类没有自己的this,是通过继承父类的this对象

class Parent {
  constructor(value) {
    this.val = value
  }
  getValue() {
    console.log(this.val)
  }
}
class Child extends Parent {
  constructor(value) {
    super(value)
    this.val = value
  }
}
let child = new Child(1)
child.getValue() // 1
child instanceof Parent // true

模块化

使用模块化有以下功能

  1. 立即执行函数

    立即执行函数是早期实现模块化的手段

    (function(globalVariable){
       globalVariable.test = function() {}
       // ... 声明各种变量、函数都不会污染全局作用域
    })(globalVariable)
    
  2. AMDCMD

    目前较少使用

  3. CommonJS

  4. ESModule

    原生实现的模块化方案,异步导入模块,采用实时绑定的方式,导入导出的值都指向同一个内存地址,所以导入值会跟随导出值变化

// 引入模块 API
import XXX from './a.js'
import { XXX } from './a.js'
// 导出模块 API
export function a() {}
export default function() {}

map,filter,reduce

["1", "2", "3"].map(parseInt)  // [1,NaN,NaN]

因为map的回调函数的参数index被当做了parseInt的基数radix,导致出现超范围的radix和不合法的进制解析

parseInt('1',0) = 1,
parseInt('2',1) = NaN,
parseInt('3',2) = NaN,

Polyfill是一个js库,主要抚平不同浏览器之间对js实现的差异。

map 作用是生成一个新数组,遍历原数组,将每个元素拿出来做一些变换然后放入到新的数组中。

[1, 2, 3].map(v => v + 1) // -> [2, 3, 4]

map 的回调函数接受三个参数,分别是当前索引元素,索引,原数组

mapforEach的不同之处在于,前者不会改变原数组,并返回经过操作后的新数组。而后者会返回undefined,并更改原数组

filter 的作用也是生成一个新数组,在遍历数组的时候将返回值为 true 的元素放入新数组,我们可以利用这个函数删除一些不需要的元素

let array = [1, 2, 4, 6]
let newArray = array.filter(item => item !== 6)
console.log(newArray) // [1, 2, 4]

map 一样,filter 的回调函数也接受三个参数,用处也相同。

const arr = [1, 2, 3]
const sum = arr.reduce((acc, current) => acc + current, 0)
console.log(sum)

对于 reduce 来说,它接受两个参数,分别是回调函数和初始值(可选),接下来我们来分解上述代码中 reduce 的过程

通过 reduce 来实现 map 函数

const arr = [1, 2, 3]
const mapArray = arr.map(value => value * 2)
const reduceArray = arr.reduce((acc, current) => {
  acc.push(current * 2)
  return acc
}, [])
console.log(mapArray, reduceArray) // [2, 4, 6]

异步

并发与并行是不同的,并发是指在一段时间内通过任务间的切换完成所有任务。并行则是cpu存在多个核心,可以同时去执行并完成多个任务。

Callback

ajax(url, () => {
    // 处理逻辑
})

Callback Hell

ajax(url, () => {
    // 处理逻辑
    ajax(url1, () => {
        // 处理逻辑
        ajax(url2, () => {
            // 处理逻辑
        })
    })
})
  1. 嵌套函数存在耦合性,一旦有所改动,就会牵一发而动全身
  2. 嵌套函数一多,就很难处理错误

Promise

构造 Promise 的时候,构造函数内部的代码是立即执行的

Promise 实现了链式调用,也就是说每次调用 then 之后返回的都是一个 Promise,并且是一个全新的 Promise,原因也是因为状态不可变。如果你在 then 中 使用了 return,那么 return 的值会被 Promise.resolve() 包装

Promise.resolve(1)
  .then(res => {
    console.log(res) // => 1
    return 2 // 包装成 Promise.resolve(2)
  })
  .then(res => {
    console.log(res) // => 2
  })

anync,await

一个函数如果加上 async ,那么该函数就会返回一个 Promise

async function test() {
  return "1"
}
console.log(test()) // -> Promise {<resolved>: "1"}

await 将异步代码改造成了同步代码,如果多个异步代码没有依赖性却使用了 await 会导致性能上的降低。

async function test() {
  // 以下代码没有依赖性的话,完全可以使用 Promise.all 的方式
  // 如果有依赖性的话,其实就是解决回调地狱的例子了
  await fetch(url)
  await fetch(url1)
  await fetch(url2)
}
let a = 0
let b = async () => {
  a = a + await 10
  console.log('2', a) // -> '2' 10
}
b()
a++
console.log('1', a) // -> '1' 1

Event Loop

当在浏览器中打开了一个标签页,就代表开启了一个进程,一个进程中可以有多个线程,比如渲染线程JS

引擎线程,HTTP请求线程等。当发起一个请求的时候,就是创建了一个线程,当请求结束后,该线程可能就会被销毁

console.log('script start')

async function async1() {
  await async2()
  console.log('async1 end')
}
async function async2() {
  console.log('async2 end')
}
async1()

setTimeout(function() {
  console.log('setTimeout')
}, 0)

new Promise(resolve => {
  console.log('Promise')
  resolve()
})
  .then(function() {
    console.log('promise1')
  })
  .then(function() {
    console.log('promise2')
  })

console.log('script end')
// script start => async2 end => Promise => script end => promise1 => promise2 => async1 end => setTimeout

创建promise对象里面的代码属于同步代码,setTimeout的任务队列优先级低于promise队列

EventLoop

javascript 为什么是单线程?

​ 假设javascript有两个线程,一个在某个DOM节点添加内容,另一个线程在这个节点上删除内容

使用web worker技术开的多线程有着诸多限制. 例如:所有新线程都受主线程的完全控制,不能独立执行。这意味着这些“线程” 实际上应属于主线程的子线程。另外,这些子线程并没有执行I/O操作的权限,只能为主线程分担一些诸如计算等任务。

把对应的回调加入当前执行栈…如此反复,进入循环。

0.1+0.2

JS 采用 IEEE 754 双精度版本(64位)

在计算机中是以二进制存储的,而0.1与0.2在二进制下都是无限循环,导致精度丢失。解决方案将他们变为整数就好

垃圾回收机制

V8实现了准确式GCGC算法采用了分代式垃圾回收机制。V8将内存分为新生代以及老生代

新生代算法

新生代中的对象一般存活时间较短,Scavenge GC算法,将内存空间分为两部分,分别为FromTo空间。在这两个空间中,必定有一个空间是使用的,另一个空间是空闲的。新分配的对象会被放入From空间中,当From空间被占满时,新生代GC就会启动。算法会检查From空间中存活的对象并复制到To空间中,如果有失活的对象就会销毁。当复制完成后将From空间和To空间互换。

老生代算法

老生代中的对象一般存活时间较长且数量较多,使用了两个算法,分别是标记清除算法和标记压缩算法

老生代中空间很复杂,有如下几个空间

enum AllocationSpace {
  // TODO(v8:7464): Actually map this space's memory as read-only.
  RO_SPACE,    // 不变的对象空间
  NEW_SPACE,   // 新生代用于 GC 复制算法的空间
  OLD_SPACE,   // 老生代常驻对象空间
  CODE_SPACE,  // 老生代代码对象空间
  MAP_SPACE,   // 老生代 map 对象
  LO_SPACE,    // 老生代大空间对象
  NEW_LO_SPACE,  // 新生代大空间对象

  FIRST_SPACE = RO_SPACE,
  LAST_SPACE = NEW_LO_SPACE,
  FIRST_GROWABLE_PAGED_SPACE = OLD_SPACE,
  LAST_GROWABLE_PAGED_SPACE = MAP_SPACE
};

在老生代对象中有如下情况会启动标记清除算法

在这个阶段,会遍历堆中的所有对象,然后标记活动对象,在标记完成后,销毁所有没有被标记的对象。

在2018年,GC技术新增了并发标记。该技术可以让GC扫描和标记对象时,同时允许JS运行

当清除对象后会造成堆内存出现碎片的情况,当碎片超过一定限制后会启动压缩算法。在压缩过程中,将活动对象向一端移动,直到所有对象都移动完成然后清理掉不需要的内存

基础知识

事件注册

如果给一个 body 中的子节点同时注册冒泡和捕获事件,事件触发会按照注册的顺序执行。

// 以下会先打印冒泡然后是捕获
node.addEventListener(
  'click',
  event => {
    console.log('冒泡')
  },
  false
)
node.addEventListener(
  'click',
  event => {
    console.log('捕获 ')
  },
  true
)

addEventListener可以接收三个参数,第三个参数可以使布尔值,也可以是对象。

对于布尔值useCapture参数来说,默认为false,代表事件是否捕获,对于对象参数来说,有以下几个属性

事件代理

如果一个节点中的子节点是动态生成的,那么子节点需要注册事件的话应该注册在父节点上

事件代理的方式相较于直接给目标注册事件来说,有以下优点:

  • 节省内存
  • 不需要给子节点注销事件

跨域

浏览器出于安全考虑,有同源策略。同源策略主要是用来防止CSRF攻击。防止利用用户的登录态发起恶意请求

JSONP
function jsonp(url, jsonpCallback, success) {
  let script = document.createElement('script')
  script.src = url
  script.async = true
  script.type = 'text/javascript'
  window[jsonpCallback] = function(data) {
    success && success(data)
  }
  document.body.appendChild(script)
}
jsonp('http://xxx', 'callback', function(value) {
  console.log(value)
})

CORS

CORS需要浏览器和后端同时支持。

服务端设置 Access-Control-Allow-Origin 就可以开启 CORS。 该属性表示哪些域名可以访问资源,如果设置通配符则表示所有网站都可以访问资源。

CORS会在发送请求时出现两种情况,分别为简单请求和复杂请求

简单请求

以 Ajax 为例,当满足以下条件时,会触发简单请求

  1. 使用下列方法之一:
    • GET
    • HEAD
    • POST
  2. Content-Type 的值仅限于下列三者之一:
    • text/plain
    • multipart/form-data
    • application/x-www-form-urlencoded
复杂请求

不符合以上条件的请求就是复杂请求。

对于复杂请求来说,首先会发起一个预检请求,该请求是 option 方法的,通过该请求来知道服务端是否允许跨域请求。

对于预检请求来说,如果你使用过 Node 来设置 CORS 的话,可能会遇到过这么一个坑。

以下以 express 框架举例:

app.use((req, res, next) => {
  res.header('Access-Control-Allow-Origin', '*')
  res.header('Access-Control-Allow-Methods', 'PUT, GET, POST, DELETE, OPTIONS')
  res.header(
    'Access-Control-Allow-Headers',
    'Origin, X-Requested-With, Content-Type, Accept, Authorization, Access-Control-Allow-Credentials'
  )
  next()
})

该请求会验证你的 Authorization 字段,没有的话就会报错。

当前端发起了复杂请求后,你会发现就算你代码是正确的,返回结果也永远是报错的。因为预检请求也会进入回调中,也会触发 next 方法,因为预检请求并不包含 Authorization 字段,所以服务端会报错。

想解决这个问题很简单,只需要在回调中过滤 option 方法即可

res.statusCode = 204
res.setHeader('Content-Length', '0')
res.end()

存储

特性 cookie localStorage sessionStorage indexDB
数据生命周期 一般由服务器生成,可以设置过期时间 除非被清理,否则一直存在 页面关闭就清理 除非被清理,否则一直存在
数据存储大小 4K 5M 5M 无限
与服务端通信 每次都会携带在 header 中,对于请求性能影响 不参与 不参与 不参与

cookie 不建议用于存储。如果没有大量数据存储需求的话,可以使用 localStoragesessionStorage 。对于不怎么改变的数据尽量使用 localStorage 存储,否则可以用 sessionStorage 存储。

属性 作用
value 如果用于保存用户登录态,应该将该值加密,不能使用明文的用户标识
http-only 不能通过 JS 访问 Cookie,减少 XSS 攻击
secure 只能在协议为 HTTPS 的请求中携带
same-site 规定浏览器不能在跨域请求中携带 Cookie,减少 CSRF 攻击

强缓存(200) 协商缓存(304)

有时候缓存是 200 OK (from disk cache)有时候会是 304 ? 看运维是否移除了 Entity Tag。移除了,就总是 200 OK (from cache)。没有移除,就两者交替出现。

他们两个的区别是 200 OK (from disk cache) 是浏览器没有跟服务器确认, 就是它直接用浏览器缓存。

304 是浏览器和服务器确认了一次缓存有效性,再用的缓存。

那么禁止200 OK (from disk cache) 这个缓存的方法是,ajax 请求是带上参数 cache: false

强缓存

强缓存可以通过设置两种 HTTP Header 实现:ExpiresCache-Control 。强缓存表示在缓存期间不需要请求,state code 为 200。

Expires
Expires: Wed, 22 Oct 2018 08:41:00 GMT

Expires 是 HTTP/1 的产物,表示资源会在 Wed, 22 Oct 2018 08:41:00 GMT 后过期,需要再次请求。并且 Expires 受限于本地时间,如果修改了本地时间,可能会造成缓存失效。

Cache-control
Cache-control: max-age=30

Cache-Control 出现于 HTTP/1.1,优先级高于 Expires 。该属性值表示资源会在 30 秒后过期,需要再次请求。

协商缓存

如果缓存过期了,就需要发起请求验证资源是否有更新。协商缓存可以通过设置两种 HTTP Header 实现:Last-ModifiedETag

当浏览器发起请求验证资源时,如果资源没有做改变,那么服务端就会返回 304 状态码,并且更新浏览器缓存有效期。

实际场景应用缓存策略
频繁变动的资源

对于频繁变动的资源,首先需要使用 Cache-Control: no-cache 使浏览器每次都请求服务器,然后配合 ETag 或者 Last-Modified 来验证资源是否有效。这样的做法虽然不能节省请求数量,但是能显著减少响应数据大小。

代码文件

这里特指除了 HTML 外的代码文件,因为 HTML 文件一般不缓存或者缓存时间很短。

一般来说,现在都会使用工具来打包代码,那么我们就可以对文件名进行哈希处理,只有当代码修改后才会生成新的文件名。基于此,我们就可以给代码文件设置缓存有效期一年 Cache-Control: max-age=31536000,这样只有当 HTML 文件中引入的文件名发生了改变才会去下载最新的代码文件,否则就一直使用缓存。

渲染

执行 JS 有一个 JS 引擎,那么执行渲染也有一个渲染引擎。

浏览器接收到 HTML 文件并转换为 DOM 树

将 CSS 文件转换为 CSSOM 树

生成渲染树

当浏览器生成渲染树以后,就会根据渲染树来进行布局(也可以叫做重排–Reflow)

为什么操作 DOM 慢

因为 DOM 是属于渲染引擎中的东西,而 JS 又是 JS 引擎中的东西。当我们通过 JS 操作 DOM 的时候,其实这个操作涉及到了两个线程之间的通信,那么势必会带来一些性能上的损耗。操作 DOM 次数一多,也就等同于一直在进行线程之间的通信,并且操作 DOM 可能还会带来重绘回流的情况,所以也就导致了性能上的问题。

插入几万个 DOM,如何实现页面不卡顿?

可以通过虚拟滚动(virtualized scroller)去解决这个问题

这种技术的原理就是只渲染可视区域内的内容,非可见区域的那就完全不渲染了,当用户在滚动的时候就实时去替换渲染的内容。

重绘(Repaint)和回流(Reflow

重绘和回流会在我们设置节点样式时频繁出现,同时也会很大程度上影响性能。

回流必定会发生重绘,重绘不一定会引发回流。回流所需的成本比重绘高的多,改变父节点里的子节点很可能会导致父节点的一系列回流。

以下几个动作可能会导致性能问题:

重绘和回流其实也和 Eventloop 有关。

  1. Eventloop 执行完 Microtasks 后,会判断 document 是否需要更新,因为浏览器是 60Hz 的刷新率,每 16.6ms 才会更新一次。
  2. 然后判断是否有 resize 或者 scroll 事件,有的话会去触发事件,所以 resizescroll事件也是至少 16ms 才会触发一次,并且自带节流功能。
  3. 判断是否触发了 media query
  4. 更新动画并且发送事件
  5. 判断是否有全屏操作事件
  6. 执行 requestAnimationFrame 回调
  7. 执行 IntersectionObserver 回调,该方法用于判断元素是否可见,可以用于懒加载上,但是兼容性不好
  8. 更新界面
  9. 以上就是一帧中可能会做的事情。如果在一帧中有空闲时间,就会去执行 requestIdleCallback回调。

以上内容来自于 HTML 文档

减少重绘和回流
<div class="test"></div>
<style>
  .test {
    position: absolute;
    top: 10px;
    width: 100px;
    height: 100px;
    background: red;
  }
</style>
<script>
  setTimeout(() => {
    // 引起回流
    document.querySelector('.test').style.top = '100px'
  }, 1000)
</script>

安全

XSS

XSS 可以分为多种类型,但是总体上分为两类:持久型和非持久型

持久型也就是攻击的代码被服务端写入进数据库中,这种攻击危害性很大,因为如果网站访问量很大的话,就会导致大量正常访问页面的用户都受到攻击。

非持久型相比于前者危害就小的多了,一般通过修改 URL 参数的方式加入攻击代码,诱导用户访问链接从而进行攻击。

转义字符

首先,对于用户的输入应该是永远不信任的。最普遍的做法就是转义输入输出的内容,对于引号、尖括号、斜杠进行转义

function escape(str) {
  str = str.replace(/&/g, '&amp;')
  str = str.replace(/</g, '&lt;')
  str = str.replace(/>/g, '&gt;')
  str = str.replace(/"/g, '&quto;')
  str = str.replace(/'/g, '&#39;')
  str = str.replace(/`/g, '&#96;')
  str = str.replace(/\//g, '&#x2F;')
  return str
}
js-xss

XSS-npm包

CSRF

CSRF 跨站请求伪造。原理就是攻击者构造出一个后端请求地址,诱导用户点击或者通过某些途径自动发起请求。如果用户是在登录状态下的话,后端就以为是用户在操作,从而进行相应的逻辑。

防范 CSRF 攻击可以遵循以下几种规则:

  1. Get 请求不对数据进行修改
  2. 不让第三方网站访问到用户 Cookie
  3. 阻止第三方网站请求接口
  4. 请求时附带验证信息,比如验证码或者 Token
验证 Referer

对于需要防范 CSRF 的请求,我们可以通过验证 Referer 来判断该请求是否为第三方网站发起的。

Token

服务器下发一个随机 Token,每次发起请求时将 Token 携带上,服务器验证 Token 是否有效。

性能优化

图片加载优化
  1. 不用图片。很多时候会使用到很多修饰类图片,其实这类修饰图片完全可以用 CSS 去代替。
  2. 对于移动端来说,屏幕宽度就那么点,完全没有必要去加载原图浪费带宽。一般图片都用 CDN 加载,可以计算出适配屏幕的宽度,然后去请求相应裁剪好的图片。
  3. 小图使用 base64 格式
  4. 将多个图标文件整合到一张图片中(雪碧图)
  5. 选择正确的图片格式:
    • 对于能够显示 WebP 格式的浏览器尽量使用 WebP 格式。因为 WebP 格式具有更好的图像数据压缩算法,能带来更小的图片体积,而且拥有肉眼识别无差异的图像质量,缺点就是兼容性并不好
    • 小图使用 PNG,其实对于大部分图标这类图片,完全可以使用 SVG 代替
    • 照片使用 JPEG
DNS 预解析

DNS 解析也是需要时间的,可以通过预解析的方式来预先获得域名所对应的 IP。

<link rel="dns-prefetch" href="//yuchengkai.cn">
节流以及防抖
懒加载

懒加载就是将不关键的资源延后加载。

懒加载的原理就是只加载自定义区域(通常是可视区域,但也可以是即将进入可视区域)内需要加载的东西。对于图片来说,先设置图片标签的 src 属性为一张占位图,将真实的图片资源放入一个自定义属性中,当进入自定义区域时,就将自定义属性替换为 src 属性,这样图片就会去下载资源,实现了图片懒加载。

懒加载不仅可以用于图片,也可以使用在别的资源上。比如进入可视区域才开始播放视频等等。

CDN

CDN 的原理是尽可能的在各个地方分布机房缓存数据,这样即使我们的根服务器远在国外,在国内的用户也可以通过国内的机房迅速加载资源。

因此,我们可以将静态资源尽量使用 CDN 加载,由于浏览器对于单个域名有并发请求上限,可以考虑使用多个 CDN 域名。并且对于 CDN 加载静态资源需要注意 CDN 域名要与主站不同,否则每次请求都会带上主站的 Cookie,平白消耗流量。

性能监控

对于性能监控来说,我们可以直接使用浏览器自带的 Performance API 来实现这个功能。

对于性能监控来说,其实我们只需要调用 performance.getEntriesByType('navigation') 这行代码就行了。

img

img

异常监控

对于代码运行错误,通常的办法是使用 window.onerror 拦截报错。该方法能拦截到大部分的详细报错信息,但是也有例外

对于异步代码来说,可以使用 catch 的方式捕获错误。比如 Promise 可以直接使用 catch 函数,async await 可以使用 try catch

但是要注意线上运行的代码都是压缩过的,需要在打包时生成 sourceMap 文件便于 debug。

对于捕获的错误需要上传给服务器,通常可以通过 img 标签的 src 发起一个请求。

另外接口异常就相对来说简单了,可以列举出出错的状态码。一旦出现此类的状态码就可以立即上报出错。接口异常上报可以让开发人员迅速知道有哪些接口出现了大面积的报错,以便迅速修复问题

TCP

HTTP TLS

GET POST
首部

首部分为请求首部和响应首部,并且部分首部两种通用,接下来我们就来学习一部分的常用首部。

通用首部

通用字段 作用
Cache-Control 控制缓存的行为
Connection 浏览器想要优先使用的连接类型,比如 keep-alive
Date 创建报文时间
Pragma 报文指令
Via 代理服务器相关信息
Transfer-Encoding 传输编码方式
Upgrade 要求客户端升级协议
Warning 在内容中可能存在错误

请求首部

请求首部 作用
Accept 能正确接收的媒体类型
Accept-Charset 能正确接收的字符集
Accept-Encoding 能正确接收的编码格式列表
Accept-Language 能正确接收的语言列表
Expect 期待服务端的指定行为
From 请求方邮箱地址
Host 服务器的域名
If-Match 两端资源标记比较
If-Modified-Since 本地资源未修改返回 304(比较时间)
If-None-Match 本地资源未修改返回 304(比较标记)
User-Agent 客户端信息
Max-Forwards 限制可被代理及网关转发的次数
Proxy-Authorization 向代理服务器发送验证信息
Range 请求某个内容的一部分
Referer 表示浏览器所访问的前一个页面
TE 传输编码方式

响应首部

响应首部 作用
Accept-Ranges 是否支持某些种类的范围
Age 资源在代理缓存中存在的时间
ETag 资源标识
Location 客户端重定向到某个 URL
Proxy-Authenticate 向代理服务器发送验证信息
Server 服务器名字
WWW-Authenticate 获取资源需要的验证信息

实体首部

实体首部 作用
Allow 资源的正确请求方式
Content-Encoding 内容的编码格式
Content-Language 内容使用的语言
Content-Length request body 长度
Content-Location 返回数据的备用地址
Content-MD5 Base64加密格式的内容 MD5检验值
Content-Range 内容的位置范围
Content-Type 内容的媒体类型
Expires 内容的过期时间
Last_modified 内容的最后修改时间
TLS

HTTPS 还是通过了 HTTP 来传输信息,但是信息通过 TLS 协议进行了加密。

TLS 协议位于传输层之上,应用层之下。首次进行 TLS 协议传输需要两个 RTT ,接下来可以通过 Session Resumption 减少到一个 RTT。

在 TLS 中使用了两种加密技术,分别为:对称加密和非对称加密。

对称加密

对称加密就是两边拥有相同的秘钥,两边都知道如何将密文加密解密。

这种加密方式固然很好,但是问题就在于如何让双方知道秘钥。因为传输数据都是走的网络,如果将秘钥通过网络的方式传递的话,一旦秘钥被截获就没有加密的意义的。

非对称加密

有公钥私钥之分,公钥所有人都可以知道,可以将数据用公钥加密,但是将数据解密必须使用私钥解密,私钥只有分发公钥的一方才知道。

这种加密方式就可以完美解决对称加密存在的问题。假设现在两端需要使用对称加密,那么在这之前,可以先使用非对称加密交换秘钥。

简单流程如下:首先服务端将公钥公布出去,那么客户端也就知道公钥了。接下来客户端创建一个秘钥,然后通过公钥加密并发送给服务端,服务端接收到密文以后通过私钥解密出正确的秘钥,这时候两端就都知道秘钥是什么了。

TLS 握手过程如下图:

img

客户端发送一个随机值以及需要的协议和加密方式。

服务端收到客户端的随机值,自己也产生一个随机值,并根据客户端需求的协议和加密方式来使用对应的方式,并且发送自己的证书(如果需要验证客户端证书需要说明)

客户端收到服务端的证书并验证是否有效,验证通过会再生成一个随机值,通过服务端证书的公钥去加密这个随机值并发送给服务端,如果服务端需要验证客户端证书的话会附带证书

服务端收到加密过的随机值并使用私钥解密获得第三个随机值,这时候两端都拥有了三个随机值,可以通过这三个随机值按照之前约定的加密方式生成密钥,接下来的通信就可以通过该密钥来加密解密

通过以上步骤可知,在 TLS 握手阶段,两端使用非对称加密的方式来通信,但是因为非对称加密损耗的性能比对称加密大,所以在正式传输数据时,两端使用对称加密的方式通信。

PS:以上说明的都是 TLS 1.2 协议的握手情况,在 1.3 协议中,首次建立连接只需要一个 RTT,后面恢复连接不需要 RTT 了。

设计模式

单例模式
class Singleton {
    constructor() {}
}
Singleton.getInstance = (function() {
    let instance
    return function() {
        if (!instance) {
            instance = new Singleton()
        }
        return instance
    }
})()
let s1 = Singleton.getInstance()
let s2 = Singleton.getInstance()
console.log('s1===s2', s1 === s2);
适配器模式

适配器用来解决两个接口不兼容的情况,不需要改变已有的接口,通过包装一层的方式实现两个接口的正常协作。

以下是如何实现适配器模式的例子

class Plug {
  getName() {
    return '港版插头'
  }
}

class Target {
  constructor() {
    this.plug = new Plug()
  }
  getName() {
    return this.plug.getName() + ' 适配器转二脚插头'
  }
}

let target = new Target()
target.getName() // 港版插头 适配器转二脚插头

在 Vue 中,我们其实经常使用到适配器模式。比如父组件传递给子组件一个时间戳属性,组件内部需要将时间戳转为正常的日期显示,一般会使用 computed 来做转换这件事情,这个过程就使用到了适配器模式。

装饰模式

装饰模式不需要改变已有的接口,作用是给对象添加功能。

以下是如何实现装饰模式的例子,使用了 ES7 中的装饰器语法

function readonly(target, key, descriptor) {
  descriptor.writable = false
  return descriptor
}

class Test {
  @readonly
  name = 'yck'
}

let t = new Test()

t.yck = '111' // 不可修改

算法

冒泡排序

function bubble(array) {
  checkArray(array);
  for (let i = array.length - 1; i > 0; i--) {
    // 从 0 到 `length - 1` 遍历
    for (let j = 0; j < i; j++) {
      if (array[j] > array[j + 1]) swap(array, j, j + 1)
    }
  }
  return array;
}
选择排序

function selection(array) {
  checkArray(array);
  for (let i = 0; i < array.length - 1; i++) {
    let minIndex = i;
    for (let j = i + 1; j < array.length; j++) {
      minIndex = array[j] < array[minIndex] ? j : minIndex;
    }
    swap(array, i, minIndex);
  }
  return array;
}
快排

快排的原理如下。随机选取一个数组中的值作为基准值,从左至右取值与基准值对比大小。比基准值小的放数组左边,大的放右边,对比完成后将基准值和第一个比基准值大的值交换位置。然后将数组以基准值的位置分为两部分,继续递归以上操作。


        function quickSort(arr){
            //如果数组<=1,则直接返回
            if(arr.length<=1){return arr;}
            var pivotIndex=Math.floor(arr.length/2);
            //找基准,并把基准从原数组删除
            var pivot=arr.splice(pivotIndex,1)[0];
            //定义左右数组
            var left=[];
            var right=[];

            //比基准小的放在left,比基准大的放在right
            for(var i=0;i<arr.length;i++){
                if(arr[i]<=pivot){
                    left.push(arr[i]);
                }
                else{
                    right.push(arr[i]);
                }
            }
            //递归
            return quickSort(left).concat([pivot],quickSort(right));
        }   
插入排序

插入排序的原理如下。第一个元素默认是已排序元素,取出下一个元素和当前元素比较,如果当前元素大就交换位置。那么此时第一个元素就是当前的最小数,所以下次取出操作从第三个元素开始,向前对比,重复之前的操作。

function Insertion(arr){
    for(let i = 1;i<arr.length;i++){
        for(let j = i-1;j>=0&&arr[j]>arr[j+1];j--){
            [arr[j],arr[j+1]] = [arr[j+1],arr[j]]
        }
    }
}

css解析选择器是从右向左解析的,因为从左向右解析,如果发现不匹配,需要进行回溯。影响性能

sort()方法在收到一个数组的时候,会判断数组的长度,如果数组小于10则选择插入排序,否则选择快速排序