【JS基础记录】20191118_作用域和作用域链

作用域和作用域链

什么是作用域

作用域指的是源代码中定义变量的区域,即一种规则,规定了如何查找变量,确定当前执行代码对变量的访问权限。

词法作用域

JS采用的是词法作用域,又称为动态作用域。

  • 静态作用域: 在函数定义的时候就决定了;
  • 动态作用域: 在函数调用的时候决定。
var value = 1;

function foo() {
    console.log(value);
}

function bar() {
    var value = 2;
    foo();
}

bar(); // 1

假设JavaScript采用静态作用域,让我们分析下执行过程:

执行 foo 函数,先从 foo 函数内部查找是否有局部变量 value,如果没有,就根据书写的位置,查找上面一层的代码,也就是 value 等于 1,所以结果会打印 1。

假设JavaScript采用动态作用域,让我们分析下执行过程:

执行 foo 函数,依然是从 foo 函数内部查找是否有局部变量 value。如果没有,就从调用函数的作用域,也就是 bar 函数内部查找 value 变量,所以结果会打印 2。

前面我们已经说了,JavaScript采用的是静态作用域,所以这个例子的结果是 1。

可执行代码

JS的可执行代码有三种:

  • 全局代码
  • 函数代码
  • eval代码

执行上下文

当JS执行一段可执行的代码时,就会创建对应的执行上下文,它有三个重要的属性:

  • 变量对象(Variable object)
  • 作用域链
  • this

执行上下文栈

为了高校有序地管理多个上下文,JS引擎会创建上下文栈来管理执行上下文。

JS开始解释执行代码的时候,最先遇到的就是全局代码,所以初始化的时候,会先向执行上下文栈压入一个全局的执行上下文:

ECStack = [];
ECStack = [
    globalContext
];

假设现在JS遇到了这段代码:

function fun3() {
    console.log('fun3')
}

function fun2() {
    fun3();
}

function fun1() {
    fun2();
}

fun1();

则上下文栈会先压入fun1,由于fun1调用了fun2,所以将fun2压入栈,由于fun2调用了fun3,所以压入fun3

// 伪代码

// fun1()
ECStack.push(<fun1> functionContext);

// fun1中竟然调用了fun2,还要创建fun2的执行上下文
ECStack.push(<fun2> functionContext);

// 擦,fun2还调用了fun3!
ECStack.push(<fun3> functionContext);

// fun3执行完毕
ECStack.pop();

// fun2执行完毕
ECStack.pop();

// fun1执行完毕
ECStack.pop();

变量对象

变量对象是与执行上下文相关的数据作用域,存储了在上下文中定义的变量和函数声明。

JS在我们直观的眼中,觉得是一行一行执行的,其实它是一段一段执行的,这就跟变量提升有关系了。

变量提升

变量和函数在内的所有声明都会在任何代码被执行前首先被处理。

var a = 2

上面这行代码会被JS理解为var aa=2var a就是一个声明。

console.log(a); // undefined
var a = 2;  

它会被理解成这个意思

var a;
console.log(a);
a = 2;

但是注意,如果你不使用var的话,可能会发生这样的情况

console.log(a);  // ReferenceError: a is not defined
a = 2; 

另外要特别注意:函数声明会被提升到普通变量之前

foo();  // 1
var foo;
function foo(){
	console.log(1);
}
foo = function(){
	console.log(2);
}

由于函数声明会被提升到普通变量之前,则实际情况为:

function foo(){
	console.log(1);
}
foo();
foo = function(){
	console.log(2);
}

为什么普通变量会不执行呢?

三种变量对象

  • 函数的所有形参(如果是函数上下文)
  • 函数声明:如果变量对象已经存在相同名称的属性,则覆盖这个属性(这个变量)
  • 变量声明:如果变量名称跟已经声明的形参或函数相同,则这个变量不会干扰到他们。

所以上面就可以解释,为什么普通变量的声明’不执行’。

作用域链

JS在查找变量时,会先从当前上下文的变量对象(Variable object, VO)中查找,如果没找到,就会从父级执行上下文的变量对象中查找,一直查找到全局上下文的变量对象,也就是全局对象。这样由多个执行上下文的变量对象构成的链表就叫做作用域链。

作用域闭包

当函数可以记住并访问所在的词法作用域时,就产生了闭包,即使函数时在当前词法作用域外执行的。

function foo(){
	var a = 2;
	function bar(){
		console.log(a);
	}
	return bar;
}
let baz = foo();
baz(); // 2

bar 在自己定义的词法作用域以外的地方执行,但是它仍然可以访问自己的词法域定义的a变量,这就是闭包。

闭包就是能够读取其他函数内部变量的函数

举一个常见的运用闭包解决问题的栗子:

for(var i=0;i<=5;i++){
	setTimeout(function timer(){
		console.log(i)
	})
}
// 由于延迟函数的回调会在循环结束时才执行,所以当i等于6时就会推出循环,然后执行timer()函数,此时timer()读取的i为6,所以打印的结果是5个6.
// for()中声明i,其实是一个全局i

for(var i = 0;i<=5;i++){
	(function(j){
		setTimeout(function timer(){
            console.log(j)
        })
	})(i)
}

p.s.使用let声明变量

关于this等后期看完书在补充。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值