10 What Are the Eight Data Types in Js and What to Pay Attention To

10 What Are the Eight Data Types in JS and What to Pay Attention To #

Hello, I am Ishikawa.

JavaScript’s data types may seem easy to understand, but are you sure you really understand them and know how to use them? In fact, if you don’t have a systematic understanding of the concept of data types, it is likely to lead to occasional hidden problems, which will create pitfalls in our written programs. For example, simple addition calculations may result in unexpected results, or you may not make good use of some of JS’s advantages, such as quickly accessing the properties of values through built-in wrapper objects.

Before we formally begin, I want to point out that although there is no shortage of books and resources introducing data types in JS, we will not go into theoretical redundancy here. However, I will quickly guide you to establish a basic framework of understanding values, and then through a deep understanding of the problems, we will achieve the goal of exploiting strengths and avoiding weaknesses.

So, how many types of values are there in JavaScript? The answer is 8. If we further categorize them, we can divide them into two categories: primitive types and object types.

Among them, primitive data types include number, boolean, string, BigInt, null, undefined, and the later-added symbol. The values of this data type are immutable.

Object data types include objects that we often talk about. The values of objects are mutable. It is a large category, and if we further divide it, it also includes commonly used arrays, functions, Date, RegExp, and the later-added Map and Set. Yes, the arrays and functions that we often use as values are all object data types.

Image

Okay, after understanding the classification of data types, next, we will further understand their principles and usage methods through various problems that may occur when different types are applied in practice, lay a solid foundation, and pave the way for subsequent learning.

Next, let’s start with primitive type data.

Primitive Types #

Primitive types are the data types we commonly use. They include basic types like numbers, strings, and booleans, as well as special types like null and undefined. Compared to object types, primitive types have the largest deviation between theory and practice from a value perspective. Additionally, because they seem relatively simple, they are also easily overlooked.

Therefore, the focus of this lesson is to magnify these issues with a magnifying glass and gain a deeper understanding of their core principles and solutions.

Numbers: Why does 0.1 + 0.2 not equal 0.3? #

Let’s start with a small experiment: enter 0.1 + 0.2 in the Chrome Developer Tools and see what the result is. Surprising or not, the result is not 0.3, but instead has a \(4 \times 10^{-17}\) at the end.

Image

Why does this happen? Let’s take a look at the data types diagram. In JavaScript, the number type includes two categories: floating-point numbers and integers.

Image

The above calculation uses floating-point numbers. What corresponds to floating-point numbers? Fixed-point numbers.

Fixed-point numbers have the advantage of being able to handle small calculations in daily life. However, they also have a major drawback: when performing calculations with very small or very large numbers, a large amount of space is wasted. For example, if we want to express the population of China, which is 1.4 billion, it would be written as 1,400,000,000 with eight zeros after 14. To solve this problem, we need to use scientific notation.

Floating-point numbers are represented using scientific notation, consisting of the mantissa, base, and exponent. In the example above, if it is expressed in scientific notation, it would be written as \(1.4 \times 10^{9}\). For decimals, it’s the opposite. For example, if the number is 2, the decimal representation would be 10 to the power of -3.

Image

We just looked at an example in base 10, but JavaScript uses IEEE 754 as the binary floating-point arithmetic standard. This standard specifies four floating-point arithmetic modes: single precision, double precision, extended single precision, and extended double precision. In this case, JavaScript chooses double precision (64-bit), which is commonly known as double or float64.

As the name implies, this method uses 64 bits. It includes 1 bit for the sign, 11 bits for the biased exponent, and 52 bits for the fraction.

Image

Since the algorithm for converting decimal to binary multiplies the decimal part of the decimal number by 2 until there is no more decimal part, some decimals in base 10 cannot be accurately represented as binary decimals. And since the floating-point number here is binary, there will be precision loss for decimals.

