JavaScript高级程序设计(第四版)阅读笔记_第三章(下)(持续更新...)

270 阅读44分钟

JavaScript高级程序设计(第四版)阅读笔记

第三章_语言基础

本文重点

  • ECMAScript有6种简单数据类型:Undefined、Null、Boolean、Number、 String 和 Symbol。不同的数据类型是有自己的特殊属性和方法的。前面的5种平时大家应该都有接触,所以重点看一下Symbol,下面的文章里面挂了链接,可以好好研究一下这个新增的数据类型。
  • 不同的数据类型可以根据typeOf来判断(书里面目前只写了这一种方法,还有很多的判断方法。)
  • (1)typeof是一个操作符,其右侧跟一个一元表达式,并返回这个表达式的数据类型。返回的结果用该类型的字符串(全小写字母)形式表示,包括number,string,boolean,undefined,object,function,symbol等。
  • (2)instanceof用来判断A是否为B的实例,表达式为:A instanceof B,如果A是B的实例,则返回true,否则返回false。instanceof检测的是原型,内部机制是通过判断对象的原型链中是否有类型的原型。
  • (3)当一个函数F被定义时,JS引擎会为F添加prototype原型,然后在prototype上添加一个constructor属性,并让其指向F的引用,F利用原型对象的constructor属性引用了自身,当F作为构造函数创建对象时,原型上的constructor属性被遗传到了新创建的对象上,从原型链角度讲,构造函数F就是新对象的类型。这样做的意义是,让对象诞生以后,就具有可追溯的数据类型。
  • (4)Object.prototype.toString() , toString()是Object的原型方法,调用该方法,默认返回当前对象的[[Class]]。这是一个内部属性,其格式为[object Xxx],其中Xxx就是对象的类型。对于Object对象,直接调用toString()就能返回[object Object],而对于其他对象,则需要通过call、apply来调用才能返回正确的类型信息。
  • JS 的操作符分为了很多种,实际从我觉得常用的大家都比较熟悉,但是这里面的位操作符对我来说确实比较新奇,之前一直没明白这个概念,这次才刚学到。
  • JavaScript的语句也有很多,其中大多数都是我们平时用的,但我个人平时很少用label,break,continue这些,所以看的时候也要注意了一下。
  • JavaScript的函数这一章只是简单的说了一下,并没有详解,应该在后面的章节会有一个详细的学习吧。

3.4 数据类型

ECMAScript有 6种简单数据类型(也称为原始类型):Undefined、Null、Boolean、Number、 String 和 Symbol。Symbol(符号)是 ECMAScript 6新增的。还有一种复杂数据类型叫 Object(对 象)。Object 是一种无序名值对的集合。因为在 ECMAScript中不能定义自己的数据类型,所有值都可以用上述 7种数据类型之一来表示。只有 7种数据类型似乎不足以表示全部数据。但 ECMAScript的数 据类型很灵活,一种数据类型可以当作多种数据类型来使用。

3.4.1 typeof 操作符

确定任意变量的数据类型。 对一个值使用 typeof 操作符会返回下列字符串之一:

  • "undefined"表示值未定义;
  • "boolean"表示值为布尔值;
  • "string"表示值为字符串;
  • "number"表示值为数值;
  • "object"表示值为对象(而不是函数)或 null;
  • "function"表示值为函数;
  • "symbol"表示值为符号

注意,因为 typeof 是一个操作符而不是函数,所以不需要参数(但可以使用参数)。 typeof在某些情况下返回的结果可能会让人费解,但技术上讲还是正确的。比如,调用typeof null 返回的是"object"。这是因为特殊值 null 被认为是一个对空对象的引用。

3.4.2 Undefined 类型

Undefined 类型只有一个值,就是特殊值 undefined。当使用 var 或 let 声明了变量但没有初始化时,就相当于给变量赋予了 undefined 值(因为默认情况下,任何未经初始化的变量都会取得 undefined 值 ):

let message; 
console.log(message == undefined); // true 

包含 undefined 值的变量跟未定义变量是有区别的。请看下面的例子:

let message;    // 这个变量被声明了,只是值为 undefined 
// 确保没有声明过这个变量 
// let age 
console.log(message); // "undefined" 
console.log(age);     // 报错

在对未初始化的变量调用 typeof 时,返回的结果是"undefined",但对未声明的变量调用它时, 返回的结果还是"undefined",这就有点让人看不懂了。比如下面的例子:

let message; // 这个变量被声明了,只是值为 undefined 
// 确保没有声明过这个变量 
// let age 
console.log(typeof message); // "undefined"
console.log(typeof age);     // "undefined" 

无论是声明还是未声明,typeof 返回的都是字符串"undefined"。逻辑上讲这是对的,因为虽然严格来讲这两个变量存在根本性差异,但它们都无法执行实际操作。

3.4.3 Null 类型

Null 类型同样只有一个值,即特殊值 null。逻辑上讲,null 值表示一个空对象指针,这也是给 typeof 传一个 null 会返回"object"的原因:

let car = null;
console.log(typeof car);  // "object" 

在定义将来要保存对象值的变量时,建议使用 null 来初始化,不要使用其他值。 这样,只要检查这个变量的值是不是 null 就可以知道这个变量是否在后来被重新赋予了一个对象的引用,比如:

if (car != null) { 
// car 是一个对象的引用 
} 

undefined 值是由 null 值派生而来的,因此 ECMA-262将它们定义为表面上相等,如下面的例子所示:

console.log(null == undefined);  // true

用等于操作符(==)比较 null 和 undefined 始终返回 true。 永远不必显式地将变量值设置为 undefined。但 null 不是这样的。任何时候,只要变量要保存对象,而当时又没有那个对象可保存,就要用 null 来填充该变量。这样就可以保持 null 是空对象指针的语义,并进一步将其与 undefined 区分开来。

3.4.4 Boolean 类型

Boolean(布尔值)类型是 ECMAScript中使用频繁的类型之一,有两个字面值:true 和 false。 这两个布尔值不同于数值,因此 true 不等于 1,false 不等于 0。

注意,布尔值字面量 true 和 false 是区分大小写的,因此 True 和 False(及其他大小混写形式) 是有效的标识符,但不是布尔值。

要将一个其他类型的值转换为布尔值,可以调用特定的 Boolean()转型函数:

let message = "Hello world!";
let messageAsBoolean = Boolean(message);

Boolean()转型函数可以在任意类型的数据上调用,而且始终返回一个布尔值。什么值能转换为 true 或 false 的规则取决于数据类型和实际的值。下表总结了不同类型与布尔值之间的转换规则。

3.4.5 Number 类型

Number 类型使用 IEEE 754格式表示整数和浮点值(在某些语言中也叫双精度值)。不同的数值类型相应地也有不同的数值字面量格式。

基本的数值字面量格式是十进制整数,使用八进制和十六进制格式创建的数值在所有数学操作中都被视为十进制数值。由于 JavaScript保存数值的方式,实际中可能存在正零(+0)和负零(0)。正零和 负零在所有情况下都被认为是等同的。

1. 浮点值

