immutable日常操作之深入API

寫在前面

本文只是個人在熟悉Immutable.js的一些個人筆記,因此我只根據我自己的情況來熟悉API,所以很多API並沒有被列舉到,比如常規的push/map/filter/reduce等等操作,這些API我認爲只要你自己稍微看一下官網的介紹都可以知道怎麼用。本文所有的代碼請參看本人的github地址https://github.com/Rynxiao/immutable-learn

一、什麼是Immutable collections

Immutable data cannot be changed once created . Persistent data presents a mutative API which does not update the data in-place, but instead always yields new updated data.

Immutable.js provides many Persistent Immutable data structures including: List, Stack, Map, OrderedMap, Set, OrderedSet and Record.

These data structures are highly efficient on modern JavaScript VMs .

Keywords:cannot be changedyields new updated dataefficient

Immutable數據結構一旦被創建就不會被修改,每次API的操作都會在此數據之上另外返回一個新的數據。同時他自身的API中提供了很多我們平時在工作中可能用到的數據結構,例如:List,Stack,Map…。

二、基本使用

2.1 npm方式
npm install immutable

調用:

const { Map } = require('immutable');
const map = Map({ a: 1, b: 2, c: 3 });
map.get('a');       // 1
2.2 瀏覽器方式

下載immutable.min.js,放在自己項目庫文件中,然後引用:

<script src="immutable.min.js"></script>
<script>
    var map = Immutable.Map({ a: 1, b: 2, c: 3 });
    map.get('a');       // 1
</script>

三、API

3.1 Collection

Immutable中的Collection是一個基類,放在後端語言Java中來說就是一個抽象類,其中定義了很多方法,提供給子類來實現。因此Collection不能直接使用其構造函數。其中它又分成了幾個子類,分別是 Collection.Keyed,Collection.Indexed, or Collection.SetList/Map/Set/Stack分別都是繼承他們而來。

提供的基本操作 (例如:get/set/update/map/filter之類的) 這裏將不會被講到,想要了解的可以具體去官網看API。

這裏講下equalshashCode方法,這在javascript的幾種數據中都不存在,是Immutable數據中特有的兩個方法,用來判斷兩個數據的是否相等。這裏強調了”值”的概念。在Immutable中,所有的數據都是以values(值)的方式體現的。如果一個數據結構中,equalshashCode方法返回的值相同,那麼Immutable即認爲它們值相等。這也是在Immutable中的is方法中有體現。

Also, an important relationship between these methods must be upheld: if two values are equal, they must return the same hashCode. If the values are not equal, they might have the same hashCode; this is called a hash collision,

看到下面幾行:

// src/is.js
export function is(valueA, valueB) {
    //...
    return !!(isValueObject(valueA) && isValueObject(valueB) && valueA.equals(valueB));
}

// src/Predicates.js
export function isValueObject(maybeValue) {
    return !!(maybeValue && 
              typeof maybeValue.equals === 'function' && 
              typeof maybeVaule.hashCode === 'function');
}

// src/CollectionImpl.js 315行
equals(other) {
    return deepEqual(this, other);
}

// src/utils/deepEqual.js
export default function deepEqual(a, b) {
    // 這裏的算法略,如果感興趣看是如何比較的可以自己去看
    // 這裏主要看一個關鍵詞
    // ...
    if (
        !isCollection(b) ||
        (a.size !== undefined && b.size !== undefined && a.size !== b.size) ||
        (a.__hash !== undefined && b.__hash !== undefined && a.__hash !== b.__hash) 
        // ...
    ) {
        return false;
    }
    // 這裏看到 __hash 這個屬性從哪來,因此我們這回去看
}

// src/CollectionImpl.js 532行
hashCode() {
    return this.__hash || (this.__hash = hashCollection(this));
}

所以,這裏暴露了一些信息:使用is函數,需要比較hash值是否相等,那麼用到hash值就必須調用hashCode函數,然後再進行具體值得比較,就會調用equals方法。

