历史

  • 1995.05:Netscape 公司的 Brendan Eich 用 10 天设计完成了语言的初版(Mocha)
  • 1996.12:JavaScript 被提交给 ECMA,并成立 TC39 技术委员会负责制定标准
  • 1997.09:正式发布《ECMA-262 第 1 版》(ECMAScript 1.0)
  • 1998.07:ECMAScript 2.0 发布
  • 1999.12:ECMAScript 3.0 发布
  • 2007 - 2008:ES3.1 和 ES4.0 之间争议过大,最终废除了 4.0 版本
  • 2009.12:ECMAScript 5.0 发布
  • 2011.06:ECMAScript 5.1 发布
  • 2015.06:ECMAScript 6.0 发布(更名为 ECMAScript 2015)
  • 每年发布一个新版本…

JavaScript 二十年

ES6+ 各版本功能列表:GitHub - sudheerj/ECMAScript-features: ECMAScript features cheatsheet

语言特点

  • 动态语言(代码内容在运行时决定,类型检查和转换是在程序执行时进行的,而不是在编译时)
  • 弱类型语言(可以隐式转换)
  • 解释型语言(执行时代码逐句解释)
  • 自动分号插入(ECMAScript® 2025 Language Specification (tc39.es)
  • 严格区分大小写

变量

  • 字母数字_$ 组成,不能以数字开头
  • 硬编码的常量一般使用全大写加下划线的形式:const DEFAULT_AGE = 18
  • 常规命名通常采用驼峰形式:firstSecond
  • 不能以 保留关键字 作为变量名

注释

  • 单行:// xxx
  • 多行:/* xxx */

代码表示 how,注释表示 why

浏览器中 JavaScript 的实现(含义)

  • 核心:ECMAScript
  • 文档对象模型:DOM

在 HTML 中引入 JavaScript

1. 内联 JavaScript

使用 <script></script> 标签插入 <body> 中,script 的执行会阻塞下面的内容加载和显示:

<body>
  ...
  <script></script>
</body>

2. 外部 JavaScript

<script src="script.js"></script> ,如果设置了 src 属性,则 script 标签内部包裹的代码会失效
使用外部引入的好处是浏览器会下载它,然后将它保存到浏览器的缓存中

当浏览器加载 HTML 时遇到 <script>...</script> 标签,浏览器就不能继续构建 DOM,它必须立刻执行此脚本,这会导致两个重要的问题:

  • 脚本不能访问到位于它们下面的 DOM 元素,因此,脚本无法给它们添加处理程序等
  • 如果页面顶部有一个笨重的脚本,它会阻塞页面。在该脚本下载并执行结束前,用户都不能看到页面内容

为什么 JS 要阻塞 DOM 构建呢?

因为 JS 代码里是可能去改变 DOM 内容的

不阻塞

在外部 JavaScript 的写法下,可以增加两种属性不阻塞页面:

  • async:异步属性,立即在后台下载,但是加载完成后立即执行,与其他脚本不会互相等待,和 DOMContentLoaded 之间也不会互相等待(独立脚本使用)
  • defer:延迟属性,立即在后台下载,但是在 DOM 解析完成之后,DOMContentLoaded 之前执行,所有 defer 的脚本按照出现顺序执行(需要整个 DOM 或加载顺序重要时使用)

3. 动态脚本

可以使用 JavaScript 动态地创建一个脚本,并将其附加(append)到文档(document)中:

let script = document.createElement('script')
script.src = "..."
document.body.append(script)

当脚本被 append 到文档时,脚本就会立即开始加载,先加载完成就先执行
默认是异步的,类似 2 中的 async 属性,可以显式设置 script.async=false 变成类似 defer 的效果,但是不会阻塞 DOMContentLoaded 事件

严格模式

ES5 增加了严格模式:"use strict"; 必须在文件的开头声明,或者在一个函数的开头声明才有效(classmodule 中会自动启用严格模式),用于修改旧有 JS 中不完善的地方。严格模式下:

  • 变量必须先声明再使用,给未经声明的变量赋值会报错:ReferenceError (非严格模式:直接给一个未声明的变量赋值会创建一个全局变量)
  • 函数声明只在块级作用域中能够访问,非严格模式下则能够全局访问
  • 对不可写的属性等进行写入操作时,会出现错误
  • 八进制不再允许使用前缀 0 表示,必须使用 0o0O
  • eval 有属于自己的词法环境。因此不能从外部访问在 eval 中声明的函数和变量
  • 增加了保留关键字
  • 不能用 with 语句
  • this 的值是 undefined,非严格模式下 this 是 window
  • 非严格模式下违反对象描述符的行为会被忽略(如:对只读的属性进行赋值),但是严格模式下会报错
  • 不能使用 arguments.calleearguments.caller
  • 严格模式下函数的参数和 arguments 对象是相互独立的,非严格模式下是相互关联的(修改形参值会同步修改 arguments)

作用域

分为:

  • 全局作用域:不在任何函数内定义的变量就具有全局作用域,范围是整个 script 标签 或单独的 js 文件
  • 函数作用域:如果一个变量在函数体内部申明,则该变量的作用域为整个函数体内
  • 块级作用域:JS 的 if,for 等没有块级作用域。但是 with 语句内部,try/catch 的 catch 语句是具有块级作用域功能的

Tip

ES6 的块级作用域必须有大括号,如果没有大括号,JavaScript 引擎就认为不存在块级作用域

下面的写法会报错:

// 声明函数
if (true)
    let func = function () {};
 
// 声明变量
if (true) let a = 1;

工作模式

  • 作用域的工作模式主要有两种:
    • 词法作用域:JS 所用。作用域是由写代码时变量定义的位置决定的(除了 this 是动态决定的)
    • 动态作用域:Bash,Perl 等。作用域是由运行时决定的
  • 作用域查找会从运行时所处的最内部开始,逐级向外进行查找,直到遇到第一个匹配的标识符
  • 内部变量会屏蔽外部变量。使用 var(而不是 let/const)声明的全局变量会自动变成全局对象(如浏览器中的 window)的属性,所以全局变量可以通过 window. xxx 访问

转译器 和 Polyfill

JavaScript 语言每年都会增加新的功能,为了在旧引擎中使用新的特性,有 2 种方式:

  • 转译器:将源码转译成另一种源码的特殊软件,主要针对从新到旧。如 Babel
  • polyfill:更新/添加新函数的脚本,主要针对从无到有。如 core-js(方法在规范中存在,但是引擎还不支持)

垃圾回收

引用计数(逐渐淘汰)

原理:跟踪每个对象被引用的次数,引用数为零时回收
缺点:无法处理循环引用

标记-清除(现代引擎主流)

步骤:

  1. 标记阶段:从根对象出发,递归标记所有可达对象
  2. 清除阶段:清除没有标记的对象

优点:解决循环引用问题
缺点:清除之后,剩余的对象内存位置是不变的,会导致空闲内存空间是不连续的,可能产生内存碎片

分代回收(V8 引擎优化)

内存分区:

  • 新生代(Young Generation):存放短暂存活对象(如局部变量)
    • 算法:Scavenge(复制存活对象到另一区域,清空当前区域)
  • 老生代(Old Generation):存放长期存活对象(如全局变量)
    • 算法:标记-清除 + 标记-整理(减少内存碎片)

同时结合增量回收,将垃圾回收任务拆分为多个小步骤,避免长时间阻塞主线程,减少页面卡顿。