likes
comments
collection
share

JS - 作用域、作用域链

作者站长头像
站长
· 阅读数 11

任何JavaScript代码片段在执行前都要进行编译,从编译到执行过程涉及到几个角色:

  • 引擎:从头到尾负责整个JavaScript程序的编译及执行过程
  • 编译器:负责词法分析、语法分析、代码生成等
  • 作用域:负责收集并维护由所有声明的标识符(变量)组成的一系列查询,并实施一套非常严格的规则,确定当前执行的代码对这些标识符的访问权限。

1. 作用域

上面的表述有点绕,简单讲,作用域是用于在运行时代码中的某些特定部分中变量,函数和对象的可访问性。

1.1 全局作用域

全局作用域为最外层的作用域。

var name = "global";
console.log(name); // global
console.log(window.name); // global

直接在代码最外层定义一个变量"name",该变量会存储在全局作用域上。在浏览器环境下,全局对象默认为window,在window上访问该标识符效果一致。

1.2 函数作用域

函数作用域的含义是指,属于这个函数的全部变量都可以在整个函数的范围内使用及复用。

function foo() {
  var age = 25;
  console.log(age);
}
foo(); // 25
console.log(age);//ReferenceError:age is not defined

在foo函数中定义了变量age,该变量属于foo的函数作用域中,因此在函数执行时可访问,而在全局作用域中并不存在age变量,因此会报引用错误。

1.3 块作用域

块级作用域由最近的一对包含花括号{} 界定。换句话说, if 块、while 块、function块,单独的块级作用域。

需要使用letconst

function fn(){
  var a = 1;
  if(a > 0){
    var c = 3;
    let b = 2;
    console.log(b);
  }
  console.log(c);
  console.log(b);
}
fn();
// 2
// 3
// ReferenceError: b is not defined

在函数fn中,增加if块,b使用let进行声明,故属于if块构成的块作用域中,而c使用var进行声明,无法劫持块作用域,故属于fn的函数作用域中。

1.4 作用域嵌套

在当前作用域中无法找到某个变量时,引擎就会在外层嵌套的作用域中继续查找,直到找到该变量,或抵达最外层的作用域(也就是全局作用域)为止。

详细在作用域链中讲讲。

2. 预编译

编译阶段发生在代码执行阶段之前,主要有三步:

  • 词法分析(词法单元)
  • 语法分析(抽象语法树AST)
  • 代码生成

编译阶段中的一部分工作就是找到所有的声明,并用合适的作用域将它们关联起来。

2.1 在代码执行之前

2.1.1 变量声明提升

在编译时,将变量的声明提升到当前作用域顶端

console.log(a); // undefined
console.log(b); // ReferenceError: b is not defined
var a = 2;

等价于

var a;
console.log(a); // undefined
console.log(b); // ReferenceError: b is not defined
a = 2;

可以看到,正是因为声明提升了,默认为undefined,所以才没有报引用错误。

2.1.2 函数整体提升

变量只会提升声明,而函数会整体进行提升,包括函数体。

函数提升优先级 比 变量提升 更高。

foo(); // 1

var foo;

function foo() {
  console.log(1);
}

可以看到foo被识别为函数名,且正常执行。

2.2 在函数执行之前

理解这个只需要掌握四部曲

(1)创建一个AO(Activation Object)

(2)找形参和变量声明,然后将形参和变量声明作为AO的属性名,属性值为undefined

(3)将实参和形参统一

(4)在函数体内找函数声明,将函数名作为AO对象的属性名,属性值为函数体

function fun(a){
    var a=1
    var a=2
    function b(){}
    var b=a
    a=function c(){}
    console.log(a);
    c=b
    console.log(c);
}
fun(2)

AO的处理过程:

// 第一步,创建AO
AO = {};
// 第二步,找形参和变量声明,然后将形参和变量声明作为AO的属性名,属性值为undefined
AO = {
    a: undefined,
    b: undefined
};
// 第三步,将实参和形参统一
AO = {
    a: 2, // 统一
    b: undefined
};
// 第四步,在函数体内找函数声明,将函数名作为AO对象的属性名,属性值为函数体
AO = {
    a: 2,
    b: function(){}
}

进入执行阶段:

// 执行var a = 1
AO = {
    a: 1,
    b: function(){}
}
// 执行 var a = 2
AO = {
    a: 2,
    b: function(){}
}
// 执行 b = a
AO = {
    a: 1,
    b: 1
}
// 执行 a=function c(){}
AO = {
    a: function c(){}
    b: 1
}
// 执行 c=b
// AO上不存在c,引擎会在作用域的上一层即全局作用域上寻找,没找到则会在全局作用域上创建,值为undefined,后被赋值为b

2.3 发生在全局

发生在全局的预编译也有自己的三部曲:

(1)创建GO(Global Object)对象

(2)找全局变量声明,将变量声明作为GO的属性名,属性值为undefined

(3)在全局找函数声明,将函数名作为GO对象的属性名,属性值赋予函数体

global=100
function fn(){
  console.log(global);//打印结果:undefined
  global=200
  console.log(global);//打印结果:200
  var global=300
}
fn()
var global

// GO:{
//     global:undefined 100
//     fn:function fn(){}
// }
// AO:{
//     global:undefined,    200
// }

其中GO的创建过程:

// 创建GO
GO = {}
// 找全局变量声明,将变量声明作为GO的属性名,属性值为undefined
GO = {
    global: undefined
}
// 在全局找函数声明,将函数名作为GO对象的属性名,属性值赋予函数体
GO = {
    global: undefined
    fn: function(){...}
}

3. 作用域链

  • 执行期上下文:当函数执行的时候,会创建一个称为执行期上下文的对象(AO对象),一个执行期上下文定义了一个函数执行时的环境。
    • 函数每次执行时,对应的执行上下文都是独一无二的,所以多次调用一个函数会导致创建多个执行期上下文,
    • 当函数执行完毕,它所产生的执行期上下文会被销毁。
  • 查找变量:从作用域链的顶端依次往下查找。
  • [[scope]]:作用域属性,也称为隐式属性,仅支持引擎自己访问。函数作用域,是不可访问的,其中存储了运行期上下文的集合。
  • 作用域链[[scope]]中所存储的执行期上下文对象的集合,这个集合呈链式连接,我们把这种链式连接叫做作用域链
function a(){
    var a=1
    console.log(a);
}
var glob=100
a()
//a 定义   a.[[scope]]--> 0:GO:{}
//a 执行   a.[[scope]]--> 0:AO:{} 1:GO:{}

当我们定义一个函数时,这个函数就拥有了scope属性

  • 当函数a被定义时就有了scope属性,它记录的是a的执行上下文,而函数a中有内容,也就有了全局执行上下文的对象了,我们把它称为GO(Global Object)。
  • 当函数a被执行时,就带来了函数a自己的执行上下文的创建,我们把它称为AO(Activation Object)。类似的原理,AO来到了GO的位置,GO向后挪了一位。
  • 访问变量时,就顺着函数a的作用域链开始查找,先从AO中寻找,找不到再一层一层向外寻找