I have a function that adds a key to incoming object, but I have been told to use the spread operator for that, I have been told that I can use the spread operator to create a new object with the same properties and then set isAvailable on it.
return new Partner(ServerConfig, capabilities, initialState)
}
class Partner {
constructor (ServerConfig, capabilities, initialState) {
initialState.isAvailable = true
So I tried something like this but couldn't succeed, can you help me? and confused, should I use the spread operator in this way, return from a function ?
newObject = {}
// use this inside a function and get value from return
return {
value: {
...newObject,
...initialState
}
}
initialState.isAvailable = true
The properties are added in order, so if you want to override existing properties, you need to put them at the end instead of at the beginning:
return {
value: {
...initialState,
...newObject
}
}
You don't need newObject (unless you already have it lying around), though:
return {
value: {
...initialState,
isAvailable: newValue
}
}
Example:
const o1 = {a: "original a", b: "original b"};
// Doesn't work:
const o2 = {a: "updated a", ...o1};
console.log(o2);
// Works:
const o3 = {...o1, a: "updated a"};
console.log(o3);
If you know the name of the property (a in the example below), then #crowder's answer is perfect:
const o3 = {...o1, a: "updated a"};
console.log(o3);
If the property name is in a variable, then you need to use Computed Property names syntax:
let variable = 'foo'
const o4 = {...o1, [variable]: "updated foo"};
console.log(o4);
Spreading the object usint ... will keep your state integrity. Let's say you have an initial state
initialState={isOpen:false,count:0}
Assume that you want to add another property but you want to keep other properties:
return { ...initialState,isAvailable:true
}
this will lead to
initialState={isOpen:false,count:0,isAvailable:true}
we just added a new property without discarding the other properties. maybe another part of your app is using other properties so we still keep them. let's say you want to update isOpen state. you spread the initial state first and then add the last piece of logic
return { ...initialState,isOpen:true
}
this will lead to this
initialState={isOpen:true,count:0,isAvailable:true}
Now maybe on your current page, since isOpen:true you might be showing a different piece of UI to the user. Let's say you want to update the count this time. You could write this too
return { count:5 }
Now your current state has only count:5 so you have lost other properties in your state. Now there is no isOpen property. If you did not handle the isOpen logic successfully while you were displaying a different UI, your app will crash. but if you handle correctly, isOpen would lead to falsy so your page will not show that piece of UI to the user.
Related
I often find myself having to build long chains before mapping over an array to check if it's defined:
this.props.photos &&
this.props.photos.activePhotos &&
this.props.photos.activePhotos.map(...
If I leave out the this.props.photos && and this.props.photos.activePhotos.length && my entire application will crash if photos or activePhotos is undefined.
Is there a way to check for these props without having to check every parent object/array of my end item?
January 2020 Update
According to the TC39 proposal, optional chaining will be available shortly within the JavaScript standard (currently in stage 4).
The syntax will be the following :
const active = this.props?.photos?.activePhotos
Or the following in your case :
(this.props?.photos?.activePhotos || []).map(...
While this is being implemented, you may want to take a look at Typescript or js compilers to try out the latest ES features
Old answer
An alternative could be to use a default value for your props when deconstructing them :
const { photos = {} } = this.props
const { activePhotos = [] } = photos
activePhotos.map(/* */)
In this case, if photos is not defined, it will be replaced with an empty object. Trying to get the activePhotos out of it will give you an empty array, allowing you to map on it in any case.
I guess you refer to optional chaining, which is stage 1 of TC39
https://github.com/tc39/proposal-optional-chaining
EDIT: The proposal is now in stage 4 (as of January 2020) and will be added into the JavaScript standard
I'm seeing two possible approaches, according to the level of nesting.
#1. If you have many nested props levels:
You can use lodash.get.
Here's how to render activePhotos, only if they exists:
// Please note how do we pass default `[]` as third parameter
// in order to not break the `.map` function
_.get(this.props, 'photos.activePhotos', []).map(...)
If you only want to check for deeply nested pros, then you can use lodash.has method:
// Will return `true` / `false`
_.has(this.props, 'photos.activePhotos')
#2. If the level of nesting is no more of 2-3 levels:
Just use the native ES6 destructuring assignment + default value feature.
const { photos = {} } = this.props
const { activePhotos = [] } = photos
// Now you can safely map over the `activePhotos`
activePhotos.map(...)
Is there a way to check for these props without having to check every
parent object/array of my end item?
In general, no.
It is not clear why the .length of the potential is checked at the code at the question.
If the goal is to reduce the code length you can us JSON.stringify() and RegExp
if (/"activePhotos":\[.*\]/.test(JSON.stringify(this.props))) // do stuff
or if the preferred approach is using AND operator
/"activePhotos":\[.*\]/.test(JSON.stringify(this.props)) && // do stuff
undefsafe is a good enough library to use. There are lot of other libraries available as well.
Simple example of how to use it
var object = {
a: {
b: {
c: 1,
d: [1,2,3],
e: 'remy'
}
}
};
console.log(undefsafe(object, 'a.b.e')); // "remy"
console.log(undefsafe(object, 'a.b.not.found')); // undefined
Here is a functional approach to optional chaining with default value return. The method uses a Maybe monad and a Proxy.
A wrap() function is used to wrap objects on which you can access any property safely. Internally, wrap creates a Proxy around your object and manages missing values using a Maybe wrapper. At the end of the chain, you unwrap the value by chaining getOrElse(default) with a default value which is returned when the propertie access fails:
class Maybe {
constructor(value) {
this.__value = value;
}
static of(value){
if (value instanceof Maybe) return value;
return new Maybe(value);
}
getOrElse(elseVal) {
return this.isNothing() ? elseVal : this.__value;
}
isNothing() {
return this.__value === null || this.__value === undefined;
}
map(fn) {
return this.isNothing()
? Maybe.of(null)
: Maybe.of(fn(this.__value));
}
}
function wrap(obj) {
function fix(object, property) {
const value = object[property];
return typeof value === 'function' ? value.bind(object) : value;
}
return new Proxy(Maybe.of(obj), {
get: function(target, property) {
if (property in target) {
return fix(target, property);
} else {
return wrap(target.map(val => fix(val, property)));
}
}
});
}
const obj = { a: 1, b: { c: [4, 1, 2] }, c: () => 'yes' };
console.log(wrap(obj).a.getOrElse(null)) // returns 1
console.log(wrap(obj).a.b.c.d.e.f.getOrElse(null)) // returns null
console.log(wrap(obj).b.c.getOrElse([])) // returns [4, 1, 2]
console.log(wrap(obj).b.c[0].getOrElse(null)) // returns 4
console.log(wrap(obj).b.c[100].getOrElse(-1)) // out-of-bounds index: returns -1
console.log(wrap(obj).c.getOrElse(() => 'no')()) // returns 'yes'
console.log(wrap(obj).d.getOrElse(() => 'no')()) // returns 'no'
See this blog post and this link for more information on Maybe monads and the use of proxies.
So I'm in a unique situation where I have two objects, and I need to compare the keys on said objects to make sure they match the default object. Here's an example of what I'm trying to do:
const _ = require('lodash');
class DefaultObject {
constructor(id) {
this.id = id;
this.myobj1 = {
setting1: true,
setting2: false,
setting3: 'mydynamicstring'
};
this.myobj2 = {
perm1: 'ALL',
perm2: 'LIMITED',
perm3: 'LIMITED',
perm4: 'ADMIN'
};
}
}
async verifyDataIntegrity(id, data) {
const defaultData = _.merge(new DefaultObject(id));
if (defaultData.hasOwnProperty('myoldsetting')) delete defaultData.myoldsetting;
if (!_.isEqual(data, defaultData)) {
await myMongoDBCollection.replaceOne({ id }, defaultData);
return defaultData;
} else {
return data;
}
}
async requestData(id) {
const data = await myMongoDBCollection.findOne({ id });
if (!data) data = await this.makeNewData(id);
else data = await this.verifyDataIntegrity(id, data);
return data;
}
Let me explain. First, I have a default object which is created every time a user first uses the service. Then, that object is modified to their customized settings. For example, they could change 'setting1' to be false while changing 'perm2' to be 'ALL'.
Now, an older version of my default object used to have a property called 'myoldsetting'. I don't want newer products to have this setting, so every time a user requests their data I check if their object has the setting 'myoldsetting', and if it does, delete it. Then, to prevent needless updates (because this is called every time a user wants their data), I check if it is equal with the new default object.
But this doesn't work, because if the user has changed a setting, it will always return false and force a database update, even though none of the keys have changed. To fix this, I need a method of comparing the keys on an object, rather any the keys and data.
That way, if I add a new option to DefaultObject, say, 'perm5' set to 'ADMIN', then it will update the user's object. But, if their object has the same keys (it's up to date), then continue along your day.
I need this comparison to be deep, just in case I add a new property in, for example, myobj1. If I only compare the main level keys (id, myobj1, myobj2), it won't know if I added a new key into myobj1 or myobj2.
I apologize if this doesn't make sense, it's a very specific situation. Thanks in advance if you're able to help.
~~~~EDIT~~~~
Alright, so I've actually come up with a function that does exactly what I need. The issue is, I'd like to minify it so that it's not so big. Also, I can't seem to find a way to check if an item is a object even when it's null. This answer wasn't very helpful.
Here's my working function.
function getKeysDeep(arr, obj) {
Object.keys(obj).forEach(key => {
if (typeof obj[key] === 'object') {
arr = getKeysDeep(arr, obj[key]);
}
});
arr = arr.concat(Object.keys(obj));
return arr;
}
Usage
getKeysDeep([], myobj);
Is it possible to use it without having to put an empty array in too?
So, if I understand you correctly you would like to compare the keys of two objects, correct?
If that is the case you could try something like this:
function hasSameKeys(a, b) {
const aKeys = Object.keys(a);
const bKeys = Object.keys(b);
return aKeys.length === bKeys.length && !(aKeys.some(key => bKeys.indexOf(key) < 0));
}
Object.keys(x) will give you all the keys of the objects own properties.
indexOf will return a -1 if the value is not in the array that indexOf is being called on.
some will return as soon as the any element of the array (aKeys) evaluates to true in the callback. In this case: If any of the keys is not included in the other array (indexOf(key) < 0)
Alright, so I've actually come up with a function that does exactly what I need. The issue is, I'd like to minify it so that it's not so big. Also, I can't seem to find a way to check if an item is a object even when it's null.
In the end, this works for me. If anyone can improve it that'd be awesome.
function getKeysDeep(obj, arr = []) {
Object.keys(obj).forEach(key => {
if (typeof obj[key] === 'object' && !Array.isArray(obj[key]) && obj[key] !== null) {
arr = this.getKeysDeep(obj[key], arr);
}
});
return arr.concat(Object.keys(obj));
}
getKeysDeep(myobj);
I'm trying to assign the value of an array element to an object. After first attempting something like, e.g.bar = foo[0]; I've discovered that any change to bar also changes foo[0], due to having the same reference.
Awesome, thought no one, and upon reading up on immutability and the ES6 Object.assign() method and spread properties, I thought it would fix the issue. However, in this case it doesn't. What am I missing?
EDIT: Sorry about the accountTypes confusion, I fixed the example.
Also, I would like to keep the class structure of Settings, so let copy = JSON.parse(JSON.stringify(original)); is not really what I'm after in this case.
//this object will change according to a selection
currentPreset;
//this should remain unchanged
presets: {name: string, settings: Settings}[] = [];
ngOnInit()
{
this.currentPreset = {
name: '',
settings: new Settings()
}
this.presets.push({name: 'Preset1', settings: new Settings({
settingOne: 'foo',
settingTwo: false,
settingThree: 14
})
});
}
/**
* Select an item from the `presets` array and assign it,
* by value(not reference), to `currentPreset`.
*
* #Usage In an HTML form, a <select> element's `change` event calls
* this method to fill the form's controls with the values of a
* selected item from the `presets` array. Subsequent calls to this
* method should not affect the value of the `presets` array.
*
* #param value - Expects a numerical index or the string 'new'
*/
setPreset(value)
{
if(value == 'new')
{
this.currentPreset.name = '';
this.currentPreset.settings.reset();
}
else
{
this.currentPreset = {...this.presets[value]};
//same as above
//this.currentPreset = Object.assign({}, this.presets[value]);
}
}
Try this : let copy = original.map(item => Object.assign({}, ...item));
This will create a new object without any reference to the old object original
In case if you want to do this for an array try the same with []
let copy = original.map(item => Object.assign([], ...item));
You have to do a deep copy, this the easiest way:
let copy = JSON.parse(JSON.stringify(original));
This doesn't really answer the question, but since the object I'm trying not to mutate doesn't have nested properties within, I call the assignment at the property level and the shallow copy here is fine.
setPreset(value)
{
if(value == 'new')
{
this.currentPreset.name = '';
this.currentPreset.settings.reset();
}
else
{
this.currentPreset.name = this.presets[value].name;
this.currentPreset.privileges = Object.assign(new Settings(),
this.presets[value].settings);
}
}
A better solution, since I'm creating a new Settings() anyway, might be to move this logic to a Settings class method and call it in the constructor
I had the same problem recently, and I could not figure out why some of my objects were changing their properties. I had to change my code to avoid mutation. Some of the answers here helped me understand afterwards, such as this great article : https://alistapart.com/article/why-mutation-can-be-scary/
I recommend it. The author gives a lot of examples and useful libraries that can outperform Object.assign() when it comes to embedded properties.
In this documentation of React, it is said that
shallowCompare performs a shallow equality check on the current props and nextProps objects as well as the current state and nextState objects.
The thing which I am unable to understand is If It shallowly compares the objects then shouldComponentUpdate method will always return true, as
We should not mutate the states.
and if we are not mutating the states then the comparison will always return false and so the shouldComponent update will always return true. I am confused about how it is working and how will we override this to boost the performance.
Shallow compare does check for equality. When comparing scalar values (numbers, strings) it compares their values. When comparing objects, it does not compare their attributes - only their references are compared (e.g. "do they point to same object?").
Let's consider the following shape of user object
user = {
name: "John",
surname: "Doe"
}
Example 1:
const user = this.state.user;
user.name = "Jane";
console.log(user === this.state.user); // true
Notice you changed users name. Even with this change, the objects are equal. The references are exactly the same.
Example 2:
const user = clone(this.state.user);
console.log(user === this.state.user); // false
Now, without any changes to object properties they are completely different. By cloning the original object, you create a new copy with a different reference.
Clone function might look like this (ES6 syntax)
const clone = obj => Object.assign({}, ...obj);
Shallow compare is an efficient way to detect changes. It expects you don't mutate data.
shallow comparison is when the properties of the objects being compared is done using "===" or strict equality and will not conduct comparisons deeper into the properties. for e.g.
// a simple implementation of the shallowCompare.
// only compares the first level properties and hence shallow.
// state updates(theoretically) if this function returns true.
function shallowCompare(newObj, prevObj){
for (key in newObj){
if(newObj[key] !== prevObj[key]) return true;
}
return false;
}
//
var game_item = {
game: "football",
first_world_cup: "1930",
teams: {
North_America: 1,
South_America: 4,
Europe: 8
}
}
// Case 1:
// if this be the object passed to setState
var updated_game_item1 = {
game: "football",
first_world_cup: "1930",
teams: {
North_America: 1,
South_America: 4,
Europe: 8
}
}
shallowCompare(updated_game_item1, game_item); // true - meaning the state
// will update.
Although both the objects appear to be same, game_item.teams is not the same reference as updated_game_item.teams. For 2 objects to be same, they should point to the same object.
Thus this results in the state being evaluated to be updated
// Case 2:
// if this be the object passed to setState
var updated_game_item2 = {
game: "football",
first_world_cup: "1930",
teams: game_item.teams
}
shallowCompare(updated_game_item2, game_item); // false - meaning the state
// will not update.
This time every one of the properties return true for the strict comparison as the teams property in the new and old object point to the same object.
// Case 3:
// if this be the object passed to setState
var updated_game_item3 = {
first_world_cup: 1930
}
shallowCompare(updated_game_item3, game_item); // true - will update
The updated_game_item3.first_world_cup property fails the strict evaluation as 1930 is a number while game_item.first_world_cup is a string. Had the comparison been loose (==) this would have passed. Nonetheless this will also result in state update.
Additional Notes:
Doing deep compare is pointless as it would significantly effect performance if the state object is deeply nested. But if its not too nested and you still need a deep compare, implement it in shouldComponentUpdate and check if that suffices.
You can definitely mutate the state object directly but the state of the components would not be affected, since its in the setState method flow that react implements the component update cycle hooks. If you update the state object directly to deliberately avoid the component life-cycle hooks, then probably you should be using a simple variable or object to store the data and not the state object.
Shallow compare works by checking if two values are equal in case of primitive types like string, numbers and in case of object it just checks the reference. So if you shallow compare a deep nested object it will just check the reference not the values inside that object.
There is also legacy explanation of shallow compare in React:
shallowCompare performs a shallow equality check on the current props and nextProps objects as well as the current state and nextState objects.
It does this by iterating on the keys of the objects being compared and returning true when the values of a key in each object are not strictly equal.
UPD: Current documentation says about shallow compare:
If your React component's render() function renders the same result given the same props and state, you can use React.PureComponent for a performance boost in some cases.
React.PureComponent's shouldComponentUpdate() only shallowly compares the objects. If these contain complex data structures, it may produce false-negatives for deeper differences. Only extend PureComponent when you expect to have simple props and state, or use forceUpdate() when you know deep data structures have changed
UPD2: I think Reconciliation is also important theme for shallow compare understanding.
The accepted answer can be a bit misleading for some people.
user = {
name: "John",
surname: "Doe"
}
const user = this.state.user;
user.name = "Jane";
console.log(user === this.state.user); // true
This statement in particular "Notice you changed users name. Even with this change objects are equal. They references are exactly same."
When you do the following with objects in javascript:
const a = {name: "John"};
const b = a;
Mutating any of the two variables will change both of them because they have the same reference. That's why they will always be equal (==, ===, Object.is()) to each other.
Now for React, the following is the shallow comparison function:
https://github.com/facebook/fbjs/blob/master/packages/fbjs/src/core/shallowEqual.js
/**
* Performs equality by iterating through keys on an object and returning false
* when any key has values which are not strictly equal between the arguments.
* Returns true when the values of all keys are strictly equal.
*/
function shallowEqual(objA: mixed, objB: mixed): boolean {
if (is(objA, objB)) {
return true;
}
if (typeof objA !== 'object' || objA === null ||
typeof objB !== 'object' || objB === null) {
return false;
}
const keysA = Object.keys(objA);
const keysB = Object.keys(objB);
if (keysA.length !== keysB.length) {
return false;
}
// Test for A's keys different from B.
for (let i = 0; i < keysA.length; i++) {
if (
!hasOwnProperty.call(objB, keysA[i]) ||
!is(objA[keysA[i]], objB[keysA[i]])
) {
return false;
}
}
return
For non-primitives (Objects), it checks:
If the first object is equal (using Object.is()) to the second.
If not, it checks if each key-value pair in the first object is equal (using Object.is()) to that of the second. This is done for the first level of keys. If the object has a key whose value is another object, this function does not check for equality further down the depth of the object.
It took me a while to actually know shallow compare and === are two different thing, especially while reading redux documentation in the following.
However, when an action is dispatched to the Redux store, useSelector() only forces a re-render if the selector result appears to be different than the last result. As of v7.1.0-alpha.5, the default comparison is a strict === reference comparison. This is different than connect(), which uses shallow equality checks on the results of mapState calls to determine if re-rendering is needed. This has several implications on how you should use useSelector().
strict equal
so step by step, the strict equal === is very consistently defined by the Javascript language, https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/Strict_equality
What it does is to compare two items by value if they are primitive, and then by reference if they are object. Of course if the types of two objects are different, they will never match.
shallow compare
shallow probably isn't a build-in feature of the language. Couple of the answers here pointed us to some variation of the implementation, https://github.com/facebook/fbjs/blob/main/packages/fbjs/src/core/shallowEqual.js
The idea is to compare two items by value if they are primitive. But for non primitives, we go one level lower. For objects, if the keys are different between two objects, we say they are not same. If the value under a key is different, we say they are not same either.
summary
This means, shallow comparison checks more than the strict equal ===, especially when it comes to the object. A quick look might suggest, === does not do too much guess work.
The shallow equal snippet by #supi above (https://stackoverflow.com/a/51343585/800608) fails if prevObj has a key that newObj doesn't have. Here is an implementation that should take that into account:
const shallowEqual = (objA, objB) => {
if (!objA || !objB) {
return objA === objB
}
return !Boolean(
Object
.keys(Object.assign({}, objA, objB))
.find((key) => objA[key] !== objB[key])
)
}
Note that the above doesn't work in Explorer without polyfills.
There is an implementation with examples.
const isObject = value => typeof value === 'object' && value !== null;
const compareObjects = (A, B) => {
const keysA = Object.keys(A);
const keysB = Object.keys(B);
if (keysA.length !== keysB.length) {
return false;
}
return !keysA.some(key => !B.hasOwnProperty(key) || A[key] !== B[key]);
};
const shallowEqual = (A, B) => {
if (A === B) {
return true;
}
if ([A, B].every(Number.isNaN)) {
return true;
}
if (![A, B].every(isObject)) {
return false;
}
return compareObjects(A, B);
};
const a = { field: 1 };
const b = { field: 2 };
const c = { field: { field: 1 } };
const d = { field: { field: 1 } };
console.log(shallowEqual(1, 1)); // true
console.log(shallowEqual(1, 2)); // false
console.log(shallowEqual(null, null)); // true
console.log(shallowEqual(NaN, NaN)); // true
console.log(shallowEqual([], [])); // true
console.log(shallowEqual([1], [2])); // false
console.log(shallowEqual({}, {})); // true
console.log(shallowEqual({}, a)); // false
console.log(shallowEqual(a, b)); // false
console.log(shallowEqual(a, c)); // false
console.log(shallowEqual(c, d)); // false
Very simple to understand it. first need to understand the pure component and regular component, if a component has coming props or state is changing then it will re-rendered the component again.
if not then not.
in regular component shouldComponentUpdate by default true. and in pure component only the time when state change with diff value.
so now what is shallow component or shallow ?
lets take an simple example.
let a = [1,2,3],
let b = [1,2,3],
a == b ==> shallow take it false,
a == c ==> shallow take it true. c has any diff value.
now i think you can understand it. the diff in both regular and pure component with shallow component
if you like it, also do like share and subscribe my youtube channel
https://www.youtube.com/muosigmaclasses
Thanks.
I feel that none of the answers actually addressed the crucial part in your question, the answers merely explain what shallow comparison is (whether they mean the JavaScript default shallow comparison that is a result of the === or == operator or React's shallowCompare() function)
To answer your question, my understanding so far of React makes me believe that yes indeed by not directly mutating the states then shouldComponentUpdate will always return true thus always causing a re-render no matter what objects we pass in setState even if the objects passed to setState hold the same values stored in the current state
example:
Say I have a React.Component with the current state and function:
this.state = {data: {num: 1}} // current state object
foo() { // something will cause this function to called, thus calling setState
this.setState( {data: {num: 1}} ); // new state object
}
You can see that setState passed the same object (value-wise) however plain React is not smart enough to realize that this component shouldn't update/re-render.
To overcome this, you have to implement your version of shouldComponentUpdate in which you apply deep comparison yourself on the state/props elements that you think should be taken into consideration.
Check out this article on lucybain.com that briefly answers this question.
The changes array of an Object.observe() callback contains objects with the following four properties:
name
object
type
oldValue
https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/observe#Parameters
Why isn't there a path provided natively? Example:
var ob = {
foo: [
{moo: "bar", val: 5},
{val: 8}
]
}
ob.foo[0].val = 1;
// callback should provide path "foo.0.val" or "foo[0].val"
There's a Node.js module that extends Object.observe() to also include the path: observed.js,
but I worry the performance gain of a native observe() will be lost (if no, could you please explain how it is implemented then?). It might be possible to browserify the module, but can't imagine it will perform well in a synchronous environment and I still wonder why nobody seems to have thought about an additional path property.
Because there is no clear path.
Consider the following:
var movall = {moo: "bar", val: 5};
var ob1 = {a: mooval};
var ob2 = {b: movall};
Now let's say I observe movall. Then I update moo. What is the path? Is it movall.moo, or ob1.a.moo, or ob2.b.moo? If I observe ob1, there is no change reported, since there is no change to any of its properties (the change was internal to one of its properties, which doesn't count).
Objects are independent of their existence nested within other objects. They can be nested within multiple other objects. There is no unique "path" that describes how to get from potentially multiple starting points down to a specific property which may have changed.
Nor does JS know the path by which you reached the property being changed. So in ob.foo[0].val = 1;, JS simply evaluates the chain, arrives at the foo[0] object, changes its val property, and at that point has no idea how it happened to arrive at foo[0]. All it knows is that foo[0] has changed. It changed within ob, but it might also have changed within some other object that happens to have foo[0] as a property.
However, you can possibly achieve what you seem to be trying to by building some machinery on top of the low-level observe/notify mechanism. We shall define a function on an object which sets up observers on its property objects, and so on recursively, and propagates change records back up with properly constructed paths:
function notifySubobjectChanges(object) {
var notifier = Object.getNotifier(object); // get notifier for this object
for (var k in object) { // loop over its properties
var prop = object[k]; // get property value
if (!prop || typeof prop !== 'object') break; // skip over non-objects
Object.observe(prop, function(changes) { // observe the property value
changes.forEach(function(change) { // and for each change
notifier.notify({ // notify parent object
object: change.object, // with a modified changerec
name: change.name, // which is basically the same
type: change.type,
oldValue: change.oldValue,
path: k +
(change.path ? '.' + change.path : '') // but has an addt'l path property
});
});
});
notifySubobjectChanges(prop); // repeat for sub-subproperties
}
}
(Note: the change object is frozen and we cannot add anything to it, so we have to copy it.)
Now
a = { a: { b: {c: 1 } } }; // nested objects
notifySubobjectChanges(a); // set up recursive observers
Object.observe(a, console.log.bind(console)); // log changes to console
a.a.b.c = 99;
>> 0: Object
name: "c"
object: Object
oldValue: 1
path: "a.b" // <=== here is your path!
type: "update"
The above code is not production-quality, use at your own risk.