You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
这里给 person 定义了一个 Symbol 作为属性键的属性,这个相比于用字符串作为属性键有啥好处呢?最明显的好处就是如果这个 person 对象是多个开发者进行开发维护,那么很容易再给 person 添加属性的时候出现同名的,如果是用字符串作为属性键那肯定是冲突了,但是如果用 Symbol 作为属性键,就不会存在这个问题了,因为它是唯一标识符,所以可以使对象的属性受到保护,不会被意外的访问或者重写。
注意一点,如果用 Symbol 作为对象的属性键的时候,for in 、Object.getOwnPropertyNames、或 Object.keys() 这里循环是无法获取 Symbol 属性键的,但是可以通过 Object.getOwnPropertySymbols() 来获取;在上面的代码基础上:
JavaScript中有哪些数据类型?
计算机世界中定义的数据类型其实就是为了描述现实世界中存在的事实而定义的。比如我们用人来举例:
有没有人在房间里?这里的有和没有就是是或者非的概念,在
JS
中对应Boolean
类型,true
表示是,false
表示非;有几个人在房间里?这里的几个表示的是一个量级概念,在
JS
中对应Number
类型,包含整数和浮点数,还有一些特殊的值,比如:-Infinity
表示负无穷大、+Infinity
表示正无穷大、NaN
表示不是一个数字;房间里的这些人都是我的朋友。这是一句陈述语句,这种文本类的信息将会以字符串形式进行存储,在
JS
中对应String
类型;房间里没有人。这里的没有代表无和空的概念,在
JS
中null
和undefined
都可以表示这个意思;现实世界中所有人都是独一无二的,这在
JS
中对应Symbol
类型,表示唯一且不可改变;Number
所表示的整数是有范围的,超出范围的数据就没法用Number
表示了,于是ES10
中提出了一种新的数据类型BigInt
,能表示任何位数的整数;以上提到的
Boolean
、Number
、String
、null
、undefined
、Symbol
和BigInt
等7种类型都是JavaScript
中的原始类型,还有一种是非原始类型叫做对象类型;比如:一个人是对象,这个人有名字、性别、年龄等;为什么要区分原始类型和对象类型?他们之间有什么区别?
原始类型的不可变性
在回答这个问题之前,我们先看一下变量在内存中是如何存储的:
执行完上面这段代码,我们发现变量
name1
的值还是不变,依然是bubuzou
。这就说明了字符串的不可变性。但是你看了下面的这段代码,你就会产生疑问了:你说字符串是不可变的,那现在不是变了嘛?
其实这只是变量的值变了,但是存在内存中的字符串依然不变。这就涉及到变量在内存中的存储了。
在
JavaScript
中,变量在内存中有2种存储方式:存在栈中和存在堆中。那么栈内存和堆内存有啥区别呢?栈内存:
堆内存:
了解完变量在内存中的存储方式有2种,那我们继续以上面那串代码为例,画出变量的存储结构图:
然后我们可以描述下当计算机执行这段代码时候的发生了什么?首先定义了一个变量
name1
并且给其赋值bubuzou
这个时候就会在内存中开辟一块空间用来存储字符串bubuzou
,然后变量指向了这个内存空间。然后再执行第二行代码let name2 = name1.concat('.com')
这里的拼接操作其实是产生了一个新字符串bubuzou.com
,所以又会为这个新字符串创建一块新内存,并且把定义的变量name2
指向这个内存地址。 所以我们看到其实整个操作bubuzou
这个字符串所在的内存其实是没有变化的,即使在第二段代码中执行了name1 += '.com'
操作,其实也只是变量name1
指向了新的字符串bubuzou.com
而已,旧的字符串bubuzou
依然存在内存中,不过一段时间后由于该字符串没有被变量所引用,所以会被当成垃圾进行回收,从而释放掉该块内存空间。从而我们得出结论:原始类型的值都是固定的,而对象类型则是由原始类型的键值对组合成一个复杂的对象;他们在内存中的存储方式是不一样的,原始类型的值直接存在栈内存中,而对象类型的实际值是存在堆内存中的,在栈内存中保存了一份引用地址,这个地址指向堆内存中的实际值,所以对象类型又习惯被叫做引用类型。
比较
当我们对两个变量进行比较的时候,不同类型的变量是有不同表现的:
我们定义了2个字符串变量和2个对象变量,他们都长一模一样,但是字符串变量会相等,对象变量却不相等。这是因为在
JavaScript
中,原型类型进行比较的时候比较的是存在栈中的值是否相等;而引用类型进行比较的时候,是比较栈内存中的引用地址是否相等。如上几个变量在内存中的存储模型如图所示:
复制
变量进行复制的时候,原始类型和引用类型变量也是有区别的,来看下面的代码:
1、
let str1 = 'hello'
: 复制前,定义了一个变量str1
,并且给其赋值hello
,这个时候hello
这个字符串就会在栈内存中被分配一块空间进行存储,然后变量str1
会指向这个内存地址;2、
let str2 = str1
:复制后,把str1
的值赋值给str2
,这个时候会在栈中新开辟一块空间用来存储str2
的值;3、
str2 = 'world'
:给str2
赋值了一个新的字符串world
,那么将新建一块内存用来存储world
,同时str2
原来的值hello
的内存空间因为没有变量所引用,所以一段时间后建被当成垃圾回收;4、
console.log( str1 )
:因为str1
和str2
的栈内存地址是不一样的,所以即使str2
的值被改变,也不会影响到str1
。然后我们继续往下,看下引用类型的复制:
原始类型进行复制的时候是变量的值进行重新赋值,而如上图所示:引用类型进行复制的时候是把变量所指向的引用地址进行赋值给新的变量,所以复制后
person1
和person2
都指向堆内存中的同一个值,所以当改变person2.name
的时候,person1.name
也会被改变就是这个原因。值传递和引用传递
先说一下结论,在
JavaScript
中,所有函数的参数传递都是按值进行传递的。看如下代码:定义了一个变量
name
,并赋值为bubuzou
,函数调用的时候传入name
,这个时候会在函数内部创建一个局部变量name
并且把全局变量的值bubuzou
传递给他,这个操作其实是在内存里新建了一块空间用来存放局部变量的值,然后又把局部变量的值改成了bubuzou.com
,这个时候其实内存中会有3块地址空间分别用来存放全局变量的值bubuzou
、局部变量原来的值bubuzou
、和局部变量新的值bubuzou.com
;一旦函数调用结束,局部变量将被销毁,一段时间后由于局部变量新旧值没有变量引用,那这两块空间将被回收释放;所以这个时候全局name
的值依然是bubuzou
。再来看看引用类型的传参,会不会有所不同呢?
引用类型进行函数传参的时候,会把引用地址复制给局部变量,所以全局的
person
和函数内部的局部变量person
是指向同一个堆地址的,所以一旦一方改变,另一方也将被改变,所以至此我们是不是可以下结论说:当函数进行传参的时候如果参数是引用类型那么就是引用传递嘛?将上面的例子改造下:
如果
person
是引用传递的话,那就会自动指向值被改为hello world
的新对象;事实上全局变量person
的引用地址自始至终都没有改变,倒是局部变量person
的引用地址发生了改变。null 和 undefined 傻傻分不清?
null
在JavaScript
中自成一种原始类型,只有一个值null
,表示无、空、值未知等特殊值。可以直接给一个变量赋值为null
:undefined
和null
一样也是自成一种原始类型,表示定义了一个变量,但是没有赋值,则这个变量的值就是undefined
:虽然可以给变量直接赋值为
undefined
也不会报错,但是原则上如果一个变量值未定,或者表示空,则直接赋值为null
比较合适,不建议给变量赋值undefined
。null
和undefined
在进行逻辑判断的时候都是会返回false
的:null
在转成数字类型的时候会变成0
,而undefined
会变成NaN
:认识新的原始类型 Symbol
Symbol
值表示唯一标识符,是ES6
中新引进的一种原始类型。可以通过Symbol()
来创建一个重要的值,也可以传入描述值;其唯一性体现在即使是传入一样的描述,他们两者之间也是不会相等的:全局的 Symbol
那还是不是任意2个描述一样的
Symbol
都是不相等的呢?答案是否定的。可以通过Symbol.for()
来查找或新建一个Symbol
:使用
Symbol.for()
可以在根据传入的描述在全局范围内进行查找,如果没找到则新建一个Symbol
,并且返回;所以当执行第二行代码Symbol.for('bubuzou')
的时候,就会找到全局的那个描述为bubuzou
的Symbol
,所以这里a
和b
是会绝对相等的。居然可以通过描述找到
Symbol
, 那是否可以通过Symbol
来找到描述呢?答案是肯定的,但是必须是全局的Symbol
,如果没找到则会返回undefined
:但是对于任何一个
Symbol
都有一个属性description
,表示这个Symbol
的描述:Symbol 作为对象属性
我们知道对象的属性键可以是字符串,但是不能是
Number
或者Boolean
;Symbol
被设计出来其实最大的初衷就是用于对象的属性键:这里给
person
定义了一个Symbol
作为属性键的属性,这个相比于用字符串作为属性键有啥好处呢?最明显的好处就是如果这个person
对象是多个开发者进行开发维护,那么很容易再给person
添加属性的时候出现同名的,如果是用字符串作为属性键那肯定是冲突了,但是如果用Symbol
作为属性键,就不会存在这个问题了,因为它是唯一标识符,所以可以使对象的属性受到保护,不会被意外的访问或者重写。注意一点,如果用
Symbol
作为对象的属性键的时候,for in
、Object.getOwnPropertyNames
、或Object.keys()
这里循环是无法获取Symbol
属性键的,但是可以通过Object.getOwnPropertySymbols()
来获取;在上面的代码基础上:你可能不知道的 Number 类型
JavaScript
中的数字涉及到了两种类型:一种是Number
类型,以64
位的格式IEEE-754
存储,也被称为双精度浮点数,就是我们平常使用的数字,其范围是BigInt
,能够表示任意长度的整数,包括超出Number
数字。常规数字和特殊数字
对于一个常规的数字,我们直接写即可,比如:
但是还有一种位数特别多的数字我们习惯用科学计数法的表示方法来写:
以上两种写法是一个意思,$10^9$ ;如果是 $10^3$ = 0.001。
1e9
表示 1 x1e-3
表示 1 /在
JavaScript
中也可以用数字表示不同的进制,比如:十进制中的10
在 二、八和十六进制中可以分别表示成0b1010
、0o12
和0xa
;其中的0b
是二进制前缀,0o
是八进制前缀,而ox
是十六进制的前缀。我们也可以通过
toString(base)
方法来进行进制之间的转换,base
是进制的基数,表示几进制,默认是10
进制的,会返回一个转换数值的字符串表示。比如:Number
类型除了常规数字之外,还包含了一些特殊的数字:NaN
:表示不是一个数字,通常是由不合理的计算导致的结果,比如数字除以字符串1 / 'a'
;NaN
和任何数进行比较都是返回false
,包括他自己:NaN == NaN
会返回false
;如何判断一个数是不是
NaN
呢?有四种方法:方法一:通过
isNaN()
函数,这个方法会对传入的字符串也返回true
,所以判断不准确,不推荐使用:方法二:通过
Number.isNaN()
,推荐使用:方法三:通过
Object.is(a, isNaN)
:方法四:通过判断
n !== n
,返回true
, 则n
是NaN
:+Infinity
:表示正无穷大,比如1/0
计算的结果,-Infinity
表示负无穷大,比如-1/0
的结果。+0
和-0
,JavaScript
中的数字都有正负之分,包括零也是这样,他们会绝对相等:为什么 0.1 + 0.2 不等于 0.3
有没有想过为什么上面的会不相等?因为数字在
JavaScript
内部是用二进制进行存储的,其遵循IEEE 754
标准的,用64
位来存储一个数字,64
位又被分隔成1
、11
和52
位来分别表示符号位、指数位和尾数位。比如十进制的
0.1
转成二进制后是多少?我们手动计算一下,十进制小数转二进制小数的规则是“乘2取整,顺序排列”,具体做法是:用2乘十进制小数,可以得到积,将积的整数部分取出,再用2乘余下的小数 部分,又得到一个积,再将积的整数部分取出,如此进行,直到积中的小数部分为零,或者达到所要求的精度为止。我们这样依次计算下去之后发现得到整数的顺序排列是$2^{-4}$ ,但是由于
$2^{-4}$ 。
0001100110011001100....
无限循环,所以理论上十进制的0.1
转成二进制后会是一个无限小数0.0001100110011001100...
,用科学计数法表示后将是1.100110011001100...
xIEEE 754
标准规定了一个数字的存储位数只能是64
位,有效位数是52
位,所以将会对1100110011001100....
这个无限数字进行舍入总共52
位作为有效位,然后二进制的末尾取舍规则是看后一位数如果是1
则进位,如果是0
则直接舍去。那么由于1100110011001100....
这串数字的第53
位刚好是1
,所以最终的会得到的数字是1100110011001100110011001100110011001100110011001101
,即1.100110011001100110011001100110011001100110011001101
x十进制转二进制也可以用
toString
来进行转化:我们发现十进制的
0.1
在转化成二进制小数的时候发生了精度的丢失,由于进位,它比真实的值更大了。而0.2
其实也有这样的问题,也会发生精度的丢失,所以实际上0.1 + 0.2
不会等于0.3
:那是不是没办法判断两个小数是否相等了呢?答案肯定是否定的,想要判断2个小数
n1
和n2
是否相等可以如下操作:方法一:两小数之差的绝对值如果比
Number.EPSILON
还小,那么说明两数是相等的。Number.EPSILON
是ES6
中的误差精度,实际值可以认为等于方法二:通过
toFixed(n)
对结果进行舍入,toFixed()
将会返回字符串,我们可以用 一元加+
将其转成数字:数值的转化
对数字进行操作的时候将常常遇到数值的舍入和字符串转数字的问题,这里我们巩固下基础。先来看舍入的:
Math.floor()
,向下舍入,得到一个整数:Math.ceil()
,向上舍入,得到一个整数:Math.round()
,对第一位小数进行四舍五入:Number.prototype.toFixed(n)
,和Math.round()
一样会进行四舍五入,将数字舍入到小数点后n
位,并且以字符串的形式返回:为什么
6.35.toFixed(1)
会等于6.3
?因为6.35
其实是一个无限小数:所以在
6.35.toFixed(1)
求值的时候会得到6.3
。再来看看字符串转数字的情况:
Number(n)
或+n
,直接将n
进行严格转化:parseInt()
,非严格转化,从左到右解析字符串,遇到非数字就停止解析,并且把解析的数字返回:parseInt()
默认是用十进制去解析字符串的,其实他是支持传入第二个参数的,表示要以多少进制的 基数去解析第一个参数:如何判断一个数是不是整数?介绍两种方法:
方法一:通过
Number.isInteger()
:方法二:
typeof num == 'number' && num % 1 == 0
引用类型
除了原始类型外,还有一个特别重要的类型:引用类型。高程里这样描述他:引用类型是一种数据结构, 用于将数据和功能组织在一起。到目前为止,我们看到最多的引用类型就是
Object
,创建一个Object
有两种方式:方式一:通过
new
操作符:方式二:通过对象字面量,这是我们最喜欢用的方式:
内置的引用类型
除了
Object
外,在JavaScript
中还有别的内置的引用类型,比如:Array
数组Date
日期RegExp
正则表达式Function
函数他们的原型链的顶端都会指向
Object
:包装类型
先来看一个问题,为什么原始类型的变量没有属性和方法,但是却能够调用方法呢?
因为
JavaScript
为了更好地操作原始类型,设计出了几个对应的包装类型,他们分别是:Boolean
Number
String
上面那串代码的执行过程其实是这样的:
用代码体现一下:
原始类型调用函数其实就是自动进行了装箱操作,将原始类型转成了包装类型,然后其实原始类型和包装类型是有本质区别的,原始类型是原始值,而包装类型是对象实例:
居然有装箱操作,那肯定也有拆箱操作,所谓的拆箱就是包装类型转成原始类型的过程,又叫
ToPromitive
,来看下面的例子:在拆箱操作的时候,默认会尝试调用包装类型的
toString()
和valueOf()
方法,对于不同的hint
调用顺序会有所区别,如果hint
是string
则优先调用toString()
,否则的话,则优先调用valueOf()
。默认情况下,一个
Object
对象具有toString()
和valueOf()
方法:类型装换
Javascript
是弱类型的语音,所以对变量进行操作的时候经常会发生类型的转换,尤其是隐式类型转换,可能会让代码执行结果出乎意料之外,比如如下的代码你能理解其执行结果嘛?类型转换规则
所以我们需要知道类型转换的规则,以下整理出一个表格,列出了常见值和类型以及转换之后的结果,仅供参考。
显示类型转换
我们平时写代码的时候应该尽量让写出来的代码通俗易懂,让别人能阅读后知道你是要做什么,所以在对类型进行判断的时候应该尽量显示的处理。
比如将字符串转成数字,可以这样:
将数字显示转成字符串可以这样:
显示转成布尔类型可以这样:
除了以上之外,还有一些关于类型转换的冷门操作,有时候也挺管用的:
直接用一元加操作符获取当前时间的毫秒数:
用
~
配合indexOf()
将操作结果直接转成布尔类型:使用
~~
对字符或数字截取整数,和Math.floor()
有稍许不同:隐式类型转换
隐式类型转换发生在
JavaScript
的运行时,通常是由某些操作符或语句引起的,有下面这几种情况:隐式转成布尔类型:
if (..)
语句中的条件判断表达式。for ( .. ; .. ; .. )
语句中的条件判断表达式(第二个)。while (..)
和do..while(..)
循环中的条件判断表达式。? :
中的条件判断表达式。||
(逻辑或)和&&
(逻辑与)左边的操作数(作为条件判断表达式)上例中的非布尔值会被隐式强制类型转换为布尔值以便执行条件判断。
需要特别注意的是
||
和&&
操作符。||
的操作过程是只有当左边的值返回false
的时候才会对右边进行求值且将它作为最后结果返回,类似a ? a : b
这种效果:而
&&
的操作过程是只有当左边的值返回true
的时候才对右边进行求值且将右边的值作为结果返回,类似a ? b : a
这种效果:数学操作符
- * /
会对非数字类型的会优先转成数字类型,但是对+
操作符会比较特殊:String
类型,被识别为字符串拼接,并会优先将另一侧转换为字符串类型。Number
类型,另一侧为原始类型,则将原始类型转换为Number
类型。Number
类型,另一侧为引用类型,将引用类型和Number
类型转换成字符串后拼接。宽松相等和严格相等
宽松相等(
==
)和严格相等(===
)在面试的时候经常会被问到,而回答一般是==
是判断值是否相等,而===
除了判断值会不会相等之外还会判断类型是否相等,这个答案不完全正确,更好的回答是:==
在比较过程中允许发生隐式类型转换,而===
不会。 那==
是怎么进行类型转换的呢?数字和字符串比,字符串将转成数字进行比较:
别的类型和布尔类型比较,布尔类型将首先转成数字进行比较,
true
转成数字1
,false
转成数字0
,注意这个是非常容易出错的一个点:所以写代码进行判断的时候一定不要写成
x == true
或x == false
这种,而应该直接if (x)
判断。null
和undefined
:null == undefined
比较结果是true
,除此之外,null
、undefined
和其他任何结果的比较值都为false
。可以认为在==
的情况下,null
和undefined
可以相互的进行隐式类型转换。原始类型和引用类型比较,引用类型会首先进行
ToPromitive
转成原始类型然后进行比较,规则参考上面介绍的拆箱操作:特殊的值
类型检测
用typeof检测原始类型
JavaScript
中有null
、undefined
、boolean
、number
、string
、Symbol
等六种原始类型,我们可以用typeof
来判断值是什么原始类型的,会返回类型的字符串表示:但是原始类型中有一个例外,
typeof null
会得到 'object',所以我们用typeof
对原始值进行类型判断的时候不能得到一个准确的答案,那如何判断一个值是不是null
类型的呢?typeof
能够对原始类型进行判断,那是否也能判断引用类型呢?从上面的结果我们可以得到这样一个结论:
typeof
对引用类型判断的时候只有function
类型可以正确判断,其他都无法正确判断具体是什么引用类型。用instanceof检测引用类型
我们知道
typeof
只能对部分原始类型进行检测,对引用类型毫无办法。JavaScript
提供了一个操作符instanceof
,我们来看下他是否能检测引用类型:我们发现数组即是
Array
的实例,也是Object
的实例,因为所以引用类型原型链的终点都是Object
,所以Array
自然是Object
的实例。那么我们得出结论:instanceof
用于检测引用类型好像也不是很靠谱的选择。用toString进行类型检测
我们可以使用
Object.prototype.toString.call()
来检测任何变量值的类型:参考文章
The text was updated successfully, but these errors were encountered: