2万字 | 前端基础拾遗90问

你们好,我是练习时长一年半的前端练习生,喜欢唱、跳、rap、敲代码。本文是笔者一年多来对前端基础知识的总结和思考,这些题目对本身是总结,对你们也是一点微薄的资料,但愿能给你们带来一些帮助和启发。成文过程当中获得了许多大佬的帮助,在此感谢恺哥的小册、神三元同窗的前端每日一问以及许多素未谋面的朋友们,让我等萌新也有机会在前人的财富中拾人牙慧,班门弄斧Thanks♪(・ω・)ノcss

本文将从如下十一个维度为读者总结前端基础知识html

JS基础

1. 如何在ES5环境下实现let

这个问题实质上是在回答letvar有什么区别,对于这个问题,咱们能够直接查看babel转换先后的结果,看一下在循环中经过let定义的变量是如何解决变量提高的问题前端

babel在let定义的变量前加了道下划线,避免在块级做用域外访问到该变量,除了对变量名的转换,咱们也能够经过自执行函数来模拟块级做用域

(function(){
  for(var i = 0; i < 5; i ++){
    console.log(i)  // 0 1 2 3 4
  }
})();

console.log(i)      // Uncaught ReferenceError: i is not defined
复制代码

不过这个问题并无结束,咱们回到varlet/const的区别上:react

  • var声明的变量会挂到window上,而letconst不会
  • var声明的变量存在变量提高,而letconst不会
  • letconst声明造成块做用域,只能在块做用域里访问,不能跨块访问,也不能跨函数访问
  • 同一做用域下letconst不能声明同名变量,而var能够
  • 暂时性死区,letconst声明的变量不能在声明前被使用

babel的转化,其实只实现了第二、三、5点git


2. 如何在ES5环境下实现const

实现const的关键在于Object.defineProperty()这个API,这个API用于在一个对象上增长或修改属性。经过配置属性描述符,能够精确地控制属性行为。Object.defineProperty() 接收三个参数:github

Object.defineProperty(obj, prop, desc)web

参数 说明
obj 要在其上定义属性的对象
prop 要定义或修改的属性的名称
descriptor 将被定义或修改的属性描述符

属性描述符 说明 默认值
value 该属性对应的值。能够是任何有效的 JavaScript 值(数值,对象,函数等)。默认为 undefined undefined
get 一个给属性提供 getter 的方法,若是没有 getter 则为 undefined undefined
set 一个给属性提供 setter 的方法,若是没有 setter 则为 undefined。当属性值修改时,触发执行该方法 undefined
writable 当且仅当该属性的writable为true时,value才能被赋值运算符改变。默认为 false false
enumerable enumerable定义了对象的属性是否能够在 for...in 循环和 Object.keys() 中被枚举 false
Configurable configurable特性表示对象的属性是否能够被删除,以及除value和writable特性外的其余特性是否能够被修改 false

对于const不可修改的特性,咱们经过设置writable属性来实现算法

function _const(key, value) {    
    const desc = {        
        value,        
        writable: false    
    }    
    Object.defineProperty(window, key, desc)
}
    
_const('obj', {a: 1})   //定义obj
obj.b = 2               //能够正常给obj的属性赋值
obj = {}                //没法赋值新对象
复制代码

参考资料:如何在 ES5 环境下实现一个const ?数据库

3. 手写call()

call() 方法使用一个指定的 this 值和单独给出的一个或多个参数来调用一个函数
语法:function.call(thisArg, arg1, arg2, ...)编程

call()的原理比较简单,因为函数的this指向它的直接调用者,咱们变动调用者即完成this指向的变动:

//变动函数调用者示例
function foo() {
    console.log(this.name)
}

// 测试
const obj = {
    name: '写代码像蔡徐抻'
}
obj.foo = foo   // 变动foo的调用者
obj.foo()       // '写代码像蔡徐抻'
复制代码

基于以上原理, 咱们两句代码就能实现call()

Function.prototype.myCall = function(thisArg, ...args) {
    thisArg.fn = this              // this指向调用call的对象,即咱们要改变this指向的函数
    return thisArg.fn(...args)     // 执行函数并return其执行结果
}
复制代码

可是咱们有一些细节须要处理:

Function.prototype.myCall = function(thisArg, ...args) {
    const fn = Symbol('fn')        // 声明一个独有的Symbol属性, 防止fn覆盖已有属性
    thisArg = thisArg || window    // 若没有传入this, 默认绑定window对象
    thisArg[fn] = this              // this指向调用call的对象,即咱们要改变this指向的函数
    const result = thisArg[fn](...args)  // 执行当前函数
    delete thisArg[fn]              // 删除咱们声明的fn属性
    return result                  // 返回函数执行结果
}

//测试
foo.myCall(obj)     // 输出'写代码像蔡徐抻'
复制代码

4. 手写apply()

apply() 方法调用一个具备给定this值的函数,以及做为一个数组(或相似数组对象)提供的参数。
语法:func.apply(thisArg, [argsArray])

apply()call()相似,区别在于call()接收参数列表,而apply()接收一个参数数组,因此咱们在call()的实现上简单改一下入参形式便可

Function.prototype.myApply = function(thisArg, args) {
    const fn = Symbol('fn')        // 声明一个独有的Symbol属性, 防止fn覆盖已有属性
    thisArg = thisArg || window    // 若没有传入this, 默认绑定window对象
    thisArg[fn] = this              // this指向调用call的对象,即咱们要改变this指向的函数
    const result = thisArg[fn](...args)  // 执行当前函数(此处说明一下:虽然apply()接收的是一个数组,但在调用原函数时,依然要展开参数数组。能够对照原生apply(),原函数接收到展开的参数数组)
    delete thisArg[fn]              // 删除咱们声明的fn属性
    return result                  // 返回函数执行结果
}

//测试
foo.myApply(obj, [])     // 输出'写代码像蔡徐抻'
复制代码

5. 手写bind()

bind() 方法建立一个新的函数,在 bind() 被调用时,这个新函数的 this 被指定为 bind() 的第一个参数,而其他参数将做为新函数的参数,供调用时使用。
语法: function.bind(thisArg, arg1, arg2, ...)

从用法上看,彷佛给call/apply包一层function就实现了bind():

Function.prototype.myBind = function(thisArg, ...args) {
    return () => {
        this.apply(thisArg, args)
    }
}
复制代码

但咱们忽略了三点:

  1. bind()除了this还接收其余参数,bind()返回的函数也接收参数,这两部分的参数都要传给返回的函数
  2. new会改变this指向:若是bind绑定后的函数被new了,那么this指向会发生改变,指向当前函数的实例
  3. 没有保留原函数在原型链上的属性和方法
Function.prototype.myBind = function (thisArg, ...args) {
    var self = this
    // new优先级
    var fbound = function () {
        self.apply(this instanceof self ? this : thisArg, args.concat(Array.prototype.slice.call(arguments)))
    }
    // 继承原型上的属性和方法
    fbound.prototype = Object.create(self.prototype);

    return fbound;
}

//测试
const obj = { name: '写代码像蔡徐抻' }
function foo() {
    console.log(this.name)
    console.log(arguments)
}

foo.myBind(obj, 'a', 'b', 'c')()    //输出写代码像蔡徐抻 ['a', 'b', 'c']
复制代码

6. 手写一个防抖函数

防抖和节流的概念都比较简单,因此咱们就不在“防抖节流是什么”这个问题上浪费过多篇幅了,简单点一下:

防抖,即短期内大量触发同一事件,只会执行一次函数,实现原理为设置一个定时器,约定在xx毫秒后再触发事件处理,每次触发事件都会从新设置计时器,直到xx毫秒内无第二次操做,防抖经常使用于搜索框/滚动条的监听事件处理,若是不作防抖,每输入一个字/滚动屏幕,都会触发事件处理,形成性能浪费。

function debounce(func, wait) {
    let timeout = null
    return function() {
        let context = this
        let args = arguments
        if (timeout) clearTimeout(timeout)
        timeout = setTimeout(() => {
            func.apply(context, args)
        }, wait)
    }
}
复制代码

7. 手写一个节流函数

防抖是延迟执行,而节流是间隔执行,函数节流即每隔一段时间就执行一次,实现原理为设置一个定时器,约定xx毫秒后执行事件,若是时间到了,那么执行函数并重置定时器,和防抖的区别在于,防抖每次触发事件都重置定时器,而节流在定时器到时间后再清空定时器

function throttle(func, wait) {
    let timeout = null
    return function() {
        let context = this
        let args = arguments
        if (!timeout) {
            timeout = setTimeout(() => {
                timeout = null
                func.apply(context, args)
            }, wait)
        }

    }
}
复制代码

实现方式2:使用两个时间戳prev旧时间戳now新时间戳,每次触发事件都判断两者的时间差,若是到达规定时间,执行函数并重置旧时间戳

function throttle(func, wait) {
    var prev = 0;
    return function() {
        let now = Date.now();
        let context = this;
        let args = arguments;
        if (now - prev > wait) {
            func.apply(context, args);
            prev = now;
        }
    }
}
复制代码

