Bootstrap

1. 语言基础

语法

区分大小写

ECMAScript 中一切都区分大小写。
无论是变量,函数名还是操作符,都区分大小写
  - 变量 test 和 变量 Test 是两个不同的变量。
  - typeof 不能作为函数名 / 变量名(因为它是一个关键字)Typeof 可以

标识符

变量,函数,属性或函数参数的名称
  - 第一个字符必须是字母,下划线 "_" / 美元符号 "$"
  - 剩下的其他字符可以是字母,下划线 "_" / 美元符号 "$" / 数字
  - 关键字,保留字,true,false 和 null 不能作为标识符
  
  
标识符中的字母可以扩展 ascii 中的字母,也可以是 unicode的字母字符(不推荐使用)

注释

// 这是单行注释示例

/*
这是多行注释示例
*/

严格模式

严格模式是一种不同的 JavaScript 解析和执行模型(ECMAScript 5)
  - ECMAScript 3 的一些不规范写法在这种模式下不会被处理
  - 对于不安全的活动将抛出错误

要对整个脚本开启严格模式, 在脚本开头加上 "use strict";
也可以单独指定一个函数在严格模式下执行,将预处理指令放到函数体开头即可
function doSomething(){
    "user strict";
    // 函数体
}
严格模式会影响 JavaScript 执行的很多方面

语句

ECMAScript 中的语句以分号结尾:
let sum = a + b  // 没有分号也有效,但不推荐
let diff = a - b;  // 加分号有效,推荐
即使语句末尾的分号不是必须的,也应该加上
  - 有助于防止省略造成的问题(比如可以避免输入内容不完整)
  - 便于开发者通过删除空行来压缩代码(没有结尾的分号,只删除空行,会导致语法错误)
  - 有助于在某些情况下提升性能(解析器会尝试在合适的位置补上分号以纠正语法错误)
多条语句可以合并到代码块中:左花括号 "{"  标识开始, 右花括号 "}"  标识结束
// if 之类的控制语句旨在执行多条语句时要求必须有代码块
// 最佳实践是始终在控制语句中使用代码块(即使要执行的只有一条语句)

// 有效,但容易导致语法错误,应该避免
if (test)
    console.log(test);

// 推荐
if (test){
    console.log(test);
}
在控制语句中,使用代码块可以让内容更清晰,在需要修改代码时也可以减少出错的可能性

关键字与保留字

···
关键字:
break,case,catch,class …

保留字:
abstract, ,boolean,byte,char …
···

关键字有特殊用途,比如表示控制语句的开始和结束,或者执行特定的操作

保留字在语言中没有特定的用途,但它们是保留给将来做关键字用的

变量

ECMAScript 变量是松散类型的(变量可以用于保存任何类型的数据)
变量只不过是一个用于保存任意值的命名占位符

有 3 个关键字可以声明变量: var, const, let
  - var 在 ECMAScript 的所有版本中都可以使用
  - const 和 let 只能在 ECMAScript 6 及更晚的版本中使用

声明

var 关键字

定义变量
var message;

定义并初始化变量
var message = "hi";
定义并初始化变量后,不仅可以改变保存的值,也可以改变值的类型
message = 1
message = "hh"

var 声明作用域

使用 var 操作符定义的变量会成为包含它的函数的局部变量(使用 var 在函数内部定义一个变量,意味着该变量将在函数退出的时候被销毁)
function test(){
    var message = "hi";  // 局部变量
}

test();
console.log(message);  // 出错(message is not defined)
在函数内部定义变量时省略 var 操作符,可以创建一个全局变量;
function test(){
    message = "hi";  // 省略 var 操作符(全局变量)
}

test();
console.log(message);  // 出错(message is not defined)
定义多个变量,可以在一条语句中用逗号分隔每个变量(及可选的初始化)
var message = "hi",
    found = false,
    age = 29;

// 插入换行和空格不是必须的,但这样有利于阅读理解
// 严格模式下,不能定义名为 eavl 和 arguments 的变量,否则会导致语法错误

var 声明提升

var 关键字声明的变量会自动提升到函数作用域顶部;
function foo(){
    console.log(age)  // undefined
    var age = 26;
}
foo()
之所以不会报错,是因为 ECMAScript 运行时把它看成等价于
function foo(){
    var age;
    console.log(age);  // undefined
    age = 26;
}
foo();
这就是所谓的提升(hoist)
使用 var 声明同一个变量也没有问题
function foo(){
    var age = 16;
    var age = 26;
    var age = 36;
    console.log(age)
}
foo()  // 36

let 声明

let 跟 var 作用差不多,但有着非常重要的区别
  - let 声明的范围是块作用域
  - var 声明的范围是函数作用域
// var
if (true){
    var name = 'Matt';
    console.log(name);  // Matt
}
console.log(name)  // Matt


// let
if (true){
    var name = 'Matt';
    console.log(name);  // Matt
}
console.log(name)  // Matt


if (true){
    let age = 18;
    console.log(age);  // 18
}
console.log(age)  // ReferenceError: age is not defined
age 变量之所以不能再 if 块外部被引用,是因为它的作用域仅限于该块内部
块作用域是函数作用域的子集(适用于 var 的作用域限制同样也适用于 let)
let 不允许同一个块作用域中出现冗余声明
let age;
let age;  // SyntaxError 标识符 age 已经声明过了

暂时性死区

let 声明的变量不会在作用域中被提升
// name 会被提升
console.log(name);
let name = 'Matt';

// age 不会被提升
console.log(age);  // ReferenceError: age 没有定义
let age = 26;
在 let 声明之前执行的瞬间被称为 ”暂时性死区“,在此阶段引用任何后面才声明的变量都会抛出 ReferenceError

全局声明

let 在全局作用域中声明的变量不会变成 window 对象的属性(var 声明的变量则会)
var name = 'Matt';
console.log(window.name);  // Matt

let age = 18;
console.log(window.age);  // undefined
let 声明仍然是在全局作用域中发生的,相应变量会在页面的生命周期内存续

条件声明

条件块中 let 声明的作用域仅限于该块(let 不能依赖条件声明模式) 

for 循环中的 let 声明

在 let 出现之前,for 循环定义的迭代变量会渗透到循环体外部
for (var i = 0; i<5; ++i){
    // 循环逻辑
}
console.log(i);  // 5
在 let 出现之后,这个问题就消失了,迭代变量的作用域仅限于 for 循环块内部
for (let i = 0; i<5; ++i){
    // 循环逻辑
}
console.log(i);  // ReferenceError: i 没有定义

const 声明

