1. 谈谈你对 this 的理解?

总结this 永远指向最后调用它的那个对象

this 的指向有以下几种情况:

  1. 默认绑定
  2. 隐式绑定
  3. 显示绑定
  4. new 绑定
  5. 箭头函数绑定

注意:箭头函数中没有 this 绑定,必须通过查找作用域链来决定其值。如果箭头函数被非箭头函数包含,则 this 绑定的是最近一层非箭头函数的 this,否则,thisundefined

2. 说一说事件循环 Event-Loop?

JS 是一门单线程语言JS 代码在执行过程中,除了依靠函数调用栈来搞定函数的执行顺序外,还可以依靠任务队列(task queue)来搞定另外一些代码的执行。整个执行过程,我们称为事件循环过程。任务队列分为:macro-task(宏任务)与 micro-task(微任务)

  • Macro-task: script(整个)、setTimeoutsetIntervalsetImmediateI/O
  • Micro-task: process.nextTickPromiseAsync/Await(实际上就是 promise)、MutationObserver(H5 新特性)

总结:执行宏任务,然后执行该宏任务产生的微任务。若微任务在执行过程中产生了新的微任务,则继续执行微任务。当微任务执行完毕后,再回到宏任务中进行下一轮循环

扩展

进程:资源分配的最小单位;线程:程序执行的最小单位

H5 的新特性 Web Worker 可以创建多线程。就是在主线程开辟的一个额外的线程,这个线程与主线程不相互影响。通过 postMessageonMessage 交互数据。它创建的子线程完全受控于主线程,且位于外部文件中,无法访问 DOM。所以它并没有改变 js 单线程的本质

  • NodeJs 中的事件循环

    1. timersj 阶段:这个阶段执行 timersetTimeoutsetInterval)的回调
    2. I/O callbacks:执行一些系统调用错误,比如网络通信的错误回调
    3. idle,prepare:仅 node 内部使用
    4. poll:获取新的 I/O 事件, 适当的条件下 node 将阻塞在这里
    5. check:执行 setImmediate() 的回调
    6. close callbacks:执行 socket 的 close 事件回调
  • node 和浏览器 event-loop 的主要区别?

    两者最主要的区别在于:浏览器中的微任务是在每个相应的宏任务中执行的; 而 node 中的微任务是在不同阶段之间执行的

考察:【promiseasync/await 的执行顺序】

3. 你了解 JS 中原型 & 原型链吗?

JS 中一切皆是对象,对象分为普通对象和函数对象。ObjectFunctionJS 自带的函数对象。凡是通过 new Function() 创建的对象都是函数对象,其他的都是普通对象

  • 构造函数:实例的构造函数属性(constructor)指向构造函数

  • 每个对象都有一个__proto__属性,但只有函数对象才有 prototype 属性。__proto__ 内置属性用于指向创建它的构造函数的原型对象。p.__proto__ === Person.prototype

  • 原型对象:是一个普通对象(除了 Function.prototype 外,它是函数对象,也没有 prototype 属性)。原型对象Person.prototype是构造函数Person的一个实例。 原型对象主要用于 继承

  • 所有函数对象的__proto__都指向 Function.prototype,它是一个空函数

    Person.prototype.constructor === Person
    person1.__proto__ === Person.prototype
    person1.constructor === Person
    
    Object.__proto__ === Function.prototype
    Function.__proto__ === Function.prototype
    Object.prototype.__proto__ === null
    

总结:原型链的形成真正是靠 __proto__ 而非 prototype

4. 作用域 与 作用域链了解吗?

