前端基础面试题

492 阅读1小时+

数据类型

基本类型:String,Number,Boolean,null,undefined,Symbol;基本类型存储在栈中;

引用类型:Object,Function,Array;引用类型存储在堆中;

数组

  • 数组中的方法
  1. push(a,b..): 向数组的末尾添加数据,改变原数组;
  2. pop(): 把数组的最后一项删除掉,改变原数组;
  3. shift(): 把数组的第一项进行删除,改变原数组;
  4. unshift(XX,...): 向数组的第一项添加新的数据,改变原数组;
  5. sort(fn): 把数组进行排序,默认顺序是将元素转成字符串,然后比较它们的UTF-16的单元值;改变原数组;
  6. reverse(): 把数组进行翻转,改变原数组;
  7. splice(索引,个数,新值):删除,或替换或新增一些数据,改变原数组;
  8. join(字符):使用指定字符进行拼接数组中的数据,返回一个字符串;
  9. toString(): 把数组中的数据通过逗号拼接成字符串;
  10. concat(arr): 连接多个数组或数据,返回一个新数组;
  11. slice(s,e): 从数组中截取一段数据,返回一个新数组;
  12. forEach(fn): 遍历数组的每一项;
  13. map(fn): 遍历数组的每一项,返回一个新数组,对于数组中的空值不做处理直接返回;
  14. filter(fn): 遍历数组,返回符合要求的数据,返回一个新数组;
  15. reduce(fn,initValue): 遍历数组,每次遍历可以获取到上次返回的结果,并且可以设置初始值,如果没有设置初始值,那么数组的第一项就是初始值,遍历的时候从第二项开始;
  16. reduceRight(fn,initValue): 和reduce一样,只是从右往左遍历;
  17. every(fn): 遍历数组,查看每一项是否符合条件,如果都符合就返回true,否则就是false;
  18. some(fn): 遍历数组,查看是否有符合条件的,最少一项,遍历到符合条件的就直接返回true,不再继续遍历剩下的;都不符合就返回false;
  19. find(fn): 遍历数组,查找第一个符合条件的元素,并且返回这个元素,如果都不符合就返回undefined;
  20. findIndex(fn): 遍历数组,查找第一个符合条件的元素的索引,并且返回这个索引,都不符合就返回-1;
  21. findLast(fn): 从右往左遍历数组,查找第一个符合条件的元素,并且返回这个元素,不符合返回undefined;
  22. findLastIndex(fn): 从右往左遍历数组,查找第一个符合条件的元素的索引,返回这个索引,不符合就返回-1;
  23. lastIndexOf(数据):从右往左查找指定的元素,找到就返回索引,否则就是-1;
  24. isArray(): 判断是否是一个数组,返回布尔值;
  25. keys(): 返回数组的所有的key是一个数组;
  26. values(): 返回数组的所有的值,是一个数组;
  27. from(类数组/迭代对象,fn,slef): 遍历类数组或迭代对象返回一个新数组;默认返回数组的每一项;
  28. flat(deep): 根据指定的深度扁平化数组,默认值是1,返回一个新数组;
  29. flatMap(fn): 遍历数组的每一项,深度为1,返回回调函数的值,组成一个新数组;
  30. fill(填充值,开始位置,结束位置):从开始到结束位置使用指定的值进行填充,修改原数组;
  • 属性:
  1. length: 获取数组的长度

位运算符

  • a | b:先把左右两边都转换为二进制,每一位进行比较,只要有1就取1,否则就取0,把比较之后的结果转成十进制;
3 | 4 // 7
// 分析:3转成二进制011,4转成二进制100,进行比较得到111,111的十进制就是7
  • a & b:左右两边都转成二进制,每一位进行比较,都为1就取1,否则就取0,比较之后的结果转成十进制;
3 & 4 // 结果就是 0  3转为二进制就是011, 4转为二进制就是100,
//两个进行对比,得000再转为十进制就是0
  • ~a: 转成二进制,再每一位进行取反,再转成十进制;
~5 // ~ 5 结果是-6  5转为二进制101,补全之后00000101,取反11111010,
// 首位是1,所以是负数,先减一之后11111001,再取反再转十进制 -6
  • a ^ b:先转成二进制,再比较每一位,不同取1,相同取0,再转成十进制;
5 ^ 1  // 结果4  5转二进制0101,1转二进制0001,对比得0100,转为十进制就是4
  • a << b:把a转成二进制,把其中的1向左移动b位,再转成十进制;
5 << 1  // 结果10,5转为二进制就是0101,然后向左移动1位,得1010,
// 然后再转为十进制就是10
  • a >> b: 把a转成二进制,把其中的1向右移动b位,再转成十进制
5 >> 1 // 结果2, 5转为二进制就是0101,然后向左移动1位,
// 得0010,然后再转为十进制就是2

执行上下文

执行上下文:指当前环境中的变量、函数式声明、作用域链、this等信息;函数或js代码在执行前会进行预编译,此时就会创建一个执行上下文,函数每次执行的时候都会创建一个执行上下文,多次执行创建多个执行上下文,因此执行上下文是独一无二的;函数执行完毕执行上下文会被销毁;

变量对象:它是一个抽象的概念,存储当前环境中所有的的变量、函数参数和函数式声明,在浏览器中全局上下文中的变量对象就是window对象;

执行上下文的生命周期:

  1. 创建阶段(预编译)
  • 获取函数的形参,值为undefined,存入到变量对象中;
  • 获取当前环境中所有的var声明的变量,值默认为undefined,存入变量对象中;没有使用变量关键字声明的变量则会存入全局执行上下文的变量对象中;let和const声明的变量不会在创建阶段进行存储,它会在执行阶段进行存储;因此它没有变量提升的概念;
  • 把实参的值赋值给形参;
  • 获取当前环境中所有的函数式声明的函数,函数名作为属性,值为函数在内存中地址的引用存入到变量对象中,如果多个重名的变量后者会覆盖前者;
  1. 执行阶段
  • 从上往下执行代码,给变量进行赋值;把let,const声明的变量存入到变量对象中,并且进行赋值;

image.png

执行上下文有全局、函数和eval执行上下文;

执行上下文的特点:

  • 单线程,只在主线程上执行
  • 同步执行,从上往下;
  • 独一无二,函数每次执行都会创建一个执行上下文;

执行栈

是一种先进后出的数据结构,用来存储代码运行的所有的执行上下文;

整体流程:

  1. 当js脚本执行的时候,会把全局的执行上下文推到栈中;
  2. 当执行其中的函数的时候会把函数的执行上下文推入到中栈;
  3. 当此函数执行完毕,就会从栈中推出此执行上下文;
  4. 当js脚本全部执行完毕,就会把全局的执行上下文推出栈中;

作用域和作用域链

作用域

可访问变量的集合(可访问变量、对象、函数的集合),每个对象中的scope就是它的作用域;
作用: 隔离变量;
js中的作用域是静态作用域;在函数定义的时候就已经确定了;
作用域类型: 全局作用域、函数作用域、块级作用域;

let、const和var

  • var声明的变量具有变量提升,能够重复声明,没有块级作用域的限制;在全局定义的变量会被挂在window上;
  • let和const声明的变量没有变量提升,必须先声明后使用;具有块级作用域的限制;不能重复声明,并且const定义常量在定义的时候就要进行初始化;let和const在全局中定义的变量不会被挂在window上;

作用域链

函数在定义的时候就确定了自己的作用域,它包含它所在环境的作用域,这些作用域成链式的链接,因此就是作用域链;

经典题目

var a = 1
function f(){ //函数在全局定义的所以函数f的作用域嵌套在GO:{ a:1,f:fn,t:fn }里
  console.log(a) //所以这里的a是全局Go下面的a
}
function t(){
  var a = 2
  f() //虽然函数f在这里执行,但是它定义在全局
}
t()

// 结果 1
var scope = "global scope";
function checkscope(){
  var scope = "local scope";
  function f(){
    console.log(scope);
  }
  return f();
}
checkscope(); 

// 结果: local scope
var scope2 = "global scope";
function checkscope2(){
  var scope2 = "local scope";
  function f2(){
    console.log(scope2);
  }
  return f2;
}
checkscope2()();
// 结果 local scope

image.png

this

this的4种绑定方式

  • 默认绑定,严格模式下函数中的this为undefined,非严格模式下this为window对象;
  • 隐式绑定,谁调用了此函数,函数中的this就指向谁;
  • 显示绑定,通过call、apply和bind显示的修改this;
  • new构造函数,this指向new出来的新对象;

注意: 箭头函数中的this是它在定义的时候所在环境中的this;

"use strict";
var a = 10; // var定义的a变量挂载到window对象上
function foo () {
    console.log('this1', this) // undefined 
    console.log(window.a) // 10
    console.log(this.a) // 报错,Uncaught TypeError: Cannot read properties of undefined (reading 'a') 
} 
console.log('this2', this) // window foo();

严格模式下,函数内部的this指向undefined;

let a = 10
const b = 20
function foo () { 
    console.log(this.a) // undefined 
    console.log(this.b) // undefined 
} 
foo(); 
console.log(window.a) // undefined

let和const定义的变量不会挂载window对象下;

var a = 1
function foo () {
  var a = 2
  console.log(this)  // window
  console.log(this.a) // 1
}
foo()
var obj2 = {
  a: 2,
  foo1: function () {
    console.log(this.a) // 2
  },
  foo2: function () {
    setTimeout(function () {
      console.log(this) // window
      console.log(this.a) // 3
    }, 0)
  }
}
var a = 3

obj2.foo1()
obj2.foo2() 

setTimeout中的回调函数是在setTimeout函数内部被执行,因此它默认被window对象调用;

var obj = {
  name: 'obj',
  foo1: () => {
    console.log(this.name) // window
  },
  foo2: function () {
    console.log(this.name) // obj
    return () => {
      console.log(this.name) // obj
    }
  }
 }
 var name = 'window'
 obj.foo1()
 obj.foo2()()

箭头函数中的this是定义时所在环境中的this;

apply、call和bind的区别

  • 三者都是用来修改this指向的
  • 三者的第一个参数都是要指向的this对象,若为undefined或null,则this为window
  • apply的第二个参数为数组、call为列表、bind可以多次传入参数,参数会进行合并;
  • apply和Call会立即执行,而bind需要再次调用,如果通过new的形式,bind绑定的this无效,new优先;

实现call、apply、bind

Function.prototype.call = function (context, ...args) {
  // 如果没有传递指定的对象就设置为window
  if (!context) {
    context = window
  }
  // 创建一个key
  const fn = Symbol()
  // 保存调用者,这样就可以在执行fn的时候是context执行的,其中的this就指向了context
  context[fn] = this
  // 执行调用者传递参数
  const result = context[fn](...args)
  // 删除保存着
  delete context[fn]
  // 返回执行结果
  return result
}
Function.prototype.apply = function (context, args) {
    if (!context) {
        context = window
    }
    const fn = Symbol()
    context[fn] = this
    const result = context[fn](...args)
    delete context[fn]
    return result
}
Function.prototype.bind = function (context, ...args) {
    if (!context) {
        context = window
    }
    // 保存调用者
    const fn = this
    const key = Symbol()
    const result = function (...args1) {
        // 如果是new result
        if (this instanceof fn) {
            this[key] = fn
            const res = this[key](...args, ...args1)
            delete this[key]
            return res
        } else {
            context[key] = fn
            const res = context[key](...args, ...args1)
            delete context[key]
            return res
        }
        // 或
    }
    
    // result的原型指向调用者的原型
    result.prototype = Object.create(fn.prototype)
    return result
}

原型和原型链

作用: 共享对象上的属性和方法;

原型

