Skip to content

《你不知道的JavaScript(下卷)》精读笔记

写在前面

  • 书籍介绍:JavaScript这门语言简单易用,很容易上手,但其语言机制复杂微妙,即使是经验丰富的JavaScript开发人员,如果没有认真学习的话也无法真正理解。本套书直面当前JavaScript开发人员不求甚解的大趋势,深入理解语言内部的机制,全面介绍了JavaScript中常被人误解和忽视的重要知识点。
  • 我的简评:《你不知道的JavaScript》系列分上中下三卷,这里是下卷,主要讲解ES6相关的。该系列书籍本人觉得就上卷写的不错,中卷有些冗余,下卷讲ES6比较粗糙。不过有空翻一翻,还是多少有些收获。
  • !!福利:文末有笔记思维导图、随书资料打包等下载地址哦

第1章 ES?现在与未来

  • 与ES5不同,ES6并不仅仅是为这个语言新增一组API。它包括一组新的语法形式,其中的一部分可能是要花些时间才能理解和熟悉的。它还包括各种各样的新的组织形式和操作各种数据类型的新的辅助API

1.2.transpiling

  • transpiling(transformation+compiling,转换+编译)的技术。简单的说,其思路是利用专门的工具把你的ES6代码转换为等价(或近似)的可以在ES5环境下工作的代码
  • 并非所有的ES6新特性都需要使用transpiler,还有polyfill(也称为shim)这种模式。在可能的情况下,polyfill会为新环境中的行为定义在旧环境中的等价行为。语法不能polyfill,而API通常可以

第2章 语法

2.1.块作用域声明

  • JavaScript中变量作用域的基本单元一直是function。如果需要创建一个块作用域,最普遍的方法除了普通的函数声明之外,就是立即调用函数表达式(IIFE)
  • let 声明:可以创建绑定到任意块的声明。只需要一对{..}就可以创建一个作用域;过早访问let 声明的引用导致的这个ReferenceError严格说叫做临时死亡区错误(TDZ)--你在访问一个已经声明但没有初始化的变量;声明时没有赋值的变量会自动赋值为undefined,所以let b;就等价于let b = undefined;;未声明的变量,所以typeof是检查它是否存在的唯一安全的方法;为什么我坚持认为应该把所有的let声明放在其所在作用域的最前面了,这样就完全避免了不小心过早访问的问题;
  • const声明:const ,用于创建常量;它是一个设定了初始值之后就只读的变量;const 声明必须要有显式的初始化;如果这个值是复杂值,比如对象或者数组,其内容仍然是可以修改的;是否使用const,理论上说,引擎更容易了解这个变量的值/类型永远不会改变,那么它就可以取消某些可能的追踪;
  • 块作用域函数:从ES6开始,块内声明的函数,其作用域在这个块内

2.2.spread/rest

  • 当...用在数组之前时(实际上是任何iterable),它会把这个变量“展开”为各个独立的值
  • ...的另外一种常见用法基本上可以被看作反向的行为;...把一系列值收集到一起成为一个数组

2.3.默认参数值

  • 可能JavaScript最常见的一个技巧就是关于设定函数参数默认值的
  • 只能通过传入比“期望”更少的参数来省略最后的若干参数(例如,后侧的),而无法省略位于参数列表中间或者起始处的参数
  • 一个很重要的需要记住的JavaScript设计原则:undefined意味着缺失。也就是说,undefined和缺失是无法区别的,至少对于函数参数来说是如此
  • 默认值表达式:默认值表达式是惰性求值的,这意味着它们只在需要的时候运行--也就是说,是在参数的值省略或者为undefined的时候;Function.prototype本身就是一个没有操作的空函数;

2.4.解构

  • 可以把将数组或者对象属性中带索引的值手动赋值看作结构化赋值。专门语法,专用于数组解构和对象解构
  • 不只是声明:除非需要把所有的赋值表达式都当作声明,否则不应该在赋值中混入声明。不然会出现语法错误
  • 重复赋值:对象解构形式允许多次列出同一个源属性(持有值类型任意);记住:解构的目的不只是为了打字更少,而是为了可读性更强;

2.5.太多,太少,刚刚好

  • 如果为比解构/分解出来的值更多的值赋值,那么就像期望的一样,多余的值会被赋为undefined
  • 默认值赋值:使用与前面默认函数参数值类似的=语法,解构的两种形式都可以提供一个用来赋值的默认值
  • 嵌套解构:可以把嵌套解构当作一种展开对象名字空间的简单方法
  • 解构参数:解构赋值/默认值会被放在参数列表中,而重组的过程会被放在函数体的return语句中

