前端入门之ES5基本语法
2019-12-12 21:40:16

基本语法

1. 语句

1
var a = 1 + 3;

语句:上述代码则是一行赋值语句,语句是为了完成某种任务而进行的操作;

表达式:1 + 3叫做表达式(expression),指一个为了得到返回值的计算式;

区别在于,前者主要为了进行某种操作,一般情况下不需要返回值;后者则是为了得到返回值,一定会返回一个值。

2. 变量

变量是对“值”的具名引用。变量就是为“值”起名,然后引用这个名字,就等同于引用这个值。

变量的名字就是变量名,JavaScript 的变量名区分大小写

1
2
3
var a = 1;	// 声明并复制
var b; // 声明
b = 1; // 赋值

如果只是声明变量而没有赋值,则该变量的值是undefinedundefined是一个特殊的值,表示“无定义”。

如果一个变量没有声明就直接使用,JavaScript 会报错,告诉你变量未定义(not defined)。

如果使用var重新声明一个已经存在的变量,是无效的;但如果第二次声明的时候还进行了赋值,则会覆盖掉前面的值。

JavaScript 引擎的工作方式是,先解析代码,获取所有被声明的变量,然后再一行一行地运行。这造成的结果,就是所有的变量的声明语句,都会被提升到代码的头部,这就叫做变量提升(hoisting)。

1
2
console.log(a);
var a = 1;

上述代码在运行时,并不会报错,而会显示 aundefined,因为实际运行的代码如下,表示变量a已声明,但还未赋值:

1
2
3
var a;
console.log(a);
a = 1;

3. 标识符

标识符(identifier)指的是用来识别各种值的合法名称。最常见的标识符就是变量名,以及函数名;

标识符命名规则如下。

  • 第一个字符,可以是任意 Unicode 字母(包括英文字母和其他语言的字母),以及美元符号($)和下划线(_)。
  • 第二个字符及后面的字符,除了 Unicode 字母、美元符号和下划线,还可以用数字0-9
  • 中文是合法的标识符,可以用作变量名;
  • JavaScript 有一些保留字,不能用作标识符;

4. 注释

历史上 JavaScript 可以兼容 HTML 代码的注释,所以<!---->也被视为合法的单行注释。

1
2
x = 1; <!-- x = 2;
--> x = 3;

-->只有在行首,才会被当成单行注释,否则会当作正常的运算;

5. 区块

JavaScript 使用大括号,将多个相关的语句组合在一起,称为“区块”(block)。

对于var命令来说,JavaScript 的区块不构成单独的作用域(scope),也就是在区块外部,仍能访问到定义的变量;

6. 条件控制

switch语句后面的表达式,与case语句后面的表示式比较运行结果时,采用的是严格相等运算符(===),而不是相等运算符(==),这意味着比较时不会发生类型转换;

1
2
3
4
5
6
7
8
9
10
var x = 1;

switch (x) {
case true:
console.log('x 发生类型转换');
break;
default:
console.log('x 没有发生类型转换');
}
// x 没有发生类型转换

由于变量x没有发生类型转换,所以不会执行case true的情况;

数据类型

JavaScript 的数据类型,共有六种。(ES6 又新增了第七种 Symbol 类型的值,暂不涉及。)

  • 数值(number):整数和小数(比如13.14)。
  • 字符串(string):文本(比如Hello World)。
  • 布尔值(boolean):表示真伪的两个特殊值,即true(真)和false(假)。
  • undefined:表示“未定义”或不存在,即由于目前没有定义,所以此处暂时没有任何值。
  • null:表示空值,即此处的值为空。
  • 对象(object):各种值组成的集合。

对象是最复杂的数据类型,又可以分成三个子类型。

  • 狭义的对象(object)
  • 数组(array)
  • 函数(function)

1. typeof 运算符

JavaScript 有三种方法,可以确定一个值到底是什么类型:

  • typeof运算符
  • instanceof运算符
  • Object.prototype.toString方法

typeof运算符可以返回一个值的数据类型。

数值、字符串、布尔值分别返回numberstringboolean,函数返回functionundefined返回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

一个变量被赋值为undefinednull,效果几乎等价,在if语句中,它们都会被自动转为false,相等运算符(==)甚至直接报告两者相等。

