# 2019年08月刊

# 2019/08/26 - 2019/09/01 ⌚️


  • 如何构建搜索二叉树 ?

    点击
    // 搜索二叉树是这么一种树,对于任何一个节点来说,它的左子树的值肯定小于它的值,它的右子树的值肯定大于它的值
    function Node(val) {
      this.left = null;
      this.right = null;
      this.value = val;
    }
    
    function generateBST(root, array) {
      var length = array.length;
    
      for (var i = 1; i < length; i++) {
        insertNode(root, array[i]);
      }
    }
    
    function insertNode(node, value) {
      if (value < node.value) {
        if (node.left === null) {
          node.left = new Node(value);
        } else {
          node = node.left;
          insertNode(node, value);
        }
      } else {
        if (node.right === null) {
          node.right = new Node(value);
        } else {
          node = node.right;
          insertNode(node, value);
        }
      }
    }
    
    var array = [2, 3, 4, 12, 3, 54, 6, 7, 1];
    var root = new Node(array[0]);
    
    generateBST(root, array);
    
    console.log(root);
    
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
  • async await在事件队列中是怎样的存在?

    点击

    从字面意思上看 await 就是等待,await 等待的是一个表达式,这个表达式的返回值可以是一个 promise 对象也可以是其他值。

    很多人以为 await 会一直等待之后的表达式执行完之后才会继续执行后面的代码,实际上 await 是一个让出线程的标志。await 后面的表达式会先执行一遍,将 await 后面的代码加入到 microtask 中,然后就会跳出整个 async 函数来执行后面的代码。

    由于因为 async await 本身就是 promise+generator 的语法糖。所以 await 后面的代码是 microtask。所以

    async function async1() {
      console.log('async1 start');
      await async2();
      console.log('async1 end');
    }
    // 等价于
    async function async1() {
      console.log('async1 start');
      Promise.resolve(async2()).then(() => {
        console.log('async1 end');
      });
    }
    
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
  • 深度优先遍历和广度优先遍历?

    点击

    深度优先:找到一个节点后,把它的后辈都找出来,最常用递归法。

    广度优先:找到一个节点后,把他同级的兄弟节点都找出来放在前边,把孩子放到后边,最常用 while

  • ['1', '2', '3'].map(parseInt) 的输出结果是什么?

    点击

    第一眼看到这个题目的时候,脑海跳出的答案是 [1, 2, 3],但是真正的答案是[1, NaN, NaN]

    首先让我们回顾一下,map 函数的第一个参数 callback:

    var new_array = arr.map(function callback(currentValue[, index[, array]]) { // Return element for new_array }[, thisArg])

    这个 callback 一共可以接收三个参数,其中第一个参数代表当前被处理的元素,而第二个参数代表该元素的索引。

    而 parseInt 则是用来解析字符串的,使字符串成为指定基数的整数。 parseInt(string, radix) 接收两个参数,第一个表示被处理的值(字符串),第二个表示为解析时的基数。

    了解这两个函数后,我们可以模拟一下运行情况

    parseInt('1', 0); //radix为0时,且string参数不以“0x”和“0”开头时,按照10为基数处理。这个时候返回1
    
    parseInt('2', 1); //基数为1(1进制)表示的数中,最大值小于2,所以无法解析,返回NaN
    
    parseInt('3', 2); //基数为2(2进制)表示的数中,最大值小于3,所以无法解析,返回NaN
    
    1
    2
    3
    4
    5

    map 函数返回的是一个数组,所以最后结果为 [1, NaN, NaN]

  • a.b.c.da['b']['c']['d'],哪个性能更高?

    点击

    a.b.c.d 比 a['b']['c']['d'] 性能高点,后者还要考虑 [ ] 中是变量的情况,再者,从两种形式的结构来看,显然编译器解析前者要比后者容易些,自然也就快一点。

    两者的 AST 对比

  • vue Diff 的比较逻辑

    点击

    Vue 只会对新旧节点父节点是相同节点那一层子节点 进行比较

    也可以说成是

    只有两个新旧节点是相同节点的时候,才会去比较他们各自的子节点

    最大的根节点一开始可以直接比较

    这也叫做 同层级比较,并不需要递归,虽然好像降低了一些复用性,也是为了避免过度优化,是一种很高效的 Diff 算法

    比较逻辑:

    1、能不移动,尽量不移动

    2、没得办法,只好移动

    3、实在不行,新建或删除

  • 组件的 data 为什么要写成函数形式?

    点击

    Vue 的组件都是可复用的,一个组件创建好后,可以在多个地方复用,而不管复用多少次,组件内的 data 都应该是相互隔离,互不影响的,所以组件每复用一次,data 就应该复用一次,每一处复用组件的 data 改变应该对其他复用组件的数据不影响。

    为了实现这样的效果,data 就不能是单纯的对象,而是以一个函数返回值的形式,所以每个组件实例可以维护独立的数据拷贝,不会相互影响。

# 2019/08/19 - 2019/08/25 ⌚️


  • 为什么 Vuex 的 mutation 中不能做异步操作?

    点击

    Vuex 中所有的状态更新的唯一途径都是 mutation,异步操作通过 Action 来提交 mutation 实现,这样使得我们可以方便地跟踪每一个状态的变化,从而让我们能够实现一些工具帮助我们更好地了解我们的应用。

  • vue 子组件为什么不可以修改父组件传递的 Prop?

    点击

    Vue 提倡单向数据流,即父级 props 的更新会流向子组件,但是反过来则不行。这是为了防止意外的改变父组件状态,使得应用的数据流变得难以理解。如果破坏了单向数据流,当应用复杂时,debug 的成本会非常高。

  • 手动实现 call、apply、bind (opens new window)

  • package.json 中库版本号详解?

    点击

    当我们看 package.json 文件时,会看到以下几种版本号的表示方式,有的是一个^,有的是~,而有的就是单纯的数字

    "babel-core": "7.0.0-bridge.0",
    "babel-eslint": "^10.0.1",
    "babel-jest": "~23.6.0",
    
    1
    2
    3

    当我们使用 npm install —save 安装时,优先会使用插入符号(^)而不是波浪线(~)

    版本号的含义 node 中的版本号都是由 3 个数字用(.)连接起来,三个数字的含义分别为 major, minor, patch

    例如,10.0.1 对应 major, minor, patch 就是:10 是 marjor version, 0 是 minor version, 1 是 patch version

    其中:

    major version: 这个版本号变化了表示有了一个不可以和上个版本兼容的更新

    minor version: 这个版本号变化了表示增加了新功能,并可以向后兼容

    patch version: 这个版本号变化了表示修复了 bug, 并可以向后兼容

    ~ 和 ^ 的区别

    波浪符号(~):会更新到当前 minor version(中间的数字)的最新版本,即 23.6.0 会更新到 23.6.x 的最新版本,如果出了一个 23.7.0,则不会自动升级

    插入符号(^): 会更新到当前 major version(前面的数字)的最新版本,即 23.6.0 会更新到 23.x.x 的最新版本,但是不会更新到 24.x.x

    最后如果前面不加符号,就安装特定版本的库,不会自动安装更高版本号的库

  • 如何实现 js 的动态引入?

    点击

    首先定义一个函数,传入两个参数,url 和 callback. callback 用于 js 加载完成时调用。

    因为可能出现重复加载,因此使用闭包来保存已经加载的 js 路径。代码如下:

    function loadScript(url, callback) {
      // 通过闭包保存已经加载过的JS,防止重复加载
      const urlList = [];
      return function() {
        if (urlList.indexOf(url) > -1) {
          return;
        }
        callback = callback || function() {};
        const script = document.createElement('script');
        script.type = 'text/javascript';
        if (script.readyState) {
          //IE
          script.onreadystatechange = function() {
            if (
              script.readyState === 'loaded' ||
              script.readyState === 'complete'
            ) {
              script.onreadystatechange = null;
              callback();
            }
          };
        } else {
          //Others
          script.onload = function() {
            callback();
          };
        }
        script.src = url;
        document.body.appendChild(script);
        urlList.push(url);
      };
    }
    
    // 使用方法
    loadScript(
      'https://cdn.bootcss.com/html2canvas/0.5.0-beta4/html2canvas.min.js'
    )();
    
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
  • BFC 的约束规则?

    点击

    内部的 Box 会在垂直方向上一个接一个的放置

    垂直方向上的距离由 margin 决定。(完整的说法是:属于同一个 BFC 的两个相邻 Box 的 margin 会发生重叠(塌陷),与方向无关。)

    每个元素的左外边距与包含块的左边界相接触(从左向右),即使浮动元素也是如此。(这说明 BFC 中子元素不会超出他的包含块,而 position 为 absolute 的元素可以超出他的包含块边界)

    BFC 的区域不会与 float 的元素区域重叠

    计算 BFC 的高度时,浮动子元素也参与计算

    BFC 就是页面上的一个隔离的独立容器,容器里面的子元素不会影响到外面元素,反之亦然

  • 如何反转二叉树??

    点击
    function Mirror(root) {
      if (root === null) {
        return;
      }
      let temp = root.left;
      root.left = root.right;
      root.right = temp;
      Mirror(root.left);
      Mirror(root.right);
    }
    
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10

# 2019/08/12 - 2019/08/18 ⌚️


# 2019/08/05 - 2019/08/11 ⌚️


  • vm.$set()实现原理是什么?

    点击

    因为组件是可以复用的,JS 里对象是引用关系,如果组件 data 是一个对象,那么子组件中的 data 属性值会互相污染,产生副作用。

    所以一个组件的 data 选项必须是一个函数,因此每个实例可以维护一份被返回对象的独立的拷贝。new Vue 的实例是不会被复用的,因此不存在以上问题。

    export function set(target: Array<any> | Object, key: any, val: any): any {
      // target 为数组
      if (Array.isArray(target) && isValidArrayIndex(key)) {
        // 修改数组的长度, 避免索引>数组长度导致splice()执行有误
        target.length = Math.max(target.length, key); // 利用数组的splice变异方法触发响应式
        target.splice(key, 1, val);
        return val;
      }
      // target为对象, key在target或者target.prototype上 且必须不能在 Object.prototype 上,直接赋值
      if (key in target && !(key in Object.prototype)) {
        target[key] = val;
        return val;
      }
      // 以上都不成立, 即开始给target创建一个全新的属性
      // 获取Observer实例
      const ob = (target: any).__ob__; // target 本身就不是响应式数据, 直接赋值
      if (!ob) {
        target[key] = val;
        return val;
      }
      // 进行响应式处理
      defineReactive(ob.value, key, val);
      ob.dep.notify();
      return val;
    }
    
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    1. 如果目标是数组,使用 vue 实现的变异方法 splice 实现响应式

    2. 如果目标是对象,判断属性存在,即为响应式,直接赋值

    3. 如果 target 本身就不是响应式,直接赋值

    4. 如果属性不是响应式,则调用 defineReactive 方法进行响应式处理

  • Vue 组件 data 为什么必须是函数 ?

    点击

    因为组件是可以复用的,JS 里对象是引用关系,如果组件 data 是一个对象,那么子组件中的 data 属性值会互相污染,产生副作用。

    所以一个组件的 data 选项必须是一个函数,因此每个实例可以维护一份被返回对象的独立的拷贝。new Vue 的实例是不会被复用的,因此不存在以上问题。

  • vue 是如何对数组方法进行变异的 ?

    点击

    简单来说,Vue 通过原型拦截的方式重写了数组的 7 个方法,首先获取到这个数组的 ob,也就是它的 Observer 对象,如果有新的值,就调用 observeArray 对新的值进行监听,然后手动调用 notify,通知 render watcher,执行 update

  • Vue 中的 key 到底有什么用?

    点击

    key 是给每一个 vnode 的唯一 id,依靠 key,我们的 diff 操作可以更准确、更快速 (对于简单列表页渲染来说 diff 节点也更快,但会产生一些隐藏的副作用,比如可能不会产生过渡效果,或者在某些节点有绑定数据(表单)状态,会出现状态错位。)

    diff 算法的过程中,先会进行新旧节点的首尾交叉对比,当无法匹配的时候会用新节点的 key 与旧节点进行比对,从而找到相应旧节点.

    更准确 : 因为带 key 就不是就地复用了,在 sameNode 函数 a.key === b.key 对比中可以避免就地复用的情况。所以会更加准确,如果不加 key,会导致之前节点的状态被保留下来,会产生一系列的 bug。

    更快速 : key 的唯一性可以被 Map 数据结构充分利用,相比于遍历查找的时间复杂度 O(n),Map 的时间复杂度仅仅为 O(1),源码如下:

  • computed 和 watch 有什么区别及运用场景? (opens new window)

    Computed本质是一个具备缓存的watcher,依赖的属性发生变化就会更新视图。
    适用于计算比较消耗性能的计算场景。当表达式过于复杂时,在模板中放入过多逻辑会让模板难以维护,可以将复杂的逻辑放入计算属性中处理。
    
    Watch没有缓存性,更多的是观察的作用,可以监听某些数据执行回调。当我们需要深度监听对象中的属性时,可以打开deep:true选项,这样便会对对象中的每一项进行监听。这样会带来性能问题,优化的话可以使用字符串形式监听,如果没有写到组件中,不要忘记使用unWatch手动注销哦。
    
    1
    2
    3
    4
  • computed 的实现原理 (opens new window)

# 2019/07/29 - 2019/08/04 ⌚️


  • Vue 响应式原理 (opens new window)

  • JavaScript 严格模式下有哪些不同?

    点击
    • 不允许不使用 var 关键字去创建全局变量,抛出 ReferenceError
    • 不允许对变量使用 delete 操作符,抛 ReferenceError
    • 不可对对象的只读属性赋值,不可对对象的不可配置属性使用 delete 操作符,不可为不可拓展的对象添加属性,均抛 TypeError
    • 对象属性名必须唯一
    • 函数中不可有重名参数
    • 在函数内部对修改参数不会反映到 arguments 中
    • 淘汰 arguments.callee 和 arguments.caller
    • 不可在 if 内部声明函数
    • 抛弃 with 语句
  • 说出下列代码的输出顺序?

    点击
    <div id="app">
      <span id="name" ref="name">{{ name }}</span>
      <button @click="change">change name</button>
      <div id="content"></div>
    </div>
    <script>
      new Vue({
        el: '#app',
        data() {
          return {
            name: 'SHERlocked93',
          };
        },
        methods: {
          change() {
            const $name = this.$refs.name;
            this.$nextTick(() => console.log('setter前:' + $name.innerHTML));
            this.name = ' name改喽 ';
            console.log('同步方式:' + this.$refs.name.innerHTML);
            setTimeout(() =>
              this.console('setTimeout方式:' + this.$refs.name.innerHTML)
            );
            this.$nextTick(() => console.log('setter后:' + $name.innerHTML));
            this.$nextTick().then(() =>
              console.log('Promise方式:' + $name.innerHTML)
            );
          },
        },
      });
    </script>
    
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30

    这里涉及的知识是 vue.$nextTick()原理,详情可查看《全面解析 Vue.nextTick 实现原理》 (opens new window)

    同步方式:SHERlocked93
    setter前:SHERlocked93
    setter后:name改喽
    Promise方式:name改喽
    setTimeout方式:name改喽
    
    1
    2
    3
    4
    5
  • 实现一个 LazyMan,可以按照以下方式调用

    LazyMan(“Hank”) 输出: Hi! This is Hank!

    LazyMan(“Hank”).sleep(10).eat(“dinner”) 输出 Hi! This is Hank! // 等待 10 秒.. Wake up after 10 Eat dinner~

    LazyMan(“Hank”).eat(“dinner”).eat(“supper”) 输出 Hi This is Hank! Eat dinner~ Eat supper~

    点击
    class LazyMan {
      constructor(name) {
        this.name = name;
        this.asyncFun = Promise.resolve();
        console.log(`--------- 我就是 ${this.name}! ---------`);
      }
      sleep(delay) {
        this.asyncFun = this.asyncFun.then(() => {
          return new Promise(resolve => {
            setTimeout(() => {
              console.log(`--------- 我睡了 ${delay / 1000}s 然后 ----------`);
              resolve();
            }, delay);
          });
        });
        return this; //提供 ”链式调用“
      }
      eat(food) {
        this.asyncFun = this.asyncFun.then(() => {
          console.log(`--------- 吃 ${food}~ ---------`);
          return Promise.resolve();
        });
        return this;
      }
    }
    
    new LazyMan('小皮咖')
      .sleep(4000)
      .eat('豆浆')
      .eat('油条')
      .sleep(2000)
      .eat('炒年糕');
    
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
  • vue.$set 的作用是?

    向响应式对象中添加一个属性,并确保这个新属性同样是响应式的,且触发视图更新。它必须用于向响应式对象上添加新属性,因为 Vue 无法探测普通的新增属性

  • 手写一个 sleep 函数?

    点击
    // 时间戳版本
    function sleep(time) {
      let startTime = new Date().getTime();
      while (new Date().getTime() - startTime < time) {}
      console.log('sleep ' + time + 'ms');
    }
    // promise
    function sleep2(time) {
      return new Promise((resolve, reject) => {
        setTimeout(() => {
          console.log('sleep ' + time + 'ms');
          resolve();
        }, time);
      });
    }
    
    // 测试
    const haha = async () => {
      console.log(11);
      sleep(2000);
      await sleep2(3000);
      console.log(222);
    };
    
    haha();
    
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
  • 在堆中怎么插入一个元素?

    将元素置于堆的末尾,然后调整此堆