JavaScript,通常简称JS,是一种编程语言,它和HTML、CSS构成万维网(World Wide Web)核心技术。截至2022年,98%的网站在客户端使用JS用于实现网页行为,经常会结合使用第三方库。所有主流的web浏览器都有一个专用的JS引擎,用于在用户设备上执行JS代码。
JS是一种高级语言,通常是即时编译的,并且符合ECMAScript标准。它包含动态类型、基于原型的面向对象、一等公民函数。它是多范式的(范式:是一种根据编程语言的特点对编程语言进行分类的方法),支持事件驱动(一种编程范式,程序的流程是由事件来决定的,比如用户动作、传感器输出、从其他程序或线程输出的消息)、函数式(一种编程范式,程序是通过应用和组合函数构成的)和命令式(一种编程范式,使用语句statement来改变程序状态)编程风格。它有用于处理文本、日期、正则表达式、标准数据结构和文档对象模型(DOM,是一种跨平台和语言无关的接口,用于把XML或者HTML文档看成一个树形结构,其中每个节点是一个对象,代表了文档的一部分。DOM使用一个逻辑树来表示一个文档)的应用程序编程接口(API)。
ECMAScript标准不包含任何的输入/输出(I/O),比如网络、存储或者图形设施( graphics facilities)。实际上,web浏览器或者其他运行时系统(支持程序运行的环境)提供了用于I/O的JS API。
JS引擎(用于执行JS代码的软件组件,第一批JS引擎仅仅是解释器,但是现在所有相关的现代引擎都使用即时编译来提高性能)最开始只用在web浏览器中,但是现在是一些服务器和多种应用的核心组件。这种用法中最流行的运行时系统是Node.js。
尽管Java和JavaScript在名称、语法和各自的标准库方面相似,但是这两种语言是截然不同的,在设计上也有很大的差异。

历史

创建于Netscape

第一款具有图形用户界面的web浏览器,Mosaic,发布于1993年。它让非技术人员可以访问,在新生的万维网的快速发展中发挥了突出的作用。Mosaic的主要开发人员随后成立了Netscape公司(网景),这个家公司在1994年发布了一个更加精致的浏览器Netscape Navigator,这款浏览器很快就成了人们最常用的。
在Web形成(formative)的这些年里,网页只能是静态的,网页在浏览器中加载之后,缺乏实现动态行为的能力。在蓬勃发展(flourishing)的web开发场景中,人们希望能消除这种限制,因此在1995年,Netscape决定在Navigator中添加一种脚本语言(scripting language or script language一种编程语言,用于操作、自定义和自动化现有系统设施功能。脚本语言通常是在运行时解释,而不是编译)。他们通过两种途径来实现这一目标:与Sun Microsystems公司合作,嵌入Java编程语言,同时还聘请 Brendan Eich 嵌入Scheme语言(Lisp编程语言家族的一个方言)。
Netscape管理层很快决定,Eich最好的选择是设计一个新的语言,其语法类似于Java,而不像Scheme或其他现有的脚本语言。虽然这个新语言和它的解释器实现叫做LiveScript,在1995年9月作为Navigator beta版中的一部分首次发布,但是在12月正式发布中,它的名称改为JavaScript。
JavaScript名称的选择引起了混淆,这意味着它和Java是直接相关的。当时,网络泡沫(dot-com boom,dot-combubble,tech bubble or the Internet bubble,是一个20世纪90年代末的股市泡沫,是一个互联网使用和应用的大规模增长时期。)已经开始,Java是热门的新语言,所以Eich认为使用JavaScript这个名称是Netscape的一种营销策略。

被Microsoft采用

微软在1995年首次推出IE浏览器(Internet Explorer),并与Netscape掀起一场浏览器大战。在JavaScript方面,微软对Navigator解析器进行了逆向工程(reverse-engineered),来创建自己的JS,叫做JScript。
JScript首次发布于1996年,同时最初支持CSS和HTML扩展。这些实现中,每一个都明显不同于Navigator中的对应实现。两者之间这些差异使得开发人员很难让他们的网页在这两种浏览器中都能很好地工作,导致“在Netscap中浏览效果最佳”和“在IE中浏览效果最好”的标语被用了很多年。

JScript的兴起

在1996年11月,Netscape向Ecma International(一个为信息和通信系统制定标准的非盈利组织,它于1994年获得了现在的名称,当时ECMA修改了它的名称,为了反映该组织的全球影响力和活动。)提交了JavaScript,作为所有浏览器供应商都可以遵守的标准规范的起点。这使得第一个ECMAScript语言规范在1997年6月正式发布。
这个标准过程持续了好几年,ECMAScript 2于1998年6月发布,ECMAScript 3于1999年12月发布。2000年开始ECMAScript 4的工作。
于此同时,微软在浏览器市场上获得了越来越大的主导地位。到21世纪初,IE的市场份额达到95%。这意味着JScript成为Web上客户端脚本的事实(de facto)标准。
微软最初参与了标准化进程,并在JScript语言中实现了一些提案,但是最终它停止了在Ecma工作上的合作。因此,ECMAScript 4被封存(was mothballed)。

发展(Growth)与标准化

