梦秋雨 2019-11-04
作用域就是变量与函数的可访问范围,即作用域控制着变量与函数的可见性和生命周期。在 JavaScript 中,变量的作用域有全局作用域和局部作用域两种。JavaScript 采用词法作用域(lexical scoping),也就是静态作用域。
函数的作用域在函数定义的时候就决定了。
js函数有一个内部属性 [[scope]],当函数创建的时候,就会保存所有父变量对象到其中,下文会详细描述
函数的作用域是在函数调用的时候才决定的。
静态作用域的语言下面的代码会打出1,因为在foo定义的时候,他的作用域就确定了在全局(后面讲变量对象的时候也会说foo是注册在全局的而不是在bar里面才注册)
执行 foo 函数,先从 foo 函数内部查找是否有局部变量 value,如果没有,就根据书写的位置,查找上面一层的代码,也就是 value 等于 1,所以结果会打印 1。
var value = 1; function foo() { console.log(value); } function bar() { var value = 2; foo(); } bar();
JavaScript代码的整个执行过程,分为两个阶段,代码编译阶段与代码执行阶段。
就是当前 JavaScript 代码被解析和执行时所在环境的抽象概念, JavaScript 中运行任何的代码都是在执行上下文中运行。
为了表示不同的运行环境,JavaScript中有一个执行上下文(Execution context,EC)的概念。也就是说,当JavaScript代码执行的时候,会进入不同的执行上下文,这些执行上下文就构成了一个执行上下文栈(Execution context stack,ECS)
当一段JavaScript代码执行的时候,JavaScript解释器会创建Execution Context,其实这里会有两个阶段:
创建阶段(当函数被调用,但是开始执行函数内部代码之前)
激活/代码执行阶段
JavaScript 引擎创建了执行上下文栈(Execution context stack,ECS)来管理执行上下文
当 JavaScript 引擎首次读取你的脚本时,它会创建一个全局执行上下文并将其推入当前的执行栈。每当发生一个函数调用,引擎都会为该函数创建一个新的执行上下文并将其推到当前执行栈的顶端。
引擎会运行执行上下文在执行栈顶端的函数,当此函数运行完成后,其对应的执行上下文将会从执行栈中弹出,上下文控制权将移到当前执行栈的下一个执行上下文。
let a = 'Hello World!'; function first() { console.log('Inside first function'); second(); console.log('Again inside first function'); } function second() { console.log('Inside second function'); } first(); console.log('Inside Global Execution Context');
一旦所有代码执行完毕,Javascript 引擎把全局执行上下文从执行栈中移除。
// 伪代码 ECStack = [ globalContext ]; // first() ECStack.push(<first> functionContext); // fun1中竟然调用了fun2,还要创建fun2的执行上下文 ECStack.push(<second> functionContext); // second()执行完毕 ECStack.pop(second); // first()执行完毕 ECStack.pop(first); // 当整个应用程序结束的时候,ECStack 才会被清空,所以程序结束之前, ECStack 最底部永远有个 globalContext:
console.log(this);// this 引用,在客户端 JavaScript 中,全局对象就是 Window 对象。 console.log(this instanceof Object);//全局对象是由 Object 构造函数实例化的一个对象。 console.log(Math.random());//.预定义了一堆,嗯,一大堆函数和属性。 console.log(this.Math.random()); var a = 1;//作为全局变量的宿主。 console.log(this.a);
在函数上下文中,我们用活动对象(activation object, AO)来表示变量对象。
变量对象VO和活动对象AO是同一个对象在不同阶段的表现形式。当进入执行环境的创捷阶段时,变量对象被创建,这时变量对象的属性无法被访问。进入执行阶段后,变量对象被激活变成活动对象,此时活动对象的属性可以被访问。
当进入执行上下文时,这时候还没有执行代码,在这个阶段中,执行上下文会分别创建变量对象,建立作用域链,以及确定this的指向。
变量对象会包括:
function foo(a) { var b = 2; var c=3; function c() {} var d = function() {}; b = 3; } foo(1);
创建阶段JavaScript解释器主要做了下面的事情:
检查上下文中的函数声明
检查上下文的变量声明
VO = { arguments: { 0: 1, length: 1 }, a: 1,//注意a已经初始化了 b: undefined, c: reference to function c(){},//如果重名后跳过了变量var c=3,只有函数c d: undefined }
上下文创建完成之后,就会开始执行代码,这个时候,会完成变量赋值,函数引用,以及执行其他代码。
注意:在创建阶段函数的变量的值就是函数引用,在这个阶段就被同名的变量又重新赋值了
AO = { arguments: { 0: 1, length: 1 }, a: 1, b: 3, c: 3,//执行阶段c又会重新被赋值 d: reference to FunctionExpression "d" }
这就是常说的什么函数声明提升优先于变量声明提升
提升只是说法,其本质就是执行执行上下文的创建和执行产生的影响
function test(arg){ console.log(arg); // function arg(){console.log('hello world') } var arg = 'hello'; function arg(){ console.log('hello world') } console.log(arg); // hello } test('hi');
第一个console.log有值因为函数在上下文创建的时候就已经给了函数引用作为值,
而变量这是先给的undefined作为初值,
在代码执行阶段变量又会重新赋值,同名变量hello覆盖了函数
全局上下文的变量对象初始化是全局对象
函数上下文创建阶段函数先注册重名覆盖,变量后注册重名跳过
函数上下文的变量对象初始化只包括 Arguments 对象
在进入执行上下文时会给变量对象添加形参、函数声明、变量声明等初始的属性值,也就是初始化变量对象
在代码执行阶段,会再次修改变量对象的属性值(这时函数就不会重新赋值了)
作用域链,是由当前环境与上层环境的一系列变量对象组成,它保证了当前执行环境对符合访问权限的变量和函数的有序访问。
上文的作用域中讲到过函数的作用域在函数定义的时候就决定了,因为函数有一个内部属性 [[scope]],当函数创建的时候,就会保存所有父变量对象到其中,当查找变量的时候,会先从当前上下文的变量对象中查找,如果没有找到,就会从自己的scope中保存的父级(词法层面上的父级)执行上下文的变量对象中查找,一直找到全局上下文的变量对象,也就是全局对象。这样由多个执行上下文的变量对象构成的链表就叫做作用域链。
在JavaScript中,我们可以将作用域定义为一套规则,这套规则用来管理引擎如何在当前作用域以及嵌套的子作用域中根据标识符名称进行变量查找。
var a = 20; function test() { var b = 10; //function innerTest() { // var c = 10; // return b + c; //} return b; } test();
执行过程
1.test 函数在全局上下文中被创建,保存全局上下文的变量对象组成的作用域链到内部属性[[scope]]
test.[[scope]] = [ globalContext.VO ];
2.创建 test 函数执行上下文,test函数执行上下文被压入执行上下文栈
ECStack = [ testContext, globalContext ];
3.test 函数并不立刻执行,开始做准备工作,第一步:复制[[scope]]属性到函数上下文,创建了作用域链
testContext = { Scope: testscope.[[scope]], }
4.第二步:用 arguments 创建活动对象,随后初始化活动对象,加入形参、函数声明、变量声明
testscopeContext = { AO: { arguments: { length: 0 }, b: undefined }, Scope: testscope.[[scope]], }
5.第三步:将活动对象压入 testscope 作用域链顶端
testscopeContext = { AO: { arguments: { length: 0 }, b: undefined }, Scope: [AO, [[Scope]]]//用Scope简写testscope.[[scope]] }
6.准备工作做完,开始执行函数,随着函数的执行,修改 AO 的属性值
testscopeContext = { AO: { arguments: { length: 0 }, b: 10 }, Scope: [AO, [[Scope]]] }
7.查找到 b 的值,返回后函数执行完毕,函数上下文从执行上下文栈中弹出
ECStack = [ globalContext ];
8.如果test内部含有innerTest函数,则在该innerTest函数创建时将test上下文中的作用域链传入(testscopeContext.Scope)
然后后循环执行和test相同的步骤
var a = 20; function test() { var b = 10; function innerTest() { var c = 10; return b + c; } return b; } test();
全局,函数test,函数innerTest的执行上下文先后创建。我们设定他们的变量对象分别为VO(global),VO(test), VO(innerTest)。而innerTest的作用域链,则同时包含了这三个变量对象,所以innerTest的执行上下文可如下表示。
innerTestContext = { AO: {...}, // 变量对象 Scope: [VO(innerTest), VO(test), VO(global)], // 作用域链 }
因为变量对象在执行上下文进入执行阶段时,就变成了活动对象,因此图中使用了AO来表示。Active Object
作用域链是由一系列变量对象组成,我们可以在这个单向通道中,查询变量对象中的标识符,这样就可以访问到上一层作用域中的变量了。