让代码少点嵌套

这期聊一下如何减少代码中的嵌套。

提前返回

一个常见的业务场景,获取用户的折扣:如果是普通用户则打几折,如果是vip用户打几折。
起初代码很简洁。

function getUserDiscount(user) {
  if (user?.isVip) return 0.88;
  return 1.00;
}

但业务总是捉摸不定,老板为了挣很多钱总是想搞vvip和vvvip。

为了鼓励用户成为vip还可以不拿老用户当人,新用户再额外优惠。

经过数次老板的一拍脑壳,产品经理的灵机一动,代码逐渐变成了这样。

function getUserDiscount(user) {
  if (user) {
    if (user.isNewUser) {
      if (user.vipLevel === 'vvvip') return 0.65;
      else if (user.vipLevel === 'vvip') return 0.72;
      else if (user.isVip) return 0.75;
      else return 0.79;
    } else {
      if (user.lastOrderDays > 90) {  // 回归老用户
        if (user.vipLevel === 'vvvip') return 0.70;
        else if (user.vipLevel === 'vvip') return 0.78;
        else if (user.isVip) return 0.82;
        else return 0.90;
      } else {
        if (user.vipLevel === 'vvvip') return 0.68;
        else if (user.vipLevel === 'vvip') return 0.75;
        else if (user.isVip) return 0.88;
      }
    }
  }
  return 1.00;
}

按照这个节奏下去,这个函数用不了多久,将变成克苏鲁的模样,没人可以理解,窥视使人疯狂。

