43

ECMAScript allows us to define getters or setters as following:

[text/javascript]

var object = {
  property: 7,
  get getable() { return this.property + 1; },
  set setable(x) { this.property = x / 2; }
};

I can work around if I'm using a class:

[text/coffeescript]

"use strict"

Function::trigger = (prop, getter, setter) ->
      Object.defineProperty @::,
              get: getter
              set: setter               

class Class
      property: ''

      @trigger 'getable', ->
               'x'

      member: 0

But what if I want to define trigger on the object directly - without using defineProperty / -ies. I want to do something like (it's not working that way):

[text/x-pseudo-coffeescript]

object =
  property: 'xhr'
  get getable: 'x'

It's working in JavaScript without any problems and I don't want my scripts to regress when I'm using CoffeeScript. Isn't there a way to do this as comfortable as in JavaScript/ECMAScript? Thanks.

fridojet
  • 1,276
  • 3
  • 15
  • 29
  • 3
    Not for now :(. A quote form [the FAQ](https://github.com/jashkenas/coffee-script/wiki/FAQ): _Q: Will you add feature X where feature X depends on a platform? A: No, implementation-specific features are not allowed as a policy. Everything that you write in CoffeeScript should be supported and runnable on any current JavaScript implementation (in practice, this means the lowest common denominator is IE6). Thus, features such as the following will not be implemented: getters & setters, yield_. I think that having getter & setter literal syntax would be a nice opt-in feature for CoffeeScript. – epidemian Jul 21 '12 at 01:09
  • @epidemian Thanks, that's what I wanted to know. - But: Is it possible to build such an opt-in feature for CoffeeScript cleanly (without modifying the compiler directly)? – fridojet Jul 21 '12 at 10:18
  • I don't that would be possible. There is [a branch](https://github.com/StanAngeloff/coffee-script/compare/getters_setters) that implements them, but it hasn't been maintained for a long time it seems (and i wouldn't recommend using a CoffeeScript fork just for this feature). I added an answer with my previous comment and a bit more information. – epidemian Jul 21 '12 at 14:12
  • Errata: i don't _think_ that would be possible :P – epidemian Jul 21 '12 at 14:49

6 Answers6

78

No, not for now :(

From the CoffeeScript FAQ:

Q: Will you add feature X where feature X depends on a platform?

A: No, implementation-specific features are not allowed as a policy. Everything that you write in CoffeeScript should be supported and runnable on any current JavaScript implementation (in practice, this means the lowest common denominator is IE6). Thus, features such as the following will not be implemented: getters & setters, yield.

Some GitHub issues about getter & setter syntax: #64, #451, #1165 (there is some nice discussion in the last one).

I personally think that having getter & setter literal syntax would be a nice opt-in feature for CoffeeScript now that defineProperty is part of the ECMAScript standard. The need for getters & setters in JavaScript can be questionable, but you're not forced to use them just because they exist.


Anyway, as you noticed, it's not that hard to implement a convenient wrapper function that calls Object.defineProperty for class declarations. I personally would use the approach suggested in here:

Function::property = (prop, desc) ->
  Object.defineProperty @prototype, prop, desc

class Person
  constructor: (@firstName, @lastName) ->
  @property 'fullName',
    get: -> "#{@firstName} #{@lastName}"
    set: (name) -> [@firstName, @lastName] = name.split ' '

p = new Person 'Robert', 'Paulson'
console.log p.fullName # Robert Paulson
p.fullName = 'Space Monkey'
console.log p.lastName # Monkey

Or, maybe create two different methods:

Function::getter = (prop, get) ->
  Object.defineProperty @prototype, prop, {get, configurable: yes}

Function::setter = (prop, set) ->
  Object.defineProperty @prototype, prop, {set, configurable: yes}

class Person
  constructor: (@firstName, @lastName) ->
  @getter 'fullName', -> "#{@firstName} #{@lastName}"
  @setter 'fullName', (name) -> [@firstName, @lastName] = name.split ' '

For plain objects you can just use Object.defineProperty (or Object.defineProperties ;) ) on the object itself as Jason proposed. Maybe wrap that in a little function:

objectWithProperties = (obj) ->
  if obj.properties
    Object.defineProperties obj, obj.properties
    delete obj.properties
  obj

rectangle = objectWithProperties
  width: 4
  height: 3
  properties:
    area:
      get: -> @width * @height

console.log rectangle.area # 12
rectangle.width = 5
console.log rectangle.area # 15
Community
  • 1
  • 1
epidemian
  • 18,817
  • 3
  • 62
  • 71
  • 3
    There is a gotcha here in derived classes using properties. `super` doesn't behave the way you might expect. Not a big deal, but worth pointing out. See https://gist.github.com/4236746 – Dave Peck Dec 07 '12 at 21:36
  • @DavePeck Nice catch! Thanks for pointing it out. The compiled output of the first example in your Gist is very screwed up. It seems in general the behaviour of `super` is quite fragile, i'm surprised that even `obj = foo: -> super` [compiles](http://coffeescript.org/#try:obj%20%3D%20foo%3A%20-%3E%20super) (to something broken). Luckily, the semantics of `super` [will probably be revised](https://github.com/michaelficarra/CoffeeScriptRedux/issues/106#issuecomment-11102238) for CoffeeScript 2. I doubt, however, that there will be any plan to make `super` be getter/setter friendly. – epidemian Dec 08 '12 at 02:14
33

Here's another approach for defining properties with getters and setters in CoffeeScript that maintains a relatively clean syntax without adding anything to the global Function prototype (which I'd rather not do):

class Person
  constructor: (@firstName, @lastName) ->
  Object.defineProperties @prototype,
    fullName:
      get: -> "#{@firstName} #{@lastName}"
      set: (name) -> [@firstName, @lastName] = name.split ' '

p = new Person 'Robert', 'Paulson'
console.log p.fullName # Robert Paulson
p.fullName = 'Space Monkey'
console.log p.lastName # Monkey

It works well with many properties. For example, here's a Rectangle class that is defined in terms of (x, y, width, height), but provides accessors for an alternative representation (x1, y1, x2, y2):

class Rectangle                                     
  constructor: (@x, @y, @w, @h) ->
  Object.defineProperties @prototype,
    x1:
      get: -> @x
      set: (@x) ->
    x2:
      get: -> @x + @w
      set: (x2) -> @w = x2 - @x
    y1:
      get: -> @y
      set: (@y) ->
    y2:
      get: -> @y + @h
      set: (y2) -> @w = y2 - @y

r = new Rectangle 5, 6, 10, 11
console.log r.x2 # 15

Here's the corresponding JavaScript code. Enjoy!

curran
  • 1,261
  • 13
  • 8
  • 2
    This is awesome! It works!! I love CoffeeScript but it's STUPID getters/setters aren't natively supported. – Pete Alvin Feb 27 '15 at 01:52
  • FWIW the `@prototype` syntax doesn't seem to be working for me (in or out of the constructor). I'm not sure why - other examples also suggest that format. But in case someone else runs into that problem simply doing `Object.defineProperties Rectangle...` (outside the scope of the class declaration) seems to work reliably for me. – Rod Dec 16 '20 at 17:51
8

You can use Object.defineProperty on straight JSON objects as well.

obj = {}
Object.defineProperty obj, 'foo',
    get: ->
        return 'bar'

The get/set notation does not work for various reasons in CoffeeScript. The biggest being that the compiler has not been built to account for get/set notation.

Note that get/set is not supported by all browsers (specifically, IE). Also note that the new ECMA standards (ECMAScript5) mentions Object.defineProperty as the way to define properties with getters/setters.

Rory O'Kane
  • 29,210
  • 11
  • 96
  • 131
Jason L.
  • 2,464
  • 1
  • 23
  • 41
  • Okay, but is there a way to *extend* CoffeeScript that way *cleanly*? – fridojet Jul 20 '12 at 21:56
  • To allow for get myGetter.. and set mySetter...? No, not without going into the compiler and making those changes. – Jason L. Jul 20 '12 at 21:58
  • I dont think that because its a "whitespace-significant language and the compiler has not been built to account for get/set notation" is the reason coffescript is not supporting it. You are implying that "whitespace-ness" is the problem cs could not have supported the syntax, where the valid reason instead is "implementation-specific features are not allowed as a policy." – Angelos Pikoulas Nov 11 '12 at 18:49
  • @Agelos you're right. For some reason the first thing I thought when I was answering this question was "that would confuse the compiler!". Your point is more accurate :) – Jason L. Nov 27 '12 at 22:27
5

Like @curran, I prefer not to modify Function prototype. Here it is what I did in one of my projects :

Define somewhere an utility function which for a given class returns 2 functions allowing you to easily add getters and setters on the prototype of the class:

gs = (obj) ->
  getter: (propName, getterFunction) ->
    Object.defineProperty obj.prototype, propName, 
      get: getterFunction
      configurable: true
      enumerable: true
  setter: (propName, setterFunction) ->
    Object.defineProperty obj.prototype, propName, 
      set: setterFunction
      configurable: true
      enumerable: true

gs stands for getter and setter.

Then, you build and import the two functions configured for your class :

class Dog
  { getter, setter } = gs @

  constructor: (name, age) -> 
    @_name = name
    @_age = age

  getter 'name', -> @_name
  setter 'name', (name) -> 
    @_name = name
    return

  getter 'age', -> @_age
  setter 'age', (age) -> 
    @_age = age
    return
Mickaël Gauvin
  • 607
  • 7
  • 15
1

An alternative approach:

get = (self, name, getter) ->
  Object.defineProperty self, name, {get: getter}

set = (self, name, setter) ->
  Object.defineProperty self, name, {set: setter}

prop = (self, name, {get, set}) ->
  Object.defineProperty self, name, {get: get, set: set}

class Demo 
  constructor: (val1, val2, val3) ->
    # getter only
    get @, 'val1', -> val1
    # setter only
    set @, 'val2', (val) -> val2 = val
    # getter and setter
    prop @, 'val3', 
      get: -> val3
      set: (val) -> val3 = val
M K
  • 9,138
  • 7
  • 43
  • 44
1

Thanks to the others that have gone before. Very generally and simply:

attribute = (self, name, getterSetterHash) ->
  Object.defineProperty self, name, getterSetterHash

class MyClass
  constructor: () ->
    attribute @, 'foo',
      get: -> @_foo ||= 'Foo' # Set the default value
      set: (who) -> @_foo = "Foo #{who}"

    attribute @, 'bar',
      get: -> @_bar ||= 'Bar'

    attribute @, 'baz',
      set: (who) -> @_baz = who


myClass = new MyClass()
alert(myClass.foo) # alerts "Foo"
myClass.foo = 'me' # uses the foo setter
alert(myClass.foo) # alerts "Foo me"
kwerle
  • 2,225
  • 22
  • 25