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

IT技术 javascript
2021-01-31 22:43:18

给定一个 JavaScript 对象,

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

和一个字符串

"a.b"

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

var val = obj.a.b

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

6个回答

最近的笔记:虽然我很高兴这个答案得到了很多人的支持,但我也有点害怕。如果需要将诸如“xabc”之类的点符号字符串转换为引用,这可能(可能)表明发生了一些非常错误的事情(除非您可能正在执行一些奇怪的反序列化)。

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

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

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

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

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

  • 这也许没问题。请注意没有点字符串“。” 在您清理过的输入片段中。

如果您发现自己一直在使用这个答案并在字符串和数组之间来回转换,那么您可能处于糟糕的情况,应该考虑另一种选择。

这是一个优雅的单线,比其他解决方案短 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.etcgivenobj和 string "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 编译器正在执行的优化,您可能希望确保不会通过常用方法(将它们放在闭包、对象或全局命名空间中)在每次调用时重新定义任何嵌套函数。

编辑

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

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

(旁注:遗憾的是不能用 Setter 返回一个对象,因为这会违反调用约定;评论者似乎指的是一个具有副作用的通用 setter 风格的函数,比如index(obj,"a.b.etc", value)do 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]”?——亚历克斯

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

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

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

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

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

让我们暂时忽略我们当然可以在语法中合法地做其他事情,例如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 identifier

一个简单的回答你的问题,虽然,如果你的字符串不包含逗号或支架,将只是以不相匹配的字符集的长度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年特别编辑:

为了句法纯度hamfistery的利益,让我们转一圈,做我们能想出的最低效、最可怕的超编程解决方案使用 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']);

输出:

对象是:{"a":{"b":{"c":1,"d":2}}}

(代理覆盖获取)objHyper['abc'] 是:1

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

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

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

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

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


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

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

  2. 这并没有从这个角度说,我们正在做的意义x['a']['x']['c'],这将失败,并在上面的例子中一个类型错误。

也就是说,您可以通过将您的减少功能替换为:

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

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

是的,但很容易将这两行代码放入一个函数中。 var setget = function( obj, path ){ function index( robj,i ) {return robj[i]}; return path.split('.').reduce( index, obj ); }
2021-03-16 22:43:18
我喜欢这个优雅的例子,谢谢 ninjagecko。我已经将它扩展到处理数组样式符号,以及空字符串 - 在这里查看我的示例:jsfiddle.net/sc0ttyd/q7zyd
2021-04-01 22:43:18
@Ricardo:Array.reduce是 ECMA-262 标准的一部分。如果你真的希望支持过时的浏览器,你可以定义Array.prototype.reduce给某个地方的示例实现(例如developer.mozilla.org/en/JavaScript/Reference/Global_Objects/...)。
2021-04-02 22:43:18
@ninjagecko 你如何把它变成一个二传手?不仅按路径返回值,而且在将新值发送到函数时设置它们?
2021-04-03 22:43:18
reduce 并非所有当前使用的浏览器都支持。
2021-04-04 22:43:18

如果您可以使用Lodash,则有一个函数可以做到这一点:

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

var val = _.get(obj, "a.b");
@B先生 最新版本的 Lodash 有第三个可选的参数defaultValue_.get()如果_.get()解析为undefined方法将返回默认值,因此将其设置为您想要的任何值并注意您设置的值。
2021-03-29 22:43:18
对于任何想知道的人,它也支持_.set(object, path, value).
2021-04-01 22:43:18
注意:_.get(object, path)如果未找到路径,则不会中断。'a.b.etc'.split('.').reduce((o,i)=>o[i], obj)做。对于我的具体情况 - 不是每种情况 - 正是我所需要的。谢谢!
2021-04-03 22:43:18

你可以使用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';

一个更复杂的递归示例。

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

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

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