const 的行为与 let 基本相同,唯一一个重要的区别是用它声明变量时必须同时初始化变量,
且尝试修改 const 声明的变量会导致运行时错误
// 不允许修改值
const age = 26;
age = 36;  

// 不允许重复声明
const name = 'Matt';
const name = 'Nicholas';  

// 声明的作用域也是块
const name = 'Matt';
if(true){
    const name = 'Nicholas';
}
console.log(name)  // Matt
const 声明的限制只适用于它指向的变量的引用
  - 如果 const 变量引用的是一个对象,修改这个对象内部的属性并不违反 const 的限制
const person = {};
person.name = 'Matt';  // ok
JavaScript 引擎会为 for 循环中的 let 声明分别创建独立的变量实例
  - 不能用 const来声明迭代变量(因为迭代变量会自增)
 for (const i = 0; i < 10; i++){}  // TypeError: 给常量赋值
如果只是想用 const 声明一个不会被修改的 for 循环变量,那也是可以的
(每次迭代只是创建新的变量,这对 for-of 和 for-in 循环特别有意义)
let i = 0;
for (const j = 7; i < 5; ++i){
	console.log(j)
}

for (const key in {'a': 1, b: 2}){
	console.log(key)
}

for (const value of [1, 2, 3, 4, 5]){
	console.log(value)
}

声明风格及最佳实践

不使用 var,const 优先,let 次之

数据类型

ECMAScript 有 6 种简单数据类型(原始类型):
Undefined, Null, Boolean, Number, String, Symbol(符号)

复杂数据类型:
object(对象,是一种无序名值对的集合)

typeof 操作符

对一个值使用 typeof 操作符会返回下列字符串之一:
undefined	-->	表示值未定义
boolean	-->	表示值为布尔值
string	-->	表示值为字符串
number	-->	表示值为字符串
number	-->	表示值为数值
object	-->	表示值为对象(而不是函数)或 null;
function	-->	表示值为函数
symbol	-->	表示值为符号

typeof 是一个操作符而不是函数,所以不需要参数(但可以使用参数)
调用 typeof null 返回 "object",这是因为特殊值 null 被认为是一个对空对象的引用
let message = "some string";  // "string"
console.log(typeof message);  // "string"
console.log(typeof(message));  // "string"
console.log(typeof 95);  // "number"

Undefined 类型

Undefined 类型只有一个值,就是特殊值 Undefined 
使用 var 或 let 声明变量但没有初始化时,就相当于给变量赋予了 Undefined  值
let message = undefined;
console.log(message == undefined);  // false
永远不用显示地给某个变量设置 undefined 值,
undefined 这个特殊值地目的就是为了正式明确空对象指针(null)和未初始化变量的区别
对未初始化的变量调用 typeof 时,返回的结果是 "undefined"
对未声明的变量调用 typeof 时,返回的结果还是 "undefined"
let message;  // 这个变量被声明了,只是值为 undefined
console.log(typeof message);  // "undefined"
console.log(typeof age);  // "undefined"
未初始化的变量会被自动赋予 undefined 值,但建议在声明变量的同时进行初始化
当 typeof 返回 undefined 时就会知道那是因为给定的变量未声明

Null 类型

null 值表示一个空对象指针(这也是给 typeof 传一个 null 会返回 "object" 的原因)
let car = null;
console.log(typeof null);  // object
用等于比较符( == )比较 null 和 undefined 始终返回 true,这个操作符会为了比较而转换它的操作数
任何时候,只要变量要保存对象,而当时又没有那个对象可保存,就要用 null 来填充该变量
(这样就可以保持 null,进一步将其与 undefined 区分开来)
null 是一个假值,如果需要,可以用更简洁的方式检测它
也有很多其他可能的值同样是假值(要明确自己想检测的就是 null 这个字面值,而不仅仅是假值)
let message = null;
let age;
if(message){
	// 这个块不会执行
}
if(!message){
	// 这个块会执行
}
if (age){
	// 这个块不会被执行
]
if(!age){
	// 这个块会执行
}

Boolean 类型

Boolean(布尔值)类型是 ECMAScript 中使用最频繁的类型之一,有两个字面值 true 和 false
这两个布尔值不同于数值,因此 true 不等于1, false 不等于 0
true 和 false 是区分大小写的(其他大小混写形式是有效的标识符,但不是布尔值)
要将其他 ECMAScript 类型的值都有相应的布尔值的等价形式
要将一个其他类型的值转换为布尔值,可以调用特定的 Boolean() 转型函数
let message = 'Hello world';
let messageBoolean = Boolean(message);
数据类型true值false 值
Booleantruefalse
String非空字符串空字符串
Number非0数值(包括无穷值)0,NaN
Object任意对象null
UndefinedN/A(不存在)undefined
// 理解转换非常重要,if 等控制流语句会自动执行其他类型值到布尔值的转换
let message = 'Hello world';
if (message){
	console.log('Value is true')
}

Number 类型

Number 类型使用 IEEE 754 格式表示整数和浮点值(在某些语言中也叫双精度值)
不同的数值类型相应地也有不同地数值字面量格式
最基本地数值字面量格式是十进制整数,直接写出来即可
let intNum = 55;
整数也可以用八进制(以 8 为基数)或十六进制(以 16 为基数)字面量表示
  - 八进制字面量:第一个数字必须是零(0),后面是相应地八进制数字(数值 0~7)
  - 十六进制字面量:前缀0x(区分大小写),后面是相应的十六进制数字(0~9 以及 A~F)十六进制数字中的字母大小写均可
let octalNum1 = 070 // 八进制的56
let octalNum2 = 079 // 无效的八进制值,当成79处理
let ocatlnum3 = 08 // 无效的八进制值,当成8处理

let hexNum1 = 0xA // 十六进制10
let hexNum2 = 0x1f // 十六进制31

// 使用八进制和十六进制格式创建的数值在所有数学操作中都被视为十进制数值
+0 和 -0 在所有情况下都被认为是等同的
console.log(+0 == -0)  // true
console.log(+0 === -0)  // true

浮点值

定义浮点值。数值中必须包含小数点,而且小数点后面必须至少有一个数字。
(虽然小数点前面不是必须有整数,但推荐加上)
let floatNum1 = 1.1;
let floatNum2 = 0.1;
let floatNum3 = .1; // 有效,但不推荐
因为存储浮点值使用的内存空间是存储整数值的两倍,所有 ECMAScript 总是想方设法把值转换为整数
在小数点后面没有数字的情况下,数值就会变成整数
数值本身就是整数,小数点后面跟着0(如1.0),它也会被转换为整数
let floatNum1 = 1.;  // 小数点后面没有数字,当成整数 1 处理
let floatNum2 = 10.0;  // 小数点后面是零,当成整数 10 处理
对于非常大或非常小的值,浮点数可以用科学计数法表示(科学记数法显得更简洁)
  - 科学记数法的格式要求是一个数值(整数或浮点数)后跟一个大写或小写字母e,再加上一个要乘 10 的多少次幂
