攻下《JavaScript高级程序设计》——第三章 基本概念 (下)

156 阅读37分钟

我们接着上一篇讲。

5.操作符

操作符,就是操作数据值的一组符号,其中有算术操作符,位操作符,关系操作符和相等操作符。

5.1 一元操作符

只能操作一个值的操作符叫做一元操作符。

  1. 递增递减操作符

递增即++,递减即--,这个借鉴于C语言,有前置型和后置型。如下:

var num = 18;
++num; // 前置型,位于变量前
--num; // 前置型递减
num++; // 后置型,位于变量后
num--; // 后置递减

使用递增递减操作符会给一个数值加1或减1,但前置型递增或递减操作符和后置型有一个非常重要的区别,我们先来段代码看一下:

var numFront = 2;
++numFront;
console.log(numFront); // 3

var numEnd = 2;
numEnd++;
console.log(numEnd); // 3

到这里,都是一样的,不管是前置型还是后置型,都使得操作符操作都变量值递增加1,那我们再看看下面的代码:

var numFront = 2;
var numFrontResult = ++numFront;
console.log(numFront); // 3
console.log(numFrontResult); // 3

var numEnd = 2;
var numEndResult = numEnd++;
console.log(numEnd); // 3
console.log(numEndResult); // 2

现在区别出现了,原因是后置递增递减操作符,递增递减操作是在包含它们的语句被请求之后才执行的。原话有点绕,其实意思就是,如果语句中存在后置递增递减操作符,那么先完成改语句的其他操作,最后再执行递增递减。来看下面这段代码:

var numEnd1 = 2;
var numEndResult1 = numEnd1++;
console.log(numEnd1); // 3
console.log(numEndResult1); // 2 先执行赋值操作,即先将numEnd1的值赋给numEndResult1,然后再执行numEnd1的递增操作

var numEnd2 = 1;
var numEnd3 = 10;
var numEndResult = numEnd2-- + numEnd3;
console.log(numEnd2); // 0
console.log(numEndResult); // 11 先执行numEnd2 + numEnd3加法操作,然后再执行numEnd2的递增操作

我们经常讲这四个操作符用于Number类型数值的递增递减,但其实这几个操作符适用于任何值,即还可以用于字符串、布尔值、浮点数和对象。应用于其他类型值时,会先将这些值转换为数字值,即相当于使用Number()函数,然后再将返回的值进行递增递减操作。如下:

var num1 = "1234";
num1++; // 1235

var num2 = "123gg";
++num2; // NaN

var num3 = false;
num3++; // 1

var num4 = {
    valueOf: function() {
        return -1;
    }
}
num4--; // -2
  1. 一元加和减操作符

一元加操作符以一个加号(+)表示,放在数值前面,对数值不产生任何影响。如下:

var num = 1;
num = +num; // 1

一元加操作这样用对数值没有任何影响,它相当于数学里的正号(+),那它的存在有什么用呢?

若一元加操作应用于非数值时,该操作符会像Number()转型函数一样对这个值进行转换。所以大家可以将这个一元加操作看做非数值到数值转换的操作,如:

var num1 = "01234";
num1 = +num1; // 1234

var num2 = "123gg";
num2 = +num2; // NaN

var num3 = false;
num3 = +num3; // 0

var num4 = {
    valueOf: function() {
        return -1;
    }
}
num4 = +num4; // -1

而一元减操作符主要是用来表示负号,如下:

var num = 1
num = -num; // -1

一元减操作符应用于非数值时,也会先将其转换为数值,然后再取其负值,如:

var num1 = "01234";
num1 = -num1; // -1234

var num2 = "123gg";
num2 = -num2; // NaN

var num3 = false;
num3 = -num3; // -0 (特殊值,后面会讲到,有+0和-0)

var num4 = {
    valueOf: function() {
        return -1;
    }
}
num4 = -num4; // 1

var num5 = true;
num5 = -num5; // -1

5.2 位操作符

位操作符用于最基本的层次上,也就是在最底层操作,速度比一般的操作会更快,它是按照内存中表示数值的位来操作数值,这里我们就要开始结合一些计算原理的知识点了。前面我们讲过ECMAScript的数值存储方式是IEEE754 64位格式存储,但位操作符并不直接操作64位的值,而是先将64位值转换成32位的整数,然后再执行操作,再把结果转换成64位。

这里来回忆一下计算机基础知识。对于有符号的整数,32位中的最高位(即第32位)表示数值的符号,即我们所说的符号位(0表示正,1表示负),其他位数用于表示整数的值。例如数值18的二进制表示是00000000000000000000000000010010,这是正数的存储方式,负数则是以二进制的补码格式存储。还记得怎么计算一个数值的二进制补码吗?例如求-18:

  1. 先求出这个数值绝对值的二进制(也就是18的二进制:00000000000000000000000000010010);
  2. 求这个二进制的反码(即将1变成0,将0变成1:11111111111111111111111111101101);
  3. 得到的这个二进制反码加1(11111111111111111111111111101110)。