原型就是一个对象,这个对象中包含一个constructor指向当前对象的构造函数和一个prototype指向当前原型的原型;

  • 所有对象的_proto_指向它的原型(构造函数的prototype)
  • 所有原型的constructor指向当前对象的构造函数
  • 所有构造函数的prototype就是它的原型
  • 除了Object其他所有对象的原型的顶层的__proto__都指向Object的原型
  • Object原型的__proto__指向null
  • 所有构造函数的__proto__指向Function的原型
  • Function的原型的__proto__指向Object的原型

image.png

console.log(Function.prototype === Function.__proto__); // true
console.log(Object.__proto__ === Function.prototype); // true
console.log(Function.prototype.__proto__ === Object.prototype); // true
console.log(Object.prototype.__proto__ === null); // true

可以通过Object.create()方法创建一个对象,并且指定这个对象的原型;

undefined和null不是对象,因此没有原型的,没有原型所以不能通过它调用任何方法;

原型链

原型对象中的prototype就是当前对象的原型的原型,一层一层的直到指向null为止,这个就是原型链; 作用: 访问当前对象上的属性或方法的时候,首先会从当前对象自身上查找,如果没有找到就从它的原型上查找,也没有找到就从它的原型的原型上查找,直到null为止;

面试题

function Person(){}
Person.prototype.name = 1
const p1 = new Person()
console.log(p1.name)
Person.prototype.name = 2
console.log(p1.name)
Person.prototype = { name: 3}
console.log(p1.name)
const p2 = new Person()
console.log(p2.name)

答案: 1 2 2 3

解析: p1的创建是在Person.prototype = { name: 3}之前,在 const p1 = new Person()的时候内部就创建了this对象,this对象的__proto__指向的是Person.prototype,修改完之后p1的原型还是之前原型的引用,因此输出2,而p2.name输出3

实现一个圣杯模式继承

function innert(fa,ch){
    // 创建一个空函数
    function F(){}
    // 修改此函数的原型为父级的原型
    F.prototype = fa.prototype
    // 子原型指向F的实例
    ch.prototype = new F()
    // 修改子原型的构造函数
    ch.prototype.constructor = ch
    // 保存父原型
    ch.prototype.ubar = fa.prototype
}

原型和作用域的区别:原型是查找对象上的属性,作用域是查找当前上下文中的变量;

instanceof

语法

object instanceof constructor

左侧不是一个对象或右侧不是一个构造函数就会返回false

作用: 检测构造函数(constructor)的prototype是否出现在对象(object)的原型链上;

class F{} 
class C extends F {
  constructor(){
    super()
  }
}

const c = new C()
const f = new F()
console.log(c instanceof F) // true
console.log(f instanceof C) // false
console.log(f instanceof Object) // true
console.log(c instanceof Object) // true

通常用于检查某个对象是否是某个构造函数的实例;

实现instanceof

function minstanceof (obj, constr) {
    const p = obj.__proto__
    if (p) {
        if (p === constr.prototype) {
            return true
        } else {
            return minstanceof(p, constr)
        }
    } else {
        return false
    }
}
class F{} 
class C extends F {
  constructor(){
    super()
  }
}
const c = new C()
const f = new F()
console.log(minstanceof(c,F)) // true
console.log(minstanceof(f,C)) // false
console.log(minstanceof(f,Object)) // true

new关键字

new一个对象,会发生什么?

  • 创建一个this变量和空对象,this指向这个对象
  • this中的__proto__指向构造器的prototype
  • 如果函数返回的不是一个对象或者函数,那么就返回this
function Person(name) {
  this.name = name
  return name;
}
let p = new Person('Tom');

实例化Person过程中,Person返回什么(或者p等于什么)?
答案:{name: 'Tom'}

4. 若将题干改为
function Person(name) {
  this.name = name
  return {}
}
let p = new Person('Tom');

实例化Person过程中,Person返回什么(或者p等于什么)?
答案:{}

解析
构造函数不需要显示的返回值。使用new来创建对象(调用构造函数)时,如果return的是非对象(数
字、字符串、布尔类型等)会忽而略返回值;如果return的是对象,则返回该对象(注:若return 
null也会忽略返回值)。

实现new

function myNew (constr,...args) {
    // 创建空对象,其中的__proto__指向构造器的prototype
    const self = Object.create(constr.prototype)
    // 执行构造函数
    const result = constr.apply(self,[...args])
    const type = typeof result
    return (type === 'object' && type !== null) || typeof result === 'function' ? result : self
}

class类

class用于创建一个类,它是构造函数的语法糖

class AA {}
console.log(typeof AA) // function

class中定义的方法和属性默认都是共有方法和属性,属性放在实例上,方法被放在原型上,可以通过实例访问,可以被继承;

class AA {
    name = 1
    constructor () {
        
    }
    getName () {
        return this.name
    }
}

// 相当于
AA.prototype = {
    constructor(){},
    getName(){ return this.name }
}

class中定义的静态方法或属性都是被放在类上,只能通过类来访问,不能通过实例访问,否则会报错;可以被继承;静态方法中的this是指当前的类;

class AA {
    static name = 1
    constructor () {
        
    }
    static getName () {
        return this.name
    }
}
// 相当于
AA.name = 1
AA.getName = function (){ return this.name }

// 使用
AA.name // 1
const a = new AA()
a.name // undefined
a.getName() // 报错

class中私有属性或方法,在属性或方法名前加上#符号,私有属性或方法只能在类内部使用,不能在外部使用,并且不能delete删除

class AA {
    // 私有属性
    #name = 1
    constructor () {
        console.log(this.#name) // 1
    }
    // 私有静态方法
    static #getName () {
        return this.#name // 访问私有属性
    }
}
const a = new AA()
a.#name // 报错,不能在外部使用

class中属性的简写

class AA {
    constructor () {
        this.name = 1
        this.age = 2
    }
}
// 相当于
class AA {
    name = 1
    age = 2
}
  • class类必须通过new关键字来执行,不能直接执行
  • class类中的方法是放在原型上的,属性放在实例上
  • class类中的静态方法和属性放在构造器上,只能通过类访问
  • class类中的静态方法中的this是当前类,所以只能访问当前类上的属性,不能访问实例上的属性和方法;
  • 可以通过extends实现继承
class C{
  name = 1
  static age = 2
  #sex = '66'
  getName () {
    console.log(this)
    return this.name
  }
  
  static getAge () {
    console.log(this)
    return this.age
  }
}
const c = new C()
console.log(c)

image.png

实现class

// 检测是否是new出来的,否则报错
function checkClass (obj, constr) {
    if (!(obj instanceof constr)) {
        return new Error('必须使用new关键字创建对象')
    }
}

// 给对象上添加属性
function addProps (obj, props) {
    props.forEach(item => {
        Object.defineProperty(obj, item.keys, {
            writable: true,
            configurable: true,
            value: item.val
        })
    })
}

function _class (_constr, _prototype, _static) {
    if (_prototype) { // 添加非静态属性
        addProps(_constr.prototype, _prototype)
    } else { // 添加静态属性
        addProps(_constr, _static)
    }
}
// 定义一个类
const People = (function(){
    function People (name) {
        checkClass(this, People)
        this.name = name
    }
    // 非静态和静态属性
    _class(People,
    [{ keys: 'getName', val: function(){ return this.name }}],
    [{keys: 'getAge', val: function(){ return this.age }}])
    return People
})()
const p = new People(1)
p.name // 1

generator和async、await

迭代器(iterator)

一个具有next方法的对象,并且next返回一个能够指示是否完成迭代的对象,

const iterator = {
    next: function () {
        return {
            value: XXX,
            done: false/true, // true表示迭代完成
        }
    }
}

迭代器创建函数(iterator creator)

返回一个迭代器的函数

function iteratorCreator(arr){
    let index = 0
    return {
        next: function () {
            return {
                value: arr[index],
                done: arr.length < ++index
            }
        }
    }
}

可迭代协议

es6规定,如果一个对象具有知名符号属性Symbol.iterator,并且该属性的值为一个迭代器创建函数;

可迭代对象:Array、Map、Set、arguments和String;

可迭代对象可以通过扩展运算符展开,可以通过for of进行遍历;

面试题

实现一个无限的锲波拉奇数列
function fbl () {
    let value = ''
    let a = 0
    let b = 1
    return {
        next: function () {
            value = a + b
            a = b
            b = value
            return {
                value: value,
                done: false
            }
        }
    }
}
const f = fbl()
f.next() // 1
f.next() // 2
f.next() // 3
f.next() // 5
...
使一个对象可迭代
const obj = {
  a: 1,
  b: 2,
}

obj[Symbol.iterator] = function () {
  const keys = Object.keys(obj)
  let index = 0
  return {
    next: function () {
      return {
        value: obj[keys[index++]],
        done: index > keys.length,
      }
    }
  }
}
const arr = [...obj] // [1,2]
怎么知道一个对象是否是可迭代对象?
  • 这个对象上具有Symbol.iterator属性
  • 这个属性的值为一个迭代器创建函数
怎么遍历一个可迭代对象?
function forOf (obj) {
    const iterator = obj[Symbol.iterator]()
    let result = iterator.next()
    while (!result.done) {
        console.log(result.value)
        result = iterator.next()
    }
}

generator(生成器)

什么是生成器?

生成器是通过一个构造函数generator创建的对象,生成器既是一个迭代器也是一个迭代对象

通过生成器函数可以得到一个生成器,生成器函数就是function和函数名之间加一个*符号

// 生成器函数
function* gen(){}
// 执行生成器函数返回一个生成器
const generator = gen()
console.log(generator)

image.png

生成器的目的:解决迭代器繁琐的写法

yield关键字和next方法

yield: 指定生成器每次执行的截止位置,yield后面的值作为迭代器的value返回;

next方法:next是生成器的方法,用来执行生成器函数内部代码,每执行一次next方法,生成器函数内部将执行到yield的位置;next方法返回一个迭代值;

// 生成器函数
function *a(){
    console.log('第1次执行')
    yield 1
    console.log('第2次执行')
    yield 2
    console.log('第3次执行')
    yield 3
}
// 生成器
const gen = a()
gen.next() // { value: 1, done: false }
gen.next() // { value: 2, done: false }
gen.next() // { value: 3, done: false }
gen.next() // { value: undefined, done: true }
gen.next() // { value: undefined, done: true }

next方法可以传递参数,传递的参数将作为yield执行之后返回的值,第一次调用next方法传递参数没有意义;因为第一次执行next方法是从函数内部的起始位置到第一个yield的后面的代码,还未执行yield;

function *a(){
    console.log('第一次执行') // '第一次执行'
    const val = yield 1
    console.log('第二次执行'+val) // 第二次执行2
    const val2 = yield 2
    console.log('第三次执行'+val2) // 第三次执行3
}
const gen = a()
gen.next(1) // { value:1, done: false } 第一次传递参数没有意义
gen.next(2) // { value:2, done: false }
gen.next(3) // { value:undefined, done: true }

生成器函数可以有返回值,返回值作为第一个done为true的value的值;

function* a(){
    yield 1
    return 2
}
const gen = a()
gen.next() // { value:1, done: false }
gen.next() // { value:2, done: true }
gen.next() // { value:undefined, done: true }
return方法

生成器的return方法,可以提前结束生成器函数的执行,可以传递一个参数,此参数会作为迭代值的value的值,并且done为true,表示结束了迭代,后面再次执行next方法都是返回done为true;

function* a(){
    console.log('第一次执行') // 第一次执行
    yield 1
    console.log('第2次执行')
    yield 2
    console.log('第3次执行')
    const val = yield 3
    console.log('第4次执行'+val)
}
const gen = a()
gen.next() // { value:1, done: false }
gen.return(5) // { value:5, done: true } 1后面的yield不再执行,所以也不输出第2次执行
gen.next() // { value:undefined, done: true }
gen.next() // { value:undefined, done: true }
gen.next() // { value:undefined, done: true }
throw方法

生成器的throw方法,可以在生成器函数中产生一个错误,再次调用next方法的时候得到的迭代值的done都是true

