Skip to content

Instantly share code, notes, and snippets.

@dSalieri
Last active August 13, 2023 03:40
Show Gist options
  • Save dSalieri/fda9cc93eebc807b3ba6b4575c67df44 to your computer and use it in GitHub Desktop.
Save dSalieri/fda9cc93eebc807b3ba6b4575c67df44 to your computer and use it in GitHub Desktop.
Создание итератора свойств объекта

Цель: Итерация по свойствам объекта с возможностью требований к свойствам.

Настройки: Принимает на вход два аргумента, первый это любой объект, свойства которого будут перечислены, второй аргумент это объект опций, но вместо объекта можно указать функцию, которая будет задавать правила по которому будет отсеивать свойства.

Номенклатура объекта опций:

{
  /// Данный режим влияет на то как будут интерпретироваться флаги, 
  /// match - это полное соответствие флагов к дескрипторам, 
  /// accessor - тогда значения флагов могут быть true или false, если true тогда тогда не важно какое значение имеет дескриптор свойства, если false тогда дескриптор с false или undefined не учитывается при перечислении
  mode: match/permission,
  /// Выбор по типу дескриптора
  type: both/data/accessor,
  /// К свойствам теперь добавляется [[Prototype]], true - прототип добавляется к перечислению и к раскрытию, false - выключен, iterate - только перечисляется, reveal - только раскрывается, only - перечисляются только прототипы
  proto: true/false/iterate/reveal/only,
  /// Раскрытие сложных типов
  reveal: {
    /// Режим раскрытия, exclude - исключающий, include - включающий (черный и белый списки)
    mode: exclude/include,
    /// Указываются типы в строковом типе
    list: [...]
  },
  /// Сортировка по значениям флагов, если какой-то из флагов не указывается, значит он не учитывается
  flags: {
    /// Эти свойства по-умолчанию не указываются (и считается что по всем свой свойствам идет совпадение)
    enumerable: any_value or true/false,
    writable: any_value or true/false,
    configurable: any_value or true/false,
    value: any_value or true/false,
    set: any_value or true/false,
    get: any_value or true/false,
  }
}

Если вам требуется другая логика и вас не устраивает тот набор опций то можете указать функцию, она имеет следующую номенклатуру:

/// this - объект с которого началось перечисление (объект-инициатор)
/// descriptor - дескриптор свойства итерируемого объекта (включает в себя тип дескриптора)
/// key - имя свойства итерируемого элемента
/// node - объект с узлами
/// node.parent - объект-родитель данному итерируемому элементу
/// node.current - текущий итерируемый объект, если не объект, тогда null
/// node.isVisited - если node.current объект и он ранее был посещен будет значение true, в противном случае false
/// proto - содержит вспомогательные флаги для определения прототипов
/// proto.is - является ли итерируемое на данный момент значение прототипом
/// proto.isIn - является ли итерируемое значение на данный момент внутри прототипа
/// trace - объект с трассировкой свойств объектов
/// trace.main - список свойств по которым нужно пройти чтобы попасть в текущий итерируемый элемент
/// trace.proto - список свойств по которым нужно пройти чтобы попасть в текущий итерируемый элемент, но начинается с прототипа
function ({descriptor, key, node, proto, trace}) {
  /// true - дескриптор допускается к перечислению и раскрытию
  /// false - дескриптор не допускается ни к перечислению ни к раскрытию
  /// iterate - дескриптор допускается только к перечислению
  /// reveal - дескриптор допускается только к раскрытию
  /// exit - полный выход из перечисления свойств объекта
  return true/false/iterate/reveal/exit;
}

Примечание: Модификация дескриптора и структур, которые обращаются к перечисляемой структуре заблокирована. Это сделано намерено чтобы уберечь от непредсказуемого поведения.

Для примеров я буду использовать следующую структуру объекта:

const obj = {
  a: 1,
  b: 2,
  c: 3,
  d: {
    value1: "text1",
    value2: "text2",
    value3: "text3",
  },
  get e() {
    return "getter value";
  },
  __proto__: {
    aaa: 100,
    bbb: 200,
    ccc: 300,
  }
}

Пример 1 (по-умолчанию, перечисляются свойства объекта, прототипы выключены, функции не раскрываются):

for (const item of createObjectIterator(obj)) console.log(item);
/// или
for (const item of createObjectIterator(obj, ({node, proto}) => {
    if (proto.is) return false;
    if (typeof node.current === "function") return "iterate";
    return true;
})) console.log(item);
/// Вывод:
/// {descriptor: Proxy(Object), key: 'a', node: {…}, proto: {…}, trace: {…}}
/// {descriptor: Proxy(Object), key: 'b', node: {…}, proto: {…}, trace: {…}}
/// {descriptor: Proxy(Object), key: 'c', node: {…}, proto: {…}, trace: {…}}
/// {descriptor: Proxy(Object), key: 'd', node: {…}, proto: {…}, trace: {…}}
/// {descriptor: Proxy(Object), key: 'value1', node: {…}, proto: {…}, trace: {…}}
/// {descriptor: Proxy(Object), key: 'value2', node: {…}, proto: {…}, trace: {…}}
/// {descriptor: Proxy(Object), key: 'value3', node: {…}, proto: {…}, trace: {…}}
/// {descriptor: Proxy(Object), key: 'e', node: {…}, proto: {…}, trace: {…}}

Пример 2 (перечисляются свойства объекта и его прототипы со всеми их свойствами, функции не раскрываются):

for (const item of createObjectIterator(obj, {proto: true})) console.log(item)
/// или
for (const item of createObjectIterator(obj, ({node}) => {
    if(typeof node.current === "function") return "iterate";
    return true;
})) console.log(item)
/// Вывод:
/// {descriptor: Proxy(Object), key: 'a', node: {…}, proto: {…}, trace: {…}}
/// {descriptor: Proxy(Object), key: 'b', node: {…}, proto: {…}, trace: {…}}
/// {descriptor: Proxy(Object), key: 'c', node: {…}, proto: {…}, trace: {…}}
/// {descriptor: Proxy(Object), key: 'd', node: {…}, proto: {…}, trace: {…}}
/// {descriptor: Proxy(Object), key: 'value1', node: {…}, proto: {…}, trace: {…}}
/// {descriptor: Proxy(Object), key: 'value2', node: {…}, proto: {…}, trace: {…}}
/// {descriptor: Proxy(Object), key: 'value3', node: {…}, proto: {…}, trace: {…}}
/// {descriptor: Proxy(Object), key: '[[Prototype]]', node: {…}, proto: {…}, trace: {…}}
/// {descriptor: Proxy(Object), key: 'constructor', node: {…}, proto: {…}, trace: {…}}
/// {descriptor: Proxy(Object), key: '__defineGetter__', node: {…}, proto: {…}, trace: {…}}
/// {descriptor: Proxy(Object), key: '__defineSetter__', node: {…}, proto: {…}, trace: {…}}
/// {descriptor: Proxy(Object), key: 'hasOwnProperty', node: {…}, proto: {…}, trace: {…}}
/// {descriptor: Proxy(Object), key: '__lookupGetter__', node: {…}, proto: {…}, trace: {…}}
/// {descriptor: Proxy(Object), key: '__lookupSetter__', node: {…}, proto: {…}, trace: {…}}
/// {descriptor: Proxy(Object), key: 'isPrototypeOf', node: {…}, proto: {…}, trace: {…}}
/// {descriptor: Proxy(Object), key: 'propertyIsEnumerable', node: {…}, proto: {…}, trace: {…}}
/// {descriptor: Proxy(Object), key: 'toString', node: {…}, proto: {…}, trace: {…}}
/// {descriptor: Proxy(Object), key: 'valueOf', node: {…}, proto: {…}, trace: {…}}
/// {descriptor: Proxy(Object), key: '__proto__', node: {…}, proto: {…}, trace: {…}}
/// {descriptor: Proxy(Object), key: 'toLocaleString', node: {…}, proto: {…}, trace: {…}}
/// {descriptor: Proxy(Object), key: '[[Prototype]]', node: {…}, proto: {…}, trace: {…}}
/// {descriptor: Proxy(Object), key: 'e', node: {…}, proto: {…}, trace: {…}}
/// {descriptor: Proxy(Object), key: '[[Prototype]]', node: {…}, proto: {…}, trace: {…}}
/// {descriptor: Proxy(Object), key: 'aaa', node: {…}, proto: {…}, trace: {…}}
/// {descriptor: Proxy(Object), key: 'bbb', node: {…}, proto: {…}, trace: {…}}
/// {descriptor: Proxy(Object), key: 'ccc', node: {…}, proto: {…}, trace: {…}}
/// {descriptor: Proxy(Object), key: '[[Prototype]]', node: {…}, proto: {…}, trace: {…}}

Пример 3 (перечисление как в for...in):

