原型继承
除了 null
和 undefined
之外,每个原始数据类型都有一个原型,即一个对应的对象包装器,用于提供处理值的方法。当在原始值上调用方法或属性查找时,JavaScript 会在后台包装原始值,并在包装器对象上调用方法或执行属性查找。
例如,字符串文字本身没有方法,但您可以对其调用 .toUpperCase()
方法,这要归功于相应的 String
对象包装器
"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的后台线程中执行,但有一些限制
- Worker 线程只能处理独立的 JavaScript 文件。
- 它们对浏览器窗口和 UI 的访问权限大大降低或没有访问权限。
- 它们与主线程通信的方式受到限制。
这些限制使它们非常适合专注于资源密集型任务,否则这些任务可能会占用主线程。
调用堆栈
用于管理“执行上下文”(正在主动执行的代码)的数据结构是一个名为调用堆栈(通常简称为“堆栈”)的列表。当脚本首次执行时,JavaScript 解释器会创建一个“全局执行上下文”并将其推送到调用堆栈,全局上下文中的语句一次执行一个,从上到下。当解释器在执行全局上下文时遇到函数调用时,它会将该调用的“函数执行上下文”推送到堆栈顶部,暂停全局执行上下文,并执行函数执行上下文。
每次调用函数时,该调用的函数执行上下文都会被推送到堆栈顶部,紧挨着当前执行上下文之上。调用堆栈以“后进先出”的方式运行,这意味着最近的函数调用(在堆栈中最高)会被执行并持续到它完成。当该函数完成时,解释器会将其从调用堆栈中删除,并且包含该函数调用的执行上下文再次成为堆栈中最高的项并恢复执行。
这些执行上下文捕获执行所需的任何值。它们还基于函数的父上下文建立函数作用域内可用的变量和函数,并确定和设置函数上下文中 this
关键字的值。
事件循环和回调队列
这种顺序执行意味着包含回调函数的异步任务(例如从服务器获取数据、响应用户交互或等待使用 setTimeout
或 setInterval
设置的计时器)要么会阻塞主线程直到该任务完成,要么会在回调函数的执行上下文添加到堆栈的那一刻意外中断当前执行上下文。为了解决这个问题,JavaScript 使用由“事件循环”和“回调队列”(有时称为“消息队列”)组成的事件驱动“并发模型”来管理异步任务。
当异步任务在主线程上执行时,回调函数的执行上下文会放置在回调队列中,而不是调用堆栈的顶部。事件循环是一种有时称为反应器的模式,它不断轮询调用堆栈和回调队列的状态。如果回调队列中有任务,并且事件循环确定调用堆栈为空,则回调队列中的任务会一次一个地推送到堆栈以供执行。