本笔记只包含该书精华部分,基础内容大多略过,也有一些自己的想法
1 块级作用域绑定
var声明及变量提升(Hoisting)机制
块级声明
块级作用域(词法作用域)存在于:
- 函数内部
- 块中(字符
{
和}
之间的区域)
let声明
禁止重声明
同一作用域中不能用let
重复定义已经存在的标识符,否则会抛出错误
const声明
临时死区(Temporal Dead Zone)
在声明前访问let/const
声明的变量即使是typeof
也会报错。
JavaScript引擎在扫描代码发现变量声明时,要么将它们提升至作用域顶部(遇到var
声明),要么将声明放到TDZ(遇到let
和const
声明)。访问TDZ中的变量会触发运行时错误。只有执行过变量声明语句后,变量才会从TDZ中移出,然后方可正常访问。
循环中的块级作用域绑定
循环中的函数
循环中的let声明
每次循环都会创建一个新变量,并以之前迭代中同名变量的值将其初始化
NOTE: let
声明在循环内部的行为是标准中专门定义的,它不一定与let
的不提升特性相关,理解这一点至关重要。事实上,早期let
实现不包括这一行为,它是后来加入的。
循环中的const声明
对于普通的for循环可以在初始化变量时使用const
,但是更改这个变量的值就会抛出错误
在for-in
或for-of
循环中使用const
时的行为与使用let
一致。之所以可以,是因为每次迭代不会修改已有绑定,而是会创建一个新绑定。
我的原理猜想
var
声明:
var funcs = [];
for (var i = 0; i < 10; i++) {
funcs.push(function(value) {
console.log(i);
});
}
funcs.forEach(function(func) {
func(); // 输出10次数字10
});
由于var
提升实际是这样:
var funcs = [];
var i;
for (i = 0; i < 10; i++) {
funcs.push(function(value) {
console.log(i);
});
}
funcs.forEach(function(func) {
func(); // 输出10次数字10
});
最终所有访问的都是同一个i
为10.
而let
是这样:
var funcs = [];
{
let prev = 0;
for (;;) {
let i = prev; // 每次循环都会创建一个新变量,并以之前迭代中同名变量的值将其初始化
if (i < 10) {
funcs.push(function(value) {
console.log(i);
});
} else {
break;
}
prev++;
}
}
funcs.forEach(function(func) {
func(); // 输出10次数字10
});
全局作用域绑定
当var
被用于全局作用域时,它会创建一个新的全局变量作为全局对象的属性。
用let/const
会在全局作用域下创建一个新的绑定,但该绑定不会添加为全局对象的属性。换句话说,用let/const
不能覆盖全局变量,而只能遮蔽它。
块级绑定最佳实践的进化
默认使用const
2 字符串和正则表达式
更好的Unicode支持
UTF-16码位
BMP(Basic Multilingual-Plane): 基本多文种平面
超出2^16
的是辅助平面
es5中,所有字符串的操作都基于16位编码单元。
codePointAt()方法
String.fromCodePoint()方法
normalize()方法
正则表达式u修饰符
正则表达式默认将字符串中的每一个字符按照16位编码单元处理。
u
修饰符将正则表达式从编码单元操作模式切换为字符模式
其他字符串变更
字符串中的子串识别
includes
startsWith
endsWith
这三个方法传入正则表达式会报错,而indexOf
和lastIndexOf
会把传入的正则表达式转化为一个字符串并搜索它
repeat()方法
其他正则表达式语法变更
正则表达式y修饰符
正则表达式的复制
es5中通过new RegExp(reg)
复制正则表达式,但不能传递第二个参数,提供修饰符,否则报错。在es6中可以。
flags属性
返回修饰符字符串
模板字面量
基本语法
多行字符串
JavaScript长期以来一直存在一个语法bug,在一个新行最前方添加\
可以承接上一行的代码,因此可以利用这个bug创造多行字符串:
var message = "Multiline \
string";
console.log(message); // "Multiline string"
应该避免使用这种方法。
字符串占位符
标签模板
3 函数
函数形参的默认值
在ECMAScript 5中模拟默认参数
ECMAScript 6中的默认参数
声明函数时,可以为任意参数指定默认值,在已指定默认参数值的参数后可以继续声明无默认值参数。
默认参数值对arguments对象的影响
在ECMAScript 5非严格模式下,函数命名参数的变化同步更新到arguments对象中
在ECMAScript 5非严格模式下,取消了arguments对象的这个令人感到困惑的行为
在ECMAScript 6中,如果一个函数使用了默认参数值,则无论是否显式定义了严格模式,arguments
对象的行为都将与ECMAScript 5严格模式下保持一致
我的原理猜想
ECMAScript 5非严格模式下arguments
机制:
// 函数foo有命名参数:first
function foo(first) {
// 内部arguments机制
// 应该是使用 Proxy,可以监听任意key,这里使用getter/setter简化说明
const arguments = {
get 0() {
return first;
},
set 0(value) {
first = value;
},
};
// 函数体
console.log(arguments[0]);
first = 2;
console.log(arguments[0]);
arguments[0] = 3;
console.log(first);
}
foo(1); // 输出 1 2 3
默认参数表达式
默认参数的临时死区
NOTE:函数参数有自己的作用域和临时死区,其与函数体的作用域是各自独立的,也就是说函数参数的默认值不可访问函数体内声明的变量
处理无命名参数
ECMAScript 5中的无命名参数
通过arguments
访问
不定参数
NOTE:函数的length
属性统计的是函数命名参数的梳理,不定参数的加入不会影响length
属性的值
不定参数的使用限制:
- 每个函数最多只能声明一个不定参数,而且一定要放在所有参数的末尾
- 不定参数不能用于对象字面量
setter
之中
增强的Function构造函数
ECMAScript 6支持在创建函数时定义默认参数和不定参数
展开运算符
name属性
如何选择合适的名称
匿名函数表达式的那么属性值对应着被赋值的变量名
name属性的特殊情况
getter/setter
会有get/set
前缀- 通过
bind
常见的会有bound
前缀 - 通过
Function
构造的是anonymous
明确函数的多重用途
JavaScript函数有两个不同的内部方法:[[Call]]
和[[Construct]]
,当通过new
关键字调用函数时,执行的是[[Construct]]
函数,它负责创建一个通常被称作实例的新对象,然后再执行函数体,将this
绑定到实例上;如果不通过new
关键字调用函数,则执行[[Call]]
函数,从而直接执行代码中的函数体。具有[[Construct]]
方法的函数被统称为构造函数。
在ECMAScript 5中判断函数被调用的方法
instanceof
缺点:不可靠,无法区分call/apply
和new
关键字调用
元属性(Metaproperty):new.target
当调用[[Contructor]]
时,new.target
指向实例;当调用[[Call]]
时,new.target
为undefined
⚠️ 在函数外使用new.target
是一个语法错误
块级函数
在ECMAScript 3和早期版本中,在代码块中声明一个块级函数严格来说是一个语法错误,但是所有浏览器仍然支持这个特性
ECMAScript 5的严格模式中引入了一个错误提示;在ECMAScript 6严格模式中,视作一个块级声明,函数可以提升到代码块顶部,非严格模式下提升至外围函数或全局作用域顶部
箭头函数
与传统函数的不同
- 没有
this
、super
、arguments
、和new.target
绑定 - 不能通过
new
关键字调用。箭头函数没有[[Construct]]
方法 - 没有原型
property
- 不可以改变
this
绑定 - 不支持
arguments
对象 - 不支持重复的命名参数。传统函数中,只有严格模式下才不能有重复的命名参数
我的原因猜想
箭头函数为什么不支持arguments
对象,不支持重复的命名参数呢?
新的标准肯定是要摒弃以前错误/不好的设计的。es6不定参数可以取代arguments
, 重复命名参数是个错误,所以既然创造的是一个新语法没有历史遗留问题,那么就可以直接摒弃,而原来的还支持是为了向前兼容,不得已而为之
箭头函数语法
创建立即执行函数表达式
箭头函数没有this绑定
箭头函数和数组
箭头函数没有arguments绑定
箭头函数的辨识方法
尾调用优化
尾调用指的是函数作为另一个函数的最后一条语句被调用
ECMAScript 6中的尾调用优化
尾调用优化需要满足的条件:
- 尾调用不访问当前栈帧的变量(也就是说函数不是一个闭包)
- 在函数内部,尾调用是最后一条语句
- 尾调用的结果作为函数返回值返回
4 扩展对象的功能性
对象类别
- 普通对象(Ordinary)对象 具有JavaScript对象所有的默认内部行为
- 特异对象(Exotic)对象 具有某些与默认行为不符的内部行为
- 标准(Standard)对象 ECMAScript 6规范中定义的对象,例如,
Array
、Date
等。标准对象既可以是普通对象,也可以是特异对象 - 内建对象 脚本开始执行时存在于JavaScript执行环境中的对象,所有标准对象都是内建对象
对象字面量语法扩展
属性初始值的简写
对象方法的简写方法
可计算属性名(Computed Property Name)
新增方法
ECMAScript其中一个设计目标是:不再创建新的全局函数,也不在Object.property
上创建新的方法。
Object.is()方法
Object.is()
= ===
+ 区分+0/-0
+ NaN
等于NaN
Object.assign()方法
Mixin
模式一般实现是使用赋值操作符来复制属性,不能复制访问器属性到接收对象中。
⚠️ Object.assign()
也是赋值操作,所以不能复制访问器属性
重复的对象字面量属性
ECMAScript 5严格模式中加入了对象字面量重复属性的校验,同时存在多个同名属性时会抛出错误
ECMAScript 6移除了这个特性
自由属性枚举顺序
影响到的方法有Object.getOwnPropertyNames()
、Reflect.ownKeys()
以及Object.assign()
方法处理属性的顺序
- 所有数字键按升序排序
- 所有字符串键按照他们被加入对象的顺序排序
- 所有
symbol
键按照它们被加入对象的顺序排序
NOTE:对于for-in
循环,由于并非所有厂商都遵循相同的实现方式,因此仍未指定一个明确的枚举顺序;而Object.keys()
和JSON.stringfy()
都指明与for-in
使用相同的枚举顺序,因此它们的枚举顺序目前也不明晰
增强对象原型
改变对象原型
Object.setPrototypeOf
通过改变内部属性[[Prototype]]
来改变原型
简化原型访问的Super引用
Super
引用相当于指向对象原型的指针,实际上也即是Object.getPrototype(this)
的值
⚠️ 必须要在使用简写方法的独享中使用Super
引用,如果在其他方法声明中使用会导致语法错误
多重继承的情况下Object.getPrototype(this)
会出现问题(递归调用),使用Super
可以迎刃而解,Super
引用不是动态变化的,它总是指向正确的对象
正式的方法定义
ECMAScript 6正式将方法定义为一个函数,它会有一个内部的[[HomeObject]]
属性来容纳这个方法从属的对象
Super
所有引用都通过[[HomeObject]]
属性来确定后续的运行过程:
- 在
[[HomeObject]]
属性上调用Object.getPropertyOf()
方法检索原型的引用 - 搜寻原型找到同名函数
- 设置
this
绑定并且调用相应的方法
5 解构:使数据访问更便捷
为何使用解构功能
对象解构
解构赋值
({ foo, bar } = obj);
一定要用一对小括号包裹解构赋值语句,JavaScript引擎将一对开放的花括号视为一个代码块,而语法规定,代码块语句不允许出现在赋值语句左侧,添加小括号后可以将块语句转化为一个表达式,从而实现整个解构赋值的过程
默认值
为非同名局部变量赋值
数组解构
解构赋值
数组解构也可用于赋值上下文,但不需要用小花括号包括表达式
默认值
嵌套数组解构
不定元素
混合解构
解构参数
必须传值的解构参数
解构参数的默认值
6 Symbol和Symbol属性
创建Symbol
NOTE: 由于Symbol
是原始值,因此调用new Symbol()
会导致程序抛出错误。
Symbol
的描述被存储在内部的[[Description]]
属性中,只有当调用Symbol
的toString
方法时才可以读取这个属性
Symbol的使用方法
Symbol共享体系
Symbol.for(key)
方法首先在全局Symbol
注册表中搜索键为key
的Symbol
是否存在,如果存在,直接返回已有Symbol
;否则,创建一个新的Symbol
,并使用这个键在Symbol
全局注册表中注册,随即返回新创建的Symbol
Symbol.keyFor(symbol)
方法在Symbol
全局注册表中检索与Symbol
有关的键
Symbol与类型强制转换
Symbol
不可以被转换为字符串/数字类型,会报错
Symbol属性检索
Object.getOwnPropertySymbols()
通过well-known Symbol暴露内部操作
- Symbol.hasInstance 一个在执行
instanceof
时调用的内部方法,用于检测对象的继承信息。 - Symbol.isConcatSpreadable 一个布尔值,用于表示当传递一个集合作为
Array.prototype.concat()
方法的参数时,是否应该将集合内的元素规整到同一层级。 - Symbol.iterator 一个返回迭代器的方法。
- Symbol.match 一个在调用
String.prototype.match()
方法时调用的方法,用于比较字符串。 - Symbol.replace 一个在调用
String.prototype.replace()
方法时调用的方法,用于替换字符串的子串。 - Symbol.search 一个在调用
String.prototype.search()
方法时调用的方法,用于在字符串中定位子串。 - Symbol.species 用于创建派生类(将在第9章讲解)的构造函数。
- Symbol.split 一个在调用
String.prototype.split()
方法时调用的方法,用于分割字符串。 - Symbol.toPrimitive 一个返回对象原始值的方法。
- Symbol.toStringTag 一个在调用
Object.prototype.toString()
方法时使用的字符串,用于创建对象描述。 - Symbol.unscopables 一个定义了一些不可被with语句引用的对象属性名称的对象集合。
重写一个由well-known Symbol定义的方法,会导致对象内部的默认行为被改变,从而一个普通对象会变为一个奇异对象。
Set集合与Map集合
ECMAScript 5中的Set集合与Map集合
Object.create(null)
该解决方案的一些问题
属性名必须是字符串类型
ECMAScript 6中的Set集合
创建Set集合合并添加元素
Set
构造函数接受可迭代对象作为参数
add(key)
has(key)
移除元素
delete(key)
clear()
Set集合的forEach()方法
参数:
- Set集合中下一次索引的位置
- 与第一个参数一样的值
- 被遍历的
Set
集合本身
将Set集合转换为数组
let set = new Set([1, 2, 3]); // array -> set
let array = [...set]; // set -> array
WeakSet集合
WeakSet
只存储对象的弱引用,并且不可以存储原始值
与Set
的差别:
WeakSet
只支持add(key)
、has(key)
、delete(key)
3个方法,传入非对象参数会报错WeakSet
不可迭代,所以不能被用于for-of
循环- 不支持
size
属性
ECMAScript 6中的Map集合
Map集合支持的方法
has(key)
、delete(key)
、clear()
Map集合的初始化方法
let map = new Map(['name', 'Nicholas'], ['name', 25]);
数组包裹数组的模式看起来可能有点儿奇怪,但由于Map
集合可以接受任意数据类型的键名,为了确保它们在被存储到Map
集合中之前不会被强制转换为其他数据类型,因而只能将它们放在数组中,因为这是唯一一种可以准确地呈现键名类型的方式
Map集合的forEach()方法
参数:
Map
集合中下一次索引的位置- 值对应的键名
Map
集合本身
WeakMap集合
键名必须是一个对象,否则报错
只支持两个可以操作键值对的方法:has(key)
、delete(key)
,不支持clear()
、forEach()
,也不支持size
属性
迭代器(Iterator)和生成器(Generator)
循环语句的问题
什么是迭代器
interface {
done: boolean;
value: any;
}
什么是生成器
生成器是一种返回迭代器的函数
生成器函数每执行一条yield
语句后函数就会自动停止执行
⚠️ yield
的使用限制
yield
关键字只可在生成器内部使用,在其他地方使用会导致程序抛出语法错误,即便在生成器内部的函数里使用也是如此
function *createIterator(items) {
items.forEach(function(item){
yield items[i];
});
}
⚠️ 生成器函数没有[[Construct]]
,有内部属性[[IsGenerator]]
,为true
生成器函数表达式
NOTE: 不能用箭头函数来创建生成器
生成器对象的方法
可迭代对象和for-of
循环
可迭代对象是具有Symbol.iterator
属性的对象
ECMAScript 6中所有集合对象(数组、Set集合及Map集合)和字符串都是可迭代对象,这些对象都有默认的迭代器
NOTE:由于生成器默认会为Symbol.iterator
属性赋值,因此所有通过生成器创建的迭代器都是可迭代对象
访问默认迭代器
使用Symbol.iterator
创建可迭代对象
内建迭代器
集合对象迭代器
entries()
、values()
、keys()
数组和Set
集合的默认迭代器是values()
,Map
集合的默认迭代器是entries()
方法
字符串迭代器
支持Unicode
NodeList迭代器
展开运算符与非数组可迭代对象
展开运算符根据默认迭代器生成值
高级迭代器功能
给迭代器传递参数
如果给迭代器的next()
方法传递参数,则这个参数的值就会替代生成器内部上一条yield语句的返回值
在迭代器中抛出错误
iterator.throw(error)
生成器返回语句
NOTE:展开运算符与for-of
循环语句会直接忽略通过return
语句指定的任何返回值,只要done
一变为true
就立即停止读取其他的值
委托生成器
异步任务执行
简单任务执行器
向任务执行器传递数据
异步任务执行器
9 JavaScript中的类
ECMAScript 5中的近类结构
类的声明
基本的类声明语法
为何使用类语法
类和自定义类型的差异:
- 函数声明可以被提升,而类声明与
let
声明类似,不能被提升;真正声明语句之前,他们会一直存在于TDZ
中 - 类声明中的所有代码将自动运行在严格模式下,而且无法强行让代码脱离严格模式执行
- 在自定义类型中,需要通过
Object.defineProperty()
方法手工指定某个方法为不可枚举;而在类中,所有方法俺都是不可枚举的 - 每个类都有一个名为
[[Construct]]
的内部方法,通过关键字new
调用那些不含[[Construct]]
的方法会导致程序抛出错误 - 使用除关键字
new
以外的方式调用类的构造函数会导致程序抛出错误 - 在类中修改类名会导致程序报错
类表达式
基本的类表达式语法
命名类表达式
作为一等公民的类
一等公民:指一个可以传入函数,可以从函数返回,并且可以复制给变量的值
访问器属性
访问器是在原型上的,也会创建一个自己的属性
可计算成员名称
生成器方法
静态成员
继承与派生类
NOTE:使用super
的小贴士
- 只可在派生类的构造函数中使用
super()
,否则报错 - 在构造函数中访问
this
之前一定要调用super()
,它负责初始化this
,否则报错 - 如果不想调用
super()
,则唯一的方法是让类的构造函数返回一个对象
Uncaught ReferenceError: Must call super constructor in derived class before accessing 'this' or returning from derived constructor
类方法遮蔽
使用super.xxx()
调用基类中的方法
静态成员继承
如果基类有静态成员,那么这些静态成员在派生类中也可用
派生自表达式的类
只要表达式可以被解析为一个函数并且具有[[Constructor]]
属性和原型,那么就可以用extends
进行派生
内建对象的继承
在ECMAScript 5的额传统继承方式中,先由派生类型创建this
的额值,然后调用基类型的构造函数。这也意味着,this
的值开始指向的是派生类的实例,但是随后会被来自基类的其他属性所修饰
ECMAScript 6中的类继承与之相反,先有基类创建this
的值,然后派生类的构造函数再修改这个值。所以一开始可以通过this
访问基类的所有内建功能,然后再正确地接收所有与之相关的功能
Symbol.species属性
内建对象继承的一个实用之处是,原本在内建对象中返回实例自身的方法将自动返回派生类的实例。背后是通过Symbol.species
属性实现这一行为
在类的构造函数中使用new.target
10 改进的数组功能
创建数组
Array.of()
方法
帮助开发者们规避通过Array
构造函数创建数组的怪异行为
Array.from()
方法
Array.from()
可以接受可迭代对象或雷书租对象作为第一个参数,第二个参数是映射函数,第三个参数是映射函数的this
如果一个对象既是类数组又是可迭代的,则迭代器优先
为所有数组添加的新方法
find()
方法和findIndex()
方法
file()
方法
copyWithin()
方法
定型数组
定型数组与普通数组的相似之处
通用方法、迭代器、of()
方法和from()
方法
定型数组与普通数组的差别
- 可以修改
length
属性来改变普通数组的大小,而定型数组的length
是一个不可写属性,所以不能修改定型数组的大小,如果尝试修改这个值,在非严格模式下会直接忽略该操作,在严格模式下会抛出错误 - 定型数组不是普通数组,不继承自
Array
,通过Array.isArray()
方法检查定型数组返回的是false
- 当操作普通数组时,其可以变大变小,但定型数组却始终保持相同的尺寸,给定型数组中不存在的数值索引赋值会被忽略
- 定型数组会检查数据的合法性,
0
被用于代替所有非法值 - 定型数组没有修改尺寸的方法
concat()
方法是因为两个定型数组合并后的结果会变得不确定,这直接违背了使用定型数组的初衷。 - 定型数组有两个附加方法
set()
和subarray()
13 用模块封装代码
什么是模块
模块是自动运行在严格模式下并且没有办法退出运行的JavaScript代码
- 模块顶部
this
是undefined
- 模块不支持HTML风格的代码注释
导出的基本语法
导入的基本语法
- 导入的变量是类似
const
的 - 导入语句会提升
导入单个绑定
导入多个绑定
导入整个模块
模块语法限制
- 必须在其他语句核函数之外使用
export
语句不允许出现在if
语句中,不能有条件或以任何方式动态导出
导入绑定的一个微妙怪异之处
import
语句为变量、函数和类创建的是只读绑定,引用值
导出和导入时重命名
模块的默认值
导出默认值
默认导出不是引用绑定
导入默认值
默认值必须排在非默认值之前
重新导出一个绑定
无绑定导入
加载模块
在Web浏览器中使用模块
<script>
元素通过src
属性指定一个地址来加载- 将代码内嵌到没有
src
属性的<script>
元素中 - 通过
Web Worker
或Service Worker
的方法加载
Web浏览器中的模块加载顺序
<script type="module">
执行时自动应用defer
属性。加载脚本文件时,defer
是可选属性;加载模块时,它就是必须属性
defer
:边解析文档边加载,全部解析完后,DOMContentLoaded事件触发之前执行
async
: 边解析文档边加载,加载完后执行,并中断文档解析
模块加载顺序:
- 按先后顺序解析模块导入语句,并且递归解析,然后加载
- 所有模块加载完成并且文档解析完毕开始按顺序并递归执行模块
Worker
默认的加载机制是按脚本的方式加载文件,可以传递第二个参数指定为"script"
:
let worker = new Worker('module.js', { type: 'module' });
Worker
只能从引用的网页相同的源加载,但是Worker
模块不会完全受限,可以加载适当的CORS
头的文件。
附录
A EcMAScript中较小的改动
使用整数
JavaScript
使用IEEE 754
编码系统来表示整数和浮点数
识别整数
Number.isInteger()
利用IEEE 754
编码系统存储浮点数和证书的方式不同的差异来判断
安全整数
IEEE 754
只能准确地表示-2^53 ~ 2^53
之间的整数
Number.isSageInteger()
识别语言可以准确表示的整数
Number.MAX_SAGE_INTEGER
和NUMBER.MIN_SAFE_INTEGER
分别表示整数范围的上线和下限
新的Math方法
基于硬件的方法,以提高数学计算的速度
Unicode标识符
有效的标识符:
- 第一个字符必须是
$
、_
或任何带有ID_Start
的派生核心属性的Unicode符号 - 后续的美国各字符必须是$
、
_、
\u200c(零宽度不连字,zero-width non-joiner)、
\u200d(零宽度连字,zero-width joiner)或具有
ID_Continue`的派生核心属性的任何Unicode符号
正式化__proto__
属性
ECMAScript标准建议使用Object.getPrototypeOf()
方法和Object.setPrototypeOf()
方法,源于__proto__
具有以下特征:
- 只能在对象字面量中制定一次
__proto__
, 如果指定两个__proto__
属性则会抛出错误。这是唯一具有该限制的对象字面量属性 - 可计算形式的
["__proto__"]
的行为类似于普通属性,不会设置或返回当前对象的原型。与对象字面量属性相关的所有规则均适用于此形式,应用不可计算的形式则会抛出一次