function *a(){
  yield 1
  yield 2
  yield 3
}
const generator = a()
generator.next() // { value: 1, done: false }
generator.throw(2) // 报错 Uncaught 2
generator.next() // { value: undefined, done: true }
生成器函数内部调用其他生成器函数
function *a(){
    yield 1
    yield 2
}
function *b(){
    yield* a() // 必须要有*符号,否则得到一个生成器
    yield 3
    yield 4
}
const gen = b()
gen.next() // { value:1, done: false }
gen.next() // { value:2, done: false }
gen.next() // { value:3, done: false }
gen.next() // { value:4, done: false }
gen.next() // { value:undefined, done: true }
面试题
  1. 使用生成器实现以下功能
function a (arr) {
    let index = 0
    return {
        next: function () {
            return {
                value: arr[index],
                done: ++index > arr.length
            }
        }
    }
}

// 生成器实现上面遍历数组
function *a(arr){
    for (let val of arr) {
        yield val
    }
}
const gen = a([1,2,3])
gen.next() // { value:1,done: false }
gen.next() // { value:2,done: false }
gen.next() // { value:3,done: false }
  1. 通过生成器创建无限斐波拉契数列
function *fbl(){
    let a = 0
    let b = 1
    while(true){
        const val = a + b
        yield val
        a = b
        b = val
    }
}
const gen = fbl()
gen.next() // { value: 1, done: false }
gen.next() // { value: 2, done: false }
gen.next() // { value: 3, done: false }
  1. 实现一个执行以下生成器的函数
function *a(){
    const params = yield {id:1}
    const res = yield getList('http:XXX', params)
    const result = yield dealRes(res)
    return result
}

function run (task) {
    const gen = task()
    let result = gen.next()
    handler()
    function handler () {
        if (result.done) {
            return
        }
        // 如果是promise
        if (typeof result.value.then === 'function') {
            // 执行promise
            result.value.then(res => {
                result = gen.next(res)
                handler()
            }, err => {
                // 抛出错误
                result = gen.throw(err)
                handler()
            })
        } else { // 普通值
            result = gen.next(result.value)
            handler()
        }
    }
}
run (a)

async和await

  • async函数是generator生成器的语法糖
  • async函数返回一个promise对象
  • await只能放在async函数内部使用,await会等待后面的promise执行完毕之后才会继续往下执行
  • await要结合try catch使用,否则后面的promise返回一个错误就会导致阻塞,不会继续往下执行了
async function a () {}
console.log(a()) // promise

image.png

await后面如果不是promise那么会通过promise进行包裹

async function a(){
    await 1
}
相当于
async function a(){
    await Promise.resolve(1)
}

await需要使用try catch包裹,否则报错之后不会继续往下执行await下面的代码

async function a(){
   const res = await Promise.reject(1) // 直接抛出reject错误,不会在执行下面的打印
   console.log(res)
}
// 通过try catch包裹
async function a(){
  try{
     const res = await Promise.reject(1)
      console.log(res)
  } catch(err) {
      console.log(err) // 1
  }
}

// 通过catch返回
async function a(){
     const res = await Promise.reject(1).catch(err => { return err })
     console.log(res)
}
实现async、await
function generatorAsync(generator){
    // 得到一个生成器
    const gen = generator()
    // 返回一个promise对象
    return new Promise((resolve,reject) => {
        function next(args){
            let result = gen.next(args)
            // 如果生成器执行完毕,直接返回结果
            if (result.done) {
                return resolve(result.value)
            } else { // 没有完毕递归执行
                return Promise.resolve(result.value).then(res=>{
                    return next(res)
                },reject)
            }
        }
        next()
    })
}

// 测试
const getData = () => {
  return new Promise((resolve) => setTimeout(() => resolve("data"), 1000));
};
const getData2 = () => {
  return new Promise((resolve,reject) => setTimeout(() => reject("出错"), 1000));
};
function* testG() {
  // await被编译成了yield
  const data = yield getData();
  console.log("data: ", data); // data
  const data2 = yield getData2(); // 报错
  console.log("data2: ", data2); 
  return "success";
}
generatorAsync(testG).then(res=>{
    console.log(res)
})

image.png

/*

          for循环中使用await会阻止for循环的遍历,使得每次遍历都是等到await异步执行之后才遍历下一次

        */
       async function a(){ 
           for(let i = 0;i < 5; i++){
              console.log('前')
              await p(i).then((n)=>{
                  console.log(n)
              })
              console.log('后')
           }
       }   

       function p(n){
           return new Promise((res,rej)=>{
                setTimeout(()=>{
                    res(n)
                },3000)
           })
       }

       a() //前 0 后 前 1 后 前 3 后 前 3 后 前 4 后
function a1 () { // 默认返回undefined
  setTimeout(() => {
    console.log('a1')
  },4000)
}

function a2 () { // 默认返回undefined
  function c() {
    return new Promise(res => {
      setTimeout(() => {
        res()
        console.log('a2')
      },2000)
    })
  }
  c()
}

async function fn () {
  await a1()
  await a2()
  console.log('完毕')
}
fn() // 完毕 a2 a1

toString和valueOf

  • Object的toString: 返回一个表示该对象的字符串;很多原始对象都自己实现了自己的toString方法;
  • Object的valueOf: 将调用它的this转为一个对象返回;很多原始的对象都自己显示了自己的valueOf方法;
  • 类型转换规则
  1. 只有做类型转换的时候会隐式的调用valueOf和toString方法,先调用valueOf方法转换为原始类型,如果无法转换为原始类型就调用toString方法返回它的字符串形式;
  2. 如果重写了这两个方法,除了手动调用toString之外,其他都优先调用valueOf;
  3. 如果重写了其中的一个方法,除了手动调用toString和valueOf之外,其他都优先调用重写的这个方法;
/*
验证: 如果重写了其中的一个方法,除了手动调用toString和valueOf之外,
其他都优先调用重写的这个方法;
*/
const obj = {
  toString () {
    return 888
  }
}
console.log(obj.valueOf()) // obj对象本身
/*
验证规则2
*/
const obj = {
  toString () {
    return 888
  },
  valueOf () {
    return 999
  }
}
console.log(+obj) // 999
// 验证规则1
const cc = Number(22)
cc.valueOf() // 22
cc.toString() //'22'
cc + 2 // 24

const obj = {
  val: [1,2,3],
  toString () {
    console.log('toString')
    return this.val + ''
  },
  valueOf () {
    console.log('valueOf')
    return this.val
  }
}
console.log(obj + 1) // valueOf toString '1,2,31' 分析:先调用toValue,得到值[1,2,3],发现它不是原始类型,再调用toString,得到'1,2,3',再加1就是'1,2,31'
console.log(obj - 1) // NaN

let a = [1,2,3] + 4 
console.log(a)  //'1,2,34'    如果一个为对象。另一个为数字或者字符串,那么先把对象转成原始值 调用toString 也就是'1,2,3'

let b = [1,2,3] - 4
console.log(b)  //NaN  [1,2,3]通过toString转成 '1,2,3' 再去-4就是NaN

let arr = [1]
console.log(arr+1) // '11' 首先调用toString() '1' + 1
console.log(arr-1) //0 

// 大括号写在最前面会被当作括号使用
let a = 1 + {}
console.log(a) // "1[object Object]"
let b = {} + 1
console.log(b) // 1  

toString方法由字符串转换优先调用,但是数字的强制转换和原始值的强制转换会优先调用 valueOf();

== 运算符的判断

  1. null除了等于自身和undefined之外,不等于其他值;
  2. undefined除了等于自身和null之外,不等于其他值;
  3. 如果一侧为字符串,另一侧为数字,那么先把字符串转成数字再比较;
  4. 如果有一侧为布尔值,那么两侧都先转为数字,再进行比较;
  5. 如果一侧为对象,另一侧为数字或字符串,那么先把对象转成基本类型再进行比较;
  6. 引用类型转为布尔值都是true
  7. Number({})为NaN,Number([])为0
Number({}) // NaN
Number([]) // 0
Number([1]) // 1
Number([1,2]) // NaN
Number(false) // 0
Number('') // 0
Number(' ') // 0
Number(null) // 0
Number(undefined) // NaN

![] == true // false 
/* 一侧为布尔,两侧都要转成数字,![]为false,
 Number(false) == Number(true),0 == 1,则为false */

[] == true // false
/*
一侧为布尔,两侧都要转成数字,[]转为数字会调用它的toString()得到'',
Number('')为0,Number(true)为1, 0 == 1则为false
*/
[1] == true // true
[2] == true // false [2]转成数字为2,true为1, 2 == 1
[2] == false // false 2 == 0
[] == ![] // true ![]为false, false转为数字为0,[]转为数字为0 0 == 0

for in、in、hasOwnPrototype和for of

  • for in:遍历对象上可枚举的属性包括它的原型;
  • in: 查看对象上是否有这个属性,不管是否可枚举,包括原型上;
  • hasOwnPrototype: 查看对象上是否有这个属性,不包括原型上,不管是否可枚举;
  • for of: 遍历可迭代的对象,并且不会遍历它原型上的属性,不管是否可枚举;如数组,对象不可迭代不能进行遍历,并且直接获取到了迭代对象的值;
let a = {
  name: 'aa',
}
// 原型上添加属性
Object.prototype.age = '原型上的age'

// 添加不可枚举的属性
Object.defineProperty(a, 'sex', {
  enumerable: false, // 不可枚举
  value: '不可枚举的sex'
})

// for in
for (let key in a) {
  console.log(key) // name age
}

// in
console.log('sex' in a) // true
console.log('age' in a) // true

// hasOwnPrototype
console.log(a.hasOwnPrototype('sex')) // true
console.log(a.hasOwnPrototype('age')) // false

const arr = [1,2]
Array.prototype.age = '原型age'
Object.defineProperty(arr, 2, {
  enumerable: false, // 不可枚举
  value: '不可枚举的sex'
})
console.log(arr) //  [ 1, 2, "不可枚举的sex" ]
for (const key of arr) {
  console.log(key) // 1, 2, "不可枚举的sex"
}

set和map

set

存储任何类型的唯一值,可以是基本类型可以是对象引用,Set是可迭代对象,因此具有迭代对象的特性;

基本使用
// 创建set
const s = new Set()
// 可以传递一个初始值,初始值必须是可迭代对象,否则报错
const s = new Set([1,2,3])

// 方法
add(value) // 添加数据,相同的数据不做任何操作
has(value) // 判断集合中是否有这个数据,有就返回true
delete(value) // 删除集合中指定的数据,返回是否删除成功
clear() // 清空集合
forEach(fn) // 遍历集合
// 属性
size // 只读,获取集合的长度

// 遍历集合的方法
for of // 因为集合是迭代对象
forEach()

注意: 集合中不存在下标

// 和数组的相互转换
const arr = [...new Set('123')]
arr // [1,2,3]
const s = new Set(arr)
s // {1, 2, 3}

// 应用:
// 数组去重
let arr = [1,2,3,3,2]
arr = [...new Set(arr)]
arr / [1,2,3]
// 字符串去重
let str = '123321'
str = [...new Set(str)].join()
str // 123

// 获取两个数组的并集
const arr1 = [1,2,3]
const arr2 = [2,3,4]
const arr = [...new Set([...arr1,...arr2])]
arr // [1,2,3,4]

// 获取两个数组的交集
const arr1 = [1,2,3,2,3]
const arr2 = [2,3,4]
const arr = [...new Set(arr1)].filter(item => {
    return arr2.includes(item) 
})
arr // [2,3]

// 获取两个数组之间的差集
const arr1 = [1,2,3,2,3]
const arr2 = [2,3,4,3,4,5,6]
const arr = [...new Set(arr1),...new Set(arr2)].filter(item => {
    return (!arr1.includes(item) && arr2.includes(item)) || (arr1.includes(item) && !arr2.includes(item))
})
arr // [1,4,5,6]
实现Set
/**
 * 只能接收可迭代对象,并且有add,has,delete,clear,forEach, size,
 * 并且它本身也是可迭代对象
 */