所以,-18的二进制表示为11111111111111111111111111101110。

但是这些只是底层运算,ECMAScript会尽力像我们隐藏所有这些信息,我们可以看一下下面这段代码:

var test = -22;
console.log(test.toString(2)); // 获取-22的二进制,结果是:-10110

上面我们把一个负数值转换成二进制时,ECMAScript并没有按照真正的存储方式给我们展现,而是在这个负数的绝对值二进制码前加了个负号,这样更加容易理解。

刚刚上面我们也说过了,ECMAScript的位操作符并不直接操作64位的值,而是先将64位值转换成32位的整数,然后再执行操作,再把32位的结果转换成64位。但是这个操作会导致NAN和Infinity值被当做0来处理。

下面我们来展开讨论几个位操作符:

5.2.1 按位非(NOT)

按位非操作符有一个(~)表示,执行按位非的结果就是返回数值的反码。下面直接来看例子:

var num1 = 25;    // 二进制00000000000000000000000000011001
var num2 = ~num1; // 二进制11111111111111111111111111100110
console.log(num2);// -26

我们在上面求一个负数的时候,是先拿到其绝对值的二进制,然后取反码,然后再加1。我们这里倒着看,按位非操作是不是直接将操作数的负值减1啦,也就是下面的代码:

var num1 = 25; 
var num2 = -num1 - 1; 
console.log(num2);// -26

5.2.2 按位与(AND)

按位与操作符由一个(&)表示,它有两个操作数,分别位于操作符左右。与运算大家都很熟悉啦,就是,两个数都是1的时候值才会返回1,任何一位是0 ,值都返回0。来直接看例子:

var num = 18 & 7;
console.log(num); //2

/*
 * 底层操作如下:
 *  18 = 0000 0000 0000 0000 0000 0000 0001 0010
 *   7 = 0000 0000 0000 0000 0000 0000 0000 0111
 * AND = 0000 0000 0000 0000 0000 0000 0000 0010
 */

5.2.3 按位或(OR)

按位或操作符由一个(|)表示,它也有两个操作数。或运算也不用多解释了,即有一个位是1结果就返回1,只有两个位都是0的情况下才返回0。还是拿18和7为例,我们看看下面或运算的结果:

var num = 18 | 7;
console.log(num); //23

/*
 * 底层操作如下:
 * 18 = 0000 0000 0000 0000 0000 0000 0001 0010
 *  7 = 0000 0000 0000 0000 0000 0000 0000 0111
 * OR = 0000 0000 0000 0000 0000 0000 0001 0111
 */

5.2.4 按位异或(XOR)

按位或操作符由一个(^)表示,也有两个操作数。异或异或,相同为0,不同为1。即两个数值对应位上只有一个1时才返回1,如果对应的两个位上都是0或者都是1,则返回0。依旧是18和7,我们来看看异或的处理:

var num = 18 ^ 7;
console.log(num); //21

/*
 * 底层操作如下:
 *  18 = 0000 0000 0000 0000 0000 0000 0001 0010
 *   7 = 0000 0000 0000 0000 0000 0000 0000 0111
 * XOR = 0000 0000 0000 0000 0000 0000 0001 0101
 */

5.2.5 左移

左移操作符由一个(<<)表示,这个操作符会将所有位向左移动指定的位数。例如:

var oldValue = 2;             // 二进制:0000 0000 0000 0000 0000 0000 0000 0010
var newValue = oldValue << 5; // 二进制:0000 0000 0000 0000 0000 0000 0100 0000  十进制是64

由上面的例子可以看出,第一,左移并不影响符号位,第二,左移后,右边的空位由符号位的值,即0补充。

5.2.6 有符号右移

有符号右移操作符由一个(>>)表示,这个操作符会将除符号位的所有位向右移动指定的位数。也就是说,有符号右移,符号位会保持原有位置不动,其余数值右移。那么很明显,有符号右移和左移的结果应该是恰恰相反,如下:

var oldValue = 64;             // 二进制:0000 0000 0000 0000 0000 0000 0100 0000
var newValue = oldValue >> 5;  // 二进制:0000 0000 0000 0000 0000 0000 0000 0010  十进制是2

由此可见,在有符号右移中,左侧因移动而多出来的空位依旧是补0(符号位的值)。

5.2.7 无符号右移

无符号右移操作符由一个(>>>)表示,这个操作符会将所有位向右移动指定的位数(包括符号位)。对于整数来说,无符号右移与有符号右移的结果相同,但是对于负数来说,情况就完全不一样了。

首先,无符号右移,无论符号位是什么,一律以0来填充空位(有符号右移是以符号位填充空位的),所以正数的无符号右移和有符号右移的结果一样,但是负数就不一样了。其次,无符号右移会把负数的二进制码当成正数的二进制码,直接来看例子:

var oldValue = -64;            // 二进制:1111 1111 1111 1111 1111 1111 1100 0000
var newValue = oldValue >>> 5; // 二进制:0000 0111 1111 1111 1111 1111 1111 1110  十进制:134217726

