Skip to content

Instantly share code, notes, and snippets.

@WebReflection
Last active July 11, 2018 10:31
  • Star 1 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
Star You must be signed in to star a gist
Save WebReflection/56d04ccb1e5b0e50c121 to your computer and use it in GitHub Desktop.
An attempt to polyfill Symbol for ES5 compatible engines
@WebReflection
Copy link
Author

Notes

  • this should already work cross realm thanks to shared prefix
  • Object.assign should be polyfilled on top. Engines with Object.assign support, already support Symbol too but those that don't should assign both Object.getOwnPropertyNames and Object.getOwnPropertySymbols properties
  • it might be necessary to feature-detect problematic engines same way I've done in lazyval
  • in case also null objects should be supported, it's necessary to wrap Object.create and try to interfer with __proto__ descriptor too so that all null objects will inherit from a null object that will have all Symbol set in there too. Quite obtrusive, but it can work ( please see next comment for an almost full working solution )

@WebReflection
Copy link
Author

Following an attempt to make __proto__ and null objects work with Symbols too. It does work apaprently with everything but {__proto__: null} literals since these won't pass through the setter.

(function (Object) {'use strict';

  // (C) Andrea Giammarchi - Mit Style

  if (Object.getOwnPropertySymbols) return;

  var
    hasConfigurableBug,
    id = 0,
    random = '' + Math.random(),
    prefix = '__\x01symbol:',
    prefixLength = prefix.length,
    GOPN = 'getOwnPropertyNames',
    create = Object.create,
    $null = create(null),
    gOPN = Object[GOPN],
    gOPD = Object.getOwnPropertyDescriptor,
    ObjectProto = Object.prototype,
    defineProperty = Object.defineProperty,
    descriptor = gOPD(Object, GOPN),
    __proto__ = gOPD(ObjectProto, '__proto__'),
    __$proto__ = {
      configurable: true,
      get: function () {
        return gPO(this);
      },
      set: function (proto) {
        sPO(this, proto === null ? $null : proto);
      }
    },
    gPO = Object.getPrototypeOf || function (o) {
      return __proto__.get.call(o);
    },
    sPO = Object.setPrototypeOf,
    get = function get(){},
    onlyNonSymbols = function (name) {
      return name.slice(0, prefixLength) !== prefix;
    },
    onlySymbols = function (name) {
      return name.slice(0, prefixLength) === prefix;
    },
    setAndGetSymbol = function (uid) {
      var descriptor = {
        enumerable: false,
        configurable: true,
        get: get,
        set: function (value) {
          if (hasConfigurableBug) {
            delete ObjectProto[uid];
            delete $null[uid];
          }
          defineProperty(this, uid, {
            enumerable: false,
            configurable: true,
            writable: true,
            value: value
          });
          if (hasConfigurableBug) {
            defineProperty(ObjectProto, uid, descriptor);
            defineProperty($null, uid, descriptor);
          }
        }
      };
      defineProperty(ObjectProto, uid, descriptor);
      defineProperty($null, uid, descriptor);
      return uid;
    }
  ;

  descriptor.value = function getOwnPropertyNames(o) {
    return gOPN(o).filter(onlyNonSymbols);
  };
  defineProperty(Object, GOPN, descriptor);

  descriptor.value = function getOwnPropertySymbols(o) {
    return gOPN(o).filter(onlySymbols);
  };
  defineProperty(Object, 'getOwnPropertySymbols', descriptor);

  function Symbol(description) {
    if (this && this !== window) {
      throw new TypeError('Symbol is not a constructor');
    }
    return setAndGetSymbol(
      prefix.concat(description || '', random, ++id)
    );
  }

  descriptor.value = Symbol;
  defineProperty(window, 'Symbol', descriptor);

  // defining `Symbol.for(key)`
  descriptor.value = function (key) {
    var uid = prefix.concat(prefix, key, random);
    return uid in ObjectProto ? uid : setAndGetSymbol(uid);
  };
  defineProperty(Symbol, 'for', descriptor);

  // defining `Symbol.keyFor(symbol)`
  descriptor.value = function (symbol) {
    return (
      (prefix + prefix) === symbol.slice(0, prefixLength * 2) &&
      -1 < gOPN(ObjectProto).indexOf(symbol)
    ) ?
      symbol.slice(prefixLength * 2, -random.length) :
      void 0
    ;
  };
  defineProperty(Symbol, 'keyFor', descriptor);

  descriptor.value = function (proto, descriptors) {
    return create.apply(
      Object,
      proto === null ?
        (arguments.length === 2 ?
          [$null, descriptors] :
          [$null]) :
        arguments
    );
  };
  defineProperty(Object, 'create', descriptor);

  if (!sPO) {
    try {
      // if it's not poisoned
      __proto__.set.call({}, null);
      sPO = function (o, p) {
        __proto__.set.call(o, p);
      };
    } catch(_) {
      sPO = function (o, p) {
        defineProperty(ObjectProto, '__proto__', __proto__);
        o.__proto__ = p;
        defineProperty(ObjectProto, '__proto__', __$proto__);
      };
    }
  }
  defineProperty(ObjectProto, '__proto__', __$proto__);

  try {
    hasConfigurableBug = Object.create(
      defineProperty(
        {},
        prefix,
        {
          get: function () {
            return defineProperty(this, prefix, {value: false})[prefix];
          }
        }
      )
    )[prefix];
  } catch(o_O) {
    hasConfigurableBug = true;
  }

}(Object));

@ljharb
Copy link

ljharb commented Apr 3, 2015

So far the caveats I notice are that Symbol.for/Symbol.keyFor won't work cross-realm, and that typeof won't report "symbol" for any value. Also, could the string version of the Symbol conflict with a shimmed Symbol created in another realm?

@WebReflection
Copy link
Author

not sure even natively for and keyFor would work cross realm (need to test) but I think there's no much I can do there (and x-realm is rarely a real-world problem). Conflicts are extremely highly improbable and using a DateTime won't give me much more security so ... not sure that's a real concern.

typeof returns a primitive as specs say, and string is the only primitive that makes sense.

Transpilers could wrap it as typeOf() and verify it's a Symbol and since Symbol() instanceof Symbol is false, as well as Symbol() instanceof Object I don't think returning an Object would be a better option than returning a string.

If you hold a Symbol you should not have problems though, and since the whole logic is based on the assumption in order to retrieve symbols you need to getOwnPropertySymbols() I guess we can live with the current state.

That being said, {__proto__: null} and null objects are IMO a bigger, probably not worth fixing, gotcha ... but that plus all other concerns are the reason we cannot possibly have a shim but only a sham for ES6

@ljharb
Copy link

ljharb commented Apr 5, 2015

The spec requires them to work cross-realm but I'm not sure how engines are doing wrt to that compliance.

Right, I think that i'll have to be a sham. Looks great so far though!

@ljharb
Copy link

ljharb commented Apr 5, 2015

Also, possibly a more reliable test for https://gist.github.com/WebReflection/56d04ccb1e5b0e50c121#file-symbol-js-L5 is if (typeof Symbol === 'function' && typeof Symbol() === 'symbol')?

@WebReflection
Copy link
Author

I think if getOwnPropertySymbols is there we have to assume somebody else either patched upfront or the browser must support Symbol natively. I don't see how that check can improve a sham, since we risk to overwrite other attempts that might be there and work as intended for reasons.

TL;DR I wouldn't go for that check, it's prolix for no advantages.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment