js 开发者手册

265 阅读8分钟

递归

递归树结构扁平化数组

有时候需要将树结构的数据平铺开来,比如将下列水果以对象形式平铺到列表中

const data = [
  {
    id: 1,
    name: '苹果',
    child: [
      {
        id: 2,
        name: "青苹果"
      }
    ]
  },
  {
    id: 3,
    name: '香蕉',
    child: [
      {
        id: 4,
        name: "青香蕉"
      }
    ]
  }
]

//方式一
function flattenTree(node = [], arr = []) {
  for (let item of node) {
    if (item.child) {
      flattenTree(item.child, arr);
    }
    arr.push({ id: item.id, name: item.name });
  }
  return arr;
}

const res = flattenTree(data);
console.log(res)
// [
//   { id: 2, name: "青苹果" },
//   { id: 1, name: "苹果" },
//   { id: 4, name: "青香蕉" },
//   { id: 3, name: "香蕉" }
// ]
//方式二
function flattenTree(tree, parentPath = "") {
  let result = [];
  for (let node of tree) {
    const path = parentPath ? `${parentPath}.${node.id}` : `${node.id}`;
    result.push({ id: node.id, path });
    if (node.children && node.children.length > 0) {
      result = result.concat(flattenTree(node.children, path));
      // result = [...result, ...flattenTree(node.children, path)];
      // result.push(...flattenTree(node.children, path));
    }
  }
  return result;
}

递归一维数组转为树结构

一维数组特征是 叶子节点有 parent 属性 等于其父节点 id 属性, don't bb ,show me the code!

🧩 示例输入:平铺数组
const list = [
  { id: 1, name: "陕西省", parentId: null },
  { id: 2, name: "四川省", parentId: null },
  { id: 9, name: "成都市", parentId: 2 },
  { id: 10, name: "西安市", parentId: 1 },
  { id: 11, name: "咸阳市", parentId: 1 },
  { id: 12, name: "汉中市", parentId: 1 },
  { id: 13, name: "武功县", parentId: 11 },
  { id: 14, name: "勉县", parentId: 12 },
];
✅ 目标输出:树结构
[
  {
    id: 1,
    name: "陕西省",
    parentId: null,
    children: [
      { id: 10, name: "西安市", parent: 1 },
      {
        id: 11,
        name: "咸阳市",
        parent: 1,
        children: [{ id: 13, name: "武功县", parent: 11 }],
      },
      {
        id: 12,
        name: "汉中市",
        children: [{ id: 14, name: "勉县", parentId: 12 }],
      },
    ],
  },
  {
    id: 2,
    name: "四川省",
    parent: null,
    children: [{ id: 9, name: "成都市", parent: 2 }],
  },
];
//方式一  推荐
function arrayToTree(items) {
    const result = []; // 存放结果集
    const itemMap = {}; // 存放路径

    // 先转化为map存储
    for (const item of items) {
      itemMap[item.id] = { ...item, children: [] };
    }

    for (const item of items) {
      const id = item.id;
      const parentId = item.parentId;

      const mapItem = itemMap[id];

      if (parentId === null) {
        result.push(mapItem); // 如果没有父节点,直接放入结果集
      } else {
        if (!itemMap[parentId]) {
          itemMap[parentId] = {
            children: [],
          };
        }
        itemMap[parentId].children.push(mapItem); // 父节点存在则挂在父节点的children下
      }
    }
    return result;
  }
const tree = arrayToTree(data);
方式二  不推荐
function listToTree(data, parentId = null) {
  const result = [];
  for (let node of data) {
    if (node.parent === parentId) {
      result.push(node);
      const child = listToTree(data, node.id);
      node.child = child;
    }
  }
  return result;
}

console.log(listToTree(data))
renderTreeNodes = areaData =>
    areaData.map(item => {
      if (item.children) {
        return (
          <TreeNode title={item.name} key={item.path} {...item}>
            {this.renderTreeNodes(item.children)}
          </TreeNode>
        );
      }
      return <TreeNode key={item.path} {...item} title={item.name} />;
    });

查找某一个节点

function findNode(key, data) {
        let result = {}
        for (let node of data) {
          if (node.value == key) {
            return node;
          }
          if (node.children) {
            return findNode(key, node.children);
          }
        }
      }

查找所有父节点

const tree = {
    id: 1,
    children: [
      {
        id: 3,
        children: [
          { id: 4, children: [] },
          { id: 5, children: [] },
        ],
      },
      {
        id: 2,
        children: [],
      },
    ],
  };

  /**
   * 查找父级节点
   * @param tree 树形结构
   * @param targetId 目标节点id
   * @returns 节点id数组
   * @example fn(tree, 2) => [1,2]
   * @example fn(tree, 4) => [1,3,4]
   */

  function fn(data, targetId) {
    for (let node of data) {
      // console.log('node1',node.id);
      if (node.id == targetId) {
        return [node.id]
      }
      if(node.children){
        // console.log('node2',node.id);
        let nodeList = fn(node.children, targetId)
        // console.log('nodeList',nodeList);
        // console.log('node3',node.id);

        if(nodeList){
          // console.log('node.id',node.id);
          return [node.id].concat(nodeList)
        }
      }
    }
  }

  // console.log(fn([tree],1))
  // console.log(fn([tree],2))
  console.log(fn([tree], 5)); //[1,3,5]

