JS块级作用域问题

Javascript有关块级作用域的问题

一、块作用域 { }

  1. JS中作用域有:全局作用域、函数作用域。没有块作用域的概念。ECMAScript 6(简称ES6)中新增了块级作用域。
    块作用域由 { } 包括,if语句和for语句里面的{ }也属于块作用域。

  2. 在javascript里是没有块级作用域的,而ES6添加了块级作用域。在ES6(ECMAScript6)之前的JS版本中没有块级作用域会出现以下问题:

    (1) if 和 for语句中定义的变量泄露变成全局变量

    1
    2
    3
    4
    for(var i=0;i<=5;i++){
    console.log("hello");
    }
    console.log(i); //6,记到6循环条件不通过

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

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    var temp = new Date();
    function f(){
    console.log(temp);
    if(false){
    var temp = "hello";
    }
    }
    f(); //undefined

    ==========================================

    //上面的代码等价于下面的代码
    var temp = new Date();
    function f(){
    console.log(temp);
    var temp = "hello";
    //var定义的变量在if中没有块级作用域的概念,从而泄露成函数作用域中的一个变量
    }
    f(); //undefined

    ==========================================

    //上面的代码等价于下面的代码,变量将会 提升 到作用域的顶端去执行变量的定义,
    //所以函数输出的是undefined代表的是temp变量的值是undefined,而不是理解成temp变量未定义。
    var temp = new Date();
    function f(){
    var temp;
    console.log(temp);
    temp = "hello";
    }
    f(); //undefined
  3. 在ES6引入块级作用域之前的版本中,要想实现块级作用域的办法是立即执行函数。立即执行匿名函数的目的是建立一个块级作用域。ES6以前变量的作用域是函数范围,有时在函数内局部需要一些临时变量,因为没有块级作用域,所以就会将局部代码封装到IIEF(立即执行函数)中,这样达到了想要的效果。临时变量被封装在IIFE中,就不会污染上层函数(var 定义的变量泄露到函数作用域中,造成污染);

    1
    2
    3
    4
    5
    //立即执行函数 达到了块级作用域的效果
    (function(){
    var temp = "hello world";
    }());
    console.log(temp);// 报错:Uncaught ReferenceError: temp is not defined

二、立即执行函数

  1. 立即执行函数模式是一种语法,可以让你的函数在定义后立即被执行。

  2. 立即执行函数的组成

    • 定义一个函数
    • 将整个函数包裹在一对括号 ‘()’ 中
      将函数声明转换为表达式
    • 在结尾加上一对括号
      让函数立即被执行
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    (function foo(){
    console.log("Hello");
    })()

    //像其它任何函数一样,一个立即执行函数也能返回值并且可以赋值给其它变量
    var num = (function () {
    return 4
    })()
    console.log(num); //4

  3. 作用:

    页面加载完成后只执行一次的设置函数。

    将设置函数中的变量包裹在局部作用域中,不会泄露成全局变量。

三、js中的变量提升和函数提升

引擎会在解释JavaScript代码之前首先对齐进行编译,编译过程中的一部分工作就是找到所有的声明,并用合适的作用域将他们关联起来,这也正是词法作用域的核心内容。

☆☆☆☆☆简单说就是 在js代码执行前引擎会先进行预编译,预编译期间会将变量声明与函数声明提升至其对应作用域的最顶端。举例来说就是:

1
2
3
4
5
6
7
console.log(a);
var a = 3;

//预编译后的代码结构可以看做如下:
var a; // 将变量a的声明提升至最顶端,赋值逻辑不提升。
console.log(a); // undefined
a = 3; // 代码执行到原位置即执行原赋值逻辑

(一) 变量提升

通常JS引擎会在正式执行之前先进行一次预编译,在这个过程中,首先将变量声明及函数声明提升至当前作用域的顶端,然后进行接下来的处理。(注:当前流行的JS引擎大都对源码进行了编译,由于引擎的不同,编译形式也会有所差异,这里说的预编译和提升其实是抽象出来的、易于理解的概念)

(1)变量提升示例

1
2
3
4
5
6
function output(){
console.log(number);
var number = 10;
}

output(); //undefined

预编译的代码逻辑为

