相信大家都非常熟悉《You Don't Know JS》(中文名:《你不知道的JS》),但是你知道吗?作者 Kyle Simpson 在2019年出版了《You Don't Know JS Yet》。五年过去,JavaScript 已经发生了许多变化,而这本书的中文版尚未面世。今天,跟本lepus一起进入经典之作吧!
(一章节太多将会分为两部分展示)
第一章:《Get Started》
很多人可能会想跳过这一章,觉得自己已经"开始"了。但事实上,这一章值得仔细品读!它揭示了 JavaScript 底层构建块的深度与微妙之处。在深入探索高级特性之前,扎实的基础知识将为你提供不可或缺的根基。这才是真正了解 JavaScript 的第一步!
JavaScript 名字的由来
很多初学者会好奇,JavaScript 和 Java 是否有关系?它是 Java 的"脚本版"吗?
其实,JavaScript 的名字更像是一次"营销恶作剧"。
最初,这门语言的设计目标是吸引 Java 程序员。当时"script"这个词非常流行,通常用来表示轻量级程序。于是,"Java" 和 "Script" 被拼凑到了一起,成了"JavaScript"。
"Java is to JavaScript as ham is to hamster." --Jeremy Keith, 2009
JavaScript 的语言规范
JavaScript 的语言规范由 TC39 组织负责管理。每当 TC39 的成员达成一致投票后,他们会将提案提交给负责发布规范的机构---------ECMA。
TC39 委员会每月都会召开一次研讨会,而每项新提案都需要经过以下五个阶段的审核流程:Stage 0 到 Stage 4,是的我们程序员计数都是从0开始的。
需要注意的是,JavaScript 并不存在多个版本。世界上只有一个 JavaScript,由 TC39 和 ECMA 维护的官方标准定义。也就是说,我们只需学习一种版本的 JavaScript,并且它可以在任何环境中使用。
web-统治JS的主场
大部分的 JS 代码都是运行在 Web 环境中的,由浏览器的引擎解析并执行。大多数情况下,规范中定义的 JS 和基于浏览器的 JS 引擎中运行的 JS 是相同的。但我们必须考虑其中可能存在的一些差异。
随着 JS 语言规范的不断更新迭代,浏览器引擎会评估新规范对网页的影响。如果某些更改破坏了现有 Web 内容,浏览器可能会拒绝采纳这些更改。此时,TC39 通常会回溯之前的规范并进行相应修改,以确保 Web 的稳定性。

并不是所有你写下的代码都是JS
听到这句话是不是感觉很荒谬?"我写的代码,我自己不知道吗?"
实际上,你可能写过这样的代码:alert('hello')
。但你知道吗,它并不在 JS 规范中!
这是因为各种 JS 环境(如浏览器 JS 引擎、Node.js 等)都会在 JS 程序的全局范围中添加特定的 API ,这些 API 提供环境特有的功能。例如浏览器提供的 alert(..)
,用于弹出提示框,还有我们常用的 fetch(..)
、getCurrentLocation(..)
和 getUserMedia(..)
,它们其实都是浏览器为开发者提供的"类 JS API 接口"。
正因如此,不同浏览器环境的差异常常导致 JS 被误解为"不可靠"。
范式
在编程语言中,范式指的是一种广泛的思维方式和方法。虽然编程风格可能多种多样,但我们总能从一门语言中看出它的主流范式。
经典的编程范式包括:程序范式、面向对象范式和函数范式。
- 程序范式:通过一组预先确定的操作,以自上而下、线性进展的方式组织代码。这些操作通常收集在被称为过程的单元中。例如 C。
- 面向对象范式:通过将逻辑和数据封装到称为类的单元中来组织代码。例如 JAVA 和 C++。
- 函数范式:通过组织纯函数(不依赖或修改外部状态的计算)来构建代码,并将这些函数视为值。例如 Haskell。
JavaScript 支持多种范式 ,赋予程序员更大的灵活性 和创造空间。
向前兼容+向后兼容
向后兼容的核心理念是:一旦某种规范被接受为有效的 JS,它在未来的版本中不会发生改变,从而使相关代码继续保持有效。 这体现了"我们不会破坏任何一个网络"的承诺。
然而,保持 JS 的向后兼容性绝非易事。每一个规范的决定都意味着"永久存在"。这些决定可能会让它名垂青史,也可能让它遗臭万年。
这条规则有一些小的例外 ,JS 进行过一些向后不兼容 的更改。但 TC39 对此非常谨慎,他们会研究网络上的现有代码(通过浏览器数据收集)来估计此类损坏的影响 ,浏览器最终决定并投票是否愿意承受用户对小规模损坏的批评,并权衡其带来的好处为更多网站(和用户)修复或改进语言的某些方面。
向前兼容的核心理念是:如果程序在较旧的 JS 引擎中运行,则在程序中添加语言的新功能不会导致该程序崩溃。
看到这里再结合平时接触过的polyfill,我相信你已经明白JS是向后兼容而不是向前兼容。
HTML 和CSS才是向前兼容,你会发现现在找出 1995 年编写的一些 HTML 或 CSS,它完全有可能在今天不起作用(或同样起作用)。但是,如果你在 2010 年的浏览器中使用 2024 年的新功能,页面不会"损坏"------无法识别的 CSS/HTML 会被跳过,而其余的 CSS/HTML 将进行相应的处理。
HTML和CSS是声明式语言,可以很自然地跳过无法被解析的语句,但是如果随意跳过JS语句,可能会影响非常大!所以尽管将向前兼容性包含在编程语言设计中似乎是可取的,但这样做通常是不切实际的。