8. 数组扁平化

对于[1, [1,2], [1,2,3]]这样多层嵌套的数组,咱们如何将其扁平化为[1, 1, 2, 1, 2, 3]这样的一维数组呢:

1.ES6的flat()

const arr = [1, [1,2], [1,2,3]]
arr.flat(Infinity)  // [1, 1, 2, 1, 2, 3]
复制代码

2.序列化后正则

const arr = [1, [1,2], [1,2,3]]
const str = `[${JSON.stringify(arr).replace(/(\[|\])/g, '')}]`
JSON.parse(str)   // [1, 1, 2, 1, 2, 3]
复制代码

3.递归
对于树状结构的数据,最直接的处理方式就是递归

const arr = [1, [1,2], [1,2,3]]
function flat(arr) {
  let result = []
  for (const item of arr) {
    item instanceof Array ? result = result.concat(flat(item)) : result.push(item)
  }
  return result
}

flat(arr) // [1, 1, 2, 1, 2, 3]
复制代码

4.reduce()递归

const arr = [1, [1,2], [1,2,3]]
function flat(arr) {
  return arr.reduce((prev, cur) => {
    return prev.concat(cur instanceof Array ? flat(cur) : cur)
  }, [])
}

flat(arr)  // [1, 1, 2, 1, 2, 3]
复制代码

5.迭代+展开运算符

// 每次while都会合并一层的元素,这里第一次合并结果为[1, 1, 2, 1, 2, 3, [4,4,4]]
// 而后arr.some断定数组中是否存在数组,由于存在[4,4,4],继续进入第二次循环进行合并
let arr = [1, [1,2], [1,2,3,[4,4,4]]]
while (arr.some(Array.isArray)) {
  arr = [].concat(...arr);
}

console.log(arr)  // [1, 1, 2, 1, 2, 3, 4, 4, 4]
复制代码

9. 手写一个Promise

实现一个符合规范的Promise篇幅比较长,建议阅读笔者上一篇文章:异步编程二三事 | Promise/async/Generator实现原理解析 | 9k字


JS面向对象

在JS中一切皆对象,但JS并非一种真正的面向对象(OOP)的语言,由于它缺乏类(class)的概念。虽然ES6引入了classextends,使咱们可以轻易地实现类和继承。但JS并不存在真实的类,JS的类是经过函数以及原型链机制模拟的,本小节的就来探究如何在ES5环境下利用函数和原型链实现JS面向对象的特性

在开始以前,咱们先回顾一下原型链的知识,后续new继承等实现都是基于原型链机制。不少介绍原型链的资料都能写上洋洋洒洒几千字,但我以为读者们不须要把原型链想太复杂,容易把本身绕进去,其实在我看来,原型链的核心只须要记住三点:

  1. 每一个对象都有__proto__属性,该属性指向其原型对象,在调用实例的方法和属性时,若是在实例对象上找不到,就会往原型对象上找
  2. 构造函数的prototype属性也指向实例的原型对象
  3. 原型对象的constructor属性指向构造函数

1. 模拟实现new

首先咱们要知道new作了什么

  1. 建立一个新对象,并继承其构造函数的prototype,这一步是为了继承构造函数原型上的属性和方法
  2. 执行构造函数,方法内的this被指定为该新实例,这一步是为了执行构造函数内的赋值操做
  3. 返回新实例(规范规定,若是构造方法返回了一个对象,那么返回该对象,不然返回第一步建立的新对象)
// new是关键字,这里咱们用函数来模拟,new Foo(args) <=> myNew(Foo, args)
function myNew(foo, ...args) {
  // 建立新对象,并继承构造方法的prototype属性, 这一步是为了把obj挂原型链上, 至关于obj.__proto__ = Foo.prototype
  let obj = Object.create(foo.prototype)  
  
  // 执行构造方法, 并为其绑定新this, 这一步是为了让构造方法能进行this.name = name之类的操做, args是构造方法的入参, 由于这里用myNew模拟, 因此入参从myNew传入
  let result = foo.apply(obj, args)

  // 若是构造方法已经return了一个对象,那么就返回该对象,不然返回myNew建立的新对象(通常状况下,构造方法不会返回新实例,但使用者能够选择返回新实例来覆盖new建立的对象)
  return Object.prototype.toString.call(result) === '[object Object]' ? result : obj
}

// 测试:
function Foo(name) {
  this.name = name
}
const newObj = myNew(Foo, 'zhangsan')
console.log(newObj)                 // Foo {name: "zhangsan"}
console.log(newObj instanceof Foo)  // true
复制代码

2. ES5如何实现继承

说到继承,最容易想到的是ES6的extends,固然若是只回答这个确定不合格,咱们要从函数和原型链的角度上实现继承,下面咱们一步步地、递进地实现一个合格的继承

一. 原型链继承

原型链继承的原理很简单,直接让子类的原型对象指向父类实例,当子类实例找不到对应的属性和方法时,就会往它的原型对象,也就是父类实例上找,从而实现对父类的属性和方法的继承

// 父类
function Parent() {
    this.name = '写代码像蔡徐抻'
}
// 父类的原型方法
Parent.prototype.getName = function() {
    return this.name
}
// 子类
function Child() {}

// 让子类的原型对象指向父类实例, 这样一来在Child实例中找不到的属性和方法就会到原型对象(父类实例)上寻找
Child.prototype = new Parent()
Child.prototype.constructor = Child // 根据原型链的规则,顺便绑定一下constructor, 这一步不影响继承, 只是在用到constructor时会须要

// 而后Child实例就能访问到父类及其原型上的name属性和getName()方法
const child = new Child()
child.name          // '写代码像蔡徐抻'
child.getName()     // '写代码像蔡徐抻'
复制代码

原型继承的缺点:

  1. 因为全部Child实例原型都指向同一个Parent实例, 所以对某个Child实例的父类引用类型变量修改会影响全部的Child实例
  2. 在建立子类实例时没法向父类构造传参, 即没有实现super()的功能
// 示例:
function Parent() {
    this.name = ['写代码像蔡徐抻'] 
}
Parent.prototype.getName = function() {
    return this.name
}
function Child() {}

Child.prototype = new Parent()
Child.prototype.constructor = Child 

// 测试
const child1 = new Child()
const child2 = new Child()
child1.name[0] = 'foo'
console.log(child1.name)          // ['foo']
console.log(child2.name)          // ['foo'] (预期是['写代码像蔡徐抻'], 对child1.name的修改引发了全部child实例的变化)
复制代码

二. 构造函数继承

构造函数继承,即在子类的构造函数中执行父类的构造函数,并为其绑定子类的this,让父类的构造函数把成员属性和方法都挂到子类的this上去,这样既能避免实例之间共享一个原型实例,又能向父类构造方法传参

function Parent(name) {
    this.name = [name]
}
Parent.prototype.getName = function() {
    return this.name
}
function Child() {
    Parent.call(this, 'zhangsan')   // 执行父类构造方法并绑定子类的this, 使得父类中的属性可以赋到子类的this上
}

//测试
const child1 = new Child()
const child2 = new Child()
child1.name[0] = 'foo'
console.log(child1.name)          // ['foo']
console.log(child2.name)          // ['zhangsan']
child2.getName()                  // 报错,找不到getName(), 构造函数继承的方式继承不到父类原型上的属性和方法
复制代码

构造函数继承的缺点:

  1. 继承不到父类原型上的属性和方法

三. 组合式继承

既然原型链继承和构造函数继承各有互补的优缺点, 那么咱们为何不组合起来使用呢, 因此就有了综合两者的组合式继承

function Parent(name) {
    this.name = [name]
}
Parent.prototype.getName = function() {
    return this.name
}
function Child() {
    // 构造函数继承
    Parent.call(this, 'zhangsan') 
}
//原型链继承
Child.prototype = new Parent()
Child.prototype.constructor = Child

//测试
const child1 = new Child()
const child2 = new Child()
child1.name[0] = 'foo'
console.log(child1.name)          // ['foo']
console.log(child2.name)          // ['zhangsan']
child2.getName()                  // ['zhangsan']
复制代码

组合式继承的缺点:

  1. 每次建立子类实例都执行了两次构造函数(Parent.call()new Parent()),虽然这并不影响对父类的继承,但子类建立实例时,原型中会存在两份相同的属性和方法,这并不优雅

四. 寄生式组合继承

为了解决构造函数被执行两次的问题, 咱们将指向父类实例改成指向父类原型, 减去一次构造函数的执行

function Parent(name) {
    this.name = [name]
}
Parent.prototype.getName = function() {
    return this.name
}
function Child() {
    // 构造函数继承
    Parent.call(this, 'zhangsan') 
}
//原型链继承
// Child.prototype = new Parent()
Child.prototype = Parent.prototype  //将`指向父类实例`改成`指向父类原型`
Child.prototype.constructor = Child

