4

Can I enforce that the prototype of an object not be changed?

Note! There are some requirements:

  • The object should behave just like a regular object literal (add/remove/configure/modify properties and descriptors on the object),
  • with literally the only new limitation being that the prototype is permanent.

So other than the prototype being permanent, I don't want to add any other limitations (tools like Object.seal/freeze/preventExtensions impose more limitations on the object).

Would I have to monkey-patch Object.prototype.__proto__ and Object.setPrototypeOf in order to achieve this?

trusktr
  • 44,284
  • 53
  • 191
  • 263
  • 5
    Have you really missed `Object.seal` or it doesn't fit you for some reason? Also, `Object.freeze` could fit you even better. – Wiktor Zychla Apr 14 '19 at 19:03
  • @WiktorZychla Ahah! I hadn't tried it, but my intuition didn't think that that would work, because I figured that `__proto__` being a getter/setter would mean that the setter could still accept the value and do as it pleased (just like in user code setters)! Thanks! Does this mean that user code setters should check if an object is sealed, and throw in that case? Otherwise, why does Object.seal disable `__proto__`, but not regular user setters? – trusktr Apr 14 '19 at 19:19
  • @WiktorZychla Made a new question about that: https://stackoverflow.com/questions/55679182 – trusktr Apr 14 '19 at 19:26
  • `__proto__` is deprecated. Do not use it any longer. – Bergi Apr 14 '19 at 20:41
  • @Bergi Even though it is officially spec'd now? Making the syntax for making object literals with pre-defined protptypes, `var obj = { __proto__: otherObj, ... }` is so niiiiiice though. In all reality, which browser or engine is going to stop supporting `__proto__` even if it is "deprecated", and now officially spec'd? In any case, I need to work with reality (apps can and do use `__proto__` today). – trusktr Apr 14 '19 at 21:03
  • @WiktorZychla Updated my answer, because I need the object to behave just like a regular object literal (adding/removing properties freely, etc) with the only (only) limitation being that the prototype is permanent. – trusktr Apr 14 '19 at 21:08
  • 1
    @trusktr It is specified only to have consistency between web-compat environments. [It is officially specified as deprecated](https://stackoverflow.com/a/36061819/1048572). – Bergi Apr 14 '19 at 21:13
  • @Bergi May be true, but still, it's very convenient, used in real apps, and probably not going away any time soon. :) – trusktr Apr 15 '19 at 18:23
  • @WiktorZychla I posted an answer with a monkey-patch approach. Are there any gotchas or problems I may have missed? – trusktr Apr 15 '19 at 19:37
  • @Bergi Any thoughts on my answer? – trusktr Apr 15 '19 at 19:44

2 Answers2

4

One option is Object​.prevent​Extensions() (note, this locks the whole object from extensions, doesn't lock only the prototype from being modified):

'use strict';
const obj = {};
Object.preventExtensions(obj);
Object.setPrototypeOf(obj, { possibleNewPrototype: 'foo' });

'use strict';
const obj = {};
Object.preventExtensions(obj);
obj.__proto__ = { possibleNewPrototype: 'foo' };
trusktr
  • 44,284
  • 53
  • 191
  • 263
CertainPerformance
  • 356,069
  • 52
  • 309
  • 320
  • I unmarked this as the answer, because I need the object to behave just like a regular object literal (adding/removing properties) with the only limitation being that the prototype is permanent. – trusktr Apr 14 '19 at 21:06
  • With that restriction too, what you're looking for [isn't possible](https://www.ecma-international.org/ecma-262/6.0/#sec-ordinary-object-internal-methods-and-internal-slots-setprototypeof-v) - the `SetPrototypeOf` internal method can be used to change the underlying prototype of an object *if and only if* the object is still extensible (and, if not extensible, other properties can't be added either). – CertainPerformance Apr 15 '19 at 01:40
  • I posted an answer with a monkey-patch approach. Are there any gotchas or problems I may have missed? – trusktr Apr 15 '19 at 19:38
  • After being in TypeScript for a while, I've come to like the idea of preventing extensions, as it enforces type safety. If I user really wants to associate new data with an object, they can use a Map or WeakMap. I believe this will lend to less breakable code. The idea of preventing extensions has grown on me. – trusktr Dec 17 '19 at 23:31
0

I came up with a monkey-patch to do what I wanted, exposing a lockPrototype function in the following snippet.

The only problem that I am aware of is that if some other code runs before my code, then they can get original references to Object.setPrototypeOf and the setter from the Object.prototype.__proto__ descriptor, and thus work around my monkey-patch. But for most cases I think it would work. A native implementation wouldn't have this problem.

Before I accept my own answer, are there any other problems with it?

Here's the example (the part labeled lockPrototype.js is the implementation, and the part labeled test.js is how it could be used):

// lockPrototype.js /////////////////////////////////////////////////////
const oldSetPrototypeOf = Object.setPrototypeOf
const lockedObjects = new WeakSet

Object.setPrototypeOf = function(obj, proto) {
  if (lockedObjects.has(obj))
    throw new TypeError("#<Object>'s prototype is locked.")

  oldSetPrototypeOf.call(Object, obj, proto)
}

const __proto__descriptor = Object.getOwnPropertyDescriptor(Object.prototype, '__proto__')

Object.defineProperty(Object.prototype, '__proto__', {
  ...__proto__descriptor,

  set(proto) {
    if (lockedObjects.has(this))
      throw new TypeError("#<Object>'s prototype is locked.")

    __proto__descriptor.set.call(this, proto)
  },
})

// export
function lockPrototype(obj) {

  // this behavior is similar to Object.seal/freeze/preventExtensions
  if (typeof obj !== "object" && typeof obj !== "function")
    return obj

  lockedObjects.add(obj)
}




// test.js //////////////////////////////////////////////////////////////
// import {lockPrototype} from './lockPrototype'

const a = {}
const b = {}
const c = {}
const proto = { n: 5 }

lockPrototype(b)
lockPrototype(c)

Object.setPrototypeOf(a, proto) // works fine

console.log('a.n', a.n) // 5

setTimeout(() => {
  console.log('b.n:', b.n) // undefined

  setTimeout(() => {
    console.log('c.n', c.n) // undefined
  })

  c.__proto__ = proto // throws
})

Object.setPrototypeOf(b, proto) // throws

(fiddle: https://jsfiddle.net/trusktr/Lnrfoj0u)

The output that you should see in Chrome devtools console is:

enter image description here

trusktr
  • 44,284
  • 53
  • 191
  • 263