给定一个 JavaScript 对象,
var obj = { a: { b: '1', c: '2' } }
和一个字符串
"a.b"
如何将字符串转换为点号表示法,以便我可以转到相应的对象?
var val = obj.a.b
如果字符串只是'a'
,我可以使用obj[a]
。但这更复杂。我想有一些简单的方法,但它暂时逃脱了我的注意力。如果字符串只是
'a'
,我可以使用obj['a']
。但这个问题更加复杂。我想可能有一些直接的方法,但目前我还无法想出来。给定一个 JavaScript 对象,
var obj = { a: { b: '1', c: '2' } }
和一个字符串
"a.b"
如何将字符串转换为点号表示法,以便我可以转到相应的对象?
var val = obj.a.b
如果字符串只是'a'
,我可以使用obj[a]
。但这更复杂。我想有一些简单的方法,但它暂时逃脱了我的注意力。'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
和字符串"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')
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)...
从“我们应该返回undefined而不是抛出错误”的1维索引情况({})['e.g.']==undefined的角度来看,这是有道理的,因此“我们应该返回undefined而不是抛出错误”在N维情况下。
从我们正在进行x['a']['x']['c']
的角度来看,这是没有意义的,在上面的示例中会导致TypeError。
也就是说,您可以用以下任一减少函数替换它:
(o,i)=> o===undefined?undefined:o[i]
,或
(o,i)=> (o||{})[i]
。
(如果您预计此类故障发生的频率足够低,则可以通过使用for循环并在下一个索引到的子结果为undefined时中断/返回,或者使用try-catch来使其更有效率。)
reduce
在当前被使用的所有浏览器中均不受支持。 - Ricardo TomasiArray.reduce
是 ECMA-262 标准的一部分。如果你真的想支持过时的浏览器,可以将 Array.prototype.reduce
定义为给定某处的示例实现(例如 https://developer.mozilla.org/en/JavaScript/Reference/Global_Objects/Array/Reduce#Compatibility)。 - ninjageckovar setget = function( obj, path ){ function index( robj,i ) {return robj[i]}; return path.split('.').reduce( index, obj ); }
- nevf_.get(object, path)
不会因为找不到路径而出错,但 'a.b.etc'.split('.').reduce((o,i)=>o[i], obj)
会。对于我的特定情况,这正是我所需要的,但并非所有情况都适用。谢谢! - Mr. B.defaultValue
。如果 _.get()
方法解析为 undefined
,则该方法返回默认值,因此将其设置为所需内容,并观察该值。 - steampowered_.set(object, path, value)
。 - Jeffrey Roosendaal?.
来防止对象访问中断:'a.b.etc'.split('.').reduce((o,i)=>o?.[i], obj) - Michael Buennpm 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';
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
set(obj, "a", 1)
后紧接着set(obj, "a.b", 2)
的情况。第二次调用试图访问a
上的b
属性,但是a
已经被设置为一个非对象(数字)。当考虑到对象可能包含数组,数组可能包含对象,对象可能包含数组时,情况变得更加复杂。 - undefinedset
函数的示例。 - undefined一个更加深入的递归示例。
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'));
原帖发布多年了,现在有一个很棒的库叫做 'object-path'。
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");
同时适用于深度嵌套的属性和数组。
var path = 'a.b.x';
var getter = new Function("obj", "return obj." + path + ";");
getter(obj);
var setter = new Function("obj", "newval", "obj." + path + " = newval;");
setter(obj, "some new val");
其他的建议有点晦涩,所以我想做出贡献:
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] } }
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也可以参考。
eval
是有害的,请勿使用。 - Kevin Ji