Furthermore, when we use addition and subtraction, the precision will be further lost because alignment is required (i.e., aligning the exponents and shifting), and then calculating. Depending on the number of decimal places returned by the JavaScript engine, there may be a third loss. As a result, the final result deviates from the actual sum or subtraction.

Now that we understand the issue of precision loss, it is not a bug, but a design that follows the standard. At the same time, we also understand the reasons for this situation and the steps in the actual calculation. So what can we do to solve the precision problem caused by it?

Usually, the solution to this problem is to enlarge and shrink in proportion. Let’s take an example: if we want to set the price of a product to 19.99 yuan, we can first convert it to 1999, so that we can perform addition calculations, and then shrink it back.

var priceBigInt = 1999n;
var priceStr = String(priceBigInt);
var priceYuan = `¥${priceStr.slice(0,-2)}.${priceStr.slice(-2)}`;
console.log(priceYuan);

NaN: How do we determine if a value is a number? #

If floating-point numbers bring us expected and reasonable surprises, NaN brings us unexpected and unreasonable surprises.

  • Surprise 1 - In IEEE 754, NaN represents “not a number”, but if we use typeof NaN to check its type, it returns “number”.

  • Surprise 2 - Primitive types have the characteristic that two values with the same numerical value are considered equal. Object types are the opposite; even if two values have the same numerical value, they are considered different, and each value has a unique identity.

We can see an example of this. When we use strict comparison between two numbers, it returns true; when we use strict comparison between two object literals, it returns false.

123 === 123 // returns true
{} === {} // returns false

Following this principle, since NaN is a number, if we compare NaN strictly with NaN, it should theoretically return true, but in reality, it returns false.

NaN === NaN // returns false
  • Surprise 3 - In JavaScript, we use isNaN to determine if a value is a number, but when we input a string, it is also treated as a number. This is because during this process, the string “0” is converted to a number. isNaN(“0”) // returns false

So, from these surprises, we can find that it is unreliable to use NaN and isNaN to determine if a value is a number.

So, how can we accurately determine if a value is a number?

We can check the type of the value and use the isFinite function to filter out NaN and Infinity. isFinite is a built-in function in JavaScript.

However, similar to surprise 3, it will convert the value in parentheses, such as a string, to a number. So we need to use typeof to ensure that this conversion issue is not overlooked.

var isNum = function isNum(value){
  return typeof value === 'number' && isFinite(value);
}

string: How long is a string? #

We know that besides undefined and null, primitive types have built-in wrapper objects. Let’s take a look at how it works using a string.

In this example, we create a string using the constructor new String(). When we want to get its length, we can use the str.length method.

var str = new String("hello");
str.length // returns 5
typeof str // returns 'object'

Even if you don’t use the constructor, you can still use string literals to get the length of a string. In this process, you can also see the return value of the length. And when you use typeof to get its type, you will still get a string, not an object.

This is because when you use the length method, the JavaScript engine temporarily creates a string wrapper object. When this object calculates the length, it disappears. So when you look back at the type of str, it returns a string, not an object.

var str = "hello";
str.length // returns 5
typeof str // returns 'string'

boolean: Can you tell true from false? #

In Java, the boolean data type includes true and false. But note that in JavaScript, false, undefined, null, 0, NaN, and ’’ are all considered false.

You might ask, so what are true values? In fact, you can use the process of elimination and consider anything except false as a true value. To facilitate the query, you can refer to the following list:

Boolean values

null: What? You’re an object? #

As mentioned earlier, null is one of the six primitive data types. But when we use typeof to get the type of null, it returns ‘object’, which means it belongs to the object type.

This is a bug, but it’s not completely illogical. This is because null is actually an empty object pointer.

So how can we determine if a value is null? The solution is to not use typeof, but to directly compare the value with null using strict comparison.

In addition to null, another similar value is undefined. If null represents an empty object, undefined represents a lack of value. But when we compare their values, they are equal. However, when we compare their data types strictly, we find that they are different.

null == undefined // returns true
null === undefined // returns false

So, we can check if a value is empty using if (x === undefined || x === null) {}, or if (!x) {…}.

So when do we use undefined and when do we use null? Usually, we do not use undefined but treat it as a return value or system exception. For example, if we declare a variable but do not assign it a value, the result is undefined. If we want to intentionally define an empty object, we can use null.

var a;
a // undefined 
var b = null;

if (b != null){
  // do something!
}

Object Types #

After discussing primitive types, let’s take a look at object types. The issues with primitive types are generally clear-cut, as we’ve seen earlier, they are caused by certain limitations, flaws, or bugs in JavaScript design.

On the other hand, the issues with object types are more about the advantages and disadvantages shown in different scenarios and usage patterns.

Why does the instanceof operator return false for instances created based on objects? #

When you create an object, you can do so using either the literal syntax or the constructor pattern (we will discuss this in more detail when we talk about design patterns later, for now, let’s just understand their different usage patterns). However, in the case where you further create an instance based on an object, and you use Object.create() as mentioned earlier in the object-oriented programming pattern, you cannot use the instanceof operator to determine which object the new instance belongs to.

This is because the relationship between these two objects is more like authorization rather than inheritance, and they do not have a hierarchical relationship, hence the incorrect result. However, instances created through the classical prototype-based inheritance can be determined which object they belong to using the instanceof operator.

Let’s compare the usage of literals, constructors, and prototype-based inheritance. You can refer to the following code examples:

// Method 1: Literals
var objA = {name: "Object A"};
var objB = Object.create(objA);
console.log(objB instanceof objA); // Returns false

// Method 2: Constructor
var objA = new Object();
objA.name = "Object A";
var objB = Object.create(objA);
console.log(objB instanceof objA); // Returns false

// Classical Prototype-Based Inheritance
var objA = function() {
  /* more code here */
}
objB = new objA();
console.log(objB instanceof objA); // Returns true

In fact, not only objects, but arrays and functions can also be created using literals instead of constructors. On the other hand, the so-called numbers, strings, and booleans can also be created using constructors in addition to literals.

Image

We know that primitive types are immutable, while object types are mutable. For example, when we define a string and convert it to uppercase using toUpperCase(), if we retrieve its value again, we will find that it is still lowercase.

var str = "hello";
str.toUpperCase(); // Returns "HELLO"
str; // Returns "hello"

However, if we try to add a property to an object, the property will change when we retrieve its property again.

var obj = { vehicle: "car" };
obj.vehicle = "bus";
console.log(obj.vehicle); // Returns "bus"

Another thing to note is that object data is only referenced on the stack and actually stored in the heap. For example, suppose we have two variables, variable personA is assigned the value {name: “John”, age: 25}, when we assign personB to personA, and then modify the name of personA, the name of personB will also change. This is because they both reference the same object in the heap.

var personA = {
    name: "John", 
    age: 25
};
var personB = personA;

personB.name = "Jack";

personA.name; // Returns "Jack"
personB.name; // Returns "Jack"

Image

How to identify an array? #

Earlier, we mentioned that arrays are actually objects. So how can we determine if a value is an array or an object? It is through the typeof operator, which tells us the type of a value. When we use typeof to determine the type of an object or an array, it returns “object” for both.

However, as mentioned earlier, typeof also returns “object” for null, so we cannot use typeof alone to determine whether a value is actually an object. However, since null is a falsy value, we can combine it with a truthy check to exclude null and determine if a value is actually an object.

if (myVal && typeof myVal == 'object') {
  // myVal is an object or an array
}

Okay, the above example can filter out objects, but we still cannot determine if an object is an array.

In fact, in JavaScript after ES5, there is an built-in isArray function, but before that, people had to determine if a value is an array by writing their own logic. However, to better understand the principles, we can create an isArray function based on the above example.