js 作为一种弱类型语言 ,变量声明没有那么严谨。于是就要有属于它独特的一套执行方式。作用域可以理解就是一块小的“地盘”一块代码段所在的地方

  • 作用域(Scope)的分类:

    1. 全局作用域
    2. 函数作用域(局部作用域)
    3. 块级作用域(ES6 新增)
  • 作用域链(Scope Chain

    内部函数访问外部函数的变量,采取的是链式查找的方法来决定取那个结构,这种结构称之为作用域链

扩展:作用域与执行上下文的区别?

  • 区别 1:

    1. 全局作用域之外,每个函数都会创建自己的作用域,作用域在函数定义时确定而不是函数调用
    2. 全局执行上下环境是在作用域确定之后,js 代码立马执行之前创建
    3. 函数执行上下环境是在调用函数时,执行函数体代码之前创建
  • 区别 2:

    1. 作用域是静态的,只要函数定义好了就会一直存在,不会变化
    2. 上下文环境是动态的,调用函数时创建,函数调用结束时上下环境会立即释放

5. 闭包 知道吗?

闭包: 是指有权访问另一个函数作用域中的变量的函数--《JavaScript 高级程序设计》

  • 闭包的特性:

    1. 函数嵌套函数
    2. 函数内部可以引用外部的参数和变量,封装私有的方法和变量,避免全局变量的污染
    3. 本质是将函数内部和外部连接起来。优点是可以读取函数内部的变量,让这些变量的值始终保存在内存中,不会在函数被调用之后自动清除
  • 闭包的好处:

    1. 可以读取函数内部的变量
    2. 让这些变量的值始终保持在内存中
  • 闭包的优点:

    1. 延长局部变量的生命周期
  • 闭包的缺点:

    1. 会导致函数的变量一直保存在内存中,过多的闭包可能会导致内存泄漏
  • 闭包的应用场景:

    闭包的两大作用:保存/保护。 在开发中, 其实我们随处可见闭包的身影, 大部分前端 JavaScript代码都是“事件驱动”的,即一个事件绑定的回调方法; 发送ajax请求成功|失败的回调;setTimeout` 的延时回调; 或者一个函数内部返回另一个匿名函数,这些都是闭包的应用

6. 判断数据类型的方式?以及优缺点?

JS中数据类型分为:基本数据类型(nullundefinedstringbooleannumber、(ES6 新增)SymbolBigInt)、引用数据类型(object)

扩展:新增两个原始数据类型:Record & Tuple:就是一个只读ObjectArray

数据判断类型方法有哪些?

  • 方式一:typeof

    1. 在基本数据类型中:除了 null 以外,使用 typeof 均可得到正确的结果
    2. 在引用数据类型中:除了 function 以外,使用 typeof 都得到 object
  • 方式二:instanceof

    用来判断 xx 是否是 xx 的实例。是则返回 true,否则返回 false。语法:[对象] instanceof [构造函数]

    • instanceof 有以下几点需要注意:
      1. 左侧必须是对象 object, 如果不是,则返回 false
      2. instanceof 检查的 原型
  • 方式三:constructor

    1. 原理是利用函数的原型对象的 constructor 属性指向其构造函数
    2. constructor 需要注意:nullundefined 是无效对象,没有 constructor
  • 方式四:toString【最好】

    1. toStringobject 原型上的方法,默认会返回[[Class]] -> [object String]
    2. toString 需要注意:对于其他对象,我们需要通过 callapplybind来改变this的指向后才能返回正确的结果

扩展obj.toString()Object.prototype.toString.call(obj)的结果不一样,这是为什么?

因为 toStringObject 的原型方法,而 Array 、Function 等类型作为 Object 的实例,都重写toString 方法。不同的对象类型调用 toString 方法时,根据原型链的知识,调用的是对应的重写之后的 toString 方法,而不会去调用 Object 上原型 toString 方法,所以采用 obj.toString()不能得到其对象类型,只能将 obj 转换为字符串类型;因此,在想要得到对象的具体类型时,应该调用 Object 上原型 toString 方法

7. JS 中继承的方式有哪些?

  • 原型链继承:父类的实例作为子类的原型

    原型链继承是比较常见的继承方式之一,其中涉及的构造函数、原型和实例,三者之间存在着一定的关系,即每一个构造函数都有一个原型对象,原型对象又包含一个指向构造函数的指针,而实例则包含一个原型对象的指针

    缺点: 两个实例使用的是同一个原型对象,内存空间是共享的。修改一个引用属性另一个跟着变

  • 构造函数继承(借用 call):复制父类的实例属性给子类

    相比第一种原型链继承方式,父类的引用属性不会被共享,优化了第一种继承方式的弊端,但是只能继承父类的实例属性和方法,不能继承原型属性或者方法

  • 组合继承(原型链继承 + 钩子函数继承)

    方式一和方式二的问题都解决了,但是从上面代码我们也可以看到 Parent3 执行了两次,造成了多构造一次的性能开销

  • 原型式继承(借用 Object.create)

    因为 Object.create 方法实现的是浅拷贝,多个实例的引用类型属性指向相同的内存,存在篡改的可能

  • 寄生式继承(借用 Object.create 和工厂模式)

  • 组合寄生式继承

    通过 Object.create 来划分不同的继承方式,最后的寄生式组合继承方式是通过组合继承改造之后的最优继承方式,而 extends 的语法糖和寄生组合继承的方式基本类似

8. JS 中数据的转换机制?

常见的类型转换有:

  • 强制转换(显示转换)

    1. Number():将任意类型的值转化为数值

      转换规则:undefined -> NaN; null -> 0; true -> 1; false -> 0; ....Object -> 先调用 toPrimitive,再调用 toNumber

      • Number()转换字符串的时候,Number('') -> 0; Number('324abc') -> NaN; Number('324') -> 324
      • Number()转换对象的时候,通常转换成 NaN(除了只包含单个数值的数组),Number({a: 1}) -> NaN; Number([1, 2, 3]) -> NaN; Number([5])-> 5
    2. parseInt()parseInt函数逐个解析字符,遇到不能转换的字符就停下来

      parseInt('32a3') // 32

    3. String():可以将任意类型的值转化成字符串

      String({a: 1})->"[object Object]";

    4. Boolean():可以将任意类型的值转为布尔值

      转换规则:Undefined, null , 0, NaN, '' -> false; {}, [] -> true

  • 自动转换(隐式转换)

    隐式转换的场景:(要求运算符两边的操作数不是同一类型)

    1. 比较运算(==!=><)、ifwhile需要布尔值地方
    2. 算术运算(+-*/%
    • undefinednullfalse+0-0NaN‘’都会被转成 false;其余的都是 true
    • 自动转换成字符串,具体规则是:先将复合类型的值转为原始类型的值,再将原始类型的值转为字符串
    • 自动转换成数值,除了+有可能把运算子转为字符串,其他运算符都会把运算子自动转成数值

扩展:

  • ===== 有什么区别?

    === 叫做严格相等,是指:左右两边不仅值要相等,类型也要相等。

    == 不像 === 那样严格,对于一般情况,只要值相等,就返回 true,但 == 还涉及一些类型转换,它的转换规则如下:

    1. 两边的类型是否相同,相同的话就比较值的大小,例如 1==2,返回 false
    2. 判断的是否是 nullundefined,是的话就返回 true
    3. 判断的类型是否是 StringNumber,是的话,把 String 类型转换成 Number,再进行比较
    4. 判断其中一方是否是 Boolean,是的话就把 Boolean 转换成 Number,再进行比较
    5. 如果其中一方为 Object,且另一方为 StringNumber 或者 Symbol,会将 Object 转换成字符串,再进行比较
    console.log({ a: 1 } == true) //false
    console.log({ a: 1 } == '[object Object]') //true
    

    注意:它们都有缺点,前者会自动转换数据类型,后者的 NaN 不等于自身,以及+0 等于-0Object.is 修复了 +0 和-0NaN 和 NaN 相等的问题

  • 对象转原始类型是根据什么流程运行的?

    对象转原始类型,会调用内置的[ToPrimitive]函数,对于该函数而言,其逻辑如下:

    1. 如果有 Symbol.toPrimitive()方法,优先调用再返回
    2. 调用 valueOf(),如果转换为原始类型,则返回
    3. 调用 toString(),如果转换为原始类型,则返回
    4. 如果都没有返回原始类型,会报错
    var obj = {
      value: 3,
      valueOf() {
        return 4
      },
      toString() {
        return '5'
      },
      [Symbol.toPrimitive]() {
        return 6
      },
    }
    console.log(obj + 1) // 输出7
    

应用:如何让 if(a == 1 && a == 2)条件成立?

var a = {
  value: 0,
  valueOf: function() {
    this.value++
    return this.value
  },
}
console.log(a == 1 && a == 2) //true

9. 说说 函数式编程里的高阶函数、函数柯里化、组合函数等?

  • 纯函数:输出不受外部环境影响,同时也不影响外部环境,无副作用
  • 高阶函数:至少满足两个条件:它接收一个或多个函数作为参数、将函数作为输出返回
    1. 高阶函数:(如果函数的参数的一个函数,如果一个函数返回了一个函数,两者都被称为高阶函数) 例子:Vue3 源码里组件挂载(createRender().createApp())、Vue3 源码里创建响应式 effect(createReactiveEffect)
    2. 高阶函数的应用: (如果一个函数中前后想执行什么操作,就可以执行高阶函数;重写一些原生的方法;AOP 面向切面编程)
  • 函数组合:就是将两个或两个以上的函数组合生成一个新函数的过程
  • 柯里化:只传递给函数一部分参数来调用它,让它返回一个函数去处理剩下的参数。直接表现形式就是,当我们有一个函数 f(a,b,c),通过柯里化转换,使得这个函数可以被这样调用 f(a)(b)(c) 例子:Vue3 中创建不同的响应式对象(createReactiveObject)

10. 深拷贝和浅拷贝的区别?如何实现一个深拷贝?(考虑正则,Date 这种类型的数据)

JavaScript 中存在两大数据类型:基本类型引用类型。 基本类型数据保存在在栈内存中;引用类型数据保存在堆内存中,引用数据类型的变量是一个指向堆内存中实际对象的引用,存在栈中

浅拷贝:对原始对象属性值的一份精确拷贝,若属性是基本类型,就拷贝其基本类型的值;若属性是引用类型,则拷贝的是内存地址。所以,若其中一个地址变了后就会影响到另一个对象

  • 常见的浅拷贝方式有:

    1. Object.assign -> Object.assign({}, obj)
    2. Array.prototype.slice(), Array.prototype.concat() -> arr.slice(0); arr.concat()
    3. ...扩展运算符 -> [...obj]
    function shallowClone(obj) {
      let newObj = {}
      for (let prop in obj) {
        if (obj.hasOwnProperty(prop)) {
          newObj[prop] = obj[prop]
        }
      }
      return newObj
    }
    

深拷贝:指的是开辟一个新的栈,两个对象属性完成相同,但是对应两个不同的地址,修改一个对象的属性,不会改变另一个对象的属性

  • 常见的深拷贝方式有:

    1. \_.cloneDeep()【lodash】 -> \_.cloneDeep(obj)

    2. jQuery.extend()【jQuery】 -> \$.extend(true, {}, obj)

    3. JSON.parse(JSON.stringify())-> JSON.parse(JSON.stringify(obj))

      这种方式最简单,但有以下弊端:

      • 当被拷贝对象中有 Date 对象,则拷贝后时间将以字符串的形式
      • 当被拷贝对象中有 RegExpError 对象,则拷贝的结果将得到一个空对象
      • 当被拷贝对象中有 undefined函数 的时,则拷贝的结果将会把函数或 undefined 丢失
    4. 手写递归

    // 写一个深拷贝,考虑正则,Date这种类型的数据
    function deepClone(target, hash = new WeakMap()) {
      if (target === null) return target // 如果是null或者undefined,不进行拷贝操作
      if (target instanceof Date) return new Date(target)
      if (target instanceof RegExp) return new RegExp(target)
      if (typeof target === 'object') {
        // 对象
        if (hash.has(target)) return hash.get(target)
        // let cloneTarget = Array.isArray(target) ? [] : {}
        let cloneTarget = target.constructor // constructor没参数可以不写()
        hash.set(target, cloneTarget)
        for (let key in target) {
          if (target.hasOwnProperty(key)) {
            cloneTarget[key] = deepClone(target[key], hash)
          }
        }
        return cloneTarget
      } else {
        // 普通值
        return target
      }
    }
    

    注意:null == undefined 为 true null === undefined 为 false

  • 两者的区别:

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

11. JS 中执行上下文 和 执行栈是什么?

执行上下文就是当前 JavaScript 代码被解析和执行时所在环境

  • 执行上下文总共有三种类型:

    1. 全局执行上下文: 默认的。不在任何函数中的代码都位于全局执行上下文中。它做了两件事:1. 创建一个全局对象,在浏览器中这个全局对象就是 window 对象。2. 将 this 指针指向这个全局对象。一个程序中只能存在一个全局执行上下文
    2. 函数执行上下文: 每次调用函数时,都会为该函数创建一个新的执行上下文。每个函数都拥有自己的执行上下文,但是只有在函数被调用的时候才会被创建
    3. Eval 函数执行上下文: 运行在 eval 函数中的代码也获得了自己的执行上下文
  • 执行上下文的生命周期包括三个阶段:创建阶段执行阶段回收阶段

    1. 创建阶段

      扩展:当函数被调用,但未执行任何其内部代码之前,会做以下三件事:

      • 创建变量对象:首先初始化函数的参数 arguments,提升函数声明和变量声明
      • 创建作用域链(Scope Chain):在执行期上下文的创建阶段,作用域链是在变量对象之后创建的。作用域链本身包含变量对象。作用域链用于解析变量。当被要求解析变量时,JavaScript 始终从代码嵌套的最内层开始,如果最内层没有找到变量,就会跳转到上一层父作用域中查找,直到找到该变量。
      • 确定 this 指向
    2. 执行阶段

      扩展:执行变量赋值、代码执行

    3. 回收阶段

      扩展:执行上下文出栈等待虚拟机回收执行上下文

  • 变量提升

    大部分编程语言都是先声明变量再使用,但在 JS 中,事情有些不一样:

    1. 变量声明提升
    console.log(a); // undefined
    var a = 10;
    // 上述代码正常输出undefined而不是报错Uncaught ReferenceError: a is not defined,这是因为声明提升(hoisting)
    <!-- 等价于 -->
    var a; // 声明,默认值是undefined
    console.log(a);
    a = 10; // 赋值
    
    1. 函数声明提升

      创建一个函数的方法有两种,一种是通过函数声明 function foo(){}, 另一种是通过函数表达式 var foo = function(){} ,那这两种在函数提升有什么区别呢?

    console.log(f1) // function f1(){}
    function f1() {} // 函数声明
    console.log(f2) // undefined
    var f2 = function() {} // 函数表达式
    

    注意:当遇到函数和变量同名且都会被提升的情况,函数声明优先级比较高,因此变量声明会被函数声明所覆盖,但是可以重新赋值。

    console.log(a) // 输出function a() {}
    function a() {}
    var a = 10
    console.log(a) // 10
    // function 声明的优先级比 var 声明高,也就意味着当两个同名变量同时被 function 和 var 声明时,function 声明会覆盖 var 声明
    <!-- 等价于 -->
    function a() {}
    var a; // undefined -> hoisting
    console.log(a) // 输出function a() {}
    a = 10;
    console.log(a) // 10
    

    总结:变量提升的规则:函数申明整体提升;变量声明提升值为 undefined

  • 确认 this 的指向

    很重要的概念 —— this 的值是在执行的时候才能确认,定义的时候不能确认!

    为什么呢 ? 因为 this 是执行上下文环境的一部分,而执行上下文需要在代码执行之前确定,而不是定义的时候。

  • 执行上下文栈

    函数多了,就有多个函数执行上下文,每次调用函数创建一个新的执行上下文,那如何管理创建的那么多执行上下文呢?JavaScript 引擎创建了执行上下文栈来管理执行上下文。可以把执行上下文栈认为是一个存储函数调用的栈结构,遵循先进后出的原则

    扩展:需要记住几个关键点:

    1. JavaScript 执行在单线程上,所有的代码都是排队执行。
    2. 一开始浏览器执行全局的代码时,首先创建全局的执行上下文,压入执行栈的顶部。
    3. 每当进入一个函数的执行就会创建函数的执行上下文,并且把它压入执行栈的顶部。当前函数执行完成后,当前函数的执行上下文出栈,并等待垃圾回收。
    4. 浏览器的 JS 执行引擎总是访问栈顶的执行上下文。
    5. 全局上下文只有唯一的一个,它在浏览器关闭时出栈。

12. 了解 预编译 吗?

  • js 运行代码共分三步:
    1. 语法分析 顾名思义就是检查一遍 JS 代码内有没有出息语法错误(比如少分号,多写括号等);语法分析期间不会执行代码
    2. 预编译 这个阶段发送在代码执行的前一刻,这个过程说白了就是在内存里面创建一个空间,用来存你定义的变量和函数(放东西的)
    3. 解析执行 执行代码

JS 在执行前会产生一个 GO(Global Object)也就是我们说的全局作用域。当一个方法被调用时会形成一个局部作用域 AO(Activation Object)

  • 预编译发生在函数执行的前一刻, 预编译阶段做了哪些事情?
    1. 创建 AO(Activation Object)对象(里面存储的是函数内部的局部变量)
    2. 找形参和变量声明,将变量和形参名做为 AO 属性名,值为 undefined
    3. 将实参和形参统一,即更改形参后的 undefined 为具体的形参值
    4. 找函数的申明 会覆盖相同变量的申明

但是:在全局作用域里,预编译过程有些许不同

  • GO 对象的过程如下:
    1. 创建 GO 对象
    2. 寻找变量声明,值设定为 undefined
    3. 寻找函数中的函数声明,将函数名作为 GO 属性名,值为函数体

13. 知道 事件代理吗?有什么应用场景?

事件委托:会把一个或者一组元素的事件委托到它的父层或者更外层元素上,真正绑定事件的是外层元素,而不是目标元素。当事件响应到目标元素上时,会通过事件冒泡机制从而触发它的外层元素的绑定事件上,然后在外层元素上去执行函数

事件代理,俗地来讲,就是把一个元素响应事件(click...)的函数委托到另一个元素

扩展:事件流的都会经过三个阶段: 捕获阶段 -> 目标阶段 -> 冒泡阶段

  • 应用场景:
    1. 列表项点击事件
    2. 动态绑定事件,动态的增加或者去除列表项元素

适合事件委托的事件有:clickmousedownmouseupkeydownkeyupkeypress

  • 事件委托存在两大优点:

    1. 减少整个页面所需的内存,提升整体性能
    2. 动态绑定,减少重复工作
  • 使用事件委托也是存在局限性:

    1. focusblur 这些事件没有事件冒泡机制,所以无法进行委托绑定事件
    2. mousemovemouseout 这样的事件,虽然有事件冒泡,但是只能不断通过位置去计算定位,对性能消耗高,因此也是不适合于事件委托的

14. 说说 JS 中的垃圾回收机制 与 内存泄漏?

JS 垃圾回收机制原理:解决内存的泄露,垃圾回收机制会定期(周期性)找出那些不再用到的内存(变量),然后释放其内存。有两种方法:标记清除【常用】,引用计数

  • 标记清除

    先所有都加上标记,再把环境中引用到的变量去除标记。剩下的就是没用的了

  • 引用计数

    跟踪记录每 个值被引用的次数。清除引用次数为 0 的变量 ⚠️ 会有循环引用问题 。循环引用如果大量存在就会导致内存泄露。)

  • 内存泄漏的识别方法:

    1. 浏览器 开发者工具 -> 选择 Timeline 面板 -> 顶部的 Capture 字段里面勾选 Memory -> 点击左上角的录制按钮 -> 在页面上进行各种操作,模拟用户的使用情况 -> 一段时间后,点击对话框的 stop 按钮,面板上就会显示这段时间的内存占用情况

      如果内存占用基本平稳,接近水平,就说明不存在内存泄漏

    2. 命令行 process.memoryUsage()返回一个对象,包含了 Node 进程的内存占用信息

    3. ES6 推出了两种新的数据结构:WeakSet 和 WeakMap。它们对于值的引用都是不计入垃圾回收机制的,所以名字里面才会有一个"Weak",表示这是弱引用

  • 扩展:内存泄漏的场景有哪些?

    1. 意外的全局变量(在局部函数作用域中也可以定义全局变量,可以通过 window 访问,所以进行内存回收时不认为其是需要回收而一直存在,只有在窗口关闭或者刷新页面才能被释放。)-》严格模式eslint、使用完就将其置为 null
    2. 被遗忘的定时器
    3. 脱离 DOM 的引用
    4. 闭包
    5. Map Set 尽量使用 WeakMap 和 WeakSet

15. WebSocket 的原理是啥?

WebSocket 是 HTML5 出的东西(协议),是一个持久化的协议. 是基于 HTTP 协议的,或者说借用了 HTTP 的协议来完成一部分握手

  • Websocket 的作用
    1. 服务端就可以主动推送信息给客户端
    2. 可以以任何一种方式非常有效地发送数据。由于已经建立了连接并且非常有效地组织了 webSocket 数据帧,因此与通过 HTTP 请求(其中必须包含标头,Cookie 等)相比,可以更高效地发送数据。
// 典型的 Websocket 握手
GET /chat HTTP/1.1
Host: server.example.com
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Key: x3JJHMbDL1EzLkh9GBhXDw== // 是一个 Base64 encode 的值,这个是浏览器随机生成的,告诉服务器:我要验证你是不是真的websocket协议
Sec-WebSocket-Protocol: chat, superchat // 是一个用户定义的字符串,用来区分同URL下,不同的服务所需要的协议;简单理解:今晚我要服务A,别搞错啦~
Sec-WebSocket-Version: 13 // 是告诉服务器所使用的 Websocket Draft (协议版本)
Origin: http://example.com

// 服务器会返回下列东西,表示已经接受到请求, 成功建立Websocket啦!
HTTP/1.1 101 Switching Protocols
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Accept: HSmrc0sMlYUkAGmm5OPpG2HaGWk= // 这个则是经过服务器确认,并且加密过后的 Sec-WebSocket-Key
Sec-WebSocket-Protocol: chat // 则是表示最终使用的协议

扩展

  1. ajax 轮询 的原理非常简单,让浏览器隔个几秒就发送一次请求,询问服务器是否有新信息。
  2. long poll 其实原理跟 ajax 轮询 差不多,都是采用轮询的方式,不过采取的是阻塞模型(一直打电话,没收到就不挂电话),也就是说,客户端发起连接后,如果没消息,就一直不返回 Response 给客户端。直到有消息才返回,返回完之后,客户端再次建立连接,周而复始。

16. require 和 import 的区别?/ ES6 Module 和 Commonjs 区别?

  1. 导入 require 导出 exports/module.exports 是 CommonJS 的标准,通常适用范围如 Node.js
  2. import/export 是 ES6 的标准
  3. commonjs 输出的,是一个值的拷贝,而 es6 输出的是值的引用
  4. commonjs 是运行时加载,es6 是编译时输出接口

注意require 是浅拷贝,也就是说你可以修改对象第二层的属性并影响原数据;import 是引用,基本数据类型,修改不会影响原数据,但是对象修改属性会影响。import 具有置顶性, 它不是一定要放在文件的顶部

  1. ES6 Module 静态引入,编译时引入
  2. CommonJs 动态引入,执行(执行)时引入
  3. 只有 ES6 Module 才能静态分析,实现 Tree-Shaking
  4. CommonJS 模块输出的是一个值的拷贝,ES6 模块输出的是值的引用

17. let const var 的区别?

  1. var 会变量提升,letconst 没有变量提升,只能在当前作用域中有效
  2. letconst 会存在暂时性死区
  3. letvar 一般是声明变量的,const 声明常量,不能修改。letconst 不能重复声明
  • var let const 最大的区别:

    var 在全局作用域声明的变量有一种行为会挂载在 window 对象上,它会创建一个新的全局变量作为全局对象的属性,这种行为说不定会覆盖到 window 对象上的某个属性,而 let const 声明的变量则不会有这一行为

18. ES6 有哪些新特性?

let,const;解构赋值;箭头函数;数组/对象/函数字符串等的扩展;ProxyReflectIteratorSet/WeakSetMap/WeakMapSymbolPromiseAsync+awaitGenerator;类;Module 模块化等

注意...在函数参数中叫剩余运算符(将所有的参数组合成一个数组,只能在最后一个参数);在函数中调用是展开运算符

19. 你知道异步解决方案有哪些?

  1. 回调函数
  2. Promise
  3. Generator
  4. Async + Await
  • Promise 的优点:

    1. 可以解决异步嵌套问题
    2. 可以解决多个异步并发的问题
  • Promise 的缺点:

    1. promise 也是基于回调的
    2. promise 无法终止
  • async + await 如何处理错误?

    Promise 可以通过 catch 捕获错误,async + await 通过await-to-js捕获错误

  • async + await 的原理?

    async + await 就是 Generator+ yield语法糖

  • 实现简版的 generator?

    function selfGenerator(arr) {
      let index = 0
      let len = arr.length
      return {
        next: function() {
          let done = index >= len
          let value = !done ? arr[index++] : undefined
    
          return {
            done: done,
            value: value,
          }
        },
        [Symbol.iterator]() {
          // yield返回iterator [...it]
          return this
        },
      }
    }
    

20. 怎么理解 Set、Map 两种数据结构?

  • Set 是一种叫做集合的数据结构,类似于数组。但成员是唯一且无需的,没有重复的值
  • Map 是一种叫做 字典的数据结构
  • WeakSetSet 的本质区别:
    1. WeakSet 只能存储对象引用,不能存放值;而 Set 对象都可以存放
    2. WeakSet 对象中存储的对象值都是被弱引用的,即垃圾回收机制不考虑 WeakSet
  • WeakMapMap 的本质区别:
    1. WeakMap 的键必须是对象,而值可以是任意的
    2. WeakMap 中的对象都是弱引用的

Object 里的 key 只能是字符串(或 Symbol),Map 的里的 key 可以放任何类型

总结SetMap 注意的应用场景在于 数组去重数据存储

21. 谈谈对 Symbol 的理解,有应用场景?

  • Symbol 的特点:
    1. Symbol 是基本数据类型,特点:独一无二【一般作为对象的 key
    2. Symbol 属性默认是不能枚举的,可通过 Object.getOwnPropertySymbols(), Reflect.keys() 获取
    3. Symbol 有两个静态方法:Symbol.for, Symbol.keyFor
    4. Symbol 具有元编程的能力,可以改写语法本身。(hasInstance, species, match,iterator, toPrimitive, toStringTag 等 11 种)
    5. Symbol 可以模拟类的私有方法

22. 普通函数与箭头函数的区别?

  1. 箭头函数是匿名函数,不能作为构造函数,不能使用 new
  2. 箭头函数不绑定 arguments,取而代之用 rest 参数...解决
  3. 箭头函数不绑定 this,会捕获其所在的上下文的 this 值,作为自己的 this 值。
  4. 箭头函数没有原型属性,undefined
  5. 箭头函数不能当做 Generator 函数,不能使用 yield 关键字

总结:普通函数的 this 指向调用它的对象;箭头函数的 this 指向调用父级的对象,如果父级作用域还是箭头函数,就继续向上找,直到 window

var a = 20
var obj = {
  a: 10,
  b: () => {
    console.log(this.a) // 浏览器是20;node中是undefined
    console.log(this) // 浏览器是Window 对象;node中是{}
  },
  c: function() {
    console.log(this.a) // 10
    console.log(this) // {a: 10, b: ƒ, c: ƒ}
  },
}
obj.b()
obj.c()

23. 能不能实现图片懒加载?

  • 方案一、clientHeight、scrollTop 和 offsetTop
<!-- 给图片一个占位符 -->
<img src="default.png" data-src="http://www.xxx.com/target.png" />

接着监听 scroll 事件来判断图片是否到达视口:

let img = document.getElementByTagName('img')
let num = img.length // 所有图片的总数
let count = 0 // 计数器,从第一张图片开始计

lazyload() // 首次加载别忘了显示图片
window.addEventListener('scroll', lazyload)
function lazyload() {
  let viewHeight = document.documentElement.clientHeight // 视口高度
  let scrollTop = document.documentElement.scrollTop || document.body.scrollTop // 滚动条卷去的高度
  for (let i = count; i < num; i++) {
    // 元素现在已经出现在视口中
    if (img[i].offsetTop < scrollTop + viewHeight) {
      if (img[i].getAttribute('src') !== 'default.png') continue
      img[i].src = img[i].getAttribute('data-src')
      count++
    }
  }
}
// 当然对scroll做节点处理,以免频繁触发
window.addEventListener('scroll', throttle(lazyload, 200))
  • 方式二、getBoundingClientRect

通过 DOM 元素的 getBoundingClientRect API,来判断图片是否出现在了当前视口。

function lazyload() {
  for (let i = count; i < num; i++) {
    // 元素现在已经出现在视口中
    if (
      img[i].getBoundingClientRect().top < document.documentElement.clientHeight
    ) {
      if (img[i].getAttribute('src') !== 'default.png') continue
      img[i].src = img[i].getAttribute('data-src')
      count++
    }
  }
}
  • 方式三、IntersectionObserver

浏览器内置的一个 API,实现了监听 window 的 scroll 事件判断是否在视口中以及节流三大功能。

let img = document.getElementByTagName('img')

const observer = new IntersectionObserver((changes) => {
  // changes 是被观察的元素集合
  for (let i = 0, len = changes.length; i < len; i++) {
    let change = changes[i]
    // 通过这个属性判断是否在视口中
    if (change.isIntersection) {
      const imgElement = change.target
      imgElement.src = imgElement.getAttribute('data-src')
      observer.unobserve(imgElement)
    }
  }
})

Array.from(img).forEach((item) => observer.observe(item))

24. requestAnimationFrame 和 requestIdleCallback 的区别?

requestAnimationFrame:

  • 定义:是每次屏幕被刷新时被调用的,此方法有一个参数,传入的这个方法将在下一次屏幕刷新时被调用
  • 作用:传入的方法在每次屏幕刷新时都会被调用

requestIdleCallback:

  • 定义:则是在每次屏幕刷新时,判断当前帧是否还会有多余的时间,如果有则会调用 requestIdleCallback
  • 作用:传入的方法在屏幕刷新完成有空余时间的时候会调用

二者区别:

前者一定会被调用,后者必须是屏幕刷新完成之后有空余时间才会进行调用。如果浏览器处于忙碌状态那么 requestIdleCallback 函数一直不会被调用。

25. 前端 js 压缩图片并上传?

主要用到的原生组件:FileReaderCanvasBlobFormData

  • 逻辑步骤:

    1. FileReader.readAsDataURL 将上传的图片文件转为 base64 格式
    2. 将 img 绘制到 canvas 画布上,canvas.toDataURL 压缩图片
    3. new Blob 将压缩后的 base64 转为 Blob 对象
    4. FormData.append 将图片文件数据存入 formData
    this.compressImage(
      files[0],
      (file) => {
        console.log(file)
        const formData = new FormData()
        formData.append('file', file, file.name || '上传图片.jpeg')
      },
      $.noop
    )
    // 压缩图片
    compressImage = (file, success, error) => {
      // 图片小于1M不压缩
      if (file.size < Math.pow(1024, 2)) {
        return success(file)
      }
    
      const name = file.name //文件名
      const reader = new FileReader()
      reader.readAsDataURL(file)
      reader.onload = (e) => {
        const src = e.target.result
    
        const img = new Image()
        img.src = src
        img.onload = (e) => {
          const w = img.width
          const h = img.height
          const quality = 0.8 // 默认图片质量为0.92
          // 生成canvas
          const canvas = document.createElement('canvas')
          const ctx = canvas.getContext('2d')
          // 创建属性节点
          const anw = document.createAttribute('width')
          anw.nodeValue = w
          const anh = document.createAttribute('height')
          anh.nodeValue = h
          canvas.setAttributeNode(anw)
          canvas.setAttributeNode(anh)
    
          // 铺底色 PNG转JPEG时透明区域会变黑色
          ctx.fillStyle = '#fff'
          ctx.fillRect(0, 0, w, h)
    
          ctx.drawImage(img, 0, 0, w, h)
          // quality值越小,所绘制出的图像越模糊
          const base64 = canvas.toDataURL('image/jpeg', quality) // 图片格式jpeg或webp可以选0-1质量区间
    
          // 返回base64转blob的值
          console.log(
            `原图${(src.length / 1024).toFixed(2)}kb`,
            `新图${(base64.length / 1024).toFixed(2)}kb`
          )
          // 去掉url的头,并转换为byte
          const bytes = window.atob(base64.split(',')[1])
          // 处理异常,将ascii码小于0的转换为大于0
          const ab = new ArrayBuffer(bytes.length)
          const ia = new Uint8Array(ab)
          for (let i = 0; i < bytes.length; i++) {
            ia[i] = bytes.charCodeAt(i)
          }
          file = new Blob([ab], { type: 'image/jpeg' })
          file.name = name
    
          success(file)
        }
        img.onerror = (e) => {
          error(e)
        }
      }
      reader.onerror = (e) => {
        error(e)
      }
    }
    
  • 遇到的一些坑:

    1. PNG 转 JPEG 时 PNG 格式的透明区域会变黑色,需要先手动铺底色
    2. toDataURL 参数为 PNG 时不支持传图片质量,所以需要写死 image/jpeg 或 image/webp,具体可以参考 toDataURL 的 api
    3. formData.append 第三个参数 filename 是有浏览器兼容性问题的,如果不传就是 filename=blob,后端校验文件名可能过不去
    4. ajax 的 contentType 和 processData 需要传 false,这和本文关系不大直接带过
    5. 在 ios 中,canvas 的长*宽有限制,图片长宽太大,会压缩失败,我将图片的长宽限制在 3000 以内就 OK 了

扩展:URL.createObjectURL(blob)和 FileReader.readAsDataURL(file)的区别?

  • 返回形式
    1. 通过 FileReader.readAsDataURL(file)可以获取一段 data:base64 的字符串
    2. 通过 URL.createObjectURL(blob)可以获取当前文件的一个内存 URL
  • 执行时间
    1. createObjectURL 是同步执行(立即的)
    2. FileReader.readAsDataURL 是异步执行(过一段时间)
  • 内存使用
    1. createObjectURL 返回一段带 hash 的 url,并且一直存储在内存中,直到 document 触发了 unload 事件(例如:document close)或者执行 revokeObjectURL 来释放
    2. FileReader.readAsDataURL 则返回包含很多字符的 base64,并会比 blob url 消耗更多内存,但是在不用的时候会自动从内存中清除(通过垃圾回收机制)
  • 兼容性方面两个属性都兼容 ie10 以上的浏览器

使用 createObjectURL 可以节省性能并更快速,只不过需要在不使用的情况下手动释放内存。如果不太在意设备性能问题,并想获取图片的 base64,则推荐使用 FileReader.readAsDataURL

26. 用过 TypeScript 吗?有什么优点,为什么用?

TypeScript 最大的优点就是其类型推断跟 VS Code 的良好搭配让代码效率有了极大提升。

统计网页中出现的标签

实现步骤:

  1. 获取所有的 DOM 节点
  2. NodeList 集合转化为数组
  3. 获取数组每个元素的标签名
  4. 去重
new Set([...document.querySelectorAll('*')].map(ele=>ele.tagName)).size
  • 说一说,如何遍历输出页面中的所有元素
const body = document.getElementsByTagName('body')[0]
const it = document.createNodeIterator(body)
let root = it.nextNode()
while (root) {
  console.log(root)
  root = it.nextNode()
}

JS 深浅拷贝

对象深浅拷贝,是面试常见的面试题之一。

原对象:

let obj = {
   a: 100,
   b: [100, 200, 300],
   c: {
      x: 10
   },
   d: /^\d+$/
}

浅克隆

浅克隆 只克隆第一层

方法一:

let obj2 = {...obj};

方法二:

let obj2 = {};
for(let key in obj) {
   if(!obj.hasOwnProperty(key)) break;
   obj2[key] = obj[key];
}

深克隆

注意:在函数、日期、正则表达式时,JSON.stringify 时,都会被转换成对象{}

方法一:

let obj3 = JSON.parse(JSON.stringify(obj));

方法二:

function deepClone(obj) {
    // 过滤一些特殊情况
    if(obj === null) return null;
    if(typeof obj !== "object") return obj;
    if (typeof window !== 'undefined' && window.JSON) { // 浏览器环境下 并支持window.JSON 则使用 JSON
        return JSON.parse(JSON.stringify(obj));
    }
    if(obj instanceof RegExp) { // 正则
         return new RegExp(obj);
    }
    if(obj instanceof Date) { // 日期
         return new Date(obj);
    }
    // let newObj = {}
    // let newObj = new Object()
    let newObj = new obj.constructor; // 不直接创建空对象的目的:克隆的结果和之前保持所属类  =》 即能克隆普通对象,又能克隆某个实例对象
    for(let key in obj) {
        if(obj.hasOwnProperty(key)) {
             newObj[key] = deepClone(obj[key]);
        }
    }
    // let newObj = obj.constructor === Array ? [] : {};
    //for(let key in obj) {
    //    newObj[key] = typeof obj[key] === 'object' ? deepCopy(obj[key]) : //obj[key];
    //}
    return newObj;
}

原生 Ajax

一个完整的 ajax 请求一般包括以下步骤:

  • 实例化 XMLHttpRequest 对象
  • 连接服务器
  • 发送请求
  • 介绍
function ajax(options) {
  let method = options.method || 'GET', // 不传则默认为GET请求
      params = options.params, // GET请求携带的参数
      data   = options.data, // POST请求传递的参数
      url    = options.url + (params ? '?' + Object.keys(params).map(key => key + '=' + params[key]).join('&') : ''),
      async  = options.async === false ? false : true,
      success = options.success,
      headers = options.headers;

  let xhr;
  // 创建xhr对象
  if(window.XMLHttpRequest) {
    xhr = new XMLHttpRequest();
  } else {
    xhr = new ActiveXObject('Microsoft.XMLHTTP');
  }

  xhr.onreadystatechange = function() {
    if(xhr.readyState === 4 && xhr.status === 200) {
      success && success(xhr.responseText);
    }
  }

  xhr.open(method, url, async);

  if(headers) {
    Object.keys(Headers).forEach(key => xhr.setRequestHeader(key, headers[key]))
  }

  method === 'GET' ? xhr.send() : xhr.send(data)
}

注意:IE5、6 不兼容 XMLHttpRequest,所以要使用 ActiveXObject()对象,并传入 'Microsoft.XMLHTTP',达到兼容目的。

  • readyState 的五种状态详解:

    0 - (未初始化)还没有调用 send()方法

    1 - (载入)已调用 send()方法,正在发送请求

    2 - (载入完成)send()方法执行完成,已经接收到全部响应内容

    3 - (交互)正在解析响应内容

    4 - (完成)响应内容解析完成,可以在客户端调用了

防抖和节流

如今前端界面效果越来越复杂,有一些频繁操作会导致页面性能和用户体验度低。像:输入框搜索会频繁调端口接口、放大缩小窗口等。

防抖 - debounce 当持续触发事件时,一定时间段内没有再触发事件,事件处理函数才会执行一次,如果设定的时间到来之前,又一次触发了事件,就重新开始延时。

const debounce = (fn, delay) => {
  let timer = null;
  return (...args) => {
    clearTimeout(timer);
    timer = setTimeout(() => {
      fn.apply(this, args);
    }, delay);
  };
};

节流 - throttle 当持续触发事件时,保证一定时间段内只调用一次事件处理函数。

const throttle = (fn, delay = 500) => {
  let flag = true;
  return (...args) => {
    if (!flag) return;
    flag = false;
    setTimeout(() => {
      fn.apply(this, args);
      flag = true;
    }, delay);
  };
};

解析 URL 参数

function parseParam(url) {
    // 将浏览器地址中 ‘?’ 后面的字符串取出来
    const paramsStr = /.+\?(.+)$/.exec(url)[1];
    // 将截取的字符串以 ‘&’ 分割后存到数组中
    const paramsArr = paramsStr.split('&');
    // 定义存放解析后的对象
    let paramsObj = {};
    // 遍历
    paramsArr.forEach(param => {
      // 判断是否含有key和value
      if (/=/.test(param)) {
        // 结构获取对象的key和value
        let [key, val] = param.split('=');
        // 解码
        val = decodeURIComponent(val);
        // 判断是否转为数字
        val = /^\d+$/.test(val) ? parseFloat(val) : val;
        // 判断存放对象中是否存在key属性
        if (paramsObj.hasOwnProperty(key)) {
          // 存在的话就存放一个数组
          paramsObj[key] = [].concat(paramsObj[key], val);
        } else {
          // 不存在就存放一个对象
          paramsObj[key] = val;
        }
      } else {
        // 没有value的情况
        paramsObj[param] = true;
      }
    })
    return paramsObj;
}
let url = 'https://www.baidu.com?username=%22tmc%22&password=%22123456%22&dutiy=%E5%89%8D%E7%AB%AF%E6%94%BB%E5%9F%8E%E7%8B%AE&flag';
console.log(parseParam(url))

{ username: '"tmc"',
  password: '"123456"',
  dutiy: '前端攻城狮',
  flag: true
}

Jsonp 的原理

function jsonp({url, params, cb}) {
   return new Promise((resolve, reject) => {
     window[cb] = function (data) {  // 声明全局变量
        resolve(data)
        document.body.removeChild(script)
      }
      params = {...params, cb}
      let arrs = []
      for(let key in params) {
         arrs.push(`${key}=${params[key]}`)
      }
      let script = document.createElement('script')
      script.src = `${url}?${arrs.join('&')}`
      document.body.appendChild(script)
   })
}

jsonp 的缺点

  1. 只能发送 Get 请求 不支持 post put delete
  2. 不安全 xss 攻击

apply 的原理

apply 的实现原理和 call 的实现原理差不多,只是参数形式不一样。--- 数组

Function.prototype.apply = function(content = window) {
    content.fn = this;
    let result;
    // 判断是否有第二个参数
    if(arguments[1]) {
        result = content.fn(...arguments[1]);
    } else {
        result = content.fn();
    }
    delete content.fn;
    return result;
}

注意:当 apply 传入的第一个参数为 null 时,函数体内的 this 会指向 window。

bind 的原理

bind 方法会创建一个新函数。当这个新函数被调用时,bind() 的第一个参数将作为它运行时的 this,之后的一序列参数将会在传递的实参前传入作为它的参数。

Function.prototype.bind = function(content) {
   if(typeof this != 'function') {
      throw Error('not a function');
   }
   let _this = this;
   let args = [...arguments].slice(1);
   return function F() {
      // 判断是否被当做构造函数使用
      if(this instanceof F) {
         return _this.apply(this, args.concat([...arguments]))
      }
      return _this.apply(content, args.concat([...arguments]))
   }
}

call 的原理

call 语法:fun.call(thisArg, arg1, arg2, arg3, .....)

call 的核心原理:

  • 将函数设为对象的属性
  • 执行和删除这个函数
  • 指定 this 到函数并传入给定参数执行函数
  • 如果不传参数,默认指向 window
Function.prototype.call2 = function(content = window) {
    // 判断是否是underfine和null
    // if(typeof content === 'undefined' || typeof content === null){
    //     content = window
    // }
    content.fn = this;
    let args = [...arguments].slice(1);
    let result = content.fn(...args);
    delete content.fn;
    return result;
}

注意:当 call 传入的第一个参数为 null 时,函数体内的 this 会指向 window。

new 的原理

实现一个 new 操作符的具体实现步骤:

  • 首先函数接受不定量的参数,第一个参数为构造函数,接下来的参数被构造函数使用
  • 然后内部创建一个空对象 obj
  • 因为 obj 对象需要访问到构造函数原型链上的属性,所以我们通过 setPrototypeOf 将两者联系起来。这段代码等同于 obj.proto = Con.prototype
  • 将 obj 绑定到构造函数上,并且传入剩余的参数
  • 判断构造函数返回值是否为对象,如果为对象就使用构造函数返回的值,否则使用 obj,这样就实现了忽略构造函数返回的原始值
/**
 * 创建一个new操作符
 * @param {*} Con 构造函数
 * @param  {...any} args 忘构造函数中传的参数
 */
  function createNew(Con, ...args) {
    let obj = {} // 创建一个对象,因为new操作符会返回一个对象
    Object.setPrototypeOf(obj, Con.prototype) // 将对象与构造函数原型链接起来
    // obj.__proto__ = Con.prototype // 等价于上面的写法
    let result = Con.apply(obj, args) // 将构造函数中的this指向这个对象,并传递参数
    return result instanceof Object ? result : obj
}

注意

一、new 操作符的几个作用:

  1. new 操作符返回一个对象,所以我们需要在内部创建一个对象
  2. 这个对象,也就是构造函数中的 this,可以访问到挂载在 this 上的任意属性
  3. 这个对象可以访问到构造函数原型链上的属性,所以需要将对象与构造函数链接起来
  4. 返回原始值需要忽略,返回对象需要正常处理

二、new 操作符的特点:

  1. new 通过构造函数 Test 创建处理的实例可以访问构造函数中的属性也可以访问构造函数原型链上的属性,所以:通过 new 操作符,实例与构造函数通过原型链连接了起来
  2. 构造函数如果返回原始值,那么这个返回值毫无意义
  3. 构造函数如果返回对象,那么这个返回值会被正常的使用,导致 new 操作符没有作用

instanceof 的原理

instanceof 用来检测一个对象在其原型链中是否存在一个构造函数的 prototype 属性

function instanceOf(left,right) {
    let proto = left.__proto__;
    let prototype = right.prototype
    while(true) {
        if(proto === null) return false
        if(proto === prototype) return true
        proto = proto.__proto__;
    }
}

Promise A+规范原理

Promise 是一种异步解决方案,本意为‘承诺’,承诺过一段时间给结果。它有三种状态,Pending、Fulfilled、Rejected,状态只能从 Pending 变成 Fulfilled,或者 Pending 变成 Rejected。状态一旦改变就无法在变了。创建 Promise 实例后,它就会立即执行。

  • Promise 的缺点:
    1. 无法取消 Promise, 一旦创建它就会立即执行, 无法中途取消
    2. 如果不设置回调函数, Promise 内部抛出错误, 不会反应到外部
    3. 当处于 Pending 状态时, 无法得知目前进展到哪一个阶段

在面试中高级前端时。要求被手写 Promise A+规范源码是必考题了。如果想详细了解,请参考 一步步教你实现 Promise/A+ 规范 完整版

class Promise {
    constructor(executor) {
        this.status = 'pending' // 初始化状态
        this.value = undefined // 初始化成功返回的值
        this.reason = undefined // 初始化失败返回的原因

        // 解决处理异步的resolve
        this.onResolvedCallbacks = [] // 存放所有成功的resolve
        this.onRejectedCallbacks = [] // 存放所有失败的reject

        /**
         * @param {*} value 成功返回值
         * 定义resolve方法
         * 注意:状态只能从pending->fulfilled和pending->rejected两个
         */
        const resolve = (value) => {
            if(this.status === 'pending') {
                this.status = 'fulfilled' // 成功时将状态转换为成功态fulfilled
                this.value = value // 将成功返回的值赋值给promise
                // 为了解决异步resolve以及返回多层promise
                this.onResolvedCallbacks.forEach(fn => {
                    fn() // 当状态变为成功态依次执行所有的resolve函数
                })
            }
        }
        const reject = (reason) => {
            if(this.status === 'pending') {
                this.status = 'rejected' // 失败时将状态转换为成功态失败态rejected
                this.reason = reason // 将失败返回的原因赋值给promise
                this.onRejectedCallbacks.forEach(fn => {
                    fn() // 当状态变为失败态依次执行所有的reject函数
                })
            }
        }
        executor(resolve, reject) // 执行promise传的回调函数
    }
    /**
     * 定义promise的then方法
     * @param {*} onFulfilled 成功的回调
     * @param {*} onRejected 失败的回调
     */
    then(onFulfilled, onRejected) {
        // 为了解决then方法返回Promise的情况
        const promise2 = new Promise((resolve, reject) => {
            if(this.status === 'fulfilled') { // 如果状态为fulfilled时则将值传给这个成功的回调
                setTimeout(() => {
                    const x = onFulfilled(this.value) // x的值有可能为 promise || 123 || '123'...
                    // 注意:此时调用promise2时还没有返回值,要用setTimeout模拟进入第二次事件循环;先有鸡先有蛋
                    resolvePromise(promise2, x, resolve, reject)
                }, 0)
            }
            if(this.status === 'rejected') {
                setTimeout(() => {
                    const x = onRejected(this.reason) // 如果状态为rejected时则将视频的原因传给失败的回调
                    resolvePromise(promise2, x, resolve, reject)
                }, 0)
            }
            if(this.status === 'pending') { // 记录-》解决异步
                this.onResolvedCallbacks.push(() => {
                    setTimeout(() => {
                        const x = onFulfilled(this.value)
                        resolvePromise(promise2, x, resolve, reject)
                    }, 0)
                })
                this.onRejectedCallbacks.push(() => {
                    setTimeout(() => {
                        const x = onRejected(this.reason)
                        resolvePromise(promise2, x, resolve, reject)
                    }, 0)
                })
            }
        })
        return promise2; // 解决多次链式调用的问题
    }
}

const resolvePromise = (promise2, x, resolve, reject) => {
    // console.log(promise2, x, resolve, reject)
    if(promise2 === x) { // 如果返回的值与then方法返回的值相同时
        throw TypeError('循环引用')
    }
    // 判断x是不是promise;注意:null的typeof也是object要排除
    if(typeof x === 'function' || (typeof x === 'object' && x !== null)) {
        try {
            const then = x.then // 获取返回值x上的then方法;注意方法会报错要捕获异常;原因111
            if(typeof then === 'function') { // 就认为是promise
                then.call(x, y => {
                    // resolve(y)
                    // 递归解析 ; 有可能返回多个嵌套的promise
                    resolvePromise(promise2, y, resolve, reject)
                }, r => {
                    reject(r)
                })
            }
        } catch(e) {
            reject(e)
        }
    } else {
        resolve(x);
    }
}
module.exports = Promise;

JS 数组

去重

普通项

let arr2 = [1, 2, 3, 2, 33, 55, 66, 3, 55];

第一种:

let newArr = [];
   arr2.forEach(item => {
       if(newArr.indexOf(item) == '-1') {
           newArr.push(item);
       }
   })
console.log(newArr);

// (6) [1, 2, 3, 33, 55, 66]

第二种:

let newArr = [...new Set(arr2)];
console.log(newArr);

// (6) [1, 2, 3, 33, 55, 66]

注意:Array.from()、filter()、for()等方法都可以完成上面数组去重。

对象项

let arr1 = [
    {id: 1, name: '汤小梦'},
    {id: 2, name: '石小明'},
    {id: 3, name: '前端开发'},
    {id: 1, name: 'web前端'}
];

实现方法:

const unique = (arr, key) => {
    return [...new Map(arr.map(item => [item[key], item])).values()]
}
console.log(unique(arr1, 'id'));

// [
	{id: 1, name: "web前端"},
	{id: 2, name: "石小明"},
	{id: 3, name: "前端开发"}
]

合并

let arr3 = ['a', 'b']
let arr4 = ['c', 'd']

方法一:ES5

let arr5 = arr3.concat(arr4);
console.log(arr5);

// ['a', 'b', 'c', 'd']

方法一:ES6

let arr6 = [...arr3, ...arr4];
console.log(arr6);

// ['a', 'b', 'c', 'd']

展平

let arr7 = [1, 2, [3, 4], [5, 6, [7, 8, 9]]];

第一种:

let arrNew = arr7.flat(Infinity);
console.log(arrNew);

// (9) [1, 2, 3, 4, 5, 6, 7, 8, 9]

第二种:

let arrNew = arr7.join().split(',').map(Number);
console.log(arrNew);

// (9) [1, 2, 3, 4, 5, 6, 7, 8, 9]

第三种:

let arrNew = arr7.toString().split(',').map(Number);
console.log(arrNew);

// (9) [1, 2, 3, 4, 5, 6, 7, 8, 9]

第四种:

const flattern = (arr) => {
     const result = []
     arr.forEach((item) => {
         if (Array.isArray(item)) {
              result.push(...flattern(item))
         } else {
              result.push(item)
         }
    })
    return result
}
flattern(arr7);

// (9) [1, 2, 3, 4, 5, 6, 7, 8, 9]

第五种:

function flatten(arr) {
    return [].concat(
        ...arr.map(x => Array.isArray(x) ? flatten(x) : x)
    )
}
flattern(arr7);

// (9) [1, 2, 3, 4, 5, 6, 7, 8, 9]

是否为数组

let arr = []

第一种:instanceof

console.log(arr instanceof Array)

第二种:constructor

console.log(arr.constructor === Array)

第三种:判断对象是否有 push 等数组的一些方法

console.log(!!arr.push && !!arr.concat)

第四种:toString

console.log(Object.prototype.toString.call(arr) === '[object Array]')

第五种:Array.isArray

console.log(Array.isArray(arr))

注意:第五种方式最优~

冒泡排序

let arr = [1, 44, 6, 77, 3, 7, 99, 12];
  • 冒泡排序算法的原理如下:
  1. 比较两个相邻的元素,若前一个比后一个大,则交换位置
  2. 第一轮的时候最后一个元素应该是最大的一个
  3. 对所有的元素重复以上的步骤,除了最后一个
function bubbleSort(arr) {
    for(let i=0; i<arr.length; i++) {
        for(let j=0; j<arr.length - i - 1; j++) {
            if(arr[j+1] < arr[j]) {
                let temp = arr[j];
                arr[j] = arr[j+1];
                arr[j+1] = temp;
            }
        }
    }
    return arr;
}
console.log(bubbleSort(arr));

// [ 1, 3, 6, 7, 12, 44, 77, 99 ]

注意:最后一个元素不用比较。

快速排序

let arr = [1, 44, 6, 77, 3, 7, 99, 12];
  • 快速排序算法的原理如下:
  1. 找基准(一般是以中间项为基准)
  2. 遍历数组,小于基准的放在 left,大于基准的放在 right
  3. 递归
function quickSort(arr) {
    if(arr.length <= 1) return arr;
    let mid = Math.floor(arr.length / 2);
    let midItem = arr.splice(mid, 1)[0];
    let leftArr = [];
    let rightArr = [];
    for(let i=0; i<arr.length; i++) {
        let current = arr[i];
        if(current >= midItem) {
            rightArr.push(current);
        } else {
            leftArr.push(current);
        }
    }
    return quickSort(leftArr).concat([midItem], quickSort(rightArr));
}

console.log(quickSort(arr));

// [ 1, 3, 6, 7, 12, 44, 77, 99 ]

从 URL 输入到页面渲染发送了什么

一、总体流程如下:

  • DNS 域名解析:将域名解析成 IP 地址
  • TCP 连接:TCP 三次握手
  • 发送 HTTP 请求
  • 服务器处理请求并返回 HTTP 报文
  • 浏览器解析并渲染页面
  • 断开 TCP 连接:TCP 四次挥手

二、URL 是啥

URL(Uniform Resource Locator),统一资源定位符,用于定位互联网上资源,俗称网址。比如 http://www.w3school.com.cn/ht...,遵守以下的语法规则:

scheme://host.domain:port/path/filename

各专业名称解析如下:

  • scheme - 定义因特网服务的类型。常见的协议有 http、https、ftp、file,其中最常见的类型是 http
  • host - 定义域主机(http 的默认主机是 www)
  • domain - 定义因特网域名,比如 http://baidu.com
  • port - 定义主机上的端口号(http 的默认端口号是 80; https 的默认端口是 443)
  • path - 定义服务器上的路径(若省略,则资源需在服务器的根目录中) filename - 定义文档/资源的名称

三、域名解析(DNS)

在浏览器输入网址后,首先要经过域名解析,因为浏览器并不能直接通过域名找到对应的服务器,而是要通过 IP 地址。

  1. 何为 IP 地址?

IP 地址是指互联网协议地址,是 IP Address 的缩写。IP 地址是 IP 协议提供的一种统一的地址格式,它为互联网上的每一个网络和每一台主机分配一个逻辑地址,以此来屏蔽物理地址的差异。IP 地址是一个 32 位的二进制数,比如 127.0.0.1 为本机 IP。

  1. 域名是什么?

域名就相当于 IP 地址乔装打扮的伪装者,带着一副面具。它的作用就是便于记忆和沟通的一组服务器的地址。

  1. 什么是域名解析?

DNS 协议提供通过域名查找 IP 地址,或逆向从 IP 地址反查域名的服务。

例如 baidu.com  220.114.23.56(服务器外网IP地址)80(服务器端口号)
  1. 浏览器如何通过域名查询对应的 IP 呢?
  • 浏览器缓存:浏览器会按照一定的频率缓存 DNS 记录。
  • 操作系统缓存:如果浏览器缓存中找不到需要的 DNS 记录,那就去操作系统中找。
  • 路由缓存:路由器也有 DNS 缓存。
  • ISP 的 DNS 服务器:ISP 是互联网服务提供商(Internet Service Provider)的简称,ISP 有专门的 DNS 服务器应对 DNS 查询请求。
  • 根服务器:ISP 的 DNS 服务器还找不到的话,它就会向根服务器发出请求,进行递归查询(DNS 服务器先问根域名服务器.com 域名服务器的 IP 地址,然后再问.baidu 域名服务器,依次类推)

  1. 总结

浏览器通过向 DNS 服务器发送域名,DNS 服务器查询到与域名相对应的 IP 地址,然后返回给浏览器,浏览器再将 IP 地址打在协议上,同时请求参数也会在协议搭载,然后一并发送给对应的服务器。接下来介绍向服务器发送 HTTP 请求阶段,HTTP 请求分为三个部分:TCP 三次握手、http 请求响应信息、关闭 TCP 连接。

四、TCP 三次握手

在客户端发送数据之前会发起 TCP 三次握手用以同步客户端和服务端的序列号和确认号,并交换 TCP 窗口大小信息。

  • TCP 三次握手的过程如下:
  1. 客户端发送一个带 SYN=1,Seq=X 的数据包到服务器端口(第一次握手,由浏览器发起,告诉服务器我要发送请求了)
  2. 服务器发回一个带 SYN=1, ACK=X+1, Seq=Y 的响应包以示传达确认信息(第二次握手,由服务器发起,告诉浏览器我准备接受了,你赶紧发送吧)
  3. 客户端再回传一个带 ACK=Y+1, Seq=Z 的数据包,代表“握手结束”(第三次握手,由浏览器发送,告诉服务器,我马上就发了,准备接受吧)
  • 为啥需要三次握手呢? 谢希仁著《计算机网络》中讲“三次握手”的目的是:“为了防止已失效的连接请求报文段突然又传送到了服务端,因而产生错误”。

五、发送 HTTP 请求

TCP 三次握手结束后,开始发送 HTTP 请求报文。

请求报文由请求行(request line)、请求头(header)、请求体三个部分组成;

  • 请求行:请求方法、URL、协议版本
  1. 请求方法包含 8 种:GET、POST、PUT、DELETE、PATCH、HEAD、OPTIONS、TRACE
  2. URL 即请求地址,由 <协议>://<主机>:<端口>/<路径>?<参数> 组成
  3. 协议版本即 http 版本号
比如:POST /chapter17/user.html HTTP/1.1
其中,POST”代表请求方法,“/chapter17/user.html”表示 URL,“HTTP/1.1”代表协议和协议的版本。现在比较流行的是 Http1.1 版本
  • 请求头中请求的附加信息

附加信息:由关键字/值对组成,每行一对,关键字和值用英文冒号“:”分隔。

请求头部通知服务器有关于客户端请求的信息。它包含许多有关的客户端环境和请求正文的有用信息。

协议头 说明 示例 状态
Accept 可接受的响应内容类型(Content-Types) Accept: text/plain 固定
Accept-Encoding 可接受的响应内容的编码方式 Accept-Encoding: gzip, deflate 固定
Accept-Charset 可接受的字符集 Accept-Charset: utf-8 固定
Accept-Language 可接受的响应内容语言列表 Accept-Language: en-US 固定
Accept-Datetime 可接受的按照时间来表示的响应内容版本 Accept-Datetime: Sat, 26 Dec 2015 17:30:00 GMT 固定
Authorization 用于表示 HTTP 协议中需要认证资源的认证信息 Authorization: Basic OSdjJGRpbjpvcGVuIANlc2SdDE== 固定
Cache-Control 用来指定当前的请求/回复中的,是否使用缓存机制 Cache-Control: no-cache 固定
Connection 客户端(浏览器)想要优先使用的连接类型 Connection: keep-aliveConnection: Upgrade 固定
Cookie 由之前服务器通过 Set-Cookie(见下文)设置的一个 HTTP 协议 Cookie Cookie: $Version=1; Skin=new; 固定
Content-Length 以 8 进制表示的请求体的长度 Content-Length: 348 固定
Content-Type 请求体的 MIME 类型 (用于 POST 和 PUT 请求中) Content-Type: application/x-www-form-urlencoded 固定
Date 发送该消息的日期和时间(以 RFC 7231 中定义的"HTTP 日期"格式来发送) Date: Dec, 26 Dec 2015 17:30:00 GMT 固定
Host 表示服务器的域名以及服务器所监听的端口号。如果所请求的端口是对应的服务的标准端口(80),则端口号可以省略 Host: www.itbilu.com:80 固定
If-Modified-Since 允许在对应的资源未被修改的情况下返回 304 未修改 If-Modified-Since: Dec, 26 Dec 2015 17:30:00 GMT 固定
If-Match 仅当客户端提供的实体与服务器上对应的实体相匹配时,才进行对应的操作。主要用于像 PUT 这样的方法中,仅当从用户上次更新某个资源后,该资源未被修改的情况下,才更新该资源 If-Match: "9jd00cdj34pss9ejqiw39d82f20d0ikd" 固定
If-None-Match 允许在对应的内容未被修改的情况下返回 304 未修改( 304 Not Modified ),参考 超文本传输协议 的实体标记 If-None-Match: "9jd00cdj34pss9ejqiw39d82f20d0ikd" 固定
Origin 发起一个针对跨域资源共享的请求(该请求要求服务器在响应中加入一个 Access-Control-Allow-Origin 的消息头,表示访问控制所允许的来源) Origin: http://www.itbilu.com 固定
Range 表示请求某个实体的一部分,字节偏移以 0 开始 Range: bytes=500-999 固定
Referer 表示浏览器所访问的前一个页面,可以认为是之前访问页面的链接将浏览器带到了当前页面。Referer 其实是 Referrer 这个单词,但 RFC 制作标准时给拼错了,后来也就将错就错使用 Referer 了 Referer: http://itbilu.com/nodejs 固定
User-Agent 浏览器的身份标识字符串 User-Agent: Mozilla/…… 固定
  • 请求体

请求体:可以承载多个请求参数的数据,包含回车符、换行符和请求数据,并不是所有请求都具有请求数据

name=tmc&password=123456
name、password为请求参数

六、服务器处理请求并返回 HTTP 报文

  • http 响应报文

响应报文由响应行(request line)、响应头部(header)、响应主体三个部分组成

  1. 响应行:协议版本,状态码,状态码描述 其中,状态码规则如下:
  • 1xx:指示信息--表示请求已接收,继续处理。
  • 2xx:成功--表示请求已被成功接收、理解、接受。
  • 3xx:重定向--要完成请求必须进行更进一步的操作。
  • 4xx:客户端错误--请求有语法错误或请求无法实现。
  • 5xx:服务器端错误--服务器未能实现合法的请求。
  1. 响应头部包含响应报文的附加信息,由 名/值 对组成
  2. 响应主体包含回车符、换行符和响应返回数据,并不是所有响应报文都有响应数据

七、浏览器解析渲染页面

浏览器解析渲染页面分为一下五个步骤:

  • 根据 HTML 解析出 DOM 树
  • 根据 CSS 解析生成 CSS 规则树
  • 结合 DOM 树和 CSS 规则树,生成渲染树
  • 根据渲染树计算每一个节点的信息
  • 根据计算好的信息绘制页面
  1. 根据 HTML 解析 DOM 树

根据 HTML 的内容,将标签按照结构解析成为 DOM 树,DOM 树解析的过程是一个深度优先遍历。即先构建当前节点的所有子节点,再构建下一个兄弟节点。 在读取 HTML 文档,构建 DOM 树的过程中,若遇到 script 标签,则 DOM 树的构建会暂停,直至脚本执行完毕。

  1. 根据 CSS 解析生成 CSS 规则树

解析 CSS 规则树时 js 执行将暂停,直至 CSS 规则树就绪。 浏览器在 CSS 规则树生成之前不会进行渲染。

  1. 结合 DOM 树和 CSS 规则树,生成渲染树

DOM 树和 CSS 规则树全部准备好了以后,浏览器才会开始构建渲染树。 精简 CSS 并可以加快 CSS 规则树的构建,从而加快页面相应速度。

  1. 根据渲染树计算每一个节点的信息 -- 布局

布局:通过渲染树中渲染对象的信息,计算出每一个渲染对象的位置和尺寸 回流:在布局完成后,发现了某个部分发生了变化影响了布局,那就需要倒回去重新渲染。

  1. 根据计算好的信息绘制页面

绘制阶段,系统会遍历呈现树,并调用呈现器的“paint”方法,将呈现器的内容显示在屏幕上。 重绘:某个元素的背景颜色,文字颜色等,不影响元素周围或内部布局的属性,将只会引起浏览器的重绘。 回流:某个元素的尺寸发生了变化,则需重新计算渲染树,重新渲染。

八、断开连接

当数据传送完毕,需要断开 tcp 连接,此时发起 tcp 四次挥手。

  • 发起方向被动方发送报文,Fin、Ack、Seq,表示已经没有数据传输了。并进入 FIN_WAIT_1 状态。(第一次挥手:由浏览器发起的,发送给服务器,我请求报文发送完了,你准备关闭吧)
  • 被动方发送报文,Ack、Seq,表示同意关闭请求。此时主机发起方进入 FIN_WAIT_2 状态。(第二次挥手:由服务器发起的,告诉浏览器,我请求报文接受完了,我准备关闭了,你也准备吧)
  • 被动方向发起方发送报文段,Fin、Ack、Seq,请求关闭连接。并进入 LAST_ACK 状态。(第三次挥手:由服务器发起,告诉浏览器,我响应报文发送完了,你准备关闭吧)
  • 发起方向被动方发送报文段,Ack、Seq。然后进入等待 TIME_WAIT 状态。被动方收到发起方的报文段以后关闭连接。发起方等待一定时间未收到回复,则正常关闭。(第四次挥手:由浏览器发起,告诉服务器,我响应报文接受完了,我准备关闭了,你也准备吧)

== 和 === 有什么区别?

=== 叫做严格相等,是指:左右两边不仅值要相等,类型也要相等

== 不像 === 那样严格,对于一般情况,只要值相等,就返回 true,但 == 还涉及一些类型转换,它的转换规则如下:

  • 两边的类型是否相同,相同的话就比较值的大小,例如 1==2,返回 false
  • 判断的是否是 null 和 undefined,是的话就返回 true
  • 判断的类型是否是 String 和 Number,是的话,把 String 类型转换成 Number,再进行比较
  • 判断其中一方是否是 Boolean,是的话就把 Boolean 转换成 Number,再进行比较
  • 如果其中一方为 Object,且另一方为 String、Number 或者 Symbol,会将 Object 转换成字符串,再进行比较
console.log({ a: 1 } == true) //false
console.log({ a: 1 } == '[object Object]') //true

注意:Object.is 修复了 +0 -0 NaN 的问题

对象转原始类型是根据什么流程运行的?

对象转原始类型,会调用内置的[ToPrimitive]函数,对于该函数而言,其逻辑如下:

  1. 如果 Symbol.toPrimitive()方法,优先调用再返回
  2. 调用 valueOf(),如果转换为原始类型,则返回
  3. 调用 toString(),如果转换为原始类型,则返回
  4. 如果都没有返回原始类型,会报错
var obj = {
  value: 3,
  valueOf() {
    return 4
  },
  toString() {
    return '5'
  },
  [Symbol.toPrimitive]() {
    return 6
  },
}
console.log(obj + 1) // 输出7

应用:如何让 if(a == 1 && a == 2)条件成立?

var a = {
  value: 0,
  valueOf: function() {
    this.value++
    return this.value
  },
}
console.log(a == 1 && a == 2) //true