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

320

给定一个 JavaScript 对象,

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

和一个字符串

"a.b"

如何将字符串转换为点号表示法,以便我可以转到相应的对象?

var val = obj.a.b
如果字符串只是'a',我可以使用obj[a]。但这更复杂。我想有一些简单的方法,但它暂时逃脱了我的注意力。
如果字符串只是'a',我可以使用obj['a']。但这个问题更加复杂。我想可能有一些直接的方法,但目前我还无法想出来。

41
@Andrey eval是有害的,请勿使用。 - Kevin Ji
5
了解:这是我刚刚完成的一些有趣的速度测试链接:http://jsperf.com/dereference-object-property-path-from-string - James Wilkins
如果性能是一个严肃的考虑因素,并且您经常重复使用相同的路径(例如在数组过滤函数内部),请使用下面我回答中描述的Function构造函数。当相同的路径被使用数千次时,Function方法可以比每次解引用时进行eval或拆分和减少路径快10倍以上。 - Kevin
1
有些情况下你必须使用eval或new Function(),尤其是当你想从模板创建一个函数时,就像JSP页面被转换为JAVA一样,没有更有效的方法来处理模板。这个eval是邪恶的教条,只是一个教条而已,真正邪恶的是评估一个你没有创建过的脚本。当然,在这种特定情况下,没有理由使用eval。 - Martijn Scheffer
显示剩余3条评论
34个回答

597

