JavaScript自检之作用域和闭包

293 阅读5分钟

这个记录(或说笔记),参照一名【合格】前端工程师的自检清单这篇文章,根据里面所提的某些问题,进行归纳和回答。总结内容,有很多来自各位前辈的文章,在这谢谢各位!

  1. JavaScript基础自检之原型和原型链
  2. JavaScript基础自检之变量和类型
  3. JavaScript基础自检之作用域和闭包

理解词法作用域和动态作用域

可以把作用域理解为变量查找的一套规则,它的工作模型一共有2种:词法作用域和动态作用域。我所知道的大多数语言采用的是词法作用域(C#、JavaScript 和Java),动态作用域相对用的比较少(Bash)。

词法作用域

词法作用域,从名称来看,可以理解为是在「词法阶段」形成的作用域。 那么何为词法阶段? 大部分语言在编译的时候都会有如下的3个阶段:

  1. 分词/词法分析
  2. 解析/语法分析
  3. 生成代码

所谓的词法阶段,就是在「分词/词法分析」时而确定的作用域。由于可以理解为,它是有变量位置以及函数作用域决定。

动态作用域

动态作用域是在运行时确定的,它与词法作用域不同的是,它更关注「在何处调用」,而词法作用域更关注「声明的在何处」。

理解JavaScript的作用域和作用域链

JavaScript的作用域

JavaScript的作用域模型采用的是「词法作用域」,也就是说它更多的关注「变量」声明在何处。这一切都是在「词法阶段」就已经确定了,而不是在「运行时」确定。

JavaScript作用域链

作用域链,我将它理解为:变量查找过程中,依次经历的「作用域」的集合。举个例子:

    // var c = '李运华'
    function foo() {
        var a = '孙婧';
        test() {
            console.log(a);
            console.log(b);
        }
        test();
    }
    foo();

在test的作用域中,并没有变量a的声明,就会去它的父级作用域(也即foo的作用域)去找,共经历了「test作用域」和「foo作用域」。而对于变量b而言,foo作用域中也没有,它就会向「顶层作用域」去找,而没有找到,共经历了「test作用域」、「foo作用域」以及「顶层作用域」。「经历的这些作用域」就被成为作用域链,也可以认为是「作用域的嵌套」。

this的原理以及几种不同场景的取值

this是什么?

「this」是在函数被调用时发生的绑定,它的指向完全取函数在哪里被调用(和动态作用域很像)。

this的绑定规则(不同场景的取值)<《你不知道的JavaScript》>

  1. 默认绑定
        'use strict';
        var a = '孙婧';
        function foo() {
            console.log(this.a); // Uncaught TypeError: Cannot read property 'a' of undefined
        }
        
        foo();
    
    默认绑定就是发生在函数独立调用时(或者是其他绑定规则不能用时)。这个例子的foo是独立的函数调用,所以此时this绑定到了全局。而在严格模式下,全局this为undefined,所以会报TypeError。
  2. 隐式绑定
        'use strict';
        var a = '李运华';
        var obj = {
            foo: function() {
                console.log(this.a);
            },
            a: '孙婧',
        }
        
        obj.foo(); // 输出:'孙婧'
        
        var _foo = obj.foo;
        _foo(); //  输出:TypeError
    
    隐式绑定的发生与否,取决于调用时是否有上下文对象。以上面的例子来说,obj.foo(),也就意味着,调用foo时,this绑定到了obj这个对象,所以就输出'孙婧';注意观察「var _foo = obj.foo」这句话,将obj.foo的引用赋值给 _foo 这个变量,此时,调用 _foo(),此时发生的是默认绑定(绑定到全局对象<正常模式>或undefined<严格模式>),由于是严格模式,console.log(undefined.a),也就会报 TypeError了。
  3. 显式绑定
        'use strict';
        var a = '李运华';
        var obj = {
            foo: function() {
                console.log(this.a);
            },
            a: '孙婧',
        }
    
        var _foo = obj.foo;
        _foo.call(obj); //  输出:
    
    显示绑定,就是使用call或apply进行绑定。具体的使用,可以点击callapply转到MDN上查看相关文档,这里就不再说了。
    我们继续说上面的例子,调用call,将obj绑定到了_foo方法中的this,所以输出'孙婧'
  4. new绑定
        'use strict';
        function foo() {
            this.name = '孙婧';
        }
        
        var fooInstance = new foo();
        console.log(fooInstance.name); // 输出 '孙婧'
    
    我在JavaScript基础自检之原型和原型链实现过一个类似new操作符功能的函数(newPolyfill),该方法详细描述了new操作符的功能。总的来说,new操作符也是调用apply或call来实现this的上下文绑定。

闭包的实现原理和作用

闭包的实现原理

什么是闭包?

    function foo() {
        // foo函数的作用域
        var a = '孙婧';
        return function() {
            // 匿名函数的作用域
            cosole.log('a');
        }
    }
    
    var _foo = foo();
    _foo(); // 输出 '孙婧'

从上面的这个例子来看,foo函数返回一个匿名函数,该匿名函数有引用foo词法作用域中的变量a,并且该匿名函数在全局作用域中被调用。
从上面的例子看出,闭包就是函数对所在的词法作用域的引用, 并且不论该函数在何处调用,都可访问其所在的词法作用域。

实现的原理

  1. 函数对其所在的词法作用域有引用。
  2. 函数在其所在词法作用域之外被调用。

闭包的作用

我们平常写的JavaScript代码中,多多少少都会用到闭包。比如我们平常的ajax调用、事件的回调、定时器或其他的异步任务中,都会使用到闭包。

写在最后

这篇文章只是归纳了作用域的基础知识,并没有展开去论述。想要去深入学习,推荐大家去学习《你不知道的JavaScript》这本书。