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.Duplexclass prototypically inherits fromstream.Readableand parasitically fromstream.Writable, butinstanceofwill work properly for both base classes due to overridingSymbol.hasInstanceonstream.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