作用域

作用域

什么是作用域?

作用域是代码在运行时,某些特定部分中的变量,函数,和对象的可访问性。作用域负责收集并维护由所有声明的标识符(变量)组成的一系列查询,并实施一套非常严格的规则,确定当前执行的代码对这些标识符的访问权限。换句话说,作用域决定了变量和函数的可访问范围,即作用域控制着变量和函数的可见性和生命周期

JavaScript中的作用域

  • 全局作用域
  • 局部作用域

如果一个变量在函数外或者大括号{}外声明的,那么就定义了一个全局作用域;在ES6之前局部作用域只包含了函数作用域,ES6为我们提供了块级作用域,也属于局部作用域。

全局作用域

拥有全局作用域的对象可以在代码的任何地方访问到。

以下情形拥有全局作用域:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
var a = 5;           //①最外层变量

function func(){ //②最外层函数
//...
}

function func1(){
a = 3; //③未经定义直接赋值的变量(由于变量提升使之成为全局变量)
var b = 4;
}

function func2(){
window.a = 5; //④通过window来添加一个全局变量
}

局部作用域

局部作用域一般只能在固定代码片段中可以访问到。最常见的为函数作用域

函数作用域

定义在函数中的变量就在函数作用域中。并且函数在每次调用时都有一个不同的作用域。这意味着同名变量可以用在不同的函数中。因为这些变量绑定在不同的函数中,拥有不同作用域,彼此之间不能访问。

函数作用域:

1
2
3
4
5
function func(){
var a = 5; //局部变量,【注意】:不能省略var,否则会因为变量提升成为全局变量
console.log(a);//函数内可以访问
}
console.log(a);//函数外不可访问

块级作用域测试:

1
2
3
4
for(var i = 0; i < 3; i++){
var num+=i;
}
console.log(i);//3 ---此处for语句块内部与外面是同一个作用域

关于变量提升

在Javascript中,函数及变量的声明都将被提升到函数的最顶部,也就是说我们可以先使用后声明,但函数表达式和变量表达式只是将函数或者变量的声明提升到函数顶部,函数表达式和变量的初始化将不被提升

变量提升的例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
var tmp = new Date();
function f() {
console.log(tmp);//undefined
if(false) {
var tmp='hello';
}
}
//【注意】这里申明提升了,定义的内容并不会提升
//等价于
var tmp = new Date();
function f() {
var tmp;
console.log(tmp);
if(false) {
tmp='hello';
}
}

存在重复声明的例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
var a = 3;
console.log(a); //3
if(true){
var a = 4; //此处声明会被忽略!!!仅用于赋值!
console.log(a);//4
}
console.log(a); //4
//【注意】在同一作用域用var声明变量多次,后面的var声明会被忽略
//等价于
var a = 3;
console.log(a);//3
if(true){
a = 4;
console.log(a);//4
}
console.log(a);//4

变量和函数同时提升:

1
2
3
4
5
6
7
8
9
//情况一
console.log(a); //[Function: a]
var a = 1;
function a(){};//函数声明形式

//情况二
console.log(a);//undefined
var a = 1;
var a = function(){};//函数表达式形式

情况二就相当于重复声明的例子,容易理解。对于情况一,其等价形式:

1
2
3
var a = function(){};
console.log(a);
a = 1;
  • 函数声明被提升到最顶上;
  • 申明只进行一次,因此后面var a = 1的申明会被忽略。
  • 函数申明的优先级优于变量申明,且函数声明会连带定义一起被提升(这里与变量不同)

块级作用域

ES6新增了letconst命令,可以用来创建块级作用域变量,使用let命令声明的变量只在let命令所在代码块内有效。

使用let声明变量,会将变量的作用域限制在当前代码块中。特点:

  • 变量不会提升到代码块顶部且不允许从外部访问块级作用域内部变量
  • 不允许反复声明
1
2
3
4
5
6
7
8
9
10
11
console.log(a);  //error
let a = 2;
for(let i = 0; i < 3; i++){
console.log(a);
}
console.log(i); //error

function func(){
var a = 1;
let a = 2;//error
}

作用域链

关于编译原理

编译原理

传统编译语言流程:

  1. 分词/词法解析:这个过程会由字符组成的字符串分解成有意义的代码块,这些代码块被称为词法单元(token)。例如:var a = 2;通常被分解为:var、a、=、2、; 。
  2. 解析/语法分析:这个过程将词法单元流(数组)转换成一个由元素逐级嵌套所组成的代表了程序语法结构的树。这个数被抽象为“抽象语法树”(AST)。
  3. 代码生成。将AST转换为可执行的过程被称为代码生成。

JavaScript编译过程不同之处:

  • JavaScript 大部分情况下编译发生在代码执行前的几微秒(甚至更短!)的时间内
  • JavaScript 引擎用尽了各种办法(比如 JIT,可以延 迟编译甚至实施重编译)来保证性能最佳

JavaScript是如何执行的

JavaScript编译过程

  • 核心重点:变量和函数在内的所有声明都会在任何代码被执行前首先被处理。
  • 函数运行的瞬间,创建一个AO (Active Object 活动对象)运行载体。

作用域链是什么?

JavaScript上每一个函数执行时,会先在自己创建的AO上找对应属性值。若找不到则往父函数的AO上找,再找不到则再上一层的AO,直到找到window(全局作用域)。而这一条形成的“AO链” 就是JavaScript中的作用域链。

参考资料

  1. 《你不知道的JavaScript》上卷
  2. 深入理解JavaScript
  3. javascript作用域