要定义浮点值,数值中必须包含小数点,而且小数点后面必须至少有一个数字。虽然小数点前面不 是必须有整数,但推荐加上。下面是几个例子:

let floatNum1 = 1.1; 
let floatNum2 = 0.1; 
let floatNum3 = .1;   // 有效,但不推荐 

因为存储浮点值使用的内存空间是存储整数值的两倍,所以 ECMAScript总是想方设法把值转换为整数。在小数点后面没有数字的情况下,数值就会变成整数。类似地,如果数值本身就是整数,只是小数点后面跟着 0(如 1.0),那它也会被转换为整数,如下例所示:

let floatNum1 = 1.;   // 小数点后面没有数字,当成整数 1 处理 
let floatNum2 = 10.0; // 小数点后面是零,当成整数 10 处理 

对于非常大或非常小的数值,浮点值可以用科学记数法来表示。科学记数法用于表示一个应该乘以 10的给定次幂的数值。ECMAScript中科学记数法的格式要求是一个数值(整数或浮点数)后跟一个大写或小写的字母 e,再加上一个要乘的 10的多少次幂。比如:

let floatNum = 3.125e7; // 等于 31250000 

浮点值的精确度高可达 17位小数,但在算术计算中远不如整数精确。例如,0.1加 0.2得到的不是 0.3,而是 0.300 000 000 000 000 04。由于这种微小的舍入错误,导致很难测试特定的浮点值。 常有的面试题会根据这一点来进行出题

2. 值的范围

由于内存的限制,ECMAScript并不支持表示这个世界上的所有数值。ECMAScript可以表示的小数值保存在 Number.MIN_VALUE 中,这个值在多数浏览器中是 5e-324;可以表示的大数值保存在 Number.MAX_VALUE 中,这个值在多数浏览器中是 1.797 693 134 862 315 7e+308。如果某个计算得到的 数值结果超出了 JavaScript 可以表示的范围,那么这个数值会被自动转换为一个特殊的 Infinity(无穷)值。任何无法表示的负数以-Infinity(负无穷大)表示,任何无法表示的正数以 Infinity(正无穷大)表示。

如果计算返回正 Infinity 或负 Infinity,则该值将不能再进一步用于任何计算。这是因为 Infinity 没有可用于计算的数值表示形式。要确定一个值是不是有限大(即介于 JavaScript 能表示的 小值和大值之间),可以使用 isFinite()函数,如下所示:

let result = Number.MAX_VALUE + Number.MAX_VALUE; 
console.log(isFinite(result));  // false

虽然超出有限数值范围的计算并不多见,但总归还是有可能的。因此在计算非常大或非常小的数值时,有必要监测一下计算结果是否超出范围。

3. NaN

有一个特殊的数值叫 NaN,意思是“不是数值”(Not a Number),用于表示本来要返回数值的操作失败了(而不是抛出错误)。比如,用 0 除任意数值在其他语言中通常都会导致错误,从而中止代码执行。但在 ECMAScript中,0、+0或-0相除会返回 NaN:

console.log(0/0);    // NaN 
console.log(-0/+0);  // NaN 

如果分子是非 0值,分母是有符号 0或无符号 0,则会返回 Infinity 或-Infinity:

console.log(5/0);   // Infinity 
console.log(5/-0);  // -Infinity 

NaN 有几个独特的属性。首先,任何涉及 NaN 的操作始终返回 NaN(如 NaN/10),在连续多步计算时这可能是个问题。其次,NaN 不等于包括 NaN 在内的任何值

console.log(NaN == NaN); // false 

为此,ECMAScript提供了 isNaN()函数。该函数接收一个参数,可以是任意数据类型,然后判断这个参数是否“不是数值”。把一个值传给 isNaN()后,该函数会尝试把它转换为数值。某些非数值的值可以直接转换成数值,如字符串"10"或布尔值。任何不能转换为数值的值都会导致这个函数返回 true。举例如下:

console.log(isNaN(NaN));     // true 
console.log(isNaN(10));      // false,10 是数值 
console.log(isNaN("10"));    // false,可以转换为数值 10 
console.log(isNaN("blue"));  // true,不可以转换为数值 
console.log(isNaN(true));    // false,可以转换为数值 1

虽然不常见,但 isNaN()可以用于测试对象。此时,首先会调用对象的 valueOf() 方法,然后再确定返回的值是否可以转换为数值。如果不能,再调用 toString()方法, 并测试其返回值。

4. 数值转换

有 3个函数可以将非数值转换为数值:Number()、parseInt()和 parseFloat()。Number()是转型函数,可用于任何数据类型。后两个函数主要用于将字符串转换为数值。对于同样的参数,这 3个 函数执行的操作也不同。 Number()函数基于如下规则执行转换。

  • 布尔值,true 转换为 1,false 转换为 0。
  • 数值,直接返回。
  • null,返回 0。
  • undefined,返回 NaN。
  • 字符串,应用以下规则:
    • 如果字符串包含数值字符,包括数值字符前面带加、减号的情况,则转换为一个十进制数值。 因此,Number("1")返回 1,Number("123")返回 123,Number("011")返回 11(忽略前面 的零)。
    • 如果字符串包含有效的浮点值格式如"1.1",则会转换为相应的浮点值(同样,忽略前面的零)。
    • 如果字符串包含有效的十六进制格式如"0xf",则会转换为与该十六进制值对应的十进制整 数值。
    • 如果是空字符串(不包含字符),则返回 0。
    • 如果字符串包含除上述情况之外的其他字符,则返回 NaN。
  • 对象,调用 valueOf()方法,并按照上述规则转换返回的值。如果转换结果是 NaN,则调用 toString()方法,再按照转换字符串的规则转换。 下面是几个具体的例子:
let num1 = Number("Hello world!");  // NaN 
let num2 = Number("");              // 0 
let num3 = Number("000011");        // 11 
let num4 = Number(true);            // 1

考虑到用 Number()函数转换字符串时相对复杂且有点反常规,通常在需要得到整数时可以优先使 用 parseInt()函数。parseInt()函数更专注于字符串是否包含数值模式。字符串前面的空格会被忽略,从第一个非空格字符开始转换。如果第一个字符不是数值字符、加号或减号,parseInt()立即返回 NaN。这意味着空字符串也会返回 NaN(这一点跟 Number()不一样,它返回 0)。如果第一个字符 是数值字符、加号或减号,则继续依次检测每个字符,直到字符串末尾,或碰到非数值字符。比如, "1234blue"会被转换为 1234,因为"blue"会被完全忽略。类似地,"22.5"会被转换为 22,因为小数点不是有效的整数字符。

假设字符串中的第一个字符是数值字符,parseInt()函数也能识别不同的整数格式(十进制、八 进制、十六进制)。换句话说,如果字符串以"0x"开头,就会被解释为十六进制整数。如果字符串以"0" 开头,且紧跟着数值字符,在非严格模式下会被某些实现解释为八进制整数。下面是一个例子:

let num1 = parseInt("1234blue");  // 1234 
let num2 = parseInt("");          // NaN
let num3 = parseInt("0xA");       // 10,解释为十六进制整数 
let num4 = parseInt(22.5);        // 22 
let num5 = parseInt("70");        // 70,解释为十进制值 
let num6 = parseInt("0xf");       // 15,解释为十六进制整数 