在21世纪初IE占据主导地位期间,客户端脚本是停滞不前的。2004年,当Netscape的继任者Mozilla发布Firefox浏览器时,这种情况开始改变。Firefox受到许多人的欢迎,拿走了IE巨大的市场份额。
在2005年,Mozilla加入了ECMA International,并开始ECMAScript for XML(E4X)标准工作。这使得Mozilla和Macromedia(后来被Adobe Systems收购)合作,后者在他们的ActionScript 3语言中实现了E4X,ActionScript 3是基于ECMAScript 4草案的。目标变成将ActionScript3标准化为新的ECMAScript 4。为此,Adobe Systems将Tamarin实现作为一个开源项目发布。然而,Tamarin和ActionScript 3和现有的客户端脚本有很大的不同,如果没有微软的合作,ECMAScript 4永远都不会实现(fruition)。
与此同时,与ECMA工作无关的开源社区正在取得非常重要的进展。在2005年,杰西·詹姆斯·加勒特(Jesse James Garrett)发布了一篇白皮书,在其中创造了Ajax这个属于,并描述了一系列技术,其中JavaScript是核心,用于创建可以在后台加载数据的应用程序,避免整个页面重新加载的需要。这引发了JavaScript一段复兴时期,以开源库和围绕它们形成的社区为先锋。很多新的库出现了,比如jQuery, Prototype, Dojo Toolkit, and MooTools。
谷歌于2008年推出了Chrome浏览器,它的V8 JS引擎比它的竞争对手更快。其关键的创新在于即时编译(JIT just-in-time compilation),因此其他浏览器供应商需要全面改造它们的引擎来实现JIT。
2008年7月,这些不同的当事人聚集在奥斯陆(Oslo)开会,使得2009年初最终达成协议,将所有相关工作合并起来,推动语言向前发展。结果是于2009年12月发布的ECMAScript 5标准。

接近成熟

这个语言的雄心勃勃的工作持续了几年,随着2015年ECMAScript 6的发布,大量的补充和完善工作最终正式化。
2009年由Ryan Dahl创建了Node.js,这个使得JS在web浏览器之外的使用量大大增长。Node结合了V8引擎,一个事件循环(an event loop),还有I/O API,从而提供了一个独立(stand-alone)的JS运行时系统。截至2018年,Node已经被数百万开发人员使用,而npm是世界上拥有最多模块的软件包管理器。
ECMAScript规范查案目前在GitHub上公开维护,版本通过定期的年度快照生成。 对于这个语言可能的修订通过一个全面的提案进行审查。现在,开发人员单独检查即将推出功能的状态,而不再查看版本号。
当前JS的生态系统有很多库和框架,已建立的编程实践,以及在浏览器之外JS的大量使用。此外,随着单页应用程序(SPA single-page application,)和其他JS密集型(JavaScript-heavy)网站的兴起,已经创建了几个transpilers,旨在帮助开发过程。

商标

“JavaScript”是Oracle Corporation在美国的商标。该商标最初于1997年5月6日颁发给Sun Microsystems, 并于2010年收购Sun时转让给Oracle。

网站客户端应用

JS是Web主流的客户端脚本语言,98%的网站(2022年中期)都使用它。脚本嵌入或包含在HTML文档中,并与DOM进行交互。所有主流的web浏览器都内置了一个JS引擎,用于在用户设备上执行代码。

脚本化行为示例

  • 通过Ajax或WebScoket,在不需要重新加载页面的情况下加载新的网页内容。例如,社交媒体用户不离开当前页面就可以发送和接收消息。
  • 网页动画,比如淡入淡出对象、调整对象大小和移动对象。
  • 玩浏览器游戏。
  • 控制流媒体的播放。
  • 生成弹出广告或警告框。
  • 在将数据发送到web服务器之前,验证web表单的输入值。
  • 记录用户行为相关数据,然后将其发送到服务器。网站所有者可以将使用数据进行分析、广告跟踪和个性化设置。
  • 将用户重定向到其他页面。
  • 存储和检索数据。

库和框架

超过80%的网站使用第三方JS库或web框架进行客户端脚本编写。
jQuery是目前最流行的库,超过75%的网站使用它。Facebook为其网站创建了React库,随后将其作为开源发布,其他网站包括Twitter,现在都在使用它。同样,谷歌为其网站(包括YouTube和Gmail)创建的Angular框架现在是其他人使用的开源项目。
相比之下,术语“Vanilla JS”是指不适用任何库或框架,而完全依赖于标准JS功能的网站。

其他应用

JS的使用已经扩展到孕育它的web浏览器之外。JS引擎现在嵌入到各种其他软件系统中,既可用于服务器端网站部署中,也可以用于非浏览器应用程序里。
促使服务器端JS使用的最初尝试是Netscape Enterprise server和微软的Internet Information Services,但是它们都是小范围的。随着Node.js和其他方法的创建,服务器端JS使用最终在2000到2009年的末期(in the late 2000s)开始增长。
Electron(以前称为Atom Shell,是一个由Github开发和维护的免费开源软件框架。该框架旨在使用web技术(主要是HTML、CSS和JS,但也可使用前端框架和Web Assembly等其他技术)创建桌面应用程序,这些技术都是使用Chromium浏览器引擎渲染,使用Node.js运行时环境作做后端。此外它还是用各种API来实现比如使用Node服务的本机集成,以及进程间通信模块等功能。它最最初是为Atom创建的项目。Electron是几个开源项目的主要GUI框架,包括Atom、Github Desktop、Light Table、Visual Studio Code、WordPress Desktop和Eclipse Theia。)、Cordova(一种移动应用成开发框架)、React Native(是由Meta Platforms, inc创建的一个开源UI软件框架,它用于开发Android、Android TV、iOS、macOS、tvOS、Web、Windows、UWP的应用程序,使开发人员能使用React框架和本机平台功能。它还被用于开发Oculus)和其他应用程序框架已经用于创建使用JS实现行为的应用程序。
其他非浏览器应用程序包括Adobe Acrobat在内,支持编写PDF文档脚本和JS写的GNOME Shell扩展。
JS最近开始出现在一些嵌入式系统中,通常是通过利用Node.js来实现。

