Repository: mrdoob/three.js (JavaScript)
Date: 2022-06-09 15:05:26+00:00
Task type: Bug Fix

Issue:
isVector3  is present when serializing Vector3
<!-- Ignoring this template may result in your bug report getting deleted -->

**Describe the bug**

A clear and concise description of what the bug is. Before submitting, please remove unnecessary sections.

since r141, when serializing Vector3 class using JSON.stringify, `isVector3` is present

**To Reproduce**

Steps to reproduce the behavior:
1. Go to '...'
2. Click on '....'
3. See error

***Code***

```js
// r141
console.log(new THREE.Vector3())
// output:
// {isVector3: true, x: 0, y: 0, z: 0}

// r140
console.log(new THREE.Vector3())
// output:
// {x: 0, y: 0, z: 0}

```

***Live example***

[*141 ](http://mrdoob.com/projects/htmleditor/#B/hVRRb9MwEH5uf8VRXlwUnFRjArXZBJSyPTAxbRMTj258bQyJHWynXTftv2M7SdewSeQhcXx3391999npqy/f5zc/LxeQ27I4HabNZ5DmyLj7DtISLYMsZ9qgPRnVdvX2wygYjN0V6FeDpeI7ePCrwZJlv9da1ZK/zVSh9BRer8IzC+aS6bWQU0iaX7VBvSrUdgq54Bxl2H304HGHnsZtJanP0iTOtKgsGJ2djHJrKzON44xL+stwLMRGU4k2llUZ21wjfkzo5N2EJvGyFgVv9mgpvPvo1OUJYAe4p0NfxIZpyFiJmkVgMpQYgUbJUaOedfY1KkeO3kVQMotasMKt0OSzgLCqZWaFkiCksGTs+AktN6BwAhK3cHN+tVjQS9SmQue8wXmwEnh/HMFWSK62VEiJ+lZwm0Pc2ztHsc5tBK6/CCYJjGcHCWiljPDp6b3LddSUNAid9HJf+x3ShgYzfRphz3Pux0kguWvm6dOFoI6FPmyVO6rOWhOBSVddx1TP+8KR9pkZkV20VgIP0MonuUvC4/nQuNKuuylYXSM87kvwpD8DJC/MB3qNMs5JGNgeqBtxD+wWl2ffrlqLr4xJ67AEM4eFHIZTd1SuxT2S5zOMXhhhF85VVpcoLfVKp6yqHNw8d6Ile+1RrspFgd7LRfmgYfd67KuOSeG7JmBFibCXn2+XamVZ0MadazTY3zgVOZKPZy847fpOk3+4os2CdMekFfiekj81GvspVOPQvvoBkq66vWaVNKpAWqg1eSL+hzsTSh+R8Xj41GBzmkLY/7Gbm6Q74WncXCHuSvGX3F8=)

[*140 ](http://mrdoob.com/projects/htmleditor/#B/bVRNb9swDD2nv4LLLgrgyg7aYUPiBNuyrD2sWNEWK3ZULCbWZkueJCf9QP77JNluYrQ+xIpIvkfykU7fffu5uPt9vYTclsX8JG1egzRHxt17kJZoGWQ50wbtbFjb9emnYTAY+1igPw1Wij/Csz8NViz7u9Gqlvw0U4XSE3i/Ds80mEumN0JOIGn+qi3qdaF2E8gF5yjD7d6Dxx16GreZpJ6lIc60qCwYnc2GubWVmcRxxiX9YzgWYqupRBvLqoxtrhE/J3R8ntAkXtWi4M0dLYV3H84dTwA7wp2f+CS2TEPGStQsApOhxAg0So4a9bSzb1C55ujHCEpmUQtWuBOafBoQ1rXMrFAShBSWjFx/QskNKMxA4g7uLm+WS3qN2lTonLe4CFYCHz9EsBOSqx0VUqK+F9zmEPfuLlFschuBqy+CcQKj6REBrZQRnp4+Oa6zJqVBqKTHfetvSBsazPQgYc9z4eUkkDw0enq6ENR1oQ9b5a5VF62JwLjLrutUz/vKNe0rMyK7aq0EnqEdn+QhCY/vh8a1dtVNwOoaYf+Sgm/6K0Dyhj7QK5RxToJgL0CdxD2we1xd/LhpLT4zJq3DEswcJ3IcTt2q3IonJK81jN6QsAvnKqtLlJb6SaesqhzcIndDS15mj3JVLgv0Xi7KB510P/v+1DEpfNUErCgRRu16ZkoaVSAt1IYcKvzlhk/pMzIaHYCaqQ2JafxXo7FfAqLD/u5FIB1D27yjTUrjZlXd6vqPyX8=)
**Expected behavior**

Should not contains `isVector3` ( also, I don't think it is a good idea to have a boolean inside an heavily use class memory wise )


**Screenshots**

![image](https://user-images.githubusercontent.com/1267483/171355967-3957749f-e54d-4677-b50f-2d2e5b3af9e7.png)

**Platform:**

 - Device: [Desktop, Mobile]
 - OS: [Windows, MacOS, Linux, Android, iOS]
 - Browser: [Chrome, Firefox, Safari, Edge]
 - Three.js version: [dev, r???]



Issue discussion:
This is duplicate of #24093.

Serializing math objects should not be done via `JSON.stringify()`. Please use `toArray()` and `fromArray()` interface.
Well, that will require a LOT of change in my code. a LOT. 
And I'm still not sure why you need a boolean to indicate if it is a Vector3 or not. This seems really really unnecessary.
I think we can solve this by adding a `toJSON` method to `Vector3`:

```js

	toJSON() {

		return { x: this.x, y: this.y, z: this.z };

	}
```

https://jsfiddle.net/8wr47h1m/
That is one way of solving it for sure ( will probably slow down the process of serialization, which is a different issue ). 

But I'm really curious about the need for a `isVector3` boolean ? 
Because every "object" class in three.js has such boolean.
Read https://github.com/mrdoob/three.js/pull/24047 for more details.
@olivierchatry 

> That is one way of solving it for sure ( will probably slow down the process of serialization, which is a different issue ).

Could you do performance test?
@mrdoob will do asap ( will probably do that tomorrow, have to work on something today ).
@Mugen87 I understand that in the prototype it seems "free" memory wise, I'm not sure CPU wise if the access is slower or not ( have no clue about how v8 or SpiderMonkey handle access to attribute in prototype ). 

The real question is why you need as `isVector3`, this seems rather odd, but maybe there is a use case I do not see.

@LeviPesin ok but how is it use ? 
> The real question is why you need as isVector3, this seems rather odd, but maybe there is a use case I do not see.

There are occasions where we have to test for the object's type. We could do this with the `instanceof` operator however that would require to import the actual type/class which is bad for tree-shaking, see https://github.com/mrdoob/three.js/issues/24006#issuecomment-1120764103.
I honestly have a really really hard time getting my head around that. For me, the less attribute you have in an heavily use class the better. I can understand for "hi level" object, but for math object, I think it should be kept to the bare minimum. But then again, it is a reflexes I had when I was using C++, so it might really not being relevant here. It just feels really wrong. 

Also, "there are occasion" is the root of all evil :).

( I'm benchmarking the `toJSON` solution now as I'm really curious about it ).
Yup HEAVY hit :

```javascript
import { Vector3 } from "./Vector3.js"


const array = []

let i 
for (i = 0; i < 100000; ++i) {
  array.push(new Vector3(Math.random(), Math.random(), Math.random()))
}

const start = performance.now()
for (i = 0; i < 100; ++i) {
  JSON.stringify(array)
}

const delta = performance.now() - start

console.log(`took ${delta} ms`)
```

Without toJSON:
```
❯ node test.mjs
took 4962.212369999848 ms
three.js/src/math on  dev [?] with  took 5s
❯ node test.mjs
took 4958.565330000594 ms
```

With toJSON:
```
❯ node test.mjs
took 5927.112598000094 ms
three.js/src/math on  dev [!?] with  took 5s
❯ node test.mjs
took 6188.454107999802 ms
```
@Mugen87 there is no performance impact adding `isVector3` as member ( tested with this code ):

```javascript
import { Vector3 } from "./Vector3.js"


const array = []

let i 
for (i = 0; i < 100000; ++i) {
  array.push(new Vector3(Math.random(), Math.random(), Math.random()))
}

const len = array.length

const start = performance.now()

for (let j = 0; j < 10000; ++j) {
  for (i = 1; i < len; ++i) {
    array[i - 1].add(array[i])
  }
}

const delta = performance.now() - start

console.log(`took ${delta} ms`)
```
Even if a custom `toJSON()` method introduces an overhead, the performance seems still acceptable. If @mrdoob is okay with it, I can make a PR and update all vector classes. 
Performance wise, it is not super good, at least for my personal use case ( I'm using serializiation on backends, generating a good amount of points ). 

It is definitely better than nothing though, even if I really don't get the need for a boolean inside a vector class. 

Thanks a lot for the help !
@Mugen87 

> If @mrdoob is okay with it, I can make a PR and update all vector classes. 

Sounds good! 👍
@olivierchatry 

<img width="968" alt="Screen Shot 2022-06-02 at 1 11 37 PM" src="https://user-images.githubusercontent.com/97088/171551580-41303062-722b-45d7-8faf-40f3de2d6aee.png">

@mrdoob the good old grep ! could have use that.

Why not use static member ?

```javascript
class Vector3 {

  static isVector3 = true;
  ...
}

const a = new Vector3()
console.log(a.constructor.isVector3)
> true
console.log(a)
> Vector3 { x: 0, y: 0, z: 0 }
```
This has been discussed before, https://github.com/mrdoob/three.js/pull/21285#issuecomment-780159867

Static properties do now allow `a.isVector3` which is the actual usage of `is*` properties. They are never called over the `prototype` reference.
I agree it is not a substitute, but it seems like way better to define a static, and access it through the constructor then adding a member ? 

I do understand that it would require a lot of change ( since it probably mean that it needs to be implemented everywhere ), but at the same time, this seems way more clean than add a bit of memory on each vector3 ? ( I know, I'm on and on about that, but this feels really **REALLY** dirty to add a boolean to a vector class that will be created on each instance.

Also if I look at the grep, from @mrdoob this can probably be done another way ? )

( I'm invoking my inner Mike Acton ! )


Hm. Can a `object.constructor.name` work? It would not quite work with inheritance, so it may be possible to make a new method (and call it instanceof, for example :-) ), which goes along prototype chain and checks if any constructor has desired name.

So it would be like the JS's own instanceof (i.e. it would not require .is booleans) and it would work with tree-shaking.
Well ...
We can also use get :

```javascript
class Vector3 {

  get isVector3() {
    return true
  }
  ...
}

const a = new Vector3()
console.log(a.isVector3)
> true
console.log(a)
> Vector3 { x: 0, y: 0, z: 0 }


```
> I do understand that it would require a lot of change ( since it probably mean that it needs to be implemented everywhere ), but at the same time, this seems way more clean than add a bit of memory on each vector3 ?

The migration effort that results from such a change is inacceptable. Besides, the additional memory allocation of a single boolean is so minor that the resulting overhead is negligible, see https://github.com/mrdoob/three.js/pull/24047#issue-1233086737. 

Turning the `is*` properties into getters has already been tried here and reverted, see https://github.com/mrdoob/three.js/pull/19997#issuecomment-673685741. 

I'm not to sure about that. at the bare minimum it's one byte per item, but given the proportion of javascript to suck up memory :
Using nodejs v16.15.0
```javascript
import { Vector3 } from "./Vector3.js"

const array = []
for (let i = 0; i < 1000000; ++i) {  
  array.push(new Vector3())
}

const used = process.memoryUsage().heapUsed / 1024 / 1024;
console.log(`The script uses approximately ${Math.round(used * 100) / 100} MB`);
````

With isVector3 as an attribute :
( run multiple time, these are two min-max value )
The script uses approximately 77.86 MB
The script uses approximately 73.4 MB

Without:
( run multiple time, only value that I get )
The script uses approximately 65.65 MB

This adds about **10 mega bytes** of memory for a million vector3. I would not call that negligible !

Side note, using accessor ( get version ) is way better :

The script uses approximately 65.65 MB
The script uses approximately 65.65 MB
The script uses approximately 65.65 MB




> Side note, using accessor ( get version ) is way better :

But slow (also as my version).

I think that the public fields (proposed by @marcofugaro) is actually a good idea? It will create only one instance per class.
It's a shame there isn't a way with class definitions to declare fields on the prototype...

I haven't seen this approach addressed for the `toJSON` use case but what if the `is*` properties were defined with "Object.defineProperty"?

```js
class Vector3() {

  constructor() {

    Object.defineProperty( this, 'isVector3', { value: true } );

    this.x = 0;
    this.y = 0;
    this.z = 0;

  }

}
```

Not sure if there's an performance implications to the above approach but it does prevent `isVector3` from getting serialized with "toJSON" meaning we can also avoid adding custom "toJSON" code to the codebase for all the math classes.
It's also the case that if @olivierchatry's use case is considered too unique that they can override the toJSON function themselves for their own app:

```js
Vector3.prototype.toJSON = function() {
  return { ... };
};
```
> I haven't seen this approach addressed for the toJSON use case but what if the is* properties were defined with "Object.defineProperty"?

#21284
@Mugen87 okay thanks for the reference. Last crazy idea, though - what if we set the prototype in the class constructor? I'm not sure how this would affect tree shaking and I don't love the idea but it would address both of the concerns raised in this thread (new `isVector3` member per instance and json serialization):

```js
class Vector3() {

  constructor() {

    Vector3.prototype.isVector3 = true;

    // or

    Object.getPrototypeOf( this ).isVector3 = true;

    this.x = 0;
    this.y = 0;
    this.z = 0;

  }

}
```

My hope would be that since the prototype access is within its own constructor that it won't affect tree shaking but I suppose that might be fairly bundler-implementation specific so maybe it's a risky assumption.
I think we can actually try using polymorphism and "does the object have the method we need" (duck typing) instead of using `.is`... See https://github.com/mrdoob/three.js/issues/24199#issuecomment-1148964423.
I'm worried that this thread seems pretty far down a very niche path... THREE.Vector3 does not need to be optimized around storing millions of Vector3 instances in memory — that's a terrible choice for performance regardless of this particular boolean property. This is the reason why three.js (and GPU APIs) have moved toward typed-array-based APIs like BufferAttribute instead. If you need to iterate over many vertices, a small number of vectors should be pooled and initialized, when needed, from compact binary views. 

Let's keep `.is` properties please, they're fine. 🙏

Solution patch:
diff --git a/src/math/Matrix3.js b/src/math/Matrix3.js
--- a/src/math/Matrix3.js
+++ b/src/math/Matrix3.js
@@ -2,7 +2,7 @@ class Matrix3 {
 
 	constructor() {
 
-		this.isMatrix3 = true;
+		Matrix3.prototype.isMatrix3 = true;
 
 		this.elements = [
 
diff --git a/src/math/Matrix4.js b/src/math/Matrix4.js
--- a/src/math/Matrix4.js
+++ b/src/math/Matrix4.js
@@ -4,7 +4,7 @@ class Matrix4 {
 
 	constructor() {
 
-		this.isMatrix4 = true;
+		Matrix4.prototype.isMatrix4 = true;
 
 		this.elements = [
 
diff --git a/src/math/Vector2.js b/src/math/Vector2.js
--- a/src/math/Vector2.js
+++ b/src/math/Vector2.js
@@ -2,7 +2,7 @@ class Vector2 {
 
 	constructor( x = 0, y = 0 ) {
 
-		this.isVector2 = true;
+		Vector2.prototype.isVector2 = true;
 
 		this.x = x;
 		this.y = y;
diff --git a/src/math/Vector3.js b/src/math/Vector3.js
--- a/src/math/Vector3.js
+++ b/src/math/Vector3.js
@@ -5,7 +5,7 @@ class Vector3 {
 
 	constructor( x = 0, y = 0, z = 0 ) {
 
-		this.isVector3 = true;
+		Vector3.prototype.isVector3 = true;
 
 		this.x = x;
 		this.y = y;
diff --git a/src/math/Vector4.js b/src/math/Vector4.js
--- a/src/math/Vector4.js
+++ b/src/math/Vector4.js
@@ -2,7 +2,7 @@ class Vector4 {
 
 	constructor( x = 0, y = 0, z = 0, w = 1 ) {
 
-		this.isVector4 = true;
+		Vector4.prototype.isVector4 = true;
 
 		this.x = x;
 		this.y = y;


Test patch:
diff --git a/test/unit/src/core/Object3D.tests.js b/test/unit/src/core/Object3D.tests.js
--- a/test/unit/src/core/Object3D.tests.js
+++ b/test/unit/src/core/Object3D.tests.js
@@ -301,9 +301,11 @@ export default QUnit.module( 'Core', () => {
 			obj.translateOnAxis( new Vector3( 0, 1, 0 ), 1.23 );
 			obj.translateOnAxis( new Vector3( 0, 0, 1 ), - 4.56 );
 
-			assert.numEqual( obj.position.x, 1, 'x is equal' );
-			assert.numEqual( obj.position.y, 1.23, 'y is equal' );
-			assert.numEqual( obj.position.z, - 4.56, 'z is equal' );
+			assert.propEqual( obj.position, {
+				x: 1,
+				y: 1.23,
+				z: - 4.56,
+			} );
 
 		} );