let floatNum = 3.125e7;  // 等于 3125000
浮点值的精确度最高可达 17 位小数,但在算数计算中远不如整数准确
0.1 加 0.2 得到的不是 0.3(而是 0.300 000 000 000 000 04)
由于这种微小的舍入错误,导致很难测试特定的浮点值
if (a + b == 0.3){  // 别这么干
	console.log('you got 0.3');
}

// 永远不要测试某个特定的浮点值

值的范围

由于内存限制,ECMAScript 并不支持表示这个世界上的所有数值
可以表示最小的数值保存在 Number.MIN_VALUE(在多数浏览器中是 5e-324)
可以表示最大的数值保存在 Number.MAX_VALUE(在多数浏览器中是 1.797 693 134 862 315 7e+308)
如果某个计算得到的数值结果超出了 JavaScript 可以表示的范围,那么这个数值会被自动转换为一个特殊的 Infinity(无穷)值。
  - 任何无法表示的负数以 -Infinity(负无穷大)表示
  - 任何无法表示的正数以 Infinity(正无穷大)表示
如果计算返回 正 Infinity 或 负 Infinity,则该值不能再进一步用于任何计算
因为 Infinity 没有可用于计算的数值表示形式。
要确定一个值是不是有限大,可以用 isFinite() 函数
let result = Number.MAX_VALUE + Number.MAX_VALUE;
console.log(isFinite(result));  // false
// 虽然超出有限数值范围的计算并不多见,但总归还是有可能的。
// 在计算非常大或非常小的数值时,有必要监测一下计算结果是否超出范围

NaN

有一个特殊的数值叫 NaN,意思是 "不是数值" (Not a Number),用于表示本来要返回的数值操作失败了(而不是抛出错误)
例如:用 0 除以任意数值在其他语言中通常都会导致错误,从而终止代码执行。但 ECMAScript 会返回 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 / 10)。在连续多步计算时这可能是个问题。
NaN 不等于包括 NaN 在内的任何值。
console.log(NaN == NaN)
ECMAScript 提供了 isNaN() 函数
该函数接收任意一个参数,可以是任意数据类型,然后判断这个参数是否”不是数值“
把一个值传给 isNaN() 后,该函数会尝试把它转换为数值。
任何不能转换为数值的值都会导致这个函数返回 true
console.log(isNaN(NaN))  // true
console.log(isNaN(10))  // false, 10 是数值
console.log(isNaN("10"))  // false 可以转换为数值
console.log(isNaN("blue"))  // true, 不可以转换为数值
console.log(isNaN(true))  // false, 可以转换为数值 1
isNaN() 可以用于测试对象。
首先会调用对象的 valueOf() 方法,然后再确定返回的值是否可以转换为数值。
如果不能,再调用 toString() 方法,并测试其返回值。

数值转换

将非数值转换为数值:Number(), parseInt(), parseFloat()
Number() 是转型函数,可以用于任何数值类型。后面两个函数主要用于将函数转换为数值
Number() 函数基于如下规则执行转换:
  - 布尔值,true 转换为 1, false 转换为 0
  - 数值,直接返回
  - null, 返回 0
  - undeined,返回 NaN
  - 字符串,应用以下规则:
      - 包含数值字符,包括数值字符前面带加,减号的情况,则转换为一个十进制数值
      - 包含有效的浮点值格式,则会转换为相应的浮点值(同样,忽略前面的零)
      - 包含有效的十六进制格式如 "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
parseInt
通常在需要得到整数时,可以优先使用 pairseInt() 函数,
parseInt() 更专注于字符串是否包含数值模式.
  - 字符串最前面的空格会被忽略,从第一个非空字符开始转换
  - 第一个字符不是数值字符,加号或减号,parseInt() 立即返回 NaN(空字符也会返回 NaN / Number() 返回 0)
  - 第一个字符是数值字符,加号或减号,继续检测下一个字符,直到字符串末尾,或碰到非数值字符
字符串中的第一个字符是数值字符,parseInt() 函数也能识别不同的整数格式(十进制,八进制,十六进制)
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() 也接收第二个参数,用于指定底数(进制数)
let num = parseInt("0xAF", 16);  // 175
// 事实上,如果提供了十六进制参数,那么字符串前面的 0x 可以省略掉
let num = parseInt("AF", 16);  // 175
let num = parseInt("AF");  // NuN(并未提供底数)
通过第二个参数,可以极大扩展转换后获得的结果类型
let num1 = parseInt("10", 2);  // 2,按二进制解析
let num2 = parseInt("10", 8);  // 8,按八进制解析
let num3 = parseInt("10", 10);  // 10,按十进制解析
let num4 = parseInt("10", 16);  // 16,按十六进制解析
建议始终传给 parseInt() 底数参数
parseFloat
parseFloat() 函数工作方式跟 parseInt() 函数类似
都是从位置 0 开始检测每个字符
也是解析到字符串末尾或者解析到一个无效的浮点数值字符为止

第一次出现的小数点是有效的,但第二次出现的小数点就无效了,此时字符串的剩余字符都会被忽略
例如:"22.34.5" 将转换成 22.34
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

String类型