//测试
const child1 = new Child()
const child2 = new Child()
child1.name[0] = 'foo'
console.log(child1.name)          // ['foo']
console.log(child2.name)          // ['zhangsan']
child2.getName()                  // ['zhangsan']
复制代码

但这种方式存在一个问题,因为子类原型和父类原型指向同一个对象,咱们对子类原型的操做会影响到父类原型,例如给Child.prototype增长一个getName()方法,那么会致使Parent.prototype也增长或被覆盖一个getName()方法,为了解决这个问题,咱们给Parent.prototype作一个浅拷贝

function Parent(name) {
    this.name = [name]
}
Parent.prototype.getName = function() {
    return this.name
}
function Child() {
    // 构造函数继承
    Parent.call(this, 'zhangsan') 
}
//原型链继承
// Child.prototype = new Parent()
Child.prototype = Object.create(Parent.prototype)  //将`指向父类实例`改成`指向父类原型`
Child.prototype.constructor = Child

//测试
const child = new Child()
const parent = new Parent()
child.getName()                  // ['zhangsan']
parent.getName()                 // 报错, 找不到getName()
复制代码

到这里咱们就完成了ES5环境下的继承的实现,这种继承方式称为寄生组合式继承,是目前最成熟的继承方式,babel对ES6继承的转化也是使用了寄生组合式继承

咱们回顾一下实现过程:
一开始最容易想到的是原型链继承,经过把子类实例的原型指向父类实例来继承父类的属性和方法,但原型链继承的缺陷在于对子类实例继承的引用类型的修改会影响到全部的实例对象以及没法向父类的构造方法传参
所以咱们引入了构造函数继承, 经过在子类构造函数中调用父类构造函数并传入子类this来获取父类的属性和方法,但构造函数继承也存在缺陷,构造函数继承不能继承到父类原型链上的属性和方法
因此咱们综合了两种继承的优势,提出了组合式继承,但组合式继承也引入了新的问题,它每次建立子类实例都执行了两次父类构造方法,咱们经过将子类原型指向父类实例改成子类原型指向父类原型的浅拷贝来解决这一问题,也就是最终实现 —— 寄生组合式继承


V8引擎机制

1. V8如何执行一段JS代码

  1. 预解析:检查语法错误但不生成AST
  2. 生成AST:通过词法/语法分析,生成抽象语法树
  3. 生成字节码:基线编译器(Ignition)将AST转换成字节码
  4. 生成机器码:优化编译器(Turbofan)将字节码转换成优化过的机器码,此外在逐行执行字节码的过程当中,若是一段代码常常被执行,那么V8会将这段代码直接转换成机器码保存起来,下一次执行就没必要通过字节码,优化了执行速度

上面几点只是V8执行机制的极简总结,建议阅读参考资料:

1.V8 是怎么跑起来的 —— V8 的 JavaScript 执行管道
2.JavaScript 引擎 V8 执行流程概述

2. 介绍一下引用计数和标记清除

  • 引用计数:给一个变量赋值引用类型,则该对象的引用次数+1,若是这个变量变成了其余值,那么该对象的引用次数-1,垃圾回收器会回收引用次数为0的对象。可是当对象循环引用时,会致使引用次数永远没法归零,形成内存没法释放。
  • 标记清除:垃圾收集器先给内存中全部对象加上标记,而后从根节点开始遍历,去掉被引用的对象和运行环境中对象的标记,剩下的被标记的对象就是没法访问的等待回收的对象。

3. V8如何进行垃圾回收

JS引擎中对变量的存储主要有两种位置,栈内存和堆内存,栈内存存储基本类型数据以及引用类型数据的内存地址,堆内存储存引用类型的数据

栈内存的回收:

栈内存调用栈上下文切换后就被回收,比较简单

堆内存的回收:

V8的堆内存分为新生代内存和老生代内存,新生代内存是临时分配的内存,存在时间短,老生代内存存在时间长

  • 新生代内存回收机制:
    • 新生代内存容量小,64位系统下仅有32M。新生代内存分为From、To两部分,进行垃圾回收时,先扫描From,将非存活对象回收,将存活对象顺序复制到To中,以后调换From/To,等待下一次回收
  • 老生代内存回收机制
    • 晋升:若是新生代的变量通过屡次回收依然存在,那么就会被放入老生代内存中
    • 标记清除:老生代内存会先遍历全部对象并打上标记,而后对正在使用或被强引用的对象取消标记,回收被标记的对象
    • 整理内存碎片:把对象挪到内存的一端

参考资料:聊聊V8引擎的垃圾回收

4. JS相较于C++等语言为何慢,V8作了哪些优化

  1. JS的问题:
    • 动态类型:致使每次存取属性/寻求方法时候,都须要先检查类型;此外动态类型也很难在编译阶段进行优化
    • 属性存取:C++/Java等语言中方法、属性是存储在数组中的,仅需数组位移就能够获取,而JS存储在对象中,每次获取都要进行哈希查询
  2. V8的优化:
    • 优化JIT(即时编译):相较于C++/Java这类编译型语言,JS一边解释一边执行,效率低。V8对这个过程进行了优化:若是一段代码被执行屡次,那么V8会把这段代码转化为机器码缓存下来,下次运行时直接使用机器码。
    • 隐藏类:对于C++这类语言来讲,仅需几个指令就能经过偏移量获取变量信息,而JS须要进行字符串匹配,效率低,V8借用了类和偏移位置的思想,将对象划分红不一样的组,即隐藏类
    • 内嵌缓存:即缓存对象查询的结果。常规查询过程是:获取隐藏类地址 -> 根据属性名查找偏移值 -> 计算该属性地址,内嵌缓存就是对这一过程结果的缓存
    • 垃圾回收管理:上文已介绍

参考资料:为何V8引擎这么快?


浏览器渲染机制

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

大致流程以下:

  1. HTML和CSS通过各自解析,生成DOM树和CSSOM树
  2. 合并成为渲染树
  3. 根据渲染树进行布局
  4. 最后调用GPU进行绘制,显示在屏幕上

2. 如何根据浏览器渲染机制加快首屏速度

  1. 优化文件大小:HTML和CSS的加载和解析都会阻塞渲染树的生成,从而影响首屏展现速度,所以咱们能够经过优化文件大小、减小CSS文件层级的方法来加快首屏速度
  2. 避免资源下载阻塞文档解析:浏览器解析到<script>标签时,会阻塞文档解析,直到脚本执行完成,所以咱们一般把<script>标签放在底部,或者加上defer、async来进行异步下载

3. 什么是回流(重排),什么状况下会触发回流

  • 当元素的尺寸或者位置发生了变化,就须要从新计算渲染树,这就是回流
  • DOM元素的几何属性(width/height/padding/margin/border)发生变化时会触发回流
  • DOM元素移动或增长会触发回流
  • 读写offset/scroll/client等属性时会触发回流
  • 调用window.getComputedStyle会触发回流

4. 什么是重绘,什么状况下会触发重绘

  • DOM样式发生了变化,但没有影响DOM的几何属性时,会触发重绘,而不会触发回流。重绘因为DOM位置信息不须要更新,省去了布局过程,于是性能上优于回流

5. 什么是GPU加速,如何使用GPU加速,GPU加速的缺点

  • 优势:使用transform、opacity、filters等属性时,会直接在GPU中完成处理,这些属性的变化不会引发回流重绘
  • 缺点:GPU渲染字体会致使字体模糊,过多的GPU处理会致使内存问题

6. 如何减小回流

  • 使用class替代style,减小style的使用
  • 使用resize、scroll时进行防抖和节流处理,这二者会直接致使回流
  • 使用visibility替换display: none,由于前者只会引发重绘,后者会引起回流
  • 批量修改元素时,能够先让元素脱离文档流,等修改完毕后,再放入文档流
  • 避免触发同步布局事件,咱们在获取offsetWidth这类属性的值时,能够使用变量将查询结果存起来,避免屡次查询,每次对offset/scroll/client等属性进行查询时都会触发回流
  • 对于复杂动画效果,使用绝对定位让其脱离文档流,复杂的动画效果会频繁地触发回流重绘,咱们能够将动画元素设置绝对定位从而脱离文档流避免反复回流重绘。

参考资料:必须明白的浏览器渲染机制


浏览器缓存策略

1. 介绍一下浏览器缓存位置和优先级

  1. Service Worker
  2. Memory Cache(内存缓存)
  3. Disk Cache(硬盘缓存)
  4. Push Cache(推送缓存)
  5. 以上缓存都没命中就会进行网络请求

2. 说说不一样缓存间的差异

  1. Service Worker

和Web Worker相似,是独立的线程,咱们能够在这个线程中缓存文件,在主线程须要的时候读取这里的文件,Service Worker使咱们能够自由选择缓存哪些文件以及文件的匹配、读取规则,而且缓存是持续性的

  1. Memory Cache

即内存缓存,内存缓存不是持续性的,缓存会随着进程释放而释放

  1. Disk Cache

