js 执行环境 & 作用域 & 作用域链 $ 变量的提升

目录

一、执行环境

1、什么是执行环境?

2、执行环境分为两种

(1)、全局执行环境

(2)、函数执行环境

二、作用域

1、 全局作用域

2、局部作用域

(1)、函数作用域

(2)、ES6的块级作用域

3、作用域的问题

(1)、内层变量可能会覆盖外层变量

(2)、用来计数的循环变量泄露为全局变量

三、作用域链

四、声明提升

1、变量的声明提升

(1)、ES5 中

(2)、ES6 中

2、函数的声明提升

(1)、ES5 中

(2)、ES6 中


一、执行环境

1、什么是执行环境?

  • js 中的执行环境,简称环境
  • 执行环境定义了变量和函数有权访问的其他数据,决定了他们各自的行为。
  • 每个执行环境都有一个变量对象,环境中定义的所有变量和函数都保存在这个对象中。js无法访问该对象,但解析器在处理数据时,会在后台访问它。

2、执行环境分为两种

全局执行环境 和 函数执行环境

(1)、全局执行环境

  • 在运行web浏览器的时候,会创建全局的执行环境,在这个环境里有个window 对象,它包括全局的变量和函数;
  • 在Node.js中全局环境里有个 global 对象;
  • 只有在全局执行环境被销毁时(比如关闭网页或浏览器)才会被一起销毁。

拓展global 对象 与 window 对象

  • 在网页中执行js代码,我们的全局对象就是window,而最顶级的global对象,在浏览器中无法直接访问,我们只能通过window这个代理来访问到全局对象的相关属性。
  • 在node中执行js代码,我们是可以直接访问到全局对象global。

(2)、函数执行环境

  • 每个函数都有自己的执行环境,当执行流进入一个函数时,函数的环境就被推入一个环境栈中;
  • 当函数执行完毕后,栈将其环境弹出,把控制权返回给之前的执行环境。

 

二、作用域

  • 作用域就是变量与函数的可访问范围,作用域控制着其内部的变量与函数的生命周期。
  • 作用域按执行环境可分为两种:全局作用域(全局环境)、局部作用域(函数环境)。
  • js在ES5中没有块级作用域,但在ES6中引入了块级作用域,块级作用域属于局部作用域的一种。
// 全局作用域 与 局部作用域
var name = 'marry';
function fn(msg) {
    var gender = 'man';
    console.log('---函数环境', name);            // marry
    console.log('---函数环境', msg);             // hello
    console.log('---函数环境', gender);          // man
    
}
fn('hello');
console.log('---全局环境', name);                // marry
console.log('---全局环境', msg);                 // msg is not defined
console.log('---全局环境', gender);              // gender is not defined

1、 全局作用域

在代码中的任何地方都能被访问。

2、局部作用域

一般只在固定的代码片段内可以访问得到。

局部作用域包括:函数作用域 和 块级作用域

(1)、函数作用域

默认变量只能在该函数内部被访问。

(2)、ES6的块级作用域

js 在保留ES5的基础之上,新增了ES6的块级作用域。

在 ES6 中,使用 let 或 const 声明的变量或常量都会形成块级作用域。

ES6 块级作用域的特点:

  • 用let声明的变量和用const声明的常量,只在当前块级作用域内有效。
  • 用let声明的变量和用const声明的常量,都不会发生提升。
  • ES6 的块级作用域是一个“暂时性死区”——在 ES6 块级作用域内,使用 let 和 const 命令声明变量之前,该变量都是不可用的。
  • 不允许用 let 或 const 重复声明的变量或常量。
  • 必须有大括号,否则 JavaScript 引擎就认为不存在块级作用域。
  • 允许块级作用域任意嵌套,内层作用域可以定义外层作用域的同名变量,但互不影响。
  • 在浏览器的 ES6 环境中,块级作用域内声明的函数遵循以下规则:
    • 允许在块级作用域内声明函数。块级作用域中,函数声明语句的行为类似于 let(即声明的函数所在的大括号会形成一个 “新的” 块级作用域)(不过尽量避免,如果确实需要,优先使用 let 创建函数表达式);
    • 声明的函数会发生提升,提升到所在的块级作用域、函数作用域 或 全局作用域的头部(但是会报错,使用 let 创建函数表达式不会报错,不过创建的函数没有声明提升了)。

