Globální objekt v JavaScriptu:záležitost platforem, nečitelný kód a nerozbití internetu

Osobně si myslím, že existuje konečný počet situací, kdy bych uvažoval o umístění něčeho do globálního objektu. Takže když jsem objevil nový návrh TC39, jehož cílem je přidat novou globální vlastnost pro přístup ke globálnímu objektu v Javascriptu, byl jsem zmatený, ale zároveň zaujatý a musel jsem se na to podívat.

Nepřidáváme mnoho proměnných do globálního objekt už, že?

Když přemýšlíme o kódu front-end, je jasné, že další globální proměnné mají silný případ použití. Knihovny jako jQuery se umísťují do globálního jmenného prostoru, aby jejich použití bylo co nejjednodušší, a to pouhým přidáním prvku skriptu na stránku HTML.

(function(window) {
  // set something to the global object
  window.$ = {};
})(window);

Je běžnou praxí používat IIFE (okamžitě vyvolaný funkční výraz), aby se zabránilo úniku proměnných do globálního rozsahu. Tento IIFE se pak provede pomocí window objekt pro nastavení nových vlastností.

Pro kód JavaScript, který má běžet pouze v jednom prostředí, není na tomto přístupu nic špatného. Pro kontext prohlížeče můžeme jednoduše předat window (nebo self nebo frames ) a pro kontext Node.js můžeme použít global , ale co JavaScript, který by měl fungovat nezávisle v jakémkoli prostředí?

Univerzální JavaScript s Browserify

jQuery zjevně není dobrým příkladem pro JavaScript, který běží všude, takže se podívejme na jiný příklad. Testovací framework Mocha běží v Node.js a prohlížeči. Typický testovací soubor Mocha vypadá následovně:

var assert = require('assert');
describe('Array', function() {
  describe('#indexOf()', function() {
    it('should return -1 when the value is not present', function() {
      assert.equal(-1, [1,2,3].indexOf(4));
    });
  });
});

Chcete-li napsat test v Mocha, musíte použít describe a it funkce. Testovací rámec umístí tyto funkce do globálního objektu, který je připraven k použití. Zdrojový kód Mocha je zpočátku napsán pro kontext Node.js, což znamená, že přístupný globální objekt je global .

// mocha.js
// setup of mocha emitting the global object
suite.emit('pre-require', global, file, self);

// bdd.js
// actual setting of new global properties
suite.on('pre-require', function (context, file, mocha) {
  var common = require('./common')(suites, context, mocha);

  context.describe = context.context = function (title, fn) {};
  context.it = context.specify = function (title, fn) {};
});

Co je tedy potřeba k tomu, aby bylo možné tento kód spustit také v kontextu prohlížeče?

Mocha používá Browserify k vytvoření dalšího souboru, který lze spustit v kontextu prohlížeče. Proces sestavení zabalí kód do IIFE a poskytne objekt s názvem global .

Pro jednoduchost se podívejme na jednodušší příklad, který nedělá nic jiného než nastavení foo proměnná do globálního rozsahu spuštěného v kontextu Node.js.

// test.js
global.foo = 'bar';

Po transformaci tohoto jednoho řádku „Node.js JavaScript“ na „prohlížeč JavaScript“ pomocí browserify dostaneme poněkud záhadný výsledek. Když se na to podíváme blíže, uvidíme, že kód používající globální objekt je nyní zabalen do IIFE, který poskytuje globální objekt jako parametr funkce. Argument funkce pro tento parametr je silně vnořený ternární operátor, který kontroluje přítomnost globálních vlastností.

(function (global) {
  global.foo = 'bar';
}).call(this, typeof global !== 'undefined' ? global : typeof self !== 'undefined' ? self : typeof window !== 'undefined' ? window : {})

Nevím jak vy, ale tohle není nic, co bych označil za přátelské pro začátečníky nebo snadno čitelné. Opravdu potřebujeme tolik kontrol, abychom zjistili, co je globální objekt v prostředí?

Vyhodnocení globálního objektu je těžší, než se očekávalo

Ukazuje se, že tento problém má ještě více úhlů. Pokud chceme napsat JavaScript, který používá správný globální objekt a může běžet v jakémkoli prostředí, stává se to složité a mnoho projektů používá k tomuto problému různé přístupy.

Pojďme se tedy znovu podívat na vygenerovaný výstup browserify.

var global = typeof global !== 'undefined' ? 
             global : 
             typeof self !== 'undefined' ? 
               self : 
               typeof window !== 'undefined' ?
               window :
               {};

Tento kód hledá jeden po druhém vlastnosti global , self a window být přítomen. Pokud žádný z nich není definován, vzdá se a přiřadí pouze nový prostý objekt. Toto hodnocení pokrývá tradiční prostředí prohlížeče, pracovníky služeb a webu a kontext Node.js.

Docela dobré – ale pokusy a omyly nejsou správné