即硬盘缓存,相较于内存缓存,硬盘缓存的持续性和容量更优,它会根据HTTP header的字段判断哪些资源须要缓存

  1. Push Cache

即推送缓存,是HTTP/2的内容,目前应用较少

3. 介绍一下浏览器缓存策略

强缓存(不要向服务器询问的缓存)

设置Expires

  • 即过时时间,例如「Expires: Thu, 26 Dec 2019 10:30:42 GMT」表示缓存会在这个时间后失效,这个过时日期是绝对日期,若是修改了本地日期,或者本地日期与服务器日期不一致,那么将致使缓存过时时间错误。

设置Cache-Control

  • HTTP/1.1新增字段,Cache-Control能够经过max-age字段来设置过时时间,例如「Cache-Control:max-age=3600」除此以外Cache-Control还能设置private/no-cache等多种字段

协商缓存(须要向服务器询问缓存是否已通过期)

Last-Modified

  • 即最后修改时间,浏览器第一次请求资源时,服务器会在响应头上加上Last-Modified ,当浏览器再次请求该资源时,浏览器会在请求头中带上If-Modified-Since 字段,字段的值就是以前服务器返回的最后修改时间,服务器对比这两个时间,若相同则返回304,不然返回新资源,并更新Last-Modified

ETag

  • HTTP/1.1新增字段,表示文件惟一标识,只要文件内容改动,ETag就会从新计算。缓存流程和 Last-Modified 同样:服务器发送 ETag 字段 -> 浏览器再次请求时发送 If-None-Match -> 若是ETag值不匹配,说明文件已经改变,返回新资源并更新ETag,若匹配则返回304

二者对比

  • ETag 比 Last-Modified 更准确:若是咱们打开文件但并无修改,Last-Modified 也会改变,而且 Last-Modified 的单位时间为一秒,若是一秒内修改完了文件,那么仍是会命中缓存
  • 若是什么缓存策略都没有设置,那么浏览器会取响应头中的 Date 减去 Last-Modified 值的 10% 做为缓存时间

参考资料:浏览器缓存机制剖析


网络相关

1. 讲讲网络OSI七层模型,TCP/IP和HTTP分别位于哪一层

alt

模型 概述 单位
物理层 网络链接介质,如网线、光缆,数据在其中以比特为单位传输 bit
数据链路层 数据链路层将比特封装成数据帧并传递
网络层 定义IP地址,定义路由功能,创建主机到主机的通讯 数据包
传输层 负责将数据进行可靠或者不可靠传递,创建端口到端口的通讯 数据段
会话层 控制应用程序之间会话能力,区分不一样的进程
表示层 数据格式标识,基本压缩加密功能
应用层 各类应用软件

2. 常见HTTP状态码有哪些

2xx 开头(请求成功)

200 OK:客户端发送给服务器的请求被正常处理并返回


3xx 开头(重定向)

301 Moved Permanently:永久重定向,请求的网页已永久移动到新位置。 服务器返回此响应时,会自动将请求者转到新位置

302 Moved Permanently:临时重定向,请求的网页已临时移动到新位置。服务器目前从不一样位置的网页响应请求,但请求者应继续使用原有位置来进行之后的请求

304 Not Modified:未修改,自从上次请求后,请求的网页未修改过。服务器返回此响应时,不会返回网页内容


4xx 开头(客户端错误)

400 Bad Request:错误请求,服务器不理解请求的语法,常见于客户端传参错误

401 Unauthorized:未受权,表示发送的请求须要有经过 HTTP 认证的认证信息,常见于客户端未登陆

403 Forbidden:禁止,服务器拒绝请求,常见于客户端权限不足

404 Not Found:未找到,服务器找不到对应资源


5xx 开头(服务端错误)

500 Inter Server Error:服务器内部错误,服务器遇到错误,没法完成请求

501 Not Implemented:还没有实施,服务器不具有完成请求的功能

502 Bad Gateway:做为网关或者代理工做的服务器尝试执行请求时,从上游服务器接收到无效的响应。

503 service unavailable:服务不可用,服务器目前没法使用(处于超载或停机维护状态)。一般是暂时状态。


3. GET请求和POST请求有何区别

标准答案:

  • GET请求参数放在URL上,POST请求参数放在请求体里
  • GET请求参数长度有限制,POST请求参数长度能够很是大
  • POST请求相较于GET请求安全一点点,由于GET请求的参数在URL上,且有历史记录
  • GET请求能缓存,POST不能

更进一步:

其实HTTP协议并无要求GET/POST请求参数必须放在URL上或请求体里,也没有规定GET请求的长度,目前对URL的长度限制,是各家浏览器设置的限制。GET和POST的根本区别在于:GET请求是幂等性的,而POST请求不是

幂等性,指的是对某一资源进行一次或屡次请求都具备相同的反作用。例如搜索就是一个幂等的操做,而删除、新增则不是一个幂等操做。

因为GET请求是幂等的,在网络很差的环境中,GET请求可能会重复尝试,形成重复操做数据的风险,所以,GET请求用于无反作用的操做(如搜索),新增/删除等操做适合用POST

参考资料:HTTP|GET 和 POST 区别?网上多数答案都是错的


4. HTTP的请求报文由哪几部分组成

一个HTTP请求报文由请求行(request line)、请求头(header)、空行和请求数据4个部分组成

响应报文和请求报文结构相似,再也不赘述

5. HTTP常见请求/响应头及其含义

通用头(请求头和响应头都有的首部)

字段 做用
Cache-Control 控制缓存 public:表示响应能够被任何对象缓存(包括客户端/代理服务器)
private(默认值):响应只能被单个客户缓存,不能被代理服务器缓存
no-cache:缓存要通过服务器验证,在浏览器使用缓存前,会对比ETag,若没变则返回304,使用缓存
no-store:禁止任何缓存
Connection 是否须要持久链接(HTTP 1.1默认持久链接) keep-alive / close
Transfer-Encoding 报文主体的传输编码格式 chunked(分块) / identity(未压缩和修改) / gzip(LZ77压缩) / compress(LZW压缩,弃用) / deflate(zlib结构压缩)

请求头

字段 做用 语法
Accept 告知(服务器)客户端能够处理的内容类型 text/html、image/*、*/*
If-Modified-Since Last-Modified的值发送给服务器,询问资源是否已通过期(被修改),过时则返回新资源,不然返回304 示例:If-Modified-Since: Wed, 21 Oct 2015 07:28:00 GMT
If-Unmodified-Since Last-Modified的值发送给服务器,询问文件是否被修改,若没有则返回200,不然返回412预处理错误,可用于断点续传。通俗点说If-Unmodified-Since是文件没有修改时下载,If-Modified-Since是文件修改时下载 示例:If-Unmodified-Since: Wed, 21 Oct 2015 07:28:00 GMT
If-None-Match ETag的值发送给服务器,询问资源是否已通过期(被修改),过时则返回新资源,不然返回304 示例:If-None-Match: "bfc13a6472992d82d"
If-Match ETag的值发送给服务器,询问文件是否被修改,若没有则返回200,不然返回412预处理错误,可用于断点续传 示例:If-Match: "bfc129c88ca92d82d"
Range 告知服务器返回文件的哪一部分, 用于断点续传 示例:Range: bytes=200-1000, 2000-6576, 19000-
Host 指明了服务器的域名(对于虚拟主机来讲),以及(可选的)服务器监听的TCP端口号 示例:Host:www.baidu.com
User-Agent 告诉HTTP服务器, 客户端使用的操做系统和浏览器的名称和版本 User-Agent: Mozilla/<version> (<system-information>) <platform> (<platform-details>) <extensions>

响应头

字段 做用 语法
Location 须要将页面从新定向至的地址。通常在响应码为3xx的响应中才会有意义 Location: <url>
ETag 资源的特定版本的标识符,若是内容没有改变,Web服务器不须要发送完整的响应 ETag: "<etag_value>"
Server 处理请求的源头服务器所用到的软件相关信息 Server: <product>

实体头(针对请求报文和响应报文的实体部分使用首部)

字段 做用 语法
Allow 资源可支持http请求的方法 Allow: <http-methods>,示例:Allow: GET, POST, HEAD
Last-Modified 资源最后的修改时间,用做一个验证器来判断接收到的或者存储的资源是否彼此一致,精度不如ETag 示例:Last-Modified: Wed, 21 Oct 2020 07:28:00 GMT
Expires 响应过时时间 Expires: <http-date>,示例:Expires: Wed, 21 Oct 2020 07:28:00 GMT

HTTP首部固然不止这么几个,但为了不写太多你们记不住(主要是别的我也没去看),这里只介绍了一些经常使用的,详细的能够看MDN的文档