可以看出,无符号右移负数时会把结果变得非常大,因为它把负数的二进制码当成正数的二进制码来计算。

5.3 布尔操作符

布尔操作符一共有三个:非(NOT),与(AND)和或(OR)。

5.3.1 逻辑非 逻辑非操作符由一个 **!**表示,有一个操作数,可应用于ECMAScript中的任何值。无论这个值是什么数据类型,这个操作符都会返回一个布尔值。逻辑非操作会先将它的操作数转换为一个布尔值,然后求其反。各类型转换为布尔类型的规则在第三章的前半部分已经讲过,在这里就不赘述了。下面直接看例子:

console.log(!false); // true
console.log(!"false"); // false
console.log(!0); // true
console.log(!1); // false
console.log(!""); // true
console.log(!{}); //false
console.log(!NaN); // true
console.log(!undefined); // true
console.log(!null); // true

在第三章的前半部分我们讲到过用Boolean()函数来将一个值转换为布尔值。但其实,还有一种方法同样可行,即用两个逻辑非操作符(!!)。

举个例子:

console.log(!!false); // false
console.log(!!"false"); // true
console.log(!!0); // false
console.log(!!1); // true
console.log(!!""); // false
console.log(!!null); // false

道理很简单,负负得正。同样的,三个!的效果与一个!的效果相同,四个!的效果与两个!的效果相同,但是在实际写代码中我想应该没人用两个以上的逻辑非操作符吧……

5.3.2 逻辑与

逻辑与操作符由&&表示,有两个操作数,可作用于任何类型。逻辑与的特点是,两个操作数都为true,结果才是true,否则结果为false。如下:

var result1 = true && false; // false
var result2 = false && true; // false
var result3 = false && false; // false
var result4 = true && true; // true

在实际的编码中,我们常用逻辑与来做短路运算。从上面我们知道,两个操作符都是true的情况下,逻辑与的返回结果才是true。那么我们也可以这么说,如果有一个操作数是false,那么压根不用看另一个操作数我们就知道,这个逻辑与运算的结果是false。

所以说我们的代码在执行的时候,遇到逻辑与运算,如果第一个操作数是true,则会向后执行,如果第一个操作数是false,那么代码将不会向后执行(因为以及没有必要了)。 代码如下:

var test = true;
var result = test && someundefinedvariable; //  这里会报错

相反,再看这段代码:

var test = false;
var result = test && someundefinedvariable; //  false(someundefinedvariable不会执行)

逻辑与遵循下列规则,如果弄懂了上面的短路运算,下面的这些规则理解起来就很简单了:

  1. 如果第一个操作数是对象,则返回第二个操作数;
  2. 如果第二个操作数是对象,则第一个操作数的求值结果必须是true,该对象才会被返回;
  3. 如果两个操作数都是对象,返回第二个操作数
  4. 如果第一个操作数是null,则返回null;
  5. 如果第一个操作数是undefined,则返回undefined;
  6. 如果第一个操作数是NaN,则返回NaN;

在使用逻辑与操作时要始终铭记它是一个短路操作符。

5.3.3 逻辑或

逻辑或操作符由||表示,有两个操作数,可作用于任何类型。逻辑或的特点是,两个操作数都为false,结果才是false,否则结果为true,它与逻辑与恰恰相反。如下:

var result1 = true && false; // true
var result2 = false && true; // true
var result3 = false && false; // false
var result4 = true && true; // true

同样的,逻辑或也是短路操作符。由上可知,逻辑或运算中,只有两个操作数都为false结果才为false,有一方为true,则结果就是true。所以,若第一个操作数为true,那不用看第二个就知道,这个逻辑或的结果是true;但是,如果第一个操作符是false,则我们还需要继续排查第二个操作符的布尔值。

在代码中,也是这样的道理,遇到逻辑或运算,如果第一个操作数是true,则不会向后执行,如果第一个操作数是false,那么代码将向后执行。举个例子:

var test = true;
var result = test || someundefinedvariable; //  true

相反的:

var test = false;
var result = test || someundefinedvariable; // 报错

逻辑或遵循下列规则:

  1. 如果第一个操作数是对象,则返回第一个操作数;
  2. 如果第一个操作数是false,则返回第二个操作数;
  3. 如果两个操作数都是对象,返回第一个操作数
  4. 如果两个操作数都是null,则返回null;
  5. 如果两个操作数都是undefined,则返回undefined;
  6. 如果两个操作数都是NaN,则返回NaN;

我们一般利用逻辑或来避免为变量赋null或undefined值,如下面的代码:

var test;
var result = test || 0; // 0

上面代码中,test是undefined,布尔值是false,则result最后的取值是第二个操作数0。

5.4 乘性操作符

ECMAScript定义了3个乘性操作符:乘法,除法和求模。操作数在非数值的情况下会用Number()函数将其转换成数值再进行运算。

5.4.1 乘法

