关于JS的变量和类型,再也不用说出what’s you problem了

论坛 期权论坛 期权     
前端少侠闯江湖   2019-7-14 05:53   3344   0
导读
JS作为一个弱编程语言,是有变量,数据结构,算法等要素的,而变量是学习JS最先接触到的东西,看起来简单但实际也有一些容易犯错或者容易忽略的地方。例如[] == ![],”4.5”>”11”为什么是true,({} +{}).length = 30,而([]+[]).length等于0。如果你不能很好的解答上面的原理,那请好好阅读这篇文章,当遇到类似的问题时,不用再说:JS,what’s you problem?
一:JS数据类型基本介绍
ECMAScript标准规定了7种数据类型,并且该7种数据类型又分为两类:原始类型和引用类型.
原始类型:存入栈中
1:Null:代表"无",是给被赋值的对象赋空值,对象的属性值可以赋值为null,其转换为number为0
2:Undefined:代表"未定义",是还没有定义,转换为number类型为NaN
3:Boolean:包含两个值true和false
4:Number:数字类型:包括整数和浮点数,以及Infinity,-Infinity,NaN
5:String: 字符串类型
6:Symbol:一种实例后返回唯一且不可改变的数据类型,这个是ES6新增的,到时在ES6中会细讲

引用类型:存入堆中      Object:
在对象这一类又可以分为:
1:Object 对象
2:Array 数组
3:Function 函数
4:Date 日期
5:RegExp(正则)

二:为什么分为原始类型和引用类型
原始类型:存入栈中,并且因为JS是弱类型语言,导致每次定义或者改变时都会在栈中新开一个地址去存储,而不是像强类型语言(C,JAVA)一样,只是在定义时在栈中新开地址存储;
举个例子:




如上,因为JS是弱类型语言,故每次修改str都不是在同一个地址上修改,而是先创建一个地址,然后再将变化的值赋值到相应的地址上,产生一个新的字符串;而不是像强类型语言一样在同一个地址上做更改(因为这和JS引擎相关,弱类型语言每次存储到内存的值的类型每次都不是确定的,故每次更改原始值都是先新开一个地址).


而如果str又重新赋值,即新开的地址又被重新被str引用,此时str才会变化.此时的内存变化如下:


  此时1005的地址的hello没有被其他值引用,会被JS引擎利用垃圾回收器自动回收的
引用类型:是将该引用值的地址存在栈中,将引用值存在堆内存中
举个例子:



如果arr1的值发生改变,arr2的值也发生改变.因为如下图,如果将arr1复制给arr2时,不是像C语言一样直接开辟一个地址空间,然后复制相应的值到相应的空间地址,而是直接复制该地址;这样如果改变了arr1或者arr2,另外的值也会相应的变化.


而如下的话,直接将arr1赋值的话,为什么arr2不会相应的发生变化呢?



这样因为如下图,arr1直接赋值,相当于其arr1在栈中的地址变化了(重新赋值了),该地址所存储的是新引用值的地址.故arr2此时还是原来的值,没有改变.


总结来说:引用类型是将该引用值的地址存在栈中,将引用值存在堆内存中.
三:参数传递
函数参数传递本质就是上述说的复制值,即将传入的参数复制给局部变量,然后是原始类型的话则相当于传入的参数的地址和局部变量的地址不一样,故值也不会相互影响,而引用值得话则可能会影响(取决于局部变量的地址是否发生改变)
如下例子:






这几个例子说明ECMAScript中所有的函数的参数都是按照值传递的,而考虑局部变量是否会改变传入的参数只需要看是否是在同一个堆地址中修改了值,如果在同一个堆中修改了值,则会相应的变化,否则不会变化。
四:浅拷贝和深拷贝
如上:当两个引用值的地址同时指向同一个堆时,如果一个值变化时,另一个值也会发生相应的变化,这样会导致很多问题的出现;而如果需要不互相影响,则需要像c,JAVA等强类型语言一样,直接在堆中开辟一个新地址,并复制相应的引用值到该地址;在相应的栈中存放新开的地址而不是直接存储原来的堆的地址;


