【问题标题】:Is there a way for the value of an object to be made aware of its own key dynamically?有没有办法让对象的值动态地知道它自己的键?
【发布时间】:2021-02-28 11:36:34
【问题描述】:

这是一个纯理论的问题(虽然我认为这是一个有趣的思考练习)。我只是在处理一个 JavaScript 对象(与文档相关),一个有点不寻常的想法掠过我的脑海:有没有办法在所述对象中创建一个键/值对条目,能够读取它自己的键作为其值的一部分?也就是说:

假设我有一个用于序列化数据的 JavaScript 对象:

{
    "someKey":()=>"M-me? MY key is '" + voodoo(this) + "'! Thanks so much for taking an interest!"
}

...有没有办法在寻址密钥时将"M-me? MY key is 'someKey'! Thanks so much for taking an interest!" 作为(尽管:相当愚蠢的)输出?我完全不在乎结构是什么样子,也不在乎 KVP 部分的值的类型是什么,也不在乎需要传递什么参数(如果有的话?我只是假设它必须是毕竟是一个函数)。

我的意思是,当然是可能;这是代码。这一切皆有可能(我见过一个可以ascertain its own SHA-512 hash 的奎因,看在上帝的份上)。但我发现这是一个有趣的思想实验,并想看看是否有人已经有一些 Code Kung Fu/Source Santeria(即使是在抽象/伪代码级别)和/或可能有一些想法的人。

我已经尝试过实际逐行解析 JavaScript 源文件并测试输出字符串的其余部分以放置它(有效,但很蹩脚......如果它是一个构造对象怎么办? ?),然后考虑对其进行字符串化并对其进行正则表达式处理(有效,但仍然很弱......过于依赖对必须是不变结构的预先了解)。

我现在正在尝试过滤对象并自行尝试隔离发出请求的密钥,我希望这会起作用(-ish),但仍然让我感觉有点像公牛瓷器店。我可以扩展对象原型(我知道,我知道。理论上,还记得吗?)所以自引用不会造成问题,但我很难为 KVP 提供一种方法来唯一地标识自己而不必搜索字符串的某些设置部分。

有人有什么想法吗?没有任何限制:这可能永远不会看到生产环境的光芒——只是一个有趣的谜题——所以随意弄乱原型,包括库,不缩进......无论什么*。坦率地说,它甚至不必在 JavaScript 中。这就是我正在使用的。现在是凌晨 2 点 30 分,我只是在琢磨它是否可行。

*(请不要不缩进。Twitch-twitch(ಥ∻.⊙)这部分我好像撒了谎。)

【问题讨论】:

  • There's no such thing as a "JSON Object"。你有一个普通的 JavaScript 对象,而不是它的序列化形式。
  • 那么,“语义细化”...嗯...认为它会起作用吗?
  • 如果您的意思是“JSON”,那么不行 - 它无法工作。如果你的意思是对象,这是可能的。语义有很大的不同。
  • 很公平。更新。有什么想法吗?
  • 什么是“JSON 形状的 JS 对象”?没有函数和循环引用的 JavaScript 对象?这是我们正在谈论的计算机;它们对“语义”非常具体和特别。你不用带词库来对抗编译器。

标签: javascript json theory self-reference quine


【解决方案1】:

反射性地查找通话中的关键

这可能是最可靠的方法。 When obj.foo() is called, then foo is executed with obj set as the value of this。这意味着我们可以从this 中查找密钥。我们可以很容易地检查对象,最难的是找到哪个键包含我们刚刚执行的函数。我们可以尝试进行字符串匹配,但它可能会失败:

const obj = {
    foo: function() { /* magic */ },
    bar: function() { /* magic */ },
}

因为函数的内容是一样的,但是keys是不同的,所以用string来区分obj.foo()obj.bar()并不容易匹配。

不过,还有一个更好的选择——给函数命名:

const obj = {
    foo: function lookUpMyOwnKey() { /* magic */ }
}

通常,无论您是否为函数命名,都几乎没有影响。然而,我们可以利用的是函数现在可以通过名称引用自身。这为我们提供了一个使用Object.entries 的相当简单的解决方案:

"use strict";