2.6.对象字面量扩展

  • 简洁属性
  • 简洁方法:严格说来,ES5定义了getter/setter字面量形式,但是没怎么被使用,主要是因为缺少transpiler来处理这个新语法(实际上也是ES5新增的唯一主要新语法)
  • 计算属性名:对象字面定义属性名位置的[..]中可以放置任意合法表达式;计算属性名最常见的用法可能就是和Symbols共同使用;
  • 设定[[Prototype]]
  • super对象

2.7.模板字面量

  • 这个特性的名称非常具有误导性,其根据你对单词模板(template)的理解而定
  • 插入字符串字面量中的换行(新行)会在字符串值中被保留
  • 插入表达式:在插入字符串字面量的${..}内可以出现任何合法的表达式,包括函数调用、在线函数表达式调用,甚至其他插入字符串字面量;插入字符串字面量在它出现的词法作用域内,没有任何形式的动态作用域;
  • 标签模板字面量:重新命名这个特性来明确表达其功能:标签字符串字面量;哪些实际应用(用来把数字格式化为美元表示法;其他应用包括全球化、本地化等的特殊处理)

2.8.箭头函数

  • 箭头函数定义包括一个参数列表(零个或多个参数,如果参数个数不是一个的话要用(..)包围起来),然后是标识=>,函数体放在最后
  • 箭头函数总是函数表达式;并不存在箭头函数声明
  • 箭头函数是匿名函数表达式--他们没有用于递归或者事件绑定/解绑定的命名引用
  • 我认为=>箭头函数转变带来的可读性与被转化函数的长度负相关。这个函数越长,=>带来的好处就越小;函数越短,=>带来的好处就越大
  • 不只是更短的语法,而是this:对=>的关注多数都在于从代码中去掉function、return和{..}节省了那些宝贵的键盘输入;对=>的关注多数都在于从代码中去掉function、return和{..}节省了那些宝贵的键盘输入;在箭头函数内部,this绑定不是动态的,而是词法的;底线:=>是关于this、arguments和super的词法绑定。这个ES6的特性设计用来修正一些常见的问题,而不是bug、巧合或者错误

2.9.for...of循环

  • for..of循环的值必须是一个iterable,或者说它必须可以转换/封箱到一个iterable对象的值。iterable就是一个能够产生迭代器供循环使用的对象
  • JavaScript中默认为(或提供)iterable的标准内建值包括:Arrays、Strings、Generators、Collections/TypedArrays
  • for..of循环也可以通过break、continue、return(如果在函数中的话)提前终止,并抛出异常

2.10.正则表达式

  • 一个事实:JavaScript中的正则表达式很长时间以来基本上没有什么变化
  • Unicode标识:在ES6之前,正则表达式只能基于PMB字符匹配,这意味着那些扩展字符会被当作两个独立的字符来匹配;在ES6中,u标识符表示正则表达式用Unicode(UTF-16)字符来解释处理字符串,把这样的扩展字符串当作单个实体来匹配;影响的是匹配部分的长度;还有一点需要注意,u标识符使得+和*这样的量词把整个Unicode码点作为单个字符而应用,而不仅仅是应用于字符的低位(也就是符号的最右部分);
  • 定点标识:ES6正则表达式中另外一个新增的标签模式是y,通常称为“定点(sticky)模式”;定点主要是指在正则表达式的起点有一个虚拟的锚点,只从正则表达式的lastIndex属性指定的位置开始匹配;
  • 正则表达式flags:ES6之前,想要通过检查一个正则表达式对象来判断它应用了哪些标识,需要把它从source属性的内容中解析出来;在ES6中,现在可以用新的flags属性直接得到这些值;ES6的另一个调整是如果把标识传给已有的正则表达式,RegExp(..)构造器现在支持flags;

2.11.数字字面量扩展

  • 虽然可以用不同进制形式指定数字,但是数字的数字值还是保存的值,并且默认的输出解释形式总是十进制

2.12.Unicode

  • Unicode字符范围从0x0000到0xFFFF,包含可能看到和接触到的所有(各种语言的)标准打印字符。这组字符称为基本多语言平面(BMP)。BMP甚至包含了像雪人这样的有趣的符号
  • 现在有了可以用于作Unicode转义(在字符串和正则表达式中)的新形式,称为Unicode码点转义
  • 支持Unicode的字符:默认情况下,JavaScript字符串运算和方法不能感知字符串中俄astral符号;如何精确计算这样的字符串长度。[...gclef].length
  • 字符定位:原生的ES6对此的答案是charAt,但不支持astral字符的原子性,也不会考虑组合符号的因素;ES6提供了codePointAt;组合String.fromCodePoint和codePointAt来获得支持Unicode的charAt的更简单且更优的方法;
  • Unicode标识符名

