原型继承
除了 null
和 undefined
之外,每个基元数据类型都有一个原型,即相应的对象封装容器,用于提供用于处理值的方法。对基元调用方法或属性查找时,JavaScript 会在后台封装基元,并改为调用封装容器对象的方法或执行属性查找。
例如,字符串字面量没有自己的方法,但借助相应的 String
对象封装容器,您可以对其调用 .toUpperCase()
方法:
"this is a string literal".toUpperCase();
> THIS IS A STRING LITERAL
这称为原型继承,即从值的相应构造函数继承属性和方法。
Number.prototype
> Number { 0 }
> constructor: function Number()
> toExponential: function toExponential()
> toFixed: function toFixed()
> toLocaleString: function toLocaleString()
> toPrecision: function toPrecision()
> toString: function toString()
> valueOf: function valueOf()
> <prototype>: Object { … }
您可以使用这些构造函数创建基元,而不仅仅是根据值定义它们。例如,使用 String
构造函数会创建一个字符串对象,而不是字符串字面量:该对象不仅包含字符串值,还包含构造函数的所有继承属性和方法。
const myString = new String( "I'm a string." );
myString;
> String { "I'm a string." }
typeof myString;
> "object"
myString.valueOf();
> "I'm a string."
在大多数情况下,生成的对象的行为与我们用来定义它们的值相同。例如,即使使用 new Number
构造函数定义数字值会导致生成一个包含 Number
原型的所有方法和属性的对象,但您可以对这些对象使用数学运算符,就像对数字字面量使用一样:
const numberOne = new Number(1);
const numberTwo = new Number(2);
numberOne;
> Number { 1 }
typeof numberOne;
> "object"
numberTwo;
> Number { 2 }
typeof numberTwo;
> "object"
numberOne + numberTwo;
> 3
您很少需要使用这些构造函数,因为 JavaScript 的内置原型继承意味着它们没有任何实际益处。使用构造函数创建基元也可能会导致意外结果,因为结果是对象,而不是简单的字面量:
let stringLiteral = "String literal."
typeof stringLiteral;
> "string"
let stringObject = new String( "String object." );
stringObject
> "object"
这可能会使严格比较运算符的使用变得复杂:
const myStringLiteral = "My string";
const myStringObject = new String( "My string" );
myStringLiteral === "My string";
> true
myStringObject === "My string";
> false
自动插入英文分号 (ASI)
在解析脚本时,JavaScript 解释器可以使用一项名为自动分号插入 (ASI) 的功能,尝试更正省略分号的情形。如果 JavaScript 解析器遇到不允许的令牌,则会尝试在该令牌前面添加英文分号以修正可能的语法错误,前提是满足以下一项或多项条件:
- 该令牌与上一个令牌之间会以换行符分隔。
- 该令牌为
}
。 - 上一个令牌为
)
,插入的分号将是do
…while
语句的结尾分号。
如需了解详情,请参阅 ASI 规则。
例如,由于 ASI,在以下语句后省略英文分号不会导致语法错误:
const myVariable = 2
myVariable + 3
> 5
不过,ASI 无法处理同一行中的多个语句。如果您要在同一行中编写多个语句,请务必用英文分号分隔它们:
const myVariable = 2 myVariable + 3
> Uncaught SyntaxError: unexpected token: identifier
const myVariable = 2; myVariable + 3;
> 5
ASI 是一种尝试性错误更正方法,而不是内置于 JavaScript 中的一种语法灵活性。请务必在适当的地方使用英文分号,以免依赖于它来生成正确的代码。
严格模式
规范 JavaScript 编写方式的标准已经远远超出了该语言在早期设计阶段考虑的任何内容。对 JavaScript 预期行为做出的每项新更改都必须避免在旧版网站中导致错误。
ES5 通过引入“严格模式”解决了 JavaScript 语义的一些长期存在的问题,而不会破坏现有实现。这种模式可让您为整个脚本或单个函数选择一组更严格的语言规则。如需启用严格模式,请在脚本或函数的第一行中使用字符串字面量 "use strict"
后跟英文分号:
"use strict";
function myFunction() {
"use strict";
}
严格模式可防止执行某些“不安全”操作或使用已废弃的功能,会抛出显式错误(而非常见的“静默”错误),并禁止使用可能会与未来语言功能冲突的语法。例如,围绕变量作用域做出的早期设计决策导致开发者在声明变量时更有可能因忽略 var
关键字而错误地“污染”全局作用域,无论包含的上下文如何:
(function() {
mySloppyGlobal = true;
}());
mySloppyGlobal;
> true
现代 JavaScript 运行时无法纠正此行为,否则可能会破坏依赖于它的任何网站(无论是出于错误还是故意)。相反,现代 JavaScript 通过让开发者为新工作选择启用严格模式,并默认仅在不会破坏旧版实现的新语言功能上下文中启用严格模式,从而防止出现这种情况:
(function() {
"use strict";
mySloppyGlobal = true;
}());
> Uncaught ReferenceError: assignment to undeclared variable mySloppyGlobal
您必须将 "use strict"
编写为字符串字面量。模板字面量 (use strict
) 不适用。此外,您还必须在任何可执行代码的预期上下文之前添加 "use strict"
。否则,解释器会忽略它。
(function() {
"use strict";
let myVariable = "String.";
console.log( myVariable );
sloppyGlobal = true;
}());
> "String."
> Uncaught ReferenceError: assignment to undeclared variable sloppyGlobal
(function() {
let myVariable = "String.";
"use strict";
console.log( myVariable );
sloppyGlobal = true;
}());
> "String." // Because there was code prior to "use strict", this variable still pollutes the global scope
按引用、按值
任何变量(包括对象的属性、函数参数以及数组、集或映射中的元素)都可以包含基元值或引用值。
将基元值从一个变量赋值给另一个变量时,JavaScript 引擎会创建该值的副本并将其赋值给变量。
当您将对象(类实例、数组和函数)分配给变量时,变量会包含对对象在内存中存储位置的引用,而不是创建该对象的新副本。因此,更改变量引用的对象会更改被引用的对象,而不仅仅是更改该变量包含的值。例如,如果您使用包含对象引用的变量初始化新变量,然后使用新变量向该对象添加属性,则该属性及其值会添加到原始对象:
const myObject = {};
const myObjectReference = myObject;
myObjectReference.myProperty = true;
myObject;
> Object { myProperty: true }
这不仅对于更改对象很重要,对于执行严格比较也很重要,因为对象之间的严格等式要求两个变量都引用同一对象才能求值为 true
。它们不能引用不同的对象,即使这些对象在结构上完全相同也是如此:
const myObject = {};
const myReferencedObject = myObject;
const myNewObject = {};
myObject === myNewObject;
> false
myObject === myReferencedObject;
> true
内存分配
JavaScript 使用自动内存管理,这意味着在开发过程中无需显式分配或取消分配内存。虽然 JavaScript 引擎的内存管理方法的详细信息超出了本单元的范围,但了解内存的分配方式有助于使用引用值。
内存中有两个“区域”:“栈”和“堆”。堆栈用于存储静态数据(基元值和对象引用),因为存储此类数据所需的固定空间量可以在脚本执行之前分配。堆用于存储对象,由于对象的大小可能会在执行期间发生变化,因此需要动态分配空间。内存由称为“垃圾回收”的进程释放,该进程会从内存中移除没有引用的对象。
主线程
JavaScript 本质上是一种单线程语言,采用“同步”执行模型,这意味着它一次只能执行一个任务。这种顺序执行上下文称为主线程。
主线程由其他浏览器任务共享,例如解析 HTML、渲染和重新渲染网页的部分内容、运行 CSS 动画,以及处理从简单(例如突出显示文本)到复杂(例如与表单元素互动)的用户互动。浏览器供应商已经找到了优化主线程执行的任务的方法,但更复杂的脚本仍可能会使用过多的主线程资源,并影响整体网页性能。
某些任务可以在称为 Web Worker 的后台线程中执行,但存在一些限制:
- 工作器线程只能对独立的 JavaScript 文件执行操作。
- 他们对浏览器窗口和界面的访问权限严重受限或完全没有访问权限。
- 它们与主线程通信的方式受到限制。
由于这些限制,这些线程非常适合执行专注且占用大量资源的任务,否则这些任务可能会占用主线程。
调用堆栈
用于管理“执行上下文”(正在积极执行的代码)的数据结构是一个名为调用堆栈(通常简称“堆栈”)的列表。首次执行脚本时,JavaScript 解释器会创建一个“全局执行上下文”并将其推送到调用堆栈,同时该全局上下文中的语句会一次执行一个,从上到下执行。当解释器在执行全局上下文时遇到函数调用时,它会将该调用的“函数执行上下文”推送到堆栈顶部,暂停全局执行上下文,并执行函数执行上下文。
每次调用函数时,系统都会将该调用的函数执行上下文推送到堆栈顶部,紧挨当前执行上下文。调用堆栈采用“后进先出”方式运作,这意味着系统会执行堆栈中最高级别的最近函数调用,并持续执行该调用,直到其解析为止。当该函数执行完毕后,解释器会将其从调用堆栈中移除,包含该函数调用的执行上下文会再次成为堆栈中的最高项,并恢复执行。
这些执行上下文会捕获执行所需的所有值。它们还会根据函数的父级上下文,确定并设置函数上下文中 this
关键字的值,并在函数上下文中建立函数作用域内可用的变量和函数。
事件循环和回调队列
这种顺序执行意味着,包含回调函数的异步任务(例如从服务器提取数据、响应用户互动或等待使用 setTimeout
或 setInterval
设置的计时器)会阻塞主线程,直到该任务完成为止,或者会在回调函数的执行上下文添加到堆栈时意外中断当前执行上下文。为解决此问题,JavaScript 使用由“事件循环”和“回调队列”(有时也称为“消息队列”)组成的事件驱动型“并发模型”来管理异步任务。
在主线程上执行异步任务时,回调函数的执行上下文会放置在回调队列中,而不是放置在调用堆栈顶部。事件循环是一种模式,有时也称为“反应器”,它会持续轮询调用堆栈和回调队列的状态。如果回调队列中有任务,并且事件循环确定调用堆栈为空,则会将回调队列中的任务一次推送到堆栈以进行执行。