乘法操作符由一个*表示,用于计算两数值的乘积。如:

var result = 10 * 2; // 20

在处理特殊值时,乘法操作符遵循以下规则:

  1. 如果操作数都是数值,执行常规乘法运算,包括符号。若乘积超过了ECMAScript数值表示的范围,则返回Infinity或-Infinity;
  2. 如果有一个操作数是NaN,则运算结果是NaN;
  3. Infinity乘以0,结果是NaN
  4. Infinity乘以非零数,结果是Infinity或-Infinity
  5. Infinity乘以Infinity,结果是Infinity
  6. 若操作数不是数值,则后台调用Number()函数将其转换成数值,再应用上述规则。

5.4.2 除法 除法操作符由一个/表示,执行第二个操作数除第一个操作数的计算。如:

var result = 10 / 2; // 5

当然,除法操作符对特殊操作值也有以下规则:

  1. 如果操作数都是数值,执行常规除法运算,包括符号。若商超过了ECMAScript数值表示的范围,则返回Infinity或-Infinity;
  2. 如果有一个操作数是NaN,则运算结果是NaN;
  3. 0被0除,结果是NaN
  4. 非零有限数被0除,结果是Infinity或-Infinity
  5. Infinity被Infinity除,结果是NaN
  6. Infinity被任何非零有限数除,结果是Infinity或-Infinity
  7. 若操作数不是数值,则后台调用Number()函数将其转换成数值,再应用上述规则。

5.4.3 求模 求模(余数)操作符由一个%表示。如:

var result = 11 % 2; // 1

求模也有其规则,如下:

  1. 如果操作数都是数值,执行常规除法运算,返回其余数即可;
  2. Infinity被任何非零有限数除,结果是NaN
  3. 有限数被0除,结果是NaN
  4. Infinity被Infinity除,结果是NaN
  5. 有限数被Infinity除,结果是被除数,也就是该有限数
  6. 被除数是0,结果是0
  7. 若操作数不是数值,则后台调用Number()函数将其转换成数值,再应用上述规则。

5.5 加性操作符

ECMAScript定义了2个加性操作符:加法和减法。但这两个操作符除了处理算数操作之外,还有其他特殊的行为,相应的操作数数值转换规则也有些复杂。

5.5.1 加法

加法操作符的用法如下:

var result = 1 + 4; // 5

如果两个操作数都是数值,则按照常规的加法计算,规则如下:

  1. 若有一个操作数是NaN,则结果是NaN;
  2. 若Infinity加Infinity,则结果是Infinity;
  3. 若-Infinity加-Infinity,则结果是-Infinity;
  4. 若Infinity加-Infinity,则结果是NaN
  5. 若+0加+0,则结果是+0;
  6. 若-0加-0,则结果是-0;
  7. 若+0加-0,则结果是+0

以上是两数值的运算,但若有一个操作数是字符串,那结果就不一样了:

  1. 若两个操作数都是字符串,则进行字符串拼接;
  2. 若只有一个操作数是字符串,则将另一个操作数转换成字符串,然后进行字符串拼接。
  3. 如果有一个操作数是对象、数值、或者布尔值,则调用其toString()方法取得对应的字符串值,然后在应用上述关于字符串的规则。对于null和undefined,则分别调用其String()函数取得字符串"null"和"undefined"

举个栗子:

var result1 = 1 + 1; // 2
var result2 = 1 + "1"; // 11
var result3 = 1 + true; // 2
var result4 = 1 + {}; // 1
var result5 = '1' + true; // 1true

下面有个小练笔,来看看问题:

首先,计算下面message的值:

var num1 = 6;
var num2 = 6;
var num3 = 6;
var message = "我不禁对他说道:" + num1 + num2 + num3; 

message的最后结果是"我不禁对他说道:666",你答对了吗?那么我们再看下面的问题,同样计算message:

var num1 = 6;
var num2 = 6;
var num3 = 6;
var message = num1 + num2 + num3 + ",我不禁对他说道"; 

这个message的结果是"18,我不禁对他说道"。

第一段代码的结果,"我不禁对他说道:666",这是因为代码是从左往右依次执行,第一个+就是字符串拼接,结果得出字符串,再往后拼接。

而第二段代码的结果是,"18,我不禁对他说道"。首先,num1和num2的计算结果是数值,再和数值num3进行算数运算,再最后遇到字符串,进行拼接。

若现在,将第一段代码换成:

var num1 = 6;
var num2 = 6;
var num3 = 6;
var message = "我不禁对他说道:" + (num1 + num2 + num3); 

则message的最后结果为"我不禁对他说道:18",因为先计算括号中的内容。

5.5.2 减法

减法操作符用法如下:

var result = 2 - 1; // 1