下面,我們來看幾個is方法的例子:

const Immutable = require('./lib/immutable.js');

let a = 1;
let b = '1';
let c = 1;
let d = { a: 1 };
let e = { a: 1 };
let f = NaN;
let g = NaN;
let h = function() { console.log('h'); }
let i = function() { console.log('h'); }
let j = 0;
let k = -0;
let l = Immutable.Map({ a: 1 });
let m = Immutable.Map({ a: 1 });
let n = {
    a: 1, 
    hashCode: function() {
        return Immutable.hash('immutable');
    },
    equals: function() {
        return true;
    }
};
let o = {
    a: 1, 
    hashCode: function() {
        return Immutable.hash('immutable');
    },
    equals: function() {
        return true;
    }
};

console.log(Immutable.is(a, b));  // false
console.log(Immutable.is(a, c));  // true
console.log(Immutable.is(d, e));  // false
console.log(Immutable.is(f, g));  // true
console.log(Immutable.is(h, i));  // false
console.log(Immutable.is(j, k));  // true
console.log(Immutable.is(l, m));  // true
console.log(Immutable.is(n, o));  // true

console.log(Immutable.isValueObject(n));  // true
console.log(Immutable.isImmutable(n));    // false
console.log(Immutable.isCollection(n));   // false

總結:

1.對於javascript中原始值的比較類似於 Object.is

需要注意的是:NaNImmutable.js中認爲是與自身相等的;+0和-0在Immutable.js中認爲相等

2.對於Immutable中的集合類型,統一作爲值比較。即當兩個集合的值相等的時候即爲相等

3.對於原始值對象,如果提供了hashCode以及equals方法,並且返回值相等,也會認爲他們是相等的

3.2 Hash

主要作用是自己要寫一個Immutable值對象的時候可能會用到,需要在hashCode方法中返回一個哈希值。

/**
 * hash(val)
 * hash接受一個參數,這個值是任意的,返回一個31位的整數
 * 作用:當使用is()函數比較時,通過返回相同的hash值來判斷兩個值是否相等
 * 技巧:equals函數返回true, hashCode函數返回相同的hash值來設計兩個值是否相等
 */

const Immutable = require('./lib/immutable.js');

let seed1 = 'seed';
let seed2 = { a: 1, b: 2 };
let seed3 = [1, 2, 3, 4];
let seed4 = [1, 2, 3];
let seed5 = Immutable.List([435, 235, 1]);

console.log(Immutable.hash(seed1));     // 3526257
console.log(Immutable.hash(seed1));     // 3526257
console.log(Immutable.hash(seed2));     // 1
console.log(Immutable.hash(seed3));     // 2
console.log(Immutable.hash(seed4));     // 3
console.log(Immutable.hash(seed5));     // -53036292
3.3 List

List繼承自Collection.Indexed,同時實現了Deque,能夠高效地從首部或者尾部添加或者刪除數據。基本操作與javascript Array類似。更多操作請參看List API

// javascript 數組
const plainArray = [1, 2, 3, 4];
const listFormPlainArray = Immutable.List(plainArray);

// iterator
const listFromIterator = Immutable.List(plainArray[Symbol.iterator]());

console.log(listFormPlainArray.toJS());     // [1, 2, 3, 4]
console.log(listFromIterator.toJS());       // [1, 2, 3, 4]

index值爲負數時,表示從尾部進行操作。

const oList = Immutable.List([0, 1, 2]);
const addFormLast = oList.set(-1, -1);
console.log(addFormLast.toJS());            // [0, 1, -1]

const deleteList1 = oList.delete(0);
console.log(deleteList1.toJS());            // [1, 2]

const deleteList2 = oList.delete(-1);
console.log(deleteList2.toJS());            // [0, 1]

List沒有明顯的’unset’(未被設置值)或者’undefined’(值設置爲undefined)數據的概念。在List#forEach中可以體現。