1
2
3
4
5
6
function output(){
var number;
console.log(number);
number = 10;
}
output(); //number值为undefined

(2)JS没有块级作用域最直观的例子

1
2
3
4
5
6
7
8
function output(){
var number = 10;
{
var number = 20;
}
console.log(number);
}
output(); //20

预编译的代码逻辑为

1
2
3
4
5
6
function output(){
var number = 10;
var number = 20; //变量覆盖
console.log(number);
}
output(); //20

(二) 函数提升

  1. 函数可以在声明之前就调用(但是在做任何开发之前作为一名合格的程序员要做的是先定义后使用),其实引擎是把函数声明整个地提升到了当前作用域的顶部
1
2
3
4
5
6
7
8
function output(){
foo();
function foo(){
console.log(10);
}
}

output(); //10

预编译后的逻辑为

1
2
3
4
5
6
7
8
//函数整体被提升
function output(){
function foo(){
console.log(10);
}
foo();
}
output(); //10
  1. 如果在同一个作用域中存在多个同名函数声明,后面出现的将会覆盖前面的函数声明:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    function output(){
    foo();
    function foo(){
    console.log(10);
    }
    function foo(){
    console.log(100);
    }
    }
    output(); //100

    预编译后的逻辑为

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    function output(){
    function foo(){
    console.log(10);
    }
    function foo(){ //函数定义覆盖
    console.log(100);
    }
    foo();
    }
    output(); //100
  2. 函数提升优先级高于变量提升

    (1)当函数声明遇到函数表达式

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    function hoistFunction() {
    foo(); // 2

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

    foo(); // 1

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

    foo(); // 1
    }

    hoistFunction();

    预编译后的逻辑为

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    function hoistFunction() {
    function foo() { //函数提升优先级最高,其次到 函数表达式 和 变量
    console.log(2);
    }
    var foo //函数表达式看做变量提升

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

    hoistFunction();

    上面的例子可以知道,函数hoistFunction存在变量提升和函数提升,首先进行的是foo函数提升,然后再是函数表达式foo看做是变量提升。函数提升只会提升函数声明不会提升函数表达式(函数表达式就可以看作成变量提升)。

    函数提升优先级最高(提升的是函数声明,首先把函数声明放到作用域的顶头),其次到函数表达式和变量(提升变量的声明,赋值逻辑不会提升)。

四、var、let、const的区别

有了上面变量和函数提升的基础,可以更好地理解var、let、const的区别。ES6新增了letconst关键字,使得js也有了“块”级作用域,而且使用letconst声明的变量和函数是不存在提升现象的(老老实实在{ }里面待着),比较有利于我们养成良好的编程习惯。

  1. var定义的变量,没有块的概念,可以跨块访问, 不能跨函数访问,有变量提升

  2. let定义的变量,只能在块作用域里访问,不能跨块访问,也不能跨函数访问,无变量提升,不可以重复声明。

  3. const用来定义常量,使用时必须初始化(即必须赋值),只能在块作用域里访问,而且不能修改,无变量提升,不可以重复声明。注意:const常量,指的是常量对应的内存地址不得改变,而不是对应的值不得改变,所有把应用类型的数据设置为常量,其内部的值是可以改变的,例如:const a={}; a.b=13; 不会报错

var:

1
2
3
4
5
function output(){
console.log(number);
var number = 10;
}
output(); //undefined

let:

只在自己所处的块级作用域生效,且没有变量提升

1
2
3
4
5
var number = 10;
if(true){
let Num1 = 100; //只在自己所处的块级作用域生效
}
console.log(Num1); //Uncaught ReferenceError: Num1 is not defined
1
2
3
4
5
function output(){
console.log(number);
let number = 10;
}
output(); //Uncaught ReferenceError: Cannot access 'number' before initialization(不能在初始化之前使用)

const:

const 与 let 的使用规范一样,与之不同的是:const 声明的是一个常量,且声明完后立刻赋值,否则会报错。

1
2
3
4
5
6
function output(){
const PI;
PI = 3.14;
console.log(PI);
}
output(); //Uncaught SyntaxError: Missing initializer in const declaration

Summary:变量、函数先定义后使用;避免定义相同的变量名


本博客所有文章除特别声明外,均采用 CC BY-SA 4.0 协议 ,转载请注明出处!