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

nev*_*evf 188 javascript

给定一个JS对象

var obj = { a: { b: '1', c: '2' } }
Run Code Online (Sandbox Code Playgroud)

和一个字符串

"a.b"
Run Code Online (Sandbox Code Playgroud)

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

var val = obj.a.b
Run Code Online (Sandbox Code Playgroud)

如果字符串只是'a'我可以使用,'a'但这更复杂.我想有一些简单的方法,但它目前逃脱了.

nin*_*cko 390

最近的一点:虽然我很高兴这个答案得到了很多赞成,但我也有点害怕.如果需要将诸如"xabc"之类的点符号字符串转换为引用,则可能表明存在一些非常错误(除非您执行某些奇怪的反序列化).它是矫枉过正,因为它是不必要的元编程,也有点违反功能性无副作用编码风格.此外,如果您执行的操作超出了您的需要,也可以获得大量的性能命中(例如,作为应用程序的默认形式,即传递对象并取消引用它们).如果由于某种原因这是服务器端的js,通常用于输入的清理.找到回答这个问题的新手应该考虑使用数组表示法,例如['x','a','b','c'],或者甚至是更直接/简单/直接的东西,如果可能的话,不要丢失跟踪引用本身,或者可能是一些预先存在的唯一ID等.

这是一款优雅的单线,比其他解决方案短10倍:

function index(obj,i) {return obj[i]}
'a.b.etc'.split('.').reduce(index, obj)
Run Code Online (Sandbox Code Playgroud)

[编辑]或在ECMAScript 6中:

'a.b.etc'.split('.').reduce((o,i)=>o[i], obj)
Run Code Online (Sandbox Code Playgroud)

(并不是说我认为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')
Run Code Online (Sandbox Code Playgroud)

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

编辑:

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

你怎么会把它变成一个二传手呢?不仅要按路径返回值,还要在将新值发送到函数时设置它们? - Swader 6月28日21:42

(旁注:遗憾的是不能与二传手返回一个对象,因为这将违反调用约定;评论者似乎改为参照与像副作用一般二传手式的功能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);
}
Run Code Online (Sandbox Code Playgroud)

演示:

> 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
Run Code Online (Sandbox Code Playgroud)

...虽然我个人推荐单独的功能setIndex(...).我想以旁注来结束,问题的原始提出者可以(应该?)使用索引数组(它们可以从中得到.split),而不是字符串; 虽然便利功能通常没什么问题.


一位评论者问:

数组怎么样?像"ab [4] .cd [1] [2] [3]"这样的东西?-AlexS

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

Javascript Arrays(它们本身就是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+序列不在集合,[]:

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

如果你的字符串不包含转义字符或"字符,并且因为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"]
Run Code Online (Sandbox Code Playgroud)

当然,要小心,永远不要相信你的数据.执行此操作的一些不良方法可能适用于某些用例还包括:

// 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
Run Code Online (Sandbox Code Playgroud)

特别2018编辑:

让我们走完整圈,做一个我们可以提出的最低效,可怕的过度编程的解决方案......为了语法纯度的混合.使用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);
}
Run Code Online (Sandbox Code Playgroud)

演示:

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']);
Run Code Online (Sandbox Code Playgroud)

输出:

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

(代理覆盖get)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只是接受一个数组作为输入keys = ['a','b','c']; obj.H[keys].


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

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

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

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

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

