Skip to content

高频面试真题

代码笔记

1. 简述

  • 验证和复习之前学过的知识
  • 在之前的知识体系里面补充其他技能
    • 占的比重不是很大,是纯api的东西,并不是成体系的
    • 游离于整体知识体系,但很重要
    • 如正则表达式、数组api等
  • 题目没有按照知识点或者难度排序(混排)
    • 面试题按照组来,每组三个
  • 只筛选了初级面试题,即本系列笔记知识体系之内的

2. 第1组面试题

2.1. 题目

  • var和let、const的区别
  • typeof返回哪些类型
  • 列举强制类型转换和隐式类型转换

2.2. var和let、const的区别

  • 答案
    • var是ES5及其之前的语法,let、const是es6语法;var有变量提升
    • var和let是变量,可修改;const是常量,不可修改
    • let、const有块级作用域,var没有
      • 块级作用域是es6的特点
  • 代码演示
    js
    /**
     * 变量提升 只有ES5及其以下有
     * es5语法只有用var定义变量,如果定义变量,
     * 他会把这个变量提前拎出来,并声明成undefined
     */
    // 使用var 
    // console.log(a) // undefined
    // var a = 200
    /*
      // 即变形为
      var a
      console.log(a) // undefined
      a = 200
    */
    
    // 使用let
    // console.log(a) // 报错
    // let a = 200
    
    /**
     * 块级作用域 es6特有
     */
    for (let i = 0; i < 10; i++) {
      let j = i + 1
      // var j = i + 1  // 有值
    }
    console.log(j)
    /**
     * 变量提升 只有ES5及其以下有
     * es5语法只有用var定义变量,如果定义变量,
     * 他会把这个变量提前拎出来,并声明成undefined
     */
    // 使用var 
    // console.log(a) // undefined
    // var a = 200
    /*
      // 即变形为
      var a
      console.log(a) // undefined
      a = 200
    */
    
    // 使用let
    // console.log(a) // 报错
    // let a = 200
    
    /**
     * 块级作用域 es6特有
     */
    for (let i = 0; i < 10; i++) {
      let j = i + 1
      // var j = i + 1  // 有值
    }
    console.log(j)
  • 延伸
    • 函数表达式有些类似变量提升,这个后面再说

2.3. typeof返回哪些类型

  • 答案
    • 值类型:undefined、string、number、boolean、symbol
    • 引用类型:object => 无法具体细分(对象或者数组)
      • 注意:typeof null === 'object'
    • 方法:function
      • 具有引用类型特点,但一般不作为引用类型的数据使用
      • 因为function是作为一个可执行的工具去使用的
      • 一般做数据存储或者变量定义的时候,我们一般会定义值类型或者引用类型(对象或者数组),存储代码中的变量或者说是数据
      • 很少在函数里面存储数据,函数是一个可执行的工具

2.4. 列举强制类型转换和隐式类型转换

  • 答案:
    • 强制类型转换:parseInt、parseFloat、toString等
    • 隐式类型转换:if、逻辑运算、==、+拼接字符串

3. 第2组面试题

3.1. 题目

  • 手写深度比较、模拟lodash isEqual
    • 两个地址不一样对象,层次比较深,怎么去比较,可能里面属性是一样的
  • split()和join()的区别
    • 基本字符串操作
  • 数组的pop、push、unshift、shift分别做什么
    • 基本数组操作

3.2. 手写深度比较

  • 场景
    js
    // 实现如下效果
    const obj1 = {a:10, b:{x:100, y:200}}
    const obj2 = {a:10, b:{x:100, y:200}}
    isEqual(obj1, obj2) === true
    // 实现如下效果
    const obj1 = {a:10, b:{x:100, y:200}}
    const obj2 = {a:10, b:{x:100, y:200}}
    isEqual(obj1, obj2) === true
  • 答案
    js
    const obj1 = {a:10, b:{x:100, y:200}}
    const obj2 = {a:10, b:{x:100, y:200}}
    // console.log(obj1, obj2);  // false
    
    // 判断是否是对象或数组
    const isObj = obj=> {
      return typeof obj === 'object' && obj !== null
    }
    
    // 实现对象深度比较
    const isEqual = (obj1, obj2)=> {
      // console.log(obj1, obj2)
    
      // 判断参数是否是对象
      // 也在递归的时候用
      // isEqual(100, 100)
      if(!isObj(obj1) || !isObj(obj2)) {
        // 参与equal的一般不是函数,一般是数据
        // 不是对象就是值类型,可以直接比较
        return obj1 === obj2
      }
    
      // 如果两个对象相等(obj1 === obj1)
      if(obj1 === obj2) {
        return true
      }
    
      // 两个都是引用类型(对象或数组),且两者地址不同
      // 深度比较全相等
      // 步骤:
      /**
       * 1. 先取出obj1和obj2的keys,比较个数,个数不一样直接false
       * 2. 个数相等,以obj1为基准,和obj2依次递归比较
       * 3. 全部遍历完,没有遇到false,就是全相等
       */
      // 取出keys
      // 如果是数组,可以获取它的索引下标
      const obj1Keys = Object.keys(obj1)
      const obj2Keys = Object.keys(obj2)
    
      // 个数不相等
      if(obj1Keys.length !== obj2Keys.length) {
        return false
      }
    
      // 个数相等,以obj1为基准,和obj2依次递归比较
      // for in 对象数组都适用
      for(let key in obj1) {
        // 比较当前key的value => 递归
        // 拿到obj1里面的值,和obj2里面的值,进入该方法
        // 该方法里面有值的比较
        // isEqual(100, 100)
        // 拿到值比较的布尔值
        // 遇到对象,再进行遍历,再到值的比较
        const res = isEqual(obj1[key], obj2[key])
    
        // 有值不相等
        if(!res) {
          return false
        }
      }
    
      // 遍历完没有遇到false,就是全相等
      return true
    }
    
    // 结果
    console.log("实现对象深度比较", isEqual(obj1, obj2))  // true
    const obj1 = {a:10, b:{x:100, y:200}}
    const obj2 = {a:10, b:{x:100, y:200}}
    // console.log(obj1, obj2);  // false
    
    // 判断是否是对象或数组
    const isObj = obj=> {
      return typeof obj === 'object' && obj !== null
    }
    
    // 实现对象深度比较
    const isEqual = (obj1, obj2)=> {
      // console.log(obj1, obj2)
    
      // 判断参数是否是对象
      // 也在递归的时候用
      // isEqual(100, 100)
      if(!isObj(obj1) || !isObj(obj2)) {
        // 参与equal的一般不是函数,一般是数据
        // 不是对象就是值类型,可以直接比较
        return obj1 === obj2
      }
    
      // 如果两个对象相等(obj1 === obj1)
      if(obj1 === obj2) {
        return true
      }
    
      // 两个都是引用类型(对象或数组),且两者地址不同
      // 深度比较全相等
      // 步骤:
      /**
       * 1. 先取出obj1和obj2的keys,比较个数,个数不一样直接false
       * 2. 个数相等,以obj1为基准,和obj2依次递归比较
       * 3. 全部遍历完,没有遇到false,就是全相等
       */
      // 取出keys
      // 如果是数组,可以获取它的索引下标
      const obj1Keys = Object.keys(obj1)
      const obj2Keys = Object.keys(obj2)
    
      // 个数不相等
      if(obj1Keys.length !== obj2Keys.length) {
        return false
      }
    
      // 个数相等,以obj1为基准,和obj2依次递归比较
      // for in 对象数组都适用
      for(let key in obj1) {
        // 比较当前key的value => 递归
        // 拿到obj1里面的值,和obj2里面的值,进入该方法
        // 该方法里面有值的比较
        // isEqual(100, 100)
        // 拿到值比较的布尔值
        // 遇到对象,再进行遍历,再到值的比较
        const res = isEqual(obj1[key], obj2[key])
    
        // 有值不相等
        if(!res) {
          return false
        }
      }
    
      // 遍历完没有遇到false,就是全相等
      return true
    }
    
    // 结果
    console.log("实现对象深度比较", isEqual(obj1, obj2))  // true

3.3. split()和join()的区别

  • 答案
    js
    // 将字符串以-分割,形成数组
    let arrRes = '1-2-2'.split('-')
    console.log(arrRes)  // ["1", "2", "2"]
    
    // 将数组以-拼接,形成字符串
    let strRes= [1, 2, 3].join('-')
    console.log(strRes);  // 1-2-3
    // 将字符串以-分割,形成数组
    let arrRes = '1-2-2'.split('-')
    console.log(arrRes)  // ["1", "2", "2"]
    
    // 将数组以-拼接,形成字符串
    let strRes= [1, 2, 3].join('-')
    console.log(strRes);  // 1-2-3

3.4. 数组的pop、push、unshift、shift分别做什么

  • 解答思路
    • 功能是什么
    • 返回值是什么
    • 是否会对原数组造成影响
  • 答案
    • pop
      • 刨除数组最后一项
      • 返回数组最后一项
      • 会改变原数组
    • shift
      • 刨除数组第一项
      • 返回数组第一项
      • 会改变原数组
    • push
      • 往后追加
      • 返回 length
      • 会改变原数组
    • unshift
      • 往前追加
      • 返回 length
      • 会改变原数组
  • 延伸
    • 数组操作分纯函数和非纯函数
    • 纯函数的要求
      • 不改变源数组(没有副作用)
      • 有返回值,且函数的返回结果只依赖于它的参数
      • 纯函数在react里面是一个特别重要的概念
    • 数组的api有哪些是纯函数
      • concat
      • map
      • filter
      • slice
    • 数组的api有哪些是非纯函数
      • push pop shift unshift
      • forEach => 它没有返回一个数组,没什么返回值
      • some => 不会改变原来数组的值,也不会返回值
      • every => 不会改变原来数组的值,也不会返回值
      • reduce => 不会改变原来数组的值,也不会返回值
    • 手写demo传送门
    js
    // 定义数组
    const arr = [10, 20, 30, 40]
    
    /**
     * 面试题
     */
    // pop
    // 刨除数组最后一项并返回
    // 会改变原数组
    const popRes = arr.pop()
    console.log("pop", popRes, arr)  // 40  // [10, 20, 30]
    
    // shift
    // 刨除数组第一项并返回
    // 会改变原数组
    const shiftRes = arr.shift()
    console.log("shift", shiftRes, arr)  // 10  // [20, 30, 40]
    
    // push
    // 往后追加
    // 返回 length
    // 会改变原数组
    const pushRes = arr.push(50)
    console.log("push", pushRes, arr)  // 5  // [10, 20, 30, 40, 50]
    
    // unshift
    // 往前追加
    // 返回 length
    // 会改变原数组
    const unshiftRes = arr.unshift(5)
    console.log("unshift", unshiftRes, arr)  // 5  // [5, 10, 20, 30, 40]
    
    /**
     * 扩展 - 纯函数
     * 
     * 纯函数在react中应用广泛
     * 纯函数的要求
     * 1. 不改变源数组(没有副作用)
     * 2. 有返回值,且函数的返回结果只依赖于它的参数
     */
    // concat
    // 追加数组
    const arr1 = arr.concat([50, 60, 70])
    // [5, 20, 30, 50, 50, 60, 70]
    console.log("纯函数-concat", arr1, arr);  // 原数组不变
    
    // map
    // 遍历
    const arr2 = arr.map(num => num * 10)
    // [50, 200, 300, 500]
    console.log("纯函数-map", arr2, arr);  // 原数组不变
    
    // filter
    // 过滤数组
    const arr3 = arr.filter(num => num > 25)
    // [30, 50]
    console.log("纯函数-filter", arr3, arr);  // 原数组不变
    
    // slice
    // 截取数组 => 不传参数类似于深拷贝
    const arr4 = arr.slice()
    // [5, 20, 30, 50]
    console.log("纯函数-slice", arr4, arr);  // 原数组不变
    
    /**
     *  扩展 - 非纯函数
     */
    // push pop shift unshift 示例见上
    // 不会改变原来数组的值,也不会返回值
    // forEach
    // some 
    // every
    // reduce
    // 定义数组
    const arr = [10, 20, 30, 40]
    
    /**
     * 面试题
     */
    // pop
    // 刨除数组最后一项并返回
    // 会改变原数组
    const popRes = arr.pop()
    console.log("pop", popRes, arr)  // 40  // [10, 20, 30]
    
    // shift
    // 刨除数组第一项并返回
    // 会改变原数组
    const shiftRes = arr.shift()
    console.log("shift", shiftRes, arr)  // 10  // [20, 30, 40]
    
    // push
    // 往后追加
    // 返回 length
    // 会改变原数组
    const pushRes = arr.push(50)
    console.log("push", pushRes, arr)  // 5  // [10, 20, 30, 40, 50]
    
    // unshift
    // 往前追加
    // 返回 length
    // 会改变原数组
    const unshiftRes = arr.unshift(5)
    console.log("unshift", unshiftRes, arr)  // 5  // [5, 10, 20, 30, 40]
    
    /**
     * 扩展 - 纯函数
     * 
     * 纯函数在react中应用广泛
     * 纯函数的要求
     * 1. 不改变源数组(没有副作用)
     * 2. 有返回值,且函数的返回结果只依赖于它的参数
     */
    // concat
    // 追加数组
    const arr1 = arr.concat([50, 60, 70])
    // [5, 20, 30, 50, 50, 60, 70]
    console.log("纯函数-concat", arr1, arr);  // 原数组不变
    
    // map
    // 遍历
    const arr2 = arr.map(num => num * 10)
    // [50, 200, 300, 500]
    console.log("纯函数-map", arr2, arr);  // 原数组不变
    
    // filter
    // 过滤数组
    const arr3 = arr.filter(num => num > 25)
    // [30, 50]
    console.log("纯函数-filter", arr3, arr);  // 原数组不变
    
    // slice
    // 截取数组 => 不传参数类似于深拷贝
    const arr4 = arr.slice()
    // [5, 20, 30, 50]
    console.log("纯函数-slice", arr4, arr);  // 原数组不变
    
    /**
     *  扩展 - 非纯函数
     */
    // push pop shift unshift 示例见上
    // 不会改变原来数组的值,也不会返回值
    // forEach
    // some 
    // every
    // reduce

4. 第3组面试题

4.1. 题目

  • 数组slice和splice的区别
  • [10, 20, 30].map(parseInt)返回结果是什么
  • ajax请求get和post的区别

