# 作用域跟闭包

# 作用域和自由变量

如果当前作用域中存在此变量声明,无论它在什么地方声明,引用此变量时就会在当前作用域中查找,不会去外层作用域了。 es6 之前只有函数作用域跟全局作用域。es6 新增块级作用域,使用 let const 定义的变量;

# 变量提升

原理:JS引擎的工作方式是先解析代码,获取所有被声明的变量;然后在运行。JS代码自上而下执行之前,浏览器首先会把所有带 var/function 关键词的进行提前 “声明” 或者 “定义” ,这种预先处理机制称之为 “变量提升”。

下面的是使用 let 定义的变量,是属于块级作用域不属于函数作用域,所以 if 语句访问的时候会直接报错: foo is not defined

function hoistVariable() {
    if (!foo) {
        let foo = 5;
    }

    console.log(foo); // 5
}

hoistVariable();

而使用 var 定义的变量是直接属于函数作用域,JS 在解析代码的时候会进行变量提升,首先定义了 foo,此时 if 判断为 undefined ,所以打印结果为 5

function hoistVariable() {
    if (!foo) {
        var foo = 5;
    }

    console.log(foo); // 5
}

hoistVariable();

上面的代码执行的其实是下面的步骤:

function hoistVariable() {
    var foo;

    if (!foo) {
        foo = 5;
    }

    console.log(foo); // 5
}

hoistVariable();

下面代码是作用域查找相关的问题: 结果为 5 却不是 3 ,是因为,在 JS 中,查询自由变量是首先在自己的作用域进行查找,如果找到了就不会往上级作用域去寻找,如果所在的作用域没有,就继续往上级作用域进行查找,如果没有找到则报错 xx is no defined

var foo = 3;

function hoistVariable() {
  var foo = foo || 5;
  console.log(foo); // 5
}
hoistVariable();

上面的代码执行的步骤如下:

var foo = 3;

// 预编译之后
function hoistVariable() {
    var foo;

    foo = foo || 5;

    console.log(foo); // 5
}

hoistVariable();

如果当前作用域中声明了多个同名变量,那么根据我们的推断,它们的同一个标识符会被提升至作用域顶部,其他部分按顺序执行,比如下面的代码:

注意这里跟函数提升不一样,函数就会直接为最后一次定义的函数;函数需要赋值,并不像变量一样先定义。

function hoistVariable() {
    var foo = 3;
    {
        var foo = 5;
    }
    console.log(foo); // 5
}

hoistVariable();

对于函数,除了使用上面的函数声明,更多时候,我们会使用函数表达式,下面是函数声明和函数表达式的对比:

// 函数声明
function foo() {
  console.log('function declaration');
}

// 匿名函数表达式
var foo = function() {
  console.log('anonymous function expression');
};

// 具名函数表达式
var foo = function bar() {
  console.log('named function expression');
};

可以看到,匿名函数表达式,其实是将一个不带名字的函数声明赋值给了一个变量,而具名函数表达式,则是带名字的函数赋值给一个变量,需要注意到是,这个函数名只能在此函数内部使用。我们也看到了,其实函数表达式可以通过变量访问,所以也存在变量提升同样的效果。 那么当函数声明遇到函数表达式时,会有什么样的结果呢,先看下面这段代码:

function hoistFunction() {
    foo(); // 2

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

    foo(); // 1

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

    foo(); // 1
}

hoistFunction();

运行后我们会发现,输出的结果依次是2 1 1,为什么会有这样的结果呢?

因为JavaScript中的函数是一等公民,函数声明的优先级最高,会被提升至当前作用域最顶端,所以第一次调用时实际执行了下面定义的函数声明,然后第二次调用时,由于前面的函数表达式与之前的函数声明同名,故将其覆盖,以后的调用也将会打印同样的结果。上面的过程经过预编译之后,代码逻辑如下:

// 预编译之后
function hoistFunction() {
    var foo;

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

    foo(); // 2

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

    foo(); // 1

    foo(); // 1
}

hoistFunction();

下面的函数和变量重名时,会如何执行:

var foo = 3;

function hoistFunction() {
    console.log(foo); // function foo() {}

    foo = 5;
    
    console.log(foo); // 5

    function foo() {}
}

hoistFunction();
console.log(foo);     // 3

我们可以看到,函数声明被提升至作用域最顶端,然后被赋值为5,而外层的变量并没有被覆盖,经过预编译之后,上面代码的逻辑是这样的:

其实如果是同名的话,只会定义一次,函数的优先权是最高的,它永远被提升至作用域最顶部,然后才是函数表达式和变量按顺序执行

// 预编译之后

var foo = 3;

function hoistFunction() {
   var foo;

   foo = function foo() {};

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

   console.log(foo); // 5
}

hoistFunction();
console.log(foo);    // 3

如果是下面的代码:

var foo = 3;

function hoistFunction() {
    console.log(foo); // function foo() {}
    var foo = 6
    console.log(foo); // 6

    function foo() {}
}

hoistFunction();
console.log(foo); 

# 作用域

  • 全局作用域
  • 函数作用域
  • 块级作用域 if、while、try等里面定义的变量
if(true) {
  let x = '1'
}
console.log(x) // x is not defined

# 自由变量

  • 一个变量在当前作用域没有定义,但被使用了
  • 向上级作用域,一层一层依次寻找,直到找到为止
  • 如果到全局作用域都没找到,则报错 xx is no defined

# 闭包

  • 作用域应用的特殊情况,有两种表现:
  • 函数作为参数被传递
  • 函数作为返回值被返回

所有的自由变量的查找,是在函数定义的地方,向上级作用查找,不是在执行的地方

// 函数做为返回值
function create() {
  let a = 100
  return function() {
    console.log(a)
  }
}

let fn = create()
let a = 200
fn() // 100

// 函数作为参数
function print(fn) {
  let b = 300
  fn()
}

let b = 400
function fns() {
  console.log(b)
}
print(fns)

# 闭包隐藏数据,只提供 API


# var 、let、const 区别

首先let声明变量分为三部分:1.创建,2.初始化,3.赋值。 而通过const命令声明的是一个常量,一旦声明,常量的值就不能改变 ,所以const声明必须立即进行初始化,不能留到以后赋值,所以const声明变量分为两部分:1.创建,2.初始化,没有赋值操作,其实相当于把初始化赋值整合成了一步,在初始化的时候进行赋值。

通过var声明的变量,变量提升时相当于把创建和初始化进行了提升,没有提升赋值操作,可以理解为var命令声明变量其实为两部分:第一部分创建的同时进行初始化,第二部分赋值。

而let和const仅仅提升了变量的创建,初始化及赋值操作都没有进行提升;let const 定义的变量不会存在变量提升

# bind 、call、apply 的区别

bind 会返回一个新的函数去执行。call、apply 直接会执行。 call、apply 之间的区别是,apply 的第二个参数是一个参数数组。

评 论:

更新: 11/21/2020, 7:00:56 PM