温故而知新,项目做得多了,发现实际应用的经验涨了,但时间一长,对于JS的一些概念性的知识点反而有些模糊了。所以萌发了重新系统回顾和复习下的念头,写下这篇文章来记录。
目录
内容
序章
JavaScript是一门高级、动态、弱类型的编程语言。JavaScript的应用范围非常广泛,包括Web开发、移动应用开发、游戏开发、桌面应用程序开发等。JavaScript的生态系统非常丰富,有大量的开源库和框架可供使用。前端同学的必备技能。
前端开发编写js通常运行在浏览器中,所以这里面又与浏览器有着许多关系。前端的html, css, js是必修功课。
一道题引起兴趣:JS的闭包与事件循环的综合题
一起看下这个经典题,分析下其中的原理吧。
for (var i=1; i<=5; i++) {
setTimeout(function timer() {
console.log(i)
}, 0 )
}
要点分析:
-
事件循环,for里面的执行被setTimeout包裹后,执行机制应该是什么样呢?
基于js的事件循环机制,setTimeout包裹的内容只会在for循环完成之后才开始执行!因为setTimeout是宏任务啊。for是同步的,同步的先跑,然后再微任务,再宏任务啊。
-
setTimeout之后,i的值是多少? 我们可以直接debug看得清清楚楚。
- 未执行 这时 i = undefined
- 进入第一次for循环 这时 i = 1
- 直接进入第二次for循环(并没有进入setTimeout内部,完全印证了JS事件循环的先后机制),这时 i = 2
- 如此重复,直到 i = 5, 注意!!!接下来重点来了!
- 进入了setTimeout,i的赋值是6!
- 跳入仔细一看,for循环还在执行,
- for循环会执行到i=6,然后6不符合条件,所以没有进入内部逻辑
- 接下来进入for循环内部后,就是执行5次setTimeout的内容,所以会输出5次i,每次都是6;
- 未执行 这时 i = undefined
JS的编译
三个步骤:
- 分词/词法分析(Tokenizing/Lexing)
- 解析/语法分析(Parsing)
- 代码生成
对于JavaScript来说,大部分情况下编译发生在代码执行前的几微秒(甚至更短!)的时间内。在我们所要讨论的作用域背后,JavaScript引擎用尽了各种办法(比如JIT,可以延迟编译甚至实施重编译)来保证性能最佳。简单地说,任何JavaScript代码片段在执行前都要进行编译(通常就在执行前)。
JS的作用域
作用域是指变量的可访问范围,JavaScript中的作用域分为全局作用域和函数作用域。全局作用域中的变量可以在整个程序中访问,而函数作用域中的变量只能在函数内部访问。JavaScript中的作用域是静态作用域,即在编译时就确定了变量的作用域。
我们肯能需要注意词法作用域和函数作用域。
- 词法作用域
词法作用域意味着作用域是由书写代码时函数声明的位置来决定的。
- 包含着整个全局作用域,其中只有一个标识符:foo。
- 包含着foo所创建的作用域,其中有三个标识符:a、bar和b。
- 包含着bar所创建的作用域,其中只有一个标识符:c。
-
函数作用域
JavaScript具有基于函数的作用域,意味着每声明一个函数都会为其自身创建一个气泡,而其他结构都不会创建作用域气泡。
JS中基于函数会形成这样一个个封闭的空间,用来存储变量,函数数据。
这里面我们就首先要理解,这些封闭的空间之间的关系
- 他们是可以嵌套的
- 他们之间彼此隔离
- 寻找变量时从嵌套最内层开始找,一层层往外找
-
块作用域
尽管函数作用域是最常见的作用域单元,当然也是现行大多数JavaScript中最普遍的设计方法,但其他类型的作用域单元也是存在的,并且通过使用其他类型的作用域单元甚至可以实现维护起来更加优秀、简洁的代码。
ES6引入了新的let关键字,提供了除var以外的另一种变量声明方式。
除了let以外,ES6还引入了const,同样可以用来创建块作用域变量,但其值是固定的(常量)。之后任何试图修改值的操作都会引起错误。
var foo = true; if (foo) { var a = 2; const b = 3; // 包含在if中的块作用域常量 a = 3; // 正常! b = 4; // 错误! } console.log(a); // 3 console.log(b); // ReferenceError!
JS的提升
到现在为止,你应该已经很熟悉作用域的概念,以及根据声明的位置和方式将变量分配给作用域的相关原理了。
直觉上会认为JavaScript代码在执行时是由上到下一行一行执行的。但实际上这并不完全正确,有一种特殊情况会导致这个假设是错误的。
var a;
console.log(a);
a = 2;
很多开发者会认为是undefined,因为var a声明在a = 2之后,他们自然而然地认为变量被重新赋值了,因此会被赋予默认值undefined。但是,真正的输出结果是2。
只有声明本身会被提升,而赋值或其他运行逻辑会留在原地。如果提升改变了代码执行的顺序,会造成非常严重的破坏。
foo();
function foo() {
console.log(a); // undefined
var a = 2;
}
foo函数的声明(这个例子还包括实际函数的隐含值)被提升了,因此第一行中的调用可以正常执行。只是输出的值为undefined。
函数声明和变量声明都会被提升。
注意下面它所遵循的2个规律:
-
注意下这里"函数会首先被提升,然后才是变量"!
简单说就是function(){}
始终有限级别更高于下面的这些变量申明
const a = function(){} const a = "aaa"
-
其次就是出现在后面的函数声明还是可以覆盖前面的。
JS的闭包
简单说JavaScript的闭包是指函数可以访问它定义时所在的词法作用域中的变量,即使函数在其他地方被调用,也可以访问这些变量。
在JavaScript中,每个函数都有一个作用域链,它包含了函数定义时所在的词法作用域和所有父级作用域。当函数需要访问一个变量时,它会先在自己的作用域中查找,如果找不到,就会沿着作用域链向上查找,直到找到为止。
如果一个函数定义在另一个函数内部,它就可以访问外部函数的变量,这种函数就是闭包。具体来说,闭包可以让我们在函数内部创建私有变量和私有方法,同时还可以访问外部函数的变量和方法。
function outer() {
var count = 0;
function inner() {
count++;
console.log(count);
}
return inner;
}
var counter = outer();
counter(); // 1
counter(); // 2
counter(); // 3
在上面的示例代码中,我们定义了一个outer函数和一个inner函数。
outer函数返回inner函数,这样就创建了一个闭包。
在inner函数中,我们访问了outer函数中的变量count,这个变量在outer函数执行完毕后仍然可以被访问。
我们将outer函数返回的inner函数赋值给了counter变量,这样就可以在全局作用域中访问inner函数了。每次调用counter函数时,它都会输出一个递增的数字,这是因为它访问了闭包中的变量count。
总之,JavaScript的闭包是一种强大的特性,它可以让我们在函数内部创建私有变量和私有方法,同时还可以访问外部函数的变量和方法。通过使用闭包,我们可以编写更加模块化、可重用和安全的代码。
JS的事件循环
javaScript的时间循环机制是指JavaScript引擎在执行代码时,会将异步任务放入事件队列中,等待执行。
当JavaScript引擎执行完同步任务后,就会从事件队列中取出一个异步任务,执行它的回调函数,然后再执行下一个异步任务,以此类推。
JavaScript中的事件队列分为宏任务队列和微任务队列。
宏任务队列中包含了所有的异步任务,如setTimeout、setInterval、XMLHttpRequest等。
微任务队列中包含了Promise的回调函数、MutationObserver的回调函数等。
当JavaScript引擎执行完所有的同步任务后,会先执行微任务队列中的所有任务,然后再执行宏任务队列中的任务。
在执行微任务队列中的任务时,如果又产生了新的微任务,就会继续执行微任务队列中的任务,直到微任务队列为空为止。
然后再执行宏任务队列中的任务,以此类推。 给个例子如下:
console.log('start');
setTimeout(function() {
console.log('setTimeout');
}, 0);
Promise.resolve().then(function() {
console.log('Promise');
});
console.log('end');
输出顺序是 start end Promise setTimeout
0 条评论