jQuery plugin authoring in CoffeeScript, with conv

2019-08-08 06:32发布

问题:

Background

So, I came across an article Essential jQuery Plugin Patterns a few months ago. I was trying to write a simple jQuery plugin and I found this article very useful. It contains a lot of information, most of which is beyond my expertise of JavaScript or jQuery. The main thing of interest was the first plugin template, dubbed jQuery Lightweight Plugin Boilerplate, which I referenced in another question a few months ago.

Recently, I started using CoffeeScript and looked up for a CoffeeScript jQuery Plugin Template, which I found here.

Both of these implement a pretty much same concept. Some of the important things different here are:

  • The private objects that are not there in the Lightweight Boilerplate
  • The namespacing block that is different in the CoffeeScript Template. (I have absolutely no idea what he did with methods[method].apply this, Array::slice.call(arguments, 1) though I somewhat understand the namespacing block of Lightweight Boilerplate.
  • In Lightweight Boilerplate chainability and iteration over objects is taken care of in the namespacing block, whereas in CoffeeScript Template method.init() needs to return $(this) as well as do $(this).each().

Research

So I went ahead and spliced both of these. I wrote my own template. Here it goes:

(($, window, document) ->

  # ---------------------------------------------------------------------------
  # Conventionally private variables
  # ---------------------------------------------------------------------------
  _PluginName = "FooBar"

  _Defaults   =
    property: 'value' # etc. etc.

  # ---------------------------------------------------------------------------
  # Private methods
  # ---------------------------------------------------------------------------
  _Debug      = (msg) ->
    window.console.log(msg)
    return

  # ---------------------------------------------------------------------------
  # Plugin Constructor
  # ---------------------------------------------------------------------------
  Plugin = (element, options) ->
    @element = element
    @options = $.extend true, {}, _Defaults, options

    # TODO: Call methods to do stuff
    return

  # ---------------------------------------------------------------------------
  # Plugin Methods
  # ---------------------------------------------------------------------------
  Plugin.prototype.init = () ->
    # TODO: Plugin initializiation logic
    return

  Plugin.prototype.destroy = () ->
    # TODO: Cleanup, unbind and eject
    return

  # ---------------------------------------------------------------------------
  # Actual plugin body
  # ---------------------------------------------------------------------------
  $.fn[_PluginName] = (options) ->
    return @.each () ->
      ($.data @, 'plugin_' + _PluginName
      new Plugin @, options
      ) unless $.data @, 'plugin_' + _PluginName
  return

) jQuery, window, document

I'm not asking for a comparison between both plugins. And my plugin is not even close to perfect. I found out from answer to another question that using the ($, window, document) closure is an overkill. There may be other faults and you are absolutely welcome to point them out and discuss them.

Nonetheless, I have some very specific questions.

Questions

  1. How safe are these conventionally 'private' objects/methods. What if someone intentionally wanted to call Plugin() instead of using the plugin the right way? Could _Defaults be accessed in any scope other than this closure?

  2. What exactly is the scope of these 'private' objects? Do they float around in jQuery namespace willy-nilly? Could they interfere with anything? How does using these 'private' variables help make the code better?

  3. I don't completely understand prototype. Are init() and destroy() properties of the Plugin() itself? Why do init() and destroy() refer to the options property of Plugin() as this.options as if init() and destroy() were defined inside the context of Plugin() itself... And what if I have an object defined as Plugin.prototype.methods? What is this for the functions inside the methods object?


NOTE: If it is difficult for anyone to follow links to view code, please let me know. I'll edit this question to include the code. I didn't include because then the question would become too long.

回答1:

When you do something like this:

(->
    Pancakes = ...
    ...
)()

the JavaScript version looks like this:

(function() {
  var Pancakes;
  Pancakes = ...
  ...
})();

In particular, Pancakes is local to the anonymous self executing function and so it cannot be accessed from outside the function. Also note that the CoffeeScript compiler will wrap each .coffee file in a self-executing function to avoid scope creep, i.e. this:

Pancakes = 6

ends up as (more or less):

(function() {
    var Pancakes = 6;
})();

so even if you don't manually add some sort of scope wrapper, CoffeeScript will add one for you. Of course, if someone is compiling the CoffeeScript themselves, they can suppress the outer function wrapper by running coffee --bare to compile the CoffeeScript. OTOH, everyone has a right to a foot-gun and they're welcome to shoot off their own feet if so desired.

That should answer 1 and 2.

As far as your prototype questions go, you should be writing:

Plugin::init = -> ...

instead of referencing prototype directly, they're the same thing but :: is more CoffeeScripty.

Back to the question at hand. When you say this:

class Pancakes

Pancakes::m = (where_is) -> 'house?'

that's the same as saying:

class Pancakes
    m: (where_is) -> 'house?'

so assigning something to prototype is just adding a method to the "class". That means that @ (AKA this) will (usually) be the object:

pancakes = new Pancakes
pancakes.m() # `@` will be `pancakes` inside `m`

Of course, the value of @ inside any CoffeeScript function depends (just like in JavaScript) on how the function is called and whether or not the function has been bound to an object.

You can verify this with a simple example:

class C
    constructor: ->
        @p = Math.random()
        console.log(@p)
C::m = -> console.log(@p)
(new C).m()

That will get you two copies of the same random number in your console as expected.

Hopefully that takes care of 3.