JS 代码技巧 vol.1 --- 10 个让代码少写 30% 的小套路
小不的代码技巧系列第 1 期 主题:判断 · 类型 · 设计模式 主打一个"踩坑 → 解法 → 坑在哪"三段式,看完就能直接抄作业 😎
哈喽哇!我是小不 ,不简说的不~
作为一个在代码界"翻车"无数次的选手,我算是看明白了:坑这东西吧,要么不踩,踩就踩大的😂
今天这期,主打一个 判断 · 类型 · 设计模式------不整虚的,全是实战里哭出来的教训。看不看随你~反正翻车实录又不收钱
Ps:文末有惊喜(不是广告!)
1. 可选链 ?. ------ 干掉 a && a.b && a.b.c
以前写这种代码,血压容易上来:
js
// 我以前的写法(血压警告)
if (user && user.profile && user.profile.address && user.profile.address.city) {
console.log(user.profile.address.city);
}
现在一行搞定:
js
console.log(user?.profile?.address?.city);
坑在哪 :可选链遇到 null/undefined 直接 undefined,但遇到 0、''、false 不会短路------别把它和 && 混用。
2. ?? vs || ------ 0 和 '' 不该被当 falsy
经典踩坑现场:
js
function setVolume(vol) {
return vol || 50; // 用户传 0 进去,默认 50 给你冒出来
}
正解:
js
function setVolume(vol) {
return vol ?? 50; // 只有 null / undefined 才用默认
}
坑在哪 :?? 不能和 || && 混用(语法直接报错),要加括号隔开。
3. 逻辑赋值三件套:??= / ||= / &&=
赋值 + 逻辑判断,一行搞定。
js
let count = 0;
count ||= 10; // count 是 0,被 || 判 falsy → 变成 10
count ??= 10; // count 是 0(不是 null/undefined)→ 不变,还是 0
let user = { name: "小不" };
user.name &&= "[已认证] " + user.name; // name 是 truthy → 加上前缀
坑在哪 :搞不清 || 和 ?? 的区别,三件套一用就翻车。0、空字符串、false 都是 ||= 的受害者。
4. 终极类型判断:Object.prototype.toString.call()
typeof [] 是 'object',typeof null 也是 'object',血压 +1。
js
function getType(val) {
return Object.prototype.toString.call(val).slice(8, -1);
// 返回: 'Array' / 'Date' / 'RegExp' / 'Map' / 'Set' / 'Promise' ...
}
typeof 该用就用(判断基础类型),复杂对象就上 toString,别在 instanceof 上死磕。
坑在哪 :instanceof 在 iframe / Web Worker 里因为原型链不同会误判,跨窗口场景老老实实用 Array.isArray()。
5. Array.isArray() ------ 比 instanceof Array 靠谱
js
Array.isArray([]); // true
Array.isArray({ length: 1 }); // false,类数组也能识别
为啥不用 instanceof Array?iframe 里 A 页面的 Array 跟 B 页面的 Array 不是同一个构造函数,[] instanceof Array 会是 false,经典玄学。
6. 单例模式:ES Module 本身就是
别再写这种了:
js
class Config {
constructor() {
/* ... */
}
}
Config.instance = null;
Config.getInstance = () => {
if (!Config.instance) Config.instance = new Config();
return Config.instance;
};
直接:
js
// config.js
export default {
apiBase: "https://api.example.com",
// ...
};
ES Module 天然单例,import 多少次都是同一份。需要懒加载再考虑 class。
坑在哪 :CommonJS (Node 老代码) 里 require() 会缓存,但缓存的是 module.exports 的引用,导出对象时小心共享状态。
7. 策略模式:消灭 if/else 嵌套
js
// 烂写法(嵌套地狱)
function calc(type, a, b) {
if (type === "add") return a + b;
else if (type === "sub") return a - b;
else if (type === "mul") return a * b;
else if (type === "div") return a / b;
else throw new Error("未知运算");
}
正解------查表:
js
const ops = {
add: (a, b) => a + b,
sub: (a, b) => a - b,
mul: (a, b) => a * b,
div: (a, b) => a / b,
};
function calc(type, a, b) {
const fn = ops[type];
if (!fn) throw new Error("未知运算");
return fn(a, b);
}
加新运算?往对象里加一行,不用动 calc。
坑在哪 :表里的 key 最好加类型约束(TS 派),不然 calc('addd', 1, 2) 拼错字符串要等到运行时才爆。
8. 观察者模式:30 行 EventEmitter
发布订阅是面试常客,也是真有用。
js
class Emitter {
constructor() {
this.listeners = new Map();
}
on(event, fn) {
if (!this.listeners.has(event)) this.listeners.set(event, []);
this.listeners.get(event).push(fn);
}
emit(event, ...args) {
(this.listeners.get(event) || []).forEach((fn) => fn(...args));
}
off(event, fn) {
const arr = this.listeners.get(event);
if (arr)
this.listeners.set(
event,
arr.filter((f) => f !== fn),
);
}
}
用起来:
js
const bus = new Emitter();
bus.on("login", (user) => console.log(user.name, "登录了"));
bus.emit("login", { name: "小不" }); // 小不 登录了
坑在哪 :忘记 off 是内存泄漏的元凶。Vue/React 组件销毁时记得解绑,或者直接用 mitt / tiny-emitter 这类库省心。
9. Proxy 模式:比 Object.freeze 灵活的"只读视图"
Object.freeze 是死板的,要么全冻要么全不冻。Proxy 可以做更细的拦截:
js
function readonly(obj) {
return new Proxy(obj, {
set(target, key, value) {
throw new Error(`属性 ${key} 不可写`);
},
deleteProperty(target, key) {
throw new Error(`属性 ${key} 不可删`);
},
});
}
const config = readonly({ api: "https://..." });
config.api = "x"; // Error: 属性 api 不可写
坑在哪 :Proxy 不会真正冻结原对象,readonly(config).api = 'x' 报错,但 config.api = 'x' 还是能改。深只读要递归包一层。
10. 装饰器模式:函数版 AOP
想在不修改原函数的前提下,加日志 / 计时 / 鉴权?
js
function withLog(fn) {
return function (...args) {
console.log("调用:", fn.name, args);
const result = fn(...args);
console.log("返回:", result);
return result;
};
}
const add = withLog((a, b) => a + b);
add(1, 2); // 调用: add [1, 2] 返回: 3
ES 装饰器提案快落地了,TS 早就能用,但函数包装写法兼容性更好,老项目也能上。
坑在哪 :装饰器会把原函数 name 改了(withLog((a, b) => ...) 拿不到原名),调试时栈追踪会丢名字,记得手动设 .name。
📦 收个尾
这 10 个技巧浓缩一下:
- 最高频踩坑 :
?.和&&混用、??误用||、structuredClone浅拷贝、catch 吞错 - 最值得收藏 :策略模式查表演示、
Object.prototype.toString.call()终极大法 - 核心思路:能让语言帮你干的,别自己手写
学到了就是赚到了,犹豫徘徊等于白来~
写到最后
想要啥技巧?评论区甩个题目过来~
- 你刚踩的坑
- 项目里反复写的代码
- 想搞清楚但一直懒得查的 API
小不看到...不一定回 😂 毕竟代码里翻车太多,腾不出手~
Ps:三连随缘,催更的会被打 😂