javascript中的函数(进阶2)

124 阅读7分钟

深入理解作用域

作用域的内部原理

内部原理 分为5个阶段(了解)

  1. 编译
  2. 执行
  3. 查询
  4. 嵌套
  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);
  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){
    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);

过程:

  1. 首先代码从上往下执行,发现函数fn,对fn进行RHS查询,在全局作用域下查找fn,成功找到fn(2),接着执行。
  2. 将实参传递给形参a,对a进行LHS查询,在函数作用域下查找a,成功找到a,并把2赋值给a。
  3. 接着对console进行RHS查询,首先在函数作用域下查找,没找到,在全局作用域下可以找到(console是一个全局内置对象)。
  4. 在console对象找log()方法,成功找到,执行console.log(a)。
  5. 然后对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中除了全局作用域,每个函数都会创建自己的作用域。作用域在函数定义的时候就已经确定了,与函数调用无关。通过作用域,可以查找作用域范围内的变量和函数有哪些,却不知道变量的值为多少,所以作用域是静态的。

对于函数来说,执行环境在函数调用时确定。执行环境包含作用域内所有的变量和函数的值。在同一个作用域下,不同的调用会产生不同的执行环境,从而产生不同的变量和值,所以执行环境是动态的。