function isKeyValueObject(value) {
return _.isPlainObject(value);
}
function isNumberLike(value) {
return _.isNumber(value) || !Number.isNaN(Number(value));
}
function isArray(value) {
return _.isArray(value);
}
function isNil(value) {
return value == null;
}
function shouldBeArray(current, parent, key) {
// already array or parent array -> need array
if (isArray(current) || isArray(parent)) {
return true;
}
// number key check
if (typeof key === 'number') {
// {} → object
if (isKeyValueObject(current)) {
return false;
}
// otherwise → default array
return true;
}
return false;
}
function createContainer(current, parent, key) {
// create a value container, need check container need to be an array or object
return shouldBeArray(current, parent, key) ? [] : {};
}
function updateValueInSource(
source,
key,
value
) {
// default object type
let freshSource = isArray(source) ? [...source] : isKeyValueObject(source) ? { ...source } : {};
freshSource[key] = value;
return freshSource;
}
function setInnerSource($source, path, value) {
// no path, replaced with value
if (path.length === 0) {
return value;
}
const parentPath = path.slice(0, path.length - 1);
const currentPath = path.slice(-1)[0];
// only support array & object, default is object
const source = isArray($source) ? [...$source] : isKeyValueObject($source) ? { ...$source } : {};
// if update parentValue, need this ptr to update value
const stack = [];
let parentValue = source;
// collect path
parentPath.forEach(path => {
const nextParentValue = parentValue[path];
let next;
if (isArray(nextParentValue)) {
next = [...nextParentValue];
} else if (isKeyValueObject(nextParentValue)) {
next = { ...nextParentValue };
} else {
next = createContainer(nextParentValue, parentValue, path);
}
// push path trace in stack, then rebuilt obj by stack from end to start
stack.push({
container: parentValue,
key: path,
});
parentValue = next;
});
let currentValue;
if (isArray(parentValue)) {
if (!isNumberLike(currentPath)) {
console.debug('invalid path');
return source;
}
const clone = [...parentValue];
clone[currentPath] = value;
currentValue = clone;
} else if (isKeyValueObject(parentValue)) {
currentValue = {
...parentValue,
[currentPath]: value,
};
}
let nextValue = currentValue;
// back DFS
for (let i = stack.length - 1; i >= 0; i--) {
const { container, key } = stack[i];
const prev = container[key];
if (Object.is(prev, nextValue)) {
nextValue = container;
continue;
}
const clone = isArray(container) ? [...container] : { ...container };
clone[key] = nextValue;
nextValue = clone;
}
return nextValue;
}
const a = {
name: 'ddd',
list: [{ index: 1 }, { index: 2 }, { index: 3 }],
obj: {
age: 12,
location: 'CN',
times: [9, 18],
obj: {
age: 26,
list: [1, 2, 3],
}
},
};
// √
const b = setInnerSource(a, ['name'], 'ccc');
console.log(b);
console.log(a === b);
console.log(a.obj === b.obj);
console.log(a.list === b.list);
console.log(a.list[1] === b.list[1]);
// √
const c = setInnerSource(a, ['list', 1], { index: 22 });
// console.log(c);
// console.log(a === c);
// console.log(a.obj === c.obj);
// console.log(a.list === c.list);
// console.log(a.list[0] === c.list[0]);
// console.log(a.list[1] === c.list[1]);
// √
const d = setInnerSource(a, ['obj', 'location'], 'ZH');
// console.log(d);
// console.log(a === d);
// console.log(a.obj === d.obj);
// console.log(a.list === d.list);
// console.log(a.obj.times === d.obj.times);
// console.log(a.obj.obj === d.obj.obj);
// console.log(a.list[0] === d.list[0]);
// √
const e = setInnerSource(a, ['list', '6'], 'CN');
// console.log(e);
// console.log(e.list);
// console.log(e.list[6]);
// console.log(a === e);
// console.log(a.obj === e.obj);
// console.log(a.list === e.list);
// console.log(a.obj.times === e.obj.times);
// console.log(a.obj.obj === e.obj.obj);
// console.log(a.list[0] === e.list[0]);
const f = setInnerSource(a, ['obj', 'obj', 'age'], 30);
// console.log(f);
// console.log(f.obj.obj);
// console.log(a === f);
// console.log(a.obj === f.obj);
// console.log(a.list === f.list);
// console.log(a.obj.times === f.obj.times);
// console.log(a.obj.obj === f.obj.obj);
// console.log(a.list[0] === f.list[0]);
// console.log(a.obj.obj.list === f.obj.obj.list);
console