前端入门8-JavaScript语法之数据类型和变量
声明
本系列文章内容全部梳理自以下几个来源:
- 《JavaScript权威指南》
- MDN web docs
- Github:smyhvae/web
- Github:goddyZhao/Translation/JavaScript
作为一个前端小白,入门跟着这几个来源学习,感谢作者的分享,在其基础上,通过自己的理解,梳理出的知识点,或许有遗漏,或许有些理解是错误的,如有发现,欢迎指点下。
PS:梳理的内容以《JavaScript权威指南》这本书中的内容为主,因此接下去跟 JavaScript 语法相关的系列文章基本只介绍 ES5 标准规范的内容、ES6 等这系列梳理完再单独来讲讲。
正文-数据类型、变量
JavaScript 里有两种数据类型:原始类型和对象类型
原始类型
原始类型里包括:
- 数字(Number)
- 布尔(Boolean)
- 字符串(String)
- null
- undefined
布尔类型和字符串类型跟 Java 没多大区别,主要就讲一下数字类型、null 和 undefined。
数字
JavaScript 里不像 Java 一样会区分 int,float,long 等之类的数字类型,全部都归属于一个 Number 数字类型中。之所以不加区分,是因为,在 JavaScript 里,所有的数字,不管整数还是小数,都用浮点数来表示,采用的是 IEEE 754标准定义的 64 位浮点格式表示数字。
那么,它所能表示的数值范围就是有限的,除了正常数值外,还有一些关键字表示特殊场景:
- Infinity(正无穷)
- -Infinity(负无穷)
- NaN(非数值)
对于小数,支持的浮动小数表示法如下:
3.14
-.2345789 // -0.23456789
-3.12e+12 // -3.12*1012
.1e-23 // 0.1*10-23=10-24=1e-24
另外,因为浮点表示法只能精确的表示如:1/2, 1/8, 1/1024 这类分数,对于 1/10 这种小数只能取近视值表示,因此在 JavaScript 里有个经典的有趣现象:
0.1 + 0.2 在 JavaScript 里是不等于 0.3 的,因为用浮点表示法,无法精确表示 0.1 和 0.2,所以会舍弃一些精度,两个近似值相加,计算结果跟实际算术运算结果自然有些偏差。
上图里也显示了,在 JavaScript 里,0.1 + 0.2 的运算结果是 0.30000000000000004。
那么,是否所有非 1/2, 1/4, 1/8 这类 1/2^n 小数的相加结果最后都不会等于实际运算结果呢?
0.1, 0.2, 0.3 都是浮点数无法精确表示的数值,所以在 JavaScript 里都是以近似值存储在内存中,那么,为何 0.1 + 0.2 != 0.3
,但 0.1 + 0.3 == 0.4
?
这是因为,JavaScript 里在处理这类小数时,允许一定程度的误差,比如 0.10000000000000001 在允许的误差中,所以在 JavaScript 里就将这个值当做 0.1 来看待处理。
所以如果两个是以近似值存储的小数运算之后的结果,在误差允许范围内,那么计算结果会按实际算术运算结果来呈现。
总之,不要用 JavaScript 来计算一些小数计算且有精度要求,如果非要不可,那么建议先将小数都按比例扩展到整数运算后,再按比例缩小,如:
还有另外一点,由于 JavaScript 的变量是不区分类型的,那么当有需要区分某个变量是不是数字时,可用内置的全局函数来处理:
isNaN()
-- 如果参数是 NaN 或者非数字值(如字符串或对象),返回 trueisFinite()
-- 如果参数不是 NaN,或 Infinity 或 -Infinity 时返回 true,通俗理解,参数是正常的数字
null
跟 Java 一样,JavaScript 里也有 null 关键字,但它的含义和用法却跟 Java 里的 null 不太一样。
在 Java 里,声明一个对象类型的变量后,如果没有对该变量进行赋值操作,默认值为 null,所以在程序中经常需要对变量进行判空处理,这是 Java 里 null 的场景。
但在 JavaScript 中,声明一个变量却没有进行赋值操作的话,默认值不是 null,而是 undefined。
那么,什么场景下,变量的值会是 null 呢?我可以告诉你,没有,没有任何场景下某个变量或某个属性的值默认会是 null,除非你在程序中手动将某个变量赋值为 null,那么此时这个变量的值才会是 null。
所以,才有些书本中会说,null 是表示程序级、正常的或在意料之中的值的空缺。意思就是说,null 是 JavaScript 设计出来的一个表示空值含义的数据类型,用来给你在程序中当有需要给某个变量手动设置为空值的场景时使用。
举个通俗的例子,对于数字类型变量,你可以用 0 表示它的初始值;对于字符串类型变量,你可以用 "" 表示它的初始值;那么对于对象类型,当你也需要给它一个表示空值无具体含义的初始值时,你就可以给它赋值为 null。
这也是为什么用 typeof 运算符获取 null 的数据类型时,会发现输出的是 Object。因为 null 实际上是个实际存在的数据值,只是它的含义是空值的意思,用于赋值给对象类型的变量。
那么,也就是说,不能沿用 Java 里使用 null 的思维应用到 JavaScript 中了,null 可以作为初始值赋值给变量,但变量如果没有进行初始化,默认值不再是 null 了,这点是 JavaScript 有区别于 Java 的地方,需要注意一下。
不然再继续挪用 Java 的使用 null 思维,可能在编程中,会遇到一些意料外,没想通的问题。
undefined
如果声明了一个变量,缺没有对这个变量进行赋值操作,那么这个值默认就是 undefined。
那么在 Java 中的判空操作来判断变量是否有进行初始化的行为在这里就是对应判断变量的值是否为 undefined 的,但实际上,在 JavaScript 里,由于 if 判断语句接收的为真值,而不像 Java 只支持布尔类型,所以基本没有类似 Java 的判空的编程场景。
undefined 还有另外一种场景:
当访问对象中不存在的属性时,此时会输出 undefined,表示这个属性并未在对象中定义。
针对这种场景,undefined 可用于判断对象中是否含有某些指定的属性。
总结一下 null 和 undefined:
- null 是用于在程序中,如果有场景需要,如某个变量在某种条件下需要有一个表示为空值含义的取值,此时,可手动为该变量赋值为 null;
- 当声明某个变量,却没有对其进行赋值初始化操作时,这个变量默认为 undefined
- 当访问对象某个不存在的属性时,会输出 undefined,可用于判断对象中是否含有指定属性
对象类型
除了原始类型外,其余都是对象类型,但有一些内置的对象类型,所以大概可以这么表示
- 对象类型(Object)
- 函数(Function)
- 数组(Array)
- 日期(Date)
- 正则(RegExp)
- ...
也就是,在 JavaScript 里,函数和数组,本质上也是对象。
变量相关
由于我本身有 Java 的基础了,所以 JavaScript 一些很基础的语法我可能会漏掉了,但影响不大。
弱类型
虽然 JavaScript 中有原始类型和对象类型,而且每个分类下又有很多细分的数据类型,但它实际上是一门弱类型语言,也叫动态语言。也就是说,使用变量时,无需指明变量是何种类型,运行期间会自动确定。
变量声明
既然使用变量时不必指明变量的数据类型,那么自然没有类似于 Java 中那么多种的变量声明方式,在 JavaScript 中声明变量很简单,都是通过 var 来:
var name = dasu;
ES5 中,声明变量的方式就是通过 var
关键字,而且同一变量重复声明不会出问题,会以后面声明的为主。
变量的提前声明
先看段代码:
<script type="text/javascript">
console.log(a); //输出 undefined
var a = 1;
console.log(a); //输出 1
b();
function b() {
console.log(a); //输出 undefined
var a = 2;
console.log(a); //输出 2
}
</script>
JavaScript 中有变量的提前声明特性,也就是在代码开始执行前,所有通过 var 或 function 声明的变量和函数都已经提前声明了(下面统称变量),所以在声明语句之前访问声明的这个变量并不会抛异常。
但提前的只有变量的声明,变量的赋值初始化操作并没有提前,所以第一行代码输出变量 a 的值时,因为变量已经被提前声明了,但没赋值,按照上面介绍的,此时变量 a 值为 undefined,当赋值语句执行完,输出自然就是赋值的 1 了。
同样,由于 b 函数已经被提前声明了,所以可以在声明它的位置之前就调用函数了,而函数调用后,开始执行函数内的代码时,也同样会有变量提前声明的特性。
因此,在执行函数内第一行代码时,输出的变量 a 是函数内声明的局部变量,而不是函数外部的变量,这点行为跟 Java 不一样,需要注意一下。
有些脚本语言并没有变量声明提前的特性,使用的变量或函数只能在声明了它的位置之后才能使用,这是 JavaScript 区别它们的一点。
全局属性
上面说过,声明变量时是通过 var
关键字声明,那如果漏掉 var 呢,看个例子:
<script type="text/javascript">
//console.log(a); //抛异常,因为没有找到a变量
a = 1;
b();
function b() {
console.log(a); //输出 1
a = 2;
console.log(a); //输出 2
}
console.log(a); //输出 2
</script>
第一行代码如果不注释掉,那么它执行的结果会是抛出一个异常,因为没有找到 a 变量。
接着执行了 a = 1
,a 是一个不存在的变量,直接对不存在的变量进行赋值语句,其实是会自动对全局对象 window 动态添加了一个 a 属性并赋值,所以后续调用了 b 函数,函数里操作的 a 其实都是来自全局对象 window 的属性 a,所以在函数内对 a 进行的操作结果,当函数执行结束后,最后再次输出 a 才会是 2。
这其实是因为对象的特性导致的,在对象一节会来讲讲,但这里要清楚一点,切记声明使用变量时,不要忘记在前面要使用 var
。
另外,顺便提一下,第一行被注释掉的代码,如果换成输出 this.a,那么此时程序是不会抛异常的,而是输出 undefined,这是因为前面也有稍微提过,访问对象不存在的属性时,会输出 undefined,都是在讲对象时会来说说。
变量作用域
ES5 中,变量有两种作用域,全局作用域和函数内作用域。
在函数外声明的变量都具有全局作用域,即使跨 js 文件都能够访问;而在函数内声明的变量,不管声明变量的语句在哪个位置,整个函数内都可以访问该变量,因为有变量的提前声明特性,所以是函数内作用域。
由于在 JavaScript 中,同一变量的重复声明不会出问题,所以对于全局变量而言,在多人协作,多模块编程中,很容易造成全局变量冲突,即我在我写的 js 文件中声明的 a 全局变量,其他人在其他 js 文件中,又声明了 a 全局变量,对于浏览器而言,它就只是简单的以后声明的为主。
但对于程序而已,就会发生不可控的问题,而且极难排查,所以要慎用全局变量。当然针对这种情况也有很多解决方案,后续讲到函数一节中会来讲讲。
包装对象
JavaScript 里的对象具有很多特性,比如可以动态为其添加属性等等。但原始类型都不具有对象的这些特性,那么当需要对原始类型也使用类似对象的特性行为时,这时候包装对象就出现了。
包装对象跟 Java 中的包装类基本是类似的概念,原始数据类似对应的对象类型的值称为包装对象:
- 数字类型 -> Number 包装对象
- 布尔类型 -> Boolean 包装对象
- 字符串类型 -> String 包装对象
- null 和 undefined 没有包装对象,所以不允许对 null 和 undefined 的变量进行属性操作
接下来就讲讲原始类型和包装对象之间的转换,存在两种场景,程序运行期间自动转换,或者手动显示的进行转换。
隐式转换
因为属性是对象才有的特性,所以当对某个原始类型的变量进行属性操作时,此时会临时创建一个包装对象,属性操作结束后销毁包装对象。
看个例子:
var s = "test"; //创建一个字符串,s是原始类型的变量
s.len = 4; //对s动态添加一个属性len并赋值,执行这行代码时,会临时创建一个包装对象,所以这里的s已经不是上面的原生类型变量,进行了一次自动转换
console.log(s.len); //输出 undefined,上一行虽然进行了一次包装对象的自动转换,但是是临时的,那一行代码执行结束,包装对象就销毁了。所以这一行又对s原始类型变量进行属性操作,又再一次创建一个临时的包装对象
需要注意一点,当对原始类型的操作进行属性操作时,会创建一个临时的包装对象,注意是临时的,属性操作完毕,包装对象就销毁了。下一次再继续对原始类型进行属性操作时,创建的又是新的一个临时包装对象。
显示转换
除了隐式的自动转换外,也可以显示的手动转换。
如果是原始类型 -> 包装类型的转换,可使用相对应的包装对象的构造函数方式:
var a = new Number(123);
var b = new Boolean(true);
var s = new String("dasu");
此时,a, b, s 都是对象类型的变量了,可以对它们进行一些属性操作。
如果是包装类型 -> 原始类型的转换,使用不加 new 的调用全局函数的方式:
var aa = Number(a);
var bb = Boolean(b);
var ss = String(s);
在后续讲函数时会讲到,一个函数被调用的方式有多种:其中,有跟 new 关键字一起使用,此时叫这个函数为构造函数;如果只是简单的调用,此时叫函数调用;如果是作为对象的属性被调用,此时称方法调用;不同的调用方式会有一些区别。
所以,这里当包装对象使用构造函数方式使用时,可以显示的将原始类型数据转换为包装对象;但如果不作为构造函数,只是简单的函数调用,其实就是将传入的参数转换为原始类型,参数不单可以是包装对象类型,也可以是其他类型。
数据类型间相互转换
上面讲了原始类型与包装对象间的相互转换,其实本质上也就是不同数据类型间的相互转换。
按数据类型细分来讲的话,一共包括:数字、布尔、字符串、null、undefined、对象(函数、数组等),由于 JavaScript 是弱类型语言,运行期间自动确定变量类型,所以,其实这些不同数据类型之间都存在相互转换的规则。
先看个例子:
10 + " objects"; // => "10 objects",这里的 10 自动转换成 "10"
"7" * "4"; // => 28, 这里的两个字符串都自动转换为数字
var n = 1 - "x"; // => NaN,字符串 "x" 无法转换为数字
n + " objects"; // => "NaN objects", NaN 转换为字符串 "NaN"
数字可以转换成字符串,字符串也可以转换为数字,原始类型也可以转换为对象类型等等,反正不同类似之间都可以相互转换。
基本转换规则
具体的规则,可以参见下表:
待转换值 | 转换为字符串 | 转换为数字 | 转换为布尔值 | 转换为对象 |
---|---|---|---|---|
undefined | "undefined" | NaN | false | throws TypeError |
null | "null" | 0 | false | throws TypeError |
true(布尔->其他) | "true" | 1 | -- | new Boolean(true) |
false(布尔->其他) | "false" | 0 | -- | new Boolean(false) |
""(空字符串->其他) | -- | 0 | false | new String("") |
"1.2"(字符串内容为数字->其他) | -- | 1.2 | true | new String("1.2") |
"dasu"(字符串内容非数字->其他) | -- | NaN | true | new String("dasu") |
0(数字->其他) | "0" | -- | false | new Number(0) |
-0(数字->其他) | "0" | -- | false | new Number(-0) |
1(数字->其他) | "1" | -- | true | new Number(1) |
NaN | "NaN" | -- | false | new Number(NaN) |
Infinity | "Infinity" | -- | true | new Number(Infinity) |
-Infinity | "-Infinity" | -- | true | new Number(-Infinity) |
{}(对象 -> 其他) | 单独讲 | 单独讲 | true | -- |
[] (数组 -> 其他) | "" | 0 | true | -- |
[1] (一个数字元素的数值 -> 其他) | "1" | 1 | true | -- |
['a'] (普通数组 -> 其他) | 使用join()方法 | NaN | true | -- |
function(){} (函数 -> 其他) | 单独讲 | NaN | true | -- |
总之不同类型之间都可以相互转换,除了 null 和 undefined 不能转换为对象之外,其余都可以。
那么什么时候会进行这些转换呢?
其实在程序运行期间,就不断的在隐式的进行着各种类型转换,比如 if 语句中不是布尔类型时,比如算术表达式两边是不同类型时等等。
那么,如何进行手动的显示转换呢?
在上一小节中,其实有稍微提过了,就是使用:
- Number()
- String()
- Boolean()
- Object()
注意是以函数调用方式使用,即不加 new 关键字的使用方式。参数传入的值就是表示上表中第一列待转换的值,而四种不同的函数,就对应着上表中右边四列的转换规则。如
Number("dasu") // => NaN,表示待转换值为字符串 "dasu",需要转换为数字类型,按照上表规则,转换结果NaN
String(true) // => "true",同理,将布尔类型true转为字符串类型
Boolean([]) // => true,将空数组转为布尔类型
Object(3) // => new Number(3),将数字类型转为包装对象
换句话说,这四个函数,其实就是用于将任意类型转换为函数对应的类型,比如 Number() 函数就是用于将任意类型转为数字类型,至于具体转换规则,就是按照表中的规则来进行转换。
一般来说,应该可以不用将表中所有的转换规则都详记,需要自己手动转换的场景应该也不多,记住一些常用基本的就行了,至于哪些是常见的,写多了就清楚了,比如数字类型 -> 布尔类型,对象类型 -> 布尔类型等。
对象转换为原始值规则
所有的数据类型之间的转换,就对象转换到原始值的规则会复杂点,其余的需要的时候,看一下表就行了。
- 对象 -> 布尔
首先,所有的对象,不管的函数、数组还是普通对象,只要这个对象是定义后存在的,那么它转换为布尔值都是 true,所以对象转布尔也很简单。反正就记住,对象存在,那么转布尔就为 true。
所以,即使一个布尔值 false,先转成包装对象 new Boolean(false),再从包装对象转为布尔值,那么此时,包装对象转布尔后是 true,因为包装对象存在,就这么简单,不关心这个包装对象原本是从布尔 false 转来的。
- 对象 -> 字符串
对象转字符串,主要是需要借助两个方法:
- 如果对象具有
toString()
,则调用这个方法,如果调用后返回了一个原始值,那么就将这个原始值转为字符串,转换结束。 - 如果对象没有
toString()
方法,或者调用该方法返回的并不是一个原始值,那么调用对象的valueOf()
方法,同样,如果调用后返回一个原始值,那么将原始值转为字符串后,转换结束。 - 否则,抛类型错误异常。
这就是对象转字符串的规则,有些内置的对象,比如函数对象,或数组对象就可能会对这两个方法进行重写,对于自定义的对象,也可以重写这两个方法,来手动控制它转成字符串的规则。
- 对象 -> 数字
对象转数字的规则,也是需要用到这两个方法,只是它将步骤替换了下:
- 如果对象具有
valueOf()
方法,且调用后返回一个原始值,那么将这个原始值转为数字,转换结束。 - 如果对象没有
valueOf()
方法,或者调用后返回的不是原始值,那么看对象是否具有toSring()
方法,且调用它后返回一个原始值,那么将原始值转为数字,转换结束。 - 否则,抛类型错误异常。
大家好,我是 dasu,欢迎关注我的公众号(dasuAndroidTv),公众号中有我的联系方式,欢迎有事没事来唠嗑一下,如果你觉得本篇内容有帮助到你,可以转载但记得要关注,要标明原文哦,谢谢支持~