PageRenderTime 66ms CodeModel.GetById 21ms RepoModel.GetById 1ms app.codeStats 0ms

/src/com/bit101/underscore/_.as

https://bitbucket.org/andytwoods/dropbox-as3-sync
ActionScript | 810 lines | 433 code | 72 blank | 305 comment | 97 complexity | 419e4f48ecc30b41f8c1c2deee0f36fb MD5 | raw file
  1. /**
  2. * underscore.as, by Alan MacDougall [http://www.github.com/amacdougall/]
  3. *
  4. * Ported from underscore.js, (c) 2011 Jeremy Ashkenas, DocumentCloud Inc.
  5. * See http://http://documentcloud.github.com/underscore/
  6. *
  7. * Like underscore.js, it is freely distributable under the MIT license.
  8. */
  9. package com.underscore {
  10. // imports
  11. import flash.utils.Dictionary;
  12. import flash.utils.Timer;
  13. import flash.events.TimerEvent;
  14. /**
  15. * An AS3 adaptation of underscore.js. Note that most collection methods accept
  16. * Array, Object, or XMLList collections, but will return only Arrays. Use
  17. * Underscore to further manipulate a data set from XML after getting an
  18. * XMLList via E4X. Consider using _.toArray on an XMLList if you need access
  19. * to a function that doesn't support XMLLists.
  20. *
  21. * Default iterator format: function(value:*, index:*?, list:*? {}
  22. */
  23. public var _:* = (function():Function {
  24. var breaker:Object = {};
  25. var _:* = function(obj:*):* {
  26. return new Wrapper(obj);
  27. };
  28. /* This hack is necessary to work around AS3's argument count
  29. * enforcement and lack of Function.arity. We want the user to
  30. * be able to use function(item), function(item, index), or
  31. * function(item, index, context) as the whim strikes them,
  32. * without having to declare function(...args) and figure out
  33. * the indexes themselves, or provide default values for an
  34. * argument they won't use often, like context.
  35. *
  36. * So instead, we just try the most common case first and then
  37. * try each subsequent case when we get ArgumentErrors. Trying
  38. * one arg first means the fewest exceptions.
  39. */
  40. var safeCall:Function = function(iterator:Function, context:Object, ...args):* {
  41. var answer:*;
  42. var additionalArgs:Array = [];
  43. while (args.length > 1) {
  44. additionalArgs.push(args.pop());
  45. }
  46. while (true) {
  47. try {
  48. answer = iterator.apply(context, args);
  49. } catch (e:ArgumentError) {
  50. if (additionalArgs.length == 0) {
  51. // if we've run out of additional args, give up
  52. throw e;
  53. }
  54. args.push(additionalArgs.pop());
  55. continue;
  56. }
  57. break;
  58. }
  59. return answer;
  60. }
  61. /* COLLECTIONS */
  62. /**
  63. * Executes the iterator on each element of the input collection. Works on
  64. * Array, Object, or XMLList collections.
  65. *
  66. * @throws ArgumentError if obj is not a collection.
  67. */
  68. var each:Function = _.each = function(obj:*, iterator:Function, context:Object = null):* {
  69. var i:int;
  70. if (obj is Array) {
  71. // TO DO: benchmark native Array.forEach
  72. for (i = 0; i < obj.length; i++) {
  73. if (safeCall(iterator, context, obj[i], i, obj) === breaker) return;
  74. }
  75. } else if (obj is Object) {
  76. for (var key:String in obj) {
  77. if (obj.hasOwnProperty(key)) {
  78. if (safeCall(iterator, context, obj[key], key, obj) === breaker) return;
  79. }
  80. }
  81. } else if (obj is XMLList) {
  82. for (i = 0; i < obj.length(); i++) {
  83. if (safeCall(iterator, context, obj[i], i, obj) == breaker) return;
  84. }
  85. } else {
  86. throw new Error("Attempted to iterate over incompatible object " +
  87. obj + "; must iterate over an Array, an Object, or an XMLList.");
  88. }
  89. };
  90. /** Returns the input items, transformed by the iterator, as an Array. */
  91. var map:Function = _.map = function(obj:*, iterator:Function, context:Object = null):Array {
  92. var results:Array = [];
  93. if (obj == null) return results;
  94. // TO DO: benchmark native Array.map
  95. each(obj, function(value:*, index:*, list:* = null):* {
  96. results.push(safeCall(iterator, context, value, index, list));
  97. });
  98. return results;
  99. };
  100. /**
  101. * Returns the input items, filtered by the iterator, as an Array. Aliased
  102. * as _.select.
  103. */
  104. _.filter = _.select = function(obj:*, iterator:Function, context:Object = null):Array {
  105. var results:Array = [];
  106. if (obj == null) return results;
  107. each(obj, function(value:*, index:* = null, list:* = null):* {
  108. safeCall(iterator, context, value, index, list) && results.push(value);
  109. });
  110. return results;
  111. };
  112. /**
  113. * Returns the input items, rejecting any which pass the truth test.
  114. */
  115. _.reject = function(obj:*, iterator:Function, context:Object = null):Array {
  116. var results:Array = [];
  117. if (obj == null) return results;
  118. each(obj, function(value:*, index:* = null, list:* = null):* {
  119. safeCall(iterator, context, value, index, list) || results.push(value);
  120. });
  121. return results;
  122. };
  123. /**
  124. * Executes the iterator on each collection element. The iterator has
  125. * access to a memo variable which can be modified by each iteration in
  126. * turn, to build a string or sum a mathematical series (for example). At
  127. * the end of the iteration, the memo is returned. If no memo is supplied,
  128. * the first value in the list is used as the memo. As in the original
  129. * underscore.js implementation, if no memo is supplied, the iterator is
  130. * not run on the first value of the list.
  131. *
  132. * Simply returns the memo if run on an empty collection.
  133. *
  134. * Iterator format: function(memo:*, value:*, index:*?, list:*?):* {}
  135. */
  136. _.reduce = _.foldl = _.inject = function(obj:*, iterator:Function, memo:* = null, context:Object = null):* {
  137. if (_.isEmpty(obj)) return memo;
  138. var memoInitialized:Boolean = (memo != null);
  139. each(obj, function(value:*, index:*, list:*):* {
  140. if (!memoInitialized) {
  141. memo = value;
  142. memoInitialized = true;
  143. } else {
  144. memo = safeCall(iterator, context, memo, value, index, list);
  145. }
  146. });
  147. return memo;
  148. }
  149. // TO DO: implement _.reduceRight
  150. /**
  151. * Returns the first collection element for which the iterator returns
  152. * true. If no element qualifies, returns null. Assigning null to a typed
  153. * variable whose type is not nullable, such as an int, will coarse the
  154. * value to 0 instead of null, so watch out.
  155. */
  156. _.detect = _.find = function(obj:*, iterator:*, context:Object = null):* {
  157. var result:* = null;
  158. any(obj, function(value:*, index:* = null, list:* = null):Boolean {
  159. if (safeCall(iterator, context, value, index, list)) {
  160. result = value;
  161. return true;
  162. }
  163. return false;
  164. });
  165. return result;
  166. };
  167. /**
  168. * True if the iterator returns true for any collection element. If the
  169. * iterator is omitted, tests the elements themselves for truthiness.
  170. */
  171. var any:Function = _.any = _.some = function(obj:*, iterator:Function = null, context:Object = null):Boolean {
  172. if (obj == null || _(obj).isEmpty()) return false;
  173. // TO DO: benchmark native Array.some
  174. iterator = iterator || identity;
  175. var result:Boolean = false;
  176. each(obj, function(value:*, index:* = null, list:* = null):* {
  177. result = safeCall(iterator, context, value, index, list);
  178. if (result) return breaker; // stop on first true value
  179. });
  180. return result;
  181. };
  182. /**
  183. * True if the iterator returns true for all collection elements. If the
  184. * iterator is omitted, tests the elements themselves for truthiness.
  185. */
  186. _.all = function(obj:*, iterator:Function = null, context:Object = null):Boolean {
  187. if (obj == null || _(obj).isEmpty()) return false;
  188. // TO DO: benchmark native Array.every
  189. iterator = iterator || identity;
  190. var result:Boolean = true;
  191. each(obj, function(value:*, index:* = null, list:* = null):* {
  192. result = safeCall(iterator, context, value, index, list);
  193. if (!result) return breaker; // stop on first false value
  194. });
  195. return result;
  196. }
  197. /**
  198. * True if the target is present in the collection. Uses strict
  199. * (threequals) equality. Named "include" in underscore.js, but since
  200. * that's a special directive in AS3, it causes an error.
  201. */
  202. _.includes = _.contains = function(obj:*, target:*):Boolean {
  203. if (obj is XMLList) throw new ArgumentError("_.includes cannot operate on an XMLList.");
  204. if (obj == null || _(obj).isEmpty()) return false;
  205. return any(obj, function(element:*):Boolean {
  206. return element === target;
  207. });
  208. }
  209. /**
  210. * Invoke the named method on each collection element. If the method name
  211. * is omitted, and the element is a function, invokes the element. Any
  212. * additional arguments will be passed as parameters to each function call.
  213. * To call a list of functions with params, _.invoke(list, null, arg1,
  214. * ...).
  215. *
  216. * @return An Array of the results of each invocation, or null if all
  217. * functions were void or returned null.
  218. */
  219. _.invoke = function(obj:*, functionName:String = null, ...args):Array {
  220. // TO DO: compact the result
  221. return map(obj, function(element:*):* {
  222. return (functionName ? element[functionName] : element).apply(element, args);
  223. });
  224. }
  225. /**
  226. * Operates on a collection of Objects, returning an array of the values of
  227. * the named property. Example:
  228. *
  229. * var hashes:Array = [{name: "foo"}, {name: "bar"}];
  230. * _(hashes).pluck("name"); // ["foo", "bar"]
  231. *
  232. * @throws ArgumentError if called on an XMLList, since XML nodes have no
  233. * single key. Could be @id, tag name, or who knows what. Use _.map!
  234. */
  235. _.pluck = function(obj:*, key:String):Array {
  236. if (obj is XMLList) throw new ArgumentError("_.pluck cannot operate on an XMLList.");
  237. return map(obj, function(element:Object):* {
  238. return element[key];
  239. });
  240. }
  241. /**
  242. * Returns the maximum value in the collection. If an iterator is passed,
  243. * it must return a numeric value for each element. Otherwise the element
  244. * itself will be compared using gt/lt operators, with undefined results if
  245. * the values are non-numeric.
  246. */
  247. _.max = function(obj:*, iterator:Function = null, context:Object = null):* {
  248. // unlike in underscore.js, "value" means numeric value, "element" is the real item
  249. var maxElement:* = null;
  250. var maxValue:Number = -Infinity;
  251. each (obj, function(element:*, index:*, list:*):void {
  252. var value:Number = iterator != null ?
  253. safeCall(iterator, context, element, index, list) :
  254. element;
  255. if (value >= maxValue) {
  256. maxValue = value;
  257. maxElement = element;
  258. }
  259. });
  260. return maxElement;
  261. };
  262. /**
  263. * Returns the minimum value in the collection. If an iterator is passed,
  264. * it must return a numeric value for each element. Otherwise the element
  265. * itself will be compared using gt/lt operators, with undefined results if
  266. * the values are non-numeric.
  267. */
  268. _.min = function(obj:*, iterator:Function = null, context:Object = null):* {
  269. // unlike in underscore.js, "value" means numeric value, "element" is the real item
  270. var minElement:* = null;
  271. var minValue:Number = Infinity;
  272. each (obj, function(element:*, index:*, list:*):void {
  273. var value:Number = iterator != null ?
  274. safeCall(iterator, context, element, index, list) :
  275. element;
  276. if (value <= minValue) {
  277. minValue = value;
  278. minElement = element;
  279. }
  280. });
  281. return minElement;
  282. };
  283. /**
  284. * Sort the objects values by running the iterator on each one. Iterator
  285. * must return a numeric value.
  286. */
  287. _.sortBy = function(obj:*, iterator:Function = null, context:Object = null):Array {
  288. // unlike in underscore.js, "value" means numeric value, "element" is the real item
  289. var results:Array = map(obj, function(element:*, index:*, list:*):Object {
  290. return {
  291. element: element,
  292. value: safeCall(iterator, context, element, index, list)
  293. };
  294. });
  295. // AS3's Array.sort mutates the array instead of returning a new sorted one.
  296. results.sort(function(left:Object, right:Object):int {
  297. if (left.value < right.value) {
  298. return -1;
  299. } else if (left.value == right.value) {
  300. return 0;
  301. } else {
  302. return 1;
  303. }
  304. });
  305. return _(results).pluck("element");
  306. };
  307. /**
  308. * Operating on a sorted array, returns the index at which the supplied
  309. * value should be inserted to main sort order. Optionally accepts an
  310. * iterator which produces a numeric value for each element; otherwise,
  311. * compares elements directly.
  312. *
  313. * Iterator format: function(element:*):Number {}
  314. */
  315. _.sortedIndex = function(list:Array, element:*, iterator:Function = null):int {
  316. iterator = iterator || identity;
  317. var low:int = 0;
  318. var high:int = list.length;
  319. while (low < high) {
  320. var mid:int = (low + high) >> 1; // i.e. Math.floor((low + high) / 2)
  321. iterator(list[mid]) < iterator(element) ? low = mid + 1 : high = mid;
  322. }
  323. return low;
  324. };
  325. /**
  326. * Transforms the collection into an Array, discarding keys if it was an
  327. * Object. If collection is already an Array, returns a shallow copy. If
  328. * collection was an XMLList, returns an array of nodes.
  329. */
  330. _.toArray = function(collection:*):Array {
  331. if (collection == null) return [];
  332. if (collection is Array) return (collection as Array).slice();
  333. var result:Array = [];
  334. for each (var element:* in collection) {
  335. result.push(element);
  336. }
  337. return result;
  338. };
  339. /** Returns the number of elements in the collection. */
  340. _.size = function(obj:*):int {
  341. return _(obj).toArray().length;
  342. }
  343. /**
  344. * True if the collection has no elements; false otherwise.
  345. *
  346. * @throws ArgumentError if obj is not an Object or Array.
  347. */
  348. _.isEmpty = function(obj:*):Boolean {
  349. if (obj is Array) {
  350. return obj.length == 0;
  351. } else if (obj is Object) {
  352. var n:int = 0;
  353. for (var key:String in obj) {n++;};
  354. return n == 0;
  355. } else {
  356. throw new ArgumentError("_.isEmpty was called with incompatible object " + obj +
  357. "; must be an Object or Array.");
  358. }
  359. };
  360. /* ARRAYS */
  361. /**
  362. * Returns the first element of the list; or if the optional n argument
  363. * is supplied, returns the first n elements of the list an an Array.
  364. */
  365. _.first = _.head = function(list:Array, n:int = -1):Object {
  366. return n == -1 ? list[0] : list.slice(0, n);
  367. };
  368. _.rest = _.tail = function(list:Array, n:int = 1):* {
  369. return list.slice(n);
  370. };
  371. /**
  372. * Returns the last element of the array. If the optional n argument
  373. * is supplied, returns an array of the last n elements of the array.
  374. */
  375. _.last = function(list:Array, n:int = -1):Object {
  376. if (n == -1) {
  377. return list[list.length - 1];
  378. } else {
  379. return list.slice(-n);
  380. }
  381. };
  382. /**
  383. * Returns an array with all false, null, undefined, NaN, or empty-string
  384. * values removed.
  385. */
  386. _.compact = function(list:Array):Array {
  387. return _(list).select(function(element:*):Boolean {
  388. return !!element; // seems to work in AS3 as well as Javascript
  389. });
  390. };
  391. /** Returns a flattened version of a nested array. */
  392. _.flatten = function(list:Array):Array {
  393. return _(list).reduce(function(accumulator:Array, element:*):Array {
  394. if (element is Array) {
  395. return accumulator.concat(_(element).flatten());
  396. }
  397. accumulator.push(element);
  398. return accumulator;
  399. }, []);
  400. };
  401. _.without = function(list:Array, ...targets):Array {
  402. return _(list).reject(function(element:*):Boolean {
  403. return _(targets).any(function(target:*):Boolean {
  404. return element == target;
  405. });
  406. });
  407. };
  408. _.unique = function(list:Array):Array {
  409. // use immediately executing function to isolate known array
  410. return (function(list:Array):Array {
  411. var known:Array = [];
  412. return _(list).select(function(element:*):Boolean {
  413. if (_(known).includes(element)) {
  414. return false;
  415. } else {
  416. known.push(element);
  417. return true;
  418. }
  419. });
  420. })(list);
  421. };
  422. /** Returns an array of all values which are present in all supplied arrays. */
  423. _.intersect = function(list:Array, ...targets):Array {
  424. return _(list).select(function(element:*):Boolean {
  425. return _(targets).all(function(target:Array):Boolean {
  426. return _(target).includes(element);
  427. });
  428. });
  429. };
  430. /** Zips multiple arrays together. Essentially rotates a nested array 90 degrees. */
  431. _.zip = function(...args):Array {
  432. var maxRowLength:int = _(args).chain().pluck("length").max().value();
  433. var results:Array = [];
  434. for (var rowIndex:int = 0; rowIndex < maxRowLength; rowIndex++) {
  435. var column:Array = [];
  436. for (var columnIndex:int = 0; columnIndex < args.length; columnIndex++) {
  437. column.push(args[columnIndex][rowIndex]);
  438. }
  439. results.push(column);
  440. }
  441. return results;
  442. }
  443. _.range = function(start:Number, stop:Number = NaN, step:Number = NaN):Array {
  444. if (isNaN(stop)) { // _.range(10) counts 0 to 9
  445. stop = start;
  446. start = 0;
  447. }
  448. step = step || 1;
  449. var length:int = Math.max(Math.ceil((stop - start) / step), 0);
  450. var index:int = 0;
  451. var range:Array = [];
  452. while (index < length) {
  453. range.push(start);
  454. start += step;
  455. index++;
  456. }
  457. return range;
  458. }
  459. /* FUNCTIONS */
  460. /**
  461. * Binds the function to the supplied object, returning a "bound" function
  462. * whose "this" is the supplied object. Optionally curries the function
  463. * with the supplied arguments. If these terms are unfamiliar, look up the
  464. * fundamentals of scope, binding, and closures in AS3 (or Javascript,
  465. * since they work almost identically and JS resources may be more common).
  466. * And prepare to be enlightened.
  467. */
  468. _.bind = function(f:Function, obj:Object = null, ...curriedArgs):Function {
  469. // The ...rest construct will yield an empty array if no extra args are passed.
  470. return function(...invocationArgs):* {
  471. return f.apply(obj, curriedArgs.concat(invocationArgs));
  472. };
  473. };
  474. /**
  475. * Binds functions to the supplied object. If no function names are given,
  476. * binds all functions found on the object. Useful when the functions will
  477. * be called in a foreign context, such as an event listener. Use this to
  478. * simulate method binding when constructing objects at runtime.
  479. */
  480. _.bindAll = function(obj:Object, ...functionNames):Object {
  481. _(functionNames).isEmpty() && (functionNames = _(obj).functions());
  482. _(functionNames).each(function(name:String):void {
  483. obj[name] = _(obj[name]).bind(obj);
  484. });
  485. return obj;
  486. };
  487. /**
  488. * Memoizes a function by caching its results in a lookup table stored in
  489. * the closure. For example, once f(x:int) is called as f(100), the result
  490. * is cached before it is returned, and future calls as f(100) will simply
  491. * return the cached result.
  492. *
  493. * Uses a Dictionary internally instead of an Object, meaning that
  494. * non-scalar arguments will be keyed by identity. If f(s:Sprite) is
  495. * called as f(foo), future calls as f(foo) will return the cached result;
  496. * but even if sprite "bar" is precisely identical to foo, f(bar) will
  497. * force a new function call and cache a new value.
  498. *
  499. * Optionally accepts a "hasher" function which produces a hash code for a
  500. * given input value. For instance, if all input values 0-10, 11-20, etc
  501. * are expected to produce the same output, hasher could be designed to
  502. * produce a single key for each level of input:
  503. * function(n:int):String {return (Math.floor(n / 10) * 10).toString();}.
  504. *
  505. * Defining a custom hasher function is the only way to memoize a function
  506. * which takes more than one argument, or to memoize a function which
  507. * might return the same result for non-identical arguments.
  508. */
  509. _.memoize = function(f:Function, hasher:Function = null):Function {
  510. var memo:Dictionary = new Dictionary(); // lookup table mapping input to output values
  511. hasher = hasher || identity;
  512. return function(...args):* {
  513. var key:String = hasher.apply(this, args);
  514. return key in memo ? memo[key] : (memo[key] = f.apply(this, args));
  515. };
  516. };
  517. /**
  518. * Executes the function after a delay. Optional args will be passed to the
  519. * function at runtime.
  520. *
  521. * @return A Timer which can be stopped to prevent the delayed execution.
  522. */
  523. /* Although AS3 has setTimeout and setInterval, the Adobe-approved timing
  524. * method is Timer, so _.delay returns a Timer which can be stopped to
  525. * prevent the delayed function.
  526. */
  527. _.delay = function(f:Function, wait:int, ...args):Timer {
  528. var timer:Timer = new Timer(wait, 1);
  529. timer.addEventListener(TimerEvent.TIMER_COMPLETE, function(event:TimerEvent):void {
  530. f.apply(f, args);
  531. });
  532. timer.start();
  533. return timer;
  534. }
  535. /**
  536. * Executes the function after the current call stack has completed. Good
  537. * for functions which should not block execution, or to call a single
  538. * event handler after many synchronous calls. Alternative strategy: create
  539. * an event handler using _.debounce(f, 0).
  540. *
  541. * @return A Timer which can be stopped to prevent the deferred execution.
  542. */
  543. _.defer = function(f:Function, ...args):Timer {
  544. return _(f).delay(0);
  545. }
  546. /**
  547. * Internal function, but debounce and throttle can be considered
  548. * convenience methods for this.
  549. */
  550. // choke implementation suggested by Nick Schaubeck
  551. var limit:Function = function(f:Function, wait:int,
  552. debounce:Boolean = false, callThrottledImmediately:Boolean = false):Function {
  553. var timer:Timer = new Timer(wait, 1);
  554. /*
  555. These variables are defined outside the returned function, but set
  556. within it. This lets us create the throttler and set up the timer
  557. events outside the returned function, which will be called many times.
  558. */
  559. var context:Object;
  560. var args:Array;
  561. var throttler:Function = function():void {
  562. timer.stop();
  563. callThrottledImmediately || f.apply(context, args);
  564. };
  565. timer.addEventListener(TimerEvent.TIMER_COMPLETE, throttler);
  566. return function(...runtimeArgs):* {
  567. args = runtimeArgs;
  568. context = this;
  569. callThrottledImmediately && !timer.running && f.apply(context, args);
  570. debounce && timer.stop();
  571. (debounce || !timer.running) && timer.start();
  572. };
  573. };
  574. /**
  575. * Returns a wrapped function which can only execute once every so many
  576. * milliseconds. As in underscore.js, even the first call is delayed by
  577. * the wait duration; but if the optional callImmediately argument is
  578. * true, the first call occurs immediately and subsequent calls are
  579. * locked out for the wait duration.
  580. */
  581. _.throttle = function(f:Function, wait:int, callImmediately:Boolean = false):Function {
  582. return limit(f, wait, false, callImmediately);
  583. };
  584. /**
  585. * Returns a wrapped function which executes once, and then fails silently
  586. * for the given cooldown period. Equivalent to calling _.throttle with the
  587. * callImmediately argument = true.
  588. */
  589. _.choke = function(f:Function, wait:int):Function {
  590. return limit(f, wait, false, true);
  591. }
  592. /**
  593. * Returns a wrapped function which only executes the most recent call,
  594. * after the given delay. Each call resets the delay. Good for performing
  595. * an update after a sequence of rapid inputs, such as typing or dragging.
  596. */
  597. _.debounce = function(f:Function, wait:int):Function {
  598. return limit(f, wait, true);
  599. };
  600. /**
  601. * Returns a function which takes the initial function as an argument.
  602. * Useful for automatically transforming or interpreting the result of the
  603. * intial function, or logging when it is called, or something. Wrapper is
  604. * welcome to pass its arguments along to the original function, or take
  605. * completely different ones.
  606. */
  607. _.wrap = function(f:Function, wrapper:Function):Function {
  608. return function(...args):* {
  609. args.unshift(f);
  610. return wrapper.apply(this, args);
  611. }
  612. };
  613. /**
  614. * Creates a function whose return value is the result of passing the
  615. * output of each function as the sole argument of the next. The classic
  616. * example is h(x) = g(f(x)) -- the input to g() is the output of f(x).
  617. * More practically, createRedSprite = _.compose(buildSprite,
  618. * turnSpriteRed);
  619. *
  620. * While underscore.js passed values from right to left, I'm passing them
  621. * left to right. It seems more natural to me. If this is actually a
  622. * violation of some functional programming convention, let me know and
  623. * I'll change it!
  624. *
  625. * Also, in underscore.js, the first function to be called could have more
  626. * than one argument, while the others could not. This seems inconsistent
  627. * both internally and with the concept of function composition. I've made
  628. * it so each function takes exactly one input value.
  629. */
  630. _.compose = function(f:Function, ...functions):Function {
  631. functions.unshift(f);
  632. return function(input:*):* {
  633. var result:* = null;
  634. _(functions).each(function(f:Function):void {
  635. result = f.call(this, result || input);
  636. });
  637. return result;
  638. }
  639. };
  640. /* OBJECTS */
  641. /**
  642. * Returns a list of the collection's keys. Returns ints if given an array.
  643. */
  644. _.keys = function(obj:*):Array {
  645. var keys:Array = [];
  646. if (obj is Array) {
  647. return _.range(0, obj.length);
  648. } else {
  649. for (var key:String in obj) {
  650. obj.hasOwnProperty(key) && keys.push(key);
  651. }
  652. }
  653. return keys;
  654. };
  655. /** Returns a list of the collection's values. */
  656. _.values = function(obj:*):Array {
  657. return _.map(obj, identity);
  658. };
  659. /** Returns a list of the names of all functions in the collection. */
  660. _.functions = _.methods = function(obj:*):Array {
  661. return _.filter(_.keys(obj), function(key:String):Boolean {
  662. return obj[key] is Function;
  663. }).sort();
  664. };
  665. /**
  666. * Extends the object with all the properties in the supplied object(s).
  667. * Alters and returns the original object, NOT a copy. To extend a copy,
  668. * var copy:Object = _(obj).chain().clone().extend(otherObj).value().
  669. *
  670. * An object can be extended with arrays, though this would not be a
  671. * terribly sensible thing to do. The opposite doesn't work at all. Fails
  672. * on XMLLists without even throwing ArgumentError, because why would you
  673. * even try to do that, seriously?
  674. */
  675. _.extend = function(obj:Object, ...args):Object {
  676. each(args, function(source:Object):void {
  677. for (var key:String in source) {
  678. obj[key] = source[key];
  679. }
  680. });
  681. return obj;
  682. };
  683. /**
  684. * Creates a shallow copy of an Array or Object.
  685. *
  686. * @throws ArgumentError if supplied an XMLList.
  687. */
  688. _.clone = function(obj:*):* {
  689. if (obj is XMLList) throw new ArgumentError("_.clone cannot operate on an XMLList.");
  690. return (obj is Array) ? obj.slice() : _({}).extend(obj);
  691. };
  692. /**
  693. * Insert this in a method chain to invoke the interceptor on the object
  694. * being chained at that point.
  695. */
  696. _.tap = function(obj:*, interceptor:Function):* {
  697. interceptor(obj);
  698. return obj;
  699. };
  700. /*
  701. * All the _.isFoo functions from underscore.js are omitted, since AS3's
  702. * equality and "is" operators do a good enough job.
  703. */
  704. /* UTILITY */
  705. /**
  706. * Run a function n times. Optional third context argument. The function
  707. * itself may accept the loop counter as its argument.
  708. */
  709. _.times = function(n:int, f:Function, context:Object = null):void {
  710. for (var i:int = 0; i < n; i++) {
  711. safeCall(f, context, i);
  712. }
  713. };
  714. _.mixin = function(obj:*):void {
  715. each(_.functions(obj), function(name:String):void {
  716. // remember, the value of an assignment statement is the value that was assigned.
  717. addToWrapper(name, _[name] = obj[name]);
  718. });
  719. };
  720. /** A default iterator used for functions which can optionally detect truthy values. */
  721. var identity:Function = function(value:*, index:* = null, list:* = null):* {
  722. return value;
  723. };
  724. /* OOP WRAPPER */
  725. _.prototype = Wrapper.prototype;
  726. var result:Function = function(obj:*, chain:Boolean):* {
  727. // if chaining, continue the chaining wrapper; otherwise return naked object
  728. return chain ? _(obj).chain() : obj;
  729. }
  730. var addToWrapper:Function = function(name:String, fn:Function):void {
  731. Wrapper.prototype[name] = function(...args):* {
  732. return result(fn.apply(_, [this._wrapped].concat(args)), this._chain);
  733. };
  734. };
  735. _.mixin(_);
  736. return _;
  737. })();
  738. }