Skip to content
This repository has been archived by the owner on Oct 10, 2019. It is now read-only.

Subclassing BigInt question #98

Closed
caiolima opened this issue Oct 28, 2017 · 8 comments
Closed

Subclassing BigInt question #98

caiolima opened this issue Oct 28, 2017 · 8 comments

Comments

@caiolima
Copy link
Collaborator

I'm not sure if it is possible to subclass BigInt. If I'm no wrong, based on BigInt constructor, we should throw TypeErrror if newTarget is not undefined. So, if we have:

class B extends BigInt {}

let b = new B();

// -------or--------

class C extends BigInt {
    constructor() {
        super();
    }
}

let c = new C();

In both cases the correct result is throw type error, right?

@littledan
Copy link
Member

I have no idea what anyone would want to subclass primitives for, but it's important that we make the semantics well-defined here. If someone knows what they're doing, they can subclass BigInt and make a constructor that doesn't throw in the sort of ES5 subclassing way:

class B extends BigInt {
    constructor(value) {
        let self = Object(BigInt(value));
        Object.setPrototypeOf(self, B.prototype);
        // do whatever else with self
        return self;
    }
}

I agree with you that both of the pieces of code you wrote should throw a TypeError.

Symbol seems to have the same behavior with subclassing as you describe. I don't know if this was identified as a downside in the development of ES6, but this was a model I was trying to follow in the BigInt specification. Anyone else know the history here? cc @allenwb @rossberg

@caiolima
Copy link
Collaborator Author

That's interesting. I'm asking that because og thisBigIntValue condition on https://tc39.github.io/proposal-bigint/#sec-properties-of-the-bigint-prototype-object. In your test case, does an instance of B go through step 2.b?

@littledan
Copy link
Member

@caiolima I don't understand your question. When would that test go through thisBigIntValue?

The subclassing trick I'm using here creates a BigInt wrapper by calling Object() rather than new BigInt(), and then returns the new instance rather than returning undefined (to get this, which would be a ReferenceError).

@caiolima
Copy link
Collaborator Author

Ooh, My question is about thisBigIntValue Abstract operation:

The abstract operation thisBigIntValue(value) performs the following steps:

  1. If Type(value) is BigInt, return value.
  2. If Type(value) is Object and value has a [[BigIntData]] internal slot, then
    a. Assert: value.[[BigIntData]] is a BigInt value.
    b. Return value.[[BigIntData]].
    Throw a TypeError exception.

If I apply this operation to a instance of B, should it throw an exception or return the BigInt?

@littledan
Copy link
Member

It should return the BigInt. The object will have a [[BigIntData]] internal slot because it is a regular old BigInt wrapper instance, whose prototype has then been changed.

@caiolima
Copy link
Collaborator Author

O think I'm done with the question then. Closing the issue for now.

@allenwb
Copy link
Member

allenwb commented Oct 30, 2017

@littledan

Generally, I would expect any new primitive wrapper "class" to follow the pattern established by, early ES in that you can use new to create wrapper instances and that subclassing works (as shown in your example).

However, in defining ES6, we decide to deviate from that pattern because of one symbol-specific anti-use case. The conceptual model of Symbols is that each primitive symbol value in a program has to be explicitly created. This model is somewhat different from the legacy primitive types whose values have a more declarative existential nature. You newer need to instantiate a primitive number value, it is expressed as a literal or is produced as a result of a computation. But symbols have to be explicitly created and TC39 chose to make Symbol() the way to do it. However, we were afraid that some JS devs might not get the message and think "I need to create a symbol, I guess I should say new Symbol()". This would be particularly bad if new Symbol() created a wrapper object because symbol wrappers have a toString method. So, consider:

let sym1 = new Symbol("supersecret");  //I think I'm creating a secret symbol property key
let objWithSecret = {
   [sym1]: "this is a big secret"
};

Because, of the Symbol toString method. The actual property name used for the property defined for objWithSecret would be the string value'Symbol("supersecret")'. The developer who made such a mistake would likely not notice that they weren't using a symbol key and there is a good change that the code would pass any tests that they wrote. But, unbeknownst to the developer the secret is exposed to any part of the program that is passed the object.

The decision to break the legacy wrapper pattern by now allowing new Symbol() was specifically made to avoid this footgun. If somebody really need to create a Symbol wrapper instance they can say: new Object(mySymbol) instead of new Symbol(mySymbol).

I don't see any similar problems with BigInt. Hopefully, devs will think of them similarly to other (non-Ssymbol) primitive types, especially like Number. For that reason, I would suggest consistently following the constructor pattern used by all of the legacy wrapper classes and not follow the special case of Symbol.

@littledan
Copy link
Member

See #13 for previous discussion of this question. Another point was to just generally discourage people from doing new BigInt out of confusion, since it doesn't really have much of a use case. Similarly to the Symbol case, the BigInt would kinda work, but not always; I'm not sure if the consequences in one case are more grave than the other (given that we didn't do private symbols). Should I reopen this issue?

Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
None yet
Projects
None yet
Development

No branches or pull requests

3 participants