要混淆JavaScipt与浏览器
语言和环境是两个不同的概念。提及JavaScript,大多数人可能会想到浏览器,脱离浏览器JavaScipt是不可能运行的,这与其他系统级的语言有着很大的不同。例如C语言可以开发系统和制造环境,而JavaScript只能寄生在某个具体的环境中才能够工作。
JavaScipt运行环境一般都有宿主环境和执行期环境。如下图所示:
宿主环境是由外壳程序生成的,比如浏览器就是一个外壳环境(但是浏览器并不是唯一,很多服务器、桌面应用系统都能也能够提供JavaScript引擎运行的环境)。执行期环境则有嵌入到外壳程序中的JavaScript引擎(比如V8引擎,稍后会详细介绍)生成,在这个执行期环境,首先需要创建一个代码解析的初始环境,初始化的内容包含:
虽然,不同的JavaScript引擎定义初始化环境是不同的,这就形成了所谓的浏览器兼容性问题,因为不同的浏览器使用不同JavaScipt引擎。不过最近的这条消息想必大家都知道——浏览器市场,微软居然放弃了自家的EDGE(IE的继任者),转而投靠竞争对手Google主导的Chromium核心(国产浏览器百度、搜狗、腾讯、猎豹、UC、傲游、360用的都是Chromium(Chromium用的是鼎鼎大名的V8引擎,想必大家都十分清楚吧),可以认为全是Chromium的马甲),真是大快人心,我们终于在同一环境下愉快的编写代码了,想想真是开心!
重温编译原理
一提起JavaScript语言,大部分的人都将其归类为“动态”或“解释执行”语言,其实他是一门“编译性”语言。与传统的编译语言不同,它不是提前编译的,编译结果也不能在分布式系统中进行移植。在介绍JavaScript编译器原理之前,小编和大家一起重温下基本的编译器原理,因为这是最基础的,了解清楚了我们更能了解JavaScript编译器。
编译程序一般步骤分为:词法分析、语法分析、语义检查、代码优化和生成字节码。具体的编译流程如下图:
分词/词法分析(Tokenizing/Lexing)
所谓的分词,就好比我们将一句话,按照词语的最小单位进行分割。计算机在编译一段代码前,也会将一串串代码拆解成有意义的代码块,这些代码块被称为词法单元(token)。例如,考虑程序var a=2。这段程序通常会被分解成为下面这些词法单元:var、a、=、2、;空格是否作为当为词法单位,取决于空格在这门语言中是否具有意义。
解析/语法分析(Parsing)
这个过程是将词法单元流转换成一个由元素逐级嵌套所组成的代表了程序语法结构的树。这个树称为“抽象语法树”(Abstract Syntax Tree,AST)。
词法分析和语法分析不是完全独立的,而是交错进行的,也就是说,词法分析器不会在读取所有的词法记号后再使用语法分析器来处理。在通常情况下,每取得一个词法记号,就将其送入语法分析器进行分析。
语法分析的过程就是把词法分析所产生的记号生成语法树,通俗地说,就是把从程序中收集的信息存储到数据结构中。注意,在编译中用到的数据结构有两种:符号表和语法树。
符号表:就是在程序中用来存储所有符号的一个表,包括所有的字符串变量、直接量字符串,以及函数和类。
语法树:就是程序结构的一个树形表示,用来生成中间代码。下面是一个简单的条件结构和输出信息代码段,被语法分析器转换为语法树之后,如以下代码:
if (typeof a=="undefined") { a=0; } else { a=a; } alert(a);
如果JavaScript解释器在构造语法树的时候发现无法构造,就会报语法错误,并结束整个代码块的解析。对于传统强类型语言来说,在通过语法分析构造出语法树后,翻译出来的句子可能还会有模糊不清的地方,需要进一步的语义检查。语义检查的主要部分是类型检查。例如,函数的实参和形参类型是否匹配。但是,对于弱类型语言来说,就没有这一步。
经过编译阶段的准备, JavaScript代码在内存中已经被构建为语法树,然后 JavaScript引擎就会根据这个语法树结构边解释边执行。
代码生成
将AST转换成可执行代码的过程被称为代码生成。这个过程与语言、目标平台相关。
了解完编译原理后,其实JavaScript引擎要复杂的许多,因为大部分情况,JavaScript的编译过程不是发生在构建之前,而是发生在代码执行前的几微妙,甚至时间更短。为了保证性能最佳,JavaScipt使用了各种办法,稍后小编将会详细介绍。
神秘的编译器——V8引擎
由于JavaScipt大多数都是运行在浏览器上,不同浏览器的使用的引擎也各不相同,以下是目前主流浏览器引擎:
由于谷歌的V8编译器的出现,由于性能良好吸引了相当的注目,正式由于V8的出现,我们目前的前端才能大放光彩,百花齐放,V8引擎用C++进行编写, 作为一个 JavaScript 引擎,最初是服役于 Google Chrome 浏览器的。它随着 Chrome 的第一版发布而发布以及开源。现在它除了 Chrome 浏览器,已经有很多其他的使用者了。诸如 NodeJS、MongoDB、CouchDB 等。最近最让人振奋前端新闻莫过于微软居然放弃了自家的EDGE(IE的继任者),转而投靠竞争对手Google主导的Chromium核心(国产浏览器百度、搜狗、腾讯、猎豹、UC、傲游、360用的都是Chromium(Chromium用的是鼎鼎大名的V8引擎,想必大家都十分清楚吧),看来V8引擎在不久的将来就会一统江湖,下面小编将重点介绍V8引擎。
当V8编译JavaScript 代码时,解析器(parser)将生成一个抽象语法树(上一小节已介绍过)。语法树是JavaScript代码的句法结构的树形表示形式。解释器 Ignition 根据语法树生成字节码。TurboFan 是V8的优化编译器,TurboFan将字节码(Bytecode)生成优化的机器代码(Machine Code)。
V8曾经有两个编译器
在5.9版本之前,该引擎曾经使用了两个编译器:
full-codegen - 一个简单而快速的编译器,可以生成简单且相对较慢的机器代码。
Crankshaft - 一种更复杂的(即时)优化编译器,可生成高度优化的代码。
V8引擎还在内部使用多个线程:
字节码
字节码是机器代码的抽象。如果字节码采用和物理 CPU 相同的计算模型进行设计,则将字节码编译为机器代码更容易。这就是为什么解释器(interpreter)常常是寄存器或堆栈。Ignition 是具有累加器的寄存器。
您可以将V8的字节码看作是小型的构建块(bytecodes as small building blocks),这些构建块组合在一起构成任何 JavaScript 功能。V8 有数以百计的字节码。比如 Add 或 TypeOf 这样的操作符,或者像 LdaNamedProperty 这样的属性加载符,还有很多类似的字节码。V8还有一些非常特殊的字节码,如 CreateObjectLiteral 或 SuspendGenerator。头文件bytecodes.h(https://github.com/v8/v8/blob/master/src/interpreter/bytecodes.h) 定义了 V8 字节码的完整列表。
在早期的V8引擎里,在多数浏览器都是基于字节码的,V8引擎偏偏跳过这一步,直接将jS编译成机器码,之所以这么做,就是节省了时间提高效率,但是后来发现,太占用内存了。最终又退回字节码了,之所以这么做的动机是什么呢?
每个字节码指定其输入和输出作为寄存器操作数。Ignition 使用寄存器 r0,r1,r2,... 和累加器寄存器(accumulator register)。几乎所有的字节码都使用累加器寄存器。它像一个常规寄存器,除了字节码没有指定。例如,Add r1 将寄存器 r1 中的值和累加器中的值进行加法运算。这使得字节码更短,节省内存。
许多字节码以 Lda 或 Sta 开头。Lda 和 Stastands 中的 a 为累加器(accumulator)。例如,LdaSmi [42] 将小整数(Smi)42 加载到累加器寄存器中。Star r0 将当前在累加器中的值存储在寄存器 r0 中。
以现在掌握的基础知识,花点时间来看一个具有实际功能的字节码。
function incrementX(obj) { return 1 + obj.x; } incrementX({x: 42}); // V8 的编译器是惰性的,如果一个函数没有运行,V8 将不会解释它
如果要查看 V8 的 JavaScript 字节码,可以使用在命令行参数中添加 --print-bytecode运行 D8 或Node.js(8.3 或更高版本)来打印。对于 Chrome,请从命令行启动 Chrome,使用 --js-flags="--print-bytecode",请参考 Run Chromium with flags。
$ node --print-bytecode incrementX.js ... [generating bytecode for function: incrementX] Parameter count 2 Frame size 8 12 E> 0x2ddf8802cf6e @ StackCheck 19 S> 0x2ddf8802cf6f @ LdaSmi [1] 0x2ddf8802cf71 @ Star r0 34 E> 0x2ddf8802cf73 @ LdaNamedProperty a0, [0], [4] 28 E> 0x2ddf8802cf77 @ Add r0, [6] 36 S> 0x2ddf8802cf7a @ Return Constant pool (size=1) 0x2ddf8802cf21: [FixedArray] in OldSpace - map=0x2ddfb2d02309 <Map(HOLEY_ELEMENTS)> - length: 1 0: 0x2ddf8db91611 <String[1]: x> Handler Table (size=16)
我们忽略大部分输出,专注于实际的字节码。接下来我们来一起分析相关的关键字节码:
LdaSmi [1]
LdaSmi [1] 将常量 1 加载到累加器中。
Star r0
接下来,Star r0 将当前在累加器中的值 1 存储在寄存器 r0 中。
LdaNamedProperty a0, [0], [4]
LdaNamedProperty 将 a0 的命名属性加载到累加器中。ai 指向 incrementX() 的第 i 个参数。在这个例子中,我们在 a0 上查找一个命名属性,这是 incrementX() 的第一个参数。该属性名由常量 0 确定。LdaNamedProperty 使用 0 在单独的表中查找名称:
- length: 1 0: 0x2ddf8db91611 <String[1]: x>
可以看到,0 映射到了 x。因此这行字节码的意思是加载 obj.x。
那么值为 4 的操作数是干什么的呢?它是函数 incrementX() 的反馈向量的索引。反馈向量包含用于性能优化的 runtime 信息。
现在寄存器看起来是这样的:
Add r0, [6]
最后一条指令将 r0 加到累加器,结果是 43。6 是反馈向量的另一个索引。
Return 返回累加器中的值。返回语句是函数 incrementX() 的结束。此时 incrementX() 的调用者可以在累加器中获得值 43,并可以进一步处理此值。
V8引擎为啥这么快?
由于JavaScript弱语言的特性(一个变量可以赋值不同的数据类型),同时很弹性,允许我们在任何时候在对象上新增或是删除属性和方法等, JavaScript语言非常动态,我们可以想象会大大增加编译引擎的难度,尽管十分困难,但却难不倒V8引擎,v8引擎运用了好几项技术达到加速的目的:
内联(Inlining):
内联特性是一切优化的基础,对于良好的性能至关重要,所谓的内联就是如果某一个函数内部调用其它的函数,编译器直接会将函数中的执行内容,替换函数方法。如下图所示:
如何理解呢?看如下代码:
function add(a, b) { return a + b; } function calculateTwoPlusFive() { var sum; for (var i=0; i <=1000000000; i++) { sum=add(2 + 5); } } var start=new Date(); calculateTwoPlusFive(); var end=new Date(); var timeTaken=end.valueOf() - start.valueOf(); console.log("Took " + timeTaken + "ms");
由于内联属性特性,在编译前,代码将会被优化成:
function add(a, b) { return a + b; } function calculateTwoPlusFive() { var sum; for (var i=0; i <=1000000000; i++) { sum=2 + 5; } } var start=new Date(); calculateTwoPlusFive(); var end=new Date(); var timeTaken=end.valueOf() - start.valueOf(); console.log("Took " + timeTaken + "ms");
如果没有内联属性的特性,你能想象运行的有多慢吗?把第一段JS代码嵌入HTML文件里,我们用不同的浏览器打开(硬件环境:i7,16G内存,mac系统),用safari打开如下图所示,17秒:
如果用Chrome打开,还不到1秒,快了16秒!
隐藏类(Hidden class):
例如C++/Java这种静态类型语言的每一个变量,都有一个唯一确定的类型。因为有类型信息,一个对象包含哪些成员和这些成员在对象中的偏移量等信息,编译阶段就可确定,执行时CPU只需要用对象首地址 —— 在C++中是this指针,加上成员在对象内部的偏移量即可访问内部成员。这些访问指令在编译阶段就生成了。
但对于JavaScript这种动态语言,变量在运行时可以随时由不同类型的对象赋值,并且对象本身可以随时添加删除成员。访问对象属性需要的信息完全由运行时决定。为了实现按照索引的方式访问成员,V8“悄悄地”给运行中的对象分了类,在这个过程中产生了一种V8内部的数据结构,即隐藏类。隐藏类本身是一个对象。
考虑以下代码:
function Point(x, y) { this.x=x; this.y=y; } var p1=new Point(1, 2);
如果new Point(1, 2)被调用,v8引擎就会创建一个引隐藏的类C0,如下图所示:
由于Point没有定于任何属性,因此“C0”为空
一旦“this.x=x”被执行,v8引擎就会创建一个名为“C1”的第二个隐藏类。基于“c0”,“c1”描述了可以找到属性X的内存中的位置(相当指针)。在这种情况下,隐藏类则会从C0切换到C1,如下图所示:
每次向对象添加新的属性时,旧的隐藏类会通过路径转换切换到新的隐藏类。由于转换的重要性,因为引擎允许以相同的方式创建对象来共享隐藏类。如果两个对象共享一个隐藏类的话,并且向两个对象添加相同的属性,转换过程中将确保这两个对象使用相同的隐藏类和附带所有的代码优化。
当执行this.y=y,将会创建一个C2的隐藏类,则隐藏类更改为C2。
隐藏类的转换的性能,取决于属性添加的顺序,如果添加顺序的不同,效果则不同,如以下代码:
function Point(x, y) { this.x=x; this.y=y; } var p1=new Point(1, 2); p1.a=5; p1.b=6; var p2=new Point(3, 4); p2.b=7; p2.a=8;
你可能以为P1、p2使用相同的隐藏类和转换,其实不然。对于P1对象而言,隐藏类先a再b,对于p2而言,隐藏类则先b后a,最终会产生不同的隐藏类,增加编译的运算开销,这种情况下,应该以相同的顺序动态的修改对象属性,以便可以复用隐藏类。
内联缓存(Inline caching)
正常访问对象属性的过程是:首先获取隐藏类的地址,然后根据属性名查找偏移值,然后计算该属性的地址。虽然相比以往在整个执行环境中查找减小了很大的工作量,但依然比较耗时。能不能将之前查询的结果缓存起来,供再次访问呢?当然是可行的,这就是内嵌缓存。
内嵌缓存的大致思路就是将初次查找的隐藏类和偏移值保存起来,当下次查找的时候,先比较当前对象是否是之前的隐藏类,如果是的话,直接使用之前的缓存结果,减少再次查找表的时间。当然,如果一个对象有多个属性,那么缓存失误的概率就会提高,因为某个属性的类型变化之后,对象的隐藏类也会变化,就与之前的缓存不一致,需要重新使用以前的方式查找哈希表。
内存管理
内存的管理组要由分配和回收两个部分构成。V8的内存划分如下:
垃圾回收
V8 使用了分代和大数据的内存分配,在回收内存时使用精简整理的算法标记未引用的对象,然后消除没有标记的对象,最后整理和压缩那些还未保存的对象,即可完成垃圾回收。为了控制 GC 成本并使执行更加稳定, V8 使用增量标记, 而不是遍历整个堆, 它试图标记每个可能的对象, 它只遍历一部分堆, 然后恢复正常的代码执行. 下一次 GC 将继续从之前的遍历停止的位置开始. 这允许在正常执行期间非常短的暂停. 如前所述, 扫描阶段由单独的线程处理.
优化回退
V8 为了进一步提升JavaScript代码的执行效率,编译器生直接生成更高效的机器码。程序在运行时,V8会采集JavaScript代码运行数据。当V8发现某函数执行频繁(内联函数机制),就将其标记为热点函数。针对热点函数,V8的策略较为乐观,倾向于认为此函数比较稳定,类型已经确定,于是编译器,生成更高效的机器码。后面的运行中,万一遇到类型变化,V8采取将JavaScript函数回退到优化前的编译成机器字节码。如以下代码:
function add(a, b) { return a + b } for (var i=0; i < 10000; ++i) { add(i, i); } add('a', 'b');//千万别这么做!
再来看下面的一个例子:
// 片段 1 var person={ add: function (a, b) { return a + b; } }; obj.name='li'; // 片段 2 var person={ add: function (a, b) { return a + b; }, name: 'li' };
以上代码实现的功能相同,都是定义了一个对象,这个对象具有一个属性name和一个方法add()。但使用片段2的方式效率更高。片段1给对象obj添加了一个属性name,这会造成隐藏类的派生。给对象动态地添加和删除属性都会派生新的隐藏类。假如对象的add函数已经被优化,生成了更高效的代码,则因为添加或删除属性,这个改变后的对象无法使用优化后的代码。
从例子中我们可以看出:
函数内部的参数类型越确定,V8越能够生成优化后的代码。
对于前端来说,HTML 都是最基础的内容。
今天,我们来了解一下 HTML 和网页有什么关系,以及与 DOM 有什么不同。通过本讲内容,你将掌握浏览器是怎么处理 HTML 内容的,以及在这个过程中我们可以进行怎样的处理来提升网页的性能,从而提升用户的体验。
不知你是否有过这样的体验:当打开某个浏览器的时候,发现一直在转圈,或者等了好长时间才打开页面……
此时的你,会选择关掉页面还是耐心等待呢?
这一现象,除了网络不稳定、网速过慢等原因,大多数都是由于页面设计不合理导致加载时间过长导致的。
我们都知道,页面是用 HTML/CSS/JavaScript 来编写的。
HTML由一系列的元素组成,通常称为HTML元素。HTML 元素通常被用来定义一个网页结构,基本上所有网页都是这样的 HTML 结构:
<html>
<head></head>
<body></body>
</html>
其中:
HTML 中的元素特别多,其中还包括可用于 Web Components 的自定义元素。
前面我们提到页面 HTML 结构不合理可能会导致页面响应慢,这个过程很多时候体现在<script>和<style>元素的设计上,它们会影响页面加载过程中对 Javascript 和 CSS 代码的处理。
因此,如果想要提升页面的加载速度,就需要了解浏览器页面的加载过程是怎样的,从根本上来解决问题。
浏览器在加载页面的时候会用到 GUI 渲染线程和 JavaScript 引擎线程(更详细的浏览器加载和渲染机制将在第 7 讲中介绍)。其中,GUI 渲染线程负责渲染浏览器界面 HTML 元素,JavaScript 引擎线程主要负责处理 JavaScript 脚本程序。
由于 JavaScript 在执行过程中还可能会改动界面结构和样式,因此它们之间被设计为互斥的关系。也就是说,当 JavaScript 引擎执行时,GUI 线程会被挂起。
以网易云课堂官网为例,我们来看看网页加载流程。
(1)当我们打开官网的时候,浏览器会从服务器中获取到 HTML 内容。
(2)浏览器获取到 HTML 内容后,就开始从上到下解析 HTML 的元素。
(3)<head>元素内容会先被解析,此时浏览器还没开始渲染页面。
我们看到<head>元素里有用于描述页面元数据的<meta>元素,还有一些<link>元素涉及外部资源(如图片、CSS 样式等),此时浏览器会去获取这些外部资源。除此之外,我们还能看到<head>元素中还包含着不少的<script>元素,这些<script>元素通过src属性指向外部资源。
(4)当浏览器解析到这里时(步骤 3),会暂停解析并下载 JavaScript 脚本。
(5)当 JavaScript 脚本下载完成后,浏览器的控制权转交给 JavaScript 引擎。当脚本执行完成后,控制权会交回给渲染引擎,渲染引擎继续往下解析 HTML 页面。
(6)此时<body>元素内容开始被解析,浏览器开始渲染页面。
在这个过程中,我们看到<head>中放置的<script>元素会阻塞页面的渲染过程:把 JavaScript 放在<head>里,意味着必须把所有 JavaScript 代码都下载、解析和解释完成后,才能开始渲染页面。
到这里,我们就明白了:如果外部脚本加载时间很长(比如一直无法完成下载),就会造成网页长时间失去响应,浏览器就会呈现“假死”状态,用户体验会变得很糟糕。
因此,对于对性能要求较高、需要快速将内容呈现给用户的网页,常常会将 JavaScript 脚本放在<body>的最后面。这样可以避免资源阻塞,页面得以迅速展示。我们还可以使用defer/async/preload等属性来标记<script>标签,来控制 JavaScript 的加载顺序。
百度首页
对于百度这样的搜索引擎来说,必须要在最短的时间内提供到可用的服务给用户,其中就包括搜索框的显示及可交互,除此之外的内容优先级会相对较低。
浏览器在渲染页面的过程需要解析 HTML、CSS 以得到 DOM 树和 CSS 规则树,它们结合后才生成最终的渲染树并渲染。因此,我们还常常将 CSS 放在<head>里,可用来避免浏览器渲染的重复计算。
我们知道<p>是 HTML 元素,但又常常将<p>这样一个元素称为 DOM 节点,那么 HTML 和 DOM 到底有什么不一样呢?
根据 MDN 官方描述:文档对象模型(DOM)是 HTML 和 XML 文档的编程接口。
也就是说,DOM 是用来操作和描述 HTML 文档的接口。如果说浏览器用 HTML 来描述网页的结构并渲染,那么使用 DOM 则可以获取网页的结构并进行操作。一般来说,我们使用 JavaScript 来操作 DOM 接口,从而实现页面的动态变化,以及用户的交互操作。
在开发过程中,常常用对象的方式来描述某一类事物,用特定的结构集合来描述某些事物的集合。DOM 也一样,它将 HTML 文档解析成一个由 DOM 节点以及包含属性和方法的相关对象组成的结构集合。
我们常见的 HTML 元素,在浏览器中会被解析成节点。比如下面这样的 HTML 内容:
<html>
<head>
<title>标题</title>
</head>
<body>
<a href='xx.com'>我的超链接</a>
<h1>页面第一标题</h1>
</body>
</html>
打开控制台 Elements 面板,可以看到这样的 HTML 结构,如下图所示:
在浏览器中,上面的 HTML 会被解析成这样的 DOM 树,如下图所示:
我们都知道,对于树状结构来说,常常使用parent/child/sibling等方式来描述各个节点之间的关系,对于 DOM 树也不例外。
举个例子,我们常常会对页面功能进行抽象,并封装成组件。但不管怎么进行整理,页面最终依然是基于 DOM 的树状结构,因此组件也是呈树状结构,组件间的关系也同样可以使用parent/child/sibling这样的方式来描述。同时,现在大多数应用程序同样以root为根节点展开,我们进行状态管理、数据管理也常常会呈现出树状结构。
我们知道,浏览器中各个元素从页面中接收事件的顺序包括事件捕获阶段、目标阶段、事件冒泡阶段。其中,基于事件冒泡机制,我们可以实现将子元素的事件委托给父级元素来进行处理,这便是事件委托。
如果我们在每个元素上都进行监听的话,则需要绑定三个事件;(假设页面上有a,b,c三个兄弟节点)
function clickEventFunction(e) {
console.log(e.target===this); // logs `true`
// 这里可以用 this 获取当前元素
}
// 元素a,b,c绑定
element2.addEventListener("click", clickEventFunction, false);
element5.addEventListener("click", clickEventFunction, false);
element8.addEventListener("click", clickEventFunction, false);
使用事件委托,可以通过将事件添加到它们的父节点,而将事件委托给父节点来触发处理函数:
function clickEventFunction(event) {
console.log(e.target===this); // logs `false`
// 获取被点击的元素
const eventTarget=event.target;
// 检查源元素`event.target`是否符合预期
// 此处控制广告面板的展示内容
}
// 元素1绑定
element1.addEventListener("click", clickEventFunction, false);
这样能解决什么问题呢?
常见的使用方式主要是上述这种列表结构,每个选项都可以进行编辑、删除、添加标签等功能,而把事件委托给父元素,不管我们新增、删除、更新选项,都不需要手动去绑定和移除事件。
如果在列表数量内容较大的时候,对成千上万节点进行事件监听,也是不小的性能消耗。使用事件委托的方式,我们可以大量减少浏览器对元素的监听,也是在前端性能优化中比较简单和基础的一个做法。
注意:
我们了解了 HTML 的作用,以及它是如何影响浏览器中页面的加载过程的,同时还介绍了使用 DOM 接口来控制 HTML 的展示和功能逻辑。我们了解了DOM解析事件委托等相关概念。
其说我爱Javascript,不如说我恨它。它是c语言和self语言lIQ的产物。18世纪英国文学家约翰逊博士说得好,它的优秀之处并非原创,它的原创之处并不优秀。这句话出自Javascript的创造者布兰登艾奇。
作为Javascript的发明人,为什么不为其感到骄傲而说出这样的话?因为他对Java一点兴趣也没有,只是为了应付公司安排的任务,他只用10天时间就把Javascript设计出来了。虽然设计初期存在诸多不够严谨的地方,但这并不影响它在之后成为世界上使用最为广泛的语言之一。
故事的序幕在1992年缓缓拉开,当时一家名为numbers的公司研发出了一种名为c简简的嵌入式脚本语言。它的初衷是创造一个功能强大到足以取代宏操作的脚本语言,并且与c语言保持高度的相似性,从而降低开发人员的学习门槛。
这款语言最初与一款名为CMV的共享工具一同推出,然而因为mm这个词在某种语境下听起来显得过于消极,同时字母c也被认为令人畏惧。numbers公司最终决定将CMM更名为scriptes。
随着时间的推移,numbers公司看准了当时势头正盛的Nanscape浏览器,这款浏览器在90年代的市场份额一度高达九成。于是他们为Netscape浏览器开发了一个可以嵌入网页的CNV版本,这也标志着scriptes成为了历史上首个客户端脚本语言。
在那个时代,上网冲浪刚刚兴起并日益普及,当时大多数人还依赖着速度约为28CBTS的调制解调器接入网络。随着网页内容逐渐丰富和复杂化,浏览速度开始显著下降。更糟糕的是,由于缺乏浏览器脚本语言,即便是简单的表单有效性验证也需要客户端与服务器之间频繁交互。这常常导致用户在提交表单后,经过漫长的30秒等待,却只收到一个自断无效的提示,这无疑让人倍感沮丧。
受到scriptist的启发,行业领军者Netscape公司开始深入思考,寻求一种客户端脚本语言来解决这一难题。Nanscape公司内的Brandon ad接受了这个挑战,他的任务是为即将在1995年发布的Nanscape Navigator2.0版本开发一个名为liver的脚本语言。不久后这个语言更名为Newscript,其初衷是为非专业开发人员提供一个便捷的工具,使得没有编程背景的网站设计者也能轻松使用,因此一个简单易学的弱类型动态解释语言应运而生。
Brandon后来回忆到,他从未想到当年吴昕设计的一个语言竟然会发展成为如今最流行的脚本语言,因此它也被誉为Javascript之父。
那为啥叫Javascript?在Netscape筹备开发浏览器脚本语言之际,一个关键事件悄然发生。1995年sun公司推出了重命名的0C语言,即Java并大力推广javablit的概念,这是一种能在浏览器中运行的客户端组件,与我们今天所熟知的Javascript的应用形态颇为相似。
Netscape看到了Java的潜力,决定与Sam公司合作让Java程序以EVID的形式直接在浏览器中运行,他们甚至一度考虑将Java直接迁入网页作为脚本语言。然而由于这会使html网页变得复杂运行缓慢且操作繁琐,最终这个计划被放弃。
然而当时Mascape的管理层对Java的热情不减,这也间接影响了即将诞生的脚本语言的命运。在这个关键时刻,34岁的Brandoni接下了这项重任。Netsk高层对他的要求是未来的脚本语言必须与JOVO保持一定的相似性,但要比Java更简单,以便更多人能够轻松上手。
然而Brandon对Java并无太大兴趣,如果不是公司的决策,他或许不会选择Java作为Javascript的设计原型。为了完成任务,他在短短10天内设计出了new script,他的设计理念融合了c语言的基本语法朝瓦的数据类型与内存管理,同时提升了函数的地位,并借鉴了self语言的基于原型的继承机制,这也使得Javascript成为了一个独特的结合体简化的函数式编程与简化的面向对象编程的交融。
然而Brandon本人对这个作品并不满意,他曾表示与其说我爱Javascript,不如说我恨它。它是c语言和self语言结合的产物。随后Netscape与sun公司合作完成了new script的实现。在Netscape Navigator2.0发布之前,为了获取sun的支持,并借助Java这一当时的热门词汇,Newscript更名为Javascript。这个名称仅仅是Netscape公司的一个市场决策,然而他们未曾预料到这个决策会带来如此大的负面影响。
多年来人们常常混淆Java和Javascript这两种毫不相干的语言,实际上它们仅仅是名字相似且有着一些公司层面的历史联系而已。Brandon IG对此深感遗憾,他在10年后的演讲Javascriptat ten years中京告到不要让市场营销决定你的语言名称。
*请认真填写需求信息,我们会在24小时内与您取得联系。