有没有办法让一个对象的值动态地知道它自己的键?

Is there a way for the value of an object to be made aware of its own key dynamically?

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

假设我有一个 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(即使在 abstract/pseudo-code 级别)and/or 可能有一些想法的人.

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

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

有人有什么想法吗?没有任何限制:这可能永远不会出现在生产环境中 - 只是一个有趣的谜题 - 所以请随意处理原型,包括库,缩进失败......随便什么*。坦率地说,它甚至不必在 JavaScript 中;这就是我正在使用的。 2:30am 在这里,我只是想看看它是否可行。

*(请不要漏缩。抽抽抽抽(ಥ∻.⊙)我好像骗了那部分。)

调用时反射查找key

这可能是最可靠的方法。 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 */ },
}

因为函数的contents会一样但是keys不同所以不容易区分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"
}));

这是非常接近完美的解决方案。如我们所见,即使函数未定义为对象的一部分而是稍后添加,它也能正常工作。所以,它完全脱离了它所属的对象。问题是它仍然是一个函数,多次添加它不会得到正确的结果:

"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 沙箱。

然而,整个事情的关键是检查调用函数的 的堆栈跟踪。这将在不同的地方有不同的格式。这种做法非常狡猾和脆弱,但它确实揭示了比通常情况下更多的关于电话的背景信息。 .

该技术在 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)
*/