特性

除非有特殊说明,否则以下所有特性,对于所有符合ECMAScript的实现都是通用的。

命令式和结构化

JS支持C语言中许多结构化编程语法(比如:if语句、while循环、switch语句、do-while循环等)。一个局部异常是作用域(One partial exception is scoping):最初JS只具有带var的函数作用域。在ECMAScript 2015中使用关键字let和const增加了块作用域。和C语言一样,JS也区分了表达式和语句。和C的一个语法差异是自动插入分号,JS允许省略分号(用于终止语句)。

弱类型

JS是弱类型的,这意味着根据所使用的操作隐式转换某些类型。

省略具体操作符号对应的类型隐式转换规则。

动态

  • 类型
    JS和大多数其他脚本语言一样,是动态类型化的。 类型和值是相关联的,而不是表达式。举个例子,一个变量最开始绑定给一个数字,可以再重新分配给一个字符串。JS支持多种方法来测试对象的类型,包括duck类型(duck测试的一个应用,“如果它走路像鸭子,叫起来像鸭子,那么它一定是只鸭子”,以确定对象是否可以用于特定目的。在duck类型中,如果一个对象具有该雷松所需的所有方法和属性,那么它就是给定类型的的对象)。
  • 运行时评估
    JS包含一个eval函数,它可以在运行时将一个字符串当作语句来执行。

面向对象(基于原型)

Douglas Crockford将JS中的原型继承描述为:
你可以创建原型对象,然后……创建新的实例。JS中的对象时可以变的,所以我们可以给新的实例增加新的字段和方法。这些实例可以作为以后更新的对象的原型。我们不需要类来创建很多相似的对象……对象继承自对象。亥有什么能比这样更面向对象呢?
在JS中,一个对象是一个关联数组,用一个原型来扩展(见下文);每个键给一个对象属性提供名称,有两种语法形式来指定这样的名称:点表示法(obj.x = 10)和括号表示法(obj[‘x’] = 10)。JS可以在运行时增加、重新绑定或者删除属性。一个对象的大多数属性(以及属于对象的原型继承链的任何属性)都可以使用一个for…in循环枚举(列举)。

  • 原型
    JS使用原型来进行继承,而许多其他的面向对象语言使用类来实现。在JS中可以使用原型来模拟很多基于类的特性。
  • 函数作为对象构造函数
    函数既作为对象构造函数,又拥有象征性角色(typical role)。在函数调用前面加上new将创建原型的实例,会从构造函数中继承属性和方法(包括Object原型的属性)。ECMAScript 5提供了Object.create方法,允许显示地创建实例,而不需要自动继承Object原型(较旧的环境可以将原型指定为null)。构造函数的prototype属性指定的对象,用于新对象的内部原型。可以通过修改作为构造函数的函数原型,来增加新的方法。JS的内置构造函数,比Array和Object,也有可以修改的原型。虽然是可以修改Object原型的,但是一般认为这么做是不好的做好,因为JS中的大多数对象会从Object中继承方法和属性,并且它们可能并不希望原型被修改。
  • 函数作为方法
    JS不像很多面向对象语言那样,JS中一个函数定义和一个方法定义没有区别。然而,在函数调用期间会有所区别:当函数作为对象的方法调用时,函数内的局部this关键字是绑定在调用的对象上的。

函数式的

JS中的函数是第一公民;函数被视为对象。正如此,一个函数可以有属性和方法,比如.call().bind()。嵌套函数是一个定义在另一个函数中的函数。每次调用外层函数时,都会重新创建一次嵌套函数。此外,每个嵌套函数形成一个语法闭包(lexical closure):外部函数的语法范围(包括任何常量、局部变量或参数值)成为每个内部函数对象内部状态的一部分,即使在外部函数执行结束后也是如此。JS也支持匿名函数。

Delegative 委派/授权

JS支持隐式和显式的委派。

作为角色的函数 (特征和混合) Functions as roles (Traits and Mixins)

JS原生支持各种基于函数的Role模式(面向角色编程是计算机编程的一种形式,旨在用类似人类对世界的概念理解的术语来表达事物,这样使程序更易于理解和维护)的实现,例如Traits和Mixins。这样的函数通过其函数体(function body)中有至少一个绑定到this关键字的方法,来定义附加的行为。需要附加行为特征的对象,并且附加的行为不是通过原型链共享的,这样的情况,需要通过对这些对象调用call或者apply,来明确地委派角色。

对象组合和继承

虽然显式的基于函数委派确实已经涵盖了JS中的组合,但隐式委派已经在每次遍历原型链时发生,比如,为了找到与对象相关但是不直接输入对象的方法。一旦找到了该方法,就会在这个对象的上下文中调用它。因此JS中的继承由绑定在构造函数原型属性的委派自动化所覆盖。

其他 Miscellaneous

JS是一个零索引的语言。(索引从0开始)

运行时环境

JS通常依赖于一个运行时环境(例如一个web浏览器)来提供对象和方法,脚本可以通过这些对象和方法与环境交互(例如,网页DOM)。 这些环境时单线程的。JS还依赖于运行时环境来提供包含/导入(include/import)脚本的能力(例如,HTML script元素)。这本身不是一个语言特性,但在大多数JS实现中是很常见的。JS一次从一个队列中处理一次消息。JS调用和每个新消息相关联的函数,用函数的参数和局部变量创建调用栈帧。调用栈基于函数的需要收缩和扩张。当函数完成时调用栈为空的情况下,JS将继续队列中的下一个消息。这叫做事件循环,描述为“运行到完成”(run to completion),因为在考虑下一个消息之前,每条消息都已经被完全处理了。然而,这个语言的并发模型将事件循环描述为非阻塞的:使用事件和回调函数执行程序的输入/输出,例如,这意味着JS可以在等数据库查询返回信息时处理鼠标点击。

