Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

作用域和闭包 —— 一名【合格】前端工程师的自检清单答案整理 #4

Open
akeymo opened this issue Dec 19, 2019 · 0 comments

Comments

@akeymo
Copy link
Owner

akeymo commented Dec 19, 2019

作用域和闭包

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

词法作用域

词法作用域是定义在词法阶段的作用域。也就是说,它的定义过程发生在代码的书写阶段。词法作用域是一套关于引擎如何寻找变量以及会在何处找到变量的规则。

动态作用域

动态作用域不关心函数和作用域如何声明以及在何处声明,只关心它们从何处调用,动态作用域是在运行时确定的,this就是如此。

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

作用域

作用域是一套规则,用于确定在何处以及如何查找变量。它是一个独立的地盘,让变量不会外泄、暴露出去。作用域最大的用处就是隔离变量,不同作用域下同名变量不会有冲突。

作用域链

当查找变量的时候,会先从当前上下文的变量对象中查找,如果没有找到,就会从父级(词法层面上的父级)执行上下文的变量对象中查找,一直找到全局上下文的变量对象,也就是全局对象。这样由多个执行上下文的变量对象构成的链表就叫做作用域链。

理解JavaScript的执行上下文栈,可以应用堆栈信息快速定位问题

执行上下文

执行上下文是评估和执行JavaScript代码环境的抽象概念。每当JS代码在运行的时候,它都是在执行上下文中运行。它有三种执行上下文类型:全局执行上下文、函数执行上下文、Eval函数执行上下文。

执行栈

栈是一种后进先出的数据结构。当JS引擎第一次遇到你的脚本时,会创建一个全局的执行上下文并且压入当前的执行栈,每当引擎遇到一个函数调用,它会为该函数创建一个新的执行上下文并压入栈的顶部。引擎会执行那些执行上下文位于栈顶的函数,当该函数执行结束时,执行上下文从栈中弹出,控制流程到达当前栈中的下一个上下文。

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

原理

  • 在运行时进行绑定,而不是在编写时绑定
  • this的绑定和函数声明的位置没有任何关系,只取决于函数的调用方式
  • 因为对象的数据结构决定,对象的变量实际上只是一个对象的内存地址,如果对象的属性值是一个函数的话,函数会被单独保存在内存中,然后再将函数的地址赋值给属性的value。函数可以在不同的环境内运行,this的产生就是为了指代函数当前的运行环境

几种不同的使用场景

  1. 函数普通调用:非严格模式下this->window,严格模式下this->undefined
  2. 函数作为对象方法调用,即调用位置有上下文对象:
    function foo(){
        console.log(this.a); 
    }
    
    var obj = {
        a: 2,
        foo: foo
    };
    
    obj.foo(); //2
    
  3. callapplybind调用:
    var name = 'global';
    function logName() {
      console.log(this.name);
    }
    
    logName(); // <- 'global'
    logName.call({ name: 'call' }); // <- 'call'
    logName.apply({ name: 'apply' }); // <- 'apply'
    logName.bind({ name: 'bind' })(); // <- 'bind'
    
  4. new调用:使用new调用函数时,会构造一个新的对象并把它绑定到函数调用中的this
    function foo(a){
        this.a = a;
    }
    
    var bar = new foo(2); 
    console.log(bar.a); //2
    
  5. 箭头函数:执行时不绑定this,箭头函数中的this的值取决于作用域链上最近的this
    function genArrowFn() {
        return () => {
            console.log(this);
        }
    }
    
    const arrowFn1 = genArrowFn();
    arrowFn1();  // <- window
    
    const arrowFn2 = genArrowFn.call({ a: 1 });
    arrowFn2(); // <- { a: 1 }
    
    // `call`、`apply`、`bind`无法改变箭头函数内this的指向,仍然在作用域链上寻找
    arrowFn1.call({ a: 2 });    // <- window
    arrowFn2.apply({ a: 2 });   // <- { a: 1 }
    arrowFn2.bind({ a: 2 })();  // <- { a: 1 }
    

闭包的实现原理和作用,可以列举几个开发中闭包的实际应用

原理

闭包是指有权访问另一个函数作用域中的变量的函数。
因为作用域链的关系,一个函数它的内部函数也会包含外部函数的作用域。那么无论通过何种手段将内部函数传递到所在词法作用域以外,它都会持有对原始定义作用域的引用,这个引用就是闭包。
JS的垃圾回收机制在函数执行结束后就会销毁函数的整个内部作用域,但因为被传递到外部的内部函数还在使用,所以闭包会阻止垃圾回收。

作用

  • 实现共有变量
  • 实现封装,属性私有化
  • 模块化开发,防止污染全局变量

闭包的实际应用

  1. 累加器,防止变量被重新赋值
    function addCount(){
        var count = 0;
        var addCount = function(){
            count++;
        }
    
        return addCount;
    }
    
    document.body.addEventListener('click', addCount);
    
  2. 私有化变量
    function boy(name) {
        this.name = name;
        var sex = 'boy';
        var saySex = function () {
            console.log('my sex is ' + sex);
        }
    }
    
    var ben = new boy('Ben');
    console.log(ben.name); // Ben
    console.log(ben.sex); // undefined
    
  3. for循环中的点击事件问题
    var liList = document.getElementsByTagName('li');
    for (var i = 0; i < liList.length; i++) {
        liList[i].onclick = (function (i) {
            var clickLi = function () {
                console.log(i);
            }
            return clickLi;
        })(i);
    }
    

理解堆栈溢出和内存泄漏的原理,如何防止

全局变量

未定义的变量会在全局对象创建一个新变量,那么当函数执行结束这个变量不会被销毁。
__防止:__使用严格模式

闭包

因为闭包是指有权访问另一个函数作用域中的变量的函数,所以会导致函数的引用一直存在而无法被回收产生内存泄漏。

DOM引用

有些情况下开发人员在数据结构中存储 DOM 节点。假设你想快速更新表格中几行的内容。如果在字典或数组中存储对每个 DOM 行的引用,就会产生两个对同一个 DOM 元素的引用:一个在 DOM 树中,另一个在字典中。如果你决定删除这些行,你需要记住让两个引用都无法访问

如何处理循环的异步操作

  • 回调函数
  • async / await
  • Promise.all

理解模块化解决的实际问题,可列举几个模块化方案并理解其中原理

解决的实际问题

  • 命名空间冲突,各个模块的命名空间独立,不会相互覆盖。
  • 文件依赖管理

举例

  1. CommonJS规范:允许通过require方法来同步加载所依赖的模块,通过module.exports来导出需要暴露的接口。同步加载意味着会阻塞。nodeJS使用这种规范
  2. AMD:异步加载,所有依赖模块的语句,都定义在一个回调函数中,等到加载完成之后,回调函数才执行
    // 定义
    // define(id, [depends], callback)
    define('module', ['dep1', 'dep2'], function(d1,d2){});
    // 加载模块
    // require([module], callback)
    require(['module', '../app'], function(module, app){});
    
  3. CMD:异步加载,和AMD相似,一个模块就是一个文件
    /**
     * require:一个方法,接受模块标识作为唯一参数,用来获取其他模块提供的接口
     * exports:一个对象,用来向外提供模块接口
     * module:一个对象,上面存储了与当前模块相关联的一些属性和方法
     */
    define(function (require, exports, module) {
        var a = require('./a');
        a.doSomething();
        // 依赖就近书写,什么时候用到什么时候引入
        var b = require('./b');
        b.doSomething();
    });
    
  4. ES6模块化:使用import导入,export导出
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

1 participant