4.2. 数组slice和splice的区别

  • 答案
    • 功能区别
      • slice => 切片
      • splice => 剪接
    • 参数和返回值
      • slice
        • slice的参数为数组下标(从开始下标到结束下标)
        • 返回值为数组,不改变原数组
      • splice
        • 第一个参数表示开始的下标,第二个参数表示长度,后面的参数是替换的内容
        • 返回值是数组,会改变原数组
    • 是否是纯函数
      • slice是纯函数
      • splice不是纯函数
    • 手写demo传送门
    js
    // 定义数组
    const arr = [10, 20, 30, 40]
    
    /**
     * slice
     */
    // 不传参数类似于深拷贝
    // 是一个纯函数
    // 不改变源数组(没有副作用)
    // 有返回值,且函数的返回结果只依赖于它的参数
    let arr1 = arr.slice()
    console.log("不传参,类似深拷贝", arr1);  // [10, 20, 30, 40]
    
    // 从下标1开始截取,截取到下标3
    // arr.slice(startIndex, endIndex)
    let arr2 = arr.slice(1, 3)
    console.log("传参,开始和结束索引", arr2);  // [20, 30]
    
    // 如果不写结束索引,表示截取到最后
    // 从下标为1的索引开始截,截到最后
    let arr3 = arr.slice(1)
    console.log("只传一个参数,从某个索引截到最后", arr3);  // [20, 30, 40]
    
    // 从末尾开始截,截最后两个
    let arr4 = arr.slice(-2)
    console.log("从末尾开始截,截最后两个", arr4);  // [30, 40]
    
    /**
     *  splice
     */
    // 第一个参数表示开始的下标
    // 第二个参数表示长度
    // 后面的参数是替换的内容
    // 前面的两个参数表示需要被替换的区域
    // 后面的参数表示当前区域要被替换的内容
    // splice(startIndex, length, 替换的内容)
    // 相当于剪切,找到数组需要被剪切的范围,然后粘贴
    let spliceRes1 = arr.splice(1, 2, 'a', 'b', 'c')
    console.log("splice用法,相当于剪切粘贴", spliceRes1, arr)  // [20, 30]  // [10, "a", "b", "c", 40]
    
    // 如果后面参数不填,相当于删除中间部分
    arr = [10, 20, 30, 40]  // 因为上面的数组已经被修改了,这里重新定义
    let spliceRes2 = arr.splice(1, 2)
    console.log("只传两个参数,相当于删除", spliceRes2, arr)  // [20, 30]  // [10, 40]
    
    // 如果不需要移除,从某个位置添加若干元素
    arr = [10, 20, 30, 40]  // 因为上面的数组已经被修改了,这里重新定义
    let spliceRes3 = arr.splice(1, 0, 666, 2333, 555)
    console.log("不需要移除,从某个位置添加若干元素", spliceRes3, arr)  // []  // [10, 666, 2333, 555, 20, 30, 40]
    // 定义数组
    const arr = [10, 20, 30, 40]
    
    /**
     * slice
     */
    // 不传参数类似于深拷贝
    // 是一个纯函数
    // 不改变源数组(没有副作用)
    // 有返回值,且函数的返回结果只依赖于它的参数
    let arr1 = arr.slice()
    console.log("不传参,类似深拷贝", arr1);  // [10, 20, 30, 40]
    
    // 从下标1开始截取,截取到下标3
    // arr.slice(startIndex, endIndex)
    let arr2 = arr.slice(1, 3)
    console.log("传参,开始和结束索引", arr2);  // [20, 30]
    
    // 如果不写结束索引,表示截取到最后
    // 从下标为1的索引开始截,截到最后
    let arr3 = arr.slice(1)
    console.log("只传一个参数,从某个索引截到最后", arr3);  // [20, 30, 40]
    
    // 从末尾开始截,截最后两个
    let arr4 = arr.slice(-2)
    console.log("从末尾开始截,截最后两个", arr4);  // [30, 40]
    
    /**
     *  splice
     */
    // 第一个参数表示开始的下标
    // 第二个参数表示长度
    // 后面的参数是替换的内容
    // 前面的两个参数表示需要被替换的区域
    // 后面的参数表示当前区域要被替换的内容
    // splice(startIndex, length, 替换的内容)
    // 相当于剪切,找到数组需要被剪切的范围,然后粘贴
    let spliceRes1 = arr.splice(1, 2, 'a', 'b', 'c')
    console.log("splice用法,相当于剪切粘贴", spliceRes1, arr)  // [20, 30]  // [10, "a", "b", "c", 40]
    
    // 如果后面参数不填,相当于删除中间部分
    arr = [10, 20, 30, 40]  // 因为上面的数组已经被修改了,这里重新定义
    let spliceRes2 = arr.splice(1, 2)
    console.log("只传两个参数,相当于删除", spliceRes2, arr)  // [20, 30]  // [10, 40]
    
    // 如果不需要移除,从某个位置添加若干元素
    arr = [10, 20, 30, 40]  // 因为上面的数组已经被修改了,这里重新定义
    let spliceRes3 = arr.splice(1, 0, 666, 2333, 555)
    console.log("不需要移除,从某个位置添加若干元素", spliceRes3, arr)  // []  // [10, 666, 2333, 555, 20, 30, 40]

4.3. [10, 20, 30].map(parseInt)返回结果是什么

  • 解题思路
    • map的参数和返回值
      • map的参数是一个函数,返回值是一个数组
      • map参数中函数的参数,一个是item,一个是index
    • parseInt的参数和返回值
      • parseInt的参数第一个是具体的数和第二个是进制位
      • 返回值是一个整型
  • 答案
    js
    // 他实际上算是简写
    // console.log([10, 20, 30].map(parseInt))  // [10, NaN, NaN]
    
    // 拆解 - 两个写法是一样的
    let res = [10, 20, 30].map((num, index)=> {
      // 第一个参数是数字,第二个参数是进制
      // 进制0和10是一样的
      // parseInt(10, 0)  // 10
      // parseInt(20, 1)  // NaN
      // parseInt(30, 2)  // NaN
      return parseInt(num, index)
    })
    console.log(res)
    // 他实际上算是简写
    // console.log([10, 20, 30].map(parseInt))  // [10, NaN, NaN]
    
    // 拆解 - 两个写法是一样的
    let res = [10, 20, 30].map((num, index)=> {
      // 第一个参数是数字,第二个参数是进制
      // 进制0和10是一样的
      // parseInt(10, 0)  // 10
      // parseInt(20, 1)  // NaN
      // parseInt(30, 2)  // NaN
      return parseInt(num, index)
    })
    console.log(res)

4.4. ajax请求get和post的区别

  • get一般用于查询操作,post一般用于用户提交操作
  • get参数拼接在url上,post放在请求体内(数据体积可更大)
  • 安全性:post易于预防CSRF

5. 第4组面试题

5.1. 题目

  • 函数call和apply的区别
  • 事件代理(委托)是什么
  • 闭包是什么,有什么特性,有什么负面影响

5.2. 函数call和apply的区别

  • 答案
    • 他们的区别主要在参数上
    • 第一个参数是this,是一样的
    • call第二个的参数开始是一个一个拆分传进去的,即参数列表
    • 第一个参数是this要指向的对象,第二个参数是数组或类数组
      • fn.call(this, p1, p2, p3)
      • fn.apply(this, arguments)
    • 两个可以相互转换,但为了方便,js做了这两种形式
    js
    function fn(x, y){
      console.log(x, y)  // 
      console.log(this) // this是指obj
    }
    
    let obj = {
      a: "obj对象"
    }
    
    // 将this指向变成obj
    fn(1, 2)  // window, 1, 2
    fn.apply(obj, [1,2]) // 第二个参数是数组  // obj, 1, 2
    fn.call(obj, 1, 2) // 第二个参数开始时参数列表  // obj, 1, 2
    fn.bind(obj, 1, 2)()  // 和call一样,但是无法直接执行
    function fn(x, y){
      console.log(x, y)  // 
      console.log(this) // this是指obj
    }
    
    let obj = {
      a: "obj对象"
    }
    
    // 将this指向变成obj
    fn(1, 2)  // window, 1, 2
    fn.apply(obj, [1,2]) // 第二个参数是数组  // obj, 1, 2
    fn.call(obj, 1, 2) // 第二个参数开始时参数列表  // obj, 1, 2
    fn.bind(obj, 1, 2)()  // 和call一样,但是无法直接执行
  • 延伸
    • call()的第一个参数是this要指向的对象,后面传入的是参数列表,参数可以是任意类型,当第一个参数为null、undefined的时候,默认指向window
    • call()改过this的指向后,会再执行函数,bind()改过this后,不执行函数,会返回一个绑定新this的函数
    • 即bind的传参和call一样,都是一个一个拆分传进去的,但是bind无法直接执行
    • 在判断数据类形式使用typeof,一般不是太准确的,我们可以使用Object.prototype.toString.call()方法来判断一个数据的数据类型
      • console.log(Object.prototype.toString.call(12)) // [object Number]
    • call、apply、bind的应用
      js
      // call()、apply()、bind() 都是用来重定义 this 这个对象的
      
      /**
       * 面试题
       */
      function fn(x, y){
        console.log(x, y)  // 
        console.log(this) // this是指obj
      }
      
      let obj = {
        a: "obj对象"
      }
      
      // 将this指向变成obj
      fn(1, 2)  // window, 1, 2
      fn.apply(obj, [1,2]) // 第二个参数是数组  // obj, 1, 2
      fn.call(obj, 1, 2) // 第二个参数开始时参数列表  // obj, 1, 2
      fn.bind(obj, 1, 2)()  // 和call一样,但是无法直接执行
      
      /**
       * call()的应用
       */
      // 利用call()判断数据类型
      console.log(Object.prototype.toString.call("qq"))            // [Object String] 返回值都是字符串类型
      console.log(Object.prototype.toString.call(12))              // [object Number]
      console.log(Object.prototype.toString.call(false))           // [object Boolean]
      console.log(Object.prototype.toString.call(undefined))       // [object Undefined]
      console.log(Object.prototype.toString.call(null))            // [object Null]
      console.log(Object.prototype.toString.call(function(){}))    // [object Function]
      console.log(Object.prototype.toString.call([]))              // [object Array]
      console.log(Object.prototype.toString.call({}))              // [object Object]
      
      // 封装
      let getType = a=> {
        let obj = Object.prototype.toString.call(a); // 区分对象类型  确定当前的数据的类型
        let sub = obj.substr(8); 
      
        // stringObject.substr(start,length)  start 要抽取的子符串的起始下标,
        // length 截取的长度,如果不写则表示从start开始截取到最后 ,stringObject表示某一字符串
        let len = sub.length;
        sub = sub.substr(0,len-1)
        let rs =  sub.toLowerCase(sub) //转换成小写
        return rs ;
      }
      console.log(getType([])); // array
      
      // 利用call()翻转字符串
      // 思路:将字符串转化为数组,借用数组中的reverse,将字符串翻转过来
      let str = "abcdefg";
      // 方法一:这种方法内有使用call()
      let arr =  Array.from(str).reverse().join("") // 将字符串转化为数组,在进行翻转,然后在进行拼接
      console.log(arr, typeof arr) // gfedcba string
      // 方法二:
      let rs = Array.prototype.reverse.call(str.split("")).join(""); 
      // splice(start,length)方法用于把一个字符串分割成字符串数组,start 表示从指定的地方分割字符串,length表示分割的长度。
      // 返回一个一个字符串数组,如果把空字符串 ("") 用为参数那么字符串中的每个字符之间都会被分割
      console.log(rs, typeof arr) // gfedcba string
      
      // 利用apply()求最大值
      // apply()所执行的操作:1.执行Math.max(1,2,3,5,4) 2.把内部的this改成arr
      var arr1 =[2,6,8,3,4,9,7,23,56,889]; 
      // 改变this指向到math.max,给math.max传值
      console.log(Math.max.apply(arr1, arr1))  // 第一个arr表示让arr借用max这个方法,第二个arr表示传给max的数据
      // call()、apply()、bind() 都是用来重定义 this 这个对象的
      
      /**
       * 面试题
       */
      function fn(x, y){
        console.log(x, y)  // 
        console.log(this) // this是指obj
      }
      
      let obj = {
        a: "obj对象"
      }
      
      // 将this指向变成obj
      fn(1, 2)  // window, 1, 2
      fn.apply(obj, [1,2]) // 第二个参数是数组  // obj, 1, 2
      fn.call(obj, 1, 2) // 第二个参数开始时参数列表  // obj, 1, 2
      fn.bind(obj, 1, 2)()  // 和call一样,但是无法直接执行
      
      /**
       * call()的应用
       */
      // 利用call()判断数据类型
      console.log(Object.prototype.toString.call("qq"))            // [Object String] 返回值都是字符串类型
      console.log(Object.prototype.toString.call(12))              // [object Number]
      console.log(Object.prototype.toString.call(false))           // [object Boolean]
      console.log(Object.prototype.toString.call(undefined))       // [object Undefined]
      console.log(Object.prototype.toString.call(null))            // [object Null]
      console.log(Object.prototype.toString.call(function(){}))    // [object Function]
      console.log(Object.prototype.toString.call([]))              // [object Array]
      console.log(Object.prototype.toString.call({}))              // [object Object]
      
      // 封装
      let getType = a=> {
        let obj = Object.prototype.toString.call(a); // 区分对象类型  确定当前的数据的类型
        let sub = obj.substr(8); 
      
        // stringObject.substr(start,length)  start 要抽取的子符串的起始下标,
        // length 截取的长度,如果不写则表示从start开始截取到最后 ,stringObject表示某一字符串
        let len = sub.length;
        sub = sub.substr(0,len-1)
        let rs =  sub.toLowerCase(sub) //转换成小写
        return rs ;
      }
      console.log(getType([])); // array
      
      // 利用call()翻转字符串
      // 思路:将字符串转化为数组,借用数组中的reverse,将字符串翻转过来
      let str = "abcdefg";
      // 方法一:这种方法内有使用call()
      let arr =  Array.from(str).reverse().join("") // 将字符串转化为数组,在进行翻转,然后在进行拼接
      console.log(arr, typeof arr) // gfedcba string
      // 方法二:
      let rs = Array.prototype.reverse.call(str.split("")).join(""); 
      // splice(start,length)方法用于把一个字符串分割成字符串数组,start 表示从指定的地方分割字符串,length表示分割的长度。
      // 返回一个一个字符串数组,如果把空字符串 ("") 用为参数那么字符串中的每个字符之间都会被分割
      console.log(rs, typeof arr) // gfedcba string
      
      // 利用apply()求最大值
      // apply()所执行的操作:1.执行Math.max(1,2,3,5,4) 2.把内部的this改成arr
      var arr1 =[2,6,8,3,4,9,7,23,56,889]; 
      // 改变this指向到math.max,给math.max传值
      console.log(Math.max.apply(arr1, arr1))  // 第一个arr表示让arr借用max这个方法,第二个arr表示传给max的数据