跨越差距
既然JS不是向前兼容的,那么不同时间使用的JS之间肯定有差距。比如你在2010的引擎中运行ES2019功能的代码,程序可能会崩溃。这是否意味着,我们JS程序员只能使用可以兼容最老的JS引擎的代码?当然不是!
我们只需格外小心处理这些差距即可。
对于不兼容的语法 ,我们需要进行转译。转译就是使用工具将程序源码从一种版本转换到其他版本。JS最常见的转译器是Babel------将新版本的JS语法转换为相应的旧版本可兼容的语法。
你所写下的源码:
js
if (something) {
let x = 3;
console.log(x);
}
else {
let x = 4;
console.log(x);
}
但是经过Babel转移后的代码可能长这样:
js
var x$0,x$1;
if(something){
x$0=3;
console.log(x$0);
}
else{
x$1=4;
console.log(x$1);
}
源码使用let
制造块级作用域,但是在旧版本JS中没有这一概念,于是Babel制造两个不相干的变量,来解决这一差距。
填补差距
对于无法使用的新添加的API,最常见的解决方法是为该缺少的 API 方法提供一个定义,这种模式称为polyfill
(也称为"shim")。
你所写下的源码:
js
var pr = getSomeRecords();
pr
.then(renderRecords) // render if successful
.catch(showError) // show an error if not
.finally(hideSpinner) // always hide the spinner
其中的finally()
是ES2019的方法,所以当我们的代码运行在pre-ES2019的环境下时,代码将会报错。于是我们需要polyfill进行填补。
一个简要的polyfill可能长这样(这是一个简要的示例,请不要在代码中使用):
js
if (!Promise.prototype.finally) {
Promise.prototype.finally = function f(fn){
return this.then(
//Promise成功后的处理
function t(v){
return Promise.resolve( fn() )
.then(function t(){
return v;
});
},
//Promise失败后的处理
function c(e){
return Promise.resolve( fn() )
.then(function t(){
throw e;
});
}
);
};
}
一般像Babel 这样的转译器会检测出你需要的polyfill并自动为你提供,但有时可能需要你特别地定义它。
编译还是解释?
在JS中,有这样一个经久不息的辩论:JS到底是解释型脚本还是编译型程序?大多数人的观点是解释性脚本,事实上,这个问题比大家想象的更加复杂。
在编程语言的历史中,大部分时候,"解释"语言和"脚本"语言与编译语言相比一直被视为低等级的。这样尖锐的原因有很多,包括人们认为性能较差 ,不喜欢某些特性:使用动态类型 而不是更成熟更严格的静态类型。
编译型语言一般会生成程序的可移植(二进制)表示形式,以便稍后分发以供执行。由于我们并没有真正观察到JS的这种模型,因此很多人声称JS不符合该类别,事实上,在过去几十年,程序的"可执行"形式的分发模型已经更加的多样化,而且相关性也越来越低。
这些错误的主张和批评应该被搁置。JS 是被解释还是被编译的真正原因与错误处理方式的本质有关。
从历史上看,解释语言通常以自上而下、逐行的方式执行。在执行开始之前,通常不会对程序进行初始处理。 对于初始处理这件事来说,还有一种语言叫做parse language
。所有的编译型语言都会预解析一遍,所以解析型语言就像是编译途中的一个站,而最终站就是code generation
,生成可执行文件。
一旦源程序被解析过后,很有可能从程序的解析形式------AST
,也就是抽象语法树(Abstract Syntax Tree)转变到可执行格式。
JS是在执行之前先解析的语言(因为它要求在代码开始执行之前报告"早期错误 "------代码中静态确定的错误,例如重复的参数名称。如果不解析代码,就无法识别这些错误),那他是编译性语言吗?答案更加接近正确。
JS在被解析后转变成二进制文件,引擎不再切换效率较低的逐行解析的模式。具体来说,这个"编译"会产生一个二进制字节码,然后交给"JS虚拟机"来执行。有些人喜欢说这个虚拟机正在"解释"字节码。但这意味着 Java 和其他十几种 JVM 驱动的语言是解释性的而不是编译性的。当然,这与 Java 等是编译语言的典型断言相矛盾。
现在来看看JS源程序整个处理过程。
- 程序离开开发人员的编辑器后,它会被 Babel 转译,然后被 Webpack(也许还有六个其他构建过程)打包,然后以非常不同的形式交付给 JS 引擎。
- JS 引擎将代码解析为 AST。
- 然后,引擎将该 AST 转换为一种字节代码,即二进制中间表示 (IR),然后由优化 JIT 编译器进一步细化/转换。
- 最后,JS VM 执行程序。
读到这里,你认为JS是编译性语言还是解析性语言呢?对于作者来说,JS应该是编译性语言,因为代码通过了编译后形成了另一种格式的文件。
严格模式
在ES5的发布中,JS添加了一个严格模式内置机制去鼓励更规范的JS书写。
相对于不便,严格模式带来的好处更为明显,但是旧习难改,曾经写下的惰性代码的根基难以撼动,所以在十多年后的的今天,严格模式的可选择性意味着他对于JS开发者来说仍然是一个非必需品。
事实上,严格模式不应该是一个限制,而是一种引导 ,引导人以最好的方式去编程。大多数JS代码都是由开发人员团队进行的,因此严格模式的"严格 "(以及像Linters这样的工具!)通常可以通过避免在非严格模式下遇到的一些更有问题的错误来协作代码。
与其去抗争严格模式带来的不便,最好的心态其实是认识到严格模式就是一个linter,去提醒你如何写有高质量,高性能的机会。
HOW TO USE:
js
//只有空格和逗号可以写在之前,否则将会导致机制失效
"use strict";
// 剩下的代码将运行在严格模式中
严格模式还可以被应用到作用域中,规则同上
js
function someOperations() {
//只有空格和逗号可以写在之前,否则将会导致机制失效
"use strict";
// 剩下的代码将运行在严格模式中
}
但是函数式严格模式与文件式严格模式不可以同时使用,你需要二选一!
很多人猜测严格模式会在不久后成为默认设置,但答案是几乎不可能 。还记得我们前面提到的向后兼容性 吗?如果JS引擎决定更新严格模式就会导致大量的代码出现非运行时错误,可能会造成很大的影响。其实我们平常书写的代码在生产模式中已经是编译后的代码了,一般情况下他们已经遵守了严格模式。
每一个文件都是一个程序
几乎所有的网站都是不同的JS代码组成而来的,人们会自然而然地认为一个应用是一个程序,但是JS不是这么看的。
在JS中,一个单独的文件就是一个隔离的程序。
这个观点如此重要的原因是错误处理。因为JS将一个文件当作一个程序处理,如果其中一个文件出现错误,并不会阻碍其他文件的执行。
唯一一种多个单独文件被视为一个程序的情况是:通过全局共享一个状态,他们在全局命名空间中混合,所以在运行时中将会被看作一个整体。
自ES6以来,除了典型的独立JS程序格式外,JS还支持模块格式。模块也基于文件。一般来说你不会考虑一个模块是一个单独的程序,但实际上JS仍然单独处理每个模块。类似于全局允许独立文件在运行时中混合的方式,将模块导入另一个模块可以使他们在运行时相互操作。