可变函数 variadic functions

可以向一个函数传递不确定数量的参数。函数可以通过形式参数(formal parameters)和局部arguments对象访问它们。可变函数也可以使用bind方法创建。

数组和对象literals

像很多脚本语言一样,数组和对象(其他语言中的关联数组)都可以用简洁的快捷语法创建。事实上,这些literals(字面表示,一种固定值的表示符号)构成了JSON数据格式的基础。

Regular expressions 正则表达式

JS还以类似Perl的方式支持正则表达式,它为文本操作提供了简洁而强大的语法,比内置的字符串函数更加复杂。

Promises and Async/await

JS支持promise和Async/await来处理异步操作。内置的Promise对象提供了处理promise和将处理程序与异步操作最终结果相关联的功能。最近,在JS规范中引入了组合方法,允许开发人员组合多个JS promise,并根据不同场景进行操作。引入的方法有:Promise.race,Promise.all,Promise.allSettled和Promise.any。Async/await允许以与普通同步函数类似的方式构造异步非阻塞函数。异步、非阻塞代码可以以最小的开销编写,其结构类似于传统的同步、阻塞代码。

Vendor-specific extensions 供应商特定扩展

历史上,一些JS引擎支持过这些非标准功能:

  • 条件catch字句(类似Java)
  • 数组包含(array comprehensions)和生成器表达式(类似Python)
  • 简明的函数表达式(function(args) expr;这个实验性语法早于箭头函数 arrow functions)
  • ECMAScript for XML(E4X),一个扩展,它为ECMAScript添加了原生XML支持(自版本21开始,Firefor不支持)

语法

具体语法和代码实例翻译略。

安全 Security

JS和DOM为恶意作者提供了通过Web在客户端计算机上运行脚本的可能性。浏览器作者使用两个限制将此风险降到最低。第一,脚本运行在沙盒(sandbox)中,在沙盒中,它们只能执行与Web相关的操作,而不能执行诸如创建文件之类的通用编程任务。第二,脚本受到同源策略(same-origin policy)的约束:一个网站上的脚本无法访问发送到另一个网站的用户名、密码或cookie等信息。大多数与JS相关的安全漏洞违反了同源策略或沙盒。
通用JS ADsafe、Secure ECMAScript(SES)的子集提供了更高级别的安全性,尤其是对第三方创建的代码(如广告)。Clouse Toolkit是另一个用于安全嵌入和隔离第三方JS和HTML的项目。
内容安全策略(Content Security Plolicy)是确保网页上只执行可信代码的主要方法。

跨站点漏洞 cross-site vulnerabilities

一个常见的与JS相关的安全问题是跨站点脚本(XSS),一种违反同源策略的情况。当攻击者可以使目标网站(如网上银行网站)在呈现给受害者的网页中包含恶意脚本时,就会出现XSS漏洞。本例中的脚本可以使用受害者的权限访问银行应用程序,可能会泄露机密信息或在未经受害者授权的情况下转移资金。XSS漏洞的一个解决方案是在显示不可信数据时使用HTML转义。
一些浏览器包括针对反射式XSS攻击的部分保护,在这种攻击中,攻击者提供了一个包含恶意脚本的URL。然而,即使是在这些浏览器的用户也容易受到其他XSS攻击,比如那些将恶意代码存储在数据库中的攻击。只有在服务器端正确地设计Web应用程序才能完全阻止XSS。XSS漏洞也可能因为浏览器作者的实现错误发生。
另一个跨站点漏洞是跨站点请求伪造(CSRF)。在CSRF,攻击者网站上的代码欺骗受害者的浏览器,使其采取用户不想采取的行动(did not intend)比如在银行转账。当目标站点仅仅依赖cookie进行请求身份验证时,来自攻击者网站上的代码就可以携带与发起用户相同的有效登录凭证。一般老说,CSRF的解决方案是要求在隐藏的表单字段(而不仅仅是在cookie)中使用身份验证值来验证任何可能产生持久影响(lasting effects)的请求。检查HTTP Referrer头信息也有所帮助。
“JS劫持”(JavaScript hijacking)是一种CSRF攻击,攻击者网站上的script标签利用受害者网站上返回JSON或JS等私人信息的页面。可能的解决方案包括:

  • 对于返回私有信息的任何响应,需要POST和GET请求参数的认证令牌

Misplaced trust in the client 对客户端的错误信任

客户端-服务器(client-server)架构的应用程序的开发人员必须认识到,不受信任的客户端可能处于攻击者的控制之下。应用程序的作者不能假设它们的JS代码会按预期运行(或者根本不会),因为代码中嵌入的任何密码都可能被坚定的对手提取出来。一些影响(implications)包括:

  • 由于原始代码必须发送给客户端,因此网站作者无法完全隐藏其JS的运行方式。虽然代码可以被混淆,但是混淆可以被逆向工程。
  • JS表单验证只为用户提供了方便,而不是安全性。如果站点验证用户同意其服务条款,或从只应该包含数字的字段中过滤掉无效字符,则必须在服务器上进行,而不仅仅是在客户端上。
  • 脚本可以被选择性地禁用,所以不能依赖JS来阻止右键单击图像来保存图像等操作。
  • 在JS中嵌入密码等敏感信息被认为是非常糟糕的做法,因为攻击者可以提取这些信息

对开发人员的错误信任