2.13.符号

  • ES6为JavaScript引入了一个新的原生类型:Symbol。它和其他原生类型不一样,symbol没有字面量形式
  • 以下几点需要注意:不能也不应该对Symbol()使用new。它并不是一个构造器,也不会创建一个对象;传给Symbol()的参数是可选的。如果传入了的话,应该是一个为这个symbol的用途给出用户友好描述的字符串;typeof的输出是一个新的值("symbol"),这是识别symbol的首选方法;
  • 符号本身的内部值-称为它的名称(name)-是不在代码中出现且无法获得的。可以把这个符号值想象为一个自动生成的、(在应用内部)唯一的字符串值
  • 符号的主要意义是创建一个类似字符串的不会与其他任何值冲突的值
  • 如果需要把符号值作为一个真正的字符串使用,那么它就需要用String()或者toString()进行显式类型转换,因为不允许隐式地把符号转换为字符串
  • 符号注册:Symbol.for()在全局符号注册表中搜索,来查看是否有描述文字相同的符号已经存在,如果有的话就返回它;换句话说,全局注册表把符号值本身根据其描述文字作为单例处理;具有讽刺意义的是,基本上符号的目的是为了取代应用中的magic字符串(赋予特殊意义的任意字符串);可以使用Symbol.keyFor()提取注册符号的描述文本(键值);
  • 作为对象属性的符号:如果把符号用作对象的属性/键值,那么它会以一种特殊的方式存储,使得这个属性不出现在对这个对象的一般属性枚举中;要取得对象的符号属性,使用Object.getOwnPropertySymbols;

2.14.小结

  • 这些新语法形式中大多数的设计目的都是消除常见编程技巧中的痛点,比如为函数设定默认值以及把参数的“其余”部分收集到数组中
  • 而像=>箭头函数这样的特性看起来是为了使代码更简洁的语法,实际上它有非常特别的行为特性,应该只在适当的时候使用

第3章 代码组织

3.1.迭代器

  • 迭代器(iterator)是一个结构化的模式,用于从源以一次一个的方式提取数据
  • 迭代器是一种有序的、连续的、基于拉取的用于消耗数据的组织方式
  • 接口:IteratorResult接口指定了从任何迭代器操作返回的值必须是下面这种形式的对象:{value: .., done: true/false}
  • next()迭代:迭代器的next(..)方法可以接受一个或多个可选参数。绝大多数内置迭代器没有利用这个功能
  • 可选的return和throw:多数内置迭代器都没有实现可选的迭代器接口-return (..)和throw(..);return(..)被定义为向迭代器发送一个信号,表明消费者代码已经完毕,不会再从其中提取任何值;throw(..)用于向迭代器报告一个异常/错误,迭代器针对这个信号的反应可能不同于针对return(..)意味着的完成信号;
  • 自定义迭代器:这种特定的用法强调了迭代器可以作为一个模式来组织功能,而不仅仅是数据

3.2.生成器

  • ES6引入了一个全新的某种程度上说是奇异的函数形式,称为生成器
  • 生成器可以在执行当中暂停自身,可以立即恢复执行也可以过一段时间之后恢复执行。所以显然它并不像普通函数那样保证运行到完毕
  • 在执行当中的每次暂停/恢复循环都提供了一个双向信息传递的机会,生成器可以返回一个值,恢复它的控制代码也可以发回一个值
  • 语法:执行生成器,比如foo(5, 10),并不实际在生成器中运行代码。相反,它会产生一个迭代器控制这个生成器执行其代码;生成器还有一个可以在其中使用的新关键字,用来标示暂停点:yield;一个永不结束的循环就意味着一个永不结束的生成器,这是完全有效的,有时候完全就是你所需要的;yield严格上说不是一个运算符,尽管像yield 1这样使用它的时候确实看起来像是运算符;yield..表达式和赋值表达式行为上的类似性有一定概念上的合理性;因为yield关键字的优先级很低,几乎yield..之后的任何表达式都会首先计算,然后再通过yield发送;和=赋值一样,yield也是“右结合”的,也就是说多个yield表达式连续出现等价于用(..)从右向左分组;yield * 可以调用另外一个生成器(通过委托到其迭代器),所以它也是可以通过调用自身执行某种生成器递归;
  • 迭代器控制:ES6之后 的一个早期提案会通过生成器内部一个独立的元属性,支持访问传入最初next(..)调用的值;可以把生成器看作是值的产生器,其中每次迭代就是产生一个值来消费;从更通用的意义上来说,可能更合理的角度是把生成器看作一个受控的、可传递的代码执行;
  • 提前完成:生成器上附着的迭代器支持可选的return(..)和throw(..)方法。这两种方法都有立即终止一个暂停的生成器的效果;return(..)除了可以手动调用,还可以在每次迭代的末尾被任何消耗迭代器的ES6构件自动调用,比如for...of循环和spread运算符;这个功能的目的是通知生成器如果控制代码不再在它上面迭代,那么它可能就会执行清理任务(释放资源,重置状态等);不要把yield语句放在finally子句内部,虽然这是有效且合法的,但确实是一个可怕的思路。它会延后你的return(..)调用的完成;和return(..)不同,迭代器的throw(..)方法从来不会被自动调用;
  • 错误处理:生成器的错误处理可以表达为try...catch,它可以在由内向外和由外向内两个方向工作
  • Transpile生成器:一个练习巩固了生成器实际上就是状态机逻辑的简化语法这个概念
  • 生成器使用:两个主要模式(产生一系列值;顺序执行的任务队列;)