5.3. 事件代理(委托)是什么

  • 答案
    • 我们在上层容器去(父级)定义一个事件
    • 根据冒泡机制和事件对象(e.target)去获取子集列表的元素
    • 使用stopPropagation取消冒泡
    • 手写demo传送门
    js
    const p1 = document.getElementById('p1')
    const body = document.body
    
    // 通用的绑定函数 - 完整版
    /* let bindEvent = (elem, type, selector, fn)=> {
        if(fn == null) {
          fn = selector
          selector = null
        }
    
        // 绑定
        elem.addEventListener(type, event=> {
          const target = event.target  // 我们当前触发的元素
          // console.log("当前触发的元素", target)
    
          if(selector) {
            console.log("代理绑定");
    
            if(target.matches(selector)) {
              fn.call(target, event)
            }
          } else {
            console.log("普通绑定");
    
            fn.call(target, event)
          }
        })
    } */
    
    // 通用的绑定函数 - 简易版
    let bindEvent = (elem, type, fn)=> {
      elem.addEventListener(type, fn)
    }
    
    // p1
    bindEvent(p1, 'click', event => {
      event.stopPropagation() // 注释掉这一行可以体会冒泡
      console.log('激活')
      console.log(event.target, event.target.textContent)  // 需要知道是谁触发的
    })
    
    // body
    bindEvent(body, 'click', event => {
      console.log('取消')
      console.log(event.target, event.target.textContent)  // 需要知道是谁触发的
    })
    const p1 = document.getElementById('p1')
    const body = document.body
    
    // 通用的绑定函数 - 完整版
    /* let bindEvent = (elem, type, selector, fn)=> {
        if(fn == null) {
          fn = selector
          selector = null
        }
    
        // 绑定
        elem.addEventListener(type, event=> {
          const target = event.target  // 我们当前触发的元素
          // console.log("当前触发的元素", target)
    
          if(selector) {
            console.log("代理绑定");
    
            if(target.matches(selector)) {
              fn.call(target, event)
            }
          } else {
            console.log("普通绑定");
    
            fn.call(target, event)
          }
        })
    } */
    
    // 通用的绑定函数 - 简易版
    let bindEvent = (elem, type, fn)=> {
      elem.addEventListener(type, fn)
    }
    
    // p1
    bindEvent(p1, 'click', event => {
      event.stopPropagation() // 注释掉这一行可以体会冒泡
      console.log('激活')
      console.log(event.target, event.target.textContent)  // 需要知道是谁触发的
    })
    
    // body
    bindEvent(body, 'click', event => {
      console.log('取消')
      console.log(event.target, event.target.textContent)  // 需要知道是谁触发的
    })

5.4. 闭包是什么,有什么特性,有什么负面影响

  • 答案
    • 闭包是什么
      • 回顾作用域和自由变量
      • 回顾闭包应用场景
        • 函数作为参数被传入
        • 函数作为返回值被返回
      • 自由变量的查找,要在函数定义的地方(作用域)(不是执行的地方)
      js
      /**
       * 理解闭包
       *
       * 跨了自己的作用域的变量都叫自由变量
       * 
       * js链式作用域:子对象会一级一级向上寻找所有父对象的变量,反之不行
       * f2可以读取f1中的变量,只要把f2作为返回值,就可以在f1外读取f1内部变量
       * 
       * 即闭包可用理解为
       * 能够读取其他函数内部变量的函数。
       * 或简单理解为定义在一个函数内部的函数,内部函数持有外部函数内变量的引用
       * 
       * 闭包用途
       * 1、读取函数内部的变量
       * 2、让这些变量的值始终保持在内存中。不会在f1调用后被自动清除。
       * 3、方便调用上下文的局部变量。利于代码封装。
       * 原因:f1是f2的父函数,f2被赋给了一个全局变量,f2始终存在内存中,f2的存在依赖f1,因此f1也始终存在内存中,不会在调用结束后,被垃圾回收机制回收。
       */
      function f1(){
        let n = 123;
      
        function f2(){    // f2是一个闭包
          console.log(n)  // 根据自由变量定义,可以拿到f1的变量
        }    
      
        return f2;
      }
      
      let res = f1()  // res是一个方法
      console.log(res)
      res()  // 执行res可用获取f1变量值
      /**
       * 理解闭包
       *
       * 跨了自己的作用域的变量都叫自由变量
       * 
       * js链式作用域:子对象会一级一级向上寻找所有父对象的变量,反之不行
       * f2可以读取f1中的变量,只要把f2作为返回值,就可以在f1外读取f1内部变量
       * 
       * 即闭包可用理解为
       * 能够读取其他函数内部变量的函数。
       * 或简单理解为定义在一个函数内部的函数,内部函数持有外部函数内变量的引用
       * 
       * 闭包用途
       * 1、读取函数内部的变量
       * 2、让这些变量的值始终保持在内存中。不会在f1调用后被自动清除。
       * 3、方便调用上下文的局部变量。利于代码封装。
       * 原因:f1是f2的父函数,f2被赋给了一个全局变量,f2始终存在内存中,f2的存在依赖f1,因此f1也始终存在内存中,不会在调用结束后,被垃圾回收机制回收。
       */
      function f1(){
        let n = 123;
      
        function f2(){    // f2是一个闭包
          console.log(n)  // 根据自由变量定义,可以拿到f1的变量
        }    
      
        return f2;
      }
      
      let res = f1()  // res是一个方法
      console.log(res)
      res()  // 执行res可用获取f1变量值
    • 闭包有什么负面影响
      • 变量会常驻内存,得不到释放 => 闭包不能乱用
      • 变量会常驻内存,并不一定是内存泄漏,闭包有可能造成内存泄漏,但不是一定会造成内存泄漏
      • 内存泄漏指的是,变量或者数据,在内存中,没有用了,应该被释放,但没有被释放
    • 闭包特性
      • 变量或者对象,在闭包中,他有可能是会被用到的,我们判断不了他未来是不是会被用到,所以我们不去释放它
      • 这不是一个bug,内存泄漏一般都是由bug造成的,但闭包是我们没法判断那个闭包的变量未来是否可用
  • 延伸
    js
    /**
     * 闭包与内存
     */
    // 自由变量示例 - 内存会被释放
    // 这段代码执行完,所有的内存都释放了
    let a = 0 // fn1函数执行完,a变量和fn1就被释放
    function fn1() {
      let a1 = 100  // fn2函数执行完,a1变量和fn2被释放
    
      function fn2() {
        let a2 = 200  // fn3函数执行完,a2、a3变量、fn3被释放
    
        function fn3() {
          let a3 = 300
    
          // 在这一步,所有的变量和方法还没有被释放调
          // 因为这些变量都还没用过
          return a + a1 + a2 + a3
        }
        fn3()
      }
      fn2()
    }
    fn1()
    
    // 闭包 - 函数作为返回值 - 内存不会被释放
    const create = ()=> {
      let a = 100
    
      // 函数作为返回值被返回
      return function() {
        // a在父级作用域定义,在子方法作用域被使用,跨作用域,属于自由变量
        // 由于这里使用了a这个自由变量,那么父作用域的a就不能被释放了
        // 这个a的定义必须要和方法(闭包作用域)一起被return
        // 这个a已经是闭包的变量了
        console.log(a)  
      }
    }
    
    let fn = create()
    a = 200  // 这个没有人用它,可以被释放
    fn()  // 100
    
    // 闭包 - 函数作为参数被传入
    let print = fn2=> {
      let b = 200  // 这里没有使用,会被释放
      fn2()
    }
    let b = 100
    let fn2 = ()=> {
      console.log("b", b)
    }
    print(fn2)  // 100
    /**
     * 闭包与内存
     */
    // 自由变量示例 - 内存会被释放
    // 这段代码执行完,所有的内存都释放了
    let a = 0 // fn1函数执行完,a变量和fn1就被释放
    function fn1() {
      let a1 = 100  // fn2函数执行完,a1变量和fn2被释放
    
      function fn2() {
        let a2 = 200  // fn3函数执行完,a2、a3变量、fn3被释放
    
        function fn3() {
          let a3 = 300
    
          // 在这一步,所有的变量和方法还没有被释放调
          // 因为这些变量都还没用过
          return a + a1 + a2 + a3
        }
        fn3()
      }
      fn2()
    }
    fn1()
    
    // 闭包 - 函数作为返回值 - 内存不会被释放
    const create = ()=> {
      let a = 100
    
      // 函数作为返回值被返回
      return function() {
        // a在父级作用域定义,在子方法作用域被使用,跨作用域,属于自由变量
        // 由于这里使用了a这个自由变量,那么父作用域的a就不能被释放了
        // 这个a的定义必须要和方法(闭包作用域)一起被return
        // 这个a已经是闭包的变量了
        console.log(a)  
      }
    }
    
    let fn = create()
    a = 200  // 这个没有人用它,可以被释放
    fn()  // 100
    
    // 闭包 - 函数作为参数被传入
    let print = fn2=> {
      let b = 200  // 这里没有使用,会被释放
      fn2()
    }
    let b = 100
    let fn2 = ()=> {
      console.log("b", b)
    }
    print(fn2)  // 100

6. 第5组面试题

6.1. 题目

  • 如何阻止事件冒泡和默认行为
  • 查找、添加、删除、移动DOM节点的方法
  • 如何减少DOM操作

6.2. 如何阻止事件冒泡和默认行为

  • 答案
    • event => 事件对象
    • 阻止冒泡:event.stopPropagation()
    • 阻止默认行为event.preventDefault()
    • 手写demo传送门
    html
    <div id="div1">
        <a id="a1" href="#">a1</a><br>
        <a id="a2" href="https://www.baidu.com/">a2</a><br>
        <a href="#">a3</a><br>
        <a href="#">a4</a><br>
        <button>加载更多...</button>
    </div>
    
    <script>
      const body = document.body
      const div1 = document.getElementById('div1');
      const a1 = document.getElementById('a1');
      const a2 = document.getElementById('a2');
    
      body.addEventListener('click', e=> {
        // 即a1触发冒泡到div1上,这里的e.target.textContent为a1
        // 即div1触发冒泡到body上,这里的e.target.textContent为div1里面所有的元素
        console.log('body clicked =>', e.target.textContent)
      })
    
      div1.addEventListener('click', e=> {
        // 冒泡的事件对象为触发的元素
        // 即a1触发冒泡到div1上,这里的e.target.textContent为a1
        console.log("div1 click =>", e.target.textContent)
    
        // 在这里使用阻止冒泡,就不会触发body上的点击事件
        // 如果不加阻止冒泡,会触发body上的点击事件
        // 如果不加阻止冒泡,点击button按钮,会打印
        // div1 click => 加载更多...
        // body clicked => 加载更多...
        // 不加则只会打印一个
        e.stopPropagation()  // 阻止冒泡
    
        // 阻止默认行为
        e.preventDefault()  // 地址无法跳转
      })
    
      a1.addEventListener('click', e=> {
        // 在这里使用阻止冒泡,就不会触发a1和body上的点击事件
        e.stopPropagation()  // 阻止冒泡
    
        console.log("a2 click =>", e.target.textContent)  
      })
    
      a2.addEventListener('click', e=> {
        // 点击a2可以触发div1和body的点击事件
        // 即a2的事件会一层一层往上冒泡,直到body上
        // a2 click => a2
        // div1 click => a2
        // body clicked => a2
        // 在div1处阻止冒泡,body事件无法触发
        console.log("a2 click =>", e.target.textContent, e.target, "标签不会跳转")  
      })
    </script>
    <div id="div1">
        <a id="a1" href="#">a1</a><br>
        <a id="a2" href="https://www.baidu.com/">a2</a><br>
        <a href="#">a3</a><br>
        <a href="#">a4</a><br>
        <button>加载更多...</button>
    </div>
    
    <script>
      const body = document.body
      const div1 = document.getElementById('div1');
      const a1 = document.getElementById('a1');
      const a2 = document.getElementById('a2');
    
      body.addEventListener('click', e=> {
        // 即a1触发冒泡到div1上,这里的e.target.textContent为a1
        // 即div1触发冒泡到body上,这里的e.target.textContent为div1里面所有的元素
        console.log('body clicked =>', e.target.textContent)
      })
    
      div1.addEventListener('click', e=> {
        // 冒泡的事件对象为触发的元素
        // 即a1触发冒泡到div1上,这里的e.target.textContent为a1
        console.log("div1 click =>", e.target.textContent)
    
        // 在这里使用阻止冒泡,就不会触发body上的点击事件
        // 如果不加阻止冒泡,会触发body上的点击事件
        // 如果不加阻止冒泡,点击button按钮,会打印
        // div1 click => 加载更多...
        // body clicked => 加载更多...
        // 不加则只会打印一个
        e.stopPropagation()  // 阻止冒泡
    
        // 阻止默认行为
        e.preventDefault()  // 地址无法跳转
      })
    
      a1.addEventListener('click', e=> {
        // 在这里使用阻止冒泡,就不会触发a1和body上的点击事件
        e.stopPropagation()  // 阻止冒泡
    
        console.log("a2 click =>", e.target.textContent)  
      })
    
      a2.addEventListener('click', e=> {
        // 点击a2可以触发div1和body的点击事件
        // 即a2的事件会一层一层往上冒泡,直到body上
        // a2 click => a2
        // div1 click => a2
        // body clicked => a2
        // 在div1处阻止冒泡,body事件无法触发
        console.log("a2 click =>", e.target.textContent, e.target, "标签不会跳转")  
      })
    </script>