null是一个表示“空”的对象,转为数值时为0undefined是一个表示”此处无定义”的原始值,转为数值时为NaN

  • null表示空值,即该处的值现在为空。调用函数时,某个参数未设置任何值,这时就可以传入null,表示该参数为空。
  • undefined表示“未定义”,常见的场景有变量声明未定义,调用函数参数未提供,函数未返回值,对象没有赋值的属性;

3. 布尔值

如果 JavaScript 预期某个位置应该是布尔值,会将该位置上现有的值自动转为布尔值。转换规则是除了下面六个值被转为false,其他值都视为true

  • undefined
  • null
  • false
  • 0
  • NaN
  • ""''(空字符串)

注意,空数组([])和空对象({})对应的布尔值,都是true

4. 整数和浮点数

JavaScript 内部,所有数字都是以64位浮点数形式储存,即使整数也是如此。所以,11.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
2
3
4
5
6
7
8
Math.pow(2, 53)
// 9007199254740992

Math.pow(2, 53) + 1
// 9007199254740992

Math.pow(2, 53) + 2
// 9007199254740994

超过这个范围的计算就无法保持精度了,也就是对十进制的15位数能保持精度。

4.2 数值范围

64位浮点数的指数部分的值最大为2047,分出一半表示负数,则 JavaScript 能够表示的数值范围为2^1024^到2^-1023^(开区间),超出这个范围的数无法表示。

  • 大于等于2的1024次方,那么就会发生“正向溢出”,这时就会返回Infinity
  • 如果一个数小于等于2的-1075次方(指数部分最小值-1023,再加上小数部分的52位),会发生为“负向溢出”,会直接返回 0;

JavaScript 提供Number对象的MAX_VALUEMIN_VALUE属性,返回可以表示的具体的最大值和最小值。

4.3 数值的表示法

科学计数法允许字母eE的后面,跟着一个整数,表示这个数值的指数部分。以下情况会表示未科学计数法:

  • 小数点前的数字多于21位;
  • 小数点后的零多于5个;

4.4 数值的进制

使用字面量(literal)直接表示一个数值时,JavaScript 对整数提供四种进制的表示方法:十进制、十六进制、八进制、二进制。

  • 十进制:没有前导0的数值。
  • 八进制:有前缀0o0O的数值,或者有前导0、且只用到0-7的八个阿拉伯数字的数值。
  • 十六进制:有前缀0x0X的数值。
  • 二进制:有前缀0b0B的数值。

默认情况下,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
2
NaN === NaN // false
NaN + 2 // NaN

Infinity表示“无穷”,用来表示两种场景。一种是一个正的数值太大,或一个负的数值太小,无法表示;另一种是非 0 数值除以 0,得到Infinity

Infinity大于一切数值(除了NaN),-Infinity小于一切数值(除了NaN),InfinityNaN比较,总是返回false

0 乘以Infinity,返回NaN;0除以Infinity,返回0Infinity除以0,返回Infinity

Infinitynull计算时,null会转成0,等同于与0的计算,Infinityundefined计算,返回的都是NaN

6. 字符串

单引号字符串的内部,可以使用双引号。双引号字符串的内部,可以使用单引号,同种引号包含需带上\转义;

由于 HTML 语言的属性值使用双引号,所以很多项目约定 JavaScript 语言的字符串只使用单引号;

字符串默认只能写在一行内,分成多行将会报错,可以在每一行的尾部使用反斜杠换行;

字符串可以被视为字符数组,因此可以使用数组的方括号运算符,用来返回某个位置的字符。

与数组的相似性仅此而已,字符串内部的单个字符无法改变和增删;

JavaScript 使用 Unicode 字符集。JavaScript 引擎内部,所有字符都用 Unicode 表示。

每个字符在 JavaScript 内部都是以16位(即2个字节)的 UTF-16 格式储存。也就是说,JavaScript 的单位字符长度固定为16位长度,即2个字节

但是,UTF-16 有两种长度:对于码点在U+0000U+FFFF之间的字符,长度为16位(即2个字节);对于码点在U+10000U+10FFFF之间的字符,长度为32位(即4个字节),而且前两个字节在0xD8000xDBFF之间,后两个字节在0xDC000xDFFF之间。举例来说,码点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
2
3
4
5
6
7
8
var obj = {
p1: 1,
p2: 2,
};
with (obj) {
p1 = 4;
p2 = 5;
}

注意,如果with区块内部有变量的赋值操作,必须是当前对象已经存在的属性,否则会创造一个当前作用域的全局变量。