function getUserDiscount(user) {
  if (user) {                                    // user存在
    if (!user.isBanned && user.accountStatus !== 'frozen') {  //账号正常没冻结
      if (user.isNewUser) {                      // 新用户优惠
        if (!user.hasUsedFirstCoupon && user.registerDays <= 7) {  //有首单券
          if (user.referralCount >= 5) {         // 有邀请码
            if (isHoliday('lunarNewYear')) {     // 节日特惠
                if (user.vipLevel === 'vvvip') return 0.55;
                else if (user.vipLevel === 'vvip') return 0.62;
                else if (user.isVip) return 0.65;
                else return 0.58; 
              } else {
                // 支付宝优惠
                switch (user.paymentMethod) {
                  case 'alipay':
                    if (user.vipLevel === 'vvvip') return 0.58;
                    // ... 嵌套继续
                    break;
                  // 20行case...

优化方法也很简单,提前退出即可。

多写几个return,而不是层层If else。

function getUserDiscount(user) {
  if (!user) return 1.00;
  if (user.isBanned || user.accountStatus === 'frozen') return 1.00;
  const vipDiscounts = { vvvip: 0.68, vvip: 0.75, vip: 0.88 };
  const vipDisc = vipDiscounts[user.vipLevel];
  if (vipDisc) {
    if (user.isNewUser) return vipDisc * 0.95;  // 新VIP额外
    if (isTyphoonSignal8() && user.location === 'TseungKwanO') return vipDisc * 0.90;  // HK本地
    return vipDisc;
  }
}

这个技巧用来写组件也非常常用。

function UserProfile({ userId }: { userId: string }) {
  const { data, isLoading, error } = useUser(userId);

  return (
    <div className="profile-card">
      {isLoading ? (
        <div className="loading">加载中...</div>
      ) : error ? (
        <div className="error">加载失败:{error.message}</div>
      ) : !data ? (
        <div className="empty">用户不存在</div>
      ) : (
        <>
          <h1>{data.name}</h1>
          <img src={data.avatar} alt={data.name} />
          <p>等级:{data.level}</p>
          <p>加入时间:{formatDate(data.joinAt)}</p>
          {/* ... 更多内容 */}
        </>
      )}
    </div>
  );
}

可以通过提前返回优化成

function UserProfile({ userId }: { userId: string }) {
  const { data, isLoading, error } = useUser(userId);

  if (isLoading) {
    return <div className="loading center">加载中...</div>;
  }

  if (error) {
    return (
      <div className="error">加载失败:{error.message}</div>
    );
  }

  if (!data) {
    return <div className="empty">用户不存在</div>;
  }

  return (
    <>
      <h1>{data.name}</h1>
      <img src={data.avatar} alt={data.name} />
      <p>等级:{data.level}</p>
      <p>加入时间:{formatDate(data.joinAt)}</p>
      {/* ... 更多内容 */}
    </>
  );
}

把大函数拆成很多小函数

这种优化方法类似于提前返回。

还是用上文组件举例。很显然它把loading状态、错误状态、404状态和正常状态的ui混杂在一起了。

写在同一个组件内,我能预见用不了多久,每个状态都会膨胀成各种奇奇怪怪的样子。

比如加载失败可能会长出【重试按钮】,404会长出【n秒后自动返回】等等。

我曾见过5000行代码聚集在同一个.vue文件里【呕吐】。

// 为了我的san值,这里不做代码展示了

为了避免san值归0,我们最好遵守单一原则。换句话讲就是把组件拆的细一点。

function LoadingState() {
  return (
    <div className="loading-center">
      <div className="spinner-large" />
      <p>正在加载用户资料...</p>
    </div>
  );
}

function ErrorState({ message }: { message: string }) {
  return (
    <div className="error-card">
      <h3>加载失败</h3>
      <p className="error-message">{message}</p>
      <div className="error-actions">
        <button onClick={() => window.location.reload()}>刷新页面</button>
        <button onClick={() => history.back()}>返回上一页</button>
      </div>
    </div>
  );
}

function NotFoundState({ userId }: { userId: string }) {
  return (
    <div className="not-found">
      <h2>用户不存在</h2>
      <p>ID: {userId}</p>
      <p>可能已被删除或您输入了错误的ID</p>
      <Link to="/users">查看其他用户</Link>
    </div>
  );
}

function UserProfile({ userId }: { userId: string }) {
  const { data, isLoading, error } = useUser(userId);

  if (isLoading) {
    return <LoadingState />;
  }

  if (error) {
    return <ErrorState message={error.message} />;
  }

  if (!data) {
    return <NotFoundState userId={userId} />;
  }

  // 幸福路径:非常干净
  return (
    <article className="user-profile-page">
      <UserHeader user={data} />
      <UserBasicInfo user={data} />
    </article>
  );
}

表驱动

根据数字返回当前周几要怎么写?

function getWeekdayName(day) {
  if (day === 0) {
    return "星期日";
  } else if (day === 1) {
    return "星期一";
  } else if (day === 2) {
    return "星期二";
  } else if (day === 3) {
    return "星期三";
  } else if (day === 4) {
    return "星期四";
  } else if (day === 5) {
    return "星期五";
  } else if (day === 6) {
    return "星期六";
  } else {
    return "無效的星期值";
  }
}

if else改成switch case也无所谓,这么写的根本问题在于无法扩展。比如做国际化,你得重写一个新函数。

支持十八国语言就得写十八个新函数。你复制粘贴了十八个新函数似乎也没啥工作量,但这玩意没法调用。

如果你的领导也是菜逼,那真是将熊熊一窝,代码只能这么写:

  if (lang === 'zh-cn') return getWeekdayNameZhCn(day);
  if (lang === 'en-us') return getWeekdayNameEnUs(day);
  if (lang === 'en-gb') return getWeekdayNameEnGb(day); 
  if (lang === 'ja')    return getWeekdayNameJa(day);

表驱动就是用索引替代if/else做逻辑判断。我们可以把相关数据整理好,通过索引来控制取哪部分数据。

const WEEKDAYS = {
  'zh-cn': {
    full:  ['星期日', '星期一', '星期二', '星期三', '星期四', '星期五', '星期六'],
    short: ['周日',   '周一',   '周二',   '周三',   '周四',   '周五',   '周六'],
    veryShort: ['日','一','二','三','四','五','六']
  },
  'en-us': {
    full:  ['Sunday', 'Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday'],
    short: ['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat'],
  },
  //...
  'ja': {
    full:  ['日曜日', '月曜日', '火曜日', '水曜日', '木曜日', '金曜日', '土曜日'],
    short: ['日', '月', '火', '水', '木', '金', '土'],
  }
  // 想加多少语言就加多少
};

function getWeekdayName(day, lang = 'zh-cn', format = 'full') {
  if (!Number.isInteger(day) || day < 0 || day > 6) {
    return '无效的星期值';
  }

  const langData = WEEKDAYS[lang] || WEEKDAYS['zh-cn'];
  const names = langData[format] || langData['full'];

  return names[day];
}

这种技巧一般用于分支较多,大于4、5个。而且每个分支的逻辑都高度类似的场景。

比如:日期、HTTP状态码、订单等业务的状态码、用户权限管理等。