最近的说明:虽然我很高兴这个答案得到了许多赞,但我也有些恐惧。如果一个人需要将点表示法字符串(如“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.etc"的情况下找到obj.a.b.etc

针对那些仍然害怕使用reduce,尽管它已经在ECMA-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编译器进行的优化,您可能希望确保通过通常的方法(将它们放入闭包、对象或全局命名空间)不会在每次调用时重新定义任何嵌套的函数。
编辑:回答评论中的一个有趣问题:
如何将其转换为setter?不仅按路径返回值,还可以在函数中设置新值吗?
(顺便说一句:很遗憾不能使用Setter返回对象,因为这会违反调用约定;评论者似乎指的是具有类似Setter的副作用的一般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]”这样的东西?–AlexS

Javascript是一种非常奇怪的语言;一般对象只能有字符串作为其属性键,因此例如如果x是一个通用对象,如x={},那么x[1]将变为x["1"]... 你读对了... 是的...

Javascript数组(它们本身是Object的实例)特别鼓励整数键,尽管你可以做一些像x=[]; x["puppy"]=5;这样的事情。

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

(请记住,无论您使用哪个JS编译器,它都可能选择编译这些内容成更合理的表示形式,如果它可以证明不违反规范。)

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

让我们暂时忽略我们可以在语法中合法地执行其他操作,例如identifier[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的子语言(我认为???),您可以先将点号转换为[]:

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

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

(幕后) objHyper是:Proxy {a: {…}}

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

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

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


根据建议,也许您希望以“更柔和”的NaN样式处理未定义的索引(例如index({a:{b:{c:...}}},'a.x.c')返回undefined而不是未捕获的TypeError)...

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

  2. 从我们正在进行x['a']['x']['c']的角度来看,这是没有意义的,在上面的示例中会导致TypeError。

也就是说,您可以用以下任一减少函数替换它:

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

(如果您预计此类故障发生的频率足够低,则可以通过使用for循环并在下一个索引到的子结果为undefined时中断/返回,或者使用try-catch来使其更有效率。)


4
reduce 在当前被使用的所有浏览器中均不受支持。 - Ricardo Tomasi
17
Array.reduce 是 ECMA-262 标准的一部分。如果你真的想支持过时的浏览器,可以将 Array.prototype.reduce 定义为给定某处的示例实现(例如 https://developer.mozilla.org/en/JavaScript/Reference/Global_Objects/Array/Reduce#Compatibility)。 - ninjagecko
2
是的,但将这两行代码放入一个函数中很容易。var setget = function( obj, path ){ function index( robj,i ) {return robj[i]}; return path.split('.').reduce( index, obj ); } - nevf
3
我喜欢这个优雅的例子,谢谢ninjagecko。我扩展了它来处理数组风格的表示法,还包括空字符串-请查看我的示例:http://jsfiddle.net/sc0ttyd/q7zyd/ - Sc0ttyD
2
@ninjagecko,您如何将此转换为setter?不仅按路径返回值,而且如果将新值发送到函数中,则还可以设置它们? - Swader
显示剩余15条评论

108

5
请注意:_.get(object, path) 不会因为找不到路径而出错,但 'a.b.etc'.split('.').reduce((o,i)=>o[i], obj) 会。对于我的特定情况,这正是我所需要的,但并非所有情况都适用。谢谢! - Mr. B.
1
@Mr.B. 最新版本的 Lodash 提供了一个可选的第三个参数 defaultValue。如果 _.get() 方法解析为 undefined,则该方法返回默认值,因此将其设置为所需内容,并观察该值。 - steampowered
17
想知道的人,它也支持 _.set(object, path, value) - Jeffrey Roosendaal
现在JavaScript有更多的惯用语,您可以使用Elvis运算符?.来防止对象访问中断:'a.b.etc'.split('.').reduce((o,i)=>o?.[i], obj) - Michael Buen

32
你可以使用 lodash.get
安装后(npm i lodash.get),按照以下方式使用:
const get = require('lodash.get');

const myObj = { 
    user: { 
        firstName: 'Stacky', 
        lastName: 'Overflowy',
        list: ['zero', 'one', 'two']
    }, 
    id: 123 
};

console.log(get(myObj, 'user.firstName')); // outputs Stacky
console.log(get(myObj, 'id'));             // outputs 123
console.log(get(myObj, 'user.list[1]'));   // outputs one

// You can also update values
get(myObj, 'user').firstName = 'John';

29

2023

每次你想要在程序中获得新功能时,都不需要引入另一个依赖项。现代JS非常强大,而且可选链操作符?.现在得到了广泛支持,使得这种任务变得非常简单。

只需一行代码,我们就可以编写一个get函数,它接受一个输入对象t和字符串path。它适用于任意嵌套层级的对象数组-

const get = (t, path) =>
  path.split(".").reduce((r, k) => r?.[k], t)
  
const mydata =
  { a: { b: [ 0, { c: { d: [ "hello", "world" ] } } ] } }

console.log(get(mydata, "a.b.1.c.d.0"))
console.log(get(mydata, "a.b.1.c.d.1"))
console.log(get(mydata, "a.b.x.y.z"))

"hello"
"world"
undefined

设置

当考虑到一个对象的值可能是其他对象或数组时,set操作是一个非平凡的函数。在处理不存在的对象或数组的深层设置时,我们应该如何处理?我们应该在途中创建它们吗?而且,如何解决这种冲突?

set(mydata, "a", 1)   // { "a": 1 }
set(mydata, "a.b", 2) // Error: cannot set "b" property on number

我们将在下面写一个简单的set -

const get = (t, path) =>
  path.split(".").reduce((r, k) => r?.[k], t)

const set = (t, path, value) => {
  if (typeof t != "object") throw Error("non-object")
  if (path == "") throw Error("empty path")
  const pos = path.indexOf(".")
  return pos == -1
    ? (t[path] = value, value)
    : set(t[path.slice(0, pos)], path.slice(pos + 1), value) 
}

// build data from previous example
const mydata = {}
set(mydata, "a", {})
set(mydata, "a.b", [])
set(mydata, "a.b.0", 0)
set(mydata, "a.b.1", {})
set(mydata, "a.b.1.c", {})
set(mydata, "a.b.1.c.d", [])
set(mydata, "a.b.1.c.d.0", "hello")
set(mydata, "a.b.1.c.d.1", "world")

// read by path
console.log(get(mydata, "a.b.1.c.d.0"))
console.log(get(mydata, "a.b.1.c.d.1"))
console.log(get(mydata, "a.b.x.y.z"))

设置(高级)

但是如果我们希望set在对象和数组不存在时自动创建它们呢?我们也可以做到 -

const get = (t, path) =>
  path.split(".").reduce((r, k) => r?.[k], t)

const set = (t, path, value) => {
  if (path == "") return value
  const [k, next] = path.split({
    [Symbol.split](s) {
      const i = s.indexOf(".")
      return i == -1 ? [s, ""] : [s.slice(0, i), s.slice(i + 1)]
    }
  })
  if (t !== undefined && typeof t !== "object")
    throw Error(`cannot set property ${k} of ${typeof t}`)
  return Object.assign(
    t ?? (/^\d+$/.test(k) ? [] : {}),
    { [k]: set(t?.[k], next, value) },
  )
}

// build data from previous example
const mydata = set({}, "a.b", [
  0,
  set({}, "c.d", ["hello", "world"])
])

// print checkpoint
console.log(JSON.stringify(mydata, null, 2))

// set additional fields
set(mydata, "a.b.1.c.d.1", "moon")
set(mydata, "a.b.1.w", "x.y.z")

// ensure changes
console.log(JSON.stringify(mydata, null, 2))
.as-console-wrapper { min-height: 100%; top: 0; }

如果我们试图在已经设置了的非对象值上设置一个键,将会引发运行时错误。
const mydata = { a: 1 }
set(mydata, "a.foo", "bar")
// Error: cannot set property "foo" of number

1
我们生活在一个美妙的新世界里。我继续感到印象深刻,多年过去了,人们仍然对我的最初帖子做出回应。 - nevf
1
厉害了兄弟,不过,你能设置新值吗? - Alauddin Afif Cassandra
肯定可以使用“set”,但是我的大脑在尝试理解时会崩溃。 - tempranova
1
set函数是一个非平凡的函数,考虑set(obj, "a", 1)后紧接着set(obj, "a.b", 2)的情况。第二次调用试图访问a上的b属性,但是a已经被设置为一个非对象(数字)。当考虑到对象可能包含数组,数组可能包含对象,对象可能包含数组时,情况变得更加复杂。 - undefined
1
对于那些感兴趣的人,我在帖子中更新了两个set函数的示例。 - undefined
显示剩余2条评论

25

一个更加深入的递归示例。

function recompose(obj, string) {
  var parts = string.split('.');
  var newObj = obj[parts[0]];
  if (parts[1]) {
    parts.splice(0, 1);
    var newString = parts.join('.');
    return recompose(newObj, newString);
  }
  return newObj;
}

var obj = { a: { b: '1', c: '2', d:{a:{b:'blah'}}}};
console.log(recompose(obj, 'a.d.a.b')); //blah


15

我建议拆分路径并对其进行迭代,然后减少您拥有的对象。该提议使用默认值处理缺失属性。

const getValue = (object, keys) => keys.split('.').reduce((o, k) => (o || {})[k], object);

console.log(getValue({ a: { b: '1', c: '2' } }, 'a.b'));
console.log(getValue({ a: { b: '1', c: '2' } }, 'foo.bar.baz'));


11

10
如果您期望多次解引用相同的路径,则为每个点符号路径构建函数实际上具有 迄今为止最佳的性能(在上面评论中扩展了 James Wilkins 链接的性能测试)。
var path = 'a.b.x';
var getter = new Function("obj", "return obj." + path + ";");
getter(obj);

使用函数构造函数与eval()在安全性和最坏情况下的性能方面存在一些相同的缺点,但在我看来,它是一个被严重低估的工具,适用于需要极端动态性和高性能的情况。我使用这种方法来构建数组过滤函数,并在AngularJS digest循环中调用它们。我的配置文件始终显示array.filter()步骤花费不到1毫秒的时间来解除引用并过滤大约2000个复杂对象,使用动态定义的深度为3-4级的路径。
当然,类似的方法也可以用于创建setter函数:
var setter = new Function("obj", "newval", "obj." + path + " = newval;");
setter(obj, "some new val");

1
如果您需要在很长一段时间内取消引用相同的路径,则上面的jsperf.com链接显示了如何保存并稍后查找函数的示例。调用Function构造函数的行为相当缓慢,因此高性能代码应该记忆结果以避免重复(如果可能的话)。 - Kevin

7

其他的建议有点晦涩,所以我想做出贡献:

Object.prop = function(obj, prop, val){
    var props = prop.split('.')
      , final = props.pop(), p 
    while(p = props.shift()){
        if (typeof obj[p] === 'undefined')
            return undefined;
        obj = obj[p]
    }
    return val ? (obj[final] = val) : obj[final]
}

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

// get
console.log(Object.prop(obj, 'a.c')) // -> 2
// set
Object.prop(obj, 'a.c', function(){})
console.log(obj) // -> { a: { b: '1', c: [Function] } }

需要解释一下。请通过编辑(更改)您的答案进行回复,而不是在评论中回复(不要包含“Edit:”、“Update:”或类似内容——答案应该看起来像是今天写的)。 - Peter Mortensen
@PeterMortensen,你晚了大约十年。无论如何,我认为这个答案现在都不是特别好的选择。 - Ricardo Tomasi

5
var a = { b: { c: 9 } };

function value(layer, path, value) {
    var i = 0,
        path = path.split('.');

    for (; i < path.length; i++)
        if (value != null && i + 1 === path.length)
            layer[path[i]] = value;
        layer = layer[path[i]];

    return layer;
};

value(a, 'b.c'); // 9

value(a, 'b.c', 4);

value(a, 'b.c'); // 4

与更简单的eval方法相比,这是大量代码,但正如Simon Willison所说,您永远不应该使用eval

此外,JSFiddle也可以参考。


value(a,'b.b.b.b.b') 不会返回 undefined - frumbert

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接