深入理解作用域
作用域的内部原理
内部原理 分为5个阶段(了解)
- 编译
- 执行
- 查询
- 嵌套
- 异常
阶段一:编译
注意:js中是不存在编译阶段,js中的代码都是边解释边执行
var a = 2;
在编译阶段又可继续划分为三个阶段:分词、解析、代码生成。编译过程就是编译器把程序分解成词法单元,将词法单元解析成AST,再把抽象的语法树转换成机器的指令等待执行的过程
分词阶段
将 var = 2; 拆解为词法单元 var,a,=,2,;然后放置到一个数组中
/*{
'var':'keyword',//关键字
'a':'indentifier',// 标识符
'=':'assignment',//分配
'2':'interger',//整数
';':'eos',//end of statement//结束语句
}*/
解析阶段
然后开始对这个数组进行解析,解析为抽象的语法树(AST),以等号为中心,拆解为左子树、右子树。
代码生成
将语法树转换成可执行的代码的过程 ,转换成一组机器指令
阶段二:执行
var a = 2;
console.log(a);
console.log(b);//报错
执行阶段:
1.引擎运行代码时,首先查找当前的作用域,看a是否在当前的作用域下,如果在,引擎就会直接使用这个变量,如果不在,就会继续查找。
2.如果找到了变量,就会将2赋值给变量,否则就会抛出异常
阶段三:查询
//LHS查询 RHS查询
var a =2;//变量出现在赋值语句左方时,为LHS查询
function add(a){
return a;
}
add(2);//RHS查寻
查询过程:
function foo(a){
console.log(a);
}
foo(2);
- foo()对foo函数对象进行RHS查询
- 函数传参a=2对a进行LHS查询
- console.log(a);对console对象进行RHS查询,并检查是否存在log方法
- console.log(a);对a进行RHS查询,并把得到的结果传给console.log(a)
阶段四:嵌套(非常重要)
嵌套:作用域变量查找机制
在当前作用域中无法找到某个变量时,引擎就会在外层嵌套的作用域中继续查找,直到找到该变量,或者是抵达最外层的作用域(全局作用域)为止
function foo(a){
function fo(){
console.log(a+b);
}
fo();
}
var b = 2;
foo(4);//6
阶段五:异常
//RHS
function fn(a){
a = b;//b is not defined
}
fn(2);
function fn2(){
var b = 0;
b();//b is not a function
}
fn2();
function fn(){
'use strict';
a = 1;//a is not defined
}
fn();
console.log(a);
案例体现
function fn(a){
console.log(a);
}
fn(2);
过程:
- 首先代码从上往下执行,发现函数fn,对fn进行RHS查询,在全局作用域下查找fn,成功找到fn(2),接着执行。
- 将实参传递给形参a,对a进行LHS查询,在函数作用域下查找a,成功找到a,并把2赋值给a。
- 接着对console进行RHS查询,首先在函数作用域下查找,没找到,在全局作用域下可以找到(console是一个全局内置对象)。
- 在console对象找log()方法,成功找到,执行console.log(a)。
- 然后对a进行RHS查询,在函数作用域下成功找到,开始执行,输出结果。
词法作用域
//1.全局作用域 包含词foo
function foo(a){//2.foo作用域 包含词 a b bar
var b = a*2;
function bar(c){//3.bar作用域 包含词 c
console.log(a,b,c);
}
bar(b*3);
}
foo(2);
遮蔽效应
在多层的嵌套作用域中,可以定义同名标示符,这叫遮蔽效应。
var a = 0;
function test(){
var a = 1;
console.log(a);//1
}
test();
声明提升
变量声明提升
声明从他们在代码中出现的位置被移动到最上面,这个过程叫变量的声明提升,预解释的过程。
注意:在函数内部声明的变量只会提升到函数中的最开始,不会提升到全局中去。
a = 2;
var a;
console.log(a);//2
var a;
console.log(a);
a = 0;
function fn(){
var b;
console.log(b);
b = 1;
function test(){
var c;
console.log(c);
c = 2;
}
}
fn();
函数声明提升
foo();
function foo(){
console.log('john');
}
var foo = function(){};
函数表达式不会提升
原因:
此时foo 被看作变量,进行变量提升。
var foo;//undefined
foo();//error
foo = function(){};
具名表达式与函数表达式相同
var foo = function fo(){};
var foo;
foo();
foo = function fn(){};
声明时的注意事项
变量的声明优先于函数的声明,但是函数的声明会覆盖未定义的同名变量。
var a;
function a(){}
console.log(a);//f{}
var a = 10;
function a(){}
console.log(a);//10
执行过程:
var a;
function a(){}
a = 10;
console.log(a);//10
1.变量的重复声明是无用的,但是函数的重复声明会覆盖前面的声明(无论是变量还是函数声明)。
var a = 1;
var a;
console.log(a);//1
2.函数的声明提升优先级(不表示顺序)高于变量的声明提升 。此处优先级表示谁覆盖谁
var a;
function a(){
console.log('john');
}
a();//john
3.后面的函数声明会覆盖前面的函数声明
fn();//fn2
function fn(){
console.log('fn');
}
function fn(){
console.log('fn2');
}
总结:应该避免在同一作用域中重复声明
作用域
作用域是一套规则,用来确定在何处以及如何查找标识符。
作用域链
由于作用域的嵌套而形成的一条链。使用作用域链主要是进行标识符的查询。
var a = 1;
var b = 2;
function fn(x){
var a = 10;
function bar(x){
var a = 100;
b = x+a;
return b;
}
bar(20);
console.log(b);
bar(200);
console.log(b);
}
fn(0);
bar函数的作用域链:bar=>fn=>全局
fn函数的作用域链:fn=>全局
自由变量
在当前作用域中存在但未声明的变量,一旦出现自由变量,就肯定会有作用域链,在根据作用域链的查找机制,查找到对应的变量。
机制:
在当前作用域下查找,查不到,然后沿着作用域链向上依次查找,直到找到为止,如果找不到,抛出异常。
执行上下文环境(比较难理解)
执行环境也叫执行上下文,执行上下文环境,每个执行环境都有一个与之关联的变量对象,环境中定义的函数和变量都保存在这个对象中。
执行流
执行的顺序称为执行流,可在浏览器中打断点查看执行过程。
执行环境栈
javaScript解释器在浏览器中是单线程的,这意味着浏览器在同一时间内只执行一个事件,对于其他的事件我们把它们排队在一个称为 执行栈的地方。执行环境栈其实就是一个出栈和压栈的过程 。
当浏览器第一次加载你的script,它默认的进了全局执行环境。如果在你的全局代码中你调用了一个函数,那么顺序流就会进入到你调用的函数当中,创建一个新的执行环境并且把这个环境添加到执行栈的顶部。
如果你在当前的函数中调用了其他函数,同样的事会再次发生。执行流进入内部函数,并且创建一个新的执行环境,把它添加到已经存在的执行栈的顶部。浏览器始终执行当前在栈顶部的执行环境。一旦函数完成了当前的执行环境,它就会被弹出栈的顶部, 把控制权返回给当前执行环境的下个执行环境。
正确理解执行环境与作用域
总结
在js中除了全局作用域,每个函数都会创建自己的作用域。作用域在函数定义的时候就已经确定了,与函数调用无关。通过作用域,可以查找作用域范围内的变量和函数有哪些,却不知道变量的值为多少,所以作用域是静态的。
对于函数来说,执行环境在函数调用时确定。执行环境包含作用域内所有的变量和函数的值。在同一个作用域下,不同的调用会产生不同的执行环境,从而产生不同的变量和值,所以执行环境是动态的。