1
2
3
4
5
6
7
8
var obj = {};
with (obj) {
p1 = 4;
p2 = 5;
}
/* 对象obj并没有p1属性,对p1赋值等于创造了一个全局变量p1 */
obj.p1 // undefined
p1 // 4

因为with区块没有改变作用域,它的内部依然是当前作用域。这造成了with语句的一个很大的弊病,就是绑定对象不明确;因此,建议不要使用with语句。

8. 函数

8.1 声明方法

(1)function 命令

function命令声明的代码区块,就是一个函数。function命令后面是函数名,函数名后面是一对圆括号,里面是传入函数的参数。函数体放在大括号里面。

1
2
3
function print(s) {
console.log(s);
}

上面的代码命名了一个print函数,以后使用print()这种形式,就可以调用相应的代码。这叫做函数的声明(Function Declaration)。

(2)函数表达式

除了用function命令声明函数,还可以采用变量赋值的写法。

1
2
3
var print = function(s) {
console.log(s);
};

这种写法将一个匿名函数赋值给变量。这时,这个匿名函数又称函数表达式(Function Expression),因为赋值语句的等号右侧只能放表达式。

采用函数表达式声明函数时,function命令后面不带有函数名。如果加上函数名,该函数名只在函数体内部有效,在函数体外部无效。

1
2
3
4
5
6
7
var print = function x(){
console.log(typeof x);
};
x
// ReferenceError: x is not defined
print()
// function

这种写法的用处有两个,一是可以在函数体内部调用自身,二是方便除错(除错工具显示函数调用栈时,将显示函数名,而不再显示这里是一个匿名函数)。因此,下面的形式声明函数也非常常见。

1
var f = function f() {};

需要注意的是,函数的表达式需要在语句的结尾加上分号,表示语句结束。而函数的声明在结尾的大括号后面不用加分号。总的来说,这两种声明函数的方式,差别很细微,可以近似认为是等价的。

(3)Function 构造函数

通过传递任意数量的参数给Function构造函数,只有最后一个参数会被当做函数体,如果只有一个参数,该参数就是函数体,这种声明函数的方式非常不直观,几乎无人使用。

8.2 函数提升

如果同一个函数被多次声明,后面的声明就会覆盖前面的声明。

函数可以调用自身,这就是递归(recursion)。

JavaScript 语言将函数看作一种值,只是一个可以执行的值,此外并无特殊之处;

JavaScript 引擎将函数名视同变量名,所以采用function命令声明函数时,整个函数会像变量声明一样,被提升到代码头部;

function命令和var赋值语句声明同一个函数,由于存在函数提升,最后会采用var赋值语句的定义。

1
2
3
4
5
6
7
8
9
10
11
console.log(f); // function f() { console.log('2');}
f() // 2
var f = function () {
console.log('1');
}

function f() {
console.log('2');
}

f() // 1

函数提升要比变量提升的优先级要高一些,且不会被变量声明覆盖,但是会被变量赋值之后覆盖。

函数的name属性返回函数的名字,如果是通过变量赋值定义的(匿名)函数,那么name属性返回变量名;

函数的length属性返回函数预期传入的参数个数,函数的toString()方法返回一个字符串,内容是函数的源码

8.3 作用域

作用域(scope)指的是变量存在的范围。在 ES5 的规范中,JavaScript 只有两种作用域:

  • 全局作用域,变量在整个程序中一直存在,所有地方都可以读取;
  • 函数作用域,变量只在函数内部存在;

对于顶层函数来说,函数外部声明的变量就是全局变量(global variable),它可以在函数内部读取;

在函数内部定义的变量,外部无法读取,称为“局部变量”(local variable);

函数内部定义的变量,会在该作用域内覆盖同名全局变量;

对于var命令来说,局部变量只能在函数内部声明,在其他区块中声明,一律都是全局变量;

8.4 函数内部的变量提升

var命令声明的变量,不管在什么位置,变量声明都会被提升到函数体的头部

1
2
3
4
5
6
7
8
9
10
11
12
13
function foo(x) {
if (x > 100) {
var tmp = x - 100;
}
}

// 等同于
function foo(x) {
var tmp;
if (x > 100) {
tmp = x - 100;
};
}

8.5 函数本身的作用域

