首頁 >web前端 >js教程 >帶有MORI的不變數據和功能性JavaScript

帶有MORI的不變數據和功能性JavaScript

Joseph Gordon-Levitt
Joseph Gordon-Levitt原創
2025-02-18 08:34:10721瀏覽

帶有MORI的不變數據和功能性JavaScript

鑰匙要點

  • > MORI利用Clojure的持久數據結構,為JavaScript開發人員不可變的數據選項提供了增強代碼簡單性和可靠性的不可能。
  • >
  • 使用MORI通過強制性不變性來促進JavaScript中的功能編程範式,從而防止意外副作用並確保整個應用程序生命週期中的數據一致性。 庫促進了處理數據的不同方法,其中函數分別在數據結構上運行,與JavaScript的典型對象方面的方法形成鮮明對比,從而允許使用更清潔,更可預測的代碼。
  • MORI的結構共享技術通過在可能的情況下重複現有數據結構來使數據操縱有效,從而可以改善應用程序的性能。
  • >通過諸如具有撤消功能的像素編輯器之類的示例,Mori展示了不變數據結構的實際應用,從而為開發人員提供了構建既複雜且穩健的功能的工具。
  • >。
  • 本文由Craig Bilner和Adrian Sandu進行了同行評審。感謝SitePoint所有的同行評審員製作SitePoint內容的最佳狀態!
>功能編程和不變數據是許多JavaScript開發人員的當前重點,因為他們試圖找到使代碼更簡單,更易於推理的方法。

>儘管JavaScript一直支持某些功能性編程技術,但它們在過去幾年中才真正流行,傳統上也沒有對不變數據的本地支持。 JavaScript仍在學習很多方面,最好的想法來自已經嘗試並測試了這些技術的語言。 在編程世界的另一個角落,Clojure是一種功能性編程語言,致力於真正的簡單性,尤其是在數據結構的情況下。 Mori是一個庫,允許我們直接從JavaScript中使用Clojure的持久數據結構。 >本文將探討這些數據結構設計背後的基本原理,並檢查一些使用它們來改善我們的應用程序的模式。我們也可以將其視為對使用Clojure或Clojurescript進行編程的JavaScript開發人員的第一個墊腳石。

>

什麼是持續數據?

>

> clojure在無法更改的

持續

的值之間進行區分,而 thristient

值在突變之間具有時間壽命。嘗試修改持續數據結構的嘗試避免通過返回使用更改的新結構來突變基礎數據。 >

>可能有助於查看這種區別在理論編程語言中的外觀。

<span>// transient list
</span>a <span>= [1, 2, 3];
</span>b <span>= a.push(4);
</span><span>// a = [1, 2, 3, 4]
</span><span>// b = [1, 2, 3, 4]
</span>
<span>// persistent list
</span>c <span>= #[1, 2, 3]
</span>d <span>= c.push(4);
</span><span>// c = #[1, 2, 3]
</span><span>// d = #[1, 2, 3, 4]
</span>

>我們可以看到,當我們將一個值推向其上時,瞬態列表被突變了。 A和B都指向相同的可變值。相比之下,在持久列表上呼叫推送返回了一個新值,我們可以看到c和d指向不同的離散列表。

>

>這些持久的數據結構無法突變,這意味著一旦我們提到了一個值,我們也可以保證它永遠不會被更改。這些保證通常可以幫助我們編寫更安全,更簡單的代碼。例如,將持久數據結構作為參數的函數無法突變它們,因此,如果該函數想要傳達有意義的更改,則必須來自返回值。這導致編寫引用透明的純函數,易於測試和優化。

更簡單,不變的數據迫使我們編寫更多功能代碼。 >

什麼是mori?

> Mori使用clojurescript編譯器來編譯Clojure標準庫中數據結構的實現,以彙編JavaScript。編譯器發出了優化的代碼,這意味著沒有其他考慮,與JavaScript的Clojure進行交流並不容易。莫里是其他考慮因素的層。

>就像Clojure一樣,Mori的功能與它們操作的數據結構分開,與JavaScript面向對象的趨勢對比。我們會發現這種差異改變了我們編寫代碼的方向。

莫里還使用結構共享來通過盡可能多的原始結構共享對數據進行有效的更改。這使得持續數據結構幾乎與常規瞬態效率一樣有效。這些概念的實現在此視頻中更詳細地介紹了。

