不知道你曾经是否对javascript的执行顺序困惑过,希望这篇文章对你有所帮助。
废话不多说,我们先来带段例子:
console.log(fn)
fn = 123
function fn(){
console.log('hello')
}
var fn
此时,控制台会打印fn这个函数
对于不熟悉js的小伙伴们,一定会认为这是什么鬼,难道js不是逐行执行的吗?
难道是我们强调大IDE帮我们做了优化,当我尝试把代码放入控制台再次输出的时候,惊人的发现打印的还是一样,这就证明js的确就是这么不可理喻的。
js的运行三步曲
- 预编译
- 词法、语法解析
- 解释执行
- 首先我们来验证一下解释执行,即解释一行执行一行;
function f() {
a
}
var b = 123
console.log(b) // 123
上述代码的f函数内写了未定义的变量a,但是f函数未执行,控制台也不会报错;假如f函数执行了呢?
function f() {
a
}
var b = 123
console.log(b) // 123
f()
// Uncaught ReferenceError: a is not defined
只有当f函数执行时,js引擎才会抛出ReferenceError
- 这样是不是能够证明js代码是解释一行执行一行
- 根据上面的结论,我们再来看看语法、解析是在执行前发生的吗?
var b = 123
console.log(b)
function f(){
=
}
// Uncaught SyntaxError: Unexpected token =
此时,控制台直接报错,抛出语法错误,不再执行任何代码。
- 由此可见语法是发生在执行之前的
预编译(es3.0)
了解的js的执行的一个基本流程后,现在我们来重点讨论一下预编译这个环节。
预编译发生在代码执行的前一刻,可能是几毫秒甚至更短
console.log(fn)
fn = 123
function fn(){
console.log('hello')
}
var fn
首页我们来看看之前的那个例子;其实关于js的执行,一直流传着这样的一个规则,即:变量、函数声明提升,当变量名和函数名冲突时,忽略变量声明。是的,确实如此,这也是来自预编译原理的一个总结,也可以解决大部分的问题,但是不能解决所有的问题。
imply global 暗示全局变量:
- 任何变量未经声明就赋值,该变量归全局所有
- 全局作用域下声明的变量和方法,归全局对象
<script>
var a = 123
console.log(a) // 123
console.log(window.a) // 123
console.log(a === window.a) // true
function f () {
b = 333
console.log(b) // 333
}
console.log(f === window.f) // true
</script>
可见直接定义变量是会造成全局污染的
b = 666
console.log(b === window.b)
console.log(b)
b = 777
console.log(b === window.b)
AO(activation object)
- 函数执行前会创建AO对象
- 找到形参和变量的声明,将它们作为AO对象的属性名,值为undefined
- 将形参和实参值进行统一
- 在函数体内找函数声明,值赋予函数体
function fn(x, y) {
console.log(x) // function ...
function x () {
console.log(123)
}
var y = 2
console.log(x) // function ...
console.log(y) // 3
}
fn(2, 3)
----------------------------------------------
step1 :AO {
x:undefined
y: undefined
}
step2: AO {
x: 2,
y:3
}
step3: AO {
x: function () {...}
y: 3
}
注意: 在if中定义的函数体会忽略,但是变量声明却不会
function fn(x, y, b) {
console.log(x) // 2
console.log(b) // 666
if(x) {
function x () {
console.log(123)
}
var b = 100
console.log(x) // function() {...}
}
var y = 2
console.log(x) // 2
console.log(y) // 3
}
fn(2, 3, 666)
GO (Global Object)
- script执行前会创建GO对象
- 找到变量的声明,将它们作为GO对象的属性名,值为undefined
- 在函数体内找函数声明,值赋予函数体
console.log(a) // ƒ a() {}
var a
function a () {}
注:在浏览器中script是一块一块加载执行的
<script>
console.log(a, 'script tag 1') // Uncaught ReferenceError: a is not defined
</script>
<script>
console.log(a, 'script tag 2') // function a(){}
var a
function a () {}
</script>
<script>
console.log(a, 'script tag 3') // function a(){}
</script>
作用域链
[[scope]]:每个javascript函数都是一个对象,对象中有的属性可以访问,有的不可以,这些特殊属性仅供js引擎存取,[[scope]]就是其中一个;[[scope]]指的就是我们所说的作用域,里面存储了运行期上下文的集合。
作用域链:[[scope]]中所存储的执行期上下文对象的集合,这个集合呈链式链接,我们把这种链接叫做作用域链。
运行期上下文:当一个函数将要被执行时,会生产一个叫做执行期上下文的内部对象。一个执行期上下文定义了函数执行时的环境;每当函数执行时都会创建与之对应的一个独一无二的上下文,所以多次调用函数就会创建多个执行上下文,当函数执行完后该上下文将被销毁
查找变量:从作用域链的顶端依次向下查找。
上个栗子,先来了解基本流程:
function a () {
var innerA = 345
function b() {
var innerB = 123
innerA = 667
}
console.log(innerA) // 667
console.log(glob) // undefined
var glob = 667
}
var glob = 889
a()
a 函数:
b函数:
我们就拿b函数简单来说,当函数b被定义时,它的作用域链里存储a的AO和GO;当函数执行的时候函数会生成b的AO; 当b函数执行完后,b的AO销毁,会回到b被定义时的状态等待再次被调用;当函数再次调用时,会再次创建b的AO,完毕后销毁,如此反复。
可以再找些栗子来印证上所述的正确性?
f3()
function f1() {
innerF1 = 'f1'
f2()
function f2() {
console.log(innerF1)
}
}
function f3() {
var innerF1 = 667
f1()
}
/**
f3 defined ---> scop0: GO
f1 defined ---> scop0: GO
f3 running ---> scop1: AO{innerF1:667}
scop1: GO
f1 running ---> scop0 : AO{f2: function}
scop1: GO
f2 defined ---> scop0: AO{f2: function}
scop1: GO
f2 running ---> scop0: AO{}
scop1: AO{f2: function}
scop2: GO
**/
为什么作用域链的作用域是共享的?
其实从上面的例子就能看出来b函数作用域链里的第1个AO就是a函数的AO;因为b函数在执行的时候,改变了innerA,且b函数执行完毕销毁后,在a函数中打印的innerA=667,可以证明b函数作用域链上第1位就是a函数执行时创建的AO。
那为什么他们要共享呢?
首先,从功能上来讲,假如b的函数不能获取外部的变量,那么全局函数和局部函数将会如何通讯,通讯的成本是否大大提高;再从性能方面来说,假如b在声明的时候,是复制了一份a的作用域链,开相当于在内存中有开辟了一个块较大的空间,而且空间内的东西都是一模一样的,如果只是引用的话,开销要小很多。
内部函数能够访问外部的变量,但为什么最外层的作用域不能访问它内部的作用语里的变量?
我猜,假如js引擎要去搜索每个函数内部的变量的话,如果一个函数里面的函数体少的话还要,假如是100个,然后这100个函数里面又有100个,那么向下查找将会是on^2的复杂度,再强大的引擎也会崩溃。反过来讲,从里往外的话,有且只有一条查找的路径。
闭包
如果理解了作用域链的话,那么闭包就很好理解了。
当一个内部函数被保留到外部,导致它的链上的对象无法被销毁,叫做闭包。
闭包其实就是作用域链上的活动对象在函数执行完毕时,依然被引用,导致内存泄漏的现象。
let arr = []
for (var i = 0; i < 10; i++) {
arr.push(function(){
console.log(i)
})
}
arr.forEach(item => item())
// 都是10...
arr里面function创建的时候scope0:go{i:10}
当arr内的function执行的时候scope0: ao{};scope1:go{i:10}
let arr = []
for (var i = 0;i < 10;i++) {
(function (i) {
var args = arguments
arr.push(function () {
console.log(i, args)
})
}(i, 1, 2, 3, 5, 6))
}
arr.forEach(item => item())
// 0 ~ 9
arr里面的function创建时有个立即执行函数,创建的ao被arr内部的function所引用
所以arr内部的函数在调用时,他的作用域链scop0是自己的ao,scop1是立即执行函数ao{i:xx},scope2:go{i:10}
当所用的变量找不到时,它会沿着作用域链的方向查找到最顶端。
总结
作用域,作用域链其实就是函数生命周期的产物
- 函数创建时
- 在作用域链上挂载声明位置时的执行上下文环境(AO|GO)
- 函数执行时
- 在作用域链上创建一个新的AO
- 将声明的变量和参数作为AO的属性,并设为undefined
- 将形参和实参进行统一
- 将函数声明作为AO的属性,
- 函数销毁
- 销毁当前AO
闭包可用于私有变量存储,模块化。