(您可以通过使用for循环并在下次索引的子结构未定义时断开/返回来提高效率,或者使用try-catch,如果您认为这样的故障非常罕见.)

  • @Ricardo:`Array.reduce`是ECMA-262标准的一部分.如果你真的希望支持过时的浏览器,可以在某处给出示例实现定义`Array.prototype.reduce`(例如https://developer.mozilla.org/en/JavaScript/Reference/Global_Objects/Array/Reduce#Compatibility ). (13认同)
  • 所有当前使用的浏览器都不支持`reduce`. (4认同)
  • 是的,但很容易将两行放入一个函数中.`var setget = function(obj,path){function index(robj,i){return robj [i]}; return path.split('.').reduce(index,obj); } (2认同)
  • 我喜欢这个优雅的例子,感谢ninjagecko.我已经扩展它来处理数组样式表示法,以及空字符串 - 请参阅我的示例:http://jsfiddle.net/sc0ttyd/q7zyd/ (2认同)
  • @ninjagecko你怎么会把它变成一个二传手呢?不仅要按路径返回值,还要在将新值发送到函数时设置它们? (2认同)

Pet*_*nov 57

如果你可以使用lodash,那么有一个函数,它正是这样做的:

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

var val = _.get(obj, "a.b");
Run Code Online (Sandbox Code Playgroud)

  • 对于任何想知道的人,它也支持`_.set(object,path,value)`. (6认同)
  • 注意:如果找不到路径,`_.get(object,path)`不会中断.`'abetc'.split('.').reduce((o,i)=> o [i],obj)`的确如此.对于我的具体情况 - 不是每个案例 - 正是我需要的.谢谢! (4认同)

Tha*_*you 34

2023年

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

通过一行代码,我们可以编写get接受输入对象t和 string 的代码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"))
Run Code Online (Sandbox Code Playgroud)

"hello"
"world"
undefined
Run Code Online (Sandbox Code Playgroud)

set当您考虑对象的值可能是其他对象或数组时,该操作是一个不平凡的函数。我们应该如何处理不存在的对象或数组的深度集?我们应该一路创造它们吗?那么如何解决这样的冲突呢?

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

我们将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"))
Run Code Online (Sandbox Code Playgroud)

设置(高级)

但是,如果我们想要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))
Run Code Online (Sandbox Code Playgroud)
.as-console-wrapper { min-height: 100%; top: 0; }
Run Code Online (Sandbox Code Playgroud)

如果我们尝试在已设置的非对象值上设置键,则会引发运行时错误 -

const mydata = { a: 1 }
set(mydata, "a.foo", "bar")
// Error: cannot set property "foo" of number
Run Code Online (Sandbox Code Playgroud)


joe*_*arl 18

一个更复杂的递归示例.

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

alert(recompose(obj,'a.d.a.b')); //blah
Run Code Online (Sandbox Code Playgroud)


小智 13

你也可以使用lodash.get

你只需安装这个软件包(npm i --save lodash.get),然后像这样使用它:

const get = require('lodash.get');

const myObj = { user: { firstName: 'Stacky', lastName: 'Overflowy' }, id: 123 };

console.log(get(myObj, 'user.firstName')); // prints Stacky
console.log(get(myObj, 'id')); //prints  123

//You can also update values
get(myObj, 'user').firstName = John;
Run Code Online (Sandbox Code Playgroud)


Kev*_*ley 9

如果您希望多次取消引用相同的路径,那么为每个点符号路径构建函数实际上具有迄今为止最好的性能(扩展了在上面的注释中与James Wilkins相关的性能测试).

var path = 'a.b.x';
var getter = new Function("obj", "return obj." + path + ";");
getter(obj);
Run Code Online (Sandbox Code Playgroud)

在安全性和最坏情况性能方面,使用Function构造函数与eval()有一些相同的缺点,但对于需要极端动态和高性能组合的情况,IMO是一个使用不当的工具.我使用这种方法来构建数组过滤器函数,并在AngularJS摘要循环中调用它们.我的配置文件始终显示array.filter()步骤花费不到1毫秒来取消引用和过滤约2000个复杂对象,使用3-4级深度的动态定义路径.

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

var setter = new Function("obj", "newval", "obj." + path + " = newval;");
setter(obj, "some new val");
Run Code Online (Sandbox Code Playgroud)


LPG*_*LPG 7

自原帖以来多年.现在有一个很棒的库叫做"对象路径". https://github.com/mariocasciaro/object-path

可在NPM和BOWER上使用 https://www.npmjs.com/package/object-path

它很简单:

objectPath.get(obj, "a.c.1");  //returns "f"
objectPath.set(obj, "a.j.0.f", "m");
Run Code Online (Sandbox Code Playgroud)

适用于深层嵌套的属性和数组.


小智 6

您可以使用 npm 提供的库,这可以简化此过程。https://www.npmjs.com/package/dot-object

 var dot = require('dot-object');

var obj = {
 some: {
   nested: {
     value: 'Hi there!'
   }
 }
};

var val = dot.pick('some.nested.value', obj);
console.log(val);

// Result: Hi there!
Run Code Online (Sandbox Code Playgroud)


McK*_*yla 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
Run Code Online (Sandbox Code Playgroud)

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

此外,JSFiddle


Ric*_*asi 5

其他建议有点神秘,所以我想我会做出贡献:

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] } }
Run Code Online (Sandbox Code Playgroud)


Tam*_*lyn 5

请注意,如果您已经在使用Lodash,则可以使用propertyget函数:

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

下划线也具有property功能,但不支持点表示法。


Nin*_*olz 5

我建议拆分路径并对其进行迭代,并减少所拥有的对象。该建议使用缺少属性的默认值

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'));
Run Code Online (Sandbox Code Playgroud)


Mar*_*ich -1

目前尚不清楚你的问题是什么。给定你的对象,obj.a.b会给你“2”,就像它一样。如果你想操作字符串以使用括号,你可以这样做:

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