与加法操作符类似,若两个操作符都是数值,则执行常规的算数减法。减法操作符遵循下面规则:

  1. 若有一个操作数是NaN,则结果是NaN;
  2. 若Infinity减Infinity,则结果是NaN
  3. 若-Infinity减-Infinity,则结果是NaN
  4. 若Infinity减-Infinity,则结果是Infinity
  5. 若-Infinity减Infinity,则结果是-Infinity
  6. 若+0减+0,则结果是+0
  7. 若-0减+0,则结果是-0
  8. 若-0减-0,则结果是+0
  9. 对象、数值、或者布尔值,null和undefined都是先调用了Number()函数,然后再根据上面的规则计算

以上是两数值的运算,但若有一个操作数是字符串,那结果就不一样了:

  1. 若两个操作数都是字符串,则进行字符串拼接;
  2. 若只有一个操作数是字符串,则将另一个操作数转换成字符串,然后进行字符串拼接。
  3. 字符串,布尔值,对象,null和undefined先调用Number()函数转换为数值,然后再根据上面的规则计算。

举几个例子:

var result1 = 5 - true; // 4(因为true转换成了1)
var result2 = 5 - "1"; // 4
var result3 = 5 - ""; // 5(因为""转换成了0)
var result4 = 5 - null; // 5(因为null转换成了0)
var result5 = 1 - NaN; // NaN

5.6 关系操作符

在ECMAScript中,有几个关系操作符:小于(<),大于(>),小于等于(<=)和大于等于(>=),这几个操作符都用于值的比较,并且和我们熟知的数学上的规则一模一样。运算的值返回布尔值。用法如下:

var result1 = 5 > 3; // true
var result2 = 5 < 3; // false

当关系操作符的操作数是非数值时,同样的,后台也会做相应的转换,转换规则如下:

  1. 如果两个操作数都是数值,执行数值比较;
  2. 如果两个操作数都是字符串,则比较两个字符串相对应的字符编码值;
  3. 如果一个操作数是数值,则将另一个操作数也转换成数值,然后再进行比较;
  4. 如果一个操作数是对象,则调用对象的valueOf()方法,用得到的结果按照前面的规则执行比较;若对象没有valueOf()方法,则调用toString()方法,并用得到的结果按照前面的规则进行比较;
  5. 如果一个操作数是布尔值,则先将其转换为数值,再执行比较;
  6. 任何操作数与NaN进行比较,都是false。

这里需要强调的一点是,如果两个操作数是字符串,那么使用关系操作数时,实际比较的是两个字符串中对应位置的每个字符串的字符编码。举个栗子:

var result = "23" < "3"; // true

上述的代码结果是true,原因是'2'的字符编码是50,而'3'的字符编码是51,50小于51,故"23" < "3"。

再如:

var result1 = "23" < 3; // false"23"转换为数值23
var result2 = "a" < 3; // false"a"转换为数值NaN
var result2 = "a" >= 3; // false;任何操作数与NaN进行比较,结果都是false

5.7 相等操作符

相等操作符在编程中非常非常常见,确定两个变量是否相等是一个非常非常重要的操作。我们都知道,ECMAScript中有相等和全等两个概念,其实在最早的ECMAScript中,在执行比较之前,都会将对象转换成相似的类型,在进行比较。后来有人提出了这种比较的合理性,最后,ECMAScript的结局方案是提供两组操作符:相等和不相等全等和不全等

5.7.1 相等和不相等

相等操作符由==表示,不相等操作符由!=表示。这两个操作符都会先转换操作数类型(强转换型),再比较它们的相等性,规则如下:

  1. 如果有一个操作数是布尔值,则在比较相等性之前先将其转换为数值;
  2. 如果一个操作数是字符串,另一个操作数是数值,那么先将字符串转换为数值,然后再进行比较;
  3. 如果一个操作数是对象,而另一个则不是,那么先调用对象的valueOf()方法,得到的基本类型值再进行上面的比较。

另外,以下几个值比较特殊,需要注意:

  1. null和undefined是相等的;
  2. 要比较相等性之前,不能将null和undefined转换成其他任何值;
  3. 如果有一个操作数是NaN,则相等操作符返回false,不相等操作符返回true(即使是NaN和NaN比较,也是同样的,它们不相等);
  4. 如果两个操作数都是对象,则比较它们是不是同一个对象。

有如下代码:

null == undefined; // true
NaN == NaN; // false
NaN == 1; // false
NaN != NaN; // true
false == 0; // true
true == 1; // true
true == 2; // false
undefined == 0; //  false
null == 0; // false
"1" == 1; // true

5.7.2 全等和不全等

全等在比较之前是不会转换操作数的,除此特点之外,全等和相等没有什么区别。全等操作符由===表示,不全等由!==表示。

可以这么说,其实全等就是相等再外加一个类型的判断。举几个例子:

"11" == 11; // true
"11" === 11; // false
null == undefined; // true
null === undefined; // false

为了在代码中保持数据类型的完整性,推荐编码时使用全等和不全等组合。

5.8 条件操作符

条件操作符也就是我们常说的三元表达式,用法如下:

var result = 1 < 5 ? '1 win' : '5 win' // '1 win'

如上面代码,程序先从?前面开始执行,结果若为true,则给变量赋予:左侧的值,否则给变量赋予:后面的值。

