# JavaScript 作用域

参考资料:视频1 (opens new window) 视频2 (opens new window)

作用域,是一套规则,用来确定在何处以及如何查找标识符

在js中,作用域分为全局作用域和函数作用域,且可以相互嵌套

# 作用域内部原理的过程

内部原理分成 编译,执行,查询,嵌套,异常

但在 js 脚本中,并没有编译阶段,是边解释边执行

# 编译阶段(了解)

var a = 2;

1.1 分词

将上述代码分成以下词法单元: var , a , = , 2 , ;

{
	"var": "keyword",	//关键字
	"a": "indentifier",	//标识符
	"=": 'assignment',	//分配
	"2": "interger",	//整数
	";": "eos"	// 结束语句
}

1.2 解析

将上一个步骤分词出来的对象或数组进行解析,生成一个抽象语法树(AST Abstract Snatax Tree)

1.3 代码生成

将AST转换成可执行的代码的过程,转换成一组机器指令

简言之,就是编译器把程序分解成词法单元,将词法单元解析成AST,再把AST转换成机器指令等待执行的过程

再次强调,在JavaScript中,没有编译阶段。

# 执行阶段(了解)

var a = 2

引擎执行代码时,首先查找当前的作用域,检测变量 a 是否在当前的作用域下。

如果是,引擎就会直接使用这个变量

如果否,引擎会继续查找该变量

​ 如果找到了变量,就会将值 2 赋给当前的变量,否则会抛出异常

console.log(a);
// 引擎执行该代码时,先查找当前的作用域,检测 a 是否存在于作用域下
// 答案是 是,所以输出结果是 2
console.log(b);
// 查找当前作用域,没有查到 变量 b ,所以抛出异常

# 查询阶段(了解)

LHS查询——赋值操作等

RHS查询——函数调用等

function foo(a) {
	console.log(a);
}
foo(2);
// 1.foo()对foo函数对象进行RHS引用
// 2.函数传参a = 2, 对a 进行LHS引用
// 3. console.log(a) ; console对象进行RHS引用,并检查其是否有log()方法
// 4. console.log(a);对 a 进行RHS引用,并把值传给console.log(a)

# 嵌套阶段(重要)

function foo(a){
	console.log(a + b);
}
var b = 2;
foo(4);
// 输出结果为何能够为 6 ,function foo(a)... 中的 b 到底是怎么得到的?
// 在当前作用域下无法查找到某个变量时,引擎就会在外层嵌套作用域中继续查找,直到找到该变量或抵达最外层作用域(全局作用域)
// 且请看 变量声明提前

// 关于嵌套作用域巩固理解
function foo (a) {
    console.log(a + b);
    function foooo () {
        console.log(a + a + b);
    }
    foooo();
}
var b = 2;
foo(4);

# 异常阶段(了解)

function fn (a) {
	a = b;
    // 查询不到 b --> b is not defined
}
fn(2);

function fn2(){
    var b = 0;
    // b 是一个变量而非函数,是通过LHS查询引用的
    b();
    // 此处调用方法b,通过RHS查询,无法查询到该结果
    // 所以报错 -> b is not function
}
fn2();

function fn3(){
    // 'use strict';
    a = 1;
}
console.log(a);
// a is not defined
// 但是如果执行了fn3之后,却可以输出 1
// a 直接变成了全局变量
// 因此,我们需要在fn3() 中,添加一句 'use strict';
fn3();
console.log(a);

# 总结案例

function fn(a) {
	console.log(a);
}
fn(2);
// 对fn()进行 RHS查询,查询到了上文 function fn()的定义——true
// 对a 进行 LHS 查询,并且在fn()作用域下查询到a,将2赋值给a
// 对console进行RHS查询
// console是内置对象,属于全局对象,在fn()作用域中查询不到,便在其嵌套作用域中查询——即在全局作用域中查询
// 在consol对象中查询log方法
// 对 a 进行 RHS查询
// 将 a 的值 传给 console.log

# 词法作用域

词法——分词

var a = 2;

function foo(a){
    var b = a * 2	// 第二执行:b = 2 * 2 = 4
    function bar(c){
        console.log(a,b,c);	// 最后执行: 2,4,12
    }
    bar(b * 3);	// 第三执行:bar(12)
}
foo(2);	//最先执行
// 这样子就生成了多个作用域
// 首先,最外层有一个全局作用域
// foo()函数中有一个foo函数作用域
// bar()函数中有一个bar函数作用域
// 这样子就行形成了三层作用域
// 而无论 foo() 函数在何处调用、执行
// 其作用域都是由其声明时的位置决定的

我们用嵌套作用域的角度分析一下这段代码

// 作用域我们这里以对象的形式表达
// 第一层:全局作用域
全局作用域:{
	// 各种各样的全局函数、变量
	// 第二层: foo
	foo:{
		a: 2,
		b: 4,
		// 第三层: bar
		bar:{
			c: 12
		}
	}
}

# 遮蔽效应

