JavaScript, 我们别无选择

JavaScript被众多开发者拉扯着长大,日复一日地驱动着网页的逻辑交互。在浏览器中,它坐着无可撼动的王座,懵懂无知的开发者似乎别无选择,日子久了,便只能屈于其淫威。Node.js的出现更把JavaScript从浏览器所在的客户端引入服务端。糟糕!它, JavaScript, 真是该死的全能。

JavaScript 从何而来

故事得从1994年说起。 那一年,网景公司(Netscape)发布了史上第一个比较成熟的网络浏览器Navigator-0.9版。不过这个浏览器确实只能用于“浏览”,一点交互的能力都没有。比如说,如果网页上有一栏”邮箱”要求填写,它无法像现在的浏览器一样直接在客户端检查用户输入的邮箱是否合规,而是傻傻的丢到服务器端判断。让这服务端承受这么大压力,那咱可不乐意。因此网景公司的工程师Brendan Eich被委派开发一个网页脚本语言,来实现些简单的网页交互。

说到这,我们不得不提起现在同样被广为人知的Java语言。1995年,Oak改名Java并推出,SUN公司便大肆宣传这个刚被推出的语言能“Write Once, Run Anywhere”。网景公司的整个管理层,都变成了Java语言的忠实信徒。不过如果直接把Java拿过来做脚本语言,会使得网页过于复杂。网景公司就想着干脆就创建一个跟Java相似但更简单的语言,刚好也蹭一波潜力巨大的Java热度,就叫JavaScript吧。其实Brendan擅长的是函数式编程,且他自己并不喜欢Java这门语言。 但何苦自己是兵不是将,只能默默敲键盘。于是乎,他想着创造一个简化的函数式编程(Brendan的专长)+ 简化的面向对象编程(公司的对Java的爱)的语言,能完成些简单操作就差不多了。1995年的九月, JavaScript就此诞生。而且,只花了10天时间。

JavaScript从诞生以来就杂糅着多个思想, 这种不左不右的态度一直延续至今。而且过短的开发周期使其宛若过度早产的孩童一般,为它之后的成长带来了许多的隐疾…

网景浏览器

JavaScript 引擎和运行环境

JavaScript是一门解释执行语言。这意味着源代码在执行前,无需编译为二进制文件 (不过 JIT (Just-In-Time compilation) 的优化还是会编译部分被多次执行的代码以加快速度)。JavaScript引擎 (engine) 就是一个运行JavaScript的容器,其本质还是一个程序,将JavaScript翻译为机器码送到CPU去执行。

JavaScript的引擎其实很多。如谷歌浏览器的JavaScript引擎Chrome V8,FireFox的引擎SpiderMonkey,它们都是用C++实现的。这么多不同的引擎,是如何做到JavaScript脚本在不同引擎下得到同样的结果呢?这一切都归功于ECMAScript语言规范,所有的引擎研发都要遵守这里面的规则。

不过我们通常不和引擎直接打交道,JavaScript引擎是会放在一个运行环境 (runtime) 中的,这个环境提供了代码在执行时能够利用的附加特性。比如说,JavaScript引擎负责源代码的解释和执行,而获取浏览器的信息(通常是通过DOM API)或响应一次鼠标的点击动作,是引擎所不具备的能力,而运行环境中的这些附加特性,成为了引擎处理这类信息和事件的桥梁。

不同的浏览器都是JavaScript的运行环境。除此之外,Node.js也是一个Javascript运行环境,其采用Chrome V8引擎。此运行环境能够使得JavaScript脱离浏览器运行,于是乎JavaScript也能在服务端放光放热,成为一众全栈工程师的首选。

JavaScript 奇特的混沌

那么JavaScript这门语言究竟有什么特点呢,或许我们能从接下来几个特性略知一二。

事件循环 (Event Loop)

JavaScript中的事件循环非常出名。众所周知,JavaScript代码是在单线程中执行的 (注意,这并不代表整个 JavaScript运行环境是在单一线程中工作的。JavaScript运行环境是存在线程池的) 。单线程会带来阻塞的问题,比如向后端发送请求,我们得等到结果返回才能继续执行剩下的程序,在这个等待过程中网页就会陷入卡死。这使得网页的交互非常不友好,显然,这个问题并不会出现在我们的浏览器中。因为事件循环就是被设计出解决这个问题的。

JavaScript的同步代码由执行栈负责,这一点应该很好理解。首先代码是从上到下执行的,但有可能存在函数内调用其他函数的情况。而栈就讲究先入后出,如方法a中调用了b,我们在执行a时,a先入栈,b再入栈,执行完b后b将出栈,最后依次执行a剩下的代码。

而对于异步代码,JavaScript不会等待它返回结果,而是将这个事件挂起,继续执行执行栈中的其他任务。当异步事件返回结果,将它放到消息队列中 (event loop) (有的地方也叫事件队列) ,被放入消息队列不会立刻执行起回调,而是等待当前执行栈中所有任务都执行完毕,主线程空闲状态,主线程会去查找事件队列中是否有任务,如果有,则取出排在第一位的事件,并把这个事件对应的回调放到执行栈中,然后执行其中的同步代码。

