Skip to content

Commit

Permalink
Merge pull request jashkenas#2494 from captbaritone/deep-2
Browse files Browse the repository at this point in the history
Allow deep property matching via array syntax
  • Loading branch information
jridgewell authored Oct 13, 2016
2 parents b8e26a9 + 3abd324 commit 08361d4
Show file tree
Hide file tree
Showing 4 changed files with 131 additions and 16 deletions.
3 changes: 3 additions & 0 deletions test/functions.js
Original file line number Diff line number Diff line change
Expand Up @@ -696,6 +696,9 @@
assert.deepEqual(_.toArray(cb(1, 2, 3, 4, 5, 6, 7, 8, 9, 10)), _.range(1, 11));
});

var deepProperty = _.iteratee(['a', 'b']);
assert.strictEqual(deepProperty({a: {b: 2}}), 2, 'treats an array as a deep property accessor');

// Test custom iteratee
var builtinIteratee = _.iteratee;
_.iteratee = function(value) {
Expand Down
18 changes: 17 additions & 1 deletion test/objects.js
Original file line number Diff line number Diff line change
Expand Up @@ -905,13 +905,27 @@
assert.notOk(_.has(child, 'foo'), 'does not check the prototype chain for a property.');
assert.strictEqual(_.has(null, 'foo'), false, 'returns false for null');
assert.strictEqual(_.has(void 0, 'foo'), false, 'returns false for undefined');

assert.ok(_.has({a: {b: 'foo'}}, ['a', 'b']), 'can check for nested properties.');
assert.notOk(_.has({a: child}, ['a', 'foo']), 'does not check the prototype of nested props.');
});

QUnit.test('property', function(assert) {
var stooge = {name: 'moe'};
assert.strictEqual(_.property('name')(stooge), 'moe', 'should return the property with the given name');
assert.strictEqual(_.property('name')(null), void 0, 'should return undefined for null values');
assert.strictEqual(_.property('name')(void 0), void 0, 'should return undefined for undefined values');
assert.strictEqual(_.property(null)('foo'), void 0, 'should return undefined for null object');
assert.strictEqual(_.property('x')({x: null}), null, 'can fetch null values');
assert.strictEqual(_.property('length')(null), void 0, 'does not crash on property access of non-objects');

// Deep property access
assert.strictEqual(_.property('a')({a: 1}), 1, 'can get a direct property');
assert.strictEqual(_.property(['a', 'b'])({a: {b: 2}}), 2, 'can get a nested property');
assert.strictEqual(_.property(['a'])({a: false}), false, 'can fetch falsey values');
assert.strictEqual(_.property(['x', 'y'])({x: {y: null}}), null, 'can fetch null values deeply');
assert.strictEqual(_.property(['x', 'y'])({x: null}), void 0, 'does not crash on property access of nested non-objects');
assert.strictEqual(_.property([])({x: 'y'}), void 0, 'returns `undefined` for a path that is an empty array');
});

QUnit.test('propertyOf', function(assert) {
Expand All @@ -930,8 +944,10 @@

var undefPropertyOf = _.propertyOf(void 0);
assert.strictEqual(undefPropertyOf('curly'), void 0, 'should return undefined when obj is undefined');
});

var deepPropertyOf = _.propertyOf({curly: {number: 2}});
assert.equal(deepPropertyOf(['curly', 'number']), 2, 'can fetch nested properties of obj');
});

QUnit.test('isMatch', function(assert) {
var moe = {name: 'Moe Howard', hair: true};
Expand Down
56 changes: 56 additions & 0 deletions test/utility.js
Original file line number Diff line number Diff line change
Expand Up @@ -336,6 +336,62 @@
}), obj.a, 'called with context');
});

QUnit.test('result can accept an array of properties for deep access', function(assert) {
var func = function() { return 'f'; };
var context = function() { return this; };

assert.strictEqual(_.result({a: 1}, 'a'), 1, 'can get a direct property');
assert.strictEqual(_.result({a: {b: 2}}, ['a', 'b']), 2, 'can get a nested property');
assert.strictEqual(_.result({a: 1}, 'b', 2), 2, 'uses the fallback value when property is missing');
assert.strictEqual(_.result({a: 1}, ['b', 'c'], 2), 2, 'uses the fallback value when any property is missing');
assert.strictEqual(_.result({a: void 0}, ['a'], 1), 1, 'uses the fallback when value is undefined');
assert.strictEqual(_.result({a: false}, ['a'], 'foo'), false, 'can fetch falsey values');

assert.strictEqual(_.result({a: func}, 'a'), 'f', 'can get a direct method');
assert.strictEqual(_.result({a: {b: func}}, ['a', 'b']), 'f', 'can get a nested method');
assert.strictEqual(_.result(), void 0, 'returns udefined if obj is not passed');
assert.strictEqual(_.result(void 1, 'a', 2), 2, 'returns default if obj is not passed');
assert.strictEqual(_.result(void 1, 'a', func), 'f', 'executes default if obj is not passed');
assert.strictEqual(_.result({}, void 0, 2), 2, 'returns default if prop is not passed');
assert.strictEqual(_.result({}, void 0, func), 'f', 'executes default if prop is not passed');

var childObj = {c: context};
var obj = {a: context, b: childObj};
assert.strictEqual(_.result(obj, 'a'), obj, 'uses the parent object as context');
assert.strictEqual(_.result(obj, 'e', context), obj, 'uses the object as context when executing the fallback');
assert.strictEqual(_.result(obj, ['a', 'x'], context), obj, 'uses the object as context when executing the fallback');
assert.strictEqual(_.result(obj, ['b', 'c']), childObj, 'uses the parent as context when accessing deep methods');

assert.strictEqual(_.result({}, [], 'a'), 'a', 'returns the default when prop is empty');
assert.strictEqual(_.result(obj, [], context), obj, 'uses the object as context when path is empty');

var nested = {
d: function() {
return {
e: function() {
return obj;
},
f: context
};
}
};
assert.strictEqual(_.result(nested, ['d', 'e']), obj, 'can unpack nested function calls');
assert.strictEqual(_.result(nested, ['d', 'f']).e(), obj, 'uses parent as context for nested function calls');
assert.strictEqual(_.result(nested, ['d', 'x'], context).e(), obj, 'uses last valid child as context for fallback');

if (typeof Symbol !== 'undefined') {
var x = Symbol('x');
var symbolObject = {};
symbolObject[x] = 'foo';
assert.strictEqual(_.result(symbolObject, x), 'foo', 'can use symbols as keys');

var y = Symbol('y');
symbolObject[y] = {};
symbolObject[y][x] = 'bar';
assert.strictEqual(_.result(symbolObject, [y, x]), 'bar', 'can use symbols as keys for deep matching');
}
});

