從babel的編譯結果學習ES6的擴充套件運算子

語言: CN / TW / HK

前言

ES6作為ES的一個劃時代的版本,使得JS這門語言在編寫大型且健壯的應用程式更進一步。ES6主要增加了很多語法糖,這些語法糖為我們的開發提效,減少實際開發中的bug功不可沒。但是,這些語法糖是如何化腐朽為神奇的,其底層到底是怎麼工作的呢,我覺得對於一個有追求的前端程式設計師來說還是有必要搞懂JS引擎到底為我們做了多少工作,做到,知其然,也知其所以然。可以訓練我們在實際的專案開發中快速定位問題的能力。本文主要闡述...擴充套件運算子)在實際開發中的應用場景,以及分析對應場景的編譯結果,闡述擴充套件運算子的執行原理。

2、常見用法及編輯結果分析

2.1 物件屬性處理

取出某些鍵,將剩餘的鍵收集到一個物件中,在實際開發中,可能你不想處理某些鍵(有點兒類似lodashomit等操作)。或者說,需要對鍵值,需要分開處理的場景。 js const obj = { a: 1, b: 2, c: 3 }; const { a, ...rest } = obj; console.log(a, obj); 編譯後的結果: js /** * 移除source物件中包含exclude的鍵(包含Symbol) * @param {Object} source 源物件 * @param {Object} excluded 用來匹配需要刪除的鍵的物件 * @returns {Object} */ function _objectWithoutProperties(source, excluded) { if (source == null) return {}; var target = _objectWithoutPropertiesLoose(source, excluded); var key, i; // 如果當前環境支援Symbol,Symbol型別的key也可以拷貝,但是對於不能列舉的Symbol屬性則不拷貝 if (Object.getOwnPropertySymbols) { var sourceSymbolKeys = Object.getOwnPropertySymbols(source); for (i = 0; i < sourceSymbolKeys.length; i++) { key = sourceSymbolKeys[i]; if (excluded.indexOf(key) >= 0) continue; // 不處理不可列舉的Symbol if (!Object.prototype.propertyIsEnumerable.call(source, key)) continue; target[key] = source[key]; } } return target; } /** * 寬鬆的移除source物件中包含exclude的鍵 * @param {Object} source 源物件 * @param {Object} excluded 用來匹配需要刪除的鍵的物件 * @returns {Object} */ function _objectWithoutPropertiesLoose(source, excluded) { if (source == null) return {}; var target = {}; // 不對當前物件原型上的屬性進行處理 var sourceKeys = Object.keys(source); var key, i; for (i = 0; i < sourceKeys.length; i++) { key = sourceKeys[i]; if (excluded.indexOf(key) >= 0) continue; target[key] = source[key]; } return target; } var obj = { a: 1, b: 2, c: 3, }; var a = obj.a, rest = _objectWithoutProperties(obj, ["a"]); console.log(a, obj); 通過分析這個編譯結果,我驚訝的發現,在物件做展開收集的時候,竟然可以能把Symbol也能處理到,學到了呀。

2.2 多個物件合併

這種場景,我個人認為是Object.assign在物件合併時的簡化寫法。 js const obj1 = { a: 1, b: 2, c: 3, }; const obj2 = { a: "xxx", d: "ddd", k: 2, }; const newObj = { ...obj1, ...obj2, }; console.log(newObj); 編譯後: ```js "use strict"; / * 獲取一個物件上包含Symbol在內的所有key * @param {Object} object 目標物件 * @param {boolean} enumerableOnly 在獲取Symbol型別的key時,是否只獲取可列舉的 * @returns */ function ownKeys(object, enumerableOnly) { var keys = Object.keys(object); if (Object.getOwnPropertySymbols) { var symbols = Object.getOwnPropertySymbols(object); // 過濾掉Symbol的key中不可列舉的 enumerableOnly && (symbols = symbols.filter(function (sym) { return Object.getOwnPropertyDescriptor(object, sym).enumerable; })), keys.push.apply(keys, symbols); } return keys; } / * 物件展開,將除了target以外的所有key全部合併到target中 * @param {Object} target * @returns / function _objectSpread(target) { // 從第一個引數開始,分別對後面的引數進行處理 for (var i = 1; i < arguments.length; i++) { var source = null != arguments[i] ? arguments[i] : {}; / 這段是babel編譯的結果,但是看起來不是那麼直觀,也於是決定對其進行改寫 / // i % 2 // ? ownKeys(Object(source), !0).forEach(function (key) { // _defineProperty(target, key, source[key]); // }) // : Object.getOwnPropertyDescriptors // ? Object.defineProperties(target, Object.getOwnPropertyDescriptors(source)) // : ownKeys(Object(source)).forEach(function (key) { // Object.defineProperty(target, key, Object.getOwnPropertyDescriptor(source, key)); // }); / 為什麼對 序號為偶數的key才去單獨處理,沒看懂這樣的意義是什麼? / if (i % 2) { ownKeys(Object(source), !0).forEach(function (key) { _defineProperty(target, key, source[key]); }); } else { if (Object.getOwnPropertyDescriptors) { Object.defineProperties(target, Object.getOwnPropertyDescriptors(source)); } else { ownKeys(Object(source)).forEach(function (key) { Object.defineProperty(target, key, Object.getOwnPropertyDescriptor(source, key)); }); } } } return target; } / * 以相容的方式定義物件的屬性和值 * @param {object} obj * @param {string} key * @param {any} value * @returns / function _defineProperty(obj, key, value) { if (key in obj) { Object.defineProperty(obj, key, { value: value, enumerable: true, configurable: true, writable: true }); } else { obj[key] = value; } return obj; }

var obj1 = { a: 1, b: 2, c: 3, }; var obj2 = { a: "xxx", d: "ddd", k: 2, }; var newObj = _objectSpread(_objectSpread({}, obj1), obj2); console.log(newObj); `` 其中_objectSpread輔助函式完成的功能則和Object.assign相同,也證明了我之前的觀點。因此,在實際開發中這個場景下的注意點就變成了對Object.assign的使用了,用...顯然更簡潔。但是上述程式碼有個地方沒太看懂,為什麼要對序號為偶數的key只獲取可列舉的Symbol`,出於效能考慮嗎?目前還不得而知。

2.3 淺拷貝

js const obj = { a: 1, b: {}, }; const copy = { ...obj }; 編譯後的程式碼和多個物件合併相似,此處不再贅述。

2.4 數組合並

js const arr = [1, 2, 3]; const b = [3, ...arr]; console.log(b); 編譯後: js var arr = [1, 2, 3]; var b = [3].concat(arr); console.log(b); 數組合並利用的是concat,是真的簡潔啊,也沒有什麼好說的。

2.5 簡化函式引數傳遞

對於某些函式接受一堆引數,如: ts Math.min(...values: number[]): number; 或者不想或者不能(嚴格模式,或者箭頭函式場景)使用arguments獲取引數。 ```js Math.min(...arr);

function D(...args) { console.log(args); }

function E(A, B, ...others) { console.log(A, B, others); } 編譯後:js "use strict"; function _toConsumableArray(arr) { return _arrayWithoutHoles(arr) || _iterableToArray(arr) || _unsupportedIterableToArray(arr) || _nonIterableSpread(); } / * 對不含[Symbol.iterator]介面呼叫時報錯 */ function _nonIterableSpread() { throw new TypeError("Invalid attempt to spread non-iterable instance.\nIn order to be iterable, non-array objects must have a Symbol.iterator method."); } / * 將其它型別轉化成陣列 * @param {any} o * @param {number} minLen * @returns / function _unsupportedIterableToArray(o, minLen) { if (!o) return; if (typeof o === "string") return _arrayLikeToArray(o, minLen); var n = Object.prototype.toString.call(o).slice(8, -1); if (n === "Object" && o.constructor) n = o.constructor.name; if (n === "Map" || n === "Set") return Array.from(o); // 位元組陣列或者arguments物件 if (n === "Arguments" || /^(?:Ui|I)nt(?:8|16|32)(?:Clamped)?Array$/.test(n)) return _arrayLikeToArray(o, minLen); / 需要注意的是,這兒對於一個普通物件是沒有任何操作的喲 / } / * 把包含Symbol.iterator介面的物件轉成陣列 * @param {{ [Symbol.iterator]: any }} iter * @returns / function _iterableToArray(iter) { if ((typeof Symbol !== "undefined" && iter[Symbol.iterator] != null) || iter["@@iterator"] != null) { return Array.from(iter); } } / * 將陣列充滿,比如這類陣列 new Array(100),但是如果直接用forEach遍歷,會跳過100個元素,經過這個操作這後會變成100個undefined,相當於填滿了陣列上的洞 * @param {Array} arr * @returns */ function _arrayWithoutHoles(arr) { if (Array.isArray(arr)) { return _arrayLikeToArray(arr); } } / * 將類陣列物件轉成陣列 * @param {any} arr 類陣列物件 * @param {number} len 類陣列物件的長度 * @returns */ function _arrayLikeToArray(arr, len) { if (len == null || len > arr.length) len = arr.length; for (var i = 0, arr2 = new Array(len); i < len; i++) { arr2[i] = arr[i]; } return arr2; } Math.min.apply(Math, _toConsumableArray(arr)); function D() { for (var _len = arguments.length, args = new Array(_len), _key = 0; _key < _len; _key++) { args[_key] = arguments[_key]; } console.log(args); }

function E(A, B) { for (var _len2 = arguments.length, others = new Array(_len2 > 2 ? _len2 - 2 : 0), _key2 = 2; _key2 < _len2; _key2++) { others[_key2 - 2] = arguments[_key2]; }

console.log(A, B, others); } ```

2.6 類陣列物件,Map,Set,String的處理。

js const str = "23333"; const chars = [...str]; [...new Set([1, 1, 2])]; [...new Map()]; [...document.querySelectorAll("div")]; const func = function () { const args = [...arguments]; console.log(args); }; 編譯後,都呼叫上述的_toConsumableArray輔助函式,此處不再贅述。

總結

擴充套件運算子的用法主要分三類: - 1、物件處理: 主要是對於物件的屬性進行操作,提取,合併等操作。對於採用擴充套件運算子進行淺克隆,底層有類似於Object.assign的邏輯,因此實際開發中這類場景可以多采用...用以簡化程式碼。 - 2、陣列處理 主要是通過concat操作,並返回一個新陣列。 - 3、對含有Symbol.iterator物件轉陣列的處理。 主要是通過使用Array.from將其轉化為陣列,有興趣的讀者可以參看一下Array.frompolyfill,或者某些場景下使用ES5及以前的Array.prototype.slice,若不含有Symbol.iterator是不能使用擴充套件運算子進行展開的。這兒有一個易錯點。 在React中,我們很有可能會看到這種程式碼 jsx class MyButton extends Component { render() { const props = { ...this.props, name: '我自己封裝的Button'}; return <Button {...props} /> } } 這個擴充套件運算子對物件的props進行批量傳遞,是React生態自己的支援(相當於是人家多了一個外掛去轉碼這個語法,而原生JS是不支援這種語法的)。各位讀者需要注意一下區別。

另外: ts interface Console { // 已省略無關程式碼 log(...data: any[]): void; } 看起來,好像跟我們的之前Math.min函式引數的定義形式差不多,那我們是否就可以console.log(...obj)了呢?

答案是不可以的,因為Object是沒有定義Symbol.iterator的,如果你執意這樣做,我們可以借用一下ArraySymbol.iterator,即: js const obj = { a: 1, b: 2, c: 3}; obj[Symbol.iterator] = Array.prototype[Symbol.iterator]; console.log(...obj); 同樣,這類操作擴充套件運算子還是相對簡潔的。

綜上所述,在合理的場景下,我們可以儘量的多用擴充套件運算子以簡化我們的程式碼(有些讀者可能會說,babel在轉化的過程中定義了這麼多輔助函式,程式碼量會好多好多,這個不比擔心,在生產環境下,我們都會用它的@babel/plugin-transform-runtime外掛,將這些輔助函式全部抽離到一個包裡面去,這樣就不用擔心程式碼的重複定義的問題了),這樣可讀性也更好了,後來的開發者維護起來也不用那麼頭疼了。

由於筆者水平有限,寫作過程中難免出現錯誤,若有紕漏,請各位讀者指正,請聯絡作者本人,郵箱[email protected],你們的意見將會幫助我更好的進步。本文乃作者原創,若轉載請聯絡作者本人。