历史
- 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)
- 每年发布一个新版本…
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";
必须在文件的开头声明,或者在一个函数的开头声明才有效(class
和 module
中会自动启用严格模式),用于修改旧有 JS 中不完善的地方。严格模式下:
- 变量必须先声明再使用,给未经声明的变量赋值会报错:ReferenceError (非严格模式:直接给一个未声明的变量赋值会创建一个全局变量)
- 函数声明只在块级作用域中能够访问,非严格模式下则能够全局访问
- 对不可写的属性等进行写入操作时,会出现错误
- 八进制不再允许使用前缀
0
表示,必须使用0o
或0O
eval
有属于自己的词法环境。因此不能从外部访问在 eval 中声明的函数和变量- 增加了保留关键字
- 不能用
with
语句 - this 的值是 undefined,非严格模式下 this 是 window
- 非严格模式下违反对象描述符的行为会被忽略(如:对只读的属性进行赋值),但是严格模式下会报错
- 不能使用
arguments.callee
和arguments.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(方法在规范中存在,但是引擎还不支持)
垃圾回收
引用计数(逐渐淘汰)
原理:跟踪每个对象被引用的次数,引用数为零时回收
缺点:无法处理循环引用
标记-清除(现代引擎主流)
步骤:
- 标记阶段:从根对象出发,递归标记所有可达对象
- 清除阶段:清除没有标记的对象
优点:解决循环引用问题
缺点:清除之后,剩余的对象内存位置是不变的,会导致空闲内存空间是不连续的,可能产生内存碎片
分代回收(V8 引擎优化)
内存分区:
- 新生代(Young Generation):存放短暂存活对象(如局部变量)
- 算法:Scavenge(复制存活对象到另一区域,清空当前区域)
- 老生代(Old Generation):存放长期存活对象(如全局变量)
- 算法:标记-清除 + 标记-整理(减少内存碎片)
同时结合增量回收,将垃圾回收任务拆分为多个小步骤,避免长时间阻塞主线程,减少页面卡顿。