3.3.模块

  • 在所有JavaScript代码中,唯一最重要的代码组织模式是模块
  • 旧方法:传统的模块模式基于一个带有内部变量和函数的外层函数,以及一个被返回的“public API”;其中常见的是异步模块定义(AMD),还有一种是通用模块定义(UMD);
  • 前进:对于ES6来说,我们不再需要依赖于封装函数和闭包提供模块支持;ES6模块和过去我们处理模块的方式之间的显著概念区别;ES6模块将会为代码组织提供完整支持,包括封装、控制公开API以及引用依赖导入;长远来看,ES6模块从本质上说必然会取代之前所有的模块格式和标准,即使是CommonJS,因为ES6模块是建立在语言的语法支持基础上的;模块transpiler/转换工具是必不可少的。不管你是在编写普通模块、AMD、UMD、CommonJS还是ES6,这些工具都不得不解析转化为对代码运行的所有环境都适用的形式;
  • 新方法:支撑ES6模块的两个主要新关键字是import和export;import和export都必须出现在使用它们的最顶层作用域。它们必须出现在所有代码块和函数的外面;没有用export标示的一切都在模块作用域内部保持私有。在模块内没有全局作用域;模块还能访问window和所有的“全局”变量,只是不作为词法上的顶层作用域;模块导出不是像你熟悉的赋值运算符=那样只是值或者引用的普通赋值。实际上,导出的是对这些东西(变量等)的绑定(类似于指针);默认导出把一个特定导出绑定设置为导入模块时的默认导出。绑定的名称就是default;每个模块只能有一个default;JavaScript引擎无法静态分析平凡对象的内容,这意味着它无法对静态import进行性能优化。让每个成员独立且显式地导出的优点是引擎可以对其进行静态分析和优化;
  • 模块依赖环:首先必须声明,尽量避免故意设计带有环形依赖的系统;本质上说,相互导入,加上检验两个import语句的有效性的静态验证,虚拟组合了两个独立的模块空间(通过绑定),这样foo(..)可以调用bar(..),反过来也是一样;import语句的静态加载语义意味着可以确保通过import相互依赖的“foo”和“bar”在其中任何一个运行之前,二者都会被加载、解析和编译;
  • 模块加载:import语句使用外部环境(浏览器、Node.js等)提供的独立机制,来实际把模块标识符字符串解析成可用的指令,用于寻找和加载所需的模块。这个机制就是系统模块加载器;如果在浏览器中,环境提供的默认模块加载器会把模块标识符解析为URL。如果在像Node.js这样的服务器上就解析为本地文件系统路径;模块加载器本身不是由ES6指定的。他是独立的、平行的标准,目前由WHATWG浏览器标准工作组管理;

3.4.类

  • class:新的ES6类机制的核心是关键字class,表示一个块,其内容定义了一个函数原型的成员;ES6 class 本身并不是一个真正的实体,而是一个包裹着其他像函数和属性这样的具体实体并把他们组合到一起的元概念;
  • extends和super:ES6还通过面向类的常用术语extends提供了一个语法糖,用来在两个函数原型之间建立[[Prototype]]委托链接-通常被误称为“继承”或者令人迷惑的标识为“原型继承”;在构造器中,super自动指向“父构造器”,在方法中,super会指向“父对象”,这样就可以访问其属性/方法了;
  • new.target:新概念,称为元属性;new target是一个新的在所有函数中都可用的“魔法”值,尽管在一般函数中它通常是undefined;在任何构造器中,new.target总是指向new实际上直接调用的构造器,即使构造器是在父类中且通过子类构造器用super(..)委托调用;除了访问静态/方法之外,类构造器中的new.target元属性没有什么其他用处;
  • static:直接添加到这个类的函数对象上,而不是在这个函数对象的prototype对象上

3.5.小结

  • 迭代器提供了对数组或运算的顺序访问。可以通过像for..of...这些新语言特性来消耗迭代器
  • 生成器是支持本地暂停/恢复的函数。通过迭代器来控制。它们可以用于编程(也是交互地,通过yield/next消息传递)生成供迭代消耗的值
  • 模块支持对实现细节的私有封装,并提供公开导出API。模块定义是基于文件的单例实例,在编译时静态决议
  • 类对基于原型的编码提供了更清晰的语法。新增的super也解决了[[prototype]]链中相对引用的棘手问题

