JavaScript作用域和执行上下文

JavaScript作用域和执行上下文

一、 作用域
作用域是用来规定变量和函数可访问范围的一套规则,js的作用域有两种,一种是全局作用域,另一种是函数作用域。我们知道在es6之前,js是没有块级作用域的,除了全局作用域,就只有函数能创建作用域。es6新出现的let和const定义的变量都具有块级作用域效果,我们接下来会谈到。下面分别介绍全局作用域和函数作用域。

1.1 全局作用域

全局变量和方法有三种定义方式,如下:

var a = 1;  //最外层声明变量
windows.b = 2;  //window对象添加属性

function fn(){
	c = 3;  //在非严格模式下未定义直接赋值的属性
}

第一种在最外层声明变量和方法,第二种给window对象添加属性和方法,第三种在非严格模式下未定义直接赋值的属性和方法(严格模式下会报错)。全局变量和方法可以在代码的任意地方被访问,但是为了多人协作开发中避免冲突或者考虑性能的优化角度,我们应该尽量避免定义全局变量和方法。

1.2 函数作用域
函数作用域中声明的变量和方法只能被下层的作用域访问,不能被其它作用域访问(包括全局作用域),看下面的例子

function fn1(){
    var a = 1;
}

function fn2(){
    console.log(a) ;  //a is not defined
}

console.log(a) ;  //a is not defined
fn2();

因为fn1和fn2两个函数不同作用域,所以fn2无法访问fn1的变量,全局作用域同样无法访问fn1的变量。函数作用域里的变量和方法可以在函数内部作用域被访问,如下

function fn(){
    var a = 1;
    function bar(){
        console.log(a); //1
    }
    bar();
}

fn();

函数bar在fn中,包含在fn作用域中,所以bar可以访问fn的变量。

1.3 let和const
es6之前js是没有块级作用域的,看一下下面的例子

for (var i = 0; i < 10; i++) {
  console.log(i); // 0, 1, 2, 3, 4 ... 9
}

console.log(i); //10

i在for循环结束后i并没有被回收,而是继续留在内存中,但是我们把var换成let再试试

for (let i = 0; i < 10; i++) {
  console.log(i); // 0, 1, 2, 3, 4 ... 9
}

console.log(i); // i is not defined

这时候i不可以在被访问了,但是我们要注意let不能在同一块下声明相同的变量,下面的代码就会报错

let a = 1;
let a = 2; // 报错:Uncaught SyntaxError: Identifier 'a' has already been declared

而var重复定义不会报错,若之前定义过的变量后面在声明就会被忽略(注意是声明而不是赋值)

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

const和let相似都是定义块级作用域,但const定义的是常量,意味着它无法被修改,并且也不能重复定义

const c = 1;
c = 2;  // 报错:Uncaught TypeError: Assignment to constant variable.

1.4 作用域链
作用域链是由当前执行环境与上层执行环境的一系列变量对象组成,它保证了当前执行环境对符合访问权限的变量和函数的有序访问。我们来看下面的例子

var a = 1;
function fn(){
    var b = a + 1;
    function bar(){
        var c = b + 1;
        console.log(c);
    }
    bar();
}
fn();

bar函数在访问b时,发现自己的作用域里并没有这个变量,所以回到上一级作用域也就是fn中去找,在fn中找到了b变量,但是因为变量b由a赋值,fn发现自己作用域没有a变量,又会到全局作用域去找(事实上bar访问b时,b已经赋值完成,这样说只是方便理解)。这个过程中使用的作用域我们可以看成一条链,长下面的样子:
bar() --> fn() --> global

当bar作用域找不到自己想要的东西时就会到上一级作用域找,上一级作用域找不到再到上上一级作用域找,一直到全局作用域,所有的作用域链的末端都是全局作用域,如果全局作用域也找不到,程序会报undefined错误。这下面还有一个例子,如果你能理解为什么会是这样,作用域这块应该差不多掌握了

var a = 1;
function fn1(){
    console.log(a); // 1
}

function fn2(){
    var a = 2;
    fn1();
}

fn2();

fn1的上一级作用域是全局作用域,不是fn2作用域,因为fn1和fn2两个作用域并不是包含关系,所以fn1无法访问fn2作用域的变量

二、执行上下文

代码运行到不同阶段会有不同的执行上下文,执行上下文可以理解为当前代码的运行环境。上下文环境主要分为全局环境和函数环境,每个函数执行的上下文环境都不同,上下文是在执行阶段才创建的,而上面所说的作用域是在编译阶段就已经确定的。

2.1 上下文的生命周期
当调用一个函数时,一个新的上下文就会被创建,一个上下文的生命周期主要分为两个阶段:创建阶段和执行阶段。创建阶段主要是创建变量对象,确认作用域链,以及确认this的指向;执行阶段主要是完成变量的赋值、函数的引用及执行其它代码。我们看下面的例子

function fn(){
    console.log(a); // undefined
    console.log(bar); // ƒ bar(){ console.log(a) }
    console.log(test);  // undefined
    
    var a = 1;
    function bar(){
        console.log(a)
    }
    var test = function(){
        console.log(a)
    }
}

fn();

上面其实就是变量提升的情况,在上下文创建的阶段,我们用var声明的变量或函数会自动提升到作用域的顶部并赋值为undefined,所以a和test会输出undefined。但函数声明不一样,在创建阶段会直接赋值给引用,所以上面打印出了函数的内容,也就是说无论我们在哪定义函数,只要在相同作用域下,我们可以在任意一个地方调用函数。上面的代码的执行顺序可以等价于

function fn(){
    function bar(){
        console.log(a)
    }
    var a = undefined;
    var test = undefined;
    
    console.log(a); // undefined
    console.log(bar); // ƒ bar(){ console.log(a) }
    console.log(test);  // undefined
    a = 1;
    test = function(){
        console.log(a)
    }
}

fn();

另外注意,函数声明的优先级高于变量声明的优先级,而且同名的函数会覆盖函数和变量,但是同名的变量不会覆盖函数

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

console.log(fn); // 1

上面代码的实际执行顺序为

function fn(){ console.log(1) }
function fn(){ console.log(2) }

console.log(fn) // function fn(){ console.log(2) }

fn = 1;
console.log(fn); // 1

2.2 函数调用栈
上下文主要分为全局上下文和函数上下文,js调用栈的方式保存不同上下文环境,这个栈就是函数调用栈。函数调用栈规定了js代码的执行顺序。栈底永远是全局上下文,栈顶则是当前执行的上下文。我们看之前的一个例子

var a = 1;
function fn(){
    var b = a + 1;
    function bar(){
        var c = b + 1;
        console.log(c);
    }
    bar();
}
fn();

下面是函数调用栈变化的过程
函数调用栈变化过程
最开始全局上下文环境被创建并入栈;接着调用fn函数,fn的上下文环境被创建并入栈;fn函数调用了bar函数,bar函数的上下文环境被创建并入栈;bar函数执行完,bar的上下文环境出栈;fn函数执行完,fn函数的上下文环境出栈;最后只剩全局上下文环境。

本文主要简单讲解了JavaScript作用域和执行上下文,若有不对的地方,欢迎指出。
参考书籍:《JavaScript核心技术开发解密》

  • 1
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值