Подтвердить что ты не робот

Почему instanceof оценивает значение true здесь?

В этом фрагменте оператор f instanceof PipeWritable возвращает 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 ... ???

Объект s:

  • Object.getPrototypeOf(s) PipeWritable {}
  • s.constructor [Function: PipeWritable]
  • PipeWritable.prototype PipeWritable {}

Объект f:

  • Object.getPrototypeOf(f) WriteStream { ... }
  • f.constructor [Function: WriteStream] ...
  • stream.WriteStream.prototype Writable { ... }

Цепочки прототипов:

Object f                    Object s
---------------------       --------------------
  Writable                    PipeWritable
    Stream                      Writable
      EventEmitter                Stream
        Object                      EventEmitter
                                      Object

Следуя определению instanceof:

Оператор instanceof проверяет, имеет ли объект в своей цепочке прототипов свойство прототипа конструктора.

Я ожидал бы, что (f instanceof PipeWritable) === false, потому что PipeWritable не находится в цепочке прототипов f (цепочка выше проверена вызовами Object.getPrototypeOf(...)).
Но он возвращает true, поэтому в моем анализе что-то не так.

Какой правильный ответ?

4b9b3361

Ответ 1

Это связано с некоторой частью кода в источнике Node.js, в _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;
  };
}

В спецификация языка оператор instanceof использует известный символ @@hasInstance, чтобы проверить, является ли объект O экземпляром конструктор C:

12.9.4 Семантика выполнения: экземпляр Оператора (O, C)

Реферат Операция InstanceofOperator (O, C) реализует общий алгоритм для определения того, наследует ли объект O путь наследования, определенный конструктором C. Эта абстрактная операция выполняет следующие шаги:

  • Если Type (C) не является объектом, бросьте исключение TypeError.
  • Пусть instOfHandler будет GetMethod (C, @@hasInstance).
  • ReturnIfAbrupt (instOfHandler).
  • Если instOfHandler не undefined, тогда а. Верните ToBoolean (Call (instOfHandler, C, "O" )).
  • Если IsCallable (C) false, бросьте исключение TypeError.
  • Вернуть OrdinaryHasInstance (C, O).

Теперь позвольте мне разбить код выше для вас, раздел по разделу:

var realHasInstance;
if (typeof Symbol === 'function' && Symbol.hasInstance) {
  …
} else {
  …
}

Вышеприведенный фрагмент определяет realHasInstance, проверяет, существует ли Symbol и существует ли известный символ hasInstance. В вашем случае это так, поэтому мы будем игнорировать ветвь else. Далее:

realHasInstance = Function.prototype[Symbol.hasInstance];

Здесь realHasInstance присваивается Function.prototype[@@hasInstance]:

19.2.3.6 Function.prototype [@@hasInstance] (V)

Когда метод @@hasInstance объекта F вызывается со значением V, выполняются следующие шаги:

Метод @@hasInstance Function просто вызывает OrdinaryHasInstance. Далее:

Object.defineProperty(Writable, Symbol.hasInstance, {
  value: function(object) {
    if (realHasInstance.call(this, object))
      return true;

    return object && object._writableState instanceof WritableState;
  }
});

Это определяет новое свойство конструктора Writable, хорошо известный символ hasInstance - по существу реализующий собственную версию hasInstance. Значение hasInstance - это функция, которая принимает один аргумент, объект, который тестируется instanceof, в этом случае f.

Следующая строка, оператор if, проверяет, является ли realHasInstance.call(this, object) правдой. Ранее упоминавшийся realHasInstance присваивается Function.prototype[@@hasInstance], который фактически вызывает внутреннюю операцию OrdinaryHasInstance (C, O). Операция OrdinaryHasInstance просто проверяет, является ли O экземпляром C, как описано вами и MDN, ища конструктор в цепочке прототипов.

В этом случае Writable f не является экземпляром подкласса Writable (PipeWritable), поэтому realHasInstance.call(this, object) является ложным. Поскольку это значение false, оно переходит к следующей строке:

return object && object._writableState instanceof WritableState;

Так как object или f в этом случае является правдивым, а поскольку f является Writable с свойством _writableState, являющимся экземпляром WritableState, f instanceof PipeWritable является true.


Причиной этой реализации является комментарии:

// Test _writableState for inheritance to account for Duplex streams,
// whose prototype chain only points to Readable.

Поскольку дуплексные потоки являются технически Writables, но их прототипные цепочки указывают только на Readable, дополнительная проверка, чтобы увидеть, является ли _writableState экземпляром WritableState, для duplexInstance instanceof Writable значение true. У этого есть побочный эффект, который вы обнаружили - писаемое существо - это экземпляр дочернего класса. Это ошибка, и ее следует сообщить.

Об этом даже сообщается в документации:

Примечание. Класс stream.Duplex прототипно наследует от stream.Readable и паразитно от stream.Writable, но instanceof будет корректно работать для обоих базовых классов из-за переопределения Symbol.hasInstance в stream.Writable.

Есть последствия для наследования паразитно от Writable, как показано здесь.


Я отправил вопрос на GitHub, и похоже, что он будет исправлен. Как Берги упомянул, добавив проверку, чтобы увидеть, если this === Writable, убедившись, что только Дуплексные потоки были экземплярами Writable при использовании instanceof. Там тянуть запрос.