6. HTTP/1.0和HTTP/1.1有什么区别

  • 长链接: HTTP/1.1支持长链接和请求的流水线,在一个TCP链接上能够传送多个HTTP请求,避免了由于屡次创建TCP链接的时间消耗和延时
  • 缓存处理: HTTP/1.1引入Entity tag,If-Unmodified-Since, If-Match, If-None-Match等新的请求头来控制缓存,详见浏览器缓存小节
  • 带宽优化及网络链接的使用: HTTP1.1则在请求头引入了range头域,支持断点续传功能
  • Host头处理: 在HTTP/1.0中认为每台服务器都有惟一的IP地址,但随着虚拟主机技术的发展,多个主机共享一个IP地址愈发广泛,HTTP1.1的请求消息和响应消息都应支持Host头域,且请求消息中若是没有Host头域会400错误

7. 介绍一下HTTP/2.0新特性

  • 多路复用: 即多个请求都经过一个TCP链接并发地完成
  • 服务端推送: 服务端可以主动把资源推送给客户端
  • 新的二进制格式: HTTP/2采用二进制格式传输数据,相比于HTTP/1.1的文本格式,二进制格式具备更好的解析性和拓展性
  • header压缩: HTTP/2压缩消息头,减小了传输数据的大小

8. 说说HTTP/2.0多路复用基本原理以及解决的问题

HTTP/2解决的问题,就是HTTP/1.1存在的问题:

  • TCP慢启动: TCP链接创建后,会经历一个先慢后快的发送过程,就像汽车启动通常,若是咱们的网页文件(HTML/JS/CSS/icon)都通过一次慢启动,对性能是不小的损耗。另外慢启动是TCP为了减小网络拥塞的一种策略,咱们是没有办法改变的。
  • 多条TCP链接竞争带宽: 若是同时创建多条TCP链接,当带宽不足时就会竞争带宽,影响关键资源的下载。
  • HTTP/1.1队头阻塞: 尽管HTTP/1.1长连接能够经过一个TCP链接传输多个请求,但同一时刻只能处理一个请求,当前请求未结束前,其余请求只能处于阻塞状态。

为了解决以上几个问题,HTTP/2一个域名只使用一个TCP⻓链接来传输数据,并且请求直接是并行的、非阻塞的,这就是多路复用

实现原理: HTTP/2引入了一个二进制分帧层,客户端和服务端进行传输时,数据会先通过二进制分帧层处理,转化为一个个带有请求ID的帧,这些帧在传输完成后根据ID组合成对应的数据。


9. 说说HTTP/3.0

尽管HTTP/2解决了不少1.1的问题,但HTTP/2仍然存在一些缺陷,这些缺陷并非来自于HTTP/2协议自己,而是来源于底层的TCP协议,咱们知道TCP连接是可靠的链接,若是出现了丢包,那么整个链接都要等待重传,HTTP/1.1能够同时使用6个TCP链接,一个阻塞另外五个还能工做,但HTTP/2只有一个TCP链接,阻塞的问题便被放大了。

因为TCP协议已经被普遍使用,咱们很难直接修改TCP协议,基于此,HTTP/3选择了一个折衷的方法——UDP协议,HTTP/2在UDP的基础上实现多路复用、0-RTT、TLS加密、流量控制、丢包重传等功能。


参考资料:http发展史(http0.九、http1.0、http1.一、http二、http3)梳理笔记 (推荐阅读)


10. HTTP和HTTPS有何区别

  • HTTPS使用443端口,而HTTP使用80
  • HTTPS须要申请证书
  • HTTP是超文本传输协议,是明文传输;HTTPS是通过SSL加密的协议,传输更安全
  • HTTPS比HTTP慢,由于HTTPS除了TCP握手的三个包,还要加上SSL握手的九个包

11. HTTPS是如何进行加密的

咱们经过分析几种加密方式,层层递进,理解HTTPS的加密方式以及为何使用这种加密方式:

对称加密

客户端和服务器公用一个密匙用来对消息加解密,这种方式称为对称加密。客户端和服务器约定好一个加密的密匙。客户端在发消息前用该密匙对消息加密,发送给服务器后,服务器再用该密匙进行解密拿到消息。

这种方式必定程度上保证了数据的安全性,但密钥一旦泄露(密钥在传输过程当中被截获),传输内容就会暴露,所以咱们要寻找一种安全传递密钥的方法。

非对称加密

采用非对称加密时,客户端和服务端均拥有一个公钥和私钥,公钥加密的内容只有对应的私钥能解密。私钥本身留着,公钥发给对方。这样在发送消息前,先用对方的公钥对消息进行加密,收到后再用本身的私钥进行解密。这样攻击者只拿到传输过程当中的公钥也没法破解传输的内容

尽管非对称加密解决了因为密钥被获取而致使传输内容泄露的问题,但中间人仍然能够用 篡改公钥的方式来获取或篡改传输内容,并且非对称加密的性能比对称加密的性能差了很多

第三方认证

上面这种方法的弱点在于,客户端不知道公钥是由服务端返回,仍是中间人返回的,所以咱们再引入一个第三方认证的环节:即第三方使用私钥加密咱们本身的公钥,浏览器已经内置一些权威第三方认证机构的公钥,浏览器会使用第三方的公钥来解开第三方私钥加密过的咱们本身的公钥,从而获取公钥,若是能成功解密,就说明获取到的本身的公钥是正确的

但第三方认证也未能彻底解决问题,第三方认证是面向全部人的,中间人也能申请证书,若是中间人使用本身的证书掉包原证书,客户端仍是没法确认公钥的真伪

数字签名

为了让客户端可以验证公钥的来源,咱们给公钥加上一个数字签名,这个数字签名是由企业、网站等各类信息和公钥通过单向hash而来,一旦构成数字签名的信息发生变化,hash值就会改变,这就构成了公钥来源的惟一标识。

具体来讲,服务端本地生成一对密钥,而后拿着公钥以及企业、网站等各类信息到CA(第三方认证中心)去申请数字证书,CA会经过一种单向hash算法(好比MD5),生成一串摘要,这串摘要就是这堆信息的惟一标识,而后CA还会使用本身的私钥对摘要进行加密,连同咱们本身服务器的公钥一同发送给我咱们。

浏览器拿到数字签名后,会使用浏览器本地内置的CA公钥解开数字证书并验证,从而拿到正确的公钥。因为非对称加密性能低下,拿到公钥之后,客户端会随机生成一个对称密钥,使用这个公钥加密并发送给服务端,服务端用本身的私钥解开对称密钥,此后的加密链接就经过这个对称密钥进行对称加密。

综上所述,HTTPS在验证阶段使用非对称加密+第三方认证+数字签名获取正确的公钥,获取到正确的公钥后以对称加密的方式通讯

参考资料:看图学HTTPS


前端安全

什么是CSRF攻击

CSRF即Cross-site request forgery(跨站请求伪造),是一种挟制用户在当前已登陆的Web应用程序上执行非本意的操做的攻击方法。

假如黑客在本身的站点上放置了其余网站的外链,例如"www.weibo.com/api,默认状况下,浏览器会带着weibo.com的cookie访问这个网址,若是用户已登陆过该网站且网站没有对CSRF攻击进行防护,那么服务器就会认为是用户本人在调用此接口并执行相关操做,导致帐号被劫持。

如何防护CSRF攻击

  • 验证Token:浏览器请求服务器时,服务器返回一个token,每一个请求都须要同时带上token和cookie才会被认为是合法请求
  • 验证Referer:经过验证请求头的Referer来验证来源站点,但请求头很容易伪造
  • 设置SameSite:设置cookie的SameSite,可让cookie不随跨域请求发出,但浏览器兼容不一

什么是XSS攻击

XSS即Cross Site Scripting(跨站脚本),指的是经过利用网页开发时留下的漏洞,注入恶意指令代码到网页,使用户加载并执行攻击者恶意制造的网页程序。常见的例如在评论区植入JS代码,用户进入评论页时代码被执行,形成页面被植入广告、帐号信息被窃取

XSS攻击有哪些类型

  • 存储型:即攻击被存储在服务端,常见的是在评论区插入攻击脚本,若是脚本被储存到服务端,那么全部看见对应评论的用户都会受到攻击。
  • 反射型:攻击者将脚本混在URL里,服务端接收到URL将恶意代码当作参数取出并拼接在HTML里返回,浏览器解析此HTML后即执行恶意代码
  • DOM型:将攻击脚本写在URL中,诱导用户点击该URL,若是URL被解析,那么攻击脚本就会被运行。和前二者的差异主要在于DOM型攻击不通过服务端

如何防护XSS攻击

  • 输入检查:对输入内容中的<script><iframe>等标签进行转义或者过滤
  • 设置httpOnly:不少XSS攻击目标都是窃取用户cookie伪造身份认证,设置此属性可防止JS获取cookie
  • 开启CSP,即开启白名单,可阻止白名单之外的资源加载和运行


排序算法

1. 手写冒泡排序

冒泡排序应该是不少人第一个接触的排序,比较简单,不展开讲解了

function bubbleSort(arr){
  for(let i = 0; i < arr.length; i++) {
    for(let j = 0; j < arr.length - i - 1; j++) {
      if(arr[j] > arr[j+1]) {
        let temp = arr[j]
        arr[j] = arr[j+1]
        arr[j+1] = temp
      }
    }
  }
  return arr
}
复制代码