6.3. 查找、添加、删除、移动DOM节点的方法

  • 答案
    • 手写demo传送门
    • 查找节点
      • 基本的查找
        • document.getElementById()
        • document.getElementsByTagName()
        • document.getElementsByClassName()
        • document.querySelector()
        • document.querySelectorAll()
      • 查找父子元素
        • xxx.parentNode => 查找父元素
        • xxx.childNodes => 查找子元素(会查到标签和文本元素)
        html
        <div id="div1" class="container">
            <p id="p1">111</p>
            <p>p222</p>
            <p>333</p>
        </div>
        
        <script>
          /**
          * 查找父子元素
          */
          // 查找父元素
          const p1 = document.getElementById('p1')
          console.log(p1.parentNode)
        
          // 查找子元素
          const div1 = document.getElementById('div1')
          console.log(div1.childNodes)  // 复数
        
          // 查找子元素里面非#text的元素,即所有的p标签
          // 我们可以使用nodeName和nodeType判断它是不是普通的p标签
          const div1ChildNodesP = Array.prototype.slice.call(div1.childNodes).filter(child=> {
            // 如果是普通p标签
            // console.log(child.nodeName)  // p #text
            // console.log(child.nodeType)  // 1 3
        
            // 普通dom节点类型为1
            // 文本类型为3
            if(child.nodeType === 1) {
              return true
            }
            return false
          })
          console.log(div1ChildNodesP)  // dom数组
          // console.log(div1ChildNodesP[0].nodeName)  // p
        </script>
        <div id="div1" class="container">
            <p id="p1">111</p>
            <p>p222</p>
            <p>333</p>
        </div>
        
        <script>
          /**
          * 查找父子元素
          */
          // 查找父元素
          const p1 = document.getElementById('p1')
          console.log(p1.parentNode)
        
          // 查找子元素
          const div1 = document.getElementById('div1')
          console.log(div1.childNodes)  // 复数
        
          // 查找子元素里面非#text的元素,即所有的p标签
          // 我们可以使用nodeName和nodeType判断它是不是普通的p标签
          const div1ChildNodesP = Array.prototype.slice.call(div1.childNodes).filter(child=> {
            // 如果是普通p标签
            // console.log(child.nodeName)  // p #text
            // console.log(child.nodeType)  // 1 3
        
            // 普通dom节点类型为1
            // 文本类型为3
            if(child.nodeType === 1) {
              return true
            }
            return false
          })
          console.log(div1ChildNodesP)  // dom数组
          // console.log(div1ChildNodesP[0].nodeName)  // p
        </script>
    • 添加节点
      • document.createElement()
      • xxx.appendChild()
      html
      <div id="div2"></div>
      
      <script>
        const div2 = document.getElementById('div2')
        // 添加新节点
        const div2p1 = document.createElement('p')  // 此时还未插入,只是定义
        div2p1.innerHTML = 'this is div2p1'  // 给这个元素加点内容
        div2.appendChild(div2p1)  // 添加新创建的元素到dom节点
      </script>
      <div id="div2"></div>
      
      <script>
        const div2 = document.getElementById('div2')
        // 添加新节点
        const div2p1 = document.createElement('p')  // 此时还未插入,只是定义
        div2p1.innerHTML = 'this is div2p1'  // 给这个元素加点内容
        div2.appendChild(div2p1)  // 添加新创建的元素到dom节点
      </script>
    • 删除节点
      • xxx.removeChild(xxxChild) => 删除子节点
      • xxx.remove() => 删除整个dom
      html
      <div id="div3">
        <a href="@">a1</a>
        <a href="@">a2</a>
      
        <div id="div3_1">
            div3_1内容
        </div>
      </div>
      
      <script>
        const div3 = document.getElementById('div3')
        const div3_a = div3.querySelector('a')  // 只获取第一个
        const div3_1 = div3.querySelector('#div3_1')
        // console.log(div3_a)
        console.log(div3_1)
      
        // 删除子节点
        div3.removeChild(div3_a)
      
        // 删除整个dom(div)
        div3_1.remove()
      </script>
      <div id="div3">
        <a href="@">a1</a>
        <a href="@">a2</a>
      
        <div id="div3_1">
            div3_1内容
        </div>
      </div>
      
      <script>
        const div3 = document.getElementById('div3')
        const div3_a = div3.querySelector('a')  // 只获取第一个
        const div3_1 = div3.querySelector('#div3_1')
        // console.log(div3_a)
        console.log(div3_1)
      
        // 删除子节点
        div3.removeChild(div3_a)
      
        // 删除整个dom(div)
        div3_1.remove()
      </script>
    • 移动DOM节点
      html
      <div id="div4">
        <p id="div4p">div4p</p>
      </div>
      <div id="div5">
        div5内容
      </div>
      
      <script>
        const div4 = document.getElementById('div4')
        const div4p = div4.getElementsByTagName('p')[0]  // 获取dom标签
        // console.log(div4p)  
      
        const div5 = document.getElementById('div5')
        div5.appendChild(div4p)
      </script>
      <div id="div4">
        <p id="div4p">div4p</p>
      </div>
      <div id="div5">
        div5内容
      </div>
      
      <script>
        const div4 = document.getElementById('div4')
        const div4p = div4.getElementsByTagName('p')[0]  // 获取dom标签
        // console.log(div4p)  
      
        const div5 = document.getElementById('div5')
        div5.appendChild(div4p)
      </script>
  • 注意事项
    • 插入和移动看似是两个方法,但实际上api是一样的

6.4. 如何减少DOM操作

  • 背景
    • dom操作非常消耗性能
  • 答案
    html
    <div id="div1" class="container">
        <p>111</p>
        <p>222</p>
        <p>333</p>
    </div>
    
    <script>
      const div1 = document.getElementById('div1')
      const pList = div1.querySelectorAll('p')
      // console.log(pList)
    
      let length = pList.length
      for(let i=0; i<length; i++) {
        // 缓存length,只进行一次dom查询
        console.log("缓存dom查询结果", pList[i])
      }
    </script>
    <div id="div1" class="container">
        <p>111</p>
        <p>222</p>
        <p>333</p>
    </div>
    
    <script>
      const div1 = document.getElementById('div1')
      const pList = div1.querySelectorAll('p')
      // console.log(pList)
    
      let length = pList.length
      for(let i=0; i<length; i++) {
        // 缓存length,只进行一次dom查询
        console.log("缓存dom查询结果", pList[i])
      }
    </script>
    • 多次dom操作,合并到一次插入(代码片段)
    html
    <ul id="list"></ul>
    
    <script>
      const list = document.getElementById('list')
      // console.log(list)
    
      // 创建一个文档片段,此时还没有插入到 DOM 结构中
      const frag = document.createDocumentFragment();
    
      // 执行插入
      for(let i=0; i<10; i++) {
        const li = document.createElement('li')
        li.innerHTML = "List item " + i
        // list.appendChild(li)  // 这样就会频繁操作dom
        frag.appendChild(li)  // 先在文档片段中插入
      }
    
      // 都完成之后,再统一插入到 DOM 结构树中
      list.appendChild(frag)
    </script>
    <ul id="list"></ul>
    
    <script>
      const list = document.getElementById('list')
      // console.log(list)
    
      // 创建一个文档片段,此时还没有插入到 DOM 结构中
      const frag = document.createDocumentFragment();
    
      // 执行插入
      for(let i=0; i<10; i++) {
        const li = document.createElement('li')
        li.innerHTML = "List item " + i
        // list.appendChild(li)  // 这样就会频繁操作dom
        frag.appendChild(li)  // 先在文档片段中插入
      }
    
      // 都完成之后,再统一插入到 DOM 结构树中
      list.appendChild(frag)
    </script>

7. 第6组面试题

7.1. 题目

  • 解释jsonp原理,为何他不是真正的ajax
  • document load和ready的区别
  • ==和===的不同

7.2. 解释jsonp原理,为何他不是真正的ajax

  • 答案

    • ajax是通过XMLHttpRequest这个api实现的,而jsonp是通过script标签实现的
    • jsonp的原理就是,定义一个全局函数,去访问一段js
    • 通过这个就可以明白,他不是一个ajax,他没有去使用XMLHttpRequest这个api
    • 手写demo传送门
    html
    <script>
      // JSONP的callback是cbt,见下面src的cb
      window.cbt = function(data) {
        // 这是我们跨域得到的信息
        console.log("访问script的src获取数据", data)
      }
    </script>
    
    <script src="https://www.baidu.com/sugrec?&prod=pc&wd=mac笔记本&cb=cbt"></script>
    <script>
      // JSONP的callback是cbt,见下面src的cb
      window.cbt = function(data) {
        // 这是我们跨域得到的信息
        console.log("访问script的src获取数据", data)
      }
    </script>
    
    <script src="https://www.baidu.com/sugrec?&prod=pc&wd=mac笔记本&cb=cbt"></script>
  • 延伸

    • 浏览器的同源策略(服务端没有同源策略)
      • 后端用nginx代理也可以算是一个跨域的解决方式
      • nginx其实算是服务端,服务端没有同源策略,服务端其实没有跨域这个说法
      • 如果按严格来说,nginx代理其实可以算是后端转发
      • 后端没有同源策略,所以不能说是跨域,应该说转发
    • 跨域(协议、域名、端口有一个不同就是跨域)
      • 为什么后端没有同源策略,前端,浏览器有同源策略
        • 安全问题,不同域不能有资源的访问
        • 后端没有同源策略,但他可以自己做一些预防
        • 因为后端代码是运行在服务器上的,可以对他做一些控制
        • 前端代码是运行在浏览器上的,所以这个控制得浏览器去做安全性的控制即同源策略
      • 要实现跨域,必须要得到服务端的支持和允许,不然就是非法的,无法请求数据
    • 哪些html标签能绕过跨域
      • img、script
      • 他可能会去请求一些外域的图片,或者使用cdn,必须要可以跨域
    • 实现一个简易的百度搜索功能(百度搜索用的是jsonp)
      html
      <div class="search_box">
          <input type="text" id="input1">
          <ul id="todolist"></ul>
      </div>
      
      <script src="../../js/lib/jquery1.8.3.min.js"></script>
      <script>
        // ajax请求
        const reqAns = (words, fn)=> {
          $.ajax({
            url: "https://www.baidu.com/sugrec",
            type: 'GET',
            dataType:"jsonp",
            data: {
              prod: "pc",
              wd: words,
            },
            success(data) {
              // console.log("ajax请求", data);
      
              let res = data.g
      
              fn(res)
            }
          })
        }
      
        // 防抖
        const debounce = (fn, delay = 500)=> {
          let timer = null;
      
          return function() {
            if(timer) {
              clearTimeout(timer)
            }
      
            timer = setTimeout(()=> {
              fn.apply(this, arguments)  
      
              timer = null
            }, delay)
          }
        }
      
        // 去除所有空格 在api上添加
        String.prototype.NoSpace = function () {
          return this.replace(/\s+/g, "");
        };
      
        // 使用防抖,优化体验
        $('#input1').keyup(debounce(function(e) {
          // 使用箭头函数,this指向为window
          // console.log(e.target)
          // console.log($(this).val())
      
          let words = $(this).val()
      
            // 参数名可以和封装的传参不一样
          reqAns(words, res=> {
            // console.log("使用防抖获取的数据", res)
            let str = ""
      
            if(!res) {
              $('#todolist').html("")
              return
            }
      
            res.forEach(item=> {
              str += `
                <li>${item.q}<li>
              `
            })
            // 去除所有空格
            str = str.NoSpace()
            // console.log("str", str)
      
            $('#todolist').html(str)
      
            let liList = $('#todolist li')
            // console.log(liList)
      
            // 由于结果数据里面有空行,空行可能是方法,可能是空的dom,也可能是undefind
            for(item in liList) {
              // console.log(liList[item].nodeName)
      
              // 判断节点属性为li,并且它里面没有内容
              // remove不能删方法
              if(liList[item].nodeName == "LI" && !liList[item].textContent) {
                // 删除节点
                liList[item].remove()
              }
            }
      
            $('#todolist li').click((e)=> {
              $('#input1').val(e.target.textContent)
              console.log($('#input1').val())
            })
          })
        }))
      
        $('#input1').focus(()=> {
          $("#todolist").show()
        })
        $('#input1').blur(()=> {
          setTimeout(()=> {
            $("#todolist").hide()
          }, 1000)
        })
      </script>
      <div class="search_box">
          <input type="text" id="input1">
          <ul id="todolist"></ul>
      </div>
      
      <script src="../../js/lib/jquery1.8.3.min.js"></script>
      <script>
        // ajax请求
        const reqAns = (words, fn)=> {
          $.ajax({
            url: "https://www.baidu.com/sugrec",
            type: 'GET',
            dataType:"jsonp",
            data: {
              prod: "pc",
              wd: words,
            },
            success(data) {
              // console.log("ajax请求", data);
      
              let res = data.g
      
              fn(res)
            }
          })
        }
      
        // 防抖
        const debounce = (fn, delay = 500)=> {
          let timer = null;
      
          return function() {
            if(timer) {
              clearTimeout(timer)
            }
      
            timer = setTimeout(()=> {
              fn.apply(this, arguments)  
      
              timer = null
            }, delay)
          }
        }
      
        // 去除所有空格 在api上添加
        String.prototype.NoSpace = function () {
          return this.replace(/\s+/g, "");
        };
      
        // 使用防抖,优化体验
        $('#input1').keyup(debounce(function(e) {
          // 使用箭头函数,this指向为window
          // console.log(e.target)
          // console.log($(this).val())
      
          let words = $(this).val()
      
            // 参数名可以和封装的传参不一样
          reqAns(words, res=> {
            // console.log("使用防抖获取的数据", res)
            let str = ""
      
            if(!res) {
              $('#todolist').html("")
              return
            }
      
            res.forEach(item=> {
              str += `
                <li>${item.q}<li>
              `
            })
            // 去除所有空格
            str = str.NoSpace()
            // console.log("str", str)
      
            $('#todolist').html(str)
      
            let liList = $('#todolist li')
            // console.log(liList)
      
            // 由于结果数据里面有空行,空行可能是方法,可能是空的dom,也可能是undefind
            for(item in liList) {
              // console.log(liList[item].nodeName)
      
              // 判断节点属性为li,并且它里面没有内容
              // remove不能删方法
              if(liList[item].nodeName == "LI" && !liList[item].textContent) {
                // 删除节点
                liList[item].remove()
              }
            }
      
            $('#todolist li').click((e)=> {
              $('#input1').val(e.target.textContent)
              console.log($('#input1').val())
            })
          })
        }))
      
        $('#input1').focus(()=> {
          $("#todolist").show()
        })
        $('#input1').blur(()=> {
          setTimeout(()=> {
            $("#todolist").hide()
          }, 1000)
        })
      </script>

7.3. document load和ready的区别

  • 答案
    • load是网页全部加载完才执行
      • 图片、视频、iframe等
    • ready是dom渲染即可执行,此时图片、视频等静态资源还没加载完
      • DOMContentLoaded事件
      • 为了让js加载更快,一般是在ready里面去做js操作