>
<span>// standard library
</span><span>Array(1, 2, 3).map(x => x * 2);
</span><span>// => [2, 4, 6]
</span>
<span>// mori
</span><span>map(x => x * 2, vector(1, 2, 3))
</span><span>// => [2, 4, 6]
</span>
為什麼有用?

> 首先,讓我們想像我們正在嘗試在我們繼承的JavaScript代碼庫中追踪一個錯誤。我們正在閱讀代碼,試圖弄清楚為什麼我們最終獲得了獎學金的錯誤價值。

>

登錄到控制台時獎學金的價值是多少?

>

>不運行代碼或閱讀deleteperson()的定義,就無法知道。它可能是一個空數組。它可能具有三個新屬性。我們希望這是一個刪除第二個元素的數組,但是由於我們通過可變的數據結構通過,因此無法保證。
<span>const fellowship = [
</span>  <span>{
</span>    <span>title: 'Mori',
</span>    <span>race: 'Hobbit'
</span>  <span>},
</span>  <span>{
</span>    <span>title: 'Poppin',
</span>    <span>race: 'Hobbit'
</span>  <span>}
</span><span>];
</span>
<span>deletePerson(fellowship, 1);
</span><span>console.log(fellowship);
</span>
更糟糕的是,該函數可以保持參考並在將來異步進行突變。從這裡開始對獎學金的所有參考都將與不可預測的價值合作。 >

將其與Mori的替代方案進行比較。

<span>// transient list
</span>a <span>= [1, 2, 3];
</span>b <span>= a.push(4);
</span><span>// a = [1, 2, 3, 4]
</span><span>// b = [1, 2, 3, 4]
</span>
<span>// persistent list
</span>c <span>= #[1, 2, 3]
</span>d <span>= c.push(4);
</span><span>// c = #[1, 2, 3]
</span><span>// d = #[1, 2, 3, 4]
</span>

>不管deleteperson()的實施如何,我們知道原始向量將被記錄,僅僅是因為可以保證它不能突變。如果我們希望該函數有用,則應返回並刪除指定項目的新向量。 >

理解流過在不變數據上工作的函數的流很容易,因為我們知道它們的唯一效果是得出並返回獨特的不變價值。

在可變數據上運行的帶有MORI的不變數據和功能性JavaScript函數並不總是返回值,它們可以突變其輸入,有時還留給程序員再次在另一側拾取值。

>

帶有MORI的不變數據和功能性JavaScript更簡單,不變的數據可實現可預測性的文化。

在實踐中

>我們將研究如何使用MORI來構建具有撤消功能的像素編輯器。以下代碼可作為Codepen可用,您也可以在文章的腳下找到。

>

我們假設您要么跟隨Codepen,要么在ES2015環境中使用MORI和以下HTML。 帶有MORI的不變數據和功能性JavaScript

設置和實用程序

讓我們開始通過破壞Mori名稱空間所需的功能開始。

<span>// standard library
</span><span>Array(1, 2, 3).map(x => x * 2);
</span><span>// => [2, 4, 6]
</span>
<span>// mori
</span><span>map(x => x * 2, vector(1, 2, 3))
</span><span>// => [2, 4, 6]
</span>
這主要是一種風格偏好。您也可以通過直接在Mori對像上訪問MORI(例如Mori.list())。

>我們要做的第一件事是設置用於查看持續數據結構的助手功能。莫里(Mori)的內部表示形式在控制台中沒有多大意義,因此我們將使用tojs()函數將它們轉換為可理解的格式。

>

>當我們需要檢查Mori的數據結構時,我們可以將此功能用作cons.log()的替代方法。
<span>const fellowship = [
</span>  <span>{
</span>    <span>title: 'Mori',
</span>    <span>race: 'Hobbit'
</span>  <span>},
</span>  <span>{
</span>    <span>title: 'Poppin',
</span>    <span>race: 'Hobbit'
</span>  <span>}
</span><span>];
</span>
<span>deletePerson(fellowship, 1);
</span><span>console.log(fellowship);
</span>
接下來,我們將設置一些配置值和實用程序函數。

>希望您注意到我們的to2d()函數返迴向量。向量有點像JavaScript數組,並且支持有效的隨機訪問。 >

構造數據