QUnit.test('_.templateSettings.variable', function(assert) {
var s = '<%=data.x%>';
var data = {x: 'x'};
Expand Down
70 changes: 55 additions & 15 deletions underscore.js
Original file line number Diff line number Diff line change
Expand Up @@ -93,7 +93,7 @@
if (_.iteratee !== builtinIteratee) return _.iteratee(value, context);
if (value == null) return _.identity;
if (_.isFunction(value)) return optimizeCb(value, context, argCount);
if (_.isObject(value)) return _.matcher(value);
if (_.isObject(value) && !_.isArray(value)) return _.matcher(value);
return _.property(value);
};

Expand Down Expand Up @@ -139,7 +139,7 @@
return result;
};

var property = function(key) {
var shallowProperty = function(key) {
return function(obj) {
return obj == null ? void 0 : obj[key];
};
Expand All @@ -150,7 +150,7 @@
// Related: http://people.mozilla.org/~jorendorff/es6-draft.html#sec-tolength
// Avoids a very nasty iOS 8 JIT bug on ARM-64. #2094
var MAX_ARRAY_INDEX = Math.pow(2, 53) - 1;
var getLength = property('length');
var getLength = shallowProperty('length');
var isArrayLike = function(collection) {
var length = getLength(collection);
return typeof length == 'number' && length >= 0 && length <= MAX_ARRAY_INDEX;
Expand Down Expand Up @@ -1342,8 +1342,19 @@

// Shortcut function for checking if an object has a given property directly
// on itself (in other words, not on a prototype).
_.has = function(obj, key) {
return obj != null && hasOwnProperty.call(obj, key);
_.has = function(obj, path) {
if (!_.isArray(path)) {
return obj != null && hasOwnProperty.call(obj, path);
}
var length = path.length;
for (var i = 0; i < length; i++) {
var key = path[i];
if (obj == null || !hasOwnProperty.call(obj, key)) {
return false;
}
obj = obj[key];
}
return !!length;
};

// Utility Functions
Expand All @@ -1370,12 +1381,31 @@

_.noop = function(){};

_.property = property;
var deepGet = function(obj, path) {
var length = path.length;
for (var i = 0; i < length; i++) {
if (obj == null) return void 0;
obj = obj[path[i]];
}
return length ? obj : void 0;
};

_.property = function(path) {
if (!_.isArray(path)) {
return shallowProperty(path);
}
return function(obj) {
return deepGet(obj, path);
};
};

// Generates a function for a given object that returns a given property.
_.propertyOf = function(obj) {
return obj == null ? function(){} : function(key) {
return obj[key];
if (obj == null) {
return function(){};
}
return function(path) {
return !_.isArray(path) ? obj[path] : deepGet(obj, path);
};
};

Expand Down Expand Up @@ -1438,14 +1468,24 @@
_.escape = createEscaper(escapeMap);
_.unescape = createEscaper(unescapeMap);

// If the value of the named `property` is a function then invoke it with the
// `object` as context; otherwise, return it.
_.result = function(object, prop, fallback) {
var value = object == null ? void 0 : object[prop];
if (value === void 0) {
value = fallback;
// Traverses the children of `obj` along `path`. If a child is a function, it
// is invoked with its parent as context. Returns the value of the final
// child, or `fallback` if any child is undefined.
_.result = function(obj, path, fallback) {
if (!_.isArray(path)) path = [path];
var length = path.length;
if (!length) {
return _.isFunction(fallback) ? fallback.call(obj) : fallback;
}
return _.isFunction(value) ? value.call(object) : value;
for (var i = 0; i < length; i++) {
var prop = obj == null ? void 0 : obj[path[i]];
if (prop === void 0) {
prop = fallback;
i = length; // Ensure we don't continue iterating.
}
obj = _.isFunction(prop) ? prop.call(obj) : prop;
}
return obj;
};

// Generate a unique integer id (unique within the entire client session).
Expand Down

0 comments on commit 08361d4

Please sign in to comment.