数据、运算符和表达式


概述

变量用来在程序中储存数据,变量类似于一个容器,变量一般由字母、数字、下划线 _ 或美元符 $ 组合而成。尝试在编写两个变量并参与计算:

a = 1;
b = 3 + a;

console.log(a, b);

上面的代码中,我们定义两个变量 ab,其中 a 储存了数字 1b 储存了数字 3 与变量 a 的和。console.log 是用来在控制台打印数据的代码,它不会想 alert 一样弹框,它可以方便我们调试,console.log(a, b) 会在控制台打印出变量 ab 存储的数据。

在上面代码中:

  • 13 都被称作 字面量,它们是不变的;

  • ab 被称作 变量,他们是用来存储字面量的容器,被存到变量中的数据被叫做变量的

  • += 号被称作 运算符,他们被用来处理运算;

  • 字面量、变量或者他们与运算符的组合又被称作 表达式,比如上述代码中,字面量 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

数字类型有两个特殊的字面量,NaNInfinity

字面量类型描述
NaNNumberNot a number 的缩写,表示不是一个数字,但是它属于 Number 类型
InfinityNumber表示无穷大

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,但是如果八进制数字字面量中存在数字 89 会被识别为十进制,开头的 0 会被抛弃。八进制使用频率比二进制更低。
  • 十六进制:十六进制的数字字面量开头是 0x0X

不同进制的数字是可以加减的,比如继续上述的代码:

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);

undefinednull 类似,但是它们表示的意义不一样。比如一个小孩子手里的棒棒糖吃光了,棒棒糖的状态应该是 null,而如果一个小孩子手里根本就没有棒棒糖,就应该使用 undefined 来表示。一个表示空值,一个表示没有。

Boolean 类型

Boolean 类型 有且仅有两个 字面量 truefalse,它们分别表示 ,也可以理解成 确定否定 或者 ,它们被用来表示完全相对的值。Boolean 类型一般被音译为 布尔类型truefalse 字面量常被称作 布尔值

布尔类型一般是逻辑运算的结果,比如大于,小于,等于等等:

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 和未声明变量 ba 没有初始值的时候,视作 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,不能使用 FooFOO 等方式访问它:

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 = &quot;&lt;div&gt;\n&quot; +
        &quot;    &lt;span&gt;content&lt;/span&gt;\n&quot; +
        &quot;&lt;/div&gt;&quot;;

不要使用字符进行其他算术运算符的计算,这样可能会导致一个错误的计算。比如:

var a = 1 / "one";
console.log(a); // NaN

赋值运算符

赋值运算符可以把表达式的计算结果存到一个变量之中,最简单的赋值运算符就是一个 = 号,在之前的代码中我们已经大量使用,常用的赋值运算符如下如下:

运算符描述
=赋值
+=加法赋值
-=减法赋值
*=乘法赋值
/=除法赋值
%=取模赋值

在程序中,你常常会遇到如下情况:

var b = 1000;
b = b + 10;
console.log(b); // 1010

在上述代码中:

  1. 我们声明了变量 b 并初始化值为 1
  2. 然后我们取 b 的值加上 10 后再次赋值给变量 b
  3. 接着我们对变量 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);

上述代码中,会依次打印 01,是不是很奇怪?其实这和自增自减运算符的计算方式有关系,自增自减运算符可以写在变量的左侧或者右侧:

  • 当写在左侧时,在自增或自减后取值
  • 当写在右侧时,在自增或自减前取值

查看下列代码:

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 12foo 的值最终是 2,很容易理解,因为它分别自增自减了两次。但是 bar 的值竟然是 12

现在我们分析下 foo++ + ++foo + --foo + foo-- 的计算过程,在此过程中,你需要牢记自增自减的规则:

  1. foo 一开始的值是 2

  2. foo++

    1. 先取值,foo 的值是 2,记作 f1
    2. 后自增,foo 自增为 3
  3. ++foo

    1. 先自增,foo 自增为 4
    2. 后取值,foo 的值为 4,记作 f2
  4. --foo

    1. 先自减,foo 自减为 3
    2. 后取值,foo 的值为 3,记作 f3
  5. foo--

    1. 先取值,foo 的值为 3,记作 f4
    2. 后自减,foo 自减为 2
  6. 最后计算 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