0
点赞
收藏
分享

微信扫一扫

前端面试js篇

芝婵 2022-02-15 阅读 54

 1. js的数据类型
  - 基本数据类型(5种): string,number,null,undefined, boolean
  - 复杂数据类型(1种): object
  - es6新增(1种):symbol、
  - BigInt

2. js的变量申明方式
  - var
  - let
  - const
  - function
  - import
  - class

3. let, const, var的区别
  - let和const是es6新增的申明类型
  - let用于申明变量,const用于申明常量(如果是一个对象,只改变里面的一个属性,最好也用const)
  - 它们都有块级作用域,不具备变量提升(会有暂时性死区),不能重复申明
  - var的作用域是全局的,会有变量提升,可以前置访问

4. 如何判断一个变量的类型,以及typeof和instanceof的区别
  - typeof 和 instanceof 常用来判断一个变量是否为空, 或者是什么类型的
  - typeof的返回值是一个字符串,用来说明变量的数据类型
  - typeof 一般只能返回如下几个结果: number, boolean, string, function, object, undefined。
    - 其中对象,数组,null,以及window,document的值都是object
  - instanceof的返回值为布尔值;
  - instanceof 用于判断一个变量是否属于某个对象的实例。

5. 用过哪些数组排序的方法

  ##### 冒泡排序
    ```
      // 冒泡排序: 比较两个相邻的项,如果第一个大于第二个则交换他们的位置,元素项向上移动至正确的顺序,就好像气泡往上冒一样
      // 冒泡demo:
      function bubbleSort(arr) {
          let len = arr.length;
          for (let i = 0; i < len; i++) {
              for (let j = 0; j < len - 1 - i; j++) {
                  if (arr[j] > arr[j+1]) {        //相邻元素两两对比
                      [arr[j + 1], arr[j]] = [arr[j], arr[j + 1]];
                  }
              }
          }
          return arr;
      }
    ```
  ##### 快速排序
    ```
      // 1) 首先,在数组中选择一个中间项作为主元
      // 2) 创建两个指针,左边的指向数组第一个项,右边的指向最后一个项,移动左指针,直到找到一个比主元大的项,接着,移动右边的指针,直到找到一个比主元小的项,然后交换它们。重复这个过程,直到
      // 左侧的指针超过了右侧的指针。这个使比主元小的都在左侧,比主元大的都在右侧。这一步叫划分操作
      // 3) 接着,算法对划分后的小数组(较主元小的值组成的的小数组, 以及较主元大的值组成的小数组)重复之前的两个步骤,直到排序完成
      // 快排demo:
      function quickSort(arr, left, right) {
          let len = arr.length;
          let partitionIndex;
          left = typeof left !== 'number' ? 0 : left;
          right = typeof right !== 'number' ? len - 1 : right;
          if (left < right) {
              partitionIndex = partition(arr, left, right);
              quickSort(arr, left, partitionIndex - 1);
              quickSort(arr, partitionIndex + 1, right);
          }
          return arr;
      }
    
      function partition(arr, left, right) {     //分区操作
          let pivot = left;                      //设定基准值(pivot)
          let index = pivot + 1;
          for (let i = index; i <= right; i++) {
              if (arr[i] < arr[pivot]) {
                  [arr[i], arr[index]] = [arr[index], arr[i]];
                  index++;
              }
          }
          [arr[pivot], arr[index - 1]] = [arr[index - 1], arr[pivot]];
          return index - 1;
      }
    ```
  ##### 选择排序
    ```
      // 选择排序:大概思路是找到最小的放在第一位,找到第二小的放在第二位,以此类推 算法复杂度O(n^2)
      // 选择demo:
      function selectionSort(arr) {
        let len = arr.length;
        let minIndex;
        for (let i = 0; i < len - 1; i++) {
          minIndex = i;
          for (let j = i + 1; j < len; j++) {
            if (arr[j] < arr[minIndex]) {     //寻找最小的数
                minIndex = j;                 //将最小数的索引保存
              }
          }
          [arr[i], arr[minIndex]] = [arr[minIndex], arr[i]];
        }
      return arr;
      }
    ```
  ##### 插入排序
    ```
      // 插入排序:每次排一个数组项,假设数组的第一项已经排序,接着,把第二项与第一项进行对比,第二项是该插入到第一项之前还是之后,第三项是该插入到第一项之前还是第一项之后还是第三项
      // 插入demo:
      function insertionSort(arr) {
        let len = arr.length;
        let preIndex, current;
        for (let i = 1; i < len; i++) {
            preIndex = i - 1;
            current = arr[i];
            while (preIndex >= 0 && arr[preIndex] > current) {
              arr[preIndex + 1] = arr[preIndex];
              preIndex--;
            }
            arr[preIndex + 1] = current;
        }
        return arr;
      }
    ```
  ##### 归并排序
    ```
      // 归并排序:Mozilla Firefox 使用归并排序作为Array.prototype.sort的实现,而chrome使用快速排序的一个变体实现的,前面三种算法性能不好,但归并排序性能不错 算法复杂度O(nlog^n)
      // 归并排序是一种分治算法。本质上就是把一个原始数组切分成较小的数组,直到每个小数组只有一个位置,接着把小数组归并成较大的数组,在归并过程中也会完成排序,直到最后只有一个排序完毕的大数组
      // 归并demo:
      function mergeSort(arr) {  //采用自上而下的递归方法
          let len = arr.length;
          if(len < 2) {
              return arr;
          }
          let middle = Math.floor(len / 2),
          left = arr.slice(0, middle),
          right = arr.slice(middle);
          return merge(mergeSort(left), mergeSort(right));
      }
    
      function merge(left, right){
          let result = [];
          while (left.length && right.length) {
              if (left[0] <= right[0]) {
                  result.push(left.shift());
              } else {
                  result.push(right.shift());
              }
          }
          result.push(...left);
          result.push(...right);
          return result;
      }
    ```
  ##### 堆排序
    ```
      //堆排序:堆排序把数组当中二叉树来排序而得名。
      // 1)索引0是树的根节点;2)除根节点为,任意节点N的父节点是N/2;3)节点L的左子节点是2*L;4)节点R的右子节点为2*R + 1
      // 本质上就是先构建二叉树,然后把根节点与最后一个进行交换,然后对剩下对元素进行二叉树构建,进行交换,直到剩下最后一个
      // 堆demo:
      var len;    //因为声明的多个函数都需要数据长度,所以把len设置成为全局变量
    
      function buildMaxHeap(arr) {   //建立大顶堆
          len = arr.length;
          for (let i = Math.floor(len / 2); i >= 0; i--) {
              heapify(arr, i);
          }
      }
    
      function heapify(arr, i) {     //堆调整
          let left = 2 * i + 1;
          let right = 2 * i + 2;
          let largest = i;
          if (left < len && arr[left] > arr[largest]) {
              largest = left;
          }
          if (right < len && arr[right] > arr[largest]) {
              largest = right;
          }
          if (largest !== i) {
              [arr[i], arr[largest]] = [arr[largest], arr[i]];
              heapify(arr, largest);
          }
      }
    
      function heapSort(arr) {
          buildMaxHeap(arr);
          for (let i = arr.length - 1; i > 0; i--) {
              [arr[0],arr[i]]=[arr[i],arr[0]];
              len--;
              heapify(arr, 0);
          }
          return arr;
      }
    ```