const fn = function lookUpMyOwnName() {
  if (typeof this !== "object" || this === null) { //in case the context is removed
    return "Sorry, I don't know";
  }

  const pair = Object.entries(this)
    .find(([, value]) => value === lookUpMyOwnName);

  if (!pair) {
    return "I can't seem to find out";
  }

  return `My name is: ${pair[0]}`
}

const obj = {
  foo: fn
}

console.log(obj.foo());
console.log(obj.foo.call(null));
console.log(obj.foo.call("some string"));
console.log(obj.foo.call({
  other: "object"
}));

非常接近完美的解决方案。正如我们所看到的,如果函数没有定义为对象的一部分而是稍后添加,它甚至可以工作。因此,它完全脱离了它所属的对象。问题是它仍然是 one 函数,并且多次添加它不会得到正确的结果:

"use strict";

const fn = function lookUpMyOwnName() {
  if (typeof this !== "object" || this === null) { //in case the context is removed
    return "Sorry, I don't know";
  }

  const pair = Object.entries(this)
    .find(([, value]) => value === lookUpMyOwnName);

  if (!pair) {
    return "I can't seem to find out";
  }

  return `My name is: ${pair[0]}`
}

const obj = {
  foo: fn,
  bar: fn
}

console.log(obj.foo()); // foo
console.log(obj.bar()); // foo...oops

幸运的是,这很容易通过使用高阶函数并动态创建 lookUpMyOwnName 来解决。这样不同的实例就不会相互识别:

"use strict";

const makeFn = () => function lookUpMyOwnName() {
//    ^^^^^^   ^^^^^
  if (typeof this !== "object" || this === null) { //in case the context is removed
    return "Sorry, I don't know";
  }

  const pair = Object.entries(this)
    .find(([, value]) => value === lookUpMyOwnName);

  if (!pair) {
    return "I can't seem to find out";
  }

  return `My name is: ${pair[0]}`
}

const obj = {
  foo: makeFn(),
  bar: makeFn()
}

console.log(obj.foo()); // foo
console.log(obj.bar()); // bar

非常确定我们找到了关键

仍有可能失败

  • 如果调用来自原型链
  • 如果属性不可枚举

例子:

"use strict";

const makeFn = () => function lookUpMyOwnName() {
//    ^^^^^^   ^^^^^
  if (typeof this !== "object" || this === null) { //in case the context is removed
    return "Sorry, I don't know";
  }

  const pair = Object.entries(this)
    .find(([, value]) => value === lookUpMyOwnName);

  if (!pair) {
    return "I can't seem to find out";
  }

  return `My name is: ${pair[0]}`
}

const obj = {
  foo: makeFn()
}

const obj2 = Object.create(obj);

console.log(obj.foo());  // foo
console.log(obj2.foo()); // unknown


const obj3 = Object.defineProperties({}, {
  foo: {
    value: makeFn(),
    enumerable: true
  },
  bar: {
    value: makeFn(),
    enumerable: false
  }
})


console.log(obj3.foo()); // foo
console.log(obj3.bar()); // unknown

是否值得制定一个过度设计的解决方案来解决一个不存在的问题只是在这里找到所有东西?

嗯,我不知道答案。无论如何我都会做到的 - 这是一个彻底通过Object.getOwnPropertyDescriptors检查它的宿主对象和它的原型链的函数,以找到它被调用的确切位置:

"use strict";

const makeFn = () => function lookUpMyOwnName() {
  if (typeof this !== "object" || this === null) {
    return "Sorry, I don't know";
  }
  
  const pair = Object.entries(Object.getOwnPropertyDescriptors(this))
    .find(([propName]) => this[propName] === lookUpMyOwnName);

  if (!pair) {//we must go DEEPER!
    return lookUpMyOwnName.call(Object.getPrototypeOf(this));
  }
  
  return `My name is: ${pair[0]}`;
}

const obj = {
  foo: makeFn()
}

const obj2 = Object.create(obj);

console.log(obj.foo());  // foo
console.log(obj2.foo()); // foo


const obj3 = Object.defineProperties({}, {
  foo: {
    value: makeFn(),
    enumerable: true
  },
  bar: {
    value: makeFn(),
    enumerable: false
  },
  baz: {
    get: (value => () => value)(makeFn()) //make a getter from an IIFE 
  }
})