下面通过代码感受一下块级作用域带来的变化:

①、用let声明的变量和用const声明的常量,只在当前块级作用域内有效。

{
    let a = 10;
    var b = 1;
}
a;// ReferenceError: a is not defined.
b;// 1

②、用let声明的变量和用const声明的常量,都不会发生提升。

var 命令会发生 “变量提升” 现象,即变量可以在声明之前使用,值为 undefined。而 let 声明的变量一定要在声明后使用,否则会报错。

// var 的情况
console.log(foo); // 输出undefined
var foo = 2;

// let 的情况
console.log(bar); // 报错ReferenceError
let bar = 2;

③、ES6 的块级作用域是一个“暂时性死区”——在 ES6 块级作用域内,使用 let 和 const 命令声明变量之前,该变量都是不可用的。

var tmp = 123;

if (true) {
  tmp = 'abc'; // ReferenceError
  let tmp;
}

④、不允许用 let 或 const 重复声明的变量或常量。

let a = 0;
var a = 1; // 报错

function func(arg) {
    let arg;
}
func() // 报错

⑤、必须有大括号,否则 JavaScript 引擎就认为不存在块级作用域。

// 第一种写法,报错
if (true) let x = 1;

// 第二种写法,不报错
if (true) {
  let x = 1;
}

⑥、允许块级作用域任意嵌套,内层作用域可以定义外层作用域的同名变量,但互不影响。

function f1() {
  let n = 5;
  if (true) {
    let n = 10;
  }
  console.log(n);// 5
}
f1();

⑦、在浏览器的 ES6 环境中,块级作用域内声明的函数遵循以下规则:

  • 允许在块级作用域内声明函数。块级作用域中,函数声明语句的行为类似于 let(即声明的函数所在的大括号会形成一个 “新的” 块级作用域)(不过尽量避免,如果确实需要,优先使用 let 创建函数表达式);
  • 声明的函数会发生提升,提升到所在的块级作用域、函数作用域 或 全局作用域的头部(但是会报错,使用 let 创建函数表达式不会报错,不过创建的函数没有声明提升了)。

下面来看一看为什么使用函数表达式?以及函数如何提升的?

在探索下面的例子时,要考注意全局作用域、函数作用域 及 块级作用域之间的影响。

function f() { console.log('I am outside!'); }

(function () {
  if (false) {
    // 重复声明一次函数f
    function f() { console.log('I am inside!'); }
  }

  f();
}());

函数提升后,实际运行的代码如下:

// ES5 环境
function f() { console.log('I am outside!'); }

(function () {
    function f() { console.log('I am inside!'); }
        if (false) {
      }
    f();
}());
// I am inside!

ES6 规定,块级作用域之中(局部作用域更为贴切),函数声明语句的行为类似于 let(即声明的函数所在的大括号会形成一个 “新的” 块级作用域,在这个块级作用域之外不可被引用)。在这个新的块级作用域之外不可引用。所以,理论上会得到“I am outside!”。但是,如果你真的在 ES6 浏览器中运行一下上面的代码,是会报错的,这是为什么呢?

因为:实际上,浏览器真正执行的代码,并没有完全遵循 ES6 的规则。各浏览器对这一规则存在异议。

// 浏览器的 ES6 环境
function f() { console.log('I am outside!'); }

(function () {
    if (false) {// 新的块级作用域
        function f() { console.log('I am inside!'); }
    }

    f();
}());
// Uncaught TypeError: f is not a function

函数提升后,实际运行的代码如下:

// 浏览器的 ES6 环境
function f() { console.log('I am outside!'); }

