【JavaScript】(附面试题)深刻理解做用域、做用域链和闭包

引言

JavaScript中有做用域、做用域链和闭包。咱们最开始可能以为知道这些的定义就算懂了(刚入门时的我也是这样),可是当深刻了解的时候,发现本身知道的只是皮毛。因此,这篇文章将详细讲解做用域、做用域链和闭包。html

咱们先借助一道题,了解一下做用域、做用域链和闭包的造成过程~前端

let x = 1;
function A(y){
   let x = 2;
   function B(z){
       console.log(x+y+z);
   }
   return B;
}
let C = A(2);
C(3);
复制代码

对于上面的这张解答图,有以下解释:web

  • 当建立一个函数时,会建立一个堆,同时初始化当前函数做用域,做用域([[Scope]])为所在上下文中的变量对象VO/AO面试

  • 当执行一个函数时,会建立新的执行上下文 -> 初始化this指向 -> 初始化做用域链([[ScopeChain]]) -> 建立AO变量对象存储变量bash

  • EC(A)执行上下文中,堆被全局变量C所占用,不能出栈销毁,此时就造成了闭包。闭包

  • 图里面红线所表明的就是做用域链,在一个做用域中,它所须要的变量在当前做用域中没有,就会一层一层向上查找。函数

这样简单的一个题,引出了做用域、做用域链、闭包的概念,下面本篇文章将正式对它们进行讲解。学习

1、做用域

做用域([[Scope]])就是变量与函数的可访问范围,即做用域控制着变量与函数的可见性和生命周期。ui

1.1 词法做用域与动态做用域

1.1.1 词法做用域

词法做用域也是静态做用域,在JavaScript中采用的就是词法做用域。词法做用域就是定义在词法阶段的做用域。换句话说,词法做用域是由你在写代码时将变量和块做用域写在哪里决定的。所以当词法分析器处理代码时会保持做用域不变(大部分状况是这样)this

下面一个例子帮助理解:

var value = 1;

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

function bar() {
    var value = 2;
    foo();
}

bar();
复制代码

假如上述例子采用了词法做用域,那么它的执行过程就是:

首先执行bar()函数,在bar()函数中执行foo()函数,foo()函数中输出value的值。它首先会查找当前做用域中是否有value,若是没有,则会向外一层查找,则最后输出了1

1.1.2 动态做用域

动态做用域便是与词法做用域相反的。咱们仍是以上面的例子为例:

假如上述例子采用动态做用域:

它依然会像采用词法做用域的形式执行函数,惟一不同的地方在于:在执行foo()函数时,他不会向外一层查找value,而是从调用的函数做用域中查找,因此最后的结果输出为2

1.2 全局做用域

在代码任何地方都能访问到的对象拥有全局做用域,通常拥有全局做用域有如下三种情形

1.2.1 最外层函数和在最外层函数外面定义的变量

var globalValue = `global value`;

function checkGlobal() {
    var localValue = `local value`;
    console.log(localValue);                // "local value"
    console.log(globalValue);               // "global value"
}

console.log(globalValue);                   // "global value"
console.log(checkGlobal);                   // "global value"
console.log(localValue);                    // "Uncaught ReferenceError: localValue is not defined"
复制代码

在上面的例子中,globalValue就是一个全局变量,不管在哪都能访问,而localValue是一个局部变量,只能在函数内部访问

1.2.2 全部未定义直接赋值的变量

function checkGlobal() {
    var localValue = 'local value';
    globalValue = 'global value';
    console.log(localValue, globalValue);    // 'local value' 'globalValue'
}

console.log(globalValue);                    // 'globalValue'
console.log(localValue);                     // "Uncaught ReferenceError: localValue is not defined"
复制代码

1.2.3 全部window对象的属性

1.3 函数做用域

函数做用域,就是指声明在函数内部的变量,它正好和全局做用域相反。内层做用域能够访问到外层做用域,而外层做用域不能访问到内层做用域。

function check() {
    var localValue = 'local value';
    console.log(localValue);          // 'local value'
}
 
console.log(localValue);              // "Uncaught ReferenceError: localValue is not defined"
复制代码

1.4 块级做用域

块级做用域可经过letconst声明,声明后的变量再指定块级做用域外没法被访问。

1.4.1 块级做用域被建立的状况

  • 在一个函数内部

  • 在一个代码块内部

1.4.2 块级做用域的特色

  • 声明的变量不会提高到代码块顶部

let或者const声明的变量不会被提高到当前做用域顶部。

function check(bool) {
    if(bool) {
        let result = 1;
        console.log(result);
    }
    console.log(result);
}

check(true);   // 1 Uncaught ReferenceError: result is not defined
check(false);  // Uncaught ReferenceError: result is not defined
复制代码

若是想要访问到result,须要本身手动将变量提高到当前做用域顶部。像这样