2. 如何优化一个冒泡排序

冒泡排序总会执行(N-1)+(N-2)+(N-3)+..+2+1趟,但若是运行到当中某一趟时排序已经完成,或者输入的是一个有序数组,那么后边的比较就都是多余的,为了不这种状况,咱们增长一个flag,判断排序是否在中途就已经完成(也就是判断有无发生元素交换)

function bubbleSort(arr){
  for(let i = 0; i < arr.length; i++) {
  let flag = true
    for(let j = 0; j < arr.length - i - 1; j++) {
      if(arr[j] > arr[j+1]) {
        flag = false
        let temp = arr[j]
        arr[j] = arr[j+1]
        arr[j+1] = temp
      }
    }
    // 这个flag的含义是:若是`某次循环`中没有交换过元素,那么意味着排序已经完成
    if(flag)break;
  }
  return arr
}
复制代码

3. 手写快速排序

快排基本步骤:

  1. 选取基准元素
  2. 比基准元素小的元素放到左边,大的放右边
  3. 在左右子数组中重复步骤一二,直到数组只剩下一个元素
  4. 向上逐级合并数组
function quickSort(arr) {
    if(arr.length <= 1) return arr          //递归终止条件
    const pivot = arr.length / 2 | 0        //基准点
    const pivotValue = arr.splice(pivot, 1)[0]
    const leftArr = []
    const rightArr = []
    arr.forEach(val => {
        val > pivotValue ? rightArr.push(val) : leftArr.push(val)
    })
    return [ ...quickSort(leftArr), pivotValue, ...quickSort(rightArr)]
}
复制代码

4. 如何优化一个快速排序

原地排序

上边这个快排只是让读者找找感受,咱们不能这样写快排,若是每次都开两个数组,会消耗不少内存空间,数据量大时可能形成内存溢出,咱们要避免开新的内存空间,即原地完成排序

咱们能够用元素交换来取代开新数组,在每一次分区的时候直接在原数组上交换元素,将小于基准数的元素挪到数组开头,以[5,1,4,2,3]为例:

咱们定义一个pos指针, 标识等待置换的元素的位置, 而后逐一遍历数组元素, 遇到比基准数小的就和arr[pos]交换位置, 而后pos++

代码实现:

function quickSort(arr, left, right) {          //这个left和right表明分区后“新数组”的区间下标,由于这里没有新开数组,因此须要left/right来确认新数组的位置
    if (left < right) {
        let pos = left - 1                      //pos即“被置换的位置”,第一趟为-1
        for(let i = left; i <= right; i++) {    //循环遍历数组,置换元素
            let pivot = arr[right]              //选取数组最后一位做为基准数,
            if(arr[i] <= pivot) {               //若小于等于基准数,pos++,并置换元素, 这里使用小于等于而不是小于, 实际上是为了不由于重复数据而进入死循环
                pos++
                let temp = arr[pos]
                arr[pos] = arr[i]
                arr[i] = temp
            }
        }
        //一趟排序完成后,pos位置即基准数的位置,以pos的位置分割数组
        quickSort(arr, left, pos - 1)        
        quickSort(arr, pos + 1, right)
    }
    return arr      //数组只包含1或0个元素时(即left>=right),递归终止
}

//使用
var arr = [5,1,4,2,3]
var start = 0;
var end = arr.length - 1;
quickSort(arr, start, end)
复制代码

这个交换的过程仍是须要一些时间理解消化的,详细分析能够看这篇:js算法-快速排序(Quicksort)

三路快排

上边这个快排还谈不上优化,应当说是快排的纠正写法,其实有两个问题咱们还能优化一下:

  1. 有序数组的状况:若是输入的数组是有序的,而取基准点时也顺序取,就可能致使基准点一侧的子数组一直为空, 使时间复杂度退化到O(n2)
  2. 大量重复数据的状况:例如输入的数据是[1,2,2,2,2,3], 不管基准点取一、2仍是3, 都会致使基准点两侧数组大小不平衡, 影响快排效率

对于第一个问题, 咱们能够经过在取基准点的时候随机化来解决,对于第二个问题,咱们能够使用三路快排的方式来优化,比方说对于上面的[1,2,2,2,2,3],咱们基准点取2,在分区的时候,将数组元素分为小于2|等于2|大于2三个区域,其中等于基准点的部分再也不进入下一次排序, 这样就大大提升了快排效率

5. 手写归并排序

归并排序和快排的思路相似,都是递归分治,区别在于快排边分区边排序,而归并在分区完成后才会排序

function mergeSort(arr) {
    if(arr.length <= 1) return arr		//数组元素被划分到剩1个时,递归终止
    const midIndex = arr.length/2 | 0
    const leftArr = arr.slice(0, midIndex)
    const rightArr = arr.slice(midIndex, arr.length)
    return merge(mergeSort(leftArr), mergeSort(rightArr))	//先划分,后合并
}

//合并
function merge(leftArr, rightArr) {
    const result = []
    while(leftArr.length && rightArr.length) {
    	leftArr[0] <= rightArr[0] ? result.push(leftArr.shift()) : result.push(rightArr.shift())
    }
    while(leftArr.length) result.push(leftArr.shift())
    while(rightArr.length) result.push(rightArr.shift())
    return result
}
复制代码

6. 手写堆排序

堆是一棵特殊的树, 只要知足这棵树是彻底二叉树堆中每个节点的值都大于或小于其左右孩子节点这两个条件, 那么就是一个堆, 根据堆中每个节点的值都大于或小于其左右孩子节点, 又分为大根堆和小根堆

堆排序的流程:

  1. 初始化大(小)根堆,此时根节点为最大(小)值,将根节点与最后一个节点(数组最后一个元素)交换
  2. 除开最后一个节点,从新调整大(小)根堆,使根节点为最大(小)值
  3. 重复步骤二,直到堆中元素剩一个,排序完成

[1,5,4,2,3]为例构筑大根堆:

代码实现:

// 堆排序
const heapSort = array => {
        // 咱们用数组来储存这个大根堆,数组就是堆自己
	// 初始化大顶堆,从第一个非叶子结点开始
	for (let i = Math.floor(array.length / 2 - 1); i >= 0; i--) {
		heapify(array, i, array.length);
	}
	// 排序,每一次 for 循环找出一个当前最大值,数组长度减一
	for (let i = Math.floor(array.length - 1); i > 0; i--) {
		// 根节点与最后一个节点交换
		swap(array, 0, i);
		// 从根节点开始调整,而且最后一个结点已经为当前最大值,不须要再参与比较,因此第三个参数为 i,即比较到最后一个结点前一个便可
		heapify(array, 0, i);
	}
	return array;
};

// 交换两个节点
const swap = (array, i, j) => {
	let temp = array[i];
	array[i] = array[j];
	array[j] = temp;
};

// 将 i 结点如下的堆整理为大顶堆,注意这一步实现的基础其实是:
// 假设结点 i 如下的子堆已是一个大顶堆,heapify 函数实现的
// 功能是其实是:找到 结点 i 在包括结点 i 的堆中的正确位置。
// 后面将写一个 for 循环,从第一个非叶子结点开始,对每个非叶子结点
// 都执行 heapify 操做,因此就知足告终点 i 如下的子堆已是一大顶堆
const heapify = (array, i, length) => {
	let temp = array[i]; // 当前父节点
	// j < length 的目的是对结点 i 如下的结点所有作顺序调整
	for (let j = 2 * i + 1; j < length; j = 2 * j + 1) {
		temp = array[i]; // 将 array[i] 取出,整个过程至关于找到 array[i] 应处于的位置
		if (j + 1 < length && array[j] < array[j + 1]) {
			j++; // 找到两个孩子中较大的一个,再与父节点比较
		}
		if (temp < array[j]) {
			swap(array, i, j); // 若是父节点小于子节点:交换;不然跳出
			i = j; // 交换后,temp 的下标变为 j
		} else {
			break;
		}
	}
}
复制代码

参考资料: JS实现堆排序

7. 归并、快排、堆排有何区别

排序 时间复杂度(最好状况) 时间复杂度(最坏状况) 空间复杂度 稳定性
快速排序 O(nlogn) O(n^2) O(logn)~O(n) 不稳定
归并排序 O(nlogn) O(nlogn) O(n) 稳定
堆排序 O(nlogn) O(nlogn) O(1) 不稳定

其实从表格中咱们能够看到,就时间复杂度而言,快排并无很大优点,然而为何快排会成为最经常使用的排序手段,这是由于时间复杂度只能说明随着数据量的增长,算法时间代价增加的趋势,并不直接表明实际执行时间,实际运行时间还包括了不少常数参数的差异,此外在面对不一样类型数据(好比有序数据、大量重复数据)时,表现也不一样,综合来讲,快排的时间效率是最高的

在实际运用中, 并不仅使用一种排序手段, 例如V8的Array.sort()就采起了当 n<=10 时, 采用插入排序, 当 n>10 时,采用三路快排的排序策略