值
原始数据和对象数据对于JS开发者来说都不陌生。
通过字面量嵌入程序的值:
js
greeting("My name is Kyle.");
"My name is Kyle."就是一个字符串字面量 。在这里,我们使用了双引号作为界定字面量的标准,但是我们也可以使用单引号。使用哪一种完全是看自己的喜好,重要的事情是:为了代码的可靠平行,请挑选一个标准,并且持续使用它!
对于字符串来说还有一个界定的标准,那就是 ` (反引号)
插值代码:
js
console.log("My name is ${ firstName }.");
// My name is ${ firstName }.
console.log('My name is ${ firstName }.');
// My name is ${ firstName }.
console.log(`My name is ${ firstName }.`);
// My name is Kyle.
除了字符串,boolean 和number也常常使用字面量表示。
js
while (false) {
console.log(3.141592);
}
除了字符串,数字和布尔值外,JS程序中的另外两个原始数据是null
和undefined
。尽管它们之间存在差异(有些历史性和某些当代),但在大多数情况下,两个价值观都可以表明价值的空白(或不存在)。
许多开发者喜欢将他们混为一谈,也就是假定这些价值无法区分,一般来说是可以的。但是,最安全最好的方式就是只使用undefined作为空值,尽管null写起来更短!
最后的原始数据将会是symbol,他是一种特殊用途的值,他的表现为隐藏的不可掩盖的值,符号几乎专门用作对象上的特殊键。
js
hitchhikersGuide[ Symbol("meaning of life") ];
// 42
你并不会在典型的JS程序中经常遇到Symbol的直接使用。它们主要用于低代码,例如在库和框架中。
数组和对象
数组是一种特殊的对象,由一组有序的,数字化索引的数据组成。
js
var names = [ "Frank", "Kyle", "Peter", "Susan" ];
names.length;
// 4
names[0];
// Frank
names[1];
// Kyle
JS中的数组和其他语言的数组有些不同,它可以包含任何类型的数据,包括原始数据和对象数据。
对象更加综合,是由无序的,键值对组成的。
类型判断
为了区分值,typeof
告诉你其内置类型(如果是原始数据)或"对象",否则:
js
typeof 42; // "number"
typeof "abc"; // "string"
typeof true; // "boolean"
typeof undefined; // "undefined"
typeof null; // "object" -- oops, bug!
typeof { "a": 1 }; // "object"
typeof [1,2,3]; // "object"
typeof function hello(){}; // "function"
注意:null的结果是object,同时function的结果是function但是array的结果却不是array。
定义与使用变量
变量必须在声明之后使用,JS中有不同的声明方式,每一种方式都有不同的隐含行为。
- var:
js
var myName = "Kyle";
var age;
- let:
js
let myName = "Kyle";
let age;
let与var有些不同,最明显的就是let的作用域 更受限制,一般是在一对大括号之内有效,这就叫做块级作用域。
块级作用域在实践中非常有用,它可以用来防止名称意外重叠
- const:
js
const myBirthday = true;
let age = 39;
if (myBirthday) {
age = age + 1; // OK!
myBirthday = false; // Error!
}
const
类似于let
但是当他声明并赋值后,就没有办法进行再赋值
。严格意义上来说const
不是无法改变而是没有办法进行重新赋值。
除了const
、let
和var
之外,还有其他语法可以在各种范围内声明变量。
js
function hello(myName) {
console.log(`Hello, ${ myName }.`);
}
hello("Kyle");
// Hello, Kyle.
函数
函数在JS中的定义是一个可以调用一次或多次的语句的集合,可以提供一些输入,并可以退还一个或多个输出的过程。
函数定义一般为;
js
function awesomeFunction(coolThings) {
// ..
return amazingStuff;
}
// let awesomeFunction = ..
// const awesomeFunction = ..
var awesomeFunction = function(coolThings) {
// ..
return amazingStuff;
};
第一个函数是函数声明式,第二个函数被分配给了变量awesomeFunction,不同于第一个,它只有在运行时才被分配给该变量。
因为篇幅太长,暂时先记录这么多。😀💖
俺也要退出前端行业了,先把草稿箱的发出来,不能让自己的辛苦白费了。😶🌫️