117

我有一个对象可以是任意数量的深度并且可以具有任何现有属性。例如:

var obj = {
    db: {
        mongodb: {
            host: 'localhost'
        }
    }
};

对此,我想设置(或覆盖)如下属性:

set('db.mongodb.user', 'root');
// or:
set('foo.bar', 'baz');

属性字符串可以有任何深度,值可以是任何类型/事物。
如果属性键已经存在,则不需要合并作为值的对象和数组。

上一个示例将生成以下对象:

var obj = {
    db: {
        mongodb: {
            host: 'localhost',
            user: 'root'
        }
    },
    foo: {
        bar: baz
    }
};

我怎样才能实现这样的功能?

4

26 回答 26

122

这个函数,使用你指定的参数,应该添加/更新obj容器中的数据。请注意,您需要跟踪obj架构中的哪些元素是容器,哪些是值(字符串、整数等),否则您将开始抛出异常。

obj = {};  // global object

function set(path, value) {
    var schema = obj;  // a moving reference to internal objects within obj
    var pList = path.split('.');
    var len = pList.length;
    for(var i = 0; i < len-1; i++) {
        var elem = pList[i];
        if( !schema[elem] ) schema[elem] = {}
        schema = schema[elem];
    }

    schema[pList[len-1]] = value;
}

set('mongo.db.user', 'root');
于 2013-09-21T20:13:23.823 回答
101

Lodash 有一个_.set()方法。

_.set(obj, 'db.mongodb.user', 'root');
_.set(obj, 'foo.bar', 'baz');
于 2016-07-27T15:03:02.967 回答
21

我只是使用 ES6 + 递归编写了一个小函数来实现目标。

updateObjProp = (obj, value, propPath) => {
    const [head, ...rest] = propPath.split('.');

    !rest.length
        ? obj[head] = value
        : this.updateObjProp(obj[head], value, rest.join('.'));
}

const user = {profile: {name: 'foo'}};
updateObjProp(user, 'fooChanged', 'profile.name');

我在响应更新状态时经常使用它,它对我来说效果很好。

于 2018-05-17T13:00:49.150 回答
19

有点晚了,但这是一个非图书馆,更简单的答案:

/**
 * Dynamically sets a deeply nested value in an object.
 * Optionally "bores" a path to it if its undefined.
 * @function
 * @param {!object} obj  - The object which contains the value you want to change/set.
 * @param {!array} path  - The array representation of path to the value you want to change/set.
 * @param {!mixed} value - The value you want to set it to.
 * @param {boolean} setrecursively - If true, will set value of non-existing path as well.
 */
function setDeep(obj, path, value, setrecursively = false) {
    path.reduce((a, b, level) => {
        if (setrecursively && typeof a[b] === "undefined" && level !== path.length){
            a[b] = {};
            return a[b];
        }

        if (level === path.length){
            a[b] = value;
            return value;
        } 
        return a[b];
    }, obj);
}

我制作的这个功能可以完全满足您的需求,甚至更多。

假设我们要更改深深嵌套在此对象中的目标值:

let myObj = {
    level1: {
        level2: {
           target: 1
       }
    }
}

所以我们会这样调用我们的函数:

setDeep(myObj, ["level1", "level2", "target1"], 3);

将导致:

myObj = { level1: { level2: { target: 3 } } }

将 set recursively 标志设置为 true 将设置对象(如果它们不存在)。

setDeep(myObj, ["new", "path", "target"], 3, true);

将导致:

obj = myObj = {
    new: {
         path: {
             target: 3
         }
    },
    level1: {
        level2: {
           target: 3
       }
    }
}
于 2017-09-01T22:50:10.330 回答
14

我们可以使用递归函数:

/**
 * Sets a value of nested key string descriptor inside a Object.
 * It changes the passed object.
 * Ex:
 *    let obj = {a: {b:{c:'initial'}}}
 *    setNestedKey(obj, ['a', 'b', 'c'], 'changed-value')
 *    assert(obj === {a: {b:{c:'changed-value'}}})
 *
 * @param {[Object]} obj   Object to set the nested key
 * @param {[Array]} path  An array to describe the path(Ex: ['a', 'b', 'c'])
 * @param {[Object]} value Any value
 */
