Monday, October 14, 2013

Writing JavaScript Prototypes in Python

Four days after Typhoon Nari, and still the power has not been restored to most areas of Tarlac, including where I live. Despite this I have managed to continue working on PythonScript at internet cafe's and other places with power and internet.

The with keyword is not often used in Python programs, probably you have only seen it used as a short cut for opening files. The with syntax also had support in early Javascript, but it is now deprecated. This makes with not useful for direct translation to JavaScript. This leaves with available for us to hijack in PythonScript as a custom syntax for other things. The new statement with javascript: now tells the compiler to treat the following block of code as inlined JavaScript. Previously you had to use the special JS(str) function to inline JavaScript as a string literal, this can still be useful, but in general leads to messy code. The new with javascript: statement can replace JS(str) in all cases where the code to be inlined is also valid Python syntax.

JavaScript Prototypes

Instead of proper classes, JavaScript has Object prototypes that can be extended with new functions that act like bound methods. In these functions the binding to this is based on the current calling context, so they break when assigned to a variable or passed to a callback function. Example:

func = document.createElement
e = func( 'a' )
e.setAttribute('id', 'myid')

The above will fail because createElement depends on its calling context of document in order to work. The ugly JavaScript way of doing this is using the bind function to create a wrapper function that binds document as the calling context:

func = document.createElement.bind( document )

Extending JavaScript Prototypes from PythonScript

Using the new with javascript: statement, we can now easily write inline JavaScript functions and assign them to prototypes using a special decorator syntax: @[class-name].prototype.[method-name]. The decorator ensures that when the method is called from PythonScript the proper calling context is bound to this.

The code below is from runtime/builtins.py it shows how the String prototype is changed to behave like Python's string class. Note that other decorators are not allowed within with javascript: blocked code.

with javascript:
    def _create_empty_object(arr):
            o = Object.create(null)
            for i in arr:
                o[ i ] = True
            return o


def _setup_str_prototype():

    with javascript:

        @String.prototype.startswith
        def func(a):
            if this.substring(0, a.length) == a:
                return True
            else:
                return False

        @String.prototype.endswith
        def func(a):
            if this.substring(this.length-a.length, this.length) == a:
                return True
            else:
                return False

        @String.prototype.join
        def func(a):
            out = ''
            if instanceof(a, Array):
                arr = a
            else:
                arr = a.__dict__.js_object
            i = 0
            for value in arr:
                out += value
                i += 1
                if i < arr.length:
                    out += this
            return out

        @String.prototype.upper
        def func():
            return this.toUpperCase()

        @String.prototype.lower
        def func():
            return this.toLowerCase()

        @String.prototype.index
        def func(a):
            return this.indexOf(a)

        @String.prototype.isdigit
        def func():
            digits = _create_empty_object( ['0','1','2','3','4','5','6','7','8','9'] )
            for char in this:
                if char in digits: pass
                else: return False
            return True


No comments:

Post a Comment