5.9 赋值操作符

我们最常见的赋值操作是=,即将右侧的值赋给左侧的变量,如下:

var result = 10;
console.log(result); // 10

除了最简单的等号赋值,我们还可以有复合赋值操作。即在=前面再添加一个乘性操作符或加性操作符或位操作符,相当于是运算的一种简写方式,如下:

var num = 10;
num = num + 10;

上面这段代码完全可以用复合赋值来代替:

var num = 10;
num += 10;

下面给出复合操作符:

  1. 乘/赋值(*=);
  2. 除/赋值(/=);
  3. 模/赋值(%=);
  4. 加/赋值(+=);
  5. 减/赋值(-=);
  6. 左移/赋值(<<=);
  7. 有符号右移/赋值(>>=);
  8. 无符号右移/赋值(>>>=);

注意,这些复合操作符不会带来任何性能的提升,主要目的是简化赋值操作。

5.10 逗号操作符

使用逗号操作符可以再一条语句中执行多个操作。可以用于多个变量的声明,或用于多个赋值语句,如下:

var num1 = 1, num2 = 2, num3 = 3;
var num = (1, 2, 3, 4, 5); // num最后等于5

上面的代码中,逗号用于赋值,但是逗号操作符总会返回表达式中的最后一项(在上面的例子中是5)。

6.语句

ECMAScript规定了以下几种语句:

6.1 if语句

if语句是大多数编程中最常用的一个语句。用法如下:

if (condition) statement1 else statement2

如上,当condition的布尔值为true时,执行statement1,当condition的布尔值为false时,执行statement2。但这里要注意的是,condition可以是一个任意表达式,这个表达式的值不一定是布尔值。但是后台会自动调用Boolean()函数,将表达式的结果转换为布尔值。

举个栗子:

var i = 8;
if (i > 5) {
    console.log('i大于5');
} else {
    console.log('i小于等于5');
}

很显然,上面的结果输出'i大于5'。

6.2 do-while语句

do-while语句是一种后测试循环语句,即只有在循环体中的代码执行后,才会测试出口条件。换句话说,在对条件表达式求值之前,循环体内的代码至少会被执行一次。这里直接给个例子:

var i = 0;
do {
    i += 2;
} while (i < 10)
console.log(i);

在上述代码中,先执行了一次循环体内的内容(即i += 2),然后再判断条件表达式。只要变量i的值小于10,循环会一直走下去。

由此我们也能得出一个结论:不管这样,在do-while语句中,循环体中的代码都至少会被执行一次。

6.3 while语句

while属于前测试循环语句,它和do-while不同,while语句在循环体内的代码被执行前会先进行出口条件求值。举个栗子:

var i = 0;
while (i < 10) {
    i += 2;
}
console.log(i);

从上面的代码我们也能看出,先判断表达式,表达式为true,再执行循环体。所以,在while语句中,循环体中的代码可能永远都不会被执行。

6.4 for语句

for语句存在极大的灵活性,大家也会发现这是在编码中最常用的一种循环语句。

for循环也是一种前测试循环语句,但它具有在执行循环之前初始化变量和定义循环后要执行的代码的能力。for循环的用法如下:

for (var i = 0; i < 10; i++) {
    console.log(i);
}

上面代码中,先定义了变量i,并给i赋值为0。然后判断,只有当条件表达式( i < 10)返回true时,才会进入for循环。for循环中,执行代码打印i,然后对循环后的表达式求值(即i++)。当条件表达式返回false时,不会进入循环。

for循环也可以用while循环来写,如下:

var i = 0;
while (i < 10) {
    console.log(i);
    i++;
}

for循环只是把与循环有关的代码集中到了一个位置,使用while循环做不到的,使用for循环也同样做不到。

此外,for语句中的初始化表达式,控制表达式和循环后表达式都是可选的。将这三个表达式全部省略,就会创造一个无限循环,如:

for (;;) {
    console.log('我很美');
}

下面代码也可以模拟出while循环,这里代码中只给出了控制表达式:

var i = 0;
for (; i < 10; ) {
    console.log(i);
    i++;
}

6.5 for-in语句

for-in 语句是一种精准的迭代语句,可以用来枚举对象的属性。用法如下:

var obj = {
    'name': 'Javascript',
    'age': '24',
    'sex': 'male'
}
for (var item in obj) {
i++
console.log(i)
    console.log(item)
}

上面的代码输出为:name, age, sex

上面的代码中,我们使用for-in循环来显示了对象obj中的所有属性。每次执行循环时,都会将obj对象中存在的一个属性名赋值给变量item。这个过程会一直持续到对象中的所有属性都被枚举一遍位置。

有两点需要注意的是:

  1. ECMAScript对象的属性是没有顺序的。因此,通过for-in循环输出的属性名的顺序是不可预测的。
  2. 以前,如果要迭代的对象是undefined或null,for-in语句会抛出错误。后来ECMAScript5更正了这一行为,对于这种情况不再抛出错误,只是不执行循环体。但我们在实际编码中还是应该先检测迭代对象。

