ref
) declarations and expressions for ECMAScriptThis proposal defines new syntax to allow for the declaration and creation of user-defined references to bindings.
This proposal introduces three main concepts:
let r = ref x
)let ref y = r
, or function f(ref y) { }
)Reference
objectsA Reference expression is a prefix unary expression that creates a Reference
object that defines a binding to its operand.
Reference expressions have the following semantics:
Reference
object is created for the actual property access.Reference
object is created for the actual property access.Reference
object is created for the binding.This behavior can be illustrated by the following syntactic conversion:
const x = ref y;
is roughly identical in its behavior to:
const x = Object.freeze({
__proto__: Reference.prototype,
get value() { return y; },
set value(_) { y = _; }
});
A Reference declaration is the declaration of a parameter or variable that dereferences a Reference
, creating a binding
in the current scope.
Reference declarations have the following semantics:
Reference
object if they can statically determine that the only use-sites are arguments to call expressions whose parameters
are declared ref
.ref x
parameter introduces a mutable binding to the underlying Reference
.
x
reads the value of the underlying Reference
. x
assigns to the value of the underlying Reference
.let ref x
declaration introduces a mutable binding to the Reference
supplied as the initializer.
x
reads the value of the underlying Reference
. x
assigns to the value of the underlying Reference
.const ref x
declaration introduces an immutable binding.
x
reads the value of the underlying Reference
. x
is an error.ref
of x
will result in an immutable Reference
.The behavior of a reference declaration can be illustrated by the following syntactic conversion:
function f(ref y) {
y = 1;
}
let ref x1 = someRef;
x1 = 1;
const ref x2 = someRef;
console.log(x2);
is roughly identical in its behavior to:
function f(ref_y) {
ref_y.value = 1;
}
let ref_x1 = someRef;
ref_x1.value = 1;
const ref_x2 = ((someRef) => Object.freeze({
__proto__: Reference.prototype,
get value() { return someRef.value; }
}))(someRef);
console.log(ref_x2.value);
Reference
objectsA Reference
object is a reified reference that contains a value
property that can be used to read from and write to a reference.
Reference objects have the following shape:
interface Reference<T> {
value: T;
[Symbol.toStringTag]: "Reference";
}
Take a reference to a variable:
let x = 1;
const r = ref x;
print(r.value); // 1
r.value = 2;
print(x); // 2;
Take a reference to a property:
let o = { x: 1 };
const r = ref o.x;
print(r.value); // 1
r.value = 2;
print(o); // { x: 2 }
Take a reference to an element:
let ar = [1];
const r = ref ar[0];
print(r.value); // 1
r.value = 2;
print(ar); // [2]
Object Binding Patterns:
let o = { x: 1 };
const { ref x } = o;
// or:
// const { x: ref x } = 0;
print(x.value); // 1
x.value = 2;
print(o); // { x: 2 }
Array Binding Patterns:
// NOTE: If an Array Binding Pattern has a `ref` declaration, array indexing is used
// instead of Symbol.iterator and rest elements are not allowed.
let ar = [1];
const [ref r] = ar;
print(r.value); // 1
r.value = 2;
print(ar); // [2]
Dereferencing:
// dereference a binding
let x = 1;
let ref y = ref x; // 'y' effectively points to 'x'
print(y); // 1
y = 2;
print(x); // 2
Dereferencing a non-Reference (other than undefined
) is a ReferenceError:
let x = 1;
let ref y = x; // TypeError: Value is not a Reference.
Dereferencing undefined
is ok, but accessing its value is a ReferenceError (typeof
can still be used to test the reference):
function f(ref y) {
typeof y; // ok, type is 'undefined'
y; // ReferenceError: y is not defined.
}
f(undefined); // ok
let x;
function g(ref y = ref x) {}
g(); // ok, parameter initialization will check whether the *argument* is undefined, not the binding.
Dereferencing an immutable Reference into a mutable Reference does not make it mutable:
let x = 1;
const ref y = ref x; // ok, `x` is mutable
let ref z = ref y; // ok, but `z` is actually immutable
z = 2; // error
Reference passing:
function update(ref r) {
r = 2;
}
let x = 1;
update(ref x);
print(x); // 2
Referencing a local declaration creates a closure:
function f() {
let x = 1;
return [ref x, () => print(x)];
}
const [r, p] = f();
print(r.value); // 1
r.value = 2;
p(); // 2
Combining reference expressions, reference parameters, and reference variables:
function max(ref first, ref second, ref third) {
const ref max = first > second ? ref first : ref second;
return max > third ? ref max : ref third;
}
let x = 1, y = 2, z = 3;
let ref w = max(ref x, ref y, ref z);
w = 4;
print(x); // 1
print(y); // 2
print(z); // 4
Forward reference to a block-scoped variable and TDZ:
let ref a_ = ref a; // ok, no error from TDZ
let a = 1;
let ref b_ = ref b;
b_ = 1; // error due to TDZ
let b;
Forward reference to member of block-scoped variable:
let ref b_ = ref b.x; // error, TDZ for `b`
let b = { x: 1 };
Forward reference to var
:
let ref d_ = ref d; // ok, no TDZ
d_ = 2; // ok
var d = 1;
Forward references for decorators:
class Node {
@Type(ref Container) // ok, no error due to TDZ
get parent() { /*...*/ }
@Type(ref Node)
get nextSibling() { /*...*/ }
}
class Container extends Node {
@Type(ref Node)
get firstChild() { /*...*/ }
}
Side effects:
let count = 0;
let e = [0, 1, 2];
let ref e_ = ref e[count++]; // `count++` evaluated when reference is taken.
print(e_); // 0
print(e_); // 0
print(count); // 1
UpdateExpression[Yield, Await]:
`ref` LeftHandSideExpression[?Yield, ?Await]
RefBinding[Yield, Await]:
`ref` BindingIdentifier[?Yield, ?Await]
LexicalBinding[In, Yield, Await]:
RefBinding[?Yield, ?Await] Initializer[?In, ?Yield, ?Await]?
VariableDeclaration[In, Yield, Await]:
RefBinding[?Yield, ?Await] Initializer[?In, ?Yield, ?Await]?
ForBinding[Yield, Await]:
RefBinding[?Yield, ?Await]
SingleNameBinding[Yield, ?Await]:
RefBinding[?Yield, ?Await] Initializer[+In, ?Yield, ?Await]?
The following is an approximate desugaring for this proposal:
// proposed syntax
function max(ref first, ref second, ref third) {
const ref max = first > second ? ref first : ref second;
return max > third ? ref max : ref third;
}
let x = 1, y = 2, z = 3;
let ref w = max(ref x, ref y, ref z);
w = 4;
print(x); // 1
print(y); // 2
print(z); // 4
// desugaring
const __ref = (get, set) => Object.freeze(Object.create(null, { value: { get, set } }));
function max(ref_first, ref_second, ref_third) {
const ref_max = ref_first.value > ref_second.value ? ref_first : ref_second;
return ref_max.value > ref_third.value ? ref_max : ref_third;
}
let x = 1, y = 2, z = 3;
const ref_x = __ref(() => x, _ => x = _);
const ref_y = __ref(() => y, _ => y = _);
const ref_z = __ref(() => z, _ => z = _);
const ref_w = max(ref_x, ref_y, ref_z);
ref_w.value = 4;
print(x); // 1
print(y); // 2
print(z); // 4
And here's the same example using an array:
// proposed syntax
function max(ref first, ref second, ref third) {
const ref max = first > second ? ref first : ref second;
return max > third ? ref max : ref third;
}
// arrays
let ar = [1, 2, 3];
let ref w = max(ref ar[0], ref ar[1], ref ar[2]);
w = 4;
print(ar[0]); // 1;
print(ar[1]); // 2;
print(ar[2]); // 4;
// desugaring
const __ref = (get, set) => Object.freeze(Object.create(null, { value: { get, set } }));
const __elemRef = (o, p) => __ref(() => o[p], _ => o[p] = _);
function max(ref_first, ref_second, ref_third) {
const ref_max = ref_first.value > ref_second.value ? ref_first : ref_second;
return ref_max.value > ref_third.value ? ref_max : ref_third;
}
let ar = [1, 2, 3];
const ref_ar0 = __elemRef(ar, 0);
const ref_ar1 = __elemRef(ar, 1);
const ref_ar2 = __elemRef(ar, 2);
const ref_w = max(ref_x, ref_y, ref_z);
ref_w.value = 4;
print(ar[0]); // 1;
print(ar[1]); // 2;
print(ar[2]); // 4;
Here's an example using private names:
// proposed syntax
class C {
#counter = 0;
get count() { return this.#counter; }
provideCounter(cb) {
cb(ref this.#counter);
}
}
function increment(ref counter) {
counter++;
}
const c = new C();
c.provideCounter(increment);
c.provideCounter(increment);
print(c.count); // 2
// desugared
const __ref = (get, set) => Object.freeze(Object.create(null, { value: { get, set } }));
class C {
#counter = 0;
get count() { return this.#counter; }
provideCounter(cb) {
cb(__ref(() => this.#counter, _ => this.#counter = _));
}
}
function increment(ref_counter) {
ref_counter.value++;
}
const c = new C();
c.provideCounter(increment);
c.provideCounter(increment);
print(c.count); // 2
We may want to make it possible to revoke a reference, for example:
let a = 1;
let { ref reference: b, revoke } = Reference.revocable(ref a);
b = 2;
console.log(a); // 2
revoke();
b = 3; // ReferenceError
However, it may be possible to do this in userland (though engines may not be able to optimize away a userland type):
function revocableReference(ref_value) {
const reference = Object.create(Reference.prototype, {
value: {
get() {
if (ref_value === null) throw new ReferenceError();
return ref_value.value;
},
set(v) {
if (ref_value === null) throw new ReferenceError();
ref_value.value = v;
}
}
});
function revoke() {
ref_value = null;
}
return { reference, revoke };
}
let a = 1;
let { ref reference: b, revoke } = revocableReference(ref a);
b = 2;
console.log(a); // 2
revoke();
b = 3; // ReferenceError