函数本身也是一个值,也有自己的作用域。它的作用域与变量一样,就是其声明时所在的作用域,与其运行时所在的作用域无关,也就是说函数执行时所在的作用域,是定义时的作用域,而不是调用时所在的作用域:

1
2
3
4
5
6
7
8
9
10
11
var a = 1;
var x = function () {
console.log(a);
};

function f() {
var a = 2;
x();
}

f() // 1

上面代码中,函数x是在函数f的外部声明的,所以它的作用域绑定外层,内部变量a不会到函数f体内取值,所以输出1,而不是2

同样的,函数体内部声明的函数,作用域绑定函数体内部。

1
2
3
4
5
6
7
8
9
10
11
function foo() {
var x = 1;
function bar() {
console.log(x);
}
return bar;
}

var x = 2;
var f = foo();
f() // 1

8.6 函数的参数

函数参数不是必需的,JavaScript 允许省略参数,省略的参数的值就变为undefined

函数参数如果是原始类型的值(数值、字符串、布尔值),传递方式是传值传递(passes by value)。这意味着,在函数体内修改参数值,不会影响到函数外部。

如果函数参数是复合类型的值(数组、对象、其他函数),传递方式是传址传递(pass by reference)。也就是说,传入函数的原始值的地址,因此在函数内部修改参数,将会影响到原始值。

如果函数内部修改的,不是参数对象的某个属性,而是替换掉整个参数,这时不会影响到原始值。

如果有同名的参数,则取最后出现的那个值,这时,如果要获得前面的值,可以使用arguments对象,例如:

1
2
3
4
5
function f(a, a) {
console.log(arguments[0]);
}

f(1) // 1

8.7 闭包

在理解了变量作用域之后,就能很快理解闭包了。

正常情况下,函数外部无法读取函数内部声明的变量。

1
2
3
4
5
6
function f1() {
var n = 999;
}

console.log(n)
// Uncaught ReferenceError: n is not defined

那就是在函数的内部,再定义一个函数:

1
2
3
4
5
6
function f1() {
var n = 999;
function f2() {
  console.log(n); // 999
}
}

f2就在函数f1内部,这时f1内部的所有局部变量,对f2都是可见的。既然f2可以读取f1的局部变量,那么只要把f2作为返回值,就可以在f1外部读取它的内部变量了。

由于在 JavaScript 语言中,只有函数内部的子函数才能读取内部变量,因此可以把闭包简单理解成“定义在一个函数内部的函数”。闭包最大的特点,就是它可以“记住”诞生的环境;

闭包的最大用处有两个,一个是可以读取外层函数内部的变量,另一个就是让这些变量始终保持在内存中,即闭包可以使得它诞生环境一直存在。请看下面的例子,闭包使得内部变量记住上一次调用时的运算结果。

1
2
3
4
5
6
7
8
9
10
11
function createIncrementor(start) {
return function () {
return start++;
};
}

var inc = createIncrementor(5);

inc() // 5
inc() // 6
inc() // 7

上面代码中,start是函数createIncrementor的内部变量。通过闭包,start的状态被保留了,每一次调用都是在上一次调用的基础上进行计算。从中可以看到,闭包inc使得函数createIncrementor的内部环境,一直存在。所以,闭包可以看作是函数内部作用域的一个接口。

为什么闭包能够返回外层函数的内部变量?原因是闭包用到了外层变量(start),导致外层函数(createIncrementor)不能从内存释放。只要闭包没有被垃圾回收机制清除,外层函数提供的运行环境也不会被清除,它的内部变量就始终保存着当前值,供闭包读取。

闭包的另一个用处,是封装对象的私有属性和私有方法;

外层函数每次运行,都会生成一个新的闭包,而这个闭包又会保留外层函数的内部变量,所以内存消耗很大。因此不能滥用闭包,否则会造成网页的性能问题。

8.8 立即调用的函数表达式(IIFE)

JavaScript 的语法,圆括号()跟在函数名之后,表示调用该函数。

在定义函数之后,立即调用该函数,会产生语法错误;当作表达式时,函数可以定义后直接加圆括号调用:

1
2
3
4
5
function(){ /* code */ }();
// SyntaxError: Unexpected token

var f = function f(){ return 1}();
f // 1

为了避免解析的歧义,JavaScript 规定,如果function关键字出现在行首,一律解释成语句。因此,引擎看到行首是function关键字之后,认为这一段都是函数的定义,不应该以圆括号结尾,所以就报错了。

