CoffeeScript: Getter/Setter in Object Initializers

2019-01-29 21:06发布

问题:

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.

回答1:

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


回答2:

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!



回答3:

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.



回答4:

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


回答5:

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


回答6:

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"