数据、运算符和表达式
概述
变量用来在程序中储存数据,变量类似于一个容器,变量一般由字母、数字、下划线 _
或美元符 $
组合而成。尝试在编写两个变量并参与计算:
a = 1;
b = 3 + a;
console.log(a, b);
上面的代码中,我们定义两个变量 a
和 b
,其中 a
储存了数字 1
,b
储存了数字 3
与变量 a
的和。console.log
是用来在控制台打印数据的代码,它不会想 alert
一样弹框,它可以方便我们调试,console.log(a, b)
会在控制台打印出变量 a
与 b
存储的数据。
在上面代码中:
1
和3
都被称作 字面量,它们是不变的;a
和b
被称作 变量,他们是用来存储字面量的容器,被存到变量中的数据被叫做变量的 值;+
和=
号被称作 运算符,他们被用来处理运算;字面量、变量或者他们与运算符的组合又被称作 表达式,比如上述代码中,字面量
1
就是一个表达式,a
也是一个表达式,他们与=
的组合a = 1
被称作赋值表达式,下一行的3 + a
被称作算符表达式,无论何种表达式,都会有一个最终计算的结果值。一组完整表达式被称作 语句,语句的结尾可以添加一个
;
号作为结束,也可以直接换行结束。
程序就是通过这样的一个个语句组合而成。
数据类型
字面量并不是只有数字,也可能文本、真假值、空值或者是更加复杂的数据。
在 JS 中,数据被分为两类,原始类型 和 引用类型:
原始类型:简单的数据,数字,文本,空值等。
类型 描述 Number
表示数字类型的数据 String
表示字符串(文本)类型的数据 Boolean
表示真假值,比如对或错、肯定或否定、是或不是 Null
表示空数据或者无效的数据 Undefined
表示未定义或不存在的数据 引用类型:各种数据的组合,用来表示复杂的数据,比如表格、列表等等。
尝试创建各种类型的字面量:
// null 类型
vnull = null;
// undefined 类型
vu = undefined;
// Number 类型
vn1 = 9876;
vn2 = 209.123;
vn2 = -1000;
vn3 = NaN;
vn4 = Infinity;
// String 类型
vs1 = "A";
vs2 = "文字 Chart ¥$ ";
vs3 = "1";
vs4 = " ";
// Boolean 类型
vb1 = false;
vb2 = true;
// 引用类型
vo1 = { name: "苏轼", birth: 1037, dead: true };
vo2 = [1, 2, 3, "one", "tow", "three"];
Number 类型
Number 类型就是数字类型,普通数字都是数字类型的字面量,比如:
a = 123;
b = 3.14;
c = -100;
JS 中只有确切的数字,没有分数,无理数等特殊数字。比如三分之一,根号二这样的数字,在 JS 中无法表示。
NaN & Infinity
数字类型有两个特殊的字面量,NaN
与 Infinity
:
字面量 | 类型 | 描述 |
---|---|---|
NaN | Number | Not a number 的缩写,表示不是一个数字,但是它属于 Number 类型 |
Infinity | Number | 表示无穷大 |
NaN
常常出现在错误的计算当中,比如:
a = 1 / "哈哈哈";
console.log(a); // NaN
上面代码中, /
表示做除法,其意思是把 1 除以文本 '哈哈哈
' 的值存到变量 a
中。这是一个数学运算,程序无法计算数字和文本的触发,所以结果会是字面量 NaN
。
无穷大的只会在数字除以 0 时出现,比如:
a = 1 / 0;
console.log(a);
上面会打印出 Infinity
。无穷大也可以是负数,比如:
a = -1 / 0;
console.log(a);
上面会打印出 -Infinity
。
任何字面量可以直接存到变量中,不是只能通过计算得到,比如:
a = NaN;
b = Infinity;
console.log(a, b);
数字范围
JS 一个数字是用一个 64 位的二进制数来储存的,你可以理解为数字是由 64 个字符 0
或者 1
来表示,它可能是这样的:
0111011000000100101010011001110001001101110010001100100101110110
这些符号中,有的决定这个数是否是含有小数,有的决定正负数,各有各的功能。这串字符的表示范围是有限的,并不能表示所有的数字。 在整数中,你可以在以下范围计算和编写字面量而不会产生错误:
-(253 - 1) 到 253 - 1;
其中,2 的 53 次方是 9007199254740992,你可以尝试编写 9007199254740992 + 1
,你会得到一个错误的结果,因为它超出了 JS 数字表示的范围。
小数同理,也只能表示一定范围的精度,比如:
a = 2 / 3;
console.log(a);
2 / 3
会在计算到一定程度后就停止,然后进行四舍五入,并不会获得一个无限小数。
因为精度的丢失,甚至引起一些精度上的错误计算,你可以尝试编写 0.1+0.2
,在 JS 中我们不会得到 0.3
。当然这种情况只是特例,JS 中的数字并不是都那么不可靠,后面我们也有方法解决这些问题。
进制
生活中我们所用到的数字是十进制的,就是满十进一。但是当我们需要处理一些特殊数据时,可能是使用到其他进制的数字,比较明显的就是 CSS 中我们对颜色的设置,通过 16 进制的方式可以用更少的字符来表示颜色。在 JS 中,也有其他进制的数字字面量:
n2 = 0b110; // 二进制
n8 = 0110; // 八进制
n16 = 0x110; // 十六进制
console.log(n2, n8, n16); // 6 72 272
console.log
会把数据都转化为十进制再打印。
- 二进制:二进制的数字字面量开头是
0b
或者0B
,使用频率很低。 - 八进制:八进制的数字字面量开头是
0
,但是如果八进制数字字面量中存在数字8
或9
会被识别为十进制,开头的0
会被抛弃。八进制使用频率比二进制更低。 - 十六进制:十六进制的数字字面量开头是
0x
或0X
。
不同进制的数字是可以加减的,比如继续上述的代码:
sum = n2 + n8 + n16;
console.log(sum); // 350
它们之间的加减并不会有任何影响,进制只是数字的表示方式而已。
指数
JS 中还有一种类似科学计数法的数字字面量,被叫做指数形式:
var foo = 1.23e3;
console.log(foo, foo * 10); // 1230 12300
上述代码中,1.23e3
相当于 1.23 乘以 10 的 3 次方。其中 e
也可以写成大写字母 E
。
String 类型
String 类型就是文本类型,在程序中常被成为 字符串类型。它用来表示一段文本数据,比如:
a = "一言既出驷马难追";
b = "String type";
console.log(a, b);
字符串类型需要是引号包裹,单引号或者双引号都可以。需要注意的是,单双引号包裹字符串时,不能换行,比如:
a = "长歌行
xxx";
上面的程序是无法运行的。如果真的需要换行,可以使用 +
号连接:
a = "长歌行" +
"xxx";
这个意义并不大,只是为了增加代码的可读性,因为变量 a
的最终结果实际是 "长歌行xxx"
,字符串内部并不会真正换行。如果需要让字符串内存在换行,可以使用 转义字符,在下一段。
转义字符
在字符类型字面中,我们必须使用英文的单引号或者双引号包裹字符串,但是可能会出现一些意外的情况,比如我的字符串中含有单引号和双引号,此时怎么办?比如一个字符串长下面这样:
I'm "iron man"!
如果使用单引号:
'I'm "iron man"!'
或者使用双引号:
"I'm "iron man"!"
这两种方式都会给代码带来解析错误,因为单引号和双引号成对就会形成字符串字面量。
此时需要使用到 转义,转义会改变字符在语法中的作用,变成另一个的字符,想转义一个字符,就在它的前面添加正斜杠 \
,我们可以把字符串内的单引号转义:
I\'m "iron man"!
此时这个单引号失去了在 JS 语法中包裹字符串的意义,是一个单纯的单引号字符,我们可以使用单双引号包裹不受其影响:
s = 'I\'m "iron man"!';
同样的方式也可以转义双引号,然后用双引号包裹字符串;
转义后的字符叫做 转义字符。转义字符并不是只有单双引号,比如前面曾经说过,字符串字面量无法换行,但是 JS 中有针对换行的转义字符 \n
,尝试下面代码:
s = "foo:Ohhhhhhh! \nbar: Haaaaaaaa!";
console.log(s);
它会在打印的时候换行。
转义字符不止这些,其他转义字符很少使用,不再介绍,使用时 查表 即可。
字符意义
我们可以在字符串中编写任何字符,比如字符 '1'
,它和数字类型的 1
并不是一个意思。
数字类型的 1
具有它的数学意义,但是字符 '1'
代表的只是一个符号,比较直观的例子是:
a = 1 + 1; // 2
b = "1" + "1"; // '11'
console.log(a, b);
上面的例子中,1 + 1
是数字计算,结果是数字 2
。而 '1' + '1'
字符连接,没有数学意义,结果是字符串 '11'
。
Null 类型
Null 类型 有且仅有一个 字面量 null
,它表示无效的数据或空值:
a = null;
console.log(null);
null
类型常常用来做程序开发时的交互,比如后端人员查询一个表格,发现表格为空时,他就很可能返回一个 null
来表示数据为空。
Undefined 类型
Undefinded 类型也是 有且仅有一个 字面量 undefined
,它表示数据未定义。
a = undefined;
console.log(a);
undefined
和 null
类似,但是它们表示的意义不一样。比如一个小孩子手里的棒棒糖吃光了,棒棒糖的状态应该是 null
,而如果一个小孩子手里根本就没有棒棒糖,就应该使用 undefined
来表示。一个表示空值,一个表示没有。
Boolean 类型
Boolean 类型 有且仅有两个 字面量 true
和 false
,它们分别表示 真
和 假
,也可以理解成 确定
和 否定
或者 对
和 错
,它们被用来表示完全相对的值。Boolean 类型一般被音译为 布尔类型,true
和 false
字面量常被称作 布尔值。
布尔类型一般是逻辑运算的结果,比如大于,小于,等于等等:
a = 1;
b = a > 10;
console.log(b);
上面代码中,a > 10
是一个判断表达式,它的意义是判断变量 a
的值是否大于 10
。显而易见,这个判断不成立,所以值为 false
。反之,当一个判断表达式成立是,它的结果就是 true
,比如:
a = 1;
b = a < 10;
console.log(b);
引用类型
引用类型又被称作 对象类型,它有三种类型的字面量:
- 函数:用来存储一段代码。
- 对象:用来存储一组有关联的数据,比如一个人的身高体重肺活量,它们可以通过对象存储到一起。
- 数组:用来存储多组数据,比如一张表格。
引用类型的数据比较复杂,后面我会单独介绍。
变量声明
目前为止,我们的变量都好像凭空出现一般,想写什么变量,就写什么变量,但实际上,程序中首次出现变量的时候,我们需要对变量进行声明。
变量声明类似于数学上的列方程时需要预设代数一样,比如鸡兔同笼的问题,50 个头,130 只脚,问有多少鸡,多少兔。使用二元一次方程解题应如下:
解:
设鸡有 x 只,设兔有 y 只,根据题意可得方程组:
x + y = 50
2x + 4y = 130
解得:
x = 35
y = 15
上述解法中,我们就预先声明了两个代数 x 与 y,方便后文列方程组。同理,在程序中,使用变量之前,也需要对变量进行声明,浏览器才能更轻易的读懂我们的程序,减少代码运行时的错误。
在 JS 中,声明变量使用 var
关键字:
var a;
var b;
a = 1;
b = a + 10;
上诉代码中,我们声明了变量 a
和变量 b
,然后才在后面的程序进行使用,声明变量使用关键字 var
,它并不是一个运算符。虽然 JS 允许你使用未声明的变量,但是使用未声明的变量可能会给程序带来预想不到的后果。
变量声明并不用作为单独的语句,你可以在变量声明的时候,给变量一个初始值,这通常是使用一个赋值表达式:
var a = 1;
var b = a + 10;
以后的变量出现的时候,几乎都会使用变量声明,我会在介绍作用域的时候,讲解声明变量的好处。
默认值与未声明
看下列代码:
var a;
console.log(a); // undefined
console.log(b); // ReferenceError: b is not defined
上面的代码并不能正确执行,打印一个已声明变量 a
和未声明变量 b
,a
没有初始值的时候,视作 undefined
,但是打印 b
直接发送程序错误,因为这是一个未声明的变量。
你可能会疑问,为什么上面的程序也没有声明变量,但是却一直没有报错?你可以思考一下,我们在使用变量的时候,是以什么样的方式访问它。
对变量的访问分为两种情况,赋值或取值,当我们给一个未声明的变量赋值时,JS 会 隐式声明 这个变量,然后进行赋值。然而当我们对一个变量进行取值的时候,它必须先被声明,不然我们无法找到这个变量,程序就会抛出错误。比如:
a = 1; // a 被赋值为 1,因为没有声明 a,程序自动声明了 a
console.log(a); // 可以找到 a 的声明,拿出 a 的值
console.log(b); // 无法找到 b 的声明,拿不出 b 的值,程序报错
声明语法
变量声明必须是语句的开头,它不是表达式,不能把声明写到表达式中:
var a = var b = 1;
这样的程序完全不能运行。
同时,你也可以使用逗号 ,
来声明多个变量:
var a = 1,
b = "变量声明的内容真是又长又臭";
标识符
在 JS 中,变量的名称有一定限制,它们必须是合法标识符。
下面是一些合法的标识符:
- 大小写英文字母
- 数字
_
和$
符号- 汉字、俄文、日文也可以,但是不被推荐使用
变量标识符有相应的规则:
- 不能使用其他特殊字符作为标识符,比如
@#%^&!
等等。 - 不能以数字开头
跟随上述的字符和规则,可以列举一些合法变量名:
// 以下变量名都合法
var a;
var abc123;
var abc_123$_123;
var __$$;
var _1_$2;
var $0__;
// 以下变量名不合法
var 99_$zxc;
var @1;
var abc_%;
在 JS 中,标识符是区分大小写的 在 JS 中,标识符是区分大小写的 在 JS 中,标识符是区分大小写的
如果你声明了一个变量 foo
,不能使用 Foo
、FOO
等方式访问它:
var foo = "bar";
console.log(Foo); // error
重复声明无效
查看一下代码:
var foo = 1;
var foo = 2;
console.log(foo);
我们声明了两次 foo
变量,程序并不会报错,打印的值是 2
。
变量的重复的声明是无效的,所以上述代码等同于:
var foo = 1;
foo = 2;
console.log(foo);
运算符
程序中有各式各项的运算符,有处理计算的,有处理逻辑的。现在我们简单介绍下常用运算符。
算符运算符
算符运算符用来处理基本运算,算符运算符有以下 5 个:
运算符 | 描述 |
---|---|
+ | 加法 |
- | 减法或取反 |
* | 乘法 |
/ | 除法 |
% | 取模 |
整数
查看下列代码:
var a = 1 + 1;
var b = 20 - 2;
var c = 10 * a * a;
var d = b / 2 / 3;
var e = b % 5;
console.log(a, b, c, d, e); // 2 18 40 3 3
前四项就是常规的四则运算,这里说一下 var e = b % 5
,这在实际上是数学上的取余数,b % 5
计算的结果就是 b
除以 5
得到的余数。
小数
在程序中小数的计算,会有一些限制:
var a = 7 + 0.5;
var b = a / 7;
var c = a % 4;
console.log(a, b, c);
因为数字范围的限制,表达式 a / 7
并没有计算完成,而是计算到一定精度后停止计算。
小数也可以参数取模运算:
var a = 12.5 % 5;
console.log(a); // 2.5
溢出和错误计算
上面说过,数字是有范围的,而且计算也可能产生错误,比如数字 0 在数学上就不能作为除数,例:
console.log(1 / 0); // Infinity
console.log(1 + undefined); // NaN
同样,如果计算结果超出了数字的表示范围,就会产生溢出,溢出的结果就是字面量 infinity
的正负值:
console.log(1e300 * 1e300);
正负值
通过 -
号还可以设置一个数字的正负值:
var a = 100;
console.log(-a); // -100
console.log(-1); // -1
需要注意的是,数字 0 在 JS 中也有正负值,尽管 0
和 -0
在 JS 中是恒等的,但是当他们用来计算的时候,是具有正负性的:
console.log(1 / 0); // Infinity
console.log(1 / -0); // -Infinity
字符运算
并不是只有数字类型的数据才能被计算,字符可以通过 +
号来拼接:
var a = "start";
var b = "end";
console.log(a + " " + b); // "start end"
字符或字符串通过 +
号可以实现拼接。通过 +
号,我们还可以让原本的字符串换行,提升代码的可读性。比如你想输出一段 HTML 代码:
var h = "<div>\n" +
" <span>content</span>\n" +
"</div>";
不要使用字符进行其他算术运算符的计算,这样可能会导致一个错误的计算。比如:
var a = 1 / "one";
console.log(a); // NaN
赋值运算符
赋值运算符可以把表达式的计算结果存到一个变量之中,最简单的赋值运算符就是一个 =
号,在之前的代码中我们已经大量使用,常用的赋值运算符如下如下:
运算符 | 描述 |
---|---|
= | 赋值 |
+= | 加法赋值 |
-= | 减法赋值 |
*= | 乘法赋值 |
/= | 除法赋值 |
%= | 取模赋值 |
在程序中,你常常会遇到如下情况:
var b = 1000;
b = b + 10;
console.log(b); // 1010
在上述代码中:
- 我们声明了变量
b
并初始化值为1
; - 然后我们取
b
的值加上10
后再次赋值给变量b
; - 接着我们对变量
b
进行其他操作,上述代码为打印;
这种情况就如同你开了一个小卖铺,每天在不停的收款一样,你的收款机一开始只有你放置 1000 元,然后有人在你的小卖铺买了一瓶 10 元的汽水,你的收款机里就多了 10 元。而收款的逻辑就是,用原本收款机里的钱加上卖出去的汽水的钱等于当前收款机里的钱。
这种操作会层出不穷,比如你那天的生意很不错:
var b = 1000;
b = b + 10; // 售出一瓶汽水
b = b + 20; // 售出两瓶汽水
b = b + 10; // 售出一包 7 元的糖果,收款 10 元,找零 3 元
b = b - 3;
b = b - 50; // 老丈人来买烟,不好意思收钱,送他一华子
b = b - 20 * 5; // 中午进货,20 瓶 5 元的汽水
b = b + 50 * 10; // 隔壁张三家孩子结婚,买了 50 包 10 元的喜糖
// ...
console.log(b); // 晚上数钱
这种基于变量本身的赋值操作,可以用对应的赋值表达式表示:
var b = 1000;
b += 10; // 售出一瓶汽水
b += 20; // 售出两瓶汽水
b += 10; // 售出一包 7 元的糖果,收款 10 元,找零 3 元
b -= 3;
b -= 48; // 老丈人来买酒,不好意思收钱,送他一瓶 48 的红酒
b -= 20 * 5; // 中午进货,20 瓶 5 元的汽水
b += 50 * 10; // 隔壁张三家孩子结婚,买了 50 包 10 元的喜糖
// ...
console.log(b); // 晚上数钱
同理还可以运用到其他运算符上:
var a = 1;
a = a * 100;
a = a / 25;
a = a % 10;
可以简化为:
var a = 1;
a *= 100;
a /= 25;
a %= 10;
自增自减运算符
运算符 | 描述 |
---|---|
-- | 自减赋值,变量自身增加 1 |
++ | 自增赋值,变量自身减少 1 |
举个例子,一个人需要统计某条街道的人流量,实验人员可以手持一个计数器,每当有一个人进入街道,就进行一次计数,程序可能会是这样的:
var a = 0; // 准备好后开始计数
a += 1; // 经过一个人计数一次
a += 1;
a += 1;
// ....
console.log(a); // 统计数据
上面代码中,我们看到 a += 1
的频率是很高的,并且每次都是增加 1
。我们可以使用自增运算符去简化:
var a = 0;
a++;
a++;
a++;
// ....
console.log(a);
同理,也有一个自减运算符:
var foo = 0;
foo--;
console.log(foo); // -1
自增自减运算符的计算方式
查看下列代码:
var foo = 0;
console.log(foo++);
console.log(foo);
上述代码中,会依次打印 0
和 1
,是不是很奇怪?其实这和自增自减运算符的计算方式有关系,自增自减运算符可以写在变量的左侧或者右侧:
- 当写在左侧时,在自增或自减后取值
- 当写在右侧时,在自增或自减前取值
查看下列代码:
var foo = 0;
var bar = foo++;
var baz = ++foo;
console.log(bar, baz);
上述代码中,会打印 0 2
。基于刚才的规则,分析得出:
bar
是被赋值foo
后,foo++
才执行,foo
的值开始是 0,所以bar
的值为 0,对foo++
取值后开始计算,foo
递增为 1。- 而
baz
是在++foo
后,获取到foo
的值,所以baz
为 2。
现在来做一个练习,写出下列程序打印的值:
var foo = 2;
var bar = foo++ + ++foo + --foo + foo--;
console.log(foo, bar);
如果你没有深刻理解自增自减运算符的规则,那么这个练习你会束手无策。它的答案是 2 12
,foo
的值最终是 2
,很容易理解,因为它分别自增自减了两次。但是 bar
的值竟然是 12
。
现在我们分析下 foo++ + ++foo + --foo + foo--
的计算过程,在此过程中,你需要牢记自增自减的规则:
foo
一开始的值是2
;foo++
- 先取值,
foo
的值是2
,记作 f1 - 后自增,
foo
自增为3
- 先取值,
++foo
- 先自增,
foo
自增为4
- 后取值,
foo
的值为4
,记作 f2
- 先自增,
--foo
- 先自减,
foo
自减为3
- 后取值,
foo
的值为3
,记作 f3
- 先自减,
foo--
- 先取值,
foo
的值为3
,记作 f4 - 后自减,
foo
自减为2
。
- 先取值,
最后计算 f1 + f2 + f3 + f4 得到 2 + 4 + 3 + 3 等于 12
实际开发一般不会出现上述情况,自增自减是为了提高代码的可读性,如果使用自增自减可读性反而降低了,那么就应该放弃使用,这是本末倒置。但是也有程序为了压榨代码的性能,频繁的使用自增自减,因为自增自减运算比赋值运算快得多,这种情况一般是在高度封装的算法中。
提高运算符的优先级
运算符之间是有优先级的,比如乘法肯定会比加法优先计算,所以乘法运算符的优先级更高,它符合数学上的规则。类似于数学中一样,你可以是用圆括号来改变运算的优先级:
console.log(3 + 2 - 5 * 0); // 5
console.log((3 + 2 - 5) * 0); // 0
每种运算符都有自己对应的优先级,优先级越大,运算越靠前,优先级可以查看 这里,没有学到的运算符可以先略过。
如何记忆那么多运算符的优先级?
它们可不想唐诗宋词,背起来朗朗上口。我建议使用语义记住大部分运算符优先级,然后在针对性的记忆一部分。
使用语法记忆,比如:
var a = 1 + 2 * 3;
根据语法,赋值运算符一般都是最后执行,乘法在数学上比加法优先,所以不用背都可以知道上面表达式的计算顺序。
你不用担心,某些特殊的运算符优先级,我都会在未来讲解时一同介绍。
逗号运算符
还有一个特殊的运算符叫做 逗号运算符,我们之前在声明变量的时候预见过它,它当时被用来分割语句赋值表达式。
使用逗号运算符可以把语句分割成多个表达式,它们被统称为 逗号表达式,其值是最后一个表达式的值:
var foo;
foo = (1 * 2, "123", 100 + 11);
console.log(foo); // 111
备注:逗号运算符是所有运算符中最低的,比赋值运算符还低,所以上面代码需要使用圆括号提高优先级,让逗号表达式先运算再赋值。
void 运算符
void
运算符用来修饰任意表达式,void
的计算结果总是 undefined
。
备注:void
的优先级较高,几乎高于所有算术逻辑等运算符。所以下面代码的算术运算会使用圆括号提高优先级。
例如:
var a = void 0;
var b = void (100 + 100);
var c = void true;
var d = void ("文本" + "信息");
console.log(a, b, c, d);
上面四个变量的值都是 undefined
,因为 void
运算符的计算结果总是 undefined
。需要注意的是,void
运算符并不是让它右边的表达式值变为 undefined
,而是让整个 void
表达式的为 undefined
。比如:
void (1 + 100);
void
运算符并不是让 (1 + 100)
变为 undefined
,而是整个表达式 void (1 + 100)
的计算结果为 undefined
,其中 (1 + 100)
的值还是 101
。