函数定义后立即调用的解决方法,就是不要让function出现在行首,让引擎将其理解成一个表达式。最简单的处理,就是将其放在一个圆括号里面:

1
2
3
(function(){ /* code */ }());
// 或者
(function(){ /* code */ })();

这就叫做“立即调用的函数表达式”(Immediately-Invoked Function Expression),简称 IIFE。

上述代码若没有分号隔开,JavaScript 会将它们连在一起解释,将第二行解释为第一行的参数

通常情况下,只对匿名函数使用这种“立即执行的函数表达式”。它的目的有两个:

  • 一是不必为函数命名,避免了污染全局变量;

  • 二是 IIFE 内部形成了一个单独的作用域,可以封装一些外部无法读取的私有变量。

8.9 eval 命令

eval命令接受一个字符串作为参数,并将这个字符串当作语句执行。

eval没有自己的作用域,都在当前作用域内执行,因此可能会修改当前作用域的变量的值,造成安全问题。

为了防止这种风险,JavaScript 规定,如果使用严格模式,eval内部声明的变量,不会影响到外部作用域

1
2
3
4
5
(function f() {
'use strict';
eval('var foo = 123');
console.log(foo); // ReferenceError: foo is not defined
})()

严格模式下,eval内部还是改写了外部变量,可见安全风险依然存在。

总之,eval的本质是在当前作用域之中,注入代码。由于安全风险和不利于 JavaScript 引擎优化执行速度,一般不推荐使用。通常情况下,eval最常见的场合是解析 JSON 数据的字符串,不过正确的做法应该是使用原生的JSON.parse方法。

1
2
3
var m = eval;
m('var x = 1');
x // 1

上面代码中,变量meval的别名。静态代码分析阶段,引擎分辨不出m('var x = 1')执行的是eval命令。

为了保证eval的别名不影响代码优化,JavaScript 的标准规定,凡是使用别名执行evaleval内部一律是全局作用域。

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
2
var a = [1, , 1];
a.length // 3

上面代码表明,数组的空位不影响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

  • 两个复合类型(对象、数组、函数)的数据比较时,不是比较它们的值是否相等,而是比较它们是否指向同一个地址;

  • undefinednull与自身严格相等,变量声明后默认值是undefined,因此两个只声明未赋值的变量是相等的;

相等运算符==用来比较相同类型的数据时,与严格相等运算符完全一样。

  • 原始类型的值会转换成数值再进行比较;
  • 对象(这里指广义的对象,包括数组和函数)与原始类型的值比较时,对象转换成原始类型的值,再进行比较;
  • undefinednull只有与自身比较,或者互相比较时,才会返回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变为11变为0)。它的返回结果有时比较难理解,因为涉及到计算机内部的数值表示机制。

因为涉及到反码,简单可以记忆为,一个数取否,等于 -1 减这个数;

对一个小数连续进行两次二进制否运算,能达到取整效果,而且是所有取整中最快的

其他类型的值取否,会先调用Number函数,将字符串转为数值

“异或运算”有一个特殊运用,连续对两个数ab进行三次异或运算,a^=b; b^=a; a^=b;,可以互换它们的值,这是互换两个变量的值的最快方法

如果左移0位,就相当于将该数值转为32位整数,等同于取整,对于正数和负数都有效;

头部补零的右移运算符(>>>)头部一律补零,而对于正数,该运算的结果与右移运算符(>>)完全一致,区别主要在于负数。

查看一个负整数在计算机内部的储存形式,最快的方法就是使用这个运算符。

1
-1 >>> 0 // 4294967295

上面代码表示,-1作为32位整数时,内部的储存形式使用无符号整数格式解读,值为 4294967295(即(2^32)-1,等于11111111111111111111111111111111)。

4. 特殊运算符

void运算符的作用是执行一个表达式,然后不返回任何值,或者说返回undefined

1
2
void 0 // undefined
void(0) // undefined

上面是void运算符的两种写法,都正确,第二种比较稳妥,因为 void优先级高,容易出错:

这个运算符的主要用途是浏览器的书签工具(Bookmarklet),以及在超级链接中插入代码防止网页跳转。

逗号运算符用于对两个表达式求值,并返回后一个表达式的值,逗号运算符的一个用途是,在返回一个值之前,进行一些辅助操作。

圆括号(())可以用来提高运算的优先级,因为它的优先级是最高的,即圆括号中的表达式会第一个运算。

