尧图网站设计 尧图网站设计YAOTU DESIGN
ARTICLE DETAIL

资讯详情

深耕网站设计与一线实操的经验洞察。

30 seconds of code:JavaScript 函数式编程中的不可变性(Immutability)完整指南

30 seconds of code:JavaScript 函数式编程中的不可变性(Immutability)完整指南 教程文档【免费下载链接】30-seconds-of-codeCoding articles to level up your development skills项目地址https://gitcode.com/gh_mirrors/30/30-seconds-of-code点击查看免费下载在 JavaScript 的函数式编程实践中不可变性Immutability是绕不开的基石性概念它决定了数据一旦创建便不再被原地修改任何变更都通过创建新数据来完成。本文以仓库中的 immutability.md 为核心骨架结合 immutability 集合 中的系列文章与源码级示例为你讲透不可变性的原理、常见误区以及数组、对象、字符串等场景下的落地写法。读完本文你将能够识别代码中的原地修改mutation并用展开运算符、structuredClone()、Object.freeze()、Proxy等工具写出真正符合函数式风格、易于推理和测试的 JavaScript 代码。什么是不可变性可变数据与不可变数据的核心差异在传统编程中数据通常是可变的mutable即创建之后可以随意被操作和修改。来看一个最常见的例子let arr [1, 2, 3]; arr.push(4); // arr 现在是 [1, 2, 3, 4]这里Array.prototype.push()直接原地修改了arr这个数组——它没有产生新数组而是改变了既有数组的内容。而在函数式编程中数据被视为不可变的immutable即一旦创建就不能被改变。与其修改既有数据不如基于旧数据创建新数据。这种做法能够防止难以预料的副作用side effects让程序更容易被推理reason aboutconst arr [1, 2, 3]; const newArr [...arr, 4]; // newArr 是 [1, 2, 3, 4]arr 仍然是 [1, 2, 3]如上例所示原始数组并未被修改而是基于旧数组创建了一个新数组。原始数组依然可用程序其他部分如果需要仍可以继续使用它。这正是不可变性与可变性最直观的区别数据共享安全互不污染。走出误区决定可变性的是操作而非let/const很多初学者误以为用const声明就是不可变用let声明就是可变。事实上并非如此。原文档中的关键提示immutability.md 的 NOTE 块明确指出了这一点在上述两个例子中数组在技术上都是可变的无论它是用let还是const创建的。区别在于第一个例子中数组通过Array.prototype.push()被原地修改mutated第二个例子中数组没有被修改not mutated。原因在于push()是原地修改数组而展开运算符...创建的是新数组。这与仓库中另一篇文章 deep-freeze-object.md 的结论完全一致const只能阻止变量被重新赋值却无法阻止对象或数组的属性被修改const myObj { a: 10, b: 20, c: 30 }; myObj.a 12; // { a: 12, b: 20, c: 30 } —— 仍然可以改 const myArr [15, 25, 35]; myArr[1] 28; // [15, 28, 35] —— 仍然可以改因此判断一段代码是否不可变看的不是声明关键字而是操作方式是否调用了会原地修改数据的 API如push、splice、属性赋值等。不可变性在函数式编程中的位置不可变性并非孤立概念它是函数式编程的核心支柱之一。functional-programming-introduction.md 将函数式编程描述为把计算视为数学函数的求值并避免改变状态和可变数据的编程范式其核心概念包括纯函数Pure functions相同的输入永远返回相同的输出且不产生副作用。比如const concat (arr, val) [...arr, val];是纯函数而const push (arr, val) arr.push(val);不是——后者原地修改了传入的数组。不可变性Immutability值一旦创建就不能被改变这与纯函数紧密相关也是本文的主题。高阶函数Higher-order functions、递归Recursion等。可以看到不可变性与纯函数互为表里纯函数承诺不修改外部状态而不可变数据则从数据结构层面保证了这种承诺可以实现。其直接收益是代码更容易推理、测试和调试因为数据不会在你不注意的地方被偷偷改动。在 JavaScript 中实践不可变性复制而非修改数组的不可变操作对数组而言最常见的不可变写法就是展开运算符spread operator,...——复制出新的数组再追加元素正如本文开头的例子。围绕这一点仓库中的 spread-operator-tricks.md 还总结了更多用法// 复制数组 const arr [1, 2, 3]; const arr2 [...arr]; // [1, 2, 3] // 合并多个数组 const arr1 [1, 2, 3]; const arr2 [4, 5, 6]; const combined [...arr1, ...arr2]; // [1, 2, 3, 4, 5, 6] // 在任意位置插入元素 const arr [1, 2, 3]; const arr2 [0, ...arr, 4]; // [0, 1, 2, 3, 4]需要注意的是展开运算符只是浅拷贝shallow clone对于嵌套的非基本类型值复制后新旧数组仍然共享引用。如果你需要复制包含对象、函数或类实例的数组可以使用全局函数structuredClone()进行深拷贝详见 copy-array.mdconst a [{ foo: bar }, { baz: qux }]; const b structuredClone(a); // a ! b, a[0] ! b[0]对象的不可变操作浅拷贝与深拷贝对象同样适用复制而非修改的原则。仓库中的 shallow-deep-clone-object.md 系统讲解了对象的克隆浅克隆可以通过展开运算符或Object.assign()完成它只克隆最外层对象const shallowClone obj Object.assign({}, obj); let obj { a: 1, b: 2 }; let clone shallowClone(obj); let otherClone shallowClone(obj); clone.b 4; otherClone.b 6; // obj { a: 1, b: 2} // clone { a: 1, b: 4 } // otherClone { a: 1, b: 6 }浅克隆对外层有效但嵌套深层对象仍会按引用传递修改克隆体的嵌套对象会影响原始对象。为此需要深克隆——递归克隆每一个嵌套的对象和数组const deepClone obj { if (obj null) return null; let clone Object.assign({}, obj); Object.keys(clone).forEach( key (clone[key] typeof obj[key] object ? deepClone(obj[key]) : obj[key]) ); if (Array.isArray(obj)) { clone.length obj.length; return Array.from(clone); } return clone; }; const a { foo: bar, obj: { a: 1, b: 2 } }; const b deepClone(a); // a ! b, a.obj ! b.obj不过手写递归深克隆无法处理类实例、函数等特殊场景且性能有限。现代 JavaScript 提供了更优方案——全局函数structuredClone()它可以深克隆对象同时适用于数组和对象是目前官方推荐、性能与可靠性俱佳的方式const a { x: 1, y: { y1: a }, z: new Set([1, 2]) }; const b structuredClone(a); // a ! b, a.y ! b.y, a.z ! b.z注意structuredClone()是较新加入语言的功能但已获得所有现代浏览器及 Node.jsv17.0.0 及以上版本的支持。强制不可变性Object.freeze()、Object.seal()与Proxy如果不仅要约定俗成地不修改数据还要在运行时强制禁止修改JavaScript 也提供了相应的工具。Object.freeze()与Object.seal()的区别仓库中的 deep-freeze-object.md 给出了两者非常清晰的行为对比const frozen Object.freeze({ username: johnsmith }); const sealed Object.seal({ username: johnsmith }); frozen.name John Smith; // frozen { username: johnsmith } sealed.name John Smith; // sealed { username: johnsmith } delete frozen.username; // frozen { username: johnsmith } delete sealed.username; // sealed { username: johnsmith } frozen.username jsmith; // frozen { username: johnsmith } sealed.username jsmith; // sealed { username: jsmith }Create新增属性Read读取Update修改值Delete删除属性Object.freeze()NoYesNoNoObject.seal()NoYesYesNo两种方法都阻止新增和删除属性区别在于Object.freeze()连既有属性的值修改也一并禁止。因此仓库建议优先使用Object.freeze()它覆盖面更广、方案更稳健。需要注意的是Object.freeze()和Object.seal()都是浅冻结——嵌套对象和数组仍然可变。如需深度冻结可以递归地对每个属性应用Object.freeze()const deepFreeze obj { Object.keys(obj).forEach(prop { if (typeof obj[prop] object !Object.isFrozen(obj[prop])) deepFreeze(obj[prop]); }); return Object.freeze(obj); };补充在严格模式use strict下修改冻结对象会直接抛出错误这能帮助你第一时间发现 bug而不是静默失败详见 deep-freeze-object.md 的严格模式小节。用Proxy实现递归不可变Object.freeze()虽然直观但在嵌套层级极深或对象生命周期很短的场景下未必是最佳选择。此时可以使用Proxy来达成同样目的——immutable-object-proxy.md 展示了如何用set()陷阱阻止修改、用get()陷阱对嵌套值递归套用代理const immutable obj new Proxy(obj, { get(target, prop) { return typeof target[prop] object ? immutable(target[prop]) : target[prop]; }, set() { throw new Error(This object is immutable.); }, }); const term { id: 1, value: hello, properties: [{ type: usage, value: greeting }], }; const immutableTerm immutable(term); const immutableProperty immutableTerm.properties[0]; immutableTerm.name hi; // Error: This object is immutable. immutableTerm.id 2; // Error: This object is immutable. immutableProperty.value pronoun; // Error: This object is immutable.这段代码的精髓在于set()陷阱拦截一切赋值操作并抛出错误get()陷阱则检查取出的值是否为对象若是则继续包一层代理从而无论对象多复杂、嵌套多深都能保证不可变。字符串JavaScript 中天然不可变的基础类型提到不可变性字符串常是困惑的源头。仓库中的 string-immutability.md 明确JavaScript 的字符串是不可变的基础类型immutable primitives——其中的字符不能被改变任何对字符串的操作实际上都会创建新字符串const x type; x[1] a; // 什么都没发生也不会抛错 console.log(x); // LOGS: type理解字符串的最好方式是把它想成数字数字同样是不可变的基础类型。假如数字可变42的含义就可能被改成13同理你无法把值为gem的字符串原地修改成gym。这种按值共享、不可变更的特性也让字符串天然契合函数式编程的不可变性要求。传递语义按值传递还是按引用传递不可变性常常与 JavaScript 的传参语义纠缠在一起。pass-by-reference-or-pass-by-value.md 给出了一个关键结论JavaScript 永远是按值传递pass-by-value但对象类型属于按值传递的引用类型——传给函数的是对象的引用副本因此在函数内部修改对象属性会影响到原对象两者指向同一个对象但重新给原变量赋值不会影响函数内持有的引用。let myObj { a: 1 }; const myFunc obj { obj.a; return obj; } let otherObj myFunc(myObj); myObj; // { a: 2 } otherObj; // { a: 2 } myObj otherObj; // true myObj { a: 4, b: 0 }; myObj; // { a: 4, b: 0 } otherObj; // { a: 2 } myObj otherObj; // false正因为对象是按引用共享的一旦某处原地修改就会波及所有持有该引用的地方——这正是不可变性想要消除的风险。理解了这套传递语义你就能更深刻地体会复制而非修改的不可变实践为何如此重要。深入仓库继续学习本文的核心内容源自 immutability.md你还可以按下面的路径在仓库中继续深入JavaScript Immutability 文章集合收录了本文相关的全部 8 篇文章入口含复制数组、深浅克隆、展开运算符技巧、深度冻结、Proxy不可变、字符串不可变、传参语义。函数式编程入门了解纯函数、高阶函数、递归等不可变性的左邻右舍。浅拷贝与深拷贝structuredClone()等克隆方案的完整对比。深度冻结对象Object.freeze()/Object.seal()行为对照表、深冻结与复杂对象冻结。基于 Proxy 的不可变对象本文 5.2 小节的完整实现上下文。克隆数组 与 展开运算符技巧数组场景的更多不可变操作写法。掌握不可变性是从能写 JavaScript迈向以函数式思维编程的关键一步。把修改数据的习惯替换成复制并产生新数据你的代码将更容易推理、测试和并行组合——这正是函数式编程最大的价值所在。赞分享教程文档【免费下载链接】30-seconds-of-codeCoding articles to level up your development skills项目地址https://gitcode.com/gh_mirrors/30/30-seconds-of-code点击查看免费下载相关推荐深入理解30-seconds-of-code中的JavaScript不可变性(Immutability)实践深入理解30 seconds of code中的JavaScript不可变性 Immutability 实践 不可变性 Immutability 是现代Java教程文档30 seconds of Python函数式编程不可变数据与纯函数实践30 seconds of Python函数式编程不可变数据与纯函数实践 你是否在Python开发中遇到过数据状态混乱、函数副作用难以调试的问题本文将通过教程文档30 seconds of codeJavaScript 偏函数应用Partial Application完整实战指南30 seconds of codeJavaScript 偏函数应用Partial Application完整实战指南 偏函数应用partial app教程文档创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
返回列表