Tento přístup není ani udržovatelný, ani příliš perspektivní a nepokrývá všechny možnosti (včera jsem se dozvěděl o d8, což je skript JavaScriptu dodávaný s V8, který žádnou z těchto globálních vlastností neobsahuje). Nevíme, co přinese budoucnost a možná bude ještě více vlastností reprezentujících globální objekt. To znamená, že naše hodnocení bude stále delší a ošklivější a ošklivější.

Není this globální?

Slyšel jsem, že říkáte, že this také odkazuje na globální objekt (alespoň někdy). Proč bychom tedy nemohli použít IIFE a projít this k tomu?

(function(global) {
  global.foo = 'bar';
})(this);

To je správně! Tento fragment funguje, ale pouze v případě, že tento fragment není vnořen do jiné funkce. Protože pak this může odkazovat na změněný kontext nebo dokonce být undefined (kód běžící v přísném režimu).

// sloppy.js | works
function getGlobal() {
  return (function(global) {
    return global;
  })(this);
}

console.log(getGlobal()); // window
// strict.js | doesn’t work
'use strict';

function getGlobal() {
  return (function(global) {
    return global;
  })(this);
}

console.log(getGlobal()); // undefined

Spoléhání na this není bezpečná možnost pro získání globálního objektu v JavaScriptu. A je také třeba říci, že moduly ES6 budou v určitém okamžiku dostupné a this na nejvyšší úrovni uvnitř modulu nebude odkazovat na globální objekt, ale bude spíše undefined (díky Axelu Rauschmayerovi za upozornění).

Jaké další možnosti tedy máme?

Konstruktor funkcí může pomoci!

Funkce jsou nezbytnou součástí každého programovacího jazyka. V JavaScriptu existuje několik způsobů, jak je vytvořit. Dva běžné jsou výrazy funkcí a deklarace funkcí, ale existuje také nepříliš známý způsob použití konstruktoru funkcí.

var fn = new Function('a', 'b', 'return a + b;');
fn(1, 2) // 3

Funkce, které byly vytvořeny pomocí konstruktoru funkcí, vždy běží v globálním rozsahu . Tato skutečnost zajišťuje, že se zabýváme globálním rozsahem a poté používáme this se stává bezpečným způsobem, jak získat aktuální globální objekt.

'use strict';

function getGlobal() {
  return (function(global) {
    return global;
  })(new Function('return this;')());
}

console.log(getGlobal());

Tento úryvek funguje v přísném režimu, uvnitř nebo vně funkcí a je pravděpodobně tou nejlepší sázkou, kterou máme.

Velkou nevýhodou konstruktoru funkcí je, že direktivy Content Security Policy zabrání jeho provedení. CSP pomáhá snižovat riziko XSS útoků a je užitečnou technologií, ale použití konstruktorů funkcí bohužel spadá do kategorie „nebezpečného dynamického vyhodnocování kódu“. Takže když chceme použít konstruktory funkcí, musíme povolit dynamické vyhodnocení kódu a to je s největší pravděpodobností něco, co nechceme dělat.

Tento chaos může být brzy opraven

Nakonec se tedy ukázalo, že v současné době neexistuje žádná stříbrná kulka, která by získala skutečný globální objekt ve všech možných prostředích. Konstruktor funkcí je nejpřesnější, ale není zaručeno, že kód, který jej používá, nebude blokován direktivami CSP.

Daniel Ehrenberg měl stejný pocit a přišel s návrhem, aby byl globální objekt snadno přístupný, aby se zbavil všech těchto hodnocení.

Zdálo se, že se tato myšlenka všem líbí a návrh je v současné době ve fázi 3 procesu TC39. Jedna věc, která může vyžadovat další diskuse, je skutečný název vlastnosti, která by měla obsahovat odkaz. Většina lidí souhlasila s global podobné prostředí Node.js.

// crappy way | today
(function (global) {
  global.foo = 'bar';
}).call(this, typeof global !== 'undefined' ? global : typeof self !== 'undefined' ? self : typeof window !== 'undefined' ? window : {})

// easy way | hopefully future
(function (global) {
  global.foo = 'bar';
}).call(this, global)

V době psaní tohoto článku lidé ověřují, zda toto přidání může mít nějaký negativní dopad na samotnou webovou platformu. Pamatujete si na drama o Array.prototype.contains? Web nezapomíná na kód, který tam byl vytlačen. Nové jazykové funkce je třeba pečlivě vyhodnotit, abyste se ujistili, že doplňky a změny nenarušují stávající webové stránky.

Ve skutečnosti se ukázalo, že přidání vlastnosti global přeruší Flickr a Jira, což pravděpodobně znamená, že návrh musí být změněn, aby používal jiný název vlastnosti. Diskuse o použití self nebo System.global již začalo.

Doufejme tedy v to nejlepší, protože i když se snažíme vyhnout použití globals, existují pro ně případy použití a ty by neměly vyžadovat použití silně vnořeného ternárního operátoru, kterému nikdo nerozumí.

  • Návrh TC39 na Github
  • Velmi zajímavý článek Axela Rauschmayera na toto téma
  • global zlomí Flickr a Jira