一尘不染

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

javascript

给定一个JS对象

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

和一个字符串

"a.b"

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

var val = obj.a.b

如果字符串是正义的'a',我可以使用obj[a]。但这更复杂。我想有一种简单的方法,但是目前可以逃脱。


阅读 324

收藏
2020-04-23

共1个答案

一尘不染

最近的说明: 虽然我很高兴这个答案获得了很多好评,但我还是有些恐惧。如果需要将点符号字符串(例如“
xabc”)转换为引用,则可能(可能)是表明发生了非常错误的信号(除非您正在执行一些奇怪的反序列化)。

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

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

* 这是不好的编程习惯(特别是不必要的元编程,并且这违反了函数的无副作用编码风格,并且会对性能造成影响)。在这种情况下发现自己的新手,应该考虑使用数组表示形式,例如[‘x’,’a’,’b’,’c’],或者如果可能的话甚至使用更直接/简单/简单的方法:例如不丢失首先跟踪引用本身(如果仅是客户端或服务器端,则是最理想的选择),等等。(预先存在的唯一ID很难添加,但如果规范另外要求它可以使用是否存在。)

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

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

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

这是一个优雅的单缸套,比其他解决方案短十倍:

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

[edit]或在ECMAScript 6中:

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

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

为了回应那些仍然担心使用reduceECMA-262标准(第5版)的人,这里有两行递归实现:

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编译器所做的优化,您可能需要确保没有通过常规方法在每次调用时都重新定义任何嵌套函数(将它们放在闭包,对象或全局名称空间中)。

编辑

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

您如何将其转换为二传手?不仅通过路径返回值,而且如果将新值发送到函数中,还要设置它们?–斯瓦德6月28日21:42

(sidenote:可悲的是无法返回带有Setter的对象,因为这将违反调用约定;注释者似乎是指具有副作用的通用setter样式函数,如index(obj,"a.b.etc", value)doing 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),而不是字符串。尽管便利功能通常没有错。


有评论者问:

数组呢?像“ ab [4] .cd [1] [2] [3]”之类的东西?–AlexS

Javascript是一种很奇怪的语言。通常,对象只能将字符串作为其属性键,因此,例如,如果x是一个通用对象,例如x={}x[1]则将变成x["1"]…您没看错…是的…

JavaScript数组(本身就是Object的实例)特别鼓励使用整数键,即使您可以执行类似的操作x=[]; x["puppy"]=5;

但通常(并且有例外)x["somestring"]===x.somestring(允许时;您不能这样做x.123)。

(请记住,如果使用的JS编译器可以证明不会违反规范,则可能会选择将其编译为更精巧的表示形式。)

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

那将等于a["b"][4]["c"]["d"][1][2][3],尽管我们可能也应该支持a.b["c\"validjsstringliteral"][3]。您必须检查有关字符串文字ecmascript语法部分,以了解如何解析有效的字符串文字。从技术上讲,您还需要检查(与我的第一个答案不同)a有效的javascript标识符

但是, 如果您的字符串不包含逗号或方括号 ,那么一个简单的答案就是匹配长度在1 ,[以上的字符序列,而不是set 或or ]

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

如果您的字符串不包含转义字符或"character,并且由于IdentifierNames是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编辑:

让我们全力以赴,并为语法 纯洁的
f谐性做一个我们可以想出的最低效,可怕的,过度编程的解决方案。使用ES6代理对象!…我们还要定义一些属性(imho很好,但是很不错),它们可能会破坏编写不正确的库。如果您关心表现,理智(您或他人的),工作等,则可能应该谨慎使用此功能。

// [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是:{“ a”:{“ b”:{“ c”:1,“ d”:2}}}

(代理覆盖获取)objHyper [‘abc’]为:1

(代理替代设置)objHyper [‘abc’] = 3,现在obj是:{“ a”:{“ b”:{“ c”:3,“ d”:2}}}

(在幕后)objHyper是:代理{a:{…}}

(快捷方式)obj.H [‘abc’] = 4

(快捷方式)obj.H [‘abc’]为obj [‘a’] [‘b’] [‘c’]为:4

效率低下的想法:您可以根据输入参数修改以上内容以进行分派;要么使用.match(/[^\]\[.]+/g)方法来支持obj['keys'].like[3]['this'],要么如果instanceof Array接受,那么就接受Array作为输入keys = ['a','b','c']; obj.H[keys]


每个建议,也许您想以一种“更软”的NaN风格处理未定义的索引(例如,index({a:{b:{c:...}}}, 'a.x.c')返回未定义而不是未捕获的TypeError)…:

1)从一维索引情况({})[‘eg’] ==
undefined中“应该返回未定义而不是抛出错误”的角度来看,这是有意义的,因此“我们应该返回未定义而不是抛出错误。错误”。

2)从我们所做的角度来看,这是 没有 意义的,x['a']['x']['c']在上面的示例中,它会因TypeError而失败。

就是说,您可以通过以下任一方法来替换约简函数,从而完成这项工作:

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

(您可以通过使用for循环并在未定义要进入下一个索引的子结果时中断/返回来提高效率,或者如果您希望这种故障很少发生,则可以使用try-
catch来提高效率。)

2020-04-23