7.4. ==和===的区别

  • 答案
    • == 会尝试类型转换
    • === 是严格相等
  • 延伸
    • 哪些场景用 ==
      • 只有一个场景 xx == null
        • 表示 xx === null || xx === undefind

8. 第7组面试题

8.1. 题目

  • 函数声明和函数表达式的区别
    • 函数声明和变量提升非常类似
  • new Object()和Object.create()的区别
    • 网红题
  • 关于this的场景题

8.2. 函数声明和函数表达式的区别

  • 概念
    • 函数声明:function fn()
    • 函数表达式:const fn = function()
  • 答案
    • 函数声明是直接用function来定义函数的
    • 函数表达式是通过先定义一个变量,再把它赋值给一个函数来定义函数的
    • 函数声明会在代码执行前预加载,而函数表达式不会
      • 这个预加载和变量提升是一样的
      • 函数表达式没有变量提升
    • 手写demo传送门
    js
    /**
     * 函数声明
     */
    const res = sum(10, 20)
    console.log(res)
    
    // 函数声明会在代码执行之前预加载,有些类似变量提升,但函数已经可以用了
    // 会先把这个函数初始化上,再执行代码
    function sum(x, y) {
      return x + y
    }
    
    /**
     * 函数表达式
     */
    const res1 = sum1(10, 20)
    console.log(res1)  // 使用函数表达式这里会报错
    
    const sum1 = function(x, y) {
      return x + y
    }
    /**
     * 函数声明
     */
    const res = sum(10, 20)
    console.log(res)
    
    // 函数声明会在代码执行之前预加载,有些类似变量提升,但函数已经可以用了
    // 会先把这个函数初始化上,再执行代码
    function sum(x, y) {
      return x + y
    }
    
    /**
     * 函数表达式
     */
    const res1 = sum1(10, 20)
    console.log(res1)  // 使用函数表达式这里会报错
    
    const sum1 = function(x, y) {
      return x + y
    }
  • 延伸
    • 函数表达式的报错
    js
    const res1 = sum1(10, 20)
    // Cannot access 'sum1' before initialization
    console.log(res1)  // 使用函数表达式这里会报错
    
    // 用let或者const不会命中变量提升
    // 这里没有做变量提升,所以上面的sum1实际上就是未被定义
    // 所以报错信息是,初始化前无法访问"sum1"
    const sum1 = function(x, y) {
      return x + y
    }
    
    const res2 = sum2(10, 20)
    // sum2 is not a function
    console.log(res1)  // 这里的报错信息不一样
    
    // 使用var会命中变量提升
    // 他会提升到最上面,实际上 sum2 === undefined
    // 所以上面的报错信息是sum2不是一个函数
    var sum2 = function(x, y) {
      return x + y
    }
    const res1 = sum1(10, 20)
    // Cannot access 'sum1' before initialization
    console.log(res1)  // 使用函数表达式这里会报错
    
    // 用let或者const不会命中变量提升
    // 这里没有做变量提升,所以上面的sum1实际上就是未被定义
    // 所以报错信息是,初始化前无法访问"sum1"
    const sum1 = function(x, y) {
      return x + y
    }
    
    const res2 = sum2(10, 20)
    // sum2 is not a function
    console.log(res1)  // 这里的报错信息不一样
    
    // 使用var会命中变量提升
    // 他会提升到最上面,实际上 sum2 === undefined
    // 所以上面的报错信息是sum2不是一个函数
    var sum2 = function(x, y) {
      return x + y
    }

8.3. new Object()和Object.create()的区别

  • 答案
    • {}等同于new Object({}),原型都是Object.prototype
    • Object.create(null)没有原型
      • 他必须传参,可以传对象,也可以传null
      • null其实是一个空对象
      • Object.create({...})没有原型的原因是他可以指定原型
        • 传入一个null,就是告诉他不要有原型
        • 传入一个对象,就是告诉他去指定原型
    • Object.create()传参之后,对象中没有值,只是将参数的对象全部将放在空对象的原型中,这是和{}最大的区别
    • Object.create()是创建一个空对象,然后把空对象的原型指向了传入的对象
    • 手写demo传送门
    js
    const obj1 = {
      a: 10,
      b: 20,
      sum() {
        return this.a + this.b
      }
    }
    
    // 如果要new Object传入一个对象的话,返回的就是这个对象的本身
    // 即全相等
    const obj3 = new Object(obj1)
    // 值和内存地址都相等
    console.log("const obj3 = new Object(obj1)", obj3 === obj1)  // true
    
    // 因为obj3是由obj1 new出来的
    // obj7是由obj1 create出来的
    // 所以 obj7的隐式原型和obj3全相等
    console.log("obj7.__proto__ === obj3", obj7.__proto__ === obj3)
    const obj1 = {
      a: 10,
      b: 20,
      sum() {
        return this.a + this.b
      }
    }
    
    // 如果要new Object传入一个对象的话,返回的就是这个对象的本身
    // 即全相等
    const obj3 = new Object(obj1)
    // 值和内存地址都相等
    console.log("const obj3 = new Object(obj1)", obj3 === obj1)  // true
    
    // 因为obj3是由obj1 new出来的
    // obj7是由obj1 create出来的
    // 所以 obj7的隐式原型和obj3全相等
    console.log("obj7.__proto__ === obj3", obj7.__proto__ === obj3)
  • 延伸
    • 一般一个对象必须有一个隐式原型
    js
    /**
     * new Object
     */
    // {}实际上就是new Object({})
    const obj1 = {
      a: 10,
      b: 20,
      sum() {
        return this.a + this.b
      }
    }
    const obj2 = new Object({
      a: 10,
      b: 20,
      sum() {
        return this.a + this.b
      }
    })
    
    // 都有隐式原型(__proto__)
    // 隐式原型都指向object构造函数的显示原型
    console.log("定义两个对象,值一样,obj1, obj2", obj1, obj2) 
    
    // 定义两个对象,他开辟了两个内存地址,所以不会相等
    console.log("定义两个对象,值一样,obj1, obj2", obj1 === obj2)  // false
    
    // 如果要new Object传入一个对象的话,返回的就是这个对象的本身
    // 即全相等
    const obj3 = new Object(obj1)
    // 值和内存地址都相等
    console.log("const obj3 = new Object(obj1)", obj3 === obj1)  // true
    
    /**
     * Object.create(null)
      */
    const obj4 = Object.create(null)
    console.log("Object.create(null),obj4", obj4)  // 没有属性,也没有原型
    
    const obj5 = new Object()  // 等价于const obj5 = {}
    // 隐式原型指向object构造函数的显示原型
    console.log("new Object(),obj5", obj5)  // 有一个隐式原型
    
    // Object.create()传对象,指向一个原型(即将他们放在空对象的原型中)
    // Object.create()传对象之后,返回的对象的隐式原型指向里面的参数的显示原型
    // 里面的参数是一个对象,这个对象的隐式原型指向object构造函数的显示原型
    const obj6 = Object.create({
      a: 10,
      b: 20,
      sum() {
        return this.a + this.b
      }
    })
    console.log("Object.create()传对象,obj6", obj6)  // 内容为空,但有原型
    console.log("obj6的属性", obj6.a)  // 但是可以找到属性
    
    // obj7的原型指向obj1
    const obj7 = Object.create(obj1)
    obj1.c = 1000  // 修改obj1
    // Object.create()里面的参数被修改之后,返回的值也会变化
    // 因为obj7的原型指向obj1
    // 即 obj7.__proto__ === obj1
    console.log("obj1被修改之后,Object.create(obj1)", obj7)
    // obj7的隐式原型指向obj1的显示原型
    console.log("obj7.__proto__ === obj1", obj7.__proto__ === obj1)  // true
    console.log("obj7 === obj1", obj7 === obj1)  // false
    
    // const obj3 = new Object(obj1)
    // 因为obj3是由obj1 new出来的
    // obj7是由obj1 create出来的
    // 所以 obj7的隐式原型和obj3全相等
    console.log("obj7.__proto__ === obj3", obj7.__proto__ === obj3)
    /**
     * new Object
     */
    // {}实际上就是new Object({})
    const obj1 = {
      a: 10,
      b: 20,
      sum() {
        return this.a + this.b
      }
    }
    const obj2 = new Object({
      a: 10,
      b: 20,
      sum() {
        return this.a + this.b
      }
    })
    
    // 都有隐式原型(__proto__)
    // 隐式原型都指向object构造函数的显示原型
    console.log("定义两个对象,值一样,obj1, obj2", obj1, obj2) 
    
    // 定义两个对象,他开辟了两个内存地址,所以不会相等
    console.log("定义两个对象,值一样,obj1, obj2", obj1 === obj2)  // false
    
    // 如果要new Object传入一个对象的话,返回的就是这个对象的本身
    // 即全相等
    const obj3 = new Object(obj1)
    // 值和内存地址都相等
    console.log("const obj3 = new Object(obj1)", obj3 === obj1)  // true
    
    /**
     * Object.create(null)
      */
    const obj4 = Object.create(null)
    console.log("Object.create(null),obj4", obj4)  // 没有属性,也没有原型
    
    const obj5 = new Object()  // 等价于const obj5 = {}
    // 隐式原型指向object构造函数的显示原型
    console.log("new Object(),obj5", obj5)  // 有一个隐式原型
    
    // Object.create()传对象,指向一个原型(即将他们放在空对象的原型中)
    // Object.create()传对象之后,返回的对象的隐式原型指向里面的参数的显示原型
    // 里面的参数是一个对象,这个对象的隐式原型指向object构造函数的显示原型
    const obj6 = Object.create({
      a: 10,
      b: 20,
      sum() {
        return this.a + this.b
      }
    })
    console.log("Object.create()传对象,obj6", obj6)  // 内容为空,但有原型
    console.log("obj6的属性", obj6.a)  // 但是可以找到属性
    
    // obj7的原型指向obj1
    const obj7 = Object.create(obj1)
    obj1.c = 1000  // 修改obj1
    // Object.create()里面的参数被修改之后,返回的值也会变化
    // 因为obj7的原型指向obj1
    // 即 obj7.__proto__ === obj1
    console.log("obj1被修改之后,Object.create(obj1)", obj7)
    // obj7的隐式原型指向obj1的显示原型
    console.log("obj7.__proto__ === obj1", obj7.__proto__ === obj1)  // true
    console.log("obj7 === obj1", obj7 === obj1)  // false
    
    // const obj3 = new Object(obj1)
    // 因为obj3是由obj1 new出来的
    // obj7是由obj1 create出来的
    // 所以 obj7的隐式原型和obj3全相等
    console.log("obj7.__proto__ === obj3", obj7.__proto__ === obj3)

8.4. 关于this的场景题

  • 手写demo传送门
  • 场景
    js
    const User = {
      count: 1,
      getCount: function() {
        return this.count
      }
    }
    
    // getCount作为一个对象的方法,里面的this指向这个对象本身
    // 对象本身的count为1
    // 用这种方法执行,相当于是当做user的一个api(属性)来执行
    console.log(User.getCount())  // ? => 1
    
    // 把User对象中的方法拿出来赋值成一个函数,然后运行
    // 最初函数里面的this的值是不知道的,只有在执行的时候才知道(谁调用指向谁)
    // 这里拿出来(赋值)作为一个独立的函数来执行,this就指向window
    const func = User.getCount
    console.log(func())  // ? => undefined
    
    // 即
    function getCount1(){ 
      return this.count
    }
    console.log(getCount1())  // undefined
    const User = {
      count: 1,
      getCount: function() {
        return this.count
      }
    }
    
    // getCount作为一个对象的方法,里面的this指向这个对象本身
    // 对象本身的count为1
    // 用这种方法执行,相当于是当做user的一个api(属性)来执行
    console.log(User.getCount())  // ? => 1
    
    // 把User对象中的方法拿出来赋值成一个函数,然后运行
    // 最初函数里面的this的值是不知道的,只有在执行的时候才知道(谁调用指向谁)
    // 这里拿出来(赋值)作为一个独立的函数来执行,this就指向window
    const func = User.getCount
    console.log(func())  // ? => undefined
    
    // 即
    function getCount1(){ 
      return this.count
    }
    console.log(getCount1())  // undefined
  • 延伸
    • 如何改变函数的this指向
    js
    const User = {
      count: 1,
      getCount: function() {
        return this.count
      }
    }
    
    function getCount1(){ 
      return this.count
    }
    console.log(getCount1())  // undefined
    
    // 如何改变 getCount1 this指向,指向user
    console.log(getCount1.bind(User)())
    console.log(getCount1.call(User))
    console.log(getCount1.apply(User))
    const User = {
      count: 1,
      getCount: function() {
        return this.count
      }
    }
    
    function getCount1(){ 
      return this.count
    }
    console.log(getCount1())  // undefined
    
    // 如何改变 getCount1 this指向,指向user
    console.log(getCount1.bind(User)())
    console.log(getCount1.call(User))
    console.log(getCount1.apply(User))

9. 第8组面试题

9.1. 题目

  • 关于作用域和自由变量的场景题(1,2)
  • 判断字符串以字母开头,后面字母数字下划线,长度6-30

