Nashorn's JSObject in context

Posted on Sun 12 November 2017 in Coding

The JSObject interface in Nashorn (the JavaScript engine in Java 8 and later) makes it possible to expose an object to script code and control in detail how it appears and behaves. Nashorn offers plenty of options for Java interop, so JSObject is certainly not the only way. However, unlike other ways it allows for a high degree of flexibility and dynamic behavior. For example, you could expose a Directory “class” and then allow script code to access files as properties:

var d = new Directory('.');
print(d['myfile.txt']);

The JSObject API documentation states that:

“Nashorn will treat objects of such classes just like nashorn script objects.”

This is not entirely true. Many Object methods are not implemented for JSObject and will behave erratically. For example, assuming X is a JSObject object, Object.prototype.hasOwnProperty.call(X, prop) always returns false, whereas Object(X) throws an exception.

There is another problem with the documentation though. As is common with API documentation, it correctly explains what each method is for, but fails to put it in context or give examples.

The purpose of this article is to explain when the different JSObject methods are called during script execution and in some cases how they should be implemented.

🛈 Specific behavior explained here has been tested with Java 8u151 and Java 9.0.1.

🛈 In the examples, I typically use “X” to refer to an instance of a class derived from JSObject.

AbstractJSObject

Nashorn provides an abstract base class, AbstractJSObject with default implementations for all JSObject methods. It is recommended to extend this class and only override methods as needed. In addition, AbstractJSObject.getDefaultValue can be overridden to provide a custom default value for the object. Read on for details.

🛈 Java 9 moves the implementation of getDefaultValue from AbstractJSObject to a default method on the JSObject interface.

Property operations

There are six methods that deal with object properties. Most of them use terminology based on member, but the ECMAScript spec doesn’t use the word member, only property and method (referring to a property with a function value). Therefore, I will use property throughout this article.

hasMember

This method is called when the in operator is used. For example, the following script expression will result in a call to hasMember with argument “foo”:

'foo' in X

Note that Nashorn will not call hasMember before arbitrary property access. In other words, you cannot assume that getMember is only called for properties for which hasMember is positive.

getMember

This method is called to retrieve the value of a named property. For example, any of the following script expressions will result in a call to getMember with argument “foo”:

X.foo
X['foo']
X.foo()

Note the last one—Nashorn will first retrieve the value of the “foo” property and then call it. If the value isn’t a function, the call will (obviously) fail.

