不可變性(Immutability
)是函數式編程的核心原則,在面向對象編程裏也有大量應用。在這篇文章裏,我會給你們秀一下到底什麼是不可變性(Immutability
)、她爲何還這麼屌、以及在JavaScript
中怎麼應用。javascript
Immutability
)?仍是先來看看關於可變性(Mutability
)的教條式定義:「liable or subject to change or alteration(譯者注:真他媽難翻,就簡單理解成'易於改變的'吧)」。在編程領域裏,咱們用可變性(Mutability
)來描述這樣一種對象,它在建立以後狀態依舊可被改變。那當咱們說不可變(Immutable
)時,就是可變(Mutable
)的對立面了(譯者注:原諒我翻的廢話又多起來) - 意思是,建立以後,就不再能被修改了。java
若是我說的又讓你感到詭異了,原諒我小小的提醒一下,其實咱們平時使用的不少東西事實上都是不可變的哦!node
var statement = 'I am an immutable value'; var otherStr = statement.slice(8, 17);
我猜沒人會吃驚,statement.slice(8, 17)
並無改變statement
變量吧(譯者注:若是你吃驚了,趕忙去補基本知識吧)?事實上,string
對象上的全部方法裏,沒有一個會修改原string
,它們一概返回新的string
。緣由簡單了,由於string
就是是不可變的(Immutable
) - 它們不能被修改,咱們能作的就是基於原string
操做後獲得一個新string
。git
注意了,string
可不是JavaScript
裏惟一內置的不可變(Immutable
)數據類型哦。number
也是不可變(Immutable
)的。不然的話,你試想下這個表達式2 + 3
,若是2
的含義能被修改,那代碼該怎麼寫啊\|_\|。聽起來荒謬吧,但咱們在編程中卻經常對object
和array
作出這種事兒。github
在JavaScript
中,string
和number
從設計之初就是不可變(Immutable
)的。可是,看看下面這個關於array
例子:編程
var arr = []; var v2 = arr.push(2);
來我問你,v2
的值是什麼?若是array
和string
、number
同樣也是不可變(Immutable
)的,那此時v2
一定是一個包含了一個數字2
的新array
。事實上,還真就不是那樣的。這裏arr
引用的array
被修改了,裏面添了一個數字2
,這時v2
的值(也就是arr.push(2)
的返回值),實際上是arr
此時的長度 - 就是1
。數組
試想咱們擁有一個不可變的數組(ImmutableArray
)。就像string
、number
那樣,她應該能像以下這樣被使用:安全
var arr = new ImmutableArray([1, 2, 3, 4]); var v2 = arr.push(5); arr.toArray(); // [1, 2, 3, 4] v2.toArray(); // [1, 2, 3, 4, 5]
相似的,也能夠有一個不可變的Map(ImmutableMap
),理論上能夠替代object
應該於多數場景,她應該有一個set
方法,不過這個set
方法不會塞任何東西到原Map
裏,而是返回一個包含了塞入值的新Map
:數據結構
var person = new ImmutableMap({name: 'Chris', age: 32}); var olderPerson = person.set('age', 33); person.toObject(); // {name: 'Chris', age: 32} olderPerson.toObject(); // {name: 'Chris', age: 33}
就像2 + 3
這個表達式裏,咱們不可能改變2
或是3
所表明的含義,一個person
在慶祝他33歲的生日,並不會影響他曾經是32歲的事實。框架
Immutability
)實戰JavaScript
裏目前尚未不可變的list
和map
,因此暫時咱們仍是須要三方庫的幫助。有兩個很不錯的,一個是Mori - 她把ClojureScript
裏持久化數據結構的API支持帶到了JavaScript
裏;另外一個是Facebook出品的immutable.js。後面的示例裏,我將使用immutable.js,由於她的API對於JavaScript
開發者更友好一些。
下面的例子裏,咱們使用不可變(Immutable
)知識來構建一個掃雷小遊戲。掃雷的遊戲面板咱們用一個不可變的map
來構建,其中tiles
(雷區區塊)部分值得關注哦,它是一個由不可變map
組成的不可變list
(譯者注:又開始繞了),其中每個不可變的map
表示一個tile
(雷區塊)。整個這個雷區面板都是由JavaScript
的object
和array
組成的,最後由immutable.js的fromJS
方法對其進行不可變化處理:
function createGame(options) { return Immutable.fromJS({ cols: options.cols, rows: options.rows, tiles: initTiles(options.rows, options.cols, options.mines) }); }
剩下的主要邏輯部分就是「掃雷」了,傳入掃雷遊戲對象(一個不可變結構)作爲第一個參數,以及要「掃」的那個tile
(雷區塊)對象,最後返回新的掃雷遊戲實例。如下咱們就要講到這個revealTile
函數。當它被調用時,tile
(雷區塊)的狀態就要被重置爲「掃過」的狀態。若是是可變編程,代碼很簡單:
function revealTile(game, tile) { game.tiles[tile].isRevealed = true; }
而後再來看看若是用上面介紹的不可變數據結構來編碼,坦白講,一開始代碼變得都點醜了:
function revealTile(game, tile) { var updatedTile = game.get('tiles').get(tile).set('isRevealed', true); var updatedTiles = game.get('tiles').set(tile, updatedTile); return game.set('tiles', updatedTiles); }
我去,醜爆了有木有!
萬幸,不可變性不止於此,必定有得救!這種需求很常見,因此工具早就考慮到了,能夠這麼操做:
function revealTile(game, tile) { return game.setIn(['tiles', tile, 'isRevealed'], true); }
如今revealTile
返回一個新的實例了,新實例裏其中一個tile
(雷區塊)的isRevealed
就和以前那個game
實例裏的不同了。這裏面用到的setIn
是一個null-safe
(空值安全)的函數,任意keyPath
中的key
不存在時,都會在這個位置建立一個新的不可變map
(譯者注:這句略繞,我的認爲既然這裏不是主講immutable.js,那就不必非提一下它的這個特性,反而不清不楚,原做沒細說,那我也就很少說了,有興趣的能夠來這裏本身揣摩)。這個null-safe
特性對於咱們如今掃雷遊戲這個例子並不合適,由於「掃」一個不存在的tile
(雷區塊)表示咱們正在試圖掃雷區之外的地方,那顯然不對!這裏須要多作一步檢查,經過getIn
方法檢查tile
(雷區塊)是否存在,而後再「掃」它:
function revealTile(game, tile) { return game.getIn(['tiles', tile]) ? game.setIn(['tiles', tile, 'isRevealed'], true) : game; }
若是tile
(雷區塊)不存在,咱們就返回原掃雷遊戲實例。這就是個可迅速上手的關於不可變性(Immutability
)的練習,想深刻了解的能夠看codepen,完整的實現都在裏面了。
你可能以爲,這他媽Performance應該low爆了吧,我只能說某些狀況下你是對的。每當你想添加點東西到一個不可變(Immutable
)對象裏時,她必定是先拷貝以存在值到新實例裏,而後再給新實例添加內容,最後返回新實例。相比可變對象,這勢必會有更多內存、計算量消耗。
由於不可變(Immutable
)對象永遠不變,實際上有一種實現策略叫「結構共享」,使得她的內存消耗遠比你想象的少。雖然和內置的array
、object
的「變化」相比仍然會有額外的開銷,但這個開始恆定,絕對能夠被不可變性(Immutability
)帶來的其它衆多優點所消磨、減小。在實踐中,不可變性(Immutability
)帶來的優點能夠極大的優化程序的總體性能,即便其中的某些個別操做開銷變大了。
各類UI框架裏,最難的部分永遠是變動追蹤(譯者注:或者叫「髒檢查」)。這是JavaScript
社區裏的廣泛問題,因此EcmaScript 7裏提供了單獨的API在保證Performance的前提下能夠追蹤變化:Object.observe()
。不少人爲之激動,但也有很多人認爲這個API然並卵。他們認爲,在任何狀況下,這個API都沒很好的解決變動追蹤問題:
var tiles = [{id: 0, isRevealed: false}, {id: 1, isRevealed: true}]; Object.observe(tiles, function () { /* ... */ }); tiles[0].id = 2;
上面例子裏,tiles[0]
的變動並無觸發observer
,因此其實這個提案即使是最簡單的變動追蹤也沒作到。那不可變性(Immutability
)又是怎麼解決的?假設有一個應用狀態a
,而後它內部有值被改變了,因而就獲得了一個新的實例b
:
if (a === b) { // 數據沒變,中止操做 }
若是應用狀態a
沒有被修改,那b
就是a
,它們指向同一個實例,===
就夠了,不用作其餘事兒。固然這須要咱們追蹤應用狀態的引用,但整個問題的複雜度被大大簡化了,如今只要判斷一下它們是否同一個實例的引用就行了,真心不用再去深刻調查裏面的某某字段是否是變了。
但願本文能某種程度上幫你瞭解不可變性(Immutability
)是如何幫咱們優化/改進代碼的,也但願這些例子從實踐角度說清楚了使用方式。不可變性(Immutability
)的熱度在持續增高,我肯定這毫不是你今年看到的關於不可變性(Immutability
)的最後一文。同志們,是時候來一發了,我相信你用事後必定會high至的,就像我如今同樣^^。