9.2. 关于作用域和自由变量的场景题

  • 手写demo传送门

  • 场景题1

    js
    // setTimeout(宏任务)属于异步队列中的任务,主线程执行完之后才会执行
    // 所以会先进行遍历,每次遍历会把setTimeout里面的方法放到异步队列中,但不会执行setTimeout
    
    // 遍历过程中,i在当前作用域(代码块)中没有定义,但是使用了,符合自由变量的条件,这个i是一个自由变量
    // 自由变量会在被执行的环境里面一层层往上找哪里定义了(这就是作用域链)
    // 此时i在全局定义,就会找到全局,即每次遍历完就会把值赋在全局
    // 全局作用域是针对所有的块
    
    // 当for循环遍历完之后,此时同步代码执行完,此时的i是全局变量是4,异步队列里面有4个setTimeout待执行
    // 开始执行异步队列里面的宏任务setTimeout,第一个setTimeout里面有个console.log(i)
    // 此时这个自由变量i同样也会在作用域链上一层层往上找,直到找到全局i为4
    
    // 于是此时打印的就是4 => 第一次打印
    // 执行下一个宏任务setTimeout
    // 以同样的方法找i
    // 于是此时打印的就是4 => 第二次打印
    // 后面同上
    let i
    for(i=1; i<=3; i++) {
      // debugger
      setTimeout(function(){
        console.log(i)  // 4 4 4
      }, 0)
    }
    
    // 先进行遍历,每次遍历会把setTimeout里面的方法放到异步队列中,但不会执行setTimeout,setTimeout中的i是一个自由变量
    // 当同步代码执行完,开始执行异步队列中的任务的时候,执行console.log(i),这个自由变量i开始往上找值,找到for里面
    // i在for里面被定义,即就是块级作用域,所以每次循环的时候,都会形成一个新的作用域块,这里的i就会不一样
    // 因为i在for里面被定义,那么每次遍历后的操作就会在块级作用域里面去找
    // 由于每次循环,都会形成一个新的作用域块,所以遍历中每次的宏任务setTimeout所在的作用域是不一样的,即里面的i也是不一样的
    
    // 第一次打印,在i=1的块级作用域中,此时打印的是1 => 第一次打印
    // 第二次打印,在i=2的块级作用域中,此时打印的是1 => 第二次打印
    // 后面同上
    for(let i=1; i<=3; i++) {
      // debugger
      setTimeout(function(){
        console.log(i)  // 1 2 3
      }, 0)
    }
    // setTimeout(宏任务)属于异步队列中的任务,主线程执行完之后才会执行
    // 所以会先进行遍历,每次遍历会把setTimeout里面的方法放到异步队列中,但不会执行setTimeout
    
    // 遍历过程中,i在当前作用域(代码块)中没有定义,但是使用了,符合自由变量的条件,这个i是一个自由变量
    // 自由变量会在被执行的环境里面一层层往上找哪里定义了(这就是作用域链)
    // 此时i在全局定义,就会找到全局,即每次遍历完就会把值赋在全局
    // 全局作用域是针对所有的块
    
    // 当for循环遍历完之后,此时同步代码执行完,此时的i是全局变量是4,异步队列里面有4个setTimeout待执行
    // 开始执行异步队列里面的宏任务setTimeout,第一个setTimeout里面有个console.log(i)
    // 此时这个自由变量i同样也会在作用域链上一层层往上找,直到找到全局i为4
    
    // 于是此时打印的就是4 => 第一次打印
    // 执行下一个宏任务setTimeout
    // 以同样的方法找i
    // 于是此时打印的就是4 => 第二次打印
    // 后面同上
    let i
    for(i=1; i<=3; i++) {
      // debugger
      setTimeout(function(){
        console.log(i)  // 4 4 4
      }, 0)
    }
    
    // 先进行遍历,每次遍历会把setTimeout里面的方法放到异步队列中,但不会执行setTimeout,setTimeout中的i是一个自由变量
    // 当同步代码执行完,开始执行异步队列中的任务的时候,执行console.log(i),这个自由变量i开始往上找值,找到for里面
    // i在for里面被定义,即就是块级作用域,所以每次循环的时候,都会形成一个新的作用域块,这里的i就会不一样
    // 因为i在for里面被定义,那么每次遍历后的操作就会在块级作用域里面去找
    // 由于每次循环,都会形成一个新的作用域块,所以遍历中每次的宏任务setTimeout所在的作用域是不一样的,即里面的i也是不一样的
    
    // 第一次打印,在i=1的块级作用域中,此时打印的是1 => 第一次打印
    // 第二次打印,在i=2的块级作用域中,此时打印的是1 => 第二次打印
    // 后面同上
    for(let i=1; i<=3; i++) {
      // debugger
      setTimeout(function(){
        console.log(i)  // 1 2 3
      }, 0)
    }
  • 场景题2

    js
    let a = 100
    function test() {
      alert(a)  // a是自由变量,找到100
      // 由于a是全局变量,
      // 这里其实是把全局变量的a给修改了
      a = 10
      alert(a)  // 10
    }
    test()
    // test方法执行完成之后,a这个全局变量实际已经被修改了
    alert(a)  // 10
    let a = 100
    function test() {
      alert(a)  // a是自由变量,找到100
      // 由于a是全局变量,
      // 这里其实是把全局变量的a给修改了
      a = 10
      alert(a)  // 10
    }
    test()
    // test方法执行完成之后,a这个全局变量实际已经被修改了
    alert(a)  // 10

9.3. 正则

  • 作用
    • 主要用来判断字符串是否符合某个规则
  • 面试题
    • 判断字符串以字母开头,后面字母数字下划线,长度6-30
    • const reg = /^[a-zA-Z]\w{5,29}$/
  • 延伸
    • 正则学习网站
    • 前后有个'/'就是一个正则表达式
    • ^xx表示以xx开头,xx$表示以xx结尾
    • []用来定义匹配的字符范围
      • 比如[a-zA-Z0-9]表示相应位置的字符要匹配英文字符和数字
      • [^xx]表示除了xx之外的字符
    • {}一般是用来匹配的长度
      • 正则中不能加空格
      • \s{1,3}表示匹配1到3个空格
    • {n}表示匹配n次,准确的数字
      • o{2},表示一个字符串匹配两个o,如food
    • {n,}表示至少匹配n次
      • o{2,},表示一个字符串至少匹配两个o,如foooood
    • ()用来提取匹配字符串
      • (0-9)匹配数字
      • (0-9)*匹配数字,可以为空(*表示0~无限)
      • (0-9)+匹配数字,不能为空(+表示1~无限)
    • \w匹配字母数字下划线
      • [A-Za-z0-9_]
      • 大写取反
    • \d匹配数字
    • .匹配除换行符以外的任意字符
    • ?匹配前面的子表达式0-1次
      • 等价于{0, 1}
    • 如果要字符串全部满足,就加/^xxx$/
      • 如果只是一部分,就不需要加
      • 如果要字符串只满足开头,就加/^xxx/
      • 如果要字符串只满足结尾,就加/xxx$/
  • + => 表达式至少出现1次,相当于 {1,}
  • * => 表达式不出现或出现任意次,相当于 {0,}
  • 手写demo传送门
js
// 邮政 - 6位数字
// \d表示数字
// {6}表示6位
const reg1 = /\d{6}/
console.log("6位数字", reg1.test("123456"))  // true

// 小写英文字母
// ^表示开头
// [a-z]表示小写英文
// +表示1~无限次
// $表示结尾
const reg2 = /^[a-z]+$/
console.log("小写英文字母", reg2.test("abc"))  // true

// 英文字母
const reg3 = /^[a-zA-Z]+$/
console.log("英文字母", reg3.test("aBc"))  // true

// 日期格式
// \d{4}表示年,四位数字
// \d{1,2}表示1~2位数字,表示月或日
const reg4 = /^\d{4}-\d{1,2}-\d{1,2}$/
console.log("日期格式", reg4.test("2021-06-28"))  // true

// 用户名 - 字母开头,字母数字下划线,长度6-18
// 5-17加上前面的开头字母,就是6-18
// \w{5, 17}表示5-17位的字母数字下划线
const reg5 = /^[a-zA-Z]\w{5,17}$/
console.log("字母开头,字母数字下划线,长度6-18", reg5.test("abc1246"))  // true

// 简单的ip匹配 - 4个数字,3个点
// \d+表示多个数字(1~无限)
// \.表示.,转义字符,不写表示匹配任意字符
const reg6 = /\d+\.\d+\.\d+\.\d+/
console.log("简单的ip匹配 - 4个数字,3个点", reg6.test("192.168.0.1"))  // true
// 邮政 - 6位数字
// \d表示数字
// {6}表示6位
const reg1 = /\d{6}/
console.log("6位数字", reg1.test("123456"))  // true

// 小写英文字母
// ^表示开头
// [a-z]表示小写英文
// +表示1~无限次
// $表示结尾
const reg2 = /^[a-z]+$/
console.log("小写英文字母", reg2.test("abc"))  // true

// 英文字母
const reg3 = /^[a-zA-Z]+$/
console.log("英文字母", reg3.test("aBc"))  // true

// 日期格式
// \d{4}表示年,四位数字
// \d{1,2}表示1~2位数字,表示月或日
const reg4 = /^\d{4}-\d{1,2}-\d{1,2}$/
console.log("日期格式", reg4.test("2021-06-28"))  // true

// 用户名 - 字母开头,字母数字下划线,长度6-18
// 5-17加上前面的开头字母,就是6-18
// \w{5, 17}表示5-17位的字母数字下划线
const reg5 = /^[a-zA-Z]\w{5,17}$/
console.log("字母开头,字母数字下划线,长度6-18", reg5.test("abc1246"))  // true

// 简单的ip匹配 - 4个数字,3个点
// \d+表示多个数字(1~无限)
// \.表示.,转义字符,不写表示匹配任意字符
const reg6 = /\d+\.\d+\.\d+\.\d+/
console.log("简单的ip匹配 - 4个数字,3个点", reg6.test("192.168.0.1"))  // true
  • 其他(不常用)
    • (\s*)表示连续空格的字符串
      • *表示0~无限
    • [0-9]{0,9}表示长度为0到9的数字字符串
    • (abc|bcd|cde)表示这一段是abc、bcd、cde三者之一
    • [^0-3] 表示找到这个位置上字符只能是除了0到3之外的所有字符

10. 第9组面试题

10.1. 题目

  • 手写字符串trim方法,保证浏览器兼容性
    • trim表示掐头去尾
    • 低版本浏览器可能不兼容
  • 如何获取多个数字中的最大值
  • 如何用JS实现继承

10.2. 手写字符串trim方法

  • 考察内容
    • 考察原型、this、正则、字符串api
  • 答案
    js
    // 在之前原生js还不支持trim语法的时候,就是用正则这种方法去解决的
    // 字符串的replace是支持正则的,把正则表达式命中的片段,替换成其他字符串
    // 考察原型、this、正则、字符串api
    // 在String的原型上去改的
    if(!String.prototype.trim) {
      String.prototype.trim = function() {
        // /^\s+/表示开头一个或多个空格
        // /\s+$/表示结尾一个或多个空格
        // 匹配开头和结尾一个或多个空格,替换成空字符
        // 原型中的this,如果是通过String.trim()执行,这个this指向字符串,即这个类的实例
        return this.replace(/^\s+/, "").replace(/\s+$/, "")
      }
    }
    
    let str = "   123  "
    console.log("使用trim", str.trim())
    // 在之前原生js还不支持trim语法的时候,就是用正则这种方法去解决的
    // 字符串的replace是支持正则的,把正则表达式命中的片段,替换成其他字符串
    // 考察原型、this、正则、字符串api
    // 在String的原型上去改的
    if(!String.prototype.trim) {
      String.prototype.trim = function() {
        // /^\s+/表示开头一个或多个空格
        // /\s+$/表示结尾一个或多个空格
        // 匹配开头和结尾一个或多个空格,替换成空字符
        // 原型中的this,如果是通过String.trim()执行,这个this指向字符串,即这个类的实例
        return this.replace(/^\s+/, "").replace(/\s+$/, "")
      }
    }
    
    let str = "   123  "
    console.log("使用trim", str.trim())

10.3. 如何获取多个数字中的最大值

  • 答案
    js
    // 自己写一个方法
    function max() {
      // arguments是类数组对象(缺少很多数组的方法)
      // call表示让一个对象调用另一个对象的方法
      // slice表示从一个数组中切割,返回新的数组,不修改切割的数组
      // 本质就是arguments这个对象使用了数组的slice这个方法,得到了参数构成的数组
      // 即获取max里面所有的参数,并将他们转换成数组
      const nums = Array.prototype.slice.call(arguments)  // 变为数组
      let max = 0
      nums.forEach(n=> {
        if(n > max) {
          max = n
        }
      })
      return max
    }
    console.log(max(10, 20, 50))
    
    // 使用api
    let maxNum = Math.max(10, 20, 50)
    let minNum = Math.min(10, 20, 50)
    console.log("使用Math api获取最值", maxNum, minNum)
    // 自己写一个方法
    function max() {
      // arguments是类数组对象(缺少很多数组的方法)
      // call表示让一个对象调用另一个对象的方法
      // slice表示从一个数组中切割,返回新的数组,不修改切割的数组
      // 本质就是arguments这个对象使用了数组的slice这个方法,得到了参数构成的数组
      // 即获取max里面所有的参数,并将他们转换成数组
      const nums = Array.prototype.slice.call(arguments)  // 变为数组
      let max = 0
      nums.forEach(n=> {
        if(n > max) {
          max = n
        }
      })
      return max
    }
    console.log(max(10, 20, 50))
    
    // 使用api
    let maxNum = Math.max(10, 20, 50)
    let minNum = Math.min(10, 20, 50)
    console.log("使用Math api获取最值", maxNum, minNum)

