There are two types of identifiers: a qualified identifier and an unqualified identifier. An unqualified identifier is one that does not indicate where it comes from.
Normally, an unqualified identifier is resolved by searching the scope chain for a variable with that name, while a qualified identifier is resolved by searching the prototype chain of an object for a property with that name.
const foo = { bar: 1 };
console.log(foo.bar);
One exception to this is the global object, which sits on top of the scope chain, and whose properties automatically become global variables that can be referred to without qualifiers.
console.log(globalThis.Math === Math);
The with
statement adds the given object to the head of this scope chain during the evaluation of its statement body. Every unqualified name would first be searched within the object (through a in
check) before searching in the upper scope chain.
Note that if the unqualified reference refers to a method of the object, the method is called with the object as its this
value.
with ([1, 2, 3]) {
console.log(toString());
}
The object may have an @@unscopables
property, which defines a list of properties that should not be added to the scope chain (for backward compatibility). See the Symbol.unscopables
documentation for more information.
The reasons to use a with
statement include saving one temporary variable and reducing file size by avoiding repeating a lengthy object reference. However, there are far more reasons why with
statements are not desirable:
- Performance: The
with
statement forces the specified object to be searched first for all name lookups. Therefore, all identifiers that aren't members of the specified object will be found more slowly in a with
block. Moreover, the optimizer cannot make any assumptions about what each unqualified identifier refers to, so it must repeat the same property lookup every time the identifier is used. - Readability: The
with
statement makes it hard for a human reader or JavaScript compiler to decide whether an unqualified name will be found along the scope chain, and if so, in which object. For example:
function f(x, o) {
with (o) {
console.log(x);
}
}
If you look just at the definition of f
, it's impossible to tell what the x
in the with
body refers to. Only when f
is called can x
be determined to be o.x
or f
's first formal parameter. If you forget to define x
in the object you pass as the second parameter, you won't get an error — instead you'll just get unexpected results. It's also unclear what the actual intent of such code would be. - Forward compatibility: Code using
with
may not be forward compatible, especially when used with something other than a plain object, which may gain more properties in the future. Consider this example:
function f(foo, values) {
with (foo) {
console.log(values);
}
}
If you call f([1, 2, 3], obj)
in an ECMAScript 5 environment, the values
reference inside the with
statement will resolve to obj
. However, ECMAScript 2015 introduces a values
property on Array.prototype
(so it will be available on every array). So, after upgrading the environment, the values
reference inside the with
statement resolves to [1, 2, 3].values
instead, and is likely to cause bugs. In this particular example, values
is defined as unscopable through Array.prototype[@@unscopables]
, so it still correctly resolves to the values
parameter. If it were not defined as unscopable, one can see how this would be a difficult issue to debug.