(function () {

    var f = undefined;

    if (false) {// 新的块级作用域
        function f() { console.log('I am inside!'); }
    }

    f();
}());
// TypeError: f is not a function

原来,如果改变了块级作用域内声明的函数的处理规则,会对之前的代码产生很大影响,为了减轻因此产生的不兼容问题,ES6 在附录里规定:浏览器的实现可以不遵守ES6的规定,有自己的行为方式。

上面的例子,是函数作用域内采用了函数声明的方式创建了一个函数,倘若在块级作用域内呢?

function f() { console.log('I am outside!'); }
{
    if(false){
        function f(){console.log("I am inside!")}
    }

    f();
}
// I am outside!

貌似没问题,但是:

{
    if(false){
        function f(){console.log("I am inside!")}
    }

    f();
}
// TypeError: f is not a function

综上所述,实际开发中,在 ES6 环境下,要尽量避免在局部作用域内声明函数,如果确实需要此操作,请优先使用 let 声明函数表达式

// 浏览器的 ES6 环境
function f() { console.log('I am outside!'); }

(function () {
    if (false) {
        // 重复声明一次函数f
        let f = function f() { console.log('I am inside!'); }
    }

    f();
}());
// I am outside!

使用 let 后,同样会在 if 的大括号内会形成一个块级作用域,不过不会不会发生声明提升。最重要的是,所有浏览器都一致支持 let 形成的块级作用域,不会像支持在局部作用域内声明函数所形成的(新的)块级作用域那样存在异议。所以,代码就能按正常理论执行。

3、作用域的问题

(1)、内层变量可能会覆盖外层变量

var tmp = new Date();

function f() {
  console.log(tmp);
  if (false) {
    var tmp = 'hello world';
  }
}

f(); // undefined

上面代码的原意是,if 代码块的外部使用外层的 tmp 变量,内部使用内层的 tmp 变量。但是,函数 f 执行后,输出结果为 undefined,原因在于发生了变量声明提升,实际执行的代码如下:

var tmp = new Date();

function f() {
    var tmp;
    console.log(tmp);
    if (false) {
        tmp = 'hello world';
    }
}

f(); // undefined

上述代码中,变量的声明提升,导致内层的 tmp 变量覆盖了外层的 tmp 变量。这种问题怎么解决呢?

块级作用域就可以:

var tmp = new Date();

function f() {
    console.log(tmp);
    if (false) {
        let tmp = 'hello world';
    }
}

f(); // Thu Jun 25 2020 14:38:12 GMT+0800 (中国标准时间)

(2)、用来计数的循环变量泄露为全局变量

var s = 'hello';

for (var i = 0; i < s.length; i++) {
    // ...
}

console.log(i); // 5

上面代码中,变量 i 只用来控制循环,但是循环结束后,它并没有消失,泄露成了全局变量,造成污染。这种问题怎么解决呢?

块级作用域就可以:

var s = 'hello';

for (let i = 0; i < s.length; i++) {
    // ...
}

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

 

三、作用域链

  • 当代码在一个环境执行时,就会创建一个作用域链。
  • 作用域链是用来保证对执行环境有权访问的所有变量和函数的有序访问
  • 作用域链的最前面(下面)始终是当前执行的代码所在环境的变量对象,下一个变量对象来自包含(外部)环境,再下一个来自下一个包含环境,依次往后(往上),直到全局执行环境的变量对象。全局执行环境的变量对象(在web浏览器中指的是 window 对象)始终是作用域链中的最后一个对象。
  • 如果执行环境是函数,则将其活动对象作为变量对象,活动对象一定包含 arguments 对象(这个对象在全局环境中是不存在的)
  • 标识符解析是沿着作用域链一级一级地搜索标识符的过程。搜索过程始终从作用域链的前端开始,逐级向后回溯,直到找到标识符为止,若找不到,就会报错。