第4章 异步流控制

  • 管理异步的主要机制一直以来都是函数回调
  • ES6增加了一个新的特性来帮助解决只用回调实现异步的严重缺陷:Promise

4.1.Promise

  • 介绍:一些错误观念:Promise不是对回调的替代。Promise在回调代码和将要执行这个任务的异步代码之间提供了一种可靠的中间机制来管理回调;可以把Promise链接到一起,这就把一系列异步完成的步骤串联了起来;Promise链提供了一个近似的异步控制流;还有一种定义Promise的方式,就是把它看作一个未来值,对一个值得独立于时间的封装容器;换句话说,Promise可以被看作是同步函数返回值的异步版本;Promise的决议结果只有两种可能:完成或拒绝,附带一个可选的单个值;Promise只能被决议(完成或者拒绝)一次;
  • 构造和使用Promise:提供给构造器Promise(..)的两个参数都是函数,一般称为resolve(..)和reject(..);Promise有一个then(..)方法,接受一个或两个回调函数作为参数;默认的成功回调把完成值传出,默认的出错回调会传递拒绝原因值;如果永远不通过then(..)或catch(..)调用来观察的话,它就会一直保持未处理状态;
  • Thenable:Promise(..)构造器的真正实例是Promise。但还有一些类promise对象,称为thenable,一般来说,它们也可以用Promise机制解释;任何提供了then(..)函数的对象(或函数)都被认为是thenable;一般来说,如果从某个其他系统接收到一个自称promise或者thenable的东西,不应该盲目信任它;避免把可能被误认为thenable的值直接用于Promise机制;
  • Promise API:Promise API还提供了一些静态方法与Promise一起工作;Promise.resolve(..)创建了一个决议到传入值的promise;resolve(..)和Promise.resolve(..)可以接受promise并接受它的状态/决议,而reject(..)Promise.reject(..)并不区分接收的值是什么;Promise.all([..])等待所有都完成(或者第一个拒绝),而Promise.race([..])等待第一个完成或者拒绝;Promise.all([])将会立即完成(没有完成值),Promise.race([])将会永远挂起。因此建议,永远不要用空数组使用这些方法;

4.2.生成器+Promise

  • 这个重要的模式需要理解一下:生成器可以yield一个promise,然后这个promise可以被绑定,用其完成值来恢复这个生成器的运行
  • Promise是一种把普通回调或者chunk控制反转反转回来的可靠系统
  • 把Promise的可信任性与生成器的同步代码组合在一起有效解决了回调所有的重要缺陷
  • 本质上说,只要代码中出现超过两个异步步骤的流控制逻辑,都可以也应该使用由run工具驱动的promise-yield生成器以异步风格表达控制流

4.3.小结

  • ES6新增了Promise来弥补回调的主要缺陷之一:缺少对可预测行为方式的保证。Promise代表了来自可能异步的任务的未来完成值,跨越同步和异步边界对行为进行规范化
  • Promise与生成器的结合完全实现了重新安排异步流控制代码来消除丑陋的回调乱炖(或称地狱)

第5章 集合

  • Map就像是一个对象(键/值对),但是键值并非只能为字符串,而是可以使用任何值-甚至是另一个对象或map
  • Set与数组(值的序列)类似,但是其中的值是唯一的;如果新增的值是重复的,就会被忽略。
  • 还有相应的弱(与内存/垃圾回收相关)版本:WeakMap和WeakSet

5.1.TypedArray

  • 实际上带类型的数组更多是为了使用类数组语义(索引访问等)结构化访问二进制数据
  • 大小端:理解下面这点很重要:arr的映射是按照运行JavaScript的平台的大小端设置(大端或小端)进行的;大小端的意思是多字节数字(比如前面代码片段中创建的16位无符号整型)中低字节(8位)位于这个数字字节表示中德右侧还是左侧;目前Web上最常用的是小端表示方式,但是肯定存在不采用这种方式的浏览器;
  • 多视图:单个buffer可以关联多个视图
  • 带类数组构造器:带类型数组构造器的实例几乎和普通原生数组完全一样。一些区别包括具有固定的长度以及值都属于某种“类型”;不能对TypedArray使用不合理的Array.prototype方法,比如修改器(splice(..)push(..)等)和concat(..);要清楚TypedArray中德元素是限制在声明的位数大小中的;要解决平方值溢出的局限,可以使用TypedArray.from(..)函数;

5.2.Map

  • 了解对象是创建无序键/值对数据结构,也称为映射(map)的主要机制
  • 对象作为映射的主要缺点是不能使用非字符串值作为键
  • 唯一的缺点就是不能使用方括号[ ]语法设置和获取值,但完全可以使用get(..)set(..)方法完美代替
  • Map值
  • Map键:map的本质是允许你把某些额外的信息(值)关联到一个对象(键)上,而无需把这个信息放入对象本身;对于map来说,尽管可以使用任意类型的值作为键,但通常我们会使用对象,因为字符串或者其他基本类型已经可以作为普通对象的键使用;

