《深入理解ES6》读书笔记

397 阅读11分钟

本笔记只包含该书精华部分,基础内容大多略过,也有一些自己的想法

1 块级作用域绑定

var声明及变量提升(Hoisting)机制

块级声明

块级作用域(词法作用域)存在于:

  • 函数内部
  • 块中(字符{}之间的区域)

let声明

禁止重声明

同一作用域中不能用let重复定义已经存在的标识符,否则会抛出错误

const声明

临时死区(Temporal Dead Zone)

在声明前访问let/const声明的变量即使是typeof也会报错。

JavaScript引擎在扫描代码发现变量声明时,要么将它们提升至作用域顶部(遇到var声明),要么将声明放到TDZ(遇到letconst声明)。访问TDZ中的变量会触发运行时错误。只有执行过变量声明语句后,变量才会从TDZ中移出,然后方可正常访问。

循环中的块级作用域绑定

循环中的函数

循环中的let声明

每次循环都会创建一个新变量,并以之前迭代中同名变量的值将其初始化

NOTE: let声明在循环内部的行为是标准中专门定义的,它不一定与let的不提升特性相关,理解这一点至关重要。事实上,早期let实现不包括这一行为,它是后来加入的。

循环中的const声明

对于普通的for循环可以在初始化变量时使用const,但是更改这个变量的值就会抛出错误

for-infor-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

这三个方法传入正则表达式会报错,而indexOflastIndexOf会把传入的正则表达式转化为一个字符串并搜索它

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/applynew关键字调用

元属性(Metaproperty):new.target

当调用[[Contructor]]时,new.target指向实例;当调用[[Call]]时,new.targetundefined

⚠️ 在函数外使用new.target是一个语法错误

块级函数

在ECMAScript 3和早期版本中,在代码块中声明一个块级函数严格来说是一个语法错误,但是所有浏览器仍然支持这个特性

ECMAScript 5的严格模式中引入了一个错误提示;在ECMAScript 6严格模式中,视作一个块级声明,函数可以提升到代码块顶部,非严格模式下提升至外围函数或全局作用域顶部

箭头函数

与传统函数的不同

  • 没有thissuperarguments、和new.target绑定
  • 不能通过new关键字调用。箭头函数没有[[Construct]]方法
  • 没有原型property
  • 不可以改变this绑定
  • 不支持arguments对象
  • 不支持重复的命名参数。传统函数中,只有严格模式下才不能有重复的命名参数
我的原因猜想

箭头函数为什么不支持arguments对象,不支持重复的命名参数呢?

新的标准肯定是要摒弃以前错误/不好的设计的。es6不定参数可以取代arguments, 重复命名参数是个错误,所以既然创造的是一个新语法没有历史遗留问题,那么就可以直接摒弃,而原来的还支持是为了向前兼容,不得已而为之

箭头函数语法

创建立即执行函数表达式

箭头函数没有this绑定

箭头函数和数组

箭头函数没有arguments绑定

箭头函数的辨识方法

尾调用优化

尾调用指的是函数作为另一个函数的最后一条语句被调用

ECMAScript 6中的尾调用优化

尾调用优化需要满足的条件:

  • 尾调用不访问当前栈帧的变量(也就是说函数不是一个闭包)
  • 在函数内部,尾调用是最后一条语句
  • 尾调用的结果作为函数返回值返回

4 扩展对象的功能性

对象类别

  • 普通对象(Ordinary)对象 具有JavaScript对象所有的默认内部行为
  • 特异对象(Exotic)对象 具有某些与默认行为不符的内部行为
  • 标准(Standard)对象 ECMAScript 6规范中定义的对象,例如,ArrayDate等。标准对象既可以是普通对象,也可以是特异对象
  • 内建对象 脚本开始执行时存在于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()方法处理属性的顺序

  1. 所有数字键按升序排序
  2. 所有字符串键按照他们被加入对象的顺序排序
  3. 所有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]]属性来确定后续的运行过程:

  1. [[HomeObject]]属性上调用Object.getPropertyOf()方法检索原型的引用
  2. 搜寻原型找到同名函数
  3. 设置this绑定并且调用相应的方法

5 解构:使数据访问更便捷

为何使用解构功能

对象解构

解构赋值

({ foo, bar } = obj);

一定要用一对小括号包裹解构赋值语句,JavaScript引擎将一对开放的花括号视为一个代码块,而语法规定,代码块语句不允许出现在赋值语句左侧,添加小括号后可以将块语句转化为一个表达式,从而实现整个解构赋值的过程

默认值

为非同名局部变量赋值

数组解构

解构赋值

数组解构也可用于赋值上下文,但不需要用小花括号包括表达式

默认值

嵌套数组解构

不定元素

混合解构

解构参数

必须传值的解构参数

解构参数的默认值

6 Symbol和Symbol属性

创建Symbol

NOTE: 由于Symbol是原始值,因此调用new Symbol()会导致程序抛出错误。

Symbol的描述被存储在内部的[[Description]]属性中,只有当调用SymboltoString方法时才可以读取这个属性

Symbol的使用方法

Symbol共享体系

Symbol.for(key)方法首先在全局Symbol注册表中搜索键为keySymbol是否存在,如果存在,直接返回已有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代码

  • 模块顶部thisundefined
  • 模块不支持HTML风格的代码注释

导出的基本语法

导入的基本语法

  • 导入的变量是类似const
  • 导入语句会提升

导入单个绑定

导入多个绑定

导入整个模块

模块语法限制

  • 必须在其他语句核函数之外使用
  • export语句不允许出现在if语句中,不能有条件或以任何方式动态导出

导入绑定的一个微妙怪异之处

import语句为变量、函数和类创建的是只读绑定,引用值

导出和导入时重命名

模块的默认值

导出默认值

默认导出不是引用绑定

导入默认值

默认值必须排在非默认值之前

重新导出一个绑定

无绑定导入

加载模块

在Web浏览器中使用模块

  • <script>元素通过src属性指定一个地址来加载
  • 将代码内嵌到没有src属性的<script>元素中
  • 通过Web WorkerService Worker的方法加载

Web浏览器中的模块加载顺序

<script type="module">执行时自动应用defer属性。加载脚本文件时,defer是可选属性;加载模块时,它就是必须属性

defer:边解析文档边加载,全部解析完后,DOMContentLoaded事件触发之前执行 async: 边解析文档边加载,加载完后执行,并中断文档解析

模块加载顺序:

  1. 按先后顺序解析模块导入语句,并且递归解析,然后加载
  2. 所有模块加载完成并且文档解析完毕开始按顺序并递归执行模块

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_INTEGERNUMBER.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__"]的行为类似于普通属性,不会设置或返回当前对象的原型。与对象字面量属性相关的所有规则均适用于此形式,应用不可计算的形式则会抛出一次