不同的数值格式很容易混淆,因此 parseInt()也接收第二个参数,用于指定底数(进制数)。如果提供了进制参数,那么字符串前面的"0x"等标识可以省掉:

let num = parseInt("0xAF", 16); // 175 
let num1 = parseInt("AF", 16);  // 175 
let num2 = parseInt("AF");      // NaN 

parseFloat()函数的工作方式跟 parseInt()函数类似,都是从位置 0开始检测每个字符。同样, 它也是解析到字符串末尾或者解析到一个无效的浮点数值字符为止。这意味着第一次出现的小数点是有效的,但第二次出现的小数点就无效了,此时字符串的剩余字符都会被忽略。因此,"22.34.5"将转换 成 22.34。 parseFloat()函数的另一个不同之处在于,它始终忽略字符串开头的零。这个函数能识别前面讨论的所有浮点格式,以及十进制格式(开头的零始终被忽略)。十六进制数值始终会返回 0。因为 parseFloat()只解析十进制值,因此不能指定底数。最后,如果字符串表示整数(没有小数点或者小数点后面只有一个零),则 parseFloat()返回整数。

let num1 = parseFloat("1234blue");  // 1234,按整数解析 
let num2 = parseFloat("0xA");       // 0 
let num3 = parseFloat("22.5");      // 22.5 
let num4 = parseFloat("22.34.5");   // 22.34 
let num5 = parseFloat("0908.5");    // 908.5 
let num6 = parseFloat("3.125e7");   // 31250000

3.4.6 String 类型