5.3.WeakMap

  • WeakMap是map的变体,二者的多数外部行为都是一样的,区别在于内部内存分配(特别是其GC)的工作方式
  • WeakMap(只)接受对象作为键。这些对象是被弱持有的,也就是说如果对象本身被垃圾回收的话,在WeakMap中的这个项目也会被移除
  • WeakMap没有size属性或clear()方法,也不会暴露任何键、值或项目上的迭代器
  • 需要注意的是,WeakMap只是弱持有它的键,而不是值

5.4.Set

  • set是一个值的集合,其中的值唯一(重复会被忽略)
  • 除了会把-0和0当作是一样的而不加区别之外,has(..)中的比较算法和Object.is(..)几乎一样
  • Set迭代器:set固有的唯一性是它最有用的特性;set的唯一性不允许强制转换,所以1和“1”被认为是不同的值;

5.5.WeakSet

  • 就像WeakMap弱持有它的键(对其值是强持有的)一样,WeakSet对其值也是弱持有的(这里并没有键)
  • WeakSet的值必须是对象,而并不像set一样可以是原生类型值

5.6.小结

  • TypedArray提供了对二进制数据buffer的各种整型类型“视图”,比如8位无符号整型和32位浮点型。对二进制数据的数组访问使得运算更容易表达和维护,从而可以更容易操纵视频、音频、canvas数据等这样的复杂数据
  • Map是键/值对,其中的键不只是字符串/原生类型,也可以是对象。Set是成员值(任意类型)唯一的列表
  • WeakMap也是map,其中的键(对象)是弱持有的,因此当它是对这个对象的最后一个引用的时候,GC(垃圾回收)可以回收这个项目。WeakSet也是set,其中的值是弱持有的,也就是如果其中的项目是对这个对象最后一个引用的时候,GC可以移除它

第6章 新增API

6.1.Array

  • 各种JavaScript用户库扩展最多的特性之一就是数组(Array)类型
  • 1.静态函数Array.of(..):Array(..)构造器有一个众所周知的陷阱,就是如果只传入一个参数,并且这个参数是数字的话,那么不会构造一个值为这个数字的单个元素的数组,而是构造一个空数组,其length属性为这个数字
  • 2.静态函数Array.from(..):JavaScript中的“类(似)数组对象”是指一个有length属性,具体说是大于等于0的整数值的对象;普遍的需求就是把他们转为真正的数组,Array.prototype.slice.call(arrLike);新的ES6 Array.from(..)方法都是更好理解、更优雅、更简洁的替代方法;
  • 3.创建数组和子类型:of(..)和from(..)都使用访问它们的构造器来构造数组
  • 4.原型方法copyWidthin(..):Array.copyWithin(..)是一个新的修改器方法,所有数组都支持;它从一个数组中复制一部分到同一个数组的另一个位置,覆盖这个位置所有原来的值;copyWithin(..)方法不会增加数组的长度。到达数组结尾复制就会停止;
  • 5.原型方法fill(..):用指定值完全(或部分)填充已存在的数组;可选接收参数start和end,它们指定了数组要填充的子集位置;
  • 6.原型方法find(..):在数组中搜索一个值得最常用方法一直是indexOf(..)方法,这个方法返回找到值的索引,如果没有找到就返回-1;indexOf(..)需要严格匹配===;ES6的find(..)一旦回到返回true/真值,会返回实际的数组值;
  • 7.原型方法findIndex(..):如果需要严格匹配的索引值,那么使用indexOf(..),如果需要自定义匹配的索引值,那么使用findIndex(..)
  • 8.原型方法entries()、values()、keys():它提供了同样的迭代器方法entries()、values()和keys(),从这个意义上说,他是一个集合

6.2.Object

  • 1.静态函数Object.is(..):静态函数Object.is(..)执行比===比较更严格的值比较;Object.is(..)调用底层的SameValue算法;如果需要严格识别NaN或者-0,那么应该选择Object.is(..)
  • 2.静态函数Object.getOwnPropertySymbols(..):它直接从对象上取得所有的符号属性
  • 3.静态函数Object.setPrototypeOf(..):设置对象的[[Prototype]]用于行为委托
  • 4.静态函数Object.assign(..):很多JavaScript库/框架提供了用于把一个对象的属性复制/混合到另一个对象中德工具;ES6新增了Object.assign(..),对于每个源来说,它的可枚举和自己拥有的(也就是不是“继承来的”)键值,包括符号都会通过简单=赋值被复制;不可枚举的属性和非自有的属性都被排除在赋值过程之外;Object.create(..)是ES5工具,创建一个[[Prototype]]链接的空对象;

