2019-08-25|2019-08-25 JavaScript作用域链

【2019-08-25|2019-08-25 JavaScript作用域链】In top-level JavaScript code (i.e., code not contained within any function definitions),
the scope chain consists of a single object, the global object. In a non-nested function,
the scope chain consists of two objects. The first is the object that defines the function’s
parameters and local variables, and the second is the global object. In a nested function,
the scope chain has three or more objects. It is important to understand how this chain
of objects is created. When a function is defined, it stores the scope chain then in effect.
When that function is invoked, it creates a new object to store its local variables, and
adds that new object to the stored scope chain to create a new, longer, chain that represents the scope for that function invocation. This becomes more interesting for nested functions because each time the outer function is called, the inner function is defined again. Since the scope chain differs on each invocation of the outer function, the inner function will be subtly different each time it is defined—the code of the inner function will be identical on each invocation of the outer function, but the scope chain associated with that code will be different.

嗯,这段话可以这样理解,当我们在全局环境下,声明定义一个函数时,函数的当时作用域链被储存下来(每个函数被定义时创建自己的基础作用域链[我个人这样称呼的]),比如下图中的 f1(),它是定义在全局的函数,则它被定义时保存了一个作用域链,为:window,它包括了一个对象。当它被调用时,创建一个对象,我们假设称之为f1的局部变量对象,它存储了函数f1()的局部变量,(比如f2()就是f1()的局部变量中的一个)然后将这个新创建的对象添加到作用域链上,这样就形成了f1()被调用(执行)时的作用域,它包括两个对象: window - f1的局部变量对象

2019-08-25|2019-08-25 JavaScript作用域链
文章图片

现在我们来看f2(),它是f1的所有局部变量中的一个,那它什么时候被定义呢?我们感觉它应该是代码写完就被定义在f1内部了,但从上面那段话我们知道,函数内部的变量在函数被调用时才会被定义,所以f2是在f1被调用时,作为f1的局部变量被定义的,作为f1的局部变量对象中的一个属性存在。同时,f2因为是一个函数,所以创建了f2的基础作用域链,这个作用域链其实就是f1此时被调用时的作用域链。当我们在f1中调用f2,那么和上面同样的过程发生了:创建f2的局部变量对象,将之添加到f2的基础作用域链上,作为f2执行时的作用域。我们现在结合引用段落对f2的调用进行理解,因为每次f1调用时,才会创建f1的局部变量对象,而f2作为f1的局部变量中的一个,在此时才会被定义,而f2作为一个函数,在定义时,创建了f2的基础作用域链(每个函数被定义时创建自己的基础作用域链),此基础作用域链就是f1的作用域链,其中包含f1此时被调用而创建的f1的局部变量对象,这个局部变量对象包含了f2作为它的一个属性。由于每次f1被调用时,都新创建其局部变量对象,其完整的作用域链此时才会形成,所以每次f1被调用时,其作用域链都是不同的,其实扩展开来,所有的函数的作用域链都是调用时才形成完整的作用域链。但其中值得注意的一点是,非嵌套函数,或者说是定义在全局的函数,其基础作用域链在浏览器对文档进行解析时就已经形成了,因为定义在全局的函数,可以看成全局对象的一个属性,视为一个全局变量。然而,嵌套函数则有所区别,嵌套函数的基础作用域链是每次包含其的外部函数被调用时才形成,并不是代码写完就已经确定了。咦,怎么感觉有点不对呢?JavaScript不是词法作用域嘛?不是看源码就能确定作用域了嘛?其实JavaScript中的函数因为完整的作用域链都是调用时才形成,所以虽然作用域是可以确定的,但完整的作用域链是每次调用才形成的。
看完了理论,我们开始实践,先从基础开始探究:
function test1() { var v1 = 111; test2(); } function test2() { var v2 = 222; console.log(v1); } test1(); //Uncaught ReferenceError: v1 is not defined

从上面的示例中,我们可以看到,test1调用test2,但test2无法访问test1中的变量,证明了JavaScript是词法作用域的语言,并不是根据调用关系来决定变量的作用域的。我们来分析一下:
test1因为是全局函数,所以test1的基础作用域链在代码解析时就已经确定,是 window
test2和test1一样,基础作用域链是 window
当执行test1时,动态生成作用域链:window-test1的局部变量对象
当在test1中调用test2时,动态生成作用域链:window - test2的局部变量对象
所以变量 v1 在test1的局部变量对象中,test2执行时自然无法访问。
然后我们来个更好玩的:
function f1() { function f2() { var f2var1 = "f2var1"; function f3() { var f3var1 = "f3var1"; console.log(f3var1); console.log(f2var1); //console.log(f4var1); //console.log(f5var1); } f4(f3); } function f4(f3) { var f4var1 = "f4var1"; //console.log(f2var1); function f5() { var f5var1 = "f5var1"; f3(); } f3(); f5(); } f2(); } function f8() { } f1();

首先,很显然的结论:在f8中无法调用f1内部的函数,这个不用多说。
我们来理理调用关系: f1 > f2 > f4 >f3、f5> f3
f1执行时,作用域链为:window - f1局部变量对象(包含f2、f4)
f2执行时,作用域链为:window - f1局部变量对象 - f2局部变量对象(包含f3)
f4执行时,作用域链为:window - f1局部变量对象 - f4局部变量对象(包含f5、f3(这个f3是从f2传递过来的,作为函数类型,是地址传递,单独作为一个变量来说,它的作用域链是f4的作用域链,但当对其取值时,得到的是f2中的f3,作为函数来调用,基础作用域链为f2的作用域链))
f4内定义的f3执行时,作用域链为:window - f1局部变量对象 - f2局部变量对象 - f3局部变量对象
所以,执行到console.log(f4var1); 会报错,因为f4var1是包含在f4局部变量对象中的。
自然,console.log(f5var1); 同样。
如果跳过这两句,
f5执行时,作用域链为:window - f1局部变量对象 - f4局部变量对象 - f5局部变量对象
f5中调用f3时,是在f4局部变量对象中找到的变量,作为函数执行时,如同之前阐述的,
作用域链为:window - f1局部变量对象 - f2局部变量对象 - f3局部变量对象
总结下来,虽然完整的变量作用域链是函数执行时才生成的,但是不影响我们判断变量的作用域。

    推荐阅读