In this snippet, the statement f instanceof PipeWritable
returns true (Node v8.4.0):
const stream = require('stream');
const fs = require('fs');
class PipeWritable extends stream.Writable {
constructor () {
super();
}
}
const s = new PipeWritable();
const f = fs.createWriteStream('/tmp/test');
console.log(f instanceof PipeWritable); // true ... ???
Object s
:
Object.getPrototypeOf(s)
is PipeWritable {}
s.constructor
is [Function: PipeWritable]
PipeWritable.prototype
is PipeWritable {}
Object f
:
Object.getPrototypeOf(f)
is WriteStream { ... }
f.constructor
is [Function: WriteStream] ...
stream.WriteStream.prototype
is Writable { ... }
Prototype chains:
Object f Object s
--------------------- --------------------
Writable PipeWritable
Stream Writable
EventEmitter Stream
Object EventEmitter
Object
Following the definition of instanceof:
The instanceof operator tests whether an object in its prototype chain has the prototype property of a constructor.
I would expect that (f instanceof PipeWritable) === false
, because PipeWritable
is not in the prototype chain of f
(the chain above is verified by calls of Object.getPrototypeOf(...)
).
But it returns true
, therefore something is wrong in my analysis.
What's the correct answer?
This is due to a certain part of code in the Node.js source, in _stream_writable.js
:
var realHasInstance;
if (typeof Symbol === 'function' && Symbol.hasInstance) {
realHasInstance = Function.prototype[Symbol.hasInstance];
Object.defineProperty(Writable, Symbol.hasInstance, {
value: function(object) {
if (realHasInstance.call(this, object))
return true;
return object && object._writableState instanceof WritableState;
}
});
} else {
realHasInstance = function(object) {
return object instanceof this;
};
}
By language specification, the instanceof
operator uses the well-known symbol @@hasInstance
to check if an object O is an instance of constructor C:
12.9.4 Runtime Semantics: InstanceofOperator(O, C)
The abstract operation InstanceofOperator(O, C) implements the generic algorithm for determining if an object O inherits from the inheritance path defined by constructor C. This abstract operation performs the following steps:
- If Type(C) is not Object, throw a TypeError exception.
- Let instOfHandler be GetMethod(C,@@hasInstance).
- ReturnIfAbrupt(instOfHandler).
- If instOfHandler is not undefined, then
a. Return ToBoolean(Call(instOfHandler, C, «O»)).- If IsCallable(C) is false, throw a TypeError exception.
- Return OrdinaryHasInstance(C, O).
Now let me break down the code above for you, section by section:
var realHasInstance;
if (typeof Symbol === 'function' && Symbol.hasInstance) {
…
} else {
…
}
The above snippet defines realHasInstance
, checks if Symbol
is defined and if the well-known symbol hasInstance
exists. In your case, it does, so we'll ignore the else
branch. Next:
realHasInstance = Function.prototype[Symbol.hasInstance];
Here, realHasInstance
is assigned to Function.prototype[@@hasInstance]
:
19.2.3.6 Function.prototype[@@hasInstance] ( V )
When the @@hasInstance method of an object F is called with value V, the following steps are taken:
- Let F be the this value.
- Return OrdinaryHasInstance(F, V).
The @@hasInstance
method of Function
just calls OrdinaryHasInstance. Next:
Object.defineProperty(Writable, Symbol.hasInstance, {
value: function(object) {
if (realHasInstance.call(this, object))
return true;
return object && object._writableState instanceof WritableState;
}
});
This defines a new property on the Writable
constructor, the well-known symbol hasInstance
-- essentially implementing its own custom version of hasInstance
. The value of hasInstance
is a function that takes one argument, the object that is being tested by instanceof
, in this case f
.
The next line, the if statement, checks if realHasInstance.call(this, object)
is truthy. Mentioned earlier, realHasInstance
is assigned to Function.prototype[@@hasInstance]
which is actually calling the internal operation OrdinaryHasInstance(C, O). The operation OrdinaryHasInstance just checks if O is an instance of C as you and MDN described, by looking for the constructor in the prototype chain.
In this case, a Writable f
is not an instance of a subclass of Writable (PipeWritable
) thus realHasInstance.call(this, object)
is false. Since that is false, it goes to the next line:
return object && object._writableState instanceof WritableState;
Since object
, or f
in this case, is truthy, and since f
is a Writable with a _writableState
property that is an instance of WritableState
, f instanceof PipeWritable
is true.
The reason for this implementation is in the comments:
// Test _writableState for inheritance to account for Duplex streams,
// whose prototype chain only points to Readable.
Because Duplex streams are technically Writables, but their prototype chains only point to Readable, an extra check to see if _writableState
is an instance of WritableState
allows duplexInstance instanceof Writable
to be true. This has a side effect that you discovered -- a Writable being 'an instance of a child class'. This is a bug and should be reported.
This is actually even reported in the documentation:
Note: The
stream.Duplex
class prototypically inherits fromstream.Readable
and parasitically fromstream.Writable
, butinstanceof
will work properly for both base classes due to overridingSymbol.hasInstance
onstream.Writable
.
There are consequences to inheriting parasitcally from Writable as shown here.
I submitted an issue on GitHub and it looks like it'll be fixed. As Bergi mentioned, adding a check to see if this === Writable
, making sure only Duplex streams were instances of Writable when using instanceof
. There's a pull request.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With