In this example, we use the characteristic of arrays that, although they are also objects from a data type perspective, the difference between them and object values lies in whether they can have a length property.

if (myVal && typeof myVal === "object" && 
typeof myVal.length === "number" 
&& !(myVal.propertyIsEnumerable("length"))) {
    console.log("yes");
}

Apart from that, we can also use the prototype we learned in the previous section about object-oriented programming to determine if a value is an array.

if (typeof Array.isArray === 'undefined') {
  Array.isArray = function (arg) {
    return Object.prototype.toString.call(arg) === "[object Array]";
  };
}

Function Literal: Declaration or Expression? #

In JavaScript, there are mainly two ways to write a function: function expressions and function declarations.

  • Function Declaration

Let’s first understand what a declaration is and what forms of declarations we have in JavaScript.

As shown in the figure below, we roughly divide declarations into four types: variables, constants, functions, and classes. From the figure, we can see that these four types of declarations are commonly used statements that we have encountered before, such as variables, constants, and functions that we mentioned when talking about functional programming and immutability, while classes were discussed when talking about object-oriented programming.

Image

After discussing declarations, let’s focus on function declarations. The biggest advantage of function declarations is that they can be used for hoisting. It can be said that unless there is a special requirement, function declarations are the default way of writing functions. The following is the abstract syntax tree (AST) of a function declaration.

Image

function a = {} // Function declaration
  • Function Expression

Now let’s take a look at function expressions. First, let’s understand what an expression is. An expression is a statement that can have many expressions, and a function can be written as such an expression. In function expressions, there are two ways to write them:

The first way is to treat the function as a literal value, the advantage of this approach is that it can be self-referenced.

var a = function() {} // Function expression

The second approach is in ES6, function expressions can also be written using arrow function syntax, which is advantageous when it comes to binding of this, parameters, and super-related issues.

var a = () => {}; // Function expression - arrow function

Extension: How to Convert Between Types? #

In addition, in JavaScript, we often encounter the problem of how to convert a value from one type to another.

Here, we can use coercion, which can be either explicit or implicit. For example, we can use explicit coercion to convert a string to a number.

var a = 42;
var b = a + ""; // implicit coercion
var c = String(a); // explicit coercion

In the above code snippet, when a = 42, it is a number. b uses implicit coercion to convert it to a string, while c uses explicit coercion to convert it to a string. In the ECMAScript official documentation, there are certain runtime conditions specified, and actual browsers use similar algorithms to determine the corresponding behavior.

Coercion Algorithm

Summary #

Through this lecture, I hope you have gained a more systematic understanding of JavaScript data types and better ways to solve related problems, so as to play up your strengths and avoid weaknesses.

In addition to addressing execution-level issues, I also hope you can appreciate the beauty and shortcomings of JavaScript. If you have seen the movie “The Good, the Bad and the Ugly,” you may know that its English name is The Good, the Bad, the Ugly. Similarly, through understanding data types in JavaScript, we can see both its beauty and flaws, which is where its brilliance lies.

Its beauty lies in its simplicity and flexibility. We can see that numbers and strings, although simple, can be flexibly used with built-in wrapper objects to access properties and utilize powerful calling capabilities; objects, functions, and arrays, although complex data types, can all be represented using literals.

At the same time, JavaScript also has its shortcomings, especially some defects and bugs with primitive data. However, it is precisely because of its flexibility and lack of too many restrictions that it initially attracted many developers. In addition to the evolution of the language itself, engineers have overcome these shortcomings in various ways.

Finally, let’s enrich the mind map mentioned at the beginning and summarize what we have learned today. Let’s put these key pieces of information into our “JS Law” layer in the information tower of our memory palace!

Image

Discussion Questions #

In the course, we have summarized that some types of data can be created using both literals and constructors. In your opinion, which way is more suitable in different scenarios?

Feel free to share your answers and thoughts in the comments section, and let’s have a discussion together. Also, please feel free to share today’s content with more friends. See you next time!