为了巩固JavaScript基础知识,去年4月买了一本《JavaScript权威指南》,断断续续地读完,记下了这篇笔记。书确实是一本很好的书,但它挺枯燥的:)更像是文档,很庆幸自己耐心读完了。
第二章 词法结构
-
JavaScript在不能把第二行解析为第一行的联需部分时,对换行符的解释有三种例外情况。
1)第一种情况涉及return、throw、yield、break和continue语句,这些语句经常独立存在,但有时候也会跟一个标识符或表达式。如果这几个单词后面(任何其它标记前面)有换行符,JavaScript就会把这个换行符解释为分号。例如:
return
true;
JavaScript假设你的意图是:
return; true;
但你的意图可能是:
return true;
这意味着,一定不能在return、break或continue等关键字和它们后面的表达式之间加入换行符。如果加入了换行符,那代码出错后的调试会非常麻烦,因为错误不明显。
2)第二种例外情况涉及++和--操作符。这些操作符既可以放在表达式前面,又可以放在表达式后面。如果想把这两个操作符作为后置操作符,那它们必须与自己操作的表达式位于同一行。
3)第三种语法涉及箭头函数,箭头=>必须跟参数列表在同一行。
第三章 类型、值和变量
3.1概述与定义
- JavaScript类型可以分为两类:原始类型和对象类型。数值、字符串、布尔值、符号、null、undefined属于原始类型,其余都是对象类型。
- JavaScript与静态语言更大的差别在于,函数和类不仅仅是语言的语法,它们本身就是可以被JavaScript程序操作的值。
- 在JavaScript中,只有null和undefined是不能调用方法的值。
3.2数值
-
数值字面量的分隔符
let billion = 1_000_000_000;//以下划线作为千分位分隔符
let bytes = 0x89_AB_CD_EF; //作为字节分隔符
let bits = 0b0001_1101_0111; //作为半字节分隔符
let fraction = 0.123_456_789; //也可以用在小数部分
-
Javascript中的算数在遇到上溢出、下溢出或被零除时不会发生错误。下溢出发生在数值操作的结果比最小可表示数值更接近0的情况下。此时,Javascript返回0。如果下溢出来自负数,Javascript返回一个被称为“负零”的特殊值。这个值几乎与常规的0完全无法区分,Javascript程序员极少需要检测它。
负零与正零值相等(即使用JavaScript的严格相等比较),这意味着除了作为除数使用,几乎无法区分这两个值。
-
非数值在JavaScript中有一个不同寻常的特性:它与任何值比较都不想等,也不等于自己。这意味着不能通过x === NaN来确定某个变量x的值是NaN。相反,此时必须写成x !== x或Number.isNaN(x)。这两个表达式当且仅当x与全局变量NaN具有相同值时才返回true。
-
1)BigInt字面量写作一串数字后跟小写字母n。默认情况下,基数是10,但可以通过前缀0b、0o、和0x来表示二进制、八进制、和十六进制BigInt。
2)可以用BigInt()函数把常规JavaScript数值或字符串转换为BigInt值。
3)标准的操作符可用于BigInt,但不能混用BigInt操作数和常规数值操作数。
4)位操作符通常可以用于BigInt操作符。但Math对象的任何函数都不接收BigInt操作数。
3.3文本
- ES5允许把反斜杠放在换行符前面从而将一个字符串字面量拆成多行。写在三行但只有一行的字符串:
-
JavaScript中的字符串是不可修改的。像replace()和toUpperCase()这样的方法都返回新的字符串,它们并不会修改调用它们的字符串。
-
标签化模板字面量。模板字面量有一个强大但不太常用的特性:如果在开头的反引号前面有一个函数名(标签),那么模板字面量中的文本和表达式的值将作为参数传给这个函数。“标签化模板字面量”的值就是这个函数的返回值。这个特性可以用于先对某些值进行HTML或SQL转义,然后再把它们插入文本。
3.5null与undefined
- null和undefined都没有属性和方法。事实上,使用.或者[]访问这两个值的属性和方法会导致TypeError。
3.6符号(Symbol)
-
Symbol()函数可选地接收一个字符串参数,返回唯一的符号值。如果提供了字符串参数,那么调用返回符号值的toString()方法得到的结果会包含该字符串。不过要注意,以相同的字符串调用两次Symbol()会产生两个完全不同的符号值。
-
为了定义一些可以与其它代码共享的符号值,Javascript定义了一个全局符号注册表。Symbol.for()函数接收一个字符串参数,返回一个与该字符串关联的符号值。如果没有符号值与该字符串关联,则会创建并返回一个新符号;否则,就会返回已有的符号。换句话说,Symbol.for()与Symbol()完全不同:Symbol()永远不会返回相同的值,而在以相同的字符串调用时Symbol.for()始终返回相同的值。传给Symbol.for()的字符串会出现在tostring()(返回符号值)的输出中。而且,这个字符串也可以通过将返回的符号传给Symbol.keyFor()来得到:
let s = Symbol.for("shared");
let t = Symbol.for("shared");
s === t // =>true
s.toSting() // =>"Symbol(shared)"
Symbol.keyFor(t) // => "shared"
3.7全局对象
- 在node中,全局对象有一个名为global的属性,其值为全局对象本身,因此在Node程序中始终可以通过global来引用全局对象。
- ES2020最终定义了globalThis作为在任何上下文中引用全局对象的标准方式。2020年初,所有现代浏览器和Node都实现了这个特性。
3.8不可修改的原始值和可修改的对象引用
-
Javascript的字符串不允许被修改,这一点不太好理解。因为字符串类似字符数组,我们或许认为可以修改某个索引位置的字符。事实上,Javascript不允许这么做。所有看起来返回一个修改后字符串的字符串方法,实际上返回的都是一个新字符串。
-
对象不是按值比较的,两个不同的对象即使拥有完全相同的属性和值,它们也不相等。同样,两个不同的数组,即使每个元素都相同,顺序也相同,它们也不相等。对象有时候被称作引用类型,以区别于JavaScript的原始类型。基于这一术语,对象值就是引用,对象是按引用比较的。换句话说,两个对象当且仅当它们引用自同一个底层对象时,才是相等的。
let a = {};
a === {...a} // =>false
3.9类型转换
-
空数组[] 转字符串为空字符串"",转换为数值0,转换为布尔值true;
一个数值元素的数组[9]转换为字符串 "9",转换为数值9,转换为布尔值true;
-
可以解析为数值的字符串都可以转换为对应的数值。字符串开头和结尾都可以有空格,但开头和末尾任何不属于数值字面量的非空格字符,都会导致字符串到数值的转换产生NaN。
-
null和undefined可以通过类型转换转换为false。但 == 操作符不会将null和undefined转换为布尔值。
null == false //=>false
unefined == false // =>false
-
parseInt()只解析整数,而parseFloat()既解析整数也解析浮点数。如果字符串以0x或0X开头,parseInt会将其解析为十六进制数值。parseInt()和parseFloat()都会跳过开头的空格,尽量多地解析数字字符,忽略后面的无关字符。如果第一个非空格字符不是有效的数值字面量,它们会返回NaN。
-
偏数值转换规则的细节可以解释为什么空数组会转换为数值0,而单元素数组也可以转换为数值。
Number([]) // =>0;
Number([99]) //=>99;
对象到数值的转换首先使用偏数值算法把对象转换为一个原始值,然后再把得到的原始值转换为数值。偏数值算法先尝试valueOf(),将toString()作为备用。Array类继承了默认的valueOf()方法,该方法不返回原始值。因此在尝试将数组转换为数值时,最终会调用toString())方法。空数组转换为空字符串。而空字符串转换为数值0。只有一个元素的数组转换为该元素对应的字符串。如果数组只包含一个数值,则该数值先转换为字符串,再转换回数值。
3.10变量声明与赋值
-
在Node和客户端Javascript模块中,全局变量的作用域是定义它们的文件。但在传统客户端JavaScript中,全局变量的作用域是定义它们的HTML文档。换句话说,如果有
<script>标签声明了一个全局变量或常量,则该变量和常量在同一个文档的任何<script>元素中(或者至少在let和const语句执行之后执行的所有脚本中)都有定义。 -
在严格模式下,如果试图使用未声明的变量,那代码运行时会触发引用错误。但在严格模式外部,如果将一个值赋给未使用let、const、或var声明的名字,则会创建一个新全局变量。而且,无论这个赋值语句在函数或代码块中被嵌套了多少次,都会创建一个全局变量。这非常容易招致缺陷,也是推荐使用全局模式的一个最好的理由。以这种方式意外创建的全局变量类似用var声明的全局变量,都定义全局对象的属性。但与通过恰当的var声明定义的属性不同,这些属性可以通过delete操作删除。
-
解构赋值左侧变量的个数不一定与右侧数组中元素的个数相同。左侧多余的变量会被设置为undefined,而右侧多余的值会被忽略。左侧的变量列表可以包含额外的逗号,以跳过右侧的某些值:
[,x,,y]=[1,2,3,4] //x==2;y==4
第四章 表达式与操作符
4.5条件式调用
- ?.()只会检查左侧的值是不是null或undefined,不会验证该值是不是函数。左侧不是函数表达式时仍然会抛出异常。
4.6对象创建表达式
-
如果在对象创建表达式中不会给构造函数传参,则可以省略圆括号:
new Object
new Date
4.7操作符概述
- 属性访问、数组索引和调用表达式的优先级高于任何操作符。
- 实践中,如果你完全不确定自己所用操作符的优先级,最简单的办法是使用圆括号明确求值顺序。
- JavaScript新增的操作符并不总是符合优先级模式。??操作符比||和&&优先级低,而实际上它相对于这两个操作符的优先级并没有定义,ES2020要求在混用??和||或&&时使用必须使用圆括号。类似地,新的幂操作符**相对于一元负值操作符的优先级也没有明确定义,因此在同时使用求负值和求幂时也必须使用圆括号,Javascript认为这种情况下不写括号就是语法错误,形如-3 * *2的表达式属于语法错误。
- JavaScript始终严格按照从左到右的顺序对表达式求值。w=x+y+z,先求值w,再求值x,y,z。在表达式中使用圆括号可以改变乘法、加法和赋值的相对顺序,但不会改变从左到右的求值顺序。求值顺序只在一种情况下会造成差异,即被求值的表达式具有副效应,这会影响其它表达式的求值。比如,表达式x递增一个变量,而表达式z会使用这个变量,此时就需要保证x先于z求值。
4.8算数表达式
- JavaScript中,所有数值都是浮点数,因此所有除法操作得到的都是浮点数,比如5/2得到2.5而不是2。被0除得到正无穷或负无穷,而0/0求值为NaN,这两种情况都不是错误。
- 虽然取模运算通常用于整数,但也可以用于浮点数。比如,6.5 % 2.1求值为0.2。
- +操作符优先字符串拼接:只要有操作数是字符串或可以转换为字符串的对象,另一个操作数也会被转换为字符串并执行拼接操作。只有任何操作数都不是字符串或者类字符串值时才会执行加法操作。
- 一元加(+)操作符将其操作数转换为数值(或NaN)并返回转换后的值。由于BigInt值不能转换为常规数值,因此这个操作符不应该用于BigInt。
4.9关系表达式
- 虽然算数操作符不允许BigInt值与常规数值混用,但比较操作符允许数值与BigInt进行比较。
- 所有大写ASCII字母比所有小写ASCII字母都小。例如,根据<操作符,字符串"Zoo"会排在字符串"aardvark"前面。
- 注意<=和>=操作符不依赖相等或严格相等操作符确定两个值是否相等。其中,<=操作符只是简单地定义为“不大于”,>=操作符定义为“不小于”。还有一个特例,即只有一个操作数是(或可以转换为)NaN,则>,<,<=,>=这几个操作符都返回false。
- in操作符期待左侧操作数是字符串、符号或可以转换为字符串的值,期待右侧操作数是对象。如果左侧的值是右侧对象的属性名,则 in 返回true。
- instanceof 在确定对象是不是某个类的实例时会考虑“超类”。如果instanceof左侧的操作数不是对象,它会返回false。如果右侧操作数不是对象的类,它会抛出TypeError。
4.11赋值表达式
-
赋值表达式的值是右侧操作数的值。作为副效应,=操作符将右侧的值赋给左侧的变量或属性,以便将来对该变量或属性的引用可以求值为这个值。
-
多数情况下,表达式a op b(其中op是操作符)都等价于表达式a = a op b;在第一行,表达式a只被求值一次。而在第二行,它会被求值两次。这两种情况只有在a包含副效应(如函数调用或递增操作符)时才会有区别。比如,下面这两个表达式就不一样了:
data[i++] *=2; //i自增一次
data[i++] = data[i++] * 2;//i会被自增两次
4.12求值表达式
- JavaScript规范中说,如果eval被以"eval"之外的其它名字调用时,它应该把字符串当成顶级全局代码来求值。被求值的代码可能定义新全局变量或全局函数,可能修改全局变量,但它不会再使用或修改调用函数的局部变量。相对而言,使用名字"eval"来调用eval()函数就叫做“直接eval”(这样就有点保留字的感觉了)。直接调用eval()使用的是调用上下文的变量环境。其它任何调用方式,包括间接调用,都使用全局对象作为变量环境。因而不能读、写或定义局部变量或函数(无论直接调用还是间接调用都只能通过var来定义新变量。在被求值的字符串中使用let和const创建的变量和常量会被限定在求值的局部作用域内,不会修改调用环境或全局环境)。
4.13其它操作符
-
先定义操作符??求值其先定义的操作数,如果其左操作数不是null或undefined,就返回该值。否则返回右操作数的值。与||的区别在于,0,false,空字符串都是假值,但这些值在某些情况下是完全有效的。||选择第一个非假值操作数,??选择第一个非null或undefined的操作数。与&&和||操作符类似,??也是短路的。
-
??于ES2020定义,正式名字叫缺值合并操作符。??优先级并不比&&和||更高或更低。如果表达式中混用了??和它们中任意一个,必须使用圆括号说明先执行哪个操作。
-
注意,如果操作数的值是null,typeof返回"object"。如果想区分null和对象,必须显式测试这个特殊值。尽管JavaScript函数是一种对象,typeof操作符也认为函数不一样,因为它们有自己的返回值。
-
被delete操作符删除的属性或数组元素不仅会被设置为undefined值,当删除一个属性时,这个属性就不复存在了。尝试读取不存在的属性会返回undefined,但可以通过in操作符测试某个属性是否存在。用delete删除某个数组中的元素会在数组中留下一个“坑”,并不会改变数组的长度。
var a = [void 0];
a.length // =>1
0 in a //true
delete a[0]
0 in a // =>false
a.length // =>1
-
delete期待它的操作数是个左值。如果操作数不是左值,delete什么也不做,且返回true。否则,delete尝试删除指定的左值。如果删除成功返回true。但是并非所有属性都是可以删除的:不可配置属性就无法删除。在严格模式下,delete操作数如果是未限定标识符,比如变量、函数或函数参数,就会导致错误。此时,delete只能作用于属性访问表达式。严格模式也会在delete尝试删除不可配置(即不可删除)属性时报错。但在非严格模式,这两种情况都不会发生异常,delete只会简单地返回false,表示不能删除操作数。
第五章 语句
5.3条件语句
- 很多程序员都有使用花括号包装if和else语句(以及其它复合语句如while循环),即使语句体中只有一个语句。始终这么做可以使代码更清晰、易读、易理解、易调试、易维护,建议这么做。
- switch语句关键字后面跟数值或字符串字面量,这是实践中使用switch语句的常见方式,但要注意ECMAScript标准允许每个case后面跟任意表达式。
- switch语句首先对跟在switch关键字后面的表达式求值,然后再按照顺序求值case表达式,直至遇到匹配的值。这里的匹配使用===全等操作符,而不是==相等操作符,因此表达式必须在没有类型转换的情况下匹配。
5.4循环语句
-
for/of循环迭代字符串,字符串是按照Unicode码点而不是UTF-16字符迭代的。一个只含一个表情符(这个表情符号需要两个UTF-16字符表示)的字符串长度为2,但如果使用for/of来迭代这个字符串,循环体只运行一次,每次迭代一个码点。
-
执行for/in语句时,JavaScript解释器首先求值object表达式。如果它求值为null或undefined,解释器会跳过循环并转移到下一个语句。否则,解释器会对每个可枚举的对象属性执行一次循环体。在每次迭代前,解释器都会求值variable表达式,并将属性名字(字符串值)赋值给它。注意,for/in循环的variable可能是任意表达式,只要能求值为赋值表达式的左值就可以。这个表达式在每次循环时都会被求值,这意味着每次求值的结果都可能不同。比如,可以用类似下面的代码把一个对象的所有属性复制到数组中:
let o = {x: 1,y:2,z:3};
let a = [],i = 0;
for (a[i++] in o) /* 空循环体 */;
-
继承的可枚举属性也可以被for/in循环枚举。这意味着如果你使用for/in循环,并且代码中会定义被所有对象继承的属性,就可能会出现意外结果。为此,很多程序员更愿意基于Object.keys()使用for/of循环,而不是使用for/in循环。
5.5跳转语句
-
要注意continue语句在while和for循环中行为的差异:while循环直接返回到它的条件,但for循环会先求其increment表达式,然后再返回其条件。我们曾认为for循环的行为“等价于”while循环。但是continue语句在两种循环中的表现不同,所以不可能单纯使用while循环来模拟for循环。
-
正常情况下,JavaScript解释器会执行到try块末尾,然后再执行finally块,从而完成必要的清理工作。如果解释器由于return、continue或break语句而离开了try块,则解释器在跳转到新目标前会执行finally块。
-
如果finally块本身由于return、continue、break或throw语句导致跳转,或者调用的方法抛出了异常,则解释器会抛弃等待的跳转,执行新跳转。例如,如果finally子句抛出异常,该异常会代替正被抛出的其他异常。如果finally子句执行了return语句,则相应方法正常返回,即使有被抛出且尚未处理的异常。
-
我们偶尔会使用catch子句,只为了检测和停止传播异常,此时我们并不关心异常的类型或者错误信息。在ES2019及之后的版本中,类似这种情况下可以省略圆括号和其中的标识符,只使用catch关键字:
//与JSON.parse()类似,但返回undefined而不是抛出异常
function parseJSON(s) {
try {
return JSON.parse(s);
} catch {
//出错了,但我们不关心错误是什么
return undefined;
}
}
5.7声明
- 无论在作用域中的任何地方声明函数,这些函数都会被“提升”,就好像是它们在该作用域的顶部定义的一样。与函数不同,类声明不会被提升。因此在代码中,不能在还没有声明类之前使用类。
第六章 对象
6.2创建对象
- 几乎所有对象都有原型,但只有少数对象有prototype属性。正是这些有prototype属性的对象为所有其他对象定义了原型。Object.prototype是为数不多的没有原型的对象,因为它不继承任何属性。其他原型对象都是常规对象,都有自己的原型。多数内置构造函数(和多数用户自定义的构造函数)的原型都继承自Object.prototype。
- Object.create()传入null可以创建一个没有原型的新对象。不过,这样创建的新对象不会继承任何东西,连toString()这种基本方法都没有(意味着不能对该对象应用+操作符)。
6.3查询和设置属性
- 属性赋值查询原型链只为确定是否允许赋值。如果o继承了一个名为x的只读属性,则不允许赋值。不过,如果允许赋值,则会只在原始对象上创建或设置属性,而不会修改原型链中的对象。查询属性时会用到原型链,而设置属性时不影响原型链是一个重要的JavaScript特性。
- 属性赋值要么失败要么在原始对象上创建或设置属性的规则有一个例外。如果o继承了属性x,而该属性是一个通过设置方法定义的访问器属性,那么就会调用该方法而不会在o上创建新属性x。要注意,此时会在对象o上而不是在定义该属性的原型对象上调用设置方法。因此如果这个设置方法定义了别的属性,那也会在o上定义同样的属性,但仍不会修改原型链。
6.4删除属性
-
delete操作符只删除自有属性,不删除继承属性(要删除继承属性,必须从定义属性的原型对象上删除。这样做会影响继承该原型的所有对象)。
-
delete不会删除configurable特性为false的属性。与通过变量声明或函数声明创建的全局对象的属性一样,某些内置对象的属性也是不可配置的。严格模式下,尝试删除不可配置的属性会导致TypeError。
var x = 1;
delete globalThis.x // 不允许删除
globalThis.b = 2;
delete globalThis.b //可以删除
6.5属性测试
- in操作符要求左边是一个属性名,右边是一个对象。如果对象有包含相应名字的自有属性或继承属性,将返回true。对象的hasOwnProperty()方法用于测试对象是否有给定名字的属性。对于继承的属性,它返回false。propertyIsEnumerable()方法细化了hasOwnProperty()测试。如果传入的命名属性是自有属性且这个属性的enumerable特性为true,这个方法会返回true。
- 有一件事in操作符可以做,而简单的属性访问技术做不到。in可以区分不存在的属性和存在但是被设置为undefined的属性。
6.6枚举属性顺序
-
ES6正式定义了枚举对象自有属性的顺序。Object.keys()、JSON.stringfy()等相关方法都是按照下面的顺序列出属性,另外也受限于它们要列出不可枚举属性还是列出字符串属性或符号属性。
- 先列出名字为非负整数的字符串属性,按照数值顺序从小到大。这条规则意味着数组和类数组对象的属性会按照顺序被枚举。
- 在列出类数组索引的所有属性之后,再列出所有剩下的字符串名字(包括看起来像负数或浮点数的名字)的属性。这些属性按照它们添加到对象的先后顺序列出。对于在对象字面量中定义的属性,按照它们在对象字面量中出现的先后顺序列出。
- 最后,名字为符号对象的属性按照它们添加到对象的先后顺序列出。
for/in循环的枚举顺序并不像Object.keys()、JSON.stringfy()等方法那么严格,但实现通常会按照上面描述的顺序枚举自有属性,然后再沿原型链上溯,以同样的顺序枚举每个原型对象的属性。不过要注意,如果已经有同名属性被枚举过了,甚至如果有一个同名属性是不可枚举的,那这个属性就不会枚举了。
6.7扩展对象
- Object.assign()以普通的属性获取和设置方式复制属性,因此如果一个来源对象有获取方法或者目标对象有设置方法,则它们会在复制期间被调用,但这些方法本身不会被复制。
6.8序列化对象
- JSON语法是JavaScript语法的子集,不能表示所有JavsScript的值。可以序列化和恢复的值包括对象、数组、字符串、有限数值、true、false和null。NaN、Infinity和-Infinity会被序列化为null。日期对象会被序列化为ISO格式的日期字符串(参见Date.toJSON()函数),但JSON.parse()会保持其字符串格式,不会恢复成原始的日期对象。函数、RegExp和Error对象以及undefined值不能被序列化或者恢复。JSON.stringfy只序列化对象的可枚举自有属性。如果属性值无法序列化,则该属性会从输出的字符串中删除。
6.9对象方法
- Object.prototype实际上并未定义toJSON()方法,但JSON.stringfy()方法会从要序列化的对象上寻找toJSON()方法。如果要序列化的对象上存在这个方法,就会调用它,然后序列化该方法的返回值,而不是原始对象。Date类定义了自己的toJSON()方法,返回一个表示日期的序列化字符串。
6.10对象字面量扩展语法
- 需要注意,虽然扩展操作符在代码中只是三个小圆点,但它可能给JavaScript解释器带来巨大的工作量。如果对象有n个属性,把这个属性扩展到另一个对象可能是一种O(n)操作。这意味着,如果在循环或递归函数中通过...向一个大对象中不断追加属性,则很可能是在写一个低效的O(n²)算法。随着n越来越大,这个算法可能会成为性能瓶颈。
第七章 数组
- JavaScript数组是基于0且使用32位数值索引的,第一个元素为0,最大可能的索引值是4294967294(2的32次方减2),即数组最多包含4294967295个元素。
7.1创建数组
- let a = new Array(1,2,3,4,"tesing") 像这样调用的话,构造函数参数会成为新数组的元素。使用数组字面量永远比像这样使用Array()构造函数更简单。
- Array.from()也接受可选的第二个参数。如果给第二个参数传入了一个函数,那么在构建新数组时,源对象的每个元素都会传入这个函数,这个函数的返回值将替代原始值成为新数组的元素(这一点与数组的map()方法很像,但在构建数组期间执行映射的效率要高于先构建一个数组再把它映射为另一个新数组)。
7.2读写数组元素
-
数组特殊的地方在于,只要你使用小于2的三十二次方-1的非负整数作为属性名,数组就会自动维护length属性的值。数组是一种特殊的对象。用于访问数组元素的方括号与用于访问对象属性的方括号是类似的。JavaScript会将数值数组索引转换为字符串,即索引1会变成字符串“1”,然后再将这个字符串作为属性名。这个从数值到字符串的转换没有什么特别的,使用普通对象也一样:
let o = {};
o[1] = "one";
o["1"] //=>"one",数值和字符串属性名是同一个
-
明确区分数组索引和对象属性名是非常有帮助的。所有索引都是属性名,但只有介于0和2的三十二次方-2之间的整数属性名才是索引。所有数组都是对象,可以在数组上以任意名字创建属性。不过,如果这个属性是数组索引,数组会有特殊的行为,即自动按需更新其length属性。
-
注意,可以使用负数或非整数值来索引数组。此时,数值会转换为字符串,而这个字符串会作为属性名。因为这个名字不是非负整数,所以会被当成常规的对象属性,而不是数组索引。另外,如果碰巧使用了非负整数的字符串来索引数组,那这个值会成为数组索引,而不是对象属性。同样,如果使用了与整数相等的浮点值也是如此:
a[1.000] = 1 //相当于a[1] = 1
7.3稀疏数组
- 足够稀疏的数组通常是以较稠密数组慢、但内存占用少的方式实现的,查询这种数组的元素与查询常规对象属性的时间相当。
7.4数组长度
-
数组实现以维护长度不变式的第二个特殊行为,就是如果将length属性设置为一个小于当前值的非负整数n,则任何索引大于或等于n的数组元素都会从数组中被删除:
a = [1,2,3,4,5]
a.length = 3; //a变成[1,2,3]
a.length = 0; //删除所用元素。a是[]
a.length = 5; //长度是5,但没有元素,类似于new Array(5)
7.6迭代数组
-
在嵌套循环中,或其他性能攸关的场合,有时候会看到简单的数组迭代循环,但只会读取一次数组长度,而不是在每个迭代中都读取一次。下面展示的两种for循环形式都是比较推荐的:
//把数组长度保存到局部变量中
for(let i = 0, len = letters.length; i < len; i++){
//循环体不变
}
//从后向前迭代数组
for(let i = letters.length - 1; i >=0; i--){
//循环体不变
}
7.8数组方法
-
forEach()并未提供一种提前终止迭代的方式。换句话说,在这里没有与常规for循环中的braek语句对等的机制。
-
对于map()方法来说,我们传入的函数应该返回值。注意,map()返回一个新数组,并不修改调用它的数组。如果数组是稀疏的,则缺失元素不会调用我们的函数,但返回的数组也会与原始数组一样稀疏:长度相同,缺失的元素也相同。
-
filter()会跳过稀疏数组中缺失的元素,它返回的数组始终是稠密的。
-
如果在空数组上调用every()和some(),按照数学的传统,every()返回true,some()返回false。
-
如果reduce()只传一个参数,不指定初始值,reduce()会使用数组的第一个元素作为初始值。这意味着首次调用传入的归并函数会以数组的第一个和第二个元素作为其第一和第二个参数。如果不传初始值,在空数组上调用reduce()会导致TypeError。如果调用它时只有一个值,比如用只包含一个元素的数组调用并且不传初始值,或者用空数组调用但传了初始值,则reduce()会直接返回这个值,并不会调用归并函数。
-
flatMap()方法与map()类似,只不过返回的数组会被自动打平,就像传给了flat()一样。换句话说,调用a.flatMap(f)等同于(但效率远高于)a.map(f).flat()。
-
concat()不会递归打平数组的数组。concat()并不修改调用它的数组,而是创建调用它的数组的副本。很多情况下,这样做都是正确的,只不过操作代价有点大。如果你发现自己正在写类似a = a.concat(x)这样的代码,那应该考虑使用push()或splice()就地修改数组,就不要再创建新数组了。
-
在给unshift传多个参数时,这些参数会一次性插入数组。这意味着一次插入和多次插入之后的数组顺序不一样:
let a = []; //a == []
a.unshift(1) //a == [1]
a.unshift(2) //a == [2,1]
a = []; //a == []
a.unshift(1,2) // a == [1,2]
-
splice()的前两个参数指定要删除哪些元素。这两个参数后面还可以跟任意多个参数,表示要在第一个参数指定的位置插入到数组中的元素。例如:
let a = [1,2,3,4,5];
a.splice(2,0,"a","b") //=>[]; a现在是[1,2,"a","b",3,4,5]
a.splice(2,2,[1,2],3) //=>["a","b"]; a现在是[1,2,[1,2],3,3,4,5]
注意,与concat()不同,splice()插入数组本身,而不是数组的元素。
-
copyWithin()本意是作为一个高性能方法,尤其对定型数组特别有用。它模仿的是C标准库的memmove()函数。注意,即使来源与目标区域有重叠,复制也是正确的。
-
indexOf()和lastIndexOf()使用===操作符比较它们的参数与数组元素。如果数组包含对象而非原始值,这些方法检查这两个引用是否引用同一个对象。
-
includes()方法与indexOf()方法有一个重要区别。indexOf()使用与===操作符同样的算法测试相等性,而该算法将非数值的值看成与其他值都不一样,包括其自身也不一样。includes()使用稍微不同的相等测试,认为NaN与自身相等。这意味着indexOf()无法检测数组中的NaN值,但includes()可以。
7.9类数组对象
-
多数JavaScript数组方法有意地设计成了泛型方法,因此除了真正的数组,同样也可以用于类数组对象。但由于类数组对象不会继承Array.prototype,所以无法直接在它们上面调用数组方法。为此,可以使用Function.call()方法来调用:
let a = {"0":"a","1":"b","2":"c",length:3}; //类数组对象
Array.prototype.join.call(a,"+") // =>"a+b+c"
Array.prototype.map.call(a,x => x.toUpperCase()) // => ["A","B","C"]
Array.prototype.slice.call(a,0) // =>["a","b", "c"]:真正的数组副本
Array.from(a) // => ["a","b","c"]: 更容易的数组复制
7.10作为数组的字符串
- 字符串是不可修改的值,因此把它们当成数组使用时,它们是只读数组。像push()、sort()、reverse()、和splice()这些就地修改数组的数组方法,对字符串都不起作用。但尝试使用数组方法修改字符串并不会导致错误,只会静默失败。
第八章 函数
8.1函数声明
- 函数声明语句会被“提升”到包含脚本、函数或代码块的顶部,因此调用以这种方式定义的函数时,调用代码可以出现在函数定义代码之前。对此,另一种表述方式是:在一个JavaScript代码块中声明的所有函数在该块的任何地方都有定义,而且它们会在JavaScript解释器开始执行该块中的任何代码之前被定义。
- 如果函数表达式包含名字,则该函数的局部作用域中也会包含一个该名字与函数对象的绑定。实际上,函数名就变成了函数体内的一个局部变量。多数定义为表达式的函数都不需要名字,这让定义更简洁。
- 使用函数声明定义函数f()与创建一个函数表达式再将其赋值给变量f有一个重要的区别。在使用声明定义时,先创建好函数对象,然后再运行包含它们的代码,而且函数的定义会被提升到顶部,因此在定义函数的语句之前就可以调用它们。但对于定义为表达式的函数就不一样了,这些函数在定义它们的表达式被求值之前是不存在的。不仅如此,要调用函数要求必须可以引用函数,在把函数表达式赋值给变量之前是无法引用函数的,因此定义为表达式的函数不能在它们的定义之前调用。
- 箭头函数没有prototype属性,这意味着箭头函数不能作为新类的构造函数。
8.2调用函数
- 对于严格模式下的函数调用,调用上下文(this值)是全局对象。但在严格模式下,调用上下文是undefined。要注意的是,使用箭头语法定义的函数又有不同:它们总是继承自身定义所在环境的this值。
- 如果你写的方法没有返回值,可以考虑让它返回this。如果能在自己的API中统一这么做,那就可以支持一种被称为方法调用链的编程风格。这样只要给对象命名,之后就可以连续调用这个对象的方法。
- this关键字不具有变量那样的作用域机制,除了箭头函数,嵌套函数不会继承包含函数的this值。如果嵌套函数被当成方法调用,那它的this值就是调用它的对象。如果嵌套函数(不是箭头函数)被当作函数来调用,则它的this值要么是全局对象(非严格模式),要么是undefined(严格模式)。有一个常见错误,对于定义在方法中的嵌套函数,如果将其当作函数来调用,以为可以使用this获得这个方法的调用上下文,这是错误的。
- 构造函数调用会创建一个新的空对象,这个对象继承构造函数的prototype属性指定的对象。构造函数就是为初始化对象而设计的,这个新创建的对象会被用作函数的调用上下文,因此在构造函数中可以通过this关键字引用这个新对象。注意,即使构造函数调用看起来像方法调用,这个新对象也仍然会被用作调用上下文。换句话说,在表达式new o.m()中,o不会用作调用上下文。
- 构造函数正常情况下不使用return关键字,而是初始化新对象并在到达函数体末尾时隐式返回这个对象。此时,这个新对象就是构造函数调用表达式的值。但是,如果构造函数显式使用了return语句返回某个对象,那该对象就会变成调用表达式的值。如果构造函数使用return但没有返回值,或者返回的是一个原始值,则这个返回值会被忽略,仍然以新创建的对象作为调用表达式的值。
8.3函数实参与形参
- 函数的形参默认值表达式会在函数调用时求值,不会在定义时求值。因此每次调用会创建一个新的形参默认值。
- 剩余形参前面有三个点,而且必须是函数声明中最后一个参数。在调用有剩余形参的函数时,传入的实参会首先赋值到非剩余形参,然后所有剩余的实参会保存在一个数组中赋值给剩余形参。最后一点很重要:在函数体内,剩余形参的值始终是数组。数组有可能为空,但剩余形参永远不可能为undefined(永远不要给剩余形参定义默认值,这样既没有用,也不合法)。
- arguments对象可以追溯到JavaScript诞生之初,有一些奇怪的历史包袱,导致它效率低且难以优化,特别是在非严格模式下,在新写的代码中应该避免使用它。在严格模式下,arguments会被当成保留字。因此不能用这个名字来声明函数形参或局部变量。
- 在ES2018中,解构对象时也可以使用剩余形参。此时剩余形参的值是一个对象,包含所有未被解构的属性。
8.5函数作为命名空间
-
(function() { //将chunkNamespace()函数重写为一个无名表达式
//要复用的代码放在这里
}()); //函数定义结束后立即调用它
在一个表达式中定义并调用匿名函数被称为“立即调用函数表达式”。function关键字前面的左开括号是必须的,如果没有它,JavaScript解释器会把function关键字作为函数声明语句来解析。有了这对括号,解释器会把它正确地识别为函数定义表达式。开头的括号也便于程序员知道这是个定义后立即调用的函数,而不是为后面使用而定义的函数。
-
函数作为命名空间的真正用武之地,还是在命名空间中定义一个或多个函数,而这些函数又使用该命名空间中的变量,最后这些函数又作为命名空间函数的返回值从内部传递出来。类似这样的函数被称为闭包。
8.6闭包
- 与多数现代编程语言一样,JavaScript使用词法作用域。这意味着函数执行时使用的是定义函数时生效的变量作用域,而不是调用函数时生效的变量作用域。为了实现词法作用域,JavaScript函数对象的内部状态不仅要包括函数代码,还要包括对函数定义所在作用域的引用。这种函数对象与作用域(即一组变量绑定)组合起来解析函数变量的机制,在计算机科学文献中被称为闭包。
- 在写闭包的时候,要注意:this是JavaScript关键字,不是变量。箭头函数继承包含它们的函数中的this值,但使用function定义的函数并非如此。因此如果要写的闭包需要使用其包含函数的this值,那应该在返回闭包之前使用箭头函数或调用bind(),也可以把外部的this值赋给闭包将继承的变量。
8.7函数属性、方法与构造函数
-
箭头函数从定义它的上下文中继承this值。这个this值不能通过call()或apply()方法重写。如果对箭头函数调用这两个方法,那第一个参数实际上会被忽略。除了作为调用上下文传给call()的第一参数,后续的所有参数都会传给被调用的函数(调用箭头函数时不会忽略这些参数)。
-
箭头函数从定义它们的环境中继承this值,且这个值不能被bind()覆盖,所以给箭头函数绑定this不会起作用。不过,由于调用bind()最常见的目的是让非箭头函数变得像箭头函数,因此这个关于绑定箭头函数的限制在实践中通常不是问题。
-
事实上,除了把函数绑定到部分对象,bind()方法还会做其他事。比如,bind()也可以执行”部分应用“,即在第一个参数之后传给bind()的参数也会随着this值一起被绑定。部分应用是函数式编程中的一个常用技术,也被称为柯里化。下面是几个使用bind()方法实现部分应用的例子:
let sum = (x,y)=>x+y; //返回2个参数之和
let succ = sum.bind(null,1); //把第一个参数绑定为1
succ(2) //=>3: x绑定到1,2会传给参数y
function f(y,z) {return this.x + y + z;}
let g = f.bind({x:1},2); //绑定this和y
g(3) //=>6: this.x绑定到1,y绑定到2,z是3
g.name //=>"bound f"
bind()返回函数的name属性由单词”bound“和调用bind()的函数的name属性构成。
-
Function()构造函数每次被调用时都会解析函数体并创建一个新函数对象。如果在循环中或者被频繁调用的函数中出现了对它的调用,可能会影响程序性能。相对而言,出现在循环中的嵌套函数和函数表达式不会每次都被重新编译。
-
Function()构造函数创建的函数不使用词法作用域,而是始终编译为如同顶级函数一样。
let scope = "global";
function constructFunction() {
let scope = "local";
return new Function("return scope");
}
constructFunction()() //=>"global"
第九章 类
9.2类和构造函数
-
只有函数对象才有prototype属性。
-
构造函数在某种意义上是定义类的,而类名按照惯例应以大写字母开头。普通函数和方法的名字则以小写字母开头。
-
构造函数不需要返回新创建的对象,它调用会自动创建新对象,并将构造函数作为该对象的方法来调用,然后返回新对象。构造函数调用与普通函数调用的这个重要区别也是我们用首字母大写的名字命名构造函数的一个原因。构造函数在编写时就会考虑它作为构造函数以new关键字来调用,因此把它们当成普通函数调用通常会有问题。这个命名约定让构造函数有别于普通函数,方便程序员知道什么时候使用new。
-
JavaScript的各种错误构造函数可以不使用new调用,如果想在自己的构造函数中模拟这个特性,可以像下面这样编码:
function C() {
if(!new.target) return new C();
// 这里是初始化代码
}
这个技术只适用于以这种老方式定义的构造函数。使用class关键字创建的类不允许不使用new调用它们的构造函数。
-
原型对象是类标识的基本:当且仅当两个对象继承同一个原型对象时,它们才是同一个类的实例。初始化新对象状态的的构造函数不是基本标识,因为两个构造函数的prototype属性可能指向同一个原型对象,此时两个构造函数都可以用于创建同一个类的实例。
-
如果不想以构造函数作为媒介,直接测试某个对象原型链中是否包含指定原型,可以使用isPrototypeOf()方法。
-
任何普通JavaScript函数(不包括箭头函数、生成器函数和异步函数)都可以用作构造函数,而构造函数调用需要一个prototype属性。为此,每个普通JavaScript函数自动拥有一个prototype属性。这个属性的值是一个对象,有一个不可枚举的constructor属性。而这个constructor属性的值就是该函数对象:
let F = function() {};
let p = F.prototype;
let c = p.constructor;
c === F //=>true: 对任何F,F.prototype.constructor === F
9.3使用class关键字的类
- 使用class关键字定义的类类体中包含了使用对象字面量方法简写形式定义的方法,因此省略了function关键字。但与对象字面量不同的是方法之间没有逗号(尽管类体与对象字面量表面上相似,但它们不是一回事。特别地,类体中不支持名/值对形式的属性定义)。
- 与函数定义表达式一样,类定义表达式也可以包含一个可选的类名。如果提供了名字,则该名字只能在类体内部访问到。
- 与函数声明不同,类声明不会被提升。尽管类声明与函数声明十分类似,但类声明不会被提升。换句话说,不能在声明类之前初始化它。
- 由于静态方法是在构造函数而非实例上调用的,所以在静态方法中使用this关键字没有什么意义。
- 私有字段必须先使用#语法声明才能使用。换句话说,如果没有直接在类体中声明#size字段,就不能在类的构造函数中写this.#size = 0;
9.4为已有类添加方法
- JavaScript基于原型的继承机制是动态的。换句话说,对象从它的原型继承属性,如果在创建对象之后修改了原型的属性,则对象继承修改后的属性。这意味着只要给原型对象添加方法,就可以增强JavaScript类。
- 给内置类型的原型添加方法通常被认为是不好的做法。因为如果未来JavaScript也定义了同名方法会导致兼容性问题。给Object.prototype添加方法也是可以的,但最好不要这样做。因为Object.prototype上的属性在for/in循环中是可见的。
9.5子类
- 如果使用extends关键字定义了一个类,那么这个类的构造函数必须使用super()调用父类构造函数。如果没有在子类中定义构造函数,解释器会自动创建一个。这个隐式定义的构造函数会获得传递给它的值,然后把这些值再传给super()。在通过super()调用父类构造函数之前,不能在构造函数中使用this关键字。这条强制规则是为了确保父类先于子类得到初始化。
第十章 模块
- 实践中,模块化的主要作用体现在封装和隐藏私有实现细节,以及保证全局命名空间清洁上,因而模块之间不会意外修改各自定义的变量、函数和类。
10.2Node中的模块
- 如果想导入Node内置的系统模块或通过包管理器安装在系统上的模块,可以使用模块的非限定名,即不带会被解析为文本系统路径的“/”字符的模块名。
10.3ES6中的模块
-
ES6模块甚至比严格模式还要更严格:在严格模式下,在作为函数调用的函数中this是undefined。而在模块中,即便在顶级代码中this也是undefined(相对而言,浏览器和Node中的脚本都将this设置为全局对象)。
-
import语句还有另外一种形式,用于导入没有任何导出的模块。要在程序中包含没有导出的模块,只要在import关键字后面直接写出模块标识符即可:
import "./analytics.js";
这样的模块会在被首次导入时运行一次(之后再导入时则什么也不做)。
-
export关键字需要as前面是一个标识符,而非表达式。这意味着不能像下面这样在导出时重命名:
export {Math.sin as sin,Math.cos as cos}; //SyntaxError
-
ES6模块有一个非常棒的特性是每个模块的导入都是静态的。因此只要有一个起始模块,浏览器就可以加载它导入的所有模块,然后加载第一批模块导入的所有模块,以此类推,直到加载完所有程序代码。
-
常规脚本与跨源脚本的另一个重要区别涉及跨源加载。常规
<script>标签可以从互联网上的任何服务器加载JavaScript代码文件,而互联网广告、分析和追踪代码都依赖这个事实。但<script type="module">增加了跨源加载的限制,即只能从包含模块的HTML文档所在的域加载模块,除非服务器添加了适当的CORS头允许跨源加载。这个新的安全机制带来了一个副作用,就是不能在开发模式下使用file: URL来测试ES6模块。为此在使用ES6模块时需要启动一个静态Web服务器来测试。 -
传给import()的参数应该是一个模块标识符,与使用静态import指令时完全一样。但对于import(),则没有使用常量字符串字面量的限制。换句话说,任何表达式只要可以求值为一个字符串且格式正确,就没问题。
第十一章 JavaScript标准库
11.1集合与映射
- Set的add()方法接收一个参数,如果传入一个数组,它会把数组而不是数组的元素添加到集合中。add()始终返回调用它的集合,因此如果想给集合添加多个值,可以连缀调用add(),如s.add('a').add('b').add('c')。
- 集合成员是根据严格相等来判断是否重复的,类似于使用===操作符。集合可以既包含数值1,也包含字符串"1",因为它认为这两个值不同。如果值是对象(或数组、函数),那么也会像使用===一样进行比较。所以不能通过字面量删除数组元素(即使两个数组包含相同的元素)。如果真想删除一个数组,必须传入该数组的引用。
- 关于集合,最重要的是要知道它专门为成员测试做了优化,无论集合有多少成员,has()方法都非常快。数组的includes()方法也执行成员测试,但其执行速度与数组大小成反比。因此,使用数组作为集合比使用真正的Set对象要慢得多。
- 数组的forEach()方法把数组索引作为回调函数的第二个参数。但集合没有索引,所以这个方法的Set类版本传给回调的第一个和第二个参数都是元素的值。
- 同样与数组类似,映射速度也很快:无论映射有多大,查询与某个键关联的值都很快(虽然没有通过索引访问数组那么快)。
- 与集合一样,任何JavaScript值都可以在映射中作为键或值。这包括null、undefined和NaN,以及对象和数组等引用类型。同样与集合一样,映射按照全等性而非相等性比较键。
- 映射的forEach()方法传给回调的参数在前,键在后,而for/of循环则是键在前,值在后。
11.3正则表达式与模式匹配
- 如果记不住哪些标点符号需要使用反斜杠转义,可以给所有标点符号字符前面都加上反斜杠。与此同时,也要知道很多字母和数字前面如果加了反斜杠也会有特殊含义,因此任何想匹配字面值的字母和数字都不应该加反斜杠。如果使用RegExp()构造函数,则要记住,正则表达式中的任何反斜杠都要写两次,因为字符串也使用反斜杠作为转义字符。
- 如果search()方法的参数不是正则表达式,它会先把这个参数传给RegExp()构造函数,把它转换为正则表达式。search()方法不支持全局搜索,因此其正则表达式参数中包含的g标志会被忽略。
- 如果正则表达式没有g标志,match()不会执行全局搜索,只会查找第一个匹配项。在非全局搜索时,match()仍然返回数组,但数组元素完全不同。在没有g标志的情况下,返回数组的第一个元素是匹配的字符串,剩下的所有元素是正则表达式中括号分组的捕获组匹配的子字符串。因此,如果match()返回一个数组a,则a[0]包含与整个正则表达式匹配的字符串,a[1]包含与第一个捕获组匹配的子字符串,以此类推。如果与replace()方法做个比较,则a[1]相当于2,以此类推。
- 可以设置RegExp对象的lastIndex属性,告诉matchAll()从字符串中的哪个索引开始匹配。但是,与其它模式匹配方法不同的是,matchAll()不会修改传入RegExp的lastIndex属性,这也使得它不太可能在代码中导致bug。
- 除了给RegExp()的第一个参数传字符串,也可以传一个RegExp对象。这样可以复制已有的正则表达式,并且修改它的标志。
- lastIndex让RegExp API很容易出错。因此在使用g或者y标志和循环时要格外注意。在ES2020及之后的版本中,应该使用String的matchAll()方法而不是exec()来避开这个问题,因为matchAll()不会修改lastIndex。
11.4日期与时间
- 如果要打印日期,默认会以本地时区打印。如果想以UTC显示日期,应该先使用toUTCString()或toISOString()转换它。
- 注意,Date类查询日的方法是getDate()和getUTCDate()。而名字听起来更自然的函数getDay()和getUTCDay()返回的是代表周几的数值(0代表周日,6表示周六)。周几字段是只读的,因此没有对应的setDay()方法。
11.6JSON序列化与解析
- 除了使用toJSON(),JSON.stringify()也支持给它传入一个数组或函数作为第二个参数来自定义其输出字符串。如果第二个参数传入的是一个字符串数组(或者数值数组,其中的数值会转换为字符串),那么这些字符串会被当作对象属性(或数组元素)的名字。任何名字不在这个数组之列的属性会被字符串化过程忽略。而且,返回字符串中包含属性的顺序也会和它们在这个数组中的顺序相同。
11.10计时器
- setTimeout()和setInterval()都返回一个值。如果把这个值保存在变量中,之后可以把它传给clearTimeout()或clearInterval()以取消对函数的调用。在浏览器中,这个返回值通常是一个数值,而在node中则是一个对象。具体什么类型其实不重要。只要把它当成一个不透明的值就可以了。这个值的唯一作用就是可以把它传给clearTimeout()以取消setTimeout()注册的函数调用(假设函数尚未被调用),或者传给clearInterval()以取消对通过setInterval()注册的函数的重复调用。
第十二章 迭代器与生成器
-
有些会接收Array对象的内置函数和构造函数(在ES6及之后的版本中)可以接收任意迭代器。例如,Set()构造函数就是这样一个API:
//字符串是可迭代的,因此两个集合相同:
new Set("abc") //=> new Set(["a","b","c"])
12.2实现可迭代对象
- 我们假想的文件内单词迭代器永远跑不到终点,也需要关闭它打开的文件。为此,除了next()方法,迭代器对象还可以实现return()方法。如果迭代在next()返回done属性为true的迭代结果之前停止(最常见的原因是通过break语句提前退出for/of循环),那么解释器就会检查迭代器对象是否有return()方法。如果有,解释器就会调用它(不传参数),让迭代器有机会关闭文件、释放内存,或者做一些其它清理工作。这个return()方法必须返回一个迭代器结果对象。这个对象的属性会被忽略,但返回非对象值会导致报错。
12.3生成器
- 不能使用箭头函数语法定义生成器函数。
12.4高级生成器特性
- 调用生成器的next()方法时,生成器函数会一直运行到到达第一个yield表达式。yield关键字后面的表达式会被求值,该值成为next()调用的返回值。此时,生成器函数就在求值yield表达式的中途停了下来。下一次调用生成器的next()方法时,传给next()的参数会变成暂停的yield表达式的值。换句话说,生成器通过yield向调用者返回值,而调用者通过next()给生成器传值。生成器和调用者是两个独立的执行流,它们交替传值(和控制权)。
第十三章 异步JavaScript
13.2 Promise
- Promise表示的是一次异步计算的未来结果。不过,不能使用它们表示重复的异步计算。可以用Promise替代XMLHttpRequest对象的“加载”(load)事件处理程序,因为回调只会被调用一次。但不能用Promise替代HTML按钮对象的“单击”(click)事件处理程序,因为我们通常允许用户多次点击按钮。
- 有时候,当API被设计为使用方法链时只会有一个对象,它的每个方法都返回对象本身,以便后续调用。然而这并不是Promise的工作方式。我们在写.then()调用链时,并不会在一个Promise上注册多个回调。相反,每个then()方法都用都返回一个新的Promise对象。这个新Promise对象在传给then()的函数执行结束才会兑现。
- .catch()回调不仅可以用于报告错误,还可以处理错误并从错误中恢复。一个错误只要传给了.catch回调,就会停止在Promise链中向下传播。.catch()回调可以抛出新错误,但如果正常返回,那这个返回值就用于解决或兑现与之关联的Promise,从而停止错误传播。
- Promise.all()的输入数组可以包含Promise对象和非Promise值。如果这个数组的某个元素不是Promise,那么它就会被当成一个已兑现Promise的值,被原封不动地复制到输出数组中。
- Promise.resolve()接收一个值作为参数,并返回一个会立即(但异步)以该值兑现的Promise。类似地,Promise.reject()也接收一个参数,并返回一个以该参数作为理由拒绝的Promise。这两个静态方法返回的Promise在被返回时并未兑现或拒绝,但它们会在当前同步代码块运行结束后立即兑现或拒绝。通常,这会在几毫秒之后发生,除非有很多待定的异步任务等待运行。
13.3 async和await
- 把函数声明为async意味着该函数会返回一个Promise,即便函数体中不出现Promise相关的代码。如果async函数正常返回,那么作为该函数真正返回值的Promise对象将解决为这个明显的返回值。如果async函数抛出异常,那么它返回的Promise对象将以该异常被拒绝。
- 可以对任何函数使用async关键字。例如,可以在function关键字作为语句和表达式时使用,也可以对箭头函数和类及对象字面量中的简写方法使用。
- 可以把await关键字想象成分隔代码体的记号,它们把函数体分隔成相对独立的同步代码块。ES2017解释器可以把函数分割成一系列独立的子函数,每个子函数都将被传给位于它前面的以await标记的那个Promise的then()方法。
第十四章 元编程
14.1 属性的特征
- 用于查询和设置属性特性的JavaScript方法使用一个被称为属性描述符的对象,这个对象用于描述属性的4个特性。属性描述符对象拥有与它所描述的属性的特性相同的属性名。因此,数据属性的属性描述符有如下属性:value、writable、enumerable和configurable。而访问器属性的属性描述符没有value和writable属性,只有get和set属性。其中,writable、enumerable和configurable属性是布尔值,而get和set属性是函数值。
14.2 对象的可扩展能力
- 要确定一个对象是否可扩展,把它传给Object.isExtensible()即可。要让一个对象不可扩展,把它传给Object.preventExtensions()即可。如此,如果再给该对象添加新属性,那么在严格模式下就会抛出TypeError,而在非严格模式下则会静默失败。此外,修改不可扩展对象的原始类型始终都会抛出TypeError。
- 把对象修改为不可扩展是不可逆的(即无法再将其改回可扩展)。也要注意,调用Object.preventExtensions()只会影响对象本身的可扩展能力。如果给一个不可扩展对象的原型添加了新属性,则这个不可扩展对象仍然会继承这些新属性。
第十五章 浏览器中的JavaScript
15.1 Web编程基础
- 包含src属性的
<script>标签就如同指定javaScript文件的内容直接出现在<script>和</script>标签之间一样。注意,即便指定了src属性,后面的</script>标签也是html文件必需的,html不支持<script/>标签。 - 如果不使用async和defer属性(特别是对那些直接包含在html中的代码),也可以选择把
<script>标签放在html文件的末尾。这样脚本在运行的时候就知道自己前面的文档内容已经解析,可以操作了。 - 在模块中,顶级声明被限制在模块内部,可以明确导出。而在非模块脚本中,顶级声明被限制在包含文档内部,顶级声明由文档中所有的脚本共享。以前的var和function声明是通过全局对象的属性共享的,而现在的const、let和class声明也会被共享且拥有相同的文档作用域,但它们不作为JavaScript可以访问到的任何对象的属性存在。
15.2 事件
- 可以多次调用addEventListener()在同一个对象上为同一事件类型注册多个处理程序。当对象上发生该事件时,所有为这个事件而注册的处理程序会按注册的顺序被调用。在同一个对象上以相同的参数多次调用addEvevtListener()没有作用,同一个处理程序只能注册一次,重复调用不会改变处理程序被调用的顺序。
15.3 操作DOM
- 有些HTML属性名是JavaScript中的保留字。对于这些属性,通用规则是对应的JavaScript属性包含前缀"html"。比如,
<label>元素在HTML中的for属性,变成了JavaScript的htmlFor属性。“class”也是JavaScript的保留字,但这个非常重要的HTML class属性是个例外,它在JavaScript代码中会变成className。 - Element的outerHTML属性与innerHTML属性类似,只是返回的值包含元素本身。在读取outerHTML时,该值包含元素的开始和结束标签。而在设置元素的outerHTML时,新内容会取代元素自身。
- Element类定义了一个innerText属性,与textContent类似。但innerText有一些少见和复杂的行为,如试图阻止表格格式化。这个属性的定义不严谨,浏览器间的实现也存在兼容性问题,因此不应该再使用了。
- 块级元素(如段落和
<div>元素)在浏览器的布局中始终是矩形。行内元素(如<span>、<code>和<b>元素)则可能跨行,因而包含多个矩形。比如,<em>和</em>标签间的文本显示在了两行上,则它的矩形会包含第一行末尾和第二行开头。如果在这个元素上调用getBoundingClientRect(),则边界矩形将包含两行的整个宽度。如果想查询行内元素中的个别矩形,可以调用getClientRects()方法,得到一个只读的类数组对象,其元素为类似getBoundingClientRect()返回的矩形对象。
15.10 位置、导航与历史
- 除了可以使用window.location和document.location引用的Location对象,以及URL()构造函数,浏览器也定义了document.URL属性。奇怪的是,这个属性的值并非URL对象,而是一个字符串,也就是当前文档的URL。
- 简单的片段标识符也是一种特殊的URL,但它不会导致浏览器加载新文档,只会把文档中id或name匹配该片段的元素滚动到浏览器窗口顶部,令其可见。作为一个特例,片段标识符#top会让浏览器跳到文档顶部(假设没有元素有id="top"属性)。
- 如果窗口包含子窗口(如
<iframe>元素),子窗口的浏览历史会按时间顺序与主窗口历史交替,这意味着在主窗口中调用history.back(),可能导致某个子窗口后退到前一个显示的文档,而主窗口则维持当前状态不变。
15.11 网络
- fetch()返回的Promise解决为一个Response对象。这个对象的status属性是HTTP状态码,如表示成功的200或表示”Not Found“的404(statusText中则是与数值状态码对应的标准英文描述)。更方便的是Response对象的ok属性,它在status为200或在200和299之间时是true,在其他情况下是false。
- 如果浏览器响应了fetch()请求,那么返回的Promise就会以一个Response对象兑现,包括响应404 Not Found和500 Internal Server Error。fetch()只在自己根本联系不到服务器时才会拒绝自己返回的Promise。如果用户的计算机断网了、服务器不响应了,或者URL指定的主机不存在,才会发生这种情况。因为这些情况对任何网络请求都可能发生,所以最好在任何fetch()调用后面都包含一个.catch()子句。
15.12 localStorage和sessionStorage
- 可以使用delete操作符删除localStorage和sessionStorage的属性,可以使用for/in循环或Object.keys()枚举Storage对象的属性。如果想删除Storage对象的所有属性,可以调用clear方法。
- Storage对象也定义了getItem()、setItem()和removeItem()方法,可以用来代替直接读写属性和delete操作符。
- Storage对象的属性只能存储字符串。如果想存取其它类型的数据,必须自己编码和解码。