少年,你渴望超程式設計的力量嗎?——symbol

lhyt發表於2019-03-22

超程式設計的概念有很多文章,通過操作更加底層的api做更多個性化的功能。一句話概括,就是用程式碼來寫程式碼

一些時候,寫各種下劃線、前字尾,為了實現一個唯一值或者祕密的特殊輔助值,用來輔助業務邏輯或者說作為一個私有的東西:

const onlyone = 'im-only@@我就不信會重複';
const obj = {};
obj[onlyone] = 'xxx';

複製程式碼

angular1暴露出來的物件裡面,經常看見$開頭或者$$開頭的變數。$開頭是比較底層的變數了,$$開頭的是更加底層的,而且我們是基本不會用上的。還有redux原始碼,首次初始化的action.type是var type = '@@redux/PROBE_UNKNOWN_ACTION_' + Math.random().toString(36).substring(7).split('').join('.');

種種例子,都可以看見,搞個花哨的名字,作為內部執行的輔助變數或者唯一變數使用。花時間想key名字,而且名字越寫越長,是不是總感覺是一種麻煩?

更重要的,物件如果暴露出去了,那人家就可以看見key值,也可以直接修改了

symbol做唯一key

const only = Symbol();
const obj = {};
obj[only] = 'xxx';
複製程式碼

此時,只有定義了symbol的作用域之下才能用到它了,暴露物件出去,就算在console裡面給人家看見一個symbol,想修改它或者讀取它也無能為力。甚至還可以有obj['Symbol()'] = 1這種操作:

在react列印出來的元件物件裡面,也可以看見一些symbol的屬性。

image

此外,symbol是不可列舉的:

const only = Symbol();
const obj = { a: 1, b: 2 };
obj[only] = 'xxx';
Object.keys(obj);
複製程式碼

同樣,JSON.stringify、for in等等也是會把symbol跳過了。

但是,想像其他變數那樣子用怎麼辦,也就是以不同姿勢使用同樣的方法,期望得到同樣的值,期望近似與symbol('a')symbol('a')是完全相等這種效果

這時候,Symbol.for剛剛好滿足需求了:

const a = Symbol.for('im not alone')
const b = Symbol.for('im not alone')
a === b
複製程式碼

這下symbol就是有點簡單資料型別的感覺了吧

Symbol.iterator

這個屬性,是一個當前物件預設的遍歷器生成函式,所以我們用obj[Symbol.iterator]可以訪問到它。自定義Symbol.iterator會覆蓋for of遍歷、...操作符。實現一個偽陣列,主要就是要把這個函式設定好就可以了:

// 方法1
var fakeArr = {}
fakeArr[Symbol.iterator] = function* () {
    yield 1;
    yield 2;
    yield 3;
};
[...fakeArr] // [1,2,3]

// 方法2
var arr = {
  0: 1,
  1: 2,
  2: 3,
  length: 3,
  [Symbol.iterator]: Array.prototype[Symbol.iterator]
};
[...arr] // [1,2,3]

// 方法3
var arr = {
  [Symbol.iterator]: (() => {
    var data = ['隨', '機', '試', '一', '下'];
    return () => {
      var cursor = 0;
      return {
        next: () => {
          if (cursor ++ < 10) {
            return {
              value: data[~~(5 * Math.random())],
              done: false
            }
          }
          return {
            done: true 
          }
        } 
      }
    }
  })()
};
// 這下就不用自己搞一個隨機陣列生成器了
[...arr];


// 自定義的遍歷物件
var o = {
  a: 1,
  b: 2,
  hidden: 'u cant find me',
  [Symbol.iterator]: () => {
    var cursor = 0;
    return {
      next() {
        if (cursor < Object.keys(o).length - 1) {
          return {
            value: Object.keys(o)[cursor ++],
            done: false,
          }
        }
        return { done: true };
      }
    }
  }
}
[...o]; // ['a', 'b']
複製程式碼

為所欲為的遍歷,可以在中間加上其他邏輯。在業務上,對於一系列規定的流程但不一定相同但結果這種邏輯,我們就可以用上它。什麼上報、條件判斷、校驗都可以做

// 條件遍歷
// 有一個按鈕陣列是這樣的[{ renderer, style,label }...],傳入物件展示按鈕
// 正常情況我們直接寫在陣列裡面沒問題
// 如果複雜一點,按照條件展示按鈕,我們就要在外面再寫其他邏輯
// 如果使用iterator,可以優雅簡化這個過程
var type = 'btn1';
var Btns = {
  btn1: {
    renderer() {
      return 1
    },
    style: {
      height: '10px'
    }
  },
  btn2: {
    renderer() {
      return 2
    },
  },
  btn3: {
    renderer() {
      return 3
    },
    label: {
      name: 'hello'
    }
  },
  *[Symbol.iterator]() {
    switch (type) {
      case 'btn1':
        yield this.btn1
        break;
    
      default:
        yield this.btn2
        break;
    }
    yield this.btn3
  }
};
console.log([...Btns]);
type = 'other';
console.log([...Btns]);
複製程式碼

可以試一下,結果是不一樣的。這樣子,我們就可以完全不用在外面關心條件遍歷,也不用在多個地方遍歷的時候寫邏輯,這個已經在物件的內部實現

Symbol.toPrimitive

另外一篇文章已經講過型別轉換關係,複雜型別隱式轉換會先toPrimitive轉回基本型別。而Symbol也有這種操作到更底層的方法:Symbol.toPrimitive,可以自定義

// 不玩包裝類的string
var s = new String('');
s[Symbol.toPrimitive] = () => { return 'this is magic' };
`${s}這不是空字串`; // "this is magic這不是空字串"

// 不想再看見[object Object]了
var o = {
  msg: '你不可以正常看見我的'
};
var _o = {
  msg: '你可以正常看見我的',
  [Symbol.toPrimitive]: () => JSON.stringify(_o)
};
`奇蹟不會發生: ${o}  奇蹟將會發生: ${_o}`;
複製程式碼

在隱式轉換中,自定義的Symbol.toPrimitive優先順序最高:

var transform = {
  valueOf() {
    return 'valueOf'
  },
  toString() {
    return 'tostring'
  },
  [Symbol.toPrimitive]() {
    return 'symbol'
  }
};
1+transform; //"1symbol"

// 永遠地告別[object Object]
Object.prototype[Symbol.toPrimitive] = function() {
  return JSON.stringify(this)
}
複製程式碼

正則相關

物件的Symbol.replace方法被String.prototype.replace呼叫時,會返回Symbol.replace的返回值。同理,match、search也差不多

// 一個具有merge功能的searchvalue
var str = new String('別看我好嗎')
str[Symbol.replace] = (oldStr, newStr) => [...new Set([...oldStr, ...newStr])].join('');
'1234'.replace(str, '3456'); // 123456

// 一個神奇的split
var magic = {
  [Symbol.split]: (() => {
    var mana = ['*', '&', '$', '#', '@'];
    var ran = () => mana[~~(Math.random() * 5)];
    return (origin) => {
      var res = '';
      var cursor = 0;
      while(cursor < origin.length) {
        var r = ran();
        res +=  r + origin[cursor ++] + r
      }
      return res;
    }
  })()
};
'你渴望力量嗎'.split(magic);
複製程式碼

正則的比較少用,而且也可以用簡單的封裝替代。

相關文章