6.3.Math

  • 更优化地执行这些计算,或者执行比手动版本精度更高的计算

6.4.Number

  • 两个新增内容就是指向已有的全局函数的引用:Number.parseInt(..)和Number.parseFloat(..)
  • 1.静态属性:新增了一些辅助数字常量:Number.EPSOLON、Number.MAX_SAFE_INTEGER、Number.MIN_SAFE_INTEGER
  • 2.静态属性Number.isNaN(..):标准全局工具isNaN(..)自出现以来就是有缺陷的,它对非数字的东西都返回true,而不是只对真实的NaN值返回true,因为它把参数强制转换为数字类型(可能会错误地导致NaN)。ES6增加了一个修正工具Number.isNaN,可以按照期望工作
  • 3.静态属性Number.isFinite(..):标准的全局isFinite(..)会对参数进行强制类型转换,但是Number.isFinite(..)会略去这种强制行为
  • 4.整型相关静态函数:JavaScript的数字值永远都是浮点数(IEE-754);检查一个值得小数部分是否非0,x===Math.floor(x),ES6新增了Number.isInteger(..);JavaScript代码本身不会因为只使用整数而运行得更快,但是,只有在使用整数时,引擎才可以采用优化技术(比如asm.js);

6.5.字符串

  • 1.Unicode函数:String.fromCodePoint(..)、String.codePointAt(..)和String.normalize(..)新增这些函数是为了提高JavaScript字符串值uiUnicode的支持;字符串原型方法normalize(..)用于执行Unicode规范化,或者把字符用“合并符”连接起来或者把合并的字符解开;
  • 2.静态函数String.raw(..):String.raw(..)工具作为内置标签函数提供,与模板字符串字面值一起使用,用于获得不应用任何转义序列的原始字符串
  • 3.原型函数repeat(..):重复字符串
  • 4.字符串检查函数:又新增了3个用于搜索/检查的新方法:startsWidth、endsWidth和includes

第7章 元编程

写在前面

  • 元编程是指操作目标是程序本身的行为特性的编程。换句话说,它是程序的编程的编程
  • 元编程关注以下一点或几点:代码查看自身,代码修改自身,代码修改默认语言特性,以便影响其他代码
  • 元编程的目标是利用语言自身的内省能力是代码的其余部分更具描述性、表达性和灵活性

7.1.函数名称

  • 默认情况下,name属性不可写,但可配置,也就是说如果需要的话,可使用Object.defineProperty(..)来手动修改

7.2.元属性

  • 元属性以属性访问的形式提供特殊的其他方法无法获取的元信息
  • 对于所有的元编程技术都要小心,不要编写过于机灵的代码,让未来的你或者其他代码维护者难以理解

7.3.公开符号

  • 除了在自己的程序中定义符号之外,JavaScript预先定义了一些内置符号,称为公开符号
  • Symbol.iterator表示任意对象上的一个专门位置(属性),语言机制自动在这个位置上寻找一个方法,这个方法构造一个迭代器来消耗这个对象的值
  • 最常见的一个元编程任务,就是在一个值上进行内省来找出它是什么种类,这通常是为了确定其上适合执行何种运算。最常用的内省技术是toString()和instanceof
  • 符号@@species,这个符号控制要生成新实例时,类的内置方法使用哪一个构造器
  • 抽象类型转换运算ToPrimitive,它用在对象为了某个操作必须被强制转换为一个原生类型值得时候。ES6之前,没有办法控制这个行为
  • ES6中,在任意对象值上作为属性的符号@@toPrimitivesymbol都可以通过指定一个方法来定制这个ToPrimitive强制转换
  • 对于正则表达式对象,有4个公开符号可以被覆盖:@@match、@@search、@@split、@@replace
  • 符号@@isConcatSpreadable可以被定义为任意对象的布尔型属性(Symbol.isConcattSpreadable),用来指示如果把它传给一个数组的concat(..)是否应该将其展开
  • 符号@@unscopables可以被定义为任意对象的对象属性(Symbol.unscopables),用来指示使用with语句时哪些属性可以或部可以暴露为词法变量
  • strict模式下不允许with语句,因此应当被认为是语言的过时特性。不要使用它

7.4.代理

  • ES6种新增的最明显的元编程特性之一是Proxy(代理)特性
  • 代理是一种由你创建的特殊的对象,它“封装”另一个普通对象--或者说挡在这个普通对象的前面
  • 代理局限性:可以在对象上执行的很广泛的一组基本操作都可以通过这些元编程处理函数trap;代理处理函数总会有一些不变形(invariant),亦即不能被覆盖的行为;这些不变性限制了自定义代理行为的能力,但它们的目的只是为了防止你创建诡异或罕见(或者不一致)的行为;
  • 可取消代理:想要创建一个在你想要停止它作为代理时便可以被停用的代理。解决方案是创建可取消代理;可取消代理用Proxy.revocable(..)创建,这是一个普通函数,而不像Proxy(..)一样是构造器;一旦可取消代理被取消,任何对它的访问(触发它的任意trap)都会抛出TypeError;可取消代理的一个可能应用场景是,在你的应用中把代理分发到第三方,其中管理你的模型数据,而不是给出真实模型本身的引用;
  • 使用代理:代理成为了代码交互的主要对象,而实际目标对象保持隐藏/被保护的状态

