h246802 / blogs Goto Github PK
View Code? Open in Web Editor NEW翻译一些文章同时记录一些学习所得
翻译一些文章同时记录一些学习所得
JavaScript 是一种单线程编程语言,这意味着同一时间只能完成一件事情。也就是说,JavaScript 引擎只能在单一线程中处理一次语句。
单线程语言简化了代码编写,因为你不必担心并发问题,但这也意味着你无法在不阻塞主线程的情况下执行网络请求等长时间操作。
想象一下从 API 中请求一些数据。根据情况,服务器可能需要一些时间来处理请求,同时阻塞主线程,让网页无法响应。
这也就是异步 JavaScript 的美妙之处了。使用异步 JavaScript(例如回调,Promise 或者 async/await),你可以执行长时间网络请求同时不会阻塞主线程。
虽然您没有必要将所有这些概念都学会成为一名出色的 JavaScript 开发人员,但了解这些对你会很有帮助 :)
所以不用多说了,让我们开始吧!
在深入研究异步 JavaScript 之前,让我们首先了解同步 JavaScript 代码在 JavaScript 引擎中的执行情况。例如:
const second = () => {
console.log('Hello there!');
}
const first = () => {
console.log('Hi there!');
second();
console.log('The End');
}
first();
要理解上述代码在 JavaScript 引擎中的执行方式,我们必须理解执行上下文和调用栈(也称为执行栈)的概念。
执行上下文是评估和执行 JavaScript 代码的环境的抽象概念。每当在 JavaScript 中运行任何代码时,它都在执行上下文中运行。
函数代码在函数执行上下文中执行,全局代码在全局执行上下文中执行。每个函数都有自己的执行上下文。
顾名思义,调用栈是一个具有 LIFO(后进先出)结构的栈,用于存储代码执行期间创建的所有执行上下文。
JavaScript 有一个单独的调用栈,因为它是一种单线程编程语言。调用栈具有 LIFO 结构,这意味着只能从调用栈顶部添加或删除元素。
让我们回到上面的代码片段以便尝试理解代码在 JavaScript 引擎中的执行方式。
const second = () => {
console.log('Hello there!');
}
const first = () => {
console.log('Hi there!');
second();
console.log('The End');
}
first();
上述代码的调用栈工作情况
当代码执行的时候,会创建一个全局执行上下文(由 main()
表示)并将其推到执行栈的顶部。当对 first()
函数调用时,它会被推送的栈的顶部。
接下来,console.log('Hi there!')
被推到调用栈的顶部,当它执行完成后,它会从调用栈中弹出。在它之后,我们调用 second()
,因此 second()
函数被推送到调用栈的顶部。
console.log('Hello there!')
被推到调用栈顶部并在完成后从调用栈中弹出。second()
函数执行完成,接着它从调用栈中弹出。
console.log('The End')
被推到调用栈顶部并在完成后被删除。之后,first()
函数执行完成,因此它从调用栈中删除。
程序此时完成其执行,因此从调用栈中弹出全局执行上下文(main()
)。
现在我们已经了解了相关调用栈的基本概念,以及同步 JavaScript 的工作原理,现在让我们回到异步 JavaScript。
假设我们正在以同步方式进行图像处理或网络请求。例如:
const processImage = (image) => {
/**
* 对图像进行一些操作
**/
console.log('Image processed');
}
const networkRequest = (url) => {
/**
* 请求网络资源
**/
return someData;
}
const greeting = () => {
console.log('Hello World');
}
processImage(logo.jpg);
networkRequest('www.somerandomurl.com');
greeting();
进行图像处理和网络请求都需要时间。因此,当 processImage()
函数调用时需要一些时间,具体多少时间根据图像的大小决定。
当 processImage()
函数完成时,它将从调用栈中删除。之后调用 networkRequest()
函数并将其推送到执行栈。同样,它还需要一些时间才能完成执行。
最后,当 networkRequest()
函数完成时,调用 greeting()
函数,因为它只包含 console.log
语句,而 console.log
语句通常很快,所以 greeting()
函数会立即执行并返回。
所以你可以看到,我们必须等到函数(例如 processImage()
或 networkRequest()
)完成。这也就意味着这些函数阻塞了调用栈或主线程。因此,在执行上述代码时,我们无法执行任何其他操作,这是不理想的。
最简单的解决办法是异步回调,我们通常使用异步回调来让代码无阻塞。例如:
const networkRequest = () => {
setTimeout(() => {
console.log('Async Code');
}, 2000);
};
console.log('Hello World');
networkRequest();
这里我使用了 setTimeout
方法来模拟网络请求。请记住,setTimeout
不是 JavaScript 引擎的一部分,它是 Web APIs(在浏览器中)和 C/C++ APIs(在 node.js 中)的一部分。
要了解如何执行此代码,我们必须了解一些其他概念,例如事件循环和回调队列(也称为任务队列或消息队列)。
JavaScript 运行时环境概述
事件循环,Web APIs 和 消息队列/任务队列 不是 JavaScript 引擎的一部分,它是浏览器的 JavaScript 运行所处环境或 Nodejs JavaScript 运行所处环境中的一部分(在 Nodejs 的环境下)。在 Nodejs 中,Web APIs 被 C/C++ APIs 取代。
现在让我们回过头看看上面的代码,看看它是如何以异步方式执行的。
const networkRequest = () => {
setTimeout(() => {
console.log('Async Code');
}, 2000);
};
console.log('Hello World');
networkRequest();
console.log('The End');
Event Loop(事件循环)
当上面的代码在浏览器中运行时,console.log('Hello World')
被推送到栈,在执行完成后从栈中弹出。紧接着,遇到 networkRequest()
的执行,因此将其推送到栈顶部。
接下来调用 setTimeout()
函数,因此将其推送到栈顶部。setTimeout()
有两个参数:1) 回调和 2) 以毫秒(ms)为单位的时间。
setTimeout()
方法在 Web APIs 环境中启动 2s
的计时器。此时,setTimeout()
已完成,并从调用栈中弹出。在它之后,console.log('The End')
被推送到栈,在执行完成后从调用栈中删除。
同时,计时器已到期,现在回调函数被推送到消息队列。但回调函数并没有立即执行,而这就是形成了一个事件循环(Event Loop)。
事件循环的作用是查看调用栈并确定调用栈是否为空。如果调用栈为空,它会查看消息队列以查看是否有任何挂起的回调等待执行。
在这个例子中,消息队列包含一个回调,此时调用栈为空。因此,事件循环(Event Loop)将回调推送到调用栈顶部。
再之后,console.log('Async Code')
被推到栈顶部,执行并从调用栈中弹出。此时,回调函数已完成,因此将其从调用栈中删除,程序最终完成。
消息队列还包含来自 DOM 事件的回调,例如点击事件和键盘事件。
例如:
document.querySelector('.btn').addEventListener('click',(event) => {
console.log('Button Clicked');
});
在DOM事件的情况下,事件监听器位于 Web APIs 环境中等待某个事件(在这种情况下是点击事件)发生,并且当该事件发生时,则回调函数被放置在等待执行的消息队列中。
事件循环再次检查调用栈是否为空,如果它为空并且执行了回调,则将事件回调推送到调用栈。
我们已经知道了如何执行异步回调和 DOM 事件,它们使用消息队列来存储等待执行的所有回调。
ES6 引入了 Promises 在 JavaScript 中使用的工作队列/微任务队列的概念。消息队列和微任务队列之间的区别在于工作队列的优先级高于消息队列,这意味着 工作队列/微任务队列中的 promise 工作将在消息队列内的回调之前执行。
例如:
console.log('Script start');
setTimeout(() => {
console.log('setTimeout');
}, 0);
new Promise((resolve, reject) => {
resolve('Promise resolved');
}).then(res => console.log(res))
.catch(err => console.log(err));
console.log('Script End');
输出:
Script start
Script End
Promise resolved
setTimeout
我们可以看到 promise 在 setTimeout
之前执行,因为 promise 响应存储在微任务队列中,其优先级高于消息队列。
让我们再看一个例子,这次有两个 promise 和两个 setTimeout。例如:
console.log('Script start');
setTimeout(() => {
console.log('setTimeout 1');
}, 0);
setTimeout(() => {
console.log('setTimeout 2');
}, 0);
new Promise((resolve, reject) => {
resolve('Promise 1 resolved');
}).then(res => console.log(res))
.catch(err => console.log(err));
new Promise((resolve, reject) => {
resolve('Promise 2 resolved');
}).then(res => console.log(res))
.catch(err => console.log(err));
console.log('Script End');
输出:
Script start
Script End
Promise 1 resolved
Promise 2 resolved
setTimeout 1
setTimeout 2
我们可以看到两个 promise 都在 setTimeout
中的回调之前执行,因为事件循环将微任务队列中的任务优先于消息队列/任务队列中的任务。
当事件循环正在执行微任务队列中的任务时,如果另一个 promise 执行 resolve 方法,那么它将被添加到同一个微任务队列的末尾,并且它将在消息队列的所有回调之前执行,无论消息队列回调等待执行花费了多少时间。
例如:
console.log('Script start');
setTimeout(() => {
console.log('setTimeout');
}, 0);
new Promise((resolve, reject) => {
resolve('Promise 1 resolved');
}).then(res => console.log(res));
new Promise((resolve, reject) => {
resolve('Promise 2 resolved');
}).then(res => {
console.log(res);
return new Promise((resolve, reject) => {
resolve('Promise 3 resolved');
})
}).then(res => console.log(res));
console.log('Script End');
输出:
Script start
Script End
Promise 1 resolved
Promise 2 resolved
Promise 3 resolved
setTimeout
因此,微任务队列中的所有任务都将在消息队列中的任务之前执行。也就是说,事件循环将首先在执行消息队列中的任何回调之前清空微任务队列。
因此,我们已经了解了异步 JavaScript 如何工作以及其他概念,例如调用栈,事件循环,消息队列/任务队列和工作队列/微任务队列,它们共同构成了 JavaScript 运行时环境。虽然您没有必要将所有这些概念都学习成为一名出色的 JavaScript 开发人员,但了解这些概念会很有帮助 :)
译者注:
文中工作队列(Job Queue)也就是微任务队列,而消息队列则是指我们通常聊得宏任务队列。
如果你是一名 JavaScript 开发者,或者想要成为一名 JavaScript 开发者,那么你必须知道 JavaScript 程序内部的执行机制。理解执行上下文和执行栈同样有助于理解其他的 JavaScript 概念如变量提升机制、作用域和闭包等。
正确理解执行上下文和执行栈的概念将有助于你成为一名更好的 JavaScript 开发人员。
所以不用多说了,让我们开始吧!
简单来说,执行上下文就是当前JavaScript代码被解析和执行时所在环境的抽象概念,JavaScript中运行的所有的代码都是在执行上下文中运行。
执行上下文总共有三种类型
全局执行上下文: 这是默认的、最基础的执行上下文。对于不在任何函数中的代码都位于全局执行上下文中。它做了两件事
this
指针指向这个全局对象。一个对象只能存在一个全局执行上下文
函数执行上下文: 每次调用函数时,都会为该函数创建一个新的执行上下文。每个函数都拥有自己的执行上下文,但是只有在函数被调用的时候才会被创建。一个程序可以存在任意数量的函数执行上下文。每当一个新的执行上下文被创建,它都会按照特定的顺序执行一系列步骤
Eval函数执行上下文: 运行在 eval
函数中的代码也获得了自己的执行上下文,但由于JavaScript开发人员通常不使用eval
,因此我不在文中讨论
执行栈,在其他语言中也会称之为调用栈,具有LIFO(后进先出)结构,用于存储代码执行期间创建的所有执行上下文
每当 JavaScript 引擎首次读取你的JavaScript代码时,它会创建一个全局执行上下文并将其推入当前的执行栈。每当JavaScript引擎找到一个函数调用,引擎都会为该函数创建一个新的执行上下文并将其推到当前执行栈的顶部
引擎会先运行其执行上下文在执行栈顶端的函数,当此函数运行完成后,其对应的执行上下文将会从执行栈中弹出,上下文控制权将移到当前执行栈的下一个执行上下文
让我们通过下面的代码示例来理解这一点:
let a = 'Hello World!';
function first() {
console.log('Inside first function');
second();
console.log('Again inside first function');
}
function second() {
console.log('Inside second function');
}
first();
console.log('Inside Global Execution Context');
上述代码的执行上下文栈。
当上述代码在浏览器中加载时,Javascript引擎会创建一个全局执行上下文并将其推送到当前执行栈。当开始对 first()
的调用时,Javascript引擎为该函数创建一个新的执行上下文,并将其推送到当前执行栈的顶部。
当从 first()
函数中调用 second()
函数时,Javascript引擎为该函数创建一个新的执行上下文,并将其推送到当前执行栈的顶部。当 second()
函数执行完成后,它的执行上下文从当前执行栈栈中弹出,上下文控制权将移到当前执行栈的下一个执行上下文,即 first
函数执行上下文
当 first()
完成时,其执行栈将从栈中移除,并且上下文控制权将移至全局执行上下文。一旦执行完所有代码,JavaScript引擎就会从当前栈中删除全局执行上下文。
到目前为止,我们已经看到 JavaScript 引擎如何管理执行上下文。现在让我们理解 JavaScript 引擎如何创建执行上下文。
执行上下文分两个阶段创建:1)创建阶段、2)执行阶段。
在任意的 JavaScript 代码被执行前,执行上下文处于创建阶段。在创建阶段总共发生了三件事
因此执行上下文可以在概念上表示为:
ExecutionContext = {
ThisBinding = <this value>,
LexicalEnvironment = {...},
VariableEnvironment = {...},
}
在全局执行上下文中,this
的值指向全局对象。(在浏览器中,this
的值指向 window对象)
在函数执行上下文中,this
的值取决于函数的调用方式。如果它被一个对象引用调用,那么 this
的值被设置为该对象,否则将其值设置为全局对象或未定义(在严格模式下)。
例如:
let person = {
name: 'peter',
birthYear: 1994,
calcAge: function() {
console.log(2018 - this.birthYear);
}
}
person.calcAge();
// 'this' 指向 'person', 因为 'calcAge' 被 'person' object 调用
let calculateAge = person.calcAge;
calculateAge();
// 'this' 指向全局window对象 因为没有给出对象引用
官方ES6 文档将词法环境定义为:
词法环境是一种规范类型,基于 ECMAScript 代码的词法嵌套结构来定义标识符与特定变量和函数的关联关系。词法环境由环境记录(environment record)和可能给为空引用(null)的外部词法环境组成
简而言之,词法环境是一个包含标识符变量映射的结构。(这里的标识符表示变量/函数的名称,变量是对实际对象【包括函数类型对象】或原始值的引用)
在词法环境中,有两个组成部分:(1)环境记录(environment record) (2)对外部词法环境的引用
词法环境有两种类型:
this
的值指向这个全局对象。环境记录
中。对外部词法环境的引用可以是全局,也可以是包含内部函数的外部函数环境。注 -对于函数环境而言,环境记录 还包含了一个 arguments
对象,该对象包含了索引和传递给函数的参数之间的映射以及传递给函数的参数的长度(数量)。
例如,以下函数的参数对象如下所示:
function foo(a, b) {
var c = a + b;
}
foo(2, 3);
// argument object
Arguments: {0: 2, 1: 3, length: 2},
环境记录(environment record) 同样有两种类型(如下所示)
抽象来讲,我们可以写出下面的伪代码来表示不同类型下的词法环境
// 全局执行上下文
GlobalExecutionContext = {
LexicalEnvironment = { // 词法环境
EnvironmentRecord:{ // 环境记录
Type:'Object', // 对象型环境记录
// 包括环境记录的变量、函数
}
outer:<null> // 对外部词法环境的引用
}
}
// 函数执行上下文
FunctionExecutionContext = {
LexicalEnvironment = { // 词法环境
EnvironmentRecord:{ // 环境记录
Type:'Declarative' // 声明性环境记录
// 包括环境记录的变量、函数、参数
},
// 对外部词法环境的引用
outer:<Global or outer function lexical environment reference>
}
}
它也是一个词法环境,其EnvironmentRecord
包含了由 VariableStatements(变量声明)
在此执行上下文创建的绑定。
如上所述,变量环境也是一个词法环境,因此它具有上面定义的词法环境的所有属性。
在ES6中,LexicalEnvironment 和 VariableEnvironment 组件的区别在于前者用于存储函数声明和变量(let
和const
)的绑定,后者仅仅用于存储变量(var
)绑定
结合实际代码可以更清晰理解概念:
let a = 20;
const b = 30;
var c;
function multiply(e, f) {
var g = 20;
return e * f * g;
}
c = multiply(20, 30);
伪代码的执行上下文如下所示:
// 全局执行上下文
GlobalExecutionContext = {
ThisBinding:<Global Object>,
LexicalEnvironment:{
EnvironmentRecord:{
Type:'Object',
a:<uninitialized>, // a变量uninitialized(未初始化),
b:<uninitialized>, // b变量uninitialized(未初始化)
multiply:<func...> //
},
outer:<null> // 引用外部词法环境
},
VariableEnvironment:{
EnvironmentRecord:{
Type:'Object',
c:<undefined> // c变量在执行上下文期间先定义为undefined
},
outer:<null> // 引用外部词法环境
}
}
// 函数执行上下文
FunctionExecutionContext = {
ThisBinding:<Global Object>, // 根据调用确定
LexicalEnvironment:{
EnvironmentRecord:{
Type:'Declarative', // 声明型词法环境
Arguments:{0:20,1:30,length:2}
},
outer:<GlobalLexicalEnvironment> // 引用全局词法环境
},
VariableEnvironment:{
EnvironmentRecord:{
Type:'Declarative',
g:<undefined> // g变量在执行上下文期间先定义为undefined
},
outer:<GlobalLexicalEnvironment> // 引用外部词法环境
}
}
注 -只有在multiply
函数调用的时候才会创建函数的执行上下文
你可能已经注意到了 let
和 const
定义的变量没有任何与之关联的值,此时为未初始化状态,但 var
定义的变量设置为undefined
。
这是因为在创建阶段,代码会被扫描并解析变量和函数声明,其中函数声明存储在环境中,而变量会被设置为undefined
(在 var
的情况下)或保持未初始化(在let
和 const
的情况下)。
这就是为什么你可以在声明之前访问var
定义的变量(尽管是 undefined
),但如果在声明之前访问 let
和 const
定义的变量就会提示引用错误的原因。
这就是我们所谓的变量提升。
这是整篇文章中最简单的部分。在此阶段,完成对所有变量的分配,最后执行代码。
注: 在执行阶段,如果 Javascript 引擎在源代码中声明的实际位置找不到 let 变量的值,那么将为其分配 undefined 值。
我们已经讨论了 JavaScript 内部是如何执行的。虽然你没有必要学习这些所有的概念从而成为一名出色的 JavaScript 开发人员,但对上述概念的理解将有助于你更轻松、更深入地理解其他概念,如变量提升、域和闭包等。
译者注:
柯里化(Currying),又称为部分求值,是把接受多个参数的函数变换成接受一个单一参数(最初函数的第一个参数)的函数,并且返回一个新的函数的技术,新函数接受余下参数并返回运算结果。
在JavaScript 中,我们所可以获取的最大的安全整数区间在于 是 -(2^{53}-1)~ 2^{53}-1 ,即:-9007199254740991 ~ 9007199254740991。也就是说超出这个数以后,精度就得不到保障。但有时候我们的需求不止要这样。我们需要更准确的数字
所以我们可以换一种思路,用字符串来表示数字,比如 "9007199254740991123",这样就不存在四舍五入导致的精度丢失的问题,我们就可以通过对字符串进行
加法
运算得到想要的结果。
看了方应杭老师的一篇解释 let 的文章,对JavaScript中的声明有了深刻的理解,模仿着按着自己思路写了些这篇文章
在我刚刚开始学前端的时候,那时候还是处于2016年的年中,也是前端最火的时候。第一次学到 JS 的时候,我记得我看的是网课,课上老师斩钉截铁的说 JS 不存在块级作用域。因此也就有了一些关于作用域的考题。例如:
for (var i = 0; i < 5; i++) {
setTimeout(function() {
console.log(new Date, i);
}, 1000);
}
console.log(new Date, i); // ?输出结果是多少
对于早已将答案背下来的我,自然可以自信的答下 5,间隔一秒后连续输出五个5
,对应的如何输出 0,1,2,3,4
,自然也有了答案:
for (var i = 0; i < 5; i++) {
(function(i){
setTimeout(function() {
console.log(new Date, i);
}, 1000);
})(i)
}
在 es6 出现之前,那位老师说的是没错的,在2015年的时候 es6 出现了。从此 JavaScript 有了它的块级作用域。对于上题也就有了更好的答案。
for (let i = 0; i < 5; i++) {
setTimeout(function() {
console.log(new Date, i);
}, 1000);
}
因此我就带着对神奇的 let 疑问去学习了它。
对于一个新的知识,我认识到 let 还是从 MDN 的文档入的手。
let 语句声明一个块级作用域的本地变量,并且可选的将其初始化为一个值。
第一次认识 let ,匆匆的过去了,得到的就这么些信息。
undefined
的过程{}
存在块级作用域这是简单获取的信息,但在阅读完 方应杭 老师的这篇文章后,又获取了更多的信息。
在文章中提出来一个在此之前我不曾了解的信息。
对于我们所了解的变量的声明以及函数声明,在JavaScript中一般是存在创建create、初始化initialize 和赋值assign三个过程的,在这三个过程中:
创建:即在变量所在作用于头部抛出变量,仅仅是抛出,是不能被使用的;
初始化:在变量初始化之前变量是不能被使用的,初始化只有一次,初始化之后变量可以使用;
赋值:即覆盖初始化的值
在let、var、const(包括函数声明)的声明过程中,他们的创建create、初始化initialize 和赋值assign是有区别的。
{
let x = 1
x = 2
}
function fn(){
var a = 1
var a = 2
}
fn()
const声明创建了一个常量
const和let比较像,也是块级作用域,区别就是,const在定义的时候必须初始化,而且不能被赋值,不能使用 const a
{
const str = 'str'
}
最后可以用一张表格进行总结
-- | var | let | const |
---|---|---|---|
变量/常量 | 变量 | 变量 | 常量 |
作用域 | 函数作用域 | 块级作用域 | 块级作用域 |
创建 | 作用域顶部创建 | 作用域顶部创建 | 作用域顶部创建 |
初始化 | let语句 | 作用域顶部 | const语句 |
赋值 | 可以赋值 | 可以赋值 | 报错 |
重复声明 | 报错 | 可以重复声明 | 报错 |
暂时性死区 | 有 | 没有 | 有 |
A declarative, efficient, and flexible JavaScript library for building user interfaces.
🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.
TypeScript is a superset of JavaScript that compiles to clean JavaScript output.
An Open Source Machine Learning Framework for Everyone
The Web framework for perfectionists with deadlines.
A PHP framework for web artisans
Bring data to life with SVG, Canvas and HTML. 📊📈🎉
JavaScript (JS) is a lightweight interpreted programming language with first-class functions.
Some thing interesting about web. New door for the world.
A server is a program made to process requests and deliver data to clients.
Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.
Some thing interesting about visualization, use data art
Some thing interesting about game, make everyone happy.
We are working to build community through open source technology. NB: members must have two-factor auth.
Open source projects and samples from Microsoft.
Google ❤️ Open Source for everyone.
Alibaba Open Source for everyone
Data-Driven Documents codes.
China tencent open source team.