JavaScript设计了良好的规则来存储变量,并且可以方便的查找到这些变量。这一套规制被称为作用域。因此可以给作用域下个定义:
作用域是一套规制,管理引擎如何在当前作用域以及嵌套的子作用域中能够根据标识符名称查找变量(标识符就是变量或函数名)
作用域又被分为静态作用域和动态作用域。静态作用域指的是词法作用域,函数的作用域在函数定义的时候就决定了。
词法作用域
词法作用域是由你在写代码时将变量和块作用域写在哪来决定的,让我们认真看下面这个例子
function foo(a) {
var b = a * 2
function bar(c) {
console.log(a, b, c) // 2,4,12
}
bar(b * 3)
}
foo(2)
JavaScript采用静态作用域,让我们分析下执行过程:
- 1、包含着整个全局作用域,其中有一个标识符: foo
- 2、包含着foo所创建的作用域,其中有三个标识符:a、bar和b
- 3、包含着bar所创建的作用域,其中有一个标识符:c
当上面console.log(...)这个代码执行时,会查找a、b、c三个变量的引用,首先会从内部作用域开始查咋,如果无法找到a,就会去上一级到嵌套的foo(...)的作用域查找,因此会打印2,4,12
1、作用域查找始终从运行的内部作用域开始,逐级向外,直到遇见第一个标识符停止。整个过程就是作用域链。2、无论函数在哪里被调用,也无论怎么调用,词法作用域都只由函数被声明时所处的位置决定。
函数作用域:属于这个函数的全部变量都可以在整个函数的范围内使用(嵌套的作用域中也可以使用),简单理解为作用域是分层的,内层作用域可以访问外层作用域的变量,反之则不行。
函数在创建的时候作用域就定好了。在做这类题的时候首先要弄懂要到创建这个函数的那个作用域中取值,而不是调用这个函数的地方——这就是静态作用域。
var a = 10
function fn() {
var b = 20
function bar() {
console.log(a + b) //30
}
return bar
}
var x = fn(),
b = 200
x() // bar()
fn()返回的是 bar 函数,赋值给 x。执行 x(),即执行 bar 函数代码。取 b 的值时,直接在 fn 作用域取出。取 a 的值时,试图在 fn 作用域取,但是取不到,只能转向创建 fn 的那个作用域中去查找,结果找到了,所以最后的结果是 30
执行上下文
JavaScript执行一段代码其实分了两个阶段,一个是编译阶段另一个是执行阶段。
- 1、编译阶段:由编译器完成,将代码翻译可执行的代码,这个阶段作用域会被确定
- 2、执行阶段:由js引擎完成,主要执行可执行的代码,这个阶段执行上下文被创建(对象被创建)
编译阶段
- 词法分析
- 语法分析(AST抽象语法树)
- 代码生成作用域确定 执行阶段
- 创建执行上下文
- 执行函数代码
- 垃圾回收
作用域在函数定义时就确定了,而不是在函数调用
执行上下文是函数执行之前创建的,执行上下问最明显的就是this的指向问题,后面有章节会详细讲到
执行上下文在运行时确定,随时可能改变;作用域在定义时就确定,并且不会改变。
函数和变量提升
上面我们都很熟悉作用域的概念里,但是作用域同其中的变量声明出现的位置非常的微妙。我也也经常会听到var会带来变量提升,ES6中的let、const块级作用域可以解决变量提升的问题。
我们先看一个例子
a = 2
var a
console.log(a) // 输出 2
函数在执行时其实会分为两个阶段,上小节已经讲了,当Var a = 2; JavaScript会将其看成两个声明:var a; 和 a = 2。第一个声明时在编译阶段进行的,第二个赋值声明会被留在原地等待执行阶段。因此会输出2
console.log(a) // 输出 undefined
var a = 2
// 等同于 实际处理流程
var a;
console.log(a)
a = 2
变量的赋值其实也可以分为三步,创建变量、变量初始化(undefined)、真正复制。上面例子可以看出var在创建和初始化时都被提升了,但是赋值没被提升,因此输出的是undefoned。
需要注意的几点
- 1、函数和变量都会被提升
- 2、函数会提升,但函数表达式不会, var foo = function(){ // 函数表达式 }
- 3、后面的提升会覆盖前面的,函数在变量前提升
let、const和var
在讲这三者时,我们先看一个典型的面试问题
for (var i = 0; i < 10; i++) {
setTimeout(function () {
console.log(i); // 输出10个10
}, 0);
}
for (let i = 0; i < 10; i++) {
setTimeout(function() {
console.log(i); // 输出0 1 2 3 4 5 6 7 8 9。
}, 0);
}
i虽然在全局作用域声明(for循环体外部),但是在for循环体局部作用域中使用的时候,变量会被固定、不会提升,不会背外界所影响,因此会依次输出0 1 2 3 4 5 6 7 8 9。而var会造成变量的提升,执行console.log此代码时,同步代码for循环已经执行完成,此时变量都是10,因此打印10个10。这块还涉及到JavaScript的事件循环机制,后面我们会根据Promise一起讲解。
const其实就是定义一个不能修改的变量,实际上保证的不是变量的值不得改动,而是指向的那个内存地址不得改动。对于复合类型,变量指向的内存地址保存的只是一个指针,const 只能保证这个指针是固定的。