export const setNestedKey = (obj, path, value) => {
  if (path.length === 1) {
    obj[path] = value
    return
  }
  return setNestedKey(obj[path[0]], path.slice(1), value)
}

它更简单!

于 2018-04-10T13:14:08.610 回答
11

受@bpmason1 的回答启发:

function leaf(obj, path, value) {
  const pList = path.split('.');
  const key = pList.pop();
  const pointer = pList.reduce((accumulator, currentValue) => {
    if (accumulator[currentValue] === undefined) accumulator[currentValue] = {};
    return accumulator[currentValue];
  }, obj);
  pointer[key] = value;
  return obj;
}

例子:

const obj = {
  boats: {
    m1: 'lady blue'
  }
};
leaf(obj, 'boats.m1', 'lady blue II');
leaf(obj, 'boats.m2', 'lady bird');
console.log(obj); // { boats: { m1: 'lady blue II', m2: 'lady bird' } }
于 2017-10-18T20:28:47.427 回答
8

Lodash 有一个名为update的方法,它可以完全满足您的需求。

该方法接收以下参数:

  1. 要更新的对象
  2. 要更新的属性的路径(属性可以深度嵌套)
  3. 返回要更新的值的函数(给定原始值作为参数)

在您的示例中,它看起来像这样:

_.update(obj, 'db.mongodb.user', function(originalValue) {
  return 'root'
})
于 2016-04-14T14:56:21.420 回答
7

ES6 也有一个很酷的方法来做到这一点,使用Computed Property NameRest Parameter

const obj = {
  levelOne: {
    levelTwo: {
      levelThree: "Set this one!"
    }
  }
}

const updatedObj = {
  ...obj,
  levelOne: {
    ...obj.levelOne,
    levelTwo: {
      ...obj.levelOne.levelTwo,
      levelThree: "I am now updated!"
    }
  }
}

如果levelThree是动态属性,即设置其中的任何属性levelTwo,则可以使用[propertyName]: "I am now updated!"wherepropertyName保存该属性的名称levelTwo

于 2018-04-18T04:35:57.117 回答
6

我使用纯 es6 和不会改变原始对象的递归提出了自己的解决方案。

const setNestedProp = (obj = {}, [first, ...rest] , value) => ({
  ...obj,
  [first]: rest.length
    ? setNestedProp(obj[first], rest, value)
    : value
});

const result = setNestedProp({}, ["first", "second", "a"], 
"foo");
const result2 = setNestedProp(result, ["first", "second", "b"], "bar");

console.log(result);
console.log(result2);

于 2020-07-30T16:35:24.290 回答
2

我创建了根据正确答案通过字符串设置和获取 obj 值的要点。您可以下载它或将其用作 npm/yarn 包。

// yarn add gist:5ceba1081bbf0162b98860b34a511a92
// npm install gist:5ceba1081bbf0162b98860b34a511a92
export const DeepObject = {
  set: setDeep,
  get: getDeep
};

// https://stackoverflow.com/a/6491621
function getDeep(obj: Object, path: string) {
  path = path.replace(/\[(\w+)\]/g, '.$1'); // convert indexes to properties
  path = path.replace(/^\./, '');           // strip a leading dot
  const a = path.split('.');
  for (let i = 0, l = a.length; i < l; ++i) {
    const n = a[i];
    if (n in obj) {
      obj = obj[n];
    } else {
      return;
    }
  }

  return obj;
}

// https://stackoverflow.com/a/18937118
function setDeep(obj: Object, path: string, value: any) {
  let schema = obj;  // a moving reference to internal objects within obj
  const pList = path.split('.');
  const len = pList.length;
  for (let i = 0; i < len - 1; i++) {
    const elem = pList[i];
    if (!schema[elem]) {
      schema[elem] = {};
    }
    schema = schema[elem];
  }

  schema[pList[len - 1]] = value;
}