<span>import <span>{ vector, hashMap }</span> from 'mori';
</span>
<span>const fellowship = vector(
</span>  <span>hashMap(
</span>    <span>"name", "Mori",
</span>    <span>"race", "Hobbit"
</span>  <span>),
</span>  <span>hashMap(
</span>    <span>"name", "Poppin",
</span>    <span>"race", "Hobbit"
</span>  <span>)
</span><span>)
</span>
<span>const newFellowship = deletePerson(fellowship, 1);
</span><span>console.log(fellowship);
</span>
>我們將使用我們的to2d()函數來創建一系列坐標,該坐標將代表畫布上的所有像素。

>我們使用range()函數來生成一個數字序列在0和高度*寬度之間(在我們的情況100)之間,並且我們使用map()將其轉換為使用to2d的2D坐標列表( )輔助功能。

<span><span><span><div</span>></span>
</span>  <span><span><span><h3</span>></span>Mori Painter<span><span></h3</span>></span>
</span><span><span><span></div</span>></span>
</span><span><span><span><div</span> id<span>="container"</span>></span>
</span>  <span><span><span><canvas</span> id<span>='canvas'</span>></span><span><span></canvas</span>></span>
</span><span><span><span></div</span>></span>
</span><span><span><span><div</span>></span>
</span>  <span><span><span><button</span> id<span>='undo'</span>></span>↶<span><span></button</span>></span>
</span><span><span><span></div</span>></span>
</span>
>可能有助於可視化坐標的結構。

>

這是坐標向量的一維序列。

>在每個坐標的旁邊,我們還需要存儲一個顏色值。 >
<span>// transient list
</span>a <span>= [1, 2, 3];
</span>b <span>= a.push(4);
</span><span>// a = [1, 2, 3, 4]
</span><span>// b = [1, 2, 3, 4]
</span>
<span>// persistent list
</span>c <span>= #[1, 2, 3]
</span>d <span>= c.push(4);
</span><span>// c = #[1, 2, 3]
</span><span>// d = #[1, 2, 3, 4]
</span>

>我們正在使用repot()函數來創建“ #FFF”字符串的無限序列。我們不必擔心會填充內存並崩潰我們的瀏覽器,因為Mori序列支持懶惰評估。我們只在稍後要求時才計算序列中項目的值。

>最後,我們想以哈希地圖的形式將坐標與我們的顏色相結合。

<span>// standard library
</span><span>Array(1, 2, 3).map(x => x * 2);
</span><span>// => [2, 4, 6]
</span>
<span>// mori
</span><span>map(x => x * 2, vector(1, 2, 3))
</span><span>// => [2, 4, 6]
</span>
>我們使用zipmap()函數來創建哈希地圖,將坐標作為鍵,顏色作為值。同樣,這可能有助於可視化數據的結構。

與JavaScript的對像不同,Mori的Hash Maps可以將任何類型的數據作為鍵。

繪製像素
<span>const fellowship = [
</span>  <span>{
</span>    <span>title: 'Mori',
</span>    <span>race: 'Hobbit'
</span>  <span>},
</span>  <span>{
</span>    <span>title: 'Poppin',
</span>    <span>race: 'Hobbit'
</span>  <span>}
</span><span>];
</span>
<span>deletePerson(fellowship, 1);
</span><span>console.log(fellowship);
</span>
為了更改像素的顏色,我們將把哈希地圖中的一個坐標與新字符串相關聯。讓我們寫一個純淨的函數,為單個像素著色。

>

>我們使用X和Y坐標來創建可以用作鍵的坐標向量,然後我們使用Assoc()將該密鑰與新顏色相關聯。請記住,由於數據結構是持續的,因此assoc.)函數將返回a

new

hash映射,而不是突變舊的。

繪畫圖片
<span>import <span>{ vector, hashMap }</span> from 'mori';
</span>
<span>const fellowship = vector(
</span>  <span>hashMap(
</span>    <span>"name", "Mori",
</span>    <span>"race", "Hobbit"
</span>  <span>),
</span>  <span>hashMap(
</span>    <span>"name", "Poppin",
</span>    <span>"race", "Hobbit"
</span>  <span>)
</span><span>)
</span>
<span>const newFellowship = deletePerson(fellowship, 1);
</span><span>console.log(fellowship);
</span>

>現在,我們有所有需要將簡單圖像繪製到畫布上的一切。讓我們創建一個函數,該函數將坐標圖的哈希映射針對像素,並將它們繪製到renderingContext2d。 讓我們花一點時間了解這裡發生了什麼。

