将点表示法的 JavaScript 字符串转换为对象引用 [英] Convert a JavaScript string in dot notation into an object reference

查看:34
本文介绍了将点表示法的 JavaScript 字符串转换为对象引用的处理方法,对大家解决问题具有一定的参考价值,需要的朋友们下面随着小编来一起学习吧!

问题描述

给定一个 JavaScript 对象,

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

和一个字符串

a.b"

如何将字符串转换为点符号以便我可以去

var val = obj.a.b

如果字符串只是'a',我可以使用obj[a].但这更复杂.我想有一些简单的方法,但目前我无法理解.

解决方案

最近的笔记:虽然我很高兴这个答案得到了很多赞成,但我也有点害怕.如果需要转换点符号字符串,如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.abetc 给定 obj 和字符串 "abetc".)

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

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

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

编辑:

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

<块引用>

你如何将它也变成一个二传手?不仅按路径返回值,而且在将新值发送到函数时设置它们?– 斯瓦德 6 月 28 日 21:42

(旁注:遗憾的是不能返回带有 Setter 的对象,因为这会违反调用约定;评论者似乎指的是具有副作用的一般 setter 样式函数,例如 index(obj,abetc", value)obj.abetc = value.)

reduce 风格不太适合这种情况,但我们可以修改递归实现:

function index(obj,is, value) {if (typeof is == 'string')return index(obj,is.split('.'), value);else if (is.length==1 && value!==undefined)返回 obj[is[0]] = 值;否则如果(is.length==0)返回对象;别的返回索引(obj[is[0]],is.slice(1), value);}

演示:

>obj = {a:{b:{etc:5}}}>索引(对象,'a.b.etc')5>index(obj,['a','b','etc']) #适用于字符串和列表5>index(obj,'a.b.etc', 123) #setter-mode - 第三个参数(可能是糟糕的形式)123>索引(对象,'a.b.etc')123

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


评论者问道:

<块引用>

数组呢?诸如a.b[4].c.d[1][2][3]"之类的东西?——亚历克斯

Javascript 是一种非常奇怪的语言;通常对象只能将字符串作为它们的属性键,例如,如果 x 是一个像 x={} 这样的通用对象,那么 x[1] 会变成 x["1"]...你没看错...是的...

Javascript 数组(它们本身是 Object 的实例)特别鼓励整数键,即使您可以执行类似 x=[]; 的操作.x[小狗"]=5;.

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

(请记住,无论您使用的是什么 JS 编译器,如果可以证明它不会违反规范,都可能会选择将这些编译器编译为更合理的表示.)

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

让我们暂时忽略我们当然可以在语法中合法地做其他事情,例如 identifier[0xFA7C25DD].asdf[f(4)?.[5]+k][false][null][未定义][NaN];整数不是(那个)特殊".

评论者的声明将等同于 a[b"][4][c"][d"][1][2][3],尽管我们可能还应该支持 ab["c"validjsstringliteral"][3].您必须查看 关于字符串文字的 ecmascript 语法部分 查看如何解析有效的字符串文字.从技术上讲,您还想检查(与我的第一个答案不同)a 是否有效 javascript 标识符.

不过,对您的问题的简单回答是,如果您的字符串不包含逗号或括号,则只是匹配长度为 1+ 的字符序列不在集合 中,[]:

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

如果您的字符串不包含转义字符或 " 字符,并且因为 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]);//非常糟糕的代码,因为 js 正则表达式很奇怪,不要使用它}>电阻[abc"、4"、c"、def"、1"、2"、gh"]

当然,请始终小心谨慎,切勿相信您的数据.一些可能适用于某些用例的糟糕方法还包括:

//hackish/wrongish;将您的字符串预处理为a.b.4.c.d.1.2.3",例如:>yourstring.replace(/]/g,"").replace(/[/g,".").split(".")a.b.4.c.d.1.2.3"//使用之前的代码


2018 年特别

为了语法purityhamfistery 的利益,让我们转一圈,做我们能想出的最低效、最可怕的过度元编程解决方案.使用 ES6 代理对象!...让我们也定义一些属性(恕我直言很好,但是)可能会破坏不正确编写的库.如果您关心绩效、理智(您的或他人的)、您的工作等,您或许应该谨慎使用它.

//[1,2,3][-1]==3 (或者只使用 .slice(-1)[0])如果 (![1][-1])Object.defineProperty(Array.prototype, -1, {get() {return this[this.length-1]}});//归功于caub//警告:这种 XTREME™ 激进方法非常低效,//特别是如果索引到多个对象,//因为您不断地动态创建包装对象,并且,//更糟糕的是,通过代理,即运行时 ~reflection,这会阻止//编译器优化//代理处理程序覆盖 obj[*]/obj.* 和 obj[*]=...var hyperIndexProxyHandler = {得到:函数(对象,键,代理){return key.split('.').reduce((o,i)=>o[i], obj);},设置:函数(对象,键,值,代理){var keys = key.split('.');var beforeLast = keys.slice(0,-1).reduce((o,i)=>o[i], obj);beforeLast[keys[-1]] = 值;},有:函数(对象,键){//等等}};功能 hyperIndexOf(目标){返回新代理(目标,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("(在幕后) objHyper 是:", objHyper);如果 (!({}).H)Object.defineProperties(Object.prototype, {H: {得到:函数(){返回 hyperIndexOf(this);//TODO:cache 作为一个不可枚举的属性来提高效率?}}});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['a.b.c'] 是:1

<块引用>

(代理覆盖集)objHyper['abc']=3,现在 obj 是:{a":{b":{c":3,d":2}}}

<块引用>

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

<块引用>

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

<块引用>

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

低效的想法:可以修改上面的,根据输入参数进行调度;要么使用 .match(/[^][.]+/g) 方法来支持 obj['keys'].like[3]['this']code>,或者如果 instanceof Array,则只接受一个数组作为输入,例如 keys = ['a','b','c'];obj.H[keys].


根据建议,也许您想以更软"的 NaN 样式方式处理未定义的索引(例如 index({a:{b:{c:...}}}, 'axc') 返回 undefined 而不是未捕获的 TypeError)...:

  1. 从我们应该返回 undefined 而不是抛出错误"的角度来看,这是有道理的.在一维索引情况下 ({})['e.g.']==undefined,所以我们应该返回 undefined 而不是抛出错误";在 N 维情况下.

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

也就是说,您可以通过以下任一方式替换您的减少功能来完成这项工作:

(o,i)=>o===undefined?undefined:o[i], 或<代码>(o,i)=>(o||{})[i].

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

>

Given a JavaScript object,

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

and a string

"a.b"

how can I convert the string to dot notation so I can go

var val = obj.a.b

If the string was just 'a', I could use obj[a]. But this is more complex. I imagine there is some straightforward method, but it escapes me at present.

解决方案

recent note: While I'm flattered that this answer has gotten many upvotes, I am also somewhat horrified. If one needs to convert dot-notation strings like "x.a.b.c" into references, it could (maybe) be a sign that there is something very wrong going on (unless maybe you're performing some strange deserialization).