class MySet {
  list
  constructor (value) {
    this.list = []
    if (typeof value[Symbol.iterator] !== 'function') {
      throw Error('参数必须是可迭代对象')
    }
    for (const item of value) {
      this.add(item)
    }
  }
  *[Symbol.iterator] () {
    for (let key in this.list) {
      yield this.list[key]
    }
  }
  add (val) {
    // 如果已经存在就直接返回
    if (!this.has(val)) {
      this.list.push(val)
    }
    return this.list
  }
  has (val) {
    for (let key in this.list) {
      if (this.equals(this.list[key], val)) {
        return true
      }
    }
    return false
  }
  equals (val1, val2) {
    if (val1 === 0  && val2 === 0) {
      return true
    }
    return Object.is(val1, val2)
  }
  delete (val) {
    if (this.has(val)) {
      this.list = this.list.filter(item => !this.equals(item,val))
      return true
    }
    return false
  }
  clear () {
    this.list.length = 0
  }
  get size () {
    return this.list.length
  }
  forEach (callBack) {
    for (const item of this.list) {
      callBack(item, item, this.list)
    }
  }
}

const ss = new MySet([1,2,3,3,2])

map

用来存储键值对数据,键是唯一的,但可以是任意类型;map是可迭代对象;

解决了对象存储键值对的问题;

基本使用
// 创建Map,可以指定参数,参数必须是一个可迭代对象,可迭代对象中每一项必须是长度为2的数组,第一项表示键,第二项表示值;
const m = new Map([[{},1],[2,2]])
m // {{} => 1, 2 => 2}
// 方法
// 添加数据,键如果存在就会覆盖之间的值
set(键,值)
// 获取数据
get(键)
// 删除数据
delete(键)
// 判断是否存在
has(键)
// 清空
clear()
// 属性
size // 获取长度,只读

// 应用:
// 和数组之间的转换
// 数组转map
let arr = [[1,1],[2,3]]
const m = new Map(arr)
m // {{1=>1},{2=>3}}
// map转数组
const arr = [... new Map([[1,1],[2,3]])]
arr // [[1,1],[2,3]]

// 遍历Map
for of 和 forEach
实现map
/* 实现map
      map是一个可迭代对象,初始化传递的参数必须是可迭代对象,迭代对象中的每一项必须是长度为2的数组
      有has get set delete clear forEach方法 和 size属性

    */
    class MyMap {
      constructor (value) {
        // [{key:,value:},{key:,value:},{key:,value:}]
        this.list = []
        // 不是可迭代对象直接返回
        if (typeof value[Symbol.iterator] !== 'function') {
          throw Error('参数必须是一个可迭代对象')
        }
        // 遍历初始值进行初始化
        for (let key of value) {
          if (typeof key[Symbol.iterator] !== 'function') {
            throw Error('参数必须是一个可迭代对象')
          }
          this.set(key[0],key[1])
        }
      }
      *[Symbol.iterator] () {
        for (let key of this.list) {
          yield key
        }
      }
      set (key, value) {
        // 如果存在就替换否则就添加
        const index = this.getIndex(key)
        if (index !== -1) {
          this.list[index].value = value
        } else {
          this.list.push({key, value})
        }
        return this.list
      }
      get (key) {
        const index = this.getIndex(key)
        if (index !== -1) {
          return this.list[index].value
        }
        return undefined
      }
      // 获取已经存在的key的索引
      getIndex (key) {
        for (let index in this.list) {
          if (key === this.list[index].key) {
            return index
          }
        }
        return -1
      }
      has (key) {
        return this.getIndex(key) !== -1
      }
      delete (key) {
        const index = this.getIndex(key)
        if (index !== -1) {
          this.list.splice(index,1)
          return true
        }
        return false
      }
      clear(){
        this.list.length = 0
        return true
      }
      forEach (fn) {
        for (let index in this.list) {
          const item = this.list[index]
          fn(item.key,item.value,this.list)
        }
      }
      get size () {
        return this.list.length
      }
    }

    const m = new MyMap([[1,1],[2,2]])
    const obj = {}
    m.set(obj,3)
    m.set(obj,4)
    m.set(obj,5)
    m.set(obj,6)
    console.log(m) // [{key: 1, value: 1},{key: 2, value: 2},{key: {}, value: 3}]
    console.log(m.has(obj)) //true
    console.log(m.has(3)) // false
    console.log(m.get(obj)) // 6
    m.delete(2)
    console.log(m) // [{key: 1, value: 1},{key: {}, value: 3}]

WeakSet和WeakMap

WeakSet

和Set一样,但是WeakSet只能存储引用类型的值,并且是唯一的,WeakSet集合中的值都是弱引用类型,在没有其他对这些值有引用的时候,就会被作为垃圾回收掉;WeakSet不是可迭代对象,因此不能被遍历,没有size

// 创建
const s = new WeakSet()

// 方法
add(obj) // 添加
delete(obj) // 删除
has(obj) // 判断

let obj = {
    name: {}
}
const s = new WeakSet()
s.add(obj.name)
s.has(obj.name) // true
delete obj.name
s.has(obj.name) // false

应用:可以调试一个对象是否被完全释放

WeakMap

和Map一样,但是只能存储键为引用类型的键,值可以是任意类型,不是迭代对象,不可枚举不可遍历;键没有被其他地方引用的时候就会被回收掉;

// 创建
const wm = new WeakMap()
wm.set(key,value) // 添加
wm.get(key) // 获取
wm.has(key) // 判断
wm.delete(key) // 删除

WeakMap出现的原因:因为Map中存储的键值对都是使用一个长度为2的数组进行存储,数组是引用类型,那么这些数组一直无法被释放,因为出现了WeakMap;

Reflect

Reflect是一个js内置对象,反射js底层的一些api给开发者使用;

常用API:
Reflect.get(对象,属性) // 获取对象的属性值
Reflect.set(对象,属性,值) // 设置对象的属性值
Reflect.apply(函数,调用对象,参数) // 和apply一样
Reflect.has(对象,属性) // 判断对象上是否有这个属性,和in一样
Reflect.ownKeys(对象) // 获取对象自身属性

Object.create()

创建一个新对象,可以指定它的原型

 const objs = {
    name: 1
}
const oq = Object.create(objs,{age:{ value:22,enumerable:true}})
console.log(oq) // {age:22}

实现Object.create

function create (obj,props) {
  function F(){}
  Object.setPrototypeOf(F.prototype, obj)
  const self = new F()
  for (let key in props) {
    Object.defineProperty(self,key,props[key])
  }
  return self
}
const objs = {
    name: 1
}
const oqq = create(objs, {age:{ value:22,enumerable:true}})
console.log(oqq) // {age:22}

拷贝

浅拷贝

只拷贝对象的最外层,至于更深层次的只会拷贝其引用;

  1. Object.assign()
  2. ...扩展运算符

深度拷贝

对于对象深层次的引用也能进行拷贝

  1. JSON.parse(JSON.stringify())
    无法拷贝函数,时间,正则,原型上的属性和方法,对于循环引用也无法进行拷贝
  2. 手写实现一个深度拷贝的方法,可以解决循环引用,多个属性引用同一个对象的问题
function deepCopy(target,hash = new WeakMap()){
    // 如果不是对象直接返回
    if (!isObject(target)) {
      return target
    }
    // 如果此对象已经存在就直接返回
    if (hash.get(target)) {
      return hash.get(target)
    }
    const newObj = Array.isArray(target) ? [] : {}
    // 日期
    if (isType(target) === "Data") {
      return new Date(target.getTime())
    }
    // 进行存储
    hash.set(target,newObj)
    // 进行遍历
    for (const key in target) {
      if (Object.hasOwnProperty.call(target, key)) {
        const element = target[key];
        // 对象或数组
        if (isObject(element)) {
          newObj[key] = deepCopy(element, hash)
        } else {
          newObj[key] = element
        }
      }
    }
    return newObj
  }

  function isObject (val) {
    if (typeof val === 'object' && val !== null) {
      return true
    }
    return false
  }
  function isType (val) {
    const type = Object.prototype.toString.call(val)
    // 日期
    if (type === '[object Date]') {
      return 'Data'
    } else if (type === '[object Array]'){
      return 'Array'
    } else if (type === '[object Object]'){
      return 'Object'
    }
    return false
  }
  const obj = {}
  const target = {
    t: new Date(),
    a: obj,
    b: obj,
    fn: () => {}
  }
  target.target = target
  deepCopy(target)

image.png

事件循环

作用:js是单线程的,同一时间只能执行一个任务,如果这个任务很消耗时间,那么后续的任务都需要排队等待,此时为阻塞状态,为了解决这个问题出现了异步任务和事件循环;

先理解几个概念:

  • 执行栈:一个栈结构,用于存放各种函数的执行环境,函数在调用前会创建执行环境,把执行环境推入到执行栈中,函数执行完毕,就会从执行栈中移除,并且销毁它的执行环境;js引擎永远都是执行执行栈顶部函数;

  • 异步函数:某些函数不会被立即执行,而是等到条件满足的时候才会执行,比如定时器和ajax请求;异步函数的执行条件是被宿主环境控制的;

  • 浏览器宿主环境的5个线程

    1. js引擎:负责执行执行栈顶层的代码
    2. GUI渲染线程:负责渲染页面
    3. 事件监听线程:负责监听各种事件的执行
    4. 计时器线程:负责计时
    5. 网络线程:负责网络通信
  • 任务队列

    1. 宏任务:setTimeout,setTimeinterval,ajax
    2. 微任务:promise,MutationObserver;

事件循环机制:
当执行栈执行同步代码的时候,遇到异步函数,会把异步函数的回调执行条件添加到宿主环境相应的线程中,当条件满足之后,线程会把回调函数添加到相应的任务队列中,微任务添加到微任务队列,宏任务添加到宏任务队列;当执行栈为空的时候,微任务队列会把回调挨个推入到执行栈中进行执行,当微任务队列执行完毕之后,宏任务队列会把它的下一个宏任务推到执行栈中执行,当一个宏任务执行完毕之后就会清空此时的微任务队列;依次循环下去;执行栈、宿主环境和任务队列相互配合就是事件循环;

注意:每执行一个宏任务的回调之后都会清空微任务队列(一个宏任务对应多个微任务)

面试题

  1. 定时器一定是在规定的时间达到之后就执行回调吗?
    不一定,如果达到了规定的时间,但是执行栈还未空闲,一直处于执行中,那么就不会准时执行回调,需要等到执行栈空闲之后才会执行回调;

  2. 以下输出什么?

