出现在feat.后面的歌手不能算作主唱,他们一般的作用是在歌曲里面进行个人特色表演.(大致就是我整一些活吧)
前言
工业革命是钢铁构建的,信息革命是则是由js构建的。25年来在不断的跌倒中稳固加强,js在应用开发届具备了统治地位吗?很难回答,但这并不是最重要的。
Brendan Eich只用了十天就完成了js的第一版。感觉上它是脆弱的,但我们都有这样的体会,第一印象往往寻在偏差。今天你将从这本书--我们大量时间讨论的产物中学习到js的方方面面和各个细节。并没每个决定都是完美的,也没有任何语言是无瑕的,但如果你只用普遍性来评价的话,js就很接近我们的答案。它是唯一一种可以部署在任何地方的语言:服务器,终端浏览器,移动端浏览器,甚至本地移动应用。
js现在被各个经验级别的,各个背景的软件工程师所使用。它被追求完美设计的人及最求简单快速产出的人同时接受。
你如何应用它完全由你决定。决定因素在于使用者。
在我十五年的软件开发经验中,js工具和最佳实践发生了戏剧性的变化。我在2004年,雅虎地球村,Macromedia flash大行其道的时候接触到了这门语言。js感觉就像一个玩具,我在当时一些流行的沙盒(RSS,My Space Profile Pages)中使用它。帮助别人修改和定制他们的网站,比如像狂野西部那样,就这样我被它深深的吸引住了。
当我刚创立我第一家公司时,为你数据库配置一个主机需要花几天的时间,js也是嵌在你的HTML中的。不存在所谓的前端应用,它更像一些零碎的工作。当ajax在jquery的传播下变得更流行,新篇章打开了应用们变得生机勃发。突然加速头都给你闪掉了(breakneck speed),突然间强大的框架被发布出来。前端设计模型!数据绑定!路由管理!响应式视图!正值这次革命的时间段,我选择去了ladygaga在硅谷的一家公司,很快的数百万用户开始使用我的代码。到现在我在硅谷待了很长一段时间了,我曾引导开源贡献者,指导了数不清的软件工程师,我也是很幸运的。我最后一家公司在2018年被Stripe收购,现在我正为这家公司建立网络金融的基础设施。(20-09-08)
在Matt去PaloAlto牵头工程起步的时候我有幸会见了他。它被称为Claco,我最近也作为顾问加入了这个项目。他对构建伟大软件的激情和能量是显而易见的,雏鸟很快有了自己的蔚蓝天空。渐渐称为硅谷标准的它,创立于一个大屋子里。但那不是一个简单的屋子。它是‘极客之家’,十来个极其优秀的工程师能在预定的时间聚集。虽然这不是高端的脱离生活的环境,我们常在上下床,街边的椅子等地方编码,日常来看我们的代码量和质量都是惊人的。结束数小时的工作后,大多数人会转移几小时的注意力去为其它项目构建自己的附带工程。
Matt是团队生产效率的核心驱动力。他是团队里最有经验的软件工程师,同时也是最纯粹最专业的。在计算机工程方面拥有一个正式学位是非凡的,所有当你在窗户上或者白板上看到算法,推导,代码时,你会知道他在构建他的下一个大工程。我们认了还成了亲近的朋友。他的才能,对导师制的推崇,能把所有事物形容成有趣的笑话,这些所有品质都很让我欣赏。
尽管Matt在软件工程和工程指导方面的才能令人惊诧,但诚然他独一无二的经验和技术栈让他成为这个世界上最有资格写这本书的人。
他刚结束了教育别人的工作,他做到了。
在Claco,他完成了一个个多样且完整的产品,端到端的,帮助老师在他们的教师里达成更好的教学体验。在DoorDash,他作为最早的工程师,构建了健壮的逻辑和物流网络,这家公司高速成长先在市值已经超过120亿美元。最后,在谷歌,Matt的软件被数全球十亿人们使用。
实至名归的所有权,跨度极大的成长,和步步踏实的登顶。大多数软件工程师整个生涯只能达到其中之一,当然还是在幸运的情况下。Matt不仅全做到了,还在‘业余时间’成为了畅销书作家,写过另外两本书关于js和angular的。说实话,我希望他的下一本书能把他藏起来的时光机原理图公之于众。😄
这本书是一个有活力工具,帮助你补全js知识达到圆满和达到一种‘真实的认知’。我钦佩你保持学习并将所想之物付诸实践的精神。拆分它的知识,做笔记,别忘了打开你的代码编辑器——毕竟,信息革命依然在起步阶段!(20-09-09)
—Zach Tratar
Software engineer at Stripe Former co-founder and CEO of Jobstar
介绍
一个谷歌的技术领导曾和我分享过一个直击心灵的关于js的解读:它不是一个真正内聚的编程语言,至少在给人的直觉上不是。ECMA-262规范定义了js,当并不存在一对于它的真正实现。进一步说,这门语言不是封闭的。与js临近的领域定义了规范API形成了名副其实的海洋让它在其中遨游,就像:DOM,网络请求,系统硬件,存储,事件,文件,密码学还有很多其它领域。浏览器们和他们各式各样的js引擎全都实现了这些它们认为合适的规范。谷歌有 BLINK/V8,火狐有GECKO/SPIDERMONKEY,safari有WEBKIT/JSCORE。浏览器在规范之下几乎可以运行所有js程序,但web因为各个浏览器的风格不同而变得特别杂乱。因而,js或许可以被更精确的描述为一些列浏览器上的应用实现。
尽管web的存粹之一者坚持js不应该成为web的必要部分之一,但他们必须承认现代浏览器已经无法离开js了。还不夸张的说js几乎是不可获取的:电话,电脑,展屏,电视,游戏控制台,智能手表,冰箱,甚至车辆未来浏览器运行js。近30亿人的智能手机上安装了web浏览器。js充满生机的社区激烈翻涌着涌现了一大批高质量的开源工程。现在浏览器以还原原生移动app的api为第一特色。在stack overflow 2019 开发者调查中,js被投票为近七年最流行的语言。
js的文艺复兴由我们推进。
在本书中,js会被从最开始的最朴素的网景浏览器到如今由五光十色的浏览器技术光谱中散射出的各个分身。本书包含了在严谨的袭击中包含大量先进的观点,然而它也保证了读者理解书中提到的点并在合适的地方使用它们。简单来说,作为一个web开发者你可以学到如何把js的解决方案应用到实际工作问题中。(20-09-10)
1 什么是js
js实现
尽管 js 和 ECMAScript 经常被当作同义词引用,js 的实际丰富度是远超 ECMA-262 标准定义的。实际上,完整的 js 实现是由下面三部分确切的部分组成的: * 核心 * 文件对象模型(the document object model DOM) * 浏览器对象模型(the browser object midel BOM)
DOM
DOM是一个扩展在HTML上的最对XML文件的应用接口。DOM通过有层级的节点的结构构建出了整个页面。html或xml页面一种包含各种类型数据的节点。参考如下HTML网页:
<html>
<head>
<title>Sample Page</title>
</head>
<body>
<p> Hello World!</p>
</body>
</html>
代码在DOM上可以被图示为一种有层级节点的形式。
通过创建一颗树来表示文件,DOM允许开发者们在一个前所未有的等级上控制它的内容和结构。通过 DOM API 我们可以轻松的对节点进行删除,新增,替换和修改。
为什么DOM是不可或缺的
IE4和网景浏览器分别支持不同形式的动态 HTML (DHTML),开发者一开始可以不重载就改变网页的样式和内容。它代表了在web技术上的一个极大突破,但同时也是一个巨大的问题。网景和微软开发DHTML的方式开始不同,因此当开发者能够写一个单一HTML就可以在任何浏览器运行的时代给之前的历史画上了句号。
注定需要一个方案解决 web 的跨平台生态问题。出于担心没有人能控制网景和微软,最后web变成了针对目标浏览器而泾渭分明的两个派系。之后出现了世界网页联盟,主体负责为web的交互建立规范,开始着手构建DOM的工作。
DOM等级
依旧是些历史鸭~就不翻啦。
BOM
IE3和网景3浏览器器定义了浏览器对象模型(BOM)允许链接并操作浏览器窗口。使用BOM,开发者可以在浏览器的显示内容容器之外和它交互。使BOM真正独立的,也是经常产生问题的,是它仅仅是一部分js的实现并且没有相关规范。这随着H5的引入改变了,试图编撰BOM作为标准规范的一部分。感谢H5,很多关于BOM的质疑消失了。
基本的,BOM处理浏览器窗口和帧,但通常任何浏览器的js插件被认为是BOM的一部分。下面距离了一些这类的扩展例子: * 弹出新浏览器窗口的能力。 * 移动,调整大小,关闭浏览器窗口的能力。 * navigator 对象,提供了浏览器的细节信息。 * location 对象,提供了浏览器中加载的页面的详细信息。 * screen 对象,提供了屏幕分辨率的细节信息。 * performance 对象,提供了浏览器内存占用的详细信息,导航行为,关键时刻统计。 * 对 cookis 的支持 * 定制对象,比如 XHR 和 IE 的 ActiveXObject 因为BOM的标准没有出现很久,每个浏览器都有自己的实现。这里已经存在了很多既存的标准,比如有一个window对象和一个navigator对象,但不同浏览器为这些对象还有其它对象定义了自己的属性。现在有了H5标准,各个浏览器BOM的实现在往一天兼容的道路上靠近。关于BOM的细节讨论,我们将在“浏览器对象模型”章节详细展现。
总结
js是一门被设计为与网页交互的脚本语言。它由以下三个不可或缺的部分组成。 * ECMAScript,在 ECMA-262 中定义并提供核心功能。 * DOM 提供操作网页内容的方法和接口。 * BOM 提供和浏览器交互的接口和方法。 在五大浏览器(IE , FireFox, Chrome,Safari,Opera)中对这三部分有不同程度的支持。对ES5的支持是普遍良好的,对ES6 和 ES7 的支持还在成长中。对DOM的支持是多样的,但等级3规定的遵守在逐渐规范化。BOM,在H5中被编撰,对于不同浏览器很多样,但这里依旧假定一些共同规范是可信的。(20-09-11)
2 html里的js
<SCRIPT> 元素
在 html 中插入 js 的基本方法是通过 <script> 元素。这个标签是由网景创造并在网景浏览器2中首次实现。它之后就被加入了 html 的正式标准。这里列举 <script> 元素的六个属性:
- async 可选的。表明脚本应该马上开始下载,但不能阻塞页面的其他动作:比如下载资源或者等待脚本加载。只对外部脚本文件生效。
- charset 可选的。代码的字符集通过 src 属性指定。这个属性很少用到,因为大多数浏览器不接受它的值。
- crossorgin 可选的。为关联请求配置 CORS 设置。默认情况下。CORS 不会被用到。crossorgin=“anonymous” 为没有认证要求类的请求配置。crossorgin=“use——credentials”将会打开认证选项,意味这外部请求需要包含认证信息。
- defer 可选的。表明脚本的执行被安全的延后到文件内容解析完毕并展示事件后。只对外部脚本生效。IE7 和 早期版本也对行内脚本生效。
- integrity 可选的。允许通过一个密钥来检查得到的数据资源来进行下级资源完好性校验。如果收到的资源的签名与这个配置项的规定不符,页面将会报错脚本不会被执行。这个用来保证 CND 不会服务恶意请求参数。
- language 废弃。过去用来表明 js 版本。
- src 可选的。表明可执行的外部代码文件。
- type 可选的。取代了 language 属性。表明改代码块中js的文件类型。通常,这个值一般是 “text/javascript”,然而,“text/script”和“text/ecmascript”都被废弃了。js 文件一般被当作“application/x-javascript”文件类型处理,尽管在type属性里这样设置可能会导致这段脚本被忽视。其他在非IE浏览器生效的配置为“application/jacascript”和“application/ecmascript”。如果值是module的话,代码将被当作 ES6 模块来处理,也只有在这样设置之后 import 和 export 关键字才会生效。
有两种使用 script 元素的方式:直接将 js 代码嵌入到页面中。或者从一个外部文件引入 js 文件。
行内的例子如下:
包含在script标签中的script代码是自上而下解释执行的。在这个例子中,一个方法被定义且存储在解释器环境中。剩下的页面内容的加载,显示,只有在script标签中的代码执行后,才能继续执行。
当使用行内 js 时,注意你的代码中不能有 “</script>” 这样的字符串。例如下面代码就会在加载过程中报错:
<script>
function sayScript() {
console.log("</script>");
}
</script>
因为行内js的词法分析过程中浏览器发现字符串“</script>”就会认为它是js的结束标签。这个问题可以通过避免“/”字符来简单回避。(转义字符 \ )(20-09-14)
为了从外部引用 js 我们必须用到 src 属性。src 的值是一个 URL 链接链向包含 js 代码的文件,就像这样:
在这个例子中,一个叫 example.js 的外部文件被引用到界面中。文件本身只需要包含将要出现在开闭 script 标签中的代码即可。执行行内js代码过程中,解释外部文件时页面构建会停止。(下载文件也会消耗一些时间)。在 XHTNL 文件中,你可以省略结束标签,就如同下例一样:
注释 通常外部js文件会有一个.js 的扩展名。这并不是必要的,因为浏览器不会检查引入的js文件的扩展名。这个特点让服务端动态生成js代码的设计有了实现的可能性,或是像 React 的JSX,TS等 js 的扩展语言能在浏览器内进行js语言转换。请注意,然而,服务器经常用文件扩展名来决定应该返回的正确媒体类型(MIME type)。如果你不用 .js 扩展名,复查一下你的服务器是否返回了你正确的媒体类型。
注意,使用了 src 属性的的 script 标签内不要在写入js代码。如果你两者都写了的话,js 文件会被下载执行,行内的 js 代码会被忽略。
script 元素可以从外部域名引入js文件是它最强大也是最受争议的能力。就像 img 元素一样, 一个内部HTML 中的 script 元素的 src 属性设置成全路径外部 URl,技能引用改为加,例子如下:
当浏览器去解析这个资源时,它会针对 src 中的具体路径发起 GET 请求,去获取资源——通常是js文件。这个初始化请求在浏览器跨域约束下,但是人任何 js 的返回和执行依旧在跨域约束下。当然,请求依旧需要遵循父页面的 HTTP/HTTPS 协议。
外部服务器的代码将会被加载并解释执行,就如同它是引用它界面的一部分。这个能力允许你用各种服务器的 js 支持起你的网页,如果必须的话。不管怎样,在引用你无法控制的服务器的 js 时需要格外小心。恶意程序可以在任何时间替换你的目标文件。当从不同的域引用 js 文件时,请保证你是该域的拥有者,或者这个域是可信的。script 标签的 intergrity 属性给了你防御这类情况的工具。然而,只有部分浏览器支持这个属性。
无论 js 代码是如何引入的, script 元素都是按照他们出现在页面中顺序来执行的(在 derfer 和 async 属性未设置的情况下。第一个 script 元素必须在第二个开始执行被完全执行,第二个必须在第三个之前,如此以往。(20-09-15)
标签摆放位置
一般情况下,全部的 script 元素被放到一个页面中的 head 元素里,如同下例:
Example HTML Page
这样规范格式的主要目的是保证外部引用,包括 CSS 文件和 JS 文件在同一个区域内。然而,在头部引用所有 js 文件意味着在渲染界面前必须下载,语法分析,解释执行完这些 js(渲染过程发生在浏览器发现第一个 body 标签开始)。对于引用了很多 js 代码的页面,这会在页面渲染中引发一个明显的延迟,这段时间内浏览器会完全空白。因此,现代web应用,一般在 body 中内容之后引入 js 。
通过这样的方式,页面在 js 代码执行前就被浏览器渲染完毕了。其结果对用户体验的提示就是能感知到页面变快了,因为空白网页的时间减少了。
延迟脚本(deferred script)
HTML 4.01中为 script 元素定义了名为 defer 的属性。defer的目的是表明这个脚本在执行时不会改变页面结构。严格来说,脚本可以在整个页面语义分析结束后被安全的执行。在script元素中设置 defer 属性向浏览器表明脚本的需要立即开始下载但是执行需要被延后。
即是这个 script 元素被放在文件的 head 中,它也会等到浏览器识别到 </html> 结束标签才会被执行。HTML 5 的规范中阐述 js 脚本会按照出现顺序执行,所以第一个 defer 标记的脚本会在第二个 defer 标记脚本前执行,他们都会在 DOMCotentLoaded 事件之前执行。实际上,可是,defer 脚本不会一直按顺序执行,也不一定是在 DCL 之前执行,所以可能的话你最好还是只使用一整个来引用比较好。
之前提到过,defer 属性只支持外部 js 文件。这是在 H5 中规定的,所以支持 H5 实现的浏览器将会在设置行内代码后忽略 defer 属性。 IE 4-7 是老版本的表现,8 及以上版本支持H5.
这个属性的指出是从 IE4,火狐3.5,sarfari5和chrome7开始的。其他浏览器都选择简单的忽略这个属性,把它当成普通的 js 来执行。因为这个原因,把有defer标签的 js 脚本放到页面底部是最好的做法。
注意 对于 XHTML 文件来说这样写这个属性 defer=“defer”
异步脚本(asynchromous scripts)
H5 为 script 标签引入了 async 属性。async 属性和 derfer 相似的改变了脚本执行的方式。在支持外部引用脚本和告知浏览器立即下载方面它和 defer 也是相似的。不同于 defer 的是,async 标记的脚本不会保证按出现顺序执行。
后面的 js 可能会在,前面的 js 前执行,所以这些 js 间不要存在依赖关系。标记为 async 的原因为表明页面不需要加载前等待某脚本的下载执行,也有另一脚本也不需要等待。因此,建议异步脚本不要执行一些修改dom的动作。
异步脚本严格的在 LOAD 事件前执行或在 DCL 前后执行。火狐3.6,safari5和Chrome7支持异步脚本。使用异步脚本意味着你遵守默认规定不会在其中使用 document.write 方法——同时优秀的web工程实践也代表你不会在其他任何地方用到这个方法。(20-09-16)
注意 对于 XHTML 文件来说这样写这个属性 async=“async”
动态脚本加载
你可以用不限于 script 标签的方式去引用资源。因为 js 是可以调用 DOM API 的,你也可以添加 script 元素,来加载指定的资源。可以通过创建 script 元素然后把他们添加到 DOM 的方式达成:
let script = document.createElement('script');
script.src = 'gibberish.js';
document.head.appendChild(script);
当然,这个请求直到 HTMLElement 元素被添加到 DOM 时才会被执行。默认的,这种方式创建的脚本是带有 async 属性的。这可能会造成问题,然而,就像所有浏览器都支持 createElement 但不是都支持 async 脚本请求。因此,为了统一动态链接的加载,你可以特别注明标签为异步的:
let script = document.createElement('script');
script.src = 'gibberish.js';
script.async = false; document.head.appendChild(script);
这种方式加载的资源不会被浏览器的预加载器识别到。这将会严重的破坏脚本们在资源加载队列中的优先级。决定于你的应用是如何工作的及如何使用的,它会严重的破坏性能。为了告知预加载器这个动态资源的存在,你可以在文件头部进行声明。
<link rel="subresource" href="gibberish.js">
在 XHTML 中的变化
Extensible HyperText Markup Language, or XHTML,是一个 HTML 作为 XML 应用的重构,不像 HTML 里,type 属性是可以在使用 script 时省略的,在 XHTML 里,script 元素需要你注明 type 属性为 text/javascript。
编码规则在 XHTML 中比 HTML 要严格的多,影响了 script 元素的行内写法。下面代码会在 HTML 中生效,但不会在 XHTML 中生效:
<script type="text/javascript">
function compare(a,b){
if(a < b){
console.log('a is less than b')
}
}
</script>
在 HTML 中,script 元素有特别的规则声明了它包含的 JS 的词法解析方式;在 XHTML 中这些特殊规则不会生效。这以为着表述中的小于符号(<)被解释为标签的开始,会引起一个 < 符号后不能是空格的语法错误。
这里有两种改正语法错误的方式,一种是替换所有 < 符号为它的 HTML 数据(<)。结果代码如下:
<script type="text/javascript">
function compare(a,b){
if(a < b){
console.log('a is less than b')
}
}
</script>
现在这个代码可以在 XHTML 中运行了,但是,这个有一些不易读懂。幸运的是我们还有另一种方法。
第二种在 XHTML 中运行的方法是,把 js 代码转换为 CDATA 部分。在 XHTML (XML)中,CDATA 部分表明,在页面的词法分析中忽略这一部分自由规范的代码。这能让你使用各种字符,包括小于符号,并不会引发语法错误。书写方式如下:
<script type="text/javascript">
<![CDATA[
function compare(a,b){
if(a < b){
console.log('a is less than b')
}
}
]]>
</script>
这解决了 XHTML 规范浏览器中的问题。然而,还有很多浏览器不遵循 XHTML 规范也不支持 CDATA 片段。为了在这类浏览器工作, CDATA 部分必须被注释掉。
<script type="text/javascript">
//<![CDATA[
function compare(a,b){
if(a < b){
console.log('a is less than b')
}
}
//]]>
</script>
这种形式在所有现代浏览器中都可以工作。虽然有一些 hack ,但它作为 XHTMl 是生效的也为 pre-XHTML 浏览器做了优雅的降级。
注意 XHTML 模式是通过标注页面的 MIME type 为 “application/xhtml+xml”。并不是所有的正式浏览器都支持这个设置功能。
废弃语法
从1995年网景发布了网景2浏览器,所有浏览器都是用 js 作为它们的默认开发语言。type 属性被作为标志 script 内容的 MIME type,但 MIME type 在跨浏览器方面没有被标准化。即是浏览器默认识别为 js,在某些情况下一些无效的或无法识别的 MIME type 的相关代码就会被跳过执行。因此,除非你在用 XHTML 或者 script 标签请求或包裹了 非js 代码,最佳实践是完全不使用 type 属性。
当 script 元素最先被引入,它就标志这与传统 HTML 页面的语义分析的背离。特殊的规则需要被应用到这个特殊的模块上,这为不支持 js 的浏览器造成了问题(最值得一提的是 Mosaic)。不支持的浏览器会直接将 js 标签的内容输出到页面上,有效的运行页面显示。
网景再开发了 Mosaic 产出了一个解决方案不支持的浏览器会把嵌入的 js 代码隐藏。最终方案是将 HTML 中的 js 代码备注起来:
<script><!--
function sayHi(){ console.log("Hi!");
}
//--></script>
用这种形式,像 Mosaic 这样的浏览器将会安全的忽略 script 标签内的内容,支持 js 的浏览器回去寻找这样的结构识别出这里有必要的 js 内容需要被执行。
虽然这个形式依旧被所有浏览器正确识别和解释执行,它不再是必须的也不应该再使用。在 XHTML 模式下,它也会导致脚本被忽略因为它在一个有效的 XML 内容中。(20-09-17)
行内代码对比引用文件
虽然可以直接在 HTML 中嵌入 js,但通常最佳实践是尽量用外部引用的方式引入 js 文件。注意,这里没有关于此实践硬性严格要求,关于外部文件的争议以下几点:
- 可维护性 分散于格式各样 HTML 页面中的 js 代码无疑是存在维护难度的。用一个文件夹管理所用到的 js 以便开发者基于文件目录修改目标代码。
- 缓存 浏览器通过特定设置缓存所有外部 js 文件,意味着如果你连两个网页使用了相同的文件,文件只需要被下载一次。这意味着更快的页面加载。
- 未来趋势 使用外部引用文件,就没必要使用 XHTML 或者文件预先声明这类 hack 方法了。引入外部文件的语法,在 HTML 和 XHTML 中也是相同。
值得一提的考虑,当配置外部文件如何被请求时是它们对请求带宽的影响。(???这段话我看不明白 One notable consideration when configuring how external files are requested is their implication on request bandwidth. )使用 SPDY/HTTP2,每个请求的开销从根本上被减少了,因而在像客户端传输脚本作为 js 组件轻量级依赖时是具备优势的。
例如,你的第一个页面可能有下列引用:
<script src="mainA.js"></script>
<script src="component1.js"></script>
<script src="component2.js"></script>
<script src="component3.js"></script>
之后的页面加载可能有下面引用:
<script src="mainB.js"></script>
<script src="component3.js"></script>
<script src="component4.js"></script>
<script src="component5.js"></script>
在初始化请求时,如果浏览器支持 SPDY/HTTP2,它可以有效的获取同个终端的文件数,接着它会将他们基于预备文件加入到浏览器缓存。从浏览器的角度来看,通过 SPDY/HTTP2 获取这些独立的资源们应该有大致和传输僵化而庞大的 js 参数相似的延迟。
第二个界面请求,因为你将你的应用划分到轻量缓存文件中,一些第二个页面也依赖的组件早已存在了缓存中。
当然,这假定浏览器支持 SPDY/HTTP2,是只对现代浏览器适用的假设。僵化而庞大的数据可能会对老的浏览器更加适配。
文件模式
IE5.5 通过使用文件类型(doctype)切换,引入了文件模式的概念。开始的两个文件模式是怪异模式(使 IE 表现的像5版本一样但包含几个非标准的特性)和标准模式(使 IE 表现的更符合规范的方式)。虽然这两种模式渲染内容上最基本的不同点是与 CSS 相关的,这里也有几处和 js 相关的影响。在些影响,我们会在本书进行讨论。
自 IE 开始引入了文件模式的概念起,其他浏览器也跟着进行匹配。随着标准被大家承认,一个被称做几乎标准的模式冉冉升起。那个模式有很多标准模式的特性但并没有那么严格的去限制。最主要的不同是处理图片周围的空格的情景(最常见的就是在表格里使用图片时)
怪异模式在所有浏览器中都是通过不写文件开头的 doctype 来切换。这考虑到不好的实践因为怪异模式很难在跨浏览器中做兼容,除非 hack 手段否则真实浏览器的等级一致性是做不到的。
下面任意 doctype 被使用就意味着打开标准模式:
<!-- HTML 4.01 Strict -->
<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01//EN" "http://www.w3.org/TR/html4/strict.dtd">
<!-- XHTML 1.0 Strict -->
<!DOCTYPE html PUBLIC
"-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
<!-- HTML5 -->
<!DOCTYPE html>
几乎标准模式(almost standards)都通过过渡性的,框架性的 doctypes 来转换,如下:
<!-- HTML 4.01 Transitional --> <!DOCTYPE HTML PUBLIC
"-//W3C//DTD HTML 4.01 Transitional//EN" "http://www.w3.org/TR/html4/loose.dtd">
<!-- HTML 4.01 Frameset -->
<!DOCTYPE HTML PUBLIC
"-//W3C//DTD HTML 4.01 Frameset//EN" "http://www.w3.org/TR/html4/frameset.dtd">
<!-- XHTML 1.0 Transitional -->
<!DOCTYPE html PUBLIC
"-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<!-- XHTML 1.0 Frameset -->
<!DOCTYPE html PUBLIC
"-//W3C//DTD XHTML 1.0 Frameset//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-frameset.dtd">
因为几乎标准模式和标准模式很接近,很少有区别。人们讨论“标准模式”可能是在说它们之一,关于文件模式的探测也不做区别。这本书中标准模式这个词就代表了除了怪异模式只外的所有模式。(20-09-18)
<NOSCRIPT> 元素
早期浏览器都很关注的一个问题是当页面不支持 script 时我们如何做到优雅的降级。最后,noscript 元素被创造出来提供给不支持 js 的浏览器。虽然事实上 100% 的现代浏览器都支持 js,这个标签对于特别声明不支持 js 的浏览器依旧是有用的。
noscript 元素可以包裹任何 HTML 元素,除了 script 元素外的所有文件 body 标签内的元素。包裹于 noscript 元素内的内容仅在以下两种情况展示:
- 浏览器不支持 js 脚本
- 浏览器的 js 支持被关闭了
如果其中任意一个条件达成,noscript 标签中的元素就会别渲染。在其他情况下,浏览器不会渲染 noscript 中的内容。
这里有个简单例子:
<!DOCTYPE html> <html>
<head>
<title>Example HTML Page</title>
<script ""defer="defer" src="example1.js"></script>
<script ""defer="defer" src="example2.js"></script>
</head>
<body>
<noscript>
<p>This page requires a JavaScript-enabled browser.</p>
</noscript>
</body>
</html>
这个例子中,当浏览器不支持 script 时就会把消息展示给用户。对于支持 js 的浏览器,这些消息永远不会被展示,即是它依旧是页面的一部分内容。
总结
js 通过 script 元素插入到 HTML 界面。这个元素可以用来把 js 嵌入到 HTML 界面中,通过结束标记的方式进行行内使用,或者引入存在于外部文件的 js。以下为关键几点:
- 为了引入 js 文件,src 属性必须设置被引入文件的 URL,这个文件可以是在和页面相同的服务器上也可以是另一个完全不同的域名。
- 所有的 script 标签都是按出现在页面中顺序被解释执行的。只要没有使用 derfer 或 async 属性,script 标签中代码必须在上一个 script 标签中代码被执行后才能开始执行。
- 对于非延迟 js,浏览器必须完全解释执行 script 标签内的代码之后才能继续渲染剩余界面。因此,script 标签往往是在页面结束位置使用,在主内容之后且恰好在 body 的结束标签前。
- 你可以延迟执行 js 到文件渲染完毕。defer 标记的脚本会按照他们出现的顺序执行。
- 你可以通过使用 async 属性,声明一个 js 文件既不需要等其他 js 执行,也不阻塞文件渲染。异步脚本不会按照他们出现在文件中位置来顺序执行。
通过使用 noscript 元素,你可以在浏览器不支持 js 时指定显示任意你想展示的内容。
3 语言基础
语法
ECMAScript 的语法很大程度的借鉴了 C 和其它 类似C 的语言如 Java 和 Perl。熟悉这类语言的开发者应该能简单的学习并使用起这门语法上自由的语言。
区分大小写
首先需要理解的基本概念是,任何东西都是区分大小写的;变量,方法名,操作符全是大小写敏感的,意味着一个名为 test 的变量是与一个名为 Test 的变量不同的。类似的,typof 不能作为一个方法名因为它是一个关键字;然而,typeof 是一个很合理的方法名。
标识符
标识符是变量的,方法的,属性的或者方法参数的名称。标识符可以是一个或多个字符以下列规则组成:
- 开头的字符必须是字母(letters),一个下划线(_)或者一个刀了符 ($)。
- 其它字符可以是字母,下划线刀了符或者数字。
字母是可能包含 SACII 或者 Unicode 字符的比如 À 和 Æ,尽管这种方式是不推荐的。
通常,ECMAScript 的标识符使用驼峰命名法,意味着第一个字母需要小写,后续的新增词汇需要通过一个大写字符来分开,如下:
firstSecond
myCar
doSomethingImportant
尽管这没有被严格要求,但遵从下述规则的形式被认为是构建 ECMAScript 方法和对象的最佳实践。
关键词,保留字,true,false 和 null 不能被用作标识符。“Keywords and Reserved Words” 章节有描述。
注释
ECMAScript 对于单行和块都使用 C 风格的注释。如下:
// single line comment
/* This is a multi-line comment */
严格模式
ECMAScript5 引入了严格模式的概念。严格模式是 js 的一个不同的词法分析和执行模式,区别于 ECMAScript3 演说的不确定行为和不安全动作的错误抛出。通过在文件首部加入如下代码,为脚本设置安全模式:
“use strict”
虽然这个看起来更像一个字符串而不是一个变量,它是一个编译附注告知 js 引擎使用严格模式运行。特地选中这样的语法来兼容 ECMAScript3 的语法。
你也可以通过在方法体顶部引入编译附注,来指定某个方法运行在严格模式下。
function doSomething() {
"use strict";
// function body
}
严格模式改变了 js 很多部分的执行,如此,本书会指出严格模式的很多特点。所有现代浏览器都支持严格模式。
语句
js 中的语句是以分号结束的,通过忽略分号让解析器决定语句在哪结束,如下方示例:
let sum = a + b // valid even without a semicolon - not recommended
let diff = a - b; // valid - preferred
虽然分号不是必须的,但你还是应该写上。引入引号避免了忽略错误,比如不结束你输入的语句,允许开发者通过移除多余的空格来压缩 js 代码。引入分号也在特定情况下提升了性能因为解析器通过插入分号来修正语法错误。
多条语句可以通过 C 语法风格合并成一个代码块,开始于一个左大括号以右大括号结束:
if (test) {
test = false; console.log(test);
}
控制语句,比如 if,仅当执行多条语句时需要代码块。然而,就算只有一条语句需要被执行,我们也会在控制语句使用代码块。
// valid, but error-prone and should be avoided
if (test)
console.log(test);
// preferred
if (test) { console.log(test); }
在控制语句中使用代码块使内容更加清晰,减少了修改代码时产生错误的可能。(20-09-22)
关键字和保留字
ECMA-262 描述了一组有特殊用处的关键字,比如声明了开始或结束语句或执行特定指令的保留字。根据规则,关键字是被保留的,无法被用作标志符或属性名称。ECMA-262 第六版的完整关键字列表如下:
break case catch class const continue debugger default delete
do
else export extends finally for function if import
in typeof instanceof var new void return while super with switch yield this
throw try
规范也提到了一批不能用来命名标识符和属性的未来保留字。就算这些保留字在现有语言中没有任何特殊用途。他们是未来关键字的保留字。
ECMA-262 第六版未来保留字:
Always reserved:
enum
Reserved in strict mode:
implements interface let
package public protected static private
Reserved in module code:
await
这些关键字还没有被用作标识符但还可以被用来命名。一般来说,最好避免使用无论是关键字还是保留字来做命名,这样确保了兼容过去和未来的 ECMAScript版本。
变量
ECMAScript 的变量是无类型的,意味着一个变量可以持有任何类型的数据。每个变量只是一个简单的为值准备的具名占位符。这里有三个关键字可以用来声明一个变量:var ,在所有版本的 ECMAScript 版本可用,let 和 const 都是在ECMASCript6 中引入的。
var 关键字
使用 var 操作符(var 是一个关键字)后面跟着一个变量名(一个操作符,如先前描述的),来定义一个变量:
var message;
这段代码定义了一个名为 message 的变量,它可以持有任何类型的值。(未初始化,它持有一个特定值 undeifne,下一节会详细讨论。)ECMAScript 实现了变量初始化,所以可以定义变量的同时给他赋值,如下例:
var message = "hi";
这里。message 被定义并持有一个 string 值 ‘hi’。做这样的初始化不会让变量被定义成 string 类型;只是简单的给这个变量分配了一个值。有可能不只改变变量存储的值,也改变了它的类型,如下:
var message = "hi";
message = 100; // legal, but not recommended
在这个例子中,message 被定义并持有一个 string 值‘hi’,然后重写入一个熟知类值100.尽管我们不建议改变一个变量持有值的数据类型,但在 SCMAScript 中这是完全生效的。
var 声明范围
使用 var 定义变量时,使它作为所处方法的本地变量是很重要的。例如,在方法里定义变量意味着变量将在方法退出时立即销毁,如下:
function test() {
var message = "hi"; // local variable
}
test();
console.log(message); // error!
这里, message 变量在 functon 中使用 var 定义。方法名为 test(),创建了变量并赋值。之后马上,变量就别销毁了所以这个例子的最后一行引发了一个错误。然而,可以通过忽略 var 操作符来定义一个全局变量,如下:
function test() {
message = "hi"; // global variable
}
test();
console.log(message); // "hi"
通过移除 var 操作符号,message 变成了全局变量。一旦 test() 方法被调用,变量就会被定义切变成外部可访问的。
注意 尽管可以通过忽律 var 来定义全局变量,到这种做法是不推荐的。在本地定义全局变量是难以维护的并且这会导致误解因为你很难在有意忽略 var 的场景下直接理解代码含义。严格模式会抛出一个 ReferenceError 当你给一个未声明的变量赋值时。
如果你需要定义多个变量,你可以通过逗号区分开各个变量来只用一个语句做到:
var message = "hi", found = false,
age = 29;
三个变量就被定义且初始化了。因为 SCMAScript 是无类型的,可以用一个语句初始化不同类型的变量。尽管插入换行和缩进比纳凉是非必须的,但这样增加了可读性。
在严格模式下,你不能给变量命名为 eval 或者 arhuments。这样做将会导致语法错误。
var 变量提升
使用 var 时,下面代码是可运行的应为变量声明使用--关键字是提升到方法范围顶部的:
function foo() { console.log(age); var age = 26;
}
foo(); // undefined
这不会报错,因为 ECMAScript 运行时对它做了这样的技术处理:
function foo() {
var age;
console.log(age);
age = 26; }
foo(); // undefined
这就是“变量提升(hoisting)”,解释器会把所有变量声明拉到它范围的顶部。它也允许你去冗余的定义变量,这不会引起什么问题:
function foo() {
var age = 16; var age = 26; var age = 36;
console.log(age);
}
foo(); // 36
let 声明
let 操作符和 var 很相似,但也有一些重要的不同点。最值得一提的是,let 为块作用域,但 var 是方法作用域。
if (true) {
var name = 'Matt'; console.log(name);
} console.log(name);
if (true) {
let age = 26; console.log(age);
} console.log(age);
// Matt // Matt
// 26
// ReferenceError: age is not defined
这里,age 变量是无法从 if 域外部访问的,因为它的作用范围不包括块的外部。块作用域是方法作用域的严格子集,所以任何对 var 生效的的范围限制也会对 let 声明生效。
let 声明不允许在一个域内冗余的定义变量。这样做会有如下结果:
var name; var name;
let age;
let age; // SyntaxError; identifier 'age' has already been declared
当然,js 引擎会追踪用作变量声明的标志符及它们定义所在的域,所以嵌套使用相同的标识符会如你所想一般不引发错误,因为没有重复定义产生。(20-09-23)
var name = 'Nicholas';
console.log(name); // 'Nicholas'
if (true) {
var name = 'Matt';
console.log(name); // 'Matt'
}
let age = 30; console.log(age); // 30
if (true) {
let age = 26;
console.log(age); // 26
}
重复声明的报错是与顺序无关的,也与是否混用 var let 无关。不同类型的关键字不会声明不同类型的变量——它们只是指定了变量的关联作用域。
var name;
let name; // SyntaxError
let age;
var age; // SyntaxError
现世的死域(temporal dead zone)
另一区分 let 和 var 的重要行为是 let 声明不会产生变量提升现象:
// name is hoisted
console.log(name); // undefined
var name = 'Matt';
// age is not hoisted
console.log(age); // ReferenceError: age is not defined
let age = 26;
当解析代码时,js 引擎会察觉到 let 声明在作用范围的稍后部分会出现,但这些变量无法在真正的声明出现前被引用。声明前的代码执行被称为“现世的死域(temporal dead zone)”,任何试图引用这类变量的行为都会抛出错误 ReferenceError。
全局声明
不像 var 关键字,当使用 let 在全局上下文声明变量时,变量不会像 var 一样关联到 widow 对象。
var name = 'Matt';
console.log(window.name); // 'Matt'
let age = 26;
console.log(window.age); // undefined
然而,let 声明还是会在全局作用域内发生,将和页面的生命周期共存。因此,你必须保证你的页面不会进行重复声明以免产生语法错误。
条件声明
当使用 var 声明变量时,因为声明被提升,js 引擎会开心的在作用域顶部将多个冗余声明合并成一个。因为 let 声明被域限制到域内,不可能检查 let 变量是否被定义了,并且有条件的定义它如果它没被定义的话。
<script>
var name = 'Nicholas'; let age = 26;
</script>
<script>
// Suppose this script is unsure about what has already been declared in the page.
// It will assume variables have not been declared.
var name = 'Matt';
// No problems here, since this will be handled as a single hoisted declaration.
// There is no need to check if it was previously declared.
let age = 36;
// This will throw an error when 'age' has already been declared.
</script>
使用 try/catch 语句或者 typeof 操作符不是解决办法,如下在条件结构体范围内使用 let 声明会导致变量限制在域内。
<script>
let name = 'Nicholas';
let age = 36;
</script>
<script>
// Suppose this script is unsure about what has already been declared in the page.
// It will assume variables have not been declared.
if (typeof name !== 'undefined') {
let name;
}
// 'name' is restricted to the if {} block scope,
// so this assignment will act as a global assignment
name = 'Matt';
try (age) {
// If age is not declared, this will throw an error
}
catch(error) {
let age;
}
// 'age' is restricted to the catch {} block scope, // so this assignment will act as a global assignment
age = 26;
</script>
因此,你无法使用条件声明的方式来处理这个 ES6 的声明关键字。
注意 无法使用在条件声明使用 let 关键字是一件好事,因为在你的代码中条件声明不是一个好的方式。它让程序流更难理解。如果你发现自己接近了这种方式,这是一个很好的机会去找一个更好的写代码方式。
在循环声明中的 let 关键字
在 let 出现前,for 循环的定义引入一个迭代变量,这个变量会泄漏到循环体之外;
for (var i = 0; i < 5; ++i) {
// do loop things
}
console.log(i); // 5
当转向使用 let 声明后,迭代比阿亮将会限制到循环体内:
for (let i = 0; i < 5; ++i) {
// do loop things
}
console.log(i); // ReferenceError: i is not defined
使用 var,一个常见的问题的是唯一的定义和迭代值的修改:
for (var i = 0; i < 5; ++i) {
setTimeout(() => console.log(i), 0)
}
// You might expect this to console.log 0, 1, 2, 3, 4
// It will actually console.log 5, 5, 5, 5, 5
这种现象发生是因为循环退出但迭代值依然被赋值引发了,循环在5的时候退出。当 timeout 晚些被执行时,它们引用了相同的变量,因此 console.log 打出了最终值。
当使用 let 去声明循环迭代器时,在背后 js 引擎实际在每个循环中声明都声明了一个新的迭代值。每次 setTimeout 引用的都是独立的实例,然后 console 会打出预期的值:循环迭代执行时迭代变量的值。
for (let i = 0; i < 5; ++i) {
setTimeout(() => console.log(i), 0)
}
// console.logs 0, 1, 2, 3, 4
这种每个迭代中声明的行为对任何方式的循环都是生效的,包括 for in 和 for of 循环。(20-09-24)
const 声明
const 和 let 相近,但有一点重要的不同——它必须被赋值初始化,并且声明后不能再被定义。试图修改一个 const 值会抛出一个运行时错误。
const age = 26;
age = 36; // TypeError: assignment to a constant
// const still disallows redundant declaration
const name = 'Matt';
const name = 'Nicholas'; // SyntaxError
// const is still scoped to blocks
const name = 'Matt';
if (true) {
const name = 'Nicholas';
}
console.log(name); // Matt
const 声明被强制指向它引用的变量。如果 const 变量引用了一个 object,它不会严格禁止修改该对象内的属性。
const person = {};
person.name = 'Matt'; // ok
尽管 js 引擎会为在循环中的 let 迭代变量创建新的实例,尽管 cosnt 的表现和 let 相近,你不能用 const 去声明循环迭代器:
for (const i = 0; i < 10; ++i) {} // TypeError: assignment to constant variable
然而,如果你想声明一个不可修改的 for 循环变量,const 是可用的--恰好地因为每次迭代都会创建一个新变量。这尤其与 for-of 和 for-in 循环相关。
let i = 0;
for (const j = 7; i < 5; ++i) {
console.log(j);
}
// 7, 7, 7, 7, 7
for (const key in {a: 1, b: 2}) {
console.log(key);
}
// a, b
for (const value of [1,2,3,4,5]) {
console.log(value);
}
// 1, 2, 3, 4, 5
声明风格和最佳实践
在 ES6 中引入了 let 和 const 客观的更好的规范了这门语言,应对日益提升的精准作用域声明及语义学。var 声明的诡异方式让整个 js 社区一期纠结掉了很多年的头发。在新关键字的启发下,这里有一些可以提升代码质量的通用模式出现。
不要用 var
有了 let 和 const,大多数开发者不再需要在编码中使用到 var。感谢对变量作用域的细心管理,只允许使用 let 和 const 的模式保证了代码的质量,本地化声明,和 const 的正确性。
相较 let 最好用 const
使用 const 声明允许浏览器运行时强制变量不可变,此外静态代码分析器会强制禁止违规的重新赋值操作。因此,很多开发者觉得这是有好处的,默认的,会将一个值声明为 const 除非他们知道将在未来某个点需要改变这个变量。允许开发者更清晰的知道这个值是不可变的,并可以快速检测到预期外的行为,比如代码试图执行预期之外的重新赋值操作。
数据类型
ECMAScript 中存在六种简单类型(也叫基础类型):Undefined,Null,Boolean,Number,String,Symbol(翻译者的话:书里还没有bigint)。Symbol 是 ES6 中新引入的。也有一种复杂类型叫做 Object,是一个无序的键值队。因为无法在 ECMAScript 中定义你自己的数据类型,所有值可以被认为是这其中类型中的一个。只有其中数据类型可能看起太少了不能完全表达数据类型;然而,ECMAScript 有动态外观使得每个单独的数据类型表现得像多种。
typeof 操作符
因为 ECMAScript 是无类型的,也就需要一个方法去决定给定变量的数据类型。typeof 操作符提供了这个信息。在一个值上使用 typeof 操作符号会返回下面字符串之一
"undefined" if the value is undefined
"boolean" if the value is a Boolean
"string" if the value is a string
"number" if the value is a number
"object" if the value is an object (other than a function) or null
"function" if the value is a function
"symbol" if the value is a Symbol
typeof 操作符是这样调用的:
let message = "some string";
console.log(typeof message); // "string"
console.log(typeof(message)); // "string"
console.log(typeof 95); // "number"
在这个例子中,不管是一个变量(message)还是一个被传入 typeof 操作符的字面意义的数字。注意,因为 typeof 是一个操作符而不是一个方法,所以不许要括号(尽管这样是可用的)。
注意这里有几个地方 typeof 返回了技术上正确的但看起来奇怪的值。调用 typeof null 返回的值为 “object”,特殊值 null 被认为是一个空对象的引用。
注意 技术上,在 ECMAScript 中方法被认为是对象,而不是其它类型。然而,它们确实有一下特别的属性,是的有必要通过 typeof 操作符来区分它们。(20-09-25)
undefined 类型
undefined 类型只有一个值,就是特殊值 undefined。当一个变量使用 var 或 let 声明但缺没有初始化,它会如下被设置为 undefined 值:
let message;
console.log(message == undefined); // true
在这个例子中,变量 message 被声明了,却没有进行初始化。当和字面的值 undefined 对比时,得到的结果是相等的。这个例子和下面是相同的:
let message = undefined;
console.log(message == undefined); // true
这里的变量 message 被指定初始化为 undefined。这是非必须的,因为默认,任何没有初始化的值会被赋上 undefined。
注意 一般来说,你不应该将一个值特地设置为 undefined。字面的 undefined 值只要是用来进行比较而且是直到 ECMA-262第三版 才被加进来的,用来规范区分一个空对象指针(null)和未初始化的变量。
注意,一个变量的值为 undefined 和一个完全未定义的变量是不同的,如下:
let message; // this variable is declared but has a value of undefined
// make sure this variable isn't declared // let age
console.log(message); // "undefined"
console.log(age); // causes an error
在这个例子中,第一个 log 会实际打印出变量 message,为 undefined。在第二个 log 中,一个未声明的变量 age 被出入到 console.log()方法中,会因为没有声明变量引发一个错误。只有一个操作符可以用来操作未声明的变量:你可以在它上面调用 typeof (也可以调用 delete 不会引发错误,但这没有用处且在严格模式下会引发错误,翻译者,自己试了下Reflect.deleteProperty() 是会报错的)。
typeof 操作符当操作一个未初始化变量时返回 undefined,但是它也会返回 undefined 当操作一个未声明的变量时,这有些令人困扰。看看这个例子:
let message; // this variable is declared but has a value of undefined
// make sure this variable isn't declared
// let age
console.log(typeof message); // "undefined"
console.log(typeof age); // "undefined
在两个例子中,在变量上调用 typeof 会返回 undefined。逻辑上,这说的通因为没有一个操作符可以区分两个变量,尽管他们技术上是很不同的。
注意 尽管未初始化的变量会自动被赋值为 undefined,还是推荐你去初始化变量。这样,当 typeof 返回 undefined 的时候,你就会简单的明白,是这个值没有被声明,而不是没有被简单的初始化。
undefined 这个值是带有’否定‘意义的。由此,你可以在需要使用它时进行简单的检查。牢记,有很多其它值也是‘否定’意义的,所以小心预测当你需要特别检测 undefined 值而不是简单的‘否定’意义值。
let message; // this variable is declared but has a value of undefined
// 'age' is not declared
if (message) {
// This block will not execute
}
if (!message) {
// This block will execute
}
if (age) {
// This will throw an error
}
null 类型
null 类型第二个只有一个值的数据类型:特殊值 null。逻辑上,null 是一个空的对象指针,所以调用 typeof 会返回“object”当在下面例子传入一个 null 时:
let car = null;
console.log(typeof car); // "object"
定义一个之后用来持有对象的变量时,建议初始为 null 而不是其它任何值。这样,你可以特定检查 null 值来看一个变量是否晚些时候被 object 赋值,比如在这个例子中:
if (car != null) {
// do something with car
}
undefined 是 null 的衍生物,所以 ECMA-262 将他们定义为表面上相等:
console.log(null == undefined); // true
console.log(null === undefined); // false 翻译者 在node10 下实验
在 null 和 undefined 间使用相等操作符(==),返回 true,注意这个操作符会为了比较目的转换它所比较的变量(操作值)。
即使 null 和 undefined 是相近的,他们的用处大不相同。如之前提到的,你不应该特意将一个值设置为 undefined,但这对 null 不适用。任何时候一个变量被需要但不可用时,null 就派上了用场。这帮助你保证了 null 的范例——它是一个空对象指针,且将它和 undefined 作区别。
null 也是否定意义的;由此可以在用到一个值时简单的进行检查。注意,和undefined 注意的一样,不翻译了。
let message = null;
let age;
if (message) { // This block
}
if (!message) { // This block
}
if (age) {
// This block
}
if (!age) {
// This block
}
boolean 类型
boolean 类型 ECMAScript 中最常用的数据类型之一,它只有两个字面值:true 和 false。这两值是与数字值相区别的,所以 true 和 1 是不相等的,flase 和 0 也是不相等的。为 Boolean 变量赋值方法如下:
let found = true;
let lost = false;
注意 boolean 字面值是大小写敏感的,所以 True 和 False(和其它大小写混合的写法)是有效的变量标识符,而不是 boolean 值。
虽然 boolean 只有两个字面值,所有类型的值在 ECMAScript 都有一个对应的 boolean 值。(省了几句废话)转换方法为:
let message = "Hello world!";
let messageAsBoolean = Boolean(message);
在这个例子中 message 被转换为一个 boolean 值且存到了 messageAsBoolean 变量中。Boolean() 转换方法在任何数据类型的值上调用且总是返回一个 boolean 值。转换规则与被转换值的类型及实际值相关。下表列出了数据类型和它们的特殊转换。
数据类型 | 转换为true的值 | 转换为false的值 |
---|---|---|
Boolean | true | false |
String | 任何非空值 | “”空字符串 |
Objct | 任何对象 | null |
Number | 非零数(包含无穷大 infinity) | 0,NAN |
Undefined | n/a | undefined |
这些转换是需要重点理解的,因为流程控制,比如 if 语句,自动的体现了 boolean 转换,如下:
let message = "Hello world!";
if (message) {
console.log("Value is true");
}
在这个例子中,log 会被打出来,因为 string 类型的 message 自动转化为它的 boolean 对应值(true)。因为这个自动转换,理解你在流程控制中的变量是很重要的。错误的使用一个对象去替代 boolean 值会彻底的改变你的程序流程。(20-09-27)
number 类型
也许 ECMAScript 中最有趣的数据类型就是 number,使用 IEEE-754 规范来代表整型和浮点值(也在某些语言中称为双精度值)。为了支持数字多样的类型,有几种不同的数字字面格式。
最基本的是十进制整型,可以直接键入,如下:
let intNum = 55; // integer
整型也可以被表示为八进制或者十六进制,第一个数字必须是0后面跟随一串八进制数字(0到7)。如果在这字面量中有一个数字被监测到超过了这个范围,开头的0就会被忽略数字会被当作十进制处理,如下面例子:
let octalNum1 = 070;// octal for 56
let octalNum2 = 079;// invalid octal - interpreted as 79
let octalNum3 = 08;// invalid octal - interpreted as 8
八进制字面量在严格模式是不生效的并且会导致 js 引擎抛出一个语法错误。
为了创建一个十六进制数,你必须指定开头两个字节 0x(大小写敏感),后续跟着任意十六进制数(0到9,A 到 F)。字符可以是大写或者小写。
let hexNum1 = 0xA; // hexadecimal for 10
let hexNum2 = 0x1f; // hexadecimal for 31
用八或者十六进制创建的数字会在所有数学运算中被当作十进制数来处理。
注意 因为 js 中数字的存储方式,它实际上可能会有大于0或者小于0.大于0和小于0是被认为在任何情况下都是对等的,但在这里做一个澄清。
浮点值
为了定义一个浮点值,你必须至少引入一个小数点且至少后面跟上一个数字。尽管小数点前没有必须要有一个整型,但我们推荐这样做。这里有一些例子:
let floatNum1 = 1.1;
let floatNum2 = 0.1;
let floatNum3 = .1; // valid, but not recommended
因为存储浮点值消耗了存储整型两倍的空间,ECMAScript 总会寻找将值转换为整型的方法。当小数点后面没有数字时,这个值就会变成整型。而且,如果值是一个整数(如1.0),它也会被转换为整型,如下例:
let floatNum1 = 1.; // missing digit after decimal - interpreted as integer 1
let floatNum2 = 10.0; // whole number - interpreted as integer 10
对于每一个大的或者非常小的数字。浮点值可以用科学计数法(e-notation)表达式。他用一个数乘以10的指定次方来表示值。ECMAScript 规范中需要有一个数(整数或者浮点数)后面跟着一个小写或者大写的 E,后面再跟着10的次方。参考如下:
let floatNum = 3.125e7; // equal to 31250000
在这个例子中,floatNum 等于 31,250,000 尽管使用科学记数法让它的表达方式变得更精简。这个符号的基本含义是“取 3.125,然后乘上 .”
科学记数法也可以表示很小的数,比如 0.00000000000000003,可以简明的表示为 3e-17.默认的,ECMAScript 转换任何任何小数点后有六个零以上的浮点值为科学记数法形式(如,0.0000003 变成 3e–7).
浮点数会精确到 17 个小数位但远不及整个数字的算术计算。例如,0.1 + 0.2 得出 0.30000000000000004 而不是 0.3.这些小的约数是的检查浮点数值变得很困难。参考下例:
if (a + b == 0.3) { // avoid!
console.log("You got 0.3.");
}
这里,测试结果表明两个数的和不等于0.3。0.05和0.025之类的是符合逻辑的。但如果是0.1 + 0.2,如我们之前讨论的,这个结果会变成否定。因此不要相信验证浮点数的结果。
注意 理解约数错误是来自 IEEE-754(二进制浮点数算术标准)的浮点计算的影响,并非 ECMAScript 中独有的。其它语言使用了相同规范的话会有相同的问题。
值范围
因为内存限制,并不是自然界所有数都能在 ECMAScript 中被表示。ECMAScript 中能表示的最小数存储在大多数浏览器中国呢 Number.MIN_VALUE 且值为 5e–324;最大值存储在 Number.MAX_VALUE 中且在大多数浏览器中为 1.7976931348623157e+308。如果一个数无法在 ECMAScript 的数字范围内被表示,该数字会自动的获取到特殊值 Infinity。任何无法被表示的负数会表示成 –Infinity,任何无法被表示的整数会表示成 Infinity。
如果一次计算返回整数或者负数的 Infinity,这个值将无法用在任何计算中。因为 Infinity 没有用来计算的数学含义。决定一个值是否为无穷(即,它在最小和最大之间),有一个 isFinite() 方法来验证。仅当参数在最小和最大之间时,这个方法会返回 true,如下例:
let result = Number.MAX_VALUE + Number.MAX_VALUE;
console.log(isFinite(result)); // false
尽管很少会有超过无限大范围的计算,但这是有可能的并且在做很小或很大的数值的计算时因该检测该过程避免出错。
注意 你也可以通过 Number.NEGATIVE _ INFINITY 和 Number.POSITIVE _ INFINITY 来获取正的和负的无穷数。如你预想的,这些属性分别包含了 –Infinity 和 Infinity 值。
NaN
有一个叫 NaN 的特殊数字值,AKA 不是个数(Not a Number),引入用来在操作符试图返回一个数字却失败了的场景(预期返回一个错误)。例如,在其语言用 0 分割任意数字一般会引发错误,在代码执行过程中。在 ECMAXcript 中,用 0 分割一个数字会返回 NaN,允许其它操作继续进行。(翻译者没搞懂,dividing any number by 0 typically causes an error,咋分割的啊?)
NaN 有一对独有属性。一,任何操作符触及 NaN 都会返回 NaN(例如:NaN /10),可能会在多步计算中引发问题。二,NaN 不等于任何值,包括 NaN,例如下例:
console.log(NaN == NaN); // false
因此,ECMAScript 提供了 isNaN() 方法。这个方法传入单个参数,可以是任何数据类型,去决定一个值是否“不是一个数字”。当一个值被传入 IsNaN(),会尝试将它转为数字。一些非数字的值会直接转化为数字,比如 String “10” 或者一个 Boolean 值。任何无法被转化为数字的值会让方法返回 true,参考下例:
console.log(isNaN(NaN)); // true
console.log(isNaN(10)); // false - 10 is a number
console.log(isNaN("10")); // false - can be converted to number 10
console.log(isNaN("blue")); // true - cannot be converted to a number
console.log(isNaN(true)); // false - can be converted to number 1
本例中测试了五个不同的值。第一个检测了 NaN 本身,显然的返回 true。接下来来那个为数字10和字符串“10”,都返回 false,因为两个的数值值为10.字符串“blue”,然而,无法被转化为数字,所以返回 true。boolean 值 true 可以被转化为 1,所以方法返回 false。
尽管一般不会这样做,isNaN() 可以在对象上使用。这种情况,object 的 ValueOf()方法会先被调用决定返回的值是否能转化为数字。如果不行,toString()方法聚会被调用且它返回的值也会被检验。这种接续的方式作用在 ECMAScript 构造方法和操作符里,会在后续的 “操作符” 章节细讲。
Number 转换
有三个方法可以将非数值值转换为数值值:Number()转化方法,parseInt()方法,parseFloat()方法。第一个方法可以用在任何数据类型上。另外两个方法用来将 string 转为 number。这些方法对于相同的输入有不同的反应。
Number()方法的转换基于下列规则:
- 当用在 Boolean 值上,true 和 false 分别转化为 1 和 0.
- 当应用于 number 时,值简单的传入然后返回。
- 当应用于 null 时,返回 0.
- 当应用于 undefined 时,返回 NaN。
- 当应用于 strings 时,下列规则会被实行:
1,如果字符串只含有数字字符,在前面加上任意 + 或者 -,它会转化为十进制数,所以 Number(“1”) 变为 1,Number(“011”)变为 11(注意开头的0,被忽略了)。
2,如果包含一个有效浮点数,则转化为数字的浮点值(重申,开头的0会被忽略)。
3,如果包含一个有效的十六进制值,如:“0xf”,他会被转化为一个值相当的整型15.
4,如果是空的,被转化为0.5,如果包含的数不在前四种场景中则转化为 NaN。 - 当应用于 object 时,valueOf()方法被调用并计入先前描述的规则转换。如果转化结果是 NaN,toString()方法被调用,接着进入转换 string 的规则。
从各种类型转化为 number 时复杂的,如之前阐述的对 Number()的规则。这里有些具体例子:
let num1 = Number("Hello world!"); // NaN
let num2 = Number(""); // 0
let num3 = Number("000011"); // 11
let num4 = Number(true); // 1
在这个例子中,string “hello world”被转化为 NaN 因为它没有对应的数值值,空的字符串会被转化为0.字符串“00011”被转化为数字11,因为初始化0会被忽略。最后,true 被转化1.(20-09-28)
注意 一元操作符 + ,我们会在后续“操作符”章节中,详细讨论,和 Number() 方法的工作过程相似。
因为 Number() 方法在转换 string 时的复杂和奇怪现象,parseInt()方法往往是处理整数时更好的选择。parseInt()方法会更精确的检验 string 看他是否匹配数字的范式。开头的空格会被忽略直到周到第一个非空格字符。如果开头不是数字,正号,负号,parseInt() 就会返回 NaN,意味着空字符转也会返回 NaN(不像 Number(),返回 0)。如果开头字符是数字,正好,负号,转换就会到达下一个字符直到找到不符合的字符。例如,“1234blue”被转换为“1234”因为blue会被忽略掉。类似的,“22.5”会被转化为22因为小数点不是一个有效的整数字符。
假设开始的第一份字符是数字,parseInt()方法也能识别不同的整数形式(十进制,8,16)。这意味着当字符串开头是“0x”,它会被转化为16进制整数;如果它以“0”开头跟着数字,他就会识别为一个8进制数。
这里举一些转换例子更好的解释了发生了什么:
let num1 = parseInt("1234blue"); // 1234
let num2 = parseInt("");// NaN
let num3 = parseInt("0xA");// 10 - hexadecimal
let num4 = parseInt(22.5); // 22
let num5 = parseInt("70");// 70 - decimal
let num6 = parseInt("0xf");// 15 - hexadecimal
所有不同的数字转化是在追踪过程中是令人困惑的,所以 parseInt()提供了第二个参数:进制。如果你直到你的值是16进制的,你可以在第二参数传入进制数 16 来保证正确的执行,如下:
let num = parseInt("0xAF", 16); // 175
实际上,通过设置16进制参数,你可以不用在开头使用“0x”,
let num1 = parseInt("AF", 16); // 175
let num2 = parseInt("AF"); // NaN
在这个例子中,第一个转换会正确进行,但第二个就会失败。不同的是第一个传入了进制数,告诉 parseInt()它将对一个十六进制数进行转化,第二个发现第一个字符不是数字然后就自动停了下来。
传递进制会完全改变转换结果,参考如下:
翻译者吐槽:这不是著名的面试题吗??[1, 2, 3].map(parseInt);
let num1 = parseInt("10", 2); // 2 - parsed as binary
let num2 = parseInt("10", 8); // 8 - parsed as octal
let num3 = parseInt("10", 10); // 10 - parsed as decimal
let num4 = parseInt("10", 16); // 16 - parsed as hexadecimal
因为进制数允许parseInt()选择如何转换输入,建议总是传入进制数来避免错误。
注意 大多数时候你都是在换十进制数,所以经常给第二个参数传入10是好的做法。
parseFolat() 方法和 parseInt() 的运行规则类似,看开始字符。它也会持续转化直到结束或者遇到浮点数中非法的值。这意味这小数点仅在第一次数显会生效,第二个是非法的,切剩下的字符串会被忽略,导致“22.34.5”被转化为 22.34.
另一个不停是开头的0会被忽略。这个方法会识别我们先前桃林的任意浮点数,就像十进制转换(开始的0总是被忽略)。16进制数会被转化为0.因为 parseInt()只转化十进制数,没有进制模式。最后一点:如果转换的全是数字(没有小数点或小数点后只有零),该方法会返回一个整型(integer)。(翻译者想到:前面提到浮点占的内存是整型的两倍。)(20-09-29)
let num1 = parseFloat("1234blue");// 1234 - integer
let num2 = parseFloat("0xA"); // 0
let num3 = parseFloat("22.5");// 22.5
let num4 = parseFloat("22.34.5");// 22.34
let num5 = parseFloat("0908.5");// 908.5
let num6 = parseFloat("3.125e7");// 31250000
string类型
string 数据表示可一个0或多个的 16-bits unicode 字符序列。字符串可以用双引号(“),单引号(‘),或者 backticks(`)描述,所以如下都是合法的:
let firstName = "John";
let lastName = 'Jacob';
let lastName = `Jingleheimerschmidt`
不像一些其它语言,可以使用不同的引号来改变字符串的解释方式,在 ECMAScript 语法中是没有区别的。注意,然而,一个字符串还有和结尾的符号必须是相同的。例如,该例子就会引发一个语法错误:(20-10-09)
let firstName = 'Nicholas"; // syntax error - quotes must match
字符常量
字符串数据类型包含了几种字符常量来表示不可打印的或其它用处的字符,如下列举:
字面字符 | 意义 |
---|---|
\n | 新的一行 |
\t | tab |
\b | 退格 |
\r | 回车 |
\f | 换页 |
\ | 下划线(\) |
' | 当引号(')——当使用的string是被单引号标注的。例如:'he said,'hi.' |
" | 双引号 |
` | 重音符 |
\xnn | 通过十六进制编码表示字符 nn 为十六进制数 0-F。例如:\x41 等价于 "A" |
\unnn | 通过十六进制码表示一个 unicode 字符。例如:\u03a3 等价于希腊字符Σ |
这些字符常量可以在string的任何位置被引入,且会被解释为当一的字符,如下: |
let text = "sigma: \u03a3.";
例子中,变量 test 是长度 9 的字符串尽转译字符长为 6 字符。整个转译序列代表了一个单一字符,所以它是如此计算的。
任何字符串的长度可以使用 length 属性获取,如下:
console.log(text.length); // 9
这个属性返回了字符串中 16-bit 字符的数量。
注意 如果一个 string 包含了双类型的字符,length 属性可能就无法确切的返回字符串中字符的数量。这个情况的缓解策略在基本引用类型章节中有详细说明。
string 的本质
string 在 ECMAScript 中是不可变的(immutable),意味着他们一旦被创建,他们的值就不可变。去改变一个变量持有的字符串值,原有的字符串会被销毁且变量会被另一个包含新值的字符串填充,如下:
let lang = "Java";
lang = lang + "Script";
这里变量 lang 被定义包含了字符串“java”。下一行,lang 被重定义去结合了“java”和“Script”,值为“javaScript”。这通过创建一个新的空间为10字符的字符串,然后用“java”和“script”填充了这个字符串变量。程序的最后一步是摧毁原先的字符串“java”和“script”,因为他们都不再被需要。这都是表像之后发生的事件,这也是老版本浏览器(such as pre–1.0 versions of Firefox and Internet Explorer 6.0)在string相关处理很慢的原因。这些低效的部分在这些浏览器的后续版本中有被定为到。
转变为一个字符串
有两种将一个值转变为字符串的方法。第一种是用 toString() 方法,这是几乎所有值都有的。这个方法的唯一工作是返回目标值的对应字符串值。参考下例:
let age = 11;
let ageAsString = age.toString(); // the string "11"
let found = true;
let foundAsString = found.toString(); // the string "true"
toString()方法在数字,布尔,对象,字符串值上都是可用的。如果值是null或者undefined,这个方法就不可用了。
在大多数例子中,toString()没有任何参数。然而,当调用者是数值时,toString()会接收一个参数:输出数字的进制。默认的,toString()会以十进制输出,但通过穿参可以输出2,8,16或任何合法的进制输出,如下:
let num = 10;
console.log(num.toString()); // "10"
console.log(num.toString(2)); // "1010"
console.log(num.toString(8)); // "12"
console.log(num.toString(10)); // "10"
console.log(num.toString(16));// "a"
这个例子展示了toString()在由进制传入时输出的变化。10 可以输出为任意进制格式的数字。注意默认(未提供参数)和出入进制数10是相同的结果。
如果你无法确认传入值是不是 null 或 undefined,你也用 String()转化方法,会不管值的类型返回一个字符串。String()方法符合下列规则:
- 如果值有toString()方法,它即会被调用(无参数)并返回结果。
- 如果值是 null,“null”被返回。 undefined 同null。
let value1 = 10;
let value2 = true;
let value3 = null;
let value4;
console.log(String(value1));// "10"
console.log(String(value2)); // "true"
console.log(String(value3)); // "null"
console.log(String(value4));// "undefined"
此处,四个值被转化为字符串,数字,布尔,null 和 undefined。数字的结果和布尔的结果和toString()被调用是相同的。因为 toString()对于 null 和 undefined 是不可用的,String()方法会简单的返回这些值的字面值。
注意 你可以通过在一个值前面(+)加上空字符串“”来把一个值转化为字符串值(晚些会在操作符一章讨论)
模版字符串
在 ECMAScript6 中引入新的可能,用模版字符串定义字符串。不像单引号和双引号对照,模版字符串识别换行字符,可以被定义扩展多行:
let myMultiLineString = 'first line\nsecond line';
let myMultiLineTemplateLiteral = `first line second line`;
console.log(myMultiLineString);
// first line
// second line"
console.log(myMultiLineTemplateLiteral);
// first line
// second line
console.log(myMultiLineString === myMultiLinetemplateLiteral); // true
如名称建议,模版字符串在定义模版时特别有用,如 HTML:
let pageHTML = `
<div>
<a href="#">
<span>Jake</span>
</a>
</div>`;
因为模版字符串会确实匹配重音符里的空格,当定义他们时特殊的处理会发生。一个正确格式的模版字符串可能会出现不合适的缩进:(20-10-10)
// This template literal has 25 spaces following the line return character
let myTemplateLiteral = `first line
second line`;
console.log(myTemplateLiteral.length); // 47
// This template literal begins with a line return character
let secondTemplateLiteral = `
first line
second line`;
console.log(secondTemplateLiteral[0] === '\n'); // true
// This template literal has no unexpected whitespace characters
let thirdTemplateLiteral = `first line
second line`;
console.log(thirdTemplateLiteral[0]);
// first line
// second line
插入文字
模版字符串最有用的特性之一就是对插补文字的支持,允许你在单句不断开的定义中插入值一个或多个值。技术上,模版字符串不是字符串,他们是特殊的 js 语法表达来评估为一个字符串。模版字符串当被定义是就快速的被评估且被转化为字符串实例,且任何插入值会被从当前作用域被引入。
这可以通过使用 ${} 中的 js 表达来完成:
let value = 5;
let exponent = 'second';
// Formerly, interpolation was accomplished as follows:
let interpolatedString =
value + ' to the ' + exponent + ' power is ' + (value * value);
// The same thing accomplished with template literals: let interpolatedTemplateLiteral =
`${ value } to the ${ exponent } power is ${ value * value }`;
console.log(interpolatedString); // 5 to the second power is 25
console.log(interpolatedTemplateLiteral); // 5 to the second power is 25
值最后会强制被用 toString()转化为字符串,但任何 js 表达式都能安全的进行插入。安全的链接模版字符串不需要额外的空格:
console.log(`Hello, ${ `World` }!`); // Hello, World!
toString() 被引入来使得表达式转化为string:
let foo = { toString: () => 'World' };
console.log(`Hello, ${ foo }!`); // Hello, World!
在插入表达式中使用函数和方法是允许的:
function capitalize(word) {
return `${ word[0].toUpperCase() }${ word.slice(1) }`;
}
console.log(`${ capitalize('hello') }, ${ capitalize('world') }!`); // Hello, World!
额外的,模版可以安全的插入变量的之前的值:
let value = ''; function append() {
value = `${value}abc`
console.log(value); }
append(); // abc
append(); // abcabc
append(); // abcabcabc
模版字符串标签方法
(20-10-12)
模版字符串也支持定义标签方法,可以用来定义特定的插入字段行为。标签方法被通过(? is passed),在模版被插入字段分割的独立的片段后,在表达式被执行后。
一个标签方法被定义为一个常规方法被应用到模版字符串中通过被前置到它之前,如下面代码展示的。标签方法将被通过模版字符串被分割成片:第一个参数是一列字符串组成的数组,剩下的参数为表达式的执行结果。这个方法的返回值是模版字符串的执行结果。
这有个最佳的展示例子:
let a = 6;
let b = 9;
function simpleTag(strings, aValExpression, bValExpression, sumExpression) {
console.log(strings);
console.log(aValExpression);
console.log(bValExpression);
console.log(sumExpression);
return 'foobar';
}
let untaggedResult = `${ a } + ${ b } = ${ a + b }`;
let taggedResult = simpleTag`${ a } + ${ b } = ${ a + b }`
// [ '', ' + ', ' = ', '' ]
// 6
// 9
// 15
console.log(untaggedResult); // "6 + 9 = 15" console.log(taggedResult); // "foobar"
因为这里的变量数是可变的,聪明的做法是使用扩展符吧他们组合成单个集合:
let a = 6;
let b = 9;
function simpleTag(strings, ...expressions) {
console.log(strings);
for(const expression of expressions) {
console.log(expression);
}
return 'foobar';
}
let taggedResult = simpleTag`${ a } + ${ b } = ${ a + b }`
// [ '', ' + ', ' = ', '' ]
// 6
// 9
// 15
console.log(taggedResult); // "foobar"
对于一个有 n 个插入字段的模版字符串,标签方法表达式的参数也会是 n,第一个参数中切割成的字符串片也会总为 n+1。因此,如果你希望“压缩(zip)”字符串并和表达式一起执行返回默认字符串,你可以这样做:
let a = 6;
let b = 9;
function zipTag(strings, ...expressions) {
return strings[0] +
expressions.map((e, i) => `${e}${strings[i + 1]}`) .join('');
}
let untaggedResult = `${ a } + ${ b } = ${ a + b }`;
let taggedResult = zipTag`${ a } + ${ b } = ${ a + b }`;
console.log(untaggedResult); // "6 + 9 = 15"
console.log(taggedResult); // "6 + 9 = 15"
原始字符串
使用模版字符串时可以直接使用原始的字符串而不是转换成实际转化字符,比如换行和 unicode 字符一类。可以通过使用 String.raw 方法来达成,默认下是生效的:
// Unicode demo
// \u00A9 is the copyright symbol
console.log(`\u00A9`); // ©
console.log(String.raw`\u00A9`); // \u00A9
// Newline demo
console.log(`first line\nsecond line`);
// first line
// second line
console.log(String.raw`first line\nsecond line`); // "first line\nsecond line"
// This does not work for actual newline characters: they do not // undergo conversion from their plaintext escaped equivalents
console.log(`first line
second line`);
// first line
// second line
console.log(String.raw`first line second line`);
// first line
// second line
在标签方法中,原始值也做为一个可用属性存在于字符串集合中的没个元素上
function printRaw(strings) {
console.log('Actual characters:');
for (const string of strings) {
console.log(string);
}
console.log('Escaped characters;');
for (const rawString of strings.raw) {
console.log(rawString);
}
}
printRaw`\u00A9${ 'and' }\n`;
// Actual characters:
// ©
// (newline)
// Escaped characters: // \u00A9
// \n
Symbol类型
ECMAScript 6 中定义了新类型 Symbol 数据类型。Symbol 是原始变量,它的实例是唯一且不可改变的。使用 Symbol 的目的是为对象属性产生唯一的标志符避免碰撞的风险。
尽管他们看起来和私有属性有很多相同点,symbol 不是为了提供私有属性行为(特别的因为 Object API 提供了简单发现 symbol 的方法)。相代替的,symbol 用来创造唯一词以作为特殊值的 key 区别于其它使用 string 为 key 的属性。(20-10-13)
基础的 Symbol 使用
Symbol 使用 Symbol方法进行实例化。因为它是它本身的基础类型,typeof 操作符将会把一个symbol 识别为 symbol。
let sym = Symbol();
console.log(typeof sym); // symbol
当运行方法时,你可以提供一个配置字符串用来标示 symbol 实例当进行调试时。该字符串时和 symbol 的定义及识别完全分开的:
let genericSymbol = Symbol();
let otherGenericSymbol = Symbol();
let fooSymbol = Symbol('foo');
let otherFooSymbol = Symbol('foo');
console.log(genericSymbol == otherGenericSymbol); // false
console.log(fooSymbol == otherFooSymbol); // false
Symbole 没有字面形式的语法,这对于它的目的来说也是至关重要的。标准说明定义了 symbol 操作允许你如何创建一个 Symbol 实例并用它来作为对象新属性的 key 以保证你不会覆盖一份已经存在的对象属性——无关于是否使用 string 或者 symbol 作为 key。
let genericSymbol = Symbol();
console.log(genericSymbol); // Symbol()
let fooSymbol = Symbol('foo');
console.log(fooSymbol); // Symbol(foo);
请注意,Symbol 方法我无法使用 new 关键字。这样做的目的是避免 symbol 对象包装,像 Boolean,String,Numer是可以的,它们支持构造行为并且实例话一个基本的包装对象:
let myBoolean = new Boolean();
console.log(typeof myBoolean); // "object"
let myString = new String();
console.log(typeof myString); // "object"
let myNumber = new Number();
console.log(typeof myNumber); // "object"
let mySymbol = new Symbol(); // TypeError: Symbol is not a constructor
如果你想运用对象包装,你可以使用 Object() 方法:
let mySymbol = Symbol();
let myWrappedSymbol = Object(mySymbol); console.log(typeof myWrappedSymbol); // "object"
使用全局 Symbol 注册
设想中当不同运行时需要共享和重用一个 symbol 实例,我们可以用string-keyed 全局 symbol 注册的方式创建和复用 symbols。
可以通过使用 Symbol.for()方法达到:(20-10-14)
let fooGlobalSymbol = Symbol.for('foo');
console.log(typeof fooGlobalSymbol); // "object"
Symbol.for()对于每个字符串 key 来说是幂等操作符。他第一次被调用时会给被传入一个字符串,他会检查全局的运行时注册记录,发现没有 symbol 存在的话,就生成一个新的 symbol 实例,并添加到注册记录。而外的,调用相同的字符串库会检查全局运行时注册记录,到对应这个字符串的 symbol , 存在的话就返回这个实例。
let fooGlobalSymbol = Symbol.for('foo'); // creates new symbol
let otherFooGlobalSymbol = Symbol.for('foo'); // reuses existing symbol
console.log(fooGlobalSymbol === otherFooGlobalSymbol); // true
在全局注册器定义的 symbol 和使用 Symbol() 创建的的对象是完全不停的,尽管他们的描述相近:
let localSymbol = Symbol('foo');
let globalSymbol = Symbol.for('foo');
console.log(localSymbol === globalSymbol); // false
全局注册器需要传入字符串 key,所以你提供给 Symbol.for() 的任何参数将会被转化为一个string。而外的,用在注册器中的字符串也被作为 symbol 的描述。
let emptyGlobalSymbol = Symbol.for();
console.log(emptyGlobalSymbol); // Symbol(undefined)
可以用 Symbol.keyFor() 检查全局注册器,传入一个 symbol 并返回这个全局symbol的标志字符串,或返回 undefined 如果这个是 symbol 不是全局symbole 的话。
// Create global symbol
let s = Symbol.for('foo'); console.log(Symbol.keyFor(s));
// foo
// Create regular symbol
let s2 = Symbol('bar'); console.log(Symbol.keyFor(s2)); // undefined
在非 symbol 的对象上使用 Symbol.keyFor() 会抛出一个类型错误:
Symbol.keyFor(123); // TypeError: 123 is not a symbol
使用 Symbols 作属性
任何地方你都可以使用 string 或者 number 作为属性,你也可以用 symbole。这包括了对象的字面属性Object.defineProperty()/Object.defineProperties().
一个对象文本可以只使用一个 symbole 做为属性包含在计算的属性语法中。
let s1 = Symbol('foo'),
s2 = Symbol('bar'),
s3 = Symbol('baz'),
s4 = Symbol('qux');
let o = {
[s1]: 'foo val'
};
// Also valid: o[s1] = 'foo val';
console.log(o);
// {Symbol{foo}: foo val}
Object.defineProperty(o, s2, {value: 'bar val'});
console.log(o);
// {Symbol{foo}: foo val, Symbol(bar): bar val}
Object.defineProperties(o, {
[s3]: {value: 'baz val'},
[s4]: {value: 'qux val'}
});
console.log(o);
// {Symbol{foo}: foo val, Symbol(bar): bar val,
// Symbol{baz}: baz val, Symbol(qux): qux val}
[s1] 这个写法请注意
如同 Object.getOwnPropertyNames() 返回对象实例的常规属性数组,Object.getOwnPropertySymbols() 返回对象实例中的 symbole 对象数组。这两种方法的返回值是完全独立的。Object .getOwnPropertyDescriptors()方法会返回对象包含的常规及 symbol 属性描述。Reflect.ownKeys() 会返回两种的 key。(20-10-15)
let s1 = Symbol('foo'),
s2 = Symbol('bar');
let o = {
[s1]: 'foo val',
[s2]: 'bar val',
baz: 'baz val',
qux: 'qux val'
};
console.log(Object.getOwnPropertySymbols(o)); // [Symbol(foo), Symbol(bar)]
console.log(Object.getOwnPropertyNames(o)); // ["baz", "qux"]
console.log(Object.getOwnPropertyDescriptors(o));
// {baz: {...}, qux: {...}, Symbol(foo): {...}, Symbol(bar): {...}}
console.log(Reflect.ownKeys(o));
// ["baz", "qux", Symbol(foo), Symbol(bar)]
因为一个属性算作这个 symbol 在内存中的一个引用,symbol 是不会丢失的如果直接创建并用作属性的话。然而,拒绝保存一个属性的特定的引用意味着需要横向移动过所有对象的 symbol 属性来恢复属性键值:(说白了就是要遍历市区了hash 的优势)
let o = {
[Symbol('foo')]: 'foo val',
[Symbol('bar')]: 'bar val'
};
console.log(o);
// {Symbol(foo): "foo val", Symbol(bar): "bar val"}
let barSymbol = Object.getOwnPropertySymbols(o)
.find((symbol) => symbol.toString().match(/bar/));
console.log(barSymbol); // Symbol(bar)
著名的 symbol
从添加 symbol 以来, ECMAScript6 也引出了著名的 symbols 合集来揭示直接链接,覆盖,仿真的内部语言行为。这些著名的 symbols 在symbol构造方法中存在为字符串属性。
一个这些著名的 symbols 的应用是重定义他们来改变原始语言构造器的行为。例如,因为,因为知道 for-of 循环将会如何使用 Symbol.iterator 属性,在任何提供它的对象上,是可以提供一个自定义的 Symbol.iterator 值在一个自定义对象中去控制 for-of 如何表现,当提供该对象时。
关于这些著名的symbol 没有什么特别的,他们是常规的字符串属性存在于 Symbol 全局标志了一个 symbol 实例。每个 著名的symbol 属性是不可写的,不可枚举的,不可配置的。(20-10-16)
注意 当讨论 ECMAScript 标准时,你将常常见到这些 symbol 被通过他们特定的名称引用,即前置@@的形式。例如。@@iterator 表示 Symbol.iterator
Symbol.asyncIterator
依据 ECMAScript 规范,这个 symbol 被用作一个属性代表“一个返回对象默认 异步迭代器的方法。由 for-await-of 语法进行调用”。它用来识别实现可异步迭代器API的方法。
如同 for-await-of 循环的语言结构使用该方法来表现出异步和迭代的特性。他们会引用被 Symbol.asyncInterator 标记的方法并期望它返回一个实现了迭代器API的对象。在很多例子中,这会采用一个 asyncGenerator 的形式,一个对象实现了这个 API:
class Foo {
async *[Symbol.asyncIterator](){}
}
let f = new Foo();
console.log(f[Symbol.asynvIterator]())
特别的,由 Symbol.asyncIterator 方法产生的对象应该通过它的 next() 方法有序的执行 Promise 实例。这可以通过特定的 next() 方法定义或隐式的通过一个异步生成器方法:
// 哦豁 看不懂了
class Emitter {
constructor(max){
this.max = max;
this.asyncIdx = 0;
}
async *[Symbol.asyncIterator](){
while(this.asyncIdx < this.max){
yield new Promise((resolve) => resolve(this.asyncIdx++))
}
}
}
async function asyncCount() {
let emitter = new Emitter(5)
console.log(emitter);
for await(const x of emitter){
console.log(x);
}
}
asyncCount()
// 0
// 1
// 2
// 3
// 4
注意 Symbol.asynvIterator 是 ES2018 的规范,所以只有新的浏览器版本会支持它。更多的细节关于 synchronous iteration 和 the for-await-of loop 的细节可以在附录A中找到。(20-10-19)
Symbol.hasInstance
依据 ECMAScript 规范,这个 symbol 被用来作为“一个决定某构造对象是否识别对象为构造器实例之一的方法。被以 instanceof 操作符的语义方法调用”的一个属性。instanceof 操作符提供了辨认一个对象的原型是否在原型链上的方法。一般像下面这样使用 instanceof :
function Foo() {}
let f = new Foo();
console.log(f instanceof Foo); // true
class Bar {}
let b = new Bar();
console.log(b instanceof Bar); // true
在 ES6 中,instanceof 操作符是使用一个 Symbol.hasInstance 方法来评估这个关系。Symbol.hasInstance 标志了一个表现相同但操作对象顺序反置的方法:
function Foo() {}
let f = new Foo();
console.log(Foo[Symbol.hasInstance](f)); // true
class Bar {}
let b = new Bar();
console.log(Bar[Symbol.hasInstance](b)); // true
这个属性被定义在 Function 的原型上,且因此它是对于默认所有函数和类都可用的。因为 instanceof 操作符和其他属性一样会寻找原型链上的属性定义,它是可以在继承的类上重定义函数为一个静态方法的:(20-10-20)
class Bar {}
class Baz extends Bar {
static [Symbol.hasInstance]() { return false;
}
}
let b = new Baz(); console.log(Bar[Symbol.hasInstance](b)); console.log(b instanceof Bar); console.log(Baz[Symbol.hasInstance](b)); console.log(b instanceof Baz);
// true // true // false // false