CoffeeScript

Möjligheter och utmaningar

1. Förord

2. Vad är CoffeeScript

3. Syntax

4. Variabeldeklarationer

5. Strängar

6. Funktioner

7. Loopar

8. Klasser

9. Utvecklingsverktyg

10. Mina erfarenheter

1. Förord

Denna uppsats är en genomgång av vad CoffeeScript är samt vad som skiljer språket från JavaScript. För att till fullo kunna ta del av innehållet krävs en viss bekantskap med JavaScript, då en hel del demonstrationer görs med hjälp av kodexempel. I kapitel 10 ger jag utrymme för mina egna synpunkter och erfarenheter av hur det är att jobba med CoffeeScript.

2. Vad är CoffeeScript

CoffeeScript är ett enkelt programmeringsspråk som kompilerar till JavaScript. Språket bidrar inte med någon extra funktionalitet utan tillför syntaktiskt socker med influenser av Ruby, Python och Lisp.

Språket publicerades den 24 December 2009 av Jeremy Ashkenas, upphovsman till de kända JavaScript-biblioteken Underscore.js och Backbone.js. CoffeeScript har nu en stor skara anhängare och är i skrivande stund 13:e största språk på GitHub.

3. Syntax

CoffeeScripts syntax skiljer sig från JavaScripts på några fundamentala punkter:

JavaScript CoffeeScript
=== is
!== isnt
! not
if ! unless
&& and
|| or
true yes, on
false no, off
this @
prototype ::

4. Variabeldeklarationer

CoffeeScript saknar nyckelordet 'var' - en variabel kan inte deklareras utan samtidigt att tillsätta den ett värde.

Variabelns scope hamnar längst upp i trädet där dess namn förekommer:

variableOne = null
myFunction = ->
    variableOne = 1
    variableTwo = 2
    return

Blir i JavaScript

var myFunction, variableOne;
variableOne = null;
myFunction = function() {
    var variableTwo;
    variableOne = 1;
    variableTwo = 2;
};

Detta innebär att det inte är möjligt att skugga en variabel i ett yttre scope. I bästa fall uppmuntrar detta till variabelnamngivning som ökar läsligheten, i värsta fall kan detta skapa grogrund för svårupptäckta buggar.

5. Strängar

CoffeeScript lånar Rubys implementation av stränginterpolation. Detta innebär möjlighet att enkelt infoga variabler och uttryck i en sträng:

name = 'Joe Anderson'
alert "My name is #{name}"

Strängar deklarerade inom enkla citationstecken har samma beteende som i JavaScript. Vidare erbjuds flerradiga strängar och strängar med bibehållen indentering:

"Det här är en
flerradig sträng.
Radbyten översätts
till mellanslag"

"""<p>
        En sträng inom
        <span>
            tre stycken dubbla citationstecken
        </span>
        bibehåller sin indentering
</p>"""

"Vill.man.inte.ha.några.blanksteg.\
mellan.rader.kan.man.undfly.dessa.\
med.ett.'backslash'"

6. Funktioner

Deklaration

I ett språk där funktioner behandlas som första klassens medborgare kan nyckelordet 'function' bli lite pratigt. Detta problem har lösts elegant i CoffeeScript, med tecknet '->' för att deklarera en anonym funktion. Detta tillsammans med den goda JavaScript-praxisen att alltid placera en callback som sista argument i en funktion erbjuder en ren och lättförståelig syntax för att hantera eventdriven utveckling:

wait 500, -> alert 'Done waiting!'

I JavaScript

wait(500, function() {
    return alert('Done waiting!');
});

Ytterligare exempel med jQuerys .animate och .post:

jQueryObject.animate opacity: 0, -> fadeOutComplete()

jQuery.post(toUrl, data)
.always ->
    hideAjaxLoader()
.done (response) ->
    display(response)
.fail ->
    alert 'An error occured!'

Implicita returvärden

Notera i den kompilerade JavaScriptkoden ovan hur uttrycket från callbackfunktionen returneras utan något ingrepp från vår sida. Detta är ett resultat av en omdebatterad egenskap i CoffeeScript Alla funktioner returnerar resultatet av sitt sista uttryck. Detta tillför språket en funktionell karaktär och möjliggör en mycket kortfattad syntax för matematiska uttryck, booleska funktioner och switchar:

# CoffeeScript
add = (a, b) -> a + b

// JavaScript
add = function(a, b) {
    return a + b;
};

# CoffeeScript
isAuthorized = (user) -> user.role is 'admin' and isValidSession(user.session)

