让代码少点嵌套
这期聊一下如何减少代码中的嵌套。
提前返回
一个常见的业务场景,获取用户的折扣:如果是普通用户则打几折,如果是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状态码、订单等业务的状态码、用户权限管理等。