3

关于作用域,变量提升,函数提升的个人理解

参考:

写博客的原因是看到两个题目

var foo = 1;
function bar() {
    if (!foo) {
        var foo = 10;
    }
    alert(foo);
}
bar();

答案是10!
你是否会疑惑条件语句if(!foo)并不会执行,为什么foo会被赋值为10

再来看第二个例子

var a = 1;
function b() {
    a = 10;
    return;
    function a() {}
}
b();
alert(a);

答案还是10吗?显然不是,alert输出了1

作用域(scope)

C语言的一个例子:

#include <stdio.h>
int main() {
    int x = 1;
    printf("%d, ", x); // 1
    if (1) {
        int x = 2;
        printf("%d, ", x); // 2
    }
    printf("%d\n", x); // 1
}

程序依次输出了1,2,1.
C语言中,我们有块级作用域(block-level scope)。在一个代码块(一对{}花括号括起来的部分)的中变量并不会覆盖掉代码块外面的变量。

JavaScript中的表现:

var x = 1;
console.log(x); // 1
if (true) {
    var x = 2;
    console.log(x); // 2
}
console.log(x); // 2

JavaScript有函数级作用域(function-level scope)。这一点和C家族完全不同。语句块,如if语言,不创建新的作用域。仅仅函数能创建新作用域。

在JavaScript中,如果我们需要实现block-level scope,我们也有一种变通的方式,那就是通过自执行函数创建临时作用域:(闭包)

function foo() {
    var x = 1;
    if (x) {
        (function () {
            var x = 2;
            // some other code
        }());
    }
    // x is still 1.
}

上面代码在if条件块中创建了一个闭包,它是一个立即执行函数,所以相当于我们又创建了一个函数作用域,所以内部的x并不会对外部产生影响。

变量作用域

一个变量的作用域表示这个变量存在的上下文。它指定了你可以访问哪些变量以及你是否有权限访问某个变量。

变量作用域分为局部作用域全局作用域

局部变量(处于函数级别的作用域)

javascript没有块级作用域(被花括号包围的);当是,javascript有拥有函数级别的作用域,也就是说,在一个函数内定义的变量只能在函数内部访问或者这个函数内部的函数访问(闭包除外)

不要忘记使用var关键字

如果声明一个变量的时候没有使用var关键字,那么这个变量将是一个全局变量

// If you don't declare your local variables with the var keyword, they are part of the global scope
var name = "Michael Jackson";
 
function showCelebrityName () {
    console.log (name);
}
 
function showOrdinaryPersonName () {    
    name = "Johnny Evers";
    console.log (name);
}
showCelebrityName (); // Michael Jackson
 
// name is not a local variable, it simply changes the global name variable
showOrdinaryPersonName (); // Johnny Evers
 
// The global variable is now Johnny Evers, not the celebrity name anymore
showCelebrityName (); // Johnny Evers
 
// The solution is to declare your local variable with the var keyword
function showOrdinaryPersonName () {    
    var name = "Johnny Evers"; // Now name is always a local variable and it will not overwrite the global variable
    console.log (name);
}
局部变量优先级大于全局变量

如果在全局作用域中什么的变量在局部作用域中再次声明,那么在局部作用域中调用这个变量时,优先调用局部作用域中声明的变量

var name = "Paul";
 
function users () {
    // Here, the name variable is local and it takes precedence over the same name variable in the global scope
var name = "Jack";
 
// The search for name starts right here inside the function before it attempts to look outside the function in the global scope
console.log (name); 
}
 
users (); // Jack
全局变量

所有在函数外面声明的变量都处于全局作用域中。在浏览器环境中,这个全局作用域就是我们的Window对象(或者整个HTML文档)。

每一个在函数外部声明或者定义的变量都是一个全局对象,所以这个变量可以在任何地方被使用,例如:

// name and sex is not in any function
var myName = "zhou";
var sex = "male";
 
//他们都处在window对象中
console.log(window.myName); //paul
console.log('sex' in window); //true

如果一个变量第一次初始化/声明的时候没有使用var关键字,那么他自动加入到全局作用域中。

setTimeout中的函数是在全局作用域中执行的

setTimeout中的函数所处在于全局作用域中,所以函数中使用this关键字时,这个this关键字指向的是全局对象(Window):

var Value1 = 200;
var Value2 = 20;
var myObj = {
  Value1 : 10,
  Value2 : 1,
  
  caleculatedIt: function(){
    setTimeout(function(){
      console.log(this.Value1 * this.Value2);
    }, 1000);
  }
}
 
myObj.caleculatedIt(); //4000

提升(Hoisting)

几个过程与词语意义

在说明提升之前,要搞清楚几个词的意义.

  1. 作用域中的名字(属性名)
    例如 var a; 中a就是名字或者叫属性名
  2. 声明
    var a;就是变量声明
    function f (){}就是函数声明
  3. 赋值
    a=1;就是赋值

需要注意的是如果这样写var b = 2;,那么这句话就是两个过程,分别是声明赋值
等于下面的代码

var b;//声明
b = 2;//赋值