To implement method support, getMember can return a regular Java function object (i.e., an object that implements a public interface annotated with FunctionalInterface. For example:

public Object getMember(String name) {
    if ("foo".equals(name)) {
        return (Supplier<String>) () -> "bar";
    }
    return null;
}

It is also possible to return a JSObject function. This is required for the default value algorithm, as we will see later. How to create a JSObject function is also covered later.

⚠ Java 9 adds support for ES6 symbols. As of this writing, JSObject does not support property access based on symbols—no call to getMember will be made and the result will be null.

setMember

This method is called when the value of a property is set. For example, any of the following script statements will result in a call to setMember with arguments “foo” and “bar”:

X.foo = 'bar';
X['foo'] = 'bar';

⚠ As with getMember, symbol-based access is not supported. Setting a property based on a symbol is simply ignored.

removeMember

This method is called when the delete operator is used. For example, any of the following script statements will result in a call to removeMember with argument “foo”:

delete X.foo;
delete X['foo'];

Furthermore, removeMember is also called for array element deletion (there is no removeSlot method):

delete X[1];

keySet

This method is used when the properties of an object are enumerated. The following script code will result in a call to keySet:

for (var k in X) {
    // ...
}

With JSObject, there is no notion of own versus prototype properties, so the customary call to Object.prototype.hasOwnProperty should be omitted (and in fact, must be omitted since, as noted, it would always return false).

The returned set should only include enumerable properties; both hasMember and getMember can be implemented for a member that is not in the key set.

🛈 To be consistent with a native array, a JSObject array should return indices but not the “length” property as keys. However, it’s possible to implement an “infinite” array (at least up to length 232), e.g. where the value of the Nth index is the Nth prime, in which case returning all keys is of course infeasible.

⚠ You’d think Object.keys would use keySet, but it doesn’t. Object.keys doesn’t have JSObject support and will throw an exception if passed such an object.

values

This method is only used to support the Nashorn for each extension:

for each (var v in X) {
    print(v);
}

My recommendation is to return values only for keys returned from the keySet method, since it makes a lot of sense to be consistent.

⚠ The values method is unfortunately not used to implement for...of. in ES6. for...of is specified to use an iterator retrieved via the @@iterator symbol, and as noted JSObject doesn’t support symbol properties.

Object character

For the lack of a better method category… there are two methods that can be said to control the character of an object: getClassName and getDefaultValue.

getClassName

This method is called by Object.prototype.toString. If you were to return “MyClass” from getClassName, the following script statement would print “[object MyClass]” (without the quotes):

print(Object.prototype.toString.call(X));

🛈 Note that to make it possible to call X.toString(), you have to return a function from the resulting call to getMember; there are no built-in methods that are callable on a JSObject object.

AbstractJSObject by default returns the name of the current class. To emulate a native object, return “Object” instead.

getDefaultValue

This method is called to obtain the default value for an object. AbstractJSObject delegates to an implementation that follows the algorithm outlined in the previous link, so there is no need to provide your own implementation. In case you still want to, here is how it works.

getDefaultValue is called with a hint, which is (as of this writing) one of the following values:

  • Number.class
  • String.class
  • null (equivalent to Number.class here)

null is used as hint to get the default value in cases such as:

'' + X
42 + X
'' == X

Number.class is used as hint when a numeric operator other than + is used, when non-strict equality is tested with a number or a boolean and in cases where a number is needed, for example:

42 * X
42 == X
true == X
Math.floor(X)

https://www.ecma-international.org/ecma-262/5.1/#sec-11.6.1

Finally, String.class is used as hint in cases such as:

objectOrArray[X]
parseInt(X, 10)

Nashorn will in its implementation of the default value algorithm make calls to getMember to retrieve one of the “toString” and “valueOf” methods. If you implement support for either of these, it has to be a JSObject function, not a regular Java function! If you see an error with message “cannot.get.default.number” or “cannot.get.default.string”, Nashorn failed to obtain a default value.

⚠ For some reason, getDefaultValue isn’t called in the expression X[X], i.e. when a JSObject object is used as property accessor for another JSObject object. The result is always null.

⚠ There is unfortunately no way to control the default value in a pure boolean context (e.g. if(X)). This would have been very useful, e.g. to implement a null object.

🛈 To learn more about the behavior in the equality cases, read about abstract equality. To learn about the behavior for the binary addition cases, read about the addition operator.

Array operations

The following methods are used for array operations: isArray, getSlot, setSlot and hasSlot.

🛈 For an array, you may want to return “Array” from getClassName, to be compatible with libraries that use Object.prototype.toString to test for an array. You should also implement support for the “length” property in hasMember and getMember.

isArray

This method determines the result of Array.isArray(X). It has no bearing on the result of the typeof operator; typeof X for an object where isArray returns true is “object” (without the quotes).

getSlot

This method returns the value of the Nth array element. The following script expression results in a call to getSlot with argument 0:

X[0]

🛈 getSlot will be called also for a negative index. However, if the index is a negative non-integer, such as -1.0d, getMember will be called instead.

setSlot

This method is called to set the value of the Nth array element. The following script statement results in a call to setSlot with arguments 0 and “testing”:

X[0] = 'testing';

The behavior for a negative index is the same as for getSlot.

hasSlot

This method is used to create function arguments in a call to Function.prototype.apply. Suppose we have a regular JavaScript function f, and X is our JSObject array. Next, suppose the following script statement is executed:

f.apply(null, X);

To create arguments for f, Nashorn does the following:

  1. Calls hasMember with argument “length” to determine if the array length is available.
  2. If the length is available, calls getMember to retrieve it, otherwise assumes length 0.
  3. For index 0 up to but not including the array length, calls hasSlot to determine if the array has an element for that index (remember, an array may be sparse).
  4. If there is an element for a particular index, calls getSlot to retrieve the value of the element at that index.

Function operations

The following methods are used for a JSObject function: isFunction, isStrictFunction and call. Technically there are more, since a function in JavaScript also can be seen as a class (type), but class-related methods are covered separately.

🛈 For a function, you may want to return “Function” from getClassName, to be compatible with libraries that use Object.prototype.toString to test for a function.

isFunction

This method has two purposes. First and foremost, Nashorn sometimes uses it to determine if a JSObject instance is callable, i.e. if it represents a function. It is not always called prior to a function invocation, so the following statement will not result in a call to isFunction:

X();

However, in the default value algorithm, Nashorn calls isFunction on a JSObject object respresenting one of the “valueOf” and “toString” methods to determine if it is callable. Creating a bound function also calls isFunction to determine if binding is possible:

var boundFun = Function.prototype.bind.call(X, null, 1);

Another case is to test if a callback function is indeed callable:

var mapped = [1, 2, 3].map(X);

The second purpose is for the typeof operator. The result of calling typeof X when isFunction returns true is “function” (without the quotes).

call

This method is called to handle the actual function call. The first argument is the “this” object and subsequent arguments are arguments passed to the function. Nashorn marshals arguments as follows:

JavaScript Java
undefined jdk.nashorn.internal.runtime.Undefined (singleton)
null null
string java.lang.String
primitive value boxed primitive
script object/array/function wrapped in an instance of jdk.nashorn.api.scripting.ScriptObjectMirror *
JSObject instance or other Java object the instance itself (no wrapping occurs)

*) It is beyond the scope of this article to go through the ScriptObjectMirror API.

