07-重学前端

开篇词 | 从今天起，重新理解前端

你好，我是 winter（加微信：642945106 发送“赠送”领取赠送精品课程 发数字“2”获取众筹列表。）。接下来的 3 个月里，我将带你重新认识前端。

前端发展史：从青铜到黄金时代

我自己是在 2006 年开始接触前端的。现在回想起来，那会儿前端还处于史前的“青铜时代”，甚至网页的主要交互都还是依靠切换超链接来完成的。

1. 前端的史前记忆：“青铜时代”

那时候，谷歌刚刚基于 Ajax 发布的 Gmail 也没多久，虽然这项伟大的技术标志着 Web 1.0（静态网页）到 Web 2.0（动态网页）的迈进，但在国内依然少有人懂，如果当时谁可以对这项技术侃侃而谈，那简直就是大神的级别了。

当时我还是个学生，喜欢前端纯粹是兴趣使然。那时我混黑白棋社区，想着给黑白棋界面写插件，但自己又不懂界面相关的知识，于是开始通过各种方式学习前端。

真想学的时候才发现网络上的前端资料很是稀缺，所以我基本上都是先从图书馆借书，然后再在电脑上跑案例验证这样的方式来学习的，现在想起来，还真是一段艰难的岁月。

当然，这段经历也为我日后的前端生涯悄悄埋下了一颗种子，我逐渐开始把自己的职业规划路线放在了前端上。

这在当时是个不可思议的想法，因为那时的前端岗位不论从收入上还是在职责上，都远落后于其他岗位。但是，我基于对技术发展趋势的判断，认为前端在未来会越来越重要。

2. 进入发展期的前端：“白银时代”

2008 年，我毕业了，也很幸运地得到了一个既能发挥我的 C++ 长处，又能兼顾前端发展规划的职位：微软北京的软件开发工程师，恰好负责的是 Windows CE 上的 IE 浏览器开发，在这里，我接触到了当时最先进的软件工程体系，并且积累了很多 UI 架构经验。

两年后，我加入了盛大做电子书，负责电子书的文本排版工作，这个工作是一个既写底层又写 JavaScript 的岗位，同时排版引擎也是浏览器的重要组成部分，也让我对浏览器的工作原理有了更深入的理解。

在盛大后期，我加入了 WebOS 项目，负责前端框架，我开始基于移动的角度思考前端交互和框架，这份工作让我离前端又近了一步。

但是很遗憾，因为种种原因，我在微软和盛大的几个项目都不算成功，除了电子书实际上市但销量不高，Windows CE 7.0 和盛大的 WebOS 都在公司内部夭折。

自己亲手构建的产品，却因为非技术原因没有服务到最终用户，对我来说，是件非常遗憾的事情。不过，这段时间，也让我更加确信前端技术的价值。

回过来看，那几年，前端技术开始了它的大踏步发展，那一段时间，可以说是前端的“白银时代”。最直观的表现之一就是前端逐步从后端分离了出来，它的代码也变得复杂了起来，还需要保存数据、处理数据、生成视图等等。

悄然之间，我发现前端已经从零散的“工序”逐步发展成为有体系和发展目标的职能，同时，在越来越大的前端团队中，工程化的思想也逐渐萌芽。我深有感触，前端已经不再是别人眼中的“小菜一碟”了。

3. 从前端到“全端”：“黄金时代”

在这样的行业背景里，从盛大离职后，我加入了阿里巴巴做手机淘宝开发，这也是我首次从事真正的前端工作。

在手机淘宝，前端团队的各种基础设施也逐渐建立了起来，从最开始的多屏适配方案、基础库、工具链到页面搭建平台和性能体系，最后到客户端融合方案 Weex，我随着团队一起经历了业务发展、团队自身成长和行业变革。

与此同时，在我加入阿里巴巴后的这段时间里，随着移动时代的到来，前端也开启了自己的“黄金时代”，它的职责变得更加重要，有了独立的发布权限，技术也变得更加复杂。

一些传统软件开发和互联网服务端的方法论逐步移植到前端开发中，并形成了前端自己的工程体系，诸如持续集成、前后端分离、线上监控……

架构方面，前端架构的任务也从简单的解决兼容和风格问题，逐步过渡到提倡组件化和 UI 架构模式，最后形成了新一代的前端框架 React、Vue 和 Angular，他们也在竞争和互相学习中成长。

前端开发之痛：散点自学 + 基础不牢

正当处于“黄金时代”的前端技术在全力以赴极速前行之时，我却发现，前端开发者们的步伐似乎渐渐有些跟不上了。

因为在我职业发展的后半段，面试和培养前端工程师已经成为我的长期工作职责。在这期间，我意识到，目前的前端教育几乎是完全缺失的。

在面试应届生过程中，我会习惯性地问表现比较好的同学“你是如何学习前端的”，而我得到的答案多是“自学”“在社团学习”，却从未听到过“在学校学习过”这样的答案。

而对于工作之后的前端开发者来说，没有系统学习的问题仍然存在，常常有一些具有多年从业经验的工程师，仍然会在看到一些用法时惊呼：“还可以这样！”

在我看来，这些用法都是一些基础的不能再基础的知识点，但是他们却浑然不知。

如果深入进去了解，你会发现，表面上看他们可能是一时忘记了，或者之前没注意，但实际上是他们对于前端的知识体系和底层原理没有真正系统地理解。

在阿里工作的时候，我戏称很多同学学前端的方式是“土法学前端”，他们对于知识的理解基本都停留在点上，从来没有大范围把这些点串成线，形成自己的知识体系，因此才会出现上面说的遗漏和盲点。

这个问题在一些一直在小公司工作的前端工程师身上非常突出。

经常能看到一些案例，一些有技术追求、有热情的工程师，因为技术敏感度和主观能动性都不错，所以工作了五六年之后，逐步开始在自己的公司做一些技术管理相关的事情了。

但是，由于他们所在公司的业务并不复杂，也没有技术积累，所以他们自身的技术水平其实并不高，可以说还处于非常初级的阶段（可能面试连阿里 P6 都过不了）。

做了管理，技术没跟上，并且还错过了最佳的学习时间，这个境遇可想而知，他们在工作中大概率只能是被动地接受需求解决问题，然后也同时焦虑着自己的未来，焦虑着自己的竞争力。

关于前端工程师成长，我认为需要两个视角。一是立足标准，系统性总结和整理前端知识，建立自己的认知和方法论；二是放眼团队，从业务和工程角度思考前端团队的价值和发展需要。只有这样做，才能够持续发展，在高速发展的技术和工程浪潮中稳稳立足。

这也正是“重学前端”这个专栏的初衷，我希望提供一些视角，带你以完备、体系化的方式理解和思考前端的基础知识和工程实践。

除此之外，前端工程师也是开发工程师的一员，除了前端自身的领域知识和工程特点外，你还需要了解程序员通用的编程能力和架构能力。

所以，想要成为优秀的前端工程师，我觉得你需要通过系统地学习和总结获取知识，通过练习获取编程能力，通过工作经验来获取架构和工程能力。

当然，一个为期 3 个月的专栏无法穷尽前端庞杂的知识，讲知识点也不是我们的目标。知识点讲的再好再全，也不一定能记得住。

我们专栏的目标是帮助你建立自己的知识体系，根据你自己的理解把前端的领域知识链接起来，形成结构，这样做，不但能帮助你记忆知识，还能在其中发现自己知识的缺失，甚至可以凭借知识体系来判断知识的重要性，来决定是否要深入学习。

在这个专栏里，我将知识分成了四个模块来讲解：

JavaScript；

CSS 和 HTML；

浏览器实践；

前端综合应用。

前三个模块是前端的基础知识，是个人的前端能力提升，而模块四则是前端团队发展相关的内容，有助于你和团队的整体提高。

在JavaScript 部分中，我主要会从文法和运行时的角度去讨论 JavaScript 语言。它们是互相关联的，而语义就是文法到运行时之间的桥梁；它们分别又是完备的，任何语言特性都离不开两者，所以从语法和运行时的角度，我们都可以了解完整的 JavaScript。

CSS 和 HTML 部分，会侧重从语言和设计思想的角度来讲解，我们同样可以对两者的全貌建立一些认知。

浏览器部分，包含了浏览器工作的原理和一些重要的 API，包括 BOM、DOM、CSSOM 和其他一些内容。了解了这些知识，你才能把 JavaScript 和 HTML、CSS 连接起来，用 JavaScript 来实现功能。

前端综合应用部分，主要是我的一些工作经验，我会选择我在手淘和淘宝工作中的一些案例来辅助讲解。

前面，我说到前端是一个非常年轻的职业，但我仍然认为前端具有很多空间和机会，一些基础设施仍然简陋，前端的能力可以带来更多的业务场景，这些有待于我们去发掘。

前端社区非常活跃，新技术也在不断出现。在这样的环境下，机会和竞争并存，学习也犹如逆水行舟，不进则退，建立自己的知识体系和方法论，你才能够保持领先优势。

我希望从我的经验出发，给你一些启发和帮助，并借由这个专栏帮你建立自己的前端知识体系。同时，我也相信，在你们中间一定会产生更多能够带领前端领域取得突破的、优秀的前端工程师。

最后，也希望你能和我分享你的前端故事和经历，你是怎么走上前端道路的？你希望将来成为怎样的前端工程师？欢迎在留言区与我分享。

01 | 明确你的前端学习路线与方法

你好，我是 winter（加微信：642945106 发送“赠送”领取赠送精品课程 发数字“2”获取众筹列表。）。今天我们一起来聊聊前端的学习路线与方法。

在“开篇词”中，我和你简单回顾了前端行业的发展，到现在为止，前端工程师已经成为研发体系中的重要岗位之一。可是，与此相对的是，我发现极少或者几乎没有大学的计算机专业愿意开设前端课程，更没有系统性的教学方案出现。大部分前端工程师的知识，其实都是来自于实践和工作中零散的学习。

这样的现状就引发了一系列的问题。

首先是前端的基础知识，常常有一些工作多年的工程师，在看到一些我认为很基础的 JavaScript 语法的时候，还会惊呼“居然可以这样”。是的，基础知识的欠缺会让你束手束脚，更限制你解决问题的思路。

其次，技术上存在短板，就会导致前端开发者的上升通道不甚顺畅。特别是一些小公司的程序员，只能靠自己摸索，这样就很容易陷入重复性劳动的陷阱，最终耽误自己的职业发展。

除此之外，前端工程师也会面临技术发展问题带来的挑战。前端社区高度活跃，前端标准也在快速更新，这样蓬勃发展对技术来说无疑是好事，但是副作用也显而易见，它使得前端工程师的学习压力变得很大。

我们就拿 JavaScript 标准来说，ES6 中引入的新特性超过了过去十年的总和，新特性带来的实践就更多了，仅仅是一个 Proxy 特性的引入，就支持了 VueJS 从 2.0 到 3.0 的内核原理完全升级。

缺少系统教育 + 技术快速革新，在这样的大环境下，前端工程师保持自学能力就显得尤其重要了。

那么，前端究竟应该怎么学呢？我想，我可以简单分享一下自己的经验。

学习路径与学习方法

首先是0 基础入门的同学，你可以读几本经典的前端教材，比如《JavaScript 高级程序设计》《精通 CSS》等书籍，去阅读一些参考性质的网站也是不错的选项，比如MDN。

如果你至少已经有了 1 年以上的工作经验，希望在技术上有一定突破，那么，这个专栏就可以是你技术进阶的一个选项了。

在这个专栏中，我希望传达的不仅仅是具体的知识点，还有体系架构和学习方法。我希望达到三个目标：

带你摸索出适合自己的前端学习方法；

帮助你建立起前端技术的知识架构；

让你理解前端技术背后的核心思想。

在开始具体的知识讲解之前，这篇文章中，我想先来谈两个前端学习方法。

第一个方法：建立知识架构

第一个方法是建立自己的知识架构，并且在这个架构上，不断地进行优化。

我们先来讲讲什么叫做知识架构？我们可以把它理解为知识的“目录”或者索引，它能够帮助我们把零散的知识组织起来，也能够帮助我们发现一些知识上的盲区。

当然，知识的架构是有优劣之分的，最重要的就是逻辑性和完备性。

我们来思考一个问题，如果我们要给 JavaScript 知识做一个顶层目录，该怎么做呢？

如果我们把一些特别流行的术语和问题，拼凑起来，可能会变成这样：

类型转换；

this 指针；

闭包；

作用域链；

原型链；

……

这其实不是我们想要的结果，因为这些知识点之间，没有任何逻辑关系。它们既不是并列关系，又不是递进关系，合在一起，也就没有任何意义。这样的知识架构，无法帮助我们去发现问题和理解问题。

如果让我来做，我会这样划分：

文法

语义

运行时

为什么这样分呢，因为对于任何计算机语言来说，必定是“用规定的文法，去表达特定语义，最终操作运行时的”一个过程。

这样，JavaScript 的任何知识都不会出现在这个范围之外，这是知识架构的完备性。我们再往下细分一个层级，就变成了这个样子：

文法

词法

语法

语义

运行时

类型

执行过程

我来解释一下这个划分。

文法可以分成词法和语法，这来自编译原理的划分，同样是完备的。语义则跟语法具有一一对应关系，这里暂时不区分。

对于运行时部分，这个划分保持了完备性，我们都知道：程序 = 算法 + 数据结构，那么，对运行时来说，类型就是数据结构，执行过程就是算法。

当我们再往下细分的时候，就会看到熟悉的概念了，词法中有各种直接量、关键字、运算符，语法和语义则是表达式、语句、函数、对象、模块，类型则包含了对象、数字、字符串等……

这样逐层向下细分，知识框架就初见端倪了。在顶层和大结构上，我们通过逻辑来保持完备性。如果继续往下，就需要一些技巧了，我们可以寻找一些线索。

比如在 JavaScript 标准中，有完整的文法定义，它是具有完备性的，所以我们可以根据它来完成，我们还可以根据语法去建立语义的知识架构。实际上，因为 JavaScript 有一份统一的标准，所以相对来说不太困难。

如果是浏览器中的 API，那就困难了，它们分布在 w3c 的各种标准当中，非常难找。但是我们要想找到一些具有完备性的线索，也不是没有办法。我喜欢的一个办法，就是用实际的代码去找：for in 遍历 window 的属性，再去找它的内容。

我想，学习的过程，实际上就是知识架构不断进化的过程，通过知识架构的自然延伸，我们可以更轻松地记忆一些原本难以记住的点，还可以发现被忽视的知识盲点。

建立知识架构，同样有利于面试，没人能够记住所有的知识，当不可避免地谈到一个记不住的知识，如果你能快速定位到它在知识架构中的位置，把一些相关的点讲出来，我想，这也能捞回不少分。（关于前端具体的知识架构，我会在 02 篇文章中详细讲解。）

第二个方法：追本溯源

第二个方法，我把它称作追本溯源。

有一些知识，背后有一个很大的体系，例如，我们对比一下 CSS 里面的两个属性：

opacity；

display。

虽然都是“属性”，但是它们背后的知识量完全不同，opacity 是个非常单纯的数值，表达的意思也很清楚，而 display 的每一个取值背后都是一个不同的布局体系。我们要讲清楚 display，就必须关注正常流（Normal Flow）、关注弹性布局系统以及 grid 这些内容。

还有一些知识，涉及的概念本身经历了各种变迁，变得非常复杂和有争议性，比如 MVC，从 1979 年至今，概念变化非常大，MVC 的定义几乎已经成了一段公案，我曾经截取了 MVC 原始论文、MVP 原始论文、微软 MSDN、Apple 开发者文档，这些内容里面，MVC 画的图、箭头和解释都完全不同。

这种时候，就是我们做一些考古工作的时候了。追本溯源，其实就是关注技术提出的背景，关注原始的论文或者文章，关注作者说的话。

操作起来也非常简单：翻翻资料（一般 wiki 上就有）找找历史上的文章和人物，再顺藤摸瓜翻出来历史资料就可以了，如果翻出来的是历史人物（幸亏互联网的历史不算悠久），你也可以试着发封邮件问问。

这个过程，可以帮助我们理解一些看上去不合理的东西，有时候还可以收获一些趣闻，比如 JavaScript 之父 Brendan Eich 曾经在 Wikipedia 的讨论页上解释 JavaScript 最初想设计一个带有 prototype 的 scheme，结果受到管理层命令把它弄成像 Java 的样子（如果你再挖的深一点，甚至能找到他对某位“尖头老板”的吐槽）。

根据这么一句话，我们再去看看 scheme，看看 Java，再看看一些别的基于原型的语言，我们就可以理解为什么 JavaScript 是现在这个样子了：函数是一等公民，却提供了 new this instanceof 等特性，甚至抄来了 Java 的 getYear 这样的 Bug。

结语

今天我带你探索了前端的学习路径，并提出了两个学习方法：你要试着建立自己的知识架构，除此之外，还要学会追本溯源，找到知识的源头。

这个专栏中，我并不奢望通过短短的 40 篇专栏，事无巨细地把前端的所有知识都罗列清楚，这本身是 MDN 这样的参考手册的工作。但是，我希望通过这个专栏，把前端技术背后的设计原理和知识体系讲清楚，让你能对前端技术产生整体认知，这样才能够在未来汹涌而来的新技术中保持领先的状态。

在你的认识中，前端知识的结构是怎样的？欢迎留言告诉我，我们一起讨论。

02 | 列一份前端知识架构图

你好，我是 winter（加微信：642945106 发送“赠送”领取赠送精品课程 发数字“2”获取众筹列表。）。

在上一篇文章中，我们简要地总结了前端的学习路径与方法，我们提到的第一个学习方法就是：建立知识框架。那么，今天我们就一起来列一份前端的知识框架图。

在开始列框架之前，我想先来谈谈我们的目标。实际上，我们在网上可以找到很多参考资料，比如 MDN 这样的参考手册，又比如一份语言标准，但是我们的课程既不是一本参考手册，也不是一份语言标准。参考手册希望做到便于查阅、便于理解和全面，语言标准的目标是严谨、无遗漏、无歧义。

而我们的课程有什么不同呢？我认为，作为一个课程，有两个目标：一个是把无法通过查阅解决的原理和背景讲清楚，另一个是把不方便查阅和记忆的内容整理好。

我会尽量避免像前面提到的两种文档一样逐条目罗列知识点和细节，当然，这不是在说两种文档没有价值，而是我们各有分工，参考手册和语言标准做的事情，我们没必要重复去做，即使做了也不一定能做得更好。

在这个课程里，我希望能和你一起打造一个前端知识的框架，再把知识点做个遍历，这其中，有原理和背景的部分，我去讲解知识的原理和背景。如果没有的话，我们就去讲整理和记忆这部分知识的方法，这样，即使你遇见无法一下子记住的知识，也可以很容易地查阅参考手册和标准来解决。

如果让我做一个划分，前端的知识在总体上分成基础部分和实践部分，基础部分包含了 JavaScript 语言（模块一）、CSS 和 HTML（模块二）以及浏览器的实现原理和 API（模块三），这三个模块涵盖了一个前端工程师所需要掌握的全部知识。

学完这三个部分，你再结合基本的编程能力，就可以应对基本的前端开发工作了。实践部分（模块四）重点会介绍我在工作过程中遇到的问题和解决方案，希望这块内容能够帮助你和你的前端团队找到可能的发展方向和着力点。

JavaScript

上面是我整理的 JavaScript 知识架构图，下面我们来具体解释一下。

在 JavaScript 的模块中，首先我们可以把语言按照文法、语义和运行时来拆分，这符合编程语言的一般规律：用一定的词法和语法，表达一定语义，从而操作运行时。

接下来，我们又按照程序的一般规律，把运行时分为数据结构和算法部分：数据结构包含类型和实例（JavaScript 的类型系统就是它的 7 种基本类型和 7 种语言类型，实例就是它的内置对象部分）。所谓的算法，就是 JavaScript 的执行过程。

类型部分中，对象比其它所有类型加起来都要更为复杂，所以我们会用较长的篇幅来讲解对象，包括它的一些历史和设计思路。

执行过程我们则需要按照从大结构到小结构的角度讲解，从最顶层的程序与模块、事件循环和微任务，到函数、再到语句级的执行。我们从粗到细地了解执行过程。

实例部分，对 JavaScript 来说类似基础库，JavaScipt 的内置对象多达 150 以上，考虑到我们即使逐次讲解也必定不如 MDN 更加细致全面，所以我们会从应用和机制的角度，挑选其中几个体系来讲解。

文法中的语法和语义基本是一一对应关系，在 JavaScript 标准中有一份语法定义表，它同样不适合一一讲解，我们会从 JavaScript 语法中特别的地方，以及与日常开发比较相关的地方来重点讲解，剩下的内容和词法部分，我们会带领大家做一些数据挖掘工作，从这份表格中找到一些和我们日常开发息息相关的内容。

语义的大部分内容我们会在运行时的讲解中透出，同时它又跟语法有对应的关系，所以我们不再单独拿出来讲解。

HTML 和 CSS

上面是我整理的 HTML 和 CSS 的知识架构图，我们来具体解释一下。

在 HTML 的部分，我们会按照功能和语言来划分它的知识，HTML 的功能主要由标签来承担，所以我们首先会把标签做一些分类，并对它们分别进行讲解。

我们都知道 HTML 的标签可以分为很多种，head 里面的我们称为元信息类标签，诸如 title、meta、style、link、base 这些，它们用来描述文档的一些基本信息。还有一类是一些诸如 section、nav 的标签，它们在视觉表现上跟 div 并没有区别，但是各有各的适用场景，我们把它们称作语义类标签。另外一类是 img、video、audio 之类的替换型媒体类标签，用来引入外部内容，平常开发中你也会经常用到。再有就是表单类的，比如 input、button。

所以，基于这样的分类，我把标签分成下面几种。

文档元信息：通常是出现在 head 标签中的元素，包含了描述文档自身的一些信息；

语义相关：扩展了纯文本，表达文章结构、不同语言要素的标签；

链接：提供到文档内和文档外的链接；

替换型标签：引入声音、图片、视频等外部元素替换自身的一类标签；

表单：用于填写和提交信息的一类标签；

表格：表头、表尾、单元格等表格的结构。

我们的重点会放在前四种标签上，表单和表格较少用到，而且基本以查阅型知识为主，这里就不拿出来讲解了。

除了标签之外，我们还应该把 HTML 当作一门语言来了解下，当然，标记语言跟编程语言不太一样，没有编程语言那么严谨，所以，我们会简要介绍 HTML 的语法和几个重要的语言机制：实体、命名空间。

最后我们会介绍下 HTML 的补充标准：ARIA，它是 HTML 的扩展，在可访问性领域，它有至关重要的作用。

CSS 部分，按照惯例，我们也会从语言和功能两个角度去介绍。在语言部分，我们会从大到小介绍 CSS 的各种语法结构，比如 @rule、选择器、单位等等。功能部分，我们大致可以分为布局、绘制和交互类。

在布局类我们介绍两个最常用的布局：正常流和弹性布局。绘制类我们则会分成图形相关的和文字相关的绘制。最后我们会介绍动画和其它交互。

浏览器的实现原理和 API

上面是我整理的浏览器知识架构图，我们来具体看一下。

浏览器部分我们会先介绍下浏览器的实现原理，这是我们深入理解 API 的基础。

我们会从一般的浏览器设计出发，按照解析、构建 DOM 树、计算 CSS、渲染、合成和绘制的流程来讲解浏览器的工作原理。

在 API 部分，我们会从 W3C 零散的标准中挑选几个大块的 API 来详细讲解，主要有：事件、DOM、CSSOM 几个部分，它们分别覆盖了交互、语义和可见效果，这是我们工作中用到的主要内容。

其他的 API 怎么办呢，别着急，在最后，我会给出一份 Chrome 已经实现的 API 跟 W3C 标准的对应关系和它的生成过程，来覆盖其它部分。

前端工程实践

最后一个模块是前端工程实践。我们在掌握了前面的基础知识之后，也就基本掌握了做一个前端工程师的底层能力。在这个模块中，我选择了性能、工具链、持续集成、搭建系统、架构与基础库这几个方向的前端工程实践案例，来与你一起分享我的经验。

性能

首先我们会谈谈性能。对任何一个前端团队而言，性能是它价值的核心指标，从早年“重构”的实践开始，前端有通过性能证明自己价值的传统。

但是性能并非细节的堆砌，也不是默默做优化，所以，我会从团队的角度来跟你一起探讨性能的方法论和技术体系。

工具链

下一个案例是工具链。这一部分，我将会探讨企业中工具链的建设思路。对一个高效又合作良好的前端团队来说，一致性的工具链是不可或缺的保障，作为开发阶段的入口，工具链又可以和性能、发布、持续集成等系统链接到一起，成为团队技术管理的基础。

持续集成

接下来还会给大家介绍前端的持续集成，持续集成并非一个新概念，但是过去持续集成概念和理论都主要针对软件开发，而对前端来说，持续集成是一个新的课题（当然对持续集成来说，前端也是一个新课题），比如 daily build 就完全不适用前端，前端代码必须是线上实时可用的。这一部分内容将会针对前端的持续集成提出一些建设的思路。

搭建系统

接下来的案例是搭建系统，前端工作往往多而繁杂，针对高重复性、可模块化的业务需求，传统的人工开发不再适用，搭建系统是大部分大型前端团队的选择。这一部分内容我将会介绍什么是搭建系统，以及一些常见的搭建系统类型。

架构与基础库

最后一个部分，会给大家介绍前端架构和基础库的知识。软件架构师主要解决功能复杂性的问题，服务端架构师主要解决高流量问题，而前端是页面间天然解耦，分散在用户端运行的系统，但是前端架构也有自己要解决的问题。

前端需求量大、专业人才稀缺，更因为前端本身运行在浏览器中，有大量兼容工作要做。所以前端架构的主要职责是兼容性、复用和能力扩展。这一部分文章我将会介绍前端架构工作的一些思路和切入点。

上面的这些案例来自我在领导手淘前端团队时的经验，和我在阿里巴巴工作参与晋升面试时听到的案例，这些内容几乎是每一个年轻的前端团队成长过程中都会需要的基础设施。

好了，前端的知识体系我们大致列出来了。你可能发现了，知识体系图中的每一个知识点，专栏里都有与之对应的文章，这也是我的初衷：希望借由讲解这 40 余个知识点，帮你建立起前端的知识框架。

（长按保存高清大图）

讲述形式

基于这份知识框架图，我们的课程主要采用两种讲述形式：一种是重点讲解的课程，一种是知识图谱型的课程。

重点讲解的课程我们会从技术的背景、原理和设计出发，把知识的内容呈现出来。这种形式适用于有体系和源流的知识，比较适合系统学习和理解，比如 JavaScript 中的对象、CSS 的排版。

知识图谱型的课程则提供一些方法，用表格或者脑图的形式来整理知识的结构。这种形式适用于零散的知识，比较适合记住大概，用到时去查阅，比如 JavaScript 的词法、HTML 中的所有标签、以及浏览器中的 API 就十分适合这样的讲解方式。

结语

今天我带你一起划分了前端的知识内容，前端的基础知识分成 JavaScript、HTML、CSS 以及浏览器四大重点模块，每个模块也分别有自己的技术重点。你可以在框架中，挑选你最需要的前端知识，按需学习。

当然，这篇文章最重要的是，我希望能帮你建立一个理解前端的全景图。这样，任何时候，你都能够体系地思考问题，分析问题。

你觉得你的划分跟我一样吗，你还有其他的想法，你觉得是否有想了解的知识不在其中，欢迎给我留言。

附录：知识点发布顺序

05 | JavaScript类型：关于类型，有哪些你不知道的细节？

你好，我是 winter。今天我们来讲讲 JavaScript 的内容，在这个部分，我首先想跟你聊一聊类型。

JavaScript 类型对每个前端程序员来说，几乎都是最为熟悉的概念了。但是你真的很了解它们吗？我们不妨来看看下面的几个问题。

为什么有的编程规范要求用 void 0 代替 undefined？

字符串有最大长度吗？

0.1 + 0.2 不是等于 0.3 么？为什么 JavaScript 里不是这样的？

ES6 新加入的 Symbol 是个什么东西？

为什么给对象添加的方法能用在基本类型上？

如果你答起来还有些犹豫的地方，这就说明你对这部分知识点，还是有些遗漏之处的。没关系，今天我来帮你一一补上。

我在前面提到过，我们的 JavaScript 模块会从运行时、文法和执行过程三个角度去剖析 JS 的知识体系，本篇我们就从运行时的角度去看 JavaScript 的类型系统。

运行时类型是代码实际执行过程中我们用到的类型。所有的类型数据都会属于 7 个类型之一。从变量、参数、返回值到表达式中间结果，任何 JavaScript 代码运行过程中产生的数据，都具有运行时类型。

类型

JavaScript 语言的每一个值都属于某一种数据类型。JavaScript 语言规定了 7 种语言类型。语言类型广泛用于变量、函数参数、表达式、函数返回值等场合。根据最新的语言标准，这 7 种语言类型是：

Undefined；

Null；

Boolean；

String；

Number；

Symbol；

Object。

除了 ES6 中新加入的 Symbol 类型，剩下 6 种类型都是我们日常开发中的老朋友了，但是，要想回答文章一开始的问题，我们需要重新认识一下这些老朋友，下面我们就来从简单到复杂，重新学习一下这些类型。

Undefined、Null

我们的第一个问题，为什么有的编程规范要求用 void 0 代替 undefined？现在我们就分别来看一下。

Undefined 类型表示未定义，它的类型只有一个值，就是 undefined。任何变量在赋值前是 Undefined 类型、值为 undefined，一般我们可以用全局变量 undefined（就是名为 undefined 的这个变量）来表达这个值，或者 void 运算来把任意一个表达式变成 undefined 值。

但是呢，因为 JavaScript 的代码 undefined 是一个变量，而并非是一个关键字，这是 JavaScript 语言公认的设计失误之一，所以，我们为了避免无意中被篡改，我建议使用 void 0 来获取 undefined 值。

Undefined 跟 null 有一定的表意差别，null 表示的是：“定义了但是为空”。所以，在实际编程时，我们一般不会把变量赋值为 undefined，这样可以保证所有值为 undefined 的变量，都是从未赋值的自然状态。

Null 类型也只有一个值，就是 null，它的语义表示空值，与 undefined 不同，null 是 JavaScript 关键字，所以在任何代码中，你都可以放心用 null 关键字来获取 null 值。

Boolean

Boolean 类型有两个值， true 和 false，它用于表示逻辑意义上的真和假，同样有关键字 true 和 false 来表示两个值。这个类型很简单，我就不做过多介绍了。

String

我们来看看字符串是否有最大长度。

String 用于表示文本数据。String 有最大长度是 2^53 - 1，这在一般开发中都是够用的，但是有趣的是，这个所谓最大长度，并不完全是你理解中的字符数。

因为 String 的意义并非“字符串”，而是字符串的 UTF16 编码，我们字符串的操作 charAt、charCodeAt、length 等方法针对的都是 UTF16 编码。所以，字符串的最大长度，实际上是受字符串的编码长度影响的。

Note：现行的字符集国际标准，字符是以 Unicode 的方式表示的，每一个 Unicode 的码点表示一个字符，理论上，Unicode 的范围是无限的。UTF 是 Unicode 的编码方式，规定了码点在计算机中的表示方法，常见的有 UTF16 和 UTF8。 Unicode 的码点通常用 U+??? 来表示，其中 ??? 是十六进制的码点值。 0-65536（U+0000 - U+FFFF）的码点被称为基本字符区域（BMP）。

JavaScript 中的字符串是永远无法变更的，一旦字符串构造出来，无法用任何方式改变字符串的内容，所以字符串具有值类型的特征。

JavaScript 字符串把每个 UTF16 单元当作一个字符来处理，所以处理非 BMP（超出 U+0000 - U+FFFF 范围）的字符时，你应该格外小心。

JavaScript 这个设计继承自 Java，最新标准中是这样解释的，这样设计是为了“性能和尽可能实现起来简单”。因为现实中很少用到 BMP 之外的字符。

Number

下面，我们来说说 Number 类型。Number 类型表示我们通常意义上的“数字”。这个数字大致对应数学中的有理数，当然，在计算机中，我们有一定的精度限制。

JavaScript 中的 Number 类型有 18437736874454810627(即 2^64-2^53+3) 个值。

JavaScript 中的 Number 类型基本符合 IEEE 754-2008 规定的双精度浮点数规则，但是 JavaScript 为了表达几个额外的语言场景（比如不让除以 0 出错，而引入了无穷大的概念），规定了几个例外情况：

NaN，占用了 9007199254740990，这原本是符合 IEEE 规则的数字；

Infinity，无穷大；

-Infinity，负无穷大。

另外，值得注意的是，JavaScript 中有 +0 和 -0，在加法类运算中它们没有区别，但是除法的场合则需要特别留意区分，“忘记检测除以 -0，而得到负无穷大”的情况经常会导致错误，而区分 +0 和 -0 的方式，正是检测 1/x 是 Infinity 还是 -Infinity。

根据双精度浮点数的定义，Number 类型中有效的整数范围是 -0x1fffffffffffff 至 0x1fffffffffffff，所以 Number 无法精确表示此范围外的整数。

同样根据浮点数的定义，非整数的 Number 类型无法用 ==（=== 也不行） 来比较，一段著名的代码，这也正是我们第三题的问题，为什么在 JavaScript 中，0.1+0.2 不能 =0.3：

  console.log( 0.1 + 0.2 == 0.3);

这里输出的结果是 false，说明两边不相等的，这是浮点运算的特点，也是很多同学疑惑的来源，浮点数运算的精度问题导致等式左右的结果并不是严格相等，而是相差了个微小的值。

所以实际上，这里错误的不是结论，而是比较的方法，正确的比较方法是使用 JavaScript 提供的最小精度值：

  console.log( Math.abs(0.1 + 0.2 - 0.3) <= Number.EPSILON);

检查等式左右两边差的绝对值是否小于最小精度，才是正确的比较浮点数的方法。这段代码结果就是 true 了。

Symbol

Symbol 是 ES6 中引入的新类型，它是一切非字符串的对象 key 的集合，在 ES6 规范中，整个对象系统被用 Symbol 重塑。

在后面的文章中，我会详细叙述 Symbol 跟对象系统。这里我们只介绍 Symbol 类型本身：它有哪些部分，它表示什么意思，以及如何创建 Symbol 类型。

Symbol 可以具有字符串类型的描述，但是即使描述相同，Symbol 也不相等。

我们创建 Symbol 的方式是使用全局的 Symbol 函数。例如：

    var mySymbol = Symbol("my symbol");

一些标准中提到的 Symbol，可以在全局的 Symbol 函数的属性中找到。例如，我们可以使用 Symbol.iterator 来自定义 for…of 在对象上的行为：

    var o = new Object

    o[Symbol.iterator] = function() {

        var v = 0

        return {

            next: function() {

                return { value: v++, done: v > 10 }

            }

        }

    };

    for(var v of o)

        console.log(v); // 0 1 2 3 ... 9

代码中我们定义了 iterator 之后，用 for(var v of o) 就可以调用这个函数，然后我们可以根据函数的行为，产生一个 for…of 的行为。

这里我们给对象 o 添加了 Symbol.iterator 属性，并且按照迭代器的要求定义了一个 0 到 10 的迭代器，之后我们就可以在 for of 中愉快地使用这个 o 对象啦。

这些标准中被称为“众所周知”的 Symbol，也构成了语言的一类接口形式。它们允许编写与语言结合更紧密的 API。

Object

Object 是 JavaScript 中最复杂的类型，也是 JavaScript 的核心机制之一。Object 表示对象的意思，它是一切有形和无形物体的总称。

下面我们来看一看，为什么给对象添加的方法能用在基本类型上？

在 JavaScript 中，对象的定义是“属性的集合”。属性分为数据属性和访问器属性，二者都是 key-value 结构，key 可以是字符串或者 Symbol 类型。

关于对象的机制，后面会有单独的一篇来讲述，这里我重点从类型的角度来介绍对象类型。

提到对象，我们必须要提到一个概念：类。

因为 C++ 和 Java 的成功，在这两门语言中，每个类都是一个类型，二者几乎等同，以至于很多人常常会把 JavaScript 的“类”与类型混淆。

事实上，JavaScript 中的“类”仅仅是运行时对象的一个私有属性，而 JavaScript 中是无法自定义类型的。

JavaScript 中的几个基本类型，都在对象类型中有一个“亲戚”。它们是：

Number；

String；

Boolean；

Symbol。

所以，我们必须认识到 3 与 new Number(3) 是完全不同的值，它们一个是 Number 类型， 一个是对象类型。

Number、String 和 Boolean，三个构造器是两用的，当跟 new 搭配时，它们产生对象，当直接调用时，它们表示强制类型转换。

Symbol 函数比较特殊，直接用 new 调用它会抛出错误，但它仍然是 Symbol 对象的构造器。

JavaScript 语言设计上试图模糊对象和基本类型之间的关系，我们日常代码可以把对象的方法在基本类型上使用，比如：

    console.log("abc".charAt(0)); //a

甚至我们在原型上添加方法，都可以应用于基本类型，比如以下代码，在 Symbol 原型上添加了 hello 方法，在任何 Symbol 类型变量都可以调用。

    Symbol.prototype.hello = () => console.log("hello");

    var a = Symbol("a");

    console.log(typeof a); //symbol，a 并非对象

    a.hello(); //hello，有效

所以我们文章开头的问题，答案就是. 运算符提供了装箱操作，它会根据基础类型构造一个临时对象，使得我们能在基础类型上调用对应对象的方法。

类型转换

讲完了基本类型，我们来介绍一个现象：类型转换。

因为 JS 是弱类型语言，所以类型转换发生非常频繁，大部分我们熟悉的运算都会先进行类型转换。大部分类型转换符合人类的直觉，但是如果我们不去理解类型转换的严格定义，很容易造成一些代码中的判断失误。

其中最为臭名昭著的是 JS 中的“ == ”运算，因为试图实现跨类型的比较，它的规则复杂到几乎没人可以记住。

这里我们当然也不打算讲解 == 的规则，它属于设计失误，并非语言中有价值的部分，很多实践中推荐禁止使用“ ==”，而要求程序员进行显式地类型转换后，用 === 比较。

其它运算，如加减乘除大于小于，也都会涉及类型转换。幸好的是，实际上大部分类型转换规则是非常简单的，如下表所示：

在这个里面，较为复杂的部分是 Number 和 String 之间的转换，以及对象跟基本类型之间的转换。我们分别来看一看这几种转换的规则。

StringToNumber

字符串到数字的类型转换，存在一个语法结构，类型转换支持十进制、二进制、八进制和十六进制，比如：

30；

0b111；

0o13；

0xFF。

此外，JavaScript 支持的字符串语法还包括正负号科学计数法，可以使用大写或者小写的 e 来表示：

1e3；

-1e-2。

需要注意的是，parseInt 和 parseFloat 并不使用这个转换，所以支持的语法跟这里不尽相同。

在不传入第二个参数的情况下，parseInt 只支持 16 进制前缀“0x”，而且会忽略非数字字符，也不支持科学计数法。

在一些古老的浏览器环境中，parseInt 还支持 0 开头的数字作为 8 进制前缀，这是很多错误的来源。所以在任何环境下，都建议传入 parseInt 的第二个参数，而 parseFloat 则直接把原字符串作为十进制来解析，它不会引入任何的其他进制。

多数情况下，Number 是比 parseInt 和 parseFloat 更好的选择。

NumberToString

在较小的范围内，数字到字符串的转换是完全符合你直觉的十进制表示。当 Number 绝对值较大或者较小时，字符串表示则是使用科学计数法表示的。这个算法细节繁多，我们从感性的角度认识，它其实就是保证了产生的字符串不会过长。

具体的算法，你可以去参考 JavaScript 的语言标准。由于这个部分内容，我觉得在日常开发中很少用到，所以这里我就不去详细地讲解了。

装箱转换

每一种基本类型 Number、String、Boolean、Symbol 在对象中都有对应的类，所谓装箱转换，正是把基本类型转换为对应的对象，它是类型转换中一种相当重要的种类。

前文提到，全局的 Symbol 函数无法使用 new 来调用，但我们仍可以利用装箱机制来得到一个 Symbol 对象，我们可以利用一个函数的 call 方法来强迫产生装箱。

我们定义一个函数，函数里面只有 return this，然后我们调用函数的 call 方法到一个 Symbol 类型的值上，这样就会产生一个 symbolObject。

我们可以用 console.log 看一下这个东西的 type of，它的值是 object，我们使用 symbolObject instanceof 可以看到，它是 Symbol 这个类的实例，我们找它的 constructor 也是等于 Symbol 的，所以我们无论从哪个角度看，它都是 Symbol 装箱过的对象：

    var symbolObject = (function(){ return this; }).call(Symbol("a"));

    console.log(typeof symbolObject); //object

    console.log(symbolObject instanceof Symbol); //true

    console.log(symbolObject.constructor == Symbol); //true

装箱机制会频繁产生临时对象，在一些对性能要求较高的场景下，我们应该尽量避免对基本类型做装箱转换。

使用内置的 Object 函数，我们可以在 JavaScript 代码中显式调用装箱能力。

    var symbolObject = Object(Symbol("a"));

    console.log(typeof symbolObject); //object

    console.log(symbolObject instanceof Symbol); //true

    console.log(symbolObject.constructor == Symbol); //true

每一类装箱对象皆有私有的 Class 属性，这些属性可以用 Object.prototype.toString 获取：

    var symbolObject = Object(Symbol("a"));

    console.log(Object.prototype.toString.call(symbolObject)); //[object Symbol]

在 JavaScript 中，没有任何方法可以更改私有的 Class 属性，因此 Object.prototype.toString 是可以准确识别对象对应的基本类型的方法，它比 instanceof 更加准确。

但需要注意的是，call 本身会产生装箱操作，所以需要配合 typeof 来区分基本类型还是对象类型。

拆箱转换

在 JavaScript 标准中，规定了 ToPrimitive 函数，它是对象类型到基本类型的转换（即，拆箱转换）。

对象到 String 和 Number 的转换都遵循“先拆箱再转换”的规则。通过拆箱转换，把对象变成基本类型，再从基本类型转换为对应的 String 或者 Number。

拆箱转换会尝试调用 valueOf 和 toString 来获得拆箱后的基本类型。如果 valueOf 和 toString 都不存在，或者没有返回基本类型，则会产生类型错误 TypeError。

    var o = {

        valueOf : () => {console.log("valueOf"); return {}},

        toString : () => {console.log("toString"); return {}}

    }

    o * 2

    // valueOf

    // toString

    // TypeError

我们定义了一个对象 o，o 有 valueOf 和 toString 两个方法，这两个方法都返回一个对象，然后我们进行 o*2 这个运算的时候，你会看见先执行了 valueOf，接下来是 toString，最后抛出了一个 TypeError，这就说明了这个拆箱转换失败了。

到 String 的拆箱转换会优先调用 toString。我们把刚才的运算从 o*2 换成 String(o)，那么你会看到调用顺序就变了。

    var o = {

        valueOf : () => {console.log("valueOf"); return {}},

        toString : () => {console.log("toString"); return {}}

    }

   String(o)

    // toString

    // valueOf

    // TypeError

在 ES6 之后，还允许对象通过显式指定 @@toPrimitive Symbol 来覆盖原有的行为。

    var o = {

        valueOf : () => {console.log("valueOf"); return {}},

        toString : () => {console.log("toString"); return {}}

    }

    o[Symbol.toPrimitive] = () => {console.log("toPrimitive"); return "hello"}

    console.log(o + "")

    // toPrimitive

    // hello

结语

在本篇文章中，我们介绍了 JavaScript 运行时的类型系统。这里回顾一下今天讲解的知识点。

除了这七种语言类型，还有一些语言的实现者更关心的规范类型。

List 和 Record： 用于描述函数传参过程。

Set：主要用于解释字符集等。

Completion Record：用于描述异常、跳出等语句执行过程。

Reference：用于描述对象属性访问、delete 等。

Property Descriptor：用于描述对象的属性。

Lexical Environment 和 Environment Record：用于描述变量和作用域。

Data Block：用于描述二进制数据。

有一个说法是：程序 = 算法 + 数据结构，运行时类型包含了所有 JavaScript 执行时所需要的数据结构的定义，所以我们要对它格外重视。

最后我们留一个实践问题，如果我们不用原生的 Number 和 parseInt，用 JS 代码实现 String 到 Number 的转换，该怎么做呢？请你把自己的代码留言给我吧！

补充阅读

事实上，“类型”在 JavaScript 中是一个有争议的概念。一方面，标准中规定了运行时数据类型； 另一方面，JS 语言中提供了 typeof 这样的运算，用来返回操作数的类型，但 typeof 的运算结果，与运行时类型的规定有很多不一致的地方。我们可以看下表来对照一下。

在表格中，多数项是对应的，但是请注意 object——Null 和 function——Object 是特例，我们理解类型的时候需要特别注意这个区别。

从一般语言使用者的角度来看，毫无疑问，我们应该按照 typeof 的结果去理解语言的类型系统。但 JS 之父本人也在多个场合表示过，typeof 的设计是有缺陷的，只是现在已经错过了修正它的时机。

猜你喜欢

06 | JavaScript对象：面向对象还是基于对象？

你好，我是 winter。

与其它的语言相比，JavaScript 中的“对象”总是显得不那么合群。

一些新人在学习 JavaScript 面向对象时，往往也会有疑惑：

为什么 JavaScript（直到 ES6）有对象的概念，但是却没有像其他的语言那样，有类的概念呢；

为什么在 JavaScript 对象里可以自由添加属性，而其他的语言却不能呢？

甚至，在一些争论中，有人强调：JavaScript 并非“面向对象的语言”，而是“基于对象的语言”。这个说法一度流传甚广，而事实上，我至今遇到的持有这一说法的人中，无一能够回答“如何定义面向对象和基于对象”这个问题。

实际上，基于对象和面向对象两个形容词都出现在了 JavaScript 标准的各个版本当中。

我们可以先看看 JavaScript 标准对基于对象的定义，这个定义的具体内容是：“语言和宿主的基础设施由对象来提供，并且 JavaScript 程序即是一系列互相通讯的对象集合”。

这里的意思根本不是表达弱化的面向对象的意思，反而是表达对象对于语言的重要性。

那么，在本篇文章中，我会尝试让你去理解面向对象和 JavaScript 中的面向对象究竟是什么。

什么是面向对象？

我们先来说说什么是对象，因为翻译的原因，中文语境下我们很难理解“对象”的真正含义。事实上，Object（对象）在英文中，是一切事物的总称，这和面向对象编程的抽象思维有互通之处。

中文的“对象”却没有这样的普适性，我们在学习编程的过程中，更多是把它当作一个专业名词来理解。

但不论如何，我们应该认识到，对象并不是计算机领域凭空造出来的概念，它是顺着人类思维模式产生的一种抽象（于是面向对象编程也被认为是：更接近人类思维模式的一种编程范式）。

那么，我们先来看看在人类思维模式下，对象究竟是什么。

对象这一概念在人类的幼儿期形成，这远远早于我们编程逻辑中常用的值、过程等概念。

在幼年期，我们总是先认识到某一个苹果能吃（这里的某一个苹果就是一个对象），继而认识到所有的苹果都可以吃（这里的所有苹果，就是一个类），再到后来我们才能意识到三个苹果和三个梨之间的联系，进而产生数字“3”（值）的概念。

在《面向对象分析与设计》这本书中，Grady Booch 替我们做了总结，他认为，从人类的认知角度来说，对象应该是下列事物之一：

一个可以触摸或者可以看见的东西；

人的智力可以理解的东西；

可以指导思考或行动（进行想象或施加动作）的东西。

有了对象的自然定义后，我们就可以描述编程语言中的对象了。在不同的编程语言中，设计者也利用各种不同的语言特性来抽象描述对象，最为成功的流派是使用“类”的方式来描述对象，这诞生了诸如 C++、Java 等流行的编程语言。

而 JavaScript 早年却选择了一个更为冷门的方式：原型（关于原型，我在下一篇文章会重点介绍，这里你留个印象就可以了）。这是我在前面说它不合群的原因之一。

然而很不幸，因为一些公司政治原因，JavaScript 推出之时受管理层之命被要求模仿 Java，所以，JavaScript 创始人 Brendan Eich 在“原型运行时”的基础上引入了 new、this 等语言特性，使之“看起来更像 Java”。

在 ES6 出现之前，大量的 JavaScript 程序员试图在原型体系的基础上，把 JavaScript 变得更像是基于类的编程，进而产生了很多所谓的“框架”，比如 PrototypeJS、Dojo。

事实上，它们成为了某种 JavaScript 的古怪方言，甚至产生了一系列互不相容的社群，显然这样做的收益是远远小于损失的。

如果我们从运行时角度来谈论对象，就是在讨论 JavaScript 实际运行中的模型，这是由于任何代码执行都必定绕不开运行时的对象模型。

不过，幸运的是，从运行时的角度看，可以不必受到这些“基于类的设施”的困扰，这是因为任何语言运行时类的概念都是被弱化的。

首先我们来了解一下 JavaScript 是如何设计对象模型的。

JavaScript 对象的特征

在我看来，不论我们使用什么样的编程语言，我们都先应该去理解对象的本质特征（参考 Grandy Booch《面向对象分析与设计》）。总结来看，对象有如下几个特点。

对象具有唯一标识性：即使完全相同的两个对象，也并非同一个对象。

对象有状态：对象具有状态，同一对象可能处于不同状态之下。

对象具有行为：即对象的状态，可能因为它的行为产生变迁。

我们先来看第一个特征，对象具有唯一标识性。一般而言，各种语言的对象唯一标识性都是用内存地址来体现的， 对象具有唯一标识的内存地址，所以具有唯一的标识。

所以，JavaScript 程序员都知道，任何不同的 JavaScript 对象其实是互不相等的，我们可以看下面的代码，o1 和 o2 初看是两个一模一样的对象，但是打印出来的结果却是 false。

    var o1 = { a: 1 };

    var o2 = { a: 1 };

    console.log(o1 == o2); // false

关于对象的第二个和第三个特征“状态和行为”，不同语言会使用不同的术语来抽象描述它们，比如 C++ 中称它们为“成员变量”和“成员函数”，Java 中则称它们为“属性”和“方法”。

在 JavaScript 中，将状态和行为统一抽象为“属性”，考虑到 JavaScript 中将函数设计成一种特殊对象（关于这点，我会在后面的文章中详细讲解，此处先不用细究），所以 JavaScript 中的行为和状态都能用属性来抽象。

下面这段代码其实就展示了普通属性和函数作为属性的一个例子，其中 o 是对象，d 是一个属性，而函数 f 也是一个属性，尽管写法不太相同，但是对 JavaScript 来说，d 和 f 就是两个普通属性。

    var o = {

        d: 1,

        f() {

            console.log(this.d);

        }

    };

所以，总结一句话来看，在 JavaScript 中，对象的状态和行为其实都被抽象为了属性。如果你用过 Java，一定不要觉得奇怪，尽管设计思路有一定差别，但是二者都很好地表现了对象的基本特征：标识性、状态和行为。

在实现了对象基本特征的基础上, 我认为，JavaScript 中对象独有的特色是：对象具有高度的动态性，这是因为 JavaScript 赋予了使用者在运行时为对象添改状态和行为的能力。

我来举个例子，比如，JavaScript 允许运行时向对象添加属性，这就跟绝大多数基于类的、静态的对象设计完全不同。如果你用过 Java 或者其它别的语言，肯定会产生跟我一样的感受。

下面这段代码就展示了运行时如何向一个对象添加属性，一开始我定义了一个对象 o，定义完成之后，再添加它的属性 b，这样操作是完全没问题的。

    var o = { a: 1 };

    o.b = 2;

    console.log(o.a, o.b); //1 2

为了提高抽象能力，JavaScript 的属性被设计成比别的语言更加复杂的形式，它提供了数据属性和访问器属性（getter/setter）两类。

JavaScript 对象的两类属性

对 JavaScript 来说，属性并非只是简单的名称和值，JavaScript 用一组特征（attribute）来描述属性（property）。

先来说第一类属性，数据属性。它比较接近于其它语言的属性概念。数据属性具有四个特征。

value：就是属性的值。

writable：决定属性能否被赋值。

enumerable：决定 for in 能否枚举该属性。

configurable：决定该属性能否被删除或者改变特征值。

在大多数情况下，我们只关心数据属性的值即可。

第二类属性是访问器（getter/setter）属性，它也有四个特征。

getter：函数或 undefined，在取属性值时被调用。

setter：函数或 undefined，在设置属性值时被调用。

enumerable：决定 for in 能否枚举该属性。

configurable：决定该属性能否被删除或者改变特征值。

访问器属性使得属性在读和写时执行代码，它允许使用者在写和读属性时，得到完全不同的值，它可以视为一种函数的语法糖。

我们通常用于定义属性的代码会产生数据属性，其中的 writable、enumerable、configurable 都默认为 true。我们可以使用内置函数 Object.getOwnPropertyDescripter 来查看，如以下代码所示：

    var o = { a: 1 };

    o.b = 2;

    //a 和 b 皆为数据属性

    Object.getOwnPropertyDescriptor(o,"a") // {value: 1, writable: true, enumerable: true, configurable: true}

    Object.getOwnPropertyDescriptor(o,"b") // {value: 2, writable: true, enumerable: true, configurable: true}

我们在这里使用了两种语法来定义属性，定义完属性后，我们用 JavaScript 的 API 来查看这个属性，我们可以发现，这样定义出来的属性都是数据属性，writeable、enumerable、configurable 都是默认值为 true。

如果我们要想改变属性的特征，或者定义访问器属性，我们可以使用 Object.defineProperty，示例如下：

    var o = { a: 1 };

    Object.defineProperty(o, "b", {value: 2, writable: false, enumerable: false, configurable: true});

    //a 和 b 都是数据属性，但特征值变化了

    Object.getOwnPropertyDescriptor(o,"a"); // {value: 1, writable: true, enumerable: true, configurable: true}

    Object.getOwnPropertyDescriptor(o,"b"); // {value: 2, writable: false, enumerable: false, configurable: true}

    o.b = 3;

    console.log(o.b); // 2

这里我们使用了 Object.defineProperty 来定义属性，这样定义属性可以改变属性的 writable 和 enumerable。

我们同样用 Object.getOwnPropertyDescriptor 来查看，发现确实改变了 writable 和 enumerable 特征。因为 writable 特征为 false，所以我们重新对 b 赋值，b 的值不会发生变化。

在创建对象时，也可以使用 get 和 set 关键字来创建访问器属性，代码如下所示：

    var o = { get a() { return 1 } };

    console.log(o.a); // 1

访问器属性跟数据属性不同，每次访问属性都会执行 getter 或者 setter 函数。这里我们的 getter 函数返回了 1，所以 o.a 每次都得到 1。

这样，我们就理解了，实际上 JavaScript 对象的运行时是一个“属性的集合”，属性以字符串或者 Symbol 为 key，以数据属性特征值或者访问器属性特征值为 value。

对象是一个属性的索引结构（索引结构是一类常见的数据结构，我们可以把它理解为一个能够以比较快的速度用 key 来查找 value 的字典）。我们以上面的对象 o 为例，你可以想象一下“a”是 key。

{writable:true,value:1,configurable:true,enumerable:true}是 value。我们在前面的类型课程中，已经介绍了 Symbol 类型，能够以 Symbol 为属性名，这是 JavaScript 对象的一个特色。

讲到了这里，如果你理解了对象的特征，也就不难理解我开篇提出来的问题。

你甚至可以理解为什么会有“JavaScript 不是面向对象”这样的说法了。这是由于 JavaScript 的对象设计跟目前主流基于类的面向对象差异非常大。

可事实上，这样的对象系统设计虽然特别，但是 JavaScript 提供了完全运行时的对象系统，这使得它可以模仿多数面向对象编程范式（下一节课我们会给你介绍 JavaScript 中两种面向对象编程的范式：基于类和基于原型），所以它也是正统的面向对象语言。

JavaScript 语言标准也已经明确说明，JavaScript 是一门面向对象的语言，我想标准中能这样说，正是因为 JavaScript 的高度动态性的对象系统。

所以，我们应该在理解其设计思想的基础上充分挖掘它的能力，而不是机械地模仿其它语言。

结语

要想理解 JavaScript 对象，必须清空我们脑子里“基于类的面向对象”相关的知识，回到人类对对象的朴素认知和面向对象的语言无关基础理论，我们就能够理解 JavaScript 面向对象设计的思路。

在这篇文章中，我从对象的基本理论出发，和你理清了关于对象的一些基本概念，分析了 JavaScript 对象的设计思路。接下来又从运行时的角度，介绍了 JavaScript 对象的具体设计：具有高度动态性的属性集合。

很多人在思考 JavaScript 对象时，会带着已有的“对象”观来看问题，最后的结果当然就是“剪不断理还乱”了。

在后面的文章中，我会继续带你探索 JavaScript 对象的一些机制，看 JavaScript 如何基于这样的动态对象模型设计自己的原型系统，以及你熟悉的函数、类等基础设施。

你还知道哪些面向对象语言，它们的面向对象系统是怎样的？请留言告诉我吧！

猜你喜欢

07 | JavaScript对象：我们真的需要模拟类吗？

早期的 JavaScript 程序员一般都有过使用 JavaScript“模拟面向对象”的经历。

在上一篇文章我们已经讲到，JavaScript 本身就是面向对象的，它并不需要模拟，只是它实现面向对象的方式和主流的流派不太一样，所以才让很多人产生了误会。

那么，随着我们理解的思路继续深入，这些“模拟面向对象”，实际上做的事情就是“模拟基于类的面向对象”。

尽管我认为，“类”并非面向对象的全部，但我们不应该责备社区出现这样的方案，事实上，因为一些公司的政治原因，JavaScript 推出之时，管理层就要求它去模仿 Java。

所以，JavaScript 创始人 Brendan Eich 在“原型运行时”的基础上引入了 new、this 等语言特性，使之“看起来语法更像 Java”，而 Java 正是基于类的面向对象的代表语言之一。

但是 JavaScript 这样的半吊子模拟，缺少了继承等关键特性，导致大家试图对它进行修补，进而产生了种种互不相容的解决方案。

庆幸的是，从 ES6 开始，JavaScript 提供了 class 关键字来定义类，尽管，这样的方案仍然是基于原型运行时系统的模拟，但是它修正了之前的一些常见的“坑”，统一了社区的方案，这对语言的发展有着非常大的好处。

实际上，我认为“基于类”并非面向对象的唯一形态，如果我们把视线从“类”移开，Brendan 当年选择的原型系统，就是一个非常优秀的抽象对象的形式。

我们从头讲起。

什么是原型？

原型是顺应人类自然思维的产物。中文中有个成语叫做“照猫画虎”，这里的猫看起来就是虎的原型，所以，由此我们可以看出，用原型来描述对象的方法可以说是古已有之。

我们在上一节讲解面向对象的时候提到了：在不同的编程语言中，设计者也利用各种不同的语言特性来抽象描述对象。

最为成功的流派是使用“类”的方式来描述对象，这诞生了诸如 C++、Java 等流行的编程语言。这个流派叫做基于类的编程语言。

还有一种就是基于原型的编程语言，它们利用原型来描述对象。我们的 JavaScript 就是其中代表。

“基于类”的编程提倡使用一个关注分类和类之间关系开发模型。在这类语言中，总是先有类，再从类去实例化一个对象。类与类之间又可能会形成继承、组合等关系。类又往往与语言的类型系统整合，形成一定编译时的能力。

与此相对，“基于原型”的编程看起来更为提倡程序员去关注一系列对象实例的行为，而后才去关心如何将这些对象，划分到最近的使用方式相似的原型对象，而不是将它们分成类。

基于原型的面向对象系统通过“复制”的方式来创建新对象。一些语言的实现中，还允许复制一个空对象。这实际上就是创建一个全新的对象。

基于原型和基于类都能够满足基本的复用和抽象需求，但是适用的场景不太相同。

这就像专业人士可能喜欢在看到老虎的时候，喜欢用猫科豹属豹亚种来描述它，但是对一些不那么正式的场合，“大猫”可能更为接近直观的感受一些（插播一个冷知识：比起老虎来，美洲狮在历史上相当长时间都被划分为猫科猫属，所以性格也跟猫更相似，比较亲人）。

我们的 JavaScript 并非第一个使用原型的语言，在它之前，self、kevo 等语言已经开始使用原型来描述对象了。

事实上，Brendan 更是曾透露过，他最初的构想是一个拥有基于原型的面向对象能力的 scheme 语言（但是函数式的部分是另外的故事，这篇文章里，我暂时不做详细讲述）。

在 JavaScript 之前，原型系统就更多与高动态性语言配合，并且多数基于原型的语言提倡运行时的原型修改，我想，这应该是 Brendan 选择原型系统很重要的理由。

原型系统的“复制操作”有两种实现思路：

一个是并不真的去复制一个原型对象，而是使得新对象持有一个原型的引用；

另一个是切实地复制对象，从此两个对象再无关联。

历史上的基于原型语言因此产生了两个流派，显然，JavaScript 显然选择了前一种方式。

JavaScript 的原型

如果我们抛开 JavaScript 用于模拟 Java 类的复杂语法设施（如 new、Function Object、函数的 prototype 属性等），原型系统可以说相当简单，我可以用两条概括：

如果所有对象都有私有字段 [[prototype]]，就是对象的原型；

读一个属性，如果对象本身没有，则会继续访问对象的原型，直到原型为空或者找到为止。

这个模型在 ES 的各个历史版本中并没有很大改变，但从 ES6 以来，JavaScript 提供了一系列内置函数，以便更为直接地访问操纵原型。三个方法分别为：

Object.create 根据指定的原型创建新对象，原型可以是 null；

Object.getPrototypeOf 获得一个对象的原型；

Object.setPrototypeOf 设置一个对象的原型。

利用这三个方法，我们可以完全抛开类的思维，利用原型来实现抽象和复用。我用下面的代码展示了用原型来抽象猫和虎的例子。

var cat = {

    say(){

        console.log("meow~");

    },

    jump(){

        console.log("jump");

    }

}

var tiger = Object.create(cat,  {

    say:{

        writable:true,

        configurable:true,

        enumerable:true,

        value:function(){

            console.log("roar!");

        }

    }

})

var anotherCat = Object.create(cat);

anotherCat.say();

var anotherTiger = Object.create(tiger);

anotherTiger.say();

这段代码创建了一个“猫”对象，又根据猫做了一些修改创建了虎，之后我们完全可以用 Object.create 来创建另外的猫和虎对象，我们可以通过“原始猫对象”和“原始虎对象”来控制所有猫和虎的行为。

但是，在更早的版本中，程序员只能通过 Java 风格的类接口来操纵原型运行时，可以说非常别扭。

考虑到 new 和 prototype 属性等基础设施今天仍然有效，而且被很多代码使用，学习这些知识也有助于我们理解运行时的原型工作原理，下面我们试着回到过去，追溯一下早年的 JavaScript 中的原型和类。

早期版本中的类与原型

在早期版本的 JavaScript 中，“类”的定义是一个私有属性 [[class]]，语言标准为内置类型诸如 Number、String、Date 等指定了 [[class]] 属性，以表示它们的类。语言使用者唯一可以访问 [[class]] 属性的方式是 Object.prototype.toString。

以下代码展示了所有具有内置 class 属性的对象：

    var o = new Object;

    var n = new Number;

    var s = new String;

    var b = new Boolean;

    var d = new Date;

    var arg = function(){ return arguments }();

    var r = new RegExp;

    var f = new Function;

    var arr = new Array;

    var e = new Error;

    console.log([o, n, s, b, d, arg, r, f, arr, e].map(v => Object.prototype.toString.call(v)));

因此，在 ES3 和之前的版本，JS 中类的概念是相当弱的，它仅仅是运行时的一个字符串属性。

在 ES5 开始，[[class]] 私有属性被 Symbol.toStringTag 代替，Object.prototype.toString 的意义从命名上不再跟 class 相关。我们甚至可以自定义 Object.prototype.toString 的行为，以下代码展示了使用 Symbol.toStringTag 来自定义 Object.prototype.toString 的行为：

    var o = { [Symbol.toStringTag]: "MyObject" }

    console.log(o + "");

这里创建了一个新对象，并且给它唯一的一个属性 Symbol.toStringTag，我们用字符串加法触发了 Object.prototype.toString 的调用，发现这个属性最终对 Object.prototype.toString 的结果产生了影响。

但是，考虑到 JavaScript 语法中跟 Java 相似的部分，我们对类的讨论不能用“new 运算是针对构造器对象，而不是类”来试图回避。

所以，我们仍然要把 new 理解成 JavaScript 面向对象的一部分，下面我就来讲一下 new 操作具体做了哪些事情。

new 运算接受一个构造器和一组调用参数，实际上做了几件事：

以构造器的 prototype 属性（注意与私有字段 [[prototype]] 的区分）为原型，创建新对象；

将 this 和调用参数传给构造器，执行；

如果构造器返回的是对象，则返回，否则返回第一步创建的对象。

new 这样的行为，试图让函数对象在语法上跟类变得相似，但是，它客观上提供了两种方式，一是在构造器中添加属性，二是在构造器的 prototype 属性上添加属性。

下面代码展示了用构造器模拟类的两种方法:

function c1(){

    this.p1 = 1;

    this.p2 = function(){

        console.log(this.p1);

    }

}

var o1 = new c1;

o1.p2();

function c2(){

}

c2.prototype.p1 = 1;

c2.prototype.p2 = function(){

    console.log(this.p1);

}

var o2 = new c2;

o2.p2();

第一种方法是直接在构造器中修改 this，给 this 添加属性。

第二种方法是修改构造器的 prototype 属性指向的对象，它是从这个构造器构造出来的所有对象的原型。

没有 Object.create、Object.setPrototypeOf 的早期版本中，new 运算是唯一一个可以指定 [[prototype]] 的方法（当时的 mozilla 提供了私有属性 __proto__，但是多数环境并不支持），所以，当时已经有人试图用它来代替后来的 Object.create，我们甚至可以用它来实现一个 Object.create 的不完整的 polyfill，见以下代码：

Object.create = function(prototype){

    var cls = function(){}

    cls.prototype = prototype;

    return new cls;

}

这段代码创建了一个空函数作为类，并把传入的原型挂在了它的 prototype，最后创建了一个它的实例，根据 new 的行为，这将产生一个以传入的第一个参数为原型的对象。

这个函数无法做到与原生的 Object.create 一致，一个是不支持第二个参数，另一个是不支持 null 作为原型，所以放到今天意义已经不大了。

ES6 中的类

好在 ES6 中加入了新特性 class，new 跟 function 搭配的怪异行为终于可以退休了（虽然运行时没有改变），在任何场景，我都推荐使用 ES6 的语法来定义类，而令 function 回归原本的函数语义。下面我们就来看一下 ES6 中的类。

ES6 中引入了 class 关键字，并且在标准中删除了所有 [[class]] 相关的私有属性描述，类的概念正式从属性升级成语言的基础设施，从此，基于类的编程方式成为了 JavaScript 的官方编程范式。

我们先看下类的基本写法：

class Rectangle {

  constructor(height, width) {

    this.height = height;

    this.width = width;

  }

  // Getter

  get area() {

    return this.calcArea();

  }

  // Method

  calcArea() {

    return this.height * this.width;

  }

}

在现有的类语法中，getter/setter 和 method 是兼容性最好的。

我们通过 get/set 关键字来创建 getter，通过括号和大括号来创建方法，数据型成员最好写在构造器里面。

类的写法实际上也是由原型运行时来承载的，逻辑上 JavaScript 认为每个类是有共同原型的一组对象，类中定义的方法和属性则会被写在原型对象之上。

此外，最重要的是，类提供了继承能力。我们来看一下下面的代码。

class Animal {

  constructor(name) {

    this.name = name;

  }

  speak() {

    console.log(this.name + ' makes a noise.');

  }

}

class Dog extends Animal {

  constructor(name) {

    super(name); // call the super class constructor and pass in the name parameter

  }

  speak() {

    console.log(this.name + ' barks.');

  }

}

let d = new Dog('Mitzie');

d.speak(); // Mitzie barks.

以上代码创造了 Animal 类，并且通过 extends 关键字让 Dog 继承了它，展示了最终调用子类的 speak 方法获取了父类的 name。

比起早期的原型模拟方式，使用 extends 关键字自动设置了 constructor，并且会自动调用父类的构造函数，这是一种更少坑的设计。

所以当我们使用类的思想来设计代码时，应该尽量使用 class 来声明类，而不是用旧语法，拿函数来模拟对象。

一些激进的观点认为，class 关键字和箭头运算符可以完全替代旧的 function 关键字，它更明确地区分了定义函数和定义类两种意图，我认为这是有一定道理的。

总结

在新的 ES 版本中，我们不再需要模拟类了：我们有了光明正大的新语法。而原型体系同时作为一种编程范式和运行时机制存在。

我们可以自由选择原型或者类作为代码的抽象风格，但是无论我们选择哪种，理解运行时的原型系统都是很有必要的一件事。

在你的工作中，是使用 class 还是仍然在用 function 来定义“类”？为什么这么做？如何把使用 function 定义类的代码改造到 class 的新语法？

欢迎给我留言，我们一起讨论。

08 | JavaScript对象：你知道全部的对象分类吗？

你好，我是 winter。

在前面的课程中，我已经讲解了 JavaScript 对象的一些基础知识。但是，我们所讲解的对象，只是特定的一部分，并不能涵盖全部的 JavaScript 对象。

比如说，我们不论怎样编写代码，都没法绕开 Array，实现一个跟原生的数组行为一模一样的对象，这是由于原生数组的底层实现了一个自动随着下标变化的 length 属性。

并且，在浏览器环境中，我们也无法单纯依靠 JavaScript 代码实现 div 对象，只能靠 document.createElement 来创建。这也说明了 JavaScript 的对象机制并非简单的属性集合 + 原型。

我们日常工作中，接触到的主要 API，几乎都是由今天所讲解的这些对象提供的。理解这些对象的性质，我们才能真正理解我们使用的 API 的一些特性。

JavaScript 中的对象分类

我们可以把对象分成几类。

宿主对象（host Objects）：由 JavaScript 宿主环境提供的对象，它们的行为完全由宿主环境决定。

内置对象（Built-in Objects）：由 JavaScript 语言提供的对象。

固有对象（Intrinsic Objects ）：由标准规定，随着 JavaScript 运行时创建而自动创建的对象实例。

原生对象（Native Objects）：可以由用户通过 Array、RegExp 等内置构造器或者特殊语法创建的对象。

普通对象（Ordinary Objects）：由{}语法、Object 构造器或者 class 关键字定义类创建的对象，它能够被原型继承。

下面我会为你一一讲解普通对象之外的对象类型。

宿主对象

首先我们来看看宿主对象。

JavaScript 宿主对象千奇百怪，但是前端最熟悉的无疑是浏览器环境中的宿主了。

在浏览器环境中，我们都知道全局对象是 window，window 上又有很多属性，如 document。

实际上，这个全局对象 window 上的属性，一部分来自 JavaScript 语言，一部分来自浏览器环境。

JavaScript 标准中规定了全局对象属性，w3c 的各种标准中规定了 Window 对象的其它属性。

宿主对象也分为固有的和用户可创建的两种，比如 document.createElement 就可以创建一些 dom 对象。

宿主也会提供一些构造器，比如我们可以使用 new Image 来创建 img 元素，这些我们会在浏览器的 API 部分详细讲解。

内置对象·固有对象

我们在前面说过，固有对象是由标准规定，随着 JavaScript 运行时创建而自动创建的对象实例。

固有对象在任何 JS 代码执行前就已经被创建出来了，它们通常扮演者类似基础库的角色。我们前面提到的“类”其实就是固有对象的一种。

ECMA 标准为我们提供了一份固有对象表，里面含有 150+ 个固有对象。你可以通过这个链接查看。

但是遗憾的是，这个表格并不完整。所以在本篇的末尾，我设计了一个小实验（小实验：获取全部 JavaScript 固有对象），你可以自己尝试一下，数一数一共有多少个固有对象。

内置对象·原生对象

我们把 JavaScript 中，能够通过语言本身的构造器创建的对象称作原生对象。在 JavaScript 标准中，提供了 30 多个构造器。按照我的理解，按照不同应用场景，我把原生对象分成了以下几个种类。

通过这些构造器，我们可以用 new 运算创建新的对象，所以我们把这些对象称作原生对象。
几乎所有这些构造器的能力都是无法用纯 JavaScript 代码实现的，它们也无法用 class/extend 语法来继承。

这些构造器创建的对象多数使用了私有字段, 例如：

Error: [[ErrorData]]

Boolean: [[BooleanData]]

Number: [[NumberData]]

Date: [[DateValue]]

RegExp: [[RegExpMatcher]]

Symbol: [[SymbolData]]

Map: [[MapData]]

这些字段使得原型继承方法无法正常工作，所以，我们可以认为，所有这些原生对象都是为了特定能力或者性能，而设计出来的“特权对象”。

用对象来模拟函数与构造器：函数对象与构造器对象

我在前面介绍了对象的一般分类，在 JavaScript 中，还有一个看待对象的不同视角，这就是用对象来模拟函数和构造器。

事实上，JavaScript 为这一类对象预留了私有字段机制，并规定了抽象的函数对象与构造器对象的概念。

函数对象的定义是：具有 [[call]] 私有字段的对象，构造器对象的定义是：具有私有字段 [[construct]] 的对象。

JavaScript 用对象模拟函数的设计代替了一般编程语言中的函数，它们可以像其它语言的函数一样被调用、传参。任何宿主只要提供了“具有 [[call]] 私有字段的对象”，就可以被 JavaScript 函数调用语法支持。

[[call]] 私有字段必须是一个引擎中定义的函数，需要接受 this 值和调用参数，并且会产生域的切换，这些内容，我将会在属性访问和执行过程两个章节详细讲述。

我们可以这样说，任何对象只需要实现 [[call]]，它就是一个函数对象，可以去作为函数被调用。而如果它能实现 [[construct]]，它就是一个构造器对象，可以作为构造器被调用。

对于为 JavaScript 提供运行环境的程序员来说，只要字段符合，我们在上文中提到的宿主对象和内置对象（如 Symbol 函数）可以模拟函数和构造器。

当然了，用户用 function 关键字创建的函数必定同时是函数和构造器。不过，它们表现出来的行为效果却并不相同。

对于宿主和内置对象来说，它们实现 [[call]]（作为函数被调用）和 [[construct]]（作为构造器被调用）不总是一致的。比如内置对象 Date 在作为构造器调用时产生新的对象，作为函数时，则产生字符串，见以下代码：

    console.log(new Date); // 1

    console.log(Date())

而浏览器宿主环境中，提供的 Image 构造器，则根本不允许被作为函数调用。

console.log(new Image);

console.log(Image());// 抛出错误

再比如基本类型（String、Number、Boolean），它们的构造器被当作函数调用，则产生类型转换的效果。

值得一提的是，在 ES6 之后 => 语法创建的函数仅仅是函数，它们无法被当作构造器使用，见以下代码：

    new (a => 0) // error

对于用户使用 function 语法或者 Function 构造器创建的对象来说，[[call]] 和 [[construct]] 行为总是相似的，它们执行同一段代码。

我们看一下示例。

function f(){

    return 1;

}

var v = f(); // 把 f 作为函数调用

var o = new f(); // 把 f 作为构造器调用

我们大致可以认为，它们 [[construct]] 的执行过程如下：

以 Object.protoype 为原型创建一个新对象；

以新对象为 this，执行函数的 [[call]]；

如果 [[call]] 的返回值是对象，那么，返回这个对象，否则返回第一步创建的新对象。

这样的规则造成了个有趣的现象，如果我们的构造器返回了一个新的对象，那么 new 创建的新对象就变成了一个构造函数之外完全无法访问的对象，这一定程度上可以实现“私有”。

function cls(){

    this.a = 100;

    return {

        getValue:() => this.a

    }

}

var o = new cls;

o.getValue(); //100

//a 在外面永远无法访问到

特殊行为的对象

除了上面介绍的对象之外，在固有对象和原生对象中，有一些对象的行为跟正常对象有很大区别。

它们常见的下标运算（就是使用中括号或者点来做属性访问）或者设置原型跟普通对象不同，这里我简单总结一下。

Array：Array 的 length 属性根据最大的下标自动发生变化。

Object.prototype：作为所有正常对象的默认原型，不能再给它设置原型了。

String：为了支持下标运算，String 的正整数属性访问会去字符串里查找。

Arguments：arguments 的非负整数型下标属性跟对应的变量联动。

模块的 namespace 对象：特殊的地方非常多，跟一般对象完全不一样，尽量只用于 import 吧。

类型数组和数组缓冲区：跟内存块相关联，下标运算比较特殊。

bind 后的 function：跟原来的函数相关联。

总结

在这篇文章中，我们介绍了一些不那么常规的对象，并且我还介绍了 JavaScript 中用对象来模拟函数和构造器的机制。

这是一些不那么有规律、不那么优雅的知识，而 JavaScript 正是通过这些对象，提供了很多基础的能力。

我们这次课程留给大家一个挑战任务：不使用 new 运算符，尽可能找到获得对象的方法。

例子：

var o = {}

var o = function(){}

请大家把自己的答案留言给我，我们来比比看谁找到的多。

小实验：获取全部 JavaScript 固有对象

我们从 JavaScript 标准中可以找到全部的 JS 对象定义。JS 语言规定了全局对象的属性。

三个值：
Infinity、NaN、undefined。

九个函数：

eval

isFinite

isNaN

parseFloat

parseInt

decodeURI

decodeURIComponent

encodeURI

encodeURIComponent

一些构造器：
Array、Date、RegExp、Promise、Proxy、Map、WeakMap、Set、WeapSet、Function、Boolean、String、Number、Symbol、Object、Error、EvalError、RangeError、ReferenceError、SyntaxError、TypeError
URIError、ArrayBuffer、SharedArrayBuffer、DataView、Typed Array、Float32Array、Float64Array、Int8Array、Int16Array、Int32Array、UInt8Array、UInt16Array、UInt32Array、UInt8ClampedArray。

四个用于当作命名空间的对象：

Atomics

JSON

Math

Reflect

我们使用广度优先搜索，查找这些对象所有的属性和 Getter/Setter，就可以获得 JavaScript 中所有的固有对象。

请你试着先不看我的代码，在自己的浏览器中计算出来 JavaScript 有多少固有对象。

var set = new Set();

var objects = [

    eval,

    isFinite,

    isNaN,

    parseFloat,

    parseInt,

    decodeURI,

    decodeURIComponent,

    encodeURI,

    encodeURIComponent,

    Array,

    Date,

    RegExp,

    Promise,

    Proxy,

    Map,

    WeakMap,

    Set,

    WeakSet,

    Function,

    Boolean,

    String,

    Number,

    Symbol,

    Object,

    Error,

    EvalError,

    RangeError,

    ReferenceError,

    SyntaxError,

    TypeError,

    URIError,

    ArrayBuffer,

    SharedArrayBuffer,

    DataView,

    Float32Array,

    Float64Array,

    Int8Array,

    Int16Array,

    Int32Array,

    Uint8Array,

    Uint16Array,

    Uint32Array,

    Uint8ClampedArray,

    Atomics,

    JSON,

    Math,

    Reflect];

objects.forEach(o => set.add(o));

for(var i = 0; i < objects.length; i++) {

    var o = objects[i]

    for(var p of Object.getOwnPropertyNames(o)) {

        var d = Object.getOwnPropertyDescriptor(o, p)

        if( (d.value !== null && typeof d.value === "object") || (typeof d.value === "function"))

            if(!set.has(d.value))

                set.add(d.value), objects.push(d.value);

        if( d.get )

            if(!set.has(d.get))

                set.add(d.get), objects.push(d.get);

        if( d.set )

            if(!set.has(d.set))

                set.add(d.set), objects.push(d.set);

    }

}

16 | JavaScript执行（一）：Promise里的代码为什么比setTimeout先执行？

你好，我是 winter。这一部分我们来讲一讲 JavaScript 的执行。

首先我们考虑一下，如果我们是浏览器或者 Node 的开发者，我们该如何使用 JavaScript 引擎。

当拿到一段 JavaScript 代码时，浏览器或者 Node 环境首先要做的就是；传递给 JavaScript 引擎，并且要求它去执行。

然而，执行 JavaScript 并非一锤子买卖，宿主环境当遇到一些事件时，会继续把一段代码传递给 JavaScript 引擎去执行，此外，我们可能还会提供 API 给 JavaScript 引擎，比如 setTimeout 这样的 API，它会允许 JavaScript 在特定的时机执行。

所以，我们首先应该形成一个感性的认知：一个 JavaScript 引擎会常驻于内存中，它等待着我们（宿主）把 JavaScript 代码或者函数传递给它执行。

在 ES3 和更早的版本中，JavaScript 本身还没有异步执行代码的能力，这也就意味着，宿主环境传递给 JavaScript 引擎一段代码，引擎就把代码直接顺次执行了，这个任务也就是宿主发起的任务。

但是，在 ES5 之后，JavaScript 引入了 Promise，这样，不需要浏览器的安排，JavaScript 引擎本身也可以发起任务了。

由于我们这里主要讲 JavaScript 语言，那么采纳 JSC 引擎的术语，我们把宿主发起的任务称为宏观任务，把 JavaScript 引擎发起的任务称为微观任务。

宏观和微观任务

JavaScript 引擎等待宿主环境分配宏观任务，在操作系统中，通常等待的行为都是一个事件循环，所以在 Node 术语中，也会把这个部分称为事件循环。

不过，术语本身并非我们需要重点讨论的内容，我们在这里把重点放在事件循环的原理上。在底层的 C/C++ 代码中，这个事件循环是一个跑在独立线程中的循环，我们用伪代码来表示，大概是这样的：

while(TRUE) {

    r = wait();

    execute(r);

}

我们可以看到，整个循环做的事情基本上就是反复“等待 - 执行”。当然，实际的代码中并没有这么简单，还有要判断循环是否结束、宏观任务队列等逻辑，这里为了方便你理解，我就把这些都省略掉了。

这里每次的执行过程，其实都是一个宏观任务。我们可以大概理解：宏观任务的队列就相当于事件循环。

在宏观任务中，JavaScript 的 Promise 还会产生异步代码，JavaScript 必须保证这些异步代码在一个宏观任务中完成，因此，每个宏观任务中又包含了一个微观任务队列：

有了宏观任务和微观任务机制，我们就可以实现 JS 引擎级和宿主级的任务了，例如：Promise 永远在队列尾部添加微观任务。setTimeout 等宿主 API，则会添加宏观任务。

接下来，我们来详细介绍一下 Promise。

Promise

Promise 是 JavaScript 语言提供的一种标准化的异步管理方式，它的总体思想是，需要进行 io、等待或者其它异步操作的函数，不返回真实结果，而返回一个“承诺”，函数的调用方可以在合适的时机，选择等待这个承诺兑现（通过 Promise 的 then 方法的回调）。

Promise 的基本用法示例如下：

    function sleep(duration) {

        return new Promise(function(resolve, reject) {

            setTimeout(resolve,duration);

        })

    }

    sleep(1000).then( ()=> console.log("finished"));

这段代码定义了一个函数 sleep，它的作用是等候传入参数指定的时长。

Promise 的 then 回调是一个异步的执行过程，下面我们就来研究一下 Promise 函数中的执行顺序，我们来看一段代码示例：

    var r = new Promise(function(resolve, reject){

        console.log("a");

        resolve()

    });

    r.then(() => console.log("c"));

    console.log("b")

我们执行这段代码后，注意输出的顺序是 a b c。在进入 console.log(“b”) 之前，毫无疑问 r 已经得到了 resolve，但是 Promise 的 resolve 始终是异步操作，所以 c 无法出现在 b 之前。

接下来我们试试跟 setTimeout 混用的 Promise。

在这段代码中，我设置了两段互不相干的异步操作：通过 setTimeout 执行 console.log(“d”)，通过 Promise 执行 console.log(“c”)

    var r = new Promise(function(resolve, reject){

        console.log("a");

        resolve()

    });

    setTimeout(()=>console.log("d"), 0)

    r.then(() => console.log("c"));

    console.log("b")

我们发现，不论代码顺序如何，d 必定发生在 c 之后，因为 Promise 产生的是 JavaScript 引擎内部的微任务，而 setTimeout 是浏览器 API，它产生宏任务。

为了理解微任务始终先于宏任务，我们设计一个实验：执行一个耗时 1 秒的 Promise。

    setTimeout(()=>console.log("d"), 0)

    var r = new Promise(function(resolve, reject){

        resolve()

    });

    r.then(() => {

        var begin = Date.now();

        while(Date.now() - begin < 1000);

        console.log("c1")

        new Promise(function(resolve, reject){

            resolve()

        }).then(() => console.log("c2"))

    });

这里我们强制了 1 秒的执行耗时，这样，我们可以确保任务 c2 是在 d 之后被添加到任务队列。

我们可以看到，即使耗时一秒的 c1 执行完毕，再 enque 的 c2，仍然先于 d 执行了，这很好地解释了微任务优先的原理。

通过一系列的实验，我们可以总结一下如何分析异步执行的顺序：

首先我们分析有多少个宏任务；

在每个宏任务中，分析有多少个微任务；

根据调用次序，确定宏任务中的微任务执行次序；

根据宏任务的触发规则和调用次序，确定宏任务的执行次序；

确定整个顺序。

我们再来看一个稍微复杂的例子：

    function sleep(duration) {

        return new Promise(function(resolve, reject) {

            console.log("b");

            setTimeout(resolve,duration);

        })

    }

    console.log("a");

    sleep(5000).then(()=>console.log("c"));

这是一段非常常用的封装方法，利用 Promise 把 setTimeout 封装成可以用于异步的函数。

我们首先来看，setTimeout 把整个代码分割成了 2 个宏观任务，这里不论是 5 秒还是 0 秒，都是一样的。

第一个宏观任务中，包含了先后同步执行的 console.log(“a”); 和 console.log(“b”);。

setTimeout 后，第二个宏观任务执行调用了 resolve，然后 then 中的代码异步得到执行，所以调用了 console.log(“c”)，最终输出的顺序才是： a b c。

Promise 是 JavaScript 中的一个定义，但是实际编写代码时，我们可以发现，它似乎并不比回调的方式书写更简单，但是从 ES6 开始，我们有了 async/await，这个语法改进跟 Promise 配合，能够有效地改善代码结构。

新特性：async/await

async/await 是 ES2016 新加入的特性，它提供了用 for、if 等代码结构来编写异步的方式。它的运行时基础是 Promise，面对这种比较新的特性，我们先来看一下基本用法。

async 函数必定返回 Promise，我们把所有返回 Promise 的函数都可以认为是异步函数。

async 函数是一种特殊语法，特征是在 function 关键字之前加上 async 关键字，这样，就定义了一个 async 函数，我们可以在其中使用 await 来等待一个 Promise。

function sleep(duration) {

    return new Promise(function(resolve, reject) {

        setTimeout(resolve,duration);

    })

}

async function foo(){

    console.log("a")

    await sleep(2000)

    console.log("b")

}

这段代码利用了我们之前定义的 sleep 函数。在异步函数 foo 中，我们调用 sleep。

async 函数强大之处在于，它是可以嵌套的。我们在定义了一批原子操作的情况下，可以利用 async 函数组合出新的 async 函数。

function sleep(duration) {

    return new Promise(function(resolve, reject) {

        setTimeout(resolve,duration);

    })

}

async function foo(name){

    await sleep(2000)

    console.log(name)

}

async function foo2(){

    await foo("a");

    await foo("b");

}

这里 foo2 用 await 调用了两次异步函数 foo，可以看到，如果我们把 sleep 这样的异步操作放入某一个框架或者库中，使用者几乎不需要了解 Promise 的概念即可进行异步编程了。

此外，generator/iterator 也常常被跟异步一起来讲，我们必须说明 generator/iterator 并非异步代码，只是在缺少 async/await 的时候，一些框架（最著名的要数 co）使用这样的特性来模拟 async/await。

但是 generator 并非被设计成实现异步，所以有了 async/await 之后，generator/iterator 来模拟异步的方法应该被废弃。

结语

在今天的文章里，我们学习了 JavaScript 执行部分的知识，首先我们学习了 JavaScript 的宏观任务和微观任务相关的知识。我们把宿主发起的任务称为宏观任务，把 JavaScript 引擎发起的任务称为微观任务。许多的微观任务的队列组成了宏观任务。

除此之外，我们还展开介绍了用 Promise 来添加微观任务的方式，并且介绍了 async/await 这个语法的改进。

最后，留给你一个小练习：我们现在要实现一个红绿灯，把一个圆形 div 按照绿色 3 秒，黄色 1 秒，红色 2 秒循环改变背景色，你会怎样编写这个代码呢？欢迎你留言讨论。

17 | JavaScript执行（二）：闭包和执行上下文到底是怎么回事？

你好，我是 winter。

在上一课，我们了解了 JavaScript 执行中最粗粒度的任务：传给引擎执行的代码段。并且，我们还根据“由 JavaScript 引擎发起”还是“由宿主发起”，分成了宏观任务和微观任务，接下来我们继续去看一看更细的执行粒度。

一段 JavaScript 代码可能会包含函数调用的相关内容，从今天开始，我们就用两节课的时间来了解一下函数的执行。

我们今天要讲的知识在网上有不同的名字，比较常见的可能有：

闭包；

作用域链；

执行上下文；

this 值。

实际上，尽管它们是表示不同的意思的术语，所指向的几乎是同一部分知识，那就是函数执行过程相关的知识。我们可以简单看一下图。

看着也许会有点晕，别着急，我会和你共同理一下它们之间的关系。

当然，除了让你理解函数执行过程的知识，理清这些概念也非常重要。所以我们先来讲讲这个有点复杂的概念：闭包。

闭包

闭包翻译自英文单词 closure，这是个不太好翻译的词，在计算机领域，它就有三个完全不相同的意义：编译原理中，它是处理语法产生式的一个步骤；计算几何中，它表示包裹平面点集的凸多边形（翻译作凸包）；而在编程语言领域，它表示一种函数。

闭包这个概念第一次出现在 1964 年的《The Computer Journal》上，由 P. J. Landin 在《The mechanical evaluation of expressions》一文中提出了 applicative expression 和 closure 的概念。

在上世纪 60 年代，主流的编程语言是基于 lambda 演算的函数式编程语言，所以这个最初的闭包定义，使用了大量的函数式术语。一个不太精确的描述是“带有一系列信息的λ表达式”。对函数式语言而言，λ表达式其实就是函数。

我们可以这样简单理解一下，闭包其实只是一个绑定了执行环境的函数，这个函数并不是印在书本里的一条简单的表达式，闭包与普通函数的区别是，它携带了执行的环境，就像人在外星中需要自带吸氧的装备一样，这个函数也带有在程序中生存的环境。

这个古典的闭包定义中，闭包包含两个部分。

环境部分

环境

标识符列表

表达式部分

当我们把视角放在 JavaScript 的标准中，我们发现，标准中并没有出现过 closure 这个术语，但是，我们却不难根据古典定义，在 JavaScript 中找到对应的闭包组成部分。

环境部分

环境：函数的词法环境（执行上下文的一部分）

标识符列表：函数中用到的未声明的变量

表达式部分：函数体

至此，我们可以认为，JavaScript 中的函数完全符合闭包的定义。它的环境部分是函数词法环境部分组成，它的标识符列表是函数中用到的未声明变量，它的表达式部分就是函数体。

这里我们容易产生一个常见的概念误区，有些人会把 JavaScript 执行上下文，或者作用域（Scope，ES3 中规定的执行上下文的一部分）这个概念当作闭包。

实际上 JavaScript 中跟闭包对应的概念就是“函数”，可能是这个概念太过于普通，跟闭包看起来又没什么联系，所以大家才不自觉地把这个概念对应到了看起来更特别的“作用域”吧（其实我早年也是这么理解闭包，直到后来被朋友纠正，查了资料才改正过来）。

执行上下文：执行的基础设施

相比普通函数，JavaScript 函数的主要复杂性来自于它携带的“环境部分”。当然，发展到今天的 JavaScript，它所定义的环境部分，已经比当初经典的定义复杂了很多。

JavaScript 中与闭包“环境部分”相对应的术语是“词法环境”，但是 JavaScript 函数比λ函数要复杂得多，我们还要处理 this、变量声明、with 等等一系列的复杂语法，λ函数中可没有这些东西，所以，在 JavaScript 的设计中，词法环境只是 JavaScript 执行上下文的一部分。

JavaScript 标准把一段代码（包括函数），执行所需的所有信息定义为：“执行上下文”。

因为这部分术语经历了比较多的版本和社区的演绎，所以定义比较混乱，这里我们先来理一下 JavaScript 中的概念。

执行上下文在 ES3 中，包含三个部分。

scope：作用域，也常常被叫做作用域链。

variable object：变量对象，用于存储变量的对象。

this value：this 值。

在 ES5 中，我们改进了命名方式，把执行上下文最初的三个部分改为下面这个样子。

lexical environment：词法环境，当获取变量时使用。

variable environment：变量环境，当声明变量时使用。

this value：this 值。

在 ES2018 中，执行上下文又变成了这个样子，this 值被归入 lexical environment，但是增加了不少内容。

lexical environment：词法环境，当获取变量或者 this 值时使用。

variable environment：变量环境，当声明变量时使用

code evaluation state：用于恢复代码执行位置。

Function：执行的任务是函数时使用，表示正在被执行的函数。

ScriptOrModule：执行的任务是脚本或者模块时使用，表示正在被执行的代码。

Realm：使用的基础库和内置对象实例。

Generator：仅生成器上下文有这个属性，表示当前生成器。

我们在这里介绍执行上下文的各个版本定义，是考虑到你可能会从各种网上的文章中接触这些概念，如果不把它们理清楚，我们就很难分辨对错。如果是我们自己使用，我建议统一使用最新的 ES2018 中规定的术语定义。

尽管我们介绍了这些定义，但我并不打算按照 JavaScript 标准的思路，从实现的角度去介绍函数的执行过程，这是不容易被理解的。

我想试着从代码实例出发，跟你一起推导函数执行过程中需要哪些信息，它们又对应着执行上下文中的哪些部分。

比如，我们看以下的这段 JavaScript 代码：

var b = {}

let c = 1

this.a = 2;

要想正确执行它，我们需要知道以下信息：

var 把 b 声明到哪里；

b 表示哪个变量；

b 的原型是哪个对象；

let 把 c 声明到哪里；

this 指向哪个对象。

这些信息就需要执行上下文来给出了，这段代码出现在不同的位置，甚至在每次执行中，会关联到不同的执行上下文，所以，同样的代码会产生不一样的行为。

在这两篇文章中，我会基本覆盖执行上下文的组成部分，本篇我们先讲 var 声明与赋值，let，realm 三个特性来分析上下文提供的信息，分析执行上下文中提供的信息。

var 声明与赋值

我们来分析一段代码：

var b = 1

通常我们认为它声明了 b，并且为它赋值为 1，var 声明作用域函数执行的作用域。也就是说，var 会穿透 for 、if 等语句。

在只有 var，没有 let 的旧 JavaScript 时代，诞生了一个技巧，叫做：立即执行的函数表达式（IIFE），通过创建一个函数，并且立即执行，来构造一个新的域，从而控制 var 的范围。

由于语法规定了 function 关键字开头是函数声明，所以要想让函数变成函数表达式，我们必须得加点东西，最常见的做法是加括号。

(function(){

    var a;

    //code

}());

(function(){

    var a;

    //code

})();

但是，括号有个缺点，那就是如果上一行代码不写分号，括号会被解释为上一行代码最末的函数调用，产生完全不符合预期，并且难以调试的行为，加号等运算符也有类似的问题。所以一些推荐不加分号的代码风格规范，会要求在括号前面加上分号。

;(function(){

    var a;

    //code

}())

;(function(){

    var a;

    //code

})()

我比较推荐的写法是使用 void 关键字。也就是下面的这种形式。

void function(){

    var a;

    //code

}();

这有效避免了语法问题，同时，语义上 void 运算表示忽略后面表达式的值，变成 undefined，我们确实不关心 IIFE 的返回值，所以语义也更为合理。

值得特别注意的是，有时候 var 的特性会导致声明的变量和被赋值的变量是两个 b，JavaScript 中有特例，那就是使用 with 的时候：

var b;

void function(){

    var env = {b:1};

    b = 2;

    console.log("In function b:", b);

    with(env) {

        var b = 3;

        console.log("In with b:", b);

    }

}();

console.log("Global b:", b);

在这个例子中，我们利用立即执行的函数表达式（IIFE）构造了一个函数的执行环境，并且在里面使用了我们一开头的代码。

可以看到，在 Global function with 三个环境中，b 的值都不一样，而在 function 环境中，并没有出现 var b，这说明 with 内的 var b 作用到了 function 这个环境当中。

var b = {} 这样一句对两个域产生了作用，从语言的角度是个非常糟糕的设计，这也是一些人坚定地反对在任何场景下使用 with 的原因之一。

let

let 是 ES6 开始引入的新的变量声明模式，比起 var 的诸多弊病，let 做了非常明确的梳理和规定。

为了实现 let，JavaScript 在运行时引入了块级作用域。也就是说，在 let 出现之前，JavaScript 的 if for 等语句皆不产生作用域。

我简单统计了下，以下语句会产生 let 使用的作用域：

for；

if；

switch；

try/catch/finally。

Realm

在最新的标准（9.0）中，JavaScript 引入了一个新概念 Realm，它的中文意思是“国度”“领域”“范围”。这个英文的用法就有点比喻的意思，几个翻译都不太适合 JavaScript 语境，所以这里就不翻译啦。

我们继续来看这段代码：

var b = {}

在 ES2016 之前的版本中，标准中甚少提及{}的原型问题。但在实际的前端开发中，通过 iframe 等方式创建多 window 环境并非罕见的操作，所以，这才促成了新概念 Realm 的引入。

Realm 中包含一组完整的内置对象，而且是复制关系。

对不同 Realm 中的对象操作，会有一些需要格外注意的问题，比如 instanceOf 几乎是失效的。

以下代码展示了在浏览器环境中获取来自两个 Realm 的对象，它们跟本土的 Object 做 instanceOf 时会产生差异：

var iframe = document.createElement('iframe')

document.documentElement.appendChild(iframe)

iframe.src="javascript:var b = {};"

var b1 = iframe.contentWindow.b;

var b2 = {};

console.log(typeof b1, typeof b2); //object object

console.log(b1 instanceof Object, b2 instanceof Object); //false true

可以看到，由于 b1、 b2 由同样的代码“ {} ”在不同的 Realm 中执行，所以表现出了不同的行为。

结语

在今天的课程中，我帮你梳理了一些概念：有编程语言的概念闭包，也有各个版本中的 JavaScript 标准中的概念：执行上下文、作用域、this 值等等。

之后我们又从代码的角度，分析了一些执行上下文中所需要的信息，并从var、let、对象字面量等语法中，推导出了词法作用域、变量作用域、Realm的设计。

最后留给你一个问题：你喜欢使用 let 还是 var？听过今天的课程，你的想法是否有改变呢？为什么？

18 | JavaScript执行（三）：你知道现在有多少种函数吗？

在前一篇文章中，我们大致了解了执行上下文是什么，也知道了任何语句的执行都会依赖特定的上下文。

一旦上下文被切换，整个语句的效果可能都会发生改变。那么，切换上下文的时机就显得非常重要了。

在 JavaScript，切换上下文最主要的场景是函数调用。在这一课，我们就来讲讲函数调用切换上下文的事情。我们在讲函数调用之前，首先来认识一下函数家族。

函数

在 ES2018 中，函数已经是一个很复杂的体系了，我在这里整理了一下。

第一种，普通函数：用 function 关键字定义的函数。

示例：

function foo(){

    // code

}

第二种，箭头函数：用 => 运算符定义的函数。
示例:

const foo = () => {

    // code

}

第三种，方法：在 class 中定义的函数。

示例：

class C {

    foo(){

        //code

    }

}

第四种，生成器函数：用 function * 定义的函数。

示例：

function* foo(){

    // code

}

第五种，类：用 class 定义的类，实际上也是函数。

示例：

class Foo {

    constructor(){

        //code

    }

}

第六 / 七 / 八种，异步函数：普通函数、箭头函数和生成器函数加上 async 关键字。

示例：

async function foo(){

    // code

}

const foo = async () => {

    // code

}

async function foo*(){

    // code

}

ES6 以来，大量加入的新语法极大地方便了我们编程的同时，也增加了很多我们理解的心智负担。要想认识这些函数的执行上下文切换，我们必须要对它们行为上的区别有所了解。

对普通变量而言，这些函数并没有本质区别，都是遵循了“继承定义时环境”的规则，它们的一个行为差异在于 this 关键字。

那么，this 关键字是什么呢，我们一起来看一看。

this 关键字的行为

this 是 JavaScript 中的一个关键字，它的使用方法类似于一个变量（但是 this 跟变量的行为有很多不同，上一节课我们讲了一些普通变量的行为和机制，也就是 var 声明和赋值、let 的内容）。

this 是执行上下文中很重要的一个组成部分。同一个函数调用方式不同，得到的 this 值也不同，我们看一个例子：

function showThis(){

    console.log(this);

}

var o = {

    showThis: showThis

}

showThis(); // global

o.showThis(); // o

在这个例子中，我们定义了函数 showThis，我们把它赋值给一个对象 o 的属性，然后尝试分别使用两个引用来调用同一个函数，结果得到了不同的 this 值。

普通函数的 this 值由“调用它所使用的引用”决定，其中奥秘就在于：我们获取函数的表达式，它实际上返回的并非函数本身，而是一个 Reference 类型（记得我们在类型一章讲过七种标准类型吗，正是其中之一）。

Reference 类型由两部分组成：一个对象和一个属性值。不难理解 o.showThis 产生的 Reference 类型，即由对象 o 和属性“showThis”构成。

当做一些算术运算（或者其他运算时），Reference 类型会被解引用，即获取真正的值（被引用的内容）来参与运算，而类似函数调用、delete 等操作，都需要用到 Reference 类型中的对象。

在这个例子中，Reference 类型中的对象被当作 this 值，传入了执行函数时的上下文当中。

至此，我们对 this 的解释已经非常清晰了：调用函数时使用的引用，决定了函数执行时刻的 this 值。

实际上从运行时的角度来看，this 跟面向对象毫无关联，它是与函数调用时使用的表达式相关。

这个设计来自 JavaScript 早年，通过这样的方式，巧妙地模仿了 Java 的语法，但是仍然保持了纯粹的“无类”运行时设施。

如果，我们把这个例子稍作修改，换成箭头函数，结果就不一样了：

const showThis = () => {

    console.log(this);

}

var o = {

    showThis: showThis

}

showThis(); // global

o.showThis(); // global

我们看到，改为箭头函数后，不论用什么引用来调用它，都不影响它的 this 值。

接下来我们看看“方法”，它的行为又不一样了：

class C {

    showThis() {

        console.log(this);

    }

}

var o = new C();

var showThis = o.showThis;

showThis(); // undefined

o.showThis(); // o

这里我们创建了一个类 C，并且实例化出对象 o，再把 o 的方法赋值给了变量 showThis。

这时候，我们使用 showThis 这个引用去调用方法时，得到了 undefined。

所以，在方法中，我们看到 this 的行为也不太一样，它得到了 undefined 的结果。

按照我们上面的方法，不难验证出：生成器函数、异步生成器函数和异步普通函数跟普通函数行为是一致的，异步箭头函数与箭头函数行为是一致的。

this 关键字的机制

说完了 this 行为，我们再来简单谈谈在 JavaScript 内部，实现 this 这些行为的机制，让你对这部分知识有一个大概的认知。

函数能够引用定义时的变量，如上文分析，函数也能记住定义时的 this，因此，函数内部必定有一个机制来保存这些信息。

在 JavaScript 标准中，为函数规定了用来保存定义时上下文的私有属性 [[Environment]]。

当一个函数执行时，会创建一条新的执行环境记录，记录的外层词法环境（outer lexical environment）会被设置成函数的 [[Environment]]。

这个动作就是切换上下文了，我们假设有这样的代码：

var a = 1;

foo();

在别处定义了 foo：

var b = 2;

function foo(){

    console.log(b); // 2

    console.log(a); // error

}

这里的 foo 能够访问 b（定义时词法环境），却不能访问 a（执行时的词法环境），这就是执行上下文的切换机制了。

JavaScript 用一个栈来管理执行上下文，这个栈中的每一项又包含一个链表。如下图所示：

当函数调用时，会入栈一个新的执行上下文，函数调用结束时，执行上下文被出栈。

而 this 则是一个更为复杂的机制，JavaScript 标准定义了 [[thisMode]] 私有属性。

[[thisMode]] 私有属性有三个取值。

lexical：表示从上下文中找 this，这对应了箭头函数。

global：表示当 this 为 undefined 时，取全局对象，对应了普通函数。

strict：当严格模式时使用，this 严格按照调用时传入的值，可能为 null 或者 undefined。

非常有意思的是，方法的行为跟普通函数有差异，恰恰是因为 class 设计成了默认按 strict 模式执行。

我们可以用 strict 达成与上一节中方法的例子一样的效果:

"use strict"

function showThis(){

    console.log(this);

}

var o = {

    showThis: showThis

}

showThis(); // undefined

o.showThis(); // o

函数创建新的执行上下文中的词法环境记录时，会根据 [[thisMode]] 来标记新纪录的 [[ThisBindingStatus]] 私有属性。

代码执行遇到 this 时，会逐层检查当前词法环境记录中的 [[ThisBindingStatus]]，当找到有 this 的环境记录时获取 this 的值。

这样的规则的实际效果是，嵌套的箭头函数中的代码都指向外层 this，例如：

var o = {}

o.foo = function foo(){

    console.log(this);

    return () => {

        console.log(this);

        return () => console.log(this);

    }

}

o.foo()()(); // o, o, o

这个例子中，我们定义了三层嵌套的函数，最外层为普通函数，两层都是箭头函数。

这里调用三个函数，获得的 this 值是一致的，都是对象 o。

JavaScript 还提供了一系列函数的内置方法来操纵 this 值，下面我们来了解一下。

操作 this 的内置函数

Function.prototype.call 和 Function.prototype.apply 可以指定函数调用时传入的 this 值，示例如下：

function foo(a, b, c){

    console.log(this);

    console.log(a, b, c);

}

foo.call({}, 1, 2, 3);

foo.apply({}, [1, 2, 3]);

这里 call 和 apply 作用是一样的，只是传参方式有区别。

此外，还有 Function.prototype.bind 它可以生成一个绑定过的函数，这个函数的 this 值固定了参数：

function foo(a, b, c){

    console.log(this);

    console.log(a, b, c);

}

foo.bind({}, 1, 2, 3)();

有趣的是，call、bind 和 apply 用于不接受 this 的函数类型如箭头、class 都不会报错。

这时候，它们无法实现改变 this 的能力，但是可以实现传参。

总结

在这一节课程中，我们认识了 ES2018 中规定的各种函数，我一共简单介绍了 8 种函数。

我们围绕 this 这个中心，介绍了函数的执行上下文切换机制。同时我们还讲解了 this 中的一些相关知识。包括了操作 this 的内置函数。

最后，留给你一个问题，你在日常开发中用过哪些函数类型呢？欢迎给我留言，我们一起讨论。

补充阅读：new 与 this

我们在之前的对象部分已经讲过 new 的执行过程，我们再来看一下：

以构造器的 prototype 属性（注意与私有字段 [[prototype]] 的区分）为原型，创建新对象；

将 this 和调用参数传给构造器，执行；

如果构造器返回的是对象，则返回，否则返回第一步创建的对象。

显然，通过 new 调用函数，跟直接调用的 this 取值有明显区别。那么我们今天讲的这些函数跟 new 搭配又会产生什么效果呢？

这里我整理了一张表：

我们可以看到，仅普通函数和类能够跟 new 搭配使用，这倒是给我们省去了不少麻烦。

19 | JavaScript执行（四）：try里面放return，finally还会执行吗？

你好，我是 winter。

在前面几篇文章中，我们已经了解了关于执行上下文、作用域、闭包之间的关系。

今天，我们则要说一说更为细节的部分：语句。

语句是任何编程语言的基础结构，与 JavaScript 对象一样，JavaScript 语句同样具有“看起来很像其它语言，但是其实一点都不一样”的特点。

我们比较常见的语句包括变量声明、表达式、条件、循环等，这些都是大家非常熟悉的东西，对于它们的行为，我在这里就不赘述了。

为了了解 JavaScript 语句有哪些特别之处，首先我们要看一个不太常见的例子，我会通过这个例子，来向你介绍 JavaScript 语句执行机制涉及的一种基础类型：Completion 类型。

Completion 类型

我们来看一个例子。在函数 foo 中，使用了一组 try 语句。我们可以先来做一个小实验，在 try 中有 return 语句，finally 中的内容还会执行吗？我们来看一段代码。

function foo(){

  try{

    return 0;

  } catch(err) {

  } finally {

    console.log("a")

  }

}

console.log(foo());

通过实际试验，我们可以看到，finally 确实执行了，而且 return 语句也生效了，foo() 返回了结果 0。

虽然 return 执行了，但是函数并没有立即返回，又执行了 finally 里面的内容，这样的行为违背了很多人的直觉。

如果在这个例子中，我们在 finally 中加入 return 语句，会发生什么呢？

function foo(){

  try{

    return 0;

  } catch(err) {

  } finally {

    return 1;

  }

}

console.log(foo());

通过实际执行，我们看到，finally 中的 return “覆盖”了 try 中的 return。在一个函数中执行了两次 return，这已经超出了很多人的常识，也是其它语言中不会出现的一种行为。

面对如此怪异的行为，我们当然可以把它作为一个孤立的知识去记忆，但是实际上，这背后有一套机制在运作。

这一机制的基础正是 JavaScript 语句执行的完成状态，我们用一个标准类型来表示：Completion Record（我在类型一节提到过，Completion Record 用于描述异常、跳出等语句执行过程）。

Completion Record 表示一个语句执行完之后的结果，它有三个字段：

[[type]] 表示完成的类型，有 break continue return throw 和 normal 几种类型；

[[value]] 表示语句的返回值，如果语句没有，则是 empty；

[[target]] 表示语句的目标，通常是一个 JavaScript 标签（标签在后文会有介绍）。

JavaScript 正是依靠语句的 Completion Record 类型，方才可以在语句的复杂嵌套结构中，实现各种控制。接下来我们要来了解一下 JavaScript 使用 Completion Record 类型，控制语句执行的过程。

首先我们来看看语句有几种分类。

普通的语句

在 JavaScript 中，我们把不带控制能力的语句称为普通语句。普通语句有下面几种。

声明类语句

var 声明

const 声明

let 声明

函数声明

类声明

表达式语句

空语句

debugger 语句

这些语句在执行时，从前到后顺次执行（我们这里先忽略 var 和函数声明的预处理机制），没有任何分支或者重复执行逻辑。

普通语句执行后，会得到 [[type]] 为 normal 的 Completion Record，JavaScript 引擎遇到这样的 Completion Record，会继续执行下一条语句。

这些语句中，只有表达式语句会产生 [[value]]，当然，从引擎控制的角度，这个 value 并没有什么用处。

如果你经常使用 chrome 自带的调试工具，可以知道，输入一个表达式，在控制台可以得到结果，但是在前面加上 var，就变成了 undefined。

Chrome 控制台显示的正是语句的 Completion Record 的 [[value]]。

语句块

介绍完了普通语句，我们再来介绍一个比较特殊的语句：语句块。

语句块就是拿大括号括起来的一组语句，它是一种语句的复合结构，可以嵌套。

语句块本身并不复杂，我们需要注意的是语句块内部的语句的 Completion Record 的 [[type]] 如果不为 normal，会打断语句块后续的语句执行。

比如我们考虑，一个 [[type]] 为 return 的语句，出现在一个语句块中的情况。

从语句的这个 type 中，我们大概可以猜到它由哪些特定语句产生，我们就来说说最开始的例子中的 return。

return 语句可能产生 return 或者 throw 类型的 Completion Record。我们来看一个例子。

先给出一个内部为普通语句的语句块：

{

  var i = 1; // normal, empty, empty

  i ++; // normal, 1, empty

  console.log(i) //normal, undefined, empty

} // normal, undefined, empty

在每一行的注释中，我给出了语句的 Completion Record。

我们看到，在一个 block 中，如果每一个语句都是 normal 类型，那么它会顺次执行。接下来我们加入 return 试试看。

{

  var i = 1; // normal, empty, empty

  return i; // return, 1, empty

  i ++;

  console.log(i)

} // return, 1, empty

但是假如我们在 block 中插入了一条 return 语句，产生了一个非 normal 记录，那么整个 block 会成为非 normal。这个结构就保证了非 normal 的完成类型可以穿透复杂的语句嵌套结构，产生控制效果。

接下来我们就具体讲讲控制类语句。

控制型语句

控制型语句带有 if、switch 关键字，它们会对不同类型的 Completion Record 产生反应。

控制类语句分成两部分，一类是对其内部造成影响，如 if、switch、while/for、try。另一类是对外部造成影响如 break、continue、return、throw，这两类语句的配合，会产生控制代码执行顺序和执行逻辑的效果，这也是我们编程的主要工作。

一般来说， for/while - break/continue 和 try - throw 这样比较符合逻辑的组合，是大家比较熟悉的，但是，实际上，我们需要控制语句跟 break 、continue 、return 、throw 四种类型与控制语句两两组合产生的效果。

通过这个表，我们不难发现知识的盲点，也就是我们最初的的 case 中的 try 和 return 的组合了。

因为 finally 中的内容必须保证执行，所以 try/catch 执行完毕，即使得到的结果是非 normal 型的完成记录，也必须要执行 finally。

而当 finally 执行也得到了非 normal 记录，则会使 finally 中的记录作为整个 try 结构的结果。

带标签的语句

前文我重点讲了 type 在语句控制中的作用，接下来我们重点来讲一下最后一个字段：target，这涉及了 JavaScript 中的一个语法，带标签的语句。

实际上，任何 JavaScript 语句是可以加标签的，在语句前加冒号即可：

    firstStatement: var i = 1;

大部分时候，这个东西类似于注释，没有任何用处。唯一有作用的时候是：与完成记录类型中的 target 相配合，用于跳出多层循环。

    outer: while(true) {

      inner: while(true) {

          break outer;

      }

    }

    console.log("finished")

break/continue 语句如果后跟了关键字，会产生带 target 的完成记录。一旦完成记录带了 target，那么只有拥有对应 label 的循环语句会消费它。

结语

我们以 Completion Record 类型为线索，为你讲解了 JavaScript 语句执行的原理。

因为 JavaScript 语句存在着嵌套关系，所以执行过程实际上主要在一个树形结构上进行， 树形结构的每一个节点执行后产生 Completion Record，根据语句的结构和 Completion Record，JavaScript 实现了各种分支和跳出逻辑。

你遇到哪些语句中的执行的实际效果，是跟你想象的有所出入呢，你可以给我留言，我们一起讨论。

26 | JavaScript词法：为什么12.toString会报错？

你好，我是 winter。

在前面的文章中，我们已经从运行时的角度了解过 JavaScript 的知识内容，在接下来的几节课，我们来了解一下 JavaScript 的文法部分。

文法是编译原理中对语言的写法的一种规定，一般来说，文法分成词法和语法两种。

词法规定了语言的最小语义单元：token，可以翻译成“标记”或者“词”，在我的专栏文章中，我统一把 token 翻译成词。

从字符到词的整个过程是没有结构的，只要符合词的规则，就构成词，一般来说，词法设计不会包含冲突。词法分析技术上可以使用状态机或者正则表达式来进行，我们的课程主要是学习词法，关于它们实现的细节就不多谈了。

概述

我们先来看一看 JavaScript 的词法定义。JavaScript 源代码中的输入可以这样分类：

WhiteSpace 空白字符

LineTerminator 换行符

Comment 注释

Token 词

IdentifierName 标识符名称，典型案例是我们使用的变量名，注意这里关键字也包含在内了。

Punctuator 符号，我们使用的运算符和大括号等符号。

NumericLiteral 数字直接量，就是我们写的数字。

StringLiteral 字符串直接量，就是我们用单引号或者双引号引起来的直接量。

Template 字符串模板，用反引号` 括起来的直接量。

这个设计符合比较通用的编程语言设计方式，不过，JavaScript 中有一些特别之处，我下面就来讲讲特别在哪里。

首先是除法和正则表达式冲突问题。我们都知道，JavaScript 不但支持除法运算符“ / ”和“ /= ”，还支持用斜杠括起来的正则表达式“ /abc/ ”。

但是，这时候对词法分析来说，其实是没有办法处理的，所以 JavaScript 的解决方案是定义两组词法，然后靠语法分析传一个标志给词法分析器，让它来决定使用哪一套词法。

JavaScript 词法的另一个特别设计是字符串模板，模板语法大概是这样的：

`Hello, ${name}`

理论上，“ ${ } ”内部可以放任何 JavaScript 表达式代码，而这些代码是以“ } ” 结尾的，也就是说，这部分词法不允许出现“ } ”运算符。

是否允许“ } ”的两种情况，与除法和正则表达式的两种情况相乘就是四种词法定义，所以你在 JavaScript 标准中，可以看到四种定义：

InputElementDiv；

InputElementRegExp；

InputElementRegExpOrTemplateTail；

InputElementTemplateTail。

为了解决这两个问题，标准中还不得不把除法、正则表达式直接量和“ } ”从 token 中单独抽出来，用词上，也把原本的 Token 改为 CommonToken。

但是我认为，从理解的角度上出发，我们不应该受到影响，所以在本课，我们依然把它们归类到 token 来理解。

对一般的语言的词法分析过程来说，都会丢弃除了 token 之外的输入，但是对 JavaScript 来说，不太一样，换行符和注释还会影响语法分析过程，这个我们将会在语法部分给你详细讲解（所以要实现 JavaScript 的解释器，词法分析和语法分析非常麻烦，需要来回传递信息）。

接下来我来给你详细介绍一下。

空白符号 Whitespace

说起空白符号，想必给大家留下的印象就是空格，但是实际上，JavaScript 可以支持更多空白符号。

<HT>(或称<TAB>) 是 U+0009，是缩进 TAB 符，也就是字符串中写的 \t 。

<VT>是 U+000B，也就是垂直方向的 TAB 符 \v，这个字符在键盘上很难打出来，所以很少用到。

<FF>是 U+000C，Form Feed，分页符，字符串直接量中写作 \f ，现代已经很少有打印源程序的事情发生了，所以这个字符在 JavaScript 源代码中很少用到。

<SP>是 U+0020，就是最普通的空格了。

<NBSP>是 U+00A0，非断行空格，它是 SP 的一个变体，在文字排版中，可以避免因为空格在此处发生断行，其它方面和普通空格完全一样。多数的 JavaScript 编辑环境都会把它当做普通空格（因为一般源代码编辑环境根本就不会自动折行……）。HTML 中，很多人喜欢用的 &nbsp; 最后生成的就是它了。

<ZWNBSP>(旧称<BOM>) 是 U+FEFF，这是 ES5 新加入的空白符，是 Unicode 中的零宽非断行空格，在以 UTF 格式编码的文件中，常常在文件首插入一个额外的 U+FEFF，解析 UTF 文件的程序可以根据 U+FEFF 的表示方法猜测文件采用哪种 UTF 编码方式。这个字符也叫做“bit order mark”。

此外，JavaScript 支持所有的 Unicode 中的空格分类下的空格，我们可以看下表：

很多公司的编码规范要求 JavaScript 源代码控制在 ASCII 范围内，那么，就只有<TAB> <VT> <FF> <SP> <NBSP>五种空白可用了。

换行符 LineTerminator

接下来我们来看看换行符，JavaScript 中只提供了 4 种字符作为换行符。

<LF>

<CR>

<LS>

<PS>

其中，<LF>是 U+000A，就是最正常换行符，在字符串中的\n。

<CR>是 U+000D，这个字符真正意义上的“回车”，在字符串中是\r，在一部分 Windows 风格文本编辑器中，换行是两个字符\r\n。

<LS>是 U+2028，是 Unicode 中的行分隔符。<PS>是 U+2029，是 Unicode 中的段落分隔符。

大部分 LineTerminator 在被词法分析器扫描出之后，会被语法分析器丢弃，但是换行符会影响 JavaScript 的两个重要语法特性：自动插入分号和“no line terminator”规则。

注释 Comment

JavaScript 的注释分为单行注释和多行注释两种：

/* MultiLineCommentChars */

// SingleLineCommentChars

多行注释中允许自由地出现MultiLineNotAsteriskChar，也就是除了*之外的所有字符。而每一个*之后，不能出现正斜杠符/。

除了四种 LineTerminator 之外，所有字符都可以作为单行注释。

我们需要注意，多行注释中是否包含换行符号，会对 JavaScript 语法产生影响，对于“no line terminator”规则来说，带换行的多行注释与换行符是等效的。

标识符名称 IdentifierName

IdentifierName可以以美元符$下划线_或者 Unicode 字母开始，除了开始字符以外，IdentifierName中还可以使用 Unicode 中的连接标记、数字、以及连接符号。

IdentifierName的任意字符可以使用 JavaScript 的 Unicode 转义写法，使用 Unicode 转义写法时，没有任何字符限制。

IdentifierName可以是Identifier、NullLiteral、BooleanLiteral或者keyword，在ObjectLiteral中，IdentifierName还可以被直接当做属性名称使用。

仅当不是保留字的时候，IdentifierName会被解析为Identifier。

注意<ZWNJ>和<ZWJ>是 ES5 新加入的两个格式控制字符，它们都是 0 宽的。

我在前面提到了，关键字也属于这个部分，在 JavaScript 中，关键字有:

await break case catch class const continue debugger default delete do else export extends finally for function if import ininstance of new return super switch this throw try typeof var void while with yield

除了上述的内容之外，还有 1 个为了未来使用而保留的关键字:

enum

在严格模式下, 有一些额外的为未来使用而保留的关键字:

implements package protected interface private public

除了这些之外，NullLiteral（null）和BooleanLiteral（true false）也是保留字，不能用于Identifier。

符号 Punctuator

因为前面提到的除法和正则问题, / 和 /= 两个运算符被拆分为 DivPunctuator，因为前面提到的字符串模板问题，}也被独立拆分。加在一起，所有符号为：

{ ( ) [ ] . ... ; , < > <= >= == != === !== + - * % ** ++ -- << >> >>> & | ^ ! ~ && || ? : = += -= *= %= **= <<= >>= >>>= &= |= ^= => / /= }

数字直接量 NumericLiteral

我们来看看今天标题提出的问题，JavaScript 规范中规定的数字直接量可以支持四种写法：十进制数、二进制整数、八进制整数和十六进制整数。

十进制的 Number 可以带小数，小数点前后部分都可以省略，但是不能同时省略，我们看几个例子：

.01

12.

12.01

这都是合法的数字直接量。这里就有一个问题，也是我们标题提出的问题，我们看一段代码：

12.toString()

这时候12. 会被当做省略了小数点后面部分的数字而看成一个整体，所以我们要想让点单独成为一个 token，就要加入空格，这样写：

12 .toString()

数字直接量还支持科学计数法，例如：

10.24E+2

10.24e-2

10.24e2

这里 e 后面的部分，只允许使用整数。当以0x 0b 或者0o 开头时，表示特定进制的整数：

0xFA

0o73

0b10000

上面这几种进制都不支持小数，也不支持科学计数法。

字符串直接量 StringLiteral

JavaScript 中的 StringLiteral 支持单引号和双引号两种写法。

    " DoubleStringCharacters "

    ' SingleStringCharacters '

单双引号的区别仅仅在于写法，在双引号字符串直接量中，双引号必须转义，在单引号字符串直接量中，单引号必须转义。字符串中其他必须转义的字符是\和所有换行符。

JavaScript 中支持四种转义形式，还有一种虽然标准没有定义，但是大部分实现都支持的八进制转义。

第一种是单字符转义。 即一个反斜杠\后面跟一个字符这种形式。

有特别意义的字符包括有SingleEscapeCharacter所定义的 9 种，见下表：

除了这 9 种字符、数字、x 和 u 以及所有的换行符之外，其它字符经过\转义后都是自身。

正则表达式直接量 RegularExpressionLiteral

正则表达式由 Body 和 Flags 两部分组成，例如：

/RegularExpressionBody/g

其中 Body 部分至少有一个字符，第一个字符不能是 *（因为 /* 跟多行注释有词法冲突）.

正则表达式有自己的语法规则，在词法阶段，仅会对它做简单解析。

正则表达式并非机械地见到/就停止，在正则表达式[ ]中的/就会被认为是普通字符。我们可以看一个例子：

/[/]/.test("/");

除了\、/ 和[ 三个字符之外，JavaScript 正则表达式中的字符都是普通字符。

用\和一个非换行符可以组成一个转义，[ ]中也支持转义。正则表达式中的 flag 在词法阶段不会限制字符。

虽然只有 ig 几个是有效的，但是任何 IdentifierPart（Identifier 中合法的字符）序列在词法阶段都会被认为是合法的。

字符串模板 Template

从语法结构上，Template 是个整体，其中的 ${ } 是并列关系。

但是实际上，在 JavaScript 词法中，包含 ${ } 的 Template，是被拆开分析的，如：

`a${b}c${d}e`

它在 JavaScript 中被认为是：

`a${

b

}c${

d

}e`

它被拆成了五个部分：

`a${ 这个被称为模板头

}c${ 被称为模板中段

}e` 被称为模板尾

b 和 d 都是普通标识符

实际上，这里的词法分析过程已经跟语法分析深度耦合了。

不过我们学习的时候，大可不必按照标准和引擎工程师这样去理解，可以认为模板就是一个由反引号括起来的、可以在中间插入代码的字符串。

模板支持添加处理函数的写法，这时模板的各段会被拆开，传递给函数当参数：

function f(){

    console.log(arguments);

}

var a = "world"

f`Hello ${a}!`; // [["Hello", "!"], world]

模板字符串不需要关心大多数字符的转义，但是至少 ${ 和 ` 还是需要处理的。

模板中的转义跟字符串几乎完全一样，都是使用 \。

总结

今天我们一起学习 JavaScript 的词法部分，这部分的内容包括了空白符号、换行符、注释、标识符名称、符号、数字直接量、字符串直接量、正则表达式直接量、字符串模板。掌握词法对我们平时调试代码至关重要。

最后，给你留一个问题：用零宽空格和零宽连接符、零宽非连接符，写一段好玩的代码。你可以给我留言，我们一起讨论。

猜你喜欢

27 |（小实验）理解编译原理：一个四则运算的解释器

你好，我是 winter。

在前面的课程中，我在 JavaScript 和 CSS 的部分，多次提到了编译原理相关的知识。这一部分的知识，如果我们从编译原理“龙书”等正规的资料中学习，就会耗费掉不少的时间，所以我在这里设计了一个小实验，帮助你快速理解编译原理相关的知识。

今天的内容比较特殊，我们来做一段详细的代码实验，详细的代码我放在了文章里，如果你正在收听音频，可以点击文章查看详情。

分析

按照编译原理相关的知识，我们来设计一下工作，这里我们分成几个步骤。

定义四则运算：产出四则运算的词法定义和语法定义。

词法分析：把输入的字符串流变成 token。

语法分析：把 token 变成抽象语法树 AST。

解释执行：后序遍历 AST，执行得出结果。

定义四则运算

四则运算就是加减乘除四种运算，例如：

1 + 2 * 3

首先我们来定义词法，四则运算里面只有数字和运算符，所以定义很简单，但是我们还要注意空格和换行符，所以词法定义大概是下面这样的。

Token

Number: 1 2 3 4 5 6 7 8 9 0 的组合

Operator: + 、-、 *、 / 之一

Whitespace: <sp>

LineTerminator：<LF> <CR>

这里我们对空白和换行符没有任何的处理，所以词法分析阶段会直接丢弃。

接下来我们来定义语法，语法定义多数采用 BNF，但是其实大家写起来都是乱写的，比如 JavaScript 标准里面就是一种跟 BNF 类似的自创语法。

不过语法定义的核心思想不会变，都是几种结构的组合产生一个新的结构，所以语法定义也叫语法产生式。

因为加减乘除有优先级，所以我们可以认为加法是由若干个乘法再由加号或者减号连接成的：

<Expression> ::=

    <AdditiveExpression><EOF>

<AdditiveExpression> ::=

    <MultiplicativeExpression>

    |<AdditiveExpression><+><MultiplicativeExpression>

    |<AdditiveExpression><-><MultiplicativeExpression>

这种 BNF 的写法类似递归的原理，你可以理解一下，它表示一个列表。为了方便，我们把普通数字也得当成乘法的一种特例了。

<MultiplicativeExpression> ::=

    <Number>

    |<MultiplicativeExpression><*><Number>

    |<MultiplicativeExpression></><Number>

好了，这就是四则运算的定义了。

词法分析：状态机

词法分析部分，我们把字符流变成 token 流。词法分析有两种方案，一种是状态机，一种是正则表达式，它们是等效的，选择你喜欢的就好，这里我都会你介绍一下状态机。

根据分析，我们可能产生四种输入元素，其中只有两种 token，我们状态机的第一个状态就是根据第一个输入字符来判断进入了哪种状态：

var token = [];

const start = char => {

    if(char === '1'

        || char === '2'

        || char === '3'

        || char === '4'

        || char === '5'

        || char === '6'

        || char === '7'

        || char === '8'

        || char === '9'

        || char === '0'

    ) {

        token.push(char);

        return inNumber;

    }

    if(char === '+'

        || char === '-'

        || char === '*'

        || char === '/'

    ) {

        emmitToken(char, char);

        return start

    }

    if(char === ' ') {

        return start;

    }

    if(char === '\r'

        || char === '\n'

    ) {

        return start;

    }

}

const inNumber = char => {

    if(char === '1'

        || char === '2'

        || char === '3'

        || char === '4'

        || char === '5'

        || char === '6'

        || char === '7'

        || char === '8'

        || char === '9'

        || char === '0'

    ) {

        token.push(char);

        return inNumber;

    } else {

        emmitToken("Number", token.join(""));

        token = [];

        return start(char); // put back char

    }

}

这个状态机非常简单，它只有两个状态，因为我们只有 Number 不是单字符的 token。

这里我的状态机实现是非常经典的方式：用函数表示状态，用 if 表示状态的迁移关系，用 return 值表示下一个状态。

下面我们来运行一下这个状态机试试看：

function emmitToken(type, value) {

    console.log(value);

}

var input = "1024 + 2 * 256"

var state = start;

for(var c of input.split(''))

    state = state(c);

state(Symbol('EOF'))

运行后我们发现输出如下：

1024

+

2

*

256

这是我们想要的答案。

语法分析：LL

做完了词法分析，我们开始进行语法分析，LL 语法分析根据每一个产生式来写一个函数，首先我们来写好函数名：

function AdditiveExpression( ){

}

function MultiplicativeExpression(){

}

为了便于理解，我们就不做流式处理了，实际上一般编译代码都应该支持流式处理。

所以我们假设 token 已经都拿到了：

var tokens = [{

    type:"Number",

    value: "1024"

}, {

    type:"+"

    value: "+"

}, {

    type:"Number",

    value: "2"

}, {

    type:"*"

    value: "*"

}, {

    type:"Number",

    value: "256"

}, {

    type:"EOF"

}];

每个产生式对应着一个函数，例如：根据产生式，我们的 AdditiveExpression 需要处理三种情况：

<AdditiveExpression> ::=

    <MultiplicativeExpression>

    |<AdditiveExpression><+><MultiplicativeExpression>

    |<AdditiveExpression><-><MultiplicativeExpression>

那么 AddititveExpression 中就要写三个 if 分支，来处理三种情况。

AdditiveExpression 的写法是根传入的节点，利用产生式合成新的节点

function AdditiveExpression(source){

    if(source[0].type === "MultiplicativeExpression") {

        let node = {

            type:"AdditiveExpression",

            children:[source[0]]

        }

        source[0] = node;

        return node;

    }

    if(source[0].type === "AdditiveExpression" && source[1].type === "+") {

        let node = {

            type:"AdditiveExpression",

            operator:"+",

            children:[source.shift(), source.shift(), MultiplicativeExpression(source)]

        }

        source.unshift(node);

    }

    if(source[0].type === "AdditiveExpression" && source[1].type === "-") {

        let node = {

            type:"AdditiveExpression",

            operator:"-",

            children:[source.shift(), source.shift(), MultiplicativeExpression(source)]

        }

        source.unshift(node);

    }

}

那么下一步我们就把解析好的 token 传给我们的顶层处理函数 Expression。

Expression(tokens);

接下来，我们看 Expression 该怎么处理它。

我们 Expression 收到第一个 token，是个 Number，这个时候，Expression 就傻了，这是因为产生式只告诉我们，收到了 AdditiveExpression 怎么办。

这个时候，我们就需要对产生式的首项层层展开，根据所有可能性调用相应的处理函数，这个过程在编译原理中称为求“closure”。

function Expression(source){

    if(source[0].type === "AdditiveExpression" && source[1] && source[1].type === "EOF" ) {

        let node = {

            type:"Expression",

            children:[source.shift(), source.shift()]

        }

        source.unshift(node);

        return node;

    }

    AdditiveExpression(source);

    return Expression(source);

}

function AdditiveExpression(source){

    if(source[0].type === "MultiplicativeExpression") {

        let node = {

            type:"AdditiveExpression",

            children:[source[0]]

        }

        source[0] = node;

        return AdditiveExpression(source);

    }

    if(source[0].type === "AdditiveExpression" && source[1] && source[1].type === "+") {

        let node = {

            type:"AdditiveExpression",

            operator:"+",

            children:[]

        }

        node.children.push(source.shift());

        node.children.push(source.shift());

        MultiplicativeExpression(source);

        node.children.push(source.shift());

        source.unshift(node);

        return AdditiveExpression(source);

    }

    if(source[0].type === "AdditiveExpression" && source[1] && source[1].type === "-") {

        let node = {

            type:"AdditiveExpression",

            operator:"-",

            children:[]

        }

        node.children.push(source.shift());

        node.children.push(source.shift());

        MultiplicativeExpression(source);

        node.children.push(source.shift());

        source.unshift(node);

        return AdditiveExpression(source);

    }

    if(source[0].type === "AdditiveExpression")

        return source[0];

    MultiplicativeExpression(source);

    return AdditiveExpression(source);

}

function MultiplicativeExpression(source){

    if(source[0].type === "Number") {

        let node = {

            type:"MultiplicativeExpression",

            children:[source[0]]

        }

        source[0] = node;

        return MultiplicativeExpression(source);

    }

    if(source[0].type === "MultiplicativeExpression" && source[1] && source[1].type === "*") {

        let node = {

            type:"MultiplicativeExpression",

            operator:"*",

            children:[]

        }

        node.children.push(source.shift());

        node.children.push(source.shift());

        node.children.push(source.shift());

        source.unshift(node);

        return MultiplicativeExpression(source);

    }

    if(source[0].type === "MultiplicativeExpression"&& source[1] && source[1].type === "/") {

        let node = {

            type:"MultiplicativeExpression",

            operator:"/",

            children:[]

        }

        node.children.push(source.shift());

        node.children.push(source.shift());

        node.children.push(source.shift());

        source.unshift(node);

        return MultiplicativeExpression(source);

    }

    if(source[0].type === "MultiplicativeExpression")

        return source[0];

    return MultiplicativeExpression(source);

};

var source = [{

    type:"Number",

    value: "3"

}, {

    type:"*",

    value: "*"

}, {

    type:"Number",

    value: "300"

}, {

    type:"+",

    value: "+"

}, {

    type:"Number",

    value: "2"

}, {

    type:"*",

    value: "*"

}, {

    type:"Number",

    value: "256"

}, {

    type:"EOF"

}];

var ast = Expression(source);

console.log(ast);

解释执行

得到了 AST 之后，最困难的一步我们已经解决了。这里我们就不对这颗树做任何的优化和精简了，那么接下来，直接进入执行阶段。我们只需要对这个树做遍历操作执行即可。

我们根据不同的节点类型和其它信息，写 if 分别处理即可：

function evaluate(node) {

    if(node.type === "Expression") {

        return evaluate(node.children[0])

    }

    if(node.type === "AdditiveExpression") {

        if(node.operator === '-') {

            return evaluate(node.children[0]) - evaluate(node.children[2]);

        }

        if(node.operator === '+') {

            return evaluate(node.children[0]) + evaluate(node.children[2]);

        }

        return evaluate(node.children[0])

    }

    if(node.type === "MultiplicativeExpression") {

        if(node.operator === '*') {

            return evaluate(node.children[0]) * evaluate(node.children[2]);

        }

        if(node.operator === '/') {

            return evaluate(node.children[0]) / evaluate(node.children[2]);

        }

        return evaluate(node.children[0])

    }

    if(node.type === "Number") {

        return Number(node.value);

    }

}

总结

在这个小实验中，我们通过一个小实验学习了编译原理的基本知识，小实验的目的是帮助你理解 JavaScript 课程中涉及到的编译原理基本概念，它离真正的编译原理学习还有很大的差距。

通过实验，我们了解了产生式、词法分析、语法分析和解释执行的过程。

最后留给你一些挑战，你可以根据自己的水平选择：

补全 emmitToken，使得我们的代码能完整工作起来。

为四则运算加入小数。

引入负数。

添加括号功能。

欢迎写好的同学留言给我。

28 | JavaScript语法（预备篇）：到底要不要写分号呢？

你好，我是 winter。

在我们介绍 JavaScript 语法的全局结构之前，我们先要探讨一个语言风格问题：究竟要不要写分号。

这是一个非常经典的口水问题，“加分号”党和“不写分号”党之间的战争，可谓是经久不息。

实际上，行尾使用分号的风格来自于 Java，也来自于 C 语言和 C++，这一设计最初是为了降低编译器的工作负担。

但是，从今天的角度来看，行尾使用分号其实是一种语法噪音，恰好 JavaScript 语言又提供了相对可用的分号自动补全规则，所以，很多 JavaScript 的程序员都是倾向于不写分号。

这里要特意说一点，在今天的文章中，我并不希望去售卖自己的观点（其实我是属于“加分号”党），而是希望比较中立地给你讲清楚相关的知识，让你具备足够的判断力。

我们首先来了解一下自动插入分号的规则。

自动插入分号规则

自动插入分号规则其实独立于所有的语法产生式定义，它的规则说起来非常简单，只有三条。

要有换行符，且下一个符号是不符合语法的，那么就尝试插入分号。

有换行符，且语法中规定此处不能有换行符，那么就自动插入分号。

源代码结束处，不能形成完整的脚本或者模块结构，那么就自动插入分号。

这样描述是比较难以理解的，我们一起看一些实际的例子进行分析：

let a = 1

void function(a){

    console.log(a);

}(a);

在这个例子中，第一行的结尾处有换行符，接下来 void 关键字接在 1 之后是不合法的，这命中了我们的第一条规则，因此会在 void 前插入换行符。

var a = 1, b = 1, c = 1;

a

++

b

++

c

这也是个著名的例子，我们看第二行的 a 之后，有换行符，后面遇到了 ++ 运算符，a 后面跟 ++ 是合法的语法，但是我们看看 JavaScript 标准定义中，有 [no LineTerminator here] 这个字样，这是一个语法定义中的规则，你可以感受一下这个规则的内容（下一小节，我会给你详细介绍 no LineTerminator here ）：

UpdateExpression[Yield, Await]:

    LeftHandSideExpression[?Yield, ?Await]

    LeftHandSideExpression[?Yield, ?Await][no LineTerminator here]++

    LeftHandSideExpression[?Yield, ?Await][no LineTerminator here]--

    ++UnaryExpression[?Yield, ?Await]

    --UnaryExpression[?Yield, ?Await]

于是，这里 a 的后面就要插入一个分号了。所以这段代码最终的结果，b 和 c 都变成了 2，而 a 还是 1。

(function(a){

    console.log(a);

})()

(function(a){

    console.log(a);

})()

这个例子是比较有实际价值的例子，这里两个 function 调用的写法被称作 IIFE（立即执行的函数表达式），是个常见技巧。

这段代码意图上显然是形成两个 IIFE。

我们来看第三行结束的位置，JavaScript 引擎会认为函数返回的可能是个函数，那么，在后面再跟括号形成函数调用就是合理的，因此这里不会自动插入分号。

这是一些鼓励不写分号的编码风格会要求大家写 IIFE 时必须在行首加分号的原因。

function f(){

    return/*

        This is a return value.

    */1;

}

f();

在这个例子中，return 和 1 被用注释分隔开了。

根据 JavaScript 自动插入分号规则，带换行符的注释也被认为是有换行符，而恰好的是，return 也有 [no LineTerminator here] 规则的要求。所以这里会自动插入分号，f 执行的返回值是 undefined。

no LineTerminator here 规则

好了，到这里我们已经讲清楚了分号自动插入的规则，但是我们要想彻底掌握分号的奥秘，就必须要对 JavaScript 的语法定义做一些数据挖掘工作。

no LineTerminator here 规则表示它所在的结构中的这一位置不能插入换行符。

自动插入分号规则的第二条：有换行符，且语法中规定此处不能有换行符，那么就自动插入分号。跟 no LineTerminator here 规则强相关，那么我们就找出 JavaScript 语法定义中的这些规则。

为了方便你理解，我把产生式换成了实际的代码。

下面一段代码展示了，带标签的 continue 语句，不能在 continue 后插入换行。

outer:for(var j = 0; j < 10; j++)

    for(var i = 0; i < j; i++)

        continue /*no LineTerminator here*/ outter

break 跟 continue 是一样的，break 后也不能插入换行：

outer:for(var j = 0; j < 10; j++)

    for(var i = 0; i < j; i++)

        break /*no LineTerminator here*/ outter

我们前面已经提到过 return 和后自增、后自减运算符。

function f(){

    return /*no LineTerminator here*/1;

}

i/*no LineTerminator here*/++

i/*no LineTerminator here*/--

以及，throw 和 Exception 之间也不能插入换行符：

throw/*no LineTerminator here*/new Exception("error")

凡是 async 关键字，后面都不能插入换行符：

async/*no LineTerminator here*/function f(){

}

const f = async/*no LineTerminator here*/x => x*x

箭头函数的箭头前，也不能插入换行

const f = x/*no LineTerminator here*/=> x*x

yield 之后，不能插入换行

function *g(){

    var i = 0;

    while(true)

        yield/*no LineTerminator here*/i++;

}

到这里，我已经整理了所有标准中的 no LineTerminator here 规则，实际上，no LineTerminator here 规则的存在，多数情况是为了保证自动插入分号行为是符合预期的，但是令人遗憾的是，JavaScript 在设计的最初，遗漏了一些重要的情况，所以有一些不符合预期的情况出现，需要我们格外注意。

不写分号需要注意的情况

下面我们来看几种不写分号容易造成错误的情况，你可以稍微注意一下，避免发生同样的问题。

以括号开头的语句

我们在前面的案例中，已经展示了一种情况，那就是以括号开头的语句：

(function(a){

    console.log(a);

})()/* 这里没有被自动插入分号 */

(function(a){

    console.log(a);

})()

这段代码看似两个独立执行的函数表达式，但是其实第三组括号被理解为传参，导致抛出错误。

以数组开头的语句

除了括号，以数组开头的语句也十分危险：

var a = [[]]/* 这里没有被自动插入分号 */

[3, 2, 1, 0].forEach(e => console.log(e))

这段代码本意是一个变量 a 赋值，然后对一个数组执行 forEach，但是因为没有自动插入分号，被理解为下标运算符和逗号表达式，我这个例子展示的情况，甚至不会抛出错误，这对于代码排查问题是个噩梦。

以正则表达式开头的语句

正则表达式开头的语句也值得你去多注意一下。我们来看这个例子。

var x = 1, g = {test:()=>0}, b = 1/* 这里没有被自动插入分号 */

/(a)/g.test("abc")

console.log(RegExp.$1)

这段代码本意是声明三个变量，然后测试一个字符串中是否含有字母 a，但是因为没有自动插入分号，正则的第一个斜杠被理解成了除号，后面的意思就都变了。

注意，我构造的这个例子跟上面的例子一样，同样不会抛错，凡是这一类情况，都非常致命。

以 Template 开头的语句

以 Template 开头的语句比较少见，但是跟正则配合时，仍然不是不可能出现：

var f = function(){

  return "";

}

var g = f/* 这里没有被自动插入分号 */

`Template`.match(/(a)/);

console.log(RegExp.$1)

这段代码本意是声明函数 f，然后赋值给 g，再测试 Template 中是否含有字母 a。但是因为没有自动插入分号，函数 f 被认为跟 Template 一体的，进而被莫名其妙地执行了一次。

总结

这一节课，我们讨论了要不要加分号的问题。

首先我们介绍了自动插入分号机制，又对 JavaScript 语法中的 no line terminator 规则做了个整理，最后，我挑选了几种情况，为你介绍了不写分号需要注意的一些常见的错误。

最后留给你一个问题，请找一些开源项目，看看它们的编码规范是否要求加分号，欢迎留言讨论。

29 | JavaScript语法（一）：在script标签写export为什么会抛错？

你好，我是 winter，今天我们进入到语法部分的学习。在讲解具体的语法结构之前，这一堂课我首先要给你介绍一下 JavaScript 语法的一些基本规则。

脚本和模块

首先，JavaScript 有两种源文件，一种叫做脚本，一种叫做模块。这个区分是在 ES6 引入了模块机制开始的，在 ES5 和之前的版本中，就只有一种源文件类型（就只有脚本）。

脚本是可以由浏览器或者 node 环境引入执行的，而模块只能由 JavaScript 代码用 import 引入执行。

从概念上，我们可以认为脚本具有主动性的 JavaScript 代码段，是控制宿主完成一定任务的代码；而模块是被动性的 JavaScript 代码段，是等待被调用的库。

我们对标准中的语法产生式做一些对比，不难发现，实际上模块和脚本之间的区别仅仅在于是否包含 import 和 export。

脚本是一种兼容之前的版本的定义，在这个模式下，没有 import 就不需要处理加载“.js”文件问题。

现代浏览器可以支持用 script 标签引入模块或者脚本，如果要引入模块，必须给 script 标签添加 type=“module”。如果引入脚本，则不需要 type。

<script type="module" src="xxxxx.js"></script>

这样，就回答了我们标题中的问题，script 标签如果不加type=“module”，默认认为我们加载的文件是脚本而非模块，如果我们在脚本中写了 export，当然会抛错。

脚本中可以包含语句。模块中可以包含三种内容：import 声明，export 声明和语句。普通语句我们会在下一课专门给你讲解，下面我们就来讲讲 import 声明和 export 声明。

import 声明

我们首先来介绍一下 import 声明，import 声明有两种用法，一个是直接 import 一个模块，另一个是带 from 的 import，它能引入模块里的一些信息。

import "mod"; // 引入一个模块

import v from "mod";  // 把模块默认的导出值放入变量 v

直接 import 一个模块，只是保证了这个模块代码被执行，引用它的模块是无法获得它的任何信息的。

带 from 的 import 意思是引入模块中的一部分信息，可以把它们变成本地的变量。

带 from 的 import 细分又有三种用法，我们可以分别看下例子：

import x from "./a.js" 引入模块中导出的默认值。

import {a as x, modify} from "./a.js"; 引入模块中的变量。

import * as x from "./a.js" 把模块中所有的变量以类似对象属性的方式引入。

第一种方式还可以跟后两种组合使用。

import d, {a as x, modify} from "./a.js"

import d, * as x from "./a.js"

语法要求不带 as 的默认值永远在最前。注意，这里的变量实际上仍然可以受到原来模块的控制。

我们看一个例子，假设有两个模块 a 和 b。我们在模块 a 中声明了变量和一个修改变量的函数，并且把它们导出。我们用 b 模块导入了变量和修改变量的函数。

模块 a：

export var a = 1;

export function modify(){

    a = 2;

}

模块 b：

import {a, modify} from "./a.js";

console.log(a);

modify();

console.log(a);

当我们调用修改变量的函数后，b 模块变量也跟着发生了改变。这说明导入与一般的赋值不同，导入后的变量只是改变了名字，它仍然与原来的变量是同一个。

export 声明

我们再来说说 export 声明。与 import 相对，export 声明承担的是导出的任务。

模块中导出变量的方式有两种，一种是独立使用 export 声明，另一种是直接在声明型语句前添加 export 关键字。

独立使用 export 声明就是一个 export 关键字加上变量名列表，例如：

export {a, b, c};

我们也可以直接在声明型语句前添加 export 关键字，这里的 export 可以加在任何声明性质的语句之前，整理如下：

var

function (含 async 和 generator)

class

let

const

export 还有一种特殊的用法，就是跟 default 联合使用。export default 表示导出一个默认变量值，它可以用于 function 和 class。这里导出的变量是没有名称的，可以使用import x from "./a.js"这样的语法，在模块中引入。

export default 还支持一种语法，后面跟一个表达式，例如：

var a = {};

export default a;

但是，这里的行为跟导出变量是不一致的，这里导出的是值，导出的就是普通变量 a 的值，以后 a 的变化与导出的值就无关了，修改变量 a，不会使得其他模块中引入的 default 值发生改变。

在 import 语句前无法加入 export，但是我们可以直接使用 export from 语法。

export a from "a.js"

JavaScript 引擎除了执行脚本和模块之外，还可以执行函数。而函数体跟脚本和模块有一定的相似之处，所以接下来，给你讲讲函数体的相关知识。

函数体

执行函数的行为通常是在 JavaScript 代码执行时，注册宿主环境的某些事件触发的，而执行的过程，就是执行函数体（函数的花括号中间的部分）。

我们先看一个例子，感性地理解一下：

setTimeout(function(){

    console.log("go go go");

}, 10000)

这段代码通过 setTimeout 函数注册了一个函数给宿主，当一定时间之后，宿主就会执行这个函数。

你还记得吗，我们前面已经在运行时这部分讲过，宿主会为这样的函数创建宏任务。

当我们学习了语法之后，我们可以认为，宏任务中可能会执行的代码包括“脚本 (script)”“模块（module）”和“函数体（function body）”。正因为这样的相似性，我们把函数体也放到本课来讲解。

函数体其实也是一个语句的列表。跟脚本和模块比起来，函数体中的语句列表中多了 return 语句可以用。

函数体实际上有四种，下面，我来分别介绍一下。

普通函数体，例如：

function foo(){

    //Function body

}

异步函数体，例如：

async function foo(){

    //Function body

}

生成器函数体，例如：

function *foo(){

    //Function body

}

异步生成器函数体，例如：

async function *foo(){

    //Function body

}

上面四种函数体的区别在于：能否使用 await 或者 yield 语句。

关于函数体、模块和脚本能使用的语句，我整理了一个表格，你可以参考一下：

讲完了三种语法结构，我再来介绍两个 JavaScript 语法的全局机制：预处理和指令序言。

这两个机制对于我们解释一些 JavaScript 的语法现象非常重要。不理解预处理机制我们就无法理解 var 等声明类语句的行为，而不理解指令序言，我们就无法解释严格模式。

预处理

JavaScript 执行前，会对脚本、模块和函数体中的语句进行预处理。预处理过程将会提前处理 var、函数声明、class、const 和 let 这些语句，以确定其中变量的意义。

因为一些历史包袱，这一部分内容非常复杂，首先我们看一下 var 声明。

var 声明

var 声明永远作用于脚本、模块和函数体这个级别，在预处理阶段，不关心赋值的部分，只管在当前作用域声明这个变量。

我们还是从实例来进行学习。

var a = 1;

function foo() {

    console.log(a);

    var a = 2;

}

foo();

这段代码声明了一个脚本级别的 a，又声明了 foo 函数体级别的 a，我们注意到，函数体级的var出现在 console.log 语句之后。

但是预处理过程在执行之前，所以有函数体级的变量 a，就不会去访问外层作用域中的变量 a 了，而函数体级的变量 a 此时还没有赋值，所以是 undefined。我们再看一个情况：

var a = 1;

function foo() {

    console.log(a);

    if(false) {

        var a = 2;

    }

}

foo();

这段代码比上一段代码在var a = 2之外多了一段 if，我们知道 if(false) 中的代码永远不会被执行，但是预处理阶段并不管这个，var 的作用能够穿透一切语句结构，它只认脚本、模块和函数体三种语法结构。所以这里结果跟前一段代码完全一样，我们会得到 undefined。

我们看下一个例子，我们在运行时部分讲过类似的例子。

var a = 1;

function foo() {

    var o= {a:3}

    with(o) {

        var a = 2;

    }

    console.log(o.a);

    console.log(a);

}

foo();

在这个例子中，我们引入了 with 语句，我们用 with(o) 创建了一个作用域，并把 o 对象加入词法环境，在其中使用了var a = 2;语句。

在预处理阶段，只认var中声明的变量，所以同样为 foo 的作用域创建了 a 这个变量，但是没有赋值。

在执行阶段，当执行到var a = 2时，作用域变成了 with 语句内，这时候的 a 被认为访问到了对象 o 的属性 a，所以最终执行的结果，我们得到了 2 和 undefined。

这个行为是 JavaScript 公认的设计失误之一，一个语句中的 a 在预处理阶段和执行阶段被当做两个不同的变量，严重违背了直觉，但是今天，在 JavaScript 设计原则“don’t break the web”之下，已经无法修正了，所以你需要特别注意。

因为早年 JavaScript 没有 let 和 const，只能用 var，又因为 var 除了脚本和函数体都会穿透，人民群众发明了“立即执行的函数表达式（IIFE）”这一用法，用来产生作用域，例如：

for(var i = 0; i < 20; i ++) {

    void function(i){

        var div = document.createElement("div");

        div.innerHTML = i;

        div.onclick = function(){

            console.log(i);

        }

        document.body.appendChild(div);

    }(i);

}

这段代码非常经典，常常在实际开发中见到，也经常被用作面试题，为文档添加了 20 个 div 元素，并且绑定了点击事件，打印它们的序号。

我们通过 IIFE 在循环内构造了作用域，每次循环都产生一个新的环境记录，这样，每个 div 都能访问到环境中的 i。

如果我们不用 IIFE：

for(var i = 0; i < 20; i ++) {

    var div = document.createElement("div");

    div.innerHTML = i;

    div.onclick = function(){

        console.log(i);

    }

    document.body.appendChild(div);

}

这段代码的结果将会是点每个 div 都打印 20，因为全局只有一个 i，执行完循环后，i 变成了 20。

function 声明

function 声明的行为原本跟 var 非常相似，但是在最新的 JavaScript 标准中，对它进行了一定的修改，这让情况变得更加复杂了。

在全局（脚本、模块和函数体），function 声明表现跟 var 相似，不同之处在于，function 声明不但在作用域中加入变量，还会给它赋值。

我们看一下 function 声明的例子

console.log(foo);

function foo(){

}

这里声明了函数 foo，在声明之前，我们用 console.log 打印函数 foo，我们可以发现，已经是函数 foo 的值了。

function 声明出现在 if 等语句中的情况有点复杂，它仍然作用于脚本、模块和函数体级别，在预处理阶段，仍然会产生变量，它不再被提前赋值：

console.log(foo);

if(true) {

    function foo(){

    }

}

这段代码得到 undefined。如果没有函数声明，则会抛出错误。

这说明 function 在预处理阶段仍然发生了作用，在作用域中产生了变量，没有产生赋值，赋值行为发生在了执行阶段。

出现在 if 等语句中的 function，在 if 创建的作用域中仍然会被提前，产生赋值效果，我们会在下一节课继续讨论。

class 声明

class 声明在全局的行为跟 function 和 var 都不一样。

在 class 声明之前使用 class 名，会抛错：

console.log(c);

class c{

}

这段代码我们试图在 class 前打印变量 c，我们得到了个错误，这个行为很像是 class 没有预处理，但是实际上并非如此。

我们看个复杂一点的例子：

var c = 1;

function foo(){

    console.log(c);

    class c {}

}

foo();

这个例子中，我们把 class 放进了一个函数体中，在外层作用域中有变量 c。然后试图在 class 之前打印 c。

执行后，我们看到，仍然抛出了错误，如果去掉 class 声明，则会正常打印出 1，也就是说，出现在后面的 class 声明影响了前面语句的结果。

这说明，class 声明也是会被预处理的，它会在作用域中创建变量，并且要求访问它时抛出错误。

class 的声明作用不会穿透 if 等语句结构，所以只有写在全局环境才会有声明作用，这部分我们将会在下一节课讲解。

这样的 class 设计比 function 和 var 更符合直觉，而且在遇到一些比较奇怪的用法时，倾向于抛出错误。

按照现代语言设计的评价标准，及早抛错是好事，它能够帮助我们尽量在开发阶段就发现代码的可能问题。

指令序言机制

脚本和模块都支持一种特别的语法，叫做指令序言（Directive Prologs）。

这里的指令序言最早是为了 use strict 设计的，它规定了一种给 JavaScript 代码添加元信息的方式。

"use strict";

function f(){

    console.log(this);

};

f.call(null);

这段代码展示了严格模式的用法，我这里定义了函数 f，f 中打印 this 值，然后用 call 的方法调用 f，传入 null 作为 this 值，我们可以看到最终结果是 null 原封不动地被当做 this 值打印了出来，这是严格模式的特征。

如果我们去掉严格模式的指令需要，打印的结果将会变成 global。

"use strict"是 JavaScript 标准中规定的唯一一种指令序言，但是设计指令序言的目的是，留给 JS 的引擎和实现者一些统一的表达方式，在静态扫描时指定 JS 代码的一些特性。

例如，假设我们要设计一种声明本文件不需要进行 lint 检查的指令，我们可以这样设计：

"no lint";

"use strict";

function doSth(){

    //......

}

//......

JavaScript 的指令序言是只有一个字符串直接量的表达式语句，它只能出现在脚本、模块和函数体的最前面。

我们看两个例子：

function doSth(){

    //......

}

"use strict";

var a = 1;

//......

这个例子中，"use strict"没有出现在最前，所以不是指令序言。

'use strict';

function doSth(){

    //......

}

var a = 1;

//......

这个例子中，'use strict'是单引号，这不妨碍它仍然是指令序言。

总结

今天，我们一起进入了 JavaScript 的语法部分，在开始学习之前，我先介绍了一部分语法的基本规则。

我们首先介绍了 JavaScript 语法的全局结构，JavaScript 有两种源文件，一种叫做脚本，一种叫做模块。介绍完脚本和模块的基础概念，我们再来把它们往下分，脚本中可以包含语句。模块中可以包含三种内容：import 声明，export 声明和语句。

最后，我介绍了两个 JavaScript 语法的全局机制：预处理和指令序言。

最后，给你留一个小任务，我们试着用 babel，分析一段 JavaScript 的模块代码，并且找出它中间的所有 export 的变量。

30 | JavaScript语法（二）：你知道哪些JavaScript语句？

你好，我是 winter。

我们在上一节课中已经讲过了 JavaScript 语法的顶层设计，接下来我们进入到更具体的内容。

JavaScript 遵循了一般编程语言的‘语句 - 表达式’结构，多数编程语言都是这样设计的。我们在上节课讲的脚本，或者模块都是由语句列表构成的，这一节课，我们就来一起了解一下语句。

在 JavaScript 标准中，把语句分成了两种：声明和语句，不过，这里的区分逻辑比较奇怪，所以，这里我还是按照自己的思路给你整理一下。

普通语句：

声明型语句：

我们根据上面的分类，来遍历学习一下这些语句。

语句块

我们可以这样去简单理解，语句块就是一对大括号。

{

    var x, y;

    x = 10;

    y = 20;

}

语句块的意义和好处在于：让我们可以把多行语句视为同一行语句，这样，if、for 等语句定义起来就比较简单了。不过，我们需要注意的是，语句块会产生作用域，我们看一个例子：

{

    let x = 1;

}

console.log(x); // 报错

这里我们的 let 声明，仅仅对语句块作用域生效，于是我们在语句块外试图访问语句块内的变量 x 就会报错。

空语句

空语句就是一个独立的分号，实际上没什么大用。我们来看一下：

;

空语句的存在仅仅是从语言设计完备性的角度考虑，允许插入多个分号而不抛出错误。

if 语句

if 语句是条件语句。我想，对多数人来说，if 语句都是熟悉的老朋友了，也没有什么特别需要注意的用法，但是为了我们课程的完备性，这里还是要讲一下。

if 语句示例如下：

if(a < b)

    console.log(a);

if 语句的作用是，在满足条件时执行它的内容语句，这个语句可以是一个语句块，这样就可以实现有条件地执行多个语句了。

if 语句还有 else 结构，用于不满足条件时执行，一种常见的用法是，利用语句的嵌套能力，把 if 和 else 连写成多分支条件判断：

if(a < 10) {

    //...

} else if(a < 20) {

    //...

} else if(a < 30) {

    //...

} else {

    //...

}

这段代码表示四个互斥的分支，分别在满足 a<10、a<20、a<30 和其它情况时执行。

switch 语句

switch 语句继承自 Java，Java 中的 switch 语句继承自 C 和 C++，原本 switch 语句是跳转的变形，所以我们如果要用它来实现分支，必须要加上 break。

其实 switch 原本的设计是类似 goto 的思维。我们看一个例子：

switch(num) {

case 1:

    print(1);

case 2:

    print 2;

case 3:

    print 3;

}

这段代码当 num 为 1 时输出 1 2 3，当 num 为 2 时输出 2 3，当 num 为 3 时输出 3。如果我们要把它变成分支型，则需要在每个 case 后加上 break。

switch(num) {

case 1:

    print 1;

    break;

case 2:

    print 2;

    break;

case 3:

    print 3;

    break;

}

在 C 时代，switch 生成的汇编代码性能是略优于 if else 的，但是对 JavaScript 来说，则无本质区别。我个人的看法是，现在 switch 已经完全没有必要使用了，应该用 if else 结构代替。

循环语句

循环语句应该也是你所熟悉的语句了，这里我们把重点放在一些新用法上。

while 循环和 do while 循环

这两个都是历史悠久的 JavaScript 语法了，示例大概如下：

let a = 100

while(a--) {

    console.log("*");

}

let a = 101;

do {

    console.log(a);

} while(a < 100)

注意，这里 do while 循环无论如何至少会执行一次。

普通 for 循环

首先我们来看看普通的 for 循环。

for(i = 0; i < 100; i++)

    console.log(i);

for(var i = 0; i < 100; i++)

    console.log(i);

for(let i = 0; i < 100; i++)

    console.log(i);

var j = 0;

for(const i = 0; j < 100; j++)

    console.log(i);

这里为了配合新语法，加入了允许 let 和 const，实际上，const 在这里是非常奇葩的东西，因为这里声明和初始化的变量，按惯例是用于控制循环的，但是它如果是 const 就没法改了。

我想，这一点可能是从保持 let 和 const 一致性的角度考虑的吧。

for in 循环

for in 循环枚举对象的属性，这里体现了属性的 enumerable 特征。

let o = { a: 10, b: 20}

Object.defineProperty(o, "c", {enumerable:false, value:30})

for(let p in o)

    console.log(p);

这段代码中，我们定义了一个对象 o，给它添加了不可枚举的属性 c，之后我们用 for in 循环枚举它的属性，我们会发现，输出时得到的只有 a 和 b。

如果我们定义 c 这个属性时，enumerable 为 true，则 for in 循环中也能枚举到它。

for of 循环和 for await of 循环

for of 循环是非常棒的语法特性。

我们先看下基本用法，它可以用于数组：

for(let e of [1, 2, 3, 4, 5])

    console.log(e);

但是实际上，它背后的机制是 iterator 机制。

我们可以给任何一个对象添加 iterator，使它可以用于 for of 语句，看下示例：

let o = {

    [Symbol.iterator]:() => ({

        _value: 0,

        next(){

            if(this._value == 10)

                return {

                    done: true

                }

            else return {

                value: this._value++,

                done: false

            };

        }

    })

}

for(let e of o)

    console.log(e);

这段代码展示了如何为一个对象添加 iterator。但是，在实际操作中，我们一般不需要这样定义 iterator，我们可以使用 generator function。

function* foo(){

    yield 0;

    yield 1;

    yield 2;

    yield 3;

}

for(let e of foo())

    console.log(e);

这段代码展示了 generator function 和 foo 的配合。

此外，JavaScript 还为异步生成器函数配备了异步的 for of，我们来看一个例子：

function sleep(duration) {

    return new Promise(function(resolve, reject) {

        setTimeout(resolve,duration);

    })

}

async function* foo(){

    i = 0;

    while(true) {

        await sleep(1000);

        yield i++;

    }

}

for await(let e of foo())

    console.log(e);

这段代码定义了一个异步生成器函数，异步生成器函数每隔一秒生成一个数字，这是一个无限的生成器。

接下来，我们使用 for await of 来访问这个异步生成器函数的结果，我们可以看到，这形成了一个每隔一秒打印一个数字的无限循环。

但是因为我们这个循环是异步的，并且有时间延迟，所以，这个无限循环的代码可以用于显示时钟等有意义的操作。

return

return 语句用于函数中，它终止函数的执行，并且指定函数的返回值，这是大家非常熟悉语句了，也没有什么特殊之处。

function squre(x){

    return x * x;

}

这段代码展示了 return 的基本用法。它后面可以跟一个表达式，计算结果就是函数返回值。

break 语句和 continue 语句

break 语句用于跳出循环语句或者 switch 语句，continue 语句用于结束本次循环并继续循环。

这两个语句都属于控制型语句，用法也比较相似，所以我们就一起讲了。需要注意的是，它们都有带标签的用法。

outer:for(let i = 0; i < 100; i++)

    inner:for(let j = 0; j < 100; j++)

        if( i == 50 && j == 50)

            break outer;

outer:for(let i = 0; i < 100; i++)

    inner:for(let j = 0; j < 100; j++)

        if( i >= 50 && j == 50)

            continue outer;

带标签的 break 和 continue 可以控制自己被外层的哪个语句结构消费，这可以跳出复杂的语句结构。

with 语句

with 语句是个非常巧妙的设计，但它把 JS 的变量引用关系变得不可分析，所以一般都认为这种语句都属于糟粕。

但是历史无法改写，现在已经无法去除 with 了。我们来了解一下它的基本用法即可。

let o = {a:1, b:2}

with(o){

    console.log(a, b);

}

with 语句把对象的属性在它内部的作用域内变成变量。

try 语句和 throw 语句

try 语句和 throw 语句用于处理异常。它们是配合使用的，所以我们就放在一起讲了。在大型应用中，异常机制非常重要。

try {

    throw new Error("error");

} catch(e) {

    console.log(e);

} finally {

    console.log("finally");

}

一般来说，throw 用于抛出异常，但是单纯从语言的角度，我们可以抛出任何值，也不一定是异常逻辑，但是为了保证语义清晰，不建议用 throw 表达任何非异常逻辑。

try 语句用于捕获异常，用 throw 抛出的异常，可以在 try 语句的结构中被处理掉：try 部分用于标识捕获异常的代码段，catch 部分则用于捕获异常后做一些处理，而 finally 则是用于执行后做一些必须执行的清理工作。

catch 结构会创建一个局部的作用域，并且把一个变量写入其中，需要注意，在这个作用域，不能再声明变量 e 了，否则会出错。

在 catch 中重新抛出错误的情况非常常见，在设计比较底层的函数时，常常会这样做，保证抛出的错误能被理解。

finally 语句一般用于释放资源，它一定会被执行，我们在前面的课程中已经讨论过一些 finally 的特征，即使在 try 中出现了 return，finally 中的语句也一定要被执行。（你可以参考第 19 讲）

debugger 语句

debugger 语句的作用是：通知调试器在此断点。在没有调试器挂载时，它不产生任何效果。

介绍完普通语句，我们再来看看声明型语句。声明型语句跟普通语句最大区别就是声明型语句响应预处理过程，普通语句只有执行过程。

var

var 声明语句是古典的 JavaScript 中声明变量的方式。而现在，在绝大多数情况下，let 和 const 都是更好的选择。

我们在上一节课已经讲解了 var 声明对全局作用域的影响，它是一种预处理机制。

如果我们仍然想要使用 var，我的个人建议是，把它当做一种“保障变量是局部”的逻辑，遵循以下三条规则：

声明同时必定初始化；

尽可能在离使用的位置近处声明；

不要在意重复声明。

例如：

var x = 1, y = 2;

doSth(x, y);

for(var x = 0; x < 10; x++)

    doSth2(x);

这个例子中，两次声明了变量 x，完成了两段逻辑，这两个 x 意义上可能不一定相关，这样，不论我们把代码复制粘贴在哪里，都不会出错。

当然，更好的办法是使用 let 改造，我们看看如何改造：

{

    let x = 1, y = 2;

    doSth(x, y);

}

for(let x = 0; x < 10; x++)

    doSth2(x);

这里我用代码块限制了第一个 x 的作用域，这样就更难发生变量命名冲突引起的错误了。

let 和 const

let 和 const 是都是变量的声明，它们的特性非常相似，所以我们放在一起讲了。let 和 const 是新设计的语法，所以没有什么硬伤，非常地符合直觉。let 和 const 的作用范围是 if、for 等结构型语句。

我们看下基本用法：

const a = 2;

if(true){

    const a = 1;

    console.log(a);

}

console.log(a);

这里的代码先在全局声明了变量 a，接下来又在 if 内声明了 a，if 内构成了一个独立的作用域。

const 和 let 语句在重复声明时会抛错，这能够有效地避免变量名无意中冲突：

let a = 2

const a = 1;

这段代码中，先用 let 声明了 a，接下来又试图使用 const 声明变量 a，这时，就会产生错误。

let 和 const 声明虽然看上去是执行到了才会生效，但是实际上，它们还是会被预处理。如果当前作用域内有声明，就无法访问到外部的变量。我们来看这段代码：

const a = 2;

if(true){

    console.log(a); // 抛错

    const a = 1;

}

这里在 if 的作用域中，变量 a 声明执行到之前，我们访问了变量 a，这时会抛出一个错误，这说明 const 声明仍然是有预处理机制的。

在执行到 const 语句前，我们的 JavaScript 引擎就已经知道后面的代码将会声明变量 a，从而不允许我们访问外层作用域中的 a。

class 声明

我们在之前的课程中，已经了解过 class 相关的用法。这里我们再从语法的角度来看一遍：

class a {

}

class 最基本的用法只需要 class 关键字、名称和一对大括号。它的声明特征跟 const 和 let 类似，都是作用于块级作用域，预处理阶段则会屏蔽外部变量。

const a = 2;

if(true){

    console.log(a); // 抛错

    class a {

    }

}

class 内部，可以使用 constructor 关键字来定义构造函数。还能定义 getter/setter 和方法。

class Rectangle {

  constructor(height, width) {

    this.height = height;

    this.width = width;

  }

  // Getter

  get area() {

    return this.calcArea();

  }

  // Method

  calcArea() {

    return this.height * this.width;

  }

}

这个例子来自 MDN，它展示了构造函数、getter 和方法的定义。

以目前的兼容性，class 中的属性只能写在构造函数中，相关标准正在 TC39 讨论。

需要注意，class 默认内部的函数定义都是 strict 模式的。

函数声明

函数声明使用 function 关键字。

在上一节课中，我们已经讨论过函数声明对全局作用域的影响了。这一节课，我们来看看函数声明具体的内容，我们先看一下函数声明的几种类型

function foo(){

}

function* foo(){

    yield 1;

    yield 2;

    yield 3;

}

async function foo(){

    await sleep(3000);

}

async function* foo(){

    await sleep(3000);

    yield 1;

}

带 * 的函数是 generator，我们在前面的部分已经见过它了。生成器函数可以理解为返回一个序列的函数，它的底层是 iterator 机制。

async 函数是可以暂停执行，等待异步操作的函数，它的底层是 Promise 机制。
异步生成器函数则是二者的结合。

函数的参数，可以只写形参名，现在还可以写默认参数和指定多个参数，看下例子：

function foo(a = 1, ...other) {

    console.log(a, other)

}

这个形式可以代替一些对参数的处理代码，表意会更加清楚。

结语

今天我们一起学习了语句家族，语句分成了普通语句和声明型语句。

普通语句部分，建议你把重点放在循环语句上面。声明型语句我觉得都很重要，尤其是它们的行为。熟练掌握了它们，我们就可以在工作中去综合运用它们，从而减少代码中的错误。新特性大多可以帮助我们发现代码中的错误。

最后留一个小作业，请你找出所有具有 Symbol.iterator 的原生对象，并且看看它们的 for of 遍历行为。

31 | JavaScript语法（三）：什么是表达式语句？

你好，我是 winter（加微信：642945106 发送“赠送”领取赠送精品课程 发数字“2”获取众筹列表。）。

不知道你有没有注意到，我们在语句部分，讲到了很多种语句类型，但是，其实最终产生执行效果的语句不多。

事实上，真正能干活的就只有表达式语句，其它语句的作用都是产生各种结构，来控制表达式语句执行，或者改变表达式语句的意义。

今天的课程，我们就深入到表达式语句中来学习一下。

什么是表达式语句

表达式语句实际上就是一个表达式，它是由运算符连接变量或者直接量构成的（关于直接量我们在下一节详细讲解）。

一般来说，我们的表达式语句要么是函数调用，要么是赋值，要么是自增、自减，否则表达式计算的结果没有任何意义。

但是从语法上，并没有这样的限制，任何合法的表达式都可以当做表达式语句使用。比如我们看下面的例子。

a + b;

这句代码计算了 a 和 b 相加的值，但是不会显示出来，也不会产生任何执行效果（除非 a 和 b 是 getter），但是不妨碍它符合语法也能够被执行。

下面我们就一起来了解下都有哪些表达式，我们从粒度最小到粒度最大了解一下。

PrimaryExpression 主要表达式

首先我们来给你讲解一下表达式的原子项：Primary Expression。它是表达式的最小单位，它所涉及的语法结构也是优先级最高的。

Primary Expression 包含了各种“直接量”，直接量就是直接用某种语法写出来的具有特定类型的值。我们已经知道，在运行时有各种值，比如数字 123，字符串 Hello world，所以通俗地讲，直接量就是在代码中把它们写出来的语法。

我们在类型部分，已经介绍过一些基本类型的直接量。比如，我们当时用 null 关键字获取 null 值，这个用法就是 null 直接量，这就是这里我们仅仅把它们简单回顾一下：

"abc";

123;

null;

true;

false;

除这些之外，JavaScript 还能够直接量的形式定义对象，针对函数、类、数组、正则表达式等特殊对象类型，JavaScript 提供了语法层面的支持。

({});

(function(){});

(class{ });

[];

/abc/g;

需要注意，在语法层面，function、{ 和 class 开头的表达式语句与声明语句有语法冲突，所以，我们要想使用这样的表达式，必须加上括号来回避语法冲突。

在 JavaScript 标准中，这些结构有的被称作直接量（Literal），有的被称作表达式（**Expression），在我看来，把它们都理解成直接量比较合适。

Primary Expression 还可以是 this 或者变量，在语法上，把变量称作“标识符引用”。

this;

myVar;

任何表达式加上圆括号，都被认为是 Primary Expression，这个机制使得圆括号成为改变运算优先顺序的手段。

(a + b);

这就是 Primary Expression 的几种形式了，接下来，我们讲讲由 Primary Expression 构成的更复杂的表达式：Member Expression。

MemberExpression 成员表达式

Member Expression 通常是用于访问对象成员的。它有几种形式：

a.b;

a["b"];

new.target;

super.b;

前面两种用法都很好理解，就是用标识符的属性访问和用字符串的属性访问。而 new.target 是个新加入的语法，用于判断函数是否是被 new 调用，super 则是构造函数中，用于访问父类的属性的语法。

从名字就可以看出，Member Expression 最初设计是为了属性访问的，不过从语法结构需要，以下两种在 JavaScript 标准中当做 Member Expression：

f`a${b}c`;

这是一个是带函数的模板，这个带函数名的模板表示把模板的各个部分算好后传递给一个函数。

new Cls();

另一个是带参数列表的 new 运算，注意，不带参数列表的 new 运算优先级更低，不属于 Member Expression。

实际上，这两种被放入 Member Expression，仅仅意味着它们跟属性运算属于同一优先级，没有任何语义上的关联。接下来我们看看 Member Expression 能组成什么。

NewExpression NEW 表达式

这种非常简单，Member Expression 加上 new 就是 New Expression（当然，不加 new 也可以构成 New Expression，JavaScript 中默认独立的高优先级表达式都可以构成低优先级表达式）。

注意，这里的 New Expression 特指没有参数列表的表达式。我们看个稍微复杂的例子：

new new Cls(1);

直观看上去，它可能有两种意思：

new (new Cls(1));

new (new Cls)(1);

实际上，它等价于第一种。我们可以用以下代码来验证：

class Cls{

  constructor(n){

    console.log("cls", n);

    return class {

      constructor(n) {

        console.log("returned", n);

      }

    }

  }

}

new (new Cls(1));

这段代码最后得到了下面这样的结果。

cls 1

returned undefined

这里就说明了，1 被当做调用 Cls 时的参数传入了。

CallExpression 函数调用表达式

除了 New Expression，Member Expression 还能构成 Call Expression。它的基本形式是 Member Expression 后加一个括号里的参数列表，或者我们可以用上 super 关键字代替 Member Expression。

a.b(c);

super();

这看起来很简单，但是它有一些变体。比如：

a.b(c)(d)(e);

a.b(c)[3];

a.b(c).d;

a.b(c)`xyz`;

这些变体的形态，跟 Member Expression 几乎是一一对应的。实际上，我们可以理解为，Member Expression 中的某一子结构具有函数调用，那么整个表达式就成为了一个 Call Expression。

而 Call Expression 就失去了比 New Expression 优先级高的特性，这是一个主要的区分。

LeftHandSideExpression 左值表达式

接下来，我们需要理解一个概念：New Expression 和 Call Expression 统称 LeftHandSideExpression，左值表达式。

我们直观地讲，左值表达式就是可以放到等号左边的表达式。JavaScript 语法则是下面这样。

a() = b;

这样的用法其实是符合语法的，只是，原生的 JavaScript 函数，返回的值都不能被赋值。因此多数时候，我们看到的赋值将会是 Call Expression 的其它形式，如：

a().c = b;

另外，根据 JavaScript 运行时的设计，不排除某些宿主会提供返回引用类型的函数，这时候，赋值就是有效的了。

左值表达式最经典的用法是用于构成赋值表达式，但是其实如果你翻一翻 JavaScript 标准，你会发现它出现在各种场合，凡是需要“可以被修改的变量”的位置，都能见到它的身影。

那么接下来我们就讲讲 AssignmentExpression 赋值表达式。

AssignmentExpression 赋值表达式

AssignmentExpression 赋值表达式也有多种形态，最基本的当然是使用等号赋值：

a = b

这里需要理解的一个稍微复杂的概念是，这个等号是可以嵌套的：

a = b = c = d

这样的连续赋值，是右结合的，它等价于下面这种：

a = (b = (c = d))

也就是说，先把 d 的结果赋值给 c，再把整个表达式的结果赋值给 b，再赋值给 a。

当然，这并非一个很好的代码风格，我们讲解语法是为了让你理解这样的用法，而不是推荐你这样写代码。

赋值表达式的使用，还可以结合一些运算符，例如：

a += b;

相当于

a = a + b;

能有这样用的运算符有下面这几种：

*=、/=、%=、+=、-=、<<=、>>=、>>>=、&=、^=、|=、**=

我想你已经注意到了，赋值表达式的等号左边和右边能用的表达式类型不一样，在这一课，我们已经关注完了表达式的左边部分（左值表达式）的语法结构，下一节课，我们将会给你重点讲解表达式的右边部分。

Expression 表达式

赋值表达式可以构成 Expression 表达式的一部分。在 JavaScript 中，表达式就是用逗号运算符连接的赋值表达式。

在 JavaScript 中，比赋值运算优先级更低的就是逗号运算符了。我们可以把逗号可以理解为一种小型的分号。

a = b, b = 1, null;

逗号分隔的表达式会顺次执行，就像不同的表达式语句一样。“整个表达式的结果”就是“最后一个逗号后的表达式结果”。比如我们文中的例子，整个“a = b, b = 1, null;”表达式的结果就是“，”后面的null。

在很多场合，都不允许使用带逗号的表达式，比如我们在前面课程中提到，export 后只能跟赋值表达式，意思就是表达式中不能含有逗号。

结语

这节课我们开始讲解了运算符和表达式的一些相关知识，这节课上，我们已经学习了赋值表达式和赋值表达式的左边部分。下节课，我们将会讲一讲赋值表达式的右边部分。

最后给你留一个作业，把今天讲到的所有运算符按优先级排列成一个表格，下节课我们会补完剩下的部分。

32 | JavaScript语法（四）：新加入的**运算符，哪里有些不一样呢？

你好，我是 winter。

上一节课我们已经给你介绍了表达式的一些结构，其中关于赋值表达式，我们讲完了它的左边部分，而留下了它右边部分，那么，我们这节课一起来详细讲解。

在一些通用的计算机语言设计理论中，能够出现在赋值表达式右边的叫做：右值表达式（RightHandSideExpression），而在 JavaScript 标准中，规定了在等号右边表达式叫做条件表达式（ConditionalExpression），不过，在 JavaScript 标准中，从未出现过右值表达式字样。

JavaScript 标准也规定了左值表达式同时都是条件表达式（也就是右值表达式），此外，左值表达式也可以通过跟一定的运算符组合，逐级构成更复杂的结构，直到成为右值表达式。

关于这块的知识，我们有时会看到按照运算符来组织的讲解形式。

这样讲解形式是因为：对运算符来说的“优先级”，如果从我们语法的角度来看，那就是“表达式的结构”。讲“乘法运算的优先级高于加法”，从语法的角度看就是“乘法表达式和加号运算符构成加法表达式”。

对于右值表达式来说，我们可以理解为以左值表达式为最小单位开始构成的，接下来我们就来看看左值表达式是如何一步步构成更为复杂的语法结构。

更新表达式 UpdateExpression

左值表达式搭配 ++ -- 运算符，可以形成更新表达式。

-- a;

++ a;

a --

a ++

更新表达式会改变一个左值表达式的值。分为前后自增，前后自减一共四种。

我们要注意一下，这里在 ES2018 中，跟早期版本有所不同，前后自增自减运算被放到了同一优先级。

一元运算表达式 UnaryExpression

更新表达式搭配一元运算符，可以形成一元运算表达式，我们看下例子：

delete a.b;

void a;

typeof a;

- a;

~ a;

! a;

await a;

他的特点就是一个更新表达式搭配了一个一元运算符。

乘方表达式 ExponentiationExpression

乘方表达式也是由更新表达式构成的。它使用**号。

++i ** 30

2 ** 30 // 正确

-2 ** 30 // 报错

我们看一下例子，-2 这样的一元运算表达式，是不可以放入乘方表达式的，如果需要表达类似的逻辑，必须加括号。

这里我们需要注意一下结合性，** 运算是右结合的，这跟其它正常的运算符（也就是左结合运算符）都不一样。

我们来看一个例子。

4 ** 3 ** 2

事实上，它是这样被运算的：

4 ** (3 ** 2)

而不是这样被运算的：

(4 ** 3) ** 2

我们来实际在代码中执行一下试试。最终结果是 262144， 而不是 4096。

在最后加一句它的特殊。

乘法表达式 MultiplicativeExpression

到这里，我们进入了比较熟悉的表达式类型，乘方表达式可以构成乘法表达式，用乘号或者除号、取余符号连接就可以了，我们看看例子：

x * 2;

乘法表达式有三种运算符：

*

/

%

它们分别表示乘、除和取余。它们的优先级是一样的，所以统一放在乘法运算表达式中。

加法表达式 AdditiveExpression

加法表达式是由乘法表达式用加号或者减号连接构成的。我们看下例子:

a + b * c

加法表达式有加号和减号两种运算符。

+

-

这就是我们小学学的加法和减法的意思了。不过要注意，加号还能表示字符串连接，这也比较符合一般的直觉。

移位表达式 ShiftExpression

移位表达式由加法表达式构成，移位是一种位运算，分成三种：

<< 向左移位

>> 向右移位

>>> 无符号向右移位

移位运算把操作数看做二进制表示的整数，然后移动特定位数。所以左移 n 位相当于乘以 2 的 n 次方，右移 n 位相当于除以 2 取整 n 次。

普通移位会保持正负数。无符号移位会把减号视为符号位 1，同时参与移位：

-1 >>> 1

这个会得到 2147483647，也就是 2 的 31 次方，跟负数的二进制表示法相关，这里就不详细讲解了。

在 JavaScript 中，二进制操作整数并不能提高性能，移位运算这里也仅仅作为一种数学运算存在，这些运算存在的意义也仅仅是照顾 C 系语言用户的习惯了。

关系表达式 RelationalExpression

移位表达式可以构成关系表达式，这里的关系表达式就是大于、小于、大于等于、小于等于等运算符号连接，统称为关系运算。

<=

>=

<

>

instanceof

in

需要注意，这里的 <= 和 >= 关系运算，完全是针对数字的，所以 <= 并不等价于 < 或 ==。例如

null <= undefined

//false

null == undefined

//true

请大家务必不要用数学上的定义去理解这些运算符。

相等表达式 EqualityExpression

在语法上，相等表达式是由关系表达式用相等比较运算符（如 ==）连接构成的。所以我们可以像下面这段代码一样使用，而不需要加括号。

a instanceof "object" == true

相等表达式由四种运算符和关系表达式构成，我们来看一下运算符：

==

!=

===

!==

相等表达式又包含一个 JavaScript 中著名的设计失误，那就是 == 的行为。

一些编程规范甚至要求完全避免使用 == 运算，我觉得这样规定是比较合理的，但是这里我还是尽量解释一下 == 的行为。

虽然标准中写的==十分复杂，但是归根结底，类型不同的变量比较时==运算只有三条规则：

undefined 与 null 相等；

字符串和 bool 都转为数字再比较；

对象转换成 primitive 类型再比较。

这样我们就可以理解一些不太符合直觉的例子了，比如：

false == '0' true

true == 'true' false

[] == 0 true

[] == false true

new Boolean('false') == false false

这里不太符合直觉的有两点：

一个是即使字符串与 boolean 比较，也都要转换成数字；
二是对象如果转换成了 primitive 类型跟等号另一边类型恰好相同，则不需要转换成数字。

此外，== 的行为也经常跟 if 的行为（转换为 boolean）混淆。总之，我建议，仅在确认 == 发生在 Number 和 String 类型之间时使用，比如：

document.getElementsByTagName('input')[0].value == 100

在这个例子中，等号左边必然是 string，右边的直接量必然是 number，这样使用 == 就没有问题了。

位运算表达式

位运算表达式含有三种：

按位与表达式 BitwiseANDExpression

按位异或表达式 BitwiseANDExpression

按位或表达式 BitwiseORExpression。

位运算表达式关系比较紧密，我们这里放到一起来讲。

按位与表达式由按位与运算符（&）连接按位异或表达式构成，按位与表达式把操作数视为二进制整数，然后把两个操作数按位做与运算。

按位异或表达式由按位异或运算符（^）连接按位与表达式构成，按位异或表达式把操作数视为二进制整数，然后把两个操作数按位做异或运算。异或两位相同时得 0，两位不同时得 1。

异或运算有个特征，那就是两次异或运算相当于取消。所以有一个异或运算的小技巧，就是用异或运算来交换两个整数的值。

let a = 102, b = 324;

a = a ^ b;

b = a ^ b;

a = a ^ b;

console.log(a, b);

按位或表达式由按位或运算符（|）连接相等表达式构成，按位或表达式把操作数视为二进制整数，然后把两个操作数按位做或运算。

按位或运算常常被用在一种叫做 Bitmask 的技术上。Bitmask 相当于使用一个整数来当做多个布尔型变量，现在已经不太提倡了。不过一些比较老的 API 还是会这样设计，比如我们在 DOM 课程中，提到过的 Iterator API，我们看下例子：

var iterator = document.createNodeIterator(document.body, NodeFilter.SHOW_TEXT | NodeFilter.SHOW_COMMENT, null, false);

var node;

while(node = iterator.nextNode())

{

    console.log(node);

}

这里的第二个参数就是使用了 Bitmask 技术，所以必须配合位运算表达式才能方便地传参。

逻辑与表达式和逻辑或表达式

逻辑与表达式由按位或表达式经过逻辑与运算符连接构成，逻辑或表达式则由逻辑与表达式经逻辑或运算符连接构成。

这里需要注意的是，这两种表达式都不会做类型转换，所以尽管是逻辑运算，但是最终的结果可能是其它类型。

比如：

false || 1;

这句将会得到结果 1。

false && undefined;

这句将会得到 undefined。

另外还有一点，就是逻辑表达式具有短路的特性，例如：

true || foo();

这里的 foo 将不会被执行，这种中断后面表达式执行的特性就叫做短路。

条件表达式 ConditionalExpression

条件表达式由逻辑或表达式和条件运算符构成，条件运算符又称三目运算符，它有三个部分，由两个运算符?和:配合使用。

condition ? branch1 : branch2

这里需要注意，条件表达式也像逻辑表达式一样，可能忽略后面表达式的计算。这一点跟 C 语言的条件表达式是不一样的。

条件表达式实际上就是 JavaScript 中的右值表达式了 RightHandSideExpression，是可以放到赋值运算后面的表达式。

总结

今天我们讲解了表达式的右边部分，讲到了包括更新表达式、一元运算表达式、乘方表达式、乘法表达式、移位表达式等 14 种表达式。至此为止，我们已经讲全了表达式。你如果有不熟悉的地方，可以随时回头查阅。

留一个小任务，我们试着总结下 JavaScript 中所有的运算符优先级和结合性。例如：

03 | HTML语义：div和span不是够用了吗？

你好，我是 winter。

今天这篇是我们正式开篇的第一篇文章，我想和你聊聊 HTML。

我猜屏幕那一边的你估计会说：“HTML 我很熟悉了，每天写，这不是初级程序员才学的内容么，这我还能不会吗？”

其实在我看来，HTML 并不简单，它是典型的“入门容易，精通困难”的一部分知识。深刻理解 HTML 是成为优秀的前端工程师重要的一步。

我们在上一篇文章中讲到了，HTML 的标签可以分为很多种，比如 head 里面的元信息类标签，又比如 img、video、audio 之类的替换型媒体标签。我今天要讲的标签是：语义类标签。

语义类标签是什么，使用它有什么好处？

语义类标签也是大家工作中经常会用到的一类标签，它们的特点是视觉表现上互相都差不多，主要的区别在于它们表示了不同的语义，比如大家会经常见到的 section、nav、p，这些都是语义类的标签。

语义是我们说话表达的意思，多数的语义实际上都是由文字来承载的。语义类标签则是纯文字的补充，比如标题、自然段、章节、列表，这些内容都是纯文字无法表达的，我们需要依靠语义标签代为表达。

在讲语义之前，我们来说说为什么要用语义。

现在我们很多的前端工程师写起代码来，多数都不用复杂的语义标签， 只靠 div 和 span 就能走天下了。

这样做行不行呢？毫无疑问答案是行。那这样做好不好呢？按照正确的套路，我应该说不好，但是在很多情况下，答案其实是好。

这是因为在现代互联网产品里，HTML 用于描述“软件界面”多过于“富文本”，而软件界面里的东西，实际上几乎是没有语义的。比如说，我们做了一个购物车功能，我们一定要给每个购物车里的商品套上 ul 吗？比如说，加入购物车这个按钮，我们一定要用 Button 吗？

实际上我觉得没必要，因为这个场景里面，跟文本中的列表，以及表单中的 Button，其实已经相差很远了，所以，我支持在任何“软件界面”的场景中，直接使用 div 和 span。

不过，在很多工作场景里，语义类标签也有它们自己无可替代的优点。正确地使用语义标签可以带来很多好处。

语义类标签对开发者更为友好，使用语义类标签增强了可读性，即便是在没有 CSS 的时候，开发者也能够清晰地看出网页的结构，也更为便于团队的开发和维护。

除了对人类友好之外，语义类标签也十分适宜机器阅读。它的文字表现力丰富，更适合搜索引擎检索（SEO），也可以让搜索引擎爬虫更好地获取到更多有效信息，有效提升网页的搜索量，并且语义类还可以支持读屏软件，根据文章可以自动生成目录等等。

不过，不恰当地使用语义标签，反而会造成负面作用。这里我们举一个常见的误区作为例子。我们都知道 ul 是无序列表，ol 是有序列表，所以很多接触过语义这个概念，半懂不懂的前端工程师，特别喜欢给所有并列关系的元素都套上 ul。

实际上， ul 是长成下面的这种样子的 (以下来自 HTML 标准)。

I have lived in the following countries:

Switzerland

Norway

United Kingdom

United States

ul 多数出现正在行文中间，它的上文多数在提示：要列举某些项。但是，如果所有并列关系都用 ul，会造成大量冗余标签。

错误地使用语义标签，会给机器阅读造成混淆、增加嵌套，给 CSS 编写加重负担。

所以，对于语义标签，我的态度是：“用对”比“不用”好，“不用”比“用错”好。当然了，我觉得有理想的前端工程师还是应该去追求“用对”它们。

与 JavaScript 这样严格的编程语言相比，HTML 中语义标签的使用更接近我们平常说话用的自然语言。我们说话并没有唯一的标准措辞，语义标签的使用也是一样。下面，我挑选了几种（我认为）比较重要的语义标签使用场景，来为你介绍一下。

作为自然语言延伸的语义类标签

其实语义问题不仅仅属于理科，它还是个文科问题。

所以我们这里讲语义标签的使用的第一个场景，也是最自然的使用场景，就是：作为自然语言和纯文本的补充，用来表达一定的结构或者消除歧义。

我们先来看看“表达一定的结构”这个场景。

在日语中，有一个语法现象叫做：ルビ，它的读音是 ruby（著名的 ruby 语言就是据此命名的），它中文的意思大约类似于注音或者意思的注解，它的形式可以看下图：

图中的例子选自动画片《某科学的超电磁炮》第二季第一话。图中把 teleport 放在空间移动上方的用法，就是日文中 ruby 的用法。“空间移动”是动画中白井黑子的技能，这里动画字幕上写的是“空间移动”，动画里的台词则用了英文发音“Teleport”，这里就形成了一个使用 ruby 的场景。

ruby 的这个形式，在中国的网友中间最近被玩出了新花样，比如表情包。

有时候微信聊天，不能用 ruby 这样的东西真的是好急啊，只好用括号代替，效果真是差了不少。

在 HTML5 中，就引入了这个表示 ruby 的标签，它由 ruby、rt、rp 三个标签来实现。

所以说，这些情况里存在的语义，其实原本就存在了，只是我们用纯文字是没法表达的，HTML 作为一种“超文本”语言，支持这些文字表达就是必要的了。

还有一种情况是，HTML 的有些标签实际上就是必要的，甚至必要的程度可以达到：如果没有这个标签，文字会产生歧义的程度。

这里我们可以介绍一下 em 标签。

今天我吃了一个苹果.

我们看看这句话，看上去它很清楚，但是实际上，这句话放到不同上下文中，可能表达完全不同的意思。

昨天我吃了一个香蕉。

今天我吃了一个苹果。

再比如：

昨天我吃了两个苹果。

今天我吃了一个苹果。

试着读一读，这两段里面的“今天我吃了一个苹果”，你是不是发现读音不自觉地发生了变化？

实际上，不仅仅是读音，这里的意思也发生了变化。前一段中，表示我今天吃的是苹果，而不是别的什么东西，后一段中，则表示我今天只吃了一个苹果，没有多吃。

当没有上下文时，如何消除歧义呢？这就要用到我们的 em 标签了。em 表示重音：

今天我吃了一个 <em> 苹果 </em>。

今天我吃了 <em> 一个 </em> 苹果。

通过 em 标签，我们可以消除这样的歧义。

一些文章常常会拿 em 和 strong 做对比，实际上，我们只要理解了 em 的真正意思，它和 strong 可谓天差地别，并没有任何混淆的可能。

作为标题摘要的语义类标签

介绍完自然语言的语义场景后，我想介绍的另一个语义重要使用场景，就是文章的结构。中国古代小说就形成了“章 - 回”的概念，西方的戏剧也有幕的区分，所以人类的自然语言作品也是如出一辙。

HTML 也应该支持这样的需求。HTML 语义标签中，有不少是用于支持这样的结构的标签。

语义化的 HTML 能够支持自动生成目录结构，HTML 标准中还专门规定了生成目录结构的算法，即使我们并不打算深入实践语义，也应该尽量在大的层面上保证这些元素的语义化使用。

首先我们需要形成一个概念，一篇文档会有一个树形的目录结构，它由各个级别的标题组成。这个树形结构可能不会跟 HTML 元素的嵌套关系一致。

例如：

<h1>HTML 语义 </h1>

<p>balah balah balah balah</p>

<h2> 弱语义 </h2>

<p>balah balah</p>

<h2> 结构性元素 </h2>

<p>balah balah</p>

......

这段 HTML 几乎是平铺的元素，但是它的标题结构是：

HTML 语义

弱语义

结构性元素

……

h1-h6 是最基本的标题，它们表示了文章中不同层级的标题。有些时候，我们会有副标题，为了避免副标题产生额外的一个层级，我们使用 hgroup 标签。

我们来看下有 / 无 hgroup 的对比：

<h1>JavaScript 对象 </h1>

<h2> 我们需要模拟类吗？</h2>

<p>balah balah</p>

......

此段生成以下标题结构：

JavaScript 对象

我们需要模拟类吗？

…

<hgroup>

<h1>JavaScript 对象 </h1>

<h2> 我们需要模拟类吗？</h2>

</hgroup>

<p>balah balah</p>

......

这一段生成以下标题结构：

JavaScript 对象——我们需要模拟类吗？

…

我们通过两个效果的对比就可以知道，在 hgroup 中的 h1-h6 被视为同一标题的不同组成部分。

从 HTML 5 开始，我们有了 section 标签，这个标签可不仅仅是一个“有语义的 div”，它会改变 h1-h6 的语义。section 的嵌套会使得其中的 h1-h6 下降一级，因此，在 HTML5 以后，我们只需要 section 和 h1 就足以形成文档的树形结构：

<section>

    <h1>HTML 语义 </h1>

    <p>balah balah balah balah</p>

    <section>

        <h1> 弱语义 </h1>

        <p>balah balah</p>

    </section>

    <section>

        <h1> 结构性元素 </h1>

        <p>balah balah</p>

    </section>

......

</section>

这段代码同样会形成前面例子的标题结构：

HTML 语义

弱语义

结构性元素

……

作为整体结构的语义类标签

我们想介绍的最后一个场景是，随着越来越多的浏览器推出“阅读模式”，以及各种非浏览器终端的出现，语义化的 HTML 适合机器阅读的特性变得越来越重要。

应用了语义化结构的页面，可以明确地提示出页面信息的主次关系，它能让浏览器很好地支持“阅读视图功能”，还可以让搜索引擎的命中率提升，同时，它也对视障用户的读屏软件更友好。

我们正确使用整体结构类的语义标签，可以让页面对机器更友好。比如，这里一个典型的 body 类似这样：

<body>

    <header>

        <nav>

            ……

        </nav>

    </header>

    <aside>

        <nav>

            ……

        </nav>

    </aside>

    <section>……</section>

    <section>……</section>

    <section>……</section>

    <footer>

        <address>……</address>

    </footer>

</body>

在 body 下面，有一个 header，header 里面是一个 nav，跟 header 同级的有一个 aside，aside 里面也有一个 nav。接下来是文章的整体，也就是一个一个的 section。section 里面可能还有嵌套，但是我们就不管了，最后是一个 footer，这个 footer 里面可能有 address 这样的内容。

除此之外，还有 article，article 是一种特别的结构，它表示具有一定独立性质的文章。所以，article 和 body 具有相似的结构，同时，一个 HTML 页面中，可能有多个 article 存在。

一个典型的场景是多篇新闻展示在同一个新闻专题页面中，这种类似报纸的多文章结构适合用 article 来组织。

<body>

    <header>……</header>

    <article>

        <header>……</header>

        <section>……</section>

        <section>……</section>

        <section>……</section>

        <footer>……</footer>

    </article>

    <article>

        ……

    </article>

    <article>

        ……

    </article>

    <footer>

        <address></address>

    </footer>

</body>

body 里面有自己的 header 和 footer，然后里面是竖篇的 article，每一个 article 里面都有自己的 header、section、footer。这是一个典型的多文章结构。

在这个结构里，我们看到了一些新标签，我也来逐个介绍一下。

header，如其名，通常出现在前部，表示导航或者介绍性的内容。

footer，通常出现在尾部，包含一些作者信息、相关链接、版权信息等。

header 和 footer 一般都是放在 article 或者 body 的直接子元素，但是标准中并没有明确规定，footer 也可以和 aside，nav，section 相关联（header 不存在关联问题）。

aside 表示跟文章主体不那么相关的部分，它可能包含导航、广告等工具性质的内容。

aside 很容易被理解为侧边栏，实际上二者是包含关系，侧边栏是 aside，aside 不一定是侧边栏。

aside 和 header 中都可能出现导航（nav 标签），二者的区别是，header 中的导航多数是到文章自己的目录，而 aside 中的导航多数是到关联页面或者是整站地图。

最后 footer 中包含 address，这是个非常容易被误用的标签。address 并非像 date 一样，表示一个给机器阅读的地址，而是表示“文章（作者）的联系方式”，address 明确地只关联到 article 和 body。

总结

本篇中我们介绍了一些基本原则和 HTML 文档的整体结构，从整体上了解了 HTML 语义。

至此，我们可以回答是否要语义化的问题：我们应该分开一些场景来看语义，把它用在合适的场景下，可以获得额外的效果。本篇文中，我们至少涉及了三个明确的场景：

自然语言表达能力的补充；

文章标题摘要；

适合机器阅读的整体结构。

下一篇中，我们会继续深入到更细致的结构中，进一步了解语义。你在工作中是否在使用语义化的标签开发？学习过本篇之后，答案有没有变化呢？你可以给我留言，我们一起讨论。

猜你喜欢

04 | HTML语义：如何运用语义类标签来呈现Wiki网页？

你好，我是 winter，今天我们继续来聊聊 HTML 模块的语义类标签。

在上一篇文章中，我花了大量的篇幅和你解释了正确使用语义类标签的好处和一些场景。那么，哪些场景适合用到语义类标签呢，又如何运用语义类标签呢？

不知道你还记不记得在大学时代，你被导师逼着改毕业论文格式的情景，如果你回想一下，你在论文中使用的那些格式，你会发现其实它们都是可以用 HTML 里的语义标签来表示的。

这正是因为 HTML 最初的设计场景就是“超文本”，早期 HTML 工作组的专家都是出版界书籍排版的专家。

所以，在这一部分，我们找了个跟论文很像的案例：wikipedia 文章，这种跟论文相似的网站比较适合用来学习语义类标签。通过分析一篇 wiki 的文章用到的语义类标签，来进一步帮你理解语义的概念。

你可以在电脑上，打开这个页面：https://en.wikipedia.org/wiki/World_Wide_Web

（为了防止这个页面被修改，我们保存了一个副本：
http://static001.geekbang.org/static/time/quote/World_Wide_Web-Wikipedia.html
）

这是一篇我们选择的 wiki 文章，虽然在原本的 wikipedia 网站中，也是大量使用了 div 和 span 来完成功能。在这里，我们来尝试分析一下，应该如何用语义类标签来呈现这样的一个页面 / 文章。

我们看一下这个页面。

aside

首先我们来看下，左侧侧边栏，根据上一篇文章中提到的语义定义，这里属于 aside 内容。是导航性质的工具内容。

article

我们来到文章主体部分，因为主体部分具有明确的独立性，所以可以用 article 来包裹。

hgroup, h1, h2

在语义的上一篇文章中，我们介绍过 hgroup 和 h1-h6 的作用，hgroup 是标题组，h1 是一级标题，h2 是二级标题。这里，World Wide Web 是文章的大标题，适合 h1 元素。

接下来出现了一个副标题。From Wikipedia, the free encyclopedia。这个地方适合使用 h2，跟 h1 组成一个 hgroup，所以代码可能是类似这样的:

<hgroup>

<h1>World Wide Web </h1>

<h2>From Wikipedia, the free encyclopedia</h2>

</hgroup>

abbr

abbr 标签表示缩写。考虑到 WWW 是 World Wide Web 的缩写，所以文中所有出现的 WWW，都应该使用 abbr 标签，

<abbr title="World Wide Web">WWW</abbr>.

hr

细心的同学会发现，在 wiki 的界面中，出现了一条很长的横线，大家都知道 hr 标签表示横向分隔线，那么这个地方是不是应该用 hr 呢？

答案是不用。我们读一下标准的定义就知道了，hr 表示故事走向的转变或者话题的转变，显然此处两个标题并非这种关系，所以我们应该使用 CSS 的 border 来把它当作纯视觉效果来实现，所以这里是不需要用 hr 的。

p

接下来一段，我们看到了三段“note”，也就是注记。它在文章中用作额外注释。

“WWW” and “The Web” redirect here. For other uses of WWW, see WWW (disambiguation). For other uses of web, see Web (disambiguation).

For the first web software, see WorldWideWeb.

Not to be confused with the Internet.

HTML 中并没有 note 相关的语义，所以，我们用普通的 p 标签，加上class="note"来实现。后面的多数自然段都是普通的段落，我们用 p 标签来实现。

strong

注意，这里 “World Wide Web (WWW)” 和 “the Web” 使用了黑体呈现，从上下文来看，这里表示这个词很重要，所以我们使用 strong 标签。

<p>

A global map of the web index for countries in 2014

<strong>The World Wide Web (WWW)</strong>, also called <strong>the Web</strong>,

......

blockquote, q, cite

接下来我们看到了一个论文中很常见的用法“引述”。

interlinked by hypertext links, and accessible via the Internet.[1]

注意看这里的 [1]，当我们把鼠标放上去的时候，出现了引述的相关信息：

“What is the difference between the Web and the Internet?”. W3C Help and FAQ. W3C. 2009. Archived from the original on 9 July 2015. Retrieved 16 July 2015.

在 HTML 中，有三个跟引述相关的标签 blockquote 表示段落级引述内容，q 表示行内的引述内容，cite 表示引述的作品名。

这里的作品名称 “What is the difference between the Web and the Internet?”，应当使用 cite 标签。

<cite>"What is the difference between the Web and the Internet?"</cite>. W3C Help and FAQ. W3C. 2009. Archived from the original on 9 July 2015. Retrieved 16 July 2015.

在文章的结尾处，有对应的 References 一节，这一节中所有的作品名称也应该加入 cite 标签。

这里我们看看引用的原文就可以知道，wiki 文章中的信息并非直接引用，如果是直接引用的内容，那么，我们还应该加上 blockquote 或者 q 标签。

time

这里除了引用的文章外，还出现了日期，为了让机器阅读更加方便，可以加上 time 标签：

<cite>"What is the difference between the Web and the Internet?"</cite>. W3C Help and FAQ. W3C. 2009. Archived from the original on <time datetime="2015-07-09">9 July 2015</time>. Retrieved <time datetime="2015-07-16">16 July 2015</time>.

figure, figcaption

我们注意一下文章的右侧，出现了几张图片，这种出现在文中的图片，不仅仅是一个 img 标签，它和下面的文字组成了一个 figure 的语法现象，figure 也是我们的一种标签（用于表示与主文章相关的图像、照片等流内容）。

<figure>

 <img src="https://.....440px-NeXTcube_first_webserver.JPG"/>

 <figcaption>The NeXT Computer used by Tim Berners-Lee at CERN.</figcaption>

</figure>

这种插入文章中的内容，不仅限图片，代码、表格等，只要是具有一定自包含性（类似独立句子）的内容，都可以用 figure。这里面，我们用 figcaption 表示内容的标题，当然，也可以没有标题。

dfn

然后我们继续往下看，来注意这一句：

The terms Internet and World Wide Web are often used without much distinction. However, the two are not the same. The Internet is a global system of interconnected computer networks. In contrast, the World Wide Web is a global collection of documents and other resources, linked by hyperlinks and URIs.

这里分别定义了 Internet 和 World Wide Web，我们应该使用 dfn 标签。

The terms Internet and World Wide Web are often used without much distinction. However, the two are not the same.

The <dfn>Internet</dfn> is a global system of interconnected computer networks.

In contrast, the <dfn>World Wide Web</dfn> is a global collection of documents and other resources, linked by hyperlinks and URIs.

代码中你可以看见，你需要在你要定义的词前后放上 dfn 标签，所以我们知道了，dfn 标签是用来包裹被定义的名词。

nav, ol, ul

接下来，几个普通的段落之后，我们看到了文章的目录。这里的目录链接到文章的各个章节，我们可以使用 nav 标签。因为这里的目录顺序不可随意变化，所以我们这里使用多级的 ol 结构。

<nav>

  <h2>Contents</h2>

  <ol>

    <li><a href="...">History</a></li>

    <li><a href="...">Function</a>

      <ol>

        <li><a href="...">Linking</a></li>

        <li><a href="...">Dynamic updates of web pages</a></li>

        ...

      </ol>

    </li>

    ...

  </ol>

</nav>

我们这里必须要指出，ol 和 ul 的区分是内容是否有顺序关系，每一项的前面不论是数字还是点，都不会影响语义的判断。所以，你可以注意一下这里，不要因为视觉表现效果，而改变语义的使用。

pre, samp, code

继续往下，我们来到了这里，我们看见这篇文章有一个很重要的特色，文章中嵌入了一些代码和一些预先编写好的段落。我们看到在“Function”小节中有一段背景色是灰色的文字。

GET /home.html HTTP/1.1

Host: www.example.org

这是一段 HTTP 协议的内容描述，因为这段内容的换行是非常严格的，所以我们不需要浏览器帮我们做自动换行，因此我们使用了 pre 标签，表示这部分内容是预先排版过的，不需要浏览器进行排版。

又因为这是一段计算机程序的示例输出，所以我们可以使用 samp 标签：

<pre><samp>

GET /home.html HTTP/1.1

Host: www.example.org

</samp></pre>

接下来 Wiki 中的内容出现了一段 HTML 代码，我们同样不希望浏览器做自动换行。

<html>

  <head>

    <title>Example.org – The World Wide Web</title>

  </head>

  <body>

    <p>The World Wide Web, abbreviated as WWW and commonly known ...</p>

  </body>

</html>

因为同时是代码，我们还需要加上 code 标签。最后的代码是 pre 标签包裹了 code 标签，code 标签包裹了 HTML 代码。

<pre><code>

&lt;html&gt;

  &lt;head&gt;

    &lt;title&gt;Example.org – The World Wide Web&lt;/title&gt;

  &lt;/head&gt;

  &lt;body&gt;

    &lt;p&gt;The World Wide Web, abbreviated as WWW and commonly known ...&lt;/p&gt;

  &lt;/body&gt;

&lt;/html&gt;

</code></pre>

在后面的代码中，还有一些在行内的 code，比如 title 和 p 括起来的内容，这些也都应该使用 code 标签。

总结

在这一篇 Wiki 文章中，已经涉及了大部分语义标签，可见 HTML 工作组对语义标签的增加是非常谨慎和保守的。

当然了，我们选择的案例不可能刚巧覆盖所有的标签，还有些没讲到的标签，我们这里稍微做一下简要的补充说明。

（长按点击大图查看）

实际上，HTML 这种语言，并不像严谨的编程语言一样，有一条非此即彼的线。一些语义的使用其实会带来争议，所以我的建议是：你可以尽量只用自己熟悉的语义标签，并且只在有把握的场景引入语义标签。这样，我们才能保证语义标签不被滥用，造成更多的问题。

你最擅长使用哪些语义标签，会把它们用在哪些场景里呢？欢迎留言告诉我，我们一起讨论。

猜你喜欢

09 | CSS语法：除了属性和选择器，你还需要知道这些带@的规则

你好，我是 winter。

今天我们进入 CSS 的学习。CSS 是前端工程师几乎每天都要用的技术了，不过 CSS 的学习资料却是最糟糕的，这是因为 CSS 并没有像 HTML 和 JavaScript 那样的一份标准文档。

如果我们到 W3C 的网站上搜索看看，可以得到一些信息：

https://www.w3.org/TR/?title=css

在这里，我们一共看到了 98 份 CSS 相关的标准，它们各自从一些角度规定了 CSS 的特性。

这里我们暂且去掉 Working Draft 状态的标准，可以得到 22 份候选标准和 6 份推荐标准。

既然我们的专栏内容强调去系统性学习 CSS，于是，面对这 22+6 份标准，我们就又需要一条线索，才能把这些离散的标准组织成易于理解和记忆的形式。

在这样的需求下，我找到的线索就是 CSS 语法，任何 CSS 的特性都必须通过一定的语法结构表达出来，所以语法可以帮助我们发现大多数 CSS 特性。

CSS 语法的最新标准，你可以戳这里查看：

https://www.w3.org/TR/css-syntax-3/

这篇文档的阅读体验其实是非常糟糕的，它对 CSS 语法的描述使用了类似 LL 语法分析的伪代码，而且没有描述任何具体的规则。

这里你就不必自己去阅读了，我来把其中一些有用的关键信息抽取出来描述一下，我们一起来看看。

我们拿到这份标准可以看到，去除空格、HTML 注释等无效信息，CSS 的顶层样式表由两种规则组成的规则列表构成，一种被称为 at-rule，也就是 at 规则，另一种是 qualified rule，也就是普通规则。

at-rule 由一个 @ 关键字和后续的一个区块组成，如果没有区块，则以分号结束。这些 at-rule 在开发中使用机会远远小于普通的规则，所以它的大部分内容，你可能会感觉很陌生。

这些 at 规则正是掌握 CSS 的一些高级特性所必须的内容。qualified rule 则是指普通的 CSS 规则，也就是我们所熟识的，由选择器和属性指定构成的规则。

at 规则

好了，现在我们已经知道了，CSS 语法的整体结构，接下来我们要做的是一个体力活，从所有的 CSS 标准里找到所有可能的 at-rule（不用谢，我已经帮你找好了，如果页面定位不准，你可以打开页面搜索关键字）。

@charset ： https://www.w3.org/TR/css-syntax-3/

@import ：https://www.w3.org/TR/css-cascade-4/

@media ：https://www.w3.org/TR/css3-conditional/

@page ： https://www.w3.org/TR/css-page-3/

@counter-style ：https://www.w3.org/TR/css-counter-styles-3

@keyframes ：https://www.w3.org/TR/css-animations-1/

@fontface ：https://www.w3.org/TR/css-fonts-3/

@supports ：https://www.w3.org/TR/css3-conditional/

@namespace ：https://www.w3.org/TR/css-namespaces-3/

这里的每一种 @规则背后，都是一组 CSS 的知识。在我们的课程中，有些会重点介绍，不过，为了先给你建立起一个整体的认知，我们这里会给所有的 @规则提供一些简单的例子和介绍。

@charset

@charset 用于提示 CSS 文件使用的字符编码方式，它如果被使用，必须出现在最前面。这个规则只在给出语法解析阶段前使用，并不影响页面上的展示效果。

@charset "utf-8";

@import

@import 用于引入一个 CSS 文件，除了 @charset 规则不会被引入，@import 可以引入另一个文件的全部内容。

@import "mystyle.css";

@import url("mystyle.css");

@import [ <url> | <string> ]

        [ supports( [ <supports-condition> | <declaration> ] ) ]?

        <media-query-list>? ;

通过代码，我们可以看出，import 还支持 supports 和 media query 形式。

@media

media 就是大名鼎鼎的 media query 使用的规则了，它能够对设备的类型进行一些判断。在 media 的区块内，是普通规则列表。

@media print {

    body { font-size: 10pt }

}

@page

page 用于分页媒体访问网页时的表现设置，页面是一种特殊的盒模型结构，除了页面本身，还可以设置它周围的盒。

@page {

  size: 8.5in 11in;

  margin: 10%;

  @top-left {

    content: "Hamlet";

  }

  @top-right {

    content: "Page " counter(page);

  }

}

@ counter-style

counter-style 产生一种数据，用于定义列表项的表现。

@counter-style triangle {

  system: cyclic;

  symbols: ‣;

  suffix: " ";

}

@ key-frames

keyframes 产生一种数据，用于定义动画关键帧。

@keyframes diagonal-slide {

  from {

    left: 0;

    top: 0;

  }

  to {

    left: 100px;

    top: 100px;

  }

}

@ fontface

fontface 用于定义一种字体，icon font 技术就是利用这个特性来实现的。

@font-face {

  font-family: Gentium;

  src: url(http://example.com/fonts/Gentium.woff);

}

p { font-family: Gentium, serif; }

@ support

support 检查环境的特性，它与 media 比较类似。

@ namespace

用于跟 XML 命名空间配合的一个规则，表示内部的 CSS 选择器全都带上特定命名空间。

@ viewport

用于设置视口的一些特性，不过兼容性目前不是很好，多数时候被 html 的 meta 代替。

其它

除了以上这些，还有些目前不太推荐使用的 at 规则。

@color-profile 是 SVG1.0 引入的 CSS 特性，但是实现状况不怎么好。

@document 还没讨论清楚，被推迟到了 CSS4 中。

@font-feature-values 。todo 查一下。

普通规则

接下来我们进入 qualified rule，也就是普通规则的部分，看看这里有什么需要我们记住的内容。

qualified rule 主要是由选择器和声明区块构成。声明区块又由属性和值构成。我在下面的列表中，介绍了这部分语法的组成要点。

普通规则

选择器

声明列表

属性

值

值的类型

函数

选择器

我们先来看看选择器，它有一份独立的标准，我们可以参考这个网址：

https://www.w3.org/TR/selectors-4/

这份标准不在我们前面的过滤条件中，它属于 CSS 和 HTML 共用的标准。

关于选择器的叠加规则等知识我们后文会专门的一节课程来讲，这里我们就从语法的角度介绍一下选择器。

在选择器标准的最后，附有一张选择器的语法表，从这份语法表，我们可以理清楚记忆选择器的思路。

我们从语法结构可以看出，任何选择器，都是由几个符号结构连接的：空格、大于号、加号、波浪线、双竖线，这里需要注意一下，空格，即为后代选择器的优先级较低。

然后对每一个选择器来说，如果它不是伪元素的话，由几个可选的部分组成，标签类型选择器，id、class、属性和伪类，它们中只要出现一个，就构成了选择器。

如果它是伪元素，则在这个结构之后追加伪元素。只有伪类可以出现在伪元素之后。我在下面用一个列表（不太严谨地）整理了选择器的语法结构：

我们在这里可以参考一个示例图：

（语法结构分析示例）
看完了选择器，我们继续来看看声明部分的语法。

声明：属性和值

声明部分是一个由“属性: 值”组成的序列。

属性是由中划线、下划线、字母等组成的标识符，CSS 还支持使用反斜杠转义。我们需要注意的是：属性不允许使用连续的两个中划线开头，这样的属性会被认为是 CSS 变量。

在CSS Variables 标准中，以双中划线开头的属性被当作变量，与之配合的则是 var 函数：

:root {

  --main-color: #06c;

  --accent-color: #006;

}

/* The rest of the CSS file */

#foo h1 {

  color: var(--main-color);

}

值的部分，主要在标准 CSS Values and Unit，根据每个 CSS 属性可以取到不同的值，这里的值可能是字符串、标识符。

CSS 属性值可能是以下类型。

CSS 范围的关键字：initial，unset，inherit，任何属性都可以的关键字。

字符串：比如 content 属性。

URL：使用 url() 函数的 URL 值。

整数 / 实数：比如 flex 属性。

维度：单位的整数 / 实数，比如 width 属性。

百分比：大部分维度都支持。

颜色：比如 background-color 属性。

图片：比如 background-image 属性。

2D 位置：比如 background-position 属性。

函数：来自函数的值，比如 transform 属性。

这里我们要重点介绍一下函数。一些属性会要求产生函数类型的值，比如 easing-function 会要求 cubic-bezier() 函数的值：

CSS 支持一批特定的计算型函数：

calc()

max()

min()

clamp()

toggle()

attr()

calc()函数是基本的表达式计算，它支持加减乘除四则运算。在针对维度进行计算时，calc() 函数允许不同单位混合运算，这非常的有用。

例如：

section {

  float: left;

  margin: 1em; border: solid 1px;

  width: calc(100%/3 - 2*1em - 2*1px);

}

max()、min() 和 clamp()则是一些比较大小的函数，max() 表示取两数中较大的一个，min() 表示取两数之中较小的一个，clamp() 则是给一个值限定一个范围，超出范围外则使用范围的最大或者最小值。

toggle() 函数在规则选中多于一个元素时生效，它会在几个值之间来回切换，比如我们要让一个列表项的样式圆点和方点间隔出现，可以使用下面代码：

ul { list-style-type: toggle(circle, square); }

attr() 函数允许 CSS 接受属性值的控制。

总结

在这一部分，我们介绍了 CSS 语法的总体结构，CSS 的语法总体结构是由两种规则列表构成，一种是 at 规则，另一种是普通规则。

在 at 规则中，我举了 13 个以上的例子，并逐个进行了简单的介绍。而在普通规则的部分，我介绍了选择器和声明区块是普通规则的主要组成部分。

并且，我给出了一个（不太严谨）的选择器语法结构，声明区块则由属性和值构成，这一部分我们重点介绍了函数。

从整体上去掌握内容，再去定位到单个细节，这对于我们学习 CSS 有非常重要的提示作用。

最后，给你留一个思考问题，CSS 的函数有很多，本文也提到了不少，请你也一起查阅资料，试着总结一下，你能找到多少种 CSS 函数？

15 | HTML元信息类标签：你知道head里一共能写哪几种标签吗？

你好，我是 winter。

我们在前面的 HTML 部分的课程中，已经学习了语义标签。这些标签涵盖了我们日常开发用到的多数标签，也是我们编写代码时最常用的一批标签。

但是我们今天要讲的标签，重要性丝毫不弱于语义类标签，这就是页面元信息类标签。

我们可以先来了解一下什么是元信息类标签。所谓元信息，是指描述自身的信息，元信息类标签，就是 HTML 用于描述文档自身的一类标签，它们通常出现在 head 标签中，一般都不会在页面被显示出来（与此相对，其它标签，如语义类标签，描述的是业务）。

元信息多数情况下是给浏览器、搜索引擎等机器阅读的，有时候这些信息会在页面之外显示给用户，有时候则不会。

元信息类标签数量不多，我在这里就逐一为你介绍一下。

head 标签

首先我们先来了解一下 head 标签，head 标签本身并不携带任何信息，它主要是作为盛放其它语义类标签的容器使用。

head 标签规定了自身必须是 html 标签中的第一个标签，它的内容必须包含一个 title，并且最多只能包含一个 base。如果文档作为 iframe，或者有其他方式指定了文档标题时，可以允许不包含 title 标签。

title 标签

title 标签表示文档的标题，从字面上就非常容易理解。这里我就讲讲需要注意的地方。

你还记得吗，我们的语义类标签中也有一组表示标题的标签：h1-h6。

heading 和 title 两个英文单词意义区分十分微妙，在中文中更是找不到对应的词汇来区分。但是实际使用中，两者确实有一定区别。

在 HTML 标准中，特意讨论了这个问题。我们思考一下，假设有一个介绍蜜蜂跳舞求偶仪式的科普页面，我们试着把以下两个文字分别对应到 title 和 h1。

蜜蜂求偶仪式舞蹈

舞蹈

在听 / 看正确答案前，你不妨先想想，自己的答案是什么呢？为什么？

好了，思考之后，我们来看看正确答案。正确答案是“蜜蜂求偶仪式舞蹈”放入 title，“舞蹈”放入 h1。

我来讲一讲为什么要这样放呢？这主要是考虑到 title 作为元信息，可能会被用在浏览器收藏夹、微信推送卡片、微博等各种场景，这时侯往往是上下文缺失的，所以 title 应该是完整地概括整个网页内容的。

而 h1 则仅仅用于页面展示，它可以默认具有上下文，并且有链接辅助，所以可以简写，即便无法概括全文，也不会有很大的影响。

base 标签

base 标签实际上是个历史遗留标签。它的作用是给页面上所有的 URL 相对地址提供一个基础。

base 标签最多只有一个，它改变全局的链接地址，它是一个非常危险的标签，容易造成跟 JavaScript 的配合问题，所以在实际开发中，我比较建议你使用 JavaScript 来代替 base 标签。

meta 标签

meta 标签是一组键值对，它是一种通用的元信息表示标签。

在 head 中可以出现任意多个 meta 标签。一般的 meta 标签由 name 和 content 两个属性来定义。name 表示元信息的名，content 则用于表示元信息的值。

它基本用法是下面这样的，你也可以自己动手尝试一下：

  <meta name=application-name content="lsForums">

这个标签表示页面所在的 web-application，名为 IsForums。

这里的 name 是一种比较自由的约定，http 标准规定了一些 name 作为大家使用的共识，也鼓励大家发明自己的 name 来使用。

除了基本用法，meta 标签还有一些变体，主要用于简化书写方式或者声明自动化行为。下面我就挑几种重点的内容来分别讲解一下。

具有 charset 属性的 meta

从 HTML5 开始，为了简化写法，meta 标签新增了 charset 属性。添加了 charset 属性的 meta 标签无需再有 name 和 content。

  <meta charset="UTF-8" >

charset 型 meta 标签非常关键，它描述了 HTML 文档自身的编码形式。因此，我建议这个标签放在 head 的第一个。

<html>

<head>

<meta charset="UTF-8">

……

这样，浏览器读到这个标签之前，处理的所有字符都是 ASCII 字符，众所周知，ASCII 字符是 UTF-8 和绝大多数字符编码的子集，所以，在读到 meta 之前，浏览器把文档理解多数编码格式都不会出错，这样可以最大限度地保证不出现乱码。

一般情况下，http 服务端会通过 http 头来指定正确的编码方式，但是有些特殊的情况如使用 file 协议打开一个 HTML 文件，则没有 http 头，这种时候，charset meta 就非常重要了。

具有 http-equiv 属性的 meta

具有 http-equiv 属性的 meta 标签，表示执行一个命令，这样的 meta 标签可以不需要 name 属性了。

例如，下面一段代码，相当于添加了 content-type 这个 http 头，并且指定了 http 编码方式。

<meta http-equiv="content-type" content="text/html; charset=UTF-8">

除了 content-type，还有以下几种命令：

content-language 指定内容的语言；

default-style 指定默认样式表；

refresh 刷新；

set-cookie 模拟 http 头 set-cookie，设置 cookie；

x-ua-compatible 模拟 http 头 x-ua-compatible，声明 ua 兼容性；

content-security-policy 模拟 http 头 content-security-policy，声明内容安全策略。

name 为 viewport 的 meta

实际上，meta 标签可以被自由定义，只要写入和读取的双方约定好 name 和 content 的格式就可以了。

我们来介绍一个 meta 类型，它没有在 HTML 标准中定义，却是移动端开发的事实标准：它就是 name 为 viewport 的 meta。

这类 meta 的 name 属性为 viewport，它的 content 是一个复杂结构，是用逗号分隔的键值对，键值对的格式是 key=value。

例如：

<meta name="viewport" content="width=500, initial-scale=1">

这里只指定了两个属性，宽度和缩放，实际上 viewport 能控制的更多，它能表示的全部属性如下：

width：页面宽度，可以取值具体的数字，也可以是 device-width，表示跟设备宽度相等。

height：页面高度，可以取值具体的数字，也可以是 device-height，表示跟设备高度相等。

initial-scale：初始缩放比例。

minimum-scale：最小缩放比例。

maximum-scale：最大缩放比例。

user-scalable：是否允许用户缩放。

对于已经做好了移动端适配的网页，应该把用户缩放功能禁止掉，宽度设为设备宽度，一个标准的 meta 如下：

<meta name="viewport" content="width=device-width,initial-scale=1,minimum-scale=1,maximum-scale=1,user-scalable=no">

其它预定义的 meta

在 HTML 标准中，还定义了一批 meta 标签的 name，可以视为一种有约定的 meta，我在这里列出来，你可以简单了解一下。

application-name：如果页面是 Web application，用这个标签表示应用名称。

author: 页面作者。

description：页面描述，这个属性可能被用于搜索引擎或者其它场合。

generator: 生成页面所使用的工具，主要用于可视化编辑器，如果是手写 HTML 的网页，不需要加这个 meta。

keywords: 页面关键字，对于 SEO 场景非常关键。

referrer: 跳转策略，是一种安全考量。

theme-color: 页面风格颜色，实际并不会影响页面，但是浏览器可能据此调整页面之外的 UI（如窗口边框或者 tab 的颜色）。

结语

在本课，我们又学习了一批标签，它们是文档用于描述自身的元信息类标签。一些元信息标签可以产生实际的行为，掌握它们对于我们编写代码是必须的。

另一些元信息仅仅是对页面的描述，掌握它们可以使我们编写的页面跟各种浏览器、搜索引擎等结合地更好。

主要包括下面这些内容。

head：元信息的容器。

title：文档标题。

base：页面的基准 URL。

meta: 元信息通用标签。

我们还展开介绍了几种重要的 meta 标签，charset 表示页面编码，http-equiv 表示命令，还介绍了一些有约定的 meta 名称。

最后，给你留一个问题，你还见过哪些 meta 标签的用法？欢迎留言告诉我。

20 | CSS 选择器：如何选中svg里的a元素？

你好，我是 winter。

我们在之前 CSS 语法课程中，已经介绍了关于选择器的一部分基础知识。在今天的这一课里，我们来系统学习一下 CSS 选择器。

在 CSS 语法课程中，我们已经见过一些选择器了，但在进入到具体的选择器介绍之前，我们首先要对选择器有一个整体的认识。

我先来讲讲选择器是什么，选择器是由 CSS 最先引入的一个机制（但随着 document.querySelector 等 API 的加入，选择器已经不仅仅是 CSS 的一部分了）。我们今天这一课，就重点讲讲 CSS 选择器的一些机制。

选择器的基本意义是：根据一些特征，选中元素树上的一批元素。

我们把选择器的结构分一下类，那么由简单到复杂可以分成以下几种。

简单选择器：针对某一特征判断是否选中元素。

复合选择器：连续写在一起的简单选择器，针对元素自身特征选择单个元素。

复杂选择器：由“（空格）”“ >”“ ~”“ +”“ ||”等符号连接的复合选择器，根据父元素或者前序元素检查单个元素。

选择器列表：由逗号分隔的复杂选择器，表示“或”的关系。

我们可以看到，选择器是由简单选择器逐级组合而成的结构，那么我们就来首先看一下简单选择器。

简单选择器

我们在前面说过，简单选择器是针对某一特征判断是否为选中元素。今天我会为你介绍一系列常见的简单选择器，我们把相似的简单选择器放在一起，这样更易于你去记忆。

类型选择器和全体选择器

我们要介绍的第一个简单选择器就是类型选择器，它根据一个元素的标签名来选中元素。

比如：

   div {

   }

这看上去非常简单，但是实际上，我们还必须要考虑 html 或者 xml 元素的命名空间问题。

比如我们的 svg 元素，实际上在： http://www.w3.org/2000/svg 命名空间之下。

svg 和 html 中都有 a 元素，我们若要想区分选择 svg 中的 a 和 html 中的 a，就必须用带命名空间的类型选择器。

<!DOCTYPE html>

<html>

<head>

  <meta charset="utf-8">

  <title>JS Bin</title>

</head>

<body>

<svg width="100" height="28" viewBox="0 0 100 28" version="1.1"

     xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">

  <desc>Example link01 - a link on an ellipse

  </desc>

  <a xlink:href="http://www.w3.org">

    <text y="100%">name</text>

  </a>

</svg>

<br/>

<a href="javascript:void 0;">name</a>

</body>

</html>

@namespace svg url(http://www.w3.org/2000/svg);

@namespace html url(http://www.w3.org/1999/xhtml);

svg|a {

  stroke:blue;

  stroke-width:1;

}

html|a {

  font-size:40px

}

这里有一个特殊的选择器，就是“ * ” ，它称为全体选择器，可以选中任意元素。它的用法跟类型选择器是完全一致的，这里就把它们放到一起介绍了。

id 选择器与 class 选择器

id 选择器和 class 选择器都是针对特定属性的选择器。id 选择器是“#”号后面跟随 id 名，class 选择器是“.”后面跟随 class 名。我们来看看基本用法：

#myid {

  stroke:blue;

  stroke-width:1;

}

.mycls {

  font-size:40px

}

这两个选择器都是在属性选择器之前就设计出来的选择器，属性选择器出来了以后，理论上可以一定程度上替代它们。但是要注意，class 选择器识别的是：用空格分隔的 class 语法。

<a class="a b c">xxx</a>

.a {

    color:red;

}

在这个例子中，我们使用了用空格分隔的 class 属性，使用“.a”“.b”或者“.c”都能够选中元素，也可以使用多个 class 选择器来要求元素具有多个类。

属性选择器

属性选择器根据 HTML 元素的属性来选中元素。属性选择器有四种形态。

第一种，[att]

直接在方括号中放入属性名，是检查元素是否具有这个属性，只要元素有这个属性，不论属性是什么值，都可以被选中。

第二种，[att=val]

精确匹配，检查一个元素属性的值是否是 val。

第三种，[att~=val]

多种匹配，检查一个元素的值是否是若干值之一，这里的 val 不是一个单一的值了，可以是用空格分隔的一个序列。

第四种，[att|=val]

开头匹配，检查一个元素的值是否是以 val 开头，它跟精确匹配的区别是属性只要以 val 开头即可，后面内容不管。

有些 HTML 属性含有特殊字符，这个时候，可以把 val 用引号括起来，形成一个 CSS 字符串。CSS 字符串允许使用单双引号来规避特殊字符，也可以用反斜杠转义，这样，就可以表示出任意属性值啦。

伪类选择器

接下来我们开始介绍伪类选择器，伪类选择器是一系列由 CSS 规定好的选择器，它们以冒号开头。伪类选择器有普通型和函数型两种。

我们首先来介绍一下伪类中最常用的部分：树结构关系伪类。

树结构关系伪类选择器

:root 伪类表示树的根元素，在选择器是针对完整的 html 文档情况，我们一般用 html 标签即可选中根元素。但是随着 scoped css 和 shadow root 等场景出现，选择器可以针对某一子树来选择，这时候就很需要 root 伪类了。

:empty 伪类表示没有子节点的元素，这里有个例外就是子节点为空白文本节点的情况。

:nth-child 和 :nth-last-child 这是两个函数型的伪类，CSS 的 An+B 语法设计的是比较复杂的，我们这里仅仅介绍基本用法。我们还是看几个例子：

:nth-last-child 的区别仅仅是从后往前数。

:first-child :last-child 分别表示第一个和最后一个元素。

:only-child 按字面意思理解即可，选中唯一一个子元素。

of-type 系列，是一个变形的语法糖，S:nth-of-type(An+B) 是:nth-child(|An+B| of S) 的另一种写法。

以此类推，还有 nth-last-of-type、first-of-type、last-of-type、only-of-type。

链接与行为伪类选择器

链接与行为是第一批设计出来的伪类，也是最常用的一批。

:any-link 表示任意的链接，包括 a、area 和 link 标签都可能匹配到这个伪类。

:link 表示未访问过的链接， :visited 表示已经访问过的链接。

:hover 表示鼠标悬停在上的元素，

:active 表示用户正在激活这个元素，如用户按下按钮，鼠标还未抬起时，这个按钮就处于激活状态

:focus 表示焦点落在这个元素之上。

:target 用于选中浏览器 URL 的 hash 部分所指示的元素。

在 Selector Level 4 草案中，还引入了 target-within、focus-within 等伪类，用于表示 target 或者 focus 的父容器。

逻辑伪类选择器

我们这里介绍一个逻辑伪类 —— :not 伪类。

这个伪类是个函数型伪类，它的作用时选中内部的简单选择器命中的元素。

*|*:not(:hover)

选择器 3 级标准中，not 只支持简单选择器，在选择器 4 级标准，则允许 not 接受一个选择器列表，这意味着选择器支持嵌套，仅靠 not 即可完成选择器的一阶真值逻辑完备，但目前还没有看到浏览器实现它。

在 Selector Level 4 草案中，还引入了:is :where :has 等逻辑伪类，但是它们有一些违背了选择器匹配 DOM 树不回溯的原则，所以这部分设计最终的命运如何还不太确定。

其它伪类选择器

还有一些草案中或者不常用的选择器，你仅做大概了解即可。

国际化：用于处理国际化和多语言问题。

dir

lang

音频 / 视频：用于区分音视频播放状态。

play

pause

时序：用于配合读屏软件等时序性客户端的伪类。

current

past

future

表格：用于处理 table 的列的伪类。

nth-col

nth-last-col

伪类是很大的一类简单选择器，它是选择器能力的一种补充。在实际使用中，我还是建议你尽量通过合适的 id 和 class 来标识元素，约束伪类的使用。最好只在不得不使用伪类的场景使用伪类，这对于 CSS 代码的性能和可读性都有好处。

结语

这一节课程中，我们介绍了 CSS 选择器的整体结构，并且介绍了一系列简单选择器。它们包括了下面这些内容。

类型选择器：根据一个元素的标签名来选中元素。

全体选择器：与类型选择器类似，选择任意元素。

id 选择器：# 后面跟随 id 名。

class 选择器：. 后面跟随 class 名。

伪类选择器：一系列由 CSS 规定好的选择器，它们以冒号开头，伪类有普通型和函数型。

在下一节课，我们开始进入到更复杂的情况，我们将会介绍选择器的组合使用方式和选择器的一些机制。

今天留给你的思考题是：用 JavaScript 实现一个能够处理所有简单选择器的 querySelector（行为伪类除外），你可以把你的答案分享出来，我们一起来探讨吧。

21 | CSS选择器：伪元素是怎么回事儿？

你好，我是 winter。

在上一篇文章中，我已经给你介绍了一些简单选择器，这一节课我会继续给你介绍选择器的几个机制：选择器的组合、选择器的优先级和伪元素。

选择器的组合

在 CSS 规则中，选择器部分是一个选择器列表。

选择器列表是用逗号分隔的复杂选择器序列；复杂选择器则是用空格、大于号、波浪线等符号连接的复合选择器；复合选择器则是连写的简单选择器组合。

根据选择器列表的语法，选择器的连接方式可以理解为像四则运算一样有优先级。

第一优先级

无连接符号

第二优先级

“空格”

“~”

“+”

“>”

“||”

第三优先级

“,”

例如以下选择器：

.c,.a>.b.d {

    /*......*/

}

我们应该理解为这样的结构。

.c,.a>.b.d

.c

.a>.b.d

.a

.b.d

.b

.d

复合选择器表示简单选择器中“且”的关系，例如，例子中的“ .b.d ”，表示选中的元素必须同时具有 b 和 d 两个 class。

复杂选择器是针对节点关系的选择，它规定了五种连接符号。

“空格”：后代，表示选中所有符合条件的后代节点， 例如“ .a .b ”表示选中所有具有 class 为 a 的后代节点中 class 为 b 的节点。

“>” ：子代，表示选中符合条件的子节点，例如“ .a>.b ”表示：选中所有“具有 class 为 a 的子节点中，class 为 b 的节点”。

“~” : 后继，表示选中所有符合条件的后继节点，后继节点即跟当前节点具有同一个父元素，并出现在它之后的节点，例如“ .a~.b ”表示选中所有具有 class 为 a 的后继中，class 为 b 的节点。

“+”：直接后继，表示选中符合条件的直接后继节点，直接后继节点即 nextSlibling。例如 “.a+.b ”表示选中所有具有 class 为 a 的下一个 class 为 b 的节点。

“||”：列选择器，表示选中对应列中符合条件的单元格。

我们在实际使用时，比较常用的连接方式是“空格”和“>”。

工程实践中一般会采用设置合理的 class 的方式，来避免过于复杂的选择器结构，这样更有利于维护和性能。

空格和子代选择器通常用于组件化场景，当组件是独立开发时，很难完全避免 class 重名的情况，如果为组件的最外层容器元素设置一个特别的 class 名，生成 CSS 规则时，则全部使用后代或者子代选择器，这样可以有效避免 CSS 规则的命名污染问题。

逗号表示“或”的关系，实际上，可以把它理解为“两条内容一样的 CSS 规则”的一种简写。如我们开头的例子，可以理解成与下面的代码等效：

.c {

    /*......*/

}

.a>.b.d {

    /*......*/

}

到这里，我们就讲完了如何用简单选择器组合成复合选择器和复杂选择器，形成选择器列表，这能够帮助我们应对各种复杂的需求。

CSS 选择器是基于规则生效的，同一个元素命中多条规则是非常常见的事情。不同规则指定同一个属性为不同值时，就需要一个机制来解决冲突。这个机制，就是接下来我们要讲的选择器优先级。

选择器的优先级

CSS 标准用一个三元组 (a, b, c) 来构成一个复杂选择器的优先级。

id 选择器的数目记为 a；

伪类选择器和 class 选择器的数目记为 b；

伪元素选择器和标签选择器数目记为 c；

“*” 不影响优先级。

CSS 标准建议用一个足够大的进制，获取“ a-b-c ”来表示选择器优先级。

即：

specificity = base * base * a + base * b + c

其中，base 是一个“足够大”的正整数。关于 base，历史中有些趣闻，早年 IE6 采用 256 进制，于是就产生“256 个 class 优先级等于一个 id”这样的奇葩问题，后来扩大到 65536，基本避免了类似的问题。

现代浏览器多采用了更大的数量，我们正常编写的 CSS 规则数量不太可能达到数万，因此我们可以认为这样的 base 就足够大了。

行内属性的优先级永远高于 CSS 规则，浏览器提供了一个“口子”，就是在选择器前加上“!import”。

这个用法非常危险，因为它相当于一个新的优先级，而且此优先级会高于行内属性。

同一优先级的选择器遵循“后面的覆盖前面的”原则，我们可以看一个例子：

<div id="my" class="x y">text<div>

.x {

    background-color:lightblue;

}

.y {

    background-color:lightgreen;

}

调换“.x”和“.y”我们可以得到不同的显示效果。选择器的优先级是针对单条规则的，多条规则的选择器同时命中元素，优先级不会发生叠加。

<div id="my" class="x y z">text<div>

.x {

    background-color:lightblue;

}

.z {

    background-color:lightblue;

}

.y {

    background-color:lightgreen;

}

在这个例子中，“.x ”和“.z ”都指定了背景色为浅蓝色，但是因为“.y ”规则在最后，所以最终显示结果为浅绿色。另外一个需要注意的是，选择器的优先级是针对复杂选择器的优先级，选择器列表不会合并计算优先级。

我们看一个例子：

<div id="my" class="x y z">text<div>

.x, .z {

    background-color:lightblue;

}

.y {

    background-color:lightgreen;

}

这里选择器列表“ .x, .z”命中了 div，但是它的两项分别计算优先级，所以最终优先级仍跟“ .y” 规则相同。

以上就是选择器优先级的相关规则了，虽然我们这里介绍了详细的计算方式，但是我认为选择器的使用上，如果产生复杂的优先级计算，代码的可读性一定是有问题的。

所以实践中，建议你“根据 id 选单个元素”“class 和 class 的组合选成组元素”“tag 选择器确定页面风格”这样的简单原则来使用选择器，不要搞出过于复杂的选择器。

伪元素

在上一课，我们有意忽略了一种重要的简单选择器：伪元素。

我之所以没有把它放在简单选择器中，是因为伪元素本身不单单是一种选择规则，它还是一种机制。

所以本节课，我就来讲一讲伪元素机制。伪元素的语法跟伪类相似，但是实际产生的效果却是把不存在的元素硬选出来。

目前兼容性达到可用的伪元素有以下几种。

::first-line

::first-letter

::before

::after

下面我们就来分别讲讲它们。

::first-line 和 ::first-letter 是比较类似的伪元素，其中一个表示元素的第一行，一个表示元素的第一个字母。

我们可以看一个示例：

<p>This is a somewhat long HTML

paragraph that will be broken into several

lines. The first line will be identified

by a fictional tag sequence. The other lines

will be treated as ordinary lines in the

paragraph.</p>

p::first-line {

    text-transform: uppercase

}

这一段代码把段落的第一行字母变为大写。注意这里的第一行指的是排版后显示的第一行，跟 HTML 代码中的换行无关。

::first-letter 则指第一个字母。首字母变大并向左浮动是一个非常常见的排版方式。

<p>This is a somewhat long HTML

paragraph that will be broken into several

lines. The first line will be identified

by a fictional tag sequence. The other lines

will be treated as ordinary lines in the

paragraph.</p>

p::first-letter {

    text-transform: uppercase;

    font-size:2em;

    float:left;

}

虽然听上去很简单，但是实际上，我们遇到的 HTML 结构要更为复杂，一旦元素中不是纯文本，规则就变得复杂了。

CSS 标准规定了 first-line 必须出现在最内层的块级元素之内。因此，我们考虑以下代码。

<div>

  <p id=a>First paragraph</p>

  <p>Second paragraph</p>

</div>

div>p#a {

    color:green;

}

div::first-line {

    color:blue;

}

这段代码最终结果第一行是蓝色，因为 p 是块级元素，所以伪元素出现在块级元素之内，所以内层的 color 覆盖了外层的 color 属性。

如果我们把 p 换成 span，结果就是相反的。

<div>

  <span id=a>First paragraph</span><br/>

  <span>Second paragraph</span>

</div>

div>span#a {

    color:green;

}

div::first-line {

    color:blue;

}

这段代码的最终结果是绿色，这说明伪元素在 span 之外。

::first-letter 的行为又有所不同，它的位置在所有标签之内，我们把前面的代码换成::first-letter。

<div>

  <span id=a>First paragraph</span><br/>

  <span>Second paragraph</span>

</div>

div>span#a {

    color:green;

}

div::first-letter {

    color:blue;

}

执行这段代码，我们可以看到，首字母变成了蓝色，这说明伪元素出现在 span 之内。

CSS 标准只要求 ::first-line 和 ::first-letter 实现有限的几个 CSS 属性，都是文本相关，这些属性是下面这些。

接下来我们说说 ::before 和 ::after 伪元素。

这两个伪元素跟前面两个不同的是，它不是把已有的内容套上一个元素，而是真正的无中生有，造出一个元素。

::before 表示在元素内容之前插入一个虚拟的元素，::after 则表示在元素内容之后插入。

这两个伪元素所在的 CSS 规则必须指定 content 属性才会生效，我们看下例子：

<p class="special">I'm real element</p>

p.special::before {

    display: block;

    content: "pseudo! ";

}

这里要注意一点，::before 和 ::after 还支持 content 为 counter，如：

<p class="special">I'm real element</p>

p.special::before {

    display: block;

    content: counter(chapno, upper-roman) ". ";

}

这对于实现一些列表样式是非常有用的。

::before 和 ::after 中支持所有的 CSS 属性。实际开发中，这两个伪元素非常有用，有了这两个伪元素，一些修饰性元素，可以使用纯粹的 CSS 代码添加进去，这能够很好地保持 HTML 代码中的语义，既完成了显示效果，又不会让 DOM 中出现很多无语义的空元素。

总结

这一课，我们讲了 CSS 选择器的三种机制：选择器的组合、选择器优先级、以及伪元素。

在选择器组合这一部分，我们讲到了，选择器的连接方式像四则运算一样有优先级，

第一优先级是无连接符号；第二优先级是：“空格”“~”“+”“>”“||”；第三优先级是“,”。

然后我们又介绍了选择器优先级的计算方式。

最后我们为大家介绍了伪元素，我们逐次讲解了

::first-line

::first-letter

::before

::after

四种伪元素。伪元素的语法跟伪类相似，但是实际产生的效果是把不存在的元素硬选出来。这一点就与伪类不太一样了。

结合上一节课我们讲的简单选择器，对它们灵活运用，就能够满足大部分 CSS 的使用场景的需求了。

最后，留给你一个问题，你所在的团队，如何规定 CSS 选择器的编写规范？你觉得它好吗？

猜你喜欢

23 | HTML链接：除了a标签，还有哪些标签叫链接？

你好，我是 winter（加微信：642945106 发送“赠送”领取赠送精品课程 发数字“2”获取众筹列表。）。

在前面的课程中，我讲到了 HTML 的语义和元信息标签，今天这一课，我们来讲另一类 HTML 元素：链接。

链接这种元素可以说是占据了整个互联网。也正是因为无处不在的超链接，才让我们的万维网如此繁荣。没有了超链接的 HTML，最多可以称为富文本，没法称作超文本（hyper text）。

我想，作为互联网从业者，我们一定对链接都非常熟悉了。链接能够帮助我们从一个网页跳转到另一个网页。

不过，除了肉眼可见的这些链接，其实 HTML 里面还规定了一些不可见链接的类型，这节课，我就来给你介绍链接家族的全员，让你对它们有一个完整的认识。

链接是 HTML 中的一种机制，它是 HTML 文档和其它文档或者资源的连接关系，在 HTML 中，链接有两种类型。一种是超链接型标签，一种是外部资源链接。

链接的家族中有 a 标签、area 标签和 link 标签。今天，我会逐一对它们进行介绍。

link 标签

提到链接，我们都知道 a 标签可以成为超链接，但是我们今天的内容，要从一个大家不太熟悉的标签开始，也就是 link 标签。

我们已经介绍过元信息类标签。实际上，我们并没有介绍完全，有些 link 标签也是元信息类标签的一种。

我们已经讲过，HTML 标准并没有规定浏览器如何使用元信息，我们还讲到了元信息中有不少是被设计成“无需被浏览器识别，而是专门用于搜索引擎看的”。

link 标签也是元信息的一种，在很多时候，它也是不会对浏览器产生任何效果的，这也是很多人会忽略 link 标签学习的原因。

link 标签会生成一个链接，它可能生成超链接，也可能生成外部资源链接。

一些 link 标签会生成超链接，这些超链接又不会像 a 标签那样显示在网页中。这就是超链接型的 link 标签。

这意味着多数浏览器中，这些 link 标签不产生任何作用。但是，这些 link 标签能够被搜索引擎和一些浏览器插件识别，从而产生关键性作用。

比如，到页面 RSS 的 link 标签，能够被浏览器的 RSS 订阅插件识别，提示用户当前页面是可以 RSS 订阅的。

另外一些 link 标签则会把外部的资源链接到文档中，也就是说，会实际下载这些资源，并且做出一些处理，比如我们常见的用 link 标签引入样式表。

除了元信息的用法之外，多数外部资源型的 link 标签还能够被放在 body 中使用，从而起到把外部资源链接进文档的作用。

link 标签的链接类型主要通过 rel 属性来区分，在本篇文章中，我们提到 xx 型 link 即表示属性 rel 为 xx 的 link，其代码类似下面：

<link rel="xx" ...>

下面我们先来看看超链接型 link 标签。

超链接类 link 标签

超链接型 link 标签是一种被动型链接，在用户不操作的情况下，它们不会被主动下载。

link 标签具有特定的 rel 属性，会成为特定类型的 link 标签。产生超链接的 link 标签包括：具有 rel=“canonical” 的 link、具有 rel="alternate"的 link、具有 rel=“prev” rel="next"的 link 等等。

canonical 型 link

这种 link 的代码写法是这样：

<link rel="canonical" href="...">

这个标签提示页面它的主 URL，在网站中常常有多个 URL 指向同一页面的情况，搜索引擎访问这类页面时会去掉重复的页面，这个 link 会提示搜索引擎保留哪一个 URL。

alternate 型 link

这种 link 的代码写法是这样：

<link rel="alternate" href="...">

这个标签提示页面它的变形形式，这个所谓的变形可能是当前页面内容的不同格式、不同语言或者为不同的设备设计的版本，这种 link 通常也是提供给搜索引擎来使用的。

alternate 型的 link 的一个典型应用场景是，页面提供 rss 订阅时，可以用这样的 link 来引入：

<link rel="alternate" type="application/rss+xml" title="RSS" href="...">

除了搜索引擎外，很多浏览器插件都能识别这样的 link。

prev 型 link 和 next 型 link

在互联网应用中，很多网页都属于一个序列，比如分页浏览的场景，或者图片展示的场景，每个网页是序列中的一个项。

这种时候，就适合使用 prev 和 next 型的 link 标签，来告诉搜索引擎或者浏览器它的前一项和后一项，这有助于页面的批量展示。

因为 next 型 link 告诉浏览器“这是很可能访问的下一个页面”，HTML 标准还建议对 next 型 link 做预处理，在本课后面的内容，我们会讲到预处理类的 link。

其它超链接类的 link

其它超链接类 link 标签都表示一个跟当前文档相关联的信息，可以把这样的 link 标签视为一种带链接功能的 meta 标签。

rel=“author” 链接到本页面的作者，一般是 mailto: 协议

rel=“help” 链接到本页面的帮助页

rel=“license” 链接到本页面的版权信息页

rel=“search” 链接到本页面的搜索页面（一般是站内提供搜索时使用）

到这里，我们已经讲完了所有的超链接类的 link 标签用法了。接下来我们讲讲外部资源类 link 标签。

外部资源类 link 标签

外部资源型 link 标签会被主动下载，并且根据 rel 类型做不同的处理。外部资源型的标签包括：具有 icon 型的 link、预处理类 link、modulepreload 型的 link、stylesheet、pingback。下面我们来一一介绍它们。

icon 型 link

这类链接表示页面的 icon。多数浏览器会读取 icon 型 link，并且把页面的 icon 展示出来。

icon 型 link 是唯一一个外部资源类的元信息 link，其它元信息类 link 都是超链接，这意味着，icon 型 link 中的图标地址默认会被浏览器下载和使用。

如果没有指定这样的 link，多数浏览器会使用域名根目录下的 favicon.ico，即使它并不存在，所以从性能的角度考虑，建议一定要保证页面中有 icon 型的 link。

只有 icon 型 link 有有效的 sizes 属性，HTML 标准允许一个页面出现多个 icon 型 link，并且用 sizes 指定它适合的 icon 尺寸。

预处理类 link

我们都知道，导航到一个网站需要经过 dns 查询域名、建立连接、传输数据、加载进内存和渲染等一系列的步骤。

预处理类 link 标签就是允许我们控制浏览器，提前针对一些资源去做这些操作，以提高性能（当然如果你乱用的话，性能反而更差）。

下面我来列一下这些 link 类型：

dns-prefetch 型 link 提前对一个域名做 dns 查询，这样的 link 里面的 href 实际上只有域名有意义。

preconnect 型 link 提前对一个服务器建立 tcp 连接。

prefetch 型 link 提前取 href 指定的 url 的内容。

preload 型 link 提前加载 href 指定的 url。

prerender 型 link 提前渲染 href 指定的 url。

modulepreload 型的 link

modulepreload 型 link 的作用是预先加载一个 JavaScript 的模块。这可以保证 JS 模块不必等到执行时才加载。

这里的所谓加载，是指完成下载并放入内存，并不会执行对应的 JavaScript。

<link rel="modulepreload" href="app.js">

<link rel="modulepreload" href="helpers.js">

<link rel="modulepreload" href="irc.js">

<link rel="modulepreload" href="fog-machine.js">

<script type="module" src="app.js">

这个例子来自 HTML 标准，我们假设 app.js 中有 import “irc” 和 import “fog-machine”, 而 irc.js 中有 import “helpers”。这段代码使用 moduleload 型 link 来预加载了四个 js 模块。

尽管，单独使用 script 标签引用 app.js 也可以正常工作，但是我们通过加入对四个 JS 文件的 link 标签，使得四个 JS 文件有机会被并行地下载，这样提高了性能。

stylesheet 型 link

样式表大概是所有人最熟悉的 link 标签用法了。它的样子是下面这样的。

<link rel="stylesheet" href="xxx.css" type="text/css">

基本用法是从一个 CSS 文件创建一个样式表。这里 type 属性可以没有，如果有，必须是"text/css"才会生效。

rel 前可以加上 alternate，成为 rel=“alternate stylesheet”，此时必须再指定 title 属性。

这样可以为页面创建一份变体样式，一些浏览器，如 Firefox 3.0，支持从浏览器菜单中切换这些样式，当然了，大部分浏览器不支持这个功能，所以仅仅从语义的角度了解一下这种用法即可。

pingback 型 link

这样的 link 表示本网页被引用时，应该使用的 pingback 地址，这个机制是一份独立的标准，遵守 pingback 协议的网站在引用本页面时，会向这个 pingback url 发送一个消息。

以上就是 link 标签的所有用法了。接下来我们来介绍一下最熟悉的 a 标签，当然了，也可能你学过了本节课以后，觉得自己其实也没那么熟悉。

a 标签

a 标签是“anchor”的缩写，它是锚点的意思，所谓锚点，实际上也是一种比喻的用法，古代船舶用锚来固定自己的位置，避免停泊时被海浪冲走，所以 anchor 标签的意思也是标识文档中的特定位置。

a 标签其实同时充当了链接和目标点的角色，当 a 标签有 href 属性时，它是链接，当它有 name 时，它是链接的目标。

具有 href 的 a 标签跟一些 link 一样，会产生超链接，也就是在用户不操作的情况下，它们不会被主动下载的被动型链接。

重点的内容是，a 标签也可以有 rel 属性，我们来简单了解一下，首先是跟 link 相同的一些 rel，包括下面的几种。

alternate

author

help

license

next

prev

search

这些跟 link 语义完全一致，不同的是，a 标签产生的链接是会实际显示在网页中的，而 link 标签仅仅是元信息。

除了这些之外，a 标签独有的 rel 类型：

tag 表示本网页所属的标签；

bookmark 到上级章节的链接。

a 标签还有一些辅助的 rel 类型，用于提示浏览器或者搜索引擎做一些处理：

nofollow 此链接不会被搜索引擎索引；

noopener 此链接打开的网页无法使用 opener 来获得当前页面的窗口；

noreferrer 此链接打开的网页无法使用 referrer 来获得当前页面的 url；

opener 打开的网页可以使用 window.opener 来访问当前页面的 window 对象，这是 a 标签的默认行为。

a 标签基本解决了在页面中插入文字型和整张图片超链接的需要，但是如果我们想要在图片的某个区域产生超链接，那么就要用到另一种标签了——area 标签。

area 标签

area 标签与 a 标签非常相似，不同的是，它不是文本型的链接，而是区域型的链接。

area 标签支持的 rel 与 a 完全一样，这里就不多说了。

area 是整个 html 规则中唯一支持非矩形热区的标签，它的 shape 属性支持三种类型。

圆形：circle 或者 circ，coords 支持三个值，分别表示中心点的 x,y 坐标和圆形半径 r。

矩形：rect 或者 rectangle，coords 支持两个值，分别表示两个对角顶点 x1，y1 和 x2，y2。

多边形：poly 或者 polygon，coords 至少包括 6 个值，表示多边形的各个顶点。

因为 area 设计的时间较早，所以不支持含有各种曲线的路径，但是它也是唯一一个支持了非矩形触发区域的元素，所以，对于一些效果而言，area 是必不可少的。

area 必须跟 img 和 map 标签配合使用。使用示例如下（例子来自 html 标准）。

<p>

 Please select a shape:

 <img src="shapes.png" usemap="#shapes"

      alt="Four shapes are available: a red hollow box, a green circle, a blue triangle, and a yellow four-pointed star.">

 <map name="shapes">

  <area shape=rect coords="50,50,100,100"> <!-- the hole in the red box -->

  <area shape=rect coords="25,25,125,125" href="red.html" alt="Red box.">

  <area shape=circle coords="200,75,50" href="green.html" alt="Green circle.">

  <area shape=poly coords="325,25,262,125,388,125" href="blue.html" alt="Blue triangle.">

  <area shape=poly coords="450,25,435,60,400,75,435,90,450,125,465,90,500,75,465,60"

        href="yellow.html" alt="Yellow star.">

 </map>

</p>

这个例子展示了在一张图片上画热区并且产生链接，分别使用了矩形、圆形和多边形三种 area。

结语

本节课我们介绍了几种链接类型。在 HTML 中，链接有两种类型。一种是超链接型标签，一种是外部资源链接。

我们逐次讲到了 link 标签、a 标签和 area 标签，link 标签一般用于看不见的链接，它可能产生超链接或者外部资源链接，a 和 area 一般用于页面上显示的链接，它们只能产生超链接。

最后，留给你一个思考问题，你的工作中，是使用过哪些类型的 link 标签的呢？

猜你喜欢

24 | CSS排版：从毕升开始，我们就开始用正常流了

你好，我是 winter。今天我们来聊聊 CSS 的正常流。

我想，在 CSS 中，大家最讨厌的大概就是排版部分了。因为早年的 CSS 设计上不能够很好地支持软件排版需求，导致大家需要使用很多黑科技，让很多新人望而却步。

现在 CSS 提供了很多种排版方式，我们有很多选项可以选择自己适合的那一种，然而，正常流却是我们绕不开的一种排版。

我们能够在网上看到关于正常流的各种资料，比如：块级格式化上下文、margin 折叠等等……这一系列的概念光是听起来就令人非常头痛。

所以我相信很多同学一定会奇怪：正常流到底正常在哪里。事实上，我认为正常流本身是简单和符合直觉的东西。

我们之所以会觉得它奇怪，是因为如果我们从严苛的 CSS 标准角度去理解正常流，规定排版的算法，就需要引入上述那些复杂的概念。但是，如果我们单纯地从感性认知的层面去理解正常流，它其实是简单的。

下面，就让我们先抛弃掉所有的已知概念，从感性认知的角度出发，一起去理解一下正常流。

正常流的行为

首先，我们先从词源来讲一讲排版这件事。

在毕昇发明活字印刷之前，排版这项工作是不存在的，相应的操作叫做“雕版”。人们要想印刷书籍，就需要依靠雕版工人去手工雕刻印版。

活字印刷的出现，将排版这个词引入进来，排版是活字印刷的 15 道工序之一，不论是古代的木质活字印刷，还是近代的铅质活字印刷，排版的过程是由排版工人一个字一个字从字架捡出，再排入版框中。实际上，这个过程就是一个流式处理的过程。

从古代活字印刷开始，到现代的出版行业，再到今天的 Web，排版过程其实并没有什么本质的变化，只不过，今天在我们的 CSS 中，排版需要处理的内容，不再是简单的大小相同的木字或者铅字，而是有着不同字体和字号的富文本，以及插入在富文本中大小不等的盒。

并且，在这些过程中，都会有一个正常流的存在。那么，正常流是什么样的呢？

我们可以用一句话来描述正常流的排版行为，那就是：依次排列，排不下了换行。这个操作很简单吧，我想，任何一个不懂排版的人都会将其作为排版时的第一反应。

理解了正常流的基本概念，剩下的功能只需要在它的基础上延伸一下就好。

在正常流基础上，我们有 float 相关规则，使得一些盒占据了正常流需要的空间，我们可以把 float 理解为“文字环绕”。

我们还有 vertical-align 相关规则规定了如何在垂直方向对齐盒。vertical-align 相关规则看起来复杂，但是实际上，基线、文字顶 / 底、行顶 / 底都是我们正常书写文字时需要用到的概念，只是我们平时不一定会总结它们。

下图展示了在不同的 vertical-align 设置时，盒与文字是如何混合排版的。为了方便你理解，我们用代码给大家标注了基线、文字顶 / 底、行顶 / 底等概念

（点击大图查看）

除此之外，margin 折叠是很多人非常不理解的一种设计，但是实际上我们可以把 margin 理解为“一个元素规定了自身周围至少需要的空间”，这样，我们就非常容易理解为什么 margin 需要折叠了。

正常流的原理

我们前面描述了正常流的行为，接下来我们要切换一下模式，用比较严谨的姿势来理解一下正常流。

在 CSS 标准中，规定了如何排布每一个文字或者盒的算法，这个算法依赖一个排版的“当前状态”，CSS 把这个当前状态称为“格式化上下文（formatting context）”。

我们可以认为排版过程是这样的：

格式化上下文 + 盒 / 文字 = 位置

formatting context + boxes/charater = positions

我们需要排版的盒，是分为块级盒和行内级盒的，所以排版需要分别为它们规定了块级格式化上下文和行内级格式化上下文。

与正常流一样，如果我们单纯地看格式化上下文，规则其实是非常简单的。

块级格式化上下文顺次排列元素：

行内级格式化上下文顺次排列元素：

注意，块级和行内级元素的排版，受文字书写方向的影响，这里我们讲上下左右只是为了方便你直观理解。

当我们要把正常流中的一个盒或者文字排版，需要分成三种情况处理。

当遇到块级盒：排入块级格式化上下文。

当遇到行内级盒或者文字：首先尝试排入行内级格式化上下文，如果排不下，那么创建一个行盒，先将行盒排版（行盒是块级，所以到第一种情况），行盒会创建一个行内级格式化上下文。

遇到 float 盒：把盒的顶部跟当前行内级上下文上边缘对齐，然后根据 float 的方向把盒的对应边缘对到块级格式化上下文的边缘，之后重排当前行盒。

我们以上讲的都是一个块级格式化上下文中的排版规则，实际上，页面中的布局没有那么简单，一些元素会在其内部创建新的块级格式化上下文，这些元素有：

浮动元素；

绝对定位元素；

非块级但仍能包含块级元素的容器（如 inline-blocks, table-cells, table-captions）；

块级的能包含块级元素的容器，且属性 overflow 不为 visible。

这里的最后一条比较绕，实际上，我个人喜欢用另一种思路去理解它：

自身为块级，且 overflow 为 visible 的块级元素容器，它的块级格式化上下文和外部的块级格式化上下文发生了融合，也就是说，如果不考虑盒模型相关的属性，这样的元素从排版的角度就好像根本不存在。

好了，到这里我们已经讲完了正常流的排版详细规则，但是理解规则仅仅是基础，我们还需要掌握一些技巧。

正常流的使用技巧

现在，我们就一起来动手用实际的例子来研究一下。我们今天来看看等分布局和自适应宽，从这两种经典布局问题入手，一起来探索一下正常流的使用技巧。

等分布局问题

横向等分布局是一个很常见的需求，按照一般的思路，我们可以使用百分比宽度来解决，我们参考以下代码：

<div class="outer">

    <div class="inner"></div>

    <div class="inner"></div>

    <div class="inner"></div>

</div>

.inner {

    width:33.33%;

    height:300px;

    display:inline-block;

    outline:solid 1px blue;

}

在这段 HTML 代码中，我们放了三个 div，用 CSS 给它们指定了百分比宽度，并且指定为 inline-block。

但是这段代码执行之后，效果跟我们预期不同，我们可以发现，每个 div 并非紧挨，中间有空白，这是因为我们为了代码格式加入的换行和空格被 HTML 当作空格文本，跟 inline 盒混排了的缘故。

解决方案是修改 HTML 代码，去掉空格和换行：

<div class="outer"><div class="inner"></div><div class="inner"></div><div class="inner"></div></div>

但是这样做影响了源代码的可读性，一个变通的方案是，改变 outer 中的字号为 0。

.inner {

    width:33.33%;

    height:300px;

    display:inline-block;

    outline:solid 1px blue;

    font-size:30px;

}

.outer {

    font-size:0;

}

在某些浏览器中，因为像素计算精度问题，还是会出现换行，我们给 outer 添加一个特定宽度：

.inner {

    width:33.33%;

    height:300px;

    display:inline-block;

    outline:solid 1px blue;

}

.outer {

    width:101px

}

这个代码在某些旧版本浏览器中会出现换行。为了保险起见，我们给最后一个 div 加上一个负的右 margin：

.outer {

    width:101px

}

.inner {

    width:33.33%;

    height:300px;

    display:inline-block;

    outline:solid 1px blue;

}

.inner:last-child {

    margin-right:-5px;

}

这样就可以解决旧版本浏览器的问题了。

除了使用 inline-block，float 也可以实现类似的效果，但是 float 元素只能做顶对齐，不如 inline-block 灵活。

自适应宽

我们再来说说自适应宽。在 IE6 统治浏览器市场的旧时代，自适应宽（一个元素固定宽度，另一个元素填满父容器剩余宽度）是个经典的布局问题，我们现在就看一下如何使用正常流来解决：

我们首先来看一下问题：

<div class="outer">

    <div class="fixed"></div>

    <div class="auto"></div>

</div>

.fixed {

    width:200px;

}

.fixed, .auto {

    height:300px;

    outline:solid 1px blue;

}

这里 fixed 这个 div 宽度已经被指定好，我们需要添加 css 代码尝试让.auto 填满剩余宽度。

使用正常流解决这个问题的思路是，利用负 margin：

.fixed {

    display:inline-block;

    vertical-align:top;

}

.auto {

    margin-left:-200px;

    width:100%;

    display:inline-block;

    vertical-align:top;

}

但是，这样做会导致 auto 中的内容位置不对，所以我们还需要使用 padding 把内容挤出来，最终完整代码如下：

.fixed {

    display:inline-block;

    vertical-align:top;

}

.auto {

    margin-left:-200px;

    padding-left:200px;

    box-sizing:border-box;

    width:100%;

    display:inline-block;

    vertical-align:top;

}

这样就给 auto 添加了 padding-left 和 box-sizing 两个属性。

总的来说，正常流布局主要是使用 inline-block 来作为内容的容器，利用块级格式化上下文的纵向排布和行内级格式化上下文的横向排布来完成布局的，我们需要根据需求的横向和纵向排布要求，来选择元素的 display 属性。

结语

这次的文章中，我们一起学习了正常流，我们可以用一句话来描述正常流的排版行为，那就是：依次排列，排不下了换行。这也是理解它最简单最源头的方式。

我们将正常流的知识分成了三个部分。

正常流的行为部分，我们从一些感性认知出发，帮助你从思路和源头上理解正常流的行为。

正常流的原理部分，我用更严格的描述方式，给你讲解了 CSS 标准中规定的正常流排版逻辑。

最后的正常流应用部分，我以两个经典布局问题等分布局和自适应宽为例，为你讲解了正常流实际使用的一些技巧。

最后，留给你一个思考题：用 JavaScript 写一个仅包含 inline-block 的正常流布局算法。你写好的话，可以留言给我，我们一起讨论。

猜你喜欢

33 | HTML替换型元素：为什么link一个CSS要用href，而引入js要用src呢？

你好，我是 winter。我们今天来讲讲替换型元素。

我们都知道一个常识，一个网页，它是由多个文件构成的，我们在之前的课程中，已经学过了一种引入文件的方案：链接。

这节课我们要讲的替换型元素，就是另一种引入文件的方式了。替换型元素是把文件的内容引入，替换掉自身位置的一类标签。

我们首先来看一种比较熟悉的标签：script 标签。

script

我们之所以选择先讲解 script 标签，是因为 script 标签是为数不多的既可以作为替换型标签，又可以不作为替换型标签的元素。

我们先来看看 script 标签的两种用法：

<script type="text/javascript">

console.log("Hello world!");

</script>

<script type="text/javascript" src="my.js"></script>

这个例子中，我们展示了两种 script 标签的写法，一种是直接把脚本代码写在 script 标签之间，另一种是把代码放到独立的 js 文件中，用 src 属性引入。

这两种写法是等效的。我想这种等效性可以帮助你理解替换型元素的“替换”是怎么一回事。

这里我们就可以回答标题中的问题了：凡是替换型元素，都是使用 src 属性来引用文件的，而我们之前的课程中已经讲过，链接型元素是使用 href 标签的。

虽然我不知道当初是怎么设计的，但是 style 标签并非替换型元素，不能使用 src 属性，这样，我们用 link 标签引入 CSS 文件，当然就是用 href 标签啦。

接下来我们再看看别的替换型元素，先来了解一下 img 标签。

img

毫无疑问我们最熟悉的替换型标签就是 img 标签了，几乎每个前端都会日常使用 img 标签。

img 标签的作用是引入一张图片。这个标签是没有办法像 script 标签那样作为非替换型标签来使用的，它必须有 src 属性才有意义。

如果一定不想要引入独立文件，可以使用 data uri，我们来看个实际的例子：

 <img src='data:image/svg+xml;charset=utf8,<svg version="1.1" xmlns="http://www.w3.org/2000/svg"><rect width="300" height="100" style="fill:rgb(0,0,255);stroke-width:1;stroke:rgb(0,0,0)"/></svg>'/>

这个例子中我们使用了 data uri 作为图片的 src，这样，并没有产生独立的文件，客观上做到了和内联相同的结果，这是一个常用的技巧。

img 标签可以使用 width 和 height 指定宽度和高度。也可以只指定其中之一。我们看个例子：

 <img src='data:image/svg+xml;charset=utf8,<svg width="600" height="400" version="1.1"

xmlns="http://www.w3.org/2000/svg"><ellipse cx="300" cy="150" rx="200" ry="80"

style="fill:rgb(200,100,50);

stroke:rgb(0,0,100);stroke-width:2"/></svg>' width="100"/>

这个例子中，为了方便你理解，我们把图片换成了椭圆，我们可以看到，当我们指定了宽度后，图片被等比例缩放了。这个特性非常重要，适用于那种我们既要限制图片尺寸，又要保持图片比例的场景。

如果从性能的角度考虑，建议你同时给出图片的宽高，因为替换型元素加载完文件后，如果尺寸发生变换，会触发重排版（这个概念我们在浏览器原理部分已经讲过，可以复习一下）。

此处要重点提到一个属性，alt 属性，这个属性很难被普通用户感知，对于视障用户非常重要，可以毫不夸张地讲，给 img 加上 alt 属性，已经做完了可访问性的一半。

img 标签还有一组重要的属性，那就是 srcset 和 sizes，它们是 src 属性的升级版（所以我们前面讲 img 标签必须有 src 属性，这是不严谨的说法）。

这两个属性的作用是在不同的屏幕大小和特性下，使用不同的图片源。下面一个例子也来自 MDN，它展示了 srcset 和 sizes 的用法

<img srcset="elva-fairy-320w.jpg 320w,

             elva-fairy-480w.jpg 480w,

             elva-fairy-800w.jpg 800w"

     sizes="(max-width: 320px) 280px,

            (max-width: 480px) 440px,

            800px"

     src="elva-fairy-800w.jpg" alt="Elva dressed as a fairy">

srcset 提供了根据屏幕条件选取图片的能力，但是其实更好的做法，是使用 picture 元素。

picture

picture 元素可以根据屏幕的条件为其中的 img 元素提供不同的源，它的基本用法如下：

<picture>

  <source srcset="image-wide.png" media="(min-width: 600px)">

  <img src="image-narrow.png">

</picture>

picture 元素的设计跟 audio 和 video 保持了一致（稍后我会为你讲解这两个元素），它跟 img 搭配 srcset 和 sizes 不同，它使用 source 元素来指定图片源，并且支持多个。

这里的 media 属性是 media query，跟 CSS 的 @media 规则一致。

video

在 HTML5 早期的设计中，video 标签跟 img 标签类似，也是使用 src 属性来引入源文件的，不过，我想应该是考虑到了各家浏览器支持的视频格式不同，现在的 video 标签跟 picture 元素一样，也是提倡使用 source 的。

下面例子是一个古典的 video 用法：

<video controls="controls" src="movie.ogg">

</video>

这个例子中的代码用 src 来指定视频的源文件。但是因为一些历史原因，浏览器对视频的编码格式兼容问题分成了几个派系，这样，对于一些兼容性要求高的网站，我们使用单一的视频格式是不合适的。

现在的 video 标签可以使用 source 标签来指定接入多个视频源。

<video controls="controls" >

  <source src="movie.webm" type="video/webm" >

  <source src="movie.ogg" type="video/ogg" >

  <source src="movie.mp4" type="video/mp4">

  You browser does not support video.

</video>

从这个例子中，我们可以看到，source 标签除了支持 media 之外，还可以使用 type 来区分源文件的使用场景。

video 标签的内容默认会被当做不支持 video 的浏览器显示的内容吗，因此，如果要支持更古老的浏览器，还可以在其中加入 object 或者 embed 标签，这里就不详细展开了。

video 中还支持一种标签：track。

track 是一种播放时序相关的标签，它最常见的用途就是字幕。track 标签中，必须使用 srclang 来指定语言，此外，track 具有 kind 属性，共有五种。

subtitles：就是字幕了，不一定是翻译，也可能是补充性说明

captions：报幕内容，可能包含演职员表等元信息，适合听障人士或者没有打开声音的人了解音频内容

descriptions：视频描述信息，适合视障人士或者没有视频播放功能的终端打开视频时了解视频内尔用

chapters：用于浏览器视频内容。

metadata：给代码提供的元信息，对普通用户不可见。

一个完整的 video 标签可能会包含多种 track 和多个 source，这些共同构成了一个视频播放所需的全部信息。

audio

接下来我们来讲讲 audio，跟 picture 和 video 两种标签一样，audio 也可以使用 source 元素来指定源文件。我们看一下例子：

<audio controls>

  <source src="song.mp3" type="audio/mpeg">

  <source src="song.ogg" type="audio/ogg">

  <p>You browser does not support audio.</p>

</audio>

但比起 video，audio 元素的历史问题并不严重，所以使用 src 也是没有问题的。

iframe

最后我们来讲一下 iframe，这个标签能够嵌入一个完整的网页。

不过，在移动端，iframe 受到了相当多的限制，它无法指定大小，里面的内容会被完全平铺到父级页面上。

同时很多网页也会通过 http 协议头禁止自己被放入 iframe 中。

iframe 标签也是各种安全问题的重灾区。opener、window.name、甚至 css 的 opacity 都是黑客可以利用的漏洞。

因此，在 2019 年，当下这个时间点，任何情况下我都不推荐在实际开发中用以前的 iframe。

当然，不推荐使用是一回事，因为没人能保证不遇到历史代码，我们还是应该了解一下 iframe 的基本用法：

<iframe src="http://time.geekbang.org"></iframe>

这个例子展示了古典的 iframe 用法。

在新标准中，为 iframe 加入了 sandbox 模式和 srcdoc 属性，这样，给 iframe 带来了一定的新场景。我们来看看例子：

<iframe sandbox srcdoc="<p>Yeah, you can see it <a href="/gallery?mode=cover&amp;amp;page=1">in my gallery</a>."></iframe>

这个例子中，使用 srcdoc 属性创建了一个新的文档，嵌入在 iframe 中展示，并且使用了 sandbox 来隔离。

这样，这个 iframe 就不涉及任何跨域问题了。

总结

这节课，我们又认识了一组 HTML 元素：替换型元素。它们的特点是，引入一个外部资源来进入页面，替换掉自身的位置。

我们通过对 script、img、picture、audio、video、iframe 几个标签的讲解，了解了不同的资源引入方式：

src 属性；

srcset 属性；

source 标签；

srcdoc 属性。

这中间，我们也介绍了一些小技巧，比如 src 属性的好朋友——data uri，这在实际开发中非常有用。

最后，留给大家一个小问题，请查资料总结一下，在多数现代浏览器兼容的范围内，src 属性支持哪些协议的 uri（如 http 和我们提到的 data）。

34 | HTML小实验：用代码分析HTML标准

你好，我是 winter。

前面的课程中，我们已经讲解了大部分的 HTML 标签。

然而，为了突出重点，我们还是会忽略一些标签类型。比如表单类标签和表格类标签，我认为只有少数前端工程师用过，比如我在整个手机淘宝的工作生涯中，一次表格类标签都没有用到，表单类则只用过 input，也只有几次。

那么，剩下的标签我们怎么样去了解它们呢？当然是查阅 HTML 标准。

由于阅读标准有一定门槛，需要了解一些机制，这节课，我为你设计了一个小实验，用 JavaScript 代码去抽取标准中我们需要的信息。

HTML 标准

我们采用 WHATWG 的 living standard 标准，我们先来看看标准是如何描述一个标签的，这里我们看到，有下面这些内容。

Categories:

    Flow content.

    Phrasing content.

    Embedded content.

    If the element has a controls attribute: Interactive content.

    Palpable content.

Contexts in which this element can be used:

    Where embedded content is expected.

Content model:

    If the element has a src attribute: zero or more track elements, then transparent, but with no media element descendants.

    If the element does not have a src attribute: zero or more source elements, then zero or more track elements, then transparent, but with no media element descendants.

Tag omission in text/html:

    Neither tag is omissible.

Content attributes:

    Global attributes

    src — Address of the resource

    crossorigin — How the element handles crossorigin requests

    poster — Poster frame to show prior to video playback

    preload — Hints how much buffering the media resource will likely need

    autoplay — Hint that the media resource can be started automatically when the page is loaded

    playsinline — Encourage the user agent to display video content within the element's playback area

    loop — Whether to loop the media resource

    muted — Whether to mute the media resource by default

    controls — Show user agent controls

    width — Horizontal dimension

    height — Vertical dimension

DOM interface:

    [Exposed=Window, HTMLConstructor]

    interface HTMLVideoElement : HTMLMediaElement {

      [CEReactions] attribute unsigned long width;

      [CEReactions] attribute unsigned long height;

      readonly attribute unsigned long videoWidth;

      readonly attribute unsigned long videoHeight;

      [CEReactions] attribute USVString poster;

      [CEReactions] attribute boolean playsInline;

    };

我们看到，这里的描述分为 6 个部分，有下面这些内容。

Categories：标签所属的分类。

Contexts in which this element can be used：标签能够用在哪里。

Content model：标签的内容模型。

Tag omission in text/html：标签是否可以省略。

Content attributes：内容属性。

DOM interface：用 WebIDL 定义的元素类型接口。

这一节课，我们关注一下 Categories、Contexts in which this element can be used、Content model 这几个部分。我会带你从标准中抓取数据，做一个小工具，用来检查 X 标签是否能放入 Y 标签内。

代码角度分析 HTML 标准

HTML 标准描述用词非常的严谨，这给我们抓取数据带来了巨大的方便，首先，我们打开单页面版 HTML 标准 https://html.spec.whatwg.org/

在这个页面上，我们执行一下以下代码：

Array.prototype.map.call(document.querySelectorAll(".element"), e=>e.innerText);

这样我们就得到了所有元素的定义了，现在有 107 个元素。

不过，比较尴尬的是，这些文本中并不包含元素名，我们只好从 id 属性中获取，最后代码类似这样：

var elementDefinations = Array.prototype.map.call(document.querySelectorAll(".element"), e => ({

  text:e.innerText,

  name:e.childNodes[0].childNodes[0].id.match(/the\-([\s\S]+)\-element:/)?RegExp.$1:null}));

接下来我们用代码理解一下这些文本。首先我们来分析一下这些文本，它分成了 6 个部分，而且顺序非常固定，这样，我们可以用 JavaScript 的正则表达式匹配来拆分六个字段。

我们这个小实验的目标是计算元素之间的包含关系，因此，我们先关心一下 categories 和 contentModel 两个字段。

for(let defination of elementDefinations) {

  console.log(defination.name + ":")

  let categories = defination.text.match(/Categories:\n([\s\S]+)\nContexts in which this element can be used:/)[1].split("\n");

  for(let category of categories) {

      console.log(category);

  }

/*

  let contentModel = defination.text.match(/Content model:\n([\s\S]+)\nTag omission in text\/html:/)[1].split("\n");

  for(let line of contentModel)

    console.log(line);

*/

}

接下来我们来处理 category。

首先 category 的写法中，最基本的就是直接描述了 category 的句子，我们把这些不带任何条件的 category 先保存起来，然后打印出来其它的描述看看：

for(let defination of elementDefinations) {

  //console.log(defination.name + ":")

  let categories = defination.text.match(/Categories:\n([\s\S]+)\nContexts in which this element can be used:/)[1].split("\n");

  defination.categories = [];

  for(let category of categories) {

    if(category.match(/^([^ ]+) content./))

      defination.categories.push(RegExp.$1);

    else

      console.log(category)

  }

/*

  let contentModel = defination.text.match(/Content model:\n([\s\S]+)\nTag omission in text\/html:/)[1].split("\n");

  for(let line of contentModel)

    console.log(line);

*/

}

这里我们要处理的第一个逻辑是带 if 的情况。

然后我们来看看剩下的情况：

 None.

 Sectioning root.

 None.

 Sectioning root.

 None.

 Form-associated element.

 Listed and submittable form-associated element.

 None.

 Sectioning root.

 None.

 If the type attribute is not in the Hidden state: Listed, labelable, submittable, resettable, and autocapitalize-inheriting form-associated element.

 If the type attribute is in the Hidden state: Listed, submittable, resettable, and autocapitalize-inheriting form-associated element.

 Listed, labelable, submittable, and autocapitalize-inheriting form-associated element.

 Listed, labelable, submittable, resettable, and autocapitalize-inheriting form-associated element.

 None.

 Listed, labelable, submittable, resettable, and autocapitalize-inheriting form-associated element.

 Listed, labelable, resettable, and autocapitalize-inheriting form-associated element.

 Labelable element.

 Sectioning root.

 Listed and autocapitalize-inheriting form-associated element.

 None.

 Sectioning root.

 None.

 Sectioning root.

 Script-supporting element.

这里出现了几个概念：

None

Sectioning root

Form-associated element

Labelable element

Script-supporting element

如果我们要真正完美地实现元素分类，就必须要在代码中加入正则表达式来解析这些规则，这里作为今天的课后问题，留给你自己完成。

接下来我们看看 Content Model，我们照例先处理掉最简单点的部分，就是带分类的内容模型：

for(let defination of elementDefinations) {

  //console.log(defination.name + ":")

  let categories = defination.text.match(/Categories:\n([\s\S]+)\nContexts in which this element can be used:/)[1].split("\n");

  defination.contentModel = [];

  let contentModel = defination.text.match(/Content model:\n([\s\S]+)\nTag omission in text\/html:/)[1].split("\n");

  for(let line of contentModel)

    if(line.match(/^([^ ]+) content./))

      defination.contentModel.push(RegExp.$1);

    else

      console.log(line)

}

好了，我们照例看看剩下了什么：

 A head element followed by a body element.

 If the document is an iframe srcdoc document or if title information is available from a higher-level protocol: Zero or more elements of metadata content, of which no more than one is a title element and no more than one is a base element.

 Otherwise: One or more elements of metadata content, of which exactly one is a title element and no more than one is a base element.

 Text that is not inter-element whitespace.

 Nothing.

 Text that gives a conformant style sheet.

 One or more h1, h2, h3, h4, h5, h6 elements, optionally intermixed with script-supporting elements.

 Nothing.

 Zero or more li and script-supporting elements.

 Either: Zero or more groups each consisting of one or more dt elements followed by one or more dd elements, optionally intermixed with script-supporting elements.

 Or: One or more div elements, optionally intermixed with script-supporting elements.

 Either: one figcaption element followed by flow content.

 Or: flow content followed by one figcaption element.

 Or: flow content.

 If the element is a child of a dl element: one or more dt elements followed by one or more dd elements, optionally intermixed with script-supporting elements.

 If the element is not a child of a dl element: flow content.

 Transparent, but there must be no interactive content or a element descendants.

 See prose.

 Text.

 If the element has a datetime attribute: Phrasing content.

 Otherwise: Text, but must match requirements described in prose below.

 Nothing.

 Transparent.

 Zero or more source elements, followed by one img element, optionally intermixed with script-supporting elements.

 Nothing.

 Zero or more param elements, then, transparent.

 Nothing.

 If the element has a src attribute: zero or more track elements, then transparent, but with no media element descendants.

 If the element does not have a src attribute: zero or more source elements, then zero or more track elements, then transparent, but with no media element descendants.

 If the element has a src attribute: zero or more track elements, then transparent, but with no media element descendants.

 If the element does not have a src attribute: zero or more source elements, then zero or more track elements, then transparent, but with no media element descendants.

 Nothing.

 Transparent.

 Nothing.

 In this order: optionally a caption element, followed by zero or more colgroup elements, followed optionally by a thead element, followed by either zero or more tbody elements or one or more tr elements, followed optionally by a tfoot element, optionally intermixed with one or more script-supporting elements.

 If the span attribute is present: Nothing.

 If the span attribute is absent: Zero or more col and template elements.

 Nothing.

 Zero or more tr and script-supporting elements.

 Zero or more td, th, and script-supporting elements.

 Nothing.

 Zero or more option, optgroup, and script-supporting elements.

 Either: phrasing content.

 Or: Zero or more option and script-supporting elements.

 Zero or more option and script-supporting elements.

 If the element has a label attribute and a value attribute: Nothing.

 If the element has a label attribute but no value attribute: Text.

 If the element has no label attribute and is not a child of a datalist element: Text that is not inter-element whitespace.

 If the element has no label attribute and is a child of a datalist element: Text.

 Text.

 Optionally a legend element, followed by flow content.

 One summary element followed by flow content.

 Either: phrasing content.

 Or: one element of heading content.

 If there is no src attribute, depends on the value of the type attribute, but must match script content restrictions.

 If there is a src attribute, the element must be either empty or contain only script documentation that also matches script content restrictions.

 When scripting is disabled, in a head element: in any order, zero or more link elements, zero or more style elements, and zero or more meta elements.

 When scripting is disabled, not in a head element: transparent, but there must be no noscript element descendants.

 Otherwise: text that conforms to the requirements given in the prose.

 Nothing (for clarification, see example).

 Transparent

 Transparent, but with no interactive content descendants except for a elements, img elements with usemap attributes, button elements, input elements whose type attribute are in the Checkbox or Radio Button states, input elements that are buttons, select elements with a multiple attribute or a display size greater than 1, and elements that would not be interactive content except for having the tabindex attribute specified.

这有点复杂，我们还是把它做一些分类，首先我们过滤掉带 If 的情况、Text 和 Transparent。

for(let defination of elementDefinations) {

  //console.log(defination.name + ":")

  let categories = defination.text.match(/Categories:\n([\s\S]+)\nContexts in which this element can be used:/)[1].split("\n");

  defination.contentModel = [];

  let contentModel = defination.text.match(/Content model:\n([\s\S]+)\nTag omission in text\/html:/)[1].split("\n");

  for(let line of contentModel)

    if(line.match(/([^ ]+) content./))

      defination.contentModel.push(RegExp.$1);

    else if(line.match(/Nothing.|Transparent./));

    else if(line.match(/^Text[\s\S]*.$/));

    else

      console.log(line)

}

这时候我们再来执行看看：

A head element followed by a body element.

One or more h1, h2, h3, h4, h5, h6 elements, optionally intermixed with script-supporting elements.

Zero or more li and script-supporting elements.

Either: Zero or more groups each consisting of one or more dt elements followed by one or more dd elements, optionally intermixed with script-supporting elements.

Or: One or more div elements, optionally intermixed with script-supporting elements.

If the element is a child of a dl element: one or more dt elements followed by one or more dd elements, optionally intermixed with script-supporting elements.

See prose.

Otherwise: Text, but must match requirements described in prose below.

Zero or more source elements, followed by one img element, optionally intermixed with script-supporting elements.

Zero or more param elements, then, transparent.

If the element has a src attribute: zero or more track elements, then transparent, but with no media element descendants.

If the element does not have a src attribute: zero or more source elements, then zero or more track elements, then transparent, but with no media element descendants.

If the element has a src attribute: zero or more track elements, then transparent, but with no media element descendants.

If the element does not have a src attribute: zero or more source elements, then zero or more track elements, then transparent, but with no media element descendants.

In this order: optionally a caption element, followed by zero or more colgroup elements, followed optionally by a thead element, followed by either zero or more tbody elements or one or more tr elements, followed optionally by a tfoot element, optionally intermixed with one or more script-supporting elements.

If the span attribute is absent: Zero or more col and template elements.

Zero or more tr and script-supporting elements.

Zero or more td, th, and script-supporting elements.

Zero or more option, optgroup, and script-supporting elements.

Or: Zero or more option and script-supporting elements.

Zero or more option and script-supporting elements.

If the element has a label attribute but no value attribute: Text.

If the element has no label attribute and is not a child of a datalist element: Text that is not inter-element whitespace.

If the element has no label attribute and is a child of a datalist element: Text.

When scripting is disabled, in a head element: in any order, zero or more link elements, zero or more style elements, and zero or more meta elements.

When scripting is disabled, not in a head element: transparent, but there must be no noscript element descendants.

Otherwise: text that conforms to the requirements given in the prose.

这下剩余的就少多了，我们可以看到，基本上剩下的都是直接描述可用的元素了，如果你愿意，还可以用代码进一步解析，不过如果是我的话，会选择手工把它们写成 JSON 了，毕竟只有三十多行文本。

好了，有了 contentModel 和 category，我们要检查某一元素是否可以作为另一元素的子元素，就可以判断一下两边是否匹配啦，首先，我们要做个索引：

var dictionary = Object.create(null);

for(let defination of elementDefinations) {

  dictionary[defination.name] = defination;

}

然后我们编写一下我们的 check 函数：

function check(parent, child) {

  for(let category of child.categories)

    if(parent.contentModel.categories.conatains(category))

      return true;

  if(parent.contentModel.names.conatains(child.name))

      return true;

  return false;

}

总结

这一节课，我们完成了一个小实验：利用工具分析 Web 标准文本，来获得元素的信息。

通过这个实验，我希望能够传递一种思路，代码能够帮助我们从 Web 标准中挖掘出来很多想要的信息，编写代码的过程，也是更深入理解标准的契机。

我们前面的课程中把元素分成了几类来讲解，但是这些分类只能大概地覆盖所有的标签，我设置课程的目标也是讲解标签背后的知识，而非每一种标签的细节。具体每一种标签的属性和细节，可以留给大家自己去整理。

这一节课的产出，则是“绝对完整的标签列表”，也是我学习和阅读标准的小技巧，通过代码我们可以从不同的侧面分析标准的内容，挖掘需要注意的点，这是一种非常好的学习方法。

35 | CSS Flex排版：为什么垂直居中这么难？

你好，我是 winter。今天我们来谈谈 Flex 排版。

我们在前面多次讲过，正常流排版的设计来源于数百年来出版行业的排版经验，而 HTML 诞生之初，也确实是作为一种“超文本”存在的。

但是，自上世纪 90 年代以来，Web 标准和各种 Web 应用蓬勃发展，网页的功能逐渐从“文本信息”向着“软件功能”过渡，这个思路的变化导致了：CSS 的正常流逐渐不满足人民群众的需求了。

这是因为文字排版的思路是“改变文字和盒的相对位置，把它放进特定的版面中”，而软件界面的思路则是“改变盒的大小，使得它们的结构保持固定”。

因此，在早年的 CSS 中，“使盒按照外部尺寸变化”的能力非常弱。在我入行前端的时间（大约 2006 年），CSS 三大经典问题：垂直居中问题，两列等高问题，自适应宽问题。这是在其它 UI 系统中最为基本的问题，而到了 CSS 中，却变成了困扰工程师的三座大山。

机智的前端开发者们，曾经创造了各种黑科技来解决问题，包括著名的 table 布局、负 margin、float 与 clear 等等。在这种情况下，Flex 布局被随着 CSS3 一起提出（最初叫 box 布局），可以说是解决了大问题。

React Native 则更为大胆地使用了纯粹的 Flex 排版，不再支持正常流，最终也很好地支持了大量的应用界面布局，这一点也证明了 Flex 排版的潜力。

今天，我们就从设计、原理和应用三个方面来学习一下 Flex 布局，我们先从设计开始。

Flex 的设计

Flex 在英文中是可伸缩的意思，一些翻译会把它译作弹性，我觉得有点不太准确，但是确实中文中没有更好的词。

Flex 排版的核心是 display:flex 和 flex 属性，它们配合使用。具有 display:flex 的元素我们称为 flex 容器，它的子元素或者盒被称作 flex 项。

flex 项如果有 flex 属性，会根据 flex 方向代替宽 / 高属性，形成“填补剩余尺寸”的特性，这是一种典型的“根据外部容器决定内部尺寸”的思路，也是我们最常用的 Windows 和 Apple 窗口系统的设计思路。

Flex 的原理

说完了设计，我们再来看看原理，Flex 的实现并不复杂，我曾经写过一个基本实现提交给 spritejs 项目，代码可以参考这里。

下面我们就来讲解一下，如何实现一个 Flex 布局。

首先，Flex 布局支持横向和纵向，这样我们就需要做一个抽象，我们把 Flex 延伸的方向称为“主轴”，把跟它垂直的方向称为“交叉轴”。这样，flex 项中的 width 和 height 就会称为交叉轴尺寸或者主轴尺寸。

而 Flex 又支持反向排布，这样，我们又需要抽象出交叉轴起点、交叉轴终点、主轴起点、主轴终点，它们可能是 top、left、bottom、right。

Flex 布局中有一种特殊的情况，那就是 flex 容器没有被指定主轴尺寸，这个时候，实际上 Flex 属性完全没有用了，所有 Flex 尺寸都可以被当做 0 来处理，Flex 容器的主轴尺寸等于其它所有 flex 项主轴尺寸之和。

接下来我们开始做 Flex 排版。

第一步是把 flex 项分行，有 Flex 属性的 flex 项可以暂且认为主轴尺寸为 0，所以，它可以一定放进当前行。

接下来我们把 flex 项逐个放入行，不允许换行的话，我们就“无脑地”把 flex 项放进同一行。允许换行的话，我们就先设定主轴剩余空间为 Flex 容器主轴尺寸，每放入一个就把主轴剩余空间减掉它的主轴尺寸，直到某个 flex 项放不进去为止，换下一行，重复前面动作。

分行过程中，我们会顺便对每一行计算两个属性：交叉轴尺寸和主轴剩余空间，交叉轴尺寸是本行所有交叉轴尺寸的最大值，而主轴剩余空间前面已经说过。

第二步我们来计算每个 flex 项主轴尺寸和位置。

如果 Flex 容器是不允许换行的，并且最后主轴尺寸超出了 Flex 容器，就要做等比缩放。

如果 Flex 容器有多行，那么根据我们前面的分行算法，必然有主轴剩余空间，这时候，我们要找出本行所有的带 Flex 属性的 flex 项，把剩余空间按 Flex 比例分给他们即可。

做好之后，我们就可以根据主轴排布方向，确定每个 flex 项的主轴位置坐标了。

如果本行完全没有带 flex 属性的 flex 项，justify-content 机制就要生效了，它的几个不同的值会影响剩余空白如何分配，作为实现者，我们只要在计算 Flex 项坐标的时候，加上一个数值即可。

例如，如果是 flex-start 就要加到第一个 flex 项身上，如果是 center 就给第一个 flex 项加一半的尺寸，如果是 space-between，就要给除了第一个以外的每个 flex 项加上“flex 项数减一分之一”。

第三步我们来计算 flex 项的交叉轴尺寸和位置。

交叉轴的计算首先是根据 align-content 计算每一行的位置，这部分跟 justify-content 非常类似。

再根据 alignItems 和 flex 项的 alignSelf 来确定每个元素在行内的位置。

计算完主轴和交叉轴，每个 flex 项的坐标、尺寸就都确定了，这样我们就完成了整个的 flex 布局。

Flex 的应用

接下来我们来尝试用 flex 排版来解决一下当年的 CSS 三大经典问题（简直易如反掌）。

垂直居中：

<div id="parent">

  <div id="child">

  </div>

</div>

#parent {

  display:flex;

  width:300px;

  height:300px;

  outline:solid 1px;

  justify-content:center;

  align-content:center;

  align-items:center;

}

#child {

  width:100px;

  height:100px;

  outline:solid 1px;

}

思路是创建一个只有一行的 flexbox，然后用 align-items:center; 和 align-content:center; 来保证行位于容器中，元素位于行中。

两列等高：

<div class="parent">

  <div class="child" style="height:300px;">

  </div>

  <div class="child">

  </div>

</div>

<br/>

<div class="parent">

  <div class="child" >

  </div>

  <div class="child" style="height:300px;">

  </div>

</div>

.parent {

  display:flex;

  width:300px;

  justify-content:center;

  align-content:center;

  align-items:stretch;

}

.child {

  width:100px;

  outline:solid 1px;

}

思路是创建一个只有一行的 flexbox，然后用 stretch 属性让每个元素高度都等于行高。

自适应宽：

<div class="parent">

  <div class="child1">

  </div>

  <div class="child2">

  </div>

</div>

.parent {

  display:flex;

  width:300px;

  height:200px;

  background-color:pink;

}

.child1 {

  width:100px;

  background-color:lightblue;

}

.child2 {

  width:100px;

  flex:1;

  outline:solid 1px;

}

这个就是 Flex 设计的基本能力了，给要自适应的元素添加 flex 属性即可。

总结

今天我们从 Flex 的设计、原理和应用三个方面一起学习了 Flex 排版。

我们先从感性的角度，介绍了 flex 的设计，flex 的设计是一种不同于流布局的，自外而内的设计思路。

接下来我们讲解了 flex 的实现原理，也就是具体的排版算法。要想理解 flex 排版的原理，主轴和交叉轴是非常重要的抽象，flex 排版三个步骤：分行、计算主轴、计算交叉轴。

最后我们给出了几个例子，解决了旧时代的 CSS 三大经典问题。

最后，给你留一个小问题，请根据我的代码和文字，编写一段使用“position:absolute”来模拟 flex 布局的 js。大家可以根据自己的水平，简化需求，比如可以实现一个仅仅支持横向的、单行的、所有 flex 项必须指定高度的 flex 布局。

38 | CSS动画与交互：为什么动画要用贝塞尔曲线这么奇怪的东西？

None

39 | HTML语言：DTD到底是什么？

你好，我是 winter（加微信：642945106 发送“赠送”领取赠送精品课程 发数字“2”获取众筹列表。）。今天，我们来聊一聊 HTML 语言。

我们平时写 HTML 语言，都习惯把关注点放到各种标签上，很少去深究它的语法。我想你应该会有模糊的感觉，HTML 这样的语言，跟 JavaScript 这样的语言会有一些本质的不同。

实际上，JavaScript 语言我们把它称为“编程语言”，它最大的特点是图灵完备的，我们大致可以理解为“包含了表达一切逻辑的能力”。像 HTML 这样的语言，我们称为“标记语言（mark up language）”，它是纯文本的一种升级，“标记”一词的概念来自：编辑审稿时使用不同颜色笔所做的“标记”。

在上世纪 80 年代，“富文本”的概念在计算机领域的热门，犹如如今的“AI”和“区块链”，而 Tim Berners-Lee 当时去设计 HTML，也并非是凭空造出来，他使用了当时已有的一种语言：SGML。

SGML 是一种古老的标记语言，可以追溯到 1969 年 IBM 公司所使用的技术，SGML 十分复杂，严格来说，HTML 是 SGML 中规定的一种格式，但是实际的浏览器没有任何一个是通过 SGML 引擎来解析 HTML 的。

今天的 HTML 仍然有 SGML 的不少影子，那么接下来我们就从 SGML 的一些特性来学习一下 HTML。这里我最想讲的是 SGML 留给 HTML 的重要的遗产：基本语法和 DTD。

基本语法

首先，HTML 作为 SGML 的子集，它遵循 SGML 的基本语法：包括标签、转义等。

SGML 还规定了一些特殊的节点类型，在我们之前的 DOM 课程中已经讲过几种节点类型，它们都有与之对应的 HTML 语法，我们这里复习一下：

这里我们从语法的角度，再逐个具体了解一下。

标签语法

标签语法产生元素，我们从语法的角度讲，就用“标签”这个术语，我们从运行时的角度讲，就用“元素”这个术语。

HTML 中，用于描述一个元素的标签分为开始标签、结束标签和自闭合标签。开始标签和自闭合标签中，又可以有属性。

开始标签：<tagname>

带属性的开始标签： <tagname attributename="attributevalue">

结束标签：</tagname>

自闭合标签：<tagname />

HTML 中开始标签的标签名称只能使用英文字母。

这里需要重点讲一讲属性语法，属性可以使用单引号、双引号或者完全不用引号，这三种情况下，需要转义的部分都不太一样。

属性中可以使用文本实体（后文会介绍）来做转义，属性中，一定需要转义的有：

无引号属性：<tab> <LF> <FF> <SPACE> &五种字符

单引号属性：' &两种字符

双引号属性：" &两种字符

一般来说，灵活运用属性的形式，是不太用到文本实体转义的。

文本语法

在 HTML 中，规定了两种文本语法，一种是普通的文本节点，另一种是 CDATA 文本节点。

文本节点看似是普通的文本，但是，其中有两种字符是必须做转义的：< 和 &。

如果我们从某处拷贝了一段文本，里面包含了大量的 < 和 &，那么我们就有麻烦了，这时候，就轮到我们的 CDATA 节点出场了。

CDATA 也是一种文本，它存在的意义是语法上的意义：在 CDATA 节点内，不需要考虑多数的转义情况。

CDATA 内，只有字符组合]]>需要处理，这里不能使用转义，只能拆成两个 CDATA 节点。

注释语法

HTML 注释语法以<!--开头，以-->结尾，注释的内容非常自由，除了-->都没有问题。
如果注释的内容一定要出现 -->，我们可以拆成多个注释节点。

DTD 语法（文档类型定义）

SGML 的 DTD 语法十分复杂，但是对 HTML 来说，其实 DTD 的选项是有限的，浏览器在解析 DTD 时，把它当做几种字符串之一，关于 DTD，我在本篇文章的后面会详细讲解。

ProcessingInstruction 语法（处理信息）

ProcessingInstruction 多数情况下，是给机器看的。HTML 中规定了可以有 ProcessingInstruction，但是并没有规定它的具体内容，所以可以把它视为一种保留的扩展机制。对浏览器而言，ProcessingInstruction 的作用类似于注释。

ProcessingInstruction 包含两个部分，紧挨着第一个问号后，空格前的部分被称为“目标”，这个目标一般表示处理 ProcessingInstruction 的程序名。

剩余部分是它的文本信息，没有任何格式上的约定，完全由文档编写者和处理程序的编写者约定。

DTD

现在我们来讲一下 DTD，DTD 的全称是 Document Type Defination，也就是文档类型定义。SGML 用 DTD 来定义每一种文档类型，HTML 属于 SGML，在 HTML5 出现之前，HTML 都是使用符合 SGML 规定的 DTD。

如果你是一个上个时代走过来的前端，一定还记得 HTML4.01 有三种 DTD。分别是严格模式、过渡模式和 frameset 模式。

<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01//EN" "http://www.w3.org/TR/html4/strict.dtd">

严格模式的 DTD 规定了 HTML4.01 中需要的标签。

<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" "http://www.w3.org/TR/html4/loose.dtd">

过渡模式的 DTD 除了 html4.01，还包含了一些被贬斥的标签，这些标签已经不再推荐使用了，但是过渡模式中仍保留了它们。

<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Frameset//EN" "http://www.w3.org/TR/html4/frameset.dtd">

frameset 结构的网页如今已经很少见到了，它使用 frameset 标签把几个网页组合到一起。

众所周知，HTML 中允许一些标签不闭合的用法，实际上这些都是符合 SGML 规定的，并且在 DTD 中规定好了的。但是，一些程序员喜欢严格遵守 XML 语法，保证标签闭合性，所以，HTML4.01 又规定了 XHTML 语法，同样有三个版本：

版本一

<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN"

"http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">

版本二

<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "

http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">

版本三

<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Frameset//EN"

"http://www.w3.org/TR/xhtml1/DTD/xhtml1-frameset.dtd">

其实你看看就知道，这些复杂的 DTD 写法并没有什么实际作用（浏览器根本不会用 SGML 引擎解析它们），因此，到了 HTML5，干脆放弃了 SGML 子集这项坚持，规定了一个简单的，大家都能记住的 DTD：

<!DOCTYPE html>

但是，HTML5 仍然保留了 HTML 语法和 XHTML 语法。

文本实体

不知道你注意到没有，HTML4.01 的 DTD 里包含了一个长得很像是 URL 的东西，其实它是真的可以访问的——但是 W3C 警告说，禁止任何浏览器在解析网页的时候访问这个 URL，不然 W3C 的服务器会被压垮。我相信很多好奇的前端工程师都把它下载下来打开过。

这是符合 SGML 规范的 DTD，我们前面讲过，SGML 的规范十分复杂，所以这里我并不打算讲 SGML（其实我也不会），但是这不妨碍我们了解一下 DTD 的内容。这个 DTD 规定了 HTML 包含了哪些标签、属性和文本实体。其中文本实体分布在三个文件中：HTMLsymbol.ent HTMLspecial.ent 和 HTMLlat1.ent。

所谓文本实体定义就是类似以下的代码：

&lt;

&nbsp;

&gt;

&amp;

每一个文本实体由&开头，由;结束，这属于基本语法的规定，文本实体可以用#后跟一个十进制数字，表示字符 Unicode 值。除此之外这两个符号之间的内容，则由 DTD 决定。

我这里数了一下，HTML4.01 的 DTD 中，共规定了 255 个文本实体，找出这些实体和它们对应的 Unicode 编码，就作为本次课程的课后小问题吧。

总结

今天的课程中我们讲了 HTML 的语法，HTML 语法源自 SGML，我们首先介绍了基本语法，包含了五种节点：标签（元素）、文本、注释、文档类型定义（DTD）和处理信息（ProcessingInstruction）。

之后我们又重点介绍了两部分内容：DTD 和文本实体。

DTD 在 HTML4.01 和之前都非常的复杂，到了 HTML5，抛弃了 SGML 兼容，变成简单的<!DOCTYPE html>。

文本实体是 HTML 转义的重要手段，我们讲解了基本用法，HTML4.01 中规定的部分，就留给大家作为课后问题了。

今天的课后问题是：HTML4.01 的 DTD 中，共规定了 255 个文本实体，请你找出这些实体和它们对应的 Unicode 编码吧。

40 | CSS渲染：CSS是如何绘制颜色的？

你好，我是 winter，今天我们来学习一下 CSS 的渲染相关的属性。

我们在布局篇讲到，CSS 的一些属性决定了盒的位置，那么今天我讲到的属性就决定了盒如何被渲染。

按照惯例，还是先从简单得讲起，首先我们来讲讲颜色。

颜色的原理

首先我们来讲讲颜色，最常见的颜色相关的属性就是 color 和 background-color。

这两个属性没什么好讲的，它们分别表示文字颜色和背景颜色，我们这里重点讲讲颜色值。

RGB 颜色

我们在计算机中，最常见的颜色表示法是 RGB 颜色，它符合光谱三原色理论：红、绿、蓝三种颜色的光可以构成所有的颜色。

为什么是这三种颜色呢？这跟人类的视神经系统相关，人类的视觉神经分别有对红、绿、蓝三种颜色敏感的类型。

顺便提一下，人类对红色的感觉最为敏感，所以危险信号提示一般会选择红色；而红绿色盲的人，就是红和绿两种神经缺失一种。其它的动物视觉跟人可能不太一样，比如皮皮虾拥有 16 种视锥细胞，所以我猜它们看到的世界一定特别精彩。

现代计算机中多用 0 - 255 的数字表示每一种颜色，这正好占据了一个字节，每一个颜色就占据三个字节。

这个数字远远超过了人体的分辨能力，因此，上世纪 90 年代刚推出这样的颜色系统的时候，它被称作真彩色。早年间还有更节约空间，但是精度更低的 16 色、256 色、8 位色和 16 位色表示法。

红绿蓝三种颜色的光混合起来就是白光，没有光就是黑暗，所以在 RGB 表示法中，三色数值最大表示白色，三色数值为 0 表示黑色。

CMYK 颜色

如果你上过小学美术课，应该听过“红黄蓝”三原色的说法，这好像跟我们说的不太一样。实际上是这样的，颜料显示颜色的原理是它吸收了所有别的颜色的光，只反射一种颜色，所以颜料三原色其实是红、绿、蓝的补色，也就是：品红、黄、青。因为它们跟红、黄、蓝相近，所以有了这样的说法。

在印刷行业，使用的就是这样的三原色（品红、黄、青）来调配油墨，这种颜色的表示法叫做 CMYK，它用一个四元组来表示颜色。

你一定会好奇，为什么它比三原色多了一种，其实答案并不复杂，在印刷行业中，黑色颜料价格最低，而品红、黄、青颜料价格较贵，如果要用三原色调配黑色，经济上是不划算的，所以印刷时会单独指定黑色。

对 CMYK 颜色表示法来说，同一种颜色会有多种表示方案，但是我们参考印刷行业的习惯，会尽量优先使用黑色。

HSL 颜色

好了，讲了这么多，其实还没有涉及今天的主角：HSL 颜色。接下来我们就讲一讲。

我们刚才讲的颜色是从人类的视觉原理建模，应该说是十分科学了。但是，人类对颜色的认识却并非来自自己的神经系统，当我们把阳光散射，可以得到七色光：红橙黄绿蓝靛紫，实际上，阳光接近白光，它包含了各种颜色的光，它散射之后，应该是个基本连续的。这说明对人的感知来说，颜色远远大于红、绿、蓝。

因此，HSL 这样的颜色模型被设计出来了，它用一个值来表示人类认知中的颜色，我们用专业的术语叫做色相（H）。加上颜色的纯度（S）和明度（L），就构成了一种颜色的表示。

在这里，我需要特别推荐 HSL 颜色，因为它是一种语义化的颜色。当我们对一张图片改变色相时，人们感知到的是“图片的颜色变了”。这里先容我卖个关子，具体的例子待我们讲完了渐变再看。

其它颜色

接下来我们讲一讲 RGBA，RGBA 是代表 Red（红色）、Green（绿色）、Blue（蓝色）和 Alpha 的色彩空间。RGBA 颜色被用来表示带透明度的颜色，实际上，Alpha 通道类似一种颜色值的保留字。在 CSS 中，Alpha 通道被用于透明度，所以我们的颜色表示被称作 RGBA，而不是 RGBO（Opacity）。

为了方便使用，CSS 还规定了名称型的颜色，它内置了大量（140 种）的颜色名称。不过这里我要挑出两个颜色来讲一讲：金（gold）和银（silver）。

如果你使用过这两个颜色，你会发现，金（gold）和银（silver）的视觉表现跟我们想象中的金色和银色相差甚远。与其被叫做金色和银色，它们看起来更像是难看的暗黄色和浅灰色。

为什么会这样呢？在人类天然的色彩认知中，实际上混杂了很多其它因素，金色和银色不仅仅是一种颜色，它还意味着一定的镜面反光程度，在同样的光照条件下，金属会呈现出更亮的色彩，这并非是用一个色值可以描述的，这就引出了我们接下来要讲的渐变。

渐变

在 CSS 中，background-image这样的属性，可以设为渐变。CSS 中支持两种渐变，一种是线性渐变，一种是放射性渐变，我们先了解一下它们的基本用法：

线性渐变的写法是：

linear-gradient(direction, color-stop1, color-stop2, ...);

这里的 direction 可以是方向，也可以是具体的角度。例如：

to bottom

to top

to left

to right

to bottom left

to bottom right

to top left

to top right

120deg

3.14rad

以上这些都是合理的方向取值。

color-stop 是一个颜色和一个区段，例如：

rgba(255,0,0,0)

orange

yellow 10%

green 20%

lime 28px

我们组合一下，产生一个“真正的金色”的背景：

<style>

#grad1 {

    height: 200px;

    background: linear-gradient(45deg, gold 10%, yellow 50%, gold 90%);

}

</style>

<div id="grad1"></div>

放射性渐变需要一个中心点和若干个颜色：

radial-gradient(shape size at position, start-color, ..., last-color);

当我们应用的每一种颜色都是 HSL 颜色时，就产生了一些非常有趣的效果，比如，我们可以通过变量来调整一个按钮的风格：

<style>

.button {

    display: inline-block;

    outline: none;

    cursor: pointer;

    text-align: center;

    text-decoration: none;

    font: 14px/100% Arial, Helvetica, sans-serif;

    padding: .5em 2em .55em;

    text-shadow: 0 1px 1px rgba(0,0,0,.3);

    border-radius: .5em;

    box-shadow: 0 1px 2px rgba(0,0,0,.2);

    color: white;

    border: solid 1px ;

}

</style>

<div class="button orange">123</div>

var btn = document.querySelector(".button");

var h = 25;

setInterval(function(){

  h ++;

  h = h % 360;

  btn.style.borderColor=`hsl(${h}, 95%, 45%)`

  btn.style.background=`linear-gradient(to bottom,  hsl(${h},95%,54.1%),  hsl(${h},95%,84.1%))`

},100);

形状

CSS 中的很多属性还会产生形状，比如我们常见的属性：

border

box-shadow

border-radius

这些产生形状的属性非常有趣，我们也能看到很多利用它们来产生的 CSS 黑魔法。然而，这里我有一个相反的建议，我们仅仅把它们用于基本的用途，把 border 用于边框、把阴影用于阴影，把圆角用于圆角，所有其它的场景，都有一个更好的替代品：datauri+svg。

总结

今天我们介绍了 CSS 中渲染相关的属性：颜色和形状。

我们重点介绍了 CSS 的颜色系统，从颜色基本原理讲解了 RGB 颜色、CMYK 颜色和 HSV 颜色，我们还讲解了 Alpha 通道。

接下来我们又讲了颜色的一个重要应用：渐变，我们可以把渐变看作是一个更复杂的颜色，它非常实用，能够用渐变绘制很多的图像。

最后我们讲解了形状相关的属性，以及 SVG 应用的一个小技巧。

思考题

折衷鹦鹉是一种可爱的鸟类，但是雄性折衷鹦鹉居然是跟雌性颜色不一样！你能用 js 和 canvas，把这只雄性折衷鹦鹉变成跟雌性一样可爱的红色吗？

41 | CSS小实验：动手做，用代码挖掘CSS属性

你好，我是 winter。

我们的课程中已经讲解了一部分 CSS 属性，但是 CSS 属性数量繁多，我们的课程只能覆盖其中一小部分，所以我设计了这个小实验，教你用代码挖掘 W3C 标准中的属性。

浏览器中已经实现的属性

首先我们来看看浏览器中实现了哪些属性。我们用一段代码来看一下。

Object.keys(document.body.style).filter(e => !e.match(/^webkit/))

这段代码思路非常简单，就是枚举 document.body.style 上的所有属性，并且去掉 webkit 前缀的私有属性。

在我的 Chrome 中，得到了这样一组属性：

alignContent, alignItems, alignSelf, alignmentBaseline, all, animation, animationDelay, animationDirection, animationDuration, animationFillMode, animationIterationCount, animationName, animationPlayState, animationTimingFunction, backfaceVisibility, background, backgroundAttachment, backgroundBlendMode, backgroundClip, backgroundColor, backgroundImage, backgroundOrigin, backgroundPosition, backgroundPositionX, backgroundPositionY, backgroundRepeat, backgroundRepeatX, backgroundRepeatY, backgroundSize, baselineShift, blockSize, border, borderBlockEnd, borderBlockEndColor, borderBlockEndStyle, borderBlockEndWidth, borderBlockStart, borderBlockStartColor, borderBlockStartStyle, borderBlockStartWidth, borderBottom, borderBottomColor, borderBottomLeftRadius, borderBottomRightRadius, borderBottomStyle, borderBottomWidth, borderCollapse, borderColor, borderImage, borderImageOutset, borderImageRepeat, borderImageSlice, borderImageSource, borderImageWidth, borderInlineEnd, borderInlineEndColor, borderInlineEndStyle, borderInlineEndWidth, borderInlineStart, borderInlineStartColor, borderInlineStartStyle, borderInlineStartWidth, borderLeft, borderLeftColor, borderLeftStyle, borderLeftWidth, borderRadius, borderRight, borderRightColor, borderRightStyle, borderRightWidth, borderSpacing, borderStyle, borderTop, borderTopColor, borderTopLeftRadius, borderTopRightRadius, borderTopStyle, borderTopWidth, borderWidth, bottom, boxShadow, boxSizing, breakAfter, breakBefore, breakInside, bufferedRendering, captionSide, caretColor, clear, clip, clipPath, clipRule, color, colorInterpolation, colorInterpolationFilters, colorRendering, columnCount, columnFill, columnGap, columnRule, columnRuleColor, columnRuleStyle, columnRuleWidth, columnSpan, columnWidth, columns, contain, content, counterIncrement, counterReset, cursor, cx, cy, d, direction, display, dominantBaseline, emptyCells, fill, fillOpacity, fillRule, filter, flex, flexBasis, flexDirection, flexFlow, flexGrow, flexShrink, flexWrap, float, floodColor, floodOpacity, font, fontDisplay, fontFamily, fontFeatureSettings, fontKerning, fontSize, fontStretch, fontStyle, fontVariant, fontVariantCaps, fontVariantEastAsian, fontVariantLigatures, fontVariantNumeric, fontVariationSettings, fontWeight, gap, grid, gridArea, gridAutoColumns, gridAutoFlow, gridAutoRows, gridColumn, gridColumnEnd, gridColumnGap, gridColumnStart, gridGap, gridRow, gridRowEnd, gridRowGap, gridRowStart, gridTemplate, gridTemplateAreas, gridTemplateColumns, gridTemplateRows, height, hyphens, imageRendering, inlineSize, isolation, justifyContent, justifyItems, justifySelf, left, letterSpacing, lightingColor, lineBreak, lineHeight, listStyle, listStyleImage, listStylePosition, listStyleType, margin, marginBlockEnd, marginBlockStart, marginBottom, marginInlineEnd, marginInlineStart, marginLeft, marginRight, marginTop, marker, markerEnd, markerMid, markerStart, mask, maskType, maxBlockSize, maxHeight, maxInlineSize, maxWidth, maxZoom, minBlockSize, minHeight, minInlineSize, minWidth, minZoom, mixBlendMode, objectFit, objectPosition, offset, offsetDistance, offsetPath, offsetRotate, opacity, order, orientation, orphans, outline, outlineColor, outlineOffset, outlineStyle, outlineWidth, overflow, overflowAnchor, overflowWrap, overflowX, overflowY, overscrollBehavior, overscrollBehaviorX, overscrollBehaviorY, padding, paddingBlockEnd, paddingBlockStart, paddingBottom, paddingInlineEnd, paddingInlineStart, paddingLeft, paddingRight, paddingTop, page, pageBreakAfter, pageBreakBefore, pageBreakInside, paintOrder, perspective, perspectiveOrigin, placeContent, placeItems, placeSelf, pointerEvents, position, quotes, r, resize, right, rowGap, rx, ry, scrollBehavior, scrollMargin, scrollMarginBlock, scrollMarginBlockEnd, scrollMarginBlockStart, scrollMarginBottom, scrollMarginInline, scrollMarginInlineEnd, scrollMarginInlineStart, scrollMarginLeft, scrollMarginRight, scrollMarginTop, scrollPadding, scrollPaddingBlock, scrollPaddingBlockEnd, scrollPaddingBlockStart, scrollPaddingBottom, scrollPaddingInline, scrollPaddingInlineEnd, scrollPaddingInlineStart, scrollPaddingLeft, scrollPaddingRight, scrollPaddingTop, scrollSnapAlign, scrollSnapStop, scrollSnapType, shapeImageThreshold, shapeMargin, shapeOutside, shapeRendering, size, speak, src, stopColor, stopOpacity, stroke, strokeDasharray, strokeDashoffset, strokeLinecap, strokeLinejoin, strokeMiterlimit, strokeOpacity, strokeWidth, tabSize, tableLayout, textAlign, textAlignLast, textAnchor, textCombineUpright, textDecoration, textDecorationColor, textDecorationLine, textDecorationSkipInk, textDecorationStyle, textIndent, textOrientation, textOverflow, textRendering, textShadow, textSizeAdjust, textTransform, textUnderlinePosition, top, touchAction, transform, transformBox, transformOrigin, transformStyle, transition, transitionDelay, transitionDuration, transitionProperty, transitionTimingFunction, unicodeBidi, unicodeRange, userSelect, userZoom, vectorEffect, verticalAlign, visibility, whiteSpace, widows, width, willChange, wordBreak, wordSpacing, wordWrap, writingMode, x, y, zIndex, zoom

数了一下，这里有 390 个属性，这非常壮观了，要想了解它们可不是一件容易的事情。接下来我们试着找到它们背后对应的标准。

小实验：找出 W3C 标准中的 CSS 属性

我们知道 CSS2.1 是一份标准，但是 CSS3 分布在无数标准中，我们需要用代码把它们整理出来，这就是我们今天的小实验内容了。

为了达到我们的目的，我们需要写一个简单的爬虫，来找一找 W3C 标准中都覆盖了哪些属性。

我们这个爬虫的思路是：用 iframe 来加载所有标准的网页，然后用 JavaScript 找出它们中间定义的属性。

第一步：找到 CSS 相关的标准。

我们来到 W3C 的 TR 页面：

https://www.w3.org/TR/?tag=css

我们必须从这个页面里抓取所有的标准名称和链接，打开它的代码，我们会发现它是有规律的，这个页面由一个巨大的列表构成，我们只需要根据 tag 选取需要的标准即可。

document.querySelectorAll("#container li[data-tag~=css] h2:not(.Retired):not(.GroupNote)")

这段代码可以找到所有 CSS 相关的标准，我们用代码把从 HTML 结构中把它们抽取出来。可以得到一个列表。
（图片较大，请等待加载。）

第二步：分析每个标准中的 CSS 属性

得到了这个标准的列表，下一步我们就是分析每个标准中的 CSS 属性。

我们打开第一个标准，试着找出属性定义：

https://www.w3.org/TR/2019/WD-css-lists-3-20190425/

经过分析，我们会发现，属性总是在一个具有 propdef 的容器中，有属性 data-dfn-type 值为 property。

这里我不得不感慨，W3C 的标准写得真的是十分严谨，这给我们带来了很大的方便。我们用以下代码获取属性：

document.querySelectorAll(".propdef [data-dfn-type=property]")

对于第一个标准 CSS Lists Module Level 3 得到了这个列表：

list-style-image

list-style-type

list-style-position

list-style

marker-side

counter-reset

counter-set

counter-increment

好了，接下来，我们来用 iframe 打开这些标准，并且用我们分析好的规则，来找出里面的属性就可以了。最终成品代码如下：

var iframe = document.createElement("iframe");

document.body.appendChild(iframe);

iframe.src = "https://www.w3.org/TR/2019/WD-css-lists-3-20190425/"

function happen(element, type){

  return new Promise(resolve => {

    element.addEventListener(type, resolve, {once: true})

  })

}

happen(iframe, "load").then(function(){

  //Array.prototype.map.call(document.querySelectorAll("#container li[data-tag~=css] h2"), e=> e.children[0].href + " |\t" + e.children[0].textContent).join("\n")

  console.log(iframe.contentWindow);

})

async function start(){

  var output = []

  for(let standard of  Array.prototype.slice.call(document.querySelectorAll("#container li[data-tag~=css] h2:not(.Retired):not(.GroupNote)"))) {

    console.log(standard.children[0].href);

    iframe.src = standard.children[0].href;

    await happen(iframe, "load");

    var properties = Array.prototype.map.call(iframe.contentWindow.document.querySelectorAll(".propdef [data-dfn-type=property]"), e => e.childNodes[0].textContent);

    if(properties.length)

        output.push(standard.children[0].textContent + " | " + properties.join(", "));

  }

  console.log(output.join("\n"))

}

start();

这样，我们就得到了每个属性属于哪个标准，我们来看看最终结果。我把它整理成了一个列表。

（图片较大，请等待加载。）

至此，我们已经找出了标准中讲解的所有属性。

结语

今天的这节课我们通过代码对标准做了分析，找出了属性和标准的对应关系。

我们的第一步是找到所有的标准列表，第二步是找到每个标准中的属性。最后得到的这个列表比较全面地覆盖了 CSS 属性，并根据标准划分好了分类，我觉得这可以作为你后续学习和精研的重要依据。

我在本篇内容的前面还有一份浏览器中已经实现的属性列表，理论上属性列表中的属性应该都出现在了我们的标准中。

那么，这次课后的小任务，就是找出被我们的代码遗漏的属性，和重复出现在多份标准中的属性，让我们的列表更为完善。

42 | HTML·ARIA：可访问性是只给盲人用的特性么？

你好，我是 winter。

我们都知道，HTML 已经是一个完整的语义系统。在前面的课程中，我们围绕着 HTML 本身做了讲解，但是在实际应用中，我们还会用到一些它的扩展。今天我们要讲的 ARIA 就是其中重要的一部分。

ARIA 全称为 Accessible Rich Internet Applications，它表现为一组属性，是用于可访问性的一份标准。关于可访问性，它被提到最多的，就是它可以为视觉障碍用户服务，但是，这是一个误解。

实际上，可访问性其实是一个相当大的课题，它的定义包含了各种设备访问、各种环境、各种人群访问的友好性。不单单是永久性的残障人士需要用到可访问性，健康的人也可能在特定时刻处于需要可访问性的环境。

我们今天讲的 ARIA，是以交互形式来标注各种元素的一类属性，所以，在 ARIA 属性中，你可以看到很多熟悉的面孔，交互形式往往跟我们直觉中的“控件”非常相似。

所以我们的课程，特意把 ARIA 加入还有一个原因：ARIA 的角色对于我们 UI 系统的设计有重要的参考意义。

综述

我们先整体来看看，ARIA 给 HTML 元素添加的一个核心属性就是 role，我们来看一个例子：

<span role="checkbox" aria-checked="false" tabindex="0" aria-labelledby="chk1-label">

</span> <label id="chk1-label">Remember my preferences</label>

这里我们给一个 span 添加了 checkbox 角色，这样，表示我们这个 span 被用于 checkbox，这意味着，我们可能已经用 JS 代码绑定了这个 span 的 click 事件，并且以 checkbox 的交互方式来处理用户操作。

同时，ARIA 系统还提供了一系列 ARIA 属性给 checkbox 这个 role，这意味着，我们可以通过 HTML 属性变化来理解这个 JavaScript 组件的状态，读屏软件等三方客户端，就可以理解我们的 UI 变化，这正是 ARIA 标准的意义。

role 的定义是一个树形的继承关系，我们先来理解一下它的整体结构：

其中，widget 表示一些可交互的组件，structure 表示文档中的结构，window 则代表窗体。

接下来，让我们分类了解一下重要的 ARIA 角色。

Widget 角色

我们刚刚已经讲过一个 widget role，就是 checkbox。

这一类角色跟我们桌面开发中的控件类似，它表示一个可交互的组件，它们有：

我们这里按照继承关系给出一份列表和简要说明：

ARIA role 允许多继承，这里有些角色我没有重复写。

注意，这些 role 可以出现在任何一个 HTML 元素之上，同时要注意，这些 ARIA 属性，不会真实地改变任何一个元素的行为，比如，我们刚才讲的 checkbox，即使我们给一个 span 添加了 Checkbox 角色，我们也需要用 JavaScript 编写相应的逻辑。

这些 widget 同时还会带来对应的 ARIA 属性，比如，我们的 Checkbox 角色，会带来两个属性：

aria-checked 表示复选框是否已经被选中；

aria-labelledby 表示复选框对应的文字。

而 Button 角色，则会带来另外两个属性：

aria-pressed 按钮是否已经被按下；

aria-expanded 按钮控制的目标是否已经被展开。

除了它们本身的属性之外，可交互组件还有继承来的属性，比如，switch 角色继承了 checkbox，因此，它也可以使用 aria-checked 属性。

在 WAI-ARIA 标准中，你可以找到所有的角色和对应的属性，我们这里就不一一列举了。

https://www.w3.org/TR/wai-aria/

很多这些 ARIA 属性都是需要在 JavaScript 中维护的。

如果我们要实现一份组件库，这些 widget role 和它们对应的 aria 属性是非常好的参考。

如果你是组件的实现者，也希望你在实现组件时把对应的 ARIA 属性自动维护好。

除了简单的 widget，还有一些比较复杂的角色，需要多个角色一起配合。我们来逐个了解一下。

Combobox 是一个带选项的输入框，我们常见的搜索引擎，一般都会提供这样的输入框，当输入时，它会提供若干提示选项。

Grid 是一个表格，它会分成行、列，行列又有行头和列头表示行、列的意义。

Tablist 是一个可切换的结构，一般被称为选项卡，它包含了 tab 头和 tabpanel，在 tab 容器中，可能包含各种组件。

Listbox 是一个可选中的列表，它内部具有角色为 Option 的选项。

Menu 是指菜单，菜单中可以加入嵌套的菜单项（Menuitem 角色），除了普通菜单项，还可以有 Menuitemcheckbox 带复选框的菜单栏和 Menuitemradio 带单选框的菜单栏。

Radiogroup 是一组互斥的单选框的容器，它的内部可以由若干个角色为 radio 的单选框。

Tree 是树形控件，它的内部含有 Treeitem 树形控件项，它还有一种升级形式是 Treegrid

structure 角色

结构角色其实跟 HTML5 中不少新标签作用重合了，这里建议优先使用 HTML5 标签。

这部分角色的作用类似于语义化标签，但是内容稍微有些不同，我们这里就不详细讲解了，仅仅给出一张图供你参考：

注：separator 在允许焦点时属于组件，在不允许焦点时属于文档结构。

这里我们需要特别提出 Landmark 角色这个概念，Landmark 角色直接翻译是地标，它是 ARIA 标准中总结的 Web 网页中最常见的 8 个结构，Landmark 角色实际上是 section 的子类，这些角色在生成页面摘要时有很大可能性需要被保留，它们是：

window 角色

在我们的网页中，有些元素表示“新窗口”，这时候，会用到 window 角色。window 系角色非常少，只有三个角色：

window

dialog

alertdialog

dialog 可能会产生“焦点陷阱”，也就是说，当这样的角色被激活时，焦点无法离开这个区域。

总结

今天我介绍了 ARIA 相关的知识，我们分几个部分学习了如何使用 ARIA 属性来提高页面的可访问性。

我们以 ARIA 角色为中心，讲解了 ARIA 定义的语义体系。我们可以把 ARIA 分为三类：

Widget 角色：主要是各种可交互的控件

结构角色：文档的结构

窗体角色：弹出的窗体

今天的课后小问题是，请找一个支持图结构可视化的 JS 库，把所有 ARIA 的继承关系用可视化的方式展现出来。

10 | 浏览器：一个浏览器是如何工作的？（阶段一）

对于前端开发来说，我们平时与浏览器打交道的时间是最多的。可浏览器对前端同学来说更多像一个神秘黑盒子的存在。我们仅仅知道它能做什么，而不知道它是如何做到的。

在我面试和接触过的前端开发者中，70% 的前端同学对这部分的知识内容只能达到“一知半解”的程度。甚至还有一部分同学会质疑这部分知识是否重要：这与我们的工作相关吗，学多了会不会偏移前端工作的方向？

事实上，我们这里所需要了解的浏览器工作原理只是它的大致过程，这部分浏览器工作原理不但是前端面试的常考知识点，它还会辅助你的实际工作，学习浏览器的内部工作原理和个中缘由，对于我们做性能优化、排查错误都有很大的好处。

在我们的课程中，我也会控制浏览器相关知识的粒度，把它保持在“给前端工程师了解浏览器”的水准，而不是详细到“给浏览器开发工程师实现浏览器”的水准。

那么，我们今天开始，来共同思考一下。一个浏览器到底是如何工作的。

实际上，对浏览器的实现者来说，他们做的事情，就是把一个 URL 变成一个屏幕上显示的网页。

这个过程是这样的：

浏览器首先使用 HTTP 协议或者 HTTPS 协议，向服务端请求页面；

把请求回来的 HTML 代码经过解析，构建成 DOM 树；

计算 DOM 树上的 CSS 属性；

最后根据 CSS 属性对元素逐个进行渲染，得到内存中的位图；

一个可选的步骤是对位图进行合成，这会极大地增加后续绘制的速度；

合成之后，再绘制到界面上。

我们在开始详细介绍之前，要建立一个感性认识。我们从 HTTP 请求回来开始，这个过程并非一般想象中的一步做完再做下一步，而是一条流水线。

从 HTTP 请求回来，就产生了流式的数据，后续的 DOM 树构建、CSS 计算、渲染、合成、绘制，都是尽可能地流式处理前一步的产出：即不需要等到上一步骤完全结束，就开始处理上一步的输出，这样我们在浏览网页时，才会看到逐步出现的页面。

首先我们来介绍下网络通讯的部分。

HTTP 协议

浏览器首先要做的事就是根据 URL 把数据取回来，取回数据使用的是 HTTP 协议（实际上这个过程之前还有 DNS 查询，不过这里就不详细展开了。）

我先来了解下 HTTP 的标准。

HTTP 标准由 IETF 组织制定，跟它相关的标准主要有两份：

HTTP1.1 https://tools.ietf.org/html/rfc2616

2.HTTP1.1 https://tools.ietf.org/html/rfc7234

HTTP 协议是基于 TCP 协议出现的，对 TCP 协议来说，TCP 协议是一条双向的通讯通道，HTTP 在 TCP 的基础上，规定了 Request-Response 的模式。这个模式决定了通讯必定是由浏览器端首先发起的。

大部分情况下，浏览器的实现者只需要用一个 TCP 库，甚至一个现成的 HTTP 库就可以搞定浏览器的网络通讯部分。HTTP 是纯粹的文本协议，它是规定了使用 TCP 协议来传输文本格式的一个应用层协议。

下面，我们试着用一个纯粹的 TCP 客户端来手工实现 HTTP 一下：

实验

我们的实验需要使用 telnet 客户端，这个客户端是一个纯粹的 TCP 连接工具（安装方法）。

首先我们运行 telnet，连接到极客时间主机，在命令行里输入以下内容：

telnet time.geekbang.org 80

这个时候，TCP 连接已经建立，我们输入以下字符作为请求：

GET / HTTP/1.1

Host: time.geekbang.org

按下两次回车，我们收到了服务端的回复：

HTTP/1.1 301 Moved Permanently

Date: Fri, 25 Jan 2019 13:28:12 GMT

Content-Type: text/html

Content-Length: 182

Connection: keep-alive

Location: https://time.geekbang.org/

Strict-Transport-Security: max-age=15768000

<html>

<head><title>301 Moved Permanently</title></head>

<body bgcolor="white">

<center><h1>301 Moved Permanently</h1></center>

<hr><center>openresty</center>

</body>

</html>

这就是一次完整的 HTTP 请求的过程了，我们可以看到，在 TCP 通道中传输的，完全是文本。

在请求部分，第一行被称作 request line，它分为三个部分，HTTP Method，也就是请求的“方法”，请求的路径和请求的协议和版本。

在响应部分，第一行被称作 response line，它也分为三个部分，协议和版本、状态码和状态文本。

紧随在 request line 或者 response line 之后，是请求头 / 响应头，这些头由若干行组成，每行是用冒号分隔的名称和值。

在头之后，以一个空行（两个换行符）为分隔，是请求体 / 响应体，请求体可能包含文件或者表单数据，响应体则是 html 代码。

HTTP 协议格式

根据上面的分析，我们可以知道 HTTP 协议，大概可以划分成如下部分。

我们简单看一下，在这些部分中，path 是请求的路径完全由服务端来定义，没有很多的特别内容；而 version 几乎都是固定字符串；response body 是我们最熟悉的 HTML，我在后面会有专门的课程介绍，这里也就不多讲了。

下面我们就来逐个给你介绍其它部分。

HTTP Method（方法）

我们首先来介绍一下 request line 里面的方法部分。这里的方法跟我们编程中的方法意义类似，表示我们此次 HTTP 请求希望执行的操作类型。方法有以下几种定义：

GET

POST

HEAD

PUT

DELETE

CONNECT

OPTIONS

TRACE

浏览器通过地址栏访问页面都是 GET 方法。表单提交产生 POST 方法。

HEAD 则是跟 GET 类似，只返回请求头，多数由 JavaScript 发起

PUT 和 DELETE 分别表示添加资源和删除资源，但是实际上这只是语义上的一种约定，并没有强约束。

CONNECT 现在多用于 HTTPS 和 WebSocket。

OPTIONS 和 TRACE 一般用于调试，多数线上服务都不支持。

HTTP Status code（状态码）和 Status text（状态文本）

接下来我们看看 response line 的状态码和状态文本。常见的状态码有以下几种。

1xx：临时回应，表示客户端请继续。

2xx：请求成功。

200：请求成功。

3xx: 表示请求的目标有变化，希望客户端进一步处理。

301&302：永久性与临时性跳转。

304：跟客户端缓存没有更新。

4xx：客户端请求错误。

403：无权限。

404：表示请求的页面不存在。

418：It’s a teapot. 这是一个彩蛋，来自 ietf 的一个愚人节玩笑。（超文本咖啡壶控制协议）

5xx：服务端请求错误。

500：服务端错误。

503：服务端暂时性错误，可以一会再试。

对我们前端来说，1xx 系列的状态码是非常陌生的，原因是 1xx 的状态被浏览器 http 库直接处理掉了，不会让上层应用知晓。

2xx 系列的状态最熟悉的就是 200，这通常是网页请求成功的标志，也是大家最喜欢的状态码。

3xx 系列比较复杂，301 和 302 两个状态表示当前资源已经被转移，只不过一个是永久性转移，一个是临时性转移。实际上 301 更接近于一种报错，提示客户端下次别来了。

304 又是一个每个前端必知必会的状态，产生这个状态的前提是：客户端本地已经有缓存的版本，并且在 Request 中告诉了服务端，当服务端通过时间或者 tag，发现没有更新的时候，就会返回一个不含 body 的 304 状态。

HTTP Head (HTTP 头)

HTTP 头可以看作一个键值对。原则上，HTTP 头也是一种数据，我们可以自由定义 HTTP 头和值。不过在 HTTP 规范中，规定了一些特殊的 HTTP 头，我们现在就来了解一下它们。

在 HTTP 标准中，有完整的请求 / 响应头规定，这里我们挑几个重点的说一下：

我们先来看看 Request Header。

接下来看一下 Response Header。

这里仅仅列出了我认为比较常见的 HTTP 头，这些头是我认为前端工程师应该做到不需要查阅，看到就可以知道意思的 HTTP 头。完整的列表还是请你参考我给出的 rfc2616 标准。

HTTP Request Body

HTTP 请求的 body 主要用于提交表单场景。实际上，http 请求的 body 是比较自由的，只要浏览器端发送的 body 服务端认可就可以了。一些常见的 body 格式是：

application/json

application/x-www-form-urlencoded

multipart/form-data

text/xml

我们使用 html 的 form 标签提交产生的 html 请求，默认会产生 application/x-www-form-urlencoded 的数据格式，当有文件上传时，则会使用 multipart/form-data。

HTTPS

在 HTTP 协议的基础上，HTTPS 和 HTTP2 规定了更复杂的内容，但是它基本保持了 HTTP 的设计思想，即：使用上的 Request-Response 模式。

我们首先来了解下 HTTPS。HTTPS 有两个作用，一是确定请求的目标服务端身份，二是保证传输的数据不会被网络中间节点窃听或者篡改。

HTTPS 的标准也是由 RFC 规定的，你可以查看它的详情链接：

https://tools.ietf.org/html/rfc2818

HTTPS 是使用加密通道来传输 HTTP 的内容。但是 HTTPS 首先与服务端建立一条 TLS 加密通道。TLS 构建于 TCP 协议之上，它实际上是对传输的内容做一次加密，所以从传输内容上看，HTTPS 跟 HTTP 没有任何区别。

HTTP 2

HTTP 2 是 HTTP 1.1 的升级版本，你可以查看它的详情链接。

https://tools.ietf.org/html/rfc7540

HTTP 2.0 最大的改进有两点，一是支持服务端推送，二是支持 TCP 连接复用。

服务端推送能够在客户端发送第一个请求到服务端时，提前把一部分内容推送给客户端，放入缓存当中，这可以避免客户端请求顺序带来的并行度不高，从而导致的性能问题。

TCP 连接复用，则使用同一个 TCP 连接来传输多个 HTTP 请求，避免了 TCP 连接建立时的三次握手开销，和初建 TCP 连接时传输窗口小的问题。

Note: 其实很多优化涉及更下层的协议。IP 层的分包情况，和物理层的建连时间是需要被考虑的。

结语

在这一节内容中，我们一起学习了浏览器的第一步工作，也就是“浏览器首先使用 HTTP 协议或 HTTPS 协议，向服务端请求页面”的这一过程。

在这个过程中，掌握 HTTP 协议是重中之重。我从一个小实验开始，带你体验了一次完整的 HTTP 请求过程。我们一起先分析了 HTTP 协议的结构。接下来，我分别介绍了 HTTP 方法、HTTP 状态码和状态文本、HTTP Head 和 HTTP Request Body 几个重点需要注意的部分。

最后，我还介绍了 HTTPS 和 HTTP 2 这两个补充版本，以便你可以更好地熟悉并理解新的特性。

你在工作中，是否已经开始使用 HTTPS 和 HTTP 2 协议了呢？用到了它们的哪些特性，请留言告诉我吧。

11 | 浏览器：一个浏览器是如何工作的？（阶段二）

你好，我是 winter，今天我们继续来看浏览器的相关内容。

我在上一篇文章中，简要介绍了浏览器的工作大致可以分为 6 个阶段，我们昨天讲完了第一个阶段，也就是通讯的部分：浏览器使用 HTTP 协议或者 HTTPS 协议，向服务端请求页面的过程。

今天我们主要来看两个过程：如何解析请求回来的 HTML 代码，DOM 树又是如何构建的。

解析代码

我们在前面讲到了 HTTP 的构成，但是我们有一部分没有详细讲解，那就是 Response 的 body 部分，这正是因为 HTTP 的 Response 的 body，就要交给我们今天学习的内容去处理了。

HTML 的结构不算太复杂，我们日常开发需要的 90% 的“词”（指编译原理的术语 token，表示最小的有意义的单元），种类大约只有标签开始、属性、标签结束、注释、CDATA 节点几种。

实际上有点麻烦的是，由于 HTML 跟 SGML 的千丝万缕的联系，我们需要做不少容错处理。“<?”和“<%”什么的也是必须要支持好的，报了错也不能吭声。

1. 词（token）是如何被拆分的

首先我们来看看一个非常标准的标签，会被如何拆分：

<p class="a">text text text</p>

如果我们从最小有意义单元的定义来拆分，第一个词（token）是什么呢？显然，作为一个词（token），整个 p 标签肯定是过大了（它甚至可以嵌套）。

那么，只用 p 标签的开头是不是合适吗？我们考虑到起始标签也是会包含属性的，最小的意义单元其实是“<p” ，所以“ <p” 就是我们的第一个词（token）。

我们继续拆分，可以把这段代码依次拆成词（token）：

<p“标签开始”的开始；

class=“a” 属性；

> “标签开始”的结束；

text text text 文本；

</p> 标签结束。

这是一段最简单的例子，类似的还有什么呢？现在我们可以来来看看这些词（token）长成啥样子：

根据这样的分析，现在我们讲讲浏览器是如何用代码实现，我们设想，代码开始从 HTTP 协议收到的字符流读取字符。

在接受第一个字符之前，我们完全无法判断这是哪一个词（token），不过，随着我们接受的字符越来越多，拼出其他的内容可能性就越来越少。

比如，假设我们接受了一个字符“ < ” 我们一下子就知道这不是一个文本节点啦。

之后我们再读一个字符，比如就是 x，那么我们一下子就知道这不是注释和 CDATA 了，接下来我们就一直读，直到遇到“>”或者空格，这样就得到了一个完整的词（token）了。

实际上，我们每读入一个字符，其实都要做一次决策，而且这些决定是跟“当前状态”有关的。在这样的条件下，浏览器工程师要想实现把字符流解析成词（token），最常见的方案就是使用状态机。

2. 状态机

绝大多数语言的词法部分都是用状态机实现的。那么我们来把部分词（token）的解析画成一个状态机看看：

当然了，我们这里的分析比较粗略，真正完整的 HTML 词法状态机，比我们描述的要复杂的多。更详细的内容，你可以参考HTML 官方文档，HTML 官方文档规定了 80 个状态（顺便一说，HTML 是我见过唯一一个标准中规定了状态机实现的语言，对大部分语言来说，状态机是一种实现而非定义）。

这里我们为了理解原理，用这个简单的状态机就足够说明问题了。

状态机的初始状态，我们仅仅区分 “< ”和 “非 <”：

如果获得的是一个非 < 字符，那么可以认为进入了一个文本节点；

如果获得的是一个 < 字符，那么进入一个标签状态。

不过当我们在标签状态时，则会面临着一些可能性。

比如下一个字符是“ ! ” ，那么很可能是进入了注释节点或者 CDATA 节点。

如果下一个字符是 “/ ”，那么可以确定进入了一个结束标签。

如果下一个字符是字母，那么可以确定进入了一个开始标签。

如果我们要完整处理各种 HTML 标准中定义的东西，那么还要考虑“ ? ”“% ”等内容。

我们可以看到，用状态机做词法分析，其实正是把每个词的“特征字符”逐个拆开成独立状态，然后再把所有词的特征字符链合并起来，形成一个联通图结构。

由于状态机设计属于编译原理的基本知识，这里我们仅作一个简要的介绍。

接下来就是代码实现的事情了，在 C/C++ 和 JavaScript 中，实现状态机的方式大同小异：我们把每个函数当做一个状态，参数是接受的字符，返回值是下一个状态函数。（这里我希望再次强调下，状态机真的是一种没有办法封装的东西，所以我们永远不要试图封装状态机。）

为了方便理解和试验，我们这里用 JavaScript 来讲解，图上的 data 状态大概就像下面这样的：

var data = function(c){

    if(c=="&") {

        return characterReferenceInData;

    }

    if(c=="<") {

        return tagOpen;

    }

    else if(c=="\0") {

        error();

        emitToken(c);

        return data;

    }

    else if(c==EOF) {

        emitToken(EOF);

        return data;

    }

    else {

        emitToken(c);

        return data;

    }

};

var tagOpenState = function tagOpenState(c){

    if(c=="/") {

        return endTagOpenState;

    }

    if(c.match(/[A-Z]/)) {

        token = new StartTagToken();

        token.name = c.toLowerCase();

        return tagNameState;

    }

    if(c.match(/[a-z]/)) {

        token = new StartTagToken();

        token.name = c;

        return tagNameState;

    }

    if(c=="?") {

        return bogusCommentState;

    }

    else {

        error();

        return dataState;

    }

};

//……

这段代码给出了状态机的两个状态示例：data 即为初始状态，tagOpenState 是接受了一个“ < ” 字符，来判断标签类型的状态。

这里的状态机，每一个状态是一个函数，通过“if else”来区分下一个字符做状态迁移。这里所谓的状态迁移，就是当前状态函数返回下一个状态函数。

这样，我们的状态迁移代码非常的简单：

var state = data;

var char

while(char = getInput())

    state = state(char);

这段代码的关键一句是“ state = state(char) ”，不论我们用何种方式来读取字符串流，我们都可以通过 state 来处理输入的字符流，这里用循环是一个示例，真实场景中，可能是来自 TCP 的输出流。

状态函数通过代码中的 emitToken 函数来输出解析好的 token（词），我们只需要覆盖 emitToken，即可指定对解析结果的处理方式。

词法分析器接受字符的方式很简单，就像下面这样：

function HTMLLexicalParser(){

    // 状态函数们……

    function data() {

        // ……

    }

    function tagOpen() {

        // ……

    }

    // ……

    var state = data;

    this.receiveInput = function(char) {

        state = state(char);

    }

}

至此，我们就把字符流拆成了词（token）了。

构建 DOM 树

接下来我们要把这些简单的词变成 DOM 树，这个过程我们是使用栈来实现的，任何语言几乎都有栈，为了给你跑着玩，我们还是用 JavaScript 来实现吧，毕竟 JavaScript 中的栈只要用数组就好了。

function HTMLSyntaticalParser(){

    var stack = [new HTMLDocument];

    this.receiveInput = function(token) {

        //……

    }

    this.getOutput = function(){

        return stack[0];

    }

}

我们这样来设计 HTML 的语法分析器，receiveInput 负责接收词法部分产生的词（token），通常可以由 emitToken 来调用。

在接收的同时，即开始构建 DOM 树，所以我们的主要构建 DOM 树的算法，就写在 receiveInput 当中。当接收完所有输入，栈顶就是最后的根节点，我们 DOM 树的产出，就是这个 stack 的第一项。

为了构建 DOM 树，我们需要一个 Node 类，接下来我们所有的节点都会是这个 Node 类的实例。

在完全符合标准的浏览器中，不一样的 HTML 节点对应了不同的 Node 的子类，我们为了简化，就不完整实现这个继承体系了。我们仅仅把 Node 分为 Element 和 Text（如果是基于类的 OOP 的话，我们还需要抽象工厂来创建对象），

function Element(){

    this.childNodes = [];

}

function Text(value){

    this.value = value || "";

}

前面我们的词（token）中，以下两个是需要成对匹配的：

tag start

tag end

根据一些编译原理中常见的技巧，我们使用的栈正是用于匹配开始和结束标签的方案。

对于 Text 节点，我们则需要把相邻的 Text 节点合并起来，我们的做法是当词（token）入栈时，检查栈顶是否是 Text 节点，如果是的话就合并 Text 节点

同样我们来看看直观的解析过程：

<html maaa=a >

    <head>

        <title>cool</title>

    </head>

    <body>

        <img src="a" />

    </body>

</html>

通过这个栈，我们可以构建 DOM 树：

栈顶元素就是当前节点；

遇到属性，就添加到当前节点；

遇到文本节点，如果当前节点是文本节点，则跟文本节点合并，否则入栈成为当前节点的子节点；

遇到注释节点，作为当前节点的子节点；

遇到 tag start 就入栈一个节点，当前节点就是这个节点的父节点；

遇到 tag end 就出栈一个节点（还可以检查是否匹配）。

我在文章里面放了一个视频，你可以点击查看用栈构造 DOM 树的全过程。

当我们的源代码完全遵循 xhtml（这是一种比较严谨的 HTML 语法）时，这非常简单问题，然而 HTML 具有很强的容错能力，奥妙在于当 tag end 跟栈顶的 start tag 不匹配的时候如何处理。

于是，这又有一个极其复杂的规则，幸好 W3C 又一次很贴心地把全部规则都整理地很好，我们只要翻译成对应的代码就好了，以下这个网站呈现了全部规则。你可以点击查看。

http://www.w3.org/html/wg/drafts/html/master/syntax.html#tree-construction

结语

好了，总结一下。在今天的文章中，我带你继续探索了浏览器的工作原理，我们主要研究了解析代码和构建 DOM 树两个步骤。在解析代码的环节里，我们一起详细地分析了一个词（token）被拆分的过程，并且给出了实现它所需要的一个简单的状态机。

在构建 DOM 树的环节中，基本思路是使用栈来构建 DOM 树为了方便你动手实践，我用 JavaScript 实现了这一过程。

今天给你留的题目是：在语法和词法的代码，我已经给出了大体的结构，请你试着把内容补充完整吧。

12 | 浏览器：一个浏览器是如何工作的（阶段三）

大家好，我是 winter。

在上一节课中，我已经讲了浏览器的 DOM 构建过程，但是这个构建的 DOM，实际上信息是不全的，它只有节点和属性，不包含任何的样式信息。

我们这一节课就来讲讲：浏览器是如何把 CSS 规则应用到节点上，并给这棵朴素的 DOM 树添加上 CSS 属性的。

整体过程

首先我们还是要感性地理解一下这个过程。

首先 CSS 选择器这个名称，可能会给你带来一定的误解，觉得好像 CSS 规则是 DOM 树构建好了以后，再进行选择并给它添加样式的。实际上，这个过程并不是这样的。

我们回忆一下我们在浏览器第一节课讲的内容，浏览器会尽量流式处理整个过程。我们上一节课构建 DOM 的过程是：从父到子，从先到后，一个一个节点构造，并且挂载到 DOM 树上的，那么这个过程中，我们是否能同步把 CSS 属性计算出来呢？

答案是肯定的。

在这个过程中，我们依次拿到上一步构造好的元素，去检查它匹配到了哪些规则，再根据规则的优先级，做覆盖和调整。所以，从这个角度看，所谓的选择器，应该被理解成“匹配器”才更合适。

我在 CSS 语法部分，已经总结了选择器的各种符号，这里再把它列出来，我们回顾一下：

空格: 后代，选中它的子节点和所有子节点的后代节点。

>: 子代，选中它的子节点。

+：直接后继选择器，选中它的下一个相邻节点。

~：后继，选中它之后所有的相邻节点。

||：列，选中表格中的一列。

关于选择器的知识，我会在 CSS 的部分继续讲解。这里我们主要介绍浏览器是如何实现这些规则的。

不知道你有没有发现，这里的选择器有个特点，那就是选择器的出现顺序，必定跟构建 DOM 树的顺序一致。这是一个 CSS 设计的原则，即保证选择器在 DOM 树构建到当前节点时，已经可以准确判断是否匹配，不需要后续节点信息。

也就是说，未来也不可能会出现“父元素选择器”这种东西，因为父元素选择器要求根据当前节点的子节点，来判断当前节点是否被选中，而父节点会先于子节点构建。

理解了 CSS 构建的大概过程，我们下面来看看具体的操作。

首先，我们必须把 CSS 规则做一下处理。作为一门语言，CSS 需要先经过词法分析和语法分析，变成计算机能够理解的结构。

这部分具体的做法属于编译原理的内容，这里就不做赘述了。我们这里假设 CSS 已经被解析成了一棵可用的抽象语法树。

我们在之前的 CSS 课程中已经介绍过 compound-selector 的概念，一个 compound-selector 是检查一个元素的规则，而一个复合型选择器，则是由数个 compound-selector 通过前面讲的符号连接起来的。

后代选择器 “空格”

我们先来分析一下后代选择器，我们来一起看一个例子：

a#b .cls {

    width: 100px;

}

可以把一个 CSS 选择器按照 compound-selector 来拆成数段，每当满足一段条件的时候，就前进一段。

比如，在上面的例子中，当我们找到了匹配 a#b 的元素时，我们才会开始检查它所有的子代是否匹配 .cls。

除了前进一段的情况，我们还需要处理后退的情况，比如，我们这样一段代码：

<a id=b>

    <span>1<span>

    <span class=cls>2<span>

</a>

<span class=cls>3<span>

当遇到 </a> 时，必须使得规则 a#b .cls 回退一步，这样第三个 span 才不会被选中。后代选择器的作用范围是父节点的所有子节点，因此规则是在匹配到本标签的结束标签时回退。

后继选择器“ ~ ”

接下来我们看下后继选择器，跟后代选择器不同的地方是，后继选择器只作用于一层，我们来看一个例子：

.cls~* {

    border:solid 1px green;

}

<div>

<span>1<span>

<span class=cls>2<span>

<span>

    3

    <span>4</span>

<span>

<span>5</span>

</div>

这里 .cls 选中了 span 2 然后 span 3 是它的后继，但是 span 3 的子节点 span 4 并不应该被选中，而 span 5 也是它的后继，因此应该被选中。

按照 DOM 树的构造顺序，4 在 3 和 5 中间，我们就没有办法像前面讲的后代选择器一样通过激活或者关闭规则来实现匹配。

但是这里有个非常方便的思路，就是给选择器的激活，带上一个条件：父元素。

注意，这里后继选择器，当前半段的 .cls 匹配成功时，后续 * 所匹配的所有元素的父元素都已经确定了（后继节点和当前节点父元素相同是充分必要条件）。在我们的例子中，那个 div 就是后继节点的父元素。

子代选择器“ >”

我们继续看，子代选择器是如何实现的。

实际上，有了前面讲的父元素这个约束思路，我们很容易实现子代选择器。区别仅仅是拿当前节点作为父元素，还是拿当前节点的父元素作为父元素。

div>.cls {

    border:solid 1px green;

}

<div>

<span>1<span>

<span class=cls>2<span>

<span>

    3

    <span>4</span>

<span>

<span>5</span>

</div>

我们看这段代码，当 DOM 树构造到 div 时，匹配了 CSS 规则的第一段，因为是子代选择器，我们激活后面的 .cls 选择条件，并且指定父元素必须是当前 div。于是后续的构建 DOM 树构建过程中，span 2 就被选中了。

直接后继选择器“ +”

直接后继选择器的实现是上述中最为简单的了，因为它只对唯一一个元素生效，所以不需要像前面几种一样反复激活和关闭规则。

一个最简单的思路是，我们可以把它当作检查元素自身的选择器来处理。即我们把 #id+.cls 都当做检查某一个元素的选择器。

另外的一种思路是：给后继选择器加上一个 flag，使它匹配一次后失效。你可以尝试一下，告诉我结果。

列选择器“ || ”

列选择器比较特别，它是专门针对表格的选择器，跟表格的模型建立相关，我们这里不详细讲了。

其它

我们不要忘记，CSS 选择器还支持逗号分隔，表示“或”的关系。这里最简单的实现是把逗号视为两条规则的一种简易写法。

比如

a#b, .cls {

}

我们当作两条规则来处理：

a#b {

}

.cls {

}

还有一个情况，就是选择器可能有重合，这样，我们可以使用树形结构来进行一些合并，来提高效率：

#a .cls {

}

#a span {

}

#a>span {

}

这里实际上可以把选择器构造成一棵树：

#a

< 空格 >.cls

< 空格 >span

>span

需要注意的是，这里的树，必须要带上连接符。

结语

这一节我们讲解了 CSS 计算的过程。CSS 计算是把 CSS 规则应用到 DOM 树上，为 DOM 结构添加显示相关属性的过程。在这一节中，我们主要介绍了选择器的几种复合结构应该如何实现。

在这一步骤之后，我们得到了一棵带有 CSS 属性的树，为我们后续继续显式打下了基础。

最后留一个问题，你认为 CSS 语法解析成什么结构，最适合我们进行 CSS 计算。

13 | 浏览器：一个浏览器是如何工作的？（阶段四）

你好，我是 winter（加微信：642945106 发送“赠送”领取赠送精品课程 发数字“2”获取众筹列表。）。

我们书接上文。浏览器进行到这一步，我们已经给 DOM 元素添加了用于展现的 CSS 属性，接下来，浏览器的工作就是确定每一个元素的位置了。我们的基本原则仍然不变，就是尽可能流式地处理上一步骤的输出。

在构建 DOM 树和计算 CSS 属性这两个步骤，我们的产出都是一个一个的元素，但是在排版这个步骤中，有些情况下，我们就没法做到这样了。

尤其是表格相关排版、flex 排版和 grid 排版，它们有一个显著的特点，那就是子元素之间具有关联性。

基本概念

首先我们先来介绍一些基本概念，使你可以感性地认识一下我们平常说的各种术语。

“排版”这个概念最初来自活字印刷，是指我们把一个一个的铅字根据文章顺序，放入板框当中的步骤，排版的意思是确定每一个字的位置。

在现代浏览器中，仍然借用了这个概念，但是排版的内容更加复杂，包括文字、图片、图形、表格等等，我们把浏览器确定它们位置的过程，叫作排版。

浏览器最基本的排版方案是正常流排版，它包含了顺次排布和折行等规则，这是一个跟我们提到的印刷排版类似的排版方案，也跟我们平时书写文字的方式一致，所以我们把它叫做正常流。

浏览器的文字排版遵循公认的文字排版规范，文字排版是一个复杂的系统，它规定了行模型和文字在行模型中的排布。行模型规定了行顶、行底、文字区域、基线等对齐方式。（你还记得小时候写英语的英语本吗？英语本上的四条线就是一个简单的行模型）

此外，浏览器支持不同语言，因为不同语言的书写顺序不一致，所以浏览器的文本排版还支持双向文字系统。

浏览器又可以支持元素和文字的混排，元素被定义为占据长方形的区域，还允许边框、边距和留白，这个就是所谓的盒模型。

在正常流的基础上，浏览器还支持两类元素：绝对定位元素和浮动元素。

绝对定位元素把自身从正常流抽出，直接由 top 和 left 等属性确定自身的位置，不参加排版计算，也不影响其它元素。绝对定位元素由 position 属性控制。

浮动元素则是使得自己在正常流的位置向左或者向右移动到边界，并且占据一块排版空间。浮动元素由 float 属性控制。

除了正常流，浏览器还支持其它排版方式，比如现在非常常用的 flex 排版，这些排版方式由外部元素的 display 属性来控制（注意，display 同时还控制元素在正常流中属于 inline 等级还是 block 等级）。

正常流文字排版

我们会在 CSS 部分详细介绍正常流排版的行为，我们这里主要介绍浏览器中的正常流。正常流是唯一一个文字和盒混排的排版方式，我们先从文字来讲起。

要想理解正常流，我们首先要回忆一下自己如何在纸上写文章。

首先，纸是有固定宽度的，虽然纸有固定高度，但是我们可以通过下一页纸的方式来接续，因此我们不存在写不下的场景。

我们书写文字的时候，是从左到右依次书写，每一个字跟上一个字都不重叠，文字之间有一定间距，当写满一行时，我们换到下一行去继续写。

书写中文时，文字的上、下、中轴线都对齐，书写英文时，不同字母的高度不同，但是有一条基线对齐。

实际上浏览器环境也很类似。但是因为浏览器支持改变排版方向，不一定是从左到右从上到下，所以我们把文字依次书写的延伸方向称为主轴或者主方向，换行延伸的方向，跟主轴垂直交叉，称为交叉轴或者交叉方向。

我们一般会从某个字体文件中获取某个特定文字的相关信息。我们获取到的信息大概类似下面：

纵向版本：

这两张图片来自著名开源字体解析库 freetype，实际上，各个库对字体的理解大同小异，我们注意一下，advance 代表每一个文字排布后在主轴上的前进距离，它跟文字的宽 / 高不相等，是字体中最重要的属性。

除了字体提供的字形本身包含的信息，文字排版还受到一些 CSS 属性影响，如 line-height、letter-spacing、word-spacing 等。

在正常流的文字排版中，多数元素被当作长方形盒来排版，而只有 display 为 inline 的元素，是被拆成文本来排版的（还有一种 run-in 元素，它有时作为盒，有时作为文字，不太常用，这里不详细讲了）。

display 值为 inline 的元素中的文字排版时会被直接排入文字流中，inline 元素主轴方向的 margin 属性和 border 属性（例如主轴为横向时的 margin-left 和 margin-right）也会被计算进排版前进距离当中。

注意，当没有强制指定文字书写方向时，在左到右文字中插入右到左向文字，会形成一个双向文字盒，反之亦然。

这样，即使没有元素包裹，混合书写方向的文字也可以形成一个盒结构，我们在排版时，遇到这样的双向文字盒，会先排完盒内再排盒外。

正常流中的盒

在正常流中，display 不为 inline 的元素或者伪元素，会以盒的形式跟文字一起排版。多数 display 属性都可以分成两部分：内部的排版和是否 inline，带有 inline- 前缀的盒，被称作行内级盒。

根据盒模型，一个盒具有 margin、border、padding、width/height 等属性，它在主轴方向占据的空间是由对应方向的这几个属性之和决定的，而 vertical-align 属性决定了盒在交叉轴方向的位置，同时也会影响实际行高。

所以，浏览器对行的排版，一般是先行内布局，再确定行的位置，根据行的位置计算出行内盒和文字的排版位置。

块级盒比较简单，它总是单独占据一整行，计算出交叉轴方向的高度即可。

绝对定位元素

position 属性为 absolute 的元素，我们需要根据它的包含块来确定位置，这是完全跟正常流无关的一种独立排版模式，逐层找到其父级的 position 非 static 元素即可。

浮动元素排版

float 元素非常特别，浏览器对 float 的处理是先排入正常流，再移动到排版宽度的最左 / 最右（这里实际上是主轴的最前和最后）。

移动之后，float 元素占据了一块排版的空间，因此，在数行之内，主轴方向的排版距离发生了变化，直到交叉轴方向的尺寸超过了浮动元素的交叉轴尺寸范围，主轴排版尺寸才会恢复。
float 元素排布完成后，float 元素所在的行需要重新确定位置。

其它的排版

CSS 的每一种排版都有一个很复杂的规定，实际实现形式也各不相同。比如如 flex 排版，支持了 flex 属性，flex 属性将每一行排版后的剩余空间平均分配给主轴方向的 width/height 属性。浏览器支持的每一种排版方式，都是按照对应的标准来实现的。

总结

这一部分，我们以正常流为主，介绍了浏览器的排版基本概念及一些算法。这里，我主要介绍了正常流中的文字排版、正常流中的盒、绝对定位元素、浮动元素排版这几种情况。最后，我还简单介绍了一下 flex 排版。这属于进阶版的排版方式了，你可以了解一下。

你平时喜欢使用方式排版呢，欢迎留言告诉我。

14 | 浏览器：一个浏览器是如何工作的？（阶段五）

你好，我是 winter。我们的浏览器系列已经进行到最后一篇。

在之前的几篇文章中，我们已经经历了把 URL 变成字符流，把字符流变成词（token）流，把词（token）流构造成 DOM 树，把不含样式信息的 DOM 树应用 CSS 规则，变成包含样式信息的 DOM 树，并且根据样式信息，计算了每个元素的位置和大小。

那么，我们最后的步骤，就是根据这些样式信息和大小信息，为每个元素在内存中渲染它的图形，并且把它绘制到对应的位置。

渲染

首先我们来谈谈渲染这个词，渲染也是个外来词，它是英文词 render 的翻译，render 这个词在英文里面，有“导致”“变成”的意思，也有“粉刷墙壁”的意思。

在计算机图形学领域里，英文 render 这个词是一个简写，它是特指把模型变成位图的过程。我们把 render 翻译成“渲染”，是个非常有意思的翻译，中文里“渲染”这个词是一种绘画技法，是指沾清水把墨涂开的意思。

所以，render 翻译成“渲染”，我认为是非常高明的，对 render 这个过程，用国画的渲染手法来概括，是颇有神似的。

我们现在的一些框架，也会把“从数据变成 HTML 代码的过程”称为 render，其实我觉得这是非常具有误导性的，我个人是非常不喜欢这种命名方式，当然了，所谓“文无第一”，在自然语言的范围里，我们很难彻底否定这种用法的合理性。

不过，在本篇文章中，我们可以约定一下，本文中出现的“渲染”一词，统一指的是它在图形学的意义，也就是把模型变成位图的过程。

这里的位图就是在内存里建立一张二维表格，把一张图片的每个像素对应的颜色保存进去（位图信息也是 DOM 树中占据浏览器内存最多的信息，我们在做内存占用优化时，主要就是考虑这一部分）。

浏览器中渲染这个过程，就是把每一个元素对应的盒变成位图。这里的元素包括 HTML 元素和伪元素，一个元素可能对应多个盒（比如 inline 元素，可能会分成多行）。每一个盒对应着一张位图。

这个渲染过程是非常复杂的，但是总体来说，可以分成两个大类：图形和文字。

盒的背景、边框、SVG 元素、阴影等特性，都是需要绘制的图形类。这就像我们实现 HTTP 协议必须要基于 TCP 库一样，这一部分，我们需要一个底层库来支持。

一般的操作系统会提供一个底层库，比如在 Android 中，有大名鼎鼎的 Skia，而 Windows 平台则有 GDI，一般的浏览器会做一个兼容层来处理掉平台差异。

这些盒的特性如何绘制，每一个都有对应的标准规定，而每一个的实现都可以作为一个独立的课题来研究，当年圆角 + 虚线边框，可是难倒了各个浏览器的工程师。考虑到这些知识互相都比较独立，对前端工程师来说也不是特别重要的细节，我们这里就不详细探究了。

盒中的文字，也需要用底层库来支持，叫做字体库。字体库提供读取字体文件的基本能力，它能根据字符的码点抽取出字形。

字形分为像素字形和矢量字形两种。通常的字体，会在 6px 8px 等小尺寸提供像素字形，比较大的尺寸则提供矢量字形。矢量字形本身就需要经过渲染才能继续渲染到元素的位图上去。目前最常用的字体库是 Freetype，这是一个 C++ 编写的开源的字体库。

在最普遍的情况下，渲染过程生成的位图尺寸跟它在上一步排版时占据的尺寸相同。

但是理想和现实是有差距的，很多属性会影响渲染位图的大小，比如阴影，它可能非常巨大，或者渲染到非常遥远的位置，所以为了优化，浏览器实际的实现中会把阴影作为一个独立的盒来处理。

注意，我们这里讲的渲染过程，是不会把子元素绘制到渲染的位图上的，这样，当父子元素的相对位置发生变化时，可以保证渲染的结果能够最大程度被缓存，减少重新渲染。

合成

合成是英文术语 compositing 的翻译，这个过程实际上是一个性能考量，它并非实现浏览器的必要一环。

我们上一小节中讲到，渲染过程不会把子元素渲染到位图上面，合成的过程，就是为一些元素创建一个“合成后的位图”（我们把它称为合成层），把一部分子元素渲染到合成的位图上面。

看到这句话，我想你一定会问问题，到底是为哪些元素创建合成后的位图，把哪些子元素渲染到合成的位图上面呢？

这就是我们要讲的合成的策略。我们前面讲了，合成是一个性能考量，那么合成的目标就是提高性能，根据这个目标，我们建立的原则就是最大限度减少绘制次数原则。

我们举一个极端的例子。如果我们把所有元素都进行合成，比如我们为根元素 html 创建一个合成后的位图，把所有子元素都进行合成，那么会发生什么呢？

那就是，一旦我们用 JavaScript 或者别的什么方式，改变了任何一个 CSS 属性，这份合成后的位图就失效了，我们需要重新绘制所有的元素。

那么如果我们所有的元素都不合成，会怎样呢？结果就是，相当于每次我们都必须要重新绘制所有的元素，这也不是对性能友好的选择。

那么好的合成策略是什么呢，好的合成策略是“猜测”可能变化的元素，把它排除到合成之外。

我们来举个例子：

<div id="a">

    <div id="b">...</div>

    <div id="c" style="transform:translate(0,0)"></div>

</div>

假设我们的合成策略能够把 a、b 两个 div 合成，而不把 c 合成，那么，当我执行以下代码时：

document.getElementById("c").style.transform = "translate(100px, 0)";

我们绘制的时候，就可以只需要绘制 a 和 b 合成好的位图和 c，从而减少了绘制次数。这里需要注意的是，在实际场景中，我们的 b 可能有很多复杂的子元素，所以当合成命中时，性能提升收益非常之高。

目前，主流浏览器一般根据 position、transform 等属性来决定合成策略，来“猜测”这些元素未来可能发生变化。

但是，这样的猜测准确性有限，所以新的 CSS 标准中，规定了 will-change 属性，可以由业务代码来提示浏览器的合成策略，灵活运用这样的特性，可以大大提升合成策略的效果。

绘制

绘制是把“位图最终绘制到屏幕上，变成肉眼可见的图像”的过程，不过，一般来说，浏览器并不需要用代码来处理这个过程，浏览器只需要把最终要显示的位图交给操作系统即可。

一般最终显式的位图位于显存中，也有一些情况下，浏览器只需要把内存中的一张位图提交给操作系统或者显式驱动就可以了，这取决于浏览器运行的环境。不过无论如何，我们把任何位图合成到这个“最终位图”的操作称为绘制。

这个过程听上去非常简单，这是因为在前面两个小节中，我们已经得到了每个元素的位图，并且对它们部分进行了合成，那么绘制过程，实际上就是按照 z-index 把它们依次绘制到屏幕上。

然而如果在实际中这样做, 会带来极其糟糕的性能。

有一个一度非常流行于前端群体的说法，讲做 CSS 性能优化，应该尽量避免"重排"和"重绘"，前者讲的是我们上一课的排版行为，后者模糊地指向了我们本课程三小节讲的三个步骤，而实际上，这个说法大体不能算错，却不够准确。

因为，实际上，“绘制”发生的频率比我们想象中要高得多。我们考虑一个情况：鼠标划过浏览器显示区域。这个过程中，鼠标的每次移动，都造成了重新绘制，如果我们不重新绘制，就会产生大量的鼠标残影。

这个时候，限制绘制的面积就很重要了。如果鼠标某次位置恰巧遮盖了某个较小的元素，我们完全可以重新绘制这个元素来完成我们的目标，当然，简单想想就知道，这种事情不可能总是发生的。

计算机图形学中，我们使用的方案就是“脏矩形”算法，也就是把屏幕均匀地分成若干矩形区域。

当鼠标移动、元素移动或者其它导致需要重绘的场景发生时，我们只重新绘制它所影响到的几个矩形区域就够了。比矩形区域更小的影响最多只会涉及 4 个矩形，大型元素则覆盖多个矩形。

设置合适的矩形区域大小，可以很好地控制绘制时的消耗。设置过大的矩形会造成绘制面积增大，而设置过小的矩形则会造成计算复杂。

我们重新绘制脏矩形区域时，把所有与矩形区域有交集的合成层（位图）的交集部分绘制即可。

总结

在这一节课程中，我们讲解了浏览器中的位图操作部分，这包括了渲染、合成和绘制三个部分。渲染过程把元素变成位图，合成把一部分位图变成合成层，最终的绘制过程把合成层显示到屏幕上。

当绘制完成时，就完成了浏览器的最终任务，把一个 URL 最后变成了一个可以看的网页图像。当然了，我们对每一个部分的讲解，都省略了大量的细节，比如我们今天讲到的绘制，就有意地无视了滚动区域。

尽管如此，对浏览器工作原理的感性认识，仍然可以帮助我们理解很多前端技术的设计和应用技巧，浏览器的工作原理和性能部分非常强相关，我们在实践部分的性能优化部分，会再次跟你做一些探讨。

实际上，如果你认真阅读浏览器系列的课程，是可以用 JavaScript 实现一个玩具浏览器的，我非常希望学习课程的同学中能有人这样做，一旦你做到了，收益会非常大。这就是我今天留给你的课外作业，你可以尝试一下。

22 | 浏览器DOM：你知道HTML的节点有哪几种吗？

你好，我是 winter。

今天我们进入浏览器 API 的学习, 这一节课，我们来学习一下 DOM API。

DOM API 是最早被设计出来的一批 API，也是用途最广的 API，所以早年的技术社区，常常用 DOM 来泛指浏览器中所有的 API。不过今天这里我们要介绍的 DOM，指的就是狭义的文档对象模型。

DOM API 介绍

首先我们先来讲一讲什么叫做文档对象模型。

顾名思义，文档对象模型是用来描述文档，这里的文档，是特指 HTML 文档（也用于 XML 文档，但是本课不讨论 XML）。同时它又是一个“对象模型”，这意味着它使用的是对象这样的概念来描述 HTML 文档。

说起 HTML 文档，这是大家最熟悉的东西了，我们都知道，HTML 文档是一个由标签嵌套而成的树形结构，因此，DOM 也是使用树形的对象模型来描述一个 HTML 文档。

DOM API 大致会包含 4 个部分。

节点：DOM 树形结构中的节点相关 API。

事件：触发和监听事件相关 API。

Range：操作文字范围相关 API。

遍历：遍历 DOM 需要的 API。

事件相关 API 和事件模型，我们会用单独的课程讲解，所以我们本篇文章重点会为你介绍节点和遍历相关 API。

DOM API 数量很多，我希望给你提供一个理解 DOM API 设计的思路，避免单靠机械的方式去死记硬背。

节点

DOM 的树形结构所有的节点有统一的接口 Node，我们按照继承关系，给你介绍一下节点的类型。

在这些节点中，除了 Document 和 DocumentFrangment，都有与之对应的 HTML 写法，我们可以看一下。

Element: <tagname>...</tagname>

Text: text

Comment: <!-- comments -->

DocumentType: <!Doctype html>

ProcessingInstruction: <?a 1?>

我们在编写 HTML 代码并且运行后，就会在内存中得到这样一棵 DOM 树，HTML 的写法会被转化成对应的文档模型，而我们则可以通过 JavaScript 等语言去访问这个文档模型。

这里我们每天都需要用到，要重点掌握的是：Document、Element、Text 节点。

DocumentFragment 也非常有用，它常常被用来高性能地批量添加节点。因为 Comment、DocumentType 和 ProcessingInstruction 很少需要运行时去修改和操作，所以有所了解即可。

Node

Node 是 DOM 树继承关系的根节点，它定义了 DOM 节点在 DOM 树上的操作，首先，Node 提供了一组属性，来表示它在 DOM 树中的关系，它们是：

parentNode

childNodes

firstChild

lastChild

nextSibling

previousSibling

从命名上，我们可以很清晰地看出，这一组属性提供了前、后、父、子关系，有了这几个属性，我们可以很方便地根据相对位置获取元素。当然，Node 中也提供了操作 DOM 树的 API，主要有下面几种。

appendChild

insertBefore

removeChild

replaceChild

这个命名跟上面一样，我们基本可以知道 API 的作用。这几个 API 的设计可以说是饱受诟病。其中最主要的批评是它不对称——只有 before，没有 after，而 jQuery 等框架都对其做了补充。

实际上，appendChild 和 insertBefore 的这个设计，是一个“最小原则”的设计，这两个 API 是满足插入任意位置的必要 API，而 insertAfter，则可以由这两个 API 实现出来。

我个人其实不太喜欢这个设计，对我而言，insertAt(pos) 更符合审美一些。当然，不论喜不喜欢，这个标准已经确定，我们还是必须要掌握它。

这里从设计的角度还想要谈一点，那就是，所有这几个修改型的 API，全都是在父元素上操作的，比如我们要想实现“删除一个元素的上一个元素”，必须要先用 parentNode 获取其父元素。

这样的设计是符合面向对象的基本原则的。还记得我们在 JavaScript 对象部分讲的对象基本特征吗？“拥有哪些子元素”是父元素的一种状态，所以修改状态，应该是父元素的行为。这个设计我认为是 DOM API 中好的部分。

到此为止，Node 提供的 API 已经可以很方便（大概吧）地对树进行增、删、遍历等操作了。

除此之外，Node 还提供了一些高级 API，我们来认识一下它们。

compareDocumentPosition 是一个用于比较两个节点中关系的函数。

contains 检查一个节点是否包含另一个节点的函数。

isEqualNode 检查两个节点是否完全相同。

isSameNode 检查两个节点是否是同一个节点，实际上在 JavaScript 中可以用“===”。

cloneNode 复制一个节点，如果传入参数 true，则会连同子元素做深拷贝。

DOM 标准规定了节点必须从文档的 create 方法创建出来，不能够使用原生的 JavaScript 的 new 运算。于是 document 对象有这些方法。

createElement

createTextNode

createCDATASection

createComment

createProcessingInstruction

createDocumentFragment

createDocumentType

上面的这些方法都是用于创建对应的节点类型。你可以自己尝试一下。

Element 与 Attribute

Node 提供了树形结构上节点相关的操作。而大部分时候，我们比较关注的是元素。Element 表示元素，它是 Node 的子类。

元素对应了 HTML 中的标签，它既有子节点，又有属性。所以 Element 子类中，有一系列操作属性的方法。

我们需要注意，对 DOM 而言，Attribute 和 Property 是完全不同的含义，只有特性场景下，两者才会互相关联（这里在后面我会详细讲解，今天的文章里我就不展开了）。

首先，我们可以把元素的 Attribute 当作字符串来看待，这样就有以下的 API：

getAttribute

setAttribute

removeAttribute

hasAttribute

如果你追求极致的性能，还可以把 Attribute 当作节点：

getAttributeNode

setAttributeNode

此外，如果你喜欢 property 一样的访问 attribute，还可以使用 attributes 对象，比如 document.body.attributes.class = “a” 等效于 document.body.setAttribute(“class”, “a”)。

查找元素

document 节点提供了查找元素的能力。比如有下面的几种。

querySelector

querySelectorAll

getElementById

getElementsByName

getElementsByTagName

getElementsByClassName

我们需要注意，getElementById、getElementsByName、getElementsByTagName、getElementsByClassName，这几个 API 的性能高于 querySelector。

而 getElementsByName、getElementsByTagName、getElementsByClassName 获取的集合并非数组，而是一个能够动态更新的集合。

我们看一个例子：

var collection = document.getElementsByClassName('winter');

console.log(collection.length);

var winter = document.createElement('div');

winter.setAttribute('class', 'winter')

document.documentElement.appendChild(winter)

console.log(collection.length);

在这段代码中，我们先获取了页面的 className 为 winter 的元素集合，不出意外的话，应该是空。

我们通过 console.log 可以看到集合的大小为 0。之后我们添加了一个 class 为 winter 的 div，这时候我们再看集合，可以发现，集合中出现了新添加的元素。

这说明浏览器内部是有高速的索引机制，来动态更新这样的集合的。所以，尽管 querySelector 系列的 API 非常强大，我们还是应该尽量使用 getElement 系列的 API。

遍历

前面已经提到过，通过 Node 的相关属性，我们可以用 JavaScript 遍历整个树。实际上，DOM API 中还提供了 NodeIterator 和 TreeWalker 来遍历树。

比起直接用属性来遍历，NodeIterator 和 TreeWalker 提供了过滤功能，还可以把属性节点也包含在遍历之内。

NodeIterator 的基本用法示例如下：

var iterator = document.createNodeIterator(document.body, NodeFilter.SHOW_TEXT | NodeFilter.SHOW_COMMENT, null, false);

var node;

while(node = iterator.nextNode())

{

    console.log(node);

}

这个 API 的设计非常老派，这么讲的原因主要有两点，一是循环并没有类似“hasNext”这样的方法，而是直接以 nextNode 返回 null 来标志结束，二是第二个参数是掩码，这两个设计都是传统 C 语言里比较常见的用法。

放到今天看，这个迭代器无法匹配 JavaScript 的迭代器语法，而且 JavaScript 位运算并不高效，掩码的设计就徒增复杂性了。

这里请你注意一下这个例子中的处理方法，通常掩码型参数，我们都是用按位或运算来叠加。而针对这种返回 null 表示结束的迭代器，我使用了在 while 循环条件中赋值，来保证循环次数和调用 next 次数严格一致（但这样写可能违反了某些编码规范）。

我们再来看一下 TreeWalker 的用法。

var walker = document.createTreeWalker(document.body, NodeFilter.SHOW_ELEMENT, null, false)

var node;

while(node = walker.nextNode())

{

    if(node.tagName === "p")

        node.nextSibling();

    console.log(node);

}

比起 NodeIterator，TreeWalker 多了在 DOM 树上自由移动当前节点的能力，一般来说，这种 API 用于“跳过”某些节点，或者重复遍历某些节点。

总的来说，我个人不太喜欢 TreeWalker 和 NodeIterator 这两个 API，建议需要遍历 DOM 的时候，直接使用递归和 Node 的属性。

Range

Range API 是一个比较专业的领域，如果不做富文本编辑类的业务，不需要太深入。这里我们就仅介绍概念和给出基本用法的示例，你只要掌握即可。

Range API 表示一个 HTML 上的范围，这个范围是以文字为最小单位的，所以 Range 不一定包含完整的节点，它可能是 Text 节点中的一段，也可以是头尾两个 Text 的一部分加上中间的元素。

我们通过 Range API 可以比节点 API 更精确地操作 DOM 树，凡是 节点 API 能做到的，Range API 都可以做到，而且可以做到更高性能，但是 Range API 使用起来比较麻烦，所以在实际项目中，并不常用，只有做底层框架和富文本编辑对它有强需求。

创建 Range 一般是通过设置它的起止来实现，我们可以看一个例子：

var range = new Range(),

    firstText = p.childNodes[1],

    secondText = em.firstChild

range.setStart(firstText, 9) // do not forget the leading space

range.setEnd(secondText, 4)

此外，通过 Range 也可以从用户选中区域创建，这样的 Range 用于处理用户选中区域:

var range = document.getSelection().getRangeAt(0);

更改 Range 选中区段内容的方式主要是取出和插入，分别由 extractContents 和 insertNode 来实现。

var fragment = range.extractContents()

range.insertNode(document.createTextNode("aaaa"))

最后我们看一个完整的例子。

var range = new Range(),

    firstText = p.childNodes[1],

    secondText = em.firstChild

range.setStart(firstText, 9) // do not forget the leading space

range.setEnd(secondText, 4)

var fragment = range.extractContents()

range.insertNode(document.createTextNode("aaaa"))

这个例子展示了如何使用 range 来取出元素和在特定位置添加新元素。

总结

在今天的文章中，我们一起了解了 DOM API 的内容。DOM API 大致会包含 4 个部分。

节点：DOM 树形结构中的节点相关 API。

事件：触发和监听事件相关 API。

Range：操作文字范围相关 API。

遍历：遍历 DOM 需要的 API。

DOM API 中还提供了 NodeIterator 和 TreeWalker 来遍历树。比起直接用属性来遍历，NodeIterator 和 TreeWalker 提供了过滤功能，还可以把属性节点也包含在遍历之内。

除此之外，我们还谈到了 Range 的一些基础知识点，这里你掌握即可。

最后，我给你留了一个题目，请你用 DOM API 来实现遍历整个 DOM 树，把所有的元素的 tagName 打印出来。

补充阅读：命名空间

我们本课介绍的所有 API，特意忽略了命名空间。

在 HTML 场景中，需要考虑命名空间的场景不多。最主要的场景是 SVG。创建元素和属性相关的 API 都有带命名空间的版本：

document

createElementNS

createAttributeNS

Element

getAttributeNS

setAttributeNS

getAttributeNodeNS

setAttributeNodeNS

removeAttributeNS

hasAttributeNS

attributes.setNamedItemNS

attributes.getNamedItemNS

attributes.removeNamedItemNS

若要创建 Document 或者 Doctype，也必须要考虑命名空间问题。DOM 要求从 document.implementation 来创建。

document.implementation.createDocument

document.implementation.createDocumentType

除此之外，还提供了一个快捷方式，你也可以动手尝试一下。

document.implementation.createHTMLDocument

猜你喜欢

25 | 浏览器CSSOM：如何获取一个元素的准确位置

你好，我是 winter。

在前面的课程中，我们已经学习了 DOM 相关的 API，狭义的 DOM API 仅仅包含 DOM 树形结构相关的内容。今天，我们再来学习一类新的 API：CSSOM。

我想，你在最初接触浏览器 API 的时候，应该都有跟我类似的想法：“好想要 element.width、element.height 这样的 API 啊”。

这样的 API 可以直接获取元素的显示相关信息，它们是非常符合人的第一印象直觉的设计，但是，偏偏 DOM API 中没有这样的内容。

随着学习的深入，我才知道，这样的设计是有背后的逻辑的，正如 HTML 和 CSS 分别承担了语义和表现的分工，DOM 和 CSSOM 也有语义和表现的分工。

DOM 中的所有的属性都是用来表现语义的属性，CSSOM 的则都是表现的属性，width 和 height 这类显示相关的属性，都属于我们今天要讲的 CSSOM。

顾名思义，CSSOM 是 CSS 的对象模型，在 W3C 标准中，它包含两个部分：描述样式表和规则等 CSS 的模型部分（CSSOM），和跟元素视图相关的 View 部分（CSSOM View）。

在实际使用中，CSSOM View 比 CSSOM 更常用一些，因为我们很少需要用代码去动态地管理样式表。

在今天的文章中，我来分别为你介绍这两部分的 API。

CSSOM

首先我们来介绍下 CSS 中样式表的模型，也就是 CSSOM 的本体。

我们通常创建样式表也都是使用 HTML 标签来做到的，我们用 style 标签和 link 标签创建样式表，例如：

<style title="Hello">

a {

  color:red;

}

</style>

<link rel="stylesheet" title="x" href="data:text/css,p%7Bcolor:blue%7D">

我们创建好样式表后，还有可能要对它进行一些操作。如果我们以 DOM 的角度去理解的话，这些标签在 DOM 中是一个节点，它们有节点的内容、属性，这两个标签中，CSS 代码有的在属性、有的在子节点。这两个标签也遵循 DOM 节点的操作规则，所以可以使用 DOM API 去访问。

但是，这样做的后果是我们需要去写很多分支逻辑，并且，要想解析 CSS 代码结构也不是一件简单的事情，所以，这种情况下，我们直接使用 CSSOM API 去操作它们生成的样式表，这是一个更好的选择。

我们首先了解一下 CSSOM API 的基本用法，一般来说，我们需要先获取文档中所有的样式表：

document.styleSheets

document 的 styleSheets 属性表示文档中的所有样式表，这是一个只读的列表，我们可以用方括号运算符下标访问样式表，也可以使用 item 方法来访问，它有 length 属性表示文档中的样式表数量。

样式表只能使用 style 标签或者 link 标签创建（对 XML 来说，还可以使用，咱们暂且不表）。

我们虽然无法用 CSSOM API 来创建样式表，但是我们可以修改样式表中的内容。

document.styleSheets[0].insertRule("p { color:pink; }", 0)

document.styleSheets[0].removeRule(0)

更进一步，我们可以获取样式表中特定的规则（Rule），并且对它进行一定的操作，具体来说，就是使用它的 cssRules 属性来实现：

document.styleSheets[0].cssRules

这里取到的规则列表，同样是支持 item、length 和下标运算。

不过，这里的 Rules 可就没那么简单了，它可能是 CSS 的 at-rule，也可能是普通的样式规则。不同的 rule 类型，具有不同的属性。

我们在 CSS 语法部分，已经为你整理过 at-rule 的完整列表，多数 at-rule 都对应着一个 rule 类型：

CSSStyleRule

CSSCharsetRule

CSSImportRule

CSSMediaRule

CSSFontFaceRule

CSSPageRule

CSSNamespaceRule

CSSKeyframesRule

CSSKeyframeRule

CSSSupportsRule

具体的规则支持的属性，建议你可以用到的时候，再去查阅 MDN 或者 W3C 的文档，在我们的文章中，仅为你详细介绍最常用的 CSSStyleRule。

CSSStyleRule 有两个属性：selectorText 和 style，分别表示一个规则的选择器部分和样式部分。

selector 部分是一个字符串，这里显然偷懒了没有设计进一步的选择器模型，我们按照选择器语法设置即可。

style 部分是一个样式表，它跟我们元素的 style 属性是一样的类型，所以我们可以像修改内联样式一样，直接改变属性修改规则中的具体 CSS 属性定义，也可以使用 cssText 这样的工具属性。

此外，CSSOM 还提供了一个非常重要的方法，来获取一个元素最终经过 CSS 计算得到的属性：

window.getComputedStyle(elt, pseudoElt);

其中第一个参数就是我们要获取属性的元素，第二个参数是可选的，用于选择伪元素。

好了，到此为止，我们可以使用 CSSOM API 自由地修改页面已经生效的样式表了。接下来，我们来一起关注一下视图的问题。

CSSOM View

CSSOM View 这一部分的 API，可以视为 DOM API 的扩展，它在原本的 Element 接口上，添加了显示相关的功能，这些功能，又可以分成三个部分：窗口部分，滚动部分和布局部分，下面我来分别带你了解一下。

窗口 API

窗口 API 用于操作浏览器窗口的位置、尺寸等。

moveTo(x, y) 窗口移动到屏幕的特定坐标；

moveBy(x, y) 窗口移动特定距离；

resizeTo(x, y) 改变窗口大小到特定尺寸；

resizeBy(x, y) 改变窗口大小特定尺寸。

此外，窗口 API 还规定了 window.open() 的第三个参数：

window.open("about:blank", "_blank" ,"width=100,height=100,left=100,right=100" )

一些浏览器出于安全考虑没有实现，也不适用于移动端浏览器，这部分你仅需简单了解即可。下面我们来了解一下滚动 API。

滚动 API

要想理解滚动，首先我们必须要建立一个概念，在 PC 时代，浏览器可视区域的滚动和内部元素的滚动关系是比较模糊的，但是在移动端越来越重要的今天，两者必须分开看待，两者的性能和行为都有区别。

视口滚动 API

可视区域（视口）滚动行为由 window 对象上的一组 API 控制，我们先来了解一下：

scrollX 是视口的属性，表示 X 方向上的当前滚动距离，有别名 pageXOffset；

scrollY 是视口的属性，表示 Y 方向上的当前滚动距离，有别名 pageYOffset；

scroll(x, y) 使得页面滚动到特定的位置，有别名 scrollTo，支持传入配置型参数 {top, left}；

scrollBy(x, y) 使得页面滚动特定的距离，支持传入配置型参数 {top, left}。

通过这些属性和方法，我们可以读取视口的滚动位置和操纵视口滚动。不过，要想监听视口滚动事件，我们需要在 document 对象上绑定事件监听函数：

document.addEventListener("scroll", function(event){

  //......

})

视口滚动 API 是页面的顶层容器的滚动，大部分移动端浏览器都会采用一些性能优化，它和元素滚动不完全一样，请大家一定建立这个区分的意识。

元素滚动 API

接下来我们来认识一下元素滚动 API，在 Element 类（参见 DOM 部分），为了支持滚动，加入了以下 API。

scrollTop 元素的属性，表示 Y 方向上的当前滚动距离。

scrollLeft 元素的属性，表示 X 方向上的当前滚动距离。

scrollWidth 元素的属性，表示元素内部的滚动内容的宽度，一般来说会大于等于元素宽度。

scrollHeight 元素的属性，表示元素内部的滚动内容的高度，一般来说会大于等于元素高度。

scroll(x, y) 使得元素滚动到特定的位置，有别名 scrollTo，支持传入配置型参数 {top, left}。

scrollBy(x, y) 使得元素滚动到特定的位置，支持传入配置型参数 {top, left}。

scrollIntoView(arg) 滚动元素所在的父元素，使得元素滚动到可见区域，可以通过 arg 来指定滚到中间、开始或者就近。

除此之外，可滚动的元素也支持 scroll 事件，我们在元素上监听它的事件即可：

element.addEventListener("scroll", function(event){

  //......

})

这里你需要注意一点，元素部分的 API 设计与视口滚动命名风格上略有差异，你在使用的时候不要记混。

布局 API

最后我们来介绍一下布局 API，这是整个 CSSOM 中最常用到的部分，我们同样要分成全局 API 和元素上的 API。

全局尺寸信息

window 对象上提供了一些全局的尺寸信息，它是通过属性来提供的，我们一起来了解一下来这些属性。

window.innerHeight, window.innerWidth 这两个属性表示视口的大小。

window.outerWidth, window.outerHeight 这两个属性表示浏览器窗口占据的大小，很多浏览器没有实现，一般来说这两个属性无关紧要。

window.devicePixelRatio 这个属性非常重要，表示物理像素和 CSS 像素单位的倍率关系，Retina 屏这个值是 2，后来也出现了一些 3 倍的 Android 屏。

window.screen （屏幕尺寸相关的信息）

window.screen.width, window.screen.height 设备的屏幕尺寸。

window.screen.availWidth, window.screen.availHeight 设备屏幕的可渲染区域尺寸，一些 Android 机器会把屏幕的一部分预留做固定按钮，所以有这两个属性，实际上一般浏览器不会实现的这么细致。

window.screen.colorDepth, window.screen.pixelDepth 这两个属性是固定值 24，应该是为了以后预留。

虽然 window 有这么多相关信息，在我看来，我们主要使用的是 innerHeight、innerWidth 和 devicePixelRatio 三个属性，因为我们前端开发工作只需要跟视口打交道，其它信息大概了解即可。

元素的布局信息

最后我们来到了本节课一开始提到的问题，我们是否能够取到一个元素的宽（width）和高（height）呢？

实际上，我们首先应该从脑中消除“元素有宽高”这样的概念，我们课程中已经多次提到了，有些元素可能产生多个盒，事实上，只有盒有宽和高，元素是没有的。

所以我们获取宽高的对象应该是“盒”，于是 CSSOM View 为 Element 类添加了两个方法：

getClientRects();

getBoundingClientRect()。

getClientRects 会返回一个列表，里面包含元素对应的每一个盒所占据的客户端矩形区域，这里每一个矩形区域可以用 x, y, width, height 来获取它的位置和尺寸。

getBoundingClientRect ，这个 API 的设计更接近我们脑海中的元素盒的概念，它返回元素对应的所有盒的包裹的矩形区域，需要注意，这个 API 获取的区域会包括当 overflow 为 visible 时的子元素区域。

根据实际的精确度需要，我们可以选择何时使用这两个 API。

这两个 API 获取的矩形区域都是相对于视口的坐标，这意味着，这些区域都是受滚动影响的。
如果我们要获取相对坐标，或者包含滚动区域的坐标，需要一点小技巧：

var offsetX = document.documentElement.getBoundingClientRect().x - element.getBoundingClientRect().x;

如这段代码所示，我们只需要获取文档跟节点的位置，再相减即可得到它们的坐标。
这两个 API 的兼容性非常好，定义又非常清晰，建议你如果是用 JavaScript 实现视觉效果时，尽量使用这两个 API。

结语

今天我们一起学习了 CSSOM 这一类型的 API。我们首先就说到了，就像 HTML 和 CSS 分别承担了语义和表现的分工，DOM 和 CSSOM 也有语义和表现的分工。

CSSOM 是 CSS 的对象模型，在 W3C 标准中，它包含两个部分：描述样式表和规则等 CSS 的模型部分（CSSOM），和跟元素视图相关的 View 部分（CSSOM View）。

最后留给你一个问题，写好欢迎留言来讨论，请找一个网页，用我们今天讲的 API，把页面上的所有盒的轮廓画到一个 canvas 元素上。

猜你喜欢

36 | 浏览器事件：为什么会有捕获过程和冒泡过程？

你好，我是 winter。这一节课，我们进入了浏览器的部分，一起来学习一下事件。

事件概述

在开始接触具体的 API 之前，我们要先了解一下事件。一般来说，事件来自输入设备，我们平时的个人设备上，输入设备有三种：

键盘；

鼠标；

触摸屏。

这其中，触摸屏和鼠标又有一定的共性，它们被称作 pointer 设备，所谓 pointer 设备，是指它的输入最终会被抽象成屏幕上面的一个点。但是触摸屏和鼠标又有一定区别，它们的精度、反应时间和支持的点的数量都不一样。

我们现代的 UI 系统，都源自 WIMP 系统。WIMP 即 Window Icon Menu Pointer 四个要素，它最初由施乐公司研发，后来被微软和苹果两家公司应用在了自己的操作系统上（关于这个还有一段有趣的故事，我附在文末了）。

WIMP 是如此成功，以至于今天很多的前端工程师会有一个观点，认为我们能够“点击一个按钮”，实际上并非如此，我们只能够点击鼠标上的按钮或者触摸屏，是操作系统和浏览器把这个信息对应到了一个逻辑上的按钮，再使得它的视图对点击事件有反应。这就引出了我们第一个要讲解的机制：捕获与冒泡。

捕获与冒泡

很多文章会讲到捕获过程是从外向内，冒泡过程是从内向外，但是这里我希望讲清楚，为什么会有捕获过程和冒泡过程。

我们刚提到，实际上点击事件来自触摸屏或者鼠标，鼠标点击并没有位置信息，但是一般操作系统会根据位移的累积计算出来，跟触摸屏一样，提供一个坐标给浏览器。

那么，把这个坐标转换为具体的元素上事件的过程，就是捕获过程了。而冒泡过程，则是符合人类理解逻辑的：当你按电视机开关时，你也按到了电视机。

所以我们可以认为，捕获是计算机处理事件的逻辑，而冒泡是人类处理事件的逻辑。

以下代码展示了事件传播顺序：

<body>

  <input id="i"/>

</body>

document.body.addEventListener("mousedown", () => {

  console.log("key1")

}, true)

document.getElementById("i").addEventListener("mousedown", () => {

  console.log("key2")

}, true)

document.body.addEventListener("mousedown", () => {

  console.log("key11")

}, false)

document.getElementById("i").addEventListener("mousedown", () => {

  console.log("key22")

}, false)

我们监听了 body 和一个 body 的子元素上的鼠标按下事件，捕获和冒泡分别监听，可以看到，最终产生的顺序是：

“key1”

“key2”

“key22”

“key11”

这是捕获和冒泡发生的完整顺序。

在一个事件发生时，捕获过程跟冒泡过程总是先后发生，跟你是否监听毫无关联。

在我们实际监听事件时，我建议这样使用冒泡和捕获机制：默认使用冒泡模式，当开发组件时，遇到需要父元素控制子元素的行为，可以使用捕获机制。

理解了冒泡和捕获的过程，我们 zai 看监听事件的 API，就非常容易理解了。

addEventListener 有三个参数：

事件名称；

事件处理函数；

捕获还是冒泡。

事件处理函数不一定是函数，也可以是个 JavaScript 具有 handleEvent 方法的对象，看下例子：

var o = {

  handleEvent: event => console.log(event)

}

document.body.addEventListener("keydown", o, false);

第三个参数不一定是 bool 值，也可以是个对象，它提供了更多选项。

once：只执行一次。

passive：承诺此事件监听不会调用 preventDefault，这有助于性能。

useCapture：是否捕获（否则冒泡）。

实际使用，在现代浏览器中，还可以不传第三个参数，我建议默认不传第三个参数，因为我认为冒泡是符合正常的人类心智模型的，大部分业务开发者不需要关心捕获过程。除非你是组件或者库的使用者，那就总是需要关心冒泡和捕获了。

焦点

我们讲完了 pointer 事件是由坐标控制，而我们还没有讲到键盘事件。

键盘事件是由焦点系统控制的，一般来说，操作系统也会提供一套焦点系统，但是现代浏览器一般都选择在自己的系统内覆盖原本的焦点系统。

焦点系统也是视障用户访问的重要入口，所以设计合理的焦点系统是非常重要的产品需求，尤其是不少国家对可访问性有明确的法律要求。

在旧时代，有一个经典的问题是如何去掉输入框上的虚线框，这个虚线框就是 Windows 焦点系统附带的 UI 表现。

现在 Windows 的焦点已经不是用虚线框表示了，但是焦点系统的设计几十年间没有太大变化。

焦点系统认为整个 UI 系统中，有且仅有一个“聚焦”的元素，所有的键盘事件的目标元素都是这个聚焦元素。

Tab 键被用来切换到下一个可聚焦的元素，焦点系统占用了 Tab 键，但是可以用 JavaScript 来阻止这个行为。

浏览器 API 还提供了 API 来操作焦点，如：

document.body.focus();

document.body.blur();

其实原本键盘事件不需要捕获过程，但是为了跟 pointer 设备保持一致，也规定了从外向内传播的捕获过程。

自定义事件

除了来自输入设备的事件，还可以自定义事件，实际上事件也是一种非常好的代码架构，但是 DOM API 中的事件并不能用于普通对象，所以很遗憾，我们只能在 DOM 元素上使用自定义事件。

自定义事件的代码示例如下（来自 MDN）：

var evt = new Event("look", {"bubbles":true, "cancelable":false});

document.dispatchEvent(evt);

这里使用 Event 构造器来创造了一个新的事件，然后调用 dispatchEvent 来在特定元素上触发。
我们可以给这个 Event 添加自定义属性、方法。

注意，这里旧的自定义事件方法（使用 document.createEvent 和 initEvent）已经被废弃。

总结

今天这一节课，我们讲了浏览器中的事件。

我们分别介绍了事件的捕获与冒泡机制、焦点机制和自定义事件。

捕获与冒泡机制来自 pointer 设备输入的处理，捕获是计算机处理输入的逻辑，冒泡是人类理解事件的思维，捕获总是在冒泡之前发生。

焦点机制则来自操作系统的思路，用于处理键盘事件。除了我们讲到的这些，随着输入设备的不断丰富，还有很多新的事件加入，如 Geolocation 和陀螺仪等。

最后给你留个小问题。请你找出你所知道的所有事件类型，和它们的目标元素类型。

WIMP 的小故事

WIMP 是由 Alan Kay 主导设计的，这位巨匠，同时也是面向对象之父和 Smalltalk 语言之父。

乔布斯曾经受邀参观施乐，他见到当时的 WIMP 界面，认为非常惊艳，不久后就领导苹果研究了新一代麦金塔系统。

后来，在某次当面对话中，乔布斯指责比尔盖茨抄袭了 WIMP 的设计，盖茨淡定地回答：“史蒂夫，我觉得应该用另一种方式看待这个问题。这就像我们有个叫施乐的有钱邻居，当我闯进去想偷走电视时，却发现你已经这么干了。”

但是不论如何，苹果和微软的数十代操作系统，极大地发展了这个体系，才有了我们今天的 UI 界面。

37 | 浏览器API（小实验）：动手整理全部API

你好，我是 winter。今天我们来讲讲浏览器 API。

浏览器的 API 数目繁多，我们在之前的课程中，已经一起学习了其中几个比较有体系的部分：比如之前讲到过的 DOM 和 CSSOM 等等。但是，如果你留意过，会发现我们讲到的 API 仍然是标准中非常小的一部分。

这里，我们不可能把课程变成一本厚厚的 API 参考手册，所以这一节课，我设计了一个实验，我们一起来给 API 分分类。

我们按照每个 API 所在的标准来分类。所以，我们用代码来反射浏览器环境中全局对象的属性，然后我们用 JavaScript 的 filter 方法来逐步过滤掉已知的属性。

接下来，我们整理 API 的方法如下：

从 Window 的属性中，找到 API 名称；

查阅 MDN 或者 Google，找到 API 所在的标准；

阅读标准，手工或者用代码整理出标准中包含的 API；

用代码在 Window 的属性中过滤掉标准中涉及的 API。

重复这个过程，我们可以找到所有的 API 对应的标准。首先我们先把前面已经讲过的 API 过滤掉。

##JavaScript 中规定的 API

大部分的 API 属于 Window 对象（或者说全局对象），我们可以用反射来看一看现行浏览器中已经实现的 API，我这里使用 Mac 下的 Chrome 72.0.3626.121 版本。

我们首先调用 Object.getOwnPropertyNames(window)。在我的环境中，可以看到，共有 821 个属性。

这里包含了 JavaScript 标准规定的属性，我们做一下过滤：

{

    let js = new Set();

    let objects = ["BigInt", "BigInt64Array", "BigUint64Array", "Infinity", "NaN", "undefined", "eval", "isFinite", "isNaN", "parseFloat", "parseInt", "decodeURI", "decodeURIComponent", "encodeURI", "encodeURIComponent", "Array", "Date", "RegExp", "Promise", "Proxy", "Map", "WeakMap", "Set", "WeakSet", "Function", "Boolean", "String", "Number", "Symbol", "Object", "Error", "EvalError", "RangeError", "ReferenceError", "SyntaxError", "TypeError", "URIError", "ArrayBuffer", "SharedArrayBuffer", "DataView", "Float32Array", "Float64Array", "Int8Array", "Int16Array", "Int32Array", "Uint8Array", "Uint16Array", "Uint32Array", "Uint8ClampedArray", "Atomics", "JSON", "Math", "Reflect", "escape", "unescape"];

    objects.forEach(o => js.add(o));

    let names = Object.getOwnPropertyNames(window)

    names = names.filter(e => !js.has(e));

}

这一部分我们已经在 JavaScript 部分讲解过了（JavaScript 对象：你知道全部的对象分类吗），所以这里我就采用手工的方式过滤出来。

DOM 中的元素构造器

接下来我们看看已经讲过的 DOM 部分，DOM 部分包含了 document 属性和一系列的构造器，我们可以用 JavaScript 的 prototype 来过滤构造器。

    names = names.filter( e => {

        try {

            return !(window[e].prototype instanceof Node)

        } catch(err) {

            return true;

        }

    }).filter( e => e != "Node")

这里我们把所有 Node 的子类都过滤掉，再把 Node 本身也过滤掉，这是非常大的一批了。

Window 对象上的属性

接下来我们要找到 Window 对象的定义，我们在下面链接中可以找到。

https://html.spec.whatwg.org/#window

这里有一个 Window 接口，是使用 WebIDL 定义的，我们手工把其中的函数和属性整理出来，如下：

 window,self,document,name,location,history,customElements,locationbar,menubar, personalbar,scrollbars,statusbar,toolbar,status,close,closed,stop,focus, blur,frames,length,top,opener,parent,frameElement,open,navigator,applicationCache,alert,confirm,prompt,print,postMessage

接下来，我们编写代码，把这些函数和属性，从浏览器 Window 对象的属性中去掉，JavaScript 代码如下：

{

    let names = Object.getOwnPropertyNames(window)

    let js = new Set();

    let objects = ["BigInt", "BigInt64Array", "BigUint64Array", "Infinity", "NaN", "undefined", "eval", "isFinite", "isNaN", "parseFloat", "parseInt", "decodeURI", "decodeURIComponent", "encodeURI", "encodeURIComponent", "Array", "Date", "RegExp", "Promise", "Proxy", "Map", "WeakMap", "Set", "WeakSet", "Function", "Boolean", "String", "Number", "Symbol", "Object", "Error", "EvalError", "RangeError", "ReferenceError", "SyntaxError", "TypeError", "URIError", "ArrayBuffer", "SharedArrayBuffer", "DataView", "Float32Array", "Float64Array", "Int8Array", "Int16Array", "Int32Array", "Uint8Array", "Uint16Array", "Uint32Array", "Uint8ClampedArray", "Atomics", "JSON", "Math", "Reflect", "escape", "unescape"];

    objects.forEach(o => js.add(o));

    names = names.filter(e => !js.has(e));

    names = names.filter( e => {

        try {

            return !(window[e].prototype instanceof Node)

        } catch(err) {

            return true;

        }

    }).filter( e => e != "Node")

    let windowprops = new Set();

    objects = ["window", "self", "document", "name", "location", "history", "customElements", "locationbar", "menubar", " personalbar", "scrollbars", "statusbar", "toolbar", "status", "close", "closed", "stop", "focus", " blur", "frames", "length", "top", "opener", "parent", "frameElement", "open", "navigator", "applicationCache", "alert", "confirm", "prompt", "print", "postMessage", "console"];

    objects.forEach(o => windowprops.add(o));

    names = names.filter(e => !windowprops.has(e));

}

我们还要过滤掉所有的事件，也就是 on 开头的属性。

names = names.filter( e => !e.match(/^on/))

webkit 前缀的私有属性我们也过滤掉：

names = names.filter( e => !e.match(/^webkit/))

除此之外，我们在 HTML 标准中还能找到所有的接口，这些我们也过滤掉：

    let interfaces = new Set();

    objects = ["ApplicationCache", "AudioTrack", "AudioTrackList", "BarProp", "BeforeUnloadEvent", "BroadcastChannel", "CanvasGradient", "CanvasPattern", "CanvasRenderingContext2D", "CloseEvent", "CustomElementRegistry", "DOMStringList", "DOMStringMap", "DataTransfer", "DataTransferItem", "DataTransferItemList", "DedicatedWorkerGlobalScope", "Document", "DragEvent", "ErrorEvent", "EventSource", "External", "FormDataEvent", "HTMLAllCollection", "HashChangeEvent", "History", "ImageBitmap", "ImageBitmapRenderingContext", "ImageData", "Location", "MediaError", "MessageChannel", "MessageEvent", "MessagePort", "MimeType", "MimeTypeArray", "Navigator", "OffscreenCanvas", "OffscreenCanvasRenderingContext2D", "PageTransitionEvent", "Path2D", "Plugin", "PluginArray", "PopStateEvent", "PromiseRejectionEvent", "RadioNodeList", "SharedWorker", "SharedWorkerGlobalScope", "Storage", "StorageEvent", "TextMetrics", "TextTrack", "TextTrackCue", "TextTrackCueList", "TextTrackList", "TimeRanges", "TrackEvent", "ValidityState", "VideoTrack", "VideoTrackList", "WebSocket", "Window", "Worker", "WorkerGlobalScope", "WorkerLocation", "WorkerNavigator"];

    objects.forEach(o => interfaces.add(o));

    names = names.filter(e => !interfaces.has(e));

这样过滤之后，我们已经过滤掉了所有的事件、Window 对象、JavaScript 全局对象和 DOM 相关的属性，但是，竟然还剩余了很多属性！你是不是很惊讶呢？好了，接下来我们才进入今天的正题。

其它属性

这些既不属于 Window 对象，又不属于 JavaScript 语言的 Global 对象的属性，它们究竟是什么呢？

我们可以一个一个来查看这些属性，来发现一些我们以前没有关注过的标准。

首先，我们要把过滤的代码做一下抽象，写成一个函数：

function filterOut(names, props) {

    let set = new Set();

    props.forEach(o => set.add(o));

    return names.filter(e => !set.has(e));

}

每次执行完 filter 函数，都会剩下一些属性，接下来，我们找到剩下的属性来看一看。

ECMAScript 2018 Internationalization API

在我的浏览器环境中，第一个属性是：Intl。

查找这些属性来历的最佳文档是 MDN，当然，你也可以使用 Google。

总之，经过查阅，我发现，它属于 ECMA402 标准，这份标准是 JavaScript 的一个扩展，它包含了国际化相关的内容：

http://www.ecma-international.org/ecma-402/5.0/index.html#Title

ECMA402 中，只有一个全局属性 Intl，我们也把它过滤掉：

names = names.filter(e => e != "Intl")

再来看看还有什么属性。

Streams 标准

接下来我看到的属性是： ByteLengthQueuingStrategy。

同样经过查阅，它来自 WHATWG 的 Streams 标准：
https://streams.spec.whatwg.org/#blqs-class

不过，跟 ECMA402 不同，Streams 标准中还有一些其它属性，这里我手工查阅了这份标准，并做了整理。

接下来，我们用代码把它们跟 ByteLengthQueuingStrategy 一起过滤掉：

names = filterOut(names, ["ReadableStream", "ReadableStreamDefaultReader", "ReadableStreamBYOBReader", "ReadableStreamDefaultController", "ReadableByteStreamController", "ReadableStreamBYOBRequest", "WritableStream", "WritableStreamDefaultWriter", "WritableStreamDefaultController", "TransformStream", "TransformStreamDefaultController", "ByteLengthQueuingStrategy", "CountQueuingStrategy"]);

好了，过滤之后，又少了一些属性，我们继续往下看。

WebGL

接下来我看到的属性是：WebGLContext​Event。

显然，这个属性来自 WebGL 标准：

https://www.khronos.org/registry/webgl/specs/latest/1.0/#5.15

我们在这份标准中找到了一些别的属性，我们把它一起过滤掉：

names = filterOut(names, ["WebGLContextEvent","WebGLObject", "WebGLBuffer", "WebGLFramebuffer", "WebGLProgram", "WebGLRenderbuffer", "WebGLShader", "WebGLTexture", "WebGLUniformLocation", "WebGLActiveInfo", "WebGLShaderPrecisionFormat", "WebGLRenderingContext"]);

过滤掉 WebGL，我们继续往下看。

Web Audio API

下一个属性是 WaveShaperNode。这个属性名听起来就跟声音有关，这个属性来自 W3C 的 Web Audio API 标准。

我们来看一下标准：

https://www.w3.org/TR/webaudio/

Web Audio API 中有大量的属性，这里我用代码做了过滤。得到了以下列表：

["AudioContext", "AudioNode", "AnalyserNode", "AudioBuffer", "AudioBufferSourceNode", "AudioDestinationNode", "AudioParam", "AudioListener", "AudioWorklet", "AudioWorkletGlobalScope", "AudioWorkletNode", "AudioWorkletProcessor", "BiquadFilterNode", "ChannelMergerNode", "ChannelSplitterNode", "ConstantSourceNode", "ConvolverNode", "DelayNode", "DynamicsCompressorNode", "GainNode", "IIRFilterNode", "MediaElementAudioSourceNode", "MediaStreamAudioSourceNode", "MediaStreamTrackAudioSourceNode", "MediaStreamAudioDestinationNode", "PannerNode", "PeriodicWave", "OscillatorNode", "StereoPannerNode", "WaveShaperNode", "ScriptProcessorNode", "AudioProcessingEvent"]

于是我们把它们也过滤掉：

names = filterOut(names, ["AudioContext", "AudioNode", "AnalyserNode", "AudioBuffer", "AudioBufferSourceNode", "AudioDestinationNode", "AudioParam", "AudioListener", "AudioWorklet", "AudioWorkletGlobalScope", "AudioWorkletNode", "AudioWorkletProcessor", "BiquadFilterNode", "ChannelMergerNode", "ChannelSplitterNode", "ConstantSourceNode", "ConvolverNode", "DelayNode", "DynamicsCompressorNode", "GainNode", "IIRFilterNode", "MediaElementAudioSourceNode", "MediaStreamAudioSourceNode", "MediaStreamTrackAudioSourceNode", "MediaStreamAudioDestinationNode", "PannerNode", "PeriodicWave", "OscillatorNode", "StereoPannerNode", "WaveShaperNode", "ScriptProcessorNode", "AudioProcessingEvent"]);

我们继续看下一个属性。

Encoding 标准

在我的环境中，下一个属性是 TextDecoder，经过查阅得知，这个属性也来自一份 WHATWG 的标准，Encoding：

https://encoding.spec.whatwg.org/#dom-textencoder

这份标准仅仅包含四个接口，我们把它们过滤掉：

names = filterOut(names, ["TextDecoder", "TextEncoder", "TextDecoderStream", "TextEncoderStream"]);

我们继续来看下一个属性。

Web Background Synchronization

下一个属性是 SyncManager，这个属性比较特殊，它并没有被标准化，但是我们仍然可以找到它的来源文档：

https://wicg.github.io/BackgroundSync/spec/#sync-manager-interface

这个属性我们就不多说了，过滤掉就好了。

Web Cryptography API

我们继续看下去，下一个属性是 SubtleCrypto，这个属性来自 Web Cryptography API，也是 W3C 的标准。

https://www.w3.org/TR/WebCryptoAPI/

这份标准中规定了三个 Class 和一个 Window 对象的扩展，给 Window 对象添加了一个属性 crypto。

names = filterOut(names, ["CryptoKey", "SubtleCrypto", "Crypto", "crypto"]);

我们继续来看。

Media Source Extensions

下一个属性是 SourceBufferList，它来自于：

https://www.w3.org/TR/media-source/

这份标准中包含了三个接口，这份标准还扩展了一些接口，但是没有扩展 window。

names = filterOut(names, ["MediaSource", "SourceBuffer", "SourceBufferList"]);

我们继续看下一个属性。

The Screen Orientation API

下一个属性是 ScreenOrientation，它来自 W3C 的 The Screen Orientation API 标准：

https://www.w3.org/TR/screen-orientation/

它里面只有 ScreenOrientation 一个接口，也是可以过滤掉的。

结语

到 Screen Orientation API，我这里看到还剩 300 余个属性没有处理，剩余部分，我想把它留给大家自己来完成。

我们可以看到，在整理 API 的过程中，我们可以找到各种不同组织的标准，比如：

ECMA402 标准来自 ECMA；

Encoding 标准来自 WHATWG；

WebGL 标准来自 Khronos；

Web Cryptography 标准来自 W3C；

还有些 API，根本没有被标准化。

浏览器环境的 API，正是这样复杂的环境。我们平时编程面对的环境也是这样的一个环境。

所以，面对如此繁复的 API，我建议在系统掌握 DOM、CSSOM 的基础上，你可以仅仅做大概的浏览和记忆，根据实际工作需要，选择其中几个来深入学习。

做完这个实验，你对 Web API 的理解应该会有很大提升。

这一节课的问题就是完成所有的 API 到标准的归类，不同的浏览器环境应该略有不同，欢迎你把自己的结果留言一起讨论。

43 | 性能：前端的性能到底对业务数据有多大的影响？

你好，我是 winter。

从今天开始，我们就从前端知识学习的部分，过渡到了实践部分。这节课我来谈谈性能。

性能是个特别有意思的话题，在我之前的工作中，从入门的初级工程师到高级别的技术专家，大家都很喜欢谈性能，我以前参与晋升评审，每年总能听到很多关于性能的晋升述职。

那么，今天我就来谈谈我眼中的性能。

性能总论

while 循环快还是 for 循环快？

|0 是不是比 Math.floor 性能好？

网上随处可以见到一类对性能的讨论。一些新人也非常热衷此类讨论。但是实际上，它们除了让你写代码的时候纠结之外，毫无意义。

为什么这样讲呢？我想讲一个小故事。

从前有个工程师，特别注重代码细节，有一天他发现系统中的一段代码写的性能很差，因此，他用汇编重写了整段代码，执行效率足足提升了三倍。但是最后，大家发现，用户反馈性能丝毫没有提高，因为他优化的那个进程名字叫“System Idle”。

所以你看，性能优化不能只着眼于局部的代码。这里，我要提出一个我的观点：一切没有 profiling 的性能都是耍流氓。凡是真正有价值的性能优化，必定是从端到端的业务场景建立体系来考虑的。

在我的认识中，性能体系的建立可以分成以下几部分：

现状评估和建立指标；

技术方案；

执行；

结果评估和监控。

下面，我就来为你一一讲解。

现状评估和建立指标

要想做好性能优化，正确地评估现状和建立指标是最关键的一步，它又往往是会被轻视的一步。

作为一个工程师，指标又要考虑两个因素。一方面，对用户来说，什么样的性能指标能更好地评估它的体验？另一方面，对公司来说，什么样的指标会影响业务价值呢？

在我公布答案之前，我希望你能思考一下，你所负责的业务，是否有前端性能指标？它是否能够满足我上面提到的两个要求？

在我之前的工作中，整个用了长达一年的时间来探索，才找到了合适的指标，并且回答好了两个问题。

性能问题可以分成很多方面，最重要的几个点是：

页面加载性能；

动画与操作性能；

内存、电量消耗。

注意，这里我们仅仅是对“性能”两个字的分析和解读，在对大量的用户数据分析后，我们发现，其实这三部分中，“页面加载性能”跟用户的流失率有非常强的关联性，而用户流失率，正是公司业务非常看重的指标。

因此，在开始阶段，我们决定把性能优化的重点放在页面加载性能上。

那么，用什么指标来衡量页面加载性能呢？最容易想到的方案是“用户平均加载时间”，事实上，我们在相当长的一段时间，也都是在使用用户平均加载时间作为性能指标。

但是，很快我们发现，这个指标有严重的问题：

当加载时间低于一定数字，用户体感差别不大了，我们经过一定的研究，认为这个数字大约是 1 秒；

少数超长时间加载的用户（如 2G），会极大影响整个指标，即指标不能反映大多数用户的体验。

于是，基于以上分析，我们设计了一个新的指标——秒开率，即一秒之内打开的用户占用户总量的百分比。这个指标后来逐渐推广到整个公司，甚至影响到了一些业内的其它企业，现在，谈秒开率已经是个非常自然的事情了，但是当初的设计确实走了不少弯路。

技术方案

有了指标，我们就有了优化的目标，接下来，就到了技术出场的环节了。

我们这里还是以加载过程为例，来讲解一下。

首先我们要简单分析一下，从输入 URL 后按下回车，到底发生了什么。

我们在浏览器的原理课程中，已经讲解了浏览器大致的工作过程，但是，我们必须理解几件事：

从域名到 IP 地址，需要用 DNS 协议查询；

HTTP 协议是用 TCP 传输的，所以会有 TCP 建立连接过程；

如果使用 HTTPS，还有有 HTTPS 交换证书；

每个网页还有图片等请求。

从这个分析和实际试验的结果看，网页的加载时间，不但跟体积有关系，还跟请求数有很大关系，因此，我们最终设计的技术方案大约可以这样划分：

这里仅仅列出了性能优化的一部分技术方案，是我认为比较重要的部分，可以看到，这里涉及的并不仅仅是前端技术，有服务端、客户端、设计师团队，所以要想做好性能优化，绝对不能把自己限制在局部的视角，必须是整个业务一起考虑，才能有良好的收效。

执行

技术方案设计好了，它是不会自己变成线上页面的，所以，有了技术方案，我们只完成了一半的工作，接下来我们还需要一个执行过程。

执行也不简单，如果说方案主要靠技术，那么执行就是靠工程实施了。

根据公司的实际情况，工程实施可能有不同的程度，我把工程水平从低到高分成三个阶段：

纯管理；

制度化；

自动化。

纯行政管理，是由经理用纯粹的管理手段来执行方案，比如说，作为前端团队的 Leader，我可以组织会议，要求整个团队使用我们前面谈的技术方案。

但是纯行政管理有一些问题，一方面，需要的行政资源不一定有，比如我没法强制让后端团队配合我，另一方面，纯粹的管理方式，团队本身的体验并不好，也不利于团队成长，最重要的是，纯粹管理方式容易造成执行不到位。这样的执行方式多数出现在非技术岗位。

制度化执行方式是用规则代替人的命令，指定责任人，通过培训、checklist、定期 review 等具体措施来保证实施。制度化执行可以极大地减轻管理工作量，一般现代互联网公司都会采用类似的方式。但是制度化执行方式还有很大成分是依靠人的主动性的，对程序员来说，还有更好的方式：自动化。

自动化的方式是在一些重要的操作路径上设置规则，针对我们的性能优化，有两个点适合做这件事：一个是把开发好的页面发布上线，另一个是开发好的页面 URL 投放到首页等处的链接。

在我之前的工作中，我们跟测试团队配合，开发了一套页面性能打分系统，它会自动扫面页面上的可优化点，并且跟发布平台和投放平台合作，把它加入日常机制中。现在多数公司都会采用制度化和自动化结合的执行方案。

结果评估和监控

执行完了之后，就要向老板汇报争取升职加薪了，还要有一定的结果总结，才是一个完整的工程实施，而且，凡是工程实施，肯定要有一定长效机制，不能优化完了退化，这些都要求有线上监控机制。

要想做线上监控，分两个部分：

数据采集；

数据展现。

数据采集部分，同样需要发布平台或者开发工具来配合，对性能数据来说，Performance API 非常好用，它是浏览器记录的性能数据，一般来说，我们用统一的代码把它上传到服务器端就够用了。

数据的展现部分就比较自由了，可以用不同的数据可视化方案来展现性能数据，没有一定之规。一般的数据监控平台，会提供报警机制，对性能来说，报警需求不是特别强烈，但是也可以设置一些条件，针对秒开率特别低的网页报警。

有了监控，再配合一定制度，就可以保障整个团队产出的性能了，要注意，性能不是一个静态的事情，指标需要不断优化，技术方案还需要不断随着技术发展迭代，制度、自动化工具也需要不断改进，最终的监控平台产品也不能不做新需求，所以性能应该成为一个团队的日常工作的一部分，持续进行。

总结

今天我们学习了前端团队工程实施中的性能体系，首先我们介绍了总体思想：性能应该是基于业务和实际用户体验需求的一种工程实施，不是纯粹的技术游戏。

接下来我们分成四个步骤介绍了性能工程体系，首先介绍了现状评估和建立指标，建立指标应当从业务的角度考虑，接下来讲了技术方案设计，技术方案应当从整体角度，基于 Profiling 的结果分析来设计。

之后我们讲了实施，我们讲了工程实施的三个层次：纯管理、制度化、工程化，最后，我们讲了结果评估和线上监控，线上监控需要从数据采集和数据展现两个部分分别实现。

最后，留一个小问题，请你为自己的团队和业务设计一下性能的整体方案，欢迎来留言分享。

44 | 工具链：什么样的工具链才能提升团队效率？

你好，我是 winter。今天我们的主题是工具。

古语云：“工欲善其事，必先利其器”，程序员群体对工具的爱好和重视是一个悠久的传统。简单趁手的工具是程序员开发的好帮手。

但是在工程方面，工具不仅仅是简单的“趁手”即可，假如一个团队人人都自己发明几个小工具，那么后果将会是灾难性的：同一个团队的同学无法互相配合写代码，一旦有人离职，可能某一个项目就永远无法跑起来了。

所以我们今天从工程的角度谈一谈工具体系的规划。

工具总论

跟性能不同，工具体系并非业务结果，所以我们没法用简单的数据指标来衡量工具，它的结果更多程度是一种开发体验：帮助技术团队内的同学提升效率和体验。

作为工程体系，我们考虑工具的时候同样要遵循基本规则：现状与指标、方案、实施、结果和监控。

不过，对工具而言，指标和结果都是一种“软性指标”，也就是团队的开发效率和开发体验。这里我不太推荐把开发效率和开发体验过度数据化，我的经验是：开发效率提升 n 倍永远是一种臆想或者主观论断。

工具体系的目标

前面已经讲到，工具是为技术团队本身服务的工程体系，那么，工具的目标是什么呢？其实每一种工具的出现，必然都有一个非常具体的目标，比如 npm 帮助我们进行包管理，Yeoman 帮助我们初始化项目模板。

但是这些目标是工具的目标，不是工具体系的目标。我们做一个假设，假如你是一个前端团队的工具体系负责人，现在要你来规划团队的工具体系，你会怎么做呢？

如果你到社区找了一大堆工具，并且把它们要解决的问题都罗列出来，作为工具体系的目标，那就完全走上了错误的道路。

实际上，在考虑具体的工具之前，我们应该解决工具体系的“元问题”，即：我们对工具本身的要求是什么？

考虑到工程行为都是团队合作，我们对工具最基本的要求就是：版本一致。

只有整个团队的工具版本一致，至少要做到避免大版本差异，才能做到互相接手代码时，团队成员能够正确的使用工具开发。

工具体系的另一个重要需求是：避免冲突，一些工具可能互相没有干扰，比如 Yeoman 和 gulp，有一些工具则由社区设计了配合方案，比如 webpack 和 babel，有一些工具，则存在着根本性冲突，如 gulp 和 grunt。

所以，在谈及具体问题之前，我们必须要有这两个要求的解决方案。这就需要引入一个新的概念：工具链。

工具链是一系列互相配合的工具，能够协作完成开发任务（注：工具链这个词最早是由 C/C++ 程序员引入的概念，一般包含编译、链接、调试等工具）。

下面我们就来谈谈工具链的设计。

工具体系的设计

要想设计一个工具链，首先我们需要整理一下，前端开发大约要做哪些事，下面是我的答案：

初始化项目；

运行和调试；

测试（单元测试）；

发布。

那么，一个前端项目的工具链，大约就会包含这些功能。一个典型的社区项目工具链可能就类似下面这样：

Yeoman

webpack

ava/nyc

aws-cli

但是，这显然不够，我们还需要一种机制，保证团队使用的工具版本一致。

轻量级的做法是，在项目初始化模板中定义 npm script 并且在 npm dev-dependency 中规定它的版本号。

重量级的做法是，开发一个包装工具，在命令行中不直接使用命令，而使用包装过的命令。如在我之前的团队，使用的工具名为 def，它规定了一些命令：

def init

def dev

def test

def publish

这样，工具链的使用者只需指定工具链名称，就不需要知道项目具体使用了哪些工具，这样只需要专注自己的需求就够了。

同时，统一的命令行入口，意味着整个团队不需要互相学习工具链，就可以接手别人的项目开发。

在稍微大一些的团队内部，往往会需要不止一种开发模式，如移动开发和桌面开发，这样，所需要的工具链也不一样，因此我们需要多条工具链。

要想开发新的工具链，可以使用复制分支的方式来扩展原来的工具链。在我原来的工作中，不同的工具链被称作“套件”，每一种套件对应着一组互相配合的工具。

工具体系的执行

因为工具体系服务的是团队内部成员，所以执行非常简单，同时，工具体系的入口是初始化项目，所以只要初始化工具在手，可以控制其它所有工具。

我们在性能的那一课里，已经讲过工程体系的执行分成三个层次：纯管理、制度化和自动化。

工具体系因为其自身特性，可以说是最容易做到自动化的一个体系了。

工具体系的监控

工具体系的结果虽然是软性的，也不能完全不做监控。

纯粹的社区方案比较难做到监控，但是如果我们使用了前面提到的统一命令行入口包装，那么就可以做一些简单的统计工作了。

一般来说，以下指标跟开发者体验较为相关：

调试 / 构建次数；

构建平均时长；

使用的工具版本；

发布次数。

在我之前的工作中，工具团队曾经从构建平均时长数据中发现构建效率问题，对 webpack 做了大量深度优化来改善开发体验。

同时，工具的相关数据还能够帮助发现一些问题，比如某个项目频繁发布，可能说明它风险很高。工具的相关数据还能帮我们发现老旧的工具，如果某个套件使用频率极低，则可以考虑把它下线。

总之，工具体系的监控不仅仅是衡量工具体系的好帮手，也是非常珍贵的研发数据，里面有很多可挖掘的价值。

总结

这一课，我们讲解了工具相关的工程知识。

我们仍然从目标、方案设计、执行和结果四个方面来讲解，工具体系的目标除了单个工具解决具体问题之外，还要注意一致性和配合问题，因此我们需要工具链。

工具链一般会涵盖研发阶段的各个主要操作。工具体系的执行比较简单，很容易就可以做到完全的自动化。工具体系的监控同样非常重要，工具的监控除了帮助我们改进工具体系，对研发体系的其它部分也有帮助。

最后，请你思考下自己所在的团队，是否已经建立了工具体系？听完了今天的课程，你认为它有哪些可改进的部分？

45 | 持续集成：几十个前端一起工作，如何保证工作质量？

你好，我是 winter。今天我们来聊聊持续集成。

持续集成是近现代软件工程中的一个非常重要的概念。它是指在软件开发过程中，以定期或者实时的方式，集成所有人的工作成果，做统一的构建和测试。

与持续集成相对的做法是：独立开发各个模块，在软件开发的最终阶段才做集成。持续集成的优势是及早处理集成阶段的问题，使软件质量和开发进度可控。

现在持续集成还有升级版本：持续交付和持续部署，这些因为需要更为完善的基础设施，目前很少有公司前端团队可以用上，我们暂且不谈。

传统的持续集成概念诞生于桌面客户端开发，在 Web 前端领域，由于技术和产品形态的差别，我们需要构建的持续集成体系也有一些区别。

持续集成总论

传统软件的持续集成主要有以下措施。

daily build：每日构建，开发者每天提交代码到代码仓库，构建一个可运行的版本。

build verification test（BVT）：构建验证测试，每日构建版本出来后，运行一组自动化的测试用例，保证基本功能可用。

对于前端来说，有一些现实的区别：

前端代码按页面自然解耦，大部分页面都是单人开发；

前端构建逻辑简单，一般开发阶段都保证构建成功，不需要构建；

前端代码一般用于开发界面，测试自动化成本极高；

前端页面跳转，是基于 url，没有明确的产品边界。

基于以上分析，传统的持续集成方案放在前端，要么不需要，要么不适用，要么实施成本高，因此我们不能套用传统的持续集成理论，而需要重新思考前端领域的持续集成体系。

持续集成的目标

前面我们已经分析过，每日构建不需要，前端构建验证测试成本过高难以实施，那么我们是不是可以有一些代替的措施呢？

首先我们要确定前端持续集成的目标，我们回到持续集成的根本理念，一是要及早集成代码形成可测试的版本，二是通过一定的测试来验证提交的代码的有效性。

持续集成的方案

我们进一步思考，前端持续集成如何完成这两个目标呢？

前端代码不需要构建，或者说只需要单页面构建，但是页面与页面之间的跳转是用 url 构成的，所以我们的可测试的版本，不可能通过“构建”来获得。

我们只能通过“发布”来获得一个前端代码的可执行版本，在传统语境中，“发布”的目标是线上生产环境，这显然不行。于是，我们就需要一个预览环境，来做一种“虚拟发布”的操作。

我们再来考虑一下，为界面编写自动化测试用例成本很高，那么如何代替构建验证测试呢？

我们回忆一下，在性能一课，我有讲过，页面的性能可以通过一些自动化工具来分析，还可以通过一些数据采集方案来发现性能问题，对于预览环境前端页面，我们可以采用同样的措施。

除了基于页面结构的分析和数据采集，我们还可以扫描代码。

综上，我认为前端的持续集成的措施应该是这样的：

预览环境，代替每日构建，前端每次（或指定次）提交代码到仓库都同步到预览环境，保证预览环境总是可用；

规则校验，代替构建验证测试，通过数据采集（如前面提到的性能数据）和代码扫描，保证提交的代码满足一定的质量要求。

接下来，让我来详细介绍一下预览环境的设计和规则校验的设计。

预览环境

前端代码发布到线上生产环境需要有线上的机器和域名，而预览环境同样需要机器和域名，不过，只需要在公司内网即可。

所以建立预览环境的第一步就是申请机器和域名，我们需要运维协助，在预览环境的机器上部署 Web 应用服务器。

有了预览环境的机器，下一步就是建立预览环境发布机制。

有些公司使用脚本发布，有些公司使用 git hook，有些公司则使用一个 Web 应用平台，进行白屏操作，因为各个公司的发布机制千差万别，我这里没办法讲解具体的方案。这里我建议，预览环境的机器发布流程应该跟线上发布保持一致，这样可以最大程度降低成本和降低心智负担。

预览环境的部署和发布机制建立是最基本的需求，在实际应用中，情况要复杂的多，可能需要多个预览环境同时存在。

比如，测试工程师可能要求一个相对稳定的环境来测试，这是一个合理的诉求，比如，全公司大部分业务都可能依赖登录页面，一旦登录页面在频繁发布导致一些预览环境的故障，可能全公司都没办法工作了。

又比如，当服务端工程师联调时，会希望前端的预览环境跟服务端的预览环境对接，而当服务端的代码部署到线上生产环境后，可能又需要前端的预览环境跟服务端线上环境对接。

这些问题都是我曾经遇到过的非常现实的问题，如果今天回过头来设计，我认为应该设计一套带参数和版本号的预览环境，为测试提供特定版本的预览环境，用参数解决那些跟服务端 API 对接问题，但是任何系统都不可能从一开始就设计完善，所以，建议你把重心放到建立预览环境的基本需求上来。

规则校验

接下来我们讲讲规则校验，规则校验可以分成三种措施：

页面结构扫描；

运行时数据采集；

代码扫描。

页面结构扫描可以使用无头浏览器（如 phantomjs）配合一些 JavaScript 代码编写的规则来完成。

运行时数据采集，可以通过在页面插入公共 js 文件的方式来完成，最基本的是用 Performance API 来采集性能数据，用 window.onerror 来采集 js 错误。

代码扫描，社区有一些现成的方案，比如 JSHint，你可以根据实际需要，选择社区方案或者自研。

持续集成的实施

持续集成的实施，是必须严格做到自动化和制度化的。我们可以通过上节课讲的工具来完成持续集成。其它部分，都可以通过工具和制度来完成，这里需要重点讲的是规则校验中的规则部分。

我们刚刚讲解的规则校验仅仅是搭建好了平台，而规则本身，我们需要先形成一个共识，然后在前端团队内部形成一定的更新机制。

这里，我建议用 issue 的方式来管理规则的提案，可以在周会或者月会上讨论，充分保证整个团队对校验规则的一致意见。

这里，我们必须警惕三种错误：

少数人拍脑袋决定校验规则；

一成不变的校验规则；

频繁无规律变化的校验规则。

只有经过民主讨论、定期更新的校验规则，才能在团队中起到积极作用。校验规则决定了整个前端团队的开发体验，所以必须非常慎重。

持续集成的结果

持续集成机制的建立本身就可以视为一种结果，它能够让整个团队的代码质量有一个基本的保障，提前发现问题，统一代码风格，从而带来开发体验和效率的提升。

此外，持续集成的结果也能够以数据的方式呈现出整个开发团队的健康状态，这是管理者会非常关注的一个点。

总结

今天我们讲解了持续集成，持续集成这个概念最早来自桌面客户端软件开发，应用到前端领域，会有一定的变化。这里我提出了一个预览环境 + 规则校验的前端持续集成体系。

预览环境需要申请机器和域名、部署和建立发布机制，规则校验有三种方法：结构扫描、数据采集和代码扫描。

持续集成的实施需要重点关注校验规则部分，要建立一个民主讨论、定期更新的校验规则。持续集成机制的建立就是其结果本身，此外，系统中产生的数据也可以有一定管理价值。

最后留一个问题，你所在的团队，是否有做持续集成呢？请你设计或者改进这个持续集成方案。

46 | 搭建系统：大量的低价值需求应该如何应对？

你好，我是 winter。

不知道你在工作中有没有遇到过这样的事情：一个运营找过来说，有一个紧急又简单的临时活动页面要做，希望打断现有的产品开发节奏临时插入。

这类页面技术难度不高，业务上通常属于“紧急不重要”的事情。

这些需求技术上没挑战，线上存在时间短，上线时间紧又没有任何调整空间，它们往往会成为前端团队里人人都不喜欢的“垃圾需求”，谁要是接了这种需求，就只能自认倒霉。

但是，这些真的是垃圾需求吗？换个视角来看，我认为它反而是宝藏。

所谓工程师，就是为了解决这些问题而存在的岗位，我们从工程的视角来看，“大量紧急不重要的页面”，才是真正的需求，现在需求有了，我们就应该按照工程的方式，定目标、设计方案、做实施、拿结果来解决问题。这就是我们今天要讲的搭建系统。

搭建系统的目标

搭建系统的目标是解决大量的简单页面生产问题。衡量这个目标的指标应该是生产页面的数量，这部分非常的明确，你如果要做搭建系统，可以根据业务的体量和服务的范围来决定具体的指标要求。

搭建系统的设计

搭建系统设计大概有几种流派，这里我介绍几种常见的搭建系统的设计：

第一种，是模板化搭建，由前端工程师生产页面模板，再由运营提供数据来完成页面，可以用以下公式来理解：

模板 + 数据 = 页面

模板化搭建是一种简单的思路，它的优点是整个系统实现简单。

第二种思路是，模块化搭建，由前端工程师生产模块，由运营把模块和数据组织成页面。

第三种思路，是数据驱动界面，这是一种比较新的思路，即数据中包含了展现自身所需要的模块相关的信息，本身决定了界面。

但是不论何种流派，都可以认为是数据、模块、模板、页面几种实体的相互作用，下面我就来详细讲解一下这几样实体。

数据

数据是用于展现界面所需要的信息。

我们按照数据用途，可以分成界面配置数据和内容数据。

界面配置数据：决定了页面上颜色、尺寸、位置、图片、文字等展现形式的数据，通常是以页面为单位的配置。

内容数据：页面要展示的信息，如电商活动页面的商品信息、文章的文字信息等。

按照数据来源，我们又可以分成运营人员手工填写的数据和来自 API 产生的数据。

运营手工填写固定数据：运营人员依靠自己的专业技能决定的数据，可能包含线下招商信息、商品选品、文章等。

来自 API 的数据：

固定数据，由服务端逻辑到指定存储处获取的数据；

用户相关数据，由算法系统或者服务端逻辑，根据用户信息或者用户喜好推荐的数据。

搭建系统本身是个产品，我们针对数据这个实体，要设计增、删、改、查的能力，根据我们以上的分析，搭建系统的数据部分有两个难点。

第一个难点是数据的手工编辑能力，现在一般的数据都会采用 JSON 格式，JSON 格式中有数字、字符串、数组、对象、布尔等数据类型，我们需要根据数据的格式定义为每一种类型设计编辑器。

但是仅仅是基本类型还不够，我们实际开发中，还需要跟实际业务结合来设计编辑器，下面，我就把我在之前的工作中设计的数据编辑器列一下。

整数：整数编辑器，可用 HTML 原生输入框<input type=number min=1 max=100/>实现。

数字：数字编辑器，可用<input type=number min=1.0 max=100.0/>实现

字符串：字符串编辑器，可用<input />实现。

URL：URL 编辑器，可用<input />配合格式校验。

图片：图片编辑器，需要自研图片上传功能。

固定字段对象：对象和字段编辑器，可用多个<input />和<label>实现。

布尔型：开关，可用<select>或者自研组件实现。

自由字段对象：需要自研 KV 输入组件。

数组：需要自研列表组件实现。

对象数组：需要自研表格组件或者列表组件实现。

矩形区域：需要自研区域选择组件。

这里要注意 JSON 是一个级联的格式，所以对象、数组中很可能需要插入各种不同的数据类型的编辑器，这部分技术上有一定挑战。此外，实践中，对象数组很多时候都来自 Excel 数据，Excel 导入也是非常重要的。

第二个难点则是跟服务端 API 的对接，对于服务端系统统一性较好的公司，这不是什么难事，对服务端系统比较奔放的公司，如果服务端 API 调用方式不统一，就非常麻烦了。这一块只能根据实际情况见招拆招，我这里没办法详细介绍，

模板

模板可以简单得理解成挖了许多坑的页面，它一般是由前端工程师来生产的一种实体。与数据之间的连接是数据的格式，对 JSON 格式来说，JSON Schema 是社区接受度较高的一个方案。

最简单的模板可以用字符串模板来设计，复杂一点的模板则可以由 JavaScript 进行渲染，通过约定全局变量名称或者约定调用函数入口做到把数据传递给模板，你可以根据实际需求复杂程度选择合适的方案。

需要注意，在产品设计上，模板可不是“增、删、改、查”那么简单，考虑到实际工程需要，模板必须是版本化的，也就是说，前端每发布一个模板，都需要永久性存储一条记录，并且产品设计上必须保持可以回滚，这样，一旦线上发现问题，可以迅速回滚到一个可工作的版本，有效降低不可用时长。

此外，模板设计还有批量更新的需求，一些运营活动可能包含数百个页面，它们使用同一套模板，产品设计上必须要注意提供批量更新机制。

模块

模块跟模板非常相似，但是从产品的角度，模块是可组合的。跟模板相似的部分如数据连接、版本化发布、批量更新等，这里就不再赘述。

模块化搭建有额外的技术难点，就是可拖拽的模块编辑器，移动端搭建布局相对简单，可以通过简单的自上而下布局和拖拽改变位置来实现。

桌面的模块拖拽比较复杂，一般都会采用一些变通的思路简化设计，如提供几种固定的布局模板，提供布局容器，或者采用纯绝对定位布局。

在一些产品设计中，会先用模块拼成模板，再指定数据源，这种模式中的”模块“，我们认为是一种开发模板的技术方案，跟我们此处讲的产品上的模块概念不同。因为在我们的认知中，模板应该是由前端工程师产生的，具有复用性的一种实体。

页面

不论是模板搭建还是模块搭建，我们的最终生产的目标都是页面。页面同样需要版本化发布，便于回滚。

页面部分实现的难点是跟发布系统的结合，在我们前面讲的所有产品实体中，模板、模块、数据都是存储在搭建系统本身的，但是页面不一样，页面必须要提供线上服务，所以页面是要发布到线上生产环境的。

如我们上一课讲的，假设前端持续集成系统有校验规则，页面也必须经过这个过程。

在我之前的工作中，是通过自建静态 Web 服务器 +CDN 回源的方式来支撑搭建系统的线上应用的。

因为服务器上只发布静态内容，并且有 CDN 挡住用户流量，所以只需要少量几台线上机器即可。

搭建系统的实施

在我工作的实践中，搭建系统的实施可以说是所有系统中最容易的了，对多数公司来说搭建系统是一种刚性需求，只要完成了产品开发，立刻会有大量的用户。

所以只要正确识别了需求，搭建系统的推行几乎完全不需要担心。

搭建系统的监控

作为一个工具型技术产品，搭建系统同样会产生大量有价值的数据，搭建系统的用户访问和生产页面数量是衡量自身的重要指标。

总结

本课我为你讲解了搭建系统，搭建系统是为了应对大量简单页面的生产需求而设计的一种工具型产品，它的目标非常明确，就是快速生产大量的页面。

方案上，它重点和难点在于几个产品实体的设计，数据部分重点在于编辑器和跟服务端 API 的对接，模板部分则主要是版本化和数据的格式定义，模块除了模板的重点，还有拖拽系统，最终产生的页面主要的难点是跟生产环境的对接。

搭建系统的实施主要是把产品在做出来，一般来讲推广是非常自然的事情，最后，搭建系统产生的数据监控关键的指标是用户访问数和生产页面数。

本课的思考问题是，请你分析一下你们公司是否有搭建系统的需求，尝试用本课的知识来设计或者改进一下你们的搭建系统。

47 | 前端架构：前端架构有哪些核心问题？

你好，我是 winter（加微信：642945106 发送“赠送”领取赠送精品课程 发数字“2”获取众筹列表。）

今天我们来谈谈架构。

在传统桌面软件开发中，架构师是一种通过设计架构保证团队能够良好分工和有序工作的岗位。

在工程领域，我们凡是要做点什么事儿，都会有明确的目的性，这个目的性，一定是为了完成生产服务业务的。

为什么桌面软件开发需要架构师和架构设计呢？因为桌面软件开发具有高度的复杂性，如果没有架构，就没法分解成互相耦合低的模块来分工。

所以一般来说，架构是为了分工而存在的。但是到了前端领域，这个问题是否还存在呢？答案是，不存在。

前端是个天然按照页面解耦的技术，在多页面架构中，页面的复杂度大约刚好适合一个人的工作量。（所以，我们的结论是，前端根本不需要架构设计。当然，我这句话是开玩笑的。）

前端不存在分工问题，但是在多人协同时，仍然要解决质量和效率的问题，这就需要组件化了。除此之外还有前端特有的兼容性问题，也是需要从架构的角度去解决的。

对于一些追求极致的团队来说，会挑战“单页面应用”，通过单页面应用来提升用户体验，单页面应用的升级版本是谷歌提出的 PWA，PWA 既是业务方案也是技术方案，在技术层面，它近乎苛刻地规定了网页的各方面的体验标准。

前端领域还有一个特有的生态：框架，第一代前端框架（如 jQuery, PrototypeJS）重点解决了兼容问题和 API 的易用性问题，在现代浏览器普及之后，这些问题逐渐变得不存在或者不重要，所以第二代前端框架（如 Vue，Angular，React）重点解决了组件化问题。选择合适的框架，可以节约架构的成本，还能够享受社区资源。

本节课，我会围绕前端架构的几个核心问题，为你介绍前端架构工作。

首先我们来讲讲组件化

组件化

组件化讲起来是个非常简单的概念，前端主要的开发工作是 UI 开发，而把 UI 上的各种元素分解成组件，规定组件的标准，实现组件运行的环境就是组件化了。

现行的组件化方案，目前有五种主流选择：

Web Component；

Vue；

React；

Angular；

自研。

Web Component 是 W3C 推行的规范，理论上是未来的选项；但是实际上这份标准的状态堪忧，Shadow DOM 的设计比较复杂，一般的前端掌握起来都比较困难。

此外，CSS 也比较难以应用，需要依靠 CSS Houdini。目前来说，我还没有看到那个前端团队实际在使用 Web Component 作为组件化方案。当然，它的优势也非常明显：不需要任何额外的运行时支持，就能在现代浏览器环境运行，也可以跟 HTML 无缝结合。

Vue 是目前最受欢迎的框架（从 github star 来看），由华人程序员尤小右开发和维护。它有两个主要特点，一个是比较符合原本的 JS/CSS/HTML 书写习惯；另一个是它绑定了 MVVM 模式，直接确定了 UI 架构，通过 DSL 的支持，数据交互非常简洁。

React 是 Facebook 推行的新一代 Web 框架。它利用 JSX 模式，把 HTML、CSS 和 JS 都放进了 JS 文件中，对于不喜欢 CSS 和 HTML 的前端工程师来说，是很理想的。它还可以迁移到 React Native，直接编写简单的客户端应用。

Angular 是 Google 推出的 Web 框架，它是比较标准的 MVVM 模式。Angular 曾经因为大版本兼容性而饱受诟病，目前它的核心竞争力是与 TypeScript 结合得较好。

上面是我对几种方案的简单介绍。但是实际上，我们做技术选型时的主要依据是团队的现状，开发移动端还是桌面端、是否跟 Native 结合、团队成员的技能分布都是需要考虑的因素，这些框架本身的特点，目前我认为仅仅是一种偏好选项，而不是关键因素。

兼容性和适配性

前端开发的特有问题就是兼容性，到了移动时代，需要面对不同的机型，我们又需要解决适配性问题。

兼容性问题到 2011 年左右都是前端的主旋律，但是在之后，随着现代浏览器的逐渐普及，兼容性问题逐渐减小，所以我们这里就不多谈兼容性问题了。

适配问题主要适配的是屏幕的三个要素：

单位英寸像素数（Pixel Per Inch，PPI）：现实世界的一英寸内像素数，决定了屏幕的显示质量

设备像素比率（Device Pixel Ratio，DPR）：物理像素与逻辑像素（px）的对应关系

分辨率（Resolution）：屏幕区域的宽高所占像素数

在当前环境下，分辨率适配可以使用 vw 单位解决，DPR 适配则需要用到 CSS 的 viewport 规则来控制缩放比例解决，而 PPI 主要影响的是文字，可以采用 media 规则来适配。

单页应用

前文已经讲过，前端架构的解耦问题不大，因为页面是天然解耦的，但是，大家都知道，浏览器加载 HTML 时是会有白屏过程的，对追求极致体验的团队来说，希望能够进一步提升体验，于是就有了“单页应用（SPA）”的概念。

单页应用是把多个页面的内容实现在同一个实际页面内的技术，因为失去了页面的天然解耦，所以就要解决耦合问题。也就是说，我们要在一个“物理页面”内，通过架构设计来实现若干个“逻辑页面”。

逻辑页面应该做到独立开发和独立发布，一种思路是，每个逻辑页面一个 JS，用一个 SPA 框架加载 JS 文件。

从交互的角度，这并不困难，但是，这里还有一个隐性需求：保持前进后退历史。

一般来说，前进后退历史使用 URL 的 Hash 部分来控制，但是 onhashchange 事件并没有提供前进或者后退信息，目前还没有完美的解决方案，只能牺牲一部分体验。实现单页应用的逻辑页面发布需要改造发布系统，在工程上，这也是一个比较大的挑战。

扩展前端新边界

除了解决现实问题，我认为前端架构的职责还包括扩展前端的边界，所以前端架构还包含了很多 Native 开发任务：如客户端和前端结合的方案 Weex 和 React Native；如前端和图形学结合的方案 GCanvas；如前端的 3D 框架 Three.js，这些都是试图用架构的手段赋予前端新的能力的尝试。

这些具体的尝试涉及很多领域知识，我这里就不做详细介绍了，但是如果你成为了一个前端架构师，我希望你也把“拓展前端边界”当做团队的核心目标之一。

总结

今天我从宏观的角度介绍了前端架构相关的知识，我重点介绍了“组件化”“适配性”“单页应用”三个前端架构需要解决的核心问题，组件化在社区有很多现成的方案，我们需要做的主要工作是框架选型。适配性需要用到 CSS 的几种特性：vw 单位、viewport 规则和 media 规则，单页应用重点是逻辑页面解耦、独立开发和发布和保持前进后退历史。

最后留一个思考问题，你所在的团队有前端架构师吗？如果有的话，他的工作职责是什么？

加餐 | 前端与图形学

（点击视频观看完整分享内容。）

主要内容

关于前端和图形学，我分成了三个部分来讲解。

第一部分是讲前端和图形学有什么样的关系。我们为什么要在前端里引入图形学，这个也是我的一段心路历程。

第二部分相对来说是比较实用的，就是图形学的应用场景。如何在前端的日常的工作中，把图形学的知识用进去，为我们的工作和业务创造价值。

最后一部分是对图形学基础设施的一些建设，目前还是一个比较初级的阶段，但是对大家来说，应该有些思路还是可以去思考的。

前端和图形学

首先讲第一部分前端和图形学，先讲讲缘起。

缘起

2011 年我做了一个分享，当时 HTML5 正火，我讲了这么一个内容叫做 gesture animation，我是用 HTML5 上的 TouchEvent，去模拟当时非常惊艳的 iOS 的操作风格。

2013 年我又开始讲一个叫做 flexible design 的东西，这是针对当时一个非常火的概念提的。那个概念能从最大的屏幕适配到最小的屏幕的一个技术方案，但是从我们当时的实现来看，这个想法是好的，概念也是高大上的，但是从落地上来看非常困难。考虑到现实情况，我提了一个 flexible design 这样的一个次级概念。

这个就是一个弹性、小范围的适配，我们只把不同的尺寸的安卓机和 iPhone 做适配，最后解决了适配的问题，并提出来了一系列的设计原则。所以在 2013 年，我们主要做得还是解决适配问题。

2016 到 2017，我在各种不同的会议上讲了三场演讲，它们的背后其实都是同一个东西，叫做 BindingX。

我是希望提出一个交互领域的通用模型，我把交互抽象成输入、输出和中间的一个表达式。通过三者之间的关系，来建立针对所有交互的领域模型。

我的三场分别是从技术的角度、从架构的角度，和从团队基础设施建设的角度，讲了三次。差不多两年的时间，我一直在研究这个方向。

16 年初的时候，我做了一件事，我让团队的一个小伙伴去找当时所有看起来比较先进的设计，他到网上到处去找，总结了这么一份 PPT。

然后呢，16 年初的时候，我们就对着 PPT 开始研究说，到底哪个东西还是我们现在的基础设施实现不了的。我们用 binding 尽量去实现了。但是还是有一部分在 2016 年是做不到的。

到了 2018 年，我们又做了一次一样的事情，我们把当时的这个 PPT 拿出来，说这个效果还有没有我们做不到的，我们发现整个的效果，我们已经全都能做到了。

当时我们觉得作为前端，至少从底层能力上来说，我们已经建设得很好了，市面上能看到的先进的交互，我们都是可以做出来的。

不过，我还是做了一些思考，其实还是能找到一些做不出来的效果，比如说一些光和影的效果，还是我们现在做不出来的。

浏览器的图形学

对浏览器来说，图形这一条线下来，它大概会是个这样一个依赖关系。

我发现前面觉得自己做不出来的东西，实际上毫无疑问都可以用 OpenGL 的 API 去解决，我觉得它其实除了大家耳熟能详的“做 3D”这一能力之外，是不是还可以用来解决我们在渲染方面的一些问题。

设计稿里的图形学

除了技术的角度，我们也做了另一个角度的分析，我们考察了一下设计师最常用的这个工具，Photoshop。它有一个工具叫滤镜。Photoshop 里能够画出来东西的，都是通过滤镜实现的。

所以，我们做了一些基础渲染型的滤镜，也有一些对图片处理的滤镜，通过对它们的灵活组合，我们可以实现各种各样的图案，比如画个火焰、画个云彩类似的效果。

基于此，我们又做了一些探索。

图形学应用场景

我们把 Photoshop 生成的图片其实分了一些不同的种类。

一种叫做图案，这个它可能是一种重复率比较高的，也可能是不重复，但是它相对来说是一种多用于背景的这样的一种东西。

还有一种，就是 Photo 图片，图片基本上就是拍照拍出来的，比如说这个图里的一个音箱，这个东西你没法去用技术去代替它，就是真实的图像。

还有一种东西叫做形状，比如三角形、圆形、方形，形状已经在浏览器里用了很成熟的技术去实现。

来自设计稿的图形：云雾

那么我们现在重点要去解决的是第一种 pattern。比如我们要实现云雾效果。

首先我们要有一个 noise，小时候看电视这个出雪花就是这样的，那个就是说来自硬件的噪声，当我们把这个噪声做一些处理，放大，放到最大，它就会变成一个模糊的几个块，再放的小一点，就变成几个不动的模糊的块，一直到最后就变成雪花点，但是我们把几张图以一定的比例做一定的叠加，然后就搞定了。

接下来我们看一下对比图，看我们的结果，我们的这个云彩和 Photoshop 这个云彩渲染出来它的形状基本上是一模一样的。

我们打开了一扇新的大门，我们仔细研究发现很多内容都是可以用 shader 去做的，如果这个想法再深入一点，我们不需要用图片了，可以直接用代码去渲染了。

我们可能未来会给设计师提供很多这样的平台、工具，让他直接在我们的这个平台上去做操作，代替原来 Photoshop 的步骤，或者我们对 Photoshop 的文件做一定的处理，来生成这种图案，这是我们的一个思路。

来自数学的图形：几何图形与分形

除了灵感来自 Photoshop 之外，还有些其他的来源，比如几何图形，如果大家看一下这个著名的 Logo，苹果的图标。

这幅图里面有很多的圆、框和螺旋线，它们总能找到一些数学的依据，设计师们做图标的时候都是要讲道理的，不能是凭空手绘的，尤其是这种著名公司的 icon。

大家不要去轻视这个简单的几何图形，简单的几何图形也可以产生一些非常好的效果，除了几何图形之外，来自数学的还有一类，非常著名高端，但是实现起来非常简单的，叫做分形。

很多广告片里面，它会用类似这样的图形做这个背景，分形本来是数学里面的一门学科，分形集合，研究分形图的性质，它的特点是每个部分都是大图的一个相似图形，所以说，它可以无限延伸下去。

分形的代码很简单，就这么多代码，但是它也是画一个像素点的，它可以画成类似雪花这样的东西。

这类的分形图呢，叫 Juila Set ，我们作为前端，我们就照着上面的数学公式把它用代码实现就好，所以非常的方便，为什么我要挑出来 Juila Set 来讲，是因为它有个特点，当你用不同的常数的时候的它会产生非常不一样的图形。

比如说我们要做一个后台的这样的系统，给我们的设计师用，你让它自己调一调这个参数，它可以调出不同的图案。

总之，我们看到了很多的可能性，而 Juila Set 只是分形里面的一个集合，而数学里面的各种各样，能画出来奇怪花纹的东西，绝对不只分形一个，这里有非常大的想象空间。

来自物理的图形：光的衍射

还有一些来自物理的一些灵感，尤其是这种光晕效果，这个光晕效果也是 Photoshop 里面提供的，用这个光晕效果也可以做很多的设计。

这个代码也惊人的简单，我们也不需要把这个东西搞的特别清楚，你知道理解我们可以有这些灵感的来源，就足够了。

相变

相变又是来自于一个新的领域的知识，就是我们可以对图片做一个处理，大家看到这个小人有帽子，作为一个有追求的前端程序员，我们可以用代码去改变它帽子的颜色。这里面涉及一个颜色的知识，叫做 hsv。

一般来说，大家只把 hsv 当作一种写颜色的方式，但我认为它是一个比 rgb 语义更好的颜色表述的方式，hsv 是用了一个色相和明度，和纯度这样的一个概念，我们要想改变一张图的色相，我们就只需要去改变它的色相。

我们把蓝色的色相变到绿色的色相就 OK 了，这里面有一个很复杂的公式，只是写起来有点吓人，其实都是加加减减就好了，我们在 hsv 完成一个相变，我们再把它转回 rgb，这样就实现了我们的这个色相变化的效果。

我们可以把这一点利用到很多场景上，比如说人民币，你从 5 元到 100 元钱，虽然大家觉得它差异很大，但它其实就是一个色相的改变。（我在前面的文章中讲到了同一种鸟颜色的转变，也是如此。）

绿幕

绿幕是电影的技术。如果大家到拍摄现场看拍电影，你会发现他们经常弄一个绿幕在上面，我们也用了一个类似的技术，我们也管它叫做绿幕。

3D 图形

最后提一下 3D 图形，因为这个是业内非常成熟的东西，我就不仔细讲了。就是我们的 ThreeJS 和 BabylonJS 提供的 3D 的效果。3D 的领域，现在是个红海竞争，你写一个引擎基本上跟别的引擎差不多，你有的功能别人都有，除非你与一个很厉害的实验室合作做了一些特殊的优化，但是，我觉得对于工程团队来说，这个代价有点高。

图形学的基础设施

最后讲一下图形学的基础设施，我们做图形的事情，还有些比较现实的问题。

基础设施：GCanvas

比如说阿里巴巴现在已经不用 web view，基本上淘宝里面的页面已经是百分之百 weex 化了，可能就有一两个页面不是，我们面对的一个非常现实的问题，就是我们在用 weex 技术，而它里面是没有 Canvas 的，如果大家没有用 weex，用了 React Native 其实也要面临一样的问题。

没有 Canvas 怎么办？其实还是很简单的道理，做一个，所以我们做了一个叫做 GCanvas 的东西。

基础设施：G3D

业界还有一个东西就是 G3D，它与 ThreeJS 一类，没有什么本质的区别。底层它会有些管理的能力，它也可以交互，我们也做了点选，拖拽、顶点变形这样的能力。

如果你对今天的内容有什么样的想法，你可以给我留言，我们一起分享。

加餐 | 前端交互基础设施的建设

分享内容大纲

Vue、React 等现代前端框架很好地解决了组件化和数据视图解耦问题。而对前端来说，新交互永远是花费时间最多的工作，新交互也是前端团队的自然价值和核心竞争力之一。

在这次话题中，我会分享在交互的基础设施的建设上的一些思考和实践，包括图形图像基础、事件机制与视图层架构模式、交互管理框架等内容。

首先我们要了解一下历史。在 70 年代，大概是 70 年代的尾巴，1979 年左右，有了特别有名的，MVC 架构。

MVC 之后，经过了差不多十几年的发展，到了 90 年代，准确地说应该是 95 年左右的时候，这个有一个公司的 CTO，叫 Mike，Mike 在 MVC 的基础上，提出来了 MVP。

到了 2005 年，2005 年微软的一个架构师，做 WPF 的，提出了 MVVM 模式。

2014 年左右的时候，出现了 FLUX，这个是 Facebook 为了它的 JSX 和 React 提出的一种模式。

后来隔了短短的一年，2015 年，同样是在 React 社区，出现了 REDUX。

对于前端来说，我们为用户创造价值才是特别回答的一个问题，这么多年过去了，前端到底为用户创造了什么价值呢？

这是 70 年代，施乐公司做的一个软件管理的流程图软件，那个时代，整个的界面就是这个样子，施乐已经算比较先进的了。

再到 90 年代，当时这个画面还是很惊艳，按钮键是立体的。现在来看这个东西就有不那么美观了。

2006 年左右的时候，Vista 的界面已经开始有了一个非常大的变化了，这时已经是设计师在主导这个界面的了，但是性能并不佳。

再之后，手机出现了，比如 iPhone 的界面，这时不但交互模式发生了巨大的改变，而且屏幕也变了，甚至我们熟悉的鼠标不见了，变成了触屏。虽然两者之间操作上有一定的相似，但是变化还是非常的。

视图的职责也在演变，70 年代，视图的职责是：任何一个视图，永远不应该知道用户的输入。

我们这个时代的视图则既负责输入，也负责输出，并且与 Model 之间有一个交互。

计算机的功能也在演变。70 年代，计算机主要用来计算。

我们今天计算机主要用来上网，基本上，大家的计算机都是 24 小时联网的，你的手机也是 24 小时联网的，所以计算机的职责在发生变化。

这个变化对于 UI 有很大的影响，1970 年的那个 MVC 那篇论文里的图，model 很大，view 很小，而到了 2018 年，今天我们很多的 model，都是放在服务端的，而今天 model 的大小已经不是说一台机器上能去存的，你存在本地的只是视图展现一点点的 model，这个是很小的一部分的东西。而同时 view 却越来越重要了。

我们来看一下视图的技术。

从最底层的有很多人是做显卡和 drivers，有这样的大佬人才。

还有现在非常流行的 OpenGL 等的 GL 层，做这一层的人非常专业，基本上都集中在各种大公司，最近苹果和安卓还竞争，推出了新一代的这个 GL 架构。

还有一个这个 Draw 层，这一层的内容非常多，基本上就爆发了，skia 是安卓的底层绘制系统，graphics.h 是最早的 C 语言带的一个图形库，基本上相当于一个基础库，还有很多 3D 引擎。

UI Framework 这一层，它提供了一套基本的 UI 结构，有了绘制层， 一般人都不会在绘制层直接去工作，需要有些控件，这层有我们比较熟悉的 Dom。GJI 是 Windows 的图形系统，WPF 也是 Windows 的图形系统。

最上面其实会有一些 DSL，这是描述图形的语言，WPF 对应的就是 XAML，JSX 对应的是 React，HTML 大家都知道了，想说这个视图技术变得越来越复杂，

那么我们的主战场是怎么样的，我们可以看一下淘宝终端技术在各层上的分布状况。

交互体系其实是这里面的一部分，但它不是这里面的全部，我觉得我们要讲这个交互呢，我们还是要做一下抽象的，我们要认识到，交互的本质是什么。

交互的本质是什么呢，我画了一个手和一个眼睛，其实无非是操作和看。

操作最常见的一个抽象的模式就是事件。这个比如说这个 touch-screen 事件，陀螺仪事件，或者是时钟芯片触发的持续事件，这些作为输入。

输出一定是通过属性的形式体现的，在任何一个现在的 UI 框架下，都是通过属性的方式反映出来的。transform 是变形，opacity 是透明度，color 是颜色，这就是一个比较完整的抽象了。你在任意的输入和输出连成一条线后，它都会产生一种效果。

不过直接把陀螺仪得到的参数输入到 transform 里肯定是不行的，它需要有个关系，我们在这里面选择了 Expression。我们可以用 JavaScript 去做计算。我觉得这是一个完备的抽象。

不过这里还有一个坑是需要迈过去的，对计算机理解的输入跟人类理解的输入有非常大的偏差，对计算机来说呢，有多少种硬件，就有多少种输入。

我们发现输入非常复杂，在做基础设施建设的时候，我们在输入上面其实投入了很大的精力，最后出来的是一个更接近于人脑概念的一系列的输入。

比如说，touch 和 gesture，我们知道触屏其实是触屏事件，触屏事件其实非常简单，只有四个，touch start，touch move，touch end，touch cancel 则不太常用。

比如我想摁或者点一个东西，它都是是 touch start，touch move，touch end，如果你要监听这些事件，中间的判断很繁琐，作为交互的基础设施，我们不可能提供这些给我们的前端工程师使用，我们肯定做一些操作。

比如手指移动 10px，我们就认为这个 touch start 到了 pan start，这个后面就是 pan move，pan end 这样，

手指很快离开，那么它就会产生一个 tap 事件。

如果超过 1.5 秒那就一个 press start，如果手指没移呢，就会产生一个 press end，如果手指移了，它还会产生一个 pan start。所以 gesture 已经比 touch 复杂了很多了。

scroll 就在 gesture 的基础上又复杂了一层，它不但手指在屏幕的时候响应，手指离开屏幕的时候它也响应，比如说轮播，它是一个变形的轮播，它在轮播的过程中，不但产生位移，还会产生大小的变化，这就让用户更舒服一些。

还有一个滚动导航，一边滚动出来一个导航，近年来还有一个交互设计，不是滚动到某个位置导航出来，而是一直再往下滚动的时候它不出来，突然往上滚动一下，导航就出来。这个部分还有更难的设计交互，所以我们还需要在 scroll 的基础上再做一层。

我们再来看陀螺仪，它只提供了三个分量，并且它是 0 到 360 度，所以如果不经过任何处理，前端工程师基本上是没有办法用的，比如在某个角度，它可能会突然从 0 跳变成 360 度，这个在数据计算时候非常可怕。

所以我们建立这样一个模型，我们把手机看作这样一个立方体，去计算在空间中对立方体产生的旋转效果，我们拿着立方体上面的一个点呢，去做我们定位的一个依据。

因为我们在用 Weex，所以有一个 Native 跟 JS 通讯的问题，比如说从 gesture 事件到 gesture handler，这一步就会到 JS 去执行，图中我们可以看到这个线，跨过中间 JS 和 Native 的分界线，跨越地非常频繁。

假如一个 Touch move 事件或者 Pan move 事件，你手指每移动一小点它都会触发一次 JS 跟 Native 的一个跨语言通讯，所以说整个的性能会非常差，最后基本上会有 5 毫秒到 10 毫秒左右的一个延迟，有 60 帧的话，每一秒钟有 300 毫秒被占掉了，帧率就下去了。

这就是我们最初开始做 Binding 模式的原因。我们的 Binding 模式，expression 传递一次给 Native，然后它会去做大量的绑定，所有的过程都是由 Native 来完成的，Native 做完了以后，还需要再更新一下 VDOM，所以这操作就完全由 Native 完成，通讯次数就降下来了。除此之外，我们还额外收获了性能上的收益。

我们的结论，其实淘宝一个交互体系是这样的，是以 Binding 为核心，下面的平台支持了 weex，Web，React Native。DSL 上面，我们支持了 View 和 Rax 两种，在上面，是由我们自己建的 Components 体系。

最后，还有一个展望，我们用绘制层相结合，会有更多的想象空间，我们通过各种各样的输入、手势、时间、陀螺仪，我们其实可以去控制矢量图，也可以去控制绘制，这些都是前端未来的想象空间。

如果你对今天的内容有所思考，可以给我留言，我们一起讨论。

新年彩蛋 | 2019，有哪些前端技术值得关注？

大家好，我是 winter，想借这个机会跟大家聊一聊 2019 年前端会发生的一些事情。

首先呢，我觉得可以谈一谈，我特别关注的图形学领域。

进击吧，图形学！

在淘宝工作的后期，我也有很大一部分精力放在了图形学上面，我是深刻地感觉到，在未来，图形学会和前端结合的更紧密。这是由于我觉得图形实际上是 UI 最重要的一部分计算机科学，同时它也是计算机领域里面的一个非常关键的学科。

我们的前端就是做 UI 的这样的一个职能，在未来，我觉得随着前端的工作层级逐渐地下沉，前端能够做到越来越多的事情。我们的图形也会逐渐的成为前端的一个基本的技能。

我在 2018 年已经可以看到很多 2D 和 3D 领域的应用了，我们会用 2D 的东西去做图片的渲染，我们会用 3D 的东西去做一些 3D 场景的呈现，在不同的公司我都会看到很多这样的案例。

另外，我觉得图形学也会大量地吸收以前在 PS 中由设计师做的功能，很多以前我们做得这种效果，包括半透明，包括模糊等等都是在 PS 里面，用图片来实现的。

但我认为在未来，随着图形学技术逐渐地发展，前端可以用代码来实现这样的效果，这样我们就可以不用设计师产出的图片了，而是设计师告诉我们用什么样的效果，我们来写代码把它实现。

这种下沉其实有个好处，我认为当前端工作层下沉到一定的程度，我们就不会有“做不出来这样的效果”这件事发生了。

这件事是很有意义的，今天你去问一个 C++ 的工程师，这个东西能不能做，他只会说这个东西我不能做，他不会说，C++ 不能做，那在我们前端这样的情况往往是刚好相反的，所以说，我觉得未来下沉，技术下沉会给我们前端带来更多的职责，也同时是一种责任。

包管理的救赎

我觉得前端第二个会非常重要的发展趋势就是：包管理。我们都知道，NPM 是一个非常好的包管理。

但是实际上，我们的 NPM 生态里也有很多的问题，包括安全问题，包括依赖太多造成的性能问题。我觉得目前到了一个重要的时间点了，那么 NPM 的这种问题，一定会有人解决。

但这种解决方案，到底是 NPM 的竞争对手会产生一个新的这样的包管理的平台；还是我们的 NPM 生态里面诞生一些更好的方案，最终把问题解决呢？这个就是我没有办法去预测的事情了。

智能研发，前端有责

另一个技术是我比较关注的是 AI 领域和前端的结合，我们的 AI 领域，今天虽然炒得非常火热，但是在我看来，真正应用的比较好的场景，就是视觉和推荐这两个领域。

我觉得智能研发的领域是整个学术界非常少关注的，那么我们的前端应该把这个责任背起来。

我在淘宝已经看到很多这个方面的努力了，我觉得在业界，这个东西在 2019 年产量应该也会越来越成熟，毕竟切图工作是一个机械性的劳动，机械性的劳动最终一定会被机器取代。

所以，我认为未来，我们前端的工作是不应该包含切图这项工作的。我也比较期待这方面能够尽快地产生一些成绩。

最后呢，我觉得其他的技术在 2019 年，应该会是一个稳定发展的状态，包括大家特别关心的三大框架，Vue、Angular、React。我认为应该是”继续繁荣发展，继续稳定发展，最后生态越来越成熟“这样的一个状态。

以上呢，就是我对 2019 年的一些预测和预期了。当然了，我非常希望我们今天中国的前端界能够诞生一些新的想法，能够让我大吃一惊，甚至是让我的预测落空，我觉得我也会非常高兴的。

在最后，再次祝大家新年快乐。你对前端的发展有什么样的判断和预测呢，欢迎给我留言，我们一起讨论。

期中答疑 | name(){}与name: function() {}，两种写法有什么区别吗？

你好，我是 winter。

随着专栏进度过半，我们专栏的评论区留言量也日渐上涨。除了大家的小作业和学习心得，我还看见很多同学们在学习过程中提出了不少问题。

这其实是一种很好的学习方式，通过问题，我们可以对这部分知识记得更为牢固。

所以，我鼓励你在阅读文章之外，多思考，多提问，把自己不懂的地方暴露出来，及时查缺补漏，这样可以更好地吸收知识。同时，你也可以通过回答别人的问题来检验自己对知识的掌握情况。

我们一起来看看，大家都提出什么问题。

1. 老师你好！我语义化标签用得很少，多数用到的是 header、footer、 nav 等语义化标签，想问老师 section 和 div 混合使用，会不会效果不好呢？

答：不会效果不好的，因为本来就是这么用的。遇到不确定的情况，请千万不要乱用标签，用 div 和 span 就好。

2. 我一直看见闭包这个词，但是一直也没有弄清楚它是什么东西，老师可以简单概括一下什么是闭包吗？

答：你可以这样理解，闭包其实就是函数，还是能访问外面变量的函数。

3.“事实上，JavaScript 中的“类”仅仅是运行时对象的一个私有属性，而 JavaScript 中是无法自定义类型的。”

文中说“类”是私有属性，可以具体表现是什么，不是很能理解具体含义？

答：私有属性当然是你无法访问的属性了，但是具体表现的话，还是有的，那就是 Object.prorotype.toString.call(x) 的行为。

无法自定义类型？请问如下编码是属于什么操作，应该怎么理解这个“类”？

function Person（）｛｝

var person = new Person（）；

答：这个代码是定义类的操作，这里注意一下，你千万不要把类和类型的概念混淆。

4. 请教老师在对象中name(){} 等同于name: function() {} ，这两个写法有什么区别呢？

答：这两个写法在使用上基本没什么区别。只有一点区别，就是函数的 name 属性不一样。可以看下这段代码：

var o = {

   myfunc(){}

}

console.log(o.myfunc.name)

我们这里按照你的第一种方法定义了方法，然后输出它的 name 属性，我们看到 name 属性是"myfunc"。

值得一提的是，如果我们给你的第二种方法添加了名字，行为还是不一样，区别在于能否在函数内用名字递归，我们看看代码：

var o2 = {

    myfunc(){

        consoe.log(myfunc); //error

    }

}

var o1 = {

    myfunc: function myfunc(){

        consoe.log(myfunc); //function myfunc

    }

}

o1.myfunc();

o2.myfunc();

这段代码中，我们试着在用两种方式定义的方法中输出函数自身的名字变量，结果是不一样的。

不过现实中，我们几乎不会关心函数的 name 属性，所以不用太在意两种定义方式的区别。

5. 我对于 JavaScript 中 Number 安全整数有个疑问。

MDN 中是（-(2^53-1)~(2^53-1)）, 犀牛书中是（-2^53~2^53）感觉都有道理。

JavaScript 中采用 IEEE754 浮点数标准进行存储， 1 个符号位，11 位指数位， 52 位尾数位。

按照分析，不考虑符号位，尾数位取值 52 个 1 就是表示的最大值了，不会有精度损失，此时指数位代表数值是 52+1023=1075，此时即为 (-(2^53-1)~(2^53-1))。

但是 2^53 这个值，存储的时候尾数是 52 个 0， 指数位为 53+1023=1076，这个值也是刚好没有精度损失的，这时表示的就是（-2^53~2^53）。

用 Math.isSafeInteger() 判断安全数范围和 MDN 中描述一样。所以被问到这个的时候， 感觉两个都是有道理的吧！老师你说对吗？

答：你分析得非常好，我觉得我都没啥可补充的了。这个地方 JavaScript 标准写得也非常模糊，我简单瞄了一下，似乎是用实验的方式来给出的安全数范围。考虑到犀牛书的时效性肯定不如 MDN，应该是参考了某一版本旧引擎给出来的数据。

所以，这类行为我们还是以实测为准吧，我们不必纠结。

6. 老师您好，下面这个自己练习的例子希望您能帮解答：

console.log('sync1');

setTimeout(function () {

    console.log('setTimeout1')

}, 0);

var promise = new Promise(function (resolve, reject) {

    setTimeout(function () {

        console.log('setTimeoutPromise')

    }, 0);

    console.log('promise');

    resolve();

});

promise.then(() => {

    console.log('pro_then');

    setTimeout(() => {

        console.log('pro_timeout');

    }, 0)

})

setTimeout(function () {

    console.log('last_setTimeout')

}, 0);

console.log('sync2');

答：这个例子挺经典的，虽然我觉得这样设计面试题非常不合适，但是我们可以以它为例，学习一下分析异步的方法。

首先我们看第一遍同步执行，这是第一个宏任务。

第一个宏任务中，调用了三次 setTimeout（Promise 中的代码也是同步执行的），调用了一次 resolve，打印了三次。

所以它产生了三个宏任务，一个微任务，两次打印。

那么，首先显示的就是 sync1、promise 和 sync2。这时，setTimeout1，setTimeoutPromise，last_setTimeout 在宏任务队列中，pro_then 在微任务队列中。

接下来，因为微任务队列没空，第一个宏任务没有结束，继续执行微任务队列，所以 pro_then，被显示出来，然后又调用了一次 setTimeout，所以 pro_timeout 进入宏任务队列，成为第 5 个宏任务。

然后，没有微任务了，执行第二个宏任务，所以接下来顺次执行宏任务，显示 setTimeout1，setTimeoutPromise，last_setTimeout，pro_timeout。

最终显示顺序是这样的。

宏任务 1

微任务 1

sync 1

promise

sync 2

微任务 2

pro_then

宏任务 2

setTimeout1

宏任务 3

setTimeoutPromise

宏任务 4

last_setTimeout

宏任务 5

pro_timeout

7. 为什么 promise.then 中的 settimeout 是最后打印的？不用管是宏任务依次执行吗？

答：因为 then 是第一个宏任务中最后执行的微任务，所以它发起的宏任务是最后入队的，依次执行就是最后。

8. 怎么确定这个微任务属于一个宏任务呢，JavaScript 主线程跑下来，遇到 setTImeout 会放到异步队列宏任务中，那下面的遇到的 promise 怎么判断出它是属于这个宏任务呢？

答：resolve 在哪个宏任务中调用，对应的 then 里的微任务就属于哪个宏任务。宏任务没有从异步队列中取出，中间所碰到的所有微任务都属于这个宏任务。

9. 为什么要设计微任务（micro task），我知道这样 JavaScript 引擎可以自主地执行任务，但这样的好处是什么？提高性能吗？

答：不是，微任务是 JavaScript 引擎内部的一种机制，如果不设计微任务，那么 JavaScript 引擎中就完全没有异步了呀，所以必须要设计微任务。

10. 现在浏览器多数实现是从右往左匹配的，那么无法保证选择器在 DOM 树构建到当前节点时，已经可以准确判断当前节点是否被选中。现在浏览器又是怎么实现在生成 DOM 树，同时进行 CSS 属性计算？

答：其实现代浏览器已经为:empty、:last等伪元素写了很多例外了，不过你说的从右往左匹配，左边的要么是当前节点的父元素，要么是前置元素，所以是可以保证准确判断的呀。

11. 请问老师，页面资源的预加载是不是可以用 link 标签实现，还有其他的方式吗？

答：预加载的方法就多啦，还可以用 JavaScript 代码预加载，甚至用本地存储缓存。

12. 老师，我有一个疑问：“词法环境”和“词法作用域”这两个概念的区别是什么？希望你能帮我解惑。

答：词法环境是运行时概念，词法作用域是语言概念，就是说，作用域指的是变量生效的那段代码，而词法环境是指运行起来之后，你这段代码访问的存储变量的内存块。

13. 想问一个问题：import 进来的引用为什么可以获取到最新的值，是类似于 getter 的机制吗?

答：这个地方略微有些复杂，我们在运行时并没有讲 import 的运行时机制，这里涉及了一个叫做 ImportEntry Record 的机制，它比 getter 的实现更底层。

我想这个地方我们没有必要去深究模块的运行时机制，它很复杂而且并不是经常要用到。你如果想了解的话，可以查阅一下。

14. 请问老师，JavaScript 的 call stack size 是多少，这个 size 的单位是啥，是调用栈中函数的个数，还是一个存储单位，比如 MB 之类的。如果调用栈中就一个函数，这个函数的参数有 100 万个，浏览器端依然会溢出，看起来是存储单位，但是没得到验证。

答：这个似乎并没有什么特别规定，我知道 JSC 里面这个东西是可以用 C++ 代码来调整的，至于浏览器调用 JavaScript 引擎的时候会怎么做，还真不好说。

不过，从编码风格上建议，不要把这种事情用函数解决啦，真要干这样的事，数组可能都不合适了，请老老实实写 ArrayBuffer 吧。

15. 老师您好，我一直有一个困惑，浏览器的鼠标事件是怎么识别到的，是碰撞检测的吗？

答：这个问题很不错，我后面在浏览器 API 的事件部分会详细讲，可以先简单说一下，这里的检测方式是从外到内，逐级分配给子元素，所以我们的事件会有捕获过程。

16. 有个问题，如果我 javaScript 代码改变了 DOM 树元素的位置，需要启动重新排版（位置改变的元素只会影响其他部分元素的位置，甚至不影响其他元素的位置。），这时会导致这棵 DOM 树的所有元素都需要重新排版、绘制和渲染吗？

答：排版应该是会重新排的，但是如果有些元素的尺寸没有改变，那么它内部不需要重排，当然也就更不需要重新渲染了，但是绘制应该是要重绘的，目前来看，浏览器还没有那么智能。

17. 老师，我是 12 年左右踏进半只脚到前端领域的，后来考研就放弃了，觉得前端不够高深，和传统工程师来说觉得门槛低很多，甚至前期我都觉得自己不是个程序员。

直到研究生毕业，才又选择前端，这是三大框架风靡，我却有点迷惘，感觉和自己认知的前端不一样，直到现在工作了差不多两年，才悟出了点道道。

作为工程师，我始终觉得前端也应该熟练算法和数据结构、数据库这些所谓的后端知识，但是平时工作场景中用到又少，不知如何学习？

答：算法和数据结构可不是什么后端知识呀，是所有程序员的基本技能。

算法主要是靠大量练习提高，数据结构可以一个一个学习，不要指望工作中用到恰巧就学了，毕竟学习要教学费而工作是领工资的，哪里会有这样的好事呢，所以还是自己多多练习呀。

18. 重学前端是夯实前端基础，那前端进阶方向在哪里? 还是一定要修一门后端语言扩展服务端，希望老师可以指点迷津。

答：我觉得任何编程相关岗位的进阶方式都是做出某某东西，而不是学会某某东西。我会在专栏课程的第四模块会讲到一些进阶可能的方向，你可以关注一下。

19. 我主业是后端，工作中也会带着做前端，自认还是能完美还原设计师的设计。但是现在感觉很多时候提前端就是 vue 等，而我还是在用 jQuery，想请老师说说看，我是不是落伍了？

答：落伍的问题不是你用什么框架，而是你在做什么东西，学什么东西。

框架不是赶时髦，追潮流，每个框架都有解决的问题，我觉得你该焦虑的不是你用的框架为什么这么老，而是你该知道这些新框架要解决什么问题，以及这些问题为什么在你的工作中不存在。

最后，我们来看看我在 JavaScript 类型那一篇中给你留的实践问题。

如果我们不用原生的 Number 和 parselnt，用 JavaScript 代码实践 String 到 Number，该怎么做呢？

答：其实这个问题我在后台没看到特别满意的答案，好像大家都很喜欢偷懒啊。

我这里给你留个例子，处理十进制整数。

function atoi(a){

    let chars = a.split("").map(e => e.charCodeAt(0) - "0".charCodeAt(0));

    let n = 0;

    for(var char of chars) {

        n *= 10;

    	n += char;

    }

    return n;

}

atoi("1001")

我比较期待大家有人能写出来带小数，甚至带科学计数法的代码，你可以尝试一下。

好了，今天的答疑环节就进行到这里，你也可以把自己想要解答的问题留言。

期末答疑（一）：前端代码单元测试怎么做？

你好，我是 winter。

我们专栏课程的知识部分已经告一段落了。今天，我来集中解决一下大家在学习中遇到的问题，我争取用快问快答的形式，咱们多回答一些问题。

1. 前端代码单元测试还是非常有必要的，不知道老师这一块是怎么做的，能否分享一下？

答：关于单元测试这件事，虽然在业务代码我没做成功过，但是它在框架和基础库开发中起到了非常积极的作用。

我们平时写代码也肯定写一部分就要运行一下试试，其实这种行为就是单元测试，只是我们把很多自动化的内容，用人肉执行了，并且没有保存和管理 case，也没有统计 code coverage。

只要选择一个好的单元测试框架，单元测试成本其实不高，尤其是比较复杂的底层库，引入单元测试不但不会消耗时间，还会提高效率。

所以我认为单元测试要实行，有两个关键点你要格外注意：一是出一套好的单元测试方案，二是在基础库和框架开发中引入单元测试。

2. 关于 Undefined ，如果一个变量没有赋值后面又赋值，这个过程就是“变量会被篡改”的意思么？而 null 为什么又不会被篡改？一个变量开始定义为 null 然后赋值其他数据这个过程不算篡改吗？

答：undefined 是个全局变量，null 是个关键字，在一些版本比较旧的浏览器上，你可以试试：

undefined = 1;

但是你在任何版本的浏览器上，都不能这么干：

null = 1;

这样的话，上面这个代码就会报错了。

3. winter 老师你好，我们公司的前端是 Node.js 写的，如何做性能监控呢，如何做页面加载优化呢，我对您的页面性能打分系统很感兴趣，能详细讲一讲吗？谢谢了。

答：首先，Node.js 写的是服务端代码跟前端性能没有任何关系，Node.js 的性能监控比前端性能监控复杂得多，你可以了解下 alinode。

前端性能打分可能主要包含几个部分。

图片：检查图片数量和图片大小，比如单个超过 50k，总量超过 400k 的图片就要注意了，如果检查到小图片，也可以建议用 data uri 内联。

请求数：检查请求数，检查是否有独立的 JS、CSS 请求，这些都是潜在的优化点。
域名：检查域名是否有在 http dns 的范围内，检查域名数量是否过多，检查资源文件域名是否属于 CDN。

实际加载时间：如果测试环境加载时间过程，也可能说明一些问题。

缓存：检查静态资源是否设置了正确的缓存。

你可以自己动手试试。

4. 有一个疑惑是，大小写的两个属性有什么区别呢，比如：

Screen，screen

Event，event

答：这里面大写的是类，小写的是对象。

5. 我还是比较认同，从左往右匹配的规则，这样就像老师讲的可以在构建 DOM 树的同时来进行匹配 CSS 规则，相当于同时构建渲染树了，而不必等到 DOM 构建完毕再进行 CSS 的规则匹配。

进行构建渲染树，虽然从右向左匹配对于复杂的选择器更优，但是这里面浪费了等待 DOM 构建完毕，才能使用的 CSS 匹配规则。而且相对于同为 id 等单一选择器而言，明显从左向右更具有优势！这是我的一点想法，望老师指正！

答：其实我在之前的分享中做过一个试验，当时的 Chrome 确实是从右往左，如果 #id 在最左，速度会更快，但是那个过程是后添加 CSS 的情况。

6. 程老师你好。我在很多地方看到的说法是：CSS 会阻塞 DOM 渲染，但不会阻塞 DOM 的解析，且 CSS 文件的请求是异步请求。

那么如果按照您所说，DOM 的构建和 CSS 属性的计算是同步的话，head 中 CSS 文件的下载以及 CSSOM 的 rule 部分的构建，应该会阻塞 HTML 的解析和 DOM 的构建。好像这两种说法之间就有了冲突。麻烦程老师有空的时候可以帮忙解释一下，万分感谢。

答：其实你误解了我的意思，DOM 构建的结果到 CSS 属性计算是个流水线，所以 CSS 会阻塞 CSS 计算，不会阻塞 DOM 构建。

7. 老师，我记得有的书上或者是资料上说超过五次的 if else 就最好用 switch case 来替换。这样效率更好。您这里为什么说不用这个呢？

答：我在性能部分讲了，“凡是不做 profiling 的性能优化都是耍流氓”。

具体的话，你可以看一看“43 | 性能：前端的性能到底对业务数据有多大的影响”这一篇。

8. 函数调用和函数执行有什么区别？ 有没有相应的标准？

答：我们一般讲“A 函数调用了 B 函数”“浏览器执行了 B 函数”，所以你看，两者的区别是主语不同，你可以感受一下区别。它们对应的标准都是 ECMA262。

9. 请问下老师，为什么 flexible 布局方案不再维护了呢？这个方案本身存在问题吗？

答：不存在问题，但是 rem 计算是个历史方案，现在，我比较推荐大家使用 vw。

10. 有的工业软件，3D 建模的也可以用浏览器来实现了，用的是 ThreeJS WebGL，老师了解这些嘛？怎么看待这个发展前景。

答：挺好的，但是我觉得这件事由本来做 3D 的工程师转型更方便。

11. 最近出于好奇，我 clone 了 github 上 chrome 的源码仓库，发现竟然有 12G 多，貌似比 linux 内核的源码还多。个人特别想探索一下浏览器源码，但面对如此庞大的代码，不禁望而生畏，也不知从何下手。

请问老师，浏览器内核源码该如何去研究，skia 渲染引擎是最先进吗，svg，canvas，WebGL 该如何选择，怎样深入地学习和掌握它们呢？望老师拨冗答疑，谢谢！

答：你先编译通过吧，然后试着跑起来打断点这样子，其实这件事没有捷径，多花些时间就可以。

skia 引擎是不是最先进，这事我也不好说，其实在工程领域我们一般不追求“最先进”，我们只需要它“足够先进”。

关于如何选择，我收到了很多问题，不过我其实都不做回答，毕竟我没法替你决定你自己的职业发展道路，这个责任实在是有些重大了。

12. 看完老师列出的表格更迷茫了，比如，为什么 transform 属于 level 1，transform 不是 CSS3 里的属性么？level4 是什么？是 CSS4 么？为什么 color 这种常用属性属于 level4? 不应该属于 level1 么？好迷茫哦。

答：我先解释一下你的问题，一方面，level4 里有不等于 level3 里没有，另一个是，W3C 定 level 并不是十分严谨，有从 1 开始，也有从 3 开始的。

除此之外，我想提醒一下，这个表格并不是给你去看 level 的，而是告诉你标准在哪里，让你去读标准的。

13. 请问老师，我后台用的是 Tomcat 服务器，前端用 ajax 请求静态资源时会间隔会报 412，也就是一次成功进入后台，一次报 412，这该怎么解决呢？

答：理论上 412 意思是你浏览器的问题。从你提供的信息我没法给你解决方案，建议你可以搞一个最小复现环境去 Tomcat 社区问。

14. 老师好，有个疑问： WHATWG 和 W3C 标准以哪个为准，这两个标准有什么区别？是不是相互不认可的。

答：这个问题比较复杂，WHATWG 最初是几家不满 W3C，出走搞出来的，后来被招安了把 HTML5 带回了 W3C，不过两边出标准的思路还是不太一样，WHATWG 是 living standard，就是随时更新标准，没有版本的概念，我是个人会倾向于 WHATWG 版本，因为比较新，而且 Chrome 跟得比较紧。

15. winter 老师好，可以给我讲一下那个 presentational attributes ，有些看不懂。

function showThis(){

console.log(this);

}

var o = {

showThis: showThis

}

showThis(); // global

o.showThis(); // o

我知道函数中的 this 是由调用函数的对象决定的，但这种理解总感觉是由结论推原因；老师能不能讲解下。

答：this 是由调用函数的对象决定，这完全是个规定，没有什么原因。（你后面的问题我节选掉了，我其实没太理解到你想表述的意思，你看到了可以再给我留言）。

16. 喜欢使用 let 和 const，看很多库里面，他们都喜欢使用 const，并且推荐使用 const，比如声明一个数组。

老师能讲解下声明一个数组用 let 和 const 有什么区别吗？在操作数组时，都是往数组中 push 值，原先的变量并物理地址并没有发生变化，如果用 let，和用 const 有什么区别，为什么 const 的性能会好一点？

答：性能好一些完全是乱说的，用 const 的话，可以避免你误把数组整个赋值掉，比较有安全感吧。

老师我有几个问题希望你回答：

老师讲到：“注意，我们这里讲的渲染过程，是不会把子元素绘制到渲染的位图上的，这样，当父子元素的相对位置发生变化时，可以保证渲染的结果能够最大程度被缓存，减少重新渲染。”

缓存的是什么东西，位图吗？如何减少重新渲染的？

是不是所有的元素都有对应的位图？

文中你举了两个例子，说如果只有一个位图，任何改变都会引起所有重新渲染。这个好理解，一个大位图就是一个单元，任何的改动相当于这个单元被变了，所以要重新创建这个，这样理解对吗？

但是你下面又说如果不合并，每次还是所有的重新渲染。感觉说的很矛盾啊。希望老师能回答一下我的疑问。

答：缓存的是位图，父子元素位置变化时，因为子元素的位图被缓存，子元素不需要重新渲染，因为父元素的位图也被缓存，父元素也不需要重新渲染，只有父子元素合成起来的位图需要被渲染。

你这样理解是对的，如果不合并，每次改点什么东西，都要重新逐级合成最终的大位图，也相当于重新渲染。

18. 老师，“当没有强制指定文字书写方向时，在左到右文字中插入右到左向文字，会形成一个双向文字盒，反之亦然。”这个能不能给个例子，不明白什么意思。

答：比如你在中文中插入几个阿拉伯语字符，阿拉伯语字符就会形成一个双向文字盒。

19. 看了您的流式数据的解释，是不是可以理解当看到页面渲染的时候，DOM 是不是有可能都还没构建完成呢？即使是理论上，有这种可能吗？

答：渲染不但发生在 DOM 构建之前，甚至可能发生在你下载网页完成之前，比如你上个 XX 软件下载站，是不是下载链接还没见到，就看到广告了（你提到的网上的那段是别人写错了。我节选掉了）。

20. footer 也可以和 aside，nav，section 相关联 (header 不存在关联问题)，不存在关联问题是什么意思啊，不应该在 header 中使用吗？下面又说 header 中可以使用 nav，老师的“关联”是什么意思啊？

答：因为 footer 有自己的语义，footer 必须要指明是谁的 footer，这样 footer 中的作者、地址才有意义，所谓的关联就是这个意思了。

21. 子元素的事件委托给父元素时，添加事件监听的第三个参数直接设置为 true，在捕获过程就判断父元素上的事件目标会比冒泡好些嘛？

答：对，但是你需要在捕获阶段想办法知道事件具体发生在哪个子元素上。

22. 老师 如果我想更深入的了解移动端的兼容问题，有没有好的学习路线呢？

答：兼容问题不是学出来的，你想在山中苦修，自出洞来无敌手？手机早就更新好几代了。所以兼容问题必须靠实践，在一个需要兼容性的业务上，不断积累和更新兼容性知识。

23. 老师，我在理解 SICP 这本书的过程抽象和数据抽象的时候，代入到前端，比如生命周期、事件处理等更多是过程的抽象，能提出一些公用的过程，而数据抽象更多指的是组件内的数据定义、api 的设计、高阶组件等。

请问我这样理解对吗？或者说老师对过程抽象和数据抽象在前端中怎么理解的？如果理解得不对，有什么好的书或者文章帮助理解？

答：SICP 讲的是很实在的编程技巧，在前端中对应的是变量、循环、条件这些东西，SICP 是一本好书，你应该更认真地去阅读它。

24. 通过这个课程老师对于前端的划分为 JavaScript、HTML 和 CSS、浏览器、工程实践几个部分。

疑问 1：现在前端基本把 Node 也包括进来了，对于 Node 的知识应该怎么定位？

疑问 2：Node 属于后端的范畴了，是否应该去学习下 java 相关的后端体系？学习了后端的体系是否会反过来促进前端知识的学习呢。

winter 老师对这两个问题怎么看呢？谢谢。

答：Node 属于后端，electron 属于客户端，不论你学哪个体系，肯定对前端都有促进作用。

但是，你不能认为懂了 Node 就懂了后端，懂了 electron 就懂了客户端，每个领域都可以很深入，知识本来就是学不完的，你能掌握多少只跟投入的时间有关，这道理是很简单的，与其困惑，不如把困惑的时间拿来学习，相信你会有所收获的。

25. 老师，CSS 是如何依附在 DOM 树上的? 是通过前面讲得 Node 实例来实现的?

答：在 Node 实例上添加属性，我在“12 | 浏览器：一个浏览器是如何工作的（阶段三）”讲到了，你可以返回去看看。

26. 清楚为什么要使用语义化标签，是否有必要使用之前，很难让自己有动机去应用每个标签。另外标签新推出如何保持实时掌握最新的标签呢，如何解决浏览器兼容的问题？

答：对语义化标签，我是建议不必追求全部掌握，现有需求再找标签即可。

27. 老师您好，有个疑问想请教一下，为啥基于对象关联风格的面向对象并不是像模拟类的行为的面向对象那么流行呢？原型委托这种的不是应该更符合 JavaScript 语言本身的设计嘛？类似这种：

var father = {

a: 1,

fn(): {

return 123;

}

}

var child = Object.create(father);

child.fn(); // 123

答：大部分人在学校学的都是 C++ 和 Java，我就没见过教原型的，这是现实，没办法。

28. 老师，可否稍加解释一下执行上下文的分类? 网络上的文章说“ JavaScript 中可执行的代码可分为三种类型：全局代码、函数代码、eval 代码，对应三种执行上下文（全局执行上下文、函数执行上下文、eval 执行上下文）。

在 ECMAScript 2018 中没有找到这种说法的依据。我的意思是，我不太清楚这些文章的说法是否正确，是否不够全面。

答：这个说法倒不能说不正确，但是严重过时了。

现在 ECMAScript 2018 里面执行上下文非常复杂，你可以看看各种 Record，重学前端课程里面也没有讲全。

29. 老师，在 ES5 之前版本规范中，会提及 JavaScript 的可执行代码分为全局、函数、Eval。但是在 ES6 之后版本规范中，再也不提及可执行代码的概念了，这是为什么呢？

答：其实这里是重构了一下表达方式，新加入了模块什么的。

30. 试过好多次，找了很多方法 flex 兼容 IE9 以下，每次都失败，有什么好的解决办法吗？

答：如果真有 IE9 兼容需求，我想只能单独写一份 CSS 了。

31. 请问大佬，如何冻结对象，一般什么样的场景可以用到？

答：Object.freeze 函数可以冻结对象。一般都是写底层库，希望避免篡改。

32.

new Promise(function(resovle, reject) {

setTimeout(resovle, duration);

})

setTimeout(resovle, duration); 和 setTimeout(() => {resovle()}, duration);

两者到底有什么区别，想不明白，求教。

答：这个区别不大的，第二种写法一般都是为了给 resolve 传参数，不过如果你不想给 resolve 传特定参数，为什么还要多写一个函数呢。

33. 我听 JavaScript 像听天书，怎么办，是不是要从 0 重新学起呀？

答：理论上，我的课程是把 JavaScript 的内容重新组织了一下，如果平时就在用 JavaScript，我认为问题不大。如果这部分完全听不懂，可能说明确实基础薄弱，建议你从 0 学起。

好了，本期的答疑告一段落，如果你还有其他的问题，或者还是没有弄清楚，你可以继续给我留言，我会继续解答大家的问题，并一起讨论。

期末答疑（二）：前端架构中，每个逻辑页面如何可以做到独立发布呢？

你好，我是 winter。

上一期的答疑文章发布之后，同学们的热情很高，我在后台又收到了一批新的问题，今天我们继续来答疑。

1. 请问前端架构中，每个逻辑页面如何可以做到独立发布呢？

答：首先，我们要知道发布是什么意思，我们平时开发好一个网页，把它放到线上真实对用户服务的机器上，这个过程叫做发布。

正常来讲，前端工程师发布的原材料是 HTML，产出是一个线上的 URL 地址。

各个公司的发布系统差异非常的大，有的是前端发 HTML，有的是前端把代码给服务端同学，改成 JSP 之类的代码，再一起发布。

对于逻辑页面而言，我们需要发布的从 HTML 变成了 JavaScript，这个 JavaScript 代码的作用就是渲染一个页面的内容。同时我们线上还需要一个公共的 HTML 文件来运行这些 JavaScript。

最后这些 JavaScript 文件只要能够做到独立发布，我们就可以认为逻辑页面是独立发布了。

2. 对于一个后端开发者来说，前端最困难的东西永远是 CSS 布局而不是 JavaScript，我们对 CSS 有很大畏惧和恐慌在于：CSS 没有很好的调试工具能解决问题，即使 chrome dev 这样的顶级 debug 工具，有时候也很难解释诸如“为什么页面上的这个盒子在这个地方”这样的问题。感觉 CSS 完全不符合编程的思路，老师有没有办法缓解一下这个问题？

答：其实我在课程中已经解释过了，早期的 CSS 从思路上来说就很别扭， 任何人都会觉得别扭，现在有了 flex 布局，我认为这个问题基本可以算解决了。

3. 最近一直在研究前端性能优化和线上错误收集，收效甚微，老师可以讲解一下大厂是怎么处理的吗？

答：这一部分，首先你需要一个比较通用的日志服务，能接受前端用 HTTP 请求的方式打一些日志进去，一般公司都会有这样的系统，如果没有，就需要新建一个，这部分比较麻烦，需要一定的专业知识。

有了这个日志服务，剩下的就是在每个前端页面插入一个 JavaScript 代码，监听 Window.onerror 可以得到错误，取 window.performance 可以得到性能，拿到以后，打日志就行了。

至于后续怎么去展示，展示了以后又怎么去推动执行，这块就需要你自己根据公司实际情况去找到解决方案了。

4. 老师，想问一下用 float 排版的时候 margin 值在不同的浏览器中会显示不同，是什么原因导致的呢？

答：我是从 IE6 时代过来，我还真没听说过 margin 值有什么兼容问题，你可以拿具体的案例来，我们一起看一看。不过我建议早日切到 flex，我们不要在没价值的事情上浪费生命。

5. 把链接分为超链接类和外部资源类是您自己的理解么，还是官方有这种分类呀，我没找到。老师可以说一下么？

答：不是，链接这个概念来自 HTML 标准（4.6.1 位置）。

https://html.spec.whatwg.org/

我在 HTML 的部分都在讲这个问题，你可以关注一下。

6. 老师，关于线上监控的数据采集和数据显示您有好的插件或者方案推荐？

这个其实没有现成的，这个东西，如果公司从数据安全的角度考虑，一般都不让用第三方的。

7. 老师提到原生构造器无法继承。而阮一峰老师表示，es6 已经可以继承原生构造函数，并且能定义子类。以我的理解来看，阮一峰老师的说法没有问题。不知道老师怎么看？

答：我查了一下，你说的没错，还真是这样，这块我理解错了，我后面会迭代一下。

8. 活动页面样式风格多变，并且有些活动页面是存在交互和购买流程等交互，这些交互怎么做成模板化？

答：这个答案很简单，只要能传参数，就能做成模板化。

9. DOM 树就是一种嵌套的数据结构吗？然后是渲染引擎将这个数据结构处理成我们看到的网页吗 ？

答：DOM 树是嵌套的树形结构，渲染过程是把它变成位图，绘制过程是把它画到显示器上。关于这部分内容，你可以回顾一下浏览器原理部分的几节课。

10. 浏览器中大多数的对象都原型继承自 Object，是否可以根据原型继承关系将 Window 上面的 api 绘制成一颗树？有了这些继承关系是否更容易理清这些全局属性呢？

答：API 不止有类，也有很多函数呀。所有的函数 API 的原型都是 Function.prototype，这不就没意义了么。

11. “宿主对象（host Objects）：由 JavaScript 宿主环境提供的对象，它们的行为完全由宿主环境决定。”

但是下面对宿主对象的解释又是：“实际上，这个全局对象 windows 上的属性，一部分来自 JavaScript 语言，一部分来自浏览器环境”。这并不像上面说的完全由宿主环境决定，这个怎么理解呢？

答：请注意看了，我这里讲宿主对象有个“们”字。

所以，这里可不是对宿主对象的解释，这是对 Window 对象的解释。Window 对象比较复杂，这块我没有详细讲，从 JavaScript 的 Global Object 的角度可以讲，浏览器部分还有个 Window Proxy 机制，我是觉得复杂又没什么实际意义就没有讲，你可以这样感性地理解一下：全局对象和 Window 对象合成了一个东西。

12. winter 老师，我看到淘宝用了 iframe 标签，能给我讲一下这个标签的使用场景和注意点吗？

答：我可以简单告诉你一个口诀：手机上不要用，PC 上除了历史包袱不要用。

13. 请问下，link preload 解析执行时机和构建 CSSOM 一样吗，HTML 从上往下解析到 link preload 才会解析执行？还是说并行解析 HTML 和 preload？

答：从标准来看应该是可以并行，但是具体怎样这块需要看浏览器源代码确定。

14. 老师，在 ES5 之前版本规范中，会提及 JavaScript 的可执行代码分为全局、函数、Eval。但是在 ES6 之后版本规范中，再也不提及可执行代码的概念了，这是为什么呢？

答：它们还在，只是现在执行过程更复杂了，没法这么简单分类了。

15. winter 老师，你提倡多继承吗？还是说尽量用聚合来解决问题？我看着 ES6 里面要实现多继承的方式也挺别扭的。

答：我不提倡多继承，Java 也不支持多继承啊。如果你想抽象可以用接口来代替，想复用可以用 Mixin 来代替。

16. 我这里还有个问题，使用 figureCaption 标签后，img 标签的 alt 属性可以缺失么？我一直觉得 alt 与 src 情同手足，什么都不应该把彼此拆散的。

答：不可以缺失，这是两个意思，figureCaption 可能是“图 1”这种东西，可不一定在描述图片内容。

17. 老师，style 既然也可以这么用<style>css 规则</style>，为什么没有 <style src=“”></style>？

答：你这个设计得不错，但是估计有了 link 以后，HTML 懒得把 style 改成 replacement 了吧。

18. 老师好，想请问下，业务场景中需要嵌入公司其他行业线的页面，这种不使用 iframe 该怎么办？

答：理论上应该让他们给你做个组件出来，但是如果实在没别的办法，就使用 iframe 吧。

19. 老师，目前有办法通过脚本反射的方式获取所有 JavaScript 原生对象吗？还是只能查文档？通过 for. in 遍历全局对象是不可以的，因为这些 JavaScript 原生对象虽然挂到了全局，但是属于不可枚举成员。

答：我们现在可以用 Object.getOwnPropertyNames，但是你 symbol 还是拿不到。

20. 老师，jquery ajax 同步请求的原理是? 目前用 axios 库，不支持同步请求，如果希望执行同步请求有什么解决办法？

答：原理是 XMLHttpRequest 这个可以传第三个参数，但是我不建议你用同步请求，会把 JS 执行线程卡住。

21. 老师您好，把 JavaScript 代码缓存在 localStorage 中，从 localStorage 取出后怎么执行？ 如果缓存的是 CSS 呢？

答：执行 JavaScript 用 eval，执行 CSS 用 document.createElement(“style”)。

22. 请问：var,let 和 const 在 babel 中都会被编译为 var, 那怎么区分 const 是常量呢？

答：如果你用 babel 的话，就不要管编译后的代码。

23. DOM 树构建与 CSSOM 构建有先后关系吗？CSS 计算与 DOM 树流式构建同步进行是不是意味着 DOM 树流式构建之前，CSSOM 已经构建完成呢？

答：我这里说的确实有点歧义，我在这里再厘清一下。CSSOM 是有 rule 部分和 view 部分的，rule 部分是在 DOM 开始之前就构件完成的，而 view 部分是跟着 DOM 同步构建的。

24. 老师能解释下这个么？

var b = 10;

(function b(){

b = 20;

console.log(b); // [Function: b]

})();

答：这个地方比较特殊，“具有名称的函数表达式”会在外层词法环境和它自己执行产生的词法环境之间，产生一个词法环境，再把自己的名称和值当作变量塞进去。

所以你这里的 b = 20 并没有改变外面的 b，而是试图改变一个只读的变量 b。这块儿的知识有点偏，你仅做理解掌握就好。

25. 关于状态机这一块，我觉得是不是可以先讲一节正则的知识点呢。理解了正则，那么大家对状态机的概念就有了更加直观的理解了。

答：一般正则都是状态机实现的，讲正则对理解它底层的状态机并没有多少意义。

当然了，词法分析也可以用正则来实现，我这里没有这么做而已，我写过一个 JavaScript 的词法分析是用正则做的，你可以参考这里:

https://github.com/wintercn/JSinJS/blob/master/source/LexicalParser.js

好了，今天的答疑就到这里，如果你还有问题，可以继续给我留言。我们一起讨论。

用户故事 | 那些你与“重学前端”的不解之缘

写在前面的话

不知不觉，专栏已经更新了一半有余了。在两个月的时间里，我收到了很多同学的留言，很开心可以跟大家保持交流，也很惊喜见证了大家的成长。

在整个学习过程中，你有没有一些学习感悟呢，有哪些新的学习收获，又有哪些实践感悟？有没有对前端的知识架构有了更为深刻的认识，是否规划好了自己的前端之路

如果你有想分享的经验，可以在文章下留言，如果你的留言被选中作为用户故事，我们将会为你送出“极客时间”& winter 联名 T 恤一件。分享你的收获，我们一起成长。

下面我们就一起来看看三位同学的分享。

极客时间用户 @阿成 分享

在我进入知乎社区的时候，winter 老师就已经不在这个社区了，虽然是退隐了江湖，但是江湖上还流传着他的传说，后来我在 Google Feed 中看到 winter 老师的开篇词，看完就感觉是我要的东西，还正好赶上打折，就毫不犹豫地买了。

我本科学的是光电信息，所以并不算是科班出身的程序员（虽然也有学 C 啦）。我本身对计算机程序一直很有兴趣，所以从大三开始，我就决定放弃从事本专业，做一名程序员。要说为什么选择前端领域，自然也是烂俗的“门槛低”理由。

不过，我没想到的是，前端的学习曲线到后面是如此陡峭，并且前端发展的速度如此之快，让人目不暇接，也让很多人产生了“技术焦虑”。

在这样的环境中，我也产生了迷茫，作为一名前端开发人员，我到底应该学什么，怎么学。毕业后两年多的时间里，我从使用原生的 HTML/CSS/JavaScript，到现在流行的视图库 Vue/React，看似我好像学会了很多，但是离开这些库，或者说将来这些库被淘汰了，我又剩下了什么呢？

我逐渐意识到，我需要的可能是一些更“底层”的东西。

那么, 什么是“底层”的东西呢，这里可不是指计算机底层，而是指不会被淘汰的东西，永远有用的东西。我认为对于任何程序员来说，最重要的就是：迅速获取某项知识的能力以及动手解决问题的能力。

前者需要我们建立自己的知识体系，也就是 winter 老师在第一讲中提到的，我们需要完善的知识架构。

我们不需要也不可能记住整个架构的知识，但我们却可以在需要的时候想起来这块知识的位置，是干什么的，能帮我们解决什么样的问题，可以通过什么关键词检索到，这就够了。

其实你让我回忆 winter 老师这么多节课的所有内容，我也是好多想不起来的，但多少都留下了印象。我们的大脑容量是有限的，所以“按需加载”也许对人脑也是个不错的策略。

后者其实就归功于 winter 老师精心设计的课后题，光看这些课文总归是差了点什么，只有你动手去做，才能真正理解这节课所讲的内容。

在做的过程中，你多半会遇到困难（这其实也是 winter 老师的本意），如果你通过努力解决掉了，这个提升才是最大的。你的方案不一定是最好的，或者你可能实在没什么头绪，这时候也可以利用搜索引擎去看看别人的方案。

因为我早上到公司要坐一个小时左右的公交，路上我基本上会用耳机听完课（不伤眼），然后剩下的时间去思考回顾一下，想想作业怎么完成。

在中午吃完饭休息的时候，动手做一下，有时候时间不一定够，就利用晚上和第二天的时间完成。每个人的情况都不一样，不过，想要有收获，就得付出多一些。

希望大家都能学有所成，有所收获。

极客时间用户 @峰峰峰 分享

先感谢极客时间给了我这次学习分享的机会，如果文中有不足之处，希望小伙伴们可以多多指导。

我是转行过来学习前端的，目前在前端这个岗位上工作了大概一年半的样子，最初知道重学前端这个专栏，也是因为朋友圈里面的一些大牛分享的，比较好奇，并且老师又是非常有名的 winter 老师，就报了这门课程。

我本人是对前端的技术非常感兴趣的，尤其是原生基础知识，因为我觉得一个人基础的扎实程度，能决定这个人在这个领域所能发展的高度。

刚开始看到课程目录的时候我很吃惊，因为这个目录和我之前所学的所有的课的目录都不一样，从文法、运行时的角度去解释 JavaScript 这门语言，这让我非常兴奋，因为我知道，这正是我所需要的。

通过跟随 winter 老师不断学习，我发现这把我原有的一些知识结构打乱了，但这并不是坏事，因为我以前的学习和知识结构都非常的混乱。

通过这次学习，让我对 JavaScript 的理解更加深入了一些，同时，重新梳理我的知识点也让我的知识结构更加的清晰了起来。

可能是我工作时间尚短的原因吧，我觉得老师的每一节课对于我来说都是一个新的东西。举个例子，就拿第 3 讲和第 4 讲来说吧，标签语义化我一直都知道，但是我在工作中用得很少，并且在理解上也只停留在非常浅的层面上，但是通过老师的讲解，让我理解到了语义化标签真正的含义。

我现在在工作都会开始使用一些我比较熟悉的语义化标签。包括一些浏览器的工作原理，关于 JavaScript 这门语言是面向对象还是面向类的解释，以及 JavaScript 的执行等等，这些知识点都让我受益良多，非常感谢 winter 老师，希望以后还能有机会跟着老师继续学习。

最后，分享一下我的学习方法吧。

先提升知识的广度，再去拓展知识的深度。

养成随时记录的习惯，可以是笔记，可以是代码（一定要写上注释）。

多逛逛技术论坛，有条件的情况下多去实验一下论坛中自己感兴趣的代码。

最后一条也是最重要的一条，坚持每天八小时工作外的学习和锻炼。

以上就是我的学习心得，不足之处还请多多见谅，希望对小伙伴们有帮助。

极客时间用户 @木易杨 分享

我看了老师在“极客 live”的直播，总结了他提到的一些观点。

对于框架的使用没必要花太多时间，应该多研究一下三大框架背后的设计思想。

当一个程序员对算法、语言标准、底层、原生、英文文档这些词汇产生恐惧感的时候，他的技术生命已经走到尽头。

前端架构主要解决的是高复用性，架构能力提升方向主要是组件库开发、前端框架实现等。

对于前端进阶这个问题，其实看书的作用和意义已经不太明显，需要寻找好的平台和合适的项目，在项目中不断克服难题并挑战自己，遇到问题再去查资料总结。如果只是闭门看书那很难成为高手，书只是基础而已，真正的应用还是在项目中。

寒冬中能做的只有提升自己，但是光靠技术是不行的。

推荐 TensorFlow、可视化切图、PWA、WebGL。

TensorFlow 可以了解使用并做点东西出来，原理很难但不影响使用。

PWA 有望进一步发展。

WebGL 在未来会是一个很好的方向，它可以实现任何你想要的界面效果，但重点需要多掌握图形学的基础知识，它和算法，数据结构一样重要。

Weex 和 RN 虽然都叫 Hybird 但不太一样，前者适合大厂主要是嵌入 APP 中使用，后者更适合创业公司。

不推荐 SSR、TypeScript、函数式编程。

SSR 不太看好，其主要是用于 SEO，不太建议用做服务端渲染，其能够使用的场景不多，而且成本代价太大。

TypeScript 是好东西，是很有前景的语言，但适用于十万行以上代码级别的大型项目，小项目并不适合，反而徒增复杂。

用 JavaScript 做函数式编程并不靠谱，Map/Reduce/Redux/Hooks 等并不是函数式编程，只是长得像而已。

PS：笔记内容由自己和群友提供，仅供参考。

我的一些思考和尝试

听完 winter 老师直播后对其观点很是赞同，但因为是面对所有人讲解，所以内容有点多范围有些广，但对个人来说还是找适合自己的方向，在自己相对熟悉的领域再去扩展去突破。横向只是拓宽你的眼界，纵向才是你的核心竞争力。

对我来说感触最大的就是 3、4、5 这几点，因为我一直在思考以下的几个问题。

我现在是高级前端，但又感觉自身很弱，那我如何才能夯实我高级的地基然后成为资深前端呢？

项目迭代节奏快日常加班又多，那我如何做才能平衡工作和学习？

单单提升技术好像还是有很大瓶颈，那我如何提升我的职场核心竞争力？

结合 winter 老师的专栏和最近看的几篇文章，说说我对这几个问题的思考。

对于第一个问题，在 2 个多月前我还是不会写文章的小白，那个时候受到一些文章的触动，尝试把我的前端之路记录下来。

我的想法很简单，就是把前端进阶 28 期的重难点知识全部讲完，目前已经进行到第 4 期了，通过写作把知识通俗易懂的介绍给别人，在这个过程自己肯定会收获很多很多。

这就是我目前在尝试的学习方式，通过写作建立自己的知识架构，并且在这个架构上不断地进行优化，时间到了自然就进阶了。

对于第二个问题，刚开始写作时精力充沛时间也很多，每天都会更新技术文章，但随着项目迭代压力增大，文章更新速度相应就变慢了一些。

虽然文章质量提升了很多，但更新速度从日更变成了周更再变成了双周更，这个说实话我自身已经很难接受了，因为速度变慢导致我年初的计划要打折扣。

所以说做项目和自我学习要如何权衡，是否说我要减少项目难度和时间并在工作中摸鱼去学习呢？

我的结论是不，因为一句话，“最好的学习就是在项目中锻炼自己”。既然我有这么好的项目去锻炼，那为什么还要摸鱼去学习呢，这不就是南辕北辙嘛，道理很简单但不是所有人都懂。

我最近在执行的方法是专注 + 锻炼 + 利用周末。

专注即工作时专注于工作，努力做好每次迭代，遇到难题迎难而上，工作时不开微信，勤用笔记安排日常工作并整理文档。

锻炼即一周抽出三天时间每次去健身房锻炼 1 小时，强壮的体魄才能撑住高强度的工作和学习，因为网易有健身房所以冬天锻炼也没什么问题，没办法去健身房的小伙伴可以尝试跑步或者在瑜伽垫上做 Keep。

利用周末即加大周末和平常晚上熬夜的时间去写文章，减少娱乐的时间，正所谓时间挤一挤总会有的，

但这件事情比较反人性，所以重在坚持。公众号写文章更容易坚持，原因在于文章发布后有正反馈，比如粉丝的增长，留言和鼓励，赞赏等等，这些都更能促使你坚持下去，时间长了自然就养成了习惯。

对于第三个问题，winter 老师也说了，寒冬中光靠技术是不行的，那应该靠什么呢？幸好在最近看到的一篇文章中找到了答案，那就是表达能力。

表达能力是形成自己的框架系统，有理有据并且逻辑清晰，而且能让外人听懂，大部分优秀的人都具备这样的能力。反观自己并没有这样的能力，所以我要努力提升这块，其中最重要的方法就是写作训练。

兜兜转转那么多，其实我一直在尝试的进阶方法就是通过写作建立自己的知识架构体系，同时提高自身的表达能力，通过正反馈机制和锻炼保证我长久的坚持下去并最终养成习惯，习惯这一模式之后变成优秀的人也只是时间问题罢了。

希望我的一些思考和尝试能对你有所帮助。

（直播回顾：https://v.qq.com/x/page/x0830fc8xoj.html）

恭喜幸运用户  @阿成、@峰峰峰、@木易杨，你们的经验被选取成为本期的用户故事。极客时间将送出“极客时间”& winter 联名 T 恤一件。1 个工作日之内，工作人员会与你取得联系。

读完了三位同学的故事后，你有哪些感受呢，留下你的收获，我们一起成长。

答疑加餐 | 学了这么多前端的“小众”知识，到底对我有什么帮助？

你好，我是 winter。这一期我想跟你谈谈前端知识的学习姿势。

课程进展至今，我已经把大部分困难的知识点都讲完了。我在后台收到了一些留言，有的同学针对前端专栏的学习方法和学习方向提出了一些问题，在本期文章中，为了让同学们更好地理解我们专栏的重点，最大程度地吸收知识，在今天的文章中，我会逐一回答同学们在学习方法上的困惑。

1. 老师讲的内容是不是工作中用不到的，对掌握前端的实际工作有什么帮助呢，我们有必要掌握这些比较偏的内容吗？

我自己一直奉行着一个观点，不要执着于知识的“临时”实用性。因为我总是感觉，掌握知识越多的人，更喜欢花费时间学习一些暂时可能无法变现的知识，原因有两点：一是其实学知识花费的时间比想象中的要少，这边在纠结它有没有用，那边可能都学完了；二是知识的实用性其实不怎么好判定，比如当年黎曼搞出非欧几何的时候，全世界都觉得纯粹是数学的思维游戏，直到黎曼死了很多年后，相对论出世，黎曼几何有了实际用途。

不过，这里我还是希望讲清楚，我想通过我们的课程传达一些什么样知识内容。

我们的每一节课标题中，都会带一个有点“小众”的问题，但是，实际上，这个标题主要是引起你学习兴趣的一个引子，解决问题只是我们学习课程的一个自然结果。

我希望的是，通过这个有点偏的问题，引起你对这部分知识领域的关注，知道这部分知识的边界在哪里，从而形成一个完备的知识网络。让你在遇见不会的问题时候，可以快速定位到知识点，达成有效学习。并且，你也可以通过自己之前没有关注过的不同视角，来重新学习一遍这部分的知识。

比如在 JavaScript 课程中，我提供了几个不同的视角来讲解 JavaScript 语言，每一个视角下，都是完整的 JavaScript 知识。

比如说，当我们写下 1+1 的时候，我们从词法的角度看，这是两个数字直接量和一个加法符号，我们从类型的角度看，两个 Number 类型相加得到的也是 Number 类型，我们从语法的角度看，这是一个加法表达式。

我们从不同的维度去认识了 JavaScript 语言，这些视角，远比你记住我们课上讲的细节更重要。如果你记住了“数字直接量可以用科学计数法，E 还可以小写”，却没有学会从词法的角度去分析 JavaScript 语言，那可谓是“入宝山空手而归了”。

2. 我从业时间不长，文章看得迷迷糊糊，是我的基础不够吗？该怎么办？

有一种说法，世界上的知识分成“我知道的”“我知道自己不知道的”和“我不知道自己不知道的”。

重学前端定位是有一定经验的前端工程师，所以它最重要的作用之一，就是帮助前端工程师发现问题，找出知识盲点。

课程设计上，我的主要思路也是“帮助”大家形成自己的体系，而不是“替”大家形成自己的体系。我在前言部分就讲到过，若论丰富全面，有 MDN 文档；若论准确权威，有标准文本，但是，我们课程的作用是传达思路，如果有知识上的缺失，你其实可以通过阅读 MDN 来补全。

3. 如果阅读文章时候有的内容看不懂，该如何学习，如何定位这块的知识呢？

这个问题比较抽象，我认为这个课程设计其实也是一种定位了。

比如，对 JavaScript 问题，先搞清楚看不懂的是词法问题、语法问题、还是运行时问题？定位清楚了问题，你已经距离解决问题前进了一大步。

在这里，我想跟大家说一下：如果你看不懂文章里的某一块知识，你可以给我留言，把具体的位置和知识点告诉我，这样我们可以进行更好的沟通和反馈，从而解决这个问题。如果你只是说不懂，我可能会一头雾水，也无从下手去帮助你解决问题。多做实时、有效的反馈，会让知识吸收的效果更好。

4. 为什么文章里有那么多的术语和英文呢，为什么不换成更通俗的名字呢？

其实，在我们的课程中，有很多次讨论到术语问题，比如“排版”，我们讲了中国古代的活字印刷，比如“渲染”，我们讲到了国画的技法。恰当地使用术语，对于传达知识是非常关键的。

翻译是一项很专业的工作，文学类的翻译讲究信、达、雅，对于技术类的名词，或许“雅”这方面可以稍微打点折扣，但是表意清晰，字句通达仍然是必须的。

很多术语有约定俗成的翻译，当我们阅读不同的文档时，可以快速地通过术语建立联系。并且，有的时候翻译本身也会造成一部分信息的丢失，所以，我有时会直接把英文也写出来，这样有助于你通过原文去理解和对照。

而为什么我不把术语换成更通俗的名字呢？

我们所讲的多数技术，跟现实生活联系不大，这种情况下，“通俗”的名字往往意味着误导。有时候，我们确定术语时，反而会尽量使它远离已有的概念。当然，确定术语并非是我的工作与专长，我们课程中的绝大多数术语，都不是我的发明创造。

5. 标准里有些东西还是看不太懂，如果可以的话，希望老师可以稍微讲解一下如何看懂标准？

我并不推荐每一个前端工程师都去阅读标准，标准一半是写给实现者，一半是写给使用者，这里本来就有很多知识上的落差，多数时候，MDN 是更好的选择。

如果一定要阅读标准，建议从自己做一个极简实现开始，我在浏览器部分，有讲解浏览器相关的知识，在 JavaScript 部分，我还设计了编译原理实验。我想，把它们落到代码上会是一个很好的开始。

6. 接手了一个新项目, 怎么对前端合理规划, 老师能不能提供一些这方面的指导和建议？

这个问题其实跟前端学习关系不大，但是我可以讲讲。

任何规划其实都差不多，得有背景、目标、方案、计划、预期结果。其实在我看来，“项目”是规划的最小单位，在项目中拆出前端来做规划，是不太合适的。

背景和目标通常来自公司的业务，方案跟具体的技术相关，计划是项目管理的领域，最后根据这些来给出预期结果。

再往下细说，一个完整的方案可能包括产品、运营、市场、技术，不同的项目，各个职能的难度不一样，有些项目可能干脆不需要某些方案——比如多数淘宝的产品，首页开个入口就有访问量了，不需要独立去做市场。

具体到技术方案，前端、后端和公司的基础设施都有一定影响，有些公司会找一个架构师来做整体方案，有些公司则是哥几个商量一下边做边出，其实因地制宜最重要，能达到目标的方案都是好方案。

有了整体方案，到前端的一亩三分地上，技术选型、工程规范是绕不开的，有些公司有统一的前端团队，框架和工具都定好了，那么项目里面，就剩下分工和代码设计问题了，也有些公司有些项目具有特殊性，需要特别定制。

实际上，我很难给出具体的“框架选 Vue”，工具用“webpack”这样的建议，因为工程领域本来就是需要很多妥协和权衡的。

不过，在我们课程的最后一部分，我选择了几个典型的基础设施和体系来讲，会给你分享这几个领域中我的认知。

在本篇文章中，我主要针对一些同学在学习上的疑问，给出了我的答案。你对前端的学习方法有什么样的困惑，欢迎给我留言，我们一起讨论。

尾声 | 长风破浪会有时，直挂云帆济沧海

你好，我是 winter（加微信：642945106 发送“赠送”领取赠送精品课程 发数字“2”获取众筹列表。）。

感觉特别长的几个月，忽然间就结束了。对我而言，这是一段奇妙的旅程，对你来说，我希望也是。

去年 9 月我离开原来的公司，打定主意要做一些程序员教育的事情，10 月份在 QCon，我遇到了“极客时间”的编辑，收到邀请来做一个专栏，于是就有了“重学前端”。

最初我答应“极客时间”的时候，其实心里想的是：反正我要做程序员教育，做一个专栏就当整理自己的知识也好。

但是从一开始的磨合和存稿阶段，我就发现这个事情没有我想的那么简单，与以前零散地写博客相比，按时按体系去产出课程需要花费成倍的时间和精力。不断查阅资料，整理代码，“重学前端”这个专栏对我自己来说，也是一个自我重塑的过程。

在这个专栏即将结束的时刻，我想借“重学前端”这个课程谈谈我对教育的理解。

教育是知识的展现形式

现在有一个特别火爆的概念叫做知识付费，其实我不太喜欢这个说法。

“重学前端”所讲的知识点，没有一条是我发明或原创的知识，这么说的原因是：知识就在那里，每个人都可以获得它。

你可以在各种文档和标准中找到它们或者它们的变体。有一些工程领域相关的知识，来自我工作中的实践，有一些也算是首创，但是我不认为这些知识属于我，我只是发现了它们。

所以我认为，知识是免费的，承载它们的教育产品才是收费的。

在整个课程写作中，我致力于在知识之间建立关系。我不希望把我的课程变成抄写文档。我从排版追溯到活字印刷，我从渲染追溯到中国画的技法，还有面向对象的起源。在这个过程中，我自己也颇有收获。我期望这样的一种组织形式能够帮助你更好地理解和记忆知识。

我有时候会觉得，这个把“知识变成课程”的过程，特别像前端工程师把数据变成可见的页面的过程，所以，从某种意义上讲，我做的事情还是前端。

教育的重点在于能力提升而不是知识积累

不过，我也不希望“重学前端”这个课程仅仅是“更容易记忆的知识”。因为在我看来，知识量并非衡量一个工程师优秀与否的因素，能力才是。

我的一点小小的奢望是：“重学前端”这个课程，能给你带来一些建立知识体系的能力。

我在“重学前端”的课程中，穿插了大量的“用代码读标准”的实验。这个小系列，算是我的一种“独门武功”。

我刚开始关注前端的 2007 年左右，国内还少有程序员知道 Web 标准这件事，想要找到资料就更难了，我也经历了最初看标准如同天书的过程。

但是我发现，其实标准中佶屈聱牙、晦涩难懂的那些概念，有着内在的联系，又因为标准文本中用词非常严谨，所以尝试用代码去理解，反而是个快捷的路径，还能找出不少边边角角的冷知识。

通过类似这样的线索，我建立了自己最初的知识体系，我开始对前端的体系的全貌有了粗浅的认知，现在，我希望通过“重学前端”课程，让你也来体会一下这个过程。

教育是一种服务，我们做得显然还不够好

作为老师，我希望这个课程能够真正帮助到你们每一个人，但是其实我们可以看到，每一节课都有一些放弃的同学。

可能有人会说，对平台方和老师来说并无所谓呀，反正付过钱了。但是在我和“极客时间”看来，这正是我们做得不够好的地方。今天“重学前端”这个课程，可能以这样的产品形态呈现出来，凭学员的自觉性来完成学习，实属一种无奈。

不论对我，还是对“极客时间”而言，培养更多优秀的工程师，潜在的价格远远高于卖一些课程得到收入。

我认为，教育是一种服务，理想的教育产品卖的应该是服务的结果，而不是服务的过程。

在未来，我会跟极客邦一起，探索一个全新的教育形态。我们很快会推出第一期线下的课程。如果你具备基本的前端技能，渴望通过自己的努力和我的指导，快速成长为一名高级前端工程师，欢迎你关注我在极客大学线下开设的前端训练营。

另外，虽然正稿已经结束，“重学前端”将来也不会变成一个“过去式”的专栏，我想把它变成一个“Ask me anything”的栏目，不论是后来购买的同学还是已经学完的同学，你们可以来这里的评论区与我交流，不限于课程内容，只要是技术和职业相关的问题，都可以来问我，我会回复或者更新答疑文章来为你解答。

专栏的课程到此告一段落，但是对我而言，这是一个起点。长风破浪会有时，直挂云帆济沧海，希望未来能与你一起遇见更好的自己。