6. 类型转换
  ##### 转Boolean
    在条件判断时,除了 undefined, null, false, NaN, '', 0, -0,其他所有值都转为 true,包括所有对象。
  ##### 对象转基本类型
    对象在转换基本类型时,首先会调用 valueOf 然后调用 toString。并且这两个方法你是可以重写的。
      let a = {
        valueOf() {
          return 0
        }
      }
    当然你也可以重写 Symbol.toPrimitive ,该方法在转基本类型时调用优先级最高。
      let a = {
        valueOf() {
          return 0;
        },
        toString() {
          return '1';
        },
        [Symbol.toPrimitive]() {
          return 2;
        }
      }
      1 + a // => 3
      '1' + a // => '12'
  ##### 四则运算符
    只有当加法运算时,其中一方是字符串类型,就会把另一个也转为字符串类型。其他运算只要其中一方是数字,那么另一方就转为数字。并且加法运算会触发三种类型转换:将值转换为原始值,转换为数字,转换为字符串。
    1 + '1' // '11'
    2 * '2' // 4
    [1, 2] + [2, 1] // '1,22,1'
    // [1, 2].toString() -> '1,2'
    // [2, 1].toString() -> '2,1'
    // '1,2' + '2,1' = '1,22,1'
    对于加号需要注意这个表达式 'a' + + 'b'
    
    'a' + + 'b' // -> "aNaN"
    // 因为 + 'b' -> NaN
    // 你也许在一些代码中看到过 + '1' -> 1
  4. == 操作符

    ![avatar](https://yck-1254263422.cos.ap-shanghai.myqcloud.com/blog/2019-06-01-043719.png)

7. 原型(重点)
  - 每个函数都有 prototype 属性,除了 Function.prototype.bind(),该属性指向原型。
  - 每个对象都有 __proto__ 属性,指向了创建该对象的构造函数的原型。其实这个属性指向了 [[prototype]],但是 [[prototype]] 是内部属性,我们并不能访问到,所以使用 _proto_ 来访问。
  - 对象可以通过 __proto__ 来寻找不属于该对象的属性,__proto__ 将对象连接起来组成了原型链。

8. new的过程做了什么
  1. 新生成了一个对象
  2. 链接到原型
  3. 绑定 this
  4. 返回新对象

9. this
  1. 普通函数的this指向调用这个函数的对象,默认是window
  2. 构造函数的this指向new出来的实例对象,而且优先级是最高的,不能被改变
  3. 箭头函数的this指向的是它外面的第一个不是箭头函数的函数的 this, 在定义时就确定了,不能被改变
  4. 事件处理函数的this指向事件对象

10. call, apply, bind 区别(重点)
  - call 和 apply 都是为了解决改变 this 的指向。作用都是相同的,只是传参的方式不同。
  - 除了第一个参数外,call 可以接收一个参数列表,apply 只接受一个参数数组。
  - bind 和其他两个方法作用也是一致的,只是该方法会返回一个函数,要调用。并且我们可以通过 bind 实现柯里化。

11. 闭包(重点)
  - 闭包的定义很简单:函数 A 返回了一个函数 B,并且函数 B 中使用了函数 A 的变量,函数 B 就被称为闭包。
  ```
    function A() {
      let a = 1
      function B() {
          console.log(a)
      }
      return B
    }
  ```
  - 为什么函数 A 已经弹出调用栈了,函数 B 还能引用到函数 A 中的变量。因为函数 A 中的变量这时候是存储在堆上的。现在的 JS 引擎可以通过逃逸分析辨别出哪些变量需要存储在堆上,哪些需要存储在栈上。
  - 经典面试题,循环中使用闭包解决 var 定义函数的问题
  ```
    for ( var i=1; i<=5; i++) {
      setTimeout( function timer() {
        console.log( i );
      }, i*1000 );
    }

    使用闭包解决
    for (var i = 1; i <= 5; i++) {
      (function(j) {
        setTimeout(function timer() {
          console.log(j);
        }, j * 1000);
      })(i);
    }
  ```
12. 深浅拷贝
  ```
    let a = {
        age: 1
    }
    let b = a
    a.age = 2
    console.log(b.age) // 2
  ```
  从上述例子中我们可以发现,如果给一个变量赋值一个对象,那么两者的值会是同一个引用,其中一方改变,另一方也会相应改变。
  通常在开发中我们不希望出现这样的问题,我们可以使用浅拷贝来解决这个问题。
  ##### 浅拷贝
    1. 使用Object.assign
    ```
      let a = {
          age: 1
      }
      let b = Object.assign({}, a)
      a.age = 2
      console.log(b.age) // 1
    ```
    2. 使用展开运算符(…)
    ```
      let a = {
          age: 1
      }
      let b = {...a}
      a.age = 2
      console.log(b.age) // 1
    ```
  - 通常浅拷贝就能解决大部分问题了,但是当我们遇到如下情况就需要使用到深拷贝了
    ```
      let a = {
          age: 1,
          jobs: {
              first: 'FE'
          }
      }
      let b = {...a}
      a.jobs.first = 'native'
      console.log(b.jobs.first) // native
      浅拷贝只解决了第一层的问题,如果接下去的值中还有对象的话,那么就又回到刚开始的话题了,两者享有相同的引用。要解决这个问题,我们需要引入深拷贝
    ```
  ##### 深拷贝
    这个问题通常可以通过 JSON.parse(JSON.stringify(object)) 来解决。
    ```
      let a = {
          age: 1,
          jobs: {
              first: 'FE'
          }
      }
      let b = JSON.parse(JSON.stringify(a))
      a.jobs.first = 'native'
      console.log(b.jobs.first) // FE
    ```
    但是该方法也是有局限性的:
      - 会忽略 undefined
      - 会忽略 symbol
      - 不能序列化函数
      - 不能解决循环引用的对象
    如果遇到这些情况,可以使用递归或者 lodash(百度去官网查看) 的深拷贝函数

13. 防抖(重点)
  你是否在日常开发中遇到一个问题,在滚动事件中需要做个复杂计算或者实现一个按钮的防二次点击操作。

  这些需求都可以通过函数防抖动来实现。尤其是第一个需求,如果在频繁的事件回调中做复杂计算,很有可能导致页面卡顿,不如将多次计算合并为一次计算,只在一个精确点做操作。

  PS:防抖和节流的作用都是防止函数多次调用。区别在于,假设一个用户一直触发这个函数,且每次触发函数的间隔小于wait,防抖的情况下只会调用一次,而节流的 情况会每隔一定时间(参数wait)调用函数。

  我们先来看一个袖珍版的防抖理解一下防抖的实现:
  ```
    // func是用户传入需要防抖的函数
    // wait是等待时间
    const debounce = (func, wait = 50) => {
      // 缓存一个定时器id
      let timer = 0
      // 这里返回的函数是每次用户实际调用的防抖函数
      // 如果已经设定过定时器了就清空上一次的定时器
      // 开始一个新的定时器,延迟执行用户传入的方法
      return function(...args) {
        if (timer) clearTimeout(timer)
        timer = setTimeout(() => {
          func.apply(this, args)
        }, wait)
      }
    }
    // 不难看出如果用户调用该函数的间隔小于wait的情况下,上一次的时间还未到就被清除了,并不会执行函数
  ```
  这是一个简单版的防抖,但是有缺陷,这个防抖只能在最后调用。一般的防抖会有immediate选项,表示是否立即调用。这两者的区别,举个栗子来说:
  - 例如在搜索引擎搜索问题的时候,我们当然是希望用户输入完最后一个字才调用查询接口,这个时候适用延迟执行的防抖函数,它总是在一连串(间隔小于wait的)函数触发之后调用。
  - 例如用户给interviewMap点star的时候,我们希望用户点第一下的时候就去调用接口,并且成功之后改变star按钮的样子,用户就可以立马得到反馈是否star成功了,这个情况适用立即执行的防抖函数,它总是在第一次调用,并且下一次调用必须与前一次调用的时间间隔大于wait才会触发。

  下面我们来实现一个带有立即执行选项的防抖函数
  ```
    // 这个是用来获取当前时间戳的
    function now() {
      return +new Date()
    }
    /**
    * 防抖函数,返回函数连续调用时,空闲时间必须大于或等于 wait,func 才会执行
    *
    * @param  {function} func        回调函数
    * @param  {number}   wait        表示时间窗口的间隔
    * @param  {boolean}  immediate   设置为ture时,是否立即调用函数
    * @return {function}             返回客户调用函数
    */
    function debounce (func, wait = 50, immediate = true) {
      let timer, context, args

      // 延迟执行函数
      const later = () => setTimeout(() => {
        // 延迟函数执行完毕,清空缓存的定时器序号
        timer = null
        // 延迟执行的情况下,函数会在延迟函数中执行
        // 使用到之前缓存的参数和上下文
        if (!immediate) {
          func.apply(context, args)
          context = args = null
        }
      }, wait)

      // 这里返回的函数是每次实际调用的函数
      return function(...params) {
        // 如果没有创建延迟执行函数(later),就创建一个
        if (!timer) {
          timer = later()
          // 如果是立即执行,调用函数
          // 否则缓存参数和调用上下文
          if (immediate) {
            func.apply(this, params)
          } else {
            context = this
            args = params
          }
        // 如果已有延迟执行函数(later),调用的时候清除原来的并重新设定一个
        // 这样做延迟函数会重新计时
        } else {
          clearTimeout(timer)
          timer = later()
        }
      }
    }
  ```
  整体函数实现的不难,总结一下。
  - 对于按钮防点击来说的实现:如果函数是立即执行的,就立即调用,如果函数是延迟执行的,就缓存上下文和参数,放到延迟函数中去执行。一旦我开始一个定时器,只要我定时器还在,你每次点击我都重新计时。一旦你点累了,定时器时间到,定时器重置为 null,就可以再次点击了。
  - 对于延时执行函数来说的实现:清除定时器ID,如果是延迟调用就调用函数

14. 节流(重点)
  防抖动和节流本质是不一样的。防抖动是将多次执行变为最后一次执行,节流是将多次执行变成每隔一段时间执行。
  ```
    /**
    * underscore 节流函数,返回函数连续调用时,func 执行频率限定为 次 / wait
    *
    * @param  {function}   func      回调函数
    * @param  {number}     wait      表示时间窗口的间隔
    * @param  {object}     options   如果想忽略开始函数的的调用,传入{leading: false}。
    *                                如果想忽略结尾函数的调用,传入{trailing: false}
    *                                两者不能共存,否则函数不能执行
    * @return {function}             返回客户调用函数
    */
    _.throttle = function(func, wait, options) {
        var context, args, result;
        var timeout = null;
        // 之前的时间戳
        var previous = 0;
        // 如果 options 没传则设为空对象
        if (!options) options = {};
        // 定时器回调函数
        var later = function() {
          // 如果设置了 leading,就将 previous 设为 0
          // 用于下面函数的第一个 if 判断
          previous = options.leading === false ? 0 : _.now();
          // 置空一是为了防止内存泄漏,二是为了下面的定时器判断
          timeout = null;
          result = func.apply(context, args);
          if (!timeout) context = args = null;
        };
        return function() {
          // 获得当前时间戳
          var now = _.now();
          // 首次进入前者肯定为 true
        // 如果需要第一次不执行函数
        // 就将上次时间戳设为当前的
          // 这样在接下来计算 remaining 的值时会大于0
          if (!previous && options.leading === false) previous = now;
          // 计算剩余时间
          var remaining = wait - (now - previous);
          context = this;
          args = arguments;
          // 如果当前调用已经大于上次调用时间 + wait
          // 或者用户手动调了时间
        // 如果设置了 trailing,只会进入这个条件
        // 如果没有设置 leading,那么第一次会进入这个条件
        // 还有一点,你可能会觉得开启了定时器那么应该不会进入这个 if 条件了
        // 其实还是会进入的,因为定时器的延时
        // 并不是准确的时间,很可能你设置了2秒
        // 但是他需要2.2秒才触发,这时候就会进入这个条件
          if (remaining <= 0 || remaining > wait) {
            // 如果存在定时器就清理掉否则会调用二次回调
            if (timeout) {
              clearTimeout(timeout);
              timeout = null;
            }
            previous = now;
            result = func.apply(context, args);
            if (!timeout) context = args = null;
          } else if (!timeout && options.trailing !== false) {
            // 判断是否设置了定时器和 trailing
          // 没有的话就开启一个定时器
            // 并且不能不能同时设置 leading 和 trailing
            timeout = setTimeout(later, remaining);
          }
          return result;
        };
      };
  ```

15. Promise(重点)
  Promise 是 ES6 新增的语法,解决了回调地狱的问题。

  可以把 Promise 看成一个状态机。初始是 pending 状态,可以通过函数 resolve 和 reject ,将状态转变为 resolved 或者 rejected 状态,状态一旦改变就不能再次变化。

  then 函数会返回一个 Promise 实例,并且该返回值是一个新的实例而不是之前的实例。因为 Promise 规范规定除了 pending 状态,其他状态是不可以改变的,如果返回的是一个相同实例的话,多个 then 调用就失去意义了。

  Promise还拥有很多的api:
   - then  promise状态变为resolve执行
   - catch   promise状态变为reject执行
   - all接受一个promise实例数组,如果每个实例都成功,那么返回成功的结果的数值,否则返回第一个失败的结果  
   - race接受一个promise实例数组,只返回第一个状态改变的返回值
   - finally

16. async 和 await
  一个函数如果加上 async ,那么该函数就会返回一个 Promise
  ```
    async function test() {
      return "1";
    }
    console.log(test()); // -> Promise {<resolved>: "1"}
  ```
  可以把 async 看成将函数返回值使用 Promise.resolve() 包裹了下。

  await 只能在 async 函数中使用
  ```
    function sleep() {
      return new Promise(resolve => {
        setTimeout(() => {
          console.log('finish')
          resolve("sleep");
        }, 2000);
      });
    }
    async function test() {
      let value = await sleep();
      console.log("object");
    }
    test()
  ```
  上面代码会先打印 finish 然后再打印 object 。因为 await 会等待 sleep 函数 resolve ,所以即使后面是同步代码,也不会先去执行同步代码再来执行异步代码。

  async 和 await 相比直接使用 Promise 来说,优势在于处理 then 的调用链,能够更清晰准确的写出代码。缺点在于滥用 await 可能会导致性能问题,因为 await 会阻塞代码,也许之后的异步代码并不依赖于前者,但仍然需要等待前者完成,导致代码失去了并发性。

  下面来看一个使用 await 的代码。
  ```
    var a = 0
    var b = async () => {
      a = a + await 10
      console.log('2', a) // -> '2' 10
      a = (await 10) + a
      console.log('3', a) // -> '3' 20
    }
    b()
    a++
    console.log('1', a) // -> '1' 1
  ```
  对于以上代码你可能会有疑惑,这里说明下原理

  - 首先函数 b 先执行,在执行到 await 10 之前变量 a 还是 0,因为在 await 内部实现了 generators ,generators 会保留堆栈中东西,所以这时候 a = 0 被保存了下来
  - 因为 await 是异步操作,遇到await就会立即返回一个pending状态的Promise对象,暂时返回执行代码的控制权,使得函数外的代码得以继续执行,所以会先执行 console.log('1', a)
  - 这时候同步代码执行完毕,开始执行异步代码,将保存下来的值拿出来使用,这时候 a = 10
  - 然后后面就是常规执行代码了

17. 解构
  ES6 允许按照一定模式,从数组和对象中提取值,对变量进行赋值,这被称为解构。
  举例:
    - let a = 1; let b = 2; [b, a] = [a, b];
    - import {Component} from 'react'
    - import {getTableData} from '../api.js'
    - getTableData().then(res => {let { data } = res})

18. 箭头函数
  1. 写法简单

    省略function关键字,如果只有一个参数可以省略小括号,代码块只有一条语句,可以省略大括号,省略大括号还有自动return的作用,等等
  2. this指向更加明确

    箭头函数的this指向的是它外面的第一个不是箭头函数的函数的 this

19. Set
  ES6 提供了新的数据结构 Set。它类似于数组,但是成员的值都是唯一的,没有重复的值,所以Set方法一般用来数组去重
  ```
    let arr = [2, 3, 3, 4, 6, 5, 3, 4, 4];
    let arr2 = [...new Set(arr)];
  ```
  这里new Set出来的不是真正的数组,所以需要用到...扩展运算符

20. 高阶函数
  - foreach
  - map
  - some
  - filter
  - every
  - reduce

 21. V8 下的垃圾回收机制
  V8 实现了准确式 GC,GC 算法采用了分代式垃圾回收机制。因此,V8 将内存(堆)分为新生代和老生代两部分。
  ##### 新生代算法
    新生代中的对象一般存活时间较短,使用 Scavenge GC 算法。
    在新生代空间中,内存空间分为两部分,分别为 From 空间和 To 空间。在这两个空间中,必定有一个空间是使用的,另一个空间是空闲的。新分配的对象会被放入 From 空间中,当 From 空间被占满时,新生代 GC 就会启动了。算法会检查 From 空间中存活的对象并复制到 To 空间中,如果有失活的对象就会销毁。当复制完成后将 From 空间和 To 空间互换,这样 GC 就结束了。
  ##### 老生代算法
    老生代中的对象一般存活时间较长且数量也多,使用了两个算法,分别是标记清除算法和标记压缩算法。
    在讲算法前,先来说下什么情况下对象会出现在老生代空间中:
    - 新生代中的对象是否已经经历过一次 Scavenge 算法,如果经历过的话,会将对象从新生代空间移到老生代空间中。
    - To 空间的对象占比大小超过 25 %。在这种情况下,为了不影响到内存分配,会将对象从新生代空间移到老生代空间中。
    在老生代中,以下情况会先启动标记清除算法:
    - 某一个空间没有分块的时候
    - 空间中被对象超过一定限制
    - 空间不能保证新生代中的对象移动到老生代中

22. eventloop(高频)
  这个最好去b站找个视频好好理解

23. 事件机制
  ##### 事件触发三阶段
    - window 往事件触发处传播,遇到注册的捕获事件会触发
    - 传播到事件触发处时触发注册的事件
    - 从事件触发处往 window 传播,遇到注册的冒泡事件会触发
  ##### 注册事件
    通常我们使用 addEventListener 注册事件,该函数的第三个参数可以是布尔值,也可以是对象。对于布尔值 useCapture 参数来说,该参数默认值为 false 。useCapture 决定了注册的事件是捕获事件还是冒泡事件。对于对象参数来说,可以使用以下几个属性
    
    - capture,布尔值,和 useCapture 作用一样
    - once,布尔值,值为 true 表示该回调只会调用一次,调用后会移除监听
    - passive,布尔值,表示永远不会调用 preventDefault
    
    一般来说,我们只希望事件只触发在目标上,这时候可以使用 stopPropagation 来阻止事件的进一步传播。通常我们认为 stopPropagation 是用来阻止事件冒泡的,其实该函数也可以阻止捕获事件。stopImmediatePropagation 同样也能实现阻止事件,但是还能阻止该事件目标执行别的注册事件。
  ##### 事件代理
    如果一个节点中的子节点是动态生成的,那么子节点需要注册事件的话应该注册在父节点上
    <ul id="ul">
      <li>1</li>
      <li>2</li>
      <li>3</li>
      <li>4</li>
      <li>5</li>
    </ul>
    <script>
      let ul = document.querySelector('##ul')
      ul.addEventListener('click', event => {
        console.log(event.target)
      })
    </script>
    事件代理的方式相对于直接给目标注册事件来说,有以下优点
    - 节省内存
    - 不需要给子节点注销事件

24. 跨域(高频、重点)
  因为浏览器出于安全考虑,有同源策略。也就是说,如果协议、域名或者端口有一个不同就是跨域,Ajax 请求会失败。
  我们可以通过以下几种常用方法解决跨域的问题
  ##### JSONP
    JSONP 的原理很简单,就是利用 <script> 标签没有跨域限制的漏洞。通过 <script> 标签指向一个需要访问的地址并提供一个回调函数来接收数据当需要通讯时。
    JSONP 使用简单且兼容性不错,但是只限于 get 请求。
  ##### CORS
    CORS 需要浏览器和后端同时支持。IE 8 和 9 需要通过 XDomainRequest 来实现。
    浏览器会自动进行 CORS 通信,实现 CORS 通信的关键是后端。只要后端实现了 CORS,就实现了跨域。
    服务端设置 Access-Control-Allow-Origin 就可以开启 CORS。 该属性表示哪些域名可以访问资源,如果设置通配符则表示所有网站都可以访问资源

  ##### document.domain
    该方式只能用于二级域名相同的情况下,比如 a.test.com 和 b.test.com 适用于该方式。
    只需要给页面添加 document.domain = 'test.com' 表示二级域名都相同就可以实现跨域

  ##### postMessage
    这种方式通常用于获取嵌入页面中的第三方页面数据。一个页面发送消息,另一个页面判断来源并接收消息

25. 模块化
  在有 Babel 的情况下,我们可以直接使用 ES6 的模块化
  ```
    // file a.js
    export function a() {}
    export function b() {}
    // file b.js
    export default function() {}

    import {a, b} from './a.js'
    import XXX from './b.js'
  ```
  ##### CommonJS
    CommonJs 是 Node 独有的规范,浏览器中使用就需要用到 Browserify 解析了。
    ```
      // a.js
      module.exports = {
          a: 1
      }
      // or
      exports.a = 1
   
      // b.js
      var module = require('./a.js')
      module.a // -> log 1
    ```
    在上述代码中,module.exports 和 exports 很容易混淆,让我们来看看大致内部实现
    ```
      var module = require('./a.js')
      module.a
      // 这里其实就是包装了一层立即执行函数,这样就不会污染全局变量了,
      // 重要的是 module 这里,module 是 Node 独有的一个变量
      module.exports = {
          a: 1
      }
      // 基本实现
      var module = {
        exports: {} // exports 就是个空对象
      }
      // 这个是为什么 exports 和 module.exports 用法相似的原因
      var exports = module.exports
      var load = function (module) {
          // 导出的东西
          var a = 1
          module.exports = a
          return module.exports
      };
    ```
    再来说说 module.exports 和 exports,用法其实是相似的,但是不能对 exports 直接赋值,不会有任何效果。
    对于 CommonJS 和 ES6 中的模块化的两者区别是:
    - 前者支持动态导入,也就是 require(${path}/xx.js),后者目前不支持,但是已有提案
    - 前者是同步导入,因为用于服务端,文件都在本地,同步导入即使卡住主线程影响也不大。而后者是异步导入,因为用于浏览器,需要下载文件,如果也采用同步导入会对渲染有很大影响
    - 前者在导出时都是值拷贝,就算导出的值变了,导入的值也不会改变,所以如果想更新值,必须重新导入一次。但是后者采用实时绑定的方式,导入导出的值都指向同一个内存地址,所以导入值会跟随导出值变化
    - 后者会编译成 require/exports 来执行的

  ##### AMD
    AMD 是由 RequireJS 提出的
    ```
      // AMD
      define(['./a', './b'], function(a, b) {
          a.do()
          b.do()
      })
      define(function(require, exports, module) {
          var a = require('./a')
          a.doSomething()
          var b = require('./b')
          b.doSomething()
      })
    ```
26. HTTP请求的过程
    1. 对www.baidu.com这个网址进行DNS域名解析,得到对应的IP地址

  2. 根据这个IP,找到对应的服务器,发起TCP的三次握手

  3. 建立TCP连接后发起HTTP请求

  4. 服务器响应HTTP请求,浏览器得到html代码

  5. 浏览器解析html代码,并请求html代码中的资源(如js、css图片等)(先得到html代码,才能去找这些资源)

  6. 浏览器对页面进行渲染呈现给用户

27. HTTP状态码

1. 100:这个状态码是告诉客户端应该继续发送请求,这个临时响应是用来通知客户端的,部分的请求服务器已   经接受,但是客户端应继续发送求请求的剩余部分,如果请求已经完成,就忽略这个响应,而且服务器会在请求完成后向客户发送一个最终的结果
2. 200:这个是最常见的http状态码,表示服务器已经成功接受请求,并将返回客户端所请求的最终结果
3. 301:客户端请求的网页已经永久移动到新的位置,当链接发生变化时,返回301代码告诉客户端链接的变化,客户端保存新的链接,并向新的链接发出请求,已返回请求结果
4.  304 Not Modified
5. 404:请求失败,客户端请求的资源没有找到或者是不存在
6. 500:服务器遇到未知的错误,导致无法完成客户端当前的请求。
7. 503:服务器由于临时的服务器过载或者是维护,无法解决当前的请求
8. 504 Gateway Timeout


 

举报

相关推荐

0 条评论