这种获取消息,执行,一直循环往复的行为,就叫做事件循环。

事件循环机制


数据类型

JavaScript是动态类型 (dynamically typed) 的语言,也就是说被定义的变量可以被随意的更改数据类型。

JavaScript有七种原始数据类型 (Primitive Values):

  1. number 用于任何类型的数字:整数或浮点数,在 ±(2^53-1) 范围内的整数。
  2. bigint 用于任意长度的整数。
  3. string 用于字符串:一个字符串可以包含 0 个或多个字符,所以没有单独的单字符类型。
  4. boolean 用于 true 和 false。
  5. null 用于未知的值 —— 只有一个 null 值的独立类型。
  6. undefined 用于未定义的值 —— 只有一个 undefined 值的独立类型。
  7. symbol 用于唯一的标识符。
    以及一种非原始数据类型:
  8. object 用于更复杂的数据结构

其他的数据类型都好理解,但这个null和undefined是在干什么?在许多语言中,有且仅有一个表示无的值。而JavaScript却打破了这种“虚无便是唯一”的美。

在最开始,null在JavaScript是一个表示“无”的特殊的object类型,且可以在动态转换时变成0。JavaScript的设计者Brendan觉得这很不合理,真正的虚无应当是啥都没有,于是他又定义了undefined,这种类型在转换成数字时会变成NaN (Not a Number)。然而事实证明这样的设计啥用都没有。现在这两个类型基本没什么区别,不过null更偏向于此处没有对象,而undefined更偏向于此处该有但还未被定义。


this究竟指向哪

许多语言都有this关键词,如Java,this是自身的一个对象,代表对象本身,可以理解为:指向对象本身的一个指针。但在JavaScript中this不是固定不变的,它会随着执行环境的改变而改变。

JavaScript的this通常指向其调用者。如以下代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
let user = { name: "John" };
let admin = { name: "Admin" };

function sayHi() {
alert( this.name ); // 是的,JavaScript允许this出现在这样的位置
}

// 在两个对象中使用相同的函数
user.f = sayHi;
admin.f = sayHi;

// 这两个调用有不同的 this 值
// 函数内部的 "this" 是“点符号前面”的那个对象
user.f(); // John(this == user)
admin.f(); // Admin(this == admin)

JavaScript也提供了call, apply,bind等方法,供我们显式的绑定函数中this的指向,这里不多做赘述。

不过在JavaScript的箭头函数中,this被设置为其被创建时的环境:

1
2
3
4
5
6
7
8
9
10
let user = {
firstName: "John",
};

firstName = "Admin"
let sayHi = () => alert(this.firstName);

user.sayHi = sayHi;

sayHi(); // Admin (this == global object)

从一个this就可以看出,JavaScript这种混沌的状态。不同的写法固然有着各自的优点,不过一种语言真容纳这么多“不同的思想”,像极了一个没有主见的君王…


闭包 (Closure)

闭包 是指一个函数可以记住其外部变量并可以访问这些变量。在某些编程语言中,这是不可能的,或者应该以一种特殊的方式编写函数来实现。但在 JavaScript 中,所有函数都是天生闭包的。
比如下面这段代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
function makeCounter() {
let count = 0;

return function() {
return count++;
};
}

let counter = makeCounter();

counter(); // 0
counter(); // 1
counter(); // 2

在一个函数调用刚开始时,会自动创建一个新的词法环境以存储这个调用的局部变量和参数。当代码要访问一个变量时,首先会搜索内部词法环境,然后搜索外部环境,然后搜索更外部的环境,以此类推,直到全局词法环境。在每次 makeCounter() 调用的开始,都会创建一个新的词法环境对象,以存储该 makeCounter 运行时的变量。makeCounter这个函数会返回一个函数,这个被返回的函数在被创建时会记住他记住创建它时的词法环,这里就包括了count = 0。所以再之后调用这个函数 (也就是counter函数时) ,可以一直修改count这个变量,而且count这个变量只能被counter函数修改。


JavaScript 依旧蒸蒸日上

JavaScript还有许多有趣的特性没有出现在这篇文章里,比如广为诟病的 ==和===,var let const, 以及 [1, 2, 3, 4] + [5, 6, 7, 8] 会等于 “1,2,3,45,6,7,8” … 感兴趣的读者可以去深入的了解这些特性。

JavaScript是浏览器唯一的王,我们没得选。许多机构,程序员依旧在在为JavaScript的成长贡献力量,JavaScript也正在逐渐努力抛弃历史遗留的问题,向着更广阔的大道走去。作为程序员,我们大可在写JavaScript时破口大骂,也可以胸怀理想,来个陈胜吴广起义,给浏览器的世界换个新王。不过,说到最后的最后,我们还是得承认它的实力,毕竟,它是一个能告诉我们“先有鸡还是先有蛋”的语言。

JavaScript下的“先有鸡还是先有蛋”