String(字符串)数据类型表示零或多个 16位 Unicode字符序列。字符串可以使用双引号(")、 单引号(')或反引号(`)标示,因此下面的代码都是合法的:

let firstName = "John";
let lastName = 'Jacob'; 
let lastName = `Jingleheimerschmidt`

以某种引号作为字符串开头,必须仍然以该种引号作为字符串结尾。

1. 字符字面量

字符串数据类型包含一些字符字面量,用于表示非打印字符或有其他用途的字符,如下表所示:

这些字符字面量可以出现在字符串中的任意位置,且可以作为单个字符被解释,字符串的长度可以通过其 length 属性获取:

let text = "This is the letter sigma: \u03a3.";
console.log(text.length); // 28 

2. 字符串的特点

ECMAScript中的字符串是不可变的(immutable),意思是一旦创建,它们的值就不能变了。要修改某个变量中的字符串值,必须先销毁原始的字符串,然后将包含新值的另一个字符串保存到该变量,如下所示:

let lang = "Java"; 
lang = lang + "Script"; 

这里,变量 lang 一开始包含字符串"Java"。紧接着,lang 被重新定义为包含"Java"和"Script" 的组合,也就是"JavaScript"。整个过程首先会分配一个足够容纳 10 个字符的空间,然后填充上 "Java"和"Script"。后销毁原始的字符串"Java"和字符串"Script",因为这两个字符串都没有用了。

3. 转换为字符串

有两种方式把一个值转换为字符串。首先是使用几乎所有值都有的 toString()方法。这个方法唯一的用途就是返回当前值的字符串等价物。比如:

let age = 11; 
let ageAsString = age.toString();      // 字符串"11" 
let found = true;
let foundAsString = found.toString();  // 字符串"true" 

toString()方法可见于数值、布尔值、对象和字符串值。(没错,字符串值也有 toString()方法, 该方法只是简单地返回自身的一个副本。)null 和 undefined 值没有 toString()方法。

多数情况下,toString()不接收任何参数。不过,在对数值调用这个方法时,toString()可以接收一个底数参数,即以什么底数来输出数值的字符串表示。默认情况下,toString()返回数值的十进制字符串表示。而通过传入参数,可以得到数值的二进制、八进制、十六进制,或者其他任何有效基数的字符串表示,默认情况下(不传参数)的输出与传入参数 10得到的结果相同。 比如:

let num = 10; 
console.log(num.toString());     // "10"
console.log(num.toString(2));    // "1010"
console.log(num.toString(8));    // "12"
console.log(num.toString(10));   // "10" 
console.log(num.toString(16));   // "a"

如果你不确定一个值是不是 null 或 undefined,可以使用 String()转型函数,它始终会返回表示相应类型值的字符串。String()函数遵循如下规则。

  • 如果值有 toString()方法,则调用该方法(不传参数)并返回结果。
  • 如果值是 null,返回"null"。
  • 如果值是 undefined,返回"undefined"。

4. 模板字面量

ECMAScript 6新增了使用模板字面量定义字符串的能力。与使用单引号或双引号不同,模板字面量保留换行字符,可以跨行定义字符串(由于模板字面量会保持反引号内部的空格,因此在使用时要格外注意。格式正确的模板字符串看起来可能会缩进不当):

let myMultiLineString = 'first line\nsecond line'; 
let myMultiLineTemplateLiteral = `first line second line`; 
 
console.log(myMultiLineString); 
// first line
// second line" 
 
console.log(myMultiLineTemplateLiteral); 
// first line 
// second line 
 
console.log(myMultiLineString === myMultiLinetemplateLiteral); // true

5. 字符串插值

模板字面量常用的一个特性是支持字符串插值,也就是可以在一个连续定义中插入一个或多个 值。技术上讲,模板字面量不是字符串,而是一种特殊的 JavaScript 句法表达式,只不过求值后得到的是字符串。所有插入的值都会使用 toString()强制转型为字符串,而且任何 JavaScript表达式都可以用于插值。嵌套的模板字符串无须转义。

let value = 5; 
let exponent = 'second'; 
// 以前,字符串插值是这样实现的:
let interpolatedString =   value + ' to the ' + exponent + ' power is ' + (value * value); 
 
// 现在,可以用模板字面量这样实现:
let interpolatedTemplateLiteral =   `${ value } to the ${ exponent } power is ${ value * value }`; 
 
console.log(interpolatedString);           // 5 to the second power is 25 
console.log(interpolatedTemplateLiteral);  // 5 to the second power is 25 

6. 模板字面量标签函数

模板字面量也支持定义标签函数(tag function),而通过标签函数可以自定义插值行为。标签函数 会接收被插值记号分隔后的模板和对每个表达式求值的结果。

let a = 6; 
let b = 9;  
 
function simpleTag(strings, aValExpression, bValExpression, sumExpression) {  
console.log(strings);  
console.log(aValExpression); 
console.log(bValExpression);  
console.log(sumExpression); 
return 'foobar'; 
} 
 
let untaggedResult = `${ a } + ${ b } = ${ a + b }`; 
let taggedResult = simpleTag`${ a } + ${ b } = ${ a + b }`; // ["", " + ", " = ", ""] // 6 // 9 // 15 
 
console.log(untaggedResult);   // "6 + 9 = 15"
console.log(taggedResult);     // "foobar" 

7. 原始字符串

使用模板字面量也可以直接获取原始的模板字面量内容(如换行符或 Unicode 字符),而不是被转换后的字符表示。为此,可以使用默认的 String.raw 标签函数:

// Unicode 示例
// \u00A9 是版权符号 
console.log(`\u00A9`);            // © 
console.log(String.raw`\u00A9`);  // \u00A9 
 
// 换行符示例 
console.log(`first line\nsecond line`);
// first line 
// second line 
 
console.log(String.raw`first line\nsecond line`); // "first line\nsecond line" 
 
// 对实际的换行符来说是不行的 // 它们不会被转换成转义序列的形式
console.log(`first line second line`); // first line // second line 
 
console.log(String.raw`first line second line`); // first line // second line 

3.4.7 Symbol 类型

Symbol(符号)是 ECMAScript 6新增的数据类型。符号是原始值,且符号实例是唯一、不可变的。 符号的用途是确保对象属性使用唯一标识符,不会发生属性冲突的危险。

尽管听起来跟私有属性有点类似,但符号并不是为了提供私有属性的行为才增加的(尤其是因为 Object API提供了方法,可以更方便地发现符号属性)。相反,符号就是用来创建唯一记号,进而用作非字符串形式的对象属性。

1. 符号的基本用法

符号需要使用 Symbol()函数初始化。因为符号本身是原始类型,所以 typeof 操作符对符号返回 symbol。

let sym = Symbol();
console.log(typeof sym); // symbol

调用 Symbol()函数时,也可以传入一个字符串参数作为对符号的描述(description),将来可以通过这个字符串来调试代码。但是,这个字符串参数与符号定义或标识完全无关:

let genericSymbol = Symbol();
let otherGenericSymbol = Symbol(); 
 
let fooSymbol = Symbol('foo'); 
let otherFooSymbol = Symbol('foo'); 
 
console.log(genericSymbol == otherGenericSymbol);  // false 
console.log(fooSymbol == otherFooSymbol);          // false 

符号没有字面量语法,这也是它们发挥作用的关键。创建 Symbol()实例并将其用作对象的新属性,就可以保证它不会覆盖已有的对象属性,无论是符号属性还是字符串属性。

let genericSymbol = Symbol(); 
console.log(genericSymbol);  // Symbol() 
 
let fooSymbol = Symbol('foo'); 
console.log(fooSymbol);      // Symbol(foo);

重要的是,Symbol()函数不能与 new 关键字一起作为构造函数使用。这样做是为了避免创建符号包装对象,像使用 Boolean、String 或 Number 那样,它们都支持构造函数且可用于初始化包含原始值的包装对象。

let myBoolean = new Boolean(); 
console.log(typeof myBoolean); // "object" 
 
let myString = new String();
console.log(typeof myString);  // "object" 
 
let myNumber = new Number(); 
console.log(typeof myNumber);  // "object" 
 
let mySymbol = new Symbol(); // TypeError: Symbol is not a constructor 
// 如果你确实想使用符号包装对象,可以借用 Object()函数: 
let mySymbol = Symbol();
let myWrappedSymbol = Object(mySymbol); 
console.log(typeof myWrappedSymbol);   // "object" 

2. 使用全局符号注册表

如果运行时的不同部分需要共享和重用符号实例,那么可以用一个字符串作为键,在全局符号注册表中创建并重用符号。 为此,需要使用 Symbol.for()方法:

let fooGlobalSymbol = Symbol.for('foo');
console.log(typeof fooGlobalSymbol); // symbol 

Symbol.for()对每个字符串键都执行幂等操作。第一次使用某个字符串调用时,它会检查全局运行时注册表,发现不存在对应的符号,于是就会生成一个新符号实例并添加到注册表中。后续使用相同字符串的调用同样会检查注册表,发现存在与该字符串对应的符号,然后就会返回该符号实例。

let fooGlobalSymbol = Symbol.for('foo');       // 创建新符号 
let otherFooGlobalSymbol = Symbol.for('foo');  // 重用已有符号 
 
console.log(fooGlobalSymbol === otherFooGlobalSymbol);  // true 

let localSymbol = Symbol('foo'); 
let globalSymbol = Symbol.for('foo'); 
 
console.log(localSymbol === globalSymbol); // false 

全局注册表中的符号必须使用字符串键来创建,因此作为参数传给 Symbol.for()的任何值都会被转换为字符串。此外,注册表中使用的键同时也会被用作符号描述。

let emptyGlobalSymbol = Symbol.for(); 
console.log(emptyGlobalSymbol);    // Symbol(undefined)

还可以使用 Symbol.keyFor()来查询全局注册表,这个方法接收符号,返回该全局符号对应的字符串键。如果查询的不是全局符号,则返回 undefined。

// 创建全局符号
let s = Symbol.for('foo');
console.log(Symbol.keyFor(s));   // foo 
 
// 创建普通符号
let s2 = Symbol('bar');
console.log(Symbol.keyFor(s2));  // undefined 
//如果传给 Symbol.keyFor()的不是符号,则该方法抛出 TypeError: 
Symbol.keyFor(123); // TypeError: 123 is not a symbol 

3. 使用符号作为属性

凡是可以使用字符串或数值作为属性的地方,都可以使用符号。这就包括了对象字面量属性和 Object.defineProperty()/Object.defineProperties()定义的属性。对象字面量只能在计算属性语法中使用符号作为属性。

let s1 = Symbol('foo'),   
s2 = Symbol('bar'),   
s3 = Symbol('baz'),  
s4 = Symbol('qux'); 
 
let o = {   [s1]: 'foo val' };
// 这样也可以:o[s1] = 'foo val'; 
 
console.log(o); 
// {Symbol(foo): foo val} 
 
Object.defineProperty(o, s2, {value: 'bar val'}); 
 
console.log(o); 
// {Symbol(foo): foo val, Symbol(bar): bar val} 
 
Object.defineProperties(o, {  
[s3]: {value: 'baz val'}, 
[s4]: {value: 'qux val'}
}); 
 
console.log(o); 
// {Symbol(foo): foo val, Symbol(bar): bar val, 
//  Symbol(baz): baz val, Symbol(qux): qux val} 

类似于Object.getOwnPropertyNames()返回对象实例的常规属性数组,Object.getOwnPropertySymbols()返回对象实例的符号属性数组。这两个方法的返回值彼此互斥。Object.getOwnPropertyDescriptors()会返回同时包含常规和符号属性描述符的对象。Reflect.ownKeys()会返回两种类型的键:

let s1 = Symbol('foo'),  
s2 = Symbol('bar');  
 
let o = {   
[s1]: 'foo val',  
[s2]: 'bar val',  
baz: 'baz val',   
qux: 'qux val' 
}; 
 
console.log(Object.getOwnPropertySymbols(o)); 
// [Symbol(foo), Symbol(bar)] 
 
console.log(Object.getOwnPropertyNames(o));
// ["baz", "qux"] 
 
console.log(Object.getOwnPropertyDescriptors(o)); 
// {baz: {...}, qux: {...}, Symbol(foo): {...}, Symbol(bar): {...}} 
 
console.log(Reflect.ownKeys(o)); 
// ["baz", "qux", Symbol(foo), Symbol(bar)] 

因为符号属性是对内存中符号的一个引用,所以直接创建并用作属性的符号不会丢失。但是,如果 没有显式地保存对这些属性的引用,那么必须遍历对象的所有符号属性才能找到相应的属性键(也就是找到相应的属性):

let o = {   
[Symbol('foo')]: 'foo val', 
[Symbol('bar')]: 'bar val'
}; 
 
console.log(o); 
// {Symbol(foo): "foo val", Symbol(bar): "bar val"} 
 
let barSymbol = Object.getOwnPropertySymbols(o).find((symbol) => symbol.toString().match(/bar/)); 
 
console.log(barSymbol); // Symbol(bar)

4. 常用内置符号

ECMAScript 6也引入了一批常用内置符号(well-known symbol),用于暴露语言内部行为,开发者可以直接访问、重写或模拟这些行为。这些内置符号都以 Symbol 工厂函数字符串属性的形式存在。 这些内置符号重要的用途之一是重新定义它们,从而改变原生结构的行为。比如,我们知道 for-of 循环会在相关对象上使用 Symbol.iterator 属性,那么就可以通过在自定义对象上重新定义 Symbol.iterator 的值,来改变 for-of 在迭代该对象时的行为。 这些内置符号也没有什么特别之处,它们就是全局函数 Symbol 的普通字符串属性,指向一个符号的实例。所有内置符号属性都是不可写、不可枚举、不可配置的

Symbol的其他属性和方法我觉得这里比较详细。而这本书上的我确实有点看不懂。 Symbol详解

3.4.8 Object 类型

ECMAScript中的对象其实就是一组数据和功能的集合。对象通过 new 操作符后跟对象类型的名称来创建。开发者可以通过创建 Object 类型的实例来创建自己的对象,然后再给对象添加属性和方法:

let o = new Object(); 
let o = new Object;  // 合法,但不推荐 

每个 Object 实例都有如下属性和方法。

  • constructor:用于创建当前对象的函数。在前面的例子中,这个属性的值就是 Object() 函数。
  • hasOwnProperty(propertyName):用于判断当前对象实例(不是原型)上是否存在给定的属性。要检查的属性名必须是字符串(如 o.hasOwnProperty("name"))或符号。
  • isPrototypeOf(object):用于判断当前对象是否为另一个对象的原型。(第 8章将详细介绍原型。)
  • propertyIsEnumerable(propertyName):用于判断给定的属性是否可以使用(本章稍后讨论的)for-in 语句枚举。与 hasOwnProperty()一样,属性名必须是字符串。
  • toLocaleString():返回对象的字符串表示,该字符串反映对象所在的本地化执行环境。
  • toString():返回对象的字符串表示。
  • valueOf():返回对象对应的字符串、数值或布尔值表示。通常与 toString()的返回值相同。

因为在 ECMAScript中 Object 是所有对象的基类,所以任何对象都有这些属性和方法。

3.5 操作符

3.5.1 一元操作符

只操作一个值的操作符叫一元操作符(unary operator)。一元操作符是ECMAScript中简单的操作符。

1. 递增/递减操作符

递增和递减操作符直接照搬自 C语言,但有两个版本:前版和后版。顾名思义,前版就是位于要操作的变量前头,后版就是位于要操作的变量后头。后版与前版的主要区别在于,后版递增和递减在语句被求值后才发生

递增和递减操作符遵循如下规则。

  • 对于字符串,如果是有效的数值形式,则转换为数值再应用改变。变量类型从字符串变成数值。
  • 对于字符串,如果不是有效的数值形式,则将变量的值设置为 NaN 。变量类型从字符串变成数值。
  • 对于布尔值,如果是 false,则转换为 0再应用改变。变量类型从布尔值变成数值。
  • 对于布尔值,如果是 true,则转换为 1再应用改变。变量类型从布尔值变成数值。
  • 对于浮点值,加 1或减 1。
  • 如果是对象,则调用其(第 5 章会详细介绍的)valueOf()方法取得可以操作的值。对得到的值应用上述规则。如果是 NaN,则调用 toString()并再次应用其他规则。变量类型从对象变成数值。
let num1 = 2; 
let num2 = 20;
let num3 = num1-- + num2; 
let num4 = num1 + num2;
let num5 = ++num1 + num2; 
console.log(num3);  //22
console.log(num4);  // 21 
console.log(num5);  // 22

let s1 = "2"; 
let s2 = "z"; 
let b = false;
let f = 1.1; 
let o = {   valueOf() {     return -1;    } }; 
 
s1++;  // 值变成数值 3 
s2++;  // 值变成 NaN 
b++;   // 值变成数值 1 
f--;   // 值变成 0.10000000000000009(因为浮点数不精确) 
o--;   // 值变成-2 

2. 一元加和减

一元加由一个加号(+)表示,放在变量前头,对数值没有任何影响。一元减由一个减号(-)表示,放在变量前头,主要用于把数值变成负值,如把 1 转换为-1。

let s1 = "01";
let s2 = "1.1";
let s3 = "z";
let b = false;
let f = 1.1;
let o = {    valueOf() {     return -1;   } }; 
 
s1 = -s1;  // 值变成数值-1 
s2 = -s2;  // 值变成数值-1.1 
s3 = -s3;  // 值变成 NaN 
b = -b;    // 值变成数值 0 
f = -f;    // 变成-1.1 
o = -o;    // 值变成数值 1

3.5.2 位操作符

ECMAScript 中的所有数值都以 IEEE-754 64 位格式存储,但位操作符并不直接操作 64 位的值。而是先将 64 位的值转换成 32 位的整数,然后执行操作,最后再将结果转换回 64 位。对于有符号的整数,32 位中的前 31 位用于表示整数的值。第 32 位用于表示数值的符号:0 表示正数,1 表示负数。这个表示符号的位叫做符号位,符号位的值决定了其他位数值的格式。 数值 18 的二进制表示是 00000000000000000000000000010010 负数同样以二进制码存储,但使用的格式是二进制补码。计算一个数值的二进制补码,需要经过下 列 3 个步骤:

  1. 求这个数值绝对值的二进制码(例如,要求18 的二进制补码,先求 18 的二进制码);
  2. 求二进制反码,即将 0 替换为 1,将 1 替换为 0;
  3. 得到的二进制反码加 1

要根据这 3 个步骤求得-18 的二进制码,首先就要求得 18 的二进制码,即:
0000 0000 0000 0000 0000 0000 0001 0010
然后,求其二进制反码,即 0 和 1 互换:
1111 1111 1111 1111 1111 1111 1110 1101
最后,二进制反码加 1:

这样,就求得了-18 的二进制表示,即 11111111111111111111111111101110。要注意的是,在处理有符号整数时,是不能访问位 31 的。 在以二进制字符串形式输出一个负数时, 我们看到的只是这个负数绝对值的二进制码前面加上了一个负号。

var num = -18;
alert(num.toString(2)); // "-10010"

要把数值-18 转换成二进制字符串时,得到的结果是"-10010"。

1.按位非(NOT)

按位非操作符由一个波浪线(~)表示,执行按位非的结果就是返回数值的反码。

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

2. 按位与(AND)

按位与操作符由一个和号字符(&)表示,它有两个操作符数。 按位与操作只在两个数值的对应位都是 1 时才返回 1,任何一位是 0,结果都是 0。

var result = 25 & 3;
alert(result); //1

可见,对 25 和 3 执行按位与操作的结果是 1。为什么呢?请看其底层操作:

3. 按位或(OR)

按位或操作符由一个竖线符号(|)表示,同样也有两个操作数。 按位或操作在有一个位是 1 的情况下就返回 1,而只有在两个位都是 0 的情况下才返回 0。

var result = 25 | 3;
alert(result); //27

这两个数值的都包含 4 个 1,因此可以把每个 1 直接放到结果中。二进制码 11011 等于十进制值 27。

4. 按位异或(XOR)

按位异或操作符由一个插入符号(^)表示,也有两个操作数。 按位异或与按位或的不同之处在于,这个操作在两个数值对应位上只有一个 1 时才返回 1,如果对应的两位都是 1 或都是 0,则返回 0。(相同为0,不同为1)

var result = 25 ^ 3;
alert(result); //26

这两个数值都包含 4 个 1,但第一位上则都是 1,因此结果的第一位变成了 0。而其他位上的 1 在另 一个数值中都没有对应的 1,可以直接放到结果中。二进制码 11010 等于十进制值 26(注意这个结果比执行按位或时小 1)。

5.左移

左移操作符由两个小于号(<<)表示,这个操作符会将数值的所有位向左移动指定的位数。

var oldValue = 2; // 等于二进制的 10
var newValue = oldValue << 5; // 等于二进制的 1000000,十进制的 64

注意,在向左移位后,原数值的右侧多出了 5 个空位。左移操作会以 0 来填充这些空位,以便得到的结果是一个完整的 32 位二进制数。

注意,左移不会影响操作数的符号位。换句话说,如果将-2 向左移动 5 位,结果将是-64,而非 64。

6. 有符号的右移

有符号的右移操作符由两个大于号(>>)表示,这个操作符会将数值向右移动,但保留符号位(即正负号标记)。有符号的右移操作与左移操作恰好相反,即如果将 64 向右移动 5 位,结果将变回 2:

var oldValue = 64; // 等于二进制的 1000000
var newValue = oldValue >> 5; // 等于二进制的 10 ,即十进制的 2

7. 无符号右移

无符号右移操作符由 3 个大于号(>>>)表示,这个操作符会将数值的所有 32 位都向右移动。对正数来说,无符号右移的结果与有符号右移相同。仍以前面有符号右移的代码为例,如果将 64 无符号右移 5 位,结果仍然还是 2:

var oldValue = 64; // 等于二进制的 1000000
var newValue = oldValue >>> 5; // 等于二进制的 10 ,即十进制的 2

无符号右移操作符会把负数的二进制码当成正数的二进制码。而且,由于负数以其绝对值的二进制补码形式表示,因此就会导致无符号右移后的结果非常之大,如下面的例子所示:

var oldValue = -64; // 等于二进制的 11111111111111111111111111000000
var newValue = oldValue >>> 5; // 等于十进制的 134217726

这里,当对-64 执行无符号右移 5 位的操作后,得到的结果是 134217726。之所以结果如此之大, 是因为-64 的二进制码为 11111111111111111111111111000000,而且无符号右移操作会把这个二进制码当成正数的二进制码,换算成十进制就是 4294967232。如果把这个值右移 5 位,结果就变成了00000111111111111111111111111110,即十进制的 134217726。

3.5.3 布尔操作符

布尔操作符一共有 3个:逻辑非、逻辑与和逻辑或。

1. 逻辑非

逻辑非操作符由一个叹号(!)表示。这个操作符始终返回布尔值,无论应用到的是什么数据类型。逻辑非操作符首先将操作数转换为布尔值,然后再对其取反。

  • 如果操作数是对象,则返回 false。
  • 如果操作数是空字符串,则返回 true。
  • 如果操作数是非空字符串,则返回 false。
  • 如果操作数是数值 0,则返回 true。
  • 如果操作数是非 0数值(包括 Infinity),则返回 false。
  • 如果操作数是 null,则返回 true。
  • 如果操作数是 NaN,则返回 true。
  • 如果操作数是 undefined,则返回 true。

以下示例验证了上述行为:

console.log(!false);   // true
console.log(!"blue");  // false 
console.log(!0);       // true 
console.log(!NaN);     // true 
console.log(!"");      // true 
console.log(!12345);   // false

同时使用两个叹号(!!),相当于调用了转型函数 Boolean()。无论操作数是什么类型,第一个叹号总会返回布尔值。第二个叹号对该布尔值取反, 从而给出变量真正对应的布尔值。结果与对同一个值使用 Boolean()函数是一样的:

console.log(!!"blue"); // true
console.log(!!0);      // false
console.log(!!NaN);    // false
console.log(!!"");     // false
console.log(!!12345);  // true

2. 逻辑与

逻辑与操作符由两个和号(&&)表示。(全真为真,否则为假) 逻辑与操作符可用于任何类型的操作数,不限于布尔值。如果有操作数不是布尔值,则逻辑与并不一定会返回布尔值,而是遵循如下规则。

  • 如果第一个操作数是对象,则返回第二个操作数。
  • 如果第二个操作数是对象,则只有第一个操作数求值为 true 才会返回该对象。
  • 如果两个操作数都是对象,则返回第二个操作数。
  • 如果有一个操作数是 null,则返回 null。
  • 如果有一个操作数是 NaN,则返回 NaN。
  • 如果有一个操作数是 undefined,则返回 undefined。

逻辑与操作符是一种短路操作符,意思就是如果第一个操作数决定了结果,那么永远不会对第二个 操作数求值。对逻辑与操作符来说,如果第一个操作数是 false,那么无论第二个操作数是什么值,结 果也不可能等于 true。

3. 逻辑或

逻辑或操作符由两个管道符(||)表示,(有真就真) 与逻辑与类似,如果有一个操作数不是布尔值,那么逻辑或操作符也不一定返回布尔值。它遵循如下规则。

  • 如果第一个操作数是对象,则返回第一个操作数。
  • 如果第一个操作数求值为 false,则返回第二个操作数。
  • 如果两个操作数都是对象,则返回第一个操作数。
  • 如果两个操作数都是 null,则返回 null。
  • 如果两个操作数都是 NaN,则返回 NaN。
  • 如果两个操作数都是 undefined,则返回 undefined。

同样与逻辑与类似,逻辑或操作符也具有短路的特性。只不过对逻辑或而言,第一个操作数求值为 true,第二个操作数就不会再被求值了。

3.5.4 乘性操作符

ECMAScript定义了 3个乘性操作符:乘法、除法和取模。这些操作符跟它们在 Java、C语言及 Perl 中对应的操作符作用一样,但在处理非数值时,它们也会包含一些自动的类型转换。如果乘性操作符有不是数值的操作数,则该操作数会在后台被使用 Number()转型函数转换为数值。这意味着空字符串会被当成 0,而布尔值 true 会被当成 1。

1. 乘法操作符

乘法操作符由一个星号(*)表示,可以用于计算两个数值的乘积。

  • 如果操作数都是数值,则执行常规的乘法运算,即两个正值相乘是正值,两个负值相乘也是正值,正负符号不同的值相乘得到负值。如果 ECMAScript 不能表示乘积,则返回 Infinity 或 -Infinity。
  • 如果有任一操作数是 NaN,则返回 NaN。
  • 如果是 Infinity 乘以 0,则返回 NaN。
  • 如果是 Infinity 乘以非0的有限数值,则根据第二个操作数的符号返回 Infinity 或-Infinity。
  • 如果是 Infinity 乘以 Infinity,则返回 Infinity。
  • 如果有不是数值的操作数,则先在后台用 Number()将其转换为数值,然后再应用上述规则。

2. 除法操作符

除法操作符由一个斜杠(/)表示,用于计算第一个操作数除以第二个操作数的商。

  • 如果操作数都是数值,则执行常规的除法运算,即两个正值相除是正值,两个负值相除也是正值,符号不同的值相除得到负值。如果ECMAScript不能表示商,则返回Infinity或-Infinity。
  • 如果有任一操作数是 NaN,则返回 NaN。
  • 如果是 Infinity 除以 Infinity,则返回 NaN。
  • 如果是 0除以 0,则返回 NaN。
  • 如果是非 0的有限值除以 0,则根据第一个操作数的符号返回 Infinity 或-Infinity。
  • 如果是 Infinity 除以任何数值,则根据第二个操作数的符号返回 Infinity 或-Infinity。
  • 如果有不是数值的操作数,则先在后台用 Number()函数将其转换为数值,然后再应用上述规则。

3. 取模操作符

取模(余数)操作符由一个百分比符号(%)表示。

  • 如果操作数是数值,则执行常规除法运算,返回余数。
  • 如果被除数是无限值,除数是有限值,则返回 NaN。
  • 如果被除数是有限值,除数是 0,则返回 NaN。
  • 如果是 Infinity 除以 Infinity,则返回 NaN。
  • 如果被除数是有限值,除数是无限值,则返回被除数。
  • 如果被除数是 0,除数不是 0,则返回 0。
  • 如果有不是数值的操作数,则先在后台用 Number()函数将其转换为数值,然后再应用上述规则。

3.5.5 指数操作符

ECMAScript 7新增了指数操作符,Math.pow()现在有了自己的操作符**,结果是一样的:

console.log(Math.pow(3, 2);    // 9 
console.log(3 ** 2);           // 9 
 
console.log(Math.pow(16, 0.5); // 4 
console.log(16** 0.5);         // 4 
let squared = 3; 
squared **= 2;
console.log(squared); // 9 
let sqrt = 16; 
sqrt **= 0.5; 
console.log(sqrt); // 4 

3.5.6 加性操作符

1. 加法操作符

加法操作符(+)用于求两个数的和。 如果两个操作数都是数值,加法操作符执行加法运算并根据如下规则返回结果:

  • 如果有任一操作数是 NaN,则返回 NaN;
  • 如果是 Infinity 加 Infinity,则返回 Infinity;
  • 如果是-Infinity 加-Infinity,则返回-Infinity;
  • 如果是 Infinity 加-Infinity,则返回 NaN;
  • 如果是+0 加+0,则返回+0;
  • 如果是-0 加+0,则返回+0;
  • 如果是-0 加-0,则返回-0。

不过,如果有一个操作数是字符串,则要应用如下规则:

  • 如果两个操作数都是字符串,则将第二个字符串拼接到第一个字符串后面;
  • 如果只有一个操作数是字符串,则将另一个操作数转换为字符串,再将两个字符串拼接在一起。

如果有任一操作数是对象、数值或布尔值,则调用它们的 toString()方法以获取字符串,然后再 应用前面的关于字符串的规则。对于 undefined 和 null,则调用 String()函数,分别获取 "undefined"和"null"。

2. 减法操作符

减法操作符(-)也是使用很频繁的一种操作符。

  • 如果两个操作数都是数值,则执行数学减法运算并返回结果。
  • 如果有任一操作数是 NaN,则返回 NaN。
  • 如果是 Infinity 减 Infinity,则返回 NaN。
  • 如果是-Infinity 减-Infinity,则返回 NaN。
  • 如果是 Infinity 减-Infinity,则返回 Infinity。
  • 如果是-Infinity 减 Infinity,则返回-Infinity。
  • 如果是+0 减+0,则返回+0。
  • 如果是+0 减-0,则返回-0。
  • 如果是-0 减-0,则返回+0。
  • 如果有任一操作数是字符串、布尔值、null 或 undefined,则先在后台使用 Number()将其转 换为数值,然后再根据前面的规则执行数学运算。如果转换结果是 NaN,则减法计算的结果是 NaN。
  • 如果有任一操作数是对象,则调用其 valueOf()方法取得表示它的数值。如果该值是 NaN,则减法计算的结果是 NaN。如果对象没有 valueOf()方法,则调用其 toString()方法,然后再 将得到的字符串转换为数值。

3.5.7 关系操作符

关系操作符执行比较两个值的操作,包括小于(<)、大于(>)、小于等于(<=)和大于等于(>=), 用法跟数学课上学的一样。这几个操作符都返回布尔值

  • 如果操作数都是数值,则执行数值比较。
  • 如果操作数都是字符串,则逐个比较字符串中对应字符的编码。
  • 如果有任一操作数是数值,则将另一个操作数转换为数值,执行数值比较。
  • 如果有任一操作数是对象,则调用其 valueOf()方法,取得结果后再根据前面的规则执行比较。 如果没有 valueOf()操作符,则调用 toString()方法,取得结果后再根据前面的规则执行比较。
  • 如果有任一操作数是布尔值,则将其转换为数值再执行比较。

3.5.8 相等操作符

1. 等于和不等于

ECMAScript中的等于操作符用两个等于号(==)表示,如果操作数相等,则会返回 true。不等于 操作符用叹号和等于号(!=)表示,如果两个操作数不相等,则会返回 true。这两个操作符都会先进 行类型转换(通常称为强制类型转换)再确定操作数是否相等。

  • 如果任一操作数是布尔值,则将其转换为数值再比较是否相等。false 转换为 0,true 转换 为 1。
  • 如果一个操作数是字符串,另一个操作数是数值,则尝试将字符串转换为数值,再比较是否 相等。
  • 如果一个操作数是对象,另一个操作数不是,则调用对象的 valueOf()方法取得其原始值,再根据前面的规则进行比较。 在进行比较时,这两个操作符会遵循如下规则。
  • null 和 undefined 相等。
  • null 和 undefined 不能转换为其他类型的值再进行比较。
  • 如果有任一操作数是 NaN,则相等操作符返回 false,不相等操作符返回 true。记住:即使两个操作数都是 NaN,相等操作符也返回 false,因为按照规则,NaN 不等于 NaN。
  • 如果两个操作数都是对象,则比较它们是不是同一个对象。如果两个操作数都指向同一个对象,则相等操作符返回 true。否则,两者不相等。

还有一些特殊的情况

2. 全等和不全等

全等和不全等操作符与相等和不相等操作符类似,只不过它们在比较相等时不转换操作数。全等操 作符由 3个等于号(===)表示,只有两个操作数在不转换的前提下相等才返回 true。 虽然 null == undefined 是 true(因为这两个值类似),但 null === undefined 是 false,因为它们不是相同的数据类型。

3.5.9 条件操作符

条件操作符是 ECMAScript中用途为广泛的操作符之一,语法跟 Java中一样:

variable = boolean_expression ? true_value : false_value;

let max = (num1 > num2) ? num1 : num2; 

3.5.10 赋值操作符

简单赋值用等于号(=)表示,将右手边的值赋给左手边的变量,复合赋值使用乘性、加性或位操作符后跟等于号(=)表示。

  • 乘后赋值(*=)
  • 除后赋值(/=)
  • 取模后赋值(%=)
  • 加后赋值(+=)
  • 减后赋值(-=)
  • 左移后赋值(<<=)
  • 右移后赋值(>>=)
  • 无符号右移后赋值(>>>=)

这些操作符仅仅是简写语法,使用它们不会提升性能。

3.5.11 逗号操作符

逗号操作符可以用来在一条语句中执行多个操作,也可以使用逗号操作符来辅助赋值。在赋值时使用逗号操作符分隔值,终会返回表达式中后一个值

let num1 = 1, num2 = 2, num3 = 3; 
let num = (5, 1, 4, 8, 0); // num 的值为 0 

3.6 语句

3.6.1 if 语句

if 语句是使用频繁的语句之一,语法如下:

if (condition) statement1 else statement2 

3.6.2 do-while 语句

do-while 语句是一种后测试循环语句,即循环体中的代码执行后才会对退出条件进行求值。换句话说,循环体内的代码至少执行一次。

do {   statement  } while (expression); 

3.6.3 while 语句

while 语句是一种先测试循环语句,即先检测退出条件,再执行循环体内的代码。因此,while 循 环体内的代码有可能不会执行。

while(expression) statement 

3.6.4 for 语句

for 语句也是先测试语句,只不过增加了进入循环之前的初始化代码,以及循环执行后要执行的表 达式,语法如下

for (initialization; expression; post-loop-expression) statement

初始化、条件表达式和循环后表达式都不是必需的。因此,下面这种写法可以创建一个无穷循环:

for (;;) { // 无穷循环   doSomething();  } 

如果只包含条件表达式,那么 for 循环实际上就变成了 while 循环:

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

3.6.5 for-in 语句

for-in 语句是一种严格的迭代语句,用于枚举对象中的非符号键属性,语法如下:

for (property in expression) statement 

如果 for-in 循环要迭代的变量是 null 或 undefined,则不执行循环体

3.6.6 for-of 语句

for-of 语句是一种严格的迭代语句,用于遍历可迭代对象的元素,语法如下:

for (property of expression) statement 
//for-in 循环显示了 BOM 对象 window 的所有属性。
//每次执行循环,都会给变量 propName 赋予一个 window 对象的属性作为值,直到 window 的所有属性都被枚举一遍。
//与 for 循环 一样,这里控制语句中的 const 也不是必需的。但为了确保这个局部变量不被修改,推荐使用 const。
 

下面是示例:

for (const el of [2,4,6,8]) {   document.write(el); } 
//for-of 语句显示了一个包含 4个元素的数组中的所有元素。
//循环会一直 持续到将所有元素都迭代完。
//与 for 循环一样,这里控制语句中的 const 也不是必需的。但为了确保 这个局部变量不被修改,推荐使用 const

3.6.7 标签语句

标签语句用于给语句加标签,语法如下: label: statement 下面是一个例子:

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

在这个例子中,start 是一个标签,可以在后面通过 break 或 continue 语句引用。

3.6.8 break 和 continue 语句

break 和 continue 语句为执行循环代码提供了更严格的控制手段。其中,break 语句用于立即退 出循环,强制执行循环后的下一条语句。而 continue 语句也用于立即退出循环,但会再次从循环顶部开始执行。

let num = 0; 
 
outermost:  
for (let i = 0; i < 10; i++) { 
  for (let j = 0; j < 10; j++) { 
   if (i == 5 && j == 5) {      
     continue outermost;    
   }    
   num++;   
  } 
} 
 
console.log(num); // 95 
//当 i 和 j 都等于 5时,会执行 continue,跳到外部循环继续执行,从而导致内部循环少执行 5次,结 果 num 等于 95。 

3.6.9 with 语句

with 语句的用途是将代码作用域设置为特定的对象,其语法是:

with (expression) statement; 

with可以延长作用域链,严格模式不允许使用 with 语句,否则会抛出错误。

3.6.10 switch 语句

switch 语句是与 if 语句紧密相关的一种流控制语句,从其他语言借鉴而来。ECMAScript中 switch 语句跟 C语言中 switch 语句的语法非常相似,这里的每个 case(条件/分支)相当于:“如果表达式等于后面的值,则执行下面的语句。”break 关键字会导致代码执行跳出 switch 语句。如果没有 break,则代码会继续匹配下一个条件。default 关键字用于在任何条件都没有满足时指定默认执行的语句(相当于 else 语句)。

switch (expression) { 
case value1:  
  statement   
  break;  
case value2:    
  statement    
  break;  
case value3:  
  statement  
  break;  
case value4: 
  statement   
  break;  
default:     
  statement 
}

3.7 函数

函数对任何语言来说都是核心组件,因为它们可以封装语句,然后在任何地方、任何时间执行。 ECMAScript中的函数使用 function 关键字声明,后跟一组参数,然后是函数体

function functionName(arg0, arg1,...,argN) {  
  statements 
}

严格模式对函数也有一些限制:

  • 函数不能以 eval 或 arguments 作为名称;
  • 函数的参数不能叫 eval 或 arguments;
  • 两个命名参数不能拥有同一个名称。

如果违反上述规则,则会导致语法错误,代码也不会执行。

3.8 小结

JavaScript的核心语言特性在 ECMA-262中以伪语言 ECMAScript的形式来定义。ECMAScript包含 所有基本语法、操作符、数据类型和对象,能完成基本的计算任务,但没有提供获得输入和产生输出的 机制。理解 ECMAScript 及其复杂的细节是完全理解浏览器中 JavaScript 的关键。下面总结一下 ECMAScript中的基本元素。

  • ECMAScript中的基本数据类型包括 Undefined、Null、Boolean、Number、String 和 Symbol。
  • 与其他语言不同,ECMAScript不区分整数和浮点值,只有 Number 一种数值数据类型。
  • Object 是一种复杂数据类型,它是这门语言中所有对象的基类。
  • 严格模式为这门语言中某些容易出错的部分施加了限制。
  • ECMAScript提供了 C语言和类 C语言中常见的很多基本操作符,包括数学操作符、布尔操作符、 关系操作符、相等操作符和赋值操作符等。
  • 这门语言中的流控制语句大多是从其他语言中借鉴而来的,比如 if 语句、for 语句和 switch 语句等。 ECMAScript中的函数与其他语言中的函数不一样。
  • 不需要指定函数的返回值,因为任何函数可以在任何时候返回任何值。
  • 不指定返回值的函数实际上会返回特殊值 undefined。

参考文献:

《JavaScript高级程序设计(第4版)》