// Usage
// import {DeepObject} from 'somePath'
//
// const obj = {
//   a: 4,
//   b: {
//     c: {
//       d: 2
//     }
//   }
// };
//
// DeepObject.set(obj, 'b.c.d', 10); // sets obj.b.c.d to 10
// console.log(DeepObject.get(obj, 'b.c.d')); // returns 10
于 2018-09-10T18:01:14.767 回答
2

我需要实现同样的事情,但是在 Node.js 中......所以,我发现了这个不错的模块:https ://www.npmjs.com/package/nested-property

例子:

var mod = require("nested-property");
var obj = {
  a: {
    b: {
      c: {
        d: 5
      }
    }
  }
};
console.log(mod.get(obj, "a.b.c.d"));
mod.set(obj, "a.b.c.d", 6);
console.log(mod.get(obj, "a.b.c.d"));
于 2019-04-30T13:15:36.300 回答
2

扩展@bpmason1提供的已接受答案,以支持字符串路径中的数组,例如字符串路径可以是'db.mongodb.users[0].name'and 'db.mongodb.users[1].name'

它将设置属性值,如果不存在,将被创建。

var obj = {};

function set(path, value) {
  var schema = obj;
  var keysList = path.split('.');
  var len = keysList.length;
  for (var i = 0; i < len - 1; i++) {
    var key = keysList[i];
    // checking if key represents an array element e.g. users[0]
    if (key.includes('[')) {
      //getting propertyName 'users' form key 'users[0]'
      var propertyName = key.substr(0, key.length - key.substr(key.indexOf("["), key.length - key.indexOf("[")).length);
      if (!schema[propertyName]) {
        schema[propertyName] = [];
      }
      // schema['users'][getting index 0 from 'users[0]']
      if (!schema[propertyName][parseInt(key.substr(key.indexOf("[") + 1, key.indexOf("]") - key.indexOf("[") - 1))]) {
        // if it doesn't exist create and initialise it
        schema = schema[propertyName][parseInt(key.substr(key.indexOf("[") + 1, key.indexOf("]") - key.indexOf("[") - 1))] = {};
      } else {
        schema = schema[propertyName][parseInt(key.substr(key.indexOf("[") + 1, key.indexOf("]") - key.indexOf("[") - 1))];
      }
      continue;
    }
    if (!schema[key]) {
      schema[key] = {};
    }
    schema = schema[key];
  } //loop ends
  // if last key is array element
  if (keysList[len - 1].includes('[')) {
    //getting propertyName 'users' form key 'users[0]'
    var propertyName = keysList[len - 1].substr(0, keysList[len - 1].length - keysList[len - 1].substr(keysList[len - 1].indexOf("["), keysList[len - 1].length - keysList[len - 1].indexOf("[")).length);
    if (!schema[propertyName]) {
      schema[propertyName] = [];
    }
    // schema[users][0] = value;
    schema[propertyName][parseInt(keysList[len - 1].substr(keysList[len - 1].indexOf("[") + 1, keysList[len - 1].indexOf("]") - keysList[len - 1].indexOf("[") - 1))] = value;
  } else {
    schema[keysList[len - 1]] = value;
  }
}

// will create if not exist
set("mongo.db.users[0].name.firstname", "hii0");
set("mongo.db.users[1].name.firstname", "hii1");
set("mongo.db.users[2].name", {
  "firstname": "hii2"
});
set("mongo.db.other", "xx");
console.log(obj);

// will set if exist
set("mongo.db.other", "yy");
console.log(obj);

于 2021-06-16T16:32:24.663 回答
1

如果您只需要更改更深的嵌套对象,那么另一种方法可能是引用该对象。由于 JS 对象由它们的引用处理,因此您可以创建对您具有字符串键访问权限的对象的引用。

例子:

// The object we want to modify:
var obj = {
    db: {
        mongodb: {
            host: 'localhost',
            user: 'root'
        }
    },
    foo: {
        bar: baz
    }
};

var key1 = 'mongodb';
var key2 = 'host';

var myRef = obj.db[key1]; //this creates a reference to obj.db['mongodb']