isStrictFunction

This method is called to decide what the “this” argument passed to call should be when the function is not called as an object method or otherwise bound to a particular instance).

For a strict function, the “this” argument is undefined. For a non-strict function, it is the global object. Examples of cases where strictness plays a role are when a non-bound function is called by itself and when a function is used as a callback:

X(1, 2)
[1, 2, 3].map(X)

⚠ Nashorn in Java 8 behaves differently in the two cases above. In the first one, it does not call isStrictFunction and always passes undefined for “this”. In other words, it always behaves as if the function is strict.

Class operations

In JavaScript, a function is similar to a class, in that you can “instantiate” a function using the new keyword. JSObject supports the following class operations: newObject, isInstance and isInstanceOf.

newObject

This method is called when the new operator is used. For example, the following script expression calls newObject with arguments “foo” and “bar”:

new X('foo', 'bar')

Arguments are marshalled in the same way as for call.

The return value can be anything, but it’d make sense to return something that can be considered to be an instance of the “class”.

isInstance

This method is used to implement the instanceof operator when a JSObject object is on the right-hand side. For example, given two JSObject objects X and Y, the following expression would call isInstance of X with Y as argument:

Y instanceof X

isInstanceOf

This method is used for reverse instanceof. For example, the following expression would call isInstanceOf with argument “foo”:

X instanceof 'foo'

However, not all uses of instanceof with the JSObject object on the left-hand side results in a call to isInstanceOf. The following does not, for example:

X instanceof Date

JSON

To create a JSON representation of a JSObject object when JSON.stringify(X) is called, Nashorn first calls getMember with the argument “toJSON”. If the result is a function (not necessarily a JSObject function), it is called to obtain the value to use to create a JSON representation. The returned value should not be JSON in itself, but rather the value to serialize to JSON.

If there is no “toJSON” method, the following steps are taken for a non-array:

  1. Call keySet to obtain all keys.
  2. For each key, call getMember with the key as argument to obtain the value to serialize as value for that key.

For an array, the following steps are taken:

  1. Call getMember with argument “length” to obtain the array length.
  2. For each index in the array, call getSlot to obtain the value to serialize as value for that index.

If the “toJSON” method returns a JSObject instance, the above steps are taken to serialize that instance.

Other

There are two methods that can/should be ignored: toNumber and eval.

toNumber

This method is deprecated and should not be implemented/overridden. getDefaultValue is called with hint Number.class instead.

eval

This method is supposedly called to evaluate JavaScript code, but as far as I can see it is never called on a JSObject object.

Miscellaneous

This article is drawing to its end. Before we part ways, there are two pieces of information I think are relevant in relation to what has been discussed. The first is how to return undefined from a JSObject method, the second is how to call non-JSObject methods on a JSObject object.

undefined

jdk.nashorn.internal.runtime.Undefined belongs to an internal package and should therefore not be referenced. In fact, in Java 9 the package isn’t exposed at all. If you want to return undefined from a call to getMember, for example, you can obtain the singleton instance of the Undefined class using the following Java function:

public static Object getUndefined(ScriptEngine engine) throws ScriptException {
    ScriptObjectMirror arrayMirror = (ScriptObjectMirror) engine.eval("[undefined]");
    return arrayMirror.getSlot(0);
}

Why wrap undefined inside an array? Because engine.eval("undefined") returns null.

Calling a non-JSObject method

It is possible to call a regular Java method on a JSObject object from a script. Suppose our famous object X is an instance of this class:

class MyObj extends AbstractJSObject {
    public int multiply(int a, int b) {
        return a * b;
    }
}

To call the multiply method from JavaScript, we can write:

var multiply = X['multiply(int,int)'];
print(multiply(5, 6));

In this case, Nashorn will verify that the passed arguments match the parameter types. Type names in a method signature should be simple names for primitive types (e.g. int as above), or the fully qualified class name (e.g. java.lang.String).

Final remarks

Exposing script objects based on JSObject is very useful and allows for great flexibility and control over script behavior. In this article, I have covered the different JSObject methods and shown how they are called during script execution. Please let me know if anything is unclear, if you have found errors or omissions, or simply if the article has been useful to you!