即将上面的复制方式变成下面的复制方式:


根据复制时会不会将该将引用值中的对象是否也全部复制分为浅拷贝和深拷贝
浅拷贝:不会影响到原始值即age等,但是会影响到引用值的属性


以上就是浅拷贝,如下所示,如果target更改了原始值则不会影响到origin,但是如果更改了引用值则会导致origin也发生改变


如上的结果可知,浅拷贝不会影响原始值而会影响到引用值,而如果需要不影响到引用值,则需要使用深拷贝。
深拷贝:如果拷贝出来的值中的引用值发生改变,原始值中得引用值不会发生改变




五:如何区分原始类型和引用类型:typeof   instanceOf  toString
在深拷贝中,使用到了typeof和 toString来判断数据类型,在这一小节将具体的介绍如何判断JS的数据类型
1:typeOf 判断出原始值是哪种数据类型(null特别注意),但是无法具体的判断引用值的具体的类型(function除外)。
适用场景:


不适用场景:以下不能显示出具体的数据类型,只是显示出object这一个具体的类型,不知道具体的类型


2:instanceof :判断
出引用类型具体是什么类型的对象
A Instanceof B 是判断B的prototype是否存在A的原型链中



注意:1:如果直接以{}开头会直接被误认为是一个块语句而不是一个空对象;
2:原始值无原型链,故不管用该方法如何判断都是false(参考上图判断原始值1的判断)
3:toString:每一个引用类型都有toString方法,默认情况下,toString()方法被每个Object对象继承,如果此方法在自定义对象中未被覆盖,使用toString()方法将返回 [object type],其中type是对象的类型。故就可以使用Object对象的toString方法来判断该数据的类型。另外,原始值的Number,String,Boolean,symbol()类型因为可以使用包装类,故可以转换成相应的对象,故也可以直接使用该方法,不会报错;
而Undefined null不能使用tostring的原因是他无法使用包装类,无法包装成对象,也就会报出无法读取对象中的属性toString。


上面提到了如果次方法在定义对象中未被覆盖,toString才会达到预想的效果,实施上,大部分都在自定义对象中重写了toString方法,故需要使用call来改变this指向。



4:重写typeof


六:类型转换
想明白类型转换首先得知道两个概念:1包装类 2:装箱和拆箱
包装类
为了便于操作基本类型值,ECMAScript还提供了几个特殊的引用类型,他们是基本类型的包装类型,简称包装类。
Boolean
Number
String
注意:包装类只有三种,undefined null Symbol没有;并且包装类型和原始类型是不同的。
因为包装类是对象,是引用值,不和原始类型一样。并且因为有了包装类的存在,导致原始值虽然不是对象,没有属性,但是当你加属性值等则系统自动帮你转换成一个包装类并进行运算(没有保存则自动删除)。
没有保存将自动删除:此时将在执行阶段发生如下几个过程:
1:创建一个Number的包装类实例
2:在实例上添加len属性
3:销毁实例


保存了将不会删除,此时b就是一个数字型对象,但是用b去做运算的话,例如如下用var c = b+1的时候,又会将该对象自动转换成一个数字类型,然后运算完了之后又变回了数字型对象。


总结:引用类型和包装类型的主要区别就是对象的生存期,包装类是原始类型在添加属性时自动生成,如果没有保存则会在该行代码执行完瞬间就立即被销毁。而引用类型则在定义后就会保存在堆中,直到离开该作用域。
装箱和拆箱
装箱转换:把基本类型转换成对应的包装类型
拆箱操作:把引用类型转换成基本类型
类型转换规则
类型转换分为两种:隐式类型转换和显示类型转换
显示类型转换
1:使用了包装类
Number (a)将a转换成数字类
String(a)将a转换成字符串类型
boolean(a) 将a转换成布尔值
注意:在if语句和逻辑语句中,如果只有单个变量,会先将变量转换为Boolean值,而一般情况下,null undefined ‘’ NaN 0 false 会转换成false,其他类型都会转换成true
(注意:不是所有认为false的类型的值转成数字类型的数据都是0;其中null,’’ false 转换成数字类型为0,而undefined NaN会转换成NaN,也不是所有转换成数字类型为0的值都是false,例如[],Symbol()),具体的转换如下所示:


2:使用了一些转换函数
parseInt 整数型转换


parseFloat(“3.1415628”)//浮点数转换
a.toString(radix)  // a以radix进制转换成字符串类型

隐式转换
在JS运算中很容易发生隐式转换,现在以 == 和 + 两个操作符来说明隐式转换的规则.
==
如最开始的问题 [] == ![]为什么是true,这就需要了在ECMAScript的操作规则了;在ECMAScript中的相当操作符由两个等于号(==)表示,如果两个操作数相当,则返回true
相等操作符会先隐式转换操作符(通常称为强制转型),然后比较它们的相等性.
即需要遵守下列基本规则:
1:如果一个操作数是布尔值,则在比较相等性之前,将其转换为数值;
2:如果一个操作是是字符串,另一个操作数是数字型,则在比较之前先将字符串转换为数字类型再比较;
3:如果一个操作数是对象,另一个不是,则调用对象的valueOf()方法,用得到的基本类型值按照前面的规则进行比较;
4:如果两个操作数都是对象,则比较这两个对象指向的堆地址是否相同,如果相同则为true,否则为false;
5:NaN和任何值比较都是false,包括其本身;
6:操作数是undefined 和null时不能转换成其他类型,即这两个值既不大于0,也不小于0,也不等于0,但是undefined == null;


各种数学运算符
加减乘除中,除了加法,其他运算符都会将操作数对各种非Number类型转换成Number类型,
而加法是个例外,执行加法操作符时:
1.当一侧为String类型,被识别为字符串拼接,并会优先将另一侧转换为字符串类型。
2.当一侧为Number类型,另一侧为原始类型,则将原始类型转换为Number类型。
3.当一侧为Number类型,另一侧为引用类型,将引用类型和Number类型转换成字符串;
4.当两侧都为引用类型时,将引用类型转换成字符串;


注意:正负则是统一转换成数字;



其他操作符:
比较
大小比较是数字只能和数字比较,字符串比较是用ASCII码来比较,多个字符是从左往右依次比较;


或 非 与
或是遇到真就返回,否则返回最后一个值,
与是遇到假就返回,否则返回最后一个值;
非是取反,两次取反为相应的布尔值;




隐式类型转换应用之length
在之前讲包装类Number是没有length这个属性的,如果number.length没有保存成Number包装类,则会自动delete掉成为undefined.那么string类型,对象数组的length属性呢?相应的’123’.length,[].length,({}).length,function.length为多少呢?
String().length
在字符串中是有length这个属性的,如下图所示:其值为字符串的长度,并且不可以被修改;


([]).length
数组是有length这个属性的,如下图所示:其值为数组中元素的长度;



({}).length
对象是没有length这个属性的,如下图所示:直接用arr.length相当于给arr赋一个length属性;


(function(){}).length
函数是有length这个属性的,如下图所示:其值为函数中形参的个数;


({}+{}).length 与([]+[]).length
因为加号两端都是引用值,依据上述的各种数学运算符的加法的第四个规则:会将两个引用对象都转换成字符串:

({}+{})为(‘[Object Object][Object Object]’)其长度为30
([]+[])为(‘’),其长度为0












分享到 :
0 人收藏
您需要登录后才可以回帖 登录 | 立即注册

本版积分规则

积分:
帖子:
精华:
期权论坛 期权论坛
发布
内容

下载期权论坛手机APP