for (const item of createObjectIterator(obj, {
  mode: "match", 
  proto: "reveal", 
  reveal: {
    mode: "exclude", 
    list: []
  }, 
  flags:{
    enumerable: true
  }
})) console.log(item)
/// или
for (const item of createObjectIterator(obj, ({descriptor, node, proto}) => {
    if (proto.is) return "reveal";
    if (descriptor.enumerable) {
        if (typeof node.current === "function") return "iterate";
        return true;
    }
})) console.log(item)
/// Вывод
/// {descriptor: Proxy(Object), key: 'a', node: {…}, proto: {…}, trace: {…}}
/// {descriptor: Proxy(Object), key: 'b', node: {…}, proto: {…}, trace: {…}}
/// {descriptor: Proxy(Object), key: 'c', node: {…}, proto: {…}, trace: {…}}
/// {descriptor: Proxy(Object), key: 'd', node: {…}, proto: {…}, trace: {…}}
/// {descriptor: Proxy(Object), key: 'value1', node: {…}, proto: {…}, trace: {…}}
/// {descriptor: Proxy(Object), key: 'value2', node: {…}, proto: {…}, trace: {…}}
/// {descriptor: Proxy(Object), key: 'value3', node: {…}, proto: {…}, trace: {…}}
/// {descriptor: Proxy(Object), key: 'e', node: {…}, proto: {…}, trace: {…}}
/// {descriptor: Proxy(Object), key: 'aaa', node: {…}, proto: {…}, trace: {…}}
/// {descriptor: Proxy(Object), key: 'bbb', node: {…}, proto: {…}, trace: {…}}
/// {descriptor: Proxy(Object), key: 'ccc', node: {…}, proto: {…}, trace: {…}}

Пример 4: (перечислить только прототипы):