npm和Bower等包管理系统在JS开发人员中很受欢迎。这样的系统允许开发者容易地管理它们的程序对其他开发者程序库的依赖。开发人员相信库的维护人员会确保它们的安全和保持最新,但是情况并非总是如此。由于这种盲目信任,会出现漏洞。依赖库可能会有新版本,导致依赖库的所有程序中出现错误或漏洞。相反,一个库可以不修补,有已知的漏洞不修复。在一项对133000网站的抽样调查中,研究人员发现37%的网站都包含了一个至少存在一个已知漏洞的库。“在ALEXA,每个网站上使用的最旧的库版本和该库最新可用版本之间的中间延迟(median lag)是1177天,一些仍然在使用的库的开发在几年前就停止了。”另一种可能是,库的维护者可能会完全删除库。这发生在2016年3月,阿泽尔.克苏鲁从npm中删除了他的仓库,这导致了成千上万依赖于他的库的程序和网站崩溃(break)。

浏览器和插件编码错误

JS为各种浏览器功能提供了接口,其中一些功能可能存在缓冲区溢出等缺陷。这些漏洞可以让攻击者编写脚本,在用户系统上运行任何他们希望的代码。此代码绝不限于其他JS应用程序。例如,缓冲区溢出漏洞可使攻击者以超级用户权限访问操作系统的API。这些缺陷影响了主要浏览器,包括Firefox、Internet Explorer和Safari。
插件,比如视频播放器、Adobe Flash和Microsoft Internet Explorer中默认启用的各种Active X控件,也可能存在可通过JS进行攻击的漏洞(此类漏洞过去曾被利用过)。
在Windows Vista中,Microsoft试图通过以有限权限运行Internet Explorer进程来控制bug(比如缓冲区溢出)的风险。谷歌Chrome浏览器类似地将其页面渲染器限制在自己的“沙盒”中。

沙盒实现错误

Web浏览器能够在沙盒之外运行JS,具有例如创建或删除文件所需的权限。这样的权限特权不打算授予来自Web的代码。
给来自于Web的JS错误地授予特权,这在IE和Firefox中产生的漏洞中扮演了一定的角色。在Windows XP Service Pack 2中,微软降级了JScript在IE中的权限。
微软Windows允许计算机硬盘上的JS源文件作为通用、非沙盒程序来启动(参阅:Windows Script Host Windows脚本主机)。这使得JS(比如VBScirpt)在理论上成为特洛伊木马的可行载体,尽管JS特洛伊木马在实践中并不常见。

硬件漏洞 Hardware vulnerabilities

2015年,安全研究人员在一篇论文中描述了一种基于JS的rowhammer攻击的概念验证(proof-of-concept)实现。
2017年,通过浏览器进行的基于JS的攻击被证明可以绕过ASLR。它叫做“ASLR⊕Cache”或者AnC。
2018年,宣布针对Intel和其他处理器中的推测执行的Spectre攻击的论文包括一个JS实现。

开发工具 Development tools

重要的工具随着语言的发展而发展。

  • 每个主要的web浏览器都有内置的web开发工具,包括JS调试器。
  • 静态程序分析工具,如ESLint和JSLint,扫描JS代码以确保符合一组标准和指南。
  • 某些浏览器具有内置的探查器,还创建了独立的分析库,例如benchmark.js和jsbench。
  • 很多文本编辑器都支持JS代码语法高亮显示。

Java

一个常见的误解是JavaScript和Java是一样的。两者确实都有类似C的语法(C语言是他们最戒指的共同祖先语言)。他们通常也是沙盒的(当在浏览器中使用时),JS是根据Java的语法和标准库设计的。特别是,所有Java关键字都保留在原始JavaScript中,JS的标准库遵循Java的命名约定,JS中额Math和Date对象基于Java1.0中的类。
Java和JS首次出现于1995年,但Java由Sun Microsystems的James Gosling开发,JS由Netscape Communications的Brendan Eich开发。
这两种语言之间的差异比它们的相似指出更加突出。Java具有静态类型,而JS的类型是动态的。Java是从编译的字节码加载的,而JS是以人类可读的源代码加载的。Java的对象是基于类的,而JS是基于原型的。最后,Java直到Java8才支持函数式编程,而JS受到Scheme的影响,从一开始就这样做了。

JSON

JSON,或者JavaScript Object Notation是一种通用数据交换格式,定义为JavaScript对象文本(literal)语法的子集。

TypeScript

TypeScript(TS)是JS的严格类型的变体(variant)。TS的不同之处在于为变量和函数引入类型注释,并引入了类型语言来描述JS中类型。除此之外,TS与JS有相同的特性集,可以轻松地将其转换成JS来运行客户端,并与其他JS代码进行交互操作。

WebAssembly

自2017年以来,web浏览器一直支持WebAssembly,一种二进制格式,使JS引擎能够以接近本地(native)速度来执行网页脚本的性能关键部分。WebAssembly代码与常规JS代码在相同的沙盒里运行。
asm.js是JS的一个子集,它是WebAssembly的前身。

Transpliers 转发器

JS是Web的主要客户端语言,许多网站都用了大量的脚本(script-heavy)。因此,已经创建了转发器来转换其他语言编写的代码,这样有助于开发过程。

https://en.wikipedia.aufe.cf/wiki/JavaScript

Single-page application

缩写SPA。一个单页应用是一种web应用程序或网站,它通过使用从web服务器中获取的新数据,来动态重写当前网页来与用户进行交互。目标是更快的交互过渡,让网站用起来感觉更像是一个本地应用。
在一个单页应用中,不会看到网页刷新,相反,所有必需的HTML、JS和CSS代码,要么是由浏览器通过单个页面加载获取的,要么是按需动态加载适当的资源并添加到页面中,通常是为了相应用户操作。

