node.js - Hata sınıflarını alt sınıflarken Object.setPrototypeOf’un performans sorunları hakkında endişelenmeli miyim?


Bu MDN uyarısı nedeniyle Object.setPrototypeOf(this, new.target.prototype) fonksiyonu hakkında sorularım var:

Uyarı: Bir nesnenin [[Prototype]]'ünü değiştirmek, modern JavaScript motorlarının özellik erişimlerini optimize etme şekline bağlı olarak, şu anda her tarayıcıda ve JavaScript motorunda çok yavaş bir işlemdir. Ayrıca, mirası değiştirmenin etkileri ince ve yaygın olup, sadece Object.setPrototypeOf(...) ifadesinde geçirilen süre ile sınırlı değildir, aynı zamanda [[Prototype]]'ü değiştirilmiş herhangi bir nesneye erişimi olan herhangi bir koda da yayılabilir. Daha fazla bilgi için JavaScript motoru temellerini: prototipleri optimize etme başlığı altında okuyabilirsiniz.

Bu özellik dilin bir parçası olduğu için, bu özelliği performanslı bir şekilde uygulamak motor geliştiricilerinin yükümlülüğüdür (ideal olarak). Motor geliştiricileri bu sorunu ele alana kadar, performans konusunda endişeleriniz varsa bir nesnenin [[Prototype]]'ünü ayarlamaktan kaçınmalısınız. Bunun yerine, Object.create() kullanarak istenen [[Prototype]]'e sahip yeni bir nesne oluşturun.

TypeScript (Node.js) içinde Object.setPrototypeOf(this, new.target.prototype) kullanmadan prototip stringini geri yüklemenin en iyi yolu ne olurdu (ama sınıfları kullanarak)? Bunun hepsi, Express'te uygulanan bir hata yönetimi ara yazılımı yüzünden; burada hatanın kaynağını belirlemek ve buna göre uygun bir yanıt döndürebilmek için instanceof kullanmam gerekiyor, ancak Error sınıfından türeyen bir sınıf yaptığımda error instanceof Error true dönüyor, ama error instanceof CustomError false dönüyor. Küçük bir araştırma yapınca bunu resmi TypeScript belgelendirmesinde buldum:

new.target desteği

new.target meta-özelliği ES2015'te tanıtılmış yeni bir sözdizimidir. Bir yapıcının örneği new ile oluşturulduğunda, new.target değeri, örneği tahsis etmek için ilk kullanılan yapıcı işlevine bir referans olacak şekilde ayarlanır. Bir işlev new ile oluşturulmak yerine çağrılırsa, new.target undefined olarak ayarlanır.

new.target, bir sınıf yapıcısında Object.setPrototypeOf veya __proto__ ayarlanması gerektiğinde kullanışlıdır. Bu tür bir kullanım örneği, NodeJS v4 ve üzeri sürümlerde Error'dan miras almaktır.

Örnek
class CustomError extends Error {
  constructor(message?: string) {
    super(message); // 'Error' breaks prototype chain here
    Object.setPrototypeOf(this, new.target.prototype); // restore prototype chain
  }
}

Bu, oluşturulan JS ile sonuçlanır

var CustomError = (function(_super) {
  __extends(CustomError, _super);
  function CustomError() {
    var _newTarget = this.constructor;
    var _this = _super.apply(this, arguments); // 'Error' breaks prototype chain here
    _this.__proto__ = _newTarget.prototype; // restore prototype chain
    return _this;
  }
  return CustomError;
})(Error);

Ve düşündüm ki tamam her şey mükemmel çünkü kod dönüştürüldüğü anda, derleyicinin yavaş olan bu fonksiyonla ilgileneceğini ve onu daha verimli başka bir prosedürle değiştireceğini varsayıyorum, ama şaşırtıcı bir şekilde durum öyle değil ve performans konusunda biraz endişeliyim (büyük bir proje üzerinde çalışıyorum).

Node 14.5.0'da TypeScript sürüm 3.9.6 kullanıyorum, yaptığım testlerin ekran görüntüleri bunlar:

  • Node ile TypeScript
  • TypeScript Oyun Alanı
  • TypeScript derleyici sonuçları