// unset & undefined
const originList = [1, 2, , 4];
const collectionList = Immutable.List(originList);

collectionList.forEach(function(v, i) {
    console.log(`${i} ${v}`);
    // 0 1
    // 1 2
    // 2 undefined
    // 3 4
});

originList.forEach(function(v, i) {
    console.log(`${i} ${v}`);
    // 0 1
    // 1 2
    // 3 4
});
3.4 Map

Map 繼承自 Collection.keyedMap是無序的,如果需要有序Map 請使用OrderedMap。更多操作請參看官網API Map API

Mapkey是任意的,甚至可以是NaN,注意key值的類型都是string,可以看以下例子。

const anyKeyMap = Immutable.Map();
console.log(anyKeyMap.set(key1, 'hello1').get(key1));   // hello1
console.log(anyKeyMap.set(key2, 'hello2').get(key2));   // hello2
console.log(anyKeyMap.set(key3, 'hello3').get(key3));   // hello3
console.log(anyKeyMap.set(key4, 'hello4').get(key4));   // hello4
console.log(anyKeyMap.set(key5, 'hello5').get(key5));   // hello5

// don't initial with a obj like this
// { NaN: 'hello' }
// Map<V>(obj: {[key: string]: V}): Map<string, V>
let key = NaN;
const initMap = Immutable.Map({ key: 'hello' });
console.log(initMap.get(key));      // undefined

如果需要在初始化Map的時候傳入初始值,那麼key值必須爲string類型,否則取到的值是undefined。看下面一個證明key值都是string的例子。

let obj = { 1: 'hello' };
console.log(Object.keys(obj));  // ['1']
console.log(obj['1']);          // hello
console.log(obj[1]);            // hello

const mapObj = Immutable.Map(obj);
console.log(mapObj.get("1"));   // hello
console.log(mapObj.get(1));     // undefined

下面主要講三個方法:

// update
// update([key, newVal,] callback)
// 1.傳入key值與回調改變值
// 2.傳入回調函數可以返回當前值
// 3.傳入key值與新設置的值以及回調函數,注意,如果新值與原來的值不相等,會返回當前值
const originMap = Immutable.Map({ 'key': 'value' });
const newMap1 = originMap.update('key', function(value) {
    return value + value;
});
const newMap2 = originMap.update(function(value) {
    return value;
});
const newMap3 = originMap.update('key1', 'one', function(value) {
    return value + value;
});
const newMap4 = originMap.update('key1', 'one', function(value) {
    return value;
});
console.log(newMap1.toJS());        // { key: 'valuevalue' }
console.log(newMap2.toJS());        // { key: 'value' }
console.log(newMap3.toJS());        // { key: 'value', key1: 'oneone' }
console.log(newMap4.toJS());        // { key: 'value' }

// merge
// 後面的值覆蓋前面的值
const one = Immutable.Map({ a: 10, b: 20, c: 30 });
const two = Immutable.Map({ a: 40, b: 60, c: 90, d: 100 });
const mergeMap1 = one.merge(two);
const mergeMap2 = two.merge(one);
console.log(mergeMap1.toJS());      // { a: 40, b: 60, c: 90, d: 100 }
console.log(mergeMap2.toJS());      // { a: 10, b: 20, c: 30, d: 100 } 

// mergeWith
const mergeWithMap = one.mergeWith(function(oldVal, newVal) {
    return oldVal / newVal;
}, two);
console.log(mergeWithMap.toJS());   // { a: 0.25, b: 0.3333333333333333, c: 0.3333333333333333, d: 100 }
3.5 Set

Set繼承自Collection.SetSet主要的一個特性就是值唯一。因此我們可以利用此特性去除重複值。看下面的例子:

const set = Immutable.Set([1, 2, 1, 4]);
console.log(set.toJS());        // [1, 2, 4]

