前言:于无声处听惊雷
当你写下自己的第一行 JavaScript 代码console.log('Hello world!')并成功运行,为迸现的 “Hello world!” 感到欣喜时,可曾想过这样一行简单的代码执行前要经过多少 JS 设计师精心设计的预处理?善战者无赫赫之功。JavaScript 的精华所在,恰恰就是这些我们经常忽视的地方。现在,让我们重新注视这些被 JS 设计师藏起来的细节,领略 JS 的独特魅力。
本文将带你探索 JS 中的各种数据类型以及它们背后与堆栈不得不说的故事。
一. JS 中的数据类型
JavaScript 是动态类型语言(变量类型在运行时由其值确定),也是弱类型语言(允许隐式类型转换)。JS 的类型中分为基本数据类型以及引用(复杂)数据类型,当你对一个变量的类型感到困惑的时候,可以使用 typeof 来获取它的类型(null 为特殊情况,后面会进行说明)。
接下来我将对 JS 的数据类型进行系统性介绍,本文篇幅较长,请已有了解的朋友可以选择性阅读数据类型或者直接跳转至 “堆栈” 篇章。
基本数据类型
基本数据类型截至目前,已有 7 种,分别是: Number、String、Boolean、Undefined、Null、Symbol(ES6 新增)、BigInt(ES2020 新增)。它们的存储位置在栈内存中(“堆栈”篇章细讲)。
1. Number(数值)
number 为数值类型,用于表示整数和浮点数。
-
常见整数类型格式为十进制,还可以用八进制(0开头,在vscode中可用0o开头避免警告)、十六进制(0x开头)表示数值。
- 运算方面: 和正常的算式计算规则一致,“+” 运算在与字符串运算时为特殊情况。
- 示例:
const decNum = 100; // 十进制 const octNum = 0o144; // 八进制 const hexNum = 0x64; // 十六进制 // 值均表示100 console.log(decNum); // 100 console.log(octNum); // 100 console.log(hexNum); // 100 // “+” 运算 const num1 = 1; const num2 = 2; const num3 = '2'; console.log(num1 + num2); // 3,这里得到的结果为数值 console.log(num1 + num3); // 12,这里得到的结果为字符串,不是数值类型 console.log(typeof (num1 + num3)); // string // 解决办法:可以为字符串前面加上运算符(比如+),使其自动转换为数值类型 console.log((+num3) + num1); // 3,此时得到的结果为数值 -
浮点类型可以使用小数点表示,也能使用科学计数法表示。在运算时,如果计算结果包含小数部分,JS 会自动转换类型为浮点类型。
- 在 JavaScript 中,所有数字都是以双精度 64 位浮点数(IEEE 754 标准)存储的,也就是说,其实 JS 中所有数字都是以浮点数的形式存储,1 和 1.0 虽然写法上分别是一个整数、一个浮点数,但是在内存中完全相同。
- 并且 JavaScript 引擎在打印数字时,会自动省略末尾无意义的小数点和零,所以 1.0 打印出来也是显示和 1 一样。
- 示例:
const intNum = 1; // 整数 const fNum1 = 1.0; // 浮点数(加上小数点后,尽管值等于1,但是类型为浮点数) const fNum2 = 0.1 // 浮点数 const fNum3 = .1; // 浮点数,值为0.1(有效,但是不推荐使用该形式) const fNum4 = 3.14e5; // 科学计数法,等于314000,结果值为整数,但语法为浮点数 const fNum5 = 3.14e2; // 等于314.0,小数部分为0,但仍是浮点数 const fNum6 = 3.14e-1; // 等于0.314,为小数,显然是浮点数 const fNum7 = 10 / 4; // 结果包含小数部分,自动转换为浮点数 console.log(intNum); // 1 console.log(fNum1); // 1 console.log(fNum2); // 0.1 console.log(fNum3); // 0.1 console.log(fNum4); // 314000 console.log(fNum5); // 314 console.log(fNum6); // 0.314 console.log(fNum7); // 2.5 -
数值类型 number 中有一个特殊的数值 NaN,意为“Not a Number”,即“不是数字”,用来表示数学运算中出现的非法或未定义的结果。
- 不自等性: 它是 JavaScript 中唯一一个不等于自身的值。
- 传播性:
NaN与任何数值进行运算都只会得到NaN,只要出现了NaN,那么最终的运算结果必然是NaN。 - 非报错性质:
NaN不是报错!它只是一种表示计算结果无效的返回值,甚至它的类型仍为 number。 - 示例:
// 一. 以下情况会导致出现 NaN // 1. 无效的数字转换 console.log(Number('abc')); // NaN console.log(parseInt('xyz')); // NaN // 2. 无意义的数学运算 console.log(0 / 0); // NaN Math.sqrt(-1); // NaN(在数学中负数的平方根是虚数,但在 JS 中会返回 NaN) // 3. 对非数字类型进行数学运算 console.log('money' * 5); // NaN // 二. NaN 的不自等性 console.log(NaN === NaN); // false // 三. 由于 NaN 的不自等性,不能直接使用 === 判断一个值是否为 NaN // 1. 使用全局函数 isNaN(),先转换参数为数字,再判断是否为 NaN console.log(isNaN(NaN)); // true console.log(isNaN('money')); // true // 2. 使用Number.isNaN(),不会对参数进行类型转换,只会判断参数恰好为 NaN 的情况 console.log(Number.isNaN(NaN)); //true console.log(Number.isNaN('money')); //false // 四. 类型判断 console.log(typeof NaN); // number
2. String(字符串)
在 JS 中,字符串是由0或者多个16位Unicode字符组成的序列,大部分文字可以用一个16位代码单元表示,但是有些 Unicode 字符(如表情符号)需要两个代码单元(代理对)。
-
在 JS 中,字符串是不可变的,一旦创建,其内容就无法更改。对字符串进行的所有操作都会返回新的字符串,而不会改变原始字符串。当我们学习了堆栈的知识,就能验证 string 的不可变性。耐心看下去哦。
- 示例:
let str = 'Hello'; str = str + ' world!'; // 这里并非修改了原字符串,而是创建了一个新的字符串 console.log(str); // Hello world! -
普通字符串:使用单引号('')或双引号("")包裹。
- 支持转义字符: 无论是单引号还是双引号,都能用来定义字符串,而且都支持转义字符(如 \'、\"、\n)。
- 单、双引号的选择: 使用单引号('')或者双引号("")包裹字符串都可以,在 JS 看来都是一样的,但是在项目中,为了保持风格,最好统一使用单引号或者双引号,可以使用 ESLint 等工具来强制统一代码风格。
- 引号嵌套规则: 使用单引号('')定义的字符串里可以直接包含双引号,反之亦然,这样能避免使用转义字符。
- 支持拼接: 普通字符串支持拼接操作,通过使用 “+” ,可以拼接得到一个新的字符串。
- 示例:
// 1. 单、双引号包裹的字符串并无区别 const str1 = 'Hello'; const str2 = "Hello"; console.log(str1); // Hello console.log(str2); // Hello // 2. 可以相互嵌套,避免使用转义字符 const str3 = '要 "Hello world" 每一天'; const str4 = "要 'Hello world' 每一天"; const str5 = "要 \"Hello world\" 每一天"; // 使用转义字符 console.log(str3); // 要 "Hello world" 每一天 console.log(str4); // 要 'Hello world' 每一天 console.log(str5); // 要 "Hello world" 每一天 // 3. 普通字符串拼接 const str6 = '每天都要学习'; const str7 = 'JavaScript'; console.log(str6 + str7); // 每天都要学习JavaScript const num = 2; // 字符串也能和数值类型进行拼接得到新字符串 console.log(str6 + str7 + num + '小时!'); // 每天都要学习JavaScript2小时!注意:频繁使用 “+” 拼接大量字符串(如循环中)可能影响性能,因为每次操作都会创建新字符串。
-
模板字符串:使用反引号(``)包裹字符串,支持变量插值和多行字符串。
- 位置:
- 模板字符串的功能很强大,但是必须使用反引号(``)才能实现变量插值和多行字符串(直接换行),使用单引号('')以及双引号("")无法做到。
- 示例:
// 变量插值 const familyName = '张'; // 姓 const givenName = '三'; // 名 const fullName = `我的名字叫${familyName}${givenName}`; // 插值表达式让字符串拼接变得方便且灵活 console.log(fullName); // 我的名字叫张三 // 多行字符串 const str1 = `我要学习 JavaScript ! 开心每一天!` console.log(str1); /* 我要学习 JavaScript ! 开心每一天! */ // 对于单、双引号,表面上的多行是没用的 const str2 = '我要学习' + 'JavaScript' + '!'; console.log(str2); // 我要学习JavaScript! // 想要做到换行,需要在换行的地方加上转义字符"\n" const str3 = '我要学习\n' + 'JavaScript\n' + '!'; console.log(str3);
3. Boolean(布尔类型)
boolean 类型就是用来作为判断条件的类型,只有两个值 true 和 false。true表示正确,可以执行;false表示错误,不能执行,就这么简单。
当然,作为基本类型之一,它的用法肯定不会这么单调,和其他类型之间还是有梦幻联动的。
- 在某些情况下(比如说条件判断、算术运算)时,JavaScript 的隐式类型转换就会偷偷开始发力,完成类型之间的转换。
- 转换规则:
- 简单数据类型 -> boolean
基本类型 转换为 Boolean的结果示例 undefinedfalseBoolean(undefined)→falsenullfalseBoolean(null)→false0(数值零)falseBoolean(0)→false0n(BigInt 零)falseBoolean(0n)→falseNaNfalseBoolean(NaN)→false""(空字符串)falseBoolean("")→false其他基本类型值(如非零数值、非空字符串) trueBoolean(1)→true;Boolean("a")→trueBoolean(Symbol())→true - 复杂数据类型 -> boolean
复杂类型 转换为 Boolean的结果示例 对象(如 {}空对象)trueBoolean({})→true数组(如 []空数组)trueBoolean([])→true函数 trueBoolean(function(){})→true正则表达式 trueBoolean(/abc/)→trueDate 对象 trueBoolean(new Date())→true
- 简单数据类型 -> boolean
4. Undefined(未定义)
undefined 类型比较特殊,只有一个特殊值 undefined,是全局对象的属性。
-
undefined语义为“未定义”,专门用来表示var或者let声明了变量却没有初始化时的值(const不算,因为const声明时必须初始化),重点在未初始化状态。 -
undefined只是表示未初始化的变量的值,不是未声明的变量的值(not defined)。 -
访问未声明的变量会报错。
-
访问引用类型对象中的未声明属性,会被自动赋值为
undefined,而不是报错。这是因为引用类型在 JavaScript 中极为灵活,可以在使用时随意添加和删除属性,访问它们的未声明属性时,就等于你已经为它们声明了这个变量,只是还没有赋值,JS 贴心地为帮它赋了undefined。 -
示例:
let a; // 变量 a 此时已经声明,但是并未初始化 console.log(a); // undefined console.log(typeof a); // undefined console.log(b); // 报错:ReferenceError: b is not defined,表示变量 b 并未声明 // 对象 const person = {}; console.log(person.name); // undefined,这种情况下不会报错
5. Null(空值)
与 undefined 类型相似,null 类型也只有一个特殊值 null,它们是 JavaScript 唯二表示“空值”的原始值。
null的语义为“有意为之的空值”,表示某个变量被明确设定为空对象引用,也就是个空对象指针,特指不存在任何对象值,重点是主动空值。null与undefined最大的区别就是使用的开发者是否 “有意为之”。null值是开发者编程时,主动使用,用来表示这个变量没有指向任何对象或者值,这是显式赋值。- 开发者:我知道这有个变量,但是我就要让它的值为空值(null),有值但是为空。
undefined值是开发者编程时,并未主动对声明变量进行初始化,JS 自动为这个变量赋undefined值,这是隐式赋值。- 开发者:我刚声明了一个变量,但是我忘记给它赋值了(undefined),无值。
null和undefined在某方面也有联系,在 JS 早期(Netscape Navigator 2.0 之前),null曾被用作 “未定义” 的默认值,但是后来为了更清晰地区分 “主动空值” 和 “未初始化状态” ,引入了undefined类型。-
使用 “==” 比较(宽松比较,类型转换后再比较)
undefined和null时,会得到 true。 -
使用 “===” 比较(严格比较,会比较类型和值是否都相同)
undefined和null时,会得到 false。 -
这两个比较结果可能会误导某些人,认为它们可以相互转换,值为相等,但是事实上,它们两个是完全独立的两个原始值。不存在派生、继承或者转换关系。在宽松比较时返回
true仅是设计师在语言设计时设立的特殊规则,仅此一例,纯属巧合。- 比如说,在检查一个变量是否有有效值时,程序员可能不关心它是
null还是undefined,只关心它是否是“空”的情况(也就是没有有效值),所以将它们定义为宽松相等,这是特例。 - 示例:
// 应用场景:检测声明的变量是否有有效值 let a; // 未定义,undefined let b = null; // 手动设置为了 null 值,null // 此时无论是检测出为 null 还是 undefined,都无所谓,因为都是属于没有有效值,能检测出来就够了 console.log(a == null); // true -> 没有有效值,目的达到了 console.log(b == undefined); // true -> 没有有效值,目的达到了 // 严格比较 console.log(a === null); // false -> undefined 不等于 null console.log(b === undefined); // false -> undefined 不等于 null - 比如说,在检查一个变量是否有有效值时,程序员可能不关心它是
-
Null的历史遗留问题
-
关于
null,还有最后一个能聊的。还记得我们刚开始特意标注的那个typeof操作符获取变量类型吗?我们提到了null在获取类型时,是一个特殊情况- 示例:
console.log(typeof null); // object -
天哪,
null难道是引用数据类型派来的间谍吗?我们基本数据类型终究还是错付了吗?这时候,我们似乎陷入了前后矛盾之中,毕竟我们刚信誓旦旦地表示null是七种基本数据类型之一,但是typeof似乎没给我们这个面子,表示null是个object对象。 -
其实这是 JS 早期的设计错误,Brendan Eich 大神仅用十天时间就设计出了 JS 这门语言,借鉴了很多语言的特性,也留下了很多bug,这是无法避免的。在 JS 最初的版本(1995)中,值在底层是以二进制形式存储的,每个值的前三位作为类型标签,用于标识数据类型。
- 其中
000代表对象;而null在底层被表示为全零(即为0×00)。这使得typeof操作符进行类型判断的时候,错误地将null的类型判定为了obejct对象,null其实还是基本数据类型。
- 其中
-
使用另一种判断方法
instanceof可以看出null并不是对象。这是因为instanceof操作符是通过检查对象的原型链(这个以后再讲是什么东西,现在只要知道只有对象才有原型链就行了)是否包含某个构造函数的prototype属性,来判断该对象是什么对象类型。对于原始值(如null、undefined、number等),永远只会返回false。- 示例:
console.log(null instanceof Object); // false -
那么是否有一天 JS 设计师会修复这个历史遗留问题?
-
抱歉,做不到。
- “屎山”代码为什么叫“屎山”,就是写到后面牵一发而动全身,动了一角都可能引起山崩,更别说是
null这种超级元老,基石一般的存在了。JS 发展到现在,用 JS 书写的代码何止千万行,其中不知道多少程序员依赖typeof null === 'object'的特性实现业务逻辑,要是改动了null,那么将会引起兼容性灾难,破坏大量现有代码。 - 所以为了杜绝后患,ECMAScript 5(2009 年)的明确规定:
typeof操作符对null的返回值被固定为"object",目的是 “保持与 JavaScript 引擎早期实现的兼容性”。相当于 JS 设计师是捏着鼻子认下了这个错,并且明确表示不会更改了,所以我们可以放心大胆的使用。
6. Symbol(独一无二)
Symbol 类型作为 ES6 新增的类型,它的作用就是创建独一无二的值,防止命名冲突。其用法为:
// 这是无描述的Symbol
const sym1 = Symbol();
// 这是有描述的Symbol
const sym2 = Symbol('Hello world!');
console.log(sym1); // Symbol()
console.log(sym2); // Symbol(Hello world!)
console.log(sym1.description); // 无描述的Symbol,其描述为undefined
console.log(sym2.description); // Hello world! 这里可以单独拿到描述
它具有以下特性:
-
唯一性: 就算传入的描述相同,创建出来的
Symbol值也不会相等。- 示例:
const sym1 = Symbol(); const sym2 = Symbol(); const sym3 = Symbol('Hello'); const sym4 = Symbol('Hello'); console.log(sym1 === sym2); // false,都为无描述仍会判定为不相等 console.log(sym3 === sym4); // false,描述相同仍会判定为不相等 -
隐藏性:
Symbol作为一个对象的属性时,不会在for in、Object.keys()等枚举操作中被枚举出来,从而实现了数据隐藏。- 示例:
const user = { userName: '张三', [Symbol(`密码`)]: '123456', age: 18 }; for (const attribute in user) { console.log(attribute, user[attribute]); } /* userName 张三 age 18 */ -
不可变性:
Symbol值一旦创建就不能被修改,具有不可变性,无法修改其描述,也无法为其添加属性和方法。Symbol的不可修改性与string类型的不可变性本质上相同,我们在“堆栈”篇章再说。
7. BigInt(任意大整数)
前面介绍 Number 类型的时候提到过,JS 中的 Number 类型数值是以双精度 64 位浮点数(IEEE 754 标准)存储的。对于整数,它的安全整数范围是-2^53 +1 到 2^53 -1,即在这个范围内的整数可以被精确表示,而超出安全整数范围的整数可能就会失去精度。在处理非常大的数值计算,比如密码学、金融计算或者时间戳等场景时,Number 类型就有些力不从心了,所以为了解决这个问题,ES2020 引入了 BigInt 类型。
BigInt 类型是专门为处理任意大整数设计的,它不遵循 Number 类型的 IEEE 754 标准。它的用法也很简单:
// 方式一:字面量形式创建,只需要在数字后面加上“n”即可
const veryLargeNum1 = 9007199254740992n; // 2^53
// 方式二:构造函数创建,使用 BigInt() 函数
const veryLargeNum2 = BigInt(9007199254740992);
- 存储方式:
BigInt能够表示任意大的整数,就是因为它的存储方式不一样。BigInt突破了固定位数限制,以动态分配内存的方式自动扩展位数,使用数组形式,将大整数拆分成多个片段,每个片段都是一个较小的数字,然后将这些片段存放在数组中。 - 原理: 可以将一个极大的整数拆分成多个32位或者64位的块,然后按照低位片段存储在数组的前端,高位片段存储在后端的顺序进行存放,方便进位操作。
引用(复杂)数据类型
JS 的引用数据类型是一个极广的范围,包括了 Object、Array、Function、Map 等,它们统称为对象,也可以这么说,“在 JS 中,除了基本数据类型,其他的类型都是对象”。将引用数据类型全部讲解是不可能的,太多了,我们着重讲一下 Object 就好了,这是它们共同的特性,可以一通百通。
对象(Object)
- JS 中的所有引用类型都继承了
Object的核心特性,并在此基础上扩展了特定功能,你可以把Object理解为引用数据类型的老祖宗,其他引用数据类型是它的后代,后代和老祖宗会有相似之处,就像儿子像爸爸妈妈一样。数组、函数、日期等本质上都是特殊类型的对象,现在我们来了解一下它们共同的特性。
引用传递
赋值和参数传递时传递引用。这和它们实际存储在堆内存中有关(细节我们将在“堆栈”篇章进行讲解)。
动态属性
在对象中,万物皆可扩展。JS 不像 Java 之类的语言在定义类时把对象的属性焊死了,在 JS 中,对象的属性是可以动态变化的,可以随时动态添加、修改或者删除属性。
- 所以之前我们介绍
undefined时,例子中直接访问对象的未声明属性,会返回undefined,其实在你访问这个未声明对象时,JS 已经帮你自动声明了,同时因为未对其进行定义,JS 自动将其赋值为了undefined,但是未声明的函数不能直接使用,会报错TypeError,不过这个其实也是undefined在发力。因为它会先查找叫这个名字的属性,未声明的情况下自然为undefined,直接把它归为变量了,调用一个变量自然会报类型错误。请看下面示例。- 示例:
const object1 = { name: '张三', sleep(){ console.log(`${this.name}正在睡觉`); } } console.log(object1.name); // 张三,此时拿到了已声明的属性 object1.name = '李四'; // 此时修改“name”属性的值为“李四” console.log(object1.name); // 李四 console.log(object1.age); // undefined,该变量未声明,JS自动添加至对象中,并赋undefined object1.gender = '男'; // 直接为对象添加一个带有值的属性 console.log(object1.gender); // 男,说明直接添加属性是有效的 // object1.eat(); 调用一个未声明的方法,报错:TypeError: object1.eat is not a function // object1.eat()等同于(object1.eat)() // 相当于JS会先查找该对象中是否有与这个属性名相等的属性,然后根据对应的是变量或者函数做出操作 (object1.sleep)(); // 张三正在睡觉,此时函数正常调用,说明确实是这个调用顺序 console.log(object1.sleep); // [Function: sleep],说明JS能够根据属性名找到并分辨出变量或者函数
原型链继承
MDN 中对原型链的定义是:JavaScript 中所有的对象都有一个内置属性,称为它的 prototype(原型)。它本身是一个对象,故原型对象也会有它自己的原型,逐渐构成了原型链。原型链终止于拥有 null 作为其原型的对象上。
-
Object.prototype的原型就是null,它是最基础的原型,所有对象都默认拥有它,所以它位于原型链的终点。 -
这里的原型
prototype对象不是一个简单的属性,它集合了很多变量以及函数。就拿我们最常用的toSring()方法来说,它就是Object.prototype中的函数。当我们创建一个普通对象并调用toString()方法时,即使该对象自身并未定义此方法,它仍然可以正常工作,这是因为 JavaScript 引擎会通过原型链找到Object.prototype.toString()方法并执行。- 示例:
// 这是我们自定义的对象,原型直接指向Object.prototype,没有二手中间商赚差价 const object1 = { name: '张三', sleep() { console.log(`${this.name}正在睡觉`); } }; // 此时我们并没有声明toString()方法,该方法是从Object.prototype处继承得来的,原汁原味 console.log(object1.toString()); // [object Object],可以正常使用 // 验证object1的原型是Object.prototype console.log(Object.getPrototypeOf(object1) === Object.prototype); // true // 验证Object.prototype的原型是null console.log(Object.getPrototypeOf(Object.prototype) === null); // true
原型对象与原型链作为 JS 中最难的知识点之一,我觉得要单独开一篇博客才能说的比较清楚。如果只是作为了解一下对象的特性,我觉得这个程度就够了,剩下的内容等我有空单独开一篇和大家聊一下。
二. 堆栈
铺垫了这么久,堆栈终于千呼万唤始出来。基本数据类型和引用数据类型最大的区别就和堆栈有密不可分的关系,因为这牵扯到它们的立身之基--存储方式。它们三者就像量子纠缠一样有着千丝万缕的联系,所以讲的时候必须一起讲,才能够将它们讲透。
栈内存与堆内存的特点
- 栈内存:
- 空间小
- 访问速度快
- 有序分配地址空间
- 结构类似于数据结构栈,后进先出
- 存放基本数据类型变量值本身或是引用数据类型变量的引用地址
- 栈内存的分配和回收由 JS 引擎自动管理,效率高
- 堆内存:
- 空间大
- 访问速度较慢
- 动态分配地址空间,无序(碎片化)
- 存放引用数据类型变量指向的实际对象
- 堆内存的分配和回收由垃圾回收机制(GC)处理,效率较低
基本数据类型存储
基本数据类型存储在栈内存中,其中存储的是基本数据类型对应的原始值,而在 JS 中,原始值是不可变的。一旦创建了,它就不能被改变。只能创建一个新值,然后重新分配给原变量。
- 示例:
var a = 1;
let b = 2;
const c = 3;
let str1 = '字符串1';
-
不可变性: 所以基本数据类型其实都有不可变性,那么为什么我们介绍基本数据类型时,只强调了
String和Symbol类型有不可变性呢?-
Number、Boolean、Null、Undefined的值本身就是单一的字面量,很容易就能看出变量无法被修改,只是用一个新值替换了。- 示例:
let b = 2; b = 5; // 重新赋一个新值,不是修改 -
String的值常常被我们拼接或是进行转大写之类的操作,很容易给我们一种可以修改的错觉。- 示例:
// 以下两种操作均不是对原字符串进行修改,而是用新值替换了旧值 let str1 = '字符串1'; str1 = str1 + '字符串后缀'; // str1与其拼接之后创建了一个新的字符串,并将其重新赋值给了str1 let str2 = 'Hello world!'; str2 = str2.toUpperCase(); // str2进行大写转换之后创建了一个新的字符串来存放转换的结果,并将其赋给了str2 -
Symbol类型就代表着唯一性,不可变性与其核心特性直接相关。每个Symbol都是独一无二的,需要强调它不可变且不可重复创建。
-
-
按值比较: 基本数据类型的复制是直接将该变量的值复制一份,然后赋给新变量,它们之间的比较是直接比较值是否相等。只要值相等,两个变量就相等。
- 示例:
const a = 1; const b = a; console.log(a === b); // true const c = 1; console.log(a === c); // true
引用数据类型存储
引用数据类型的变量的值我们一般称为对象,我们说的引用数据类型实际存储在堆内存中,其实指的是对象的变量名和引用地址存储在栈内存中,对象的值存储在堆内存中(因为对象的值一般比较大)。堆内存中的值可以同时被多个变量指向。
你可以把这个行为想象成海盗把财宝藏在了加勒比海上的某个无名小岛上,为了不遗失这份隐秘的财宝,绘制了一张藏宝图用以指引海盗寻回宝藏。藏宝图就是栈内存中的变量名和引用地址,财宝就是堆内存中的实际对象,没有藏宝图的指引,财宝将迷失在茫茫加勒比海上,无从寻起。
示例:
const treasureMap1 = {
name: '宝藏1',
money: 123456789123333
};
const treasureMap2 = {
name: '宝藏2',
money: 12345678912444
}
const treasureMap3 = {
name: '宝藏3',
money: 123456789123555
}
-
可修改性: 由于对象的值实际存在于堆内存中,所以引用数据类型的可修改性,实际上指的是通过修改堆内存中的值达到修改对象内部元素的目的,而栈内存中的变量名和引用地址则无变化,并且对象还是那个对象,没有创建新对象。
- 示例:
const treasureMap3 = { name: '宝藏3', money: 123456789123555 }; console.log(treasureMap3); // { name: '宝藏3', money: 123456789123555 } treasureMap3.money = 100; // 宝藏被人偷走了很多,只剩100块 console.log(treasureMap3); // { name: '宝藏3', money: 100 } -
共享内存: 由于栈内存中存储的只是指向堆内存中具体位置的引用地址,所以栈内存中可以有多个变量同时指向同一个堆内存中的对象。就像一份宝藏可以有很多份藏宝图,藏宝图可以被复制多份,但是他们最后找到的宝藏都是同一个。同样,如果堆内存中的对象发生了修改,那么所有指向它的变量最终得到的值也都是修改后的。
-
多变量指向同一个对象
- 示例:
const treasureMap1 = { name: '宝藏1', money: 123456789123333 }; const treasureMap2 = { name: '宝藏2', money: 12345678912444 }; const treasureMap3 = treasureMap1; // 这里相当于把藏宝图1的内容复制了一份,赋给了藏宝图3 // 指引地址相同,最终找到的宝藏都是一样的 console.log(treasureMap1 === treasureMap3); // true console.log(treasureMap1); // { name: '宝藏1', money: 123456789123333 } console.log(treasureMap3); // { name: '宝藏1', money: 123456789123333 } -
对象内容修改后,多个变量指向得到的值也是同样发生变化
- 示例:
// 最终指向的对象发生变化,所有指向其的变量得到的值也是变化后的 treasureMap1.money = 0; // 目的地中宝藏失窃,海盗白忙活了 console.log(treasureMap1); // { name: '宝藏1', money: 0 } console.log(treasureMap3); // { name: '宝藏1', money: 0 }
-
-
按引用比较: 引用类型的复制属于是将栈内存中的引用地址复制了一份(浅拷贝),赋给新变量,它们之间的比较是通过引用地址来比较的。如果引用地址不一样,那么就算两个变量分别指向的对象内容完全一样,那么也不相等!
- 示例:
const obj1 = { name: '对象1' }; const obj2 = obj1; // 将引用地址赋给obj2,二者引用地址相同,指向同一个对象 console.log(obj1 === obj2); // true const obj3 = { name: '对象1' }; // obj3与obj1指向的对象内容完全相同 console.log(obj1 === obj3); // false,引用地址不同,判定为不相等
var、let、const 与堆栈的联系
相信大家学习了基本数据类型的不可变性和引用数据类型的可变性之后,会感到和我们 JS 中声明变量的三种方式 var、let、const 的特性很相似。之前我在我的上一篇博客《深入理解JS(一) - 提升与TDZ》中有详细讲过它们的特性,这里我就重提 var、let 声明的为变量可变,而 const 声明的为常量不可变,其余的和本节内容无关,感兴趣可以去我的上一篇博客看看,这里不多赘述。
栈内存
-
var、let
- 其声明的基本数据类型变量的值可变(可以创建一个新值,替换掉旧值)
- 其声明的引用数据类型变量的引用地址可变(让变量绑定一个新的引用地址,指向新的对象)
- 示例:
// 基本数据类型 var a = 1; let b = 2; // 重新赋值 a = 3; b = 4; console.log(a); // 3 console.log(b); // 4 // 引用数据类型 var c = { name: '变量1' } let d = { name: '变量2' } // 改变变量中的引用地址,重新绑定对象 c = {name: '变量3'}; d = {name: '变量4'}; console.log(c); // { name: '变量3' } console.log(d); // { name: '变量4' } -
const
- 其声明的基本数据类型常量不可变(禁止重新赋值)
- 其声明的引用数据类型常量的引用地址不可变(禁止常量重新绑定引用地址)
- 示例:
const c1 = 1; c1 = 2; // 报错:TypeError: Assignment to constant variable. 禁止重新赋值 const c2 = { name: '常量1' }; c2 = { name: '常量2' }; // 报错:TypeError: Assignment to constant variable. 禁止重新绑定对象
堆内存
const 的不可变只是针对于栈内存中存放的值或者是引用地址,对于堆内存,var、let、const 声明的引用数据类型变量或是常量,都可以修改其引用地址指向的对象内部状态。
- 示例:
var obj1 = {
name: '变量1'
}
let obj2 = {
name: '变量2'
}
const obj3 = {
name: '常量1'
}
obj1.name = '已改变';
obj2.name = '已改变';
obj3.name = '已改变';
console.log(obj1); // { name: '已改变' }
console.log(obj2); // { name: '已改变' }
console.log(obj3); // { name: '已改变' }
三. 结语
这篇博客写得很长,给每一个读到这里的朋友点赞,希望这篇博客能在 JS 学习方面帮助到你。你可能一次看不完,但我觉得你可以先挑你喜欢的部分看,把这篇博客当成工具书,有什么不懂的地方就可以来这看看。如果我有什么缺漏或是错误,请各位在评论区指出,感激不尽。