Thursday, 7 June 2012

A Scope Gotcha In CoffeeScript

I think a , and — after developing with it throughout May — have grown to like it a lot. However, hidden amongst all the smooth elegance are a few rough edges, to confuse and cut the unwary. Like me. This is the story of a small scope that caught me out twice.

Thanks to , I might implement a widget to manage a list of items a little like this, and when the user clicks the fat arrow sets a allowing an easy call to my new object.

class Foo 
 constructor: ->
   @items = []
   
 doSomething: ->
 
 buildList: ->
   $ul = $('<ul/>')
   for item, index in @items 
      $li = $('<li/>')
      $li.on 'click', =>
        $li.addClass 'active'
        @doSomething()
        false
      $ul.append $li

Except this doesn't quite work. To see why the active class ends up on last list element, take a look at the compiled :

var Foo;
var __bind = function(fn, me){ return function(){ return fn.apply(me, arguments); }; };
Foo = (function() {
  function Foo() {
    this.items = [];
  }
  Foo.prototype.doSomething = function() {};
  Foo.prototype.buildList = function() {
    var $li, $ul, index, item, _len, _ref, _results;
    $ul = $('<ul/>');
    _ref = this.items;
    _results = [];
    for (index = 0, _len = _ref.length; index < _len; index++) {
      item = _ref[index];
      $li = $('<li/>');
      $li.on('click', __bind(function() {
        $li.addClass('active');
        this.doSomething();
        return false;
      }, this));
      _results.push($ul.append($li));
    }
    return _results;
  };
  return Foo;
})();

Oops - $li is defined right at the top of buildList. The context will contain the current value, from the last iteration.

There are several ways to improve this code and make this problem go away. One easy technique is just to factor out a method to build the list item.

class Foo 
 constructor: ->
   @items = []
   
 doSomething: ->
 
 buildListItem: (item, index) ->
      $li = $('<li/>')
      $li.on 'click', =>
        $li.addClass 'active'
        @doSomething()
        false
      $ul.append $li
  
 buildList: ->
   $ul = $('<ul/>')
   for item, index in @items 
      $ul.append @buildListItem item, index

Though this applies equally to JavaScript, I think that the very cleanness of CoffeeScript allows this sort of bug to creep in more easily. The lesson to take away — keep CoffeeScript methods small and objects well factored.

No comments:

Post a Comment