diff --git a/test/collections.js b/test/collections.js index ac6f27d24..ce780f873 100644 --- a/test/collections.js +++ b/test/collections.js @@ -451,7 +451,7 @@ QUnit.test('invoke', function(assert) { - assert.expect(5); + assert.expect(13); var list = [[5, 1, 7], [3, 2, 1]]; var result = _.invoke(list, 'sort'); assert.deepEqual(result[0], [1, 5, 7], 'first array sorted'); @@ -468,6 +468,38 @@ assert.raises(function() { _.invoke([{a: 1}], 'a'); }, TypeError, 'throws for non-functions'); + + var getFoo = _.constant('foo'); + var getThis = function() { return this; }; + var item = { + a: { + b: getFoo, + c: getThis, + d: null + }, + e: getFoo, + f: getThis, + g: function() { + return { + h: getFoo + }; + } + }; + var arr = [item]; + assert.deepEqual(_.invoke(arr, ['a', 'b']), ['foo'], 'supports deep method access via an array syntax'); + assert.deepEqual(_.invoke(arr, ['a', 'c']), [item.a], 'executes deep methods on their direct parent'); + assert.deepEqual(_.invoke(arr, ['a', 'd', 'z']), [void 0], 'does not try to access attributes of non-objects'); + assert.deepEqual(_.invoke(arr, ['a', 'd']), [null], 'handles deep null values'); + assert.deepEqual(_.invoke(arr, ['e']), ['foo'], 'handles path arrays of length one'); + assert.deepEqual(_.invoke(arr, ['f']), [item], 'correct uses parent context with shallow array syntax'); + assert.deepEqual(_.invoke(arr, ['g', 'h']), [void 0], 'does not execute intermediate functions'); + + arr = [{ + a: function() { return 'foo'; } + }, { + a: function() { return 'bar'; } + }]; + assert.deepEqual(_.invoke(arr, 'a'), ['foo', 'bar'], 'can handle different methods on subsequent objects'); }); QUnit.test('invoke w/ function reference', function(assert) { diff --git a/underscore.js b/underscore.js index 8fe4d61fa..c88ccdcf1 100644 --- a/underscore.js +++ b/underscore.js @@ -146,6 +146,15 @@ }; }; + 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; + }; + // Helper for collection methods to determine whether a collection // should be iterated as an array or as an object. // Related: http://people.mozilla.org/~jorendorff/es6-draft.html#sec-tolength @@ -282,11 +291,24 @@ }; // Invoke a method (with arguments) on every item in a collection. - _.invoke = restArgs(function(obj, method, args) { - var isFunc = _.isFunction(method); - return _.map(obj, function(value) { - var func = isFunc ? method : value[method]; - return func == null ? func : func.apply(value, args); + _.invoke = restArgs(function(obj, path, args) { + var contextPath, func; + if (_.isFunction(path)) { + func = path; + } else if (_.isArray(path)) { + contextPath = path.slice(0, -1); + path = path[path.length - 1]; + } + return _.map(obj, function(context) { + var method = func; + if (!method) { + if (contextPath && contextPath.length) { + context = deepGet(context, contextPath); + } + if (context == null) return void 0; + method = context[path]; + } + return method == null ? method : method.apply(context, args); }); }); @@ -1382,15 +1404,6 @@ _.noop = function(){}; - 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);