基本语法
1. 语句
1 | var a = 1 + 3; |
语句:上述代码则是一行赋值语句,语句是为了完成某种任务而进行的操作;
表达式:1 + 3
叫做表达式(expression),指一个为了得到返回值的计算式;
区别在于,前者主要为了进行某种操作,一般情况下不需要返回值;后者则是为了得到返回值,一定会返回一个值。
2. 变量
变量是对“值”的具名引用。变量就是为“值”起名,然后引用这个名字,就等同于引用这个值。
变量的名字就是变量名,JavaScript 的变量名区分大小写;
1 | var a = 1; // 声明并复制 |
如果只是声明变量而没有赋值,则该变量的值是undefined
。undefined
是一个特殊的值,表示“无定义”。
如果一个变量没有声明就直接使用,JavaScript 会报错,告诉你变量未定义(not defined)。
如果使用var
重新声明一个已经存在的变量,是无效的;但如果第二次声明的时候还进行了赋值,则会覆盖掉前面的值。
JavaScript 引擎的工作方式是,先解析代码,获取所有被声明的变量,然后再一行一行地运行。这造成的结果,就是所有的变量的声明语句,都会被提升到代码的头部,这就叫做变量提升(hoisting)。
1 | console.log(a); |
上述代码在运行时,并不会报错,而会显示 a
为 undefined
,因为实际运行的代码如下,表示变量a
已声明,但还未赋值:
1 | var a; |
3. 标识符
标识符(identifier)指的是用来识别各种值的合法名称。最常见的标识符就是变量名,以及函数名;
标识符命名规则如下。
- 第一个字符,可以是任意 Unicode 字母(包括英文字母和其他语言的字母),以及美元符号(
$
)和下划线(_
)。 - 第二个字符及后面的字符,除了 Unicode 字母、美元符号和下划线,还可以用数字
0-9
。
- 中文是合法的标识符,可以用作变量名;
- JavaScript 有一些保留字,不能用作标识符;
4. 注释
历史上 JavaScript 可以兼容 HTML 代码的注释,所以<!--
和-->
也被视为合法的单行注释。
1 | x = 1; <!-- x = 2; |
-->
只有在行首,才会被当成单行注释,否则会当作正常的运算;
5. 区块
JavaScript 使用大括号,将多个相关的语句组合在一起,称为“区块”(block)。
对于var
命令来说,JavaScript 的区块不构成单独的作用域(scope),也就是在区块外部,仍能访问到定义的变量;
6. 条件控制
switch
语句后面的表达式,与case
语句后面的表示式比较运行结果时,采用的是严格相等运算符(===
),而不是相等运算符(==
),这意味着比较时不会发生类型转换;
1 | var x = 1; |
由于变量x
没有发生类型转换,所以不会执行case true
的情况;
数据类型
JavaScript 的数据类型,共有六种。(ES6 又新增了第七种 Symbol 类型的值,暂不涉及。)
- 数值(number):整数和小数(比如
1
和3.14
)。 - 字符串(string):文本(比如
Hello World
)。 - 布尔值(boolean):表示真伪的两个特殊值,即
true
(真)和false
(假)。 undefined
:表示“未定义”或不存在,即由于目前没有定义,所以此处暂时没有任何值。null
:表示空值,即此处的值为空。- 对象(object):各种值组成的集合。
对象是最复杂的数据类型,又可以分成三个子类型。
- 狭义的对象(object)
- 数组(array)
- 函数(function)
1. typeof 运算符
JavaScript 有三种方法,可以确定一个值到底是什么类型:
typeof
运算符instanceof
运算符Object.prototype.toString
方法
typeof
运算符可以返回一个值的数据类型。
数值、字符串、布尔值分别返回number
、string
、boolean
,函数返回function
,undefined
返回undefined
,利用这一点,typeof
可以用来检查一个没有声明的变量,而不报错;
对象返回object
,空数组([]
)的类型也是object
,这表示在 JavaScript 内部,数组本质上只是一种特殊的对象。
instanceof
运算符可以区分数组和对象:
1
2
3
4
5 var o = {};
var a = [];
o instanceof Array // false
a instanceof Array // true
null
返回object
,这是由于历史原因造成的,最初设计 JavaScript 时没有将null
作为一种单独的数据类型,而是归为了一种特殊的object
。
2. null 和 undefined
一个变量被赋值为undefined
和null
,效果几乎等价,在if
语句中,它们都会被自动转为false
,相等运算符(==
)甚至直接报告两者相等。
null
是一个表示“空”的对象,转为数值时为0
;undefined
是一个表示”此处无定义”的原始值,转为数值时为NaN
。
null
表示空值,即该处的值现在为空。调用函数时,某个参数未设置任何值,这时就可以传入null
,表示该参数为空。undefined
表示“未定义”,常见的场景有变量声明未定义,调用函数参数未提供,函数未返回值,对象没有赋值的属性;
3. 布尔值
如果 JavaScript 预期某个位置应该是布尔值,会将该位置上现有的值自动转为布尔值。转换规则是除了下面六个值被转为false
,其他值都视为true
。
undefined
null
false
0
NaN
""
或''
(空字符串)
注意,空数组([]
)和空对象({}
)对应的布尔值,都是true
;
4. 整数和浮点数
JavaScript 内部,所有数字都是以64位浮点数形式储存,即使整数也是如此。所以,1
与1.0
是相同的,是同一个数。
当需要整数才能完成计算时, JavaScript 会自动把64位浮点数,转成32位整数;
4.1 数值精度
根据国际标准 IEEE 754,JavaScript 浮点数的64个二进制位,从最左边开始,是这样组成的。
- 第1位:符号位,
0
表示正数,1
表示负数 - 第2位到第12位(共11位):指数部分
- 第13位到第64位(共52位):小数部分(即有效数字)
符号位决定了一个数的正负,指数部分决定了数值的大小,小数部分决定了数值的精度。
指数部分一共有11个二进制位,大小范围就是0到2047。IEEE 754 规定,如果指数部分的值在0到2047之间(不含两个端点),那么有效数字的第一位默认总是1,不保存在64位浮点数之中。也就是说,有效数字这时总是1.xx...xx
的形式,其中小数部分xx...xx
的部分保存在64位浮点数之中,最长为52位。因此,JavaScript 提供的有效数字最长为53个二进制位,即精度最多只能到53个二进制位;
1 | Math.pow(2, 53) |
超过这个范围的计算就无法保持精度了,也就是对十进制的15位数能保持精度。
4.2 数值范围
64位浮点数的指数部分的值最大为2047,分出一半表示负数,则 JavaScript 能够表示的数值范围为2^1024^到2^-1023^(开区间),超出这个范围的数无法表示。
- 大于等于2的1024次方,那么就会发生“正向溢出”,这时就会返回
Infinity
; - 如果一个数小于等于2的-1075次方(指数部分最小值-1023,再加上小数部分的52位),会发生为“负向溢出”,会直接返回 0;
JavaScript 提供Number
对象的MAX_VALUE
和MIN_VALUE
属性,返回可以表示的具体的最大值和最小值。
4.3 数值的表示法
科学计数法允许字母e
或E
的后面,跟着一个整数,表示这个数值的指数部分。以下情况会表示未科学计数法:
- 小数点前的数字多于21位;
- 小数点后的零多于5个;
4.4 数值的进制
使用字面量(literal)直接表示一个数值时,JavaScript 对整数提供四种进制的表示方法:十进制、十六进制、八进制、二进制。
- 十进制:没有前导0的数值。
- 八进制:有前缀
0o
或0O
的数值,或者有前导0、且只用到0-7的八个阿拉伯数字的数值。 - 十六进制:有前缀
0x
或0X
的数值。 - 二进制:有前缀
0b
或0B
的数值。
默认情况下,JavaScript 内部会自动将八进制、十六进制、二进制转为十进制。
5. 特殊数值
JavaScript 内部实际上存在2个0
:一个是+0
,一个是-0
,区别就是64位浮点数表示法的符号位不同。它们是等价的。
唯一有区别的场合是,+0
或-0
当作分母,返回的值是不相等的,除以正零得到+Infinity
,除以负零得到-Infinity
;
NaN
,表示“非数字”(Not a Number),主要出现在将字符串解析成数字出错的场合,逻辑错误的数学函数的运算结果也会出现NaN
;
NaN
不是独立的数据类型,而是一个特殊数值,它的数据类型依然属于Number
:
1 | typeof NaN // 'number' |
NaN
不等于任何值,包括它本身,NaN
与任何数(包括它自己)的运算,得到的都是NaN
:
1 | NaN === NaN // false |
Infinity
表示“无穷”,用来表示两种场景。一种是一个正的数值太大,或一个负的数值太小,无法表示;另一种是非 0 数值除以 0,得到Infinity
。
Infinity
大于一切数值(除了NaN
),-Infinity
小于一切数值(除了NaN
),Infinity
与NaN
比较,总是返回false
;
0 乘以Infinity
,返回NaN
;0除以Infinity
,返回0
;Infinity
除以0,返回Infinity
;
Infinity
与null
计算时,null
会转成0,等同于与0
的计算,Infinity
与undefined
计算,返回的都是NaN
;
6. 字符串
单引号字符串的内部,可以使用双引号。双引号字符串的内部,可以使用单引号,同种引号包含需带上\
转义;
由于 HTML 语言的属性值使用双引号,所以很多项目约定 JavaScript 语言的字符串只使用单引号;
字符串默认只能写在一行内,分成多行将会报错,可以在每一行的尾部使用反斜杠换行;
字符串可以被视为字符数组,因此可以使用数组的方括号运算符,用来返回某个位置的字符。
与数组的相似性仅此而已,字符串内部的单个字符无法改变和增删;
JavaScript 使用 Unicode 字符集。JavaScript 引擎内部,所有字符都用 Unicode 表示。
每个字符在 JavaScript 内部都是以16位(即2个字节)的 UTF-16 格式储存。也就是说,JavaScript 的单位字符长度固定为16位长度,即2个字节。
但是,UTF-16 有两种长度:对于码点在U+0000
到U+FFFF
之间的字符,长度为16位(即2个字节);对于码点在U+10000
到U+10FFFF
之间的字符,长度为32位(即4个字节),而且前两个字节在0xD800
到0xDBFF
之间,后两个字节在0xDC00
到0xDFFF
之间。举例来说,码点U+1D306
对应的字符为𝌆,
它写成 UTF-16 就是0xD834 0xDF06
。
JavaScript 对 UTF-16 的支持是不完整的,由于历史原因,只支持两字节的字符,不支持四字节的字符。
7. 对象
什么是对象?简单说,对象就是一组“键值对”(key-value)的集合,是一种无序的复合数据集合。
对象的所有键名都是字符串(ES6 又引入了 Symbol 值也可以作为键名),所以加不加引号都可以,会被自动转为字符串;
如果键名不符合标识名的条件,且也不是数字,则必须加上引号,否则会报错;
对象的每一个键名又称为“属性”(property),它的“键值”可以是任何数据类型,属性可以动态创建,不必在对象声明时就指定;
如果不同的变量名指向同一个对象,那么它们都是这个对象的引用,也就是说指向同一个内存地址。修改其中一个变量,会影响到其他所有变量。
但是,这种引用只局限于对象,如果两个变量指向同一个原始类型的值。那么,变量这时都是值的拷贝。
对象采用大括号表示,这导致了一个问题:如果行首是一个大括号,它到底是表达式还是语句?
1 | { foo: 123 } |
为了避免这种歧义,当无法确定是对象还是代码块,一律解释为代码块。
如果要解释为对象,最好在大括号前加上圆括号。因为圆括号的里面,只能是表达式,所以确保大括号只能解释为对象。
数值键名不能使用点运算符(因为会被当成小数点),只能使用方括号运算符
查看一个对象本身的所有属性,可以使用Object.keys
方法。
delete
命令用于删除对象的属性需要注意的是,delete
命令只能删除对象本身的属性,无法删除继承的属性;
in
运算符用于检查对象是否包含某个属性,但它不能识别哪些属性是对象自身的,哪些属性是继承的,可以使用对象的hasOwnProperty
方法判断一下,是否为对象自身的属性
for...in
循环用来遍历一个对象的全部属性:
- 它遍历的是对象所有可遍历(enumerable)的属性,会跳过不可遍历的属性。
- 它不仅遍历对象自身的属性,还遍历继承的属性。
with
语句是操作同一个对象的多个属性时,提供一些书写的方便:
1 | var obj = { |
注意,如果with
区块内部有变量的赋值操作,必须是当前对象已经存在的属性,否则会创造一个当前作用域的全局变量。
1 | var obj = {}; |
因为with
区块没有改变作用域,它的内部依然是当前作用域。这造成了with
语句的一个很大的弊病,就是绑定对象不明确;因此,建议不要使用with
语句。
8. 函数
8.1 声明方法
(1)function 命令
function
命令声明的代码区块,就是一个函数。function
命令后面是函数名,函数名后面是一对圆括号,里面是传入函数的参数。函数体放在大括号里面。
1 | function print(s) { |
上面的代码命名了一个print
函数,以后使用print()
这种形式,就可以调用相应的代码。这叫做函数的声明(Function Declaration)。
(2)函数表达式
除了用function
命令声明函数,还可以采用变量赋值的写法。
1 | var print = function(s) { |
这种写法将一个匿名函数赋值给变量。这时,这个匿名函数又称函数表达式(Function Expression),因为赋值语句的等号右侧只能放表达式。
采用函数表达式声明函数时,function
命令后面不带有函数名。如果加上函数名,该函数名只在函数体内部有效,在函数体外部无效。
1 | var print = function x(){ |
这种写法的用处有两个,一是可以在函数体内部调用自身,二是方便除错(除错工具显示函数调用栈时,将显示函数名,而不再显示这里是一个匿名函数)。因此,下面的形式声明函数也非常常见。
1 | var f = function f() {}; |
需要注意的是,函数的表达式需要在语句的结尾加上分号,表示语句结束。而函数的声明在结尾的大括号后面不用加分号。总的来说,这两种声明函数的方式,差别很细微,可以近似认为是等价的。
(3)Function 构造函数
通过传递任意数量的参数给Function
构造函数,只有最后一个参数会被当做函数体,如果只有一个参数,该参数就是函数体,这种声明函数的方式非常不直观,几乎无人使用。
8.2 函数提升
如果同一个函数被多次声明,后面的声明就会覆盖前面的声明。
函数可以调用自身,这就是递归(recursion)。
JavaScript 语言将函数看作一种值,只是一个可以执行的值,此外并无特殊之处;
JavaScript 引擎将函数名视同变量名,所以采用function
命令声明函数时,整个函数会像变量声明一样,被提升到代码头部;
用function
命令和var
赋值语句声明同一个函数,由于存在函数提升,最后会采用var
赋值语句的定义。
1 | console.log(f); // function f() { console.log('2');} |
函数提升要比变量提升的优先级要高一些,且不会被变量声明覆盖,但是会被变量赋值之后覆盖。
函数的name
属性返回函数的名字,如果是通过变量赋值定义的(匿名)函数,那么name
属性返回变量名;
函数的length
属性返回函数预期传入的参数个数,函数的toString()
方法返回一个字符串,内容是函数的源码
8.3 作用域
作用域(scope)指的是变量存在的范围。在 ES5 的规范中,JavaScript 只有两种作用域:
- 全局作用域,变量在整个程序中一直存在,所有地方都可以读取;
- 函数作用域,变量只在函数内部存在;
对于顶层函数来说,函数外部声明的变量就是全局变量(global variable),它可以在函数内部读取;
在函数内部定义的变量,外部无法读取,称为“局部变量”(local variable);
函数内部定义的变量,会在该作用域内覆盖同名全局变量;
对于var
命令来说,局部变量只能在函数内部声明,在其他区块中声明,一律都是全局变量;
8.4 函数内部的变量提升
var
命令声明的变量,不管在什么位置,变量声明都会被提升到函数体的头部。
1 | function foo(x) { |
8.5 函数本身的作用域
函数本身也是一个值,也有自己的作用域。它的作用域与变量一样,就是其声明时所在的作用域,与其运行时所在的作用域无关,也就是说函数执行时所在的作用域,是定义时的作用域,而不是调用时所在的作用域:
1 | var a = 1; |
上面代码中,函数x
是在函数f
的外部声明的,所以它的作用域绑定外层,内部变量a
不会到函数f
体内取值,所以输出1
,而不是2
。
同样的,函数体内部声明的函数,作用域绑定函数体内部。
1 | function foo() { |
8.6 函数的参数
函数参数不是必需的,JavaScript 允许省略参数,省略的参数的值就变为undefined
;
函数参数如果是原始类型的值(数值、字符串、布尔值),传递方式是传值传递(passes by value)。这意味着,在函数体内修改参数值,不会影响到函数外部。
如果函数参数是复合类型的值(数组、对象、其他函数),传递方式是传址传递(pass by reference)。也就是说,传入函数的原始值的地址,因此在函数内部修改参数,将会影响到原始值。
如果函数内部修改的,不是参数对象的某个属性,而是替换掉整个参数,这时不会影响到原始值。
如果有同名的参数,则取最后出现的那个值,这时,如果要获得前面的值,可以使用arguments
对象,例如:
1 | function f(a, a) { |
8.7 闭包
在理解了变量作用域之后,就能很快理解闭包了。
正常情况下,函数外部无法读取函数内部声明的变量。
1 | function f1() { |
那就是在函数的内部,再定义一个函数:
1 | function f1() { |
数f2
就在函数f1
内部,这时f1
内部的所有局部变量,对f2
都是可见的。既然f2
可以读取f1
的局部变量,那么只要把f2
作为返回值,就可以在f1
外部读取它的内部变量了。
由于在 JavaScript 语言中,只有函数内部的子函数才能读取内部变量,因此可以把闭包简单理解成“定义在一个函数内部的函数”。闭包最大的特点,就是它可以“记住”诞生的环境;
闭包的最大用处有两个,一个是可以读取外层函数内部的变量,另一个就是让这些变量始终保持在内存中,即闭包可以使得它诞生环境一直存在。请看下面的例子,闭包使得内部变量记住上一次调用时的运算结果。
1 | function createIncrementor(start) { |
上面代码中,start
是函数createIncrementor
的内部变量。通过闭包,start
的状态被保留了,每一次调用都是在上一次调用的基础上进行计算。从中可以看到,闭包inc
使得函数createIncrementor
的内部环境,一直存在。所以,闭包可以看作是函数内部作用域的一个接口。
为什么闭包能够返回外层函数的内部变量?原因是闭包用到了外层变量(start
),导致外层函数(createIncrementor
)不能从内存释放。只要闭包没有被垃圾回收机制清除,外层函数提供的运行环境也不会被清除,它的内部变量就始终保存着当前值,供闭包读取。
闭包的另一个用处,是封装对象的私有属性和私有方法;
外层函数每次运行,都会生成一个新的闭包,而这个闭包又会保留外层函数的内部变量,所以内存消耗很大。因此不能滥用闭包,否则会造成网页的性能问题。
8.8 立即调用的函数表达式(IIFE)
JavaScript 的语法,圆括号()
跟在函数名之后,表示调用该函数。
在定义函数之后,立即调用该函数,会产生语法错误;当作表达式时,函数可以定义后直接加圆括号调用:
1 | function(){ /* code */ }(); |
为了避免解析的歧义,JavaScript 规定,如果function
关键字出现在行首,一律解释成语句。因此,引擎看到行首是function
关键字之后,认为这一段都是函数的定义,不应该以圆括号结尾,所以就报错了。
函数定义后立即调用的解决方法,就是不要让function
出现在行首,让引擎将其理解成一个表达式。最简单的处理,就是将其放在一个圆括号里面:
1 | (function(){ /* code */ }()); |
这就叫做“立即调用的函数表达式”(Immediately-Invoked Function Expression),简称 IIFE。
上述代码若没有分号隔开,JavaScript 会将它们连在一起解释,将第二行解释为第一行的参数。
通常情况下,只对匿名函数使用这种“立即执行的函数表达式”。它的目的有两个:
一是不必为函数命名,避免了污染全局变量;
二是 IIFE 内部形成了一个单独的作用域,可以封装一些外部无法读取的私有变量。
8.9 eval 命令
eval
命令接受一个字符串作为参数,并将这个字符串当作语句执行。
eval
没有自己的作用域,都在当前作用域内执行,因此可能会修改当前作用域的变量的值,造成安全问题。
为了防止这种风险,JavaScript 规定,如果使用严格模式,eval
内部声明的变量,不会影响到外部作用域。
1 | (function f() { |
严格模式下,eval
内部还是改写了外部变量,可见安全风险依然存在。
总之,eval
的本质是在当前作用域之中,注入代码。由于安全风险和不利于 JavaScript 引擎优化执行速度,一般不推荐使用。通常情况下,eval
最常见的场合是解析 JSON 数据的字符串,不过正确的做法应该是使用原生的JSON.parse
方法。
1 | var m = eval; |
上面代码中,变量m
是eval
的别名。静态代码分析阶段,引擎分辨不出m('var x = 1')
执行的是eval
命令。
为了保证eval
的别名不影响代码优化,JavaScript 的标准规定,凡是使用别名执行eval
,eval
内部一律是全局作用域。
9. 数组
本质上,数组属于一种特殊的对象。
数组的特殊性体现在,它的键名是按次序排列的一组整数(0,1,2…)。
对于数值的键名,不能使用点结构。
JavaScript 使用一个32位整数,保存数组的元素个数。这意味着,数组成员最多只有 4294967295 个(2^32^ - 1)个,也就是说length
属性的最大值就是 4294967295。
length
属性是一个动态的值,等于键名中的最大整数加上 1,数组的数字键不需要连续,这也表明数组是一种动态的数据结构,可以随时增减数组的成员。
length
属性是可写的,若设置小于原有的大小,数组会动态减少;清空数组的一个有效方法,就是将length
属性设为 0。
for...in
不仅会遍历数组所有的数字键,还会遍历非数字键,所以数组的遍历可以考虑使用for
循环或while
循环。
9.1 数组的空位
当数组的某个位置是空元素,即两个逗号之间没有任何值,我们称该数组存在空位(hole)。
1 | var a = [1, , 1]; |
上面代码表明,数组的空位不影响length
属性。
数组的空位是可以读取的,返回undefined
,使用delete
命令删除一个数组成员,会形成空位,并且不会影响length
属性。
数组的某个位置是空位,与某个位置是undefined
,是不一样的。如果是空位,使用数组的forEach
方法、for...in
结构、以及Object.keys
方法进行遍历,空位都会被跳过。
运算符
加法运算符+
是在运行时决定,到底是执行相加,还是执行连接。也就是说,运算子的不同,导致了不同的语法行为,这种现象称为“重载”(overload)。
如果运算子是对象,必须先转成原始类型的值,然后再相加:
- 自动调用对象的
valueOf
方法,总是返回对象自身; - 自动调用对象的
toString
方法,将其转为字符串;
如果运算子是一个Date
对象的实例,那么会优先执行toString
方法。
余数运算符的运算结果的正负号由第一个运算子的正负号决定;
+
数值运算符的作用在于可以将任何值转为数值(与Number
函数的作用相同),-
负数值运算符效果一致,不过符号相反,数值运算符号和负数值运算符,都会返回一个新的值,而不会改变原始变量的值;
数运算符(**
)完成指数运算,前一个运算子是底数,后一个运算子是指数;
指数运算符是右结合,而不是左结合。即多个指数运算符连用时,先进行最右边的计算;
1. 比较运算符
字符串的比较进行比较时是按照字典顺序进行比较,JavaScript 引擎内部首先比较首字符的 Unicode 码点;
非字符串的比较,其中至少一个是字符串,原始类型值则转换为数字对比,对象先转为原始类型再执行toString
方法;
JavaScript 提供两种相等运算符:==
和===
。
简单说,它们的区别是相等运算符(==
)比较两个值是否相等,严格相等运算符(===
)比较它们是否为“同一个值”。
如果两个值不是同一类型,严格相等运算符(===
)直接返回false
,而相等运算符(==
)会将它们转换成同一个类型,再用严格相等运算符进行比较;
如果两个值的类型不同,直接返回
false
。同一类型的原始类型的值(数值、字符串、布尔值)比较时,值相同就返回
true
,值不同就返回false
。两个复合类型(对象、数组、函数)的数据比较时,不是比较它们的值是否相等,而是比较它们是否指向同一个地址;
undefined
和null
与自身严格相等,变量声明后默认值是undefined
,因此两个只声明未赋值的变量是相等的;
相等运算符==
用来比较相同类型的数据时,与严格相等运算符完全一样。
- 原始类型的值会转换成数值再进行比较;
- 对象(这里指广义的对象,包括数组和函数)与原始类型的值比较时,对象转换成原始类型的值,再进行比较;
undefined
和null
只有与自身比较,或者互相比较时,才会返回true
;与其他类型的值比较时,结果都为false
。
2. 布尔运算符
对于非布尔值,取反运算符会将其转为布尔值。可以这样记忆,以下六个值取反后为true
,其他值都为false
。
undefined
null
false
0
NaN
- 空字符串(
''
)
如果对一个值连续做两次取反运算,等于将其转为对应的布尔值,与Boolean
函数的作用相同。
且运算符(&&
)往往用于多个表达式的求值:
如果第一个运算子的布尔值为
true
,则返回第二个运算子的值(注意是值,不是布尔值);如果第一个运算子的布尔值为
false
,则直接返回第一个运算子的值,且不再对第二个运算子求值。
跳过第二个运算子的机制,被称为“短路”。可用短路代替if
语句;
且运算符可以多个连用,这时返回第一个布尔值为false
的表达式的值。如果所有表达式的布尔值都为true
,则返回最后一个表达式的值。
或运算符(||
)如果第一个运算子的布尔值为true
,则返回第一个运算子的值,且不再对第二个运算子求值;如果第一个运算子的布尔值为false
,则返回第二个运算子的值。
或运算符常用于为变量设置默认值:
1 | text = text || ''; |
3. 二进制运算符
位运算符只对整数起作用,如果一个运算子不是整数,会自动转为整数后再执行。另外,虽然在 JavaScript 内部,数值都是以64位浮点数的形式储存,但是做位运算的时候,是以32位带符号的整数进行运算的,并且返回值也是一个32位带符号的整数;
位运算只对整数有效,遇到小数时,会将小数部分舍去,只保留整数部分。所以,将一个小数与0
进行二进制或运算,等同于对该数去除小数部分,即取整数位。
二进制与运算符(&
)的规则是逐位比较两个运算子,两个二进制位之中只要有一个位为0
,就返回0
,否则返回1
;
二进制否运算符(~
)将每个二进制位都变为相反值(0
变为1
,1
变为0
)。它的返回结果有时比较难理解,因为涉及到计算机内部的数值表示机制。
因为涉及到反码,简单可以记忆为,一个数取否,等于 -1 减这个数;
对一个小数连续进行两次二进制否运算,能达到取整效果,而且是所有取整中最快的;
其他类型的值取否,会先调用Number
函数,将字符串转为数值
“异或运算”有一个特殊运用,连续对两个数a
和b
进行三次异或运算,a^=b; b^=a; a^=b;
,可以互换它们的值,这是互换两个变量的值的最快方法
如果左移0位,就相当于将该数值转为32位整数,等同于取整,对于正数和负数都有效;
头部补零的右移运算符(>>>
)头部一律补零,而对于正数,该运算的结果与右移运算符(>>
)完全一致,区别主要在于负数。
查看一个负整数在计算机内部的储存形式,最快的方法就是使用这个运算符。
1 | -1 >>> 0 // 4294967295 |
上面代码表示,-1
作为32位整数时,内部的储存形式使用无符号整数格式解读,值为 4294967295(即(2^32)-1
,等于11111111111111111111111111111111
)。
4. 特殊运算符
void
运算符的作用是执行一个表达式,然后不返回任何值,或者说返回undefined
。
1 | void 0 // undefined |
上面是void
运算符的两种写法,都正确,第二种比较稳妥,因为 void
优先级高,容易出错:
这个运算符的主要用途是浏览器的书签工具(Bookmarklet),以及在超级链接中插入代码防止网页跳转。
逗号运算符用于对两个表达式求值,并返回后一个表达式的值,逗号运算符的一个用途是,在返回一个值之前,进行一些辅助操作。
圆括号(()
)可以用来提高运算的优先级,因为它的优先级是最高的,即圆括号中的表达式会第一个运算。
注意,因为圆括号不是运算符,所以不具有求值作用,只改变运算的优先级。
JavaScript 语言的大多数运算符是“左结合”,少数运算符是“右结合”,其中最主要的是赋值运算符(=
)和三元条件运算符(?:
)。
1 | w = x = y = z; |
上面代码的解释方式如下。
1 | w = (x = (y = z)); |
上面的两行代码,都是右侧的运算数结合在一起。
另外,指数运算符(**
)也是右结合。
1 | 2 ** 3 ** 2 |
数据类型转换
1. 强制转换
只要有一个字符无法转成数值,整个字符串就会被转为NaN
,而parseInt
逐个解析字符;
1 | Number('324abc') // NaN |
Number()
方法的参数是对象时,将返回NaN
,除非是包含单个数值的数组;
String
函数可以将任意类型的值转化成字符串:
1 | String(123) // "123" |
String()
方法的参数如果是对象,返回一个类型字符串;如果是数组,返回该数组的字符串形式:
1 | String({a: 1}) // "[object Object]" |
Boolean()
函数可以将任意类型的值转为布尔值:
除了以下五个值的转换结果为false
,其他的值全部为true
。
undefined
null
0
(包含-0
和+0
)NaN
''
(空字符串)
所有对象(包括空对象)的转换结果都是true
,甚至连false
对应的布尔对象new Boolean(false)
也是true
;
2. 自动转换
- 不同类型的数据互相运算;
- 对非布尔值类型的数据求布尔值;
- 对非数值类型的值使用一元运算符(即
+
和-
)
自动转换的规则是这样的:预期什么类型的值,就调用该类型的转换函数。比如,某个位置预期为字符串,就调用String()
函数进行转换。如果该位置既可以是字符串,也可能是数值,那么默认转为数值。
错误处理机制
Error
构造函数,所有抛出的错误都是这个构造函数的实例:
- message:错误提示信息
- name:错误名称(非标准属性)
- stack:错误的堆栈(非标准属性)
Error
的6个派生对象:
SyntaxError
解析代码时发生的语法错误;ReferenceError
引用一个不存在的变量时发生的错误;RangeError
一个值超出有效范围时发生的错误;TypeError
对象是变量或参数不是预期类型时发生的错误;URIError
对象是 URI 相关函数的参数不正确时抛出的错误,主要涉及encodeURI()
、decodeURI()
、encodeURIComponent()
、decodeURIComponent()
、escape()
和unescape()
这六个函数;eval
函数没有被正确执行时,会抛出EvalError
错误。该错误类型已经不再使用了;
1. 自定义错误
1 | function UserError(message) { |
上面代码自定义一个错误对象UserError
,让它继承Error
对象;
2. 处理错误语法
throw
语句的作用是手动中断程序执行,抛出一个错误,实际上,可以抛出任何类型的值;
JavaScript 提供了try...catch
结构,允许对错误进行处理,选择是否往下执行;
try
代码块抛出的错误,被catch
代码块捕获后,程序会继续向下执行;
catch
代码块之中,还可以再抛出错误,甚至使用嵌套的try...catch
结构;
为了捕捉不同类型的错误,catch
代码块之中可以加入判断语句;
try...catch
结构允许在最后添加一个finally
代码块,表示不管是否出现错误,都必需在最后运行的语句;
1 | function f() { |
catch
代码块之中,触发转入finally
代码块的标志,不仅有return
语句,还有throw
语句;
1 | function f() { |
面代码中,进入catch
代码块之后,一遇到throw
语句,就会去执行finally
代码块,其中有return false
语句,因此就直接返回了,不再会回去执行catch
代码块剩下的部分了。
代码风格
始终坚持一种选择。不要一会使用 Tab 键,一会使用空格键,进行缩进;
建议总是使用大括号表示区块,表示区块起首的大括号,不要另起一行
用空区分函数调用和表达式:
- 表示函数调用时,函数名与左括号之间没有空格;
- 表示函数定义时,函数名与左括号之间没有空格;
- 其他情况时,前面位置的语法元素与左括号之间,都有一个空格;
由于解释引擎自动添加分号的行为难以预测,因此编写代码的时候不应该省略行尾的分号;
不写结尾的分号,可能会导致脚本合并出错。所以,有的代码库在第一行语句开始前,会加上一个分号;
JavaScript 最大的语法缺点,可能就是全局变量对于任何一个代码块,都是可读可写。这对代码的模块化和重复使用,非常不利。因此,建议避免使用全局变量,可以考虑用大写字母表示变量名;
变量声明都放在代码块的头部,所有函数都应该在使用之前定义。函数内部的变量声明,都应该放在函数的头部;
不要使用
with
语句,会造成混淆;建议不要使用相等运算符(
==
),只使用严格相等运算符(===
);建议不要将不同目的的语句,合并成一行;
建议自增(
++
)和自减(--
)运算符尽量使用+=
和-=
代替;建议
switch...case
结构可以用对象结构代替:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19function doAction(action) {
var actions = {
'hack': function () {
return 'hack';
},
'slash': function () {
return 'slash';
},
'run': function () {
return 'run';
}
};
if (typeof actions[action] !== 'function') {
throw new Error('Invalid action.');
}
return actions[action]();
}
console 对象与控制台
开发者工具以后,顶端有多个面板:
- Elements:查看网页的 HTML 源码和 CSS 代码。
- Resources:查看网页加载的各种资源文件(比如代码文件、字体文件 CSS 文件等),以及在硬盘上创建的各种内容(比如本地缓存、Cookie、Local Storage等)。
- Network:查看网页的 HTTP 通信情况。
- Sources:查看网页加载的脚本源码。
- Timeline:查看各种网页行为随时间变化的情况。
- Performance:查看网页的性能情况,比如 CPU 和内存消耗。
- Console:用来运行 JavaScript 命令
1. log, info, debug
1 | console.log(' %s + %s = %s', 1, 1, 2) |
%o
对象的链接%c
CSS 格式字符串
console.info
是console.log
方法的别名,用法完全一样。只不过console.info
方法会在输出信息的前面,加上一个蓝色图标;
console.debug
方法与console.log
方法类似,会在控制台输出调试信息。但是,默认情况下,console.debug
输出的信息不会显示,只有在打开显示级别在verbose
的情况下,才会显示;
console
对象的所有方法,都可以被覆盖。因此,可以按照自己的需要,定义console.log
方法
2. warn, error
warn
方法和error
方法也是在控制台输出信息,它们与log
方法的不同之处在于,warn
方法输出信息时,在最前面加一个黄色三角,表示警告;
error
方法输出信息时,在最前面加一个红色的叉,表示出错。同时,还会高亮显示输出文字和错误发生的堆栈。其他方面都一样;
3. table
对于某些复合类型的数据,console.table
方法可以将其转为表格显示;
4. count
count
方法用于计数,输出它被调用了多少次,可以接受一个字符串作为参数,作为标签,对执行次数进行分类;
5. dir, dirxml
dir
方法用来对一个对象进行检查(inspect),并以易于阅读和打印的格式显示,对于输出 DOM 对象非常有用,因为会显示 DOM 对象的所有属性;
dirxml
方法主要用于以目录树的形式,显示 DOM 节点,如果参数不是 DOM 节点,而是普通的 JavaScript 对象,则等价于dir
;
6. assert
console.assert
方法主要用于程序运行过程中,进行条件判断,如果不满足条件,就显示一个错误,但不会中断程序执行。这样就相当于提示用户,内部状态不正确。
接受两个参数,第一个参数是表达式,第二个参数是字符串。当第一个参数为false
,才会提示有错误,在控制台输出第二个参数,否则不会有任何结果。
7. time, timeEnd
这两个方法用于计时,可以算出一个操作所花费的准确时间,它们的参数是计时器的名称;
8. group, groupEnd, groupCollapse
console.group
和console.groupEnd
这两个方法用于将显示的信息分组。它只在输出大量信息时有用,分在一组的信息,可以用鼠标折叠/展开;
console.groupCollapsed
方法与console.group
方法很类似,唯一的区别是该组的内容,在第一次显示时是收起的(collapsed),而不是展开的;
9. trace, clear
console.trace
方法显示当前执行的代码在堆栈中的调用路径;
console.clear
方法用于清除当前控制台的所有输出;
10. 控制台命令行 API
$_
属性返回上一个表达式的值,\$0
-\$4
分别代表了最近五个 Elements 面板中选中的元素;$(selector)
返回第一个匹配的元素,$$(selector)
返回选中的DOM对象;$x(path)
方法返回一个数组,包含匹配特定 XPath 表达式的所有 DOM 元素;inspect(object)
方法打开相关面板,并选中相应的元素,显示它的细节;getEventListeners(object)
方法返回一个对象,该对象的成员为object
登记了回调函数的各种事件(比如click
或keydown
),每个事件对应一个数组,数组的成员为该事件的回调函数;monitorEvents(object[, events])
方法监听特定对象上发生的特定事件。事件发生时,会返回一个Event
对象,包含该事件的相关信息。unmonitorEvents
方法用于停止监听;
11. debugger
debugger
语句主要用于除错,作用是设置断点。如果有正在运行的除错工具,程序运行到debugger
语句时会自动停下。如果没有除错工具,debugger
语句不会产生任何结果,JavaScript 引擎自动跳过这一句。
Chrome 浏览器中,当代码运行到debugger
语句时,就会暂停运行,自动打开脚本源码界面。