多维数组降维

const data = [1, 2, 3, [4, 5, [6, 7]], [8, 9]];

function flatArray(arr) {
  let result = [];
  for (let item of arr) {
    if (!Array.isArray(item)) {
      result.push(item);
    } else {
      result = [...result, ...flatArray(item)];
    }
  }
  return result;
}
console.log(flatArray(data));
// [1, 2, 3, 4, 5, 6, 7, 8, 9];

高阶函数

reduce

需求:找出数组中top值最大的元素
 const linkSections = [{link:'a',top:1},{link:'b',top:3},{link:'c',top:2}];
 const maxSection = linkSections.reduce((prev, curr) => (curr.top > prev.top ? curr : prev));
 console.log(maxSection) //{link: 'b', top: 3}
 

防抖与节流

防抖函数的核心思路如下:

  • 当触发一个函数时,并不会立即执行这个函数,而是会延迟(通过定时器来延迟函数的执行)
    • 如果在延迟时间内,有重新触发函数,那么取消上一次的函数执行(取消定时器);
    • 如果在延迟时间内,没有重新触发函数,那么这个函数就正常执行(执行传入的函数);

接下来,就是将思路转成代码即可:

  • 定义debounce函数要求传入两个参数
    • 需要处理的函数fn;
    • 延迟时间;
  • 通过定时器来延迟传入函数fn的执行
    • 如果在此期间有再次触发这个函数,那么clearTimeout取消这个定时器;
    • 如果没有触发,那么在定时器的回调函数中执行即可;
function debounce(fn, delay) {
  var timer = null;
  return function() {
    if (timer) clearTimeout(timer);
    timer = setTimeout(function() {
      fn();
    }, delay);
  }
}

优化参数和this

我们知道在oninput事件触发时会有参数传递,并且触发的函数中this是指向当前的元素节点的

  • 目前我们fn的执行是一个独立函数调用,它里面的this是window
  • 我们需要将其修改为对应的节点对象,而返回的function中的this指向的是节点对象;
  • 目前我们的fn在执行时是没有传递任何的参数的,它需要将触发事件时传递的参数传递给fn
  • 而我们返回的function中的arguments正是我们需要的参数;

所以我们的代码可以进行如下的优化:

function debounce(fn, delay) {
  var timer = null;
  return function() {
    if (timer) clearTimeout(timer);
    // 获取this和argument
    var _this = this;
    var _arguments = arguments;
    timer = setTimeout(function() {
      // 在执行时,通过apply来使用_this和_arguments
      fn.apply(_this, _arguments);
    }, delay);
  }
}

优化取消功能

有时候,在等待执行的过程中,可能需要取消之前的操作:

  • 比如用户进行了搜索,但是还没有来得及发送搜索的情况下,退出了界面;
  • 当用户退出时,之前的操作就可以取消掉;

我们这里将delay时间改长,并且在下方增加一个按钮:

  • 在延迟时间内,我们点击按钮,就取消之前的函数执行;

这一次我给出完整的代码结构:

  • HTML代码;
  • 第一个script标签中封装的是debounce函数;
  • 第二个script标签中是业务逻辑js代码;
<body>

  <input class="search" type="text">
  <button class="cancel-btn">取消事件</button>

  <script>
    function debounce(fn, delay) {
      var timer = null;
      var handleFn = function() {
        if (timer) clearTimeout(timer);
        // 获取this和argument
        var _this = this;
        var _arguments = arguments;
        timer = setTimeout(function() {
          // 在执行时,通过apply来使用_this和_arguments
          fn.apply(_this, _arguments);
        }, delay);
      }

      // 取消处理
      handleFn.cancel = function() {
        if (timer) clearTimeout(timer);
      }

      return handleFn;
    }
  </script>
  <script>
    // 1.获取输入框
    var search = document.querySelector(".search");

    // 2.监听输入内容,发送ajax请求
    // 2.1.定义一个监听函数
    var counter = 0;
    function searchChange(e) {
      counter++;
      console.log("发送"+ counter +"网络请求");
      console.log(this);
      console.log(e.target.value);
    }

    // 对searchChange处理
    var _searchChange = debounce(searchChange, 3000);

    // 绑定oninput
    search.oninput = _searchChange;

    // 3.取消事件
    var cancelBtn = document.querySelector(".cancel-btn");
    cancelBtn.onclick = function(event) {
      _searchChange.cancel();
    }

  </script>
</body>

节流基本功能

节流函数的默认实现思路我们采用时间戳的方式来完成:

  • 我们使用一个last来记录上一次执行的时间
    • 每次准备执行前,获取一下当前的时间now:now - last > interval
    • 那么函数执行,并且将now赋值给last即可
function throttle(fn, interval) {
  var last = 0;
  return function() {
    // this和argument
    var _this = this;
    var _arguments = arguments;
    var now = new Date().getTime();
    if (now - last > interval) {
      fn.apply(_this, _arguments);
      last = now;
    }
  }
}

