February 1999 Draft
JavaScript 2.0
Declarations
|
Thursday, February 18, 1999
JavaScript 2.0 follows the streaming execution model -- the entire program is processed in one pass. Unlike in JavaScript 1.x, declarations take effect only when they are executed. A declaration that is never executed is ignored.
The streaming execution model considerably simplifies the language and allows a JavaScript 2.0 interpreter to treat programs read from a file identically to programs typed in via an interactive console. Also, a JavaScript 2.0 interpreter or just-in-time compiler may start to execute a script even before it has finished downloading all of it. This was not possible in JavaScript 1.x because the interpreter was required to scan the entire program for declarations in one pass before executing any of the code in the following pass. Streaming also simplifies the execution model for web pages that contain multiple JavaScript scripts and permits one to safely coalesce adjacent SCRIPT tags on a page or divide a single SCRIPT tag into several.
One of the most significant advantages of streaming is that it allows JavaScript 2.0 scripts to turn parts of themselves on and off based on dynamically obtained information. For example, a script or library could define additional functions and classes if it runs on an environment that supports CSS unit arithmetic while still working on environments that do not.
The streaming execution model requires identifiers naming functions and variables to be defined before they are used. A
use occurs when an identifier is read, written, or called, at which point that identifier is resolved to a variable or a function
according to the scoping rules. A reference from within a control statement such as if
and while
is resolved only when execution reaches the reference. References from within the body of a function are resolved only after
the function is called and execution reaches those references.
According to these rules, the following program is correct and would print 7
:
function f(int a) int { return a+b; } var int b = 4; print(f(3));
Assuming that variable b
is predefined by the host if featurePresent
is true, this program would
also work:
function f(int a) int { return a+b; } if (!featurePresent) { package var int b = 4; } print(f(3));
On the other hand, the following program would produce an error because a
is referenced before it is defined:
print(f(3)); function f(int a) int { return a+b; }
Defining mutually recursive functions is not a problem as long as one defines all of them before calling them.
Should we define a more eager identifier resolving mechanism for some functions (for example all functions
that are not declared volatile
and do not contain a call to eval
)? This would let the compiler resolve
all of the identifiers in a function at the first time the function is called.
Several different kinds of declarations that can be present in JavaScript 2.0 programs:
var
[TypeExpression] Identifier [=
AssignmentExpression] ,
... ,
[TypeExpression] Identifier [=
AssignmentExpression] ;
const
[TypeExpression] Identifier =
AssignmentExpression ,
... ,
[TypeExpression] Identifier =
AssignmentExpression ;
getter
| setter
] function
Identifier (
Parameters )
[TypeExpression] Blocktraditional
function
Identifier (
Identifier ,
... ,
Identifier )
Blockfield
[TypeExpression] Identifier [=
AssignmentExpression] ,
... ,
[TypeExpression] Identifier [=
AssignmentExpression] ;
getter
| setter
] [final
] [override
] method
Identifier (
Parameters )
[TypeExpression] Blockgetter
| setter
] [final
] [override
] method
Identifier (
Parameters )
[TypeExpression] ;
constructor
Identifier (
Parameters )
Blockclass
Identifier [extends
TypeExpression] Blockclass
extends
TypeExpression Blockversion
Version [>
VersionList] ;
All of these declarations share several common scoping rules:
getter
and a setter
may be defined with the same name and that versions have a namespace separate from other declarations).Rules 3 and 4 state that once an identifier is resolved to a variable or function in a scope, that resolution cannot be changed. This permits efficient compilation and avoids confusion with programs such as:
const int b = 7; function f() int { function g() int {return b} var a = g(); const int b = 8; return g() - a; }
Most lexical scopes are established by Block productions in the grammar. Lexical scopes nest, and a declaration in an inner scope can shadow declarations in outer ones.
Do we want to collapse all block scopes into one inside functions? On one hand this complicates the language conceptually and surprises Java and C++ programmers. On the other hand, this would match JavaScript 1.x better and simplify closure creation when a closure is created nested inside several blocks in a function.
A declaration with a Visibility prefix does not apply to the current Block. Instead, it applies either to the current package (if outside a ClassDefinition's Block) or to the current class (if inside a ClassDefinition's Block). In addition to making the declaration global in this way, Visibility also specifies the declaration's visibility from other packages or classes. Visibility can take one of the following forms:
Visibility | Access allowed from |
---|---|
private |
only within current class |
package |
only within current package |
public VersionsAndRenames |
within any package that imports this package |
Declarations at the top level of a Program or at the top level of a ClassDefinition's
Block may omit Visibility,
in which case they are treated as if they had package
visibility. When used outside a
ClassDefinition's Block,
private
is equivalent to package
.
In the example below the comments indicate the scope and visibility of each declaration:
var a0; //
Package-visible global variable
private var a1 = true; //
Package-visible global variable
package var a2; //
Package-visible global variable
public var a3; //
Public global variable
if (a1) {
var b0; //
Local to this block
private var b1; //
Package-visible global variable
package var b2; //
Package-visible global variable
public var b3; //
Public global variable
}
public function F() { //
Public global function
var c0; //
Local to this function
private var c1; //
Package-visible global variable
package var c2; //
Package-visible global variable
public var c3; //
Public global variable
}
function G() { //
Package-visible global function
var d0; //
Never defined because G
isn't called
private var d1; //
Never defined because G
isn't called
package var d2; //
Never defined because G
isn't called
public var d3; //
Never defined because G
isn't called
}
class C { //
Package-visible global class
var e0; //
Package-visible class variable
private var e1; //
Class-visible class variable
package var e2; //
Package-visible class variable
public var e3; //
Public class variable
field e4; //
Package-visible instance variable
private field e5; //
Class-visible instance variable
package field e6; //
Package-visible instance variable
public field e7; //
Public instance variable
function H() { //
Package-visible class function
var f0; //
Local to this function
private var f1; //
Class-visible class variable
package var f2; //
Package-visible class variable
public var f3; //
Public class variable
private field f4; //
Class-visible instance variable
package field f5; //
Package-visible instance variable
public field f6; //
Public instance variable
}
public method I() {} //
Public class method
H();
}
F();
A public
declaration's identifier is exported to other packages. To help avoid accidental collisions between
identifiers declared in different packages, identifiers can be selectively exported depending on the version requested by
an importing package. An identifier declaration with a version number newer than that requested by the importer will not be
seen by that importer. The versioning facilities also include additional facilities that allow
robust removal and renaming of identifiers.
VersionsAndRenames describes the set of versions in which an identifier is exported, together with a possible alias for the identifier:
<
VersionRange [:
Identifier] ,
... ,
VersionRange [:
Identifier] >
]..
[Version]Suppose a client package C imports version V of package P that exports identifier N with some VersionsAndRenames. If the VersionsAndRenames's VersionRange includes version V, then package C can use the corresponding Identifier alias to access package P's N. If the Identifier alias is omitted, then package C can use N to access package P's N. Multiple VersionRanges operate independently.
In most cases VersionsAndRenames is just a version number between <
and >
:
public<1.2> const z = 3;
If VersionsAndRenames is omitted, version 1.0
is assumed.
Waldemar Horwat Last modified Thursday, February 18, 1999 |