myRef[key2] = 'my new string';

// The object now looks like:
var obj = {
    db: {
        mongodb: {
            host: 'my new string',
            user: 'root'
        }
    },
    foo: {
        bar: baz
    }
};
于 2013-12-29T07:47:19.007 回答
1

另一种方法是使用递归来挖掘对象:

(function(root){

  function NestedSetterAndGetter(){
    function setValueByArray(obj, parts, value){

      if(!parts){
        throw 'No parts array passed in';
      }

      if(parts.length === 0){
        throw 'parts should never have a length of 0';
      }

      if(parts.length === 1){
        obj[parts[0]] = value;
      } else {
        var next = parts.shift();

        if(!obj[next]){
          obj[next] = {};
        }
        setValueByArray(obj[next], parts, value);
      }
    }

    function getValueByArray(obj, parts, value){

      if(!parts) {
        return null;
      }

      if(parts.length === 1){
        return obj[parts[0]];
      } else {
        var next = parts.shift();

        if(!obj[next]){
          return null;
        }
        return getValueByArray(obj[next], parts, value);
      }
    }

    this.set = function(obj, path, value) {
      setValueByArray(obj, path.split('.'), value);
    };

    this.get = function(obj, path){
      return getValueByArray(obj, path.split('.'));
    };

  }
  root.NestedSetterAndGetter = NestedSetterAndGetter;

})(this);

var setter = new this.NestedSetterAndGetter();

var o = {};
setter.set(o, 'a.b.c', 'apple');
console.log(o); //=> { a: { b: { c: 'apple'}}}

var z = { a: { b: { c: { d: 'test' } } } };
setter.set(z, 'a.b.c', {dd: 'zzz'}); 

console.log(JSON.stringify(z)); //=> {"a":{"b":{"c":{"dd":"zzz"}}}}
console.log(JSON.stringify(setter.get(z, 'a.b.c'))); //=> {"dd":"zzz"}
console.log(JSON.stringify(setter.get(z, 'a.b'))); //=> {"c":{"dd":"zzz"}}
于 2014-11-20T16:50:25.200 回答
1

迟到 - 这是一个香草 js 函数,它接受路径作为参数并返回修改后的对象/json

let orig_json = {
  string: "Hi",
  number: 0,
  boolean: false,
  object: {
    subString: "Hello",
    subNumber: 1,
    subBoolean: true,
    subObject: {
      subSubString: "Hello World"
    },
    subArray: ["-1", "-2", "-3"]
  },
  array: ["1", "2", "3"]
}

function changeValue(obj_path, value, json) {
  let keys = obj_path.split(".")
  let obj = { ...json },
    tmpobj = {},
    prevobj = {}
  for (let x = keys.length - 1; x >= 0; x--) {
    if (x == 0) {
      obj[keys[0]] = tmpobj
    } else {
      let toeval = 'json.' + keys.slice(0, x).join('.');
      prevobj = { ...tmpobj
      }
      tmpobj = eval(toeval);
      if (x == keys.length - 1) tmpobj[keys[x]] = value
      else {
        tmpobj[keys[x]] = prevobj
      }
    }
  }
  return obj
}

let newjson = changeValue("object.subObject.subSubString", "Goodbye world", orig_json);
console.log(newjson)

于 2021-05-28T20:43:16.233 回答
1

添加或覆盖属性的另一种解决方案:

function propertySetter(property, value) {
  const sampleObject = {
    string: "Hi",
    number: 0,
    boolean: false,
    object: {
      subString: "Hello",
      subNumber: 1,
      subBoolean: true,
      subObject: {
        subSubString: "Hello World",
      },
      subArray: ["-1", "-2", "-3"],
    },
    array: ["1", "2", "3"],
  };

  const keys = property.split(".");
  const propertyName = keys.pop();
  let propertyParent = sampleObject;
  while (keys.length > 0) {
    const key = keys.shift();
    if (!(key in propertyParent)) {
      propertyParent[key] = {};
    }
    propertyParent = propertyParent[key];
  }
  propertyParent[propertyName] = value;
  return sampleObject;
}