console.log(obj3.foo()); // foo
console.log(obj3.bar()); // bar
console.log(obj3.baz()); // baz

使用代理(轻微作弊)

这是另一种选择。定义一个Proxy 来拦截对对象的所有调用,这可以直接告诉你调用了什么。这有点作弊,因为该函数并不会真正查找自身,但从外部看它可能看起来像这样。

仍然可能值得列出,因为它具有非常功能强大且管理成本低的优势。无需递归遍历原型链和所有可能的属性来找到一个:

"use strict";


//make a symbol to avoid looking up the function by its name in the proxy
//and to serve as the placement for the name
const tellMe = Symbol("Hey, Proxy, tell me my key!"); 
const fn = function ItrustTheProxyWillTellMe() {
  return `My name is: ${ItrustTheProxyWillTellMe[tellMe]}`;
}
fn[tellMe] = true;

const proxyHandler = {
  get: function(target, prop) { ///intercept any `get` calls
    const val = Reflect.get(...arguments);
    //if the target is a function that wants to know its key
    if (val && typeof val === "function" && tellMe in val) {
      //attach the key as @@tellMe on the function
      val[tellMe] = prop;
    }
    
    return val;
  }
};

//all properties share the same function
const protoObj = Object.defineProperties({}, {
  foo: {
    value: fn,
    enumerable: true
  },
  bar: {
    value: fn,
    enumerable: false
  },
  baz: {
    get() { return fn; }
  }
});

const derivedObj = Object.create(protoObj);
const obj = new Proxy(derivedObj, proxyHandler);

console.log(obj.foo()); // foo
console.log(obj.bar()); // bar
console.log(obj.baz()); // baz

查看调用堆栈

这是草率且不可靠的,但仍然是一种选择。这将非常依赖于此代码所在的环境,因此我将避免进行实现,因为它需要绑定到 StackSnippet 沙箱。

然而,整个事情的关键是检查调用函数的 where 的堆栈跟踪。这在不同的地方会有不同的格式。这种做法非常狡猾和脆弱,但它确实揭示了比你通常能得到的更多关于通话的背景信息。 It might be weirdly useful in specific circumstances.

该技术在this article by David Walsh 中显示,这里是它的缩写——我们可以创建一个Error 对象,它会自动收集堆栈跟踪。大概是这样我们可以扔掉它并稍后检查它。相反,我们现在可以检查它并继续:

// The magic
console.log(new Error().stack);

/* SAMPLE:

Error
    at Object.module.exports.request (/home/vagrant/src/kumascript/lib/kumascript/caching.js:366:17)
    at attempt (/home/vagrant/src/kumascript/lib/kumascript/loaders.js:180:24)
    at ks_utils.Class.get (/home/vagrant/src/kumascript/lib/kumascript/loaders.js:194:9)
    at /home/vagrant/src/kumascript/lib/kumascript/macros.js:282:24
    at /home/vagrant/src/kumascript/node_modules/async/lib/async.js:118:13
    at Array.forEach (native)
    at _each (/home/vagrant/src/kumascript/node_modules/async/lib/async.js:39:24)
    at Object.async.each (/home/vagrant/src/kumascript/node_modules/async/lib/async.js:117:9)
    at ks_utils.Class.reloadTemplates (/home/vagrant/src/kumascript/lib/kumascript/macros.js:281:19)
    at ks_utils.Class.process (/home/vagrant/src/kumascript/lib/kumascript/macros.js:217:15)
*/

【讨论】:

  • 那……其实还挺棒的。我喜欢!我会继续摆弄,但这实际上是解决“问题”的一个相当优雅的解决方案(就是这样,哈哈)。
  • “是否值得制作一个过度设计的解决方案来解决一个不存在的问题,只是为了在这里找到所有东西?”罗弗莱。什么时候不是!?我承认:这有点像对汤饼干进行喷砂,但它完全有效,伙计。
猜你喜欢
  • 2016-03-07
  • 2017-12-06
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2015-11-18
  • 1970-01-01
  • 2013-08-08
  • 1970-01-01
相关资源
最近更新 更多