>

>我們正在使用每個()在像素上迭代哈希地圖上迭代。它將每個密鑰和值(作為序列一起)作為p傳遞到回調函數中。然後,我們使用inarray()函數將其轉換為可能破壞的數組,因此我們可以挑選出所需的值。

>

最後,我們使用帆布方法將彩色矩形繪製到上下文本身上。
<span><span><span><div</span>></span>
</span>  <span><span><span><h3</span>></span>Mori Painter<span><span></h3</span>></span>
</span><span><span><span></div</span>></span>
</span><span><span><span><div</span> id<span>="container"</span>></span>
</span>  <span><span><span><canvas</span> id<span>='canvas'</span>></span><span><span></canvas</span>></span>
</span><span><span><span></div</span>></span>
</span><span><span><span><div</span>></span>
</span>  <span><span><span><button</span> id<span>='undo'</span>></span>↶<span><span></button</span>></span>
</span><span><span><span></div</span>></span>
</span>

將其接線

現在,我們需要進行一些管道,以使所有這些部分將所有這些零件一起工作。

<span>const {
</span>  list<span>, vector, peek, pop, conj, map, assoc, zipmap,
</span>  range<span>, repeat, each, count, intoArray, toJs
</span><span>} = mori;
</span>
>我們將掌握畫布,並使用它來創建上下文來呈現我們的圖像。我們還將適當地調整它的大小以反映我們的尺寸。

>

>最後,我們將使用像素通過油漆方法繪製的像素來傳遞上下文。幸運的是,您的畫布應該像白色像素一樣渲染。不是最令人興奮的揭露,但我們越來越接近。
<span>const log = (<span>...args</span>) => {
</span>  <span>console.log(...args.map(toJs))
</span><span>};
</span>
>

互動

>我們想收聽點擊事件,並使用它們以較早的draw()函數來更改特定像素的顏色。

>
<span>// the dimensions of the canvas
</span><span>const [height, width] = [20, 20];
</span>
<span>// the size of each canvas pixel
</span><span>const pixelSize = 10;
</span>
<span>// converts an integer to a 2d coordinate vector
</span><span>const to2D = (i) => vector(
</span>  i <span>% width,
</span>  <span>Math.floor(i / width)
</span><span>);
</span>

>我們將單擊偵聽器附加到我們的畫布上,並使用事件坐標來確定要繪製哪個像素。我們使用此信息使用我們的draw()函數創建新的像素哈希地圖。然後,我們將其繪製到我們的上下文中,並覆蓋我們畫的最後一幀。

在這一點上,我們可以將黑色像素繪製到畫布中,每個幀將基於上一個,創建一個複合圖像。

>跟踪幀

要實施撤消,我們將需要將每個歷史性的修訂存儲到像素哈希地圖上,因此我們將來可以再次檢索它們。

我們正在使用列表來存儲我們繪製的不同的“幀”。列表支持在頭部的有效添加,O(1)查找第一項,這使其非常適合表示堆棧。
<span>// transient list
</span>a <span>= [1, 2, 3];
</span>b <span>= a.push(4);
</span><span>// a = [1, 2, 3, 4]
</span><span>// b = [1, 2, 3, 4]
</span>
<span>// persistent list
</span>c <span>= #[1, 2, 3]
</span>d <span>= c.push(4);
</span><span>// c = #[1, 2, 3]
</span><span>// d = #[1, 2, 3, 4]
</span>
我們需要修改單擊偵聽器以與幀堆棧一起使用。

我們正在使用PEEK()函數將框架放在堆棧頂部。然後,我們使用它來使用draw()函數創建一個新幀。最後,我們將conj()用於

conjoin

框架頂部的新框架。
<span>// standard library
</span><span>Array(1, 2, 3).map(x => x * 2);
</span><span>// => [2, 4, 6]
</span>
<span>// mori
</span><span>map(x => x * 2, vector(1, 2, 3))
</span><span>// => [2, 4, 6]
</span>

儘管我們正在更改本地狀態(frame = conj(框架,newFrame)),但我們實際上並未突變任何數據。 撤消更改

>最後,我們需要實現一個撤消按鈕,以從堆棧中彈出頂框。 >