6.6 label语句

使用label语句可以在代码中添加标签,以便将来使用。用法如下:

start: for (var i = 0; i < 10; i++) {
    console.log(i);
}

for (var i = 0 ; i < 10 ; i++){
             for (var j = 0 ; j < 10 ; j++){
                  if( i == 5 && j == 5 ){
                        break;
                  }
             num++;
             }
        }

上面代码中就定义了一个start标签将来可以由break或continue语句引用。加标签的语句一般都要与for语句等循环语句配合使用

来我们看个例子:

首先看普通循环:

var num = 0;
for (var i = 0; i < 10; i++) {
    for (var j = 0; j < 10; j++) {
        if (i == 5 && j == 5) {
            break;
        }
        num++;
    }
}
console.log(num);// 循环在 i 为5,j 为5的时候跳出 j循环,但会继续执行 i 循环,输出 95

那我们再来看配合标签使用的循环:

var num = 0;
test:
for (var i = 0; i < 10; i++) {
    for (var j = 0; j < 10; j++) {
        if (i == 5 && j == 5) {
            break test;
        }
        num++;
    }
}
console.log(num);// 循环在 i 为5,j 为5的时候跳出双循环,返回到outPoint层继续执行,输出 55

而配了label使用的continue是这样的:跳出当前循环,并跳转到test(标签)下的for循环继续执行:

var num = 0;
test:
for (var i = 0; i < 10; i++) {
    for (var j = 0; j < 10; j++) {
        if (i == 5 && j == 5) {
            continue test;
        }
        num++;
    }
}
console.log(num);// 循环在 i 为5,j 为5的时候跳出 j循环,继续执行 i 循环,输出 95(和不配合label使用的break效果一样)

6.7 break和continue语句

break和continue用于在循环中精准地控制代码的执行。其中,break会立即跳出循环,执行循环后面的代码;但continue只是结束本次循环,跳出循环后会从循环的顶部继续执行。所以,break是跳出循环,continue是跳出本次循环执行下次循环。用法如下:

先看break:

var num = 0;
for (var i = 0; i < 10; i++) {
    if (i === 5) {
        break;
    }
     num++;
}
console.log(num); // 5

由上面代码可以看出,该循环中i会由0递增到10,但是在循环体内部有一条件判断语句,若i等于5则跳出循环。所以代码在执行到i为5的时候直接跳出了循环,结束了i的递增。i从0到5,一共会执行6次循环,但是当i等于5时,直接跳出循环,循环里面的代码不会再执行了,所以num++执行5次,所以结果是5。

下面来看continue:

var num = 0;
for (var i = 0; i < 10; i++) {
    if (i === 5) {
        continue;
    }
     num++;
}
console.log(num); // 9

很明显,num++执行了9次。原因是,当i等于5时,由于continue的缘故,跳出本次循环,循环内代码不再执行。跳出后又开始下一次循环,也就是跳出i等于5的循环后,直接开始i等于6的循环。

在上面label语句中,我们也讲到过。当多层循环嵌套时,有时候continue或者break可以配合label语句来实现一些需求。例子就不再举了,有一点需要注意的是,虽然continue,break和label可以执行复杂的操作,但是如果使用过度,会给调试带来麻烦,所以在使用label标签时,一定要使用描述性的标签,也注意不要嵌套过多的循环。

6.8 with语句

with语句的作用是将代码的作用域设置到一个特定对象中。主要是为了简化多次编写同一个对象的工作。使用方法如下:

先看下面一段简单代码:

var qs = location.search.substring(1);
var hostName = location.hostName;
var url = location.href;

以上代码都包含location对象。如果使用with语句,可改为:

with (location) {
    var qs = search.substring(1);
    var hostName = hostName;
    var url = href;
}

有两点需要注意:

  1. 严格模式下不允许使用with语句,否则将视为语法错误;
  2. 大量使用with语句会导致性能下降,也会给调试代码造成困难。 因此建议大家在开发时,慎用with语句。

6.9 switch语句

switch语句是一种流控制语句。用法如下:

switch (i) {
    case 1:
        console.log('是1');
        break;
    case 2:
        console.log('是2');
        break;
    case 3:
        console.log('是3');
        break;
    default:
        console.log('不是123');
}

上面的代码等同于:

if (i === 1) {
    console.log('是1');
} else if (i === 2) {
    console.log('是2');
} else if (i === 3) {
    console.log('是3');
} else {
    console.log('不是123');
}

在上面的switch例子中,若表达式等于case后的值,则执行对应case下的代码(例如,若i的值是1,则执行输出“是1”),而break的意思是跳出switch语句。若这里省略了break,则执行完当前case后还会再执行下一个case。default关键字用于在表达式不匹配前面任何一种情形时,即相当于else。

在ECMAScript中,可以在switch语句中使用任何数据类型(这与其他语言不同,有些语言智能使用数值类型)。还有就是,每个case的值不一定是常量,也可以是变量或者是表达式。举个栗子:

switch ('hello world') {
    case 'hello' + 'world:
        console.log('正确答案!');
        break;
    case 'oh my god':
        console.log('错误答案');
        break;
    default:
        console.log('什么都不是');
}

上面的例子中就展示了字符串类型的判断,而在case中使用了字符串拼接的表达式。代码执行结果是“正确答案!”。 当然,我们还能这么用,下面再看个例子:

var num = 25;
switch (true) {
    case num < 0:
        console.log('负数');
        break;
    case num >= 0 && num <= 10:
        console.log('0到10之间');
        break;
    case num > 10 && num <= 20:
        console.log('10到20之间');
        break;
    default:
        console.log('大于20');
}

上例中,case的值就是表达式,并且使用了变量。

有一点需要注意的是,switch语句在比较时使用的事全等操作符

7.函数

函数是任何语言的核心概念,说白了,编程不都是围绕着函数转嘛。在ECMAScript中的函数通过function关键字来声明。 函数的使用方法没有什么特别的,在这里就不赘述了,上面的例子中也有。有一点需要提醒一下,当没有给函数指定返回值时,函数返回undefined。

这里需要注意下,严格模式对函数有一些要求:

  1. 不能把函数命名为eval或arguments;
  2. 不能把参数命名为eval或arguments;
  3. 不能出现两个参数同名的情况。

7.1 理解参数

我们在ECMAScript中传参时会发现,我们传参的数量和参数的类型都没有被限定。即不管一个函数在定义时接收多少参数,你在调用该函数时可以不管可接收的参数数量,你可以传少,也可以传更多,更可以不传。还有就是,这些参数的类型并不会给限制,传什么类型的参数都行。有的童鞋可以看出来啦,这与很多其他语言都不同。

上述的主要原因呢就在于ECMAScript的参数在内部是用一个数组表示的。函数在接收的时候,始终都是接收这么一个数组,无论数组中有无元素,也不管元素的类型,函数都是OK的。在函数内部,我们可以通过arguments对象来访问这个参数数组,从而获取我们传递的每个参数。

但是,严格来说,arguments并不是数组,而是一个类数组(也就是一个类似数组的对象,只是在这个对象中,key值为数组的索引值)。如下图,是一个arguments的结构:

我们可以通过arguments[0],arguments[1]……来依次访问我们传进来的参数,通过访问length属性,也可以知道我们传递了多少参数进来。我们再看下面这种例子:

上例中的test2函数并没有命名参数,但我们依旧可以正常工作。这就说明:命名的参数只提供便利,但并不是必须的。而且在ECMAScript中命名参数时并不需要函数签名,没有那些条条框框的东西。

还有就是,arguments对象可以和命名参数一起用,如下:

function test3(arg1, arg2) {
    console.log(arguments[0] + arg2) // 等价于arg1 + arg2
}

很有意思的是,arguments的值永远都会和对应命名参数的值保持一致。举个栗子:

function test4(arg1, arg2) {
    arguments[0] = 20
    console.log(arg1) // 20
}

不管传进来的第一个参数arg1是什么,每次test4()函数都会重写arg1参数,将其值改为20。因为arguments对象中的值会自动反映到对应命名参数。但是它们二者的内存空间确实独立的,不要以为两个值保持一致就访问的是同一内存空间啦~

但是!!!上面的这个赋值在严格模式下是无效的,重写arguments会报错。

那么,如果修改命名参数的值,arguments会做相应的变化嘛?答案是不会。在函数中修改命名参数的值不会影响arguments。

还有几点需要注意:

  1. 如果调用函数时只传递了一个参数,那么设置arguments[1]的值不会反映到命名函数中。因为arguments的长度由实际传进来的参数数量决定,而不是由定义函数时的命名参数数量决定
  2. ECMAScript中所有的参数传递都是直接传递值,而不可能像一些语言一样直接传递引用(即传递参数的地址)。
  3. 若命名参数没有被传递值,那么这个命名参数就是undefined。

7.2 没有重载

重载在很多语言中有,比如Java。它的意思是函数或者方法有相同的名称,但是参数不相同。上面我们说过函数签名,为一个函数编写两个定义,只要这两个定义的函数签名不同即可。

但是在ECMAScript中并不能实现重载,因为它没有函数签名,它的参数是由数组表示的。没有函数签名,就不能做到真正意义上的重载。

我们先来看个例子:

function test6(num) {
    console.log('num', num);
}

function test6(num) {
    console,log('double num', num * 2);
}

test6(2); // 4

由上面的例子我们可以看出,在ECMAScript中定义两个相同名字的函数,该函数名只属于后面一个函数。

第三章讲的是ECMAScript的基本概念,到这里就结束啦~~虽然有一丢丢长,但是也算是全面。这一章中只是大致介绍了一些概念,像函数啊对象啊这些复杂的概念之后会另起篇章细讲。开始下一章啦,加油加油!!!!