写给前端的数据结构和算法 - 理解递归算法, 并实现 Proxy 的 Deep 版本

2,492 阅读4分钟

前言

最近一直在研究数据结构和算法在前端领域的应用, 颇有一些心得, 希望能开一个系列讲讲, 能帮助大家更好的抽象和设计 JavaScript 代码. 因为这两天在写一个有趣的东西, 其中涉及到将对象 Proxy 化, 但原生的 Proxy 是浅处理, 并不支持深度处理一个对象, 就和深度拷贝一样, 核心的算法就是递归.

加上递归在基础算法中算是比较难以理解的一种算法, 实际应用中也不太容易写, 就以这个问题, 让我们来聊聊递归

正文

掘金上讲算法的文章其实还挺多的, 但考虑前端这个场景其实不太贴, 更多还是从算法本身, 或者通用技术的角度去讲解, 或者是从 LeetCode 这种算法题的角度来讲解, 普遍都是作者理解, 读者不理解, 为了更好的理解递归, 我们先从字面上来看

递归的字面理解

很多计算机术语到了国内翻译过来有的非常传神, 有的就很扯淡, 比如 Socket 套接字. 但是递归不一样, 在我看来递归这个翻译非常的传神, 以汉语的言简意赅很好的解释了 recursion 这个词.

所谓递者, 就是向前传递,

所谓归者, 就是向后回溯,

按照这个理解递归的图形化差不多是这样

而递归之所以难以理解, 其实就在于这个 逻辑断面

递归中的逻辑断面

递归是一种特殊的算法, 在前端或者说 JavaScript 的领域通常我们说的递归其实是不完整的, 你使用一个 function 实现一个递归函数, 其实依赖的是 JavaScript 运行环境给你提供的 函数栈

也就意味着, 递归算法依赖一种特殊的结构

而我们知道 的特征是 先进后出

如果把上面的图用栈来表示, 大概是这样的

怎么解释这个图呢, 我们配上一段代码可能更容易理解, 让我们去深度遍历一个对象, 然后把子对象 proxy 化

const nestObject = {
	a:1,
    b:{
    	a:1,
        b:{
        	a:1
        }
    },
    c:2
}

function deepProxy(target){
	// 递代码
	let proxyTarget = new Proxy(target, proxyHandle)
	for(let key in target){
    	if(target.hasOwnProperty(key)){
        	proxyTarget[key] = deepProxy(proxyTarget[key]) // 调用自身就是逻辑断面
        }
    }
    // 归代码
    retrun proxyTarget
}

正是由于调用自身产生了逻辑断面, 通常我们对函数的线性执行理解被打破了, deepProxy() 之后的代码都被延迟执行, 直到所有的 递代码 全部执行完毕, 这个全部指的就是循环的次数, 也就是嵌套对象的层数.

这种现象很好的解释了两个问题

  • 递归循环为什么导致栈溢出
  • 尾递归的优化原理

递归循环导致栈溢出

因为逻辑断面的存在打破了常规函数的执行方式, 导致函数栈无法销毁一个只执行了一半的函数, 尤其是其中声明的变量, 都必须保存在对应的堆中, 那么随着递归循环次数的增多必然会导致栈达到上限, 无法继续保存更多的函数执行环境.

尾递归的优化原理

同理可得, 如果没有逻辑断面, 即递归函数里的代码都只有 递代码 没有 归代码, JavaScript 引擎就可以优化函数栈, 并且很放心的把执行过的递代码占用的内存回收掉, 不需要保存下来, 这样即便循环的次数再多也不会对导致栈溢出.

附上相对完整的 deepProxy 代码

const proxyHandle = {
  get(target, props) {
    console.log(props)
    return Reflect.get(target, props)
  },
  set(target, props, value) {
    console.log(props)
    return Reflect.set(target, props, value)
  }
}

function isObject(target) {
  return typeof target === 'object'
}

function isNotArray(target) {
  return Array.isArray(target) !== true
}

function deepProxy(target) {
  let proxyTarget = new Proxy(target, proxyHandle)
  for (let key in target) {
    if (target.hasOwnProperty(key)) {
      if (isObject(target[key]) && isNotArray(target[key])) {
        proxyTarget[key] = deepProxy(proxyTarget[key])
      }
    }
  }
  return proxyTarget
}

export default deepProxy

后话

递归最大的好处其实就是优雅, 可以非常优雅的去遍历各种数据结构, 无论是线性的还是树或者图, 你都可以通过基本的递归算法来实现遍历算法, 理解和掌握递归有助于你更好的运用这些常用的数据结构, 写出抽象更好的代码.

这种优雅还体现在, 你不需要一个额外的上下文来让递归函数共享, 递归函数本身就可以组合成一个上下文.