设计模式

设计模式有许多种,这里挑出几个经常使用的:

设计模式 描述 例子
单例模式 一个类只能构造出惟一实例 Redux/Vuex的store
工厂模式 对建立对象逻辑的封装 jQuery的$(selector)
观察者模式 当一个对象被修改时,会自动通知它的依赖对象 Redux的subscribe、Vue的双向绑定
装饰器模式 对类的包装,动态地拓展类的功能 React高阶组件、ES7 装饰器
适配器模式 兼容新旧接口,对类的包装 封装旧API
代理模式 控制对象的访问 事件代理、ES6的Proxy

1. 介绍一下单一职责原则和开放封闭原则

  • 单一职责原则:一个类只负责一个功能领域中的相应职责,或者能够定义为:就一个类而言,应该只有一个引发它变化的缘由。

  • 开放封闭原则:核心的思想是软件实体(类、模块、函数等)是可扩展的、但不可修改的。也就是说,对扩展是开放的,而对修改是封闭的。


2. 单例模式

单例模式即一个类只能构造出惟一实例,单例模式的意义在于共享、惟一Redux/Vuex中的store、JQ的$或者业务场景中的购物车、登陆框都是单例模式的应用

class SingletonLogin {
  constructor(name,password){
    this.name = name
    this.password = password
  }
  static getInstance(name,password){
    //判断对象是否已经被建立,若建立则返回旧对象
    if(!this.instance)this.instance = new SingletonLogin(name,password)
    return this.instance
  }
}
 
let obj1 = SingletonLogin.getInstance('CXK','123')
let obj2 = SingletonLogin.getInstance('CXK','321')
 
console.log(obj1===obj2)    // true
console.log(obj1)           // {name:CXK,password:123}
console.log(obj2)           // 输出的依然是{name:CXK,password:123}
复制代码

3. 工厂模式

工厂模式即对建立对象逻辑的封装,或者能够简单理解为对new的封装,这种封装就像建立对象的工厂,故名工厂模式。工厂模式常见于大型项目,好比JQ的$对象,咱们建立选择器对象时之因此没有new selector就是由于$()已是一个工厂方法,其余例子例如React.createElement()Vue.component()都是工厂模式的实现。工厂模式有多种:简单工厂模式工厂方法模式抽象工厂模式,这里只以简单工厂模式为例:

class User {
  constructor(name, auth) {
    this.name = name
    this.auth = auth
  }
}

class UserFactory {
  static createUser(name, auth) {
    //工厂内部封装了建立对象的逻辑:
    //权限为admin时,auth=1, 权限为user时, auth为2
    //使用者在外部建立对象时,不须要知道各个权限对应哪一个字段, 不须要知道赋权的逻辑,只须要知道建立了一个管理员和用户
    if(auth === 'admin')  new User(name, 1)
    if(auth === 'user')  new User(name, 2)
  }
}

const admin = UserFactory.createUser('cxk', 'admin');
const user = UserFactory.createUser('cxk', 'user');
复制代码

4. 观察者模式

观察者模式算是前端最经常使用的设计模式了,观察者模式概念很简单:观察者监听被观察者的变化,被观察者发生改变时,通知全部的观察者。观察者模式被普遍用于监听事件的实现,有关观察者模式的详细应用,能够看我另外一篇讲解Redux实现的文章

//观察者
class Observer {    
  constructor (fn) {      
    this.update = fn    
  }
}
//被观察者
class Subject {    
    constructor() {        
        this.observers = []          //观察者队列 
    }    
    addObserver(observer) {          
        this.observers.push(observer)//往观察者队列添加观察者 
    }    
    notify() {                       //通知全部观察者,其实是把观察者的update()都执行了一遍 
        this.observers.forEach(observer => {        
            observer.update()            //依次取出观察者,并执行观察者的update方法 
        })    
    }
}

var subject = new Subject()       //被观察者
const update = () => {console.log('被观察者发出通知')}  //收到广播时要执行的方法
var ob1 = new Observer(update)    //观察者1
var ob2 = new Observer(update)    //观察者2
subject.addObserver(ob1)          //观察者1订阅subject的通知
subject.addObserver(ob2)          //观察者2订阅subject的通知
subject.notify()                  //发出广播,执行全部观察者的update方法
复制代码

有些文章也把观察者模式称为发布订阅模式,其实两者是有所区别的,发布订阅相较于观察者模式多一个调度中心。


5. 装饰器模式

装饰器模式,能够理解为对类的一个包装,动态地拓展类的功能,ES7的装饰器语法以及React中的高阶组件(HoC)都是这一模式的实现。react-redux的connect()也运用了装饰器模式,这里以ES7的装饰器为例:

function info(target) {
  target.prototype.name = '张三'
  target.prototype.age = 10
}

@info
class Man {}

let man = new Man()
man.name // 张三
复制代码

6. 适配器模式

适配器模式,将一个接口转换成客户但愿的另外一个接口,使接口不兼容的那些类能够一块儿工做。咱们在生活中就经常有使用适配器的场景,例如出境旅游插头插座不匹配,这时咱们就须要使用转换插头,也就是适配器来帮咱们解决问题。

class Adaptee {
  test() {
      return '旧接口'
  }
}
 
class Target {
  constructor() {
      this.adaptee = new Adaptee()
  }
  test() {
      let info = this.adaptee.test()
      return `适配${info}`
  }
}
 
let target = new Target()
console.log(target.test())
复制代码

7. 代理模式

代理模式,为一个对象找一个替代对象,以便对原对象进行访问。即在访问者与目标对象之间加一层代理,经过代理作受权和控制。最多见的例子是经纪人代理明星业务,假设你做为一个投资者,想联系明星打广告,那么你就须要先通过代理经纪人,经纪人对你的资质进行考察,并通知你明星排期,替明星本人过滤没必要要的信息。事件代理、JQuery的$.proxy、ES6的proxy都是这一模式的实现,下面以ES6的proxy为例:

const idol = {
  name: '蔡x抻',
  phone: 10086,
  price: 1000000  //报价
}

const agent = new Proxy(idol, {
  get: function(target) {
    //拦截明星电话的请求,只提供经纪人电话
    return '经纪人电话:10010'
  },
  set: function(target, key, value) {
    if(key === 'price' ) {
      //经纪人过滤资质
      if(value < target.price) throw new Error('报价太低')
      target.price = value
    }
  }
})


agent.phone        //经纪人电话:10010
agent.price = 100  //Uncaught Error: 报价太低
复制代码


HTML相关

1. 说说HTML5在标签、属性、存储、API上的新特性

  • 标签:新增语义化标签(aside / figure / section / header / footer / nav等),增长多媒体标签videoaudio,使得样式和结构更加分离
  • 属性:加强表单,主要是加强了input的type属性;meta增长charset以设置字符集;script增长async以异步加载脚本
  • 存储:增长localStoragesessionStorageindexedDB,引入了application cache对web和应用进行缓存
  • API:增长拖放API地理定位SVG绘图canvas绘图Web WorkerWebSocket

2. doctype的做用是什么?

声明文档类型,告知浏览器用什么文档标准解析这个文档:

  • 怪异模式:浏览器使用本身的模式解析文档,不加doctype时默认为怪异模式
  • 标准模式:浏览器以W3C的标准解析文档

3. 几种前端储存以及它们之间的区别

  • cookies: HTML5以前本地储存的主要方式,大小只有4k,HTTP请求头会自动带上cookie,兼容性好
  • localStorage:HTML5新特性,持久性存储,即便页面关闭也不会被清除,以键值对的方式存储,大小为5M
  • sessionStorage:HTML5新特性,操做及大小同localStorage,和localStorage的区别在于sessionStorage在选项卡(页面)被关闭时即清除,且不一样选项卡之间的sessionStorage不互通
  • IndexedDB: NoSQL型数据库,类比MongoDB,使用键值对进行储存,异步操做数据库,支持事务,储存空间能够在250MB以上,可是IndexedDB受同源策略限制
  • Web SQL:是在浏览器上模拟的关系型数据库,开发者能够经过SQL语句来操做Web SQL,是HTML5之外一套独立的规范,兼容性差

4. href和src有什么区别

href(hyperReference)即超文本引用:当浏览器遇到href时,会并行的地下载资源,不会阻塞页面解析,例如咱们使用<link>引入CSS,浏览器会并行地下载CSS而不阻塞页面解析. 所以咱们在引入CSS时建议使用<link>而不是@import

<link href="style.css" rel="stylesheet" />
复制代码

src(resource)即资源,当浏览器遇到src时,会暂停页面解析,直到该资源下载或执行完毕,这也是script标签之因此放底部的缘由

<script src="script.js"></script>
复制代码

5. meta有哪些属性,做用是什么

meta标签用于描述网页的元信息,如网站做者、描述、关键词,meta经过name=xxxcontent=xxx的形式来定义信息,经常使用设置以下:

  • charset:定义HTML文档的字符集