console.log(propertySetter("object.subObject.anotherSubString", "Hello you"));

console.log(propertySetter("object.subObject.subSubString", "Hello Earth"));

console.log(propertySetter("object.subObject.nextSubString.subSubSubString", "Helloooo"));

于 2021-05-28T21:08:03.137 回答
1

受 ImmutableJS setIn 方法的启发,该方法永远不会改变原始方法。这适用于混合数组和对象嵌套值。

function setIn(obj = {}, [prop, ...rest], value) {
    const newObj = Array.isArray(obj) ? [...obj] : {...obj};
    newObj[prop] = rest.length ? setIn(obj[prop], rest, value) : value;
    return newObj;
}

var obj = {
  a: {
    b: {
      c: [
        {d: 5}
      ]
    }
  }
};

const newObj = setIn(obj, ["a", "b", "c", 0, "x"], "new");

//obj === {a: {b: {c: [{d: 5}]}}}
//newObj === {a: {b: {c: [{d: 5, x: "new"}]}}}
于 2021-08-28T17:28:10.620 回答
0

如果您想要一个需要先前属性存在的函数,那么您可以使用类似的东西,它还会返回一个标志,说明它是否设法找到并设置嵌套属性。

function set(obj, path, value) {
    var parts = (path || '').split('.');
    // using 'every' so we can return a flag stating whether we managed to set the value.
    return parts.every((p, i) => {
        if (!obj) return false; // cancel early as we havent found a nested prop.
        if (i === parts.length - 1){ // we're at the final part of the path.
            obj[parts[i]] = value;          
        }else{
            obj = obj[parts[i]]; // overwrite the functions reference of the object with the nested one.            
        }   
        return true;        
    });
}
于 2018-09-10T18:29:38.423 回答
0

受 ClojureScript 的启发assoc-inhttps://github.com/clojure/clojurescript/blob/master/src/main/cljs/cljs/core.cljs#L5280),使用递归:

/**
 * Associate value (v) in object/array (m) at key/index (k).
 * If m is falsy, use new object.
 * Returns the updated object/array.
 */
function assoc(m, k, v) {
    m = (m || {});
    m[k] = v;
    return m;
}

/**
 * Associate value (v) in nested object/array (m) using sequence of keys (ks)
 * to identify the path to the nested key/index.
 * If one of the values in the nested object/array doesn't exist, it adds
 * a new object.
 */
function assoc_in(m={}, [k, ...ks], v) {
    return ks.length ? assoc(m, k, assoc_in(m[k], ks, v)) : assoc(m, k, v);
}

/**
 * Associate value (v) in nested object/array (m) using key string notation (s)
 * (e.g. "k1.k2").
 */
function set(m, s, v) {
    ks = s.split(".");
    return assoc_in(m, ks, v);
}

笔记:

通过提供的实现,

assoc_in({"a": 1}, ["a", "b"], 2) 

返回

{"a": 1}

在这种情况下,我希望它会引发错误。如果需要,您可以添加签入assoc以验证m是对象还是数组,否则会引发错误。

于 2019-09-10T17:21:59.263 回答
0

我试着简单地写下这个set 方法,它可能对某人有帮助!

function set(obj, key, value) {
 let keys = key.split('.');
 if(keys.length<2){ obj[key] = value; return obj; }

 let lastKey = keys.pop();

 let fun = `obj.${keys.join('.')} = {${lastKey}: '${value}'};`;
 return new Function(fun)();
}

var obj = {
"hello": {
    "world": "test"
}
};

set(obj, "hello.world", 'test updated'); 
console.log(obj);

set(obj, "hello.world.again", 'hello again'); 
console.log(obj);

set(obj, "hello.world.again.onece_again", 'hello once again');
console.log(obj);

于 2020-07-01T20:08:59.547 回答
0
const set = (o, path, value) => {
    const props = path.split('.');
    const prop = props.shift()
    if (props.length === 0) {
        o[prop] = value
    } else {
        o[prop] = o[prop] ?? {}
        set(o[prop], props.join('.'), value)
    }
}
于 2021-02-04T15:22:59.523 回答
0