String(字符串)数据类型表示零或多个 16位 Unicode 字符序列
字符串可以使用双引号("),单引号('),反引号(`)标示
let firstName = "john";
let lastName = 'Jacob';
let lastName = `Jingleheimerschmidt`;
以某种引号作为字符串开头,必须仍然以该种引号作为字符串结尾。

字符字面量

字面量含义
\n换行
\t制表符
\b退格
\r回车
\f换页
\反斜杠
单引号,在字符串以单引号标示时使用
"双引号
`反引号
\xnn以十六进制编码 nn 表示的字符串(n是十六进制数字0-F)
\unnn以十六进制编码 nnnn 表示的 Unicode 字符(n是十六进制数字0-F)
let text = "This is the letter sigma: \u03a3.";
// 即使包含 6 个字符长的转义序列,变量 text 仍然是 28 个字符长
// 转义序列表示一个字符,所以只算一个字符
// 字符的长度可以通过 length 属性获取
console.log(text.length)  // 28
// 此属性返回字符串中 16 位字符的个数
如果字符串种包含双字节字符,那么 length 属性返回的值可能不是准确的字符数

字符串的特点

字符串是不可变的(一旦创建,它们的值就不能变了)
要修改某个变量中字符串的值,只能重新赋值
let lang = "Java";
lange = lange + "Script";

转换为字符串

 使用几乎所有值都有的 toString() 方法(唯一的用途就是返回当前值的字符串等价物)
let age = 11;
let agsAsString = age.toString();  // 字符串 "11"

let found = true;
let fundAsString = found.toString();  // 字符串 "true"
toString() 可见于数值,布尔值,对象,和字符串
null 和 undefined值 没有 toString() 方法
toString() 不接收任何参数,但在对数值调用这个方法时,toString() 可以接收一个底数参数
(以什么底数来输出数值的字符串表示,默认情况下返回数值十进制的字符串表示)
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,可以使用 toString() 转型函数,它始终会返回表示相应类型值得字符串
String() 函数遵循如下规则:
  - 值由 toString() 方法,调用该方法(不传参数)并返回结果
  - 值是 null,返回 ”null“
  - 值是 undefined,返回 ”undefined“
let value1 = 10;
let value2 = true;
let value3 = null;
let value4

console.log(String(value1))  // "10"
console.log(String(value2))  // "true"
console.log(String(value3))  // "null"
console.log(String(value4))  // "undefined"
// null 和 undefined 没有 toString() 方法,所以 String() 方法就直接返回了这两个值的字面量文本
用 加号操作符 给一个值加上一个空字符串也可以将其转换为字符串

模板字符串

与使用单引号双引号不同,模板字面量保留换行字符,可以跨行定义字符串
let myMultiLineString = "first line\nsecond line";
let myMultiLineTemplateLiteral = `first line
second line`;
console.log(myMultiLineString)
console.log(myMultiLineTemplateLiteral)
console.log(myMultiLineString === myMultiLineTemplateLiteral)

在这里插入图片描述

字符串插值

字符串插值通过在 ${}中使用一个 JavaScript 表达式实现
let value = 5;
let exponent = "second";
// 以前,字符串插值是这样实现的
let interpolatedString = value + ' to the ' + exponent + ' power is ' + (value * value);
console.log(interpolatedString)

// 现在
let interpolatedTemplateLiteral = `${value} to the ${exponent} power is ${value * value}`
console.log(interpolatedTemplateLiteral)
所有插入的值都会使用 toString() 强制转型为字符串
任何 JavaScript 表达式都可以用于插值
嵌套的模板字符串无需转义

原始字符串

获取原始的模板字面量内容(如换行符或 Unicode 字符)而不是被转换后的字符
可以使用 String.raw 标签函数
// Unicode 示例
// \u00A9 是版权符号
console.log('\u00A9')  // ©
console.log(String.raw`\u00A9`)  // \u00A9

// 换行符示例
console.log(`first line\nsecond line`)
console.log(String.raw`first line\nsecond line`)  // first line\nsecond line

Symbol 类型(了解用法包括特性即可)

Symbol(符号) 是ES6 新增的数据类型
  - 符号是原始值,且符号实例是唯一,不可变的
  - 符号的用途是确保对象属性唯一标识符,不会发生属性冲突的危险
  - 符号就是用来创建唯一记号,进而用作非字符串形式的对象属性

符号基本用法

符号需要使用 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 fooSymbo = 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();  // Symbol is not a constructor
如果想用符号包装对象,可以借用 Object() 函数;
let mySymbol = Symbol();
let myWrappendSymbol = Object(mySymbol);
console.log(typeof myWrappendSymbol);  // object

使用全局符号注册表

如果运行时的不同部分需要共享和重用符号实例,那么可以用一个字符串作为键,在全局符号注册表中创建并重用符号
需要使用 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
即使采用相同的符号描述,在全局注册表中定义的符号跟使用 Symbol() 定义的符号也不等同
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

使用符号作为属性

凡是可以用字符串或数值作为属性的地方,都可以使用符号
(包括了对象字面量属性和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: 'baz val'});
console.log(o)  // {Symbol(foo): 'foo val', Symbol(bar): 'baz val'}


Object.defineProperties(o, {
	[s3]:{value: 'baz val'},
	[s4]:{value: 'qux val'}
})
console.log(o)  // {Symbol(foo): 'foo val', Symbol(bar): 'baz val', Symbol(baz): 'baz val', Symbol(qux): 'qux val'}
Object.getOwnPropertyNames() 返回对象实例的常规属性数组
Object.getOwnProperty-Symbols() 返回对象实例的符号属性数组
这两个方法的返回值彼此互斥

Object.getOwnProperty-Descriptors() 会返回同时包含常规和符号属性描述符的对象
Reflect,ownKeys() 会返回两种类型的键:
let s1 = Symbol('foo');
let 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(function(symbol){
	return symbol.toString().match('bar');
});
console.log(barSymbol);  // Symbol(bar)

Object 类型

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

let o = new Object();

ECMAScript 中的 Object 也是派生其他对象的基类
Object 类型的所有属性和方法在派生的对象上同样存在

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

属性或方法描述
constructor构造器: 用于创建当前对象的函数
hasOwnProperty(propertyName)用于判断当前对象实例(不是原型)上是否存在给定的属性。要检查的属性必需是字符串或符号
isPrototypeof(object)用于判断当前对象是否为另一个对象的原型
propertyIsEnumerable(propertyName)用于判断给定的属性是否可以使用,属性名必须是字符串
toLocaleString()返回对象的字符串表示,该字符串反应对象所在的本地化执行环境
toString()返回对象的字符串表示
valueOf()返回对象对应的字符串,数值或布尔值表示,通常与 toString() 的返回值相同。因为 ECMAScript 中 Object 是所有对象的基类,所以任何对象都有这些属性和方法

操作符

一元操作符

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

递增/递减操作符(操作的差异)

前缀版和后缀版
前缀版就是位于要操作的变量前头 ++variable
后缀版就是位于要操作的变量后头 variable++
递增和递减操作符操作的差异:
  - 前缀递增/递减:先运算,再返回值
  - 后缀递增/递减:先返回值,再运算
前缀递增/递减

先运算,再返回值

let age = 29;
++ age
// 前缀递增操作符把 age 的值变成了 30(给之前的 29 加 1)
// 实际上它等于如下表达式
let age = 29;
age = age + 1;
// 前缀递减操作符也类似,只不过是从一个数值减1。
// 使用前缀递减操作符,只要把两个减号(--)放在牵头即可
let age = 29;
--age
// 执行操作后,变量 age 的值变成了 28(从 29 减 1)
后缀递增/递减

先返回值,再运算

let num1 = 2;
let num2 = 20;
let num3 = num1-- + num2;  // 22
let num4 = num1 + num2;  // 21
console.log(num3);
console.log(num4);
某些情况下,这种差异没什么影响
let age = 29;
age++;
递增和递减操作符遵循如下规则

可以作用域任何值, 意思是不限于整数----字符串,布尔值,浮点值,甚至对象都可以

递增和递减遵循如下规则:

类型规则
有效字符串如果是有效的数值形式,则转换为数值再应用改变。变量类型从字符串变成数值
无效字符串如果不是有效的数值形式,则将变量的值设置为 NaN,变量类型从字符串变成数值
布尔值 false如果是 false,则转换为 0 再应用改变、变量类型从布尔值变成数值
布尔值 true如果是true,则转换为 1 再应用改变,变量类型从布尔值变成数值
对象调用 valueOf() 方法取得可以操作的值。对得到的值应用上述规则,如果是 NaN,则调用 toString() 并再次应用其它规则。变量类型从对象变成数值
let s1 = "2";
let s2 = "z";
let b = false;
let f = 1.1;
let o = {
    valueOf(){
        return -1;
    }
}

console.log(++s1);  // 值变成数值 3
console.log(++s2);   // 值变成 NaN
console.log(++b);  // 值变成数值 1
console.log(--f);  // 值变成 0.1.....3(因为浮点值不精确)
console.log(--o);  // 值变成 -2

一元加和减

一元加操作符

一元加由一个加号(+)表示,放在变量前头,对数值没有任何影响

let num = 25;
num = +num;
console.log(num);

如果将一元应用到非数值,则会执行与使用 Number() 转型函数一样的类型转换:
布尔值 false 和 true 转换为 0 和 1
字符串根据特殊规则进行解析,对象会调用它们的 valueOf() 和/或 toString() 方法

let s1 = "01";
let s2 = "1.1";
let s3 = "z";
let b = false;
let f = 1.1;
let o = {
    valueOf(){
        return -1;
    }
}

s1 = +s1;
s2 = +s2;
s3 = +s3
b = +b
f = +f
o = +o

console.log(s1)  // 值变成数值 1
console.log(s2)  // 值变成数值 1.1
console.log(s3)  // 值变成 NaN
console.log(b)  // 值变成数值 0
console.log(f)  // 值不变,还是 1.1
console.log(o)  // 值变成数值 -1

一元减操作符

一元减由一个减号(-)表示,放在变量前头,主要用于把数值变成负值

let num = 25;
num = -num;
console.log(num);

对数值使用一元减会将其变成相应的负值。
在应用到非数值时,一元减会遵循与一元加同样的规则,先对它们进行转换,然后再取负值

let s1 = "01";
let s2 = "1.1";
let s3 = "z";
let b = false
let o = {
    valueOf(){
        return -1;
    }
}

s1 = -s1;
s2 = -s2;
s3 = -s3
b = -b
f = -f
o = -o

console.log(s1)  // 值变成数值 -1
console.log(s2)  // 值变成数值 -1.1
console.log(s3)  // 值变成 NaN(Not a Number)
console.log(b)  // 值变成数值 0
console.log(f)  // 变成 -1.1
console.log(o)  // 值变成数值 1

一元加和减操作符主要用于基本的算数,也可以用于数据类型的转换

位操作符

在对 ECMAScript 中的数值应用位操作符时,后台会发生转换:64位数值会转换为 32位数值,然后执行位操作,最后再把结果从 32 位转换为 64 位存储起来。
整个过程就像处理 32 位数值一样,这让二进制操作变得与其他语言中类似。
但这个转换也导致了一个奇特的副作用,即特殊值 NaN 和 Infinity 在位操作中都会被当成 0 处理
如果将位操作符应用到非数值,那么首先会使用 Number() 函数将该值转换为数值(这个过程时自动的),然后再应用为操作, 最终结果是数值

按位非 ~

按位非操作符用波浪符(~)表示,它的作用是返回数值的一补数
按位非是 ECMAScript 中为数不多的几个二进制数学操作符之一

let num1 = 25;  //      二进制 0000 0000 0000 0000 0000 0000 0001 1001
let num2 = ~num1;  //   二进制 1111 1111 1111 1111 1111 1111 1111 00110
// num2 按位非操作符用到了数值25,得到的结果是 -26
// 按位非得最终效果是对数值取反并减 1
console.log(num2)  // -26

// 像执行了如下的操作结果一样
console.log(-25 - 1)

// 尽管两者返回的结果一样,但位操作的速度快得多,因为位操作是在数值的底层表示上完成的

按位与 &

按位与符号用和号(&)表示,有两个操作数
本质上,按位与就是将两个数的每一个位对齐,然后基于真值表中的规则,对每一位执行相应的与操作

第一个数值的位第二个数值的位结果
111
100
010
000

按位与操作符在两个位都是 1 时返回 1,在任何一位是 0 时返回 0

let result = 25 & 3;
console.log(result)  // 1

/*
result   二进制 0000 0000 0000 0000 0000 0000 0001 1001
3        二进制 0000 0000 0000 0000 0000 0000 0000 0011
----------------------------------------------------------
结果      二进制 0000 0000 0000 0000 0000 0000 0000 0001
 */

按位或 |

按位或操作符用管道符(|)表示,同样有两个操作数
按位或遵循如下真值表

第一个数值的位第二个数值的位结果
111
101
011
000
按位与操作符在两个位都是 1 时返回 1,在任何一位是 1 时返回 1
let result = 25 | 3;
console.log(result)  // 27

/*
result   二进制 0000 0000 0000 0000 0000 0000 0001 1001
3        二进制 0000 0000 0000 0000 0000 0000 0000 0011
----------------------------------------------------------
结果      二进制 0000 0000 0000 0000 0000 0000 0001 1011
 */

按位异或

按位异或用脱字符(^)表示,同样有两个操作数

第一个数值的位第二个数值的位结果
110
101
011
000

按位异或与按位或的区别是:它只在一位是 1 的时候返回1(两位都是 1或0,则返回 0)

let result = 25 ^ 3;
console.log(result)  // 26

/*
result   二进制 0000 0000 0000 0000 0000 0000 0001 1001
3        二进制 0000 0000 0000 0000 0000 0000 0000 0011
----------------------------------------------------------
结果      二进制 0000 0000 0000 0000 0000 0000 0001 1010

*/

左移 <<

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

let oldValue = 2;
let newValue = oldValue << 5;
console.log(newValue)

/*
oldValue 0000 0000 0000 0000 0000 0000 0000 0010
<< 5
newValue 0000 0000 0000 0000 0000 0000 0100 0000

 */

在左移位 n 次后,数值右端会空出 n 位,左移会以 0 填充这些空位,让结果是完整的 32 位数值
左移会保留它所操作数值的符号(符号位)例:-2 左移 5 位,得到 -64 而不是 正64

有符号右移

有符号右移由两个大于号(>>)表示,会将数值的所有 32 位都向右移,同时保留符号(正或负)
有符号右移实际上是左移的逆运算。

let oldValue = 64;
let newValue = oldValue >> 5;
console.log(newValue)  // 2

/*
oldValue 0000 0000 0000 0000 0000 0000 0100 0000
newValue 0000 0000 0000 0000 0000 0000 0000 0010
 */
 

同样,移位后就会出现空位
右移后空位会出现在左侧,且在符号位之后 ECMAScript 会用符号位的值来填充这些空位,以得到完整的数值

无符号右移

无符号右移用 3 个大于号表示(>>>),会将数值的所有 32 位都向右移
对于正数,无符号右移与有符号右移结果相同

let oldValue = 64;
let newValue = oldValue >>> 5;
console.log(newValue)  // 2

/*
oldValue 0000 0000 0000 0000 0000 0000 0100 0000
newValue 0000 0000 0000 0000 0000 0000 0000 0010
 */
 

对于负数,有时候差异会非常大。与有符号右移不同,无符号右移会给空位补 0,而不管符号位是什么。
对正数来说,这跟有符号右移效果相同。
对负数来说,结果就差太多了
无符号右移将负数的二进制表示当成正数的二进制表示来处理
因为负数是其他绝对值的二补数,所以右移之后结果变得非常之大

let oldValue = -64;
let newValue = oldValue >>> 5;
console.log(newValue)  // 134217726

/*
oldValue 1111 1111 1111 1111 1111 1111 1100 0000
>>> 5(会将符号位变成0,既正数)
newValue 0000 0111 1111 1111 1111 1111 1111 1110

 */

布尔操作符

逻辑非

逻辑非操作符由一个叹号(!)表示,可应用给 ECMAScript 中的任何值。
这个操作符始终返回布尔值,然后再对其取反。

逻辑非操作符会遵循如下规则

如果操作数是对象,则返回 false
如果操作数是空字符串,则返回 true
如果操作数是非空字符串,则返回 false
如果操作数是数值 0,则返回 true
如果操作数是数值 非0,则返回 false
如果操作数是 null,则返回 true
如果操作数是 NaN,则返回 true
如果操作数是 undefined,则返回 true
console.log(!{});  // false
console.log(!{'a': 'b'})  // false
console.log('===============')

console.log(!"")  // true
console.log(!"blue")  // false
console.log('===============')

console.log(!0)  // true
console.log(!1)  // false
console.log('===============')

console.log(!null)  // true
console.log(!NaN)  // true
console.log(!undefined)  // true

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

console.log(!!"blue")
console.log(Boolean("blue"))
console.log('=====')

console.log(!!0)
console.log(Boolean(0))
console.log('=====')

console.log(!!NaN)
console.log(Boolean(NaN))
console.log('=====')

console.log(!!undefined)  // false
console.log(Boolean(undefined))
console.log('=====')

console.log(!!null)
console.log(Boolean(null))
console.log('=====')

逻辑与(一假则假)

逻辑与操作符由两个和号(&&)表示,应用到两个值

let result = true && false;
第一个操作数第二个操作数结果
truetruetrue
truefalsefalse
falsetruefalse
falsefalsefalse
逻辑与操作符可以用于任何类型的操作数,不限于布尔值。
如果由操作数不是布尔值,则逻辑与并不一定会返回布尔值,而是遵循如下规则:
  - 如果第一个操作数是对象,则返回第二个操作数。
  - 如果第二个操作数是对象,则只有第一个操作数求值为 true 才会返回该对象
  - 如果两个操作数都是对象,则返回第二个操作数
  - 如果有一个操作数是 null,则返回 null
  - 如果有一个操作数是 NaN,则返回 NaN
  - 如果有有一个操作数是 undefined,则返回 undefined

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

let found = true;
let result = (found && someUndeclareVariable)  // 这里会报错
console.log(result)  // 不会执行这一行
let found = false;
let result = (found && someUndeclareVariable)
console.log(result)  // 正常执行

逻辑或(一真则真)

逻辑或操作符由两个管道符(||)表示

let result = true || true

逻辑或操作符遵循如下真值表:

第一个操作数第二个操作数结果
truetruetrue
truefalsetrue
falsetruetrue
falsefalsefalse
如果有一个操作数不是布尔值,那么逻辑或操作符也不一定返回布尔值。它遵循如下规则
  - 如果第一个操作数是对象,则返回第一个操作数
  - 如果第一个操作数求值为 false,则返回第二个操作数
  - 如果两个操作数都是对象,则返回第一个操作数
  - 如果两个操作数都是 null,则返回 null(第二个 null)
  - 如果两个操作数都是 undefined,则返回 undefined(第二个 undefined)
let found = true;
let result = (found || someUndeclareVariable)
console.log(result)  // 正常执行
let found = false;
let result = (found || someUndeclareVariable)  // 这里会报错 someUndeclareVariable is not defined
console.log(result)

乘性操作符

乘法操作符(*)

乘法操作符由一个(*)表示,可以用于计算两个数值的乘积,其语法表类似于 C 语言

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

除法操作符(/)

触发操作符由一个斜杠(/)表示,用于计算第一个操作数的商

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

取模操作(%)

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

let result = 26 % 5; // 等于 1
与其他乘性操作符一样,取模操作符对特殊值也有一些特殊的行为
  - 如果操作数是数值,则执行常规除法运算,返回余数
  - 如果被除数是无限值,除数是有限制,则返回 NaN
  - 如果被除数是有限制,除数是 0,则返回 NaN
  - 如果是 Infinity 除以 Infinity,则返回 NaN
  - 如果被除数是有限值,除数是无限值,则返回被除数。
  - 如果被除数是 0,除数不是 0,则返回 0
  - 如果有不是数值的操作数,则先在后台调用 Number() 函数将其转换为数值,然后再应用上述规则

指数操作符

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

console.log(Math.pow(2, 3))
console.log(2 ** 3)

指数操作符也有自己的指数赋值操作符 **=,该操作符执行指数运算和结果的赋值操作

let squared = 2;
squared **= 3;
console.log(squared)

加性操作符

加性操作符,即假发和减法操作符,一般都是变成语言中最简单的操作符
在 ECMAScript 中,这两个操作符拥有一些特殊的行为。
与乘性操作符类似,加性操作符在后台会发生不同数据类型的转换。
只不过对着两个操作符来说,转换规则不是那么直观

加法操作符

加法操作符(+)用于求两个数的和

let result = 1 + 2;
如果两个操作数都是数值,加法操作符执行加法运算并根据如下规则返回结果:
  - 如果有任一操作数是 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"
let result1 = 5 + 5;  // 两个数值
console.log(result1)  // 10

let result2 = 5 + "5";  // 一个数值和一个字符串
console.log(result2)  // 55

减法操作符

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

let result = 2 - 1;
减法操作符也有一组规则用于处理 ECMAScript 中不同类型之间的转换
  - 如果两个操作数都是数值,则执行数学减法运算并返回结果。
  - 如果有任一操作数是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() 方法,然后再将得到的字符串转为数值
let result1 = 5 -true;  // true 被转换为1,所以结果是4
let result2 = NaN -1;  // NaN
let result3 = 5 - 3;  // 2
let result4 = 5 - "";  // "" 被转换为 0,所以结果是 5
let result5 = 5 - "2";  // "2" 被转换为 2,所以结果是 3
let result6 = 5 - null;  // null 被转换为 0,所以结果是 5
console.log(result1)
console.log(result2)
console.log(result3)
console.log(result4)
console.log(result5)
console.log(result6)

关系操作符

关系操作符执行比较两个值的操作,包括 <, >, <=, >=
用法跟数学课上学的一样

let result1 = 5 > 3;  // true
let result2 = 5 < 3;  // false
与 ECMAScript 中的其他操作符一样,在将它们应用到不同数据类型时也会发生类型转换和其他行为
 - 如果操作数都是数值,则执行数值比较
 - 如果操作数都是字符串,则逐个比较字符串中对应的字符的编码
 - 如果有任一操作数是数值,则将另一个操作数转换为数值,执行数值比较
 - 如果有任一操作数是对象,则调用其 valueOf() 方法,取得结果后再根据前面的规则执行比较
   - 如果没有 valueOf() 操作符,则调用 toString() 方法,取得结果后再根据前面的规则执行比较
 - 如果有任一操作数是布尔值,则将其转换为数值再执行比较
// 在使用关系操作符比较两个字符串时,会发生一个有趣的现象
// 很多人认为小于意味着 “字母顺序靠前”
// 大于意味着 “字母顺序靠后”
// 实际上不是这么一回事

// 对于字符串而言,关系操作符会比较字符串中对应字符的编码,而这些编码是数值
// 比较完之后,会返回布尔值
// 大写字母的编码都小于小写字母的编码
let result1 = 'Brick' < 'alphabet';  // true
console.log('result1:  ', result1)

// 比较字符串 "23" 和 "3" 时,会逐个比较它们的字符编码( "2" 的编码是 50, "3" 的编码是 51)
let result2 = "23" < "3"
console.log('result2:  ', result2)

// 如果有一个操作数是数值,那么比较的结果就对了
let result3 = "23" < 3
console.log('result3:  ', result3)
let result4 = 23 < "3"
console.log('result4:  ', result4)

// 字符串不能转换为数值的情况
let result5 = "a" < 3;  // a 会转换为 NaN,所以结果是 false
console.log('result5:  ', result5)

// 因为字符 “a" 不能转换成任何有意义的数值,所以只能转换为 NaN
// 任何关系的操作符在涉及比较 NaN 时都返回 false
let result6 = NaN < 3;
let result7 = NaN >= 3;
console.log('result6:  ', result6)
console.log('result7:  ', result7)

相等操作符

ECMAScript 提供了两组操作符:第一组时等于和不等于,第二组是全等和不全等,它们在比较之前不执行转换

等于和不等于

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

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

全等和不全等

全等和不全等操作符与相等和不相等操作符类似,只不过它们在比较相等时不转换操作数

全等操作符由三个等号(===)表示
只有两个操作数在不转换的前提下相等才返回 true

let result1 = ("55" == 55);  // true 转换后相等
let result2 = ("55" === 55);  // false 不相等,因为数据类型不同

不全等操作符用一个感叹号和两个等于号(!==)表示
只有两个操作数在不转换的前提下不相等才返回 true

let result1 = ("55" != 55)  // false,转换后相等
let result2 = ("55" !== 55)  // true,数据类型不同

另外,虽然 null == undefined 是 ture(因为这两个值类似)
但 null === undefined 是 fasle,因为它们不是相同的数据类型

推荐使用 全等和不全等操作符。这样有助于在代码中保持数据类型的完整性

条件操作符

条件操作符是 ECMAScript 中用途最广泛的操作符之一

varible = boolean_expression ? true_value : false_value;
如果 boolean_expression 是 turue,则返回 true_value 的值
如果 boolean_expression 是 false,则返回 false_value 的值

赋值操作符

简单赋值用等于号(=)表示,将右手边的值赋给左手边的变量

let num = 10;

复合赋值使用乘性,加性或位操作符后跟等于号(=)表示
这些赋值操作符是类似如下常见赋值操作的简写形式

let num = 10;
num = num + 10
// 第二行代码可以通过复合赋值来完成
// num += 10
每个数学操作符以及其他一些操作符都有对应的符合赋值操作符
  - 乘后赋值 *=
  - 除后赋值 /=
  - 取模后赋值 %=
  - 加后赋值 +=
  - 减后赋值 -=
  - 左移后赋值 <<=
  - 右移后赋值 >>=
  - 无符号右移后赋值 >>>=
这些操作符仅仅是简写语法,使用它们并不会提升性能

逗号操作符

逗号操作符可以用来在一条语句中执行多个操作

let num1 = 1, num2 = 2, num3 = 3;

也可以使用逗号操作符来辅助赋值
在赋值时使用逗号操作符分割值,最终会返回表达式中最后一个值;

let num = (5, 1, 4, 8, 0)// num 的值为 0
// 因为 0 是表达式中最后一个值
// 逗号操作符的这种使用场景并不多见,但这种行为确实存在

语句

if 语句

if (condition) statement1 else statement2

if (i > 25){
    console.log('Greater than 25'); // 只有一行代码的语句块
}else{
    console.log("Less than or equal to 25.")  // 一个语句块
}
// 最佳的实践是使用语句块,即使只有一行代码要执行也是如此
// 因为语句块可以避免对什么条件下执行什么产生困惑

可以连续使用多个if语句
if (condition1) statement1 else if(condition2) statement2 else statement3

// 例:
if (i > 25) {
    console.log("Greate than 25.");
} else if (i < 0) {
    console.log("Less than 0.");
} else {
    console.log("Between 0 and 25, inclusive.");
}

do-while 语句

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

/*
do{
    statement
} while (expression);

 */
/*
do{
    statement
} while (expression);

 */
let i = 10;
do{
    i += 2;
}while(i < 10)
console.log(i)


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

后测试循环经常用于这种情形:循环体内代码在推出前至少要执行一次

while 语句

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

// while (expression) statement
let i = 0;
while (i < 10) {
    i += 2
    console.log('i: >> ', i)
}
console.log(i)

let a = 20;
while (i < 10) {
    a += 2
    console.log('a: >> ', a)
}
console.log(a)

for 语句

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

// for (initialization; expression; post-loop-expression) statement
let count = 10;
for (let i = 0; i < count; i++) {
    console.log('i >> ', i)
}

无法通过 while 循环实现的逻辑,也无法使用 for 循环实现
for 循环只是将循环相关的代码封装在了一起而已

在 for 循环的初始化代码中,其实是可以不使用变量声明关键字的。这样初始化定义的迭代器变量在循环执行完成后几乎不可能再用到了。
最清晰的写法是使用 let 声明迭代变量,这样就可以将这个变量的作用域限定在循环中

初始化,条件表达式和循环后的表达式都不是必需的

/*
    // 无穷循环
    for ( ; ;){
        DoSomething()
    }
*/
// 如果包含条件表达式,那么 for 循环是实际上就是 while 循环

for-in 循环

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

// for(property in expression) statement
for (const properName in window) {
    console.log(properName)
    document.write(properName)
}
// 这个例子使用 for-in 循环显示了 BOM 对象 window 的所有属性
// 每次执行循环,都会给变量 propName 赋予一个 window 对象的属性作为值,直到 window 的所有属性都被枚举一遍

与 for 循环一样,这里控制语句的 const 也不是必须的,为了确保这个局部变量不被修改,推荐使用 const
ECMAScript 中对象的属性是无序的,因此 for-in 语句不能保证返回对象属性的顺序(所有的枚举属性都会返回一次,但返回的顺序可能会因浏览器而异)

for-of 语句

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

// for (property of expression) statement
for (const el of [2, 4, 6, 8]){
    document.write(String(el));
    console.log(el);
}

与 for 循环一样,这里控制语句中的 const 也不是必须的,但为了确保这个局部变量不被修改,推荐使用 const
for-of 循环会按照可迭代对象的 next() 方法产生值得顺序迭代元素。
如果尝试迭代的变量不支持迭代,则 for-of 语句会抛出错误

ES2018 对 for-of 语句进行了扩展,增加了 for-await-of 循环,以支持生成期约的(promise)的异步可迭代对象

标签对象(不常见)

标签语句用于给语句添加标签,语法如下

label; satement

// label; statement
start: for(let i = 0; i< count; i++){
    console.log(i)
}
// start 是一个标签
// 通过 break 或 continue 语句引用
// 标签语句的典型应用场景是嵌套循环

break 和 continue 语句

break:用于立即退出循环,强制执行循环后的下一条语句
continue:用于立即退出循环,但会再次从循环顶部开始执行

let num1 = 0
for (let i = 1; i < 10; i++) {
    num1++;
    break  // 立即退出循环,执行下一条语句
}
console.log(num1); // 4


let num2 = 0
for (let i = 1; i < 10; i++) {
    num2++;
    continue  // 跳过这次循环,继续下一次循环
}
console.log(num2); // 9

break 与 continue 都可以与标签语句一起使用,返回代码中特定的位置(这通常是在嵌套循环中)

with 语句

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

with (expression) statement;

使用 with 语句的主要场景是针对一个对象反复操作,这时候将代码作用域设置为该对象能提供遍历

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


// 上面代码中的每一行都使用到了 location 对象
// 使用 with 语句,可以少写一些代码
with (location) {
    let qs = search.substring(1);
    let hostName = hostname;
    let url = href;
}

swicth 语句

switch 语句是与 if 语句紧密相关的一种流控制语句

switch (expression) {
    case value1:
        statement;
        break

    case value2:
        statement;
        break

    case value...:
        statement...;
        break

    default:
        statement
        break
}

每个 case (条件/分支)相当于:如果表达式等于后面的值,则执行下面的语句
break 关键字会导致代码执行跳出 switch,如果没有 break,则代码会继续匹配下一个条件
default 关键字用于在任何条件都没有满足时指定默认执行的语句(相当于 else 语句)

// 有了 switch 语句,开发者就用不着写类似这样的代码了
if (i == 25) {
    console.log("25")
} else if (i == 35) {
    console.log("35")
} else if (i == 45) {
    console.log(45)
} else {
    console.log("Other")
}
// 可以这样写
switch (i) {
    case 25:
        console.log("25")
        break;
    case 35:
        console.log("35")
        break;
    case 45:
        console.log("45")
        break
    default:
        console.log("Other")
}

为了避不必要的条件判断,最好给每个条件后面都加上 break 语句
如果确实需要连续匹配几个条件,那么推荐写个注释表明是故意忽略了 break 语句

switch 语句可以用于所有数据类型(在很多语言中,它只能用于数值)
条件的值不需要是常量,也可以是变量或表达式

switch 语句在比较每个条件的值时会使用全等操作符,因此不会强制转换数据类型(例:字符串“10” 不等于 数值10)

函数

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

// 语法
function functionName(arg0, arg1, ..., argN){
    statements
}
function sayHi(name, message){
    console.log("Hello " + name  + ", " + message);
}
// 通过函数名来调用函数,要传给函数的参数放在括号里(如果有多个参数,则用逗号隔开)
sayHi("Nicholas", "how are you today?");
console.log("==========")

function sum(sum1, sum2){
    return sum1 + sum2
}
//函数 sum() 会将两个值相加并返回结果。
// 除了 return 语句之外没有任何特殊声明表明该函数有返回值。
// return 语句后面的代码不会被执行
const result = sum(5, 10)
console.log(result)

最佳实践是函数要么返回值,要么不返回值。只在某个条件下返回值的函数会带来麻烦,尤其是调试时

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

- 函数不能以 eval 或 arguments 作为名称;
- 函数的参数不能叫 eval 或 argument;
- 两个命名参数不能拥有同一个名称.
如果违反上述规则,会导致语法错误,代码也不会执行
;