for (const item of createObjectIterator(obj, {proto: "only"})) console.log(item)
/// или
for (const item of createObjectIterator(obj, ({proto}) => {
    if (proto.is) return true;
})) console.log(item)
/// Вывод:
/// {descriptor: Proxy(Object), key: '[[Prototype]]', node: {…}, proto: {…}, trace: {…}}
/// {descriptor: Proxy(Object), key: '[[Prototype]]', node: {…}, proto: {…}, trace: {…}}
/// {descriptor: Proxy(Object), key: '[[Prototype]]', node: {…}, proto: {…}, trace: {…}}
function* createObjectIterator(obj, options) {
const isRevealed = (value) => (typeof value === "object" && value !== null) || typeof value === "function";
const is = (list) => {
const toList = (v) => [...v[Symbol.iterator] && typeof v === "object" ? v : [v]];
const isHandler = (v) => typeof v === "function";
const A = toList(list);
return {
intersectWith(value){
const B = !isHandler(value) ? new Set(toList(value)) : value;
return A.some((v) => !isHandler(B) ? B.has(v) : B(v));
},
equalTo(value) {
const B = toList(value);
if (A.length !== B.length) return false;
let i = A.length;
while (i--) {
if (!Object.is(A[i], B[i])) return false;
}
return true;
}
}
};
const getIterator = (obj) => new Set([...Reflect.ownKeys(obj), protoKey])[Symbol.iterator]();
const sortOf = (arg) => Object.prototype.toString.call(arg).slice(8, -1).toLowerCase();
const returnValue = (value) => {
const aliaces = {
true: "normal",
false: "abrupt",
};
const list = {
normal: {iterate: true, reveal: true},
abrupt: {iterate: false, reveal: false},
iterate: {reveal: false},
reveal: {iterate: false},
};
return {...list.normal, ...list[aliaces[value] ?? value] ?? list.abrupt};
};
const descriptorWithType = (obj, prop) => {
const desc = prop === protoKey ? {
writable: false,
enumerable: false,
configurable: false,
value: Reflect.getPrototypeOf(obj),
} : Reflect.getOwnPropertyDescriptor(obj, prop);
const result = { descriptor: desc, type: undefined };
if (is(["writable", "value"]).intersectWith((v) => Reflect.has(desc, v))) result.type = "data";
else if (is(["get", "set"]).intersectWith((v) => Reflect.has(desc, v))) result.type = "accessor";
return result;
};
if (!isRevealed(obj)) {
throw TypeError("Can't be iterable");
}
if (typeof options !== "function") {
options = {
mode: "permission",
type: "both",
proto: false,
...{
...options,
reveal: {
mode: "exclude",
list: ["function"],
...options?.reveal,
},
flags: {
...options?.flags,
},
},
};
if (!is(["match", "permission"]).intersectWith(options.mode)) {
throw TypeError("options.mode must be either 'match' or 'permission'");
}
if (options.mode === "permission" && is(Object.values(options.flags)).intersectWith((value) => typeof value === "boolean")) {
throw TypeError("options.flags should have values either true or false (boolean value), because of options.mode that equal to 'permission'");
}
if (!is(["both", "data", "accessor"]).intersectWith(options.type)) {
throw TypeError("options.type must be either 'both' or 'data' or 'accessor'");
}
if (!is([true, false, "iterate", "reveal", "only"]).intersectWith((value) => options.proto === value)) {
throw TypeError("options.proto must be one of true, false, 'iterate', 'reveal' or 'only' values");
}
if (typeof options.reveal === "object") {
if (!is(["exclude", "include"]).intersectWith((value) => options.reveal.mode === value)) {
throw TypeError("options.reveal.mode must be either 'exclude' or 'include'");
}
if (!Array.isArray(options.reveal.list)) {
throw TypeError("options.reveal.list must be an array type");
}
} else {
throw TypeError("options.reveal must be an object type");
}
}
const protoKey = Symbol("[[Prototype]]");
const proxyOptions = {
get() {
const value = Reflect.get(...arguments);
return isRevealed(value) ? new Proxy(value, this) : value;
},
set() {
return false;
},
defineProperty() {
return false;
},
deleteProperty() {
return false;
}
};
const traceStack = {
main: [],
proto: [],
isProtoRevealed: false,
__proto__: {
get active(){
return this.isProtoRevealed ? this.proto : this.main;
}
}
};
const proxy = new Proxy(obj, proxyOptions);
const stack = [{
name: "",
node: obj,
it: getIterator(obj),
}];
const nodes = new Set([obj]);
while (true) {
const last = stack.at(-1);
if (last === undefined) break;
let key;
while ((key = last.it.next().value)) {
const { type, descriptor } = descriptorWithType(last.node, key);
const proxyDescriptor = new Proxy({type, ...descriptor}, proxyOptions);
const parentNodeProxy = new Proxy(last.node, proxyOptions);
const revealed = isRevealed(proxyDescriptor.value);
const record = {
descriptor: proxyDescriptor,
key: protoKey === key ? protoKey.description : key,
node: {
parent: parentNodeProxy,
current: revealed ? proxyDescriptor.value : null,
...revealed ? {isVisited: nodes.has(descriptor.value)} : {},
},
proto: {
is: key === protoKey,
isIn: traceStack.proto.length > 0,
},
trace: {
main: [...traceStack.main],
proto: [...traceStack.proto],
},
};
const method = (value) => {
return {
function: () => {
const condition = options.call(proxy, record);
if (condition === "exit") return condition;
return returnValue(condition) ?? returnValue("abrupt");
},
object: () => {
if (key === protoKey) {
if (options.proto === "only") return returnValue(true);
return returnValue(options.proto);
}
if (options.proto === "only") return returnValue("abrupt");
if (options.mode === "permission") {
if (type === "data" && is(["both", "data"]).intersectWith(options.type)) {
if (
is([options.flags.writable, descriptor.writable]).equalTo([false, false]) ||
is([options.flags.value, descriptor.value]).equalTo([false, undefined])
) return returnValue("abrupt");
}
if (type === "accessor" && is(["both", "accessor"]).intersectWith(options.type)) {
if (
is([options.flags.set, descriptor.set]).equalTo([undefined, false]) ||
is([options.flags.get, descriptor.get]).equalTo([undefined, false])
) return returnValue("abrupt");
}
if (
is([options.flags.enumerable, descriptor.enumerable]).equalTo([false, false]) ||
is([options.flags.configurable, descriptor.configurable]).equalTo([false, false])
) return returnValue("abrupt");
}
if (options.mode === "match") {
if (["both", type].every((value) => value !== options.type)) return returnValue("abrupt");
if (!Object.entries(descriptor).every(([key, value]) => {
if (!Reflect.has(options.flags, key)) return true;
return Object.is(Reflect.get(options.flags, key), value);
})) return returnValue("abrupt");
}
return returnValue("normal");
},
}[typeof value]();
};
const completion = method(options);
if (completion === "exit") return;
if (completion.iterate) yield record;
if (!completion.reveal) continue;
if (typeof options === "object" && key !== protoKey) {
const listExpr = options.reveal.list.some((item) => sortOf(descriptor.value) === item);
if (options.reveal.mode === "exclude" && listExpr) continue;
if (options.reveal.mode === "include" && !listExpr) continue;
}
if (isRevealed(descriptor.value) && !nodes.has(descriptor.value)) {
if (key === protoKey && !traceStack.isProtoRevealed) {
traceStack.isProtoRevealed = true;
}
traceStack.active.push(protoKey === key ? protoKey.description : key);
stack.push({
name: key === protoKey ? protoKey : key,
node: descriptor.value,
it: getIterator(descriptor.value),
});
nodes.add(descriptor.value);
break;
}
}
if (key === undefined) {
stack.pop();
traceStack.active.pop();
if (traceStack.proto.length === 0) {
traceStack.isProtoRevealed = false;
}
}
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment