堆栈空间
js的数据主要存储在栈空间和堆空间中,下面来看看栈空间和堆空间的概念。
下面来看一段代码:
function fn() {
var a = "hello";
var b = a;
var c = { name: "CUGGZ"};
var d = c;
}
fn()
当这段代码执行时,需要先进行编译,并创建执行上下文,最后在按照顺序执行代码。当执行到第三行时,调用栈执行状态如下:
此时变量 a 和 b 的值都被保存在执行上下文中, 而执行上下文又被压入到了栈中,所以变量 a 和 b 的值都是存放在栈中 的。
接下来继续执行后面的代码。当执行到第四行代码时,JavaScript 引擎判断变量 c 的值是一个引用类型,这时JavaScript 引擎会将该对象分配到堆空间里,分配后该对象会有一个在堆中的地址,然后再将该数据的地址写进 c 的变量值,最终分配好内存的执行上下文如下:
可以看到,对象类型存储在堆空间中,在栈空间中只保留了对象的引用地址,当 JavaScript 访问该数据时,会通过栈中的引用地址来访问。
所以,基本数据类型的值直接保存在栈中,引用类型的值会存放在堆中。
那为什么要区分堆空间和栈空间呢?将数据都存在栈空间中不行吗?
答案肯定是不可以的。JavaScript 引擎需要使用栈来维护程序执行期间上下文的状态,如果将所有数据都放在栈空间中,就会影响到上下文切换的效率,进而影响到整个程序的执行效率。
所以,通常情况下,栈空间不会设置的很大,主要用来存放一些基本类型的小数据。由于引用类型的数据占用空间都比较大,所以这类数据会被存放到堆中,堆空间比较大,能存放很多较大的数据。
最后,我们再看看上面实例代码中第五行,也就是将变量 c 赋值给变量 d 是怎么执行的。在 JavaScript 中,原始类型的赋值会完整复制变量值,而引用类型的赋值是复制引用地址。 所以d = c 的操作就是把 c 的引用地址赋值给 d,如下图所示:
可以看到,变量 c 和 d 都指向了同一个堆中的对象,当我们修改c的值时,d也会发生变化。
储存方式
基础数据类型:
存储在栈(stack)中,占据空间小、大小固定,属于被频繁使用数据,所以放入栈中存储。
引用数据类型:
同时存储在栈(stack)和堆(heap)中,占据空间大、大小不固定。
栈中存储了指针,该指针指向堆中该实体的起始地址。
区别
基础数据类型的赋值会重新开辟一个内存空间,两个值互不干扰,完全独立。
let a = 1;
let b = a;
b = 2
console.log(a,b) // 1 2
引用数据类型 会复制一份引用地址给赋值的对象,它们共同指向了同一个堆内存放的对象,一个对象的值改变,另外一个也会跟着改变。如果一个对象重新赋值,则不会再影响另外一个对象。
let a = {name:"jimmy"}
let b = a;
b.name = "chimmy";
console.log(a) // {name:"chimmy"}
b = {name:"jeck"}
console.log(a) // {name:"chimmy"}
console.log(b) // {name:"jeck"}
let a = {name:'jimmy',age:22}
function change(obj){
obj.age = 24;
obj = {
name:'chimmy',
age:21
}
return obj;
}
let b = change(a);
console.log(b.age); // 21
console.log(a.age); // 24
函数传参进来的obj,传递的是obj在堆中的内存地址,通过调用 obj.age = 24,改变了 a 对象的 age 属性,但obj = { name:'chimmy', age:21 } 却又把obj 变成了另一个内存地址,将 { name:'chimmy', age:21 } 存入其中,最后返回 b 的值就变成了 { name:'chimmy', age:21 }。而a打印的结果为{name:'jimmy',age:24}
注意:对于引用类型的变量,==和===只会判断引用的地址是否相同,而不会判断对象具体的属性以及值是否相同。因此,如果两个变量指向相同的对象,则返回true。
所以{}==={},[]===[],[1,2] ===[1,2],{name:'jimmy'}==={name:"jimmy"} 都是false
如果想判断两个不同的对象或者数组是否真的相同,一个简单的方法就是将它们转换为字符串然后判断。
另一个方法就是递归地判断每一个属性的值,直到类型为基本类型,然后判断是否相同。
数据类型
js目前一共有8种数据类型 基础数据类型:number string null boolean undefined symbol bigint 引用数据类型:object
注意:
function 和 array 不是数据类型 广义上,他们属于对象。
NaN 也不是数据类型
typeof NaN // number
NaN === NaN // false
1. undefined
undefined 类型表示未定义,它的类型只有一个值,就是 undefined。任何变量在赋值前是 undefined 类型、值为 undefined,可以用全局变量 undefined 来表达这个值。可以通过以下方式来得到 undefined:
(1)声明了一个变量,但没有赋值
var foo; //undefined
(2)引用未定义的对象属性
var obj = {}
obj.b // undefined
(3)函数定义了形参,但没有传递实参
function fn(a) {
console.log(a); //undefined
}
fn();
(4)执行 void 表达式;
void 0 // undefined
推荐通过 void 表达式来得到 undefined 值,因为这种方式既简便又不需要引用额外的变量和属性;同时它作为表达式还可以配合三目运算符使用,代表不执行任何操作。
如下面的代码就表示满足条件 x 大于 0 且小于 5 的时候执行函数 fn,否则不进行任何操作:
x > 0 && x < 5 ? fn() : void 0;
那如何判断一个变量的值是否为 undefined 呢?可以通过 typeof 关键字获取变量 x 的类型,然后与 'undefined' 字符串做真值比较
if(typeof x === 'undefined') {
...
}
2. null
ull 数据类型和 undefined 类似,只有一个值 null,表示变量被置为空对象,而非一个变量最原始的状态。null 是 JavaScript 保留关键字,而 undefined 只是一个常量。也就是说可以声明名称为 undefined 的变量,但将 null 作为变量使用时则会报错。
对于null,还有一个比较关键的问题,来看代码:
typeof null == 'object' // true
实际上,null 有自己的类型 null,而不属于Object类型,typeof 之所以会判定为 Object 类型,是因为历史原因造成的。
3. boolean
Boolean 数据类型只有两个值:true 和 false,分别代表真和假。很多时候我们需要将各种表达式和变量转换成 boolean 数据类型来当作判断条件。
下面是将星期数转换成中文的函数,比如输入数字 1,函数就会返回“星期一”,输入数字 2 会返回“星期二”,以此类推,如果未输入数字则返回 undefined:
function getWeek(week) {
const dict = ['日', '一', '二', '三', '四', '五', '六'];
if(week) return `星期${dict[week]}`;
}
这里在 if 语句中会进行类型转换,将 week 变量转换成 boolean 数据类型,而 0、空字符串、null、undefined 在转换时都会返回 false。所以在输入 0 并不会返回“星期日”,而会返回 undefined。这是我们需要注意的问题。
4. string
string 用于表示字符串,string 有最大长度是 2的53次方 - 1,这个所谓的最大长度并不是指字符数,而是字符串的 UTF16 编码长度。 字符串的 charAt、charCodeAt、length 等方法针对的都是 UTF16 编码。所以,字符串的最大长度,实际上是受字符串的编码长度影响的。
JavaScript 中的字符串是永远无法变更的,一旦构造出来,就无法用任何方式改变其内容,所以字符串具有值类型的特征。
5. number
Number 类型表示数字。JavaScript 中的 Number 类型基本符合 IEEE 754-2008 规定的双精度浮点数规则,但是 JavaScript 为了表达几个额外的语言场景(比如为了不让除以 0 出错,而引入了无穷大的概念),规定了几个例外情况:
-
NaN,占用了 9007199254740990,这原本是符合 IEEE 规则的数字,通常在计算失败时会得到该值。要判断一个变量是否为 NaN,则可以通过 Number.isNaN 函数进行判断。
-
Infinity,无穷大,在某些场景下比较有用,比如通过数值来表示权重或者优先级,Infinity 可以表示最高优先级或最大权重。
-
-Infinity,无穷小。
注意,JavaScript 中有 +0
和 -0
的概念,在加法类运算中它们没有区别,但是除法时需要特别注意。可以使用 1/x 是 Infinity 还是 -Infinity来区分 +0 和 -0。
根据双精度浮点数的定义,Number 类型中有效的整数范围是 -0x1fffffffffffff
至 0x1fffffffffffff
,所以 Number 无法精确表示此范围外的整数。根据浮点数的定义,非整数的 Number 类型无法用 ==
或者 ===
来比较,这也就是在 JavaScript 中为什么 0.1+0.2 !== 0.3
。
出现这种情况的原因在于计算的时候,JavaScript 引擎会先将十进制数转换为二进制,然后进行加法运算,再将所得结果转换为十进制。在进制转换过程中如果小数位是无限的,就会出现误差。
6. Symbol
Symbol 是 ES6 中引入的新数据类型,它表示一个唯一的常量,通过 Symbol 函数来创建对应的数据类型,创建时可以添加变量描述,该变量描述在传入时会被强行转换成字符串进行存储:
var a = Symbol('1')
var b = Symbol(1)
a.description === b.description // true
var c = Symbol({id: 1})
c.description // [object Object]
var d = Symbol('1')
d == a // false
基于以上特性,Symbol 属性类型比较适合用于两类场景中:常量值和对象属性。
(1)避免常量值重复
getValue 函数会根据传入字符串参数 key 执行对应代码逻辑:
function getValue(key) {
switch(key){
case 'A':
...
case 'B':
...
}
}
getValue('B');
这段代码对调用者而言非常不友好,因为代码中使用了魔术字符串(Magic string,指的是在代码之中多次出现、与代码形成强耦合的某一个具体的字符串或者数值),导致调用 getValue 函数时需要查看函数代码才能找到参数 key 的可选值。所以可以将参数 key 的值以常量的方式声明:
const KEY = {
alibaba: 'A',
baidu: 'B',
}
function getValue(key) {
switch(key){
case KEY.alibaba:
...
case KEY.baidu:
...
}
}
getValue(KEY.baidu);
但这样也并非完美,假设现在要在 KEY 常量中加入一个 key,根据对应的规则,很有可能会出现值重复的情况:
const KEY = {
alibaba: 'A',
baidu: 'B',
tencent: 'B'
}
这就会出现问题:
getValue(KEY.baidu) // 等同于 getValue(KEY.tencent)
所以在这种场景下更适合使用 Symbol,不需要关心值本身,只关心值的唯一性:
const KEY = {
alibaba: Symbol(),
baidu: Symbol(),
tencent: Symbol()
}
(2)避免对象属性覆盖
函数 fn 需要对传入的对象参数添加一个临时属性 user,但可能该对象参数中已经有这个属性了,如果直接赋值就会覆盖之前的值。此时就可以使用 Symbol 来避免这个问题。创建一个 Symbol 数据类型的变量,然后将该变量作为对象参数的属性进行赋值和读取,这样就能避免覆盖的情况:
function fn(o) { // {user: {id: xx, name: yy}}
const s = Symbol()
o[s] = 'zzz'
}
7. BigInt
BigInt 可以表示任意大的整数。其语法如下:
BigInt(value);
其中 value 是创建对象的数值。可以是字符串或者整数。
在 JavaScript 中,Number 基本类型可以精确表示的最大整数是2的53次方。因此早期会有这样的问题:
let max = Number.MAX_SAFE_INTEGER; // 最大安全整数
let max1 = max + 1
let max2 = max + 2
max1 === max2 // true
有了BigInt之后,这个问题就不复存在了:
let max = BigInt(Number.MAX_SAFE_INTEGER);
let max1 = max + 1n
let max2 = max + 2n
max1 === max2 // false
注意,BigInt 和 Number 不是严格相等的,但是宽松相等:
10n === 10 // false
10n == 10 // true
Number 和 BigInt 可以进行比较:
1n < 2; // true
2n > 1; // true
2 > 2; // false
2n > 2; // false
2n >= 2; // true
8. Object
Object 是 JavaScript 中最复杂的类型,它表示对象。在 JavaScript 中,对象的定义是属性的集合。简单地说,Object 类型数据就是键值对的集合,键是一个字符串(或者 Symbol) ,值可以是任意类型的值; 复杂地说,Object 又包括很多子类型,比如 Date、Array、Set、RegExp。
其实,JavaScript的几个基本数据类型在对象类型中都有一个对应的类:
-
Number;
-
String;
-
Boolean;
-
Symbol。
对于 Number 类,1 与 new Number(1) 是完全不同的值,一个是 Number 类型, 一个是对象类型。Number、String 和 Boolean 构造器是两用的:当跟 new 搭配时,它们产生对象;当直接调用时,它们表示强制类型转换。Symbol 函数比较特殊,直接用 new 调用它会抛出错误,但它仍然是 Symbol 对象的构造器。
数据类型检测
typeof
基础数据类型
typeof "string" // string
typeof 1 // number
typeof undefined // undefined
typeof true // boolean
typeof Symbol() // symbol
typeof null // object
基础数据类型,typeof 只有 null 的判定会有误差。
注意点:
typeof NaN // number
typeof Number(1) // number; // Number会尝试把参数解析成数值
NaN是一个比较特殊的值,它不等于自身 NaN === NaN 或者 NaN == NaN 都为fasle
要想准确判断是否为NaN,
1.可以使用 Object.is(NaN,NaN) 来判断
2.可以利用NaN不等于自身这一特性来判断
function selfIsNaN(value){
return value !== value
}
3.ES6 在 Number 对象上提供的isNaN()方法
Number.isNaN(NaN); // true 本身是NaN
引用数据类型
typeof {} // 'object'
typeof [] // 'object'
typeof(() => {}) // 'function'
typeof 操作符会对对象类型及其子类型,譬如函数(可调用对象)、数组(有序索引对象)等进行判定,除了函数其他都会得到 object 的结果。
综上:使用typeof 检测数据类型无法准确识别null 数组 对象等,有缺陷。
instanceof
通过 instanceof 操作符可以对对象类型进行判定,其原理就是测试构造函数的 prototype 是否出现在被检测对象的原型链上。
[] instanceof Array // true
({}) instanceof Object // true
(()=>{}) instanceof Function // true
instanceof只能准确判断出数组和方法,不能准确判断出对象
let arr = [];
let fuc = () => {};
arr instanceof Object // true
fuc instanceof Object // true
Array和Function属于Object子类型,所以Object 构造函数在 arr和fuc的原型链上。所以
arr instanceof Object // true
fuc instanceof Object // true
Object.prototype.toString
toString() 是 Object 的原型方法,调用该方法,可以统一返回格式为 “[object Xxx]” 的字符串,其中 Xxx 就是对象的类型。对于 Object 对象,直接调用 toString() 就能返回 [object Object];而对于其他对象,则需要通过 call 来调用,才能返回正确的类型信息。
PS:调用call 是因为有些数据类型没有toString的方法,使用call,可以改变this的指向。相当于借用了Object.prototype.toString这个方法。关于call,可以参考我的另外一篇文章new,call,apply,bind知多少
Object.prototype.toString({}) // "[object Object]"
Object.prototype.toString.call({}) // 同上结果,加上call也ok
Object.prototype.toString.call(1) // "[object Number]"
Object.prototype.toString.call('1') // "[object String]"
Object.prototype.toString.call(true) // "[object Boolean]"
Object.prototype.toString.call(function(){}) // "[object Function]"
Object.prototype.toString.call(null) //"[object Null]"
Object.prototype.toString.call(undefined) //"[object Undefined]"
Object.prototype.toString.call(/123/g) //"[object RegExp]"
Object.prototype.toString.call(new Date()) //"[object Date]"
Object.prototype.toString.call([]) //"[object Array]"
Object.prototype.toString.call(document) //"[object HTMLDocument]"
Object.prototype.toString.call(window) //"[object Window]"
Object.prototype.toString.call() 可以准确的判断出数据类型,甚至可以把 document 和 window 都区分开来。
实现一个通用的判断数据类型的方法
function getType(type){
if(typeof type !== "object"){
return typeof type;
}
return Object.prototype.toString.call(type).slice(8, -1).toLowerCase();
}
数据类型转换
强制类型转换
转为字符串
String()
String(null) // "null"
String(undefined) // "undefined"
String(true) // "true"
String([]) // " "
String([1,2]) // "1,2"
String({}) // “[object Object]”
String(function(){ }) // “function (){}”
转为数字
Number()
转换规则
- 如果是布尔值,true 和 false 分别被转换为 1 和 0;
- 如果是数字,返回自身;
- 如果是 null,返回 0;
- 如果是 undefined,返回 NaN;
- 如果是字符串,遵循以下规则:如果字符串中只包含数字(或者是 0X / 0x 开头的十六进制数字字符串,允许包含正负号),则将其转换为十进制;如果字符串中包含有效的浮点格式,将其转换为浮点数值;如果是空字符串,将其转换为 0;如果不是以上格式的字符串,均返回 NaN;
6.如果是 Symbol,抛出错误;
7.如果是对象,调用对象的 valueOf() 方法,然后依据前面的规则转换返回的值;如果转换的结果是 NaN ,则调用对象的 toString() 方法,再次依照前面的顺序转换返回对应的值。
第七点简单的规则也可以这样理解,Number方法的参数是对象时,将返回NaN,除非是包含单个数值的数组或者空数组。
Number函数将字符串转为数值,要比parseInt函数严格很多。基本上,只要有一个字符无法转成数值,整个字符串就会被转为NaN。
Number(true); // 1
Number(false); // 0
Number([]); // 0
Number({}); // NaN
Number('0111'); // 111
Number(null); // 0
Number(''); // 0
Number('1a'); // NaN
Number(-0X11); // -17
Number('0X11') // 17
parseInt()和parsefloat()
首先查看位置 0 处的字符,判断它是否是个有效数字;如果不是,该方法将返回 NaN(not a number),不再继续执行其他操作。但如果该字符是有效数字,该方法将查看位置 1 处的字符,进行同样的测试。这一过程将持续到发现非有效数字的字符为止,然后把该字符之前的有效数字字符串转换成数字。如下图:
转换为布尔值
Boolean() 除了 undefined、 null、 false、 ''、 0(包括 +0,-0)、 NaN 转换出来是 false,其他都是 true。
隐式类型转换
通过逻辑运算符 (&&、 ||、 !)、运算符 (+、-、*、/)、关系操作符 (>、 <、 <= 、>=)、相等运算符 (==) 或者 if/while 条件的操作,如果遇到两个数据类型不一样的情况,都会出现隐式类型转换。
!!
会隐式转化为boolean 进行比较
'==' 的隐式类型转换规则
- 规则1:如果其中一个操作值是 null 或者 undefined,那么另一个操作符必须为 null 或者 undefined,才会返回 true,否则都返回 false
- 规则2:两个操作值如果为 string 和 number 类型,那么就会将字符串转换为 number
- 规则3:如果一个操作值是 boolean,那么转换成 number
- 规则4:如果其中一个是 Symbol 类型,那么返回 false;
- 规则5:如果一个操作值为 object 且另一方为 string、number 或者 symbol,就会把 object 转为原始类型再进行判断(调用 object 的 valueOf/toString 方法进行转换)。
null == undefined // true 规则1
null == 0 // false 规则1
null == "" // false 规则1
'123' == 123 // 规则2
""== 0 0==false 1==true // 都为true // 规则3
var a = {
value: 0,
valueOf: function() {
this.value++;
return this.value;
}
};
// 注意这里a又可以等于1、2、3
console.log(a == 1 && a == 2 && a ==3); //true Object隐式转换 规则5
实际开发中,用 === 代替 == 判断是否相等
'+' 的隐式类型转换规则
'+' 号操作符,不仅可以用作数字相加,还可以用作字符串拼接。仅当 '+' 号两边都是数字时,进行的是加法运算;如果两边都是字符串,则直接拼接,无须进行隐式类型转换。
- 如果其中有一个是字符串,另外一个是 undefined、null 或布尔型,则调用 toString() 方法进行字符串拼接;如果是纯对象、数组、正则等,则默认调用对象的转换方法会存在优先级,然后再进行拼接。
- 如果其中有一个是数字,另外一个是 undefined、null、布尔型或数字,则会将其转换成数字进行加法运算
- 如果其中一个是字符串、一个是数字,则按照字符串规则进行拼接。
1 + 2 // 3 常规情况
'1' + '2' // '12' 常规情况
// 下面看一下特殊情况
'1' + undefined // "1undefined" 规则1,undefined转换字符串
'1' + null // "1null" 规则1,null转换字符串
'1' + true // "1true" 规则1,true转换字符串
'1' + 1n // '11' 比较特殊字符串和BigInt相加,BigInt转换为字符串
1 + undefined // NaN 规则2,undefined转换数字相加NaN
1 + null // 1 规则2,null转换为0
1 + true // 2 规则2,true转换为1,二者相加为2
1 + 1n // 错误 不能把BigInt和Number类型直接混合相加
'1' + 3 // '13' 规则3,字符串拼接
Object 的转换规则
首先会调用原型链上的valueOf()方法,如果已经转换为基础类型,则返回,如果还不是基础类型,继续调用toString()方法,如果转换为基础类型,则返回;如果还不是基础类型,则抛出错误。
下面这个例子,重写了obj对象的valueOf和toString方法
var obj = {
value: 1,
valueOf() {
return 2;
},
toString() {
return '3'
},
}
console.log(obj + 1); // 输出3
10 + {} 结果为 "10[object Object]"
{}会默认调用valueOf是转化为{},不是基础类型,则继续转换,调用toString,返回结果"[object Object]",于是和10进行'+'运算,按照字符串拼接规则来,所以为"10[object Object]"
valueOf 和 toString 都是obj对象原型上的方法,关于原型可以参考我的这篇文章 js原型原型链知多少
关于Object这个构造函数有那些方法,以及详细的介绍。可以参考我的这篇文章 Object
自测是否已经完全掌握转换规则
1.'123' == 123 // false or true?
2.'' == null // false or true?
3.'' == 0 // false or true?
4.[] == 0 // false or true?
5.[] == '' // false or true?
6.[] == ![] // false or true?
7.null == undefined // false or true?
8.Number(null) // 返回什么?
9.Number('') // 返回什么?
10.parseInt(''); // 返回什么?
11.{}+10 // 返回什么?
答案:
1 true
2 false
3 true
4 true
5 true
6 true
7 true
8 0
9 0
10 NaN
11 10[object Object]
FAQ:
为什么[] == 0,[]=="" 为true
当判断中,发现有一边不是原始值类型,就会先调用valueOf方法进行转换
[].valueOf() // []
发现valueOf转化完后,依然不是原始值类型,那继续用toString方法转换
[].toString() // ""
转换完毕,发现转成了原始值类型""
所以 []=="" 为true
"" == 0 判断时,会继续用Number进行转换判断
Number("")==0
所以[]==0 为true
为什么[] == ![]结果为true
根据运算符优先级 ,! 的优先级是大于 == 的,所以先会执行 ![]
!可将变量转换成boolean类型,null、undefined、NaN以及空字符串('')取反都为true,其余都为false
所以 ! [] 运算后的结果就是 false
也就是 [] == ! [] 相当于 [] == false
根据'==' 的隐式类型转换规则第三条
根据上面提到的规则(如果有一个操作数是布尔值,则在比较相等性之前先将其转换为数值——false转换为0,而true转换为1),则需要把 false 转成 0
也就是 [] == ! [] 相当于 [] == false 相当于 [] == 0
第一个例子解释了 []==0 为什么 为true
实现 a==1&&a==2&&a==3 成立
// 对象实现
let a = {
value: 1,
// toString() {
// return this.value++;
// },
// 或者
valueOf() {
return this.value++;
},
};
// 数组实现
let a = [1, 2, 3];
a.toString = a.shift;
// 函数实现
let a = (function () {
let i = 1;
return {
toString() {
return i++;
},
};
})();
// defineProperty 或者代理实现
Reflect.defineProperty(window, "a", {
get() {
this.val ? this.val : (this.val = 1);
return this.val++;
},
});
console.log(a == 1 && a == 2 && a == 3);