语法分析
语义分析
JS源代码
tokens
AST
字节码
开始运行程序
JS
源代码经过语法分析,转化成 tokens
tokens
经过语义分析,转化为 AST
(抽象语法树)
- 抽象语法树会被转化为字节码
JS
运行时开始运行这段上面生成代码
如果你想更好地理解这个过程,可以把它想象成一个人在读一篇文章。首先,他会把文章分成很多小段,也就是 tokens
。然后,他会把这些小段组合起来,形成一个完整的句子或者段落,也就是 AST
。最后,他会理解这个句子或者段落的意思,并且可以用自己的话来表达出来,也就是字节码。
当函数运行时,会执行以下步骤:
- 函数声明:当代码执行到函数声明的时候,
JavaScript
会询问作用域链,看看是否已经声明了 template
函数。如果没有声明,就会在当前作用域中创建一个 template
函数(这里template
是没有声明的,所以会在全局作用域中创建)。
console.log
中的 console
是内置对象,虽然不是我们声明的,但是它已经在全局作用域了。
-
执行 template
:JavaScript
同样会询问作用域链,看看是否已经声明了 template
函数。如果没有声明,就会报Reference Error
。
-
进入到template函数中:代码进入到了 template
函数中。我们创建了一个新的作用域,并将其指向全局作用域,从而形成了一个新的作用域链。
-
检查 num
变量:JavaScript
同样会询问作用域链,看看是否已经声明了 num
变量。在 template
函数中的新的作用域中找不到 num
变量时,它就会沿着链向上查找(如果当前作用域找到就返回),最终都找不到时就会报Reference Error
。这个过程类似于原型链。
-
……
剩下的部分我就不再解释了,我懒得写相信你应该能够理解。
实际上,作用域背后的原理是词法环境
。词法环境由两部分组成:
- 环境记录:这其实就是
JavaScript
用来存储变量的地方,一个 key-value
对在这里被称为一个 binding
。
- 外部环境的引用。
全局作用域,总是出现在作用域的最外层。全局作用域对应的环境就是全局环境
,全局作用域的外部环境引用
是 null。
由此,我们发现由于 Javascript
的解析逻辑,就会产生作用域链。当访问一个变量时,解释器会首先在当前作用域查找标示符,如果没有找到,就去父作用域找,直到找到该变量的标示符或者不在父作用域中
每一个子函数都会拷贝上级的作用域,形成一个作用域的链条。 比如:
var x = 10;
function foo() {
var y = 20;
function bar() {
var z = 30;
console.log(x + y + z); // 60
}
bar();
}
foo();
由此,闭包产生的本质就是,当前环境中存在指向父级作用域的引用。
❓闭包有哪些表现形式?
明白了本质之后,我们就来看看,在真实的场景中,究竟在哪些地方能体现闭包的存在?
-
返回一个函数:
// 定义一个函数,接受一个参数 x,并返回一个新的函数
function makeAdder(x) {
// 返回一个匿名函数,该函数接受一个参数 y,并返回 x + y
return function(y) {
return x + y;
};
}
// 调用 makeAdder() 函数,并传入 5 作为参数,得到一个新的函数 add5
var add5 = makeAdder(5);
// 调用 add5() 函数,并传入 2 作为参数,得到结果 7
var result = add5(2);
// 输出结果
console.log(result); // 7
-
把整个函数作为参数传入:
// 定义一个比较函数,按照字符串长度升序排列
function compareByLength(a, b) {
return a.length - b.length;
}
// 创建一个字符串数组
let fruits = ["apple", "banana", "cherry", "durian", "elderberry"];
// 调用 sort() 方法,并传入比较函数作为参数
// 这就是闭包
fruits.sort(compareByLength);
// 输出排序后的数组
console.log(fruits); // ["apple", "banana", "cherry", "durian", "elderberry"]
-
在定时器、事件监听、Ajax请求、跨窗口通信、Web Workers或者任何异步中,只要使用了回调函数,实际上就是在使用闭包。
// 定时器
setTimeout(function timeHandler(){
console.log('111');
},100)
-
IIFE(立即执行函数表达式)创建闭包, 保存了全局作用域window
和当前函数的作用域
,因此可以全局的变量。
var x = 2;
(function IIFE(){
// 输出2
console.log(x)
})();
❓思考:为什么不能像开头那样写?
我们回到文章首页那道题:
for (var i = 1; i <= 5; i++) {
setTimeout(function() {
console.log(i);
}, i * 1000);
}
你觉得这样会打印出什么呢?1、2、3、4、5 吗?不好意思,答错了。实际上,这样会打印出 6、6、6、6、6 。为什么呢?因为当你的 setTimeout
轮到执行的时候,循环已经结束了,i 的值已经变成了 6。
而且,用的是 var 来声明 i,这样就把 i 变成了全局变量?。
所以,当你的 setTimeout
里面的函数想要找 i 的时候,它就会去全局找,发现了 i 是 6,就打印出来了?。
为什么会全部输出6?如何改进,让它输出1,2,3,4,5?(方法越多越好)
因为setTimeout
为宏任务,由于JavaScript
中单线程eventLoop
机制,在主线程同步任务执行完后才去执行宏任务,因此循环结束后setTimeout
中的回调才依次执行,但输出i的时候当前作用域没有,往上一级再找,发现了i,此时循环已经结束,i变成了6。因此会全部输出6。就像是一条单行道,一次只能走一个车?。所以,setTimeout
就像是一个耐心的司机,它会把自己的车停在路边,等到前面的车都走完了,才会开上去?。
解决方法:
1、利用IIFE(立即执行函数表达式)当每次for循环时,把此时的i变量传递到定时器中
for(var i = 1;i <= 5;i++){
(function(j){
setTimeout(function() {
console.log(j);
}, j * 1000);
})(i)
}
2、给定时器传入第三个参数, 作为timer函数的第一个函数参数
for(var i = 1;i <= 5;i++){
setTimeout(function timer(j){
console.log(j)
}, j * 1000, i)
}
3、使用ES6中的let
for (let i = 1; i <= 5; i++) {
setTimeout(function() {
console.log(i);
}, i * 1000);
}
? 你觉得怎么样?这篇文章可以给你带来帮助吗?如果你有任何疑问或者想进一步讨论相关话题,请随时发表评论分享您的想法,让其他人从中受益。?✨
© 版权声明
文章版权归作者所有,未经允许请勿转载,侵权请联系 admin@trc20.tw 删除。
THE END
喜欢就支持一下吧
Warning: mysqli_query(): (HY000/3): Error writing file '/tmp/MYRcPCn3' (Errcode: 28 - No space left on device) in /www/wwwroot/583.cn/wp-includes/class-wpdb.php on line 2345