console.log("script start");
async function async1() {
  await async2(); // await 隐式返回promise
  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 async1 end promise1 promise2 setTimeout

事件循环和浏览器更新渲染

  • 浏览器更新渲染会在一轮事件循环的宏任务和微任务执行完成之后进行更新渲染,但并不是每轮循环都是更新渲染,在一帧时间内多次修改dom,并不会多次绘制,而是积攒起来在一帧时间的时候进行绘制一次;
  • 如果希望在每轮event loop都即时呈现变动,可以使用requestAnimationFrame。
  • 如果有大量的任务需要等待执行,可以将dom的变动放在微任务中,那么就可以在本次事件循环中进行更新渲染;

为什么vue.nextTick要优先使用微任务?
因为浏览器的更新渲染是在宏任务和微任务执行完毕之后才会进行更新然后,如果使用微任务在本次事件循环中就可以更新dom,更快的渲染出来,如果使用宏任务会在下一次事件循环中更新;

浏览器从输入url到页面展示都经历了什么?

整体流程:

  1. 取缓存,浏览器首先会从缓存中获取,如果存在就返回(强缓存),如果没有就进行下一步;
  2. 取ip, 从DNS缓存中根据域名查找对应的ip,如果没有就从本地的hosts中查找,也没有的话,DNS解析器会从DNS服务器中进行获取;
  3. 如果是https协议,在会话层会进行SSL协商;
  4. 创建TCP连接(三次握手);
  5. TCP连接成功之后进行数据请求(请求内容:请求头,请求行和请求体);
  6. 如果请求协议是http1.1或2.0,默认会进行长连接(connect:keep-alive);
  7. 服务端返回数据(响应行,响应头,响应体);
  8. 如果返回状态是301或302就进行重定向(比如http重定向到https)
  9. 如果返回状态为304,那么就从浏览器缓冲中进行获取(协商缓存)
  10. 解析html文件(进入浏览器渲染阶段)

浏览器从输入url到页面展示都经历了什么

浏览器渲染流程

html渲染流程

image.png

大概步骤:
1.创建dom树;
2.创建样式表;
3.dom树和样式表结合生成render树;
4.布局;
5.对布局树进行分层;生成图层树;
6.绘制每个图层;
7.合并图层;

具体分析:

  1. 创建dom树,html解析器解析html元素生成dom树;
  2. 创建样式规则,css解析器解析css和内联样式生成样式表;
  3. 生成render树,dom树和样式表进行关联起来,每个元素节点都有一个attr方法,此方法用于接收对应的css样式信息,最终返回一个render对象,这些render对象最终结合起来生成一个render树;
  4. 布局,确定render树上每个节点的位置;
  5. 对布局树进行分层,生成图层树;
  6. 单独绘制每个图层;
  7. 把绘制的每个图层进行合并;

注意:

  • dom树的构建是在页面加载完毕之后吗?
    dom树的构建是循序渐进的,浏览器为了增加用户体验能够尽快显示页面,所以不会等到页面加载完成之后才去渲染;

  • render树的构建是在dom树和样式表构建之后吗?
    它们三个是交叉进行的一边加载一边解析一边渲染

  • css解析需要注意什么?
    css解析是从右往左的,因此嵌套的越深解析的越慢

  • 操作dom的代价?
    每操作一次dom,就会进行一次上面的整个流程的执行,频繁的操作会导致页面卡顿;

重绘和回流

回流:元素的大小,位置改变或添加删除一个元素等元素的几何信息,都会导致render树进行部分或全部的重新构建;

重绘: 元素的外表样式改变,如背景色,颜色,透明度等会引起重新绘制,重新绘制无需进行布局和分层阶段,直接根据样式表进行绘制;

回流一定会重绘,重绘不一定会回流;回流的代价远大于重绘;

如何减少重绘和回流?

  • css方面:
  1. 使用绝对定位或固定定位使得当前元素脱离文档流;
  2. 避免使用Table布局,否则会引起回流
  3. 使用visibility替换display:none(前者重绘,后者回流)
  • js方面
  1. 使用文档碎片批量处理dom元素
  2. 使用类来进行集中修改样式

页面解析加载流程

  1. 浏览器获取到html文件之后,开始解析html,document.readyState = 'loading'状态,创建document对象,解析web;
  2. 遇到link标签,开启一个线程异步加载css,继续解析web;
  3. 遇到script标签,如果没有设置async和defer属性,阻塞web解析;加载并且执行js;
  4. 如果设置了async和defer属性,那么开启一个线程异步加载js,设置async属性的会等到js加载完成之后就执行,此时会阻塞web解析;
  5. 遇到img元素,解析img,开启一个线程异步加载img的src链接,继续解析web;
  6. dom解析完成,设置document.readyState为interactive;设置defer属性的脚本开始按照顺序执行;
  7. 并且document触发DOMContentLoaded事件,从脚本同步阶段进入到事件驱动阶段;
  8. 异步脚本执行完毕并且图片引入完成之后,document.readyState为complete,触发window的onload事件;

css会阻塞浏览器的渲染和后面js的执行,不会阻塞解析; js会阻塞浏览器解析;图片不会造成阻塞;

为什么css要放在头部?

css不会影响html的解析,只会影响渲染;如果放在底部,html会解析渲染完成,在解析css的时候就会引起回流和重绘;如果放在头部,css在加载的时候和html解析并行进行,css加载完成后就可以绘制;

defer和async的区别

  • defer和async都是异步加载文件
  • defer:异步加载完成之后,需要等到文档解析完成之后才会执行,document.readyState为interactive;设置defer属性的Script内部还能在代码;如果有多个defer文件,下载完成之后按顺序执行;
  • async:异步加载完成之后立即执行,会阻塞当前的文档解析和渲染;如果有多个async文件,哪个先下载完成就执行哪个;
  • 同时设置了两个属性,会采用async;
  • DOMContentLoaded事件会等到defer脚本加载执行完毕之后触发;

load和DOMContentLoaded的区别?

  • load会等待所有的资源都加载执行完毕之后触发;
  • DOMContentLoaded在html解析完成之后触发,不会等待图片加载完成也不会等待async的脚本执行完成;

说说http和https

http是超文本传输协议,是应用在应用层,基于TCP进行链接的,http协议是无状态的;主要包括请求行+请求头+空格+请求体,响应行+响应头+空格+响应体;

https:它是基于http进行了SSL或TSL认证;等于http+证书认证+加密传输的数据,默认端口是443;它比http安全;

SSL或TSL认证流程?

  1. 客户端向服务端发起请求,客户端会携带TSL版本,加密套件和一个随机数,这里用1表示;
  2. 服务端收到请求之后会携带TSL版本,加密套件,随机数2,证书和公钥进行响应;
  3. 客户端接收到响应之后,通过公钥对预主密钥进行加密,把加密之后的密钥和随机数3发给服务端;
  4. 服务端接收到请求之后,通过私钥进行解密获取到预主密钥;
  5. 服务端和客户端就可以通过预主密钥和三个随机数进行加密生成会话密钥,通过会话密钥就可以对传输的数据进行加解密;

注意: 整个流程采用了混合加密,非对称加密对预主密钥进行加解密,对称加密对传输的数据进行加解密;传输随机数的目的就是防止数据被篡改过;

怎么解决http的无状态?

  1. cookie:客户端第一次访问服务端的时候由服务端创建并返回,保存于客户端,以后每次请求客户端都会在请求头中携带上cookie;如果没有设置过期时间,那么它会在浏览器关闭的时候被销毁;设置了过期时间就会保存在客户端的磁盘上;可以通过document.cookie操作cookie;
  2. session: 由服务端创建并且保存在服务端,会把sessionId通过cookie返回给客户端,以后每次请求都会通过cookie携带sessionId,浏览器关闭之后会被销毁;
  3. token: 由服务端创建一个加密的token并返回给客户端,客户端每次请求都会在请求头中携带这个token;

cookie:存储在客户端不安全,并且携带的数据大小为4kb左右; session:存储在服务端相比cookie安全,但是会给服务端造成负担,依赖于cookie;

说下http0.9、http1.0、http1.1、http2.0和http3.0的区别?

  • http0.9: 早期用于网页传输,不能设置请求和响应头;
  • http1.0: 可以设置请求和响应头,根据请求头的不同进行不同的响应,每次请求都需要重新建立TCP连接;不支持断点续传,每次都是完整的数据;header中可以设置Expires缓存字段;
  • http1.1: 可以设置长连接,可以复用创建的链接,使用了摘要算法进行身份验证,支持断点续传;新增了控制缓存的标识;header中可以设置cache-control字段;
  • http2.0: 头部压缩,使用了二进制分帧,多路复用,主动推送;
  • http3.0: 采用了UDP传输协议;

为什么http1.1没有多路复用而http2.0有?

因为1.1中数据是通过文本进行传输,文本传输无法知道此次的响应应该属于哪个请求,而2.0中采用了二进制分帧,每帧中有一个标识,表示当前帧属于哪个流,从而就执行属于哪个请求;

简述多路复用?

http2.0中采用了二进制分帧,帧是数据的最小单位,每一帧中有标识当前帧属于哪个数据流,一条数据流由多个帧组成,每条数据流表示一个请求和响应,通过帧中的标识就知道当前属于哪个请求,从而可以实现多路复用;

说下TCP

TCP是传输控制协议,它应用在传输层,负责端到端的连接,并且是面向字节流,它的传输是可靠的,流量和拥塞是可控的;并且是全双工;

说下TCP的三次握手和四次挥手

  • TCP三次握手
    1. 客户端向服务端发起连接,发送SYN包;此时不包含应用层的数据,客户端处于SYN-SENT状态;
    2. 服务端接收到请求之后响应SYN和ACK包;此时不包含应用层的数据,服务端处于SYN-RCVD状态;
    3. 客户端接收到响应之后发送ACK包;可以携带客户端的数据;
  • TCP四次挥手
    1. 客户端向服务端发起请求,报文中携带FIN包表示要断开连接,此时进入FIN_WAIT_1状态,等待服务端的确认;
    2. 服务端接收到请求之后发送ACK包表示确认;此时服务端还有正在响应的数据,等待一会数据全部响应完毕之后进行下一步;客户端接收到ACK进入FIN_WAIT_2状态,等待服务器的FIN,服务端进入close-wait状态;
    3. 服务端再次发送FIN包表示确认断开,服务端进入last-ack状态;
    4. 客户端接收到响应之后发送ACK包表示确认断开,客户端处于TIME_WAIT状态,服务端接收到之后处于close状态;

序列号:计算机生成的随机数,每次发送数据,此随机数就会加1,用来解决数据包不重复,不丢弃,按需传输;

确认号:为了确保是同一个连接和不丢包的问题;

TCP报文中: SYN(Synchronization同步):1表示希望建立连接;ACK(acknowledgment确认):1确认应答;
FIN(finish结束):1表示断开;

TCP为什么是三次握手和四次挥手不是两次和三次?

防止造成历史连接的错乱,如果第一次客户端发送SYN包,由于网络问题服务端没有接收到,超时之后客户端再次发送一个SYN,此时服务端接收到之后回复syn和ack进行连接,如果第一次的syn此时也发送出去,服务端接收到之后也会回复syn+ack进行链接,这样就造成一个请求多次连接的问题;

TCP怎么解决丢包和乱序问题?

TCP会有一个数据缓存区,会把数据分割成多份,可以一次或多次发送,每次数据包的报文由序列号,数据长度和数据组成,接收端在接收到数据之后,会回复ACK进行确认,ACK就是接收到的序列号加长度,也就是下一条数据的起始序列号,这样就可以防止乱序;接收完数据之后,会根据序列号进行重组,如果有丢包,数据的长度就会有变化,根据缺失包的前后序列号可以确认丢包的序列号,客户端再次发送ACK进行获取,这样就解决了丢包问题;

为什么TCP四次挥手之后处于time-wait状态?

如果客户端最后一次发送ACK包就断开连接,由于网络问题服务端没有接收到,那么服务端就会再次发送Fin包并且一直处于last-ack状态;

TCP和UDP的区别?

TCP和UDP都是应用在传输层的,TCP需要确认连接,分包传输,需要三次握手比较耗时,而UDP无须确认连接,相对TCP建立比较快,但是会丢包,因此UDP通常用在视频和聊天中;

IOS模型

  • 五层:应用层、传输层、网络层、数据链路层和物理层;
  • 七层:应用层、表示层、会话层、传输层、网络层、数据链路层和物理层;

POST和GET的区别

  1. get请求方式,参数拼接在连接上,因此不安全;而且浏览器对链接的长度有限制,因此参数不能太多;
  2. post请求方式,参数会放在请求体中,相对比较安全。而且大小没有限制;
  3. get请求会被浏览器进行缓存,而post不会;
  4. get请求浏览器会把请求头和data通过一个Tcp数据包发给服务区;而post请求会把请求头和data分成两个TCP数据包进行发送,先发送头,等到服务器响应100之后,再发送data;

定时器

实现setTimeout和setInterval

// setTimeout
function mySetTimeout (callBack, time) {
    const timer = setInterval(() => {
        clearInterval(timer)
        callBack && callBack()
    }, time || 0)
}

// setInterval
function mySetInterval (callBack, time) {
    let timer
    function interval () {
       clearTimeout(timer)
       timer = setTimeout(()=>{
           callBack && callBack()
           interval()
       },time||0)
    }
    interval()
    return {
        cancel: function () {
            clearTimeout(timer)
        }
    }
}

XMLHttpRequest请求

/*
xhr.readyState  发送的状态
    0: 未初始化 未调用send()方法
    1: 读取中, 已经调用send()方法,在发送中
    2: 已读取,接收到全部响应的数据
    3: 交互中, 正在解析响应内容
    4: 请求完成,响应内容解析完成
    
xhr.status  服务器返回的状态码
  200: 成功
  304:  资源未被修改
  404:  请求的文件不存在
  500:  服务器内部错误
  
*/
// 1. 创建XMLHttpRequest对象
const xml = new XMLHttpRequest()
// 2. 设置请求地址和方式
xml.open(url,get/post,async)
// 3. 监听请求状态
xml.onReadyStateChange = function () {
    if (xml.readyState === 4 && xml.status === 200) {
        // ....
    }
}
// 4. 发起请求
xml.send(数据)

设计模式

单例模式

一个类只能被实例化一次;

应用:弹框,管理命名空间防止冲突;

// 缺点:外部可以修改instance属性
class Single {
    constructor(name){
        this.name = name
    }
    static getInstance (name) {
        if (!this.instance) {
            this.instance = new Singlne(name)
        }
        return this.instance
    }
}
let s1 = Single.getInstance("name1"); 
let s2 = Single.getInstance("name2");
console.log(s1 === s2); // true

// 通过圣杯实现
const Single = (function () {
    let instance = null
    return function (name) {
        this.name = name
        if (!instance) {
            instance = this
        }
        return instance
    }
})()
const s1 = new Single(1)
const s2= new Single(2)
console.log(s1 === s2); // true

实现一个创建单例模式的函数

function createSingle (fn) {
    let single = null
    return function (...args) {
        if (!single) {
            single = fn.apply(this, [...args])
        }
        return single
    }
}

// 测试创建一个弹框
var createPrompt = function (style, message) {
    var div = document.createElement('div');
    var strHtml = '<p>'+ message +'</p><span class='prompt-close'>×</span>'
    div.className = 'prompt ' + style;
    div.innerHTML = strHtml;
    div.style.display = 'none';
    div.addEventListener('click', function () {
      div.style.display = 'none';
    }, false)
    document.body.appendChild(div);
    return div;
}
var alert = createSingle(createPrompt)
var dom = alert('success', '测试一下')
dom.style.display = 'block'
var dom2 = alert('fail', '测试一下')
dom2.style.display = 'block'

策略模式

将每个独立的逻辑封装起来,不受外部的改变,可以看作是if/else的另一种形式;

优点:提高代码的复用率,减少冗余;缺点:需要编写使用文档,要不然需要阅读源码;

应用: 表单验证

function Validate () {}
    Validate.prototype.rules = {
      // 必填项
      isRequired: function (str) { // 每个都是独立的模块
        // 除去首尾空格
        const value = str.replace(/(^\s*)|(\s*$)/g, '')
        return value !== ''
      },
      // 最小长度
      minLength: function (str, len) {
        return str.length >= len
      }
    }
    Validate.prototype.test = function (rules) {
      // 存放校验结果
      let result = ''
      // 遍历传递进来的规则
      for (const key in rules) {
        if (Object.hasOwnProperty.call(rules, key)) {
          const element = rules[key];
          // 遍历规则中的数组
          for(let i = 0, len = element.length; i < len; i++) {
            const item = element[i]
            const name = item.rule
            let value = item.value
            if (!Array.isArray(value)) {
              value = [value]
            }
            const message = item.message
            // 执行校验规则
            const res = this.rules[name].apply(this, value)
            // 被校验住就跳出循环
            if (!res) {
              result = {
                key,
                message,
              }
              break
            }
          }
          if (result) {
            break
          }
        }
      }
      return result
    }

    // 对外扩展
    Validate.prototype.extends = function (key, fn) {
      this.rules[key] = fn
    }
    // 测试
    const result = Validate.test({
      'username': [{rule: 'isRequired', value: '', message: '用户名不能为空!'}],
      'password1': [
        {rule: 'isRequired', value: '123', message: '密码不能为空!'},
        {rule: 'minLength', value: ['123', 6], message: '密码长度不能小于6个字符!'}
      ],
    })

代理模式

代理一个对象,可以为其添加一些功能,也可以去掉原有的一些功能;

  • 虚拟代理:把开销很大的对象,延迟到真正需要它的时候才去创建,图片加载,文件上传;
  • 安全代理:控制对象的访问权限,前端校验
  • 智能代理:为其添加一些额外的功能
  • 远程代理:一个对象将不同空间的对象进行局部代理
// 图片懒加载
// 之前的功能,图片显示在页面上
    function MyImg (id) {
      const img = new Image()
      this.setSrc = function (src) {
        img.src = src
      }
      document.getElementById(id).appendChild(img)
    }

    // 进行代理
    const ProxyImg = (() => {
      const img = new Image()
      const oImg = new MyImg('img')
      // 当真正的图片加载完成之后,设置为原图片
      img.onload = function () {
        oImg.setSrc(img.src)
      }
      return function(src,loadingSrc){
        img.src = src
        oImg.setSrc(loadingSrc)
      }
    })()
    ProxyImg('a.png', 'load.png')

装饰者模式

在不改变原对象的情况下,在程序运行期间动态的添加功能;

应用场景:需要给当前对象或函数添加额外的功能,但是又不能影响到原函数或对象,就可以使用装饰者模式;

// 函数执行执行添加功能
function before (fn,bfn) {
  return function () {
    bfn.apply(this,arguments)
    return fn.apply(this,arguments)
  }
}
// 函数执行之后添加功能
function after (fn,afn) {
  return function () {
    fn.apply(this,arguments)
    return afn.apply(this,arguments)
  }
}
function fuc() {
  console.log(2);
}
function fuc1() {
  console.log(1);
}
function fuc3() {
  console.log(3);
}
fucb = before(fuc,fuc1)
fuca = after(fuc,fuc3);
fucb(); // 1 2
fuca()  // 2 3

装饰者模式和代理模式的区别?

  • 相同点: 都是额外的添加功能
  • 不同点:代理模式是在一开始就要确定额外的功能,而装饰者是在程序运行的时候添加额外的功能,代理模式只能代理一层,而装饰者是一条链式修饰;

为什么使用装饰者模式而不使用继承?

  • 继承具有父子耦合问题,父类修改子类也会跟着改变,而装饰者不不具有此问题

工厂模式

用来创建对象的一种模式

       function Factory (post) {
          if (this instanceof Factory) {
            return new this[post]()
          } else {
            new throw('Object must be new')
          }
        }
        // 具体的对象逻辑放在子类
        Factory.prototype = {
          'coder': function () {
            this.post = 'coder'
            this.work = '敲代码'
          },
          'hr': function () {
            this.post = 'hr'
            this.work = '招人'
          },
          'boss': function () {
            this.post = 'boss'
            this.work = '审批文件'
          }
        }


        let coder = new Factory('coder')
        console.log(coder.work) // 敲代码 

        // 使用工厂方法改造之后,如果我们需要添加新的岗位信息,只要在Factory.prototype中添加。

发布订阅模式

订阅者订阅相关的主题,发布者通过发布主题事件的方式通知相关的订阅者;

组成:发布,订阅者集合,添加订阅者,删除订阅者

优点:解耦对象之间的关系

缺点:订阅者需要存储起来,比较消耗内存,并且解耦了对象之间的关系,所以难以追踪和维护

应用:EventBus

// 调度中心
class EventBus {
  constructor(){
    this.list = []
  }
  // 添加订阅者
  on (type, callback) {
    if (!this.list[type]) {
      this.list[type] = []
    }
    this.list[type].push(callback)
  }
  // 删除订阅者
  delete (type, callback) {
    if (this.list[type]) {
      this.list[type] = this.list[type].filter(item => item !== callback)
    }
  }
  // 通知订阅者
  emit (type, ...args) {
    if (this.list[type]) {
      this.list[type].forEach(item => {
        item.apply(this, args)
      })
    }
  }
  // 添加一次
  once (type, callback) {
    this.on(type, function (...args) {
      callback && callback(...args)
      this.delete(type, callback)
    })
  }
}

// 测试
let event = new EventBus();
// 添加订阅者
event.on("change", (...args) => {
  console.log(args);
});
// 只执行一次
event.once("change", (...args) => {
  console.log(args);
});
// 当发布者数据变化的时候通过事件中心通知订阅者
event.emit("change", 1, 2); // 1 2
event.emit("change", 2, 3); // 2 3

观察者模式

一个对象有多个依赖于它的观察者,当对象变化的时候通知观察者

   // 一个对象
    const data = { name: 1}

    // 观察者
    function a () {
      console.log('接收到了观察对象的变化')
    }

    const proxy = new Proxy(data,{
      get (target, key, receiver) {
        return Reflect.get(target, key, receiver)
      },
      set (target, key, value, receiver) {
        // 数据变化的时候直接通知观察者
        a()
        return Reflect.set(target, key, value, receiver)
      }
    })

观察者和订阅者模式的区别?

  • 都是实现对象之间一对多的关系
  • 观察者,对象和观察者之间没有完全解耦;对象发生变化的时候直接通知观察者;而发布订阅者中间有调度中心,发布者和订阅者完全解耦,它们之间的通信靠调度中心

实现reduce

reduce方法是把数组中的每个元素按照顺序执行一个回调函数,每次执行回调函数会把上次回调函数返回的值作为参数传递进去,可以执行初始值;

初始值不存在,就会取数组的第一项作为初始值,从第二项开始遍历数组

[].reduce((上个返回值,当前值,当前索引,原数组) => {},初始值)

const arr = [1,2,3]
arr.reduce((pre,cur) => { return pre+ cur },null)
// 6

arr.reduce((pre,cur) => { return pre+ cur },undefined)
// NaN

实现reduce

const arr = [1,2,3]
function myReduce (callback, initVal) {
  let arr = [...this]
  let prev = ''
  // 初始值不存在
  if (initVal === null) {
    // 取数组的第一项
    initVal = arr[0]
    arr = arr.splice(1)
  }
  prev = initVal
  arr.forEach((item,index) => {
    prev = callback(prev,item,index,this)
  })
  return prev
}
myReduce.call(arr,(prev,cur) => {
  return prev + cur
},1) // 7

实现函数组合管道

函数组合,实际上就是把处理数据的函数像管道一样连接起来,然后让数据穿过管道得到最终的结果

compose函数要求:可执行同步方法,也可执行异步方法,两者都可以兼容

// 实现函数组合实现管道
function compose (list=[]) {
  const initVal = list.shift()
  return function (...args) {
    return list.reduce((prev, cur) => {
      console.log(prev)
      return prev.then(res => {
        return cur(res)
      })
    },Promise.resolve(initVal(...args)))
  }
}
let async1 = data => {
  return new Promise(resolve => {
    setTimeout(() => {
      console.log("async1");
      resolve(data);
    }, 1000);
  });
};
let async2 = data => {
  return new Promise(resolve => {
    setTimeout(() => {
      console.log("async2");
      resolve(data + 1);
    }, 2000);
  });
};
let async3 = data => {
  return new Promise(resolve => {
    setTimeout(() => {
      console.log("async3");
      resolve(data + 2);
    }, 3000);
  });
};
let composeFn = compose([async1, async2, async3]);
composeFn(0).then(res => {
  console.log(res);
});

// async1 async2 async3 3

// 方式2 通过async await
function compose(arr) {
      return async function a(val){
        let result = val
        for (let i = 0; i< arr.length; i++) {
          result = await Promise.resolve(arr[i](result)).then(res => res)
        }
        return result
      }
    }

实现flat

flat是用来扁平化数组的,可以传递一个参数,根据这个参数来实现扁平化的深度;返回一个新数组;

const arr = [1,2,[3,[4]]]
arr.flat(1) // [1,2,3,[4]]
arr.flat(2) // [1,2,3,4]

实现flat

function myFlat(arr,deep){
  // 如果没有深度或深度为0就直接返回数组
  if (!deep) {
    return arr
  }
  return arr.reduce((prev, cur) => {
    // 如果当前值为一个数组,那么就通过递归处理
    if (Array.isArray(cur)) {
      return prev.concat(myFlat(cur,deep-1))
    }  else {
      return prev.concat(cur)
    }
  },[])
}
// 测试
const arr = [1,2,[3,[4]]]
myFlat(arr,1) //  [1,2,3,[4]]
myFlat(arr,2) // [1,2,3,4]

实现Object.is

判断两个值是否相同,解决了恒等的一些特殊情况

-0 === 0 // true
-0 === +0 // true
NaN === NaN // false

Object.is(-00) // false
Object.is(-0,+0) // false
Object.is(NaN,NaN) // true

function is (a,b) {
    // -0
    if ((1/val1 === Infinity && 1/val2 === -Infinity) 
        || (1/val1 === -Infinity && 1/val2 === Infinity)) {
        return false
      }
    // NaN
    if ((a+'') === 'NaN' && (b + '') === 'NaN') {
        return true
    }
    // 或 NaN不等于自身
    //if (a !== a && b !== b) {
    //    return true
    //}
    return a === b
}

实现map

map方法返回一个新数组,这个数组中的值由原数组中每个元素调用回调函数返回的值组成

// map((当前值,当前索引,原数组) => {},调用回调函数的this)
const arr = [1,2,3]
arr.map(item => 2 * item) // [2,4,6]

实现map

function myMap (callback,self) {
    const arr = [...this]
    let result = []
    for (let i = 0, len = arr.length; i < len; i++) {
      result.push(callback.call(self || null, arr[i], i, arr))
    }
    return result
}
let arr = [1, 2, 3];
console.log(myMap.call(arr, item => item * 2)); // [2, 4, 6]

实现some

some方法测试数组中的至少有一个元素符合回调函数返回的条件就返回true,都不符合才返回false;空数组直接返回false;满足条件之后就不再继续遍历了;

// some((当前值,当前索引,原数组)=>{},调用回调函数的this)
const arr = [1,2,3]
arr.some(item => item > 8) // false
arr.some(item => item > 2) // true

实现some

function mySome(callback, self){
  const arr = [...this]
  if (!arr.length) {
    return false
  }
  for (let i = 0, len = arr.length; i < len; i++) {
    const result = callback.call(self,arr[i],i,arr)
    if (result) {
      return true
    }
  }
  return false
}

const arr = [1,2,3]
mySome.call(arr,item=>item>8) // false
mySome.call(arr,item=>item>2) // true

实现一个能够判断所有类型的方法

function isType (target) {
  return Object.prototype.toString.call(target).split(' ')[1].slice(0,-1).toLocaleLowerCase()
}
isType('') // string
isType([]) //array
isType() // undefined

image.png

函数柯里化

将具有多个参数的函数,转换成一系列使用一个参数的函数执行

function fb(a,b,c,d){
    return a+b+c+d
}
fb(1,2,3,4) // 10
// 转换之后
let fn1 = klh(fn);
f1(1)(2)(3)(4) // 10

实现klh

function klh(fn){
    // 如果传递进来的函数的参数长度小于等于1,直接返回这个函数
    if (fn.length <= 1) {
    return fn
    }
    return function inner(...args) {
    // 如果当前函数的参数和传递进来的参数相等直接执行
    if (args.length === fn.length) {
      return fn(...args)
    } else { // 如果不相等,继续返回一个函数,拼接参数继续递归
      return (...args2) => {
        return inner(...args,...args2) // 关键是拼接参数进行递归
      }
    }

    }
}

防抖

不管频率触发多快,只会在最后一次触发的指定时间点执行;

应用:搜索框的限制

/**
 * 
 * @param time 触发回调的时间
 * @param callback 回调
 * @param flag 是否第一次执行
 * @returns 
 */
function debounce(time, callback, flag){
  let timer
  return function (...args) {
    timer && clearTimeout(timer)
    if (!timer && flag) {
      callback && callback(...args)
    }
    timer = setTimeout(() => {
      callback && callback(...args)
    },time)
  }
}
// 测试
function fn(a) {
  console.log("执行:", a);
}
let debounceFn = debounce(3000, fn, true);
debounceFn(1);
debounceFn(2);
debounceFn(3);

// 先打印 执行1
// 3s后打印: 执行3

函数节流

不管频率触发多快,只会在指定的时间点执行;

应用:滚动的优化

function throttle (time, callback, flag) {
  let timer
  return function (...args) {
    if (timer) {
      return
    }
    if (flag && !timer) {
      callback && callback(...args)
      flag = false
    }
    timer = setTimeout(() => {
      callback && callback(...args)
      timer = null
    },time)
  }
}
function fn2(a) {
  console.log("执行:", a);
}
let throttleFn = throttle(3000, fn2, true);
throttleFn(1);
throttleFn(2);
throttleFn(3);

// 先打印: 执行1
// 3秒之后: 执行1

实现定时删除的localStorage

超过了规定时间,就删除localStorage中的内容

// 实现一个定时删除localStorage
    const storage = {
      set (key, value, time) {
        if (!localStorage) {
          return null
        }
        const value = {
          data: value,
          time: time
        }
        localStorage.setItem(key, JSON.stringify(value))
      },
      get (key) {
        if (!localStorage) {
          return null
        }
        const value = localStorage.getItem(key)
        const data = (value && JSON.parse(value))
        const time = data.time
        if (new Date().getTime() > new Date(time).getTime()) {
          localStorage.removeItem(key)
          return null
        }
        return data.data
      }
    }

实现render函数

vue中render函数用来根据虚拟dom创建真实的dom

function render (vnode) {
  // 如果是字符串表示文本,创建文本节点
  if (typeof vnode === 'string') {
    return document.createTextNode(vnode)
  }
  // 否则就是dom节点,创建对应的dom
  const dom = document.createElement(vnode.tag)
  // 如果有属性就遍历属性添加到dom上
  if (vnode.attrs) {
    Object.keys(vnode.attrs).forEach(key => {
      dom.setAttribute(key,vnode.attrs[key])
    })
  }
  // 如果有子元素就进行遍历递归处理,添加到父元素上
  if (vnode.children) {
    vnode.children.forEach(item => {
      dom.appendChild(render(item))
    })
  }
  return dom
}

图片懒加载

只有图片滚动到可见区域的时候才显示真正的图片

// <img src="./loading.jpg" data-src="https://123.jpeg">
/*
*  方式1 通过intersectionObserve 
*  优点:无需监听滚动
   缺点:兼容性问题,ios12.1以下不支持
*/
function loadImg () {
  // 获取到页面上展示的所有的图片
  const imgList = document.getElementsByTagName('img')
  // 通过intersectionObserve进行观察元素的位置
  const observe = new IntersectionObserver((list)=> {
    // 遍历当前可见的所有img元素
    list.forEach(item => {
      // 判断是否已经显示在可见区域
      if (item.intersectionRatio > 0) {
        item.target.src = item.target.getAttribute('data-src')
        // 停止观察已经设置过src的元素
        observe.unobserve(item.target)
      }
    })
  })
  // 监听所有的图片
  imgList.forEach(element => {
    observe.observe(element)
  });
}

/*
    方式2
    通过监听滚动,判断每个图片的顶部距离可视区域的顶部的距离是否小于一屏的高度,小于就显示
    
*/
let n = 0
function lazyLoad () {
  // 屏幕的高度
  const seeHeight = document.documentElement.clientHeight
  // 获取所有的图片
  const imgList = document.getElementsByTagName('img') || []
  // 遍历图片
  for (let i = n; i < imgList.length; i++) {
    const element = imgList[i]
    // 如果当前图片的顶部到可视区域的顶部小于屏幕高度表示需要显示,加载图片
    if (element.getBoundingClientRect().top < seeHeight) {
      if (element.getAttribute("src") === "loading.jpg") {
        element.src= element.getAttribute("data-src")
      }
    }
    n = i + 1
  }
}
window.onscroll = lazyLoad;

实现最大并发

每次同时发起n个请求,如果有一条返回结果就继续加一条请求,直到所有的请求都完成;

function maxRequest (list, maxNum) {
  return function fn (){
    // 取当前请求数量和最大请求数量的最小值
    const num = Math.min(list.length, maxNum)
    for (let i = 0; i < num; i++) {
      // 从头取出一个进行执行
      const f = list.shift()
      // 当前最大请求数量减一
      maxNum--
      // 执行请求
      f().finally(()=>{
        // 执行完最大请求数加1,进行递归
        maxNum++
        fn()
      })
    }
  }
}
// 测试
function requst1(){
  return new Promise(res => {
    setTimeout(() => {
      console.log(1)
      res(1)
    },1000)
  })
}
function requst2(){
  return new Promise(res => {
    setTimeout(() => {
      console.log(2)
      res(2)
    },2000)
  })
}
function requst3(){
  return new Promise(res => {
    setTimeout(() => {
      console.log(3)
      res(3)
    },3000)
  })
}
maxRequest([requst1,requst2,requst3,requst1], 2)() // 1 2  1 3

实现LazyMan

LazyMan主要是链式执行任务,每个任务执行完成之后再执行下个任务;

/**
 * 实现一个LazyMan,可以按照以下方式调用:
    (1)LazyMan(“Hank”)
      输出:
      Hi! This is Hank!

    (2)LazyMan(“Hank”).sleep(10).eat(“dinner”)
      输出:
      Hi! This is Hank!
      //等待10秒..
      Wake up after 10
      Eat dinner~

    (3)LazyMan(“Hank”).eat(“dinner”).eat(“supper”)
      输出:
      Hi This is Hank!
      Eat dinner~
      Eat supper~

    (4)LazyMan(“Hank”).sleepFirst(5).eat(“supper”)
      输出:
      //等待5秒
      Wake up after 5
      Hi This is Hank!
      Eat supper
 */
class LazyMan {
  constructor (name) {
    // 存储任务
    this.task = []
    // 执行输出名称的函数
    function fn () {
      console.log('Hi This is ' + name)
      this.next();
    }
    // 存储函数
    this.task.push(fn)
    // 通过定时器执行下个任务
    setTimeout(() => {
      this.next()
    },0)
  }
  // 执行任务的函数
  next () {
    if (this.task.length) {
      // 从头取出函数执行
      this.task.shift().call(this)
    }
  }
  // 睡觉函数
  sleep (num) {
    const self = this
    function fn () {
      setTimeout(() => {
        console.log('Wake up after' + num)
        // 执行完睡觉执行下个任务
        self.next()
      },num * 1000)
    }
    // 把睡觉进行包裹存入到任务列表中
    self.task.push(fn)
    return this
  }
  sleepFirst (num) {
    const self = this
    function fn () {
      setTimeout(() => {
        console.log('Wake up after' + num)
        self.next()
      },num * 1000)
    }
    self.task.unshift(fn)
    return this
  }
  eat (food) {
    this.task.push(() => {
      console.log('eat' + food)
      this.next()
    })
    return this
  }
}
new LazyMan('Hank').sleepFirst(5).eat('supper').sleep(2)
// Wake up after5  Hi This is Hank  eatsupper  Wake up after2

CSS

link、style、@import的区别

  • 加载顺序
    1. link和页面加载的时候一起加载
    2. @import在页面全部加载完成之后开始加载
  • 加载内容
    1. link不仅可以加载样式文件还可以加载其他文件
    2. style定义当前页面的样式
    3. @import只能加载样式文件

css权重

!important(最大) > 行内样式(1000) > id(100) > class、伪类、属性选择器(10) > 元素选择器,伪元素(1) > 通配符,后代选择器,子选择器,相邻选择器(0);
如果多个选择器配合使用,那么它的权重就是它们相加的结果;

Css3硬件加速

css3硬件加速又称GPU渲染,利用GPU渲染减少CPU的使用;从而可以提升性能;

开启GPU的属性:

  • transform不为none;
  • opacity
  • filter
  • will-change

过度的使用GPU会导致内存问题,所以需要平衡使用;

移动端实现1px

原因:不同设备像素比下1px显示出来的效果不同,比如设备像素比为2,那么1px占据两个物理像素,因此显示的比较粗; 解决:通过伪类实现1px再通过tansform的scalc进行缩放相应的比例;2倍屏缩放0.5,三倍屏缩放0.33;通过媒体查询可以得到具体的设备像素比;

// 四条边框都为1px
a:after{
 content:“”;
 position:absolute;
 left:0;
 ruight:0;
 width:100%;
 heihgt:100;
 border:1px solid #000;
 tranform-origin:(0,0);
 tranfiorm: scale(0.5);
}

BFC块级格式化上下文

具有BFC的元素就是一个独立的容器,它内部和外部的元素互不干扰,比如一个浮动的元素就会触发BFC;

触发BFC的条件:

  1. 浮动不为none的元素
  2. 具有绝对或固定定位的元素
  3. overflow不为visible的元素
  4. 行内块级元素
  5. flex布局的元素

BFC规则的约束: BFC元素中的布局就是常规布局,其中的元素相互影响,垂直方向上margin会重叠;

BFC解决的问题:清除浮动overflow为hidden,解决外边距重叠;

margin重叠问题

  1. 上面元素的marign-bottom和下一个元素的margin-top
  • 都为正数取最大的那个
  • 都为负数取绝对值最大的那个
  • 一正一负取相加的结果
  1. margin-bottom取负值,自身不动,下面的元素向上移动
  2. margin-top取负值,自身向上移动,下面的元素也会上移;

元素层级遮挡问题

  1. b移动到a的上面,那么b的层级高于a,b元素的背景会在a元素的上面,但是a元素的内容在b元素的背景上;a元素的内容在b元素的内容上;(两个块级元素重叠,后者会覆盖前者的背景但是无法覆盖其内容)

image.png

  1. 两个都是行内元素,或一个行内一个行内块级元素,或两个都是行内块级元素的时候,后者会覆盖前者的背景和内容;

两个行内块:

image.png

image.png

a行内,b为行内块

image.png

  1. 一个块级元素覆盖行内块块级元素或行内元素,块级元素除了文字,其他都会被行内块或行内覆盖

a为行内块,b为块级

image.png

a为行内,b为块级

image.png

image.png

注意:都是没有设置定位的两个元素,如果设置定位属性,那么定位属性的层级高,会覆盖其他元素的内容和背景;

  1. 两个元素都有定位,a为行内,b为块级;b覆盖a

image.png image.png

总结:行内和行内块为一层级,块级为一个层级,行内块层级大于块级的层级;如果没有设置定位属性的两个块级元素,后者会覆盖前者的背景,但是不会覆盖内容;行内或行内块元素组合,后者会覆盖前者的背景和内容;行内或行内块和块级元素,块级元素的背景会被行内或行内块覆盖,内容不会被覆盖;

image.png

盒模型

标准盒模型: box-sizing: content-box; 盒子的大小就是内容的大小; 非标准盒模型:box-sizing: border-box;盒子的大小等于内容的大小+内边距+边框的大小

实现一个自适应的正方形

// 1. 使用vw
.a{
    width: 10vw;
    height: 10vw;
}

// 2. 使用百分比加padding-top
.a{
  width: 10%;
  padding-bottom: 10%;
  background: red;
  height: 0;
}

清除浮动

  1. 给父元素设置高度
  2. overflow: hidden
  3. 浮动元素的后面添加一个空元素,clear:both;
  4. 添加一个伪元素,clear:both;

伪元素和伪类

  • 伪元素:创建一些不在文档树中的元素,为其添加一些样式,比如::before,::after,::first-line,::first-letter,::selection,::placeholder;
  • 伪类:通过伪类添加一些不能为常规css选择器获取到的信息,比如 :hover,:active,:first-of-type,:last-of-type,:checked等;

position定位

  1. static:默认值,没有定位,元素正常在文档流中;
  2. relative: 相对定位,相对于自身进行定位;不脱离文档流;
  3. absolute:绝对定位,相对于外层非statc属性的元素进行定位;
  4. fixed: 固定定位,相对于可视窗口进行定位;
  5. sticky: 粘性定位,页面滚动的时候让元素固定到窗口的顶部;
  6. inherit: 从父元素继承postion;

如果上级元素设置transform的任意属性,会导致内部的fixed元素相对于此元素进行定位;这个现象目前只有谷歌和火狐浏览器中出现;

display

  1. none: 隐藏一个元素,并且不会出现在文档流中;
  2. block: 显示并且让一个元素变为块级元素;
  3. flex: 让此元素具有flex布局;
  4. inline:让一个元素变为行内元素;
  5. inline-block:让一个元素变为行内块级元素;
  6. inherit: 从父元素继承display;
  7. table:作为块级表格;
  8. list-item: 给元素添加列表标记;

行内块及元素的问题

行内块级元素之间会有空隙,是因为行内块级元素之间如果有换行空格等合并多余的空格,只留一个空格;默认会设置它的white-sapce为normal;

解决:把行内块级元素放在一起,中间不留空隙;给父元素的字体大小设置为0;设置浮动;

px、em、rem、vw和vh

  1. px: 绝对单位,页面按照精确的像素进行展示;
  2. em: 相对单位,相对于自身或父元素中的字体大小;
  3. rem: 相对单位,相对于根元素的字体大小;
  4. vw和vh: 相对单位,按照浏览器的可视区域,把可视区域分成100分,1vw占一份;

为什么移动端使用二倍图

因为设备的物理像素不同,有的设备是1px占据两个物理像素,为了不失真采用二倍图;

如何实现小于12px的字体

通过transform的scale进行缩放,或者采用em进行设置相对大小;

元素的竖向百分比是相对于容器的高度吗?

不是,margin-top,margin-bottom,padding-top,padding-bottom的百分比会相对于容器自身的宽度;height属性是相对于父元素的高度;

省略号

单行省略

oveflow: hidden;
white-space: nowrap;
text-overflow: ellipsis;

多行省略

overflow: hidden;
text-overflow: ellpisis;
-webkit-line-clamp: 3; // 设置显示的行数

line-height的值

  1. 带有单位:具体的行高
  2. 只有数字或百分比:会把比例传递到后代,父元素line-height:1.5;那么子元素的行高就是子元素的字体大小*1.5或百分比,子元素没有字体会继承父元素的字体;

动画

  1. css动画:animate,transition和transform配合;
  2. js动画:requestAnimationFrame或改变元素的几何信息配合定时器;

浏览器原理相关

缓存

强缓存优先于协商缓存,若强缓存生效直接采用强缓存,否则采用协商缓存;协商缓存是由服务器决定是否使用缓存,如果协商缓存失效返回200,如果生效返回304;

缓存位置

memory Cache: 内存中的缓存,随着浏览器的关闭而清除;
Disk Cache: 硬盘中的缓存,永久性缓存,需要手动清除,可以跨站点跨页面访问;

缓存类型

强缓存和协商缓存

强缓存

强缓存主要可以通过http头中的Expires和Cache-control两个字段来控制,如果两个字段都存在Cache-control优先于Expires;如果命中强缓存,浏览器不再发起请求,直接从缓存中获取;

  • Expires:是HTTP1.0中的属性,只能设置于响应头中,其值表示缓存的时间是服务端的时间,在这个时间段内再次访问就会直接从缓存中获取,它和客户端的时间进行比较;
    优点:可以设置过期时间;
    缺点:到了过期时间,不管文件是否被修改都直接从服务端获取;依赖于客户端的时间,如果时间不准确导致缓存失效;

  • Cache-control: 是HTTP1.1中的属性,可以设置于请求和响应头中,可以组合使用多条属性,属性之间通过,连接;优先级高于Expires;

- 响应头中的Chache-control属性
    1. max-age:过期时间,单位是秒,相对于请求时间;
    2. s-maxage: 用于缓存服务器上的缓存过期时间,优先级高于max-age;
                 配合public一起使用;
    3. no-cache: 只能终端浏览器缓存,每次使用缓存前需要向服务端进行确认是否过期;
    4. no-store: 禁止任何缓存;
    5. public: 被任何节点缓存;
    6. private: 只能被终端浏览器缓存
- 请求头中的Cache-control属性
    1. max-stale: 过期之后的几秒内还是有效的;
    2. min-fresh: 缓存到期前的几秒前有效;
    3. only-if-cache: 只获取代理服务器的缓存,如果代理服务器上的缓存失效直接返回5044. no-cache: 不使用强制缓存,使用协商缓存;
协商缓存

如果没有命中强缓存,浏览器会请求服务器,服务器会进行协商缓存的校验,协商缓存主要通过以下四个属性进行判断是否命中;Etag和If-None-Match,Last-Modified和If-Modified-Since;它们两两组合使用;如果命中协商缓存,服务端返回304,浏览器从缓存中获取;

Etag: 存在于响应头中,资源标识;根据资源大小和修改时间生成;
If-None-Match: 存在于请求头中,上一次服务端返回的Etag的值,服务端会通过这个值和服务端的Etag进行对比,判断资源是否被修改;

Last-Modified: 存在于响应头中,资源最近修改的时间;
If-Modified-Since: 存在于请求头中,上次服务端返回的Last-Modified的值;服务端会通过这个值和服务端的Last-Modified的值进行对比,判断资源是否被修改过;
  • Last-Modified和If-Modified-Since 客户端请求服务端的时候,服务端响应头中会携带Last-modified字段,表示修改的时间,精确到秒;客户端再次请求的时候会携带If-Modified-Since字段,它的值为服务端返回的Last-Modified字段的值,服务端会比较两个值,如果不一样表示修改过资源,直接返回资源状态为200;如果一样表示未修改过,直接返回304状态码,浏览器从缓存中获取;它们出现在HTTP1.0中;

缺点:精确到秒,在最后一秒即修改了资源同时又获取资源,那么还是会命中协商缓存;

  • Etag和If-None-Match 客户端请求服务端的时候,服务端响应头中会携带Etag字段,Etag由资源大小和修改时间计算生成;客户端再次请求的时候请求头中携带If-None-Match字段,其值为服务端返回的Etag字段的值,服务端会对比两个值,如果一样直接返回304,否则返回新的资源和200状态码;它们出现在HTTP1.1中;它们优先级高于Last-Modified;

优点:解决了时间精确到秒的问题;
缺点: 相比Last-Modified比较消耗性能;

普通刷新和强制刷新

  • 普通刷新(F5):不使用强制缓存,使用协商缓存;
  • 强制刷新:不使用任何缓存,直接从服务器获取资源;

小结

客户端首次请求资源的时候,服务端返回资源和状态码200,客户端会把响应头,请求时间和资源缓存起来;下一次请求的时候,浏览器会从缓存中获取到对应的资源,比较本次请求时间和缓存中对应的请求时间的差,如果这个差值没有超过了响应头中的cache-control的max-age或Expires的值,那么就直接获取缓存中的资源返回200状态码;否则请求服务器;请求头中携带If-None-Match或If-Modified-Since字段;服务端会通过Etag或Last-Modified字段和If-None-Match或If-Modified-Since的值进行比较,判断值是否相同,相同就命中协商缓存直接返回304,客户端从缓存中获取,否则没有命中协商缓存,返回新的资源和200状态码;