3

I have always been told “Classes are just syntactic sugar for prototypes.”

But in this example this shows this isn't true.

function SubArray() {}
SubArray.prototype = new Array( );
console.log(Array.isArray(new SubArray()))  // false

and the same example with classes.

SubArray = class extends Array{}
console.log(Array.isArray(new SubArray()))  // true

Funnily instanceof works fine on both new SubArray instanceof Array. Why isn’t Array.isArray returning true with prototype here?

Lime
  • 13,400
  • 11
  • 56
  • 88
  • classes are syntactic sugar for the prototypal pattern in javascript – Hyyan Abo Fakher Jul 10 '18 at 15:03
  • 2
    class are mostly syntax sugar for prototypes, the main difference is that classes can extend built-ins while regular prototypes can't. http://2ality.com/2015/02/es6-classes-final.html#subclassing-built-in-constructors – Axnyff Jul 10 '18 at 15:04
  • 4
    Because inheriting from builtins is one of the few bits where classes are more than sugar. – Bergi Jul 10 '18 at 15:04
  • https://stackoverflow.com/questions/36419713/are-es6-classes-just-syntactic-sugar-for-the-prototypal-pattern-in-javascript – Hyyan Abo Fakher Jul 10 '18 at 15:05
  • 2
    Possible duplicate of [Are the es6 classes really semantic sugar?](https://stackoverflow.com/q/48036853/1048572) – Bergi Jul 10 '18 at 15:06

5 Answers5

6

That's because you did not write the code that class is actually syntactic sugar of:

function SubArray () {
  if (!(new.target)) {
    throw new TypeError("Class constructor SubArray cannot be invoked without 'new'")
  }

  return Reflect.construct(Array, arguments, new.target)
}

Object.setPrototypeOf(SubArray.prototype, Array.prototype)
Object.setPrototypeOf(SubArray, Array)

console.log(Array.isArray(new SubArray())) // true

The above should behave identically to the example you provided using class syntax. Unfortunately not all of this behavior can be accurately reproduced without other ES6 constructs like new.target and Reflect.construct(), but at least those aren't necessarily required in order to produce your desired behavior:

function SubArray () {
  if (!(this instanceof SubArray)) {
    throw new TypeError("Class constructor SubArray cannot be invoked without 'new'")
  }

  return Array.apply(this, arguments)
}

SubArray.prototype = Object.create(Array.prototype)
// the line below is not necessary for Array.isArray()
// but accurately reproduces behavior of `class SubArray extends Array {}`
SubArray.__proto__ = Array // implementation hack if Object.setPrototypeOf() is not available

console.log(Array.isArray(new SubArray())) // true

The key here is that you delegate construction of the instantiated object to the Array constructor in order to initialize the object as an Array exotic object. So hypothetically, all that's strictly necessary is the following:

function SubArray () {
  return Array.call(this)
}

console.log(Array.isArray(new SubArray())) // true

But of course, you won't have access to Array.prototype methods in this case, so you should stick to class syntax or the second example if you have to support ES5.

Edit

I did some tinkering and I personally think this is a horrible idea, but if you want to emulate class as closely as possible in ES5, you can opt out of strict mode in order to have access to arguments.caller:

// DON'T ACTUALLY DO THIS
// just for demonstration purposes

function SubArray () {
  // this is about as close as you can get to new.target in ES5
  if (!(this instanceof SubArray) && !(arguments.caller && this instanceof arguments.caller)) {
    throw new TypeError("Class constructor SubArray cannot be invoked without 'new'")
  }

  return Array.apply(this, arguments)
}

SubArray.prototype.__proto__ = Array.prototype
SubArray.__proto__ = Array

// we want FooBar to extend SubArray sloppily
function FooBar () {
  if (!(this instanceof SubArray) && !(arguments.caller && this instanceof arguments.caller)) {
    throw new TypeError("Class constructor FooBar cannot be invoked without 'new'")
  }

  return SubArray.apply(this, arguments)
}

FooBar.prototype.__proto__ = SubArray.prototype
FooBar.__proto__ = SubArray

try {
  SubArray()
} catch (e) {
  console.log(e.toString())
}

console.log(new SubArray(1, 2, 3))

try {
  FooBar()
} catch (e) {
  console.log(e.toString())
}

console.log(new FooBar(1, 2, 3))
Patrick Roberts
  • 49,224
  • 10
  • 102
  • 153
0

Consider the steps to perform Array.isArray():

IsArray ( argument )

The abstract operation IsArray takes one argument argument, and performs the following steps:

If Type(argument) is not Object, return false.
If argument is an Array exotic object, return true.
If argument is a Proxy exotic object, then
    If the value of the [[ProxyHandler]] internal slot of argument is null, throw a TypeError exception.
    Let target be the value of the [[ProxyTarget]] internal slot of argument.
    Return IsArray(target).
Return false.

Your example fails at the step "If argument is an Array exotic object, return true." because it doesn't have it's own length property.

function SubArray() {}
SubArray.prototype = new Array( );
console.log(Array.isArray(new SubArray())) // false


const sa = new SubArray();
console.log(typeof sa); // object
console.log(Object.getOwnPropertyDescriptor(sa, "length")); // undefined
zero298
  • 25,467
  • 10
  • 75
  • 100
  • 2
    In practice setting .length doesn't seem to make `Array.isArray` return true though – Lime Jul 10 '18 at 15:14
  • 2
    An object doesn't become an Array exotic object just because it has an own length property. – Bergi Jul 10 '18 at 15:15
  • @William @bergi I'm not trying to imply that setting a length causes an Object to become an Array. I'm saying that's where the Object fails the `isArray` function. Besides, setting an explicit `length` would create a `configurable` property. Array exotics have non-configurable `length`. – zero298 Jul 10 '18 at 15:17
0

Thanks to rlemon in chat you can do something similar to what Patrick did with out Reflect and should theoretically have IE11 support.

function Foo() {
    const arr = new Array(...arguments);
    arr.__proto__ = Foo.prototype;
    return arr;
}

Foo.prototype.constructor = Foo;

Foo.prototype.__proto__ = Array.prototype;

Array.isArray( new Foo(123) ) 
Lime
  • 13,400
  • 11
  • 56
  • 88
  • I don't see what the point of avoiding `Reflect` is here. You're still using several ES6 constructs in order to implement this, and this does not emulate what `class extends` does. – Patrick Roberts Jul 10 '18 at 15:49
  • @PatrickRoberts probably makes sense to use `__proto__`. Usuing caniuse.com it appears IE11(some still support this saddly) supports `__proto__` but not Reflect. – Lime Jul 10 '18 at 15:53
  • Yeah, but `Foo` is not a constructor in this implementation. It completely discards the instantiated object created with `new Foo` and returns a different one constructed with `new Array`. – Patrick Roberts Jul 10 '18 at 15:57
  • @PatrickRoberts in practice what does this mean? `instanceof` and the `constructor` are correct. – Lime Jul 10 '18 at 15:58
  • 1
    `class Bar extends Foo {} console.log(new Bar() instanceof Bar) // false!!` – Patrick Roberts Jul 10 '18 at 16:00
0

This method makes use of iframes and allows you to extend the class if you choose using class extend fuctionality.

var iframe = document.createElement("iframe");
iframe.style.display = "none";
document.body.appendChild(iframe);

frames[frames.length - 1].document.write(
  "<script>parent.SubArray = Array;<\/script>"
);

SubArray.prototype.__proto__ = Array.prototype;
SubArray.__proto__ = Array;
console.log(Array.isArray(new SubArray()));
console.log(new SubArray() instanceof Array);
console.log(new SubArray() instanceof SubArray);

SubArray2 = class extends SubArray {} 

console.log(new SubArray2() instanceof SubArray)
Lime
  • 13,400
  • 11
  • 56
  • 88
-2

ES5 way of doing things do not allow you to properly to inherit from Array. ES6 makes it work properly.

Daniel A. White
  • 187,200
  • 47
  • 362
  • 445