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'