作用域的查找,从运行时所处的最内部作用于开始,逐级向上进行,知道遇到第一个匹配的标识符为止

在多层的嵌套作用域可以定义同名的标识符,这叫做遮蔽效应

var a = 0;
function test () {
	var a = 1;
	console.log(a);
	function test2 () {
		var a = 233;	// 直到遇到第一个匹配的标识符
		console.log(a);	// 所以输出的是233,这种现象叫做遮蔽效应
	}
    test2();
}
test();	// 1 233

# 声明提升

可以查看 声明提升 文档

声明提升

# 变量声明提升

console.log(a);
var a = 2;
// 明明代码从上往下执行
// 但是为什么能够得到a,而不是报错
// 且得到的a是undefined,而不是2
// 预解释——变量提升

console.log(b);
var b = 0;
function fn(){
    console.log(c);
    var c = 1;
    function test(){
        console.log(d);
        var d = 2;
    }
    test();
}
fn();

// 上述代码的内部实现如下:
var a;
var b;
function fn(){
    var c;
    function test(){
        var d;
        console.log(d);
        d = 2;
    }
    console.log(c);
    c = 1;
    test();
}
console.log(a);
a = 2;
console.log(b);
b = 0;
fn();

# 函数声明提升

foo();
function foo(){
	console.log('233');
}
// 函数声明会提升
// 函数表达式不会提升
// 函数声明会在 变量声明之后

bar();	// bar is not a function
var bar = function () {
    console.log('233');
}
// 生命表达式,提升的是 var bar;

test();	// not a function
test233();	// not a function
var test = function test233 (){
    console.log('test233');
}

# 作用域链

各个作用域的嵌套关系,组成了一条作用域链。

var a = 2;

function foo(a){
    var b = a * 2	// 第二执行:b = 2 * 2 = 4
    function bar(c){
        console.log(a,b,c);	// 最后执行: 2,4,12
    }
    bar(b * 3);	// 第三执行:bar(12)
}
foo(2);

// 依旧以这个例子为例
// 例子中 bar的作用域链 bar -> foo -> 全局
// foo 作用域链 foo -> 全局

使用作用域链主要是进行标识符的查询,标识符解析就是沿着作用域链一级一级地搜索标识符地过程,而作用域链就是保证对变量和函数地有序访问。

# 自由变量

在当前作用域中存在,但未在当前作用域中声明的变量

var a = 1;
var b = 2;
// fn -> 全局
function fn(x){
	var a = 10;
    // bar -> fn -> 全局
	function bar(x) {
		var a = 100;
        // 这里的b就是自由变量
        // 存在,当并未在当前作用域中声明
		b = x + a;
		return b;
	}
	bar(20);
	bar(200);
}
fn(0);

一旦存在自由变量,就肯定会有作用域链,再根据做根据作用域链查找机制,查找到对应的变量

查找机制:在当前作用域中发现没有该变量,然后沿着作用域链往上级查找,直到查到对应的变量为止,如果查找不到,直接报错

# 执行环境与执行流

执行环境也叫执行上下文,执行上文环境

每个执行环境,都有一个与之关联的变量对象,在环境中定义的函数和变量都保存在这个对象

var a = 1;
var b = 2;
// fn -> 全局
function fn(x){
    // arguments
    // this
	var a = 10;
    // bar -> fn -> 全局
	function bar(x) {
		var a = 100;
		b = x + a;
		return b;
	}
	bar(20);
	bar(200);
}
fn(0);
// 当 fn(0) 执行时,执行的环境中保存着
/*
	x:0,
	a:undefined,
	bar:function,
	arguments: [0],
	this: window
*/

// 当bar(20)执行时,执行环境中保存着:
/*
	x:20,
	a:undefined,
	arguments: [0],
	this: window
*/

执行流——即代码的执行顺序

# 执行环境栈

其实就是一个出栈、压栈的过程

var a = 1;
var b = 2;
// fn -> 全局
function fn(x){
    // arguments
    // this
	var a = 10;
    // bar -> fn -> 全局
	function bar(x) {
		var a = 100;
		b = x + a;
		return b;
	}
	bar(20);
	bar(200);
}
fn(0);

当fn(0)执行之前,全局执行环境会压入执行环境栈

当fn(0)执行之时,fn执行环境会压入执行环境栈,且处于活跃状态

而当fn(0)执行完成时,fn执行环境就会进行出栈,全局执行环境为活跃状态

以bar(20)执行时为例:

// 栈顶——活跃状态
bar(20)执行环境:{
    x: 20,
    a: undefined,
    全局中b: 2this: window
}
//
fn(0)执行环境:{
    x: 0,
    a: 10,
    bar: function,
    this: window
}
// 栈底
全局执行环境:{
    a: 1,
    b: 2,
    fn: function,
    this: window
}

# 关键词回忆:

嵌套

作用域

遮蔽效应

声明提升

作用域链

自由变量

执行环境、执行环境栈

更新时间: 6/27/2021, 11:35:50 PM