含泪整理 面试题 更新中...

149 阅读28分钟

Js


数据类型

  1. 原始数据类型: Number、String、Boolean、undefined、null、Symbol(es6,表示独一无二的值)
  2. 对象类型:包括Array、Date、RegExp、Function、Error、ASrguments

原始数据存储在栈中,对象数据存储在堆中,在栈中之保存一个指针(地址),通过指针去找到相应的对象

0.1+0.3 !== 0.3 这个0.1用二进制来表示会变成一个无限循环的一个数字,js的number采用的是浮点类型,后面的无限循环会被裁减掉,会失去精度,所以会出现这个不等于的现象

  1. typeof判断数据类型

    typeof对于原始值来说都可以正常显示类型,但是unll显示的是‘object’,因为js中使用的是低位存储变量的类型信息,000开头表示的是对象,而null表示的全0,所以typeof判断结果为对象

typeof 1 // 'number'
typeof '1' // 'string'
typeof undefined // 'undefined'
typeof true // 'boolean'
typeof Symbol() // 'symbol'
// typeof判断对象类型的时候,除了函数结果为function外,其余的都是object
typeof [] // 'object'
typeof {} // 'object'
typeof console.log // 'function'
// 通过instanceof判断类型,内部机制是通过原型链来判断
  1. Object.prototype.toString.call

    返回[object, class],能正确判断所有的数据类型

    1. 如果this是undefined,返回[object, Undefined]
    2. 如果this是null,返回[object, Null]
    3. 获取this对象的[[Class]]属性的值,class一个字符串值,表明了该对象的类型
    4. 返回[object,[[Class]]]的结果
  2. instanceof

    原理:测试构造函数的prototype是否出现在被检测对象的原型链中,返回boolean

    // 并不准确,有时也会有问题
    function Fn() {}
    Fn.prototype = new Array; // 原型继承:让子类的原型等于父类的一个实例
    var f = new Fn;
    console.log(f instanceof Array); // true
    
    // 模拟instanceof实现
    function instanceof (left, right) {
      vat prototype = right.prototype
      left  = left.__proto__
      while (true) {
        if (left === null || left === undefined) {
            return false
        }
        if (prototype === left) {
            return true
        }
        left = left.__proto__
      }
    }
    
  3. constructor

    构造函数检测

    function Fn() {}
    Fn.prototype = new Array;
    var f = new Fn;
    console.log(f.constructor); // Array
    

    把类的原型重写,把之前的constructor覆盖掉了,导致判断错误

this指向

  1. 默认绑定:在宽松模式下在全局作用域下this指向window
  2. 隐式绑定:当一个函数被赋值给一个对象的属性时,函数中的this将自动指向这个改对象
  3. 显示绑定:通过call、apply、bind显示的将this绑定到一个上下文中即显示绑定
  4. new绑定:通过构造函数new出来创建的对象中this总是指向新创建的对象
  5. 箭头函数中的this指向它的封闭环境,捕获其所在上下文的this值作为自己的this,就是离它最近的普通函数的this,所以箭头函数中的this不会被显示绑定的方法改变

new操作符

  1. 创建一个新的对象
  2. 将构造函数的作用域赋值给新的对象(因此this指向新的对象)
  3. 执行构造函数中的代码(为这个新对象添加属性)
  4. 返回新的对象
function new (fun) {
  if (typeof fun !== 'function') {
      throw 'fun must be a function'
  }
  //Object.create第一个参数是对象,使用现有对象来提供新创建的对象的__proto__
  var newObj = Object.create(fun.prototype)
  var argsArr = [].slice.call(arguments, 1)
  fun.apply(newObj, argsArr)
  return newObj
}

apply、call、bind的区别

  1. apply、call都是改变this的指向,apply接收的是一个参数,参数为数组,call可接受多个参数的列表
  2. apply、call会立即执行函数,bind为函数绑定新的上下文,并返回这个函数,不会立即执行这个函数
  3. 应用:Math.max.apply(Math, [1, 2, 3])、调用父结构函数实现继承
// call可接收多个参数
Function.prototype.call1 = function (ctx, ...args) {
  if (typeof this !== 'function') {
      throw 'this must be a function'
  }
  ctx = ctx || window
  // this就是待执行的函数
  ctx.fn = this
  // fn执行并改变了this的指向
  var result = ctx.fn(...args)
  // 删除fn
  delelte ctx.fn
  // 返回执行的结果
  return result
}

// apply接收一个数组参数
Function.prototype.apply1 = function (ctx, args) {
  if (typeof this !== 'function') {
      throw 'this must be a function'
  }
  ctx = ctx || window
  // this就是待执行的函数
  ctx.fn = this
  // fn执行并改变了this的指向,参数处理和call不同
  var result = ctx.fn(...args)
  // 删除fn
  delelte ctx.fn
  // 返回执行的结果
  return result
}

// bind需要返回一个函数,接收多个参数
Function.prototype.bind1 = function (ctx, ...args) {
  if (typeof this !== 'function') {
      throw 'this must be a function'
  }
  // 保存待执行的函数
  var that = this
  return function () {
    var allArgs = [...args].concat([...arguments])
    return that.apply(ctx, allArgs)
  }
}

// 使用:调用父构造函数实现继承
function  SuperType(){
    this.color=["red", "green", "blue"];
}
function  SubType(){
    // 核心代码,继承自SuperType
    SuperType.call(this);
}

面向对象 原型继承

面向对象就有一个类的概念,js中一切皆对象,可以通过类创建任意多个的具有相同属性和方法的对象,这样比你一个一个的创建更加的方便,提高代码的可复用性,高内聚低耦合,代码冗余度低

创建对象的方法:

  1. 工厂模式

    直接一个函数通过传递参数的方法创建出多个具有不同属性值的对象

    优点:减少的代码的重复

    缺点:没有解决对象识别的问题,不能知道对象的类型,都是Object类型,就是没有类的概念

    function Cat(name,color) {
      return {
        name:name,
        color:color
      }
    }
    
  2. 构造函数模式

    没有显示的创建对象,通过new操作符来创建新对象

    优点:它有了类的概念,构造函数就相当于一个类,对象的原型中有个constructor属性,指向构造函数,可以判断出对象是通过那个构造函数

    缺点:构造函数内每个方法都要在每个实例上重新创建一遍,浪费内存空间

    function Cat (name, age) {
      this.name = name
      this.age = age
    }
    var cat = new Cat('猫'2)
    
  3. 原型模式

    每个函数都有一个prototyoe原型属性,这个属性指向一个对象,这个对象上的属性和方法是可以被实例共享的

    优点:不会重复创建相同的属性和方法

    缺点:不能重写原型中的值

    function Cat(name,color){
     this.name = name;
     this.color = color;
    }
    Cat.prototype.type = "猫科动物";
    Cat.prototype.eat = function(){alert("吃老鼠")};
    
  4. 构造函数和原型结合模式

    构造函数模式用于定义实例属性,原型模式用于共享的方法和属性,这是现在比较常用的一种方法

  5. 寄生构造函数模式

    构造函数在不返回值的情况下,默认会返回新对象实例,而通过在构造函数末尾添加一个return语句,可以重写调用构造函数时返回的值

    // 这种创建对象的方法可以用在一些想对已有的对象做扩展的时候
    function Persion (name, age) {
      var o = new Object()
      o.name = name
      o.age = age
      o.sayName = function () {
        alert(this.name)
      }
      return o
    }
    
    const friend = new Persion('A', 1)
    friend.sayName() // A
    

原型链:

每一个实例对象中都有一个属性__proto__指向它构造函数的原型prototype,实例可以使用构造函数原型中的所有的属性和方法,原型其实也是一个对象,我们可以将这个对象的__proto__指向另一个构造函数的实例,此时这个原型对象就拥有了另一个构造函数原型上的所有属性和方法,另一个构造函数的原型可以继续指向另外的构造函数的实例,直到某个构造函数的原型指向了Object的原型的时候终止,因为所有的引用类型都是继承自Object的,这样就实现了一个链条式的关系,也实现了继承,需要注意的是当把原型指向另一个实例对象的时候,需要把原型中的constructor属性给改过来,指向自己的构造函数,不然会出现混乱 原型其实保存的是一个指针,所以通过原型链找到的属性和方法都是指向同一个属性和方法,且不能在子类型的构造函数是不能向父类型的构造函数传参

继承方法:

  1. 借用构造函数

    通过call、apply方法在子类型中调用父类型的方法,

    优点:这样就可以实现向父类型传递参数了

    缺点:原型上定义的方法对于子类型是不可见的,就不能复用原型上的方法

    function Super () {
      this.colors = ['red', 'green']
    }
    
    function Sub () {
      Super.call(this)
    }
    
  2. 组合继承

    将原型链和借用构造函数的方法一起使用,是常用的一种继承的方法

    优点:既可以在子类中向父类型中传参,有可以实现原型上方法的复用

    缺点:需要调用两次父类型的构造函数,一次是在创建子类原型的时候,一次是在子类型构造函数内部,在原型上创建不必要的过多的属性

    function Super (name) {
      this.name = name
      this.colors = ['red', 'green']
    }
    Super.peototype.sayName = function () {
      alert(this.name)
    }
    
    function Sub () {
      Super.call(this)
    }
    Sub.prototype = new Super()
    // 手动将constructor替换
    Sub.prototype.constructor = Sub
    
  3. 原型式继承

    借助原型用已有的对象创建新对象,不用创建自定义类型,还将一个构造函数的原型指向另一个构造函数的实例,而是直接将一个构造函数的原型指向一个已有的对象

    优点:只是想让一个对象和另一个对象保持类似的情况是可以用的,不用创建新的构造函数,简单方便

    缺点:包含引用类型值的时候都是共享同一个值,改变其中一个,其余的都会被改变

    // es5中Object.create()方法的实现,一个是新对象原型的对象,一个是新对象额外属性的对象
    // 相当于浅拷贝父类的原型对象,直接把父类的原型对象赋值给子类的原型属性
    function object (o) {
      function F () {}
      F.prototyoe = o
      return new F()
    }
    
   
