给定一个JavaScript对象,

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

和字符串

"a.b"

我怎么把字符串转换成点符号呢

var val = obj.a.b

如果字符串只是'a',我可以使用obj[a]。但这个更复杂。我想应该有什么简单的方法,但现在想不起来了。


当前回答

我不清楚你的问题是什么。给定你的对象,obj。a.b会给你原来的2。如果你想操纵字符串使用括号,你可以这样做:

var s = 'a.b';
s = 'obj["' + s.replace(/\./g, '"]["') + '"]';
alert(s); // displays obj["a"]["b"]

其他回答

解决方案:

function deepFind(key, data){
  return key.split('.').reduce((ob,i)=> ob?.[i], data)
}

用法:

const obj = {
   company: "Pet Shop",
   person: {
      name: "John"
   },
   animal: {
      name: "Lucky"
   }
}

const company = deepFind("company", obj) 
const personName = deepFind("person.name", obj) 
const animalName = deepFind("animal.name", obj) 

注意,如果你已经在使用Lodash,你可以使用属性或获取函数:

var obj = { a: { b: '1', c: '2' } };
_.property('a.b')(obj); // => 1
_.get(obj, 'a.b'); // => 1

js也有一个属性函数,但是不支持点表示法。

虽然我很高兴这个答案得到了很多赞,但我也有点害怕。如果需要将“x.a.b.c”这样的点符号字符串转换为引用,则可能(可能)表明发生了一些非常错误的事情(除非您正在执行一些奇怪的反序列化)。

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

当然,如果您的用例很小,并且不会遇到性能问题,并且您不需要在抽象的基础上构建以使其更复杂,那么这样做通常是好的。事实上,如果这样可以降低代码复杂性并使事情变得简单,那么您可能应该继续执行OP要求的操作。然而,如果情况并非如此,请考虑以下是否适用:

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

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

情况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.c etc和字符串“a.b.c 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编译器正在进行的优化,您可能希望确保任何嵌套函数不会在每次调用时通过常规方法重新定义(将它们放置在闭包、对象或全局名称空间中)。

编辑:

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

如何将其转换为setter呢?不仅按路径返回值,而且还设置它们,如果一个新值被发送到函数?- Swader 6月28日21:42

(旁注:遗憾的是不能返回带有Setter的对象,因为那样会违反调用约定;评论者似乎指的是带有副作用的一般setter样式的函数,比如index(obj,"a.b.。Etc”,value) doing obj. a.b.c 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]”?-AlexS

Javascript是一种非常奇怪的语言;一般情况下,对象只能使用字符串作为属性键,例如,如果x是一个泛型对象,如x={},那么x[1]将变成x["1"]…你没看错……是的……

Javascript数组(本身就是Object的实例)特别鼓励使用整型键,即使你可以这样做x=[];x[“小狗”]= 5;。

但一般情况下(也有例外),x["somestring"]===x。Somestring(当允许时;你不能做x.123)。

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

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

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

评论者的声明将等效于a["b"][4]["c"]["d"][1][2][3],尽管我们可能也应该支持a.b["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]);
  // 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是:{" a":{"b":{"c":1,"d":2}}}}}}

(代理覆盖get) objHyper['a.b.c']是:1

(代理覆盖集)objHyper [' a.b.c ') = 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']。例如[3]['this'],或者instanceof Array,则只接受数组作为输入,例如keys = ['a','b','c'];obj.H(键)。


每个建议,也许你想要处理未定义的索引在一个'软' nan风格的方式(例如索引({a:{b:{c:…}}}, 'a.x.c')返回未定义的而不是未捕获的TypeError)…

从“我们应该返回undefined而不是抛出一个错误”的角度来看,这在一维索引情况下是有意义的({})['例如。']==undefined,所以在n维情况下“我们应该返回undefined而不是抛出错误”。 从我们正在执行x['a']['x']['c']的角度来看,这是没有意义的,在上面的示例中,这将失败并出现TypeError。

也就是说,你可以通过将你的约简函数替换为:

(哦,我)= > o = = =定义?定义:o[我],或 (哦,我)= > (o | |{})[我]。

(你可以通过使用for循环,并在你的下一个索引的子结果是未定义的时候中断/返回,或者如果你希望这样的失败足够少,使用try-catch来提高效率。)

如果您希望将任何包含点符号键的对象转换为这些键的数组版本,可以使用此方法。


这将转换为

{
  name: 'Andy',
  brothers.0: 'Bob'
  brothers.1: 'Steve'
  brothers.2: 'Jack'
  sisters.0: 'Sally'
}

to

{
  name: 'Andy',
  brothers: ['Bob', 'Steve', 'Jack']
  sisters: ['Sally']
}

convertDotNotationToArray(objectWithDotNotation) {

    Object.entries(objectWithDotNotation).forEach(([key, val]) => {

      // Is the key of dot notation 
      if (key.includes('.')) {
        const [name, index] = key.split('.');

        // If you have not created an array version, create one 
        if (!objectWithDotNotation[name]) {
          objectWithDotNotation[name] = new Array();
        }

        // Save the value in the newly created array at the specific index 
        objectWithDotNotation[name][index] = val;
        // Delete the current dot notation key val
        delete objectWithDotNotation[key];
      }
    });

}

如果希望多次解除对同一路径的引用,为每个点符号路径构建函数实际上具有迄今为止最好的性能(扩展James Wilkins在上面的评论中链接到的性能测试)。

var path = 'a.b.x';
var getter = new Function("obj", "return obj." + path + ";");
getter(obj);

Using the Function constructor has some of the same drawbacks as eval() in terms of security and worst-case performance, but IMO it's a badly underused tool for cases where you need a combination of extreme dynamism and high performance. I use this methodology to build array filter functions and call them inside an AngularJS digest loop. My profiles consistently show the array.filter() step taking less than 1ms to dereference and filter about 2000 complex objects, using dynamically-defined paths 3-4 levels deep.

当然,类似的方法也可以用于创建setter函数:

var setter = new Function("obj", "newval", "obj." + path + " = newval;");
setter(obj, "some new val");