Cevap #1

Bunun için endişelenmezdim.

  • Her şeyden önce, genel bir kural olarak, programınızın çalışma süresinin çoğunu hata nesneleri oluşturmakla geçireceğini beklememelisiniz: bunun yerine, başarılı bir şekilde istekleri yerine getirecektir. Nadiren çalışacak bir kodu mikro-optimize etmemeniz gerekir: kapsamlı testler yapmadığınız sürece, bu tür mikro-optimizasyonlar sadece çok, çok daha sonra ortaya çıkacak hataların eklenme riskini taşır.

  • Uyarı, yalnızca nesnenin prototipini gereksiz yere değiştirmekten sizi caydırmayı amaçlamaktadır. Yani bunu, kaçınılabilir bir şekilde (veri yapılarını yeniden tasarlayarak) ve nesne zaten yoğun bir şekilde kullanılacak diğer yerlere verildikten sonra yapmak anlamına gelir. Bunun nedeni, prototipleri değiştirmenin, o uyarı kutusunda bağlantısı verilen makalede açıklandığı gibi, özellik arama önbelleklerinin dayandığı değişmezlikleri bozmasıdır. Makalenin yazarı bile, bunu makalenin özetinde şu şekilde yazmaktadır (vurgular bana aittir):

    Prototiplerinize dokunmayın! Ya da eğer gerçekten prototiplere dokunmanız gerekiyorsa, diğer kod çalışmadan önce dokunun, böylece en azından kodunuz çalışırken motor içindeki tüm optimizasyonları geçersiz kılmazsınız.

    Error sınıfından türetme durumunda, doğruluk açısından nesnenin prototipini bir şekilde değiştirmeniz gerçekten gerekir (ister Object.setPrototypeOf, ister eski yöntem __proto__ ile) ve bu kaçınılmazdır; aksi takdirde instanceof düzgün çalışmaz (soruda da belirtildiği gibi). Ve bunu kurucuda, yani nesnenin ömrünün başlarında yaptığınız için, performans açısından da nispeten zararsızdır; çünkü söz konusu özellik arama önbelleklerinin o noktada muhtemelen henüz oluşturulmamış olması beklenir.

    Bunu Object.hasOwn'in aşağıdaki yeniden uygulanışı ile karşılaştırın:

    function hasOwn(obj, key) {
        var proto = Object.getPrototypeOf(obj);
        Object.setPrototypeOf(obj, null);
        try {
            return key in obj;
        } finally {
            Object.setPrototypeOf(obj, proto);
        }
    }
    

    Gerçek Object.hasOwn (veya Object.prototype.hasOwnProperty) yerine onu kullanmak, programınızın performansını temelde yok edecek korkunç bir fikir olurdu.

  • Son olarak ama önemlisi: Error alt sınıflarının doğru prototipe sahip olmamasıyla ilgili sorunlar, yalnızca alt sınıflama ECMAScript 5 özellikleri kullanılarak taklit edildiğinde, yani Error.apply çağrıldığında ortaya çıkar. Yerel ECMAScript 6 alt sınıflaması kullanıldığında, bu sorun ortadan kalkar (daha sonra belirtildiği gibi, eğer TypeScript Function.prototype.apply yerine Reflect.construct kullanmış olsaydı, sorun yine ortadan kalkardı, fakat şu ana kadar bunu yapmamıştır). Node 14 hedeflediğiniz için, TypeScript'i ECMAScript 6 kodu üretmesi için yapılandırabilir ve bu konuyu unutabilirsiniz. Hatta Object.setPrototypeOf çağrısını olduğu gibi bırakabilirsiniz; böylece o sınıfa ECMAScript 5'e derlenecek bir kodda ihtiyaç duyulursa hazır olur; prototip baştan doğru şekilde ayarlanmışsa, çağrı boş bir işlem haline gelir ve performans üzerinde herhangi bir etkisi önemsiz olur.

İlgili makaleler