本笔记只包含该书精华部分,基础内容大多略过,也有一些自己的想法
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修饰符将正则表达式从编码单元操作模式切换为字符模式
其他字符串变更
字符串中的子串识别
includesstartsWithendsWith
这三个方法传入正则表达式会报错,而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__"]的行为类似于普通属性,不会设置或返回当前对象的原型。与对象字面量属性相关的所有规则均适用于此形式,应用不可计算的形式则会抛出一次