JS系列2-怎麼把一個物件當做陣列使用

怎麼把一個物件當做陣列使用?

我們知道在JS中物件和陣列的操作方式是不一樣的,但是我們可以通過封裝,給物件加一層包裝器,讓它可以和陣列擁有同樣的使用方式。我們主要藉助Object.keys()、Object.values()、Object.entries()、Proxy。

Object.keys

看一下MDN上的解釋:

Object.keys() 方法會返回一個由一個給定物件的自身可列舉屬性組成的陣列,陣列中屬性名的排列順序和正常迴圈遍歷該物件時返回的順序一致。

也就是Object.keys可以獲取物件的所有屬性名,並生成一個陣列。

var obj = { a: 0, b: 1, c: 2 }; console.log(Object.keys(obj)); // console: ['a', 'b', 'c']

Object.values

看一下MDN上的解釋:

Object.values()方法返回一個給定物件自身的所有可列舉屬性值的陣列,值的順序與使用for...in迴圈的順序相同 ( 區別在於 for-in 迴圈列舉原型鏈中的屬性 )。

Object.values()返回一個陣列,元素是物件上找到的可列舉屬性值。

var obj = { foo: 'bar', baz: 42 }; console.log(Object.values(obj)); // ['bar', 42]

Object.entries

看一下MDN上的解釋:

Object.entries()方法返回一個給定物件自身可列舉屬性的鍵值對陣列,其排列與使用 for...in 迴圈遍歷該物件時返回的順序一致(區別在於 for-in 迴圈還會列舉原型鏈中的屬性)。

Object.entries()返回一個陣列,元素是由屬性名和屬性值組成的陣列。

const obj = { foo: 'bar', baz: 42 }; console.log(Object.entries(obj)); // [ ['foo', 'bar'], ['baz', 42] ]

Proxy

Proxy是JS最新的物件代理方式,用於建立一個物件的代理,從而實現基本操作的攔截和自定義(如屬性查詢、賦值、列舉、函式呼叫等)。

使用Proxy可以封裝物件的原始操作,在執行物件操作的時候,會經過Proxy的處理,這樣我們就可以實現陣列操作命令。

基礎 get 示例

const handler = {   get: function(obj, prop) {     return prop in obj ? obj[prop] : 37;   } } const p = new Proxy({}, handler); p.a = 1; p.b = undefined; console.log(p.a, p.b) console.log('c' in p, p.c)

以上示例的中,當物件中不存在屬性名時,預設返回值為37

無操作轉發代理

使用Proxy包裝原生物件生成一個代理物件p,對代理物件的操作會轉發到原生物件上。

let target = {}; let p = new Proxy(target, {}); p.a = 37;   // 操作轉發到目標 console.log(target.a);    // 37. 操作已經被正確地轉發

我們要實現以下幾個函式:forEach、map、filter、reduce、slice、find、findKey、includes、keyOf、lastKeyOf。

實現陣列函式

forEach

陣列中的forEach函式定義:arr.forEach(callback(currentValue [, index [, array]])[, thisArg])

陣列中的forEach需要傳入一個函式,函式的第一個引數是當前操作的元素值,第二個引數是當前操作的元素索引,第三個引數是正在操作的物件。

對於物件,我們將引數定為:currentValue、key、target。我們可以使用Object.keys來遍歷物件。

Object.keys(target).forEach(key => callback(target[key], key, target))

這裡需要target和callback引數,我們通過函式封裝一下

function forEach(target, callback) {   Object.keys(target).forEach(key => callback(target[key], key, target)) }

這樣我們就可以使用以下方式呼叫:

const a = {a: 1, b: 2, c: 3} forEach(a, (v, k) => console.log(`${k}-${v}`)) // a-1 // b-2 // c-3

通過Proxy封裝:

const handler = {   get: function(obj, prop) {     return forEach(obj)   } } const p = new Proxy(a, handler) p.forEach((v, k) => console.log(`${k}-${v}`))

以上方式當然是不行的,我們主要看最後一句,其執行方式和陣列的forEach完全相同,我們在呼叫Proxy封裝的物件時,獲取資料時,會呼叫get函式,第一個引數為原生物件,第二個引數為屬性名-forEach,在這裡就要修改我們的forEach函式了。首先p.forEach的引數是一個函式,因此我們代理物件的返回值需要接收一個函式作為引數,因此修改如下:

function forEach(target) {   return (callback) => Object.keys(target).forEach(key => callback(target[key], key, target)) }

因此完成程式碼為:

function forEach(target) {   return (callback) => Object.keys(target).forEach(key => callback(target[key], key, target)) } const handler = {   get: function(obj, prop) {     return forEach(obj)   } } const a = {a: 1, b: 2, c: 3} const p = new Proxy(a, handler) p.forEach((v, k) => console.log(`${k}-${v}`)) // a-1 // b-2 // c-3

我們應該把以上程式碼封裝為模組,方便對外使用:

const toKeyedArray = (obj) => {   const methods = {     forEach(target) {       return (callback) => Object.keys(target).forEach(key => callback(target[key], key, target));     }   }   const methodKeys = Object.keys(methods)   const handler = {     get(target, prop) {       if (methodKeys.includes(prop)) {         return methods[prop](target)       }       return Reflect.get(...arguments)     }   }   return new Proxy(obj, handler) } const a = { a: 1, b: 2, c: 3} const p = toKeyedArray(a) p.forEach((v, k) => console.log(`${k}-${v}`))

以上是forEach的實現和封裝,其他函式的實現方式類似。

全部原始碼如下:

const toKeyedArray = (obj) => {   const methods = {     forEach(target) {       return (callback) => Object.keys(target).forEach(key => callback(target[key], key, target));     },     map(target) {       return (callback) =>         Object.keys(target).map(key => callback(target[key], key, target));     },     reduce(target) {       return (callback, accumulator) =>         Object.keys(target).reduce(           (acc, key) => callback(acc, target[key], key, target),           accumulator         );     },     forEach(target) {       return callback =>         Object.keys(target).forEach(key => callback(target[key], key, target));     },     filter(target) {       return callback =>         Object.keys(target).reduce((acc, key) => {           if (callback(target[key], key, target)) acc[key] = target[key];           return acc;         }, {});     },     slice(target) {       return (start, end) => Object.values(target).slice(start, end);     },     find(target) {       return callback => {         return (Object.entries(target).find(([key, value]) =>           callback(value, key, target)         ) || [])[0];       };     },     findKey(target) {       return callback =>         Object.keys(target).find(key => callback(target[key], key, target));     },     includes(target) {       return val => Object.values(target).includes(val);     },     keyOf(target) {       return value =>         Object.keys(target).find(key => target[key] === value) || null;     },     lastKeyOf(target) {       return value =>         Object.keys(target)           .reverse()           .find(key => target[key] === value) || null;     }   }   const methodKeys = Object.keys(methods)   const handler = {     get(target, prop) {       if (methodKeys.includes(prop)) {         return methods[prop](target)       }       const [keys, values] = [Object.keys(target), Object.values(target)];       if (prop === 'length') return keys.length;       if (prop === 'keys') return keys;       if (prop === 'values') return values;       if (prop === Symbol.iterator)         return function* () {           for (value of values) yield value;           return;         };       return Reflect.get(...arguments)     }   }   return new Proxy(obj, handler) } const x = toKeyedArray({ a: 'A', b: 'B' }); x.a;          // 'A' x.keys;       // ['a', 'b'] x.values;     // ['A', 'B'] [...x];       // ['A', 'B'] x.length;     // 2 // Inserting values x.c = 'c';    // x = { a: 'A', b: 'B', c: 'c' } x.length;     // 3 // Array methods x.forEach((v, i) => console.log(`${i}: ${v}`)); // LOGS: 'a: A', 'b: B', 'c: c' x.map((v, i) => i   v);                         // ['aA', 'bB, 'cc] x.filter((v, i) => v !== 'B');                  // { a: 'A', c: 'c' } x.reduce((a, v, i) => ({ ...a, [v]: i }), {});  // { A: 'a', B: 'b', c: 'c' } x.slice(0, 2);                                  // ['A', 'B'] x.slice(-1);                                    // ['c'] x.find((v, i) => v === i);                      // 'c' x.findKey((v, i) => v === 'B');                 // 'b' x.includes('c');                                // true x.includes('d');                                // false x.keyOf('B');                                   // 'b' x.keyOf('a');                                   // null x.lastKeyOf('c');                               // 'c'


JS系列1-布林陷阱以及如何避免