// JavaScript
isAuthorized = function(user) {
    return user.role === 'admin' && isValidSession(user.session);
};

# CoffeeScript
colorForCode = (code) ->
    if code is 5 or code < 2
        'blue'
    else 'yellow'

// JavaScript
colorForCode = function(code) {
    if (code === 5 || code < 2) {
        return 'blue';
    } else {
        return 'yellow';
    }
};

Även loopar deklarerade sist i en funktion kan returnera ett värde. Detta behandlas i nästa kapitel.

Standardvärden i argument

Liksom Python och Ruby har CoffeeScript stöd för standardvärden i de fall argument ej är definerade:

# CoffeeScript
welcome = (message = 'Hello!') -> console.log message
welcome() # 'Hello!' loggas

// JavaScript
var welcome;
welcome = function(message) {
    if (message == null) {
        message = 'Hello!';
    }
    return console.log(message);
};

7. Loopar

Loopar är ett område där CoffeeScript tar ett avsteg från sin föregångare. Alla ursprungliga 'for'-loopar är undangömda och ersätts med CoffeeScript-specifika 'for in' för listor och 'for of' för objekt:

# CoffeeScript
for key, value of obj
    console.log key, value

// JavaScript
var key, value;
for (key in obj) {
    value = obj[key];
    console.log(key, value);
}

# CoffeeScript
for item, index in array
    console.log item, index

// JavaScript
var index, item, _i, _len;
for (index = _i = 0, _len = array.length; _i < _len; index = ++_i) {
    item = array[index];
    console.log(item, index);
}

Notera att CoffeeScript inte kompilerar 'for in'-loopen till en 'foreach' - detta är en optimering som ger stora prestandavinster[1] i de fall man inte behöver ett 'function scope'. CoffeeScript cachar också listans längd vilket är en optimering för äldre versioner av Internet Explorer, utan några direkta fördelar i moderna browsers[2].

Vidare erbjuds möjligheter till kortfattade filtreringar och mappningar, exempelvis:

# CoffeeScript
arr = [2, 1, 2, 3, 5, 4, 3]
filtered = (item for item in arr when item is 3)
# filtered blir = [3, 3]

// JavaScript
var arr, filtered, item;
arr = [2, 1, 2, 3, 5, 4, 3];
filtered = (function() {
    var _i, _len, _results;
    _results = [];
    for (_i = 0, _len = arr.length; _i < _len; _i++) {
        item = arr[_i];
        if (item === 3) {
            _results.push(item);
        }
    }
    return _results;
})();

Även loopar har implicita returvärden när de är placerade sist i en funktion:

# CoffeeScript
newItems = do ->
    for item in items
        newItemFrom(item)

// JavaScript
var newItems;
newItems = (function() {
    var item, _i, _len, _results;
    _results = [];
    for (_i = 0, _len = items.length; _i < _len; _i++) {
        item = items[_i];
        _results.push(newItemFrom(item));
    }
    return _results;
})();

Vill man ha tillgång till JavaScripts ursprungliga loopfunktionalitet får man använda sig av while-loopen eller CoffeScripts negativa variant, 'until':

# CoffeeScript
until gameEnded()
    newTurn()

// JavaScript
while (!gameEnded()) {
    newTurn();
}

[1] http://jsperf.com/for-vs-foreach/65

[2] http://jsperf.com/caching-array-length/4

8. Klasser

Förutom alternativ till nyckelorden 'this' (@) och 'prototype' (::) erbjuder CoffeeScript syntaktiskt socker för enklare hantering av arv. Detta genom nyckelordet 'extends'. Men först ett exempel på hur man definerar en klass i CoffeeScript

# CoffeeScript
class Person
    constructor: (@name) ->
    greeting: -> "My name is #{@name}"

// JavaScript
var Person;

Person = (function() {
  function Person(name) {
    this.name = name;
  }

  Person.prototype.greeting = function() {
    return "My name is " + this.name;
  };

  return Person;

})();

Notera hur CoffeeScript mappar argumenten i konstruktorn till klassens egenskaper. 'Person'-klassen kan nu lätt ärvas och utvecklas till en 'Employee':

# CoffeeScript
class Employee extends Person
    constructor: (@name, @company) ->
    greeting: -> "#{super()} and I work at #{@company}"