历史

“单页应用"这个术语的起源并不清楚,虽然早在2003年就讨论过这个概念了。
Stuart Morris,是一名威尔士Cardiif大学的编程专业学生,他于2002年,在slashdotslash.com写了自包含(Self-Contained)的网站,这和"单页应用"有着相同的目标和功能。在同年稍晚,Lucas Birdeau, Kevin Hakman, Michael Peachey 和 Clifford Yeh在美国专利8136109中描述了一个单页应用实现。
JS可以在web浏览器中用于展示用户界面(UI)、运行应用程序逻辑,以及与web服务器进行通信。可以使用成熟且免费的库来支持构建单页应用,从而减少开发人员必须编写的JS代码数量。

技术方法

有很多技术可以让浏览器保留一个单个网页(不进行页面刷新),即使是在应用程序需要与服务器进行通信的情况。

文档哈希(Document Hashes)

HTML作者可以利用元素ID来显示或隐藏HTML文档的不同部分。然后,使用CSS,作者可以使用”#target"选择器仅显示浏览器导航到的页面部分。

JS框架

已经采用了SPA原则的web浏览器JS框架和库,比如AngularJS,Ember.js,ExtJS,Knockout.js,Meteor.js,React,Vue.js和Svelte。其中除了ExtJS,其他都是免费的。

  • AngularJS
    AngularJS是一个完全客户端的框架。它的模板是基于双向UI数据绑定的(UI data binding,是一种用来简化GUI程序开发的软件设计模式。UI数据绑定将UI元素绑定到一个应用程序域模型上。大多数框架使用观察者模式作为底层绑定机制。为了有效地工作,UI数据绑定必须处理输入验证和数据类型映射。绑定控件bound control是一个小部件widget,它的值绑定绑定到数据集中的字段,例如,表行中的列。当控件的退出事件exit event触发时,对控件中数据所做的变更将会自动保存到数据库中)数据绑定是一种在模型发生更改时自动更新试图的方法,同样,数据绑定在视图更新时也会自动更新模型。HTML模板在浏览器中编译。编译步骤创建纯HTML,浏览器把它渲染到实时视图中。后续页面视图会重复这一步。在传统的服务器端HTML编程中,控制器和模型等概念在一个服务器进程中交互,来生成新的HTML视图。在AngularJS框架中,控制器和模型状态(model states)在客户端浏览器中维护。因此,可以在不和服务器进行任何交互的情况下,生成新的网页。
  • Ember.js
    Ember.js是一种基于模型-视图-控制器(MVC)软件架构模式的客户端JS web应用程序框架。它允许开发人员通过把常见习惯用法和最佳实践结合到一个框架中来创建可伸缩的单页应用程序。这个框架提供了非常丰富的对象模型、声明性双向数据绑定、计算属性、自动更新由Handler.js提供的模板,以及用于应用程序状态管理的路由。
  • ExtJS
    ExtJS也是一个客户端框架,允许创建MVC应用。它有自己的事件系统、窗口和布局管理、状态管理(存储)和各种UI组件(网格、对话窗口、表单元素等)。它有自己的类系统,其中包含动态或静态加载器。使用ExtJS构建的应用程序既可以单独存在(状态在浏览器中),也可以使用服务器(例如,使用REST API来填充其内部存储fill its internal stores)。ExtJS只有使用localStorage的内置功能,所以大型应用程序需要一个服务器来存储状态。
  • Knockout.js
    Knockout.js是一个客户端框架,它使用基于MVVM(模型-视图-视图模型https://en.wikipedia.aufe.cf/wiki/Model%E2%80%93view%E2%80%93viewmodel)模式的模板。
  • Meteor.js
    Meteor.js是一个专门为SPA设计的全栈(客户端-服务器)JS框架。它的特点是比Angular、Ember或ReactJS的数据绑定更简单,并且使用分布式数据协议和发布订阅模式把数据的变更自动实时传播到客户端,而不用开发人员写任何同步代码。全栈反应性(reactivity)确保了所有层,从数据库到模板,当必要时会自动更新自己。服务器端渲染等生态软件包解决了SEO搜索引擎优化问题。
  • React
    React是一个用于构建用户界面的JS库。它是Facebook、Instagram和一个由个人开发者和企业组成的社区来维护的。React使用了一个新的语言,这个语言混合了JS和HTML(HTML的子集)。一些公司通过Redux(一个JS库)来使用React,这个库增加了状态管理功能,(使用一些其他的库)允许开发人员创建复杂的应用程序。
  • Vue.js
    Vue.js是一个JS框架,用于构建用户界面。Vue开发人员也提供了Vuex来实现状态管理。
  • Svelte
    Svelte是一个用于构建用户界面的框架,它将Svelte代码编译成JS DOM(Document Object Model 文档对象模型)操作,避免了将框架绑定到客户端的需要,并允许使用更简单的应用程序开发语法。

Ajax

截至2006年,使用的技术中最出名的是Ajax。Ajax通过异步请求,从服务器获取XML或JSON数据,比如,是哦那个JS的XMLHttpRequest或者更加现代的fetch()(从2017年起),或者已经不推荐使用的ActiveX Object。和大多数SPA框架所采用的声明式方法不同,使用Ajax,网站直接使用JS或一个JS库,比如jQuery,来操作DOM和编辑HTML元素。一些库让Ajax变得更加流行,比如jQuery,提供了更加简单的语法,并且规范了不同浏览器之间的Ajax行为,这些浏览器历史上有不同的Ajax行为表现。

WebScokets 网络套接字

WebSockets是一种双向实时客户端-服务器通信技术,是HTML5规范的一部分。对于实时通信,它的使用在性能和简单性上由于Ajax。

Server-sent events 服务器发送事件

服务器发送事件(SSEs)是一种技术,通过该技术,服务器可以向浏览器客户端发起数据传送。一旦建立了初始连接,事件流将保持打开状态,直到客户端关闭这个事件流。SSEs是基于传统HTTP发送的,并且具有WebSockets设计上缺少的各种功能,比如自动重新连接、事件ID和发送任意事件的能力。

浏览器插件

尽管这个方法是过时的,但也是可以通过使用浏览器插件技术(比如Silverlight、Flash或Java applets)实现对服务器异步调用的。

数据传输(XML、JSON和Ajax)

向服务器发起的请求往往会返回原始数据(比如XML或JSON),或者新的HTML。在服务器返回HTML的情况下,客户端上的JS会更新DOM的部分区域。当返回的是原始数据时,通常使用一个客户端JS XML/(XSL)进程(对于JSON,则使用模板)将原始数据转换成HTML,然后把HTML用于更新DOM的部分区域。

服务器体系结构 Server architecture

精简服务器体系结构 Thin server architecture

一个单页应用程序SPA将逻辑从服务器移到客户端,web服务器的角色演变成一个纯数据API或者web服务(web service)。这种架构转变,在某些圈子里(in some circles)被称为“精简服务器架构”(Thin Server Architecture),来强调复杂性已经从服务器转移到了客户端,并认为这样最终会降低整个系统的复杂性。

Thick stateful server architecture

服务器将所需的状态保存在页面的客户端状态的内存中。用这个方法,当任何请求到达服务器时(通常时用户操作),服务器会发送适当的HTML和(或)JS,其中包括具体的更改,以使客户端进入新的所需状态(通常会添加/删除/更新客户端DOM的一部分)。同时,在服务器中的状态也会更新。大部分逻辑都会在服务器上执行,HTML通常也会在服务器上渲染。在某些方面,服务器模拟了一个web浏览器,接收事件,并在服务器状态中执行增量修改,这些修改会自动传播到客户端中。这种方法需要更多的服务器内存和服务器处理能力,但是这样的优势在于简化了开发模型,因为:a) 应用程序通常在服务器中完全编码的 fully coded in the server b) 服务器中的数据和UI状态共享在同一内存空间中,不需要定制的 客户端/服务器 client/server 通信网桥(communication bridges)。

