阅读视图
JavaScript篇:回调地狱退散!6年老前端教你写出优雅异步代码
eval:JavaScript里的双刃剑,用好了封神,用不好封号!
大家好,我是江城开朗的豌豆,一名拥有6年以上前端开发经验的工程师。我精通HTML、CSS、JavaScript
等基础前端技术,并深入掌握Vue、React、Uniapp、Flutter
等主流框架,能够高效解决各类前端开发问题。在我的技术栈中,除了常见的前端开发技术,我还擅长3D开发,熟练使用Three.js
进行3D图形绘制,并在虚拟现实与数字孪生技术上积累了丰富的经验,特别是在虚幻引擎开发方面,有着深入的理解和实践。
我一直认为技术的不断探索和实践是进步的源泉,近年来,我深入研究大数据算法的应用与发展,尤其在数据可视化和交互体验方面,取得了显著的成果。我也注重与团队的合作,能够有效地推动项目的进展和优化开发流程。现在,我担任全栈工程师,拥有CSDN博客专家认证及阿里云专家博主称号,希望通过分享我的技术心得与经验,帮助更多人提升自己的技术水平,成为更优秀的开发者。
技术qq交流群:906392632
大家好,我是小杨,一个写了6年前端的老司机。今天要聊一个让人又爱又恨的JavaScript特性——eval()
。这玩意儿就像编程界的"瑞士军刀",功能强大但危险系数极高,新手容易滥用,老手又避之不及。到底该不该用?怎么安全地用?今天我就用几个血泪教训带大家彻底搞懂它!
一、eval是什么?代码里的"魔术师"
简单说,eval()
能把字符串当代码执行:
const codeStr = 'console.log("Hello eval!")';
eval(codeStr); // 输出:Hello eval!
我第一次见到这功能时惊为天人:"这不就是动态执行代码的黑科技吗?!" 于是兴冲冲地写了个"万能计算器":
function calculate(expr) {
return eval(expr); // 千万别学我!
}
console.log(calculate('2 + 2 * 3')); // 8
console.log(calculate('Math.pow(2, 10)')); // 1024
结果上线三天就被安全团队约谈了... (后面会讲为什么)
二、为什么说eval是"危险分子"?
1. 安全漏洞:XSS攻击直通车
如果执行用户输入的字符串:
const userInput = "alert('你的cookie是:'+document.cookie)";
eval(userInput); // 完蛋,用户脚本被执行了!
2. 性能杀手:引擎优化全失效
JS引擎原本可以预编译和优化代码,但遇到eval()
就不得不:
- 启动解释器
- 创建新作用域
- 放弃静态分析
3. 调试噩梦
错误堆栈会显示eval at <anonymous>
,根本找不到问题源头!
三、安全使用eval的三大法则
虽然危险,但在某些场景下不得不用(比如解析JSON的老浏览器环境)。这时要遵守:
法则1:永远不要直接执行用户输入
// 错误示范
eval(req.body.userCode);
// 正确做法
function safeEval(code) {
if (/alert|document|window/.test(code)) {
throw new Error('危险代码!');
}
return eval(code);
}
法则2:用Function构造器替代
const calculator = new Function('expr', 'return (' + expr + ')');
console.log(calculator('2 + 2')); // 4
优点:
- 只在全局作用域执行
- 稍微安全一丢丢
法则3:严格模式限制
"use strict";
eval = 1; // 报错!严格模式下不能覆盖eval
四、真实案例:我踩过的三个坑
案例1:动态生成函数
// 需求:根据API返回的函数名执行对应方法
const funcName = apiResponse.method; // 比如"showDialog"
// 菜鸟时期的我:
eval(funcName + '()'); // 可能执行任意代码!
// 现在的我:
const allowedMethods = { showDialog: true };
if (allowedMethods[funcName]) {
window[funcName]?.();
}
案例2:JSON解析(上古时期)
// 2008年的老代码(那时候没有JSON.parse):
const data = eval('(' + jsonStr + ')');
// 2023年的正确姿势:
const data = JSON.parse(jsonStr);
案例3:沙箱环境
// 用Proxy做个简单沙箱
function safeEval(code) {
const sandbox = new Proxy({}, {
has: () => true, // 欺骗in操作符
get: (target, key) => {
if (['window','document'].includes(key))
throw new Error(`禁止访问 ${key}`);
return target[key];
}
});
return (new Function('with(this){return ' + code + '}')).call(sandbox);
}
五、现代替代方案
场景 | eval做法 | 更优方案 |
---|---|---|
动态执行代码 | eval(str) | Function构造函数 |
JSON解析 | eval('('+json+')') | JSON.parse |
动态属性访问 | eval('obj.'+key) | obj[key] |
模板引擎 | eval拼接字符串 | 模板字面量${}
|
数学表达式计算 | eval('1+1') | 第三方库(如math.js) |
六、什么时候非用eval不可?
- 开发调试工具(如浏览器控制台本身)
- 编写DSL语言(如某些低代码平台)
- 教学演示(比如教人理解AST解析)
我在写在线代码编辑器时,最终选择了
new Function()
+Web Worker的方案,既安全又不会阻塞主线程。
总结:eval如老虎,摸前要三思
- ✅ 能用别的方案就别用eval
- ✅ 必须用时严格过滤输入
- ✅ 优先考虑Function构造函数
你们在项目里用过eval吗?有没有因此翻过车?欢迎在评论区分享你的"惊魂时刻"~
我是小杨,下期可能会讲《如何安全地动态执行代码》,感兴趣的话点个关注不迷路! 🔐
JavaScript篇:前端定时器黑科技:不用setInterval照样玩转循环任务
大家好,我是江城开朗的豌豆,一名拥有6年以上前端开发经验的工程师。我精通HTML、CSS、JavaScript
等基础前端技术,并深入掌握Vue、React、Uniapp、Flutter
等主流框架,能够高效解决各类前端开发问题。在我的技术栈中,除了常见的前端开发技术,我还擅长3D开发,熟练使用Three.js
进行3D图形绘制,并在虚拟现实与数字孪生技术上积累了丰富的经验,特别是在虚幻引擎开发方面,有着深入的理解和实践。
我一直认为技术的不断探索和实践是进步的源泉,近年来,我深入研究大数据算法的应用与发展,尤其在数据可视化和交互体验方面,取得了显著的成果。我也注重与团队的合作,能够有效地推动项目的进展和优化开发流程。现在,我担任全栈工程师,拥有CSDN博客专家认证及阿里云专家博主称号,希望通过分享我的技术心得与经验,帮助更多人提升自己的技术水平,成为更优秀的开发者。
技术qq交流群:906392632
大家好,我是小杨,一个干了快6年的前端老司机。今天要和大家分享一个特别实用的定时器技巧——用setTimeout实现setInterval。这个方案不仅能解决setInterval的一些痛点,还能让我们的定时任务更加可控。
一、为什么不用setInterval?
先说说我为什么研究这个方案。去年在做一个大屏数据实时刷新功能时,发现直接用setInterval会有个恶心的问题:
setInterval(() => {
// 模拟网络请求
console.log('执行任务', new Date().getSeconds());
}, 1000);
看起来每1秒执行一次对吧?但如果网络卡顿导致函数执行超过1秒呢?这时候就会发现多个任务挤在一起执行,就像早高峰的地铁一样让人崩溃。
二、setTimeout的救场方案
后来我改用setTimeout递归调用的方式,完美解决了这个问题:
function 我的循环任务() {
console.log('执行任务', new Date().getSeconds());
// 在函数末尾重新调用自己
setTimeout(我的循环任务, 1000);
}
// 启动任务
setTimeout(我的循环任务, 1000);
这个方案的精妙之处在于:每次都是等上次任务完全执行完,才重新计时。就像排队上厕所,必须等前一个人出来,下个人才能进去。
三、升级版:可控定时器
后来我又做了个加强版,加上了启动/停止功能:
let timer = null;
let count = 0;
function 我的可中断任务() {
console.log(`执行第${++count}次`, new Date().getSeconds());
if(count < 5) { // 只执行5次
timer = setTimeout(我的可中断任务, 1000);
}
}
// 启动
timer = setTimeout(我的可中断任务, 1000);
// 随时可以停止
// clearTimeout(timer);
这样写有三个好处:
- 避免任务堆积
- 可以精确控制执行次数
- 随时能终止任务
四、实战中的应用场景
这个技巧在我工作中帮了大忙,比如:
- 轮询接口:检查订单状态,直到支付成功
- 动画序列:实现复杂的多段动画效果
- 倒计时:更精准的秒表功能
// 倒计时示例
function 倒计时(剩余秒数) {
console.log(`剩余:${剩余秒数}秒`);
if(剩余秒数 > 0) {
setTimeout(() => 倒计时(剩余秒数 - 1), 1000);
}
}
倒计时(10); // 开始10秒倒计时
五、注意事项
虽然这个方案很香,但也要注意:
- 记得保存timer变量,否则没法清除
- 递归调用要注意停止条件,避免内存泄漏
- 长时间运行的任务可能会造成调用栈过深
六、总结
setTimeout实现setInterval的方案,就像是用乐高积木拼出了现成玩具的功能,虽然多写几行代码,但获得了更大的灵活性和可控性。特别适合需要精确控制执行时机的场景。
大家如果有更好的实现方案,欢迎在评论区交流~如果觉得有用,别忘了点赞收藏!
JavaScript篇:自定义事件:让你的代码学会'打小报告'
大家好,我是江城开朗的豌豆,一名拥有6年以上前端开发经验的工程师。我精通HTML、CSS、JavaScript
等基础前端技术,并深入掌握Vue、React、Uniapp、Flutter
等主流框架,能够高效解决各类前端开发问题。在我的技术栈中,除了常见的前端开发技术,我还擅长3D开发,熟练使用Three.js
进行3D图形绘制,并在虚拟现实与数字孪生技术上积累了丰富的经验,特别是在虚幻引擎开发方面,有着深入的理解和实践。
我一直认为技术的不断探索和实践是进步的源泉,近年来,我深入研究大数据算法的应用与发展,尤其在数据可视化和交互体验方面,取得了显著的成果。我也注重与团队的合作,能够有效地推动项目的进展和优化开发流程。现在,我担任全栈工程师,拥有CSDN博客专家认证及阿里云专家博主称号,希望通过分享我的技术心得与经验,帮助更多人提升自己的技术水平,成为更优秀的开发者。
技术qq交流群:906392632
大家好,我是小杨,一个在前端界摸爬滚打6年的老油条。今天我要和大家聊聊怎么让代码组件之间"说悄悄话"——没错,就是自定义事件!
一、为什么需要自定义事件?
想象一下这个场景:
// 传统写法:直接调用
function updateUser() {
updateProfile();
updateAvatar();
updateSettings();
// 我加了新功能还得回来改这里!
}
// 事件驱动写法
document.dispatchEvent(new CustomEvent('userUpdated'));
小杨解说:自定义事件就像办公室里的广播系统,谁想听就自己接,不用挨个通知!
二、基础用法:创建和监听
1. 创建自定义事件
// 简单版
const event = new Event('myEvent');
// 高级版(可以带数据)
const event = new CustomEvent('myEvent', {
detail: {
name: '我',
age: 18
}
});
2. 监听事件
document.addEventListener('myEvent', function(e) {
console.log(`收到事件!数据:${e.detail.name} ${e.detail.age}`);
});
三、实战案例:购物车系统
// 商品组件
class Product {
addToCart() {
document.dispatchEvent(new CustomEvent('cartAdd', {
detail: { id: 123, name: '前端秘籍' }
}));
}
}
// 购物车组件
document.addEventListener('cartAdd', function(e) {
console.log(`把 ${e.detail.name} 加入购物车`);
});
// 用户组件
document.addEventListener('cartAdd', function() {
console.log('更新用户购物车数量');
});
小杨踩坑记:曾经没加detail导致数据传丢,debug到怀疑人生!
四、高级技巧
1. 事件命名空间
// 避免冲突
document.dispatchEvent(new CustomEvent('me:cartAdd'));
2. 事件冒泡控制
const event = new CustomEvent('bubbleEvent', {
bubbles: true, // 允许冒泡
cancelable: true // 允许取消
});
3. 移除监听
function handleEvent() {
console.log('我只执行一次!');
document.removeEventListener('oneTimeEvent', handleEvent);
}
document.addEventListener('oneTimeEvent', handleEvent);
五、Vue/React中的自定义事件
1. Vue版
// 子组件
this.$emit('me-event', { data: 123 });
// 父组件
<Child @me-event="handleEvent" />
2. React版
// 父组件
<Child onMeEvent={handleEvent} />
// 子组件
props.onMeEvent({ data: 123 });
六、性能优化
- 避免滥用:太多事件会让代码变成"广播体操"
- 及时销毁:SPA记得在组件卸载时移除监听
- 事件池:高频事件考虑复用事件对象
七、与原生事件的区别
特性 | 自定义事件 | 原生事件 |
---|---|---|
触发方式 | 手动dispatch | 浏览器自动触发 |
事件类型 | 任意自定义名称 | click/keydown等 |
数据传递 | 通过detail | 有限的事件对象 |
八、总结
- 自定义事件是解耦神器
- 适合组件通信、插件开发等场景
- 记得给事件起个清晰的名字
- 移除不需要的监听防止内存泄漏
思考题:
const event = new CustomEvent('meetup', {
detail: { time: new Date() }
});
document.addEventListener('meetup', function(e) {
console.log(e.detail.time.toLocaleString());
});
setTimeout(() => {
document.dispatchEvent(event);
}, 1000);
// 1秒后事件触发时,输出的时间是创建时还是触发时的时间?
欢迎在评论区讨论你的答案!下期我会分享更多前端设计模式的实战技巧。
JavaScript篇:"闭包:天使还是魔鬼?6年老司机带你玩转JS闭包"
大家好,我是江城开朗的豌豆,一名拥有6年以上前端开发经验的工程师。我精通HTML、CSS、JavaScript
等基础前端技术,并深入掌握Vue、React、Uniapp、Flutter
等主流框架,能够高效解决各类前端开发问题。在我的技术栈中,除了常见的前端开发技术,我还擅长3D开发,熟练使用Three.js
进行3D图形绘制,并在虚拟现实与数字孪生技术上积累了丰富的经验,特别是在虚幻引擎开发方面,有着深入的理解和实践。
我一直认为技术的不断探索和实践是进步的源泉,近年来,我深入研究大数据算法的应用与发展,尤其在数据可视化和交互体验方面,取得了显著的成果。我也注重与团队的合作,能够有效地推动项目的进展和优化开发流程。现在,我担任全栈工程师,拥有CSDN博客专家认证及阿里云专家博主称号,希望通过分享我的技术心得与经验,帮助更多人提升自己的技术水平,成为更优秀的开发者。
技术qq交流群:906392632
大家好,我是小杨,一个被JS闭包折磨了6年又爱又恨的前端工程师。今天我要带大家深入理解闭包这个让人又爱又恨的特性,分享那些年我踩过的坑和总结的优化技巧。
一、闭包是什么?一个简单的例子
function outer() {
let me = '小杨';
return function inner() {
console.log(`大家好,我是${me}`);
};
}
const sayHello = outer();
sayHello(); // "大家好,我是小杨"
看到没?inner
函数记住了outer
函数的me
变量,这就是闭包!
二、闭包的三大妙用(天使面)
1. 创建私有变量
function createCounter() {
let count = 0;
return {
increment() { count++ },
getCount() { return count }
};
}
const counter = createCounter();
counter.increment();
console.log(counter.getCount()); // 1
console.log(counter.count); // undefined
2. 实现函数柯里化
function multiply(a) {
return function(b) {
return a * b;
};
}
const double = multiply(2);
console.log(double(5)); // 10
3. 事件处理中的妙用
function setupButtons() {
for(var i = 1; i <= 3; i++) {
(function(index) {
document.getElementById(`btn-${index}`)
.addEventListener('click', function() {
console.log(`我是按钮${index}`);
});
})(i);
}
}
三、闭包的三大坑(魔鬼面)
1. 内存泄漏
function leakMemory() {
const bigData = new Array(1000000).fill('*');
return function() {
console.log('我还记得bigData');
};
}
const leaked = leakMemory();
// bigData本应该被回收,但闭包让它一直存在
2. 性能问题
function slowPerformance() {
const data = {}; // 大对象
return function(key, value) {
data[key] = value;
// 每次调用都要访问闭包变量
};
}
3. 意外的变量共享
function createFunctions() {
let funcs = [];
for(var i = 0; i < 3; i++) {
funcs.push(function() {
console.log(i); // 全是3!
});
}
return funcs;
}
四、闭包优化六大法则(6年经验总结)
1. 及时释放引用
function createHeavyObject() {
const heavy = new Array(1000000).fill('*');
return {
useHeavy: function() {
// 使用heavy
},
cleanup: function() {
heavy = null; // 手动释放
}
};
}
2. 使用块级作用域
// 修复前面的共享变量问题
function createFixedFunctions() {
let funcs = [];
for(let i = 0; i < 3; i++) { // 使用let
funcs.push(function() {
console.log(i); // 0,1,2
});
}
return funcs;
}
3. 避免不必要的闭包
// 不好的写法
function unneededClosure() {
const data = {};
return function() {
// 根本不使用data,却形成了闭包
console.log('Hello');
};
}
// 好的写法
function noClosure() {
console.log('Hello');
}
4. 使用WeakMap管理私有变量
const privateData = new WeakMap();
class MyClass {
constructor() {
privateData.set(this, {
secret: '我是私有数据'
});
}
getSecret() {
return privateData.get(this).secret;
}
}
5. 合理使用IIFE
// 立即执行函数减少闭包生命周期
(function() {
const tempData = processData();
// 使用tempData
})(); // 执行完立即释放
6. 使用模块化
// 模块化天然适合管理闭包
const counterModule = (function() {
let count = 0;
return {
increment() { count++ },
getCount() { return count }
};
})();
五、真实案例分享
案例1:我曾经在项目中遇到一个页面卡顿问题,最后发现是因为一个事件处理函数形成了闭包,持有了一个大DOM树的引用。解决方案是:
// 修复前
function setup() {
const bigElement = document.getElementById('big');
button.addEventListener('click', function() {
// 持有bigElement引用
console.log(bigElement.id);
});
}
// 修复后
function setup() {
const id = 'big';
button.addEventListener('click', function() {
// 只存储需要的id
console.log(id);
});
}
六、总结
闭包就像一把双刃剑:
✅ 优点:实现私有变量、函数柯里化、模块化等
❌ 缺点:可能导致内存泄漏、性能问题
记住我的6年经验总结:
- 及时释放不再需要的引用
- 优先使用块级作用域
- 避免不必要的闭包
- 合理使用WeakMap和模块化
- 善用开发者工具检查内存
最后留个思考题:
function createFunctions() {
let funcs = [];
for(var i = 0; i < 3; i++) {
funcs.push(function(j) {
return function() {
console.log(j);
};
}(i));
}
return funcs;
}
const funcs = createFunctions();
funcs[0](); // 输出什么?为什么?
欢迎在评论区讨论你的答案!下期我会分享更多JS高级技巧。
JavaScript篇:解密JS执行上下文:代码到底是怎么被执行的?
大家好,我是江城开朗的豌豆,一名拥有6年以上前端开发经验的工程师。我精通HTML、CSS、JavaScript
等基础前端技术,并深入掌握Vue、React、Uniapp、Flutter
等主流框架,能够高效解决各类前端开发问题。在我的技术栈中,除了常见的前端开发技术,我还擅长3D开发,熟练使用Three.js
进行3D图形绘制,并在虚拟现实与数字孪生技术上积累了丰富的经验,特别是在虚幻引擎开发方面,有着深入的理解和实践。
我一直认为技术的不断探索和实践是进步的源泉,近年来,我深入研究大数据算法的应用与发展,尤其在数据可视化和交互体验方面,取得了显著的成果。我也注重与团队的合作,能够有效地推动项目的进展和优化开发流程。现在,我担任全栈工程师,拥有CSDN博客专家认证及阿里云专家博主称号,希望通过分享我的技术心得与经验,帮助更多人提升自己的技术水平,成为更优秀的开发者。
技术qq交流群:906392632
大家好,我是小杨,一个和JS相爱相杀6年的前端工程师。今天我要带大家揭开JavaScript代码执行的神秘面纱,保证让你看完后恍然大悟:"原来我的代码是这样跑的!"
一、执行上下文:代码的"舞台"
想象一下,JS引擎就像个剧场,每次函数调用就像一场新的演出。而执行上下文就是这个演出的"舞台",决定了哪些"演员"(变量和函数)可以上场。
先看个简单例子:
function sayHello() {
let me = '小杨';
console.log(`大家好,我是${me}`);
}
sayHello();
当调用sayHello()
时,JS就会创建一个新的执行上下文。
二、执行上下文的"人生三阶段"
每个执行上下文都会经历三个阶段:
-
创建阶段:准备舞台
- 创建变量对象(VO)
- 建立作用域链
- 确定this指向
-
执行阶段:正式演出
- 变量赋值
- 函数调用
- 执行代码
-
销毁阶段:演出结束
- 出栈等待垃圾回收
三、变量提升的真相
来看个经典例子:
console.log(me); // undefined
var me = '小杨';
console.log(me); // '小杨'
为什么不会报错?因为在创建阶段,变量声明会被提升,但赋值不会。
小杨踩坑记:
function test() {
console.log(me); // undefined
if(false) {
var me = '小杨';
}
}
test();
即使if条件为false,变量声明依然会被提升!
四、作用域链:变量的"寻亲记"
let name = '全局小杨';
function outer() {
let name = '外层小杨';
function inner() {
console.log(name); // '外层小杨'
}
inner();
}
outer();
JS会沿着作用域链一层层往上找变量,就像寻亲一样。
五、this指向:最难捉摸的"演员"
this的指向总让人头大,记住几个规则:
- 普通函数调用:this指向window(严格模式undefined)
- 方法调用:this指向调用对象
- new调用:this指向新创建的对象
let obj = {
me: '小杨',
say: function() {
console.log(this.me);
}
};
obj.say(); // '小杨'
let fn = obj.say;
fn(); // undefined (this指向window)
六、闭包:执行上下文的"遗产"
function createCounter() {
let count = 0;
return function() {
count++;
console.log(count);
};
}
let counter = createCounter();
counter(); // 1
counter(); // 2
即使createCounter的执行上下文已经销毁,内部函数依然能访问count变量,这就是闭包的魔力。
七、实战应用:避免常见坑
- 避免变量污染:
// 错误做法
for(var i=0; i<5; i++) {
setTimeout(() => {
console.log(i); // 全是5
}, 100);
}
// 正确做法
for(let i=0; i<5; i++) {
setTimeout(() => {
console.log(i); // 0,1,2,3,4
}, 100);
}
- 合理使用闭包:
// 缓存计算结果
function createCache() {
let cache = {};
return function(key, value) {
if(value !== undefined) {
cache[key] = value;
}
return cache[key];
}
}
八、总结
- 执行上下文是JS代码执行的环境
- 变量提升和作用域链是理解JS的关键
- this指向需要根据调用方式判断
- 闭包可以让函数"继承"执行上下文的变量
最后留个思考题:
let obj = {
me: '小杨',
say: () => {
console.log(this.me);
}
};
obj.say(); // 输出什么?为什么?
欢迎在评论区讨论你的答案!下期我会详细讲解箭头函数的this指向问题。
JavaScript篇:如何实现add(1)(2)(3)()=6?揭秘链式调用的终极奥义!
大家好,我是江城开朗的豌豆,一名拥有6年以上前端开发经验的工程师。我精通HTML、CSS、JavaScript
等基础前端技术,并深入掌握Vue、React、Uniapp、Flutter
等主流框架,能够高效解决各类前端开发问题。在我的技术栈中,除了常见的前端开发技术,我还擅长3D开发,熟练使用Three.js
进行3D图形绘制,并在虚拟现实与数字孪生技术上积累了丰富的经验,特别是在虚幻引擎开发方面,有着深入的理解和实践。
我一直认为技术的不断探索和实践是进步的源泉,近年来,我深入研究大数据算法的应用与发展,尤其在数据可视化和交互体验方面,取得了显著的成果。我也注重与团队的合作,能够有效地推动项目的进展和优化开发流程。现在,我担任全栈工程师,拥有CSDN博客专家认证及阿里云专家博主称号,希望通过分享我的技术心得与经验,帮助更多人提升自己的技术水平,成为更优秀的开发者。
技术qq交流群:906392632
大家好,我是小杨,一个沉迷于JavaScript各种骚操作的前端老司机。今天咱们来玩点有意思的——如何实现一个可以无限链式调用的add函数,最终在空调用时返回累加结果?听起来是不是很酷?让我们一步步揭开这个技巧的神秘面纱!
一、先看看我们要实现的效果
add(1)(2)(3)(); // 期望输出 6
add(1,2,3)(4)(); // 期望输出 10
add(1)(2,3)(4,5)(); // 期望输出 15
这种函数调用方式在函数式编程中被称为柯里化(Currying) ,但比普通柯里化更灵活,因为它支持:
- 单参数或多参数调用
- 无限链式调用
- 空调用时返回计算结果
二、基础版:单参数链式调用
我们先从简单的单参数版本来理解核心思路:
function add(num) {
let sum = num;
const innerAdd = (nextNum) => {
if (nextNum === undefined) {
return sum;
}
sum += nextNum;
return innerAdd;
};
return innerAdd;
}
console.log(add(1)(2)(3)()); // 输出 6
关键点解析:
-
add
函数初始化累加值 -
返回的
innerAdd
函数可以:- 接收新数字并累加,然后返回自身(继续链式调用)
- 无参数调用时返回累加结果
-
通过闭包保持对
sum
的引用
三、升级版:支持多参数调用
现在我们来增强功能,支持每次调用传入多个参数:
function add(...args) {
let sum = args.reduce((acc, val) => acc + val, 0);
const innerAdd = (...nextArgs) => {
if (nextArgs.length === 0) {
return sum;
}
sum += nextArgs.reduce((acc, val) => acc + val, 0);
return innerAdd;
};
return innerAdd;
}
console.log(add(1,2,3)(4)()); // 输出 10
console.log(add(1)(2,3)(4,5)()); // 输出 15
改进点:
- 使用剩余参数
...args
接收任意数量参数 - 用
reduce
计算参数总和 - 同样通过闭包保持
sum
的状态
四、终极版:支持直接取值和链式调用
有时候我们可能想直接获取当前值而不需要空调用:
function add(...args) {
let sum = args.reduce((acc, val) => acc + val, 0);
const innerAdd = (...nextArgs) => {
sum += nextArgs.reduce((acc, val) => acc + val, 0);
return innerAdd;
};
// 添加valueOf方法,可以在需要原始值时自动调用
innerAdd.valueOf = () => sum;
// 添加toString方法,方便输出查看
innerAdd.toString = () => sum.toString();
return innerAdd;
}
// 使用方式1:传统空调用
console.log(add(1)(2)(3)()); // 输出 6
// 使用方式2:直接参与运算(自动调用valueOf)
const result = add(1)(2)(3) + 4; // 6 + 4 = 10
console.log(result); // 输出 10
// 使用方式3:直接输出(自动调用toString)
console.log(add(1)(2)(3)); // 输出 6
高级技巧:
- 实现
valueOf
方法让对象在需要原始值时自动转换 - 实现
toString
方法让对象在被当作字符串时友好显示 - 这样函数既可以被链式调用,也可以直接参与运算
五、原理深度剖析
这个实现的魔法主要依赖于几个JavaScript特性:
- 闭包(Closure) :内部函数保持对外部变量的引用
- 高阶函数(Higher-order Function) :函数返回函数
- 剩余参数(Rest Parameters) :处理不定数量参数
-
对象原始值转换:通过
valueOf
和toString
控制对象到原始值的转换
六、实际应用场景
虽然这种写法看起来很炫酷,但在实际项目中要谨慎使用。适合的场景包括:
- 构建数学计算库的流畅接口
- 创建DSL(领域特定语言)
- 函数式编程工具函数
- 面试时展示JS功底(笑)
七、扩展思考:如何实现减法?
基于同样思路,我们可以扩展出支持加减乘除的链式计算器:
function calc(initial = 0) {
let result = initial;
const methods = {
add: (...args) => {
result += args.reduce((a, b) => a + b, 0);
return methods;
},
subtract: (...args) => {
result -= args.reduce((a, b) => a + b, 0);
return methods;
},
valueOf: () => result
};
return methods;
}
const total = calc()
.add(1,2).subtract(3)
.add(4).add(5,6) + 7;
console.log(total); // 输出 16
八、总结与最佳实践
-
核心模式:函数返回函数 + 闭包保存状态
-
参数处理:使用剩余参数处理多参数情况
-
终止条件:空调用或隐式转换触发结果返回
-
注意事项:
- 这种模式可能降低代码可读性
- 在团队项目中要确保大家都理解这种写法
- 考虑使用TypeScript添加类型提示
记住,强大的JavaScript特性是一把双刃剑,用得好能让代码更优雅,用不好会让同事抓狂。关键是找到平衡点!