如果您想深入更新或插入对象,请尝试以下操作:-

 let init = {
       abc: {
           c: {1: 2, 3: 5, 0: {l: 3}},
           d: 100
       }
    }
    Object.prototype.deepUpdate = function(update){
       let key = Object.keys(update);
       key.forEach((k) => {
           if(typeof update[key] == "object"){
              this[k].deepUpdate(update[key], this[k])
           }
           else 
           this[k] = update[k]
       })
    }

    init.deepUpdate({abc: {c: {l: 10}}})
    console.log(init)

但请确保它会更改原始对象,您可以使其不更改原始对象:

JSON.parse(JSON.stringify(init)).deepUpdate({abc: {c: {l: 10}}})
于 2021-10-02T17:00:11.823 回答
0

这是使用 ES 12 的解决方案

function set(obj = {}, key, val) {
  const keys = key.split('.')
  const last = keys.pop()
  keys.reduce((o, k) => o[k] ??= {}, obj)[last] = val
}

(对于旧版本的 javascript,您可以o[k] || o[k] = {}改为在 reduce 中执行)

首先,我们设置keys为除最后一个键之外的所有内容的数组。

然后在 reduce 中,累加器每次都深入一层obj ,如果未定义该键的值,则将其初始化为空对象。

最后,我们将最后一个键的值设置为val

于 2021-11-08T22:14:29.500 回答
0

改进 bpmason1 的答案:-添加一个 get() 函数。- 不需要定义全局存储对象 - 可以从同一个域 iFrame 访问

function set(path, value) 
{
  var schema = parent.document;
  path="data."+path;
  var pList = path.split('.');
  var len = pList.length;
  for(var i = 0; i < len-1; i++) 
  {
    if(!schema[pList[i]]) 
      schema[pList[i]] = {}
    schema = schema[pList[i]];
  }
  schema[pList[len-1]] = value;
}

function get(path) 
{
  path="data."+path;
  var schema=parent.document;
  var pList = path.split('.');
  for(var i = 0; i < pList.length; i++) 
    schema = schema[pList[i]];
  return schema;
}

set('mongo.db.user', 'root');
set('mongo.db.name', 'glen');

console.log(get('mongo.db.name'));  //prints 'glen'
于 2021-11-28T02:29:41.167 回答
0

作为@aheuermann sed,您可以setlodash库中使用,

但是,如果您出于某种原因不想添加lodash到项目中,则可以使用递归函数来设置/覆盖对象中的值。

/**
 * recursion function that called in main function 
 * @param obj initial JSON
 * @param keysList array of keys
 * @param value value that you want to set
 * @returns final JSON
 */
function recursionSet(obj, keysList, value) {
    const key = keysList[0]
    if (keysList.length === 1) return { ...obj, [key]: value }
    return { ...obj, [key]: (recursionSet(obj?.[key] || {}, keysList.slice(1), value)) }
}

/**
 * main function that you can call for set a value in an object by nested keys
 * @param obj initial JSON
 * @param keysString nested keys that seprated by "."
 * @param value value that you want to set
 * @returns final JSON
 */
function objectSet(obj, keysString, value) {
    return recursionSet(obj, keysString.split('.'), value)
}

// simple usage
const a1 = {}
console.log('simple usage:', objectSet(a1, "b.c.d", 5))

// keep the initial data
const a2 = {b:{e: 8}}
console.log('keep the initial data:', objectSet(a2, "b.c.d", 5))

// override data
const a3 = {b:{e: 8, c:2}}
console.log('override data:', objectSet(a3, "b.c.d", 5))

// complex value
const a4 = {b:{e: 8, c:2}}
console.log('complex value:', objectSet(a4, "b.c.d", {f:12}))

于 2022-02-27T05:05:27.073 回答
-1

JQuery 有一个扩展方法:

https://api.jquery.com/jquery.extend/

只需将覆盖作为对象传递,它将合并两者。

于 2019-01-10T22:40:55.523 回答