Thick stateless server architecture

这是有状态服务器方法的一种变体(variant)。客户端页面通常通过Ajax请求向服务器发送表示其当前状态的数据。使用这些数据,服务器就能重建出网页需要变更部分所对应的客户端状态,并生成需要的数据或代码(例如,JSON或JS),这数据或代码返回给客户端,让客户端进入新的状态,通常根据激发起请求的客户端操作,修改网页的DOM树。according to the client action that motivated the request。
这种方法需要更多的数据发送到服务器,并且可能每个请求要更多的计算资源,来部分或完全重建服务器中的客户端网页状态。同时,这种方法更容易扩展,因为,服务器中没有保存每个客户端网页数据(per-client page data),因此,可将Ajax请求发送到不同的服务器节点,而不需要会话数据或服务器关联。

本地运行 Running locally

某些SPA单页应用程序可以使用文件URI方案(file URI scheme)从本地文件执行。这使用户可以从服务器下载SPA,从本地存储设备运行该文件,而不需要依赖服务器连接。如果像这样的SPA想要存储和更新数据,它就必须要使用基于浏览器的Web存储(Web Storage)。这些应用程序得益于HTML5的进展。

Challenges with the SPA model SPA模型面临的挑战

因为SPA是从无状态页面重绘模型(stateless page-redraw model)演化而来的,浏览器最初就是为无状态页面重绘模型而设计出来的。所以已经出现了一些新的挑战。可能的解决方案(具有不同的复杂性、综合性和作者控制author control)包括:

  • 客户端JS库
  • 专门用于SPA模型的服务器端web框架
  • 为SPA模型设计的浏览器和HTML5规范的演变 (The evolution of browsers and the HTML5 specification, designed for the SPA model.)

搜索引擎优化 Search-engine optimization

