作用域
- 作用域就是代码的执行环境,全局执行环境就是全局作用域,函数的执行环境就是私有作用域,它们都是栈内存。总的来说,作用域就是代码执行时开辟的栈内存。
总结 | 描述 |
---|---|
私有作用域 | 函数执行都会形成一个私有作用域 |
全局作用域 | 页面一打开就会形成一个全局的作用域 |
私有变量 | 在私有作用域里边形成的变量 (通过 var 声明; 形参) |
全局变量 | 在全局作用域形成的变量(var a = 12 或者函数内没有声明,直接赋值的变量) |
- 某个执行环境中所有的代码执行完毕后,该环境被销毁,保存在其中的所有变量和函数定义也随之销毁(全局执行环境直到应用程序退出时,如关闭浏览器或网页,才会被销毁)
- 每个函数都有自己的执行环境。当执行流进入一个函数时,函数的环境就会被推入一个环境栈中。而在函数执行之后,栈将被环境弹出,把控制权返回给之前的执行环境。ECMAScript 程序中的执行流正是由这个方便的机制控制着。
作用域链
- 当代码在一个环境中执行时,会创建变量对象的一个作用域链(作用域形成的链条)
- 作用域链的前端,始终都是当前执行的代码所在环境的变量对象
- 作用域链中的下一个对象来自于外部环境,而在下一个变量对象则来自下一个外部环境,一直到全局执行环境
- 全局执行环境的变量对象始终都是作用域链上的最后一个对象
- 内部环境可以通过作用域链访问所有外部环境,但外部环境不能访问内部环境的任何变量和函数。
- 所以执行函数时,作用域链是从内到外来排序的。(有形参找形参,没有才找外部的全局变量)
形参与实参
参数 | 概念 |
---|---|
形参(形式参数) | 是在定义函数名和函数体的时候使用的参数,目的是用来接收调用该函数时传递的参数。 |
实参(实际参数) | 是在调用时传递给函数的参数,即传递给被调用函数的值。 |
- 形参变量只有在被调用时才分配内存单元,在调用结束时,即刻释放所分配的内存单元。因此,形参只在函数内部有效。函数调用结束返回主调用函数后则不能再使用该形参变量。(随着函数被调用而新建,随着函数销毁而销毁)
- 函数调用中发生的数据传送是单向的,即只能把实参的值传给形参,而不能把形参的值传给实参。因此,在函数调用的过程中,形参的值可以改变,而实参的值则不会变化。
- 参考
执行栈
例子
注意:执行函数时,作用域链是从内到外来排序的。(有形参找形参,没有才找外部的全局变量)
1 | var count = 0; |
- 函数的形参属于函数执行上下文,所以当指定这个形参后,它就随着函数被调用而新建,随着函数销毁而销毁。
- 指定了这个形参,调用函数时传递进来的实参count(0)会沿着作用域链找到私有变量 count(接下来的操作都在私有变量上进行),而不是全局变量count。
步骤
我们用执行栈来理解一下
- 函数每次被调用都会产生新的执行上下文(私有变量 count),实参count(全局变量count)的数据(0)被传入函数,私有变量 count变为0,并被压入执行栈,执行完毕后输出1,当前上下文接着被弹出执行栈,私有变量 count被销毁。
- 再次调用时又随着函数被调用而新建私有变量 count,重复第一步。
- (函数内的操作都在私有变量身上进行,随着私有变量的销毁上一次的操作就没了)所以第一次调用应该返回 1,第二次调用也应该返回 1,第 n 次调用都应该返回 1。
1 | var count = 0; |
- 函数的形参属于函数执行上下文,所以当指定这个形参后,它就随着函数被调用而新建,随着函数销毁而销毁。如果不指定这个形参,实参count(0)传进函数以后找不到形参就会沿着作用域链找到全局变量 count,它属于全局执行上下文,这个时候再去调用 foo() 函数就会读写这个全局变量(也就是函数里的所有操作都是在全局变量上进行的)。
- 全局变量不会随着函数的调用而新建,也不会随着函数的销毁而销毁。
- 每个 foo() 函数调用后,给 count(全局变量) 加一,然后被弹出执行栈,而全局执行上下文的生命周期将伴随着整个程序,所以第一次调用打印 1,第二次调用打印 2,第 n 次调用打印 n。
JS的同步与异步
为什么会有同步和异步
因为JavaScript是单线程,因此单一时间内只能处理单一任务,所有任务都需要排队,前一个任务执行完,才能继续执行下一个任务。
但是,如果前一个任务的执行时间很长,比如文件的读取操作或ajax操作,后一个任务就不得不等着,拿ajax来说,当用户向后台获取大量的数据时,不得不等到所有数据都获取完毕才能进行下一步操作,用户只能在那里干等着,严重影响用户体验。
因此,JavaScript在设计的时候,就已经考虑到这个问题,主线程可以完全不用等待文件的读取完毕或ajax的加载成功,可以先挂起处于等待中的加载任务,先运行排在后面的任务,等到文件的读取或ajax有了结果后,再回过头执行挂起的任务(这就是异步),因此,任务就可以分为同步任务和异步任务。
同步任务
同步任务是指在主线程上排队执行的任务,只有前一个任务执行完毕,才能继续执行下一个任务。
当我们打开网站时,网站的渲染过程,比如元素的渲染,其实就是一个同步任务。
异步任务
异步任务是指不进入主线程,而进入任务队列的任务,只有任务队列通知主线程,某个异步任务可以执行了,该任务才会进入主线程。
当我们打开网站时,像图片的加载,音乐的加载,其实就是一个异步任务
js的执行机制(事件循环)
- 同步任务进入总线程,异步任务不会进入主线程,而是会先进入任务队列,任务队列其实是一个先进先出的数据结构,也是一个事件队列。
- 比如说文件读取操作,因为这是一个异步任务,因此该任务会被添加到任务队列中,等到IO(输入输出)完成后,就会在任务队列中添加一个事件,表示异步任务准备好啦,可以进入执行栈啦~但是这时候呀,主线程不一定有空,当主线程处理完其它任务有空时,就会读取任务队列,读取里面有哪些事件,排在前面的事件会被优先进行处理,如果该任务指定了回调函数,那么主线程在处理该事件时,就会执行回调函数中的代码,也就是执行异步任务啦
- 单线程从从任务队列中读取任务是不断循环的,每次栈被清空后,都会在任务队列中读取新的任务,如果没有任务,就会等,直到有新的任务,这就叫做任务循环,因为每个任务都是由一个事件触发的,因此也叫作事件循环。
步骤总结:
- 所有同步任务都在主线程上执行,行成一个执行栈
- 主线程之外,还存在一个任务队列,只要异步任务有了结果,就会在任务队列中放置一个事件
- 一旦执行栈中的所有同步任务执行完毕,系统就会读取任务队列,看看里面还有哪些事件,那些对应的异步任务,于是结束等待状态,(异步任务)进入执行栈,开始执行
- 主线程不断的重复上面的第三步
注意:同步任务由JS引擎执行,异步任务挂起时由浏览器的引擎执行,直到有了结果才放入任务队列等待被执行。
宏任务和微任务
- 所有同步任务和异步任务又会分为宏任务和微任务,所以异步任务中也分先后。
- 可参考笔记宏任务和微任务
js的异步编程
回调函数
这是异步编程最基本的方法。
- 例:假定有两个函数f1和f2,后者等待前者的执行结果,如果f1是一个很耗时的任务,可以考虑改写f1,把f2写成f1的回调函数。【此时f2是异步的,她耗费的时间并不影响同步任务的执行】
1
2
3
4
5
6
7
8
9
10
11
12
13function f2() {
console.log("f2");
}
console.log("start");
function f1(callback) {
console.log("f1");
setTimeout(function () {
callback();
}, 1000);
}
f1(f2);
console.log("end");
// start f1 end f2
Promise
可参考笔记Promise1、Promise2、Promise3.
事件监听
任务的执行不取决于代码的顺序,而取决于某个事件是否发生。
- 例:为f1绑定一个事件,当f1发生done事件,就执行f2。
发布/订阅
与”事件监听”类似。
- 我们假定,存在一个”信号中心”,某个任务执行完成,就向信号中心”发布”(publish)一个信号,其他任务可以向信号中心”订阅”(subscribe)这个信号,从而知道什么时候自己可以开始执行。这就叫做”发布/订阅模式”(publish-subscribe pattern),又称”观察者模式”(observer pattern)。
- 这种方法的性质与”事件监听”类似,但是明显优于后者。因为我们可以通过查看”消息中心”,了解存在多少信号、每个信号有多少订阅者,从而监控程序的运行。
前端使用异步的场景
- 定时任务:setTimeout,setInverval(可参考笔记“setTimeout()与setInterval()”)
- 网络请求:ajax请求,动态
<img>
加载 - 事件绑定:事件函数绑定以后并不会等触发事件函数才继续执行程序,程序照常执行,等需要触发的时候才会执行事件绑定函数。