![[assets/Pasted image 20230904150346.png|200]]
Javascript 一直在使用,但是还真就没有太系统地去认识过它,虽然多数应用场景并不需要,但是这件事儿绝对是有必要进行的。这段时间不很忙,系统地阅读这本经典之作,顺便做下摘录。
前言
作者马特
我很高兴在马特第一次到帕洛阿尔托的一家小型创业公司领导工程化时结识了他。那家公司叫 Claco,当时我刚成为它的顾问。他追求伟大软件的活力和激情溢于言表,而这家羽翼未丰的公司很快就开发出一款漂亮的产品。一如为硅谷公司设立标杆的惠普,这家创业公司也诞生在一间平房里。但这可不是寻常的民房,而是一间“黑客屋”,里面十几位才华横溢的软件工程师经常通宵达旦地工作。虽然过的不是什么高档次生活——他们坐的都是别人扔在大街上的那种沙发床和旧椅子——他们在这间房子里每天所写代码的数量和质量却引人瞩目。连续工作几小时后,大多数人会把精力投入到公司的另一个子项目上,然后又是几个小时的工作。 不太会写代码的人也常受启发,发现自己学习的渴望,然后仅仅几个星期后就变成了代码能手。
马特是促成这种开发效率的关键角色。他是“黑客屋”里经验最丰富的人,恰好也是思维最清晰、最专业的一个。拿到计算机工程学位并不能说明什么,只要在窗户或者白板上看到马特写的算法、性能计算以及代码,你就知道马特又在专注于他的下一个大项目。 随着我对他了解的加深,我们成为了好朋友。他的领悟能力,他对培训工作的热爱,以及几乎可以把所有东西转化成笑话的能力,都是我所欣赏的品质。
![[assets/Pasted image 20230905144013.png|200]] ![[assets/Pasted image 20230905143856.png|200]]
> Matt Frisbie - 马特·费里斯比
:: 唯有专注,方能成事!唯有兴趣,方能专注!如果没有兴趣,还能很好地专注,那么恭喜你,你是个天才!比如钱学森、于敏……
关于 JavaScript
关于 JavaScript,谷歌公司的一位技术经理曾经跟我分享过一个无法反驳的观点。他说 JavaScript 并不是一门真正有内聚力的编程语言,至少形式上不是。 ECMA-262 规范定义了 JavaScript,但 JavaScript 没有唯一正确的实现。 更重要的是,这门语言与其宿主关系密切。实际上宿主为 JavaScript 定义了与外界交互所需的全部 API: DOM、网络请求、系统硬件、存储、事件、文件、加密,还有数以百计的其他 API。各种浏览器及其 JavaScript 引擎都按照自己的理解实现了这些规范。 Chrome 有 Blink/V8,Firefox 有 Gecko/SpiderMonkey, Safari 有 WebKit/JavaScriptCore,微软有 Trident/EdgeHTML/Chakra。浏览器以合规的方式运行绝大多数 JavaScript,但 Web 上随处可见迎合各种浏览器偏好的页面。因此,对 JavaScript 更准确的定位应该是一组浏览器实现。
Web 纯化论者可能认为 JavaScript 本身并非网页不可或缺的部分,但他们必须承认,如果没有 JavaScript,那么现代 Web 势必发生严重倒退。毫不夸张地讲, JavaScript 才是真正不可或缺的。
:: 有时候感觉这些 “Web 纯化论者” 像极了所谓的“环保主义者”……
我们正迎来 JavaScript 的文艺复兴。
本书内容
本书第 4 版全面深入地介绍了 JavaScript 开发者必须掌握的前端开发技术,涉及 JavaScript 的基础特性和高级特性。
本书从 JavaScript 的起源开始,逐步讲解到今天的最新技术。书中详尽讨论了 JavaScript 的各个方面,重点介绍 ECMAScript 和 DOM 标准。
在此基础上,接下来的各章揭示了 JavaScript 的基本概念,包括类、期约、迭代器、代理,等等。另外,书中还深入探讨了客户端检测、事件、动画、表单、错误处理及 JSON。
本书最后介绍近几年来涌现的最新和最重要的规范,包括 Fetch API、模块、工作者线程、服务线程以及大量新 API。
:: 先通看一遍,再重点分项看喽~
本书包含如下这些章:
第 1 章,介绍 JavaScript 的起源:从哪里来,如何发展,以及现今的状况。这一章会谈到 JavaScript 与 ECMAScript 的关系、 DOM、 BOM,以及 Ecma 和 W3C 相关的标准。
第 2 章,了解 JavaScript 如何与 HTML 结合来创建动态网页,主要介绍在网页中嵌入 JavaScript 的不同方式,还有 JavaScript 的内容类型及其与 <script>
元素的关系。
第 3 章,介绍语言的基本概念,包括语法和流控制语句;解释 JavaScript 与其他类 C 语言在语法上的异同点。在讨论内置操作符时也会谈到强制类型转换。此外还将介绍所有的原始类型,包括 Symbol。
第 4 章,探索 JavaScript 松散类型下的变量处理。这一章将涉及原始类型与引用类型的不同,以及与变量有关的执行上下文。此外,这一章也会讨论 JavaScript 中的垃圾回收,涉及在变量超出作用域时如何回收内存。
第 5 章,讨论 JavaScript 所有内置的引用类型,如 Date、 Regexp、原始类型及其包装类型。每种引用类型既有理论上的讲解,也有相关浏览器实现的剖析。
第 6 章,继续讨论内置引用类型,包括 Object、 Array、 Map、 WeakMap、 Set 和 WeakSet 等。
第 7 章,介绍 ECMAScript 新版中引入的两个基本概念:迭代器和生成器,并分别讨论它们最基本的行为和在当前语言环境下的应用。
第 8 章,解释如何在 JavaScript 中使用类和面向对象编程。首先会深入 JavaScript 的 Object 类型,进而探讨原型式继承,接下来全面介绍 ES6 类及其与原型式继承的紧密关系。
第 9 章,介绍两个紧密相关的概念: Proxy(代理)和 Reflect(反射) API。代理和反射用于拦截和修改这门语言的基本操作。
第 10 章,探索 JavaScript 最强大的一个特性:函数表达式,主要涉及闭包、 this 对象、模块模式、创建私有对象成员、箭头函数、默认参数和扩展操作符。
第 11 章,介绍两个紧密相关的异步编程构造: Promise 类型和 async/await。这一章讨论 JavaScript 的异步编程范式,进而介绍期约( promise)与异步函数的关系。
第 12 章,介绍 BOM,即浏览器对象模型,跟与浏览器本身交互的 API 相关。所有 BOM 对象都会涉及,包括 window、 document、 location、 navigator 和 screen 等。
第 13 章,解释检测客户端机器及其能力的不同手段,包括能力检测和用户代理字符串检测。这一章讨论每种手段的优缺点,以及适用的场景。
第 14 章,介绍 DOM,即文档对象模型,主要是 DOM Level 1 定义的 API。这一章将简单讨论 XML 及其与 DOM 的关系,进而全面探索 DOM 以及如何利用它操作网页。
第 15 章,解释其他 DOM API,包括浏览器本身对 DOM 的扩展,主要涉及 Selectors API、 Element Traversal API 和 HTML5 扩展。
第 16 章,在之前两章的基础上,解释 DOM Level 2 和 Level 3 对 DOM 的扩展,包括新增的属性、方法和对象。这一章还会介绍 DOM4 的相关内容,比如 Mutation Observer。
第 17 章,解释事件在 JavaScript 中的本质,以及事件的起源及其在 DOM 中的运行方式。
第 18 章,围绕 <canvas>
标签讨论如何创建动态图形,包括 2D 和 3D 上下文(WebGL)等动画和游戏开发所需的基础。这一章还会讨论 WebGL1 和 WebGL2。
第 19 章,探索使用 JavaScript 增强表单交互及突破浏览器限制,主要讨论文本框、选择框等表单元素及数据验证和操作。
第 20 章,介绍各种 JavaScript API,包括 Atomics、 Encoding、 File、 Blob、 Notifications、 Streams、Timing、 Web Components 和 Web Cryptography。
第 21 章,讨论浏览器如何处理 JavaScript 代码中的错误及几种错误处理方式。这一章同时介绍了每种浏览器的调试工具和技术,包括简化调试过程的建议。
第 22 章,介绍通过 JavaScript 读取和操作 XML 数据的特性,解释了不同浏览器支持特性和对象的差异,提供了简化跨浏览器编码的建议。这一章也讨论了使用 XSLT 在客户端转换 XML 数据。
第 23 章,介绍作为 XML 替代的 JSON 数据格式,还讨论了浏览器原生解析和序列化 JSON,以及使用 JSON 时要注意的安全问题。
第 24 章,探讨浏览器请求数据和资源的常用方式,包括早期的 XMLHttpRequest 和现代的 Fetch API。
第 25 章,讨论应用程序离线时在客户端机器上存储数据的各种技术。先从 cookie 谈起,然后讨论 Web Storage 和 IndexedDB。
第 26 章,介绍模块模式在编码中的应用,进而讨论 ES6 模块之前的模块加载方式,包括 CommonJS、AMD 和 UMD。最后介绍新的 ES6 模块及其正确用法。
第 27 章,深入介绍专用工作者线程、共享工作者线程和服务工作者线程。其中包括工作者线程在操作系统和浏览器层面的实现,以及使用各种工作者线程的最佳策略。
第 28 章,探讨在企业级开发中进行 JavaScript 编码的最佳实践。其中提到了提升代码可维护性的编码惯例,包括编码技巧、格式化及通用编码建议。深入讨论应用性能和提升速度的技术。最后介绍与上线部署相关的话题,包括项目构建流程。
第 1 章什么是 JavaScript
第1章 什么是JavaScript
1.1 简短的历史回顾
1.2 JavaScript实现
1.3 JavaScript版本
1.4 小结
1995 年, JavaScript 问世。当时,它的主要用途是代替 Perl 等服务器端语言处理输入验证。如今, JavaScript 的应用也不再局限于数据验证,而是渗透到浏览器窗口及其内容的方方面面。
从简单的输入验证脚本到强大的编程语言, JavaScript 的崛起没有任何人预测到。它很简单,学会用只要几分钟;它又很复杂,掌握它要很多年。要真正学好用好 JavaScript,理解其本质、历史及局限性是非常重要的。
:: 本质,历史,局限性。
1.1 简短的历史回顾
1995 年,网景公司一位名叫 Brendan Eich 的工程师,开始为即将发布的 Netscape Navigator 2 开发一个叫 Mocha(后来改名为 LiveScript)的脚本语言。当时的计划是在客户端和服务器端都使用它,它在服务器端叫 LiveWire。
:: 好的嘛,原来一开始就有在服务器端使用的打算。后来被 Node (Deno) 实现了,也还行。
![[assets/Pasted image 20230905154059.png]]
> Brendan Eich 布兰登·艾克 - JavaScript 作者
为了赶上发布时间,与 Sun 联盟,蹭 Java 的热度,改名为 JavaScript 。1996 年 8 月,微软携 IE(JScript)重磅进入 Web 浏览器领域,挠局者!
1997 年, JavaScript 1.1 作为提案被提交给欧洲计算机制造商协会( Ecma)。他们花了数月时间打造出 ECMA-262,也就是 ECMAScript(发音为“ek-ma-script”)这个新的脚本语言标准。
1998 年,国际标准化组织( ISO)和国际电工委员会( IEC)也将 ECMAScript 采纳为标准( ISO/IEC-16262)。自此以后,各家浏览器均以 ECMAScript 作为自己 JavaScript 实现的依据,虽然具体实现各有不同。
1.2 JavaScript 实现
完整的 JavaScript 实现包含以下几个部分:
- 核心( ECMAScript)
- 文档对象模型( DOM)
- 浏览器对象模型( BOM)
![[assets/Pasted image 20230905155352.png]]
1.2.1 ECMAScript
ECMAScript 并不局限于 Web 浏览器。事实上,这门语言没有输入和输出之类的方法。
:: 没有系统级 IO,只能做脚本了……
Web 浏览器只是 ECMAScript 实现可能存在的一种宿主环境( host environment)。宿主环境提供 ECMAScript 的基准实现和与环境自身交互必需的扩展。 扩展(比如 DOM)使用 ECMAScript 核心类型和语法,提供特定于环境的额外功能。其他宿主环境还有服务器端 JavaScript 平台 Node. js 和即将被淘汰的 Adobe Flash。
:: 扩展提供特定于环境的额外功能!
ECMA-262 到底定义了什么?
- 语法
- 类型
- 语句
- 关键字
- 保留字
- 操作符
- 全局对象
ECMAScript 版本:
ES3 标志着 ECMAScript 作为一门真正的编程语言的时代终于到来了。
2009 年 12 月 3 日正式发布的 ES5 致力于厘清第 3 版存在的歧义,也增加了新功能。新功能包括原生的解析和序列化 JSON 数据的 JSON 对象、方便继承和高级属性定义的方法,以及新的增强 ECMAScript 引擎解释和执行代码能力的严格模式。
2015 年 6 月发布了 ES6 (又叫 ES2015)。这一版包含了大概这个规范有史以来最重要的一批增强特性。 ES6 正式支持了类、模块、迭代器、生成器、箭头函数、期约、反射、代理和众多新的数据类型。
:: 4 呢?步子迈太大,扯着蛋了…… 同时期的 3.1 变成了后来的 5 !
后续版本,除了 ES8 (ES2017) 主要增加了异步函数( async/await)……
:: 反正随后的几个版本变动不大,都是小修小补。
1.2.2 DOM
文档对象模型( DOM, Document Object Model)是一个应用编程接口( API),用于在 HTML 中使用扩展的 XML。 DOM 将整个页面抽象为一组分层节点。 HTML 或 XML 页面的每个组成部分都是一种节点,包含不同的数据。
DOM 通过创建表示文档的树,让开发者可以随心所欲地控制网页的内容和结构。使用 DOM API,可以轻松地删除、添加、替换、修改节点。
1. 为什么 DOM 是必需的?
为了保持 Web 跨平台的本性,人们担心如果无法控制网景和微软各行其是,那
么 Web 就会发生分裂,导致人们面向浏览器开发网页。就在这时,万维网联盟( W3C, World Wide Web Consortium)开始了制定 DOM 标准的进程。
2. DOM 级别
1998 年 10 月, DOM Level 1 成为 W3C 的推荐标准。这个规范由两个模块组成: DOM Core 和 DOM HTML。前者提供了一种映射 XML 文档,从而方便访问和操作文档任意部分的方式;后者扩展了前者,并增加了特定于 HTML 的对象和方法。
DOM 并非只能通过 JavaScript 访问,而且确实被其他很多语言实现了。
DOM Level 1 的目标是映射文档结构,而 DOM Level 2 的目标则宽泛得多。这个对最初 DOM 的扩展增加了对( DHTML 早就支持的)鼠标和用户界面事件、范围、遍历(迭代 DOM 节点的方法)的支持,而且通过对象接口支持了层叠样式表( CSS)。另外, DOM Level 1 中的 DOM Core 也被扩展以包含对 XML 命名空间的支持。
DOM Level 3 进一步扩展了 DOM,增加了以统一的方式加载和保存文档的方法(包含在一个叫 DOM Load and Save 的新模块中),还有验证文档的方法( DOM Validation)。在 Level 3 中, DOM Core 经过扩展支持了所有 XML 1.0 的特性,包括 XML Infoset、 XPath 和 XML Base。
目前, W3C 不再按照 Level 来维护 DOM 了,而是作为 DOM Living Standard 来维护,其快照称为 DOM4。 DOM4 新增的内容包括替代 Mutation Events 的 Mutation Observers。
:: W3C 还是比较靠谱的 !
3. 其他 DOM
除了 DOM Core 和 DOM HTML 接口,有些其他语言也发布了自己的 DOM 标准。下面列出的语言是基于 XML 的,每一种都增加了该语言独有的 DOM 方法和接口:
- 可伸缩矢量图( SVG, Scalable Vector Graphics)
- 数学标记语言( MathML, Mathematical Markup Language)
- 同步多媒体集成语言( SMIL, Synchronized Multimedia Integration Language)
1.2.3 BOM
浏览器对象模型( BOM) API,用于支持访问和操作浏览器的窗口。使用 BOM,开发者可以操控浏览器显示页面之外的部分。
总体来说, BOM 主要针对浏览器窗口和子窗口( frame),不过人们通常会把任何特定于浏览器的扩展都归在 BOM 的范畴内。比如,下面就是这样一些扩展:
- 弹出新浏览器窗口的能力;
- 移动、缩放和关闭浏览器窗口的能力;
- navigator 对象,提供关于浏览器的详尽信息;
- location 对象,提供浏览器加载页面的详尽信息;
- screen 对象,提供关于用户屏幕分辨率的详尽信息;
- performance 对象,提供浏览器内存占用、导航行为和时间统计的详尽信息;
- 对 cookie 的支持;
- 其他自定义对象,如 XMLHttpRequest 和 IE 的 ActiveXObject。
1.3 JavaScript 版本
……
1.4 小结
JavaScript 是一门用来与网页交互的脚本语言,包含以下三个组成部分。
- ECMAScript:由 ECMA-262 定义并提供核心功能。
- 文档对象模型( DOM):提供与网页内容交互的方法和接口。
- 浏览器对象模型( BOM):提供与浏览器交互的方法和接口。
JavaScript 的这三个部分得到了五大 Web 浏览器( IE、 Firefox、 Chrome、 Safari 和 Opera)不同程度的支持。所有浏览器基本上对 ES5( ECMAScript 5)提供了完善的支持,而对 ES6( ECMAScript 6)和 ES7( ECMAScript 7)的支持度也在不断提升。这些浏览器对 DOM 的支持各不相同,但对 Level 3 的支持日益趋于规范。 HTML5 中收录的 BOM 会因浏览器而异,不过开发者仍然可以假定存在很大一部分公共特性。
第 2 章 HTML 中的 JavaScript
第2章 HTML中的JavaScript
2.1 <script>元素
2.2 行内代码与外部文件
2.3 文档模式
2.4 <noscript>元素
2.5 小结
将 JavaScript 引入网页,首先要解决它与网页的主导语言 HTML 的关系问题。
2.1 <script>
元素
将 JavaScript 插入 HTML 的主要方法是使用 <script>
元素,它有下列 8 个属性:
- async:可选。表示应该立即开始下载脚本,但不能阻止其他页面动作,比如下载资源或等待其他脚本加载。只对外部脚本文件有效。
- charset:可选。使用 src 属性指定的代码字符集。这个属性很少使用,因为大多数浏览器不在乎它的值。
- crossorigin:可选。配置相关请求的 CORS(跨源资源共享)设置。默认不使用 CORS。
crossorigin="anonymous"
配置文件请求不必设置凭据标志。crossorigin="use-credentials"
设置凭据标志,意味着出站请求会包含凭据。 - defer:可选。表示脚本可以延迟到文档完全被解析和显示之后再执行。只对外部脚本文件有效。
- integrity:可选。允许比对接收到的资源和指定的加密签名以验证子资源完整性( SRI,Subresource Integrity)。如果接收到的资源的签名与这个属性指定的签名不匹配,则页面会报错,脚本不会执行。这个属性可以用于确保内容分发网络( CDN, Content Delivery Network)不会提供恶意内容。
- language:废弃。
- src:可选。表示包含要执行的代码的外部文件。
- type:可选。代替 language,表示代码块中脚本语言的内容类型(也称 MIME 类型)。如果这个值是 module,则代码会被当成 ES6 模块,而且只有这时候代码中才能出现 import 和 export 关键字。
:: 一定要注意第三方脚本的安全性 !
使用 <script>
的方式有两种:通过它直接在网页中嵌入 JavaScript 代码,以及通过它在网页中包含外部 JavaScript 文件。
按照惯例,外部 JavaScript 文件的扩展名是 .js。这不是必需的,因为浏览器不会检查所包含 JavaScript 文件的扩展名。 这就为使用服务器端脚本语言动态生成 JavaScript 代码,或者在浏览器中将 JavaScript 扩展语言(如 TypeScript,或 React 的 JSX)转译为 JavaScript 提供了可能性。不过要注意,服务器经常会根据文件扩展来确定响应的正确 MIME 类型。如果不打算使用. js 扩展名,一定要确保服务器能返回正确的 MIME 类型。
<script>
元素的一个最为强大、同时也备受争议的特性是,它可以包含来自外部域的 JavaScript
文件。浏览器在解析这个资源时,会向 src 属性指定的路径发送一个 GET 请求,以取得相应资源,假定是一个 JavaScript 文件。这个初始的请求不受浏览器同源策略限制,但返回并被执行的 JavaScript 则受限制。当然,这个请求仍然受父页面 HTTP/HTTPS 协议的限制。
:: ?TODO 如何理解 “返回并被执行的 JavaScript 则受限制” ?
来自外部域的代码会被当成加载它的页面的一部分来加载和解释。这个能力可以让我们通过不同的
域分发 JavaScript。
不管包含的是什么代码,浏览器都会按照 <script>
在页面中出现的顺序依次解释它们,前提是它们没有使用 defer 和 async 属性。第二个 <script>
元素的代码必须在第一个 <script>
元素的代码解释完毕才能开始解释,第三个则必须等第二个解释完,以此类推。
2.1.1 标签位置
过去,所有 <script>
元素都被放在页面的 <head>
标签内,目的是把外部的 CSS 和 JavaScript 文件都集中放到一起。不过,意味着必须把所有 JavaScript 代码都下载、解析和解释完成后,才能开始渲染页面(页面在浏览器解析到 <body>
的起始标签时开始渲染),在此期间浏览器窗口完全空白。为解决这个问题,现代 Web 应用程序通常将所有 JavaScript 引用放在 <body>
元素中的页面内容后面。这样一来,页面会在处理 JavaScript 代码之前完全渲染页面。
2.1.2 推迟执行脚本
HTML 4.01 为 <script>
元素定义了一个叫 defer 的属性。这个属性表示脚本在执行的时候不会改
变页面的结构。也就是说,脚本会被延迟到整个页面都解析完毕后再运行。因此,在 <script>
元素上设置 defer 属性,相当于告诉浏览器 立即下载,但延迟执行。
HTML5 规范要求脚本应该按照它们出现的顺序执行,因此第一个推迟的脚本会在第二个推迟的脚本之前执行,,而且两者都会在 DOMContentLoaded 事件之前执行。不过在实际当中,推迟执行的脚本不一定总会按顺序执行或者在 DOMContentLoaded 事件之前执行,因此最好只包含一个这样的脚本。
defer 属性只对外部脚本文件才有效,这是 HTML5 中明确规定的!
2.1.3 异步执行脚本
HTML5 为 <script>
元素定义了 async 属性。从改变脚本处理方式上看, async 属性与 defer 类似。当然,它们两者也都只适用于外部脚本,都会告诉浏览器立即开始下载。
不过,与 defer 不同的是,标记为 async 的脚本并不保证能按照它们出现的次序执行。第二个脚本可能先于第一个脚本执行。因此,重点在于它们之间没有依赖关系。
给脚本添加 async 属性的目的是告诉浏览器,不必等脚本下载和执行完后再加载页面,同样也不必等到该异步脚本下载和执行后再加载其他脚本。正因为如此,异步脚本不应该在加载期间修改 DOM。
异步脚本保证会在页面的 load 事件前执行,但可能会在 DOMContentLoaded 之前或之后。
2.1.4 动态加载脚本
除了 <script>
标签,还有其他方式可以加载脚本。因为 JavaScript 可以使用 DOM API,所以通过向 DOM 中动态添加 script 元素同样可以加载指定的脚本。只要创建一个 script 元素并将其添加到 DOM 即可。
|
|
当然,在把 HTMLElement 元素添加到 DOM 且执行到这段代码之前不会发送请求。默认情况下,以这种方式创建的 <script>
元素是以异步方式加载的,相当于添加了 async 属性。
以这种方式获取的资源对浏览器预加载器是不可见的。这会严重影响它们在资源获取队列中的优先
级,可能会严重影响性能。要想让预加载器知道这些动态请求文件的存在,可以在文档头部显式声明它们:
|
|
2.1.5 XHTML 中的变化
……
:: 已退出历史舞台,不用再了解。发现,在 Web 端太过严苛的东西从来都不怎么受欢迎。
2.2 行内代码与外部文件
虽然可以直接在 HTML 文件中嵌入 JavaScript 代码,但通常认为最佳实践是尽可能将 JavaScript 代码放在外部文件中。不仅更容易维护,浏览器还会根据特定的设置缓存所有外部链接的 JavaScript 文件,页面加载更快。
2.3 文档模式
IE5.5 发明了文档模式的概念,即可以使用 doctype 切换文档模式。最初的文档模式有两种: 混杂 模式( quirks mode)和标准模式( standards mode)。前者让 IE 像 IE5 一样(支持一些非标准的特性),后者让 IE 具有兼容标准的行为。虽然这两种模式的主要区别只体现在通过 CSS 渲染的内容方面,但对 JavaScript 也有一些关联影响,或称为副作用。
2.4 <noscript>
元素
针对早期浏览器不支持 JavaScript 的问题,需要一个页面优雅降级的处理方案。最终, <noscript>
元素出现,被用于给不支持 JavaScript 的浏览器提供替代内容。
2.5 小结
JavaScript 是通过 <script>
元素插入到 HTML 页面中的。这个元素可用于把 JavaScript 代码嵌入到 HTML 页面中,跟其他标记混合在一起,也可用于引入保存在外部文件中的 JavaScript。本章的重点可以总结如下。
- 要包含外部 JavaScript 文件,必须将 src 属性设置为要包含文件的 URL。文件可以跟网页在同 一台服务器上,也可以位于完全不同的域。
- 所有
<script>
元素会依照它们在网页中出现的次序被解释。在不使用 defer 和 async 属性的 情况下,包含在<script>
元素中的代码必须严格按次序解释。 - 对不推迟执行的脚本,浏览器必须解释完位于
<script>
元素中的代码,然后才能继续渲染页面 的剩余部分。为此,通常应该把<script>
元素放到页面末尾,介于主内容之后及</body>
标签 之前。 - 可以使用 defer 属性把脚本推迟到文档渲染完毕后再执行。推迟的脚本原则上按照它们被列出 的次序执行。
- 可以使用 async 属性表示脚本不需要等待其他脚本,同时也不阻塞文档渲染,即异步加载。异 步脚本不能保证按照它们在页面中出现的次序执行。
- 通过使用
<noscript>
元素,可以指定在浏览器不支持脚本时显示的内容。如果浏览器支持并启 用脚本,则<noscript>
元素中的任何内容都不会被渲染。
:: defer 是文档渲染优先,同 defer 原则上顺序执行;async 是不阻塞文档渲染,也不用等待其他脚本执行。
第 3 章语言基础
第3章 语言基础
3.1 语法
3.2 关键字与保留字
3.3 变量
3.4 数据类型
3.5 操作符
3.6 语句
3.7 函数
3.8 小结
任何语言的核心所描述的都是这门语言在最基本的层面上如何工作,涉及语法、操作符、数据类型 以及内置功能,在此基础之上才可以构建复杂的解决方案。如前所述, ECMA-262 以一个名为 ECMAScript 的伪语言的形式,定义了 JavaScript 的所有这些方面。
3.1 语法
3.1.1 区分大小写
ECMAScript 中一切都区分大小写。无论是变量、函数名还是操作符,都区分大小写。
3.1.2 标识符
所谓标识符,就是变量、函数、属性或函数参数的名称。
:: 看,没有属性值,它并不受影响。
标识符可以由一或多个下列字符组成:
- 第一个字符必须是一个字母、下划线(
_
)或美元符号($
); - 剩下的其他字符可以是字母、下划线、美元符号或数字;
- 按照惯例, ECMAScript 标识符使用驼峰大小写形式(一般小驼峰,非强制)。
3.1.3 注释
ECMAScript 采用 C 语言风格的注释,包括单行注释(//
)和块注释 (/* ……*/
)。
3.1.4 严格模式
ECMAScript 5 增加了严格模式( strict mode)的概念。严格模式是一种不同的 JavaScript 解析和执行模型, ECMAScript 3 的一些不规范写法在这种模式下会被处理,对于不安全的活动将抛出错误。
要对整个脚本启用严格模式,在脚本开头加上这一行:
"use strict";
虽然看起来像个没有赋值给任何变量的字符串,但它其实是一个预处理指令。
也可以单独指定一个函数在严格模式下执行,只要把这个预处理指令放到函数体开头即可:
|
|
3.1.5 语句
ECMAScript 中的语句以分号结尾。没有分号也有效,但不推荐!
:: 思考一下,为什么推荐加分号呢?不加有什么坏处?
多条语句可以合并到一个 C 语言风格的代码块中。代码块由一个左花括号( {
)标识开始,一个右花括号( }
)标识结束。
if 之类的控制语句只在执行多条语句时要求必须有代码块(推荐)。不过,最佳实践是始终在控制语句中使用代码块,即使要执行的只有一条语句。
3.2 关键字与保留字
ECMA-262 描述了一组保留的关键字,这些关键字有特殊用途,比如表示控制语句的开始和结束, 或者执行特定的操作。按照规定,保留的关键字不能用作标识符或属性名。
规范中也描述了一组未来的保留字,同样不能用作标识符或属性名。
一般来说,最好还是不要使用关键字和保留字作为标识符和属性名,以确保兼容过去和未来的 ECMAScript 版本。
:: 按他说的做,别给自己找麻烦 😅
3.3 变量
ECMAScript 变量是松散类型的,意思是变量可以用于保存任何类型的数据。每个变量只不过是一
个用于保存任意值的命名占位符。
有 3 个关键字可以声明变量: var、 const 和 let。其中, var 在 ECMAScript 的所有版本中都可以使用,而 const 和 let 只能在 ECMAScript 6 及更晚的版本中使用。
3.3.1 var 关键字
要定义变量,可以使用 var 操作符(注意 var 是一个关键字),后跟变量名(即标识符)。
var message; // 不初始化的情况下,变量会保存一个特殊值 undefined
1. var 声明作用域
关键的问题在于,使用 var 操作符定义的变量会成为包含它的函数的局部变量。比如,使用 var
在一个函数内部定义一个变量,就意味着该变量将在函数退出时被销毁:
|
|
:: 好的吧,以前没有意识到这一点……
这里, message
变量是在函数内部使用 var 定义的。函数叫 test()
,调用它会创建这个变量并给它赋值。调用之后变量随即被销毁,因此示例中的最后一行会导致错误。
不过,在函数内定义变量时省略 var 操作符,可以创建一个全局变量:
|
|
去掉之前的 var 操作符之后, message
就变成了全局变量。只要调用一次函数 test()
(同样,你要是不调用这个函数,访问 message
就会报错),就会定义这个变量,并且可以在函数外部访问到。
虽然可以通过省略 var 操作符定义全局变量,但不推荐这么做。在局部作用域中定义的全局变量很难维护,也会造成困惑。这是因为不能一下子断定省略 var 是不是有意而为之。在严格模式下,如果像这样给未声明的变量赋值,则会导致抛出 ReferenceError。
如果需要定义多个变量,可以在一条语句中用逗号分隔每个变量(及可选的初始化)。
在严格模式下,不能定义名为 eval 和 arguments 的变量,否则会导致语法错误。
2. var 声明提升
使用这个关键字声明的变量会自动提升到函数作用域顶部。这就是所谓的“提升”( hoist),也就是把所有变量声明都拉到函数作用域的顶部。此外,反复多次使用 var 声明同一个变量也没有问题(解释器会将冗余声明合并成一个)。
3.3.2 let 声明
let 跟 var 的作用差不多,但有着非常重要的区别。最明显的区别是, let 声明的范围是块作用域,而 var 声明的范围是函数作用域。
let 也不允许同一个块作用域中出现冗余声明。 当然, JavaScript 引擎会记录用于变量声明的标识符及其所在的块作用域,因此嵌套使用相同的标识符不会报错,而这是因为同一个块中没有重复声明。
对声明冗余报错不会因混用 let 和 var 而受影响。这两个关键字声明的并不是不同类型的变量,
它们只是指出变量在相关作用域如何存在。
var name;
let name; // SyntaxError
let age;
var age; // SyntaxError
1. 暂时性死区
let 与 var 的另一个重要的区别,就是 let 声明的变量不会在作用域中被提升。
在解析代码时, JavaScript 引擎也会注意出现在块后面的 let 声明,只不过在此之前不能以任何方 式来引用未声明的变量。在 let 声明之前的执行瞬间被称为“暂时性死区”( temporal dead zone),在此阶段引用任何后面才声明的变量都会抛出 ReferenceError。
2. 全局声明
与 var 关键字不同,使用 let 在全局作用域中声明的变量不会成为 window 对象的属性( var 声 明的变量则会)。
不过, let 声明仍然是在全局作用域中发生的,相应变量会在页面的生命周期内存续。因此,为了 避免 SyntaxError,必须确保页面不会重复声明同一个变量。
3. 条件声明
在使用 var 声明变量时,由于声明会被提升, JavaScript 引擎会自动将多余的声明在作用域顶部合并为一个声明。因为 let 的作用域是块,所以不可能检查前面是否已经使用 let 声明过同名变量,同时也就不可能在没有声明的情况下声明它。
<script>
var name = 'Nicholas';
let age = 26;
</script>
<script>
// 假设脚本不确定页面中是否已经声明了同名变量
// 那它可以假设还没有声明过
var name = 'Matt';
// 这里没问题,因为可以被作为一个提升声明来处理
// 不需要检查之前是否声明过同名变量
let age = 36;
// 如果 age 之前声明过,这里会报错
</script>
:: 这段有点晦涩,可以理解为 var 允许同一作用域内重复声明,所有可以尽情认为其还没有声明过;而 let 在同一作用域内不允许重复声明,就不能这样‘肆无忌惮’。
使用 try/catch 语句或 typeof 操作符也不能解决,因为条件块中 let 声明的作用域仅限于该块。为此,对于 let 这个新的 ES6 声明关键字,不能依赖条件声明模式。
不能使用 let 进行条件式声明是件好事,因为条件声明是一种反模式,它让程序变得更难理解。如果你发现自己在使用这个模式,那一定有更好的替代方式。
4. for 循环中的 let 声明
在 let 出现之前, for 循环定义的迭代变量会渗透到循环体外部:
|
|
改成使用 let 之后,这个问题就消失了,因为迭代变量的作用域仅限于 for 循环块内部:
|
|
在使用 var 的时候,最常见的问题就是对迭代变量的奇特声明和修改:
|
|
之所以会这样,是因为在退出循环时,迭代变量保存的是导致循环退出的值: 5。在之后执行超时 逻辑时,所有的 i 都是同一个变量,因而输出的都是同一个最终值。
而在使用 let 声明迭代变量时, JavaScript 引擎在后台会为每个迭代循环声明一个新的迭代变量。
每个 setTimeout
引用的都是不同的变量实例,所以 console.log
输出的是我们期望的值,也就是循环执行过程中每个迭代变量的值。
:: 看,JavaScript 引擎在做事!为什么 JS 引擎会为每个迭代循环声明一个新的迭代变量呢?它不得不这么做,因为 let 不允许重复声明变量。
|
|
这种每次迭代声明一个独立变量实例的行为适用于所有风格的 for 循环,包括 for-in
和 for-of
循环。
3.3.3 const 声明
const 的行为与 let 基本相同,唯一一个重要的区别是用它声明变量时必须同时初始化变量,且 尝试修改 const 声明的变量会导致运行时错误。
const 声明的限制只适用于它指向的变量的引用。换句话说,如果 const 变量引用的是一个对象, 那么修改这个对象内部的属性并不违反 const 的限制。
:: 这是因为变量中存放的是指向对象存放的‘真实存储位置’的一个内存地址,不能变的也只是这个内存地址,而不是对象的内容。
JavaScript 引擎会为 for 循环中的 let 声明分别创建独立的变量实例,虽然 const 变量跟 let 变 量很相似,但是不能用 const 来声明迭代变量(因为迭代变量会自增)。
不过,如果你只想用 const 声明一个不会被修改的 for 循环变量,那也是可以的。也就是说,每
次迭代只是创建一个新变量。这对 for-of
和 for-in
循环特别有意义:
|
|
3.3.4 声明风格及最佳实践
ECMAScript 6 增加 let 和 const 从客观上为这门语言更精确地声明作用域和语义提供了更好的支 持。行为怪异的 var 所造成的各种问题,已经让 JavaScript 社区为之苦恼了很多年。😅
- 不使用 var;
- const 优先, let 次之。
使用 const 声明可以让浏览器运行时强制保持变量不变,也可以让静态代码分析工具提前发现不合法的赋值操作。因此,很多开发者认为应该优先使用 const 来声明变量,只在提前知道未来会有修改时,再使用 let。这样可以让开发者更有信心地推断某些变量的值永远不会变,同时也能迅速发现因意外赋值导致的非预期行为。
3.4 数据类型
ECMAScript 有 6 种简单数据类型(也称为原始类型): Undefined、 Null、 Boolean、 Number、String 和 Symbol(ES6 新增)。还有一种复杂数据类型叫 Object(对象),Object 是一种无序名值对的集合。
因为在 ECMAScript 中不能定义自己的数据类型,所有值都可以用上述 7 种数据类型之一来表示。
3.4.1 typeof 操作符
:: 是的,
typeof
是一个操作符!
因为 ECMAScript 的类型系统是松散的,所以需要一种手段来确定任意变量的数据类型。 typeof
操作符就是为此而生的。对一个值使用 typeof 操作符会返回下列字符串之一:
- “undefined” 表示值未定义;
- “boolean” 表示值为布尔值;
- “string” 表示值为字符串;
- “number” 表示值为数值;
- “object” 表示值为对象(而不是函数)或 null;
- “function” 表示值为函数;
- “symbol” 表示值为符号。
严格来讲,函数在 ECMAScript 中被认为是对象,并不代表一种数据类型。可是,函数也有自己特殊的属性。为此,就有必要通过 typeof 操作符来区分函数和其他对象。
3.4.2 Undefined 类型
Undefined 类型只有一个值,就是特殊值 undefined
。当使用 var 或 let 声明了变量但没有初始
化时,就相当于给变量赋予了 undefined
值。
一般来说,永远不用显式地给某个变量设置 undefined 值。字面值 undefined 主要用于比较,而且在 ECMA-262 第 3 版之前是不存在的。增加这个特殊值的目的就是 为了正式明确空对象指针( null)和未初始化变量的区别。
注意,包含 undefined 值的变量跟未定义变量是有区别的。对未声明的变量,只能执行一个有用的操作,就是对它调用 typeof。
需要注意的是,无论是声明还是未声明, typeof 返回的都是字符串 “undefined”。为什么呢?逻辑上讲这是对的,因为虽然严格来讲这两个变量存在根本性差异, 但它们都无法执行实际操作。
:: 看,这就是 JS 的奇葩之处了……
3.4.3 Null 类型
Null 类型同样只有一个值,即特殊值 null。逻辑上讲, null 值表示一个空对象指针,这也是给
typeof 传一个 null 会返回"object"的原因。
在定义将来要保存对象值的变量时,建议使用 null 来初始化,不要使用其他值。这样,只要检查
这个变量的值是不是 null 就可以知道这个变量是否在后来被重新赋予了一个对象的引用。
undefined 值是由 null 值派生而来的,因此 ECMA-262 将它们定义为表面上相等。
|
|
但要注意,即使 null 和 undefined 有关系,它们的用途也是完全不一样的。
如前所述,永远不必显式地将变量值设置为 undefined。但 null 不是这样的。任何时候,只要变量要保存对象,而当时又没有那个对象可保存,就要用 null 来填充该变量。这样就可以保持 null 空对象指针的语义,并进一步将其与 undefined 区分开来。
3.4.4 Boolean 类型
Boolean(布尔值)类型是 ECMAScript 中使用最频繁的类型之一,有两个字面值: true 和 false。
注意,布尔值字面量 true 和 false 是区分大小写的,因此 True 和 False(及其他大小混写形式)是有效的标识符,但不是布尔值。
虽然布尔值只有两个,但所有其他 ECMAScript 类型的值都有相应布尔值的等价形式。要将一个其他类型的值转换为布尔值,可以调用特定的 Boolean()
转型函数。像 if 等流控制语句会自动执行其他类型值到布尔值的转换。
除了 '' (空字符串)、0、NaN(Not a Number)、null
和 undefined
会转成 false
,其余都是 true
。
3.4.5 Number 类型
ECMAScript 中最有意思的数据类型或许就是 Number 了。
Number 类型使用 IEEE 754 格式表示整数和浮点值(在某些语言中也叫双精度值)。不同的数值类型相应地也有不同的数值字面量格式。
最基本的数值字面量格式是十进制整数,直接写出来即可。
整数也可以用八进制(以 8 为基数)或十六进制(以 16 为基数)字面量表示。
对于八进制字面量,第一个数字必须是零( 0),然后是相应的八进制数字(数值 0~7)。如果字面量中包含的数字超出了应有的范围,就会忽略前缀的零,后面的数字序列会被当成十进制数,如下所示:
|
|
八进制字面量在严格模式下是无效的,会导致 JavaScript 引擎抛出语法错误。
要创建十六进制字面量,必须让真正的数值前缀 0x(区分大小写),然后是十六进制数字( 0~9 以及 A~F)。十六进制数字中的字母大小写均可。下面是几个例子:
|
|
:: 前缀是区分大小写的哦!这一点与其他语言不同,比如 C 。
使用八进制和十六进制格式创建的数值在所有数学操作中都被视为十进制数值。
:: 在数学操作的时候会被自动转换为十进制!
1. 浮点值
要定义浮点值,数值中必须包含小数点,而且小数点后面必须至少有一个数字。虽然小数点前面不是必须有整数,但推荐加上。
因为存储浮点值使用的内存空间是存储整数值的两倍,所以 ECMAScript 总是想方设法把值转换为整数。在小数点后面没有数字的情况下,数值就会变成整数。如下例所示:
|
|
对于非常大或非常小的数值,浮点值可以用科学记数法来表示。科学记数法用于表示一个应该乘以 10 的给定次幂的数值。 ECMAScript 中科学记数法的格式要求是一个数值(整数或浮点数)后跟一个大写或小写的字母 e,再加上一个要乘的 10 的多少次幂。比如:
|
|
在这个例子中, floatNum1 等于 31 250 000,只不过科学记数法显得更简洁。
浮点值的精确度最高可达 17 位小数,但在算术计算中远不如整数精确。例如, 0.1 加 0.2 得到的不是 0.3,而是 0.300 000 000 000 000 04。由于这种微小的舍入错误,导致很难测试特定的浮点值。比如下面的例子:
|
|
因此永远不要测试某个特定的浮点值。
注意:之所以存在这种舍入错误,是因为使用了 IEEE 754 数值,这种错误并非 ECMAScript 所独有。其他使用相同格式的语言也有这个问题。
2. 值的范围
由于内存的限制, ECMAScript 并不支持表示这个世界上的所有数值。 ECMAScript 可以表示的最小数值保存在 Number.MIN_VALUE
中,这个值在多数浏览器中是 5e-324
;可以表示的最大数值保存在 Number.MAX_VALUE
中,这个值在多数浏览器中是 1.797 693 134 862 315 7e+308
。
如果某个计算得到的数值结果超出了 JavaScript 可以表示的范围,那么这个数值会被自动转换为一个特殊的 Infinity(无穷)值。任何无法表示的负数以 -Infinity(负无穷大)表示,任何无法表示的正数以 Infinity(正无穷大)表示。
注意:使用
Number.NEGATIVE_INFINITY
和Number.POSITIVE_INFINITY
也可以获取正、负 Infinity。没错,这两个属性包含的值分别就是 -Infinity 和 Infinity。
如果计算返回正 Infinity 或负 Infinity,则该值将不能再进一步用于任何计算。这是因为 Infinity 没有可用于计算的数值表示形式。
那么,如何确定一个值是不是有限大(即介于 JavaScript 能表示的最小值和最大值之间)呢?可以使用 isFinite()
函数,有限的则返回 true
。
3. NaN
有一个特殊的数值叫 NaN,意思是“不是数值”( Not a Number),用于表示本来要返回数值的操作失败了(而不是抛出错误)。
比如,用 0 除任意数值在其他语言中通常都会导致错误,从而中止代码执行。但在 ECMAScript 中, 0、 +0 或 -0 相除会返回 NaN:
|
|
如果分子是非 0 值,分母是有符号 0 或无符号 0,则会返回 Infinity 或 -Infinity。
NaN 有几个独特的属性:
- 首先,任何涉及 NaN 的操作始终返回 NaN(如 NaN/10);
- 其次, NaN 不等于包括 NaN 在内的任何值。
ECMAScript 提供了 isNaN()
函数。该函数接收一个参数,可以是任意数据类型,然后判断这个参数是否“不是数值”。
把一个值传给 isNaN()
后,该函数会尝试把它转换为数值。某些非数值的值可以直接转换成数值,如字符串 “10” 或布尔值。任何不能转换为数值的值都会导致这个函数返回 true。举例如下:
|
|
4. 数值转换
有 3 个函数可以将非数值转换为数值: Number()
、 parseInt()
和 parseFloat()
。
Number()
是转型函数,可用于任何数据类型。后两个函数主要用于将字符串转换为数值。对于同样的参数,这 3 个函数执行的操作也不同。
Number()
函数基于如下规则执行转换:
- 布尔值, true 转换为 1, false 转换为 0;
- 数值,直接返回;
- null,返回 0;
- undefined,返回 NaN;
- 字符串,应用以下规则:
- 如果字符串包含数值字符,包括数值字符前面带加、减号的情况,则转换为一个十进制数值;
- 如果字符串包含有效的浮点值格式如"1.1",则会转换为相应的浮点值(同样,忽略前面的零);
- 如果字符串包含有效的十六进制格式如"0xf",则会转换为与该十六进制值对应的十进制整数值;
- 如果是空字符串(不包含字符),则返回 0;
- 如果字符串包含除上述情况之外的其他字符,则返回 NaN;
- 对象,调用
valueOf()
方法,并按照上述规则转换返回的值。如果转换结果是 NaN,则调用toString()
方法,再按照转换字符串的规则转换。
从不同数据类型到数值的转换有时候会比较复杂,看一看 Number()
的转换规则就知道了。
|
|
考虑到用 Number()
函数转换字符串时相对复杂且有点反常规,通常在需要得到整数时可以优先使用 parseInt()
函数。
parseInt()
函数更专注于字符串是否包含数值模式。字符串最前面的空格会被忽略,从第一个非空格字符开始转换。如果第一个字符不是数值字符、加号或减号, parseInt()
立即返回 NaN。这意味着空字符串也会返回 NaN(这一点跟 Number()
不一样,它返回 0)。如果第一个字符是数值字符、加号或减号,则继续依次检测每个字符,直到字符串末尾,或碰到非数值字符。
假设字符串中的第一个字符是数值字符, parseInt()
函数也能识别不同的整数格式(十进制、八进制、十六进制)。不同的数值格式很容易混淆,因此 parseInt()
也接收第二个参数,用于指定底数(进制数)。
parseFloat()
函数的工作方式跟 parseInt()
函数类似,都是从位置 0 开始检测每个字符。同样,它也是解析到字符串末尾或者解析到一个无效的浮点数值字符为止。这意味着第一次出现的小数点是有效的,但第二次出现的小数点就无效了,此时字符串的剩余字符都会被忽略。
parseFloat()
函数的另一个不同之处在于,它始终忽略字符串开头的零。parseFloat()
只解析十进制值,因此不能指定底数。最后,如果字符串表示整数(没有小数点或者小数点后面只有一个零),则 parseFloat()
返回整数。
3.4.6 String 类型
String(字符串)数据类型表示零或多个 16 位 Unicode 字符序列。字符串可以使用双引号(")、单引号(’)或反引号( `)标示。
跟某些语言(比如 C 、Shell)中使用不同的引号会改变对字符串的解释方式不同, ECMAScript 语法中表示字符串的引号没有区别。
1. 字符字面量
字符串数据类型包含一些字符字面量,用于表示非打印字符或有其他用途的字符。如:\n
表示换行,\r
表示回车等。
这些字符字面量可以出现在字符串中的任意位置,且可以作为单个字符被解释。字符串的长度可以通过其 length 属性获取。
注意:如果字符串中包含双字节字符,那么 length 属性返回的值可能不是准确的字符数。
2. 字符串的特点
ECMAScript 中的字符串是 不可变的( immutable),意思是一旦创建,它们的值就不能变了。要修改某个变量中的字符串值,必须先销毁原始的字符串,然后将包含新值的另一个字符串保存到该变量。
3. 转换为字符串
有两种方式把一个值转换为字符串。
首先,是使用几乎所有值都有的 toString()
方法。这个方法唯一的用途就是返回当前值的字符串等价物。toString()
方法可见于数值、布尔值、对象和字符串值。
但是 null
和 undefined
值没有 toString()
方法,它们怎么办呢?
可以使用 String()
转型函数,它始终会返回表示相应类型值的字符串。 String()
函数遵循如下规则:
- 如果值有
toString()
方法,则调用该方法(不传参数)并返回结果; - 如果值是
null
,返回"null" ; - 如果值是
undefined
,返回"undefined"。
当然,用加号操作符给一个值加上一个空字符串 ""
也可以将其转换为字符串。
4. 模板字面量
ECMAScript 6 新增了使用模板字面量定义字符串的能力。与使用单引号或双引号不同,模板字面量保留换行字符,可以跨行定义字符串。
由于模板字面量会保持反引号内部的空格,因此在使用时要格外注意。
5. 字符串插值
模板字面量最常用的一个特性是支持字符串插值,也就是可以在一个连续定义中插入一个或多个值。⭐ 技术上讲,模板字面量不是字符串,而是一种特殊的 JavaScript 句法表达式,只不过求值后得到的是字符串。 模板字面量在定义时立即求值并转换为字符串实例,任何插入的变量也会从它们最接近的作用域中取值。
字符串插值通过在 ${}
中使用一个 JavaScript 表达式实现。
所有插入的值都会使用 toString()
强制转型为字符串,而且任何 JavaScript 表达式都可以用于插值。嵌套的模板字符串无须转义:
|
|
6. 模板字面量标签函数
TODO…
7. 原始字符串
使用模板字面量也可以直接获取原始的模板字面量内容(如换行符或 Unicode 字符),而不是被转换后的字符表示。为此,可以使用默认的 String. raw 标签函数:
|
|
3.4.7 Symbol 类型
Symbol(符号)是 ECMAScript 6 新增的数据类型。符号是原始值,且符号实例是唯一、不可变的。符号的用途是确保对象属性使用唯一标识符,不会发生属性冲突的危险。
符号就是用来创建唯一记号,进而用作非字符串形式的对象属性。
1. 符号的基本用法
符号需要使用 Symbol()
函数初始化。因为符号本身是原始类型,所以 typeof 操作符对符号返回
symbol 。
调用 Symbol ()函数时,也可以传入一个字符串参数作为对符号的描述( description),将来可以通过这个字符串来调试代码。但是,这个字符串参数与符号定义或标识 完全无关。
|
|
符号没有字面量语法,这也是它们发挥作用的关键。按照规范,你只要创建 Symbol()
实例并将其
用作对象的新属性,就可以保证它不会覆盖已有的对象属性,无论是符号属性还是字符串属性。
最重要的是, Symbol()
函数不能与 new
关键字一起作为构造函数使用。这样做是为了避免创建符
号包装对象。如果你确实想使用符号包装对象,可以借用 Object()
函数。
|
|
2. 使用全局符号注册表
如果运行时的不同部分需要共享和重用符号实例,那么可以用一个字符串作为键,在全局符号注册
表中创建并重用符号。为此,需要使用 Symbol.for()
方法。
Symbol.for()
对每个字符串键都执行幂等操作。第一次使用某个字符串调用时,它会检查全局运行时注册表,发现不存在对应的符号,于是就会生成一个新符号实例并添加到注册表中。后续使用相同字符串的调用同样会检查注册表,发现存在与该字符串对应的符号,然后就会返回该符号实例。
|
|
全局注册表中的符号必须使用字符串键来创建,因此作为参数传给 Symbol.for ()的任何值都会被转换为字符串。此外,注册表中使用的键同时也会被用作符号描述。
|
|
还可以使用 Symbol.keyFor()
来查询全局注册表,这个方法接收符号,返回该全局符号对应的字
符串键。如果查询的不是全局符号,则返回 undefined。
|
|
如果传给 Symbol.keyFor()
的不是符号,则该方法抛出 TypeError。
TODO…
3.4.8 Object 类型
ECMAScript 中的对象其实就是一组数据和功能的集合。对象通过 new 操作符后跟对象类型的名称来创建。
Object 的实例本身并不是很有用,但理解与它相关的概念非常重要。类似 Java 中的 java.lang.Object
, ECMAScript 中的 Object 也是派生其他对象的基类。Object 类型的所有属性和方法在派生的对象上同样存在。
每个 Object 实例都有如下属性和方法:
constructor
:用于创建当前对象的函数;hasOwnProperty (propertyName)
:用于判断当前对象实例(不是原型)上是否存在给定的属性。要检查的属性名必须是字符串(如 o.hasOwnProperty (“name”))或符号;isPrototypeOf (object)
:用于判断当前对象是否为另一个对象的原型;propertyIsEnumerable (propertyName)
:用于判断给定的属性是否可以使用 for-in 语句枚举。与hasOwnProperty()
一样,属性名必须是字符串;toLocaleString()
:返回对象的字符串表示,该字符串反映对象所在的本地化执行环境;toString()
:返回对象的字符串表示;valueOf()
:返回对象对应的字符串、数值或布尔值表示。通常与 toString ()的返回值相同
3.5 操作符
ECMA-262 描述了一组可用于操作数据值的操作符,包括数学操作符(如加、减)、位操作符、关系操作符和相等操作符等。 ECMAScript 中的操作符是独特的,因为它们可用于各种值,包括字符串、数值、布尔值,甚至还有对象。在应用给对象时,操作符通常会调用 valueOf()
和/或 toString()
方法来取得可以计算的值。
3.5.1 一元操作符
只操作一个值的操作符叫一元操作符( unary operator)。
1. 递增、递减操作符
递增和递减操作符直接照搬自 C 语言,但有两个版本:前缀版和后缀版。
无论使用前缀递增还是前缀递减操作符,变量的值都会在语句被求值之前改变。(在计算机科学中,这通常被称为具有副作用。)
后缀版与前缀版的主要区别在于,后缀版递增和递减在语句被求值后才发生。
把递增操作符放到变量后面不会改变语句执行的结果,因为递增是唯一的操作。可是,在跟其他操作混合时,差异就会变明显,比如:
|
|
这 4 个操作符可以作用于任何值,意思是不限于整数——字符串、布尔值、浮点值,甚至对象都可以。
2. 一元加和减
一元加和减操作符对大多数开发者来说并不陌生,它们在 ECMAScript 中跟在高中数学中的用途一样。
一元加由一个加号( +)表示,放在变量前头,对数值没有任何影响。如果将一元加应用到非数值,则会执行与使用 Number()
转型函数一样的类型转换。
3.5.2 位操作符
位操作符用于数值的底层操作,也就是操作内存中表示数据的比特(位)。
ECMAScript 中的所有数值都以 IEEE 754 64 位格式存储,但位操作并不直接应用到 64 位表示,而是先把值转换为 32 位整数,再进行位操作,之后再把结果转换为 64 位。对开发者而言,就好像只有 32 位整数一样,因为 64 位整数存储格式是不可见的。既然知道了这些,就只需要考虑 32 位整数即可。
有符号整数使用 32 位的前 31 位表示整数值。第 32 位表示数值的符号,如 0 表示正, 1 表示负。这一位称为符号位( sign bit),它的值决定了数值其余部分的格式。
正值以真正的二进制格式存储,即 31 位中的每一位都代表 2 的幂。第一位(称为第 0 位)表示 2^0,第二位表示 2^1,依此类推。如果一个位是空的,则以 0 填充, 相当于忽略不计。
负值以一种称为二补数(或补码)的二进制编码存储。如何获得一个数值的补码呢?3 步:
- 确定绝对值的二进制表示;
- 找到数值的一补数(或反码),换句话说,就是每个 0 都变成 1,每个 1 都变成 0;
- 给结果加 1。
基于上述步骤确定 -18 的二进制表示:
// 绝对值 18 的二进制表示
0000 0000 0000 0000 0000 0000 0001 0010
// 然后,计算一补数(反码)
1111 1111 1111 1111 1111 1111 1110 1101
// 最后,给一补数加 1
1111 1111 1111 1111 1111 1111 1110 1110 // 这就是 -18 的二进制表示
要注意的是,在处理有符号整数时,我们无法访问第 31 位。❓ :: 此处应该是 32 位吧?
ECMAScript 会帮我们记录这些信息。在把负值输出为一个二进制字符串时,我们会得到一个前面加了减号的绝对值,如下所示:
|
|
在将 -18 转换为二进制字符串时,结果得到 -10010。转换过程会求得二补数,然后再以更符合逻辑的形式表示出来。
默认情况下, ECMAScript 中的所有整数都表示为有符号数。不过,确实存在无符号整数。对无符号整数来说,第 32 位不表示符号,因为只有正值。无符号整数比有符号整数的范围更大,因为符号位被用来表示数值了。
在对 ECMAScript 中的数值应用位操作符时,后台会发生转换: 64 位数值会转换为 32 位数值,然后执行位操作,最后再把结果从 32 位转换为 64 位存储起来。整个过程就像处理 32 位数值一样,这让二进制操作变得与其他语言中类似。但这个转换也导致了一个奇特的副作用, 即特殊值 NaN 和 Infinity 在位操作中都会被当成 0 处理。
如果将位操作符应用到非数值,那么首先会使用 Number()
函数将该值转换为数值(这个过程是自动的),然后再应用位操作。最终结果是数值。
1. 按位非
按位非操作符用波浪符( ~
)表示,它的作用是返回数值的一补数(反码)。
|
|
:: 思考一下,为什么是 -26 呢?
2. 按位与
按位与操作符用和号( &
)表示,有两个操作数。本质上,按位与就是将两个数的每一个位对齐,然后基于真值表中的规则,对每一位执行相应的与操作。
按位与操作在两个位都是 1 时返回 1,在任何一位是 0 时返回 0。
3. 按位或
按位或操作符用管道符( |
)表示,同样有两个操作数。按位或操作在至少一位是 1 时返回 1,两位都是 0 时返回 0。
4. 按位异或
按位异或用脱字符( ^
)表示,同样有两个操作数。按位异或与按位或的区别是,它只在一位上是 1 的时候返回 1(两位都是 1 或 0,则返回 0)。
5. 左移
左移操作符用两个小于号( <<
)表示,会按照指定的位数将数值的所有位向左移动。左移后数值右端的空位用 0 填充。
注意,左移会保留它所操作数值的符号。
6. 有符号右移
有符号右移由两个大于号( >>
)表示,会将数值的所有 32 位都向右移,同时保留符号(正或负)。有符号右移实际上是左移的逆运算。
同样,移位后就会出现空位。不过,右移后空位会出现在左侧,且在符号位之后(见图 3-3)。ECMAScript 会用符号位的值来填充这些空位,以得到完整的数值。
7. 无符号右移
无符号右移用 3 个大于号表示( >>>
),会将数值的所有 32 位都向右移。
对于正数,无符号右移与有符号右移结果相同。对于负数,有时候差异会非常大。与有符号右移不同,无符号右移会给空位补 0,而不管符号位是什么。
3.5.3 布尔操作符
对于编程语言来说,布尔操作符跟相等操作符几乎同样重要。如果没有能力测试两个值的关系,那么像 if-else 和循环这样的语句也没什么用了。布尔操作符一共有 3 个:逻辑非、逻辑与和逻辑或。
1. 逻辑非
逻辑非操作符由一个叹号( !
)表示,可应用给 ECMAScript 中的任何值。这个操作符始终返回布尔值,无论应用到的是什么数据类型。逻辑非操作符首先将操作数转换为布尔值,然后再对其取反。
逻辑非操作符也可以用于把任意值转换为布尔值。同时使用两个叹号( !!
),相当于调用了转型函数 Boolean()
。
2. 逻辑与
逻辑与操作符由两个和号( &&
)表示,应用到两个值。
逻辑与操作符可用于任何类型的操作数,不限于布尔值。如果有操作数不是布尔值,则逻辑与并不一定会返回布尔值。
逻辑与操作符是一种短路操作符,意思就是如果第一个操作数决定了结果,那么永远不会对第二个操作数求值。
3. 逻辑或
逻辑或操作符由两个管道符( ||
)表示。与逻辑与类似,如果有一个操作数不是布尔值,那么逻辑或操作符也不一定返回布尔值。
同样与逻辑与类似,逻辑或操作符也具有短路的特性。只不过对逻辑或而言,第一个操作数求值为 true,第二个操作数就不会再被求值了。利用这个行为,可以避免给变量赋值 null 或 undefined。
3.5.4 乘性操作符
ECMAScript 定义了 3 个乘性操作符:乘法、除法和取模。这些操作符跟它们在 Java、 C 语言及 Perl 中对应的操作符作用一样,但在处理非数值时,它们也会包含一些自动的类型转换。如果乘性操作符有不是数值的操作数,则该操作数会在后台被使用 Number()
转型函数转换为数值。这意味着空字符串会被当成 0,而布尔值 true 会被当成 1。
3.5.5 指数操作符
3.5.6 加性操作符
3.5.7 关系操作符
3.5.8 相等操作符
3.5.9 条件操作符
3.5.10 赋值操作符
3.6 语句
3.7 函数
3.8 小结
第 8 章对象、类与面向对象编程
第 8 章 对象、类与面向对象编程
8.1 理解对象
8.2 创建对象
8.3 继承
8.4 类
8.5 小结
目录
封面
作者简介
数字版权声明
扉页
版权页
献词
译者序
序
前言
致谢
目录
第1章 什么是JavaScript
1.1 简短的历史回顾
1.2 JavaScript实现
1.3 JavaScript版本
1.4 小结
第2章 HTML中的JavaScript
2.1 <script>元素
2.2 行内代码与外部文件
2.3 文档模式
2.4 <noscript>元素
2.5 小结
第3章 语言基础
3.1 语法
3.2 关键字与保留字
3.3 变量
3.4 数据类型
3.5 操作符
3.6 语句
3.7 函数
3.8 小结
第4章 变量、作用域与内存
4.1 原始值与引用值
4.2 执行上下文与作用域
4.3 垃圾回收
4.4 小结
第5章 基本引用类型
5.1 Date
5.2 RegExp
5.3 原始值包装类型
5.4 单例内置对象
5.5 小结
第6章 集合引用类型
6.1 Object
6.2 Array
6.3 定型数组
6.4 Map
6.5 WeakMap
6.6 Set
6.7 WeakSet
6.8 选代与扩展操作
6.9 小结
第7章 选代器与生成器
7.1 理解迭代
7.2 迭代器模式
7.3 生成器
7.4 小结
第8章 对象、类与面向对象编程
8.1 理解对象
8.2 创建对象
8.3 继承
8.4 类
8.5 小结
第9章 代理与反射
9.1 代理基础
9.2 代理捕获器与反射方法
9.3 代理模式
9.4 小结
第10章 函数
10.1 箭头函数
10.2 函数名
10.3 理解参数
10.4 没有重载
10.5 默认参数值
10.6 参数扩展与收集
10.7 函数声明与函数表达式
10.8 函数作为值
10.9 函数内部
10.10 函数属性与方法
10.11 函数表达式
10.12 递归
10.13 尾用优化
10.14 包
10.15 立即调用的函数表达式
10.16 私有变量
10.17 小结
第11章 期约与异步函数
11.1 异步编程
11.2 期约
11.3 异步函数
11.4 小结
第12章 BOM
12.1 window对象
12.2 location对象
12.3 navigator对象
12.4 screen对象
12.5 history对象
12.6 小结
第13章 客户端检测
13.1 能力检测
13.2 用户代理检测
13.3 软件与硬件检测
13.4 小结
第14章 DOM
14.1节点层级
14.2 DOM编程
14.3 MutationObserver接口
14.4 小结
第15章 DOM扩展
15.1 Selectors API
15.2 元素遍历
15.3 HTML5
15.4 专有扩展
15.5 小结
第16章 DOM2和DOM3
16.1 DOM的演进
16.2 样式
16.3 遍历
16.4 范围
16.5 小结
第17章 事件
17.1 事件流
17.2 事件处理程序
17.3 事件对象
17.4 事件类型
17.5 内存与性能
17.6 模拟事件
17.7 小结
第18章 动画与Canvas图形
18.1 使用requestAnimationFrame
18.2 基本的画布功能
18.3 2D绘图上下文
18.4 WebGL
18.5 小结
第19章 表单脚本
19.1 表单基础
19.2 文本框编程
19.3 选择框编程
19.4 表单序列化
19.5 富文本编辑
19.6 小结
第20章 JavaScript API
20.1 Atomics与SharedArrayBuffer
20.2 跨上下文消息
20.3 Encoding API
20.4 File API与Blob API
20.5 媒体元素
20.6 原生拖放
20.7 Notifications API
20.8 Page Visibility API
20.9 Streams API
20.10 计时API
20.11 Web组件
20.12 Web Cryptography API
20.13 小结
第21章 错误处理与调试
21.1 浏览器错误报告
21.2 错误处理
21.3 调试技术
21.4 旧版IE的常见错误
21.5 小结
第22章处理XML
22.1 浏览器对XML DOM的支持
22.2 浏览器对XPath的支持
22.3 浏览器对XSLT的支持
22.4 小结
第23章 JSON
23.1 语法
23.2 解析与序列化
23.3 小结
第24章 网络请求与远程资源
24.1 XMLHttpRequest对象
24.2 进度事件
24.3 跨域资源共享
24.4 替代性跨源技术
24.5 Fetch API
24.6 Beacon API
24.7 Web Socket
24.8 安全
24.9 小结
第25章 客户端存储
25.1 cookie
25.2 Web Storage
25.3 IndexedDB
25.4 小结
第26章 模块
26.1 理解模块模式
26.2 凑合的模块系统
26.3 使用ES6之前的模块加载器
26.4 使用ES6模块
26.5 小结
第27章 工作者线程
27.1 工作者线程简介
27.2 专用工作者线程
27.3 共享工作者线程
27.4 服务工作者线程
27.5 小结
第28章 最佳实践
28.1 可维护性
28.2 性能
28.3 部署
28.4 小结
附录A ES2018 和ES2019
A.1 异步选代
A.2 对象字面量的剩余操作符和扩展操作符
A.3 Promise.prototype.finally()
A.4 正则表达式相关特性
A.5 数组打平方法
A.6 Object.fromEntries()
A.7 字符串修理方法
A.8 Symbolprototype.description
A.9 可选的catch绑定
A.10 其他新增内容
附录B 严格模式
B.1 选择使用
B.2 变量
B.3 对象
B.4 函数
B.5 this强制转型
B.6 类与模块
B.7 其他变化
附录C JavaScript库和框架
C.1 框架
C.2 通用库
C.3 动画与特效
附录D JavaScript工具
D.1 包管理
D.2 模块加载器
D.3 模块打包器
D.4 编译/转译工具及静态类型系统
D.5 高性能脚本工具
D.6 编辑器
D.7 构建工具、自动化系统和任务运行器
D.8 代码检查和格式化
D.9 压缩工具
D.10 单元测试
D.11 文档生成器