// 去除list中的相同值
const list = Immutable.List([1, 2, 3, 4, 5, 3, 2, 9, 0]);
const setList = Immutable.Set(list);
console.log(list);          // List [ 1, 2, 3, 4, 5, 3, 2, 9, 0 ]
console.log(setList);       // Set { 1, 2, 3, 4, 5, 9, 0 }

既然繼承自Collection,那麼就會存在Collection中的一些方法。具體操作方法參看官網Set API

// fromKeys 
const originObj = { a: 1, b: 2, c: 3, d: 4, a: 5 };
const mapIterator = Immutable.Map(originObj)[Symbol.iterator]();
const iterator2 = [ ['key', 'value'], ['key1', 'value2'], ['key', 'value3'] ];
console.log(Immutable.Set.fromKeys(mapIterator));   // Set { "a", "b", "c", "d" }
console.log(Immutable.Set.fromKeys(iterator2));     // Set { "key", "key1" }
console.log(Immutable.Set.fromKeys(originObj));     // Set { "a", "b", "c", "d" }

// 交集
// intersect
const set1 = Immutable.Set(['a', 'b', 'c']);
const set2 = Immutable.Set(['a', 'c', 'd']);    
const intersected = Immutable.Set.intersect([set1, set2]);
console.log(intersected);   // Set { "a", "c" }

// 並集
// union
const unioned = Immutable.Set.union([set1, set2]);
console.log(unioned);       // Set { "a", "c", "d", "b" }

// add
const addSet = Immutable.Set([1, 2, 3, 4]);
const newSet = addSet.add(5);
console.log(newSet.toJS());     // [ 1, 2, 3, 4, 5 ]
3.6 Stack

Stack 繼承自 Collection.Indexed。在添加和刪除數據上有非常高的效率。操作總是從棧頂開始,提供的push/pop/peek方法只是因爲我們熟悉了這些API。不建議使用reverse() 效率不高。具體操作方法參看官網Stack API

const Immutable = require('./lib/immutable.js');

// peek
// similar to first
const stack = Immutable.Stack([1, 2, 3, 4]);
console.log(stack.peek());          // 1
console.log(stack.first());         // 1

// has
console.log(stack.has(2));          // true

// includes
// similar to contains
console.log(stack.includes(3));     // true

// last
console.log(stack.last());
3.7 Seq

Seq 繼承自 CollectionSeq是不可變的,一旦被創建就不可修改,由一些函數引起的變化將會返回一個新的SeqSeq的一個重要特性就是懶計算。只有當被調用時纔會開始計算。具體看以下例子:

const Immutable = require('./lib/immutable.js');

// 在未調用時並不會執行
// 不信可以將Seq換成List試試,會全部執行
const oddSquares = Immutable.Seq([1, 2, 3, 4, 5, 6, 7, 8]).filter(function(x) {
    console.log('filter', x);
    return x % 2 !== 0;
}).map(function(x) {
    console.log('map', x);
    return x * x;
});

// filter 1
// map 1
// 1

console.log(oddSquares.get(0));     // 調用發現,filter中只執行一次,map中也執行了一次
3.8 其他
3.8.1 fromJS

fromJS(val[, callback(key, value, path)])

fromJS有兩個參數,其中回調函數可選,作用是將原始值類型轉換爲Immutable的集合。如果不提供回調,默認的轉換行爲是:Array -> Lists, Object -> Maps

const Immutable = require('./lib/immutable.js');

let obj = { a: { b: [10, 20, 30] }, c: 40 };

let iObj = Immutable.fromJS(obj, function(key, value, path) {
    let isIdxed = Immutable.isIndexed(value);
    console.log(key, value, path, isIdxed);
    return isIdxed ? value.toList() : value.toOrderedMap();
});