注意,因为圆括号不是运算符,所以不具有求值作用,只改变运算的优先级。

JavaScript 语言的大多数运算符是“左结合”,少数运算符是“右结合”,其中最主要的是赋值运算符(=)和三元条件运算符(?:)。

1
2
w = x = y = z;
q = a ? b : c ? d : e ? f : g;

上面代码的解释方式如下。

1
2
w = (x = (y = z));
q = a ? b : (c ? d : (e ? f : g));

上面的两行代码,都是右侧的运算数结合在一起。

另外,指数运算符(**)也是右结合。

1
2
3
2 ** 3 ** 2
// 相当于 2 ** (3 ** 2)
// 512

数据类型转换

1. 强制转换

只要有一个字符无法转成数值,整个字符串就会被转为NaN,而parseInt逐个解析字符;

1
2
3
4
5
Number('324abc') // NaN
Number('') // 0
Number(true) // 1
Number(false) // 0
Number(undefined) // NaN

Number()方法的参数是对象时,将返回NaN,除非是包含单个数值的数组;

String函数可以将任意类型的值转化成字符串:

1
2
3
4
5
String(123) // "123"
String('abc') // "abc"
String(true) // "true"
String(undefined) // "undefined"
String(null) // "null

String()方法的参数如果是对象,返回一个类型字符串;如果是数组,返回该数组的字符串形式:

1
2
String({a: 1}) // "[object Object]"
String([1, 2, 3]) // "1,2,3"

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
2
3
4
5
6
7
function UserError(message) {
this.message = message || '默认信息';
this.name = 'UserError';
}

UserError.prototype = new Error();
UserError.prototype.constructor = UserError;

上面代码自定义一个错误对象UserError,让它继承Error对象;

2. 处理错误语法

throw语句的作用是手动中断程序执行,抛出一个错误,实际上,可以抛出任何类型的值;

JavaScript 提供了try...catch结构,允许对错误进行处理,选择是否往下执行

try代码块抛出的错误,被catch代码块捕获后,程序会继续向下执行;

catch代码块之中,还可以再抛出错误,甚至使用嵌套的try...catch结构;

为了捕捉不同类型的错误,catch代码块之中可以加入判断语句;

try...catch结构允许在最后添加一个finally代码块,表示不管是否出现错误,都必需在最后运行的语句;

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
function f() {
try {
console.log(0);
throw 'bug';
} catch(e) {
console.log(1);
return true; // 这句原本会延迟到 finally 代码块结束再执行
console.log(2); // 不会运行
} finally {
console.log(3);
return false; // 这句会覆盖掉前面那句 return
console.log(4); // 不会运行
}

console.log(5); // 不会运行
}

var result = f();
// 0
// 1
// 3

result
// false

catch代码块之中,触发转入finally代码块的标志,不仅有return语句,还有throw语句;

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
function f() {
try {
throw '出错了!';
} catch(e) {
console.log('捕捉到内部错误');
throw e; // 这句原本会等到finally结束再执行
} finally {
return false; // 直接返回
}
}

try {
f();
} catch(e) {
// 此处不会执行
console.log('caught outer "bogus"');
}

// 捕捉到内部错误

面代码中,进入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
    19
    function 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
2
3
4
5
6
7
8
9
10
11
console.log(' %s + %s = %s', 1, 1, 2)

console.log({foo: 'bar'})
// Object {foo: "bar"}
console.log(Date)
// function Date() { [native code] }

console.log(
'%cThis text is styled!',
'color: red; background: yellow; font-size: 24px;'
)
  • %o 对象的链接
  • %c CSS 格式字符串

console.infoconsole.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.groupconsole.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登记了回调函数的各种事件(比如clickkeydown),每个事件对应一个数组,数组的成员为该事件的回调函数;
  • monitorEvents(object[, events])方法监听特定对象上发生的特定事件。事件发生时,会返回一个Event对象,包含该事件的相关信息。unmonitorEvents方法用于停止监听;

11. debugger

debugger语句主要用于除错,作用是设置断点。如果有正在运行的除错工具,程序运行到debugger语句时会自动停下。如果没有除错工具,debugger语句不会产生任何结果,JavaScript 引擎自动跳过这一句。

Chrome 浏览器中,当代码运行到debugger语句时,就会暂停运行,自动打开脚本源码界面。