JavaScript笔记(三)
读书笔记
《JavaScript高级程序设计(第4版)》
语言基础
JavaScript是通过<script>元素插入到HTML页面中的。
- 要包含外部JavaScript文件,必须将src属性设置为要包含文件的URL。文件可以跟网页在同一台服务器上,也可以位于完全不同的域。
- 所有<script>元素会依照它们在网页中出现的次序被解释。在不使用defer和async属性的情况下,包含在<script>元素中的代码必须严格按次序解释。
- 对不推迟执行的脚本,浏览器必须解释完位于<script>元素的代码,然后才能继续渲染页面的剩余部分。为此,通常应该把<script>元素放到页面末尾,介于主内容之后及</body>标签之前。
- 可以使用defer属性把脚本推迟到文档渲染完毕后再执行。推迟的脚本原则上按照它们被列出的次序执行。
- 可以使用async属性表示脚本不需要等待其他脚本,同时也不阻塞文档渲染,即异步加载。异步脚本不能保证按照它们在页面中出现的次序执行。
- 通过使用<noscript>元素,可以指定在浏览器不支持脚本时显示的内容。如果浏览器支持并启用脚本,则<noscript>元素中的任何内容都不会被渲染。
内容概要
3.1 语法
3.1.1 区分大小写
ECMAScript中一切都区分大小写。
3.1.2 标识符
注意:关键字、保留字、true、false和null不能作为标识符。
3.1.3 注释
ECMAScript采用C语言风格的注释,包括单行注释和块注释。
- // :单行注释
- /* 注释 */ :多行注释
3.1.4 严格模式
严格模式(strict mode)是一种不同的JavaScript解析和执行模型,ECMAScript3的一些不规范写法在这种模式下会被处理,对于不安全的活动将抛出错误。
要对整个脚本启用严格模式,在脚本开头加上这一行:
"use strict";
这是一个预处理指令。任何支持的JavaScript引擎看到它都会切换到严格模式。
也可以单独指定一个函数在严格模式下执行,只要把这个预处理指令放到函数体开头即可:
function doSomething() {
"use strict";
//函数体
}
3.1.5 语句
ECMAScript中的语句以分号结尾。
3.1.6 关键字与保留字
保留的关键字不能用作标识符或属性名。
3.3 变量
ECMAScript变量是松散类型的,意思是变量可以用于保存任何类型的数据。每个变量只不过是一个用于保存任意值的命名占位符。
var在ECMAScript的所有版本中都可以使用,
const和let只能在ECMAScript6及更晚的版本中使用。
3.3.1 var关键字
- var声明作用域
使用var操作符定义的变量会成为包含它的函数的局部变量。
如果需要定义多个变量,可以在一条语句中用逗号分隔每个变量(及可选的初始化):
var message = "hi",
found = false,
age = 29;
因为ECMAScript是松散类型的,所以使用不同数据类型初始化的变量可以用一条语句来声明。插入换行和空格缩进并不是必需的,但这样有利于阅读理解。
> 在严格模式下,不能定义名为eval和arguments的变量,否则会导致语法错误。
function foo(){
console.log(age);
var age = 26;
}
foo(); //undefined
之所以不会报错,是因为ECMAScript运行时把它看成等价于如下代码:
function foo(){
var age;
console.log(age);
age = 26;
}
foo(); //undefined
此外,反复多次使用var声明同一个变量也没有问题:
function foo() {
var age = 16;
var age = 26;
var age = 36;
console.log(age);
}
foo(); //36
3.3.2 let声明
let和var的作用差不多,但有着非常重要的区别。
let声明的范围是块作用域,而var声明的范围是函数作用域。
if(true) {
var name = 'Matt';
console.log(name); //Matt
}
console.log(name); //Matt
if(true) {
let age = 26;
console.log(age); //26
}
console.log(age); //ReferenceError: age没有定义
let也不允许同一个块作用域中出现冗余声明。这样会导致报错:
var name;
var name;
let age;
let age; //SyntaxError:标识符age已经声明过了
-
暂时性死区
let声明的变量不会在作用域中被提升。在let声明之前的执行瞬间被称为“暂时性死区”(temporal dead zone),在此阶段引用任何后面才声明的变量都会抛出ReferenceError。 -
全局声明
与var关键字不同,使用let在全局作用域中声明的变量不会成为window对象的属性(var声明的变量则会)。var name = 'Matt'; console.log(window.name); //'Matt' let age = 26; console.log(window.age); //undefined
加粗样式let声明仍然是在全局作用域中发生的,相应变量会在页面的生命周期内存续。因此,为了避免SyntaxError,必须确保页面不会重复声明同一个变量。
-
条件声明
由于var声明会被提升,JavaScript引擎会自动将多余的声明在作用域顶部合并为一个声明。因为let的作用域是块,所以不可能检查前面是否已经使用let声明过同名变量,同时也就不可能在没有声明的情况下声明它。<script> var name = 'Nicholas'; let age = 26; </script> <script> var name = 'Matt'; //不需要检查之前是否声明过同名变量 let age = 36; //如果age之前声明过,这里会报错 </script>
-
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没有定义
3.3.3 const声明
const的行为与let基本相同,唯一一个重要的区别是用它声明变量时必须同时初始化变量,且尝试修改const声明的变量会导致运行时错误。
const age = 36;
age = 36; //TypeError:给常量赋值
//const也不允许重复声明
const name = 'Matt';
const name = 'Nicholas'; //SyntaxError
//const声明的作用域也是块
const name = 'Matt';
if(true){
const name = 'Nicholas';
}
const.log(name); //Matt
const声明的限制只适用于它指向的变量的引用。换句话说,如果const变量引用的是一个对象,那么修改这个对象内部的属性并不违反const的限制。
const person = {};
person.name = 'Matt'; //ok
JavaScript引擎会为for循环中的let声明分别创建独立的变量实例,虽然const变量跟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);
}
// 7,7,7,7,7
for(const key in {a: 1, b: 2}){
console.log(key);
}
//a,b
for(const value of [1,2,3,4,5]){
console.log(value);
}
//1,2,3,4,5
3.3.4声明风格及最佳实践
- 不使用var
只使用let和const有助于提升代码质量,因为变量有了明确的作用域、声明位置,以及不变的值。 - const优先,let次之
使用const声明可以让浏览器运行时强制保持变量不变,也可以让静态代码分析工具提前发现不合法的赋值操作。只在提前知道未来会有修改时,再使用let。
3.4 数据类型
ECMAScript有6种简单数据类型(原始类型):Undefined、Null、Boolean、Number、String和Symbol。1种复杂数据类型:Object(对象)。Object是一种无序名值对的集合。
3.4.1 typeof操作符
确定任意变量的数据类型。对一个值使用typeof操作符会返回下列字符串之一:
- “undefined”:值未定义
- “boolean”:值为布尔值
- “string”:值为字符串
- “number”:值为数值
- “object”:值为对象(而不是函数)或null
- “function”:值为函数
- “symbol”:值为符号
特殊值null被认为是一个对空对象的引用。
let message = "some string";
console.log(typeof messsage); //"string"
console.log(typeof(message)); //"string"
console.log(typeof 95); //"number"
注意:严格来讲,函数在ECMAScript中被认为是对象,并不代表一种数据类型。函数也有自己特殊的属性,通过typeof操作符来区分函数和其他对象。
3.4.2 Undefined类型
Undefined类型只有一个值,就是特殊值undefined。当使用var或let声明了变量但没有初始化时,就相当于给变量赋予了undefined值:
let message;
console.log(message == undefined); //true
在默认情况下,任何未经初始化的变量都会取得undefined值。
3.4.3 Null类型
Null类型同样只有一个值,即特殊值null。逻辑上讲,null值表示一个空对象指针,这也是给typeof传一个null会返回"object"的原因:
let car = null;
console.log(typeof car); //"object"
在定义将来要保存对象值的变量时,建议使用null来初始化,不要使用其他值。
if(car != null){
//car是一个对象的引用
}
undefined值是由null值派生而来的,因此ECMA-262将它们定义为表面上相等。
console.log(null == undefined); //true
永远不必显式地将变量值设置为 undefined。但 null 不是这样的。任何时候,只要变量要保存对象,而当时又没有那个对象可保存,就要用 null 来填充该变量。这样就可以保持 null 是空对象指针的语义,并进一步将其与 undefined 区分开来。
null 是一个假值。也有很多其他可能的值同样是假值。所以一定要明确自己想检测的就是 null 这个字面值,而不仅仅是假值。
let message = null;
let age;
if (message) {
// 这个块不会执行
}
if (!message) {
// 这个块会执行
}
if (age) {
// 这个块不会执行
}
if (!age) {
// 这个块会执行
}
3.4.4 Boolean 类型
布尔值字面量 true 和 false 是区分大小写的
要将一个其
他类型的值转换为布尔值,可以调用特定的 Boolean()转型函数:
let message = "Hello World!";
let messageAsBoolean = Boolean(message);
数据类型 | 转换为 true 的值 | 转换为 false 的值 |
---|---|---|
Boolean | true | false |
String | 非空字符串 | “”(空字符串) |
Number | 非零数值(包括无穷值) | 0、NaN |
Object | 任意对象 | null |
Undefined | N/A(不存在) | undefined |
3.4.5 Number 类型
Number 类型使用 IEEE 754 格式表示整数和浮点值(在某些语言中也叫双精度值)。不同的数值类型相应地也有不同的数值字面量格式。
最基本的数值字面量格式是十进制整数,直接写出来即可:
let intNum = 55; // 整数
整数也可以用八进制(以 8 为基数)或十六进制(以 16 为基数)字面量表示。对于八进制字面量,
第一个数字必须是零(0),然后是相应的八进制数字(数值 0~7)。如果字面量中包含的数字超出了应
有的范围,就会忽略前缀的零,后面的数字序列会被当成十进制数,如下所示:
let octalNum1 = 070; // 八进制的 56
let octalNum2 = 079; // 无效的八进制值,当成 79 处理
let octalNum3 = 08; // 无效的八进制值,当成 8 处理
要创建十六进制字面量,必须让真正的数值前缀 0x(区分大小写),然后是十六进制数字(0~9 以 及 A~F)。十六进制数字中的字母大小写均可。
let hexnum1 = 0xA; // 十六进制 10
let hexnum2 = 0x1f; // 十六进制 31
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
2. 值的范围
ECMAScript 可以表示的最小数值保存在 Number.MIN_VALUE
中,这个值在多数浏览器中是 5e324;可以表示的最大数值保存在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
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
4. 数值转换
有 3 个函数可以将非数值转换为数值:Number()、parseInt()和 parseFloat()。Number()是
转型函数,可用于任何数据类型。后两个函数主要用于将字符串转换为数值。
Number()
Number()
函数基于如下规则执行转换。
- 布尔值,true 转换为 1,false 转换为 0。
- 数值,直接返回。
- null,返回 0。
- undefined,返回 NaN。
- 对象,调用 valueOf()方法,并按照上述规则转换返回的值。如果转换结果是 NaN,则调用
toString()方法,再按照转换字符串的规则转换。 - 字符串,应用以下规则
如果字符串包含数值字符,包括数值字符前面带加、减号的情况,则转换为一个十进制数值。
因此,Number(“1”)返回 1,Number(“123”)返回 123,Number(“011”)返回 11(忽略前面
的零)。
如果字符串包含有效的浮点值格式如"1.1",则会转换为相应的浮点值(同样,忽略前面的零)。
如果字符串包含有效的十六进制格式如"0xf",则会转换为与该十六进制值对应的十进制整
数值。
如果是空字符串(不包含字符),则返回 0。
如果字符串包含除上述情况之外的其他字符,则返回 NaN。
let num1 = Number("Hello World!"); // NaN
let num2 = Number(""); // 0
let num3 = Number("000011"); // 11
let num4 = Number(true); // 1
字符串"Hello world"转换之后是 NaN,因为它找不到对应的数值。
空字符串转换后是 0。
字符串 000011 转换后是 11,因为前面的零被忽略了。
最后,true 转换为 1。
parseInt()
通常在需要得到整数时可以优先使用 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()也接收第二个参数,用于指定底数(进制数)。如
果知道要解析的值是十六进制,那么可以传入 16 作为第二个参数,以便正确解析:
let num = parseInt("0xAF", 16); // 175
事实上,如果提供了十六进制参数,那么字符串前面的"0x"可以省掉:
let num1 = parseInt("AF", 16); // 175
let num2 = parseInt("AF"); // NaN
在这个例子中,第一个转换是正确的,而第二个转换失败了。区别在于第一次传入了进制数作为参
数,告诉 parseInt()要解析的是一个十六进制字符串。而第二个转换检测到第一个字符就是非数值字
符,随即自动停止并返回 NaN。
通过第二个参数,可以极大扩展转换后获得的结果类型。比如:
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 开始检测每个字符。
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 字符序列。字符串可以使用双引号(")、
单引号(')或反引号(`)标示
1. 字符字面量
字符串数据类型包含一些字符字面量,用于表示非打印字符或有其他用途的字符
字 面 量 | 含 义 |
---|---|
\n | 换行 |
\t | 制表 |
\b | 退格 |
\r | 回车 |
\f | 换页 |
\ | 反斜杠(\) |
’ | 单引号(‘),在字符串以单引号标示时使用,例如’He said, ‘hey.’’ |
" | 双引号(“),在字符串以双引号标示时使用,例如"He said, “hey.”” |
` | 反引号(`),在字符串以反引号标示时使用,例如`He said, `hey.`` |
\xnn | 以十六进制编码 nn 表示的字符(其中 n 是十六进制数字 0~F),例如\x41 等于"A" |
\unnnn | 以十六进制编码 nnnn 表示的 Unicode 字符(其中 n 是十六进制数字 0~F),例如\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()返回数值的十进制字符串表示。而通过传入参数,可以得到数值的二进制、八进制、十六进制,或者其他任何有效基数的字符串表示
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()函数遵循如下规则。
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"
这里展示了将 4 个值转换为字符串的情况:一个数值、一个布尔值、一个 null 和一个 undefined。
数值和布尔值的转换结果与调用 toString()相同。因为 null 和 undefined 没有 toString()方法,
所以 String()方法就直接返回了这两个值的字面量文本。
4. 模板字面量
与使用单引号或双引号不同,模板字面量保留换行字符,可以跨行定义字符串:
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 句法表达式,只不过求值后得到的是字符串。模板字面量在定义时立即求值并转换为字符串实例,任何插入的变量也会从它们最接近的作用域中取值。
字符串插值通过在${}中使用一个 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
所有插入的值都会使用 toString()强制转型为字符串,而且任何 JavaScript 表达式都可以用于插
值。嵌套的模板字符串无须转义:
console.log(`Hello, ${ `World` }!`); // Hello, World!
将表达式转换为字符串时会调用 toString():
let foo = { toString: () => 'World' };
console.log(`Hello, ${ foo }!`); // Hello, World!
function capitalize(word) {
return `${ word[0].toupperCase() }${ word.slice(1) }`;
}
console.log(`${ capitalize('hello') }, ${ capitalize('world') }!`); // Hello, World!
此外,模板也可以插入自己之前的值:
let value = '';
function append() {
value = `${value}abc`
console.log(value);
}
append(); // abc
append(); // abcabc
append(); // abcabcabc
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"
因为表达式参数的数量是可变的,所以通常应该使用剩余操作符(rest operator)将它们收集到一个数组中:
let a = 6;
let b = 9;
function simpleTag(strings, ...expressions) {
console.log(strings);
for(const expression of expressions) {
console.log(expression);
}
return 'foobar';
}
let taggedResult = simpleTag`${ a } + ${ b } = ${ a + b }`;
// ["", " + ", " = ", ""]
// 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 linesecond line`);
// first line
// second line
console.log(String.raw`first line
second line`);
// first line
// second line
另外,也可以通过标签函数的第一个参数,即字符串数组的.raw 属性取得每个字符串的原始内容:
function printRaw(strings) {
console.log('Actual characters:');
for (const string of strings) {
console.log(string);
}
console.log('Escaped characters;');
for (const rawString of strings.raw) {
console.log(rawString);
}
}
printRaw`\u00A9${ 'and' }\n`;
// Actual characters:
// ©
//(换行符)
// Escaped characters:
// \u00A9
// \n
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
即使采用相同的符号描述,在全局注册表中定义的符号跟使用 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
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)]
总结
1、Symbol(key)与Symbol.for(key)的区别
- Symbol.for(key)会在全局环境下搜索是否存在对应key的Symbol对象,如果不存在时创建该对象并添加到全局环境下
- Symbol()不会进行该搜索,即使key值相同,创建的symbol对象也是不同的
2、注意事项
Symbol 本质上是一种唯一标识符,可用作对象的唯一属性名,这样其他人就不会改写或覆盖你设置的属性值。
- Symbol值不能与其他类型的值进行运算
- Symbol 值不可以和其他类型值进行混合运算,否则会报错
- Symbol值如果想要作为属性名,那就不能再用点运算符,因为点运算符后面跟的总是字符串
- 在对象内部使用Symbol值作为属性名的时候,必须要将值放在方括号中
3.4.8 Object 类型
以通过创建 Object 类型的实例来创建自己的对象,然后再给对象添加属性和方法:
let o = new Object();
Object 的实例本身并不是很有用,但理解与它相关的概念非常重要。类似 Java 中的 java.lang.
Object,ECMAScript 中的 Object 也是派生其他对象的基类。Object 类型的所有属性和方法在派生
的对象上同样存在。
每个 Object 实例都有如下属性和方法。
- constructor:用于创建当前对象的函数。在前面的例子中,这个属性的值就是 Object() 函数
- hasOwnProperty(propertyName):用于判断当前对象实例(不是原型)上是否存在给定的属性。要检查的属性名必须是字符串(如 o.hasOwnProperty(“name”))或符号。
- isPrototypeOf(object):用于判断当前对象是否为另一个对象的原型。(第 8 章将详细介绍 原型。)
- propertyIsEnumerable(propertyName):用于判断给定的属性是否可以使用(本章稍后讨 论的)for-in语句枚举。与 hasOwnproperty()一样,属性名必须是字符串。
- toLocaleString():返回对象的字符串表示,该字符串反映对象所在的本地化执行环境。
- toString():返回对象的字符串表示。
- valueOf():返回对象对应的字符串、数值或布尔值表示。通常与toString()的返回值相同。 因为在 ECMAScript 中 Object 是所有对象的基类,所以任何对象都有这些属性和方法。第8 章将 介绍对象间的继承机制。
3.5 操作符
ECMA-262 描述了一组可用于操作数据值的操作符,包括数学操作符(如加、减)、位操作符、关系操作符和相等操作符等。ECMAScript 中的操作符是独特的,因为它们可用于各种值,包括字符串、数值、布尔值,甚至还有对象。在应用给对象时,操作符通常会调用 valueOf()和/或 toString()方法来取得可以计算的值。
3.5.1 一元操作符
只操作一个值的操作符叫一元操作符(unary operator)。一元操作符是 ECMAScript中最简单的操作符。
1. 递增/递减操作符
前缀递增操作符会给数值加 1,把两个加号(++)放到变量前头即可:
let age = 29;
++age;
使用前缀递减操作符,只要把两个减号(–)放到变量前头即可:
let age = 29;
--age;
递增和递减操作符遵循如下规则。
对于字符串,如果是有效的数值形式,则转换为数值再应用改变。变量类型从字符串变成数值。
对于字符串,如果不是有效的数值形式,则将变量的值设置为 NaN 。变量类型从字符串变成
数值。
对于布尔值,如果是 false,则转换为 0 再应用改变。变量类型从布尔值变成数值。
对于布尔值,如果是 true,则转换为 1 再应用改变。变量类型从布尔值变成数值。
对于浮点值,加 1 或减 1。 如果是对象,则调用其(第 5 章会详细介绍的)valueOf()方法取得可以操作的值。对得到的
值应用上述规则。如果是 NaN,则调用 toString()并再次应用其他规则。变量类型从对象变成
数值。
下面的例子演示了这些规则:
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. 一元加和减
一元加和减操作符对大多数开发者来说并不陌生,它们在 ECMAScript 中跟在高中数学中的用途一
样。一元加由一个加号(+)表示,放在变量前头,对数值没有任何影响:
let num = 25;
num = +num;
console.log(num); // 25
如果将一元加应用到非数值,则会执行与使用 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; // 值变成数值 1
s2 = +s2; // 值变成数值 1.1
s3 = +s3; // 值变成 NaN
b = +b; // 值变成数值 0
f = +f; // 不变,还是 1.1
o = +o; // 值变成数值-1
一元减由一个减号(-)表示,放在变量前头,主要用于把数值变成负值,如把 1 转换为-1。示例
如下:
let num = 25;
num = -num;
console.log(num); // -25
对数值使用一元减会将其变成相应的负值(如上面的例子所示)。在应用到非数值时,一元减会遵
循与一元加同样的规则,先对它们进行转换,然后再取负值:
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 位表示,而是先把值转换为32 位整数,再进行位操作,之后再把结果转换为 64 位。对开发者而言,就好像只有 32 位整数一样,因
为 64 位整数存储格式是不可见的。既然知道了这些,就只需要考虑 32 位整数即可。
有符号整数使用 32 位的前 31 位表示整数值。第 32 位表示数值的符号,如 0 表示正,1 表示负。这一位称为符号位(sign bit),它的值决定了数值其余部分的格式。正值以真正的二进制格式存储,即 31位中的每一位都代表 2 的幂。第一位(称为第 0 位)表示 20,第二位表示 21,依此类推。
特殊值NaN 和Infinity在位操作中都会被当成 0 处理。如果将位操作符应用到非数值,那么首先会使用 Number()函数将该值转换为数值(这个过程是自动的),然后再应用位操作。最终结果是数值。
1. 按位非
按位非操作符用波浪符(~)表示,它的作用是返回数值的一补数。按位非是 ECMAScript 中为数
不多的几个二进制数学操作符之一。看下面的例子:
let num1 = 25; // 二进制 00000000000000000000000000011001
let num2 = ~num1; // 二进制 11111111111111111111111111100110
console.log(num2); // -26
这里,按位非操作符作用到了数值 25,得到的结果是-26。由此可以看出,按位非的最终效果是对
数值取反并减 1,就像执行如下操作的结果一样:
let num1 = 25;
let num2 = -num1 - 1;
console.log(num2); // "-26"
实际上,尽管两者返回的结果一样,但位操作的速度快得多。这是因为位操作是在数值的底层表示
上完成的。
2. 按位与
按位与操作符用和号(&)表示,有两个操作数。本质上,按位与就是将两个数的每一个位对齐,
然后基于真值表中的规则,对每一位执行相应的与操作。
第一个数值的位 | 第二个数值的位 | 结 果 |
---|---|---|
1 | 1 | 1 |
1 | 0 | 0 |
0 | 1 | 0 |
0 | 0 | 0 |
3. 按位或
按位或操作符用管道符(|)表示,同样有两个操作数。按位或遵循如下真值表:
第一个数值的位 | 第二个数值的位 | 结 果 |
---|---|---|
1 | 1 | 1 |
1 | 0 | 1 |
0 | 1 | 1 |
0 | 0 | 0 |
4. 按位异或
按位异或用脱字符(^)表示,同样有两个操作数。下面是按位异或的真值表:
第一个数值的位 | 第二个数值的位 | 结 果 |
---|---|---|
1 | 1 | 0 |
1 | 0 | 1 |
0 | 1 | 1 |
0 | 0 | 0 |
按位异或与按位或的区别是,它只在一位上是 1 的时候返回 1(两位都是 1 或 0,则返回 0) |
5. 左移
左移操作符用两个小于号(<<)表示,会按照指定的位数将数值的所有位向左移动。
比如,如果数值 2(二进制 10)向左移 5 位,就会得到 64(二进制 1000000),如下所示:
let oldValue = 2; // 等于二进制 10
let newValue = oldValue << 5; // 等于二进制 1000000,即十进制 64
注意在移位后,数值右端会空出 5 位。左移会以 0 填充这些空位,让结果是完整的 32 位数值
注意,左移会保留它所操作数值的符号。比如,如果-2 左移 5 位,将得到-64,而不是正 64。
6. 有符号右移
有符号右移由两个大于号(>>)表示,会将数值的所有 32 位都向右移,同时保留符号(正或负)。
有符号右移实际上是左移的逆运算。
比如,如果将 64 右移 5 位,那就是 2:
let oldValue = 64; // 等于二进制 1000000
let newValue = oldValue >> 5; // 等于二进制 10,即十进制 2
同样,移位后就会出现空位。不过,右移后空位会出现在左侧,且在符号位之后。ECMAScript 会用符号位的值来填充这些空位,以得到完整的数值。
7. 无符号右移
无符号右移用 3 个大于号表示(>>>),会将数值的所有 32 位都向右移。对于正数,无符号右移与有符号右移结果相同。
仍然以前面有符号右移的例子为例,64 向右移动 5 位,会变成 2:
let oldValue = 64; // 等于二进制 1000000
let newValue = oldValue >>> 5; // 等于二进制 10,即十进制 2
对于负数,有时候差异会非常大。与有符号右移不同,无符号右移会给空位补 0,而不管符号位是什么。对正数来说,这跟有符号右移效果相同。但对负数来说,结果就差太多了。无符号右移操作符将负数的二进制表示当成正数的二进制表示来处理。因为负数是其绝对值的二补数,所以右移之后结果变得非常之大,如下面的例子所示:
let oldValue = -64; // 等于二进制 11111111111111111111111111000000
let newValue = oldValue >>> 5; // 等于十进制 134217726
在对-64 无符号右移 5 位后,结果是 134 217 726。这是因为-64 的二进制表示是 11111111111111111111111111000000,无符号右移却将它当成正值,也就是 4 294 967 232。把这个值右移 5 位后,结果是00000111111111111111111111111110,即 134 217 726。
3.5.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. 逻辑与
逻辑与操作符由两个和号(&&)表示,应用到两个值,如下所示:
let result = true && false;
逻辑与操作符遵循如下真值表:
第一个操作数 | 第二个操作数 | 结 果 |
---|---|---|
true | true | true |
true | false | false |
false | true | false |
false | false | false |
逻辑与操作符可用于任何类型的操作数,不限于布尔值。如果有操作数不是布尔值,则逻辑与并不
一定会返回布尔值,而是遵循如下规则。
- 如果第一个操作数是对象,则返回第二个操作数。
- 如果第二个操作数是对象,则只有第一个操作数求值为 true才会返回该对象。
- 如果两个操作数都是对象,则返回第二个操作数。
- 如果有一个操作数是 null,则返回 null。
- 如果有一个操作数是NaN,则返回 NaN。
- 如果有一个操作数是 undefined,则返回 undefined。
逻辑与操作符是一种短路操作符,意思就是如果第一个操作数决定了结果,那么永远不会对第二个操作数求值。对逻辑与操作符来说,如果第一个操作数是 false,那么无论第二个操作数是什么值,结果也不可能等于 true。看下面的例子:
let found = true;
let result = (found && someUndeclaredVariable); // 这里会出错
console.log(result); // 不会执行这一行
上面的代码之所以会出错,是因为 someUndeclaredVariable 没有事先声明,所以当逻辑与操作符对它求值时就会报错。变量 found 的值是 true,逻辑与操作符会继续求值变量 someUndeclaredVariable。但是由于 someUndeclaredVariable 没有定义,不能对它应用逻辑与操作符,因此就报错了。假如变量 found 的值是 false,那么就不会报错了:
let found = false;
let result = (found && someUndeclaredVariable); // 不会出错
console.log(result); // 会执行
这里,console.log 会成功执行。即使变量 someUndeclaredVariable 没有定义,由于第一个操作数是 false,逻辑与操作符也不会对它求值,因为此时对&&右边的操作数求值是没有意义的。在使用逻辑与操作符时,一定别忘了它的这个短路的特性。
3. 逻辑或
逻辑或操作符由两个管道符(||)表示,比如:
let result = true || false;
逻辑或操作符遵循如下真值表:
第一个操作数 | 第二个操作数 | 结 果 |
---|---|---|
true | true | true |
true | false | true |
false | true | true |
false | false | false |
同样与逻辑与类似,逻辑或操作符也具有短路的特性。只不过对逻辑或而言,第一个操作数求值为true,第二个操作数就不会再被求值了。看下面的例子:
let found = true;
let result = (found || someUndeclaredVariable); // 不会出错
console.log(result); // 会执行
跟前面的例子一样,变量 someUndeclaredVariable 也没有定义。但是,因为变量 found 的值为 true,所以逻辑或操作符不会对变量 someUndeclaredVariable 求值,而直接返回 true。假如把found 的值改为 false,那就会报错了:
let found = false;
let result = (found || someUndeclaredVariable); // 这里会出错
console.log(result); // 不会执行这一行
利用这个行为,可以避免给变量赋值 null 或 undefined。比如:
let myObject = preferredobject || backupObject;
在这个例子中,变量 myObject 会被赋予两个值中的一个。其中,preferredobject 变量包含首选的值,backupObject 变量包含备用的值。如果 preferredobject 不是 null,则它的值就会赋给myObject;如果 preferredobject 是 null,则 backupObject 的值就会赋给 myObject。这种模式在 ECMAScript 代码中经常用于变量赋值,本书后面的代码示例中也会经常用到。
3.5.4 乘性操作符
ECMAScript 定义了 3 个乘性操作符:乘法、除法和取模。
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
3.5.6 加性操作符
加性操作符,即加法和减法操作符,一般都是编程语言中最简单的操作符。
1. 加法操作符
加法操作符(+)用于求两个数的和,比如:
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"
以上代码展示了加法操作符的两种运算模式。正常情况下,5 + 5 等于 10(数值),如前两行代码
所示。但是,如果将一个操作数改为字符串,比如"5",则相加的结果就变成了"55"(原始字符串值),
因为第一个操作数也会被转换为字符串。
ECMAScript 中最常犯的一个错误,就是忽略加法操作中涉及的数据类型。比如下面这个例子:
let num1 = 5;
let num2 = 10;
let message = "The sum of 5 and 10 is " + num1 + num2;
console.log(message); // "The sum of 5 and 10 is 510"
这里,变量 message 中保存的是一个字符串,是执行两次加法操作之后的结果。有人可能会认为最终得到的字符串是"The sum of 5 and 10 is 15"。可是,实际上得到的是"The sum of 5 and 10 is 510"。这是因为每次加法运算都是独立完成的。第一次加法的操作数是一个字符串和一个数值(5),结果还是一个字符串。第二次加法仍然是用一个字符串去加一个数值(10),同样也会得到一个字符串。如果想真正执行数学计算,然后把结果追加到字符串末尾,只要使用一对括号即可:
let num1 = 5;
let num2 = 10;
let message = "The sum of 5 and 10 is " + (num1 + num2);
console.log(message); // "The sum of 5 and 10 is 15"
在此,我们用括号把两个数值变量括了起来,意思是让解释器先执行两个数值的加法,然后再把结
果追加给字符串。因此,最终得到的字符串变成了"The sum of 5 and 10 is 15"。
2. 减法操作符
减法操作符(-)也是使用很频繁的一种操作符,比如:
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
3.5.7 关系操作符
关系操作符执行比较两个值的操作,包括小于(<)、大于(>)、小于等于(<=)和大于等于(>=),用法跟数学课上学的一样。这几个操作符都返回布尔值。
3.5.8 相等操作符
1. 等于和不等于
ECMAScript 中的等于操作符用两个等于号(==)表示,如果操作数相等,则会返回 true。不等于操作符用叹号和等于号(!=)表示,如果两个操作数不相等,则会返回 true。这两个操作符都会先进行类型转换(通常称为强制类型转换)再确定操作数是否相等。
在转换操作数的类型时,相等和不相等操作符遵循如下规则。
- 如果任一操作数是布尔值,则将其转换为数值再比较是否相等。false 转换为 0,true 转换为1。
- 如果一个操作数是字符串,另一个操作数是数值,则尝试将字符串转换为数值,再比较是否相等。
- 如果一个操作数是对象,另一个操作数不是,则调用对象的valueOf()方法取得其原始值,再根据前面的规则进行比较。
在进行比较时,这两个操作符会遵循如下规则。
- null 和 undefined 相等。
- null 和 undefined 不能转换为其他类型的值再进行比较。
- 如果有任一操作数是NaN,则相等操作符返回 false,不相等操作符返回 true。记住:即使两个操作数都是 NaN,相等操作符也返回false,因为按照规则,NaN 不等于 NaN。
- 如果两个操作数都是对象,则比较它们是不是同一个对象。如果两个操作数都指向同一个对象,则相等操作符返回 true。否则,两者不相等。
下表总结了一些特殊情况及比较的结果。
表 达 式 | 结 果 |
---|---|
null == undefined | true |
“NaN” == NaN | false |
5 == NaN | false |
NaN == NaN | false |
NaN != NaN | true |
false == 0 | true |
true == 1 | true |
true == 2 | false |
undefined == 0 | false |
null == 0 | false |
true == 2 | true |
2. 全等和不全等
全等和不全等操作符与相等和不相等操作符类似,只不过它们在比较相等时不转换操作数。全等操
作符由 3 个等于号(= = =)表示,只有两个操作数在不转换的前提下相等才返回 true,比如:
let result1 = ("55" = = 55); // true,转换后相等
let result2 = ("55" = = = 55); // false,不相等,因为数据类型不同
在这个例子中,第一个比较使用相等操作符,比较的是字符串"55"和数值 55。如前所述,因为字符串"55"会被转换为数值 55,然后再与数值 55 进行比较,所以返回 true。第二个比较使用全等操作符,因为没有转换,字符串和数值当然不能相等,所以返回 false。
不全等操作符用一个叹号和两个等于号(! = =)表示,只有两个操作数在不转换的前提下不相等才返回 true。比如:
let result1 = ("55" != 55); // false,转换后相等
let result2 = ("55" != = 55); // true,不相等,因为数据类型不同
这一次,第一个比较使用不相等操作符,它会把字符串"55"转换为数值 55,跟第二个操作数相等。既然转换后两个值相等,那就返回 false。第二个比较使用不全等操作符。这时候可以这么问:“字符串 55 和数值 55 有区别吗?”答案是“有”(true)。另外,虽然 null = = undefined 是 true(因为这两个值类似),但 null === undefined 是false,因为它们不是相同的数据类型。
3.5.9 条件操作符
条件操作符是 ECMAScript 中用途最为广泛的操作符之一,语法跟 Java 中一样:
variable = boolean_expression ? true_value : false_value;
3.5.10 赋值操作符
简单赋值用等于号(=)表示。
复合赋值使用乘性、加性或位操作符后跟等于号(=)表示。
- 乘后赋值(*=)
- 除后赋值(/=)
- 取模后赋值(%=)
- 加后赋值(+=)
- 减后赋值(-=)
- 左移后赋值(<<=)
- 右移后赋值(>>=)
- 无符号右移后赋值(>>>=)
这些操作符仅仅是简写语法,使用它们不会提升性能。
3.5.11 逗号操作符
逗号操作符可以用来在一条语句中执行多个操作,如下所示:
let num1 = 1, num2 = 2, num3 = 3;
在一条语句中同时声明多个变量是逗号操作符最常用的场景。不过,也可以使用逗号操作符来辅助
赋值。在赋值时使用逗号操作符分隔值,最终会返回表达式中最后一个值:
let num = (5, 1, 4, 8, 0); // num 的值为 0
在这个例子中,num 将被赋值为 0,因为 0 是表达式中最后一项。逗号操作符的这种使用场景并不
多见,但这种行为的确存在。
3.6 语句
ECMA-262 描述了一些语句(也称为流控制语句),而 ECMAScript 中的大部分语法都体现在语句中。语句通常使用一或多个关键字完成既定的任务。语句可以简单,也可以复杂。简单的如告诉函数退出,复杂的如列出一堆要重复执行的指令。
3.7 函数
函数对任何语言来说都是核心组件,因为它们可以封装语句,然后在任何地方、任何时间执行。
ECMAScript 中的函数使用 function 关键字声明,后跟一组参数,然后是函数体。
3.8 小结
JavaScript 的核心语言特性在 ECMA-262 中以伪语言 ECMAScript 的形式来定义。ECMAScript 包含
所有基本语法、操作符、数据类型和对象,能完成基本的计算任务,但没有提供获得输入和产生输出的
机制。理解 ECMAScript 及其复杂的细节是完全理解浏览器中 JavaScript 的关键。下面总结一下ECMAScript 中的基本元素。