function check(bool) {
    let result = null;
    if(bool){
        result = 1
    }
    console.log(result);
}
....
复制代码
  • 禁止重复声明

在同层级的做用域内,已经声明过的变量,不能够再次声明。

// 1.同层级做用域
var bool = true;
let bool = false;    // Uncaught SyntaxError: Identifier 'bool' has already been declared

// 不一样层级做用域
var bool = true;
function check() {
    let bool = false;   // 这里不会报错
    // .....
} 

复制代码
  • 在循环中的使用

for循环中声明的变量仅在循环内部使用。

for(let i = 0; i < 1; i++) {
    console.log(i);    // 0
}
console.log(i);        // Uncaught ReferenceError: i is not defined
复制代码

可是循环内部又是一个单独的做用域

for(let i = 0; i < 2; i++) {
    let i = 'hello';
    console.log(i);    // 'hello' 'hello' 
}
复制代码

2、做用域链

2.1 做用域链的定义及造成

当所须要的变量在所在的做用域中查找不到的时候,它会一层一层向上查找,直到找到全局做用域尚未找到的时候,就会放弃查找。这种一层一层的关系,就是做用域链。

var a = 1;

function check() {
    return function() {
        console.log(a); // 当前做用域内找不到a,会向上一层一层查找,最后找到了全局下的a,输出结果为1
        console.log(b); // 同理,因此输出"Uncaught ReferenceError: b is not defined"
    }
}

var func = check();   // 此时返回匿名函数
func();               // 执行匿名函数
复制代码

3、闭包

3.1 闭包的定义

当函数能够记住并访问所在的词法做用域时,就产生了闭包。即便函数是在当前词法做用域以外执行。

3.2 闭包的造成

咱们来看一段代码

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

var bar = foo();
bar();
复制代码

foo()函数的执行结果返回给bar,而此时因为变量a还在使用,于是没有被销毁,而后执行bar()函数。这样,咱们就能在外部做用域访问到函数内部做用域的变量。这个就是闭包。

闭包的造成条件:

  • 函数嵌套

  • 内部函数引用外部函数的局部变量

3.3 闭包的做用

  • 能够读取函数内部的变量

  • 可使变量的值长期保存在内存中,生命周期比较长。

  • 可用来实现JS模块(JQuery库等)

JS模块是具备特定功能的JS文件,将全部的数据和功能都封装在一个函数内部(私有的),只向外暴露一个包含多个方法的对象或函数,模块的使用者,只须要经过模块暴露的对象调用方法来实现对应的功能。

(function() {
    var a = 1;
    
    function test() {
        return a;
    }
    
    window.module = {a, test};  // 向外暴露
})()
复制代码
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <script src="./1.js"> </script>
    <title>Document</title>
</head>
<body>
    <script>
        console.log(module.a);          // 1
        console.log(module.test());     // 1
    </script>
</body>
</html>
复制代码

3.4 闭包的特性

  • 每一个函数都是闭包,函数可以记住本身定义时所处的做用域,函数走到了哪,定义时的做用域就到了哪。

  • 内存泄漏

内存泄漏就是一个对象在你不须要它的时候仍然存在。因此不能滥用闭包。当咱们使用完闭包后,应该将引用变量置为null

function outer(){
    var num = 0;
    return function add(){
        num++;
        console.log(num);
    };
 }
var func1 = outer();
func1();              // 1
func1();              // 2  [没有被释放,一直被占用]
var func2 = outer();
func2();              // 1  [从新引用函数时,闭包是新的]
func2();              // 2
复制代码

3.5 闭包的应用

如今要求实现点击第几个button就输出几

先写一个html

<button>1</button>
<button>2</button>
复制代码

再来写JS

let buttons = document.getElementsByTagName('button');

for(var i = 0; i < buttons.length; i++) {
    buttons[i].onclick = function() {
        console.log(i + 1);
    }
}
复制代码

第一次咱们可能会写出这样的代码,可是咱们会发现,这个代码存在问题,不管我点第几个按钮,都会输出3。这是由于此时的i是全局变量,当执行点击事件时,i已经变成了3

咱们能够用两种方式解决这个问题

  • let声明

将上述代码中的var改成let

  • 闭包
for(var i = 0; i < buttons.length; i++) {
    (function(k){
        buttons[k].onclick = function() {
            console.log(k + 1);
        }
    })(i)
}
复制代码

面试题

let x = 5;
function fn(x) {
    return function(y) {
        console.log(y + (++x));
    }
}
let f = fn(6);
f(7);   
console.log(x);
复制代码

总结

本篇文章主要讲解了关于做用域、做用域链和闭包的知识,若是以为对你有帮助,能够给本篇文章点个赞呀~若是有哪里不对的地方,还请你们指出来,咱们共同窗习、共同进步~

最后,分享一下个人公众号「web前端日记」,欢迎你们前来关注~