4. 寄生式继承

   就是传入一个对象,通过原型式继承创建一个新对象,为新对象添加方法,最后返回新的对象

   ```js
   function create (o) {
     var clone = object(o)
     clone.sayHi = function () {
       console.log('hi')
     }
     return clone
   }
  1. 寄生组合式继承

    不必为了指定子类型的的原型调用父类型的构造函数,创建一个父类型原型的一个副本,直接将子类型的原型指向这个父类型原型的副本就可以了

    function Parent(value) {
      this.val = value
    }
    Parent.prototype.getValue = function() {
      console.log(this.val)
    }
    
    function Child(value) {
      Parent.call(this, value)
    }
    // 手动修改constructor属性
    Child.prototype = Object.create(Parent.prototype, {
      constructor: {
        value: Child, // 属性的值
        enumerable: false, // 能否通过for...in循环返回属性
        writable: true, // 能否修改属性的值
        configurable: true // 能否delete删除属性,修改属性的特性
      }
    })
    
    const child = new Child(1)
    
    child.getValue() // 1
    child instanceof Parent // true
    

构造函数、原型、实例的关系

每个构造函数都有一个原型对象prototyoe,prototype原型中都有一个constructor属性指向构造函数,实例中都有一个指向原型对象的一个指针__proto__,

深拷贝浅拷贝

浅拷贝:只对跟属性创建新对象,也就是第一级属性,如果第二级属性还是对象,拷贝的是对象的地址

  1. Object.assign 可拷贝symbol属性
  2. 扩展运算符也是浅拷贝 { ...obj }
  3. Array.prototype.slice()

深拷贝:完完全全创建一个新的对象

  1. JSON.parse(JSON.stringify()),可满足大部分的要求

    注:如果值为函数、undefined、symbol经序列化后,键值对消失

    ​ 无法拷贝不可枚举的属性,无法拷贝对象原型链

    ​ Date变为string,正则变为空对象,NaN、Infinity变为null

    ​ 无法解决对象的循环应用的问题

  2. 实现深拷贝

    用Reflect.ownkeys()可以便利不可枚举的属性和symbol类型

    用Date、RegExp创建新的实例

    用weakMap解决成环的问题,将所有的对象作为weakMap的键名保存起来,第二次进入的时候直接返回,weakMap.has对象是否已被拷贝,weakMap.get返回已被拷贝的对象

    用eval()对函数进行拷贝,但一般为匿名函数会报错,所有要加变量名,箭头函数可以

    还有DOM拷贝的问题

weakMap:只接受对象作为键名,null除外,不可便利

weakMap中键名所引用的对象是弱引用,垃圾回收机制不将该引用考虑在内,只要所引用的对象在其他地方的引用都被清空后,垃圾回收机制就会将这个对象回收释放内存空间,防止了内存泄漏,

内存泄漏:两个对象之间的循环引用,a对象有属性指向b,b对象有属性指向a,a和b就永远不会被垃圾回收机制回收,因为引用的次数永远不会为0,这就导致了内存的泄漏

weakMap应用:数据缓存,保存某些对象的对应的值或计算出来的值,但不想管理这些数据的引用状态的时候可以使用weakMap,用于保存DOM元素,不用担心当DOM移除时需要手动清空引用

const isComplexDataType = obj => (typeof obj === 'object' || typeof obj === 'function') && (obj !== null)

const deepClone = function (obj, hash = new WeakMap()) {

    if (obj.constructor === Date) return new Date(obj);   //日期对象就返回一个新的日期对象
    if (obj.constructor === RegExp) return new RegExp(obj);  //正则对象就返回一个新的正则对象

    //如果成环了,参数obj = obj.loop = 最初的obj 会在WeakMap中找到第一次放入的obj提前返回第一次放入WeakMap的cloneObj
    if (hash.has(obj)) return hash.get(obj)

    let allDesc = Object.getOwnPropertyDescriptors(obj);     //遍历传入参数所有键的特性
    let cloneObj = Object.create(Object.getPrototypeOf(obj), allDesc); //继承原型链

    hash.set(obj, cloneObj)

    for (let key of Reflect.ownKeys(obj)) {   //Reflect.ownKeys(obj)可以拷贝不可枚举属性和符号类型
        // 如果值是引用类型(非函数)则递归调用deepClone
        cloneObj[key] =
            (isComplexDataType(obj[key]) && typeof obj[key] !== 'function') ?
                deepClone(obj[key], hash) : obj[key];
    }
    return cloneObj;
};

闭包

闭包是指有权限访问另一个函数作用域中变量的函数,可以让我们间接访问函数内部的变量(一般使用以需要传一个参数给函数,但有想让函数之后执行可以使用闭包)

这个跟作用域链有关,每一个函数的被执行的时候都会创建一个执行环境和一个对应的作用域,这个作用域会被保存在内部的[[scope]]属性中,当调用函数内部的函数时,会创建一个新的执行环境和作用域,并通过复制上一个函数[[scope]]创建新的作用域链,然后推入执行栈中(执行栈是一个后进先出的结构)执行

闭包的缺点是因为会保存上一级的作用域,所以导致上一级的作用域在内存中无法释放,导致内存泄漏,要手动置为空才能释放内存,所以要谨慎使用闭包

作用域链本质上是指向变量对象的指针列表,只引用但不实际包含变量对象

严格模式和宽松模式的区别

让js代码在更严格的条件下执行,让代码更加的严谨,必须在顶部写‘use strict’,如果某个函数需要使用严格模式,在函数体内部顶部写‘use strict’

  1. 严格模式取消了默认绑定,在全局环境声明的变量不会挂在到window下,所以全局环境中的函数this是undefined
  2. 所有变量都必须显示声明
  3. 所有函数必须在顶级作用域内被声明(在全局或函数作用域内),不能在块级作用域内
  4. 不能对arguments赋值,不会追踪参数的变化
  5. 不能设置或删除不可改变的属性
  6. with不能被调用
  7. 没有8进制的数字,在宽松模式下,以0开头的数字会被解读为8进制的数字

null和undefined的区别

  1. undefined表示没有值,既不是原始值也不是对象,访问未初始化的变量、缺失的参数、缺失的属性、函数中没有任何显示的返回时,返回值都是undefined,undefined强制转化为数字的时候是NaN
  2. null表示没有对象,原型链最顶端的元素,当字符串中没有匹配到正则表达式的结果的时候,返回的是null,null强制转化为数字的时候是0

节流/防抖

防抖:在事件触发后的多少秒后执行一次

// flag判断是否立即执行一次
function debounce(event, time, flag) {
  let timer = null;
  return function (...args) {
    clearTimeout(timer);
    if (flag && !timer) {
      event.apply(this, args);
    }
    timer = setTimeout(() => {
      event.apply(this, args);
    }, time);
  };
}

节流:在多少秒后事件执行

// 最后一次如果触发的的时候还在delay的时间之内,不会被触发
function throttle(event, time) {
  let pre = 0;
  return function (...args) {
    if (Date.now() - pre > time) {
      pre = Date.now();
      event.apply(this, args);
    }
  }
}

// 第一次不会立即的执行
function throttle(event, time) {
  let timer = null;
  return function (...args) {
    if (!timer) {
      timer = setTimeout(() => {
        timer = null;
        event.apply(this, args);
      }, time);
    }
  }
}

// 结合时间戳和定时器
function throttle(event, time) {
  let pre = 0;
  let timer = null;
  return function (...args) {
    if (Date.now() - pre > time) {
      clearTimeout(timer);
      timer = null;
      pre = Date.now();
      event.apply(this, args);
    } else if (!timer) {
      timer = setTimeout(() => {
        event.apply(this, args);
      }, time);
    }
  }
}

es6


var、let、const

  1. var

    • var存在变量提升

    • var不存在块级作用域的概念

    • 在全局环境声明的变量会挂载到window上

  2. let

    • 不存在变量作用域的提升
    • 在同一个作用域内,同一个变量不能被重复声明
    • let声明的变量不会到全局作用域上
    • let有块级作用域的概念
  3. const

    • const和let基本一致,但是const声明的变量不能被重新复制,如果死引用类型的值,因为在栈内存中保存的是它的指针,只要不改变这个指针就行,可以去修改内部的属性

解构赋值

当值为undefined是彩绘使用默认值,为null的时候不会使用默认值

扩展运算符

  • 相当于一个浅拷贝
  • 数组扩展运算符在函数传参中比较多用,之前用apply可以实现将数组转为函数的参数
  • 在es5中数组的复制和合并可以用arr.concat()方法来实现,es6可以使用扩展运算符实现

字符串扩展

模版字符串来拼接字符串

Array的扩展

  • 任何定义了遍历器(Iterator)接口的对象都可以通过扩展运算符转为真正的数组

    let nodeList = document.querySelectorAll('div')
    // nodeList是一个类数组
    let array = [...nodeList]
    
  • Array.from()

    可将类似数组的对象和可遍历的对象(具有遍历器的对象),包括Set和Map

    Array.from还接受第二个参数用于对数组的元素进行处理

    let arrayLike = {
        '0': 'a',
        '1': 'b',
        '2': 'c',
        length: 3
    };
    // 类似数组的本质特征是有length属性
    // 类似数组的对象还有DOM操作返回的NodeList集合,还有函数内部的arguments对象
    
    // ES5的写法
    var arr1 = [].slice.call(arrayLike); // ['a', 'b', 'c']
    
    // ES6的写法
    let arr2 = Array.from(arrayLike); // ['a', 'b', 'c']
    
  • Array.isArray

    判断这个值是否是数组,是返回true

  • Array.of

    将一组值转为数组,主要为了弥补new Array()只有一个参数时的不足

    Array.of(3, 11, 8) // [3,11,8]
    Array.of(3) // [3]
    Array.of(3).length // 1
    
  • copyWithin()

    接受三个参数

    • target(必需):从该位子开始替换数据,如果死负数表示倒数

    • start(可选):从该位子开始读取数据,默认0,负数表示从末尾开始计算

    • end(可选):到该位子前停止读取数据,默认等与数组长度,负数表示从末尾开始计算

    [1, 2, 3, 4, 5].copyWithin(0, 3)
    // [4, 5, 3, 4, 5]
    
  • find和findIndex

    find返回第一个符合条件的元素

    findIndex返回第一个符合条件的元素的位子,如果没有返回-1

    注:这两个方法都可以找到NaN,弥补了indexOf的不足(无法找到NaN)

    [1, 4, -5, 10].find((n) => n < 0)
    // -5
    
    [1, 5, 10, 15].findIndex(function(value, index, arr) {
      return value > 9;
    }) // 2
    
  • fill

    用于填充一个数组,如果填充的是一个对象,那么被赋值的是这个对象的内存地址

    接受3个参数

    • target(必需):待填充的元素
    • start(可选):从该位子开始替换
    • end(可选):到该位子前替换结束
  • entries、keys、values

    3个方法都返回一个遍历器对象,entries是对键值对的遍历,keys是对键名的遍历,values是对键值的遍历

    for (let index of ['a', 'b'].keys()) {
      console.log(index);
    }
    // 0
    // 1
    
    for (let elem of ['a', 'b'].values()) {
      console.log(elem);
    }
    // 'a'
    // 'b'
    
    for (let [index, elem] of ['a', 'b'].entries()) {
      console.log(index, elem);
    }
    // 0 "a"
    // 1 "b"
    
  • includes

    返回一个布尔值,表示数组中是否包含给定的值,可以正确判读啊NaN元素与字符串的includes相似

  • flat和flatMap

    方法都返回新数组,不改变原来数组

    flat用于将数组拉平成一维数组,默认只拉平一层,可以传参表示想拉平的层级,如果原数组有空位flat会跳过空位

    flatMap对数组元素执行一个方法,然后对返回值组成的数组执行flat

    [1, 2, , 4, 5].flat()
    // [1, 2, 4, 5]
    
    // 相当于 [[[2]], [[4]], [[6]], [[8]]].flat()
    [1, 2, 3, 4].flatMap(x => [[x * 2]])
    // [[2], [4], [6], [8]]
    
  • reduce

    接受两个参数,分别为回调函数和初始值

    回调函数中接受四个参数,分别是累积值、当前元素、当前索引、原数组

    const arr = [1, 2, 3]
    // 初始值是数组,在第二次回调中,会将上一次返回的值作为第一个参数传入
    const reduceArray = arr.reduce((acc, current) => {
      acc.push(current * 2)
      return acc
    }, [])
    console.log(reduceArray) // [2, 4, 6]
    
    // 模拟reduce
    Array.prototype.myReduce = function (cb, initVal) {
      let prev = initVal || 0
      for (let i = 0; i < this.length; i++) {
        prev = cb(prev, this[i], i, this)
      }
      return prev
    }
    
  • every、some、filter

对象的扩展

只有for...in会遍历继承的可枚举的属性,其余的都只会遍历自身可枚举的属性

  • for...in循环:只遍历对象自身的和继承的可枚举的属性。

  • Object.keys():返回对象自身的所有可枚举的属性的键名。

  • JSON.stringify():只串行化对象自身的可枚举的属性。

  • Object.assign(): 只拷贝对象自身的可枚举的属性。

  • Reflect.ownKeys():返回一个数组,包含对象自身所有的键名,包含Symbol和不可枚举的键名

  • 新增关键字super,指向当前对象的原型对象,只有在对象的方法中使用,目前只有对象方法的简写可以让js引擎确认定义的是对象的方法

  • Object.is()

    比较两个值是否严格相等,与===的行为基本一致

    +0 === -0 //true
    NaN === NaN // false
    
    Object.is(+0, -0) // false
    Object.is(NaN, NaN) // true
    
    Object.prototypr.is = function (a, b) {
      if (a === b) {
          return x !== 0 || false
      }
      return a !== a && b !== b
    }
    
  • Object.assign()

    用于合并对象,第一个是目标对象,后面的都是源对象,将源对象自身中所有可枚举属性复制到目标对象

  • Object.keys(),Object.values(),Object.entries()

    方法同数组的方法

Set

类似于数组,但是里面的元素都是唯一的没有重复的值

Set的方法:add、clear、delete、has、keys、values、entries

// add
let set = new Set([1,2,3,4,3,2,1])
set.add(5)
console.log(set) // Set { 1, 2, 3, 4, 5 }

// 添加一个已有的值,则不会添加进去
set.add(1)
console.log(set) // Set { 1, 2, 3, 4, 5 }

// delete
set.delete(3)
console.log(set) // Set { 1, 2, 4, 5 }

// has
set.has(1) // true

// entries
console.log(set.entries()) // SetIterator { [ 1, 1 ],[ 2, 2 ],[ 4, 4 ],[ 5, 5 ] }

// clear
set.clear()
console.log(set) // Set {}
  • 去重

    function distinct(arr1,arr2){
        return [...new Set([...arr1,...arr2])]
    }
    
    let arr = distinct([1,2,3],[2,3,4,5])
    console.log(arr) // [1,2,3,4,5]
    
  • 交集

    function intersect(arr1,arr2) {
      return arr1.filter(item => new Set(arr2).has(item))
    }
    
    console.log(intersect([1,2,3],[2,3,4,5])) // [2,3]
    
  • 差集

    function difference(arr1,arr2){
        return arr1.filter(item => !new Set(arr2).has(item))
    }
    
    console.log(difference([1,2,3],[2,3,4,5])) // [1]
    

Map

类似于对象,但不能有重复的key

Map的方法:set、clear、delete、has、keys、values、entries,还有size属性

const m = new Map();
const o = {p: 'Hello World'};

m.set(o, 'content')
m.get(o) // "content"

m.has(o) // true
m.delete(o) // true
m.has(o) // false

Symbol

Symbol表示独一无二的值,是一个常量,一般用作对象的属性

Symbol.for() 会生成一个唯一表示,它接受一个字符串作为参数,然后搜索有没有以该参数作为名称的 Symbol 值。如果有,就返回这个 Symbol 值,否则就新建一个以该字符串为名称的 Symbol 值,并将其注册到全局

let s1 = Symbol()
let s2 = Symbol()
console.log(s1 === s2) // false

// 因为Symbol生成的是一个独一无二的值,为常量,一般是作为对象的属性
let obj = {
  [s1]:1,
  [s2]:2
}
let s1 = Symbol.for('foo')
let s2 = Symbol.for('foo')
console.log(s1 === s2) // true

// 也可以通过Symbol.keyFor把标识找出来
console.log(Symbol.keyFor(s1)) // foo

Promise

异步编程的一种解决方案,解决了地狱回调的问题,对象的状态不受外界的影响,一旦状态改变就不会在变回来了

  • then()

  • catch:catch可以捕获then中抛出的错误

  • finally:不管是fulfilledrejected都会执行这里面的方法,它的回调函数不接受任何参数

  • all:将多个promise实例包装成一个新的promise实例,所有的都成功才会调用成功的回调,有一个失败就会调用失败的回调

  • race:有一个实例的状态改变,整个的状态就跟着一起改变

  • allSettled:等所有的peomise都返回结果,不敢fulfilled还是rejected,之后整个状态才会结束,且状态都是fulfilled,返回结果是一个数组包对象,每个对象都有status属性,该属性的值只可能是字符串fulfilled或字符串rejectedfulfilled时,对象有value属性,rejected时有reason属性,对应两种状态的返回值

  • any:只要参数实例有一个变成fulfilled状态,包装实例就会变成fulfilled状态;如果所有参数实例都变成rejected状态,包装实例就会变成rejected状态

    // 简易版
    const PENDING = 'pending'
    const RESLOVED = 'resloved'
    const REJECTED = 'rejected'
    
    function MyPromise = function (fn) {
      const that = this
      // 保存状体
      that.state = PENDING
      // 保存reslove、reject传入的值
      that.value = null
      // 保存成功的回调
      that.reslovedCbs = []
      // 保存失败的回调
      that.rejectedCbs = []
      // 传入fn的resolve函数
      function reslove (value) {
        // setTimeout是保证执行的顺序,then传入的函数在后面执行
        setTimeout(() => {
          if (that.state === PENDING) {
            that.state = RESLOVED
            that.value = value
            that.reslovedCbs.map(cb => {
              cb(that.value)
            })
          }
        }, 0)
      }
      // 传入fn的reject函数
      function reject (value) {
        setTimeout(() => {
          if (that.state === PENDING) {
            that.state = REJECTED
            that.value = value
            that.rejectedCbs.map(cb => {
              cb(that.value)
            })
          }
        }, 0)
      }
      // 执行promise的回调函数
      try {
        fn(reslove, reject)
      } catch (e) {
        reject(e)
      }
    }
    // promise的then方法
    MyPromise.prototype.then = function (onReslove, onReject) {
      const that = this
      if (that.state === PENDING) {
        // 将回调函数加入到回调的数组中
        that.reslovedCbs.push(onReslove)
        that.rejectedCbs.push(onReject)
      }
      if (that.state === RESOLVED) {
        onFulfilled(that.value)
      }
      if (that.state === REJECTED) {
        onRejected(that.value)
      }
    }
    

Proxy

  • get(target, propKey, receiver):拦截对象属性的读取,比如proxy.fooproxy['foo']

  • set(target, propKey, value, receiver):拦截对象属性的设置,比如proxy.foo = vproxy['foo'] = v,返回一个布尔值。

  • has(target, propKey):拦截propKey in proxy的操作,返回一个布尔值。

  • deleteProperty(target, propKey):拦截delete proxy[propKey]的操作,返回一个布尔值。

  • ownKeys(target):拦截Object.getOwnPropertyNames(proxy)Object.getOwnPropertySymbols(proxy)Object.keys(proxy)for...in循环,返回一个数组。该方法返回目标对象所有自身的属性的属性名,而Object.keys()的返回结果仅包括目标对象自身的可遍历属性。

  • getOwnPropertyDescriptor(target, propKey):拦截Object.getOwnPropertyDescriptor(proxy, propKey),返回属性的描述对象。

  • defineProperty(target, propKey, propDesc):拦截Object.defineProperty(proxy, propKey, propDesc)Object.defineProperties(proxy, propDescs),返回一个布尔值。

  • preventExtensions(target):拦截Object.preventExtensions(proxy),返回一个布尔值。

  • getPrototypeOf(target):拦截Object.getPrototypeOf(proxy),返回一个对象。

  • isExtensible(target):拦截Object.isExtensible(proxy),返回一个布尔值。

  • setPrototypeOf(target, proto):拦截Object.setPrototypeOf(proxy, proto),返回一个布尔值。如果目标对象是函数,那么还有两种额外操作可以拦截。

  • apply(target, object, args):拦截 Proxy 实例作为函数调用的操作,比如proxy(...args)proxy.call(object, ...args)proxy.apply(...)

  • construct(target, args):拦截 Proxy 实例作为构造函数调用的操作,比如new proxy(...args)

    let dog = {
      name:"小黄",
      firends:[{
        name:"小红"
      }]
    }
    
    // 1.首先new一个Proxy对象
    let proxy = new Proxy(dog, {
        get(target,property){
            console.log('get被监控到了')
            return target[property]
        },
        set(target,property,value){
            console.log('set被监控到了')
            target[property] = value
        }
    })
    
    dog.name = '小红'  // set值时,发现不会打印 'set被监控到了'
    dog.name // get值时,发现不会打印 'get被监控到了'
    
    // proxy相当于是一个壳,代理我们需要监控的数据,也就是我们要通过proxy来访问内部数据才会被监控到
    
    proxy.name = '小红' // 打印输出 'set被监控到了'
    proxy.name // 打印输出 'get被监控到了'
    

注意:

  1. 在 Proxy 代理的情况下,目标对象内部的this关键字会指向 Proxy 代理

  2. 有些原生对象的内部属性,只有通过正确的this才能拿到,所以 Proxy 也无法代理这些原生对象的属性

    const target = new Date();
    const handler = {};
    const proxy = new Proxy(target, handler);
    
    proxy.getDate();
    // TypeError: this is not a Date object.
    // 解决方法
    const target = new Date('2015-01-01');
    const handler = {
      get(target, prop) {
        if (prop === 'getDate') {
          return target.getDate.bind(target);
        }
        return Reflect.get(target, prop);
      }
    };
    const proxy = new Proxy(target, handler);
    
    proxy.getDate() // 1
    

Iterator

遍历器:为各种数据结构(Array、Map、Set、String、函数的 arguments 对象、NodeList 对象)提供统一的访问接口,使数据结构的成员能够按某种次序排序,可以使用for...of循环,默认的 Iterator 接口部署在数据结构的Symbol.iterator属性

对象没有iterator因为,对象的属性没有先后顺序

const obj = {
  [Symbol.iterator] : function () {
    this.count = 0;
    return {
      next () {
        if (count < this.length) {
          return {done: false, value: this[count++]};
        }
        return {done: true, value: undefined};
      }
    };
  }
};

遍历过程:

  1. 创建一个指针对象,指向当前数据结构的起始位子,也就是说遍历器对象本质上就是一个指针对象
  2. 第一次调用指针对象的next方法,可以将指针指向数据结构的第一个成员
  3. 第二次调用指针对象的next方法,指针就指向数据结构的第二个成员
  4. 不断调用指针对象的next方法,直到它指向数据结构的结束位子
  5. 每一次调用next方法,都会返回一个包含valuedone两个属性的对象,value属性是当前成员的值,done属性是一个布尔值,表示遍历是否结束

调用遍历器的方法

  1. for...of,会直接执行遍历器函数
  2. 结构赋值
  3. 扩展运算符
  4. yield*

Generator

Generator函数是一个状态机,封装了多个内部状态,执行它会返回一个遍历器对象

调用 Generator 函数后,该函数并不执行,返回的也不是函数运行结果,而是一个遍历器对象

next方法可以带一个参数,该参数就会被当作上一个yield表达式的返回值

  1. next方法
  2. throw方法,将yield表达式替换成一个throw语句抛出异常
  3. return方法,可以返回给定的值,并且终结遍历 Generator 函数
  4. yield*表达式,用来在一个 Generator 函数里面执行另一个 Generator 函数,不然需要手动遍历
// generator的异步执行
function* gen(){
  var url = 'https://api.github.com/users/github';
  var result = yield fetch(url);
  console.log(result.bio);
}

var g = gen();
var result = g.next();

result.value.then(function(data){
  return data.json();
}).then(function(data){
  g.next(data);
});

thunk函数:将多参数函数替换成一个值接受一个回调函数作为参数的单参数函数,就是return出一个只接受一个回调函数作为参数的函数

/ 正常版本的readFile(多参数版本)
fs.readFile(fileName, callback);

// Thunk版本的readFile(单参数版本)
var Thunk = function (fileName) {
  return function (callback) {
    return fs.readFile(fileName, callback);
  };
};

var readFileThunk = Thunk(fileName);
readFileThunk(callback);
// generator的流程管理
var fs = require('fs');
var thunkify = require('thunkify');
var readFileThunk = thunkify(fs.readFile);

var gen = function* (){
  // yield命令用于将程序的执行权移出 Generator 函数
  var r1 = yield readFileThunk('/etc/fstab');
  console.log(r1.toString());
  var r2 = yield readFileThunk('/etc/shells');
  console.log(r2.toString());
};

var g = gen();
// Thunk 函数又将执行权通过回调函数交还给 Generator 函数
var r1 = g.next();
r1.value(function (err, data) {
  if (err) throw err;
  var r2 = g.next(data);
  r2.value(function (err, data) {
    if (err) throw err;
    g.next(data);
  });
});

generator的自动执行器交回执行权的方法

  1. 回调函数,将异步操作包装成Thunk函数,在回调函数里面交回执行权

  2. Promise对象,在then方法里面交回执行权

    // 基于Thunk实现自动执行器,yield后面必须是Thunk函数
    function run(fn) {
      var gen = fn();
    
      function next(err, data) {
        var result = gen.next(data);
        if (result.done) return result.value;
        result.value(next);
      }
    
      next();
    }
    
    function* g() {
      // ...
    }
    
    run(g);
    
    // 基于Promise实现自动执行器,yield后面必须是返回Promise对象
    function run (gen) {
      var g = gen()
      
      function next (data) {
        var result = g.next(data)
        if (result.done) return result.value
        result.value.then(function (data) {
          next(data)
        })
      }
      next()
    }
    

async

就是Generator函数的语法糖

优点:

  1. 内置了执行器
  2. 更好的语义
  3. 更广的适用性,await后面可以不是Promise对象,(async会自动将其转为Promise对象)
  4. 返回值是Promise,可以使用then方法,不过必须等所有的awaitpromise都执行完后才会执行then

缺点:

  1. 必须等前面的await执行完后才会执行下一个await
  2. 任何一个await语句后面的 Promise 对象变为reject状态,那么整个async函数都会中断执行,可以放在try{} catch() {}中或者给await后面的promise添加catch方法来解决这个问题

class

只是一个语法糖,constructor方法就是es5中的构造函数,方法也都定义在了类的原型prototype

不同点:

  1. class内部定义的方法都是不可枚举的,需要Object.getOwnPropertyNames方法才能获取
  2. 类必须用new调用,否则会报错
  3. 没有constructor,会自动创建一个constructor函数

注意点:

  1. this指向,将实例中的方法单独拿出来的时候执行时,this指向问题

    class Logger {
      printName(name = 'there') {
        this.print(`Hello ${name}`);
      }
    
      print(text) {
        console.log(text);
      }
    }
    
    const logger = new Logger();
    const { printName } = logger;
    printName(); // TypeError: Cannot read property 'print' of undefined
    
    // 解决,用bind改变this的指向,或者用箭头函数
    class Logger {
      constructor() {
        this.printName = this.printName.bind(this);
        this.getThis = () => this;
      }
    }
    
  2. 静态方法

    在类中的方法前面加上static表明这个方法是静态的,不会被实例继承,可以直接通过类调用,静态方法中this指的是类,而不是实例,父类的静态方法可以被子类继承,静态方法也是可以从super对象上调用的

    class Foo {
      static classMethod() {
        return 'hello';
      }
    }
    
    class Bar extends Foo {
      static classMethod() {
        return super.classMethod() + ', too';
      }
    }
    
    Bar.classMethod() // "hello, too"
    
  3. 静态属性

    是类本身的属性,不是实例上的属性

    class Foo {
    }
    
    Foo.prop = 1;
    Foo.prop // 1
    
  4. 实例属性除了定义在constructor()方法里面的this上面,也可以定义在类的最顶层

    class IncreasingCounter {
      _count = 0; // 实例属性
    
      increment() {
        this._count++;
      }
    }
    
  5. new.target属性

    返回new命令作用于的那个构造函数,如果构造函数不是通过new命令或Reflect.construct()调用的,new.target会返回undefined,因此这个属性可以用来确定构造函数是怎么调用的

    注意子类继承父类时,new.target会返回子类

    class Rectangle {
      constructor(length, width) {
        console.log(new.target === Rectangle);
        // ...
      }
    }
    
    class Square extends Rectangle {
      constructor(length) {
        super(length, width);
      }
    }
    
    var obj = new Square(3); // 输出 false
    
  6. 子类必须在constructor方法中先调用super方法,否则新建实例时会报错

    这是因为子类自己的this对象,必须先通过父类的构造函数完成塑造,得到与父类同样的实例属性和方法,然后再对其进行加工,加上子类自己的实例属性和方法。如果不调用super方法,子类就得不到this对象

  7. 父类的静态方法,也会被子类继承

  8. super

    super作为函数调用时,代表父类的构造函数,super虽然代表了父类A的构造函数,但是返回的是子类B的实例

    class A {
      constructor() {
        console.log(new.target.name);
      }
    }
    class B extends A {
      constructor() {
        super();
      }
    }
    new A() // A
    new B() // B
    

    super作为对象时,在普通方法中,指向父类的原型对象;在静态方法中,指向父类,在子类普通方法中通过super调用父类的方法时,方法内部的this指向当前的子类实例

    class A {
      constructor() {
        this.x = 1;
      }
      print() {
        console.log(this.x);
      }
    }
    
    class B extends A {
      constructor() {
        super();
        this.x = 2;
      }
      m() {
        super.print();
      }
    }
    
    let b = new B();
    b.m() // 2
    
  9. Prototype、__proto__

    子类的__proto__属性,表示构造函数的继承,总是指向父类

    子类prototype属性的__proto__属性,表示方法的继承,总是指向父类的prototype属性

    class A {
    }
    
    class B {
    }
    
    // B 的实例继承 A 的实例
    Object.setPrototypeOf(B.prototype, A.prototype);
    // 类似 B.prototype.__proto__ = A.prototype;
    
    // B 继承 A 的静态属性
    Object.setPrototypeOf(B, A);
    // 类似 B.__proto__ = A;
    
    const b = new B();
    
  10. minix模式的实现

    将多个类混入另一个类

    function mix(...mixins) {
      class Mix {
        constructor() {
          for (let mixin of mixins) {
            copyProperties(this, new mixin()); // 拷贝实例属性
          }
        }
      }
    
      for (let mixin of mixins) {
        copyProperties(Mix, mixin); // 拷贝静态属性
        copyProperties(Mix.prototype, mixin.prototype); // 拷贝原型属性
      }
    
      return Mix;
    }
    
    function copyProperties(target, source) {
      for (let key of Reflect.ownKeys(source)) {
        if ( key !== 'constructor'
          && key !== 'prototype'
          && key !== 'name'
        ) {
          let desc = Object.getOwnPropertyDescriptor(source, key);
          Object.defineProperty(target, key, desc);
        }
      }
    }
    

Decorator

装饰器只能用于类和类的方法,如果同一个方法有多个装饰器,先从外到内进入,然后由内向外执行,装饰器是在编译的时候执行的,不是在运行的时候执行

需要对一些方法添加某些操作的时候可以使用,比如打印参数之类的

  • 装饰类

    @annotation
    class MyClass { }
    // target就是类本身
    function annotation(target) {
       target.annotated = true;
    }
    
    // 装饰类的原理
    @decorator
    class A {}
    
    // 等同于
    
    class A {}
    A = decorator(A) || A;
    
  • 装饰方法或属性

    class MyClass {
      @readonly
      method() { }
    }
    // 第一个参数类的原型对象(实例还没生成),第二个参数属性名,第三个参数该属性的描述对象
    function readonly(target, name, descriptor) {
      descriptor.writable = false;
      return descriptor;
    }
    
    // 1. 先将 Object.getOwnPropertyDescriptor() 返回的属性描述符对象做了一份拷贝
    var desc = {};
    Object["ke" + "ys"](descriptor).forEach(function(key) {
    	desc[key] = descriptor[key];
    });
    desc.enumerable = !!desc.enumerable;
    desc.configurable = !!desc.configurable;
    
    // 如果没有 value 属性或者没有 initializer 属性,表明是 getter 和 setter
    if ("value" in desc || desc.initializer) {
    	desc.writable = true;
    }
    
    // 2. 多个 decorators,reverse方法使装饰器由内向外执行
    desc = decorators
    	.slice()
    	.reverse()
    	.reduce(function(desc, decorator) {
    		return decorator(target, property, desc) || desc;
    	}, desc);
    
    // 3. 使用 Object.defineProperty() 设置处理完的属性描述对象
    if (desc.initializer === void 0) {
    	Object["define" + "Property"](target, property, desc);
    	desc = null;
    }
    return desc;
    

Mixin

可以实现对象继承的一种替代方案

// 在装饰器里面把要继承的方法放入类的原型中
export function mixins(...list) {
  return function (target) {
    Object.assign(target.prototype, ...list);
  };
}

import { mixins } from './mixins';

const Foo = {
  foo() { console.log('foo') }
};

@mixins(Foo)
class MyClass {}

let obj = new MyClass();
obj.foo() // "foo"
// 通过类的继承实现Minix
let MyMixin = (superclass) => class extends superclass {
  foo() {
    console.log('foo from MyMixin');
  }
};

class MyClass extends MyMixin(MyBaseClass) {
  /* ... */
}

let c = new MyClass();
c.foo(); // "foo from MyMixin"

css


盒子模型

标准盒子模型:宽度=内容的宽度(content)+ padding + border + margin 低版本IE盒子模型:宽度=内容宽度(content + padding + border)+ margin

可以通过box-sizing属性来设置盒子模型的解析方式

context-box:W3C的标准盒子模型,设置元素的 height/width 属性指的是content部分的高/宽 border-box:IE传统盒子模型。设置元素的height/width属性指的是border + padding + content部分的高/宽

BFC

块级格式化上下文,是一个独立的渲染区域,用于决定块盒子的布局及浮动相互影响范围的一个区域

BFC创建的一个方法

  1. 浮动,float不为none的元素
  2. 定位,position为absolute或fixed
  3. 行内块,display为inline-block
  4. 表格单元格,display为table-cell
  5. overflow,overflow不为visible的元素
  6. 弹性盒,display为flex或inline-flex的元素

BFC范围 一个BFC包含它所有的子元素,但不包含创建了新的BFC的子元素的内部元素,即一个元素不能同时存在于两个BFC中,BFC就是想把在不同块之间的元素隔离开来,互不影响,所以一个元素不能同时在两个BFC中存在

BFC效果

  1. BFC内部的盒会在垂直方向上一个接一个的排列
  2. 处于同一个BFC中的元素相互影响,垂直方向上的距离有margin决定(两个相邻的元素垂直方向的margin可能重叠)
  3. 每个元素的margin box的左边与容器块border box的左边行接触,即使存在浮动也是如此
  4. BFC是页面上的一个隔离的独立容器,容器里面的子元素不会影响到外面的元素,反之亦然
  5. 计算BFC高度的时候,考虑BFC所包含的所有的元素,包括浮动的元素
  6. 浮动盒区域不叠加到BFC上

垂直居中

  • 居中元素定宽高适用

    • absolute + 负margin

      .wp {
          position: relative;
      }
      .box {
          position: absolute;;
          top: 50%;
          left: 50%;
          margin-left: -50px;
          margin-top: -50px;
      }
      
    • absolute + margin auto

      /* 定位代码 */
      .wp {
          position: relative;
      }
      .box {
          position: absolute;;
          top: 0;
          left: 0;
          right: 0;
          bottom: 0;
          margin: auto;
      }
      
    • absolute + calc

  • 居中元素不定宽高

    • absolute + transform

      /* 定位代码 */
      .wp {
          position: relative;
      }
      .box {
          position: absolute;
          top: 50%;
          left: 50%;
          transform: translate(-50%, -50%);
      }
      
    • lineheight

      /* 定位代码 */
      .wp {
          line-height: 300px;
          text-align: center;
          font-size: 0px;
      }
      .box {
          font-size: 16px;
          display: inline-block;
          vertical-align: middle;
          line-height: initial;
          text-align: left; /* 修正文字 */
      }
      
    • css-table

      .wp {
          display: table-cell;
          text-align: center;
          vertical-align: middle;
      }
      .box {
          display: inline-block;
      }
      
    • flex

      .wp {
          display: flex;
          justify-content: center;
          align-items: center;
      }
      
    • grid

      .wp {
          display: grid;
      }
      .box {
          align-self: center;
          justify-self: center;
      }
      

浏览器是怎样解析CSS选择器的

CSS选择器的解析是从右向左解析的。若从左向右的匹配,发现不符合规则,需要进行回溯,会损失很多性能。若从右向左匹配,先找到所有的最右节点,对于每一个节点,向上寻找其父节点直到找到根元素或满足条件的匹配规则,则结束这个分支的遍历。两种匹配规则的性能差别很大,是因为从右向左的匹配在第一步就筛选掉了大量的不符合条件的最右节点(叶子节点),而从左向右的匹配规则的性能都浪费在了失败的查找上面。 而在 CSS 解析完毕后,需要将解析的结果与 DOM Tree 的内容一起进行分析建立一棵 Render Tree,最终用来进行绘图。在建立 Render Tree 时(WebKit 中的「Attachment」过程),浏览器就要为每个 DOM Tree 中的元素根据 CSS 的解析结果(Style Rules)来确定生成怎样的 Render Tree。

浏览器


浏览器缓存

DNS

域名于IP地址相互映射的一个分布式数据库

DNS解析

  1. 首先搜索浏览器自身的DNS缓存,如果存在,则域名解析到此完成。
  2. 如果浏览器自身的缓存里面没有找到对应的条目,那么会尝试读取操作系统的hosts文件看是否存在对应的映射关系,如果存在,则域名解析到此完成。
  3. 如果本地hosts文件不存在映射关系,则查找本地DNS服务器(ISP服务器,或者自己手动设置的DNS服务器),如果存在,域名到此解析完成。
  4. 如果本地DNS服务器还没找到的话,它就会向根服务器发出请求,进行查询。

CDN

存放静态资源的服务器

当浏览器向CDN节点请求数据时,CDN节点会判断缓存数据是否过期,若缓存数据并没有过期,则直接将缓存数据返回给客户端;否则,CDN节点就会向服务器发出回源请求,从服务器拉取最新数据,更新本地缓存,并将最新数据返回给客户端

CDN优点:

  1. CDN节点解决了跨运营商和跨地域访问的问题,访问延时大大降低
  2. 减轻了服务器的压力,大部分请求在CDN边缘节点完成,CDN起到了分流作用,减轻了源服务器的负载

HTTP缓存

  • 强缓存: 会先根据本地缓存资源的 header 中的信息判断是否命中强缓存

    • expires:该字段是 http1.0 时的规范,它的值为一个绝对时间的 GMT 格式的时间字符串,比如 Expires:Mon,18 Oct 2066 23:59:59 GMT。这个时间代表着这个资源的失效时间,在此时间之前,即命中缓存。这种方式有一个明显的缺点,由于失效时间是一个绝对时间,所以当服务器与客户端时间偏差较大时,就会导致缓存混乱
    • catch-control:是 http1.1 时出现的 header 信息,主要是利用该字段的 max-age 值来进行判断,它是一个相对时间
      1. no-cache:需要进行协商缓存,发送请求到服务器确认是否使用缓存
      2. no-store:禁止使用缓存,每一次都要重新请求数据
      3. public:可以被所有的用户缓存,包括终端用户和 CDN 等中间代理服务器
      4. private:只能被终端用户的浏览器缓存,不允许 CDN 等中继缓存服务器对其缓存
  • 协商缓存:当强缓存没有命中的时候,浏览器会发送一个请求到服务器,服务器根据 header 中的部分信息来判断是否命中缓存。如果命中,则返回 304 ,告诉浏览器资源未更新,可使用本地的缓存

    • Last-Modify/If-Modify-Since

      浏览器第一次请求一个资源的时候,服务器返回的 header 中会加上 Last-Modify,Last-modify 是一个时间标识该资源的最后修改时间

      当浏览器再次请求该资源时,request 的请求头中会包含 If-Modify-Since,该值为缓存之前返回的 Last-Modify。服务器收到 If-Modify-Since 后,根据资源的最后修改时间判断是否命中缓存

      如果命中缓存,则返回 304,并且不会返回资源内容,并且不会返回 Last-Modify

      缺点:短时间内资源发生了改变,Last-Modified 并不会发生变化,资源周期性变化的时候,当资源修改回来,缓存不可使用

    • ETag/If-None-Match

      Etag/If-None-Match 返回的是一个校验码。ETag 可以保证每一个资源是唯一的,资源变化都会导致 ETag 变化。服务器根据浏览器上送的 If-None-Match 值来判断是否命中缓存

浏览器渲染

js是单线程的,浏览器内核是多线程的

  1. GUI渲染线程:解析HTML、css构建DOM树布局绘制,与js引擎互斥

  2. js引擎线程:执行任务队列中等执行的js脚本

  3. 定时器触发线程:setTimeout、setInterval,主线程遇到定时器时,将定时器交给该线程,计数完将待执行的事件加入到任务队列的尾部,等待js引擎线程执行

  4. 事件触发线程:将准备好的事件交给js引擎线程执行,setTimeout计时结束通过该线程将回调任务加入到定时器触发线程

  5. 异步HTTP请求线程:负责执行异步请求的函数线程(Promise、ajax),主线程中遇到异步请求,交给该线程处理,状态变更,事件触发线程将回调加入到任务队列的尾部,等待js线程执行

渲染过程:

  1. 先从http请求获取HTML的原始字节数据,通过编码转化成文件指定的编码字符
  2. 令牌化:根据HTML规范将字符串转化为各种令牌(每个令牌都有特殊的含义和规则),令牌记录的标签的开始和结束
  3. 生成节点:每个令牌被转换成定义其属性和规则的对象,就是节点对象,生成了DOM树
  4. 布局:根据盒模型和BFC计算每个DOM元素在屏幕上显示的大小和位子
  5. 绘制:像素的填充,一个DOM的可视效果,在多个渲染层上同时进行
  6. 渲染层合并:浏览器按照一定的规则合并渲染层,渲染层是用来实现层叠上下文,保证元素正确合成,展示元素的重叠和半透明的效果

渲染层的创建:

  1. 形成层叠上下文的元素,会创建新的渲染层
  2. 有明确定位的,透明的,css滤镜,transform属性
  3. overflow不为visible
  4. 不需要绘制的节点,比如空的DIV

渲染层提升为合成层:

  1. 在运行中的animation和transition中有opacity、transform、filiter
  2. will-change为opacity、transform、top、left
  3. 有合成层后代本身有transform、opacity、fixed的

提升合成层的优点:

  1. 合成层的位图会交由GPU处理,比cpu快,GPU专门处理图像显示,做了很多的优化
  2. 合成层重绘只有重绘这一层,不影响其他的层
  3. 对于transform、opacity,只需要一个合成图层的时间
  4. 可做的优化:
    1. 把动画效果中的元素提升为合成层,(will-change:transform)
    2. 用css3来实现一些动画效果
    3. 减少重绘区域,一些固定不变的区域可以提升为合成层,比如页面的头部
  5. 注意:
    1. 开启GPU加速后会建立新的图层,新的图层就需要一定的内存空间,而且图层在合成时,图层越多耗费的时间肯定也是越多的,所以疯狂的开启GPU加速,不但不能解决性能问题,反而可能会带来性能问题

重绘和回流

  1. 回流:当元素的内容结构、大小、位子、尺寸发生变化的时候,重新计算样式和渲染书
  2. 重绘:只是颜色上的变化,不影响布局的会重绘,回流一定包含重绘

浏览器事件循环

宏任务:setTimeout、setInterval、script 微任务:new Promise().then()、mutationObserver

事件循环:一开始执行栈为空,宏任务只有一个script标签,这个宏任务创建一个执行上下文,包括词法环境、变量环境,之后被压入执行栈开始执行中,过程中会生成新的宏任务和微任务,等到同步的代码执行完成后被移除执行栈(执行栈是一个后进先出的结构),在执行微任务队列,直到微任务全部执行完成,队列为空之后,执行页面的重新渲染,在检查是否有宏任务需要执行,有的话继续执行宏任务,

node Event Loop

  1. timers 阶段:这个阶段执行timer(setTimeout、setInterval)的回调,并且是由 poll 阶段控制的

  2. I/O callbacks 阶段:处理一些上一轮循环中的少数未执行的 I/O 回调

  3. idle, prepare 阶段:仅node内部使用

  4. poll 阶段:获取新的I/O事件

    如果 poll 队列不为空,会遍历回调队列并同步执行,直到队列为空或者达到系统限制

    如果 poll 队列为空时,会有两件事发生

    • 如果有 setImmediate 回调需要执行,poll 阶段会停止并且进入到 check 阶段执行回调
    • 如果没有 setImmediate 回调需要执行,会等待回调被加入到队列中并立即执行回调,这里同样会有个超时时间设置防止一直等待下去
  5. check 阶段:执行 setImmediate() 的回调

  6. close callbacks 阶段:执行 socket 的 close 事件回调

注:process.nextTick()是独立于 Event Loop 之外的,它有一个自己的队列,当每个阶段完成后,如果存在 nextTick 队列,就会清空队列中的所有回调函数,并且优先于其他 microtask 执行

在node中微任务会在事件循环的各个阶段之间执行,就是每个阶段执行完毕后,都会去查看微任务的队列是否有任务,如果有就执行微任务

安全知识防范

  • XSS

    XSS攻击就是把其他代码注入到你的网页中,一般是在传参中这段代码加进去比如输入框中输入,前端吧这段输入传给后端,后端就会保存包数据库中

    防御:

    1. 转义字符:对于用户的输入永远不信任,对于引号、尖括号、斜杠进行转义

    2. csp:建立白名单,告诉浏览器哪些外部资源可以加载

      设置 HTTP Header 中的 Content-Security-Policy

      设置 meta 标签的方式 <meta http-equiv="Content-Security-Policy">

      如:Content-Security-Policy: default-src ‘self’只允许加载本站资源

  • CSRF:跨站请求伪造

    攻击者构造出一个后端请求地址,进行请求,如果用户是登录状态,后端就会执行相印的逻辑

    防御:

    1. 不让第三方网站访问用户的cookie,可以对 Cookie 设置 SameSite 属性。该属性表示 Cookie 不随着跨域请求发送
    2. 服务器下发一个随机的token,每次请求时将token带上,服务器去验证这个token是否有效

http


http跨域

跨域是浏览器同源策略的一种限制,当协议、端口、域名任意一个不同时,就不能向目标地址发送请求,这是为了用户信息的安全考虑

解决方法:

  1. jsonp:因为script标签没有同源策略的限制,所以可以解决跨域的问题,在script的src后面加上一个回调函数的函数名,请求时,后端获取回调函数的函数名,将数据通过回调函数传参

  2. postmessage:通过postmessage(data, origin)来传参,origin是协议+主机+端口(*表示任意的,/表示同源),通过window.messge来接受参数

  3. nginx:nginx做一个代理,前端不直接访问服务器,通过访问nginx来访问服务器,这样就对服务器多了一层保护,在nginx这边就可以去拦截掉一些危险或没有权限的请求,nginx的优点就是反向代理和负载均衡

    反向代理:就是代理服务器和真正server服务器可以直接互相访问,属于一个LAN(服务器内网);代理对用户是透明的,即无感知。不论加不加这个反向代理,用户都是通过相同的请求进行的,且不需要任何额外的操作;代理服务器通过代理内部服务器接受域外客户端的请求,并将请求发送到对应的内部服务器上

    负载均衡:内容被部署在若干台服务器上,可以把这些机子看成一个集群,那么Nginx可以将接收到的客户端请求“均匀地”分配到这个集群中所有的服务器上(内部模块提供了多种负载均衡算法)

    nginx作用:

    1. 适配PC与移动环境,可以判断请求是来自移动端还是pc端,然后重定向到相应的页面中
    2. 页面内容的修改,向页面底部或者顶部插入额外的css或js文件,从而修改页面的内容
  4. node的中间件的代理,通过代理服务器来实现数据的转发

http请求头部及含义

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

http请求过程

OSI模型:

  1. 应用层:http协议
  2. 表示层:加密、解密等
  3. 会话层:通信连接、维持会话等
  4. 传输层:TCP、UDP
  5. 网络层:IP
  6. 链路层:网络特有的链路接口
  7. 物理层:网络物理硬件

TCP是面向连接的协议,连接之前需要3次握手连接会话

  1. 客户端到服务器,发起连接请求
  2. 服务器响应客户端
  3. 客户端连接,开始传输

http是超文本传输协议,构建在TCP之上,属于应用层协议 https服务就是工作在TLS/SSL上的http,更加的安全,TLS/SSL是一个公钥和私钥的结构,它是一个非对称的结构,每个客户端和服务器都有自己的公私钥,公钥用来传输要加密的数据,私钥用来解密接受到的数据,公钥加密的数据只能通过私钥才能解密,所以在进行安全传输之前客户端和服务端要互换公钥,客户端发送数据时需要用客户端的公钥进行加密,服务端发送数据时需要用客户端的公钥进行加密,这样完成加密解密的过程

http1和http2

  1. http1在同一域名下有请求数量的限制,http2引入了多路复用的技术,这个技术可以只通过一个 TCP 连接就可以传输所有的请求数据,解决了请求数量限制的问题
  2. http1通过文本的方式传输数据,在http2中所有的传输的数据被分割,并采用二进制格式编码并传输
  3. http1中使用文本的形式传输header,在http2中对header进行编码压缩,减小header的大小

vue(vue3新特性)


diff算法

img

diff的过程就是调用名为patch的函数,比较新旧节点,根据比较结果更新D真实DOM

  1. 判断是不是相同节点

  2. patch:如果不是相同节点,就把新节点加到父节点中,删除旧节点

  3. patchVnode:如果是相同节点(key,tag,isComment,节点上的属性是否相同),

  4. 如果老节点有子节点而新节点没有子节点,就移除老的子节点

  5. 如果老节点没有子节点,新节点有子节点,就把新节点的子节点加入到父节点中

  6. 如果新旧节点都只有文本节点且不相同,那么直接替换文本节点,因为文本节点不会再有子节点了

  7. updateChildren:如果两者都有子节点,就执行updateChildren函数比较子节点,这是diff的核心,diff是逐层比较的

  8. oldChvCh各有两个头尾的变量StartIdxEndIdx,它们的2个变量相互比较,一共有4种比较方式。如果4种比较都没匹配,如果设置了key,就会用key进行比较,在比较的过程中,变量会往中间靠,一旦StartIdx>EndIdx表明oldChvCh至少有一个已经遍历完了,就会结束比较

    img

  9. oldS > oldE表示oldCh先遍历完,那么就将多余的vCh根据index添加到dom中去(如上图)

  10. S > E表示vCh先遍历完,那么就在真实dom中将区间为[oldS, oldE]的多余节点删掉

vue异步更新队列

只要观察到数据变化,Vue 将开启一个队列,并缓冲在同一事件循环中发生的所有数据改变。如果同一个 watcher 被多次触发,只会被推入到队列中一次。这种在缓冲时去除重复数据对于避免不必要的计算和 DOM 操作上非常重要。然后,在下一个的事件循环“tick”中,Vue 刷新队列并执行实际 (已去重的) 工作。Vue 在2.6内部尝试对异步队列优先使用Promise,在Promise不存在的情况下使用MutationObserver,这两个方法都会在microtask中执行,会比setTimeout更早执行,所以优先使用。 如果上述两种方法都不支持的环境则会使用setTimeout

  1. 遍历属性为其增加get,set方法,在get方法中会收集依赖(dep.subs.push(watcher)),而set方法则会调用dep的notify方法,此方法的作用是通知subs中的所有的watcher并调用watcher的update方法,我们可以将此理解为设计模式中的发布与订阅
  2. 默认情况下update方法被调用后会触发queueWatcher函数,此函数的主要功能就是将watcher实例本身加入一个队列中(queue.push(watcher)),然后调用nextTick(flushSchedulerQueue)
  3. flushSchedulerQueue是一个函数,目的是调用queue中所有watcher的watcher.run方法,而run方法被调用后接下来的操作就是通过新的虚拟dom与老的虚拟dom做diff算法后生成新的真实dom
  4. 只是此时我们flushSchedulerQueue并没有执行,第二步的最终做的只是将flushSchedulerQueue又放进一个callbacks队列中(callbacks.push(flushSchedulerQueue)),然后异步的将callbacks遍历并执行(此为异步更新队列)

在2.5的版本中nextTick 的实现是 microTimerFuncmacroTimerFunc 组合实现的,next-tick 还对外暴露了两个函数:nextTick 以及 withMacroTask,如 v-on 绑定的事件回调函数的处理,会强制走 macro task,会出现问题,DOM操作与执行时间间隔太长

在2.6中利用最典型的两个微任务,promise.then 以及MutationObserver,并添加 setImmediatesetTimeout,作为降级方案

vue执行顺序

dep.notify() -> watcher.update()将方法放入到异步队列中 -> 异步队列中异步执行callbacks的watcher.run() -> 执行patch()方法,判断新旧DOM节点是否相同 -> 相同则执行patchVnode()判断是否有子节点 -> 都有子节点则执行updateChildren去比较子节点用diff算法 -> 跟新DOM节点

数据结构和算法


冒泡排序

img

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

选择排序

img

function sort (arr) {
  let len = arr.length
  let minIndex, temp
  for(let i = 0;i < len - 1; i++) {
    minIndex = i
    for(j = i + 1; j < len; j++) {
      if (arr[j] < arr[minIndex]) {
        minIndex = j
      }
    }
    temp = arr[i]
    arr[i] = arr[minIndex]
    arr[minIndex] = temp
  }
  return arr
}

插入排序

img

function sort (arr) {
  let len = arr.length
  let  preIndex, current
  for(let i = 1; i < len; i++) {
    preIndex = i - 1
    current = arr[i]
    while(preIndex >= 0 && arr[preIndex] > current) {
      arr[preIndex+1] = arr[preIndex]
      preIndex--
    }
    arr[preIndex+1] = current
  }
  return arr
}

希尔排序

img

function sort (arr) {
  let len = arr.length
  let temp, gap = 1
  while (gap < len / 3) {
    // 动态定义间隔序列
    gap = gap * 3 + 1
  }
  
  for (gap; gap > 0; gap = Math.floor(gap / 3)) {
    for (let i = gap; i < len; i++) {
      temp = arr[i]
      let j = i - gap
      for (; j >= 0 && arr[j] > temp; j -= gap) {
        arr[j + gap] = arr[j]
      }
      arr[j + gap] = temp
    }
  } 
  return arr
}

归并排序

img

function mergeSort (arr) {
  if (arr.length < 2) {
    return arr
  }
  const mid = Math.floor(arr.length/2)
  const left = arr.slice(0, mid)
  const right = arr.slice(mid)
  return merge(mergeSort(left), mergeSort(right))
}

function merge (left, right) {
  const temp = []
  while (left.length && right.length) {
    if (left[0] < right[0]) {
      temp.push(left.shift())
    } else {
      temp.push(right.shift())
    }
  }
  // 将最后一位也添加到temp中
  while (left.length) {
    temp.push(left.shift())
  }
  while (right.length) {
    temp.push(right.shift())
  }
  return temp
}

快速排序

img

function quickSort(array, start, end) {
  if (end - start < 1) {
    return;
  }
  const target = array[start];
  let l = start;
  let r = end;
  while (l < r) {
    while (l < r && array[r] >= target) {
      r--;
    }
    array[l] = array[r];
    while (l < r && array[l] < target) {
      l++;
    }
    array[r] = array[l];
  }
  array[l] = target;
  quickSort(array, start, l - 1);
  quickSort(array, l + 1, end);
  return array;
}

ts


数据类型

  1. number、string、boolean、undefined、null、object、数组、元祖、枚举、any、void、never

  2. 元祖 Tuple:特殊的数组,已经预定义了数组的长度和数组元素的数据类型

    const messyArray = [' something', 2, true, undefined, null];
    const tuple: [number, string, string] = [24, "Indrek" , "Lasn"]
    
  3. 枚举 enum:将一组数值赋予友好的名字,默认是从0开始,也可以手动为1开始

  4. void:表示不是任何值,一般用在函数的返回值上,如函数没有return,就用void

  5. any:表示任意值,当不知道数据是什么类型的时候可以用any,就是什么类型都可以,但是慎用,不然就失去了ts的意义

  6. never:就是用在无法到达的终点,一般用在throw new Error()中,或者while(true){}这种死循环中

ts高级应用

  1. 断言:手动指定一个值的类型,用尖括号或as表示,一般在联合类型中使用的比较多,有时候我们需要在还不知道类型的情况下就去使用联合类型中的某种类型的属性或方法,这个时候用断言就不会报错

    function getLength(something: string | number): number {
        if ((<string>something).length) {
            return (<string>something).length;
        } else {
            return something.toString().length;
        }
    }
    
  2. 泛型:可以进行类型的计算,类型的使用范围更广,支持组件或方法的可复用性,它的数据类型是灵活可变的

    function gen_func1<T>(arg: T): T {
        return arg;
    }
    // 或者
    let gen_func2: <T>(arg: T) => T = function (arg) {
        return arg;
    }
    
    gen_func1<string>('Hello world');
    gen_func2('Hello world'); 
    // 第二种调用方式可省略类型参数,因为编译器会根据传入参数来自动识别对应的类型
    
  3. 接口(InterFace)和自定义类型(type)的区别

    相同点:

    1. 都可以用来描述一个函数或对象

      interface User {
        name: string
        age: number
      }
      
      type User = {
        name: string
        age: number
      };
      
      interface SetUser {
        (name: string, age: number): void;
      }
      type SetUser = (name: string, age: number): void;
      
    2. 都允许扩展

      interface Name { 
        name: string; 
      }
      interface User extends Name { 
        age: number; 
      }
      
      type Name = { 
        name: string; 
      }
      type User = Name & { age: number  };
      
      type Name = { 
        name: string; 
      }
      interface User extends Name { 
        age: number; 
      }
      
      interface Name { 
        name: string; 
      }
      type User = Name & { 
        age: number; 
      }
      

    不同点:

    1. type可以声明类型别名、联合类型、元祖等,表达功能更加的强大

    2. type语句中还可以使用typeof来获取实例的类型来进行赋值

    3. interface可以进行声明合并,同名的interface就自动合并,可以设置可选属性,只读属性,只能表示object/class/function的类型

      interface Person {
        readonly name: string;  // 只读属性
        age?: number;  // 可选属性
        gender?: number;
      }
      // 如果声明两次会将两次自动进行合并
      
  4. 实现(implements)和继承(extends)区别

    implements能够用它来明确的强制一个类去符合某种契约

    interface IDeveloper {
       name: string;
       age?: number;
    }
    // OK
    class dev implements IDeveloper {
        name = 'Alex';
        age = 20;
    }
    // OK
    class dev2 implements IDeveloper {
        name = 'Alex';
    }
    // Error
    class dev3 implements IDeveloper {
        name = 'Alex';
        age = '9';
    }
    

    extends是继承父类,可以混合使用

    class A extends B implements C,D,E
    
  5. 声明文件(declare)和命名空间(namespace)

    declare当使用第三方库的时候,我们需哟啊引入她的声明,才能获得对应的代码补全

    namespace就是内部模块也成为命名空间

  6. 访问修饰符,private、pubic、protected

    1. 默认为public

    2. private就不能在声明她的外部使用,相当于时私有属性

    3. protected和private很像,但是protected成员在派生类中可以使用

      class Animal {
          protected name: string;
          constructor(theName: string) {
              this.name = theName;
          }
      }
      
      class Rhino extends Animal {
          constructor() {
              super('Rhino');
         }         
         getName() {
             console.log(this.name) //此处的name就是Animal类中的name
         }
      } 
      
  7. 可选参数(?:)和非空断言操作符(!.)

    function buildName(firstName: string, lastName?: string) {
        return firstName + ' ' + lastName
    }
    
    // 错误演示
    buildName("firstName", "lastName", "lastName")
    // 正确演示
    buildName("firstName")
    // 正确演示
    buildName("firstName", "lastName")
    
    // 非空断言的的作用是断言某个变量不会是null或undefined
    let s = e!.name;  // 断言e是非空并访问name属性
    
  8. Vue-property-decorator和vuex-class提供的装饰器

    @Prop、@PropSync、@Provide、@Model、@Watch、@Inject、@Emit、@Component

    @State、@Getter、@Action、@Mutation

    // 正如你所看到的,我们在生命周期 列表那都添加private XXXX方法,因为这不应该公开给其他组件。
    
    // 而不对method做私有约束的原因是,可能会用到@Emit来向父组件传递信息
    import { Component, Vue, Prop } from 'vue-property-decorator';
    import { State, Getter } from 'vuex-class';
    import { count, name } from '@/person'
    import { componentA, componentB } from '@/components';
    
    @Component({
        components:{ componentA, componentB},
    })
    export default class HelloWorld extends Vue{
    	@Prop(Number) readonly propA!: number | undefined
        @Prop({ default: 'default value' }) readonly propB!: string
        @Prop([String, Boolean]) readonly propC!: string | boolean | undefined
      
      // 原data
      message = 'Hello'
      
      // 计算属性
    	private get reversedMessage (): string[] {
      	return this.message.split('').reverse().join('')
      }
      // Vuex 数据
      @State((state: IRootState) => state . booking. currentStep) step!: number
    	@Getter( 'person/name') name!: name
      
      // method
      public changeMessage (): void {
        this.message = 'Good bye'
      },
      public getName(): string {
        let storeName = name
        return storeName
      }
    	// 生命周期
      private created ():void { },
      private mounted ():void { },
      private updated ():void { },
      private destroyed ():void { }
    }
    
  9. keyof

    // set
    type Size = 'small' | 'default' | 'big' | 'large';
    // map
    interface IA {
        a: string
        b: number
    }
    
    // map => set
    type IAKeys = keyof IA;    // 'a' | 'b'
    type IAValues = IA[keyof IA];    // string | number
    
    // set => map
    type SizeMap = {
        [k in Size]: number
    }
    // 等价于
    type SizeMap2 = {
        small: number
        default: number
        big: number
        large: number
    }
    // 深度遍历只读属性
    type DeepReadony<T> = {
        readonly [P in keyof T]: DeepReadony<T[P]>
    }
    
    interface SomeObject {
      a: {
        b: {
          c: number;
        };
      };
    }
    
    const obj: Readonly<SomeObject> = { a: { b: { c: 2 } } };
    obj.a.b.c = 3;    // TS不会报错
    
    const obj2: DeepReadony<SomeObject> = { a: { b: { c: 2 } } };
    obj2.a.b.c = 3;    // Cannot assign to 'c' because it is a read-only property.
    
    

ts优点

typeScript的定位

  1. 是javeScript的超集
  2. 是编译期的行为
  3. 不引入额外的开销
  4. 不改变运行时的行为
  5. 可以在ts中写js

作用:

  1. 对代码起到一个约束的作用,检查传参的类型,返回值的类型是否正确,并有很好的IDE的支持,在你编写代码的时候就作出响应的提示
  2. 也相当于时一个注释,让之后一起开发的同学能够更好的了解代码中的大数据结构

node


fs模块

  1. 异步读取

    fs.readFile(path[, options], callback)
    
  2. 同步读取

    fs.readFileSync(path[, options])
    
    
  3. 异步写入

    fs.writeFile(file, data[, options], callback)
    
    
  4. 同步写入

    fs.writeFileSync(file, data[, options])
    
    
  5. 追加文件

    fs.appendFile(file, data[, options], callback)
    
    
  6. 拷贝文件

    function copy(src,target){
      fs.readFile(src,function(err,data){
        fs.writeFile(target,data);
      })
    }
    
    

js设计模式

单例模式

单例模式一般用在全局只需要一个对象存在的时候可以用单例模式,如果以存在对象就直接返回这个对象,比如vuex

class Singleton {
  constructor() {}
}

Singleton.getInstance = (function() {
  let instance
  return function() {
    if (!instance) {
      instance = new Singleton()
    }
    return instance
  }
})()

let s1 = Singleton.getInstance()
let s2 = Singleton.getInstance()
console.log(s1 === s2) // true

代理模式

一个对象设置一个代理,可以控制对他的访问,可以在代理层就去做一些处理,比如图片的懒加载,在图片加载出来之前先用一张占位图代替,比如事件代理,将点击事件之类的放在ul上,而不是直接加载li上

<ul id="ul">
    <li>1</li>
    <li>2</li>
    <li>3</li>
    <li>4</li>
    <li>5</li>
</ul>
<script>
    let ul = document.querySelector('#ul')
    ul.addEventListener('click', (event) => {
        console.log(event.target);
    })
</script>

策略模式

将一系列的算法都封装在一起,就是将方法和使用方法的实现分离开来

/*策略类*/
var levelOBJ = {
    "A": function(money) {
        return money * 4;
    },
    "B" : function(money) {
        return money * 3;
    },
    "C" : function(money) {
        return money * 2;
    } 
};
/*环境类*/
var calculateBouns =function(level,money) {
    return levelOBJ[level](money);
};
console.log(calculateBouns('A',10000)); // 40000

装饰者模式

在不改变对象的基础上,给对象动态添加方法

Function.prototype.after = function (afterFn {
  var self = this
  return function () {
    var res = self.apply(this, argument)
    afterFn.apply(this, argiment)
    return ret
  }
}

中介者模式

通过中介者来实现通行,解除了对象之间的耦合关系,适合联动关系比较多的对象

var goods = {   //手机库存
    'red|32G': 3,
    'red|64G': 1,
    'blue|32G': 7,
    'blue|32G': 6,
};
//中介者
var mediator = (function() {
    var colorSelect = document.getElementById('colorSelect');
    var memorySelect = document.getElementById('memorySelect');
    var numSelect = document.getElementById('numSelect');
    return {
        changed: function(obj) {
            switch(obj){
                case colorSelect:
                    //TODO
                    break;
                case memorySelect:
                    //TODO
                    break;
                case numSelect:
                    //TODO
                    break;
            }
        }
    }
})();
colorSelect.onchange = function() {
    mediator.changed(this);
};
memorySelect.onchange = function() {
    mediator.changed(this);
};
numSelect.onchange = function() {
    mediator.changed(this);
};

首先要知道这些设计模式的存在,当你遇到问题的时候才能知道用什么去解决

优化


flutter && dart


面试题


  1. 千分位

    console.info( str.replace(/\d{1,3}(?=(\d{3})+$)/g, function(s){
     return s + ','
    }) )
    
    
  2. babel原理

    babel是一个js编译器,能把最新的js代码编译成当下可执行的版本

    1. 解析,接收代码并输出AST,这个过程包括词法分析和语法分析
      • 词法分析,把字符串形式的代码转换成令牌(tokens)流,令牌相当于一个扁平的语法片段数组
      • 语法分析,将令牌流转换成AST形式(抽象语法树)
    2. 接收AST并对其进行遍历,在这个过程中对节点进行添加、更新、移除等操作,这是最复杂的一个过程,返回一个替换后的AST
    3. 深度遍历AST,将转换后的AST再转换成字符串形式的代码