10.4. 如何用JS实现继承

  • 答案
    js
    // 父类
    class People {
      constructor(name) {
        this.name = name
      }
      eat() {
        console.log(`${this.name} eat something`)
      }
    }
    
    /**
     * 使用class继承
     */
    // 子类
    class Student extends People {
      constructor(name, number) {
        super(name)
        this.number = number
      }
      sayHi() {
        console.log(`姓名 ${this.name} 学号 ${this.number}`)
      }
    }
    // 实例
    const xialuo = new Student('夏洛', 100)
    xialuo.eat()  // 夏洛 eat something
    xialuo.sayHi()  // 姓名 夏洛 学号 100
    console.log("student实例的隐式原型与student的显示原型等价", xialuo.__proto__  === Student.prototype)
    console.log("student显示原型的隐式原型与people的显示原型等价", Student.prototype.__proto__ === People.prototype)
    // student实例的隐式原型 === student的显示原型
    // student显示原型的隐式原型 === people的显示原型
    // 所以student实例的隐式原型的隐式原型 ===  people的显示原型
    // people和object同理,就形成了原型链
    console.log("student实例的隐式原型的隐式原型与people的显示原型等价", xialuo.__proto__.__proto__ === People.prototype)
    
    /**
     * 使用prototype添加方法
     */
    // 这里使用箭头函数没有this
    People.prototype.reading = function() {
      console.log(`${this.name} can reading`)
    }
    const people = new People("绿巨人")
    people.reading()  // 绿巨人 can reading
    console.log("people实例的隐式原型与people的显示原型等价", people.__proto__  === People.prototype)
    console.log("people显示原型的隐式原型与object的显示原型等价", People.prototype.__proto__ === Object.prototype)
    
    /**
     * 使用prototype继承
     * 
     * 如果父类不是用clss而是function的时候,可以使用call或apply,即将父对象的构造函数绑定在子对象上,来实现继承
     * Animal.apply(this, arguments);
     * 
     * 这里使用将teacher的prototype对象(显示原型),指向一个People的实例的方法来实现继承
     */
    // 构造函数
    function Teacher(name, major){
      this.name = name; // 定义自己的属性
      this.major = major; // 定义自己的属性
    }
    
    // 任何一个prototype对象都有一个constructor属性,用这种方式实现继承之后,Teacher的构造函数指向也发生了变化
    Teacher.prototype = new People()  // 实现继承
    Teacher.prototype.constructor = Teacher  // 重新指向Teacher子类的构造函数
    Teacher.prototype.teach = function() {
      console.log(`${this.name} 教授 ${this.major}`)
    }
    
    let teacher = new Teacher('王老师', '语文')
    teacher.eat()  // 王老师 eat something
    teacher.teach()  // 王老师 教授 语文
    // 父类
    class People {
      constructor(name) {
        this.name = name
      }
      eat() {
        console.log(`${this.name} eat something`)
      }
    }
    
    /**
     * 使用class继承
     */
    // 子类
    class Student extends People {
      constructor(name, number) {
        super(name)
        this.number = number
      }
      sayHi() {
        console.log(`姓名 ${this.name} 学号 ${this.number}`)
      }
    }
    // 实例
    const xialuo = new Student('夏洛', 100)
    xialuo.eat()  // 夏洛 eat something
    xialuo.sayHi()  // 姓名 夏洛 学号 100
    console.log("student实例的隐式原型与student的显示原型等价", xialuo.__proto__  === Student.prototype)
    console.log("student显示原型的隐式原型与people的显示原型等价", Student.prototype.__proto__ === People.prototype)
    // student实例的隐式原型 === student的显示原型
    // student显示原型的隐式原型 === people的显示原型
    // 所以student实例的隐式原型的隐式原型 ===  people的显示原型
    // people和object同理,就形成了原型链
    console.log("student实例的隐式原型的隐式原型与people的显示原型等价", xialuo.__proto__.__proto__ === People.prototype)
    
    /**
     * 使用prototype添加方法
     */
    // 这里使用箭头函数没有this
    People.prototype.reading = function() {
      console.log(`${this.name} can reading`)
    }
    const people = new People("绿巨人")
    people.reading()  // 绿巨人 can reading
    console.log("people实例的隐式原型与people的显示原型等价", people.__proto__  === People.prototype)
    console.log("people显示原型的隐式原型与object的显示原型等价", People.prototype.__proto__ === Object.prototype)
    
    /**
     * 使用prototype继承
     * 
     * 如果父类不是用clss而是function的时候,可以使用call或apply,即将父对象的构造函数绑定在子对象上,来实现继承
     * Animal.apply(this, arguments);
     * 
     * 这里使用将teacher的prototype对象(显示原型),指向一个People的实例的方法来实现继承
     */
    // 构造函数
    function Teacher(name, major){
      this.name = name; // 定义自己的属性
      this.major = major; // 定义自己的属性
    }
    
    // 任何一个prototype对象都有一个constructor属性,用这种方式实现继承之后,Teacher的构造函数指向也发生了变化
    Teacher.prototype = new People()  // 实现继承
    Teacher.prototype.constructor = Teacher  // 重新指向Teacher子类的构造函数
    Teacher.prototype.teach = function() {
      console.log(`${this.name} 教授 ${this.major}`)
    }
    
    let teacher = new Teacher('王老师', '语文')
    teacher.eat()  // 王老师 eat something
    teacher.teach()  // 王老师 教授 语文

11. 第10组面试题

11.1. 题目

  • 如何捕获JS程序中的异常
  • 什么是JSON
  • 获取当前页面url的参数

11.2. 如何捕获JS程序中的异常

  • 答案
    • 手动捕获异常 => 使用try...catch...
    • 自动捕获异常 => 使用window.onerror
    • 手写demo传送门
    js
    /**
     * 使用try...catch...
     */
    // 手动捕获异常
    try {
      // todo
    } catch(ex) {
      console.error(ex)  // 手抖捕获catch
    } finally {
      // todo
    }
    
    /**
     * 使用window.onerror
     */
    // 兜底方案
    // 监听前端页面,比如页面上线了,要统计监听查看页面有没有js报错
    // 但不可能每个地方都加try{}catch{},他只在高风险的地方添加
    // 其他地方使用window.onerror,他会自动捕获页面上出现的一些问题
    // 自动捕获异常
    // message => 报错信息
    // source => 源码
    // lineNum => 行号
    // colNum => 列号
    // error => 错误栈
    window.onerror = function(message, source, lineNum, colNum, error) {
      // 1. 对跨域的js,如cdn,不会有详细的报错信息
      // 2. 对于压缩的js,还要配合sourceMap反查到未压缩代码的行、列
      // 压缩之后可能只有一行,他的行号可能永远只有一行或者两行
    }
    /**
     * 使用try...catch...
     */
    // 手动捕获异常
    try {
      // todo
    } catch(ex) {
      console.error(ex)  // 手抖捕获catch
    } finally {
      // todo
    }
    
    /**
     * 使用window.onerror
     */
    // 兜底方案
    // 监听前端页面,比如页面上线了,要统计监听查看页面有没有js报错
    // 但不可能每个地方都加try{}catch{},他只在高风险的地方添加
    // 其他地方使用window.onerror,他会自动捕获页面上出现的一些问题
    // 自动捕获异常
    // message => 报错信息
    // source => 源码
    // lineNum => 行号
    // colNum => 列号
    // error => 错误栈
    window.onerror = function(message, source, lineNum, colNum, error) {
      // 1. 对跨域的js,如cdn,不会有详细的报错信息
      // 2. 对于压缩的js,还要配合sourceMap反查到未压缩代码的行、列
      // 压缩之后可能只有一行,他的行号可能永远只有一行或者两行
    }

11.3. 什么是JSON

  • 答案
    • json是一种数据格式,本质是一段字符串
    • json格式和js对象结构一致,对js语言更友好
      • 其实在json普及之前,js里面的一些数据格式的操作都是用xml去做的
      • XMLHttpRequest这个api其实就是由xml引起的名字,一直沿用到现在
    • window.JSON是一个全局对象
      • JSON.stringify(obj) => 对象转json字符串
      • JSON.parse(str) => json字符串转对象
  • 注意事项
    • json格式和js不一样的地方是,他的键和值必须用双引号引起来,js里面可以用单引号,但json里面必须用双引号,值的话,布尔类型和整型不用引号

11.4. 获取当前页面url的参数

  • 答案
    • 传统方式 => location.search
      • 获取?后面的内容,然后做字符串截取,用正则匹配
    • 新的api => URLSearchParams
      • 很简单,但要考虑浏览器兼容问题
    • 手写demo传送门
    js
    // 给当前地址添加参数
    history.pushState("","","?a=123&b=666")
    console.log("获取当前页面路径", window.location.href)
    
    // 获取参数,即地址?后面的所有内容
    console.log("获取参数", location.search)
    
    /**
     * 传统方式
     * 封装成函数 - 获取某个参数的值
     */
    let query1 = name=> {
      // substr和array.slice有点类似
      // 传入1表示从1开始截取,截到最后
      // 干掉第一个字符 => ?
      const search = location.search.substr(1)
      // console.log(search)
    
      // RegExp是正则的构造函数
      // new RegExp('^abc$') === /^abc$/
      // search: a=123&b=666
      // 每个键名(a,b)前,要么是字符串开始,要么就是&符号
      // 用括号表示他们是一个整体,|表示或
      // 这里的name表示键名
      // 中括号中有^表示非,这里的[^&]表示非&的字符
      // *表示有0个或者多个,整体表示,值的匹配规则为非&字符,并且有0个或者多个
      // (&|$)表示最后出现&(123&,表示还有值)或者直接结尾
      // i表示大小写不区分,即也可以写成
      // /(^|&)${name}=([^&]*)(&|$)/i
      const reg = new RegExp(`(^|&)${name}=([^&]*)(&|$)`, 'i')
    
      // match表示匹配,拿字符串去匹配这个正则表达式
      // 他返回的是一个数组
      // 第0项表示从某个参数(b)开始,匹配的整个这个参数的内容(&b=666)
      // match的匹配是按照括号来的,括号表示一个整体
      // 第1项返回的是第一个括号里面的内容
      // 第二项匹配的是第二个括号里面,即值
      const res = search.match(reg)
      // console.log(res)
    
      // 如果没匹配到
      if(res === null) {
        return null
      }
    
      // 如果匹配到了
      // 数组的第2项表示他的值
      return res[2]
    }
    console.log("传统方式获取路径上b参数的值", query1('b'))
    
    /**
     * 新的api => URLSearchParams
      * 封装成函数 - 获取某个参数的值
      */
    let query2 = name=> {
      const search = location.search
      const p = new URLSearchParams(search)
    
      return p.get(name)
    }
    console.log("新的api获取路径上b参数的值", query2('b'))
    // 给当前地址添加参数
    history.pushState("","","?a=123&b=666")
    console.log("获取当前页面路径", window.location.href)
    
    // 获取参数,即地址?后面的所有内容
    console.log("获取参数", location.search)
    
    /**
     * 传统方式
     * 封装成函数 - 获取某个参数的值
     */
    let query1 = name=> {
      // substr和array.slice有点类似
      // 传入1表示从1开始截取,截到最后
      // 干掉第一个字符 => ?
      const search = location.search.substr(1)
      // console.log(search)
    
      // RegExp是正则的构造函数
      // new RegExp('^abc$') === /^abc$/
      // search: a=123&b=666
      // 每个键名(a,b)前,要么是字符串开始,要么就是&符号
      // 用括号表示他们是一个整体,|表示或
      // 这里的name表示键名
      // 中括号中有^表示非,这里的[^&]表示非&的字符
      // *表示有0个或者多个,整体表示,值的匹配规则为非&字符,并且有0个或者多个
      // (&|$)表示最后出现&(123&,表示还有值)或者直接结尾
      // i表示大小写不区分,即也可以写成
      // /(^|&)${name}=([^&]*)(&|$)/i
      const reg = new RegExp(`(^|&)${name}=([^&]*)(&|$)`, 'i')
    
      // match表示匹配,拿字符串去匹配这个正则表达式
      // 他返回的是一个数组
      // 第0项表示从某个参数(b)开始,匹配的整个这个参数的内容(&b=666)
      // match的匹配是按照括号来的,括号表示一个整体
      // 第1项返回的是第一个括号里面的内容
      // 第二项匹配的是第二个括号里面,即值
      const res = search.match(reg)
      // console.log(res)
    
      // 如果没匹配到
      if(res === null) {
        return null
      }
    
      // 如果匹配到了
      // 数组的第2项表示他的值
      return res[2]
    }
    console.log("传统方式获取路径上b参数的值", query1('b'))
    
    /**
     * 新的api => URLSearchParams
      * 封装成函数 - 获取某个参数的值
      */
    let query2 = name=> {
      const search = location.search
      const p = new URLSearchParams(search)
    
      return p.get(name)
    }
    console.log("新的api获取路径上b参数的值", query2('b'))

12. 第11组面试题

12.1. 题目

  • 将url参数解析为js对象
  • 手写数组flatern,考虑多层级
    • 把数组拍平
    • 多层级表示要递归
  • 数组去重
    • 算是网红题

12.2. 将url参数解析为js对象

  • 答案
    js
    // 给当前地址添加参数
    history.pushState("","","?a=123&b=666")
    
    /**
     * 传统方式,分析search
     */
    let queryToObj1 = ()=> {
      const res = {}
      const search = location.search.substr(1)
    
      // 根据&分割成数组
      search.split('&').forEach(paramStr=> {
        const arr = paramStr.split('=')
        const key = arr[0]
        const val = arr[1]
        res[key] = val
      })
    
      return res
    }
    console.log("使用传统方式", queryToObj1())
    
    /**
     * 使用新的api
     */
    const queryToObj2 = ()=> {
      let res = {}
      let pList = new URLSearchParams(location.search)
      // 注意,是先val再是key
      pList.forEach((val, key)=> {
        res[key] = val
      })
    
      return res
    }
    console.log("使用新的api", queryToObj2())
    // 给当前地址添加参数
    history.pushState("","","?a=123&b=666")
    
    /**
     * 传统方式,分析search
     */
    let queryToObj1 = ()=> {
      const res = {}
      const search = location.search.substr(1)
    
      // 根据&分割成数组
      search.split('&').forEach(paramStr=> {
        const arr = paramStr.split('=')
        const key = arr[0]
        const val = arr[1]
        res[key] = val
      })
    
      return res
    }
    console.log("使用传统方式", queryToObj1())
    
    /**
     * 使用新的api
     */
    const queryToObj2 = ()=> {
      let res = {}
      let pList = new URLSearchParams(location.search)
      // 注意,是先val再是key
      pList.forEach((val, key)=> {
        res[key] = val
      })
    
      return res
    }
    console.log("使用新的api", queryToObj2())