由于一些主流的Web搜索引擎的爬虫不执行JS,对于想采用SPA模型的面向公众的网站来说,SEO(搜索引擎优化)历来都是一个问题。
2009至2015年,Google Webmaster Central 提出并推荐了一种“Ajax爬取方案”(Ajax crawling scheme),在对有状态Ajax页面的片段标识符中(in fragment idenfifiers for stateful Ajax pages),使用一个初始感叹号(#!)。SPA站点必须要实现特殊的行为,来允许搜索引擎爬虫提取相关元数据。对于不支持这种URL哈希方案(URL hash scheme)的搜索引擎,SPA的哈希URLs(hashed URLs)让然不可见。包括W3C的Jeni Tennison在内的很多作者都认为使用这些“hash-bang” URI存在问题,因为它们会使得浏览器没有激活JS的用户无法访问页面。它们还破坏了HTTP referer头,因为浏览器不允许发送referer头部中的片段标识符(fragment identifier)。在2005年,谷歌又否了他们之前提出的这个hash-bang AJAX爬虫提议。
或者,应用程序可以在服务器上渲染第一个页面加载,并在客户端上更新后续页面。这在传统上很难,因为渲染代码可能需要在服务器和客户端上用不同的语言或框架编写。使用少逻辑(logic-less)的模板,从一种语言交叉编译到另一种语言,或者在服务器和客户端上使用相同的语言,可能有助于增加可以共享的代码量。
2018年,谷歌推出了动态渲染,可以作为出于索引目而希望给爬虫提供非JS版本(non-JavaScript heavy version)页面的网站另一种选择。动态渲染在客户端渲染的页面版本和为特定用户代理(user agent)提供的预渲染页面版本之间切换。这种方法涉及到web服务器检测爬虫(通过用户代理),并将其路由到渲染器,然后从中为其提供一个更简单版本的HTML内容。
因为SEO兼容性在SPA中不是微不足道的,所以需要值得注意的是,SPA通常不用于需要或想要搜索引擎索引的环境中。使用场景包括将藏在身份验证系统后的私有数据显示出来的应用程序。在这些应用程序是消费品的情况下,应用程序登录页和营销站点通常使用经典的“页面重绘”模型,这为应用程序在搜索引擎查询中显示命中提供了足够多的元数据。博客、支持论坛和其他传统的页面重绘artifacts通常位于SPA周围(sit around),可以为搜索引擎提供相关术语。
2021年,特别是谷歌,普通SPA的SEO兼容性很简单,只需要满足几个简单的条件。现在也可以找到为使用了选择性预渲染的更高级SPA提供的使用指南。
增加服务端和客户端之间共享代码的数量的一种方法是,使用logic-less无逻辑模板语言,比如Mustache或Handlebars。这些模板可以从不同的主机语言渲染,比如服务器上的Ruby和客户端上的JS。然而,仅仅共享模板通常需要复制用于选择正确模板并使用数据填充模板的业务逻辑代码。当只更新页面中的一笑部分时(比如在一个大模板中一个文本输入框的值),从模板渲染可能性能会产生负面影响。替换整个模板也可能会干扰用户的选择或光标位置,而仅更新更改的值可能不会。为了避免这些问题,应用程序可以使用UI数据绑定或细粒度DOM操作,来仅仅更新页面适当的部分,而不是重新渲染整个模板。

客户端/服务器代码分区 Client/Server code partitioning

浏览器历史记录 Browser history

根据定义,SPA是“单个页面”,这个模型打破了浏览器使用“前进”和“后退”按钮进行页面历史导航的设计。当用户按下后退按钮,期望看到SPA中的前一个屏幕状态,但卸载了应用程序的单个页面,并显示了浏览器历史中的前一个页面时,这会造成可用性障碍。 SPA的传统解决方案是根据当前屏幕状态更改浏览器URL的哈希片段标识符。这可以通过JS实现,并在浏览器中建立URL历史事件。只要SPA能从URL哈希中包含的信息灰灰相同的屏幕状态,就会保留与其的回退按钮行为。
为了进一步解决这个问题,HTML5规范已经引进了pushState和replaceState来提供对实际URL和浏览器历史的编程访问。

Analytics 分析

像Google Analytics等工具严重依赖于一个新页面加载所引起的在浏览器中整个新页面的加载。SPA却不是这么工作的。
在第一个页面加载后,所有后续页面和内容变更都由应用程序内部处理,应用程序应该简单地调用又给函数来更新分析包。如果没能成功调用这样的函数,浏览器就永远不会触发一个新的页面加载,不会向浏览器历史中增加任何记录,并且分析包也不知道谁在网站上在做什么。

安全扫描 Security Scanning

和搜索引擎爬虫遇到的问题相似,DAST(dynamic application security testing,动态应用程序测试)工具可能会和JS丰富的应用程序发生冲突。问题可能包括缺少超链接、内存使用和SPA加载的资源,通常由应用程序编程接口或API提供。单页应用程序仍然面临和传统网页相同的安全风险,比如Cross-Site Scripting(XSS),但也有许多其他独特的漏洞,例如通过API的数据暴露、客户端逻辑,以及服务器端安全的客户端实施。为了能有效地扫描一个单页应用程序,DAST扫描器必须要以可靠和可重复的方式导航客户端应用,以便发现应用程序的所有区域,并拦截应用程序发送给服务器的所有请求(比如API请求)。很少有商业工具可以执行此类操作,但是此类工具确实存在。

将页面加载(page load)添加到SPA中

我们可以使用HTML5的history API将页面加载事件加到一个单页应用程序中的。这将有助于集成分析功能。困难在于管理,并确保准确追踪所有的事。这包括检查丢失的报告和重复条目。 一些框架提供了针对大多数提供商的免费分析集成。虽然开发人员可以将它们集成到应用程序中,并确保一切正常工作,但是没有必要从头开始做。

加速网页加载

有些方法可以加快SPA的初始化加载,例如SPA登录页或主页的选择性预渲染、缓存和各种代码拆分技术,包括在需要时延迟加载模块。但它不可能摆脱一个事实,即它需要下载框架,至少是一些程序代码;如果网页是动态的,则将命中数据的API。这是一个“现在付钱给我,或者以后再付钱”的权衡方案。性能和等待时间问题仍然是开发人员必须做出的决定。

页面生命周期

SPA在初始化页面加载中被完全加载了,然后按需从服务器中加载的新页面区域更新现有页面的区域。为了避免过度下载目前用不到的功能,一个单页应用程序通常会按需逐步(progressively)下载更多的功能,或者页面的小片段,或者完整的屏幕模块。
通过这种方式,SPA中的“状态”和传统网站中的“页面”之间就存在了一种类比。因为同一页面中的“状态导航”类似于页面导航,理论上,任何基于页面的网站都可以转换成单个页面(single-page),只替换同一个页面中更改的部分。
web上的SPA方法类似于本地桌面应用中流行的单文档界面(single-document interface)表示技术。

参照 See also

https://en.wikipedia.aufe.cf/wiki/Single-page_application

如何解释transpilers: https://en.wikipedia.aufe.cf/wiki/Source-to-source_compiler