/**
 * b Seq [ 10, 20, 30 ] [ 'a', 'b' ] true
 * a Seq { "b": List [ 10, 20, 30 ] } [ 'a' ] false
 * b Seq [ 10, 20, 30 ] [ 'a', 'b' ] true
 *   Seq { "a": OrderedMap { "b": List [ 10, 20, 30 ] }, "c": 40 } [] false
 * b Seq [ 10, 20, 30 ] [ 'a', 'b' ] true
 * a Seq { "b": List [ 10, 20, 30 ] } [ 'a' ] false
 * b Seq [ 10, 20, 30 ] [ 'a', 'b' ] true
 */

console.log(Immutable.isCollection(iObj));  // true
console.log(Immutable.isCollection(obj));   // false
3.8.2 Range 區間選擇器

Range([start, end, step])

返回一個區間的List,若step有值,則在此區間上按照step來劃分值,默認值:start=1, end=infinity, step=1,if start === end returns []

const Immutable = require('./lib/immutable.js');

console.log(Immutable.Range());             // Range [ 0...Infinity ]
console.log(Immutable.Range(10));           // Range [ 10...Infinity ]
console.log(Immutable.Range(10, 30, 5));    // Range [ 10...30 by 5 ]
console.log(Immutable.Range(10, 10));       // Range []

console.log(Immutable.isImmutable(Immutable.Range()));  // true
3.8.3 Record 記錄時光機

Record(defaultVal[, description])

  • Record必須要有默認值,如果不傳直接報錯,如果傳值爲空對象,後續任何操作將會無效
  • isRecord方法用來判斷當前對象是否是Record的一個實例
  • 多次remove掉的記錄會變爲初始值,之後刪除多次將會變得無效
  • Record可以添加描述
  • Record可以被繼承,可以添加自己的方法賦予更多功能
const Immutable = require('./lib/immutable.js');

const DefaultRecord = Immutable.Record({ a: 1, b: 2 }); 
const RewriteRecord = new DefaultRecord({ b: 3 });

console.log(Immutable.Record.isRecord(DefaultRecord));      // false
console.log(Immutable.Record.isRecord(RewriteRecord));      // true

const ReRewriteRecord = new DefaultRecord({ b: 4 });

console.log(ReRewriteRecord.get('a'));                  // 1
console.log(ReRewriteRecord.get('b'));                  // 4

const removeRecord = ReRewriteRecord.remove('b');

console.log(removeRecord.get('b'));                     // 2

const reRemoveRecord = removeRecord.remove('b');

console.log(reRemoveRecord.get('b'));                   // 2

// getDescriptiveName()

const Person = Immutable.Record({ name: null }, 'Person');
const me = Person({ name: 'Ryn' });
console.log(me.toString());                             // Person { name: "Ryn" }
console.log(Immutable.Record.getDescriptiveName(me));   // Person

// no-default

const NoDefaultRecord = Immutable.Record({});
const writeRecord = new NoDefaultRecord({ a: 1 });
console.log(writeRecord.get('a'));                      // undefined

// extends

class ClassRecord extends Immutable.Record({ a: 1, b: 2 }) {
    getSum() {
        return this.a + this.b;
    }
}

const myClassRecord = new ClassRecord({ b: 3 });
console.log(myClassRecord.getSum());
3.8.4 Repeat

Repeat(val[, times])

const Immutable = require('./lib/immutable.js');

console.log(Immutable.Repeat('hello'));         // Repeat [ hello Infinity times ]
console.log(Immutable.Repeat('hello', 4));      // Repeat [ hello 4 times ] 

四、總結

Javascript中對象都是參考類型,也就是a = { a: 1 }; b = a; b.a = 10;你發現a.a也變成10了。可變的好處是節省內存或是利用可變性做一些事情,但是,在複雜的開發中它的副作用遠比好處大的多。於是纔有了淺copy和深copy,就是爲了解決這個問題。Immutable.js的應用主要是在其不變性上,這對於層次比較深的值比較、拷貝上面將會變得十分有用處。

發佈了60 篇原創文章 · 獲贊 29 · 訪問量 13萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章