>單擊“撤消按鈕”時,我們檢查當前是否有任何幀要撤消,然後使用pop()函數用不再包含頂幀的新列表替換幀。 >最後,我們將新堆棧上的頂幀傳遞給油漆()函數以反映更改。在這一點上,您應該能夠繪製並撤消對畫布的更改。 > demo

這是我們最終得到的:
<span>const fellowship = [
</span>  <span>{
</span>    <span>title: 'Mori',
</span>    <span>race: 'Hobbit'
</span>  <span>},
</span>  <span>{
</span>    <span>title: 'Poppin',
</span>    <span>race: 'Hobbit'
</span>  <span>}
</span><span>];
</span>
<span>deletePerson(fellowship, 1);
</span><span>console.log(fellowship);
</span>

請參閱codepen上的sitepoint(@sitepoint)的筆莫里像素。

擴展

這是您可以改善此應用程序的方式的想法列表:>

添加一個調色板,允許用戶在繪製

之前選擇顏色

>使用本地存儲來保存會話之間的幀

使CTRL Z鍵盤快捷鍵撤消更改

允許用戶在拖動鼠標時繪製

    通過移動索引指針來實現重做,而不是從堆棧中刪除幀
  • >仔細閱讀相同程序的clojurescript源
  • 結論
  • >我們已經研究了向量,列表,範圍和哈希地圖,但是Mori還附帶了集合,排序的集合和隊列,這些數據結構中的每一個都帶有用於與之合作的多態性功能的補充。
  • >我們幾乎沒有刮過可能的表面,但是希望您能看到足夠的時間來重視將持久數據與一組功能強大的簡單功能配對的重要性。

    經常詢問有關不變數據和功能性JavaScript的問題

    JavaScript中不變性的概念是什麼?這意味著一旦分配了一個值,就無法更改它。這個概念對於功能編程至關重要,因為它有助於避免副作用,並使您的代碼更容易預測和易於理解。它還可以通過允許有效的數據檢索和內存使用量來提高應用程序的性能。

    >

    > MORI庫如何幫助處理JavaScript中的不變數據?

    持續的數據結構中的JavaScript。這些數據結構是不可變的,這意味著它們一旦創建就無法更改。這有助於保持數據的完整性並避免意外修改。 MORI還提供了一套豐富的功能性編程實用程序,使操縱這些數據結構變得更容易。

    >

    >使用MORI而不是本機JavaScript方法處理不變數據有什麼好處?確實提供了處理不變數據的方法,Mori提供了一種更有效,更健壯的方法。與本機JavaScript方法相比,MORI的持續數據結構更快,並且消耗的內存更少。此外,莫里(Mori 。由於創建一旦創建就無法更改不變的對象,因此可以在多個函數調用中安全地重複使用,而不會被修改。這會導致有效的內存使用和更快的數據檢索,從而提高了應用程序的整體性能。

    >可突變和不可變的數據結構之間有什麼區別?

    > MORI如何處理數據操作?

    MORI提供了豐富的功能編程實用程序來操縱數據。這些實用程序允許您在數據結構上執行諸如地圖,減少,過濾等的各種操作,而無需更改原始數據。

    Mori中的持續數據結構是什麼?在Mori中,是不可變的數據結構,在修改後可以保留先前版本的數據。這意味著每次您對持久數據結構執行操作時,都會創建一個新版本,並保留舊版本。

    MORI如何確保數據完整性?

    MORI通過提供不可變的數據結構來確保數據完整性。由於創建後無法修改這些數據結構,因此消除了意外數據修改的風險。這有助於維持數據的完整性。

    >

    在JavaScript中使用MORI?

    在JavaScript中使用MORI的功能編程具有什麼優勢。通過避免副作用,它使您的代碼更容易預測,並且更容易理解。它還通過允許有效的數據檢索和內存使用量來增強您的應用程序的性能。

    >

    >我如何在我的JavaScript項目中開始使用Mori?

    在您的JavaScript項目中開始使用Mori,您需要在您的項目中包括Mori庫。您可以通過通過NPM安裝它或將其直接包含在HTML文件中來執行此操作。包含庫後,您可以在代碼中開始使用MORI的功能和數據結構。

    >

以上是帶有MORI的不變數據和功能性JavaScript的詳細內容。更多資訊請關注PHP中文網其他相關文章!

陳述:
本文內容由網友自願投稿,版權歸原作者所有。本站不承擔相應的法律責任。如發現涉嫌抄襲或侵權的內容,請聯絡admin@php.cn