var color = "blue";
function changeColor(){
    var anotherColor = "red";
    function swapColor(){
        var tempColor = anotherColor;
        anotherColor = color;
        color = tempColor;
        console.log('-----1', color);                        // red
        console.log('-----1', anotherColor);                 // blue
        console.log('-----1', tempColor);                    // red
    }
    swapColor();
    console.log('-----2', color);                            // red
    console.log('-----2', anotherColor);                     // blue
    console.log('-----2', tempColor);                        // tempColor is not defined
}
changeColor();
console.log('-----3', color);                                // red
console.log('-----3', anotherColor);                         // anotherColor is not defined
console.log('-----3', tempColor);                            // tempColor is not defined

以上代码共涉及3个执行环境:全局环境、changeColor()局部环境、swapColor()局部环境。

代码执行顺序:

  1. 先创建一个变量color,并为其赋值为blue,
  2. 然后执行 “changeColor()” 语句,调用changeColor函数,
  3. 然后创建一个变量anotherColor,并为其赋值为red,
  4. 然后执行 “swapColor()” 语句,调用swapColor函数,
  5. 然后创建一个变量tempColor,并拷贝anotherColor的值,
  6. 然后anotherColor拷贝color的值,
  7. 然后color拷贝tempColor的值,
  8. 然后log打印swapColor函数环境里的值,
  9. 然后log打印changeColor函数环境里的值,
  10. 最后log打印全局环境里的值。

执行第8步时,log依次分别打印:color、anotherColor、tempColor,但是swapColor函数环境里只定义了变量tempColor,于是“浏览器”会沿着作用域链当前执行环境(swapColor函数环境)的包含环境(changeColor函数环境),在changeColor函数环境里找到了变量anotherColor的定义,继续找下一个包含环境(全局环境),在全局环境window对象上找到了变量color的定义,完成任务结束寻找。

执行第9步时,log依次分别打印:color、anotherColor、tempColor,但是changeColor函数环境里只定义了变量anotherColor,于是“浏览器”会沿着作用域链当前执行环境(changeColor函数环境)的包含环境(全局环境),在全局环境里找到了变量color的定义,然后找tempColor,找不到。因为作用域链都是从下往上找,找到最上头就是全局环境里的window对象,既然没有,就只能报错 “tempColor is not defined”了。

执行第10步时,log依次分别打印:color、anotherColor、tempColor,但是全局环境里只定义了变量color,然后找 anotherColor 和 tempColor,找不到。因为作用域链都是从下往上找,找到最上头就是全局环境里的window对象,既然没有,就只能报错  “anotherColor is not defined” 、“tempColor is not defined”了。

 

四、声明提升

1、变量的声明提升

(1)、ES5 中

ES5中使用var声明的变量都会发生提升。

// var 的情况
console.log(foo); // 输出undefined
var foo = 2;

(2)、ES6 中

在ES6中使用let声明的变量,以及使用const声明的常量,均不会发生提升。 

// let 的情况
console.log(bar); // 报错ReferenceError
let bar = 2;

2、函数的声明提升

(1)、ES5 中

function f() { console.log('I am outside!'); }

(function () {
  if (false) {
    // 重复声明一次函数f
    function f() { console.log('I am inside!'); }
  }

  f();
}());

发生提升后,实际执行的代码为:

// ES5 环境
function f() { console.log('I am outside!'); }

(function () {
    function f() { console.log('I am inside!'); }
    if (false) {}

    f();
}());
//  “I am inside!”

(2)、ES6 中

在浏览器的 ES6 环境中:

  • 使用 let 和 const 声明的函数表达式,不会发生声明提升
  • 使用 var 声明的函数表达式会发生提升,提升到所在的块级作用域、函数作用域 或 全局作用域的头部(但是在块级作用域内用 var 创建函数会报错,不推荐用这种方式创建函数)。
  • 直接用函数声明的方式创建一个函数,该函数会发生声明提升(但是在块级作用域内直接用函数声明的方式创建一个函数会报错,不推荐用这种方式创建函数)。
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值