// JavaScript
var Employee,
    __hasProp = {}.hasOwnProperty,
    __extends = function (child, parent) {
        for (var key in parent) {
            if (__hasProp.call(parent, key)) child[key] = parent[key];
        }

        function ctor() {
            this.constructor = child;
        }
        ctor.prototype = parent.prototype;
        child.prototype = new ctor();
        child.__super__ = parent.prototype;
        return child;
    };

Employee = (function (_super) {
    __extends(Employee, _super);

    function Employee(name, company) {
        this.name = name;
        this.company = company;
    }

    Employee.prototype.greeting = function() {
        return "" + (Employee.__super__.greeting.call(this)) + " and I work at " + this.company;
    };

    return Employee;

})(Person);

I den ärvda klassen får vi tillgång till 'super'-nyckelordet, vilket är oumbärligt i de fall man behöver en avancerad klassstruktur. Som framgår av exemplet behövs en hel del boilerplate för att uppnå detta i JavaScript.

9. Utvecklingsverktyg

Kompilering

CoffeeScript kan kompileras live i en webläsare genom att importera följande script:

<script src="http://coffeescript.org/extras/coffee-script.js">

Och injicera sin kod i följande metod:

CoffeeScript.compile(myCode)

Bäst är dock att förkompilera sin kod, antingen genom ett plugin i en texteditor eller via kommandotolken. Kommandotolken körs i node.js och kan enkelt installeras via npm:

$ npm install -g coffee-script

Nu kan man sätta kompilatorn i 'watch mode' för att omkompilera valda filer så fort de sparas:

$ coffee --compile --watch myCoffeeFolder

Man kan också starta kompilatorn i interaktivt läge för att testa några rader kod:

$ coffee
coffee> 1 + 1
2
coffee>

I Chrome Web Store finns flera plugin som gör att Chrome-konsolen kan tolka CoffeeScript. Detta effektiviserar tester och experiment då CoffeeScript syntax är så kompakt.

Debuggning

Sedan mars 2013 har kompilatorn stöd för 'source maps', vilket gör att man kan debugga CoffeeScript direkt i Chrome, Firefox och IE11. En source map skapas genom att lägga till flaggan 'map':

$ coffee --map --compile myfile.coffee

Editorer, IDE:n

CoffeeScript stöds av de flesta editorer värda namnet[0]. Många plugin har (utöver syntax highlighting) stöd för livekompilering till JavaScript i ett separat fönster.

[0] https://github.com/jashkenas/coffee-script/wiki/Text-editor-plugins

Migrering

Det finns ett utmärkt verktyg för att konvertera existerande projekt till CoffeeScript

$ npm install -g js2coffee
$ js2coffee file.js > file.coffee

Viss handpåläggning krävs om man vill använda CoffeeScripts klass- och loopsyntax.

10. Mina erfarenheter

Jag har i skrivande stund (maj 2014) jobbat med CoffeeScript i över 3 månader och det har varit en mycket trevlig erfarenhet. Ett av mina projekt har varit ett multiplattforms-CMS med tusentals rader kod och jag upplever att produktiviteten ökar. Jag ska försöka ge några förklaringar till detta.

Kortfattad syntax

CoffeeScripts minimalistiska syntax innebär inte bara att man rent fysiskt skriver snabbare - man slipper även en hel del boilerplate för bland annat klasser, funktionsdeklarationer och loopar, detta tack vare det syntaktiska sockret.

Ökad läslighet

Mindre kod innebär i CoffeeScripts fall också mer läslig kod - Det mesta som är borttaget är överflödiga tecken och symboler såsom parenteser, klammerparenteser, semikolon och variabeldeklarationer. I och med att klammerparenteser ersätts av indentering försvinner också en hel del radbrytningar vilket gör koden mer kompakt och överblickbar.

En annan anledning till läsligheten är de semantiska symbolerna. Tillsammans med en genomtänkt namngivning av variabler resulterar de i en kod som ligger väldigt nära mänskligt språk.

Snabbare prototypning och refaktorering

I och med att kodstrukturen är så enkel i CoffeeScript så blir varje refaktorering snabbare och mindre mödosam. Tack vare detta ökar chansen att kodbasen håller en högre kvalité under längre projekt. Likaså blir prototypning och experimenterande enklare - det går väldigt snabbt att skapa ett test case, något som ökar möjligheterna till att hitta bra lösningar på längre sikt. I och med verktyg som coffee-cli och CoffeeScript i Chrome-konsolen påskyndas också felsökning och testning i projekt som körs skarpt.


Erik Österberg, maj 2014

erik82osterberg@gmail.com

@e_osterberg

www.erikosterberg.com