JavaScript之数据类型 - 原始类型和引用类型

1,967 阅读7分钟

前言

JavaScript有8种数据类型,分为两大类,原始类型7种引用类型1种。分别是:

  • 原始类型(Primitive):Null、Undefined、Boolean、String、Number、BigInt和Symbol。
  • 引用类型(Reference):Object(Array、Function)。

一、数据类型

1、typeof 操作符

将一个 变量 或一个 数值字面量 传给 typeof 操作符,就可以得到这个变量或数值字面量的数据类型。注意,因为 typeof 是一个操作符而不是函数,所以不需要参数(但可以使用参数)。

typeof null === 'object';             // Null
typeof undefined === 'undefined';     // Undefined
typeof false === 'boolean';           // Boolean
typeof '' === 'string';               // String
typeof 3.14 === 'number';             // Number
typeof 42n === 'bigint';              // Bigint
typeof Symbol() === 'symbol';         // Symbol
typeof function() {} === 'function';  // Function
typeof [1, 2, 4] === 'object';        // Array
typeof {a: 1} === 'object';           // Object

这里留下一个问题,

typeof null === 'object';

2、Undefined 类型和 Null类型

(1)、Undefined 类型

Undefined类型 只有一个值,就是特殊值 undefined。当使用 var 或 let 声明了变量但没有初始化时,就相当于给变量赋予了 undefined 值。

但是在对 已声明但未初始化的变量 调用 typeof 时,返回的结果是"undefined",但对 未声明的变量 调用它时,返回的结果还是"undefined",这就有点让人看不懂了。

let message1;
console.log(typeof message1);   // "undefined"  这个变量被声明了,只是值为 undefined
console.log(typeof message2);   // "undefined"  这个变量未声明

在JavaScript红皮书上这样说:

无论是声明还是未声明, typeof 返回的都是字符串"undefined"。逻辑上讲这是对的,因为虽然严格来讲这两个变量存在根本性差异, 但它们都无法执行实际操作。

所以在平时写代码时候我们通常建议在声明变量的同时,就对变量进行初始化。这样,当 typeof 返回"undefined"时,你就会知道那是因为给定的变量尚未声明,而不是声明了但未初始化。

(2)、Null类型

Null 类型 同样只有一个值,即特殊值 null。逻辑上讲, null 值表示一个空对象指针,这也是给 typeof 传一个 null 会返回"object"的原因。

在JavaScript红皮书上建议:

在定义将来要保存对象值的变量时,建议使用 null 来初始化,不要使用其他值。这样,只要检查这个变量的值是不是 null ,就可以知道这个变量是否在后来被赋了一个值。

(3)、undefined 和 null 区别

  • undefined表示“缺少值”,就是此处应该有个值,但还没给值。
    • 变量声明了,但未赋值,undefined
    • Object对象有属性未被赋值,undefined
    • 函数没有返回值时,默认返回的是 undefined
  • null 表示“没有对象”,即此处不该有值
    • 作为函数参数,表示该函数的参数不是对象。
    • 作为对象原型链的终点。
5 + null = 5;
5 + undefined = NaN;  // NaN是number类型

二、原始值和引用值的 动态属性

1、值的定义

原始值和引用值的定义方式很类似,都是创建一个变量,然后给它赋一个值。不过,在变量保存了这个值之后,可以对这个值做什么,则大有不同。

2、引用值的动态属性

引用值 ,可以随时添加、修改和删除其属性和方法。

let person = new Object();
person.name = "Nicholas";
console.log(person.name); // "Nicholas"

这里首先创建了一个对象,然后把它保存在变量 person 中。然后,给这个对象添加了 name 属性,并赋值了一个字符串"Nicholas"。在此之后,就可以访问这个新属性,直到 对象被销毁或属性被显式地删除。

3、原始值的动态属性

原始值 , 不能有属性,尽管给原始值添加属性不会报错。

let name = "Nicholas";
name.age = 27;
console.log(name.age); // undefined

这里,代码想给字符串 name 定义一个 age 属性并给该属性赋值 27。紧接着在下一行,字符串 name 的属性不见了,显示为 undefined。所以,只有引用值可以动态添加后面可以使用的属性。

注意原始类型的初始化可以只使用原始字面量形式(也就是那7种原始值)。如果使用的是 new 关键字,则JavaScript 会创建一个 Object 类型的实例,但其行为类似原始值。

let name1 = "Nicholas";
let name2 = new String("Matt");
name1.age = 18;
name2.age = 20;
console.log(name1.age);      // undefined
console.log(name2.age);      // 20
console.log(typeof name1);   // string
console.log(typeof name2);   // object

三、原始值和引用值的 复制值

1、原始值的复制类型

在通过变量把一个原始值赋值到另一个变量时,原始值会被复制到新变量的位置。

let num1 = 5;
let num2 = num1;

num1 包含数值 5。当把 num2 初始化为 num1 时, num2 也会得到数值 5,但这个值跟存储在 num1 中的 5 是完全独立的,因为它是那个值的副本。这两个变量独立使用,互不干扰。

image.png

2、引用值的复制类型

引用值从一个变量赋给另一个变量时,存储在变量中的值也会被复制到新变量所在的位置。区别在于引用值这里复制的值实际上是一个指针,它指向的是存储在堆内存中的对象。操作完成后,两个变量实际上指向同一个对象,因此对其中一个对象的改变会在另一个对象上反映出来。

let obj1 = new Object();
let obj2 = obj1;
obj1.name = "Nicholas";
console.log(obj2.name); // "Nicholas"

上例中,变量 obj1 保存了一个新对象的实例。然后,这个值被复制到 obj2,此时两个变 量都指向了同一个对象。在给 obj1 创建属性 name 并赋值后,通过 obj2 也可以访问这个属性,因为它们都指向同一个对象。下图展示了变量堆内存中对象之间的关系。

image.png

四、原始值和引用值的 传参

首先放出结论:ECMAScript 中,所有函数的参数都是按值传递的。

既然传参是按值传递,那就意味着函数外的值会被复制到函数内部的参数中,就像从一个变量复制到另一个变量一样。如果是原始值,那么就跟原始值变量的复制一样,如果是引用值,那么就跟引用值变量的复制一样。

按值传递:值会被复制到一个局部变量。(即一个命名参数,或者用 ECMAScript 的话说, 就是 arguments 对象中的一个槽位)。
按引用传递:值在内存中的位置会被保存在一个局部变量,这意味着对本地变量的修改会反映到函数外部。(这在 ECMAScript 中是不可能的)

function Set(num,obj) {
    num += 10;
    obj.name = "Nicholas";
}
let count  = 30;
let person = new Object();
Set(count,person);
console.log(person);    // {name: 'Nicholas'}
console.log(count);     // 30

这个例子中,设置了两个变量,原始值变量 count 和引用值变量 person 。然后,这个对象被传给Set()方法,并被复制到参数 obj 中。
在函数内部, objperson 都指向同一个对象。结果就是,即使对象是按值传进函数的, obj 也会通过引用传递访问存在堆内存中对象。所以当函数内部给 obj 设置了 name 属性时,函数外部的对象也会反映这个变化,因为 obj 指向的对象保存在全局作用域的堆内存上。但这样就有同学认为这是按引用传递的了,那我们改写一下代码:

function Set(num,obj) {
    num += 10;
    obj.name = "Nicholas";
    obj = new Object();
    obj.name = "Greg";
}
let count  = 30;
let person = new Object();
Set(count,person);
console.log(person);    // {name: 'Nicholas'}
console.log(count);     // 30

这个例子前后唯一的变化就是 Set()中多了两行代码,将 obj赋予新属性后,又重新定义为一个有着不同 name 属性的新对象。
person 传入 Set()时,其 name 属性被设置为"Nicholas"。然后变量 obj 被设置为一个新对象且 name 属性被设置为"Greg"。如果 person按引用传递的,那么 person 应该自动将指针改为指向 name 为"Greg"的对象。可是,当我们再次访问 person.name 时, 它的值是"Nicholas",这表明函数中参数的值改变之后,原始的引用仍然没变。
obj 在函数内部被重写时,它变成了一个指向本地对象的指针,而这个本地对象在函数执行结束时就被销毁了。