That is to say, novices who find their way to this answer must ask themselves the question "why am I doing this?"

It is of course generally fine to do this if your use case is small and you will not run into performance issues, AND you won't need to build upon your abstraction to make it more complicated later. In fact, if this will reduce code complexity and keep things simple, you should probably go ahead and do what OP is asking for. However, if that's not the case, consider if any of these apply:

case 1: As the primary method of working with your data (e.g. as your app's default form of passing objects around and dereferencing them). Like asking "how can I look up a function or variable name from a string".

  • This is bad programming practice (unnecessary metaprogramming specifically, and kind of violates function side-effect-free coding style, and will have performance hits). Novices who find themselves in this case, should instead consider working with array representations, e.g. ['x','a','b','c'], or even something more direct/simple/straightforward if possible: like not losing track of the references themselves in the first place (most ideal if it's only client-side or only server-side), etc. (A pre-existing unique id would be inelegant to add, but could be used if the spec otherwise requires its existence regardless.)

case 2: Working with serialized data, or data that will be displayed to the user. Like using a date as a string "1999-12-30" rather than a Date object (which can cause timezone bugs or added serialization complexity if not careful). Or you know what you're doing.

  • This is maybe fine. Be careful that there are no dot strings "." in your sanitized input fragments.

If you find yourself using this answer all the time and converting back and forth between string and array, you may be in the bad case, and should consider an alternative.

Here's an elegant one-liner that's 10x shorter than the other solutions:

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

[edit] Or in ECMAScript 6:

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

(Not that I think eval always bad like others suggest it is (though it usually is), nevertheless those people will be pleased that this method doesn't use eval. The above will find obj.a.b.etc given obj and the string "a.b.etc".)

In response to those who still are afraid of using reduce despite it being in the ECMA-262 standard (5th edition), here is a two-line recursive implementation:

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')

Depending on the optimizations the JS compiler is doing, you may want to make sure any nested functions are not re-defined on every call via the usual methods (placing them in a closure, object, or global namespace).

edit:

To answer an interesting question in the comments:

how would you turn this into a setter as well? Not only returning the values by path, but also setting them if a new value is sent into the function? – Swader Jun 28 at 21:42

(sidenote: sadly can't return an object with a Setter, as that would violate the calling convention; commenter seems to instead be referring to a general setter-style function with side-effects like index(obj,"a.b.etc", value) doing obj.a.b.etc = value.)

The reduce style is not really suitable to that, but we can modify the recursive implementation:

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);
}

Demo:

> 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

...though personally I'd recommend making a separate function setIndex(...). I would like to end on a side-note that the original poser of the question could (should?) be working with arrays of indices (which they can get from .split), rather than strings; though there's usually nothing wrong with a convenience function.


A commenter asked:

what about arrays? something like "a.b[4].c.d[1][2][3]" ? –AlexS

Javascript is a very weird language; in general objects can only have strings as their property keys, so for example if x was a generic object like x={}, then x[1] would become x["1"]... you read that right... yup...

Javascript Arrays (which are themselves instances of Object) specifically encourage integer keys, even though you could do something like x=[]; x["puppy"]=5;.

But in general (and there are exceptions), x["somestring"]===x.somestring (when it's allowed; you can't do x.123).

(Keep in mind that whatever JS compiler you're using might choose, maybe, to compile these down to saner representations if it can prove it would not violate the spec.)

So the answer to your question would depend on whether you're assuming those objects only accept integers (due to a restriction in your problem domain), or not. Let's assume not. Then a valid expression is a concatenation of a base identifier plus some .identifiers plus some ["stringindex"]s.

Let us ignore for a moment that we can of course do other things legitimately in the grammar like identifier[0xFA7C25DD].asdf[f(4)?.[5]+k][false][null][undefined][NaN]; integers are not (that) 'special'.

Commenter's statement would then be equivalent to a["b"][4]["c"]["d"][1][2][3], though we should probably also support a.b["c"validjsstringliteral"][3]. You'd have to check the ecmascript grammar section on string literals to see how to parse a valid string literal. Technically you'd also want to check (unlike in my first answer) that a is a valid javascript identifier.

A simple answer to your question though, if your strings don't contain commas or brackets, would be just be to match length 1+ sequences of characters not in the set , or [ or ]:

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

If your strings don't contain escape characters or " characters, and because IdentifierNames are a sublanguage of StringLiterals (I think???) you could first convert your dots to []:

> 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"]

Of course, always be careful and never trust your data. Some bad ways to do this that might work for some use cases also include:

// 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


Special 2018 edit:

Let's go full-circle and do the most inefficient, horribly-overmetaprogrammed solution we can come up with... in the interest of syntactical purityhamfistery. With ES6 Proxy objects!... Let's also define some properties which (imho are fine and wonderful but) may break improperly-written libraries. You should perhaps be wary of using this if you care about performance, sanity (yours or others'), your job, etc.

// [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);
}

Demo:

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']);

Output:

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}}}