function f (){}函数在声明时,声明与赋值都同时进行.

理解函数是一等公民

参考:
阮一峰:函数是一等公民
JavaScript 语言将函数看作一种,与其它值(数值、字符串、布尔值等等)地位相同。凡是可以使用值的地方,就能使用函数。

  • 可以把函数赋值给变量和对象的属性
  • 可以当作参数传入其他函数
  • 可以作为函数的结果返回

函数只是一个可以执行的,此外并无特殊之处。

由于函数与其他数据类型地位平等,所以在 JavaScript 语言中又称函数为第一等公民

function add(x, y) {
  return x + y;
}

// 将函数赋值给一个变量
var operator = add;

// 将函数作为参数和返回值
function a(op){
  return op;
}
a(add)(1, 1)
// 2
函数声明会覆盖变量声明

因为其是一等公民,与其他值地位相同,所以函数声明会覆盖变量声明

如果存在函数声明和变量声明(注意:仅仅是声明,还没有被赋值),而且变量名跟函数名是相同的,那么,它们都会被提示到外部作用域的开头,但是,函数的优先级更高,所以变量的值会被函数覆盖掉。

// Both the variable and the function are named myName
var myName;

function myName () {
console.log ("Rich");
}
 
// The function declaration overrides the variable name
console.log(typeof myName); // function

但是,如果这个变量或者函数其中是赋值了的,那么另外一个将无法覆盖它:

// But in this example, the variable assignment overrides the function declaration
var myName = "Richard"; // This is the variable assignment (initialization) that overrides the function declaration.
 
function myName () {
console.log ("Rich");
}
 
console.log(typeof myName); // string

因为上面的代码等价于

var myName;
function myName () {
console.log ("Rich");
}
//上面是提升的区域
myName= "Richard";//然后再赋值
console.log(typeof myName); // string

变量的提升与函数的提升

在Javascript中,变量进入一个作用域可以通过下面四种方式:

  • 语言自定义变量:所有的作用域中都存在this和arguments这两个默认变量
  • 函数形参:函数的形参存在函数作用域中
  • 函数声明:function foo() {}
  • 变量定义:var foo

(下面会详细的讲这四种方式)

在代码运行前,函数声明和变量定义通常会被解释器移动到其所在作用域的最顶部

如何理解这句话呢?

function foo() {
    bar();
    var x = 1;
}

上面这段在吗,被代码解释器编译完后,将变成下面的形式:

function foo() {
    var x;
    bar();
    x = 1;
}

我们注意到,x变量的声明被移动到函数(自己所在的作用域)的最顶部。然后在bar()后,再对其进行赋值,即赋值的位置不变,还是原来的位置.
再来看一个例子,下面两段代码其实是等价的:

function foo() {
    if (false) {
        var x = 1;
    }
    return;
    var y = 1;
}
function foo() {
    var x, y;
    if (false) {
        x = 1;
    }
    return;
    y = 1;
}

所以变量的上升(Hoisting)只是其声明(定义)上升,而变量的赋值并不会上升

我们都知道,创建一个函数的方法有两种,一种是通过函数声明function foo(){}
另一种是通过定义一个变量var foo = function(){} 那这两种在代码执行上有什么区别呢?

来看下面的例子:

function test() {
    foo(); // TypeError "foo is not a function"
    bar(); // "this will run!"
    var foo = function () { // function expression assigned to local variable 'foo'
        alert("this won't run!");
    }
    function bar() { // function declaration, given the name 'bar'
        alert("this will run!");
    }
}
test();

在这个例子中,foo()调用的时候报错了,而bar能够正常调用
我们前面说过变量会上升,所以var foo首先会上升到函数体顶部,然而此时的foo为undefined,所以执行报错TypeError "foo is not a function"。而对于函数bar, 函数本身也是一种变量,所以也存在变量上升的现象,但是它这种声明方法会上升了整个函数(包括声明和赋值),所以bar()才能够顺利执行。

在这种情况下,仅仅函数声明的函数体被提升到顶部。名字“foo”被提升(即声明变量),但后面的函数体(即复制的部分),在执行的时候才被指派。

所以以上代码相当于下面

function test() {
    var foo;
    function bar() { // function declaration, given the name 'bar'
    alert("this will run!");
    }
    foo(); // TypeError "foo is not a function"
    bar(); // "this will run!"
    foo = function () { // function expression assigned to local variable 'foo'
        alert("this won't run!");
    }
}
test();

再回到一开始我们提出的两个例子,能理解其输出原理了吗?

var foo = 1;
function bar() {
    if (!foo) {
        var foo = 10;
    }
    alert(foo);
}
bar();

其实就是:

var foo = 1;
function bar() {
    var foo;//foo此时被初始化为undefined
    if (!foo) {//!undefined是true
        foo = 10;
    }
    alert(foo);//输出的是局部变量foo
}
bar();

那么下面

var a = 1;
function b() {
    a = 10;
    return;
    function a() {}
}
b();
alert(a);

其实就是:

