将点表示法中的 JavaScript 字符串转换为对象引用

2022-08-30 00:10:00

给定一个 JavaScript 对象,

var obj = { a: { b: '1', c: '2' } }

和一个字符串

"a.b"

如何将字符串转换为点表示法,以便我可以去

var val = obj.a.b

如果字符串只是,我可以使用.但这更复杂。我想有一些直接的方法,但它目前逃脱了我。'a'obj[a]


答案 1

最近的笔记:虽然我对这个答案得到了许多支持感到受宠若惊,但我也有些惊恐。如果需要将像“x.a.b.c”这样的点符号字符串转换为引用,它可能(也许)表明发生了一些非常错误的事情(除非你正在执行一些奇怪的反序列化)。

也就是说,找到这个答案的新手必须问自己一个问题:“我为什么要这样做?

当然,如果你的用例很小,并且你不会遇到性能问题,并且你不需要在你的抽象基础上进行构建,使其以后变得更加复杂,那么这样做通常是可以的。事实上,如果这将降低代码复杂性并保持简单,那么你应该继续做OP所要求的事情。但是,如果不是这种情况,请考虑以下任何一项是否适用:

案例 1:作为处理数据的主要方法(例如,作为应用传递对象并取消引用对象的默认形式)。就像问“如何从字符串中查找函数或变量名称”一样。

  • 这是糟糕的编程实践(特别是不必要的元编程,并且有点违反函数副作用的编码风格,并且会达到性能)。在这种情况下,新手应该考虑使用数组表示,例如['x','a','b','c'],或者如果可能的话,甚至可以更直接/简单/直接地使用数组表示:例如,首先不要丢失对引用本身的跟踪(如果它只是客户端或仅服务器端,则最理想)等(预先存在的唯一ID将无法添加, 但是,如果规范要求无论如何都存在,则可以使用。

案例 2:使用序列化数据或将向用户显示的数据。就像使用日期作为字符串“1999-12-30”而不是Date对象一样(如果不小心,这可能会导致时区错误或增加序列化复杂性)。或者你知道你在做什么。

  • 这也许很好。请注意,经过清理的输入片段中没有点字符串 “.”。

如果您发现自己一直在使用此答案并在字符串和数组之间来回转换,则可能处于不良情况,应该考虑其他方法。

这是一个优雅的单行本,比其他解决方案短10倍:

function index(obj,i) {return obj[i]}
'a.b.etc'.split('.').reduce(index, obj)

[编辑]或者在 ECMAScript 6 中:

'a.b.etc'.split('.').reduce((o,i)=> o[i], obj)

(并不是说我认为eval总是像其他人所说的那样糟糕(尽管它通常是这样),但是那些人会很高兴这种方法不使用eval。上面将找到给定和字符串。obj.a.b.etcobj"a.b.etc"

为了回应那些仍然害怕使用它的人,尽管它在ECMA-262标准(第5版)中,这里有一个两行递归实现:reduce

function multiIndex(obj,is) {  // obj,['1','2','3'] -> ((obj['1'])['2'])['3']
    return is.length ? multiIndex(obj[is[0]],is.slice(1)) : obj
}
function pathIndex(obj,is) {   // obj,'1.2.3' -> multiIndex(obj,['1','2','3'])
    return multiIndex(obj,is.split('.'))
}
pathIndex('a.b.etc')

根据 JS 编译器正在执行的优化,您可能希望确保在每次调用时都不会通过常用方法(将它们放在闭包、对象或全局命名空间中)重新定义任何嵌套函数。

编辑

要回答评论中的一个有趣的问题:

你如何把它变成一个二传手?不仅要按路径返回值,还要在将新值发送到函数中时设置它们?– Swader Jun 28 at 21:42

(旁注:遗憾的是,不能返回带有 Setter 的对象,因为这会违反调用约定;commenter 似乎指的是一个具有副作用的通用 setter 样式函数,就像在做一样。index(obj,"a.b.etc", value)obj.a.b.etc = value

样式并不适合,但我们可以修改递归实现:reduce

function index(obj,is, value) {
    if (typeof is == 'string')
        return index(obj,is.split('.'), value);
    else if (is.length==1 && value!==undefined)
        return obj[is[0]] = value;
    else if (is.length==0)
        return obj;
    else
        return index(obj[is[0]],is.slice(1), value);
}

演示:

> obj = {a:{b:{etc:5}}}

> index(obj,'a.b.etc')
5
> index(obj,['a','b','etc'])   #works with both strings and lists
5

> index(obj,'a.b.etc', 123)    #setter-mode - third argument (possibly poor form)
123

> index(obj,'a.b.etc')
123

...虽然我个人建议做一个单独的函数。我想以旁注结束,这个问题的原始提出者可以(应该?)使用索引数组(它们可以从中获取),而不是字符串;虽然便利功能通常没有什么问题。setIndex(...).split


一位评论者问道:

数组呢?类似于“a.b[4].c.d[1][2][3]”?–亚历克斯

Javascript是一种非常奇怪的语言;一般来说,对象只能有字符串作为其属性键,所以例如,如果是一个通用对象,如,那么就会变成...你没看错...是的。。。xx={}x[1]x["1"]

Javascript数组(它们本身就是Object的实例)特别鼓励整数键,即使你可以做类似的事情。x=[]; x["puppy"]=5;

但总的来说(也有例外),(当它被允许时;你不能这样做)。x["somestring"]===x.somestringx.123

(请记住,无论你使用什么JS编译器,如果它能证明它不会违反规范,可能会选择将这些编译成更合理的表示形式。

因此,您的问题的答案将取决于您是否假设这些对象只接受整数(由于您的问题域中的限制)。让我们假设不是。然后,有效表达式是基本标识符加上一些 s 加上一些 s 的串联。.identifier["stringindex"]

让我们暂时忽略一下,我们当然可以在语法中合法地做其他事情,例如;整数不是“特殊”的。identifier[0xFA7C25DD].asdf[f(4)?.[5]+k][false][null][undefined][NaN]

然后,注释者的声明将等效于 ,尽管我们可能也应该支持 。您必须检查字符串文本上的 ecmascript 语法部分,以查看如何分析有效的字符串文本。从技术上讲,你还想检查(不像我的第一个答案)这是一个有效的javascript标识符a["b"][4]["c"]["d"][1][2][3]a.b["c\"validjsstringliteral"][3]a

但是,如果您的字符串不包含逗号或括号,则只需匹配长度为1 +的字符序列(不在集合中)或:,[]

> "abc[4].c.def[1][2][\"gh\"]".match(/[^\]\[.]+/g)
// ^^^ ^  ^ ^^^ ^  ^   ^^^^^
["abc", "4", "c", "def", "1", "2", ""gh""]

如果你的字符串不包含转义字符或“字符,并且因为标识符名称是StringLiterals的子语言(我认为???)您可以先将点转换为 []:

> var R=[], demoString="abc[4].c.def[1][2][\"gh\"]";
> for(var match,matcher=/^([^\.\[]+)|\.([^\.\[]+)|\["([^"]+)"\]|\[(\d+)\]/g; 
      match=matcher.exec(demoString); ) {
  R.push(Array.from(match).slice(1).filter(x=> x!==undefined)[0]);
  // extremely bad code because js regexes are weird, don't use this
}
> R

["abc", "4", "c", "def", "1", "2", "gh"]

当然,请始终小心,永远不要信任您的数据。一些可能适用于某些用例的不良方法还包括:

// hackish/wrongish; preprocess your string into "a.b.4.c.d.1.2.3", e.g.: 
> yourstring.replace(/]/g,"").replace(/\[/g,".").split(".")
"a.b.4.c.d.1.2.3"  //use code from before

2018年特别编辑:

让我们全力以赴,做我们能想到的最低效,可怕的过度元数据化解决方案......为了句法纯度仓鼠。使用 ES6 代理对象!...让我们也定义一些属性(恕我直言,这些属性很好,很棒,但)可能会破坏编写不当的库。如果你关心性能,理智(你或别人的),你的工作等,你也许应该谨慎使用它。

// [1,2,3][-1]==3 (or just use .slice(-1)[0])
if (![1][-1])
    Object.defineProperty(Array.prototype, -1, {get() {return this[this.length-1]}}); //credit to caub

// WARNING: THIS XTREME™ RADICAL METHOD IS VERY INEFFICIENT,
// ESPECIALLY IF INDEXING INTO MULTIPLE OBJECTS,
// because you are constantly creating wrapper objects on-the-fly and,
// even worse, going through Proxy i.e. runtime ~reflection, which prevents
// compiler optimization

// Proxy handler to override obj[*]/obj.* and obj[*]=...
var hyperIndexProxyHandler = {
    get: function(obj,key, proxy) {
        return key.split('.').reduce((o,i)=> o[i], obj);
    },
    set: function(obj,key,value, proxy) {
        var keys = key.split('.');
        var beforeLast = keys.slice(0,-1).reduce((o,i)=> o[i], obj);
        beforeLast[keys[-1]] = value;
    },
    has: function(obj,key) {
        //etc
    }
};
function hyperIndexOf(target) {
    return new Proxy(target, hyperIndexProxyHandler);
}

演示:

var obj = {a:{b:{c:1, d:2}}};
console.log("obj is:", JSON.stringify(obj));

var objHyper = hyperIndexOf(obj);
console.log("(proxy override get) objHyper['a.b.c'] is:", objHyper['a.b.c']);
objHyper['a.b.c'] = 3;
console.log("(proxy override set) objHyper['a.b.c']=3, now obj is:", JSON.stringify(obj));

console.log("(behind the scenes) objHyper is:", objHyper);

if (!({}).H)
    Object.defineProperties(Object.prototype, {
        H: {
            get: function() {
                return hyperIndexOf(this); // TODO:cache as a non-enumerable property for efficiency?
            }
        }
    });

console.log("(shortcut) obj.H['a.b.c']=4");
obj.H['a.b.c'] = 4;
console.log("(shortcut) obj.H['a.b.c'] is obj['a']['b']['c'] is", obj.H['a.b.c']);

输出:

obj is: {“a”:{“b”:{“c”:1,“d”:2}}}

(proxy override get) objHyper['a.b.c'] is: 1

(proxy override set) objHyper['a.b.c']=3, now obj is: {“a”:{“b”:{“c”:3,“d”:2}}}

(幕后)objHyper is: Proxy {a: {...}}

(快捷方式) obj.H['a.b.c']=4

(快捷方式) obj.H['a.b.c'] is obj['a']['b']['c'] is: 4

低效的想法:您可以修改上述内容以根据输入参数进行调度;要么使用该方法来支持 ,要么如果 ,则只需接受数组作为输入,如 。.match(/[^\]\[.]+/g)obj['keys'].like[3]['this']instanceof Arraykeys = ['a','b','c']; obj.H[keys]


根据建议,您可能希望以“更柔和”的NaN风格的方式处理未定义的索引(例如 返回未定义而不是未捕获的类型错误)...:index({a:{b:{c:...}}}, 'a.x.c')

  1. 从1维索引情况({})['例如']==未定义的角度来看,这是有道理的,因此在N维情况下“我们应该返回未定义而不是抛出错误”。

  2. 从我们正在做的角度来看,这是有意义的,在上面的例子中,如果TypeError会失败。x['a']['x']['c']

也就是说,您可以通过将约化函数替换为以下任一值来完成这项工作:

(o,i)=> o===undefined?undefined:o[i]或。(o,i)=> (o||{})[i]

(您可以通过使用 for 循环并在未定义下一个索引的子结果时中断/返回,或者如果您预计此类故障非常罕见,则可以使用 try-catch 来提高效率。


答案 2

如果你可以使用Lodash,有一个函数,它完全可以做到这一点:

_.get(object, path, [defaultValue])

var val = _.get(obj, "a.b");