JS面试题

性能优化

  • 是一个综合性问题,没有标准答案,但是要求尽量全面
  • 默写细节问题可能会单独提问:手写防抖,节流
  • 只关注核心点,针对面试

性能优化原则

  • 多使用内存,缓存或其他方法
  • 减少cpu计算量,减少网络加载耗时
  • (适用于所有编程的性能优化---空间换时间)

从何入手

  • 加载更快

    • 减少资源体积:压缩代码
    • 减少访问次数:合并代码(减少访问次数),SSR服务器端渲染,缓存
    • 使用更快的网络:CDN
  • 让渲染更快

    • CSS放在head,js放在body最下面

    • 尽早开始执行js,用DOMContentLoaded触发

    • 懒加载(图片懒加载,上滑加载更多)

    • 对DOM查询进行缓存

    • 频繁dom操作,合并到一起插入dom结构

    • 节流throttle防抖debounce

      • 防抖场景

        • 监听一个输入框的文字变化后触发change事件

        • 直接用keyup事件,则会触发change事件

        • 防抖:用户输入结束或暂停时,才会触发change事件

          <input type="text" id="input1">
          const input1 = document.getElementById('input1')
          let timer = null
          input1.addEventListener('keyup', function () {
            if (timer) {
              clearTimeout(timer)
            }
            timer = setTimeout(() => {
              // 模拟触发 change 事件
              console.log(input1.value);
          
              // 清空定时器
              timer = null
            }, 500)
          })
          
          // 防抖
          function debounce(fn, delay = 500) {
            // timer 是闭包中的
            let timer = null;
          
            return function () {
              if (timer) {
                clearTimeout(timer)
              }
              timer = setTimeout(() => {
                fn.apply(this, arguments);
                timer = null
              }, delay)
            }
          }
          
          input1.addEventListener('keyup', debounce(function (e) {
            console.log(e.target);
            console.log(input1.value)
          }, 600));
          
      • 节流

        • 拖拽一个元素时,要随时拿到该元素被拖拽的位置

        • 直接用drag事件,则会频繁触发,很容易导致卡顿

        • 节流:无论拖拽速度多快,都会每隔100ms触发一次

          const div1 = document.getElementById('div1')
          
          // let timer = null
          // div1.addEventListener('drag', function (e) {
          //     if (timer) {
          //         return
          //     }
          //     timer = setTimeout(() => {
          //         console.log(e.offsetX, e.offsetY)
          
          //         timer = null
          //     }, 100)
          // })
          
          // 节流
          function throttle(fn, delay = 100) {
              let timer = null;
          
              return function () {
                  if (timer) {
                      return
                  }
                  timer = setTimeout(() => {
                      fn.apply(this, arguments)
                      timer = null
                  }, delay)
              }
          }
          
          div1.addEventListener('drag', throttle(function (e) {
              console.log(e.offsetX, e.offsetY)
          }));
          
    • SSR(server side render)

      • 服务器端渲染:将网页和数据一起加载,一起渲染
      • 非SSR(前后端分离):先加载网页,在加载数据,在渲染数据
      • 早先的JSP,ASP,PHP,现在的vue React SSR

        静态资源加hash后缀,根据文件内容计算hash

        文件内容不变,则hash不变,则url不变、

        url和文件不变,则会自动触发http缓存机制,返回304

        先加载一个预览图,在加载实际图​

        安全

        • xss跨站请求攻击
          • 一个博客网站,我发表一篇博客,其中嵌入<script>脚本(document.cookie)
          • 脚本内容:获取cookie,发送到我的服务器(服务器配合跨域)
          • 发布这篇博客,有人查看它,我轻松获取访问者cookie
          • 预防:
            • 替换特殊字符,如<变成&lt;>变成&gt;
            • <script>变为&lt;script&gt;,直接显示,而不会作为脚本执行
            • 前后端都替换
        • xsrf跨站请求伪造
          • 你正在购物,看中了某个商品,商品id是100
          • 付费接口是xxx.com/pay?id=100,但没有任何验证
          • 我是攻击者,看中了一个商品,id是200
          • 我向你发送一封电子邮件,邮件标题很吸引人
          • 但邮件正文隐藏着<img src=xxx.com/pay?id=200/>
          • 你查看邮件,就帮我购买了id是200的商品
          • 预防:
            • 使用post接口
            • 增加验证,例如密码,短信验证码,指纹等
        • var和let const 的区别

          • var是es5语法,let const 是es6语法;var有变量提升
          • var和let是变量,可修改;const是常量,不可修改
          • let const有块作用域,var没有
        • split()和join()区别

          • split(separator) 方法用于把一个字符串分割成字符串数组。
            • separator可选。指定要使用的分隔符。如果省略该参数,则使用逗号作为分隔符
          • join() 方法用于把数组中的所有元素放入一个字符串。
        • 数组的pop push unshift shift 分别是是什么

          • 功能是什么

          • 返回值是什么

          • 是否会对原数组造成影响

             const arr = [10, 20, 30, 40]
            
            //  pop 方法用于删除并返回数组的最后一个元素。
             const popRes = arr.pop()
             console.log(popRes, arr);
            
            //  shift 方法用于删除并返回数组的第一个元素。
             const shiftRes = arr.shift()
             console.log(shiftRes, arr)
            
            //  push 
             const pushRes = arr.push(50) // 返回 原数组length
             console.log(pushRes, arr)
            
            // unshift
             const unshiftRes = arr.unshift(5) // 返回 原数组length
             console.log(unshiftRes, arr)
            
        • 手写深度比较,模拟lodash isEqual

          // 判断是否是对象或数组
          function isObject(obj) {
              return typeof obj === 'object' && obj !== null
          }
          // 全相等(深度)
          function isEqual(obj1, obj2) {
              if (!isObject(obj1) || !isObject(obj2)) {
                  // 值类型(注意,参与 equal 的一般不会是函数)
                  return obj1 === obj2
              }
              if (obj1 === obj2) {
                  return true
              }
              // 两个都是对象或数组,而且不相等
              // 1. 先取出 obj1 和 obj2 的 keys ,比较个数
              const obj1Keys = Object.keys(obj1);
              const obj2Keys = Object.keys(obj2);
              if (obj1Keys.length !== obj2Keys.length) {
                  return false
              }
              // 2. 以 obj1 为基准,和 obj2 一次递归比较
              for (let key in obj1) {
                  // 比较当前 key 的 val —— 递归!!!
                  const res = isEqual(obj1[key], obj2[key]);
                  if (!res) {
                      return false
                  }
              }
              // 3. 全相等
              return true
          }
          
          // 测试
          const obj1 = {
              a: 100,
              b: {
                  x: 100,
                  y: 200
              }
          }
          const obj2 = {
              a: 100,
              b: {
                  x: 100,
                  y: 200
              }
          }
          // console.log( obj1 === obj2 )
          console.log( isEqual(obj1, obj2) )
          
          const arr1 = [1, 2, 3]
          const arr2 = [1, 2, 3, 4]
          
        • 数组的api,有哪些纯函数?

          • 不改变原数组(没有副作用)

          • 返回一个新数组

            • 纯函数

              • concat()
              • map()
              • filter()
              • slice()
            • 非纯函数

              • push pop shift unshift

              • forEach

                • forEach() 方法用于调用数组的每个元素,并将元素传递给回调函数。没有返回值undefined

                  注意: forEach() 对于空数组是不会执行回调函数的。

              • some

                • some() 方法用于检测数组中的元素是否满足指定条件(函数提供)。
                • some() 方法会依次执行数组的每个元素:
                • 如果有一个元素满足条件,则表达式返回true , 剩余的元素不会再执行检测。
                • 如果没有满足条件的元素,则返回false。
                  注意: some() 不会对空数组进行检测。

 

          • 注意: some() 不会改变原始数组。
        • every

          every() 方法用于检测数组所有元素是否都符合指定条件(通过函数提供)。
          every() 方法使用指定函数检测数组中的所有元素:
          如果数组中检测到有一个元素不满足,则整个表达式返回 false ,且剩余的元素不会再进行检测。
          如果所有元素都满足条件,则返回 true。
          注意: every() 不会对空数组进行检测。
          注意: every() 不会改变原始数组
          
        • reduce

          reduce() 方法接收一个函数作为累加器,数组中的每个值(从左到右)开始缩减,最终计算为一个值。
          reduce() 可以作为一个高阶函数,用于函数的 compose。
          注意: reduce() 对于空数组是不会执行回调函数的。
          返回计算结果
          
  • 数组slice和splice的区别

    • 功能的区别(slice-切片,splice-剪接)

    • 参数和返回值

    • 是否是纯函数

      //slice是纯函数,返回新数组,不改变原数组
      const arr = [10, 20, 30, 40, 50]
      const arr1 = arr.slice()//不传参数直接返回原函数内容
      const arr2 = arr.slice(1, 4);//20,30,40
      const arr3 = arr.slice(2);//30,40,50  从2截取到末尾
      const arr4 = arr.slice(-3);//30,40,50  倒数第3位开始截取到末尾
      // splice 非纯函数,修改原数组,返回被删除的元素数组
      const spliceRes = arr.splice(1, 2, 'a', 'b', 'c');
      //arr: [10, "a", "b", "c", 40, 50], spliceRes:20,30 
      const spliceRes1 = arr.splice(1, 2)
      //arr:[10,40,50]  spliceRes1:20,30
      const spliceRes2 = arr.splice(1, 0, 'a', 'b', 'c')
      //arr:[10, "a", "b", "c", 20, 30, 40, 50],spliceRes2:[]
      
  • [10,20,30].map(parseInt)返回的结果是什么?

    //parseInt(数值,进制);  每一位数值不能大于等于进制数
    const res = [10, 20, 30].map(parseInt)
    console.log(res)
    // 拆解
    [10, 20, 30].map((num, index) => {
        return parseInt(num, index);
    });
    //10,NaN,NaN
    
  • ajax请求get和post的区别?

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

    fn.call(this,p1,p2,p3);
    fn.apply(this,arguments);
    
  • 事件代理(委托)是什

  • 闭包是什么,有什么特性?有什么负面影响

    • 回顾作用域和自由变量
    • 回顾闭包应用场景:作为参数被传入,作为返回值被返回
    • 回顾:自由变量的查找,要在函数定义的地方(而非函数执行的地方)
    • 负面影响
      • 变量会常驻内存,得不到释放,闭包不要乱用
  • 如何阻止事件冒泡和默认行为?

  • 查找,添加,删除,移动DOM节点的方法?

  • 如何减少DOM操作?

    • 缓存DOM查询结果
    • 多次DOM操作,合并到一次插入