12.3. 手写数组flatern,考虑多层级

  • 需求
    • 不管数组里面有多少层,全部按照顺序拍平到一个数组里面
  • 答案
    js
    /**
     * 对于简单的二层结构拍平
     * 对于更深层次的数组,要是用递归
     */
    let arr0 = [1, 2, [3, 4], [5, 6], 7]
    // apply第一个参数是this
    // 第二个参数是把所有的参数放到一个集合中
    // 类似于call,参数展开来写
    // 以下三个等价
    // concat表示连接数组
    // arrayObject.concat(arrayX,arrayX,......,arrayX)
    // 表示通过把所有 arrayX 参数添加到 arrayObject 中生成一个新的数组
    // 如果要进行 concat() 操作的参数是数组,那么添加的是数组中的元素,而不是数组
    console.log("apply", Array.prototype.concat.apply([], arr0))  // [1, 2, 3, 4, 5, 6, 7]
    console.log("call", Array.prototype.concat.call([], 1, 2, [3, 4], [5, 6], 7))
    console.log("[]", [].concat(1, 2, [3, 4], [5, 6], 7))
    
    /**
     * 对于复杂深层次数组拍平
     * 递归
     */
    let arr = [1, 2, [3, 4, [10, 20, [100, 200]]], 5]
    // console.log(arr)
    
    // 拍平数组
    let flat = arr=> {
      // 验证arr中,有无深层数组
      // some表示只要有一个符合
      // 即判断当前数组中,是否有一个是数组的形式
      // 按顺序查找,有一个就返回
      // 这个是简写,实际上是返回item
      let isDeep = arr.some(item => item instanceof Array)
      // 表示递归的终止条件
      if(!isDeep) {
        return arr  // 已经是平的了,即[1, 2, 3, 4]
      }
    
      // 开始第一层拍平
      let res = Array.prototype.concat.apply([], arr)
      // console.log(res)
    
      // 递归,每次判断数组是否拍平,如果没有就返回,如果有层级,继续
      return flat(res)
    }
    
    let res = flat(arr)
    console.log("递归拍平数组", res)
    /**
     * 对于简单的二层结构拍平
     * 对于更深层次的数组,要是用递归
     */
    let arr0 = [1, 2, [3, 4], [5, 6], 7]
    // apply第一个参数是this
    // 第二个参数是把所有的参数放到一个集合中
    // 类似于call,参数展开来写
    // 以下三个等价
    // concat表示连接数组
    // arrayObject.concat(arrayX,arrayX,......,arrayX)
    // 表示通过把所有 arrayX 参数添加到 arrayObject 中生成一个新的数组
    // 如果要进行 concat() 操作的参数是数组,那么添加的是数组中的元素,而不是数组
    console.log("apply", Array.prototype.concat.apply([], arr0))  // [1, 2, 3, 4, 5, 6, 7]
    console.log("call", Array.prototype.concat.call([], 1, 2, [3, 4], [5, 6], 7))
    console.log("[]", [].concat(1, 2, [3, 4], [5, 6], 7))
    
    /**
     * 对于复杂深层次数组拍平
     * 递归
     */
    let arr = [1, 2, [3, 4, [10, 20, [100, 200]]], 5]
    // console.log(arr)
    
    // 拍平数组
    let flat = arr=> {
      // 验证arr中,有无深层数组
      // some表示只要有一个符合
      // 即判断当前数组中,是否有一个是数组的形式
      // 按顺序查找,有一个就返回
      // 这个是简写,实际上是返回item
      let isDeep = arr.some(item => item instanceof Array)
      // 表示递归的终止条件
      if(!isDeep) {
        return arr  // 已经是平的了,即[1, 2, 3, 4]
      }
    
      // 开始第一层拍平
      let res = Array.prototype.concat.apply([], arr)
      // console.log(res)
    
      // 递归,每次判断数组是否拍平,如果没有就返回,如果有层级,继续
      return flat(res)
    }
    
    let res = flat(arr)
    console.log("递归拍平数组", res)

12.4. 数组去重

  • 答案
    • 传统方式:遍历元素挨个比较、去重
    • ES6:使用Set
      • 特点:无序结构,且不能重复
    • 需要考虑计算效率
      • set不需要遍历,效率比较高
    • 手写demo传送门
    js
    let arr = [30, 10, 20, 30, 40, 10]
    
    /**
     * 传统方法,遍历
     * 有序
     */
    const unique1 = arr=> {
      let res = []
    
      // 相当于遍历两次,效率比set低
      arr.forEach(item=> {
        // 判断当前元素在res里面是否存在
        if(res.indexOf(item) < 0) {
          // 如果当前项在res数组里面没有,就添加
          res.push(item)
        }
      })
    
      return res
    }
    let res1 = unique1(arr)
    console.log("传统方法去重", res1)
    
    /**
     * ES6方法 Set
      * 无序结构
      */
    const unique2 = arr=> {
      // set可以接收数组来构建
      let set = new Set(arr)
      // console.log(set)  // 不是一个数组
      // ...实际上是一个结构,表示提取里面的内容
      return [...set]
    }
    let res2 = unique2(arr)
    console.log("ES6方法去重", res2)
    let arr = [30, 10, 20, 30, 40, 10]
    
    /**
     * 传统方法,遍历
     * 有序
     */
    const unique1 = arr=> {
      let res = []
    
      // 相当于遍历两次,效率比set低
      arr.forEach(item=> {
        // 判断当前元素在res里面是否存在
        if(res.indexOf(item) < 0) {
          // 如果当前项在res数组里面没有,就添加
          res.push(item)
        }
      })
    
      return res
    }
    let res1 = unique1(arr)
    console.log("传统方法去重", res1)
    
    /**
     * ES6方法 Set
      * 无序结构
      */
    const unique2 = arr=> {
      // set可以接收数组来构建
      let set = new Set(arr)
      // console.log(set)  // 不是一个数组
      // ...实际上是一个结构,表示提取里面的内容
      return [...set]
    }
    let res2 = unique2(arr)
    console.log("ES6方法去重", res2)

13. 第12组面试题

13.1. 题目

  • 手写深拷贝
  • 介绍一下RAF
    • requestAnimationFrame
    • 做动画用的,算是性能优化的一部分(体验优化)
  • 前端性能如何优化?一般从哪几个方面考虑

13.2. 手写深拷贝

  • 答案
    js
    const deepClone = (obj = {})=> {
      // obj是null,或者不是对象或数组,直接返回
      if(typeof obj !== 'object' || obj == null ) {
        // 递归里面,如果是值,直接返回
        return obj;
      }
    
      // 递归中如果遇到对象里面的值是对象或者数组,走下面的逻辑
      // 初始化返回结果
      let result;
      // 判断是否是数组
      if(obj instanceof Array) {
        result = []
      } else {
        result = {}
      }
    
      // 无论对象还是数组,都可以使用for in遍历
      for(let key in obj) {
        // 判断这个key是不是这个对象自身所拥有的属性
        // 保证key不是原型的属性
        if(obj.hasOwnProperty(key)) {
          // 递归(重点)
          // obj[key]表示值
          // 递归是为了防止对象中有深层次的东西,因为你不知道要拷贝的对象中有多少层
          result[key] = deepClone(obj[key]);
        }
      }
    
      // 返回结果
      return result
    }
    
    let obj = {
      age: 20,
      name: "xxx",
      address: {
        city: "beijing"
      },
      arr: ["a", "b", "c"]
    }
    
    let objcopy = deepClone(obj);
    console.log("深拷贝", objcopy)
    const deepClone = (obj = {})=> {
      // obj是null,或者不是对象或数组,直接返回
      if(typeof obj !== 'object' || obj == null ) {
        // 递归里面,如果是值,直接返回
        return obj;
      }
    
      // 递归中如果遇到对象里面的值是对象或者数组,走下面的逻辑
      // 初始化返回结果
      let result;
      // 判断是否是数组
      if(obj instanceof Array) {
        result = []
      } else {
        result = {}
      }
    
      // 无论对象还是数组,都可以使用for in遍历
      for(let key in obj) {
        // 判断这个key是不是这个对象自身所拥有的属性
        // 保证key不是原型的属性
        if(obj.hasOwnProperty(key)) {
          // 递归(重点)
          // obj[key]表示值
          // 递归是为了防止对象中有深层次的东西,因为你不知道要拷贝的对象中有多少层
          result[key] = deepClone(obj[key]);
        }
      }
    
      // 返回结果
      return result
    }
    
    let obj = {
      age: 20,
      name: "xxx",
      address: {
        city: "beijing"
      },
      arr: ["a", "b", "c"]
    }
    
    let objcopy = deepClone(obj);
    console.log("深拷贝", objcopy)
  • 注意事项
    • Object.assign不是深拷贝 => 浅拷贝
    js
    /**
     * 验证Object.assign不是深拷贝
     */
    let obj0 = {
      a: 1,
      b: 20
    }
    // 向obj0追加,不是拷贝
    Object.assign(obj0, {
      c: 40
    })
    console.log("Object.assign追加", obj0)
    
    // 将新对象和obj0合并,得到一个新对象,不是拷贝
    let obj01 = Object.assign({}, obj0, {
      d: 20
    })
    // 原对象不变
    console.log("obj0, obj01原对象", obj0, obj01)
    obj0.a = 666
    // obj01的a的值没有变,看似深拷贝
    console.log("浅层次,改了obj0的a的值,查看 obj01", obj0, obj01)
    
    // 实际上是浅拷贝,层级一深就不行了
    let obj00 = {
      a: 10,
      b: {
        x: 100,
        y: 100
      }
    }
    let obj001 = Object.assign({}, obj00, {
      c: 300
    })
    console.log("obj00, obj001原对象", obj00, obj001)
    // 修改深层次的值
    obj00.b.x = 666
    // 深层次的
    console.log("深层次,改了obj00的x的值,查看 obj001", obj00, obj001)
    /**
     * 验证Object.assign不是深拷贝
     */
    let obj0 = {
      a: 1,
      b: 20
    }
    // 向obj0追加,不是拷贝
    Object.assign(obj0, {
      c: 40
    })
    console.log("Object.assign追加", obj0)
    
    // 将新对象和obj0合并,得到一个新对象,不是拷贝
    let obj01 = Object.assign({}, obj0, {
      d: 20
    })
    // 原对象不变
    console.log("obj0, obj01原对象", obj0, obj01)
    obj0.a = 666
    // obj01的a的值没有变,看似深拷贝
    console.log("浅层次,改了obj0的a的值,查看 obj01", obj0, obj01)
    
    // 实际上是浅拷贝,层级一深就不行了
    let obj00 = {
      a: 10,
      b: {
        x: 100,
        y: 100
      }
    }
    let obj001 = Object.assign({}, obj00, {
      c: 300
    })
    console.log("obj00, obj001原对象", obj00, obj001)
    // 修改深层次的值
    obj00.b.x = 666
    // 深层次的
    console.log("深层次,改了obj00的x的值,查看 obj001", obj00, obj001)

13.3. 介绍一下RAF

  • 背景
    • 我们不管用js还是css执行动画,想要动画流畅,更新频率要在60帧/s,即一秒钟动画要动60次,即16.67ms更新一次视图
      • 1000/60 约等于 16.67,是一个无限循环的小数
    • 这样人的肉眼就会觉得这个动画很流畅,不卡顿
    • 如果用js去控制动画的话,要用setTimeout
    • setTimeout要手动控制频率
    • 而RAF,浏览器会自动控制
    • 后台标签或隐藏iframe中,RAF会暂停,而setTimeout依然执行
      • 有些不应该耗费性能的地方
      • Chrome已经最小化了
      • setTimeout不是做动画用的,他主要是用来做异步定时器的
  • 答案
    • RAF,全称requestAnimationFrame,他是浏览器自带的api,主要用来做动画,浏览器会自动控制,比如在一些不应该耗费性能的地方会自动暂停动画的渲染,主要就是RAF的功劳
    • 手写demo传送门
    html
    <style type="text/css">
      #div1 {
        width: 100px;
        height: 50px;
        background-color: red;
        margin-bottom: 10px;
      }
      #div2 {
        width: 100px;
        height: 50px;
        background-color: red;
      }
    </style>
    <div id="div1"></div>
    <div id="div2"></div>
    
    <script>
      /**
       * 3s宽度从100px变为640px,即增加540px
       * 60帧/s => 3s 180 帧 => 每次变化 3px => 每次增加16.7ms
       */
      let curWidth = 100  // 当前宽度是100
      let maxWidth = 640  // 最大宽度640
    
      let $div1 = document.querySelector('#div1')
      let $div2 = document.querySelector('#div2')
    
      // 使用setTimeout
      const animate1 = ()=> {
        curWidth = curWidth + 3
        $div1.style.cssText=`width:${curWidth}px`
    
        // 如果当前宽度小于最大宽度
        if(curWidth < maxWidth) {
          // 类似递归
          // 16.67ms更新一次视图,即每16.7ms,增加3px
          // 需要自己控制时间
          // 如果增加1px,为了保证流畅度,要16.7/3
          // 执行次数会变多
          // 帧率变低会卡顿,帧率变高会耗性能,需要自己评估
          setTimeout(animate1, 16.7)
        }
      }
      animate1()
    
      // 使用RAF
      // 切换选项卡或者最小化浏览器,他会暂停
      let animate2 = ()=> {
        curWidth = curWidth + 3
        $div2.style.cssText=`width:${curWidth}px`
    
        if(curWidth < maxWidth) {
          // 时间不用自己控制
          window.requestAnimationFrame(animate2)
        }
      }
      animate2()
    </script>
    <style type="text/css">
      #div1 {
        width: 100px;
        height: 50px;
        background-color: red;
        margin-bottom: 10px;
      }
      #div2 {
        width: 100px;
        height: 50px;
        background-color: red;
      }
    </style>
    <div id="div1"></div>
    <div id="div2"></div>
    
    <script>
      /**
       * 3s宽度从100px变为640px,即增加540px
       * 60帧/s => 3s 180 帧 => 每次变化 3px => 每次增加16.7ms
       */
      let curWidth = 100  // 当前宽度是100
      let maxWidth = 640  // 最大宽度640
    
      let $div1 = document.querySelector('#div1')
      let $div2 = document.querySelector('#div2')
    
      // 使用setTimeout
      const animate1 = ()=> {
        curWidth = curWidth + 3
        $div1.style.cssText=`width:${curWidth}px`
    
        // 如果当前宽度小于最大宽度
        if(curWidth < maxWidth) {
          // 类似递归
          // 16.67ms更新一次视图,即每16.7ms,增加3px
          // 需要自己控制时间
          // 如果增加1px,为了保证流畅度,要16.7/3
          // 执行次数会变多
          // 帧率变低会卡顿,帧率变高会耗性能,需要自己评估
          setTimeout(animate1, 16.7)
        }
      }
      animate1()
    
      // 使用RAF
      // 切换选项卡或者最小化浏览器,他会暂停
      let animate2 = ()=> {
        curWidth = curWidth + 3
        $div2.style.cssText=`width:${curWidth}px`
    
        if(curWidth < maxWidth) {
          // 时间不用自己控制
          window.requestAnimationFrame(animate2)
        }
      }
      animate2()
    </script>

13.4. 前端性能如何优化

  • 答案
    • 原则:多使用内存、缓存,减少计算,减少网络请求
    • 方向:加载页面、渲染页面、页面操作流畅
      • 加载页面:
        • 减少资源体积:压缩代码
        • 减少访问次数:合并代码、雪碧图、ssr服务端渲染、缓存
        • 使用更快的网络:CDN
      • 渲染页面:
        • CSS放在head,JS放在body最下面
        • JS用DOMContentLoaded触发
        • 对DOM查询进行缓存
        • 频繁DOM操作,使用代码片段合并到一起插入DOM结构
      • 页面操作流畅:
        • 动画使用requestAnimationFrame
        • 频繁输入或者频繁操作的时候最后触发 => 防抖 => 输入框监听
        • 频繁输入或者频繁操作的时候,保持一个频率,连续触发 => 节流 => 拖拽