(behind the scenes) objHyper is: Proxy {a: {…}}

(shortcut) obj.H['a.b.c']=4

(shortcut) obj.H['a.b.c'] is obj['a']['b']['c'] is: 4

inefficient idea: You can modify the above to dispatch based on the input argument; either use the .match(/[^][.]+/g) method to support obj['keys'].like[3]['this'], or if instanceof Array, then just accept an Array as input like keys = ['a','b','c']; obj.H[keys].


Per suggestion that maybe you want to handle undefined indices in a 'softer' NaN-style manner (e.g. index({a:{b:{c:...}}}, 'a.x.c') return undefined rather than uncaught TypeError)...:

  1. This makes sense from the perspective of "we should return undefined rather than throw an error" in the 1-dimensional index situation ({})['e.g.']==undefined, so "we should return undefined rather than throw an error" in the N-dimensional situation.

  2. This does not make sense from the perspective that we are doing x['a']['x']['c'], which would fail with a TypeError in the above example.

That said, you'd make this work by replacing your reducing function with either:

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

(You can make this more efficient by using a for loop and breaking/returning whenever the subresult you'd next index into is undefined, or using a try-catch if you expect such failures to be sufficiently rare.)

这篇关于将点表示法的 JavaScript 字符串转换为对象引用的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持IT屋!

查看全文
登录 关闭
扫码关注1秒登录
发送“验证码”获取 | 15天全站免登陆