var a = 1;
function b() {
    function a() {}
    a = 10;//相当于只是覆盖了在b函数这个作用域内声明的a函数所以这个a=10只是局部变量,影响不到外面的a
    return;
}
b();
alert(a);

这就是为什么,我们写代码的时候,变量定义总要写在最前面

四中变量进入作用域被提升的顺序

以下文字引用自阮一峰的教程函数一章(2.7)下方评论(需要梯子才能看到评论)
关于 Hoisting 那部分,有两点值得说明:

  1. Hoisting 的作用范围是随着函数作用域的。我理解在这里尚未讲到函数作用域,不过可以提一句提醒读者注意,然后链接至作用域的部分进一步探讨;
  2. “Hoisting 只对 var 声明的变量有效”,不尽然如此(意思是不完全对)。变量声明的提升并非 hoisting 的全部,JavaScript 有四种让声明在作用域内获得提升的途径(按优先级):

语言定义的声明,如 this 和 arguments。你不能在作用域内重新定义叫做 this 的变量,是因为 this是语言自动定义的声明,并且它的优先级最高,也就是被 Hoisting 到最顶上了,没人能覆盖它
形式参数。虽然我们无需用 var 来修饰形式参数,但是形式参数的确也是变量,并且被自动提升到次高的优先级

函数声明。除了 var 以外,function declaration 也可以定义新的命名,并且同样会被 hoisting
至作用域顶部,仅次于前两者

-最后,是本文提到的常规变量,也就是 var 声明的变量

对于上面话的理解:
也就是说优先级越高的越没法被覆盖.那么表现在代码层,提升时就会转换成下面这样:
提升时代码表现上的排序为:

1 var a;
2 function f (){};
3 形参
4 this和arguments

这就解释了在提升时,函数声明时赋值声明的函数会覆盖函数表达式方式的声明(function f (){};)的函数,因为函数表达式提升了(创建,初始化,赋值都被提升),而赋值时的函数只在赋值的时候才被解析.

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

function f() {
  console.log('2');
}

f() // 1
如果同时采用function命令和赋值语句声明同一个函数,最后总是采用赋值语句的定义。

提升的本质

参考
方应杭的文章:let
要搞清楚提升的本质,需要理解 JS 变量的「创建create、初始化initialize 和赋值assign」

有的地方把创建说成是声明(declare),为了将这个概念与变量声明区别开,我故意不使用声明这个字眼。

有的地方把初始化叫做绑定(binding),但我感觉这个词不如初始化形象。

我们来看看 var 声明的「创建、初始化和赋值」过程

假设有如下代码:

function fn(){
  var x = 1
  var y = 2
}
fn()

在执行 fn 时,会有以下过程(不完全):

  1. 进入 fn,为 fn 创建一个环境。
  2. 找到 fn 中所有用 var 声明的变量,在这个环境中「创建」这些变量(即 x 和 y)。
  3. 将这些变量「初始化」为 undefined。
  4. 开始执行代码
  5. x = 1 将 x 变量「赋值」为 1
  6. y = 2 将 y 变量「赋值」为 2

也就是说 var 声明会在代码执行之前就将「创建变量,并将其初始化为 undefined」。

这就解释了为什么在 var x = 1 之前 console.log(x) 会得到 undefined。

接下来来看 function 声明的「创建、初始化和赋值」过程

假设代码如下:

fn2()

function fn2(){
  console.log(2)
}

JS 引擎会有一下过程:

  1. 找到所有用 function
  2. 声明的变量,在环境中「创建」这些变量。
  3. 将这些变量「初始化」并「赋值」为 function(){console.log(2) }。(三步全部都被提升)
  4. 开始执行代码 fn2()

也就是说 function 声明会在代码执行之前就「创建、初始化并赋值」。 (三步全部都被提升)

接下来看 let 声明的「创建、初始化和赋值」过程

假设代码如下:

{
  let x = 1
  x = 2
}

我们只看 {} 里面的过程:

找到所有用 let 声明的变量,在环境中「创建」这些变量
开始执行代码(注意现在还没有初始化)
执行 x = 1,将 x 「初始化」为 1(这并不是一次赋值,如果代码是 let x,就将 x 初始化为 undefined)
执行 x = 2,对 x 进行「赋值」
这就解释了为什么在 let x 之前使用 x 会报错:

let x = 'global'
{
  console.log(x) // Uncaught ReferenceError: x is not defined
  let x = 1
}

原因有两个

console.log(x) 中的 x 指的是下面的 x,而不是全局的 x
执行 log 时 x 还没「初始化」,所以不能使用(也就是所谓的暂时死区)
看到这里,你应该明白了 let 到底有没有提升:

**let 的「创建」过程被提升了,但是初始化没有提升。
var 的「创建」和「初始化」都被提升了。
function 的「创建」「初始化」和「赋值」都被提升了。**

最后看 const,其实 const 和 let 只有一个区别,那就是 const 只有「创建」和「初始化」,没有「赋值」过程。

这四种声明,用下图就可以快速理解:
四种声明
所谓暂时死区,就是不能在初始化之前,使用变量。


风彻
1.5k 声望142 粉丝