优化最后执行

默认情况下,我们的防抖函数最后一次是不会执行的

  • 因为没有达到最终的时间,也就是条件now - last > interval满足不了的
  • 但是,如果我们希望它最后一次是可以执行的,那么我们可以让其传入对应的参数来控制

我们来看一下代码如何实现:

  • 我们增加了else语句:
    • 所以我们可以使用timer变量来记录定时器是否已经开启
    • 已经开启的情况下,不需要开启另外一个定时器了
    • else语句表示没有立即执行的情况下,就会开启定时器;
    • 但是定时器不需要频繁的开启,开启一次即可
  • 如果固定的频率中执行了回调函数
    • 因为刚刚执行过回调函数,所以定时器到时间时不需要执行;
    • 所以我们需要取消定时器,并且将timer赋值为null,这样的话可以开启下一次定时器;
  • 如果定时器最后执行了,那么timer需要赋值为null
    • 因为下一次重新开启时,只有定时器为null,才能进行下一次的定时操作;
function throttle(fn, interval) {
  var last = 0;
  var timer = null;
  return function() {
    // this和argument
    var _this = this;
    var _arguments = arguments;
    var now = new Date().getTime();
    if (now - last > interval) {
      if (timer) {
        clearTimeout(timer);
        timer = null;
      }
      fn.apply(_this, _arguments);
      last = now;
    } else if (timer === null) { // 只是最后一次
      timer = setTimeout(function() {
        timer = null;
        fn.apply(_this, _arguments);
      }, interval);
    }
  }
}

二分法计算最近的数组下标

function binarySearch (key,dataSource) {
    //var value = 0;
    var left = 0;
    var right = dataSource.length;
    while (left <= right) {
      var center = Math.floor((left + right) / 2);
      if (key < dataSource[center]) {
        right = center - 1;
      } else {
        left = center + 1;
      }
    }
    return right;
};
//测试数据
var tempArray = new Array("0", "10", "20", "30");
var obj = 28;
console.log(binarySearch(obj,tempArray));

图片懒加载

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>Document</title>

    <style>
      .img-box .lazy-img {
        width: 100%;
        height: 600px; /*如果已知图片高度可以设置*/
      }
      .lazy-img {
        border: 1px solid #ddd;
      }
    </style>
  </head>
  <body>
    <div class="img-box">
      <img
        class="lazy-img"
        data-src="https://t7.baidu.com/it/u=1732966997,2981886582&fm=193&f=GIF"
        alt="懒加载1"
        src="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mNkYAAAAAYAAjCB0C8AAAAASUVORK5CYII="
      />
      <img
        class="lazy-img"
        data-src="https://t7.baidu.com/it/u=1785207335,3397162108&fm=193&f=GIF"
        alt="懒加载2"
        src="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mNkYAAAAAYAAjCB0C8AAAAASUVORK5CYII="
      />
      <img
        class="lazy-img"
        data-src="https://t7.baidu.com/it/u=2581522032,2615939966&fm=193&f=GIF"
        alt="懒加载3"
        src="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mNkYAAAAAYAAjCB0C8AAAAASUVORK5CYII="
      />
      <img
        class="lazy-img"
        data-src="https://t7.baidu.com/it/u=245883932,1750720125&fm=193&f=GIF"
        alt="懒加载4"
        src="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mNkYAAAAAYAAjCB0C8AAAAASUVORK5CYII="
      />
      <img
        class="lazy-img"
        data-src="https://t7.baidu.com/it/u=3423293041,3900166648&fm=193&f=GIF"
        alt="懒加载5"
        src="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mNkYAAAAAYAAjCB0C8AAAAASUVORK5CYII="
      />
    </div>

    <script>
      // 方式一:采用getBoundingClientRect 监听滚动
      function lazyLoad() {
        let imgs = document.querySelectorAll("img[data-src]");
        for (let i = 0; i < imgs.length; i++) {
          let rect = imgs[i].getBoundingClientRect();
          console.log(rect);
          if (rect.top < window.innerHeight) {
            let newImg = new Image();
            // newImg.src = imgs[i].getAttribute("data-src");
            newImg.src = imgs[i].dataset.src;
            newImg.onload = function () {
              imgs[i].src = newImg.src;
            };
            imgs[i].removeAttribute("data-src");
          }
        }
      }
      // lazyLoad();
      // window.addEventListener("scroll", lazyLoad);

      // 方式二:采用IntersectionObserver API
      function observer() {
        const observe = new IntersectionObserver((entries) => {
          console.log('entries',entries);
          entries.forEach((entrie) => {
             //与视口交叉
            if (entrie.isIntersecting) {
              const img = entrie.target
              let newImg = new Image();
              newImg.src = img.dataset.src;
              newImg.onload = function () {
                img.src = newImg.src;
              };
              img.removeAttribute("data-src");
            }
          });
        },{
          rootMargin:'-100px'
        });
        let images = document.querySelectorAll("img[data-src]");
        images.forEach((img) => observe.observe(img));
      }

      observer()
    </script>
  </body>
</html>

结果:

image.png