7.5.Reflect API

  • Reflect对象是一个平凡对象(就像Math),不像其他内置原生值一样是函数/构造器
  • 它持有对应于各种可控的元编程任务的静态函数
  • 有一个区别是如果第一个参数(目标对象)不是对象的话,Object.*相应工具会试图把它类型转换为一个对象
  • Reflect的元编程能力提供了模拟各种语法特性的编程等价物,把之前隐藏的抽象操作暴露出来。可以利用这些能力扩展功能和API,以实现领域特定语言(DSL)
  • 属性排序:在ES6之前,一个对象键/属性的列出顺序是依赖于具体实现,并未在规范中定义;对于ES6来说,拥有属性的列出顺序是是由[[OwnPropertyKeys]]算法定义的;其顺序为(首先,按照数字上升排序,枚举所有整数索引拥有的属性;然后,按照创建顺序枚举其余的拥有的字符串属性名;最后,按照创建顺序枚举拥有的符号属性;)

7.6.特性测试

  • 特性测试,就是一种由你运行的用来判断一个特性是否可用的测试
  • JavaScript中最常用的特性测试是检查一个API是否存在,如果不存在的话,定义一个polyfill

7.7.尾递归调用

  • 通常,在一个函数内部调用另一个函数的时候,会分配第二个栈帧来独立管理第二个函数调用的变量/函数
  • 当考虑递归编程的时候(一个函数重复调用自身)-或者两个或多个函数彼此调用形成递归-调用栈的深度很容易达到成百上千,甚至更多
  • JavaScript引擎不得不设置一个武断的限制来防止这种编程技术引起浏览器和设备内存消耗尽而崩溃。“RangeError:Maximum call stack size exceeded”
  • 调用栈深度限制不由规范控制。它是依赖于具体实现的,并且根据浏览器和设备不同而有所不同
  • 尾调用优化(Tail Call Optimization, TCO) - 于是额外的栈帧分配是不需要的。引擎不需要对下一个函数调用创建一个新的栈帧,只需复用已有的栈帧

第8章 ES6之后

  • 在新特性还没有被需要支持的所有浏览器都实现的情况下,transpiler和polyfill是我们迁移到新特性的桥梁

8.1.异步函数

  • async function 本质上就是生成器+Promise+run(..)模式的语法糖;它们底层的运作方式是一样的
  • 警告:async function有一个没有解决的问题,因为它只返回一个promise,所以没有办法从外部取消一个正在运行的async function实例;Promise是不可取消的(至少在编写本部分的时候是如此);

8.2.Object.observe

  • 可能在后ES6,我们将会看到通过工具Object.observe(..)直接添加到语言中的支持。
  • 本质上说,这个思路就是你可以建立一个侦听者(listener)来观察对象的改变,然后在每次变化发生时调用一个回调
  • 可以观察的改变有6种类型:add、update、delete、reconfigure、setPrototype、preventExtensions
  • 1.自定义改变事件:除了前面6类内置改变事件,也可以侦听和发出自定义改变事件
  • 2.结束观测

8.3.幂运算符

  • 有提案提出为JavaScript新增一个运算符用于执行幂运算,就像Math.pow(..)一样

8.4.对象属性与...

  • ...运算符展开和收集数组的用法很直观,那么对于对象呢

8.5.Array#includes

  • 出现了一个获得了大量支持的提案,提出增加一个真正返回布尔值的数组搜索方法includes(..)
  • Array.includes(..)使用的匹配逻辑能够找到Nan值,但是无法区分-0和0

8.6.SIMD

  • SIMD API暴露了可以同时对多个数字值运算的各种底层(CPU)指令

8.7.WebAssembly(WASM)

  • 最近(以及不久的将来)JavaScript语言设计修改上的最大压力之一就是需要成为更适合从其他语言(比如C/C++、ClojureScript)变换/交叉编译的目标语言
  • ASM.js是合法JavaScript的一个子集,这个子集最严格地限制了那些使得JavaScript引擎难以优化的行为。结果就是兼容ASM.js的代码运行在支持ASM的引擎上时效率有巨大的提升,几乎与原生优化的等价C程序相当
  • WASM提出了一种代码的高度压缩AST(语法树)二进制表示格式,然后可以直接向JavaScript引擎发出指令,而它的基础结构,不需要通过JavaScript解析,甚至不需要符合JavaScript的规则

写在后面