[譯]JavaScript中的不可變性(Immutability)

不可變性(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操做後獲得一個新stringgit

注意了,string可不是JavaScript裏惟一內置的不可變(Immutable)數據類型哦。number也是不可變(Immutable)的。不然的話,你試想下這個表達式2 + 3,若是2的含義能被修改,那代碼該怎麼寫啊\|_\|。聽起來荒謬吧,但咱們在編程中卻經常對objectarray作出這種事兒。github

JavaScript充滿變化

JavaScript中,stringnumber從設計之初就是不可變(Immutable)的。可是,看看下面這個關於array例子:編程

var arr = [];
var v2 = arr.push(2);

來我問你,v2的值是什麼?若是arraystringnumber同樣也是不可變(Immutable)的,那此時v2一定是一個包含了一個數字2的新array。事實上,還真就不是那樣的。這裏arr引用的array被修改了,裏面添了一個數字2,這時v2的值(也就是arr.push(2)的返回值),實際上是arr此時的長度 - 就是1數組

試想咱們擁有一個不可變的數組(ImmutableArray)。就像stringnumber那樣,她應該能像以下這樣被使用:安全

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歲的事實。框架

JavaScript不可變性(Immutability)實戰

JavaScript裏目前尚未不可變的listmap,因此暫時咱們仍是須要三方庫的幫助。有兩個很不錯的,一個是Mori - 她把ClojureScript裏持久化數據結構的API支持帶到了JavaScript裏;另外一個是Facebook出品的immutable.js。後面的示例裏,我將使用immutable.js,由於她的API對於JavaScript開發者更友好一些。

下面的例子裏,咱們使用不可變(Immutable)知識來構建一個掃雷小遊戲。掃雷的遊戲面板咱們用一個不可變的map來構建,其中tiles(雷區區塊)部分值得關注哦,它是一個由不可變map組成的不可變list(譯者注:又開始繞了),其中每個不可變的map表示一個tile(雷區塊)。整個這個雷區面板都是由JavaScriptobjectarray組成的,最後由immutable.jsfromJS方法對其進行不可變化處理:

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怎麼樣?

你可能以爲,這他媽Performance應該low爆了吧,我只能說某些狀況下你是對的。每當你想添加點東西到一個不可變(Immutable)對象裏時,她必定是先拷貝以存在值到新實例裏,而後再給新實例添加內容,最後返回新實例。相比可變對象,這勢必會有更多內存、計算量消耗。

由於不可變(Immutable)對象永遠不變,實際上有一種實現策略叫「結構共享」,使得她的內存消耗遠比你想象的少。雖然和內置的arrayobject的「變化」相比仍然會有額外的開銷,但這個開始恆定,絕對能夠被不可變性(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至的,就像我如今同樣^^。

原文地址:Immutability in JavaScript

相關文章
相關標籤/搜索