<meta charset="UTF-8" >
复制代码
  • http-equiv:可用于模拟http请求头,可设置过时时间、缓存、刷新
<meta http-equiv="expires" content="Wed, 20 Jun 2019 22:33:00 GMT"复制代码
  • viewport:视口,用于控制页面宽高及缩放比例
<meta 
    name="viewport" 
    content="width=device-width, initial-scale=1, maximum-scale=1"
>
复制代码

6. viewport有哪些参数,做用是什么

  • width/height,宽高,默认宽度980px
  • initial-scale,初始缩放比例,1~10
  • maximum-scale/minimum-scale,容许用户缩放的最大/小比例
  • user-scalable,用户是否能够缩放 (yes/no)

7. http-equive属性的做用和参数

  • expires,指定过时时间
  • progma,设置no-cache能够禁止缓存
  • refresh,定时刷新
  • set-cookie,能够设置cookie
  • X-UA-Compatible,使用浏览器版本
  • apple-mobile-web-app-status-bar-style,针对WebApp全屏模式,隐藏状态栏/设置状态栏颜色


CSS相关

清除浮动的方法

为何要清除浮动:清除浮动是为了解决子元素浮动而致使父元素高度塌陷的问题

1.添加新元素

<div class="parent">
  <div class="child"></div>
  <!-- 添加一个空元素,利用css提供的clear:both清除浮动 -->
  <div style="clear: both"></div>
</div>  
复制代码

2.使用伪元素

/* 对父元素添加伪元素 */
.parent::after{
  content: "";
  display: block;
  height: 0;
  clear:both;
}
复制代码

3.触发父元素BFC

/* 触发父元素BFC */
.parent {
  overflow: hidden;
  /* float: left; */
  /* position: absolute; */
  /* display: inline-block */
  /* 以上属性都可触发BFC */
}
复制代码

介绍一下flex布局

其实我原本还写了一节水平/垂直居中相关的,不过感受内容过于基础还占长篇幅,因此删去了,做为一篇总结性的文章,这一小节也不该该从“flex是什么”开始讲,主轴、侧轴这些概念相信用过flex布局都知道,因此咱们直接flex的几个属性讲起:

容器属性(使用在flex布局容器上的属性)

  • justify-content
    定义了子元素在主轴(横轴)上的对齐方式
.container {
    justify-content: center | flex-start | flex-end | space-between | space-around;
    /* 主轴对齐方式:居中 | 左对齐(默认值) | 右对齐 | 两端对齐(子元素间边距相等) | 周围对齐(每一个子元素两侧margin相等) */
}
复制代码
  • align-items
    定义了定义项目在交叉轴(竖轴)上对齐方式
.container {
    align-items: center | flex-start | flex-end | baseline | stretch;
    /* 侧轴对齐方式:居中 | 上对齐 | 下对齐 | 项目的第一行文字的基线对齐 | 若是子元素未设置高度,将占满整个容器的高度(默认值) */
}
复制代码
  • flex-direction
    主轴(横轴)方向
.container {
    flex-direction: row | row-reverse | column | column-reverse;
    /* 主轴方向:水平由左至右排列(默认值) | 水平由右向左 | 垂直由上至下 | 垂直由下至上 */
}
复制代码
  • flex-wrap
    换行方式
.container {
    flex-wrap: nowrap | wrap | wrap-reverse;
    /* 换行方式:不换行(默认值) | 换行 | 反向换行 */
}
复制代码
  • flex-flow
    flex-flow属性是flex-direction属性和flex-wrap的简写
.container {
    flex-flow: <flex-direction> || <flex-wrap>;
    /* 默认值:row nowrap */
}
复制代码
  • align-content
    定义多根轴线的对齐方式
.container {
    align-content: center | flex-start | flex-end | space-between | space-around | stretch;
    /* 默认值:与交叉轴的中点对齐 | 与交叉轴的起点对齐 | 与交叉轴的终点对齐 | 与交叉轴两端对齐 | 每根轴线两侧的间隔都相等 | (默认值):轴线占满整个交叉轴 */
}
复制代码

项目属性(使用在容器内子元素上的属性)

  • flex-grow
    定义项目的放大比例,默认为0,即便有剩余空间也不放大。若是全部子元素flex-grow为1,那么将等分剩余空间,若是某个子元素flex-grow为2,那么这个子元素将占据2倍的剩余空间
.item {
  flex-grow: <number>; /* default 0 */
}
复制代码
  • flex-shrink
    定义项目的缩小比例,默认为1,即若是空间不足,子元素将缩小。若是全部子元素flex-shrink都为1,某个子元素flex-shrink为0,那么该子元素将不缩小
.item {
  flex-shrink: <number>; /* default 1 */
}
复制代码
  • flex-basis
    定义在分配多余空间以前,项目占据的主轴空间,默认auto,即子元素原本的大小,若是设定为一个固定的值,那么子元素将占据固定空间
.item {
  flex-basis: <length> | auto; /* default auto */
}
复制代码
  • flex
    flex属性是flex-grow, flex-shrinkflex-basis的简写,默认值为0 1 auto,即有剩余空间不放大,剩余空间不够将缩小,子元素占据自身大小
.item {
  flex: none | [ <'flex-grow'> <'flex-shrink'>? || <'flex-basis'> ]
}
复制代码

flex有两个快捷值:autonone,分别表明1 1 auto(有剩余空间则平均分配,空间不够将等比缩小,子元素占据空间等于自身大小)和0 0 auto(有剩余空间也不分配,空间不够也不缩小,子元素占据空间等于自身大小)

  • order
    定义项目的排列顺序。数值越小,排列越靠前,默认为0
.item {
  order: <integer>;
}
复制代码
  • align-self
    定义单个子元素的排列方式,例如align-items设置了center,使得全部子元素居中对齐,那么能够经过给某个子元素设置align-self来单独设置子元素的排序方式
.item {
  align-self: auto | flex-start | flex-end | center | baseline | stretch;
}
复制代码

参考资料:阮一峰Flex布局


常见布局

编辑中,请稍等-_-||


什么是BFC

BFC全称 Block Formatting Context 即块级格式上下文,简单的说,BFC是页面上的一个隔离的独立容器,不受外界干扰或干扰外界

如何触发BFC

  • float不为 none
  • overflow的值不为 visible
  • position 为 absolute 或 fixed
  • display的值为 inline-block 或 table-cell 或 table-caption 或 grid

BFC的渲染规则是什么

  • BFC是页面上的一个隔离的独立容器,不受外界干扰或干扰外界
  • 计算BFC的高度时,浮动子元素也参与计算(即内部有浮动元素时也不会发生高度塌陷)
  • BFC的区域不会与float的元素区域重叠
  • BFC内部的元素会在垂直方向上放置
  • BFC内部两个相邻元素的margin会发生重叠

BFC的应用场景

  • 清除浮动:BFC内部的浮动元素会参与高度计算,所以可用于清除浮动,防止高度塌陷
  • 避免某元素被浮动元素覆盖:BFC的区域不会与浮动元素的区域重叠
  • 阻止外边距重叠:属于同一个BFC的两个相邻Box的margin会发生折叠,不一样BFC不会发生折叠


总结

对于前端基础知识的讲解,到这里就告一小段落。前端的世界纷繁复杂,远非笔者寥寥几笔所能勾画,笔者就像在沙滩上拾取贝壳的孩童,有时侥幸拾取收集一二,就为之欢欣鼓舞,火烧眉毛与伙伴们分享。

最后还想可耻地抒(自)发(夸)一下(•‾̑⌣‾̑•)✧˖°:
不知不觉,在掘金已经水了半年有余,这半年来我写下了近6万字,不过其实一共只有5篇文章,这是由于我并不想写水文,不想把基础的东西水上几千字几十篇来混赞升级。写下的文章,首先要能说服本身。要对本身写下的东西负责任,即便是一张图、一个标点。例如第一张图,我调整了不下十次,第一次我直接截取babel的转化结果,以为很差看,换成了代码块,仍是很差看,又换成了carbon的代码图,第一次下载,发现两张图宽度不同,填充宽度从新下载,又发现本身的代码少了一个空格,从新下载,为了实现两张图并排效果,写了一个HTML来调整两张图的样式,为了保证每张图的内容和边距一致,我一边截图,一边记录下每次截图的尺寸和边距,每次截图都根据上一次的数据调整边距。

其实我并不是提倡把时间花在这些细枝末节上,只是单纯以为,文章没写好,就不能发出来,就像小野二郎先生说的那样:“菜作的很差,就不能拿给客人吃”,世间的大道理,每每都这样通俗简单。

往期文章:

1. 异步编程二三事 | Promise/async/Generator实现原理解析 | 9k字
2. 10行代码看尽redux实现 —— redux & react-redux & redux中间件设计实现 | 8k字
3.红黑树上红黑果,红黑树下你和我 —— 红黑树入门 | 6k字
4. 深刻React服务端渲染原理 | 1W字

相关文章
相关标签/搜索