闭包、作用域、原型链

229 阅读8分钟

一、闭包

1、作用域的生命周期

  • 全局作用域:进入页面时创建,关闭页面时销毁
  • 局部作用域:函数执行时创建,执行完毕销毁

2、闭包

  • 定义:闭包是一个函数以及其捆绑的周边环境状态的引用的组合
  • 闭包与 函数、作用域、垃圾回收机制 息息相关
  • 分类
    • 无效闭包:函数嵌套;内部函数引用了外部函数的变量
          function outer(){
              var count = 0
              function inner(){
                      count++
                      console.log(count)
              }
              inner()
          }
          outer()  //1
          outer()  //1
          outer()  //1
      
    • 有效闭包:
      • 函数嵌套
      • 内部函数引用了外部函数的变量
      • 在函数外部保持对函数内部的引用和调用
       function outer(){                        function outer(){   
            var count = 0                           var count = 0
            function inner(){           =>          return function(){
                    count++                                count++
                    console.log(count)                     console.log(count)   
            }                                        }
           return inner                         }
        }
        var fn = outer()  //保持对函数的引用和调用
        fn()  //1
        fn()  //2
        fn()  //3
      
    • 优点
      • 在外部操作局部变量,延长了生命周期
      • 在外部知道函数内部的执行状态
      • 局部变量在函数执行后不会销毁,闭包是函数外部和内部的沟通桥梁
    • 缺点
      • 占用内存空间 ->尽量少使用闭包
      • 很容易造成内存泄漏 ->及时释放内存空间:fn = null

3、文档碎片

html文档中的Dom操作性能消耗很大,如果频繁操作DOM会带来页面的性能问题
  • 文档碎片(DocumentFragment)
    • 是一种轻量级的节点对象,是一种虚拟节点对象
    • 通常作为一个容器使用(空容器),不存在DOM树中
    • let fragment = document.createDocumentFragment()

二、作用域

1、变量提升

  • 声明提升机制: 在JS中函数声明或变量声明时,'声明'会提升到当前作用域顶部
    var a = 1;                               function test(){
    function test(){                                console.log(a); // undefined
        console.log( a );        =>                 var  a = 2;
        var a = 2;                                  console.log(a); // 2
        console.log( a );                        }    
    }                                        var a; 
    test();                                  a = 1; 
                                             test()

变量和函数的优先级:函数>变量

形参和实参的优先级

    var bb = 1;                           function aa(){...}
    function aa(bb) {                     var bb = undefined;
        bb = 2;                           bb =  1                         
        alert(bb);   //2     =>           aa(bb); //1
};                                        function aa(1){
    aa(bb);                                    bb = 1 -> 2                    
    alert(bb);                                 alert(bb);   //2    
                                           }
                                            alert(bb); //1

2、JS解析器 (为什么存在变量提升?

  • JS解析器:浏览器中有一套专门解析执行JS代码的程序就叫解析器(JS引擎)

  • 工作步骤:

    • 1.预解析代码
      • 主要工作扫描一些关键字 var function等,和解析参数(形成和实参)保存在内存中
      • 变量 形参 的默认值为 undefined
      • 函数预解析时值为函数本身
      • 当变量和函数重名时,函数优先
      • 当形参和局部变量重名时,形参优先
    • 2.逐行执行代码
      • 按照书写顺序从上到下依次执行
      • 在执行过程中,内存中的变量值随时可能被修改
        var a = 1;
        function test(){
            console.log( a );
            var a = 2;
            console.log( a );
        }
        test();
    
        //1.解析全局作用域                       //3.解析局部作用域  fn()
        {                                       {
            a:undefined; => 1                         a:undefined; => 2
            test:function (){                         arguments:[length:0]
                       console.log( a );        }
                       var a = 2;
                       console.log( a );
                      }
        }                                       
    
        //2.逐行执行代码                        //4.逐行执行代码 
            a = 1;                                    console.log( a );  //undefined
            test()                                    a = 2;
                                                      console.log( a );  //2
    

3、作用域链 (解释器工作原理

  • 定义

    • 作用域链简单讲,是多个作用域嵌套形成的由内到外的结构,用于查找变量(变量 函数 参数等)
    • 作用域链本质上是一个指向变量对象的指针列表
  • 执行上下文(执行环境)

    • 每一个执行上下文都包含 变量对象/活动对象 、arguments、this 、作用域链等信息
    • 在每一个段代码执行之前,都会先创建好这段代码的执行上下文
    • 进入页面时,浏览器解析到 <script> 标签时,JS引擎开始工作,先创建全局执行上下文
    • 当调用一个函数时,先创建该函数的执行上下文
  • 模拟执行环境

    • 执行上下文(执行环境) Excution Context -> EC
    • 变量对象 Variable Object -> VO
    • 活动对象 Activation Object -> AO
    • 作用域链 Scope Chain -> SC
        console.log( this );// window
        var x = 1;
        function test(a,b){
            var y = 2;
            console.log( a + b );
            console.log( x + y );
            console.log( this );
            console.log( abc );
        }
        test(3,4);
    
        //1.创建执行环境
        global_EC={  //创建全局执行环境
                VO:{  //变量对象 == window对象
                     x:undefined; => 1
                     test:function test(){...};
                   },
                this:windows,
                SC:[global_EC.VO]   //作用域链
           }  
        test_EC={   //函数执行环境
                AO:{    //活动对象
                        a:undefined;  => 3
                        b:undefined;  => 4
                        y:undefined;  => 2
                   },
                this:windows,
                arguments:[3,4;length:2;callee:test],
                SC:[ test_EC.AO , global_EC.VO ]  
           }
        //2.逐行执行代码
            console.log( this );  //window  (global_EC.VO
            x = 1;   // global_EC.x  undefined => 1
            test(3,4)
                y = 2;
                //作用域链查找,test_EC.AO -> global_EC.VO
                console.log( a + b ); // 3+4  
                console.log( x + y ); // 1+2
                console.log( this );  // window
                console.log( abc );   //报错
    

4、词法作用域(静态作用域)

  • 静态作用域:主要关注的是函数在哪定义,而不是关注它在哪调用
  • 动态作用域:主要关注的是函数在哪调用,而不是它在哪定义
        var a = 10;
        function fn1(){
            console.log( a );
        }
        function fn2(){
            var a = 20;
            fn1();
        }
        fn2();
    
        //1.创建执行环境
        golbal_EC={                           
              VO:{
                      a:undefined; => 10
                      fn1:function fn1(){...};
                      fn2:function fn2(){...};
                 },
              this:window,
              SC:[golbal_EC.VO]
             }     
        fn1_EC={                                fn2_EC={
              AO:{   },                             AO:{
              this:window,                          a:undefined; => 20
              SC:[fn1_EC.VO , golbal_EC.VO]         fn1:fuction fn1(){...}
             }                                     },
                                                    this:window,
                                                    SC:[fn2_EC.VO , golbal_EC.VO]  
                                                  }
                                              
    //2.逐行执行代码
       a = 10; undefined -> 10
       fn2()
           a = 20;
           fn1()
               console.log( a );  // 10  (fn1_EC.VO -> golbal_EC.VO
    

5、IIFE函数(立即执行函数)

    (function (){// 匿名函数
        console.log( '立即执行函数' );
    })();
  • 原理:
    • js中能立即被执行的只有表达式,普通函数被定义不会立即执行,而是等待被调用
    • 运算符避开了JS预解析,将函数声明转化成了表达式,使其可以立即执行
  • 多种写法
        +function (){...}();
        -function (){...}();
        !function (){...}();
        let result =( function (a,b){return a+b }(1,3) )
        console.log(result) 
    
  • 作用
    • 提供一个封闭的空间(局部作用域)
    • 不污染外部命名空间(局部作用域)

立即执行函数的变量提升

    var a = b = 10;                     var a = b : undefined
    (function () {                      a = b = 10
        var a = b = 20;     =>          (function () {      
    })();                                     var a  : undefined            
    console.log(b);                           a = b = 20  //var a=20(局部),b=20(全局)   
                                         })(); //IIFE避开js预解析,不提升(相当于fn())
                                         console.log(a); //10
                                         console.log(b); //20

三、原型链

1、构造函数:用于创建特定类型的对象

  • 对象的constructor属性,返回创建该对象的构造函数
        console.log( num.constructor );// Number
        var num = 123;                       =>   String
        var str = 'abc';                     =>   Number
        var boo = true;                      =>   Boolean
        var arr = [1,2,3];                   =>   Array
        var obj = {a:123};                   =>   Object
        var fun = function (){};             =>   Function
    

this指向:谁调用指向谁

  • 1.在全局作用域使用时,this指向window对象
  • 2.函数找不到调用对象时,this指向window对象
         //1.非严格模式
         let fn = function(){};  
         fn()   //this -> window
         //2.严格模式
         let fn = function(){};  
         fn()   //this -> undefined
    
  • 3.this所在的函数,谁调用该函数this就指向谁
        let obj={
            say:function(){console.log(this)}
        }
        obj.say()   //this  ->  obj
    
  • 4.在构造函数中,this指向创建出来的实例对象
        function Person(name,age){
            this.name=name;
            this.age=age     //this  -> 实例对象
        }
    
  • 5.事件处理函数中,this指向添加事件的dom对象
        btn.onclick = function (){
             console.log( this );// btn
        }
        btn.show() //this -> btn
    

改变this指向 : bind(),call(),apply()

      let obj1 = {
          name: 'obj1',
          fn: function (a, b) {
              console.log(this.name);
              return a+b
          }
      };
      let obj2 = {
          name: 'obj2',
      };
  • bindcall/apply 的区别:

    • bind不会执行前面的函数( ),返回一个新的函数
      • 若参数为基本数据类型,则自动包装成对象
      • 定时器必须用bind( )
        let res = obj1.fn.bind(obj2, 1, 2)(); //obj2 3
        console.log(res === obj1.fn); //false
      //若参数为基本数据类型,则自动包装成对象
        function fn (){console.log(this)}
        let num = 2
        fn.bind(num)() // Number {2}
      //定时器必须用bind()  不需要立即执行
        setTimeout (function (){...}.bind(obj,1,2),1000)
      
    • call/apply 会执行前面的函数,返回函数的结果
        let res = obj1.fn.call(obj2, 3, 4); //obj2 7
        let res = obj1.fn.apply(obj2,[5,6]); //obj2 11
      
  • callapply 的区别:

    • call 传参: 逗号隔开的参数列表 a,b,c
    • apply 传参: 数组 [a,b,c]

2、prototype原型(显式原型)

  • 每个函数(构造函数)都有一个prototype属性,属性值为一个对象,即原型对象
  • 当创建一个函数时,JS引擎自动为函数添加prototype属性
  • 非函数对象,没有prototype属性
  • 作用: 所有添加到原型对象上的属性方法,都将被函数的实例对象共享(继承)
        let fn = function(){};
        console.log(fn.prototype);  //{constructor: fn} 原型对象
        let num=123; //非函数对象     
        console.log( num.prototype ); // undefined
    
  • ECMA内置构造函数: Number String Array Boolean Object Function...
        console.log( Number.prototype );// {constructor: Number}
        console.log( String.prototype );// {constructor: String}
        console.log( Array.prototype  );// {constructor: Array }
        console.log( Object.prototype );// {constructor: Object}
    

3、__proto__原型(隐式原型)

  • 每个对象都有一个隐藏属性__proto__,指向创建它的构造函数的prototype属性
  • __proto__作用:用于维护原型链
    当我们访问对象的属性时,JS引擎优先在对象自身查找该属性,如果自身没有该属性,
    通过对象的__proto__属性向上层原型对象查找,一直到null
    
        let num=123;
        console.log( num.__proto__ === Number.prototype );// true
        function Human(name){
            this.name = name;
        };
        var zhangsan = new Human('张三');
        console.log( zhangsan.__proto__ ===  Human.prototype);// true
    

4、原型链

image.png

    let num = 123
    String.prototype.haha = '哈哈'
    Object.prototype.hehe = '呵呵'
    console.log(num.haha) // undefined
    console.log(num.hehe) //'呵呵'    
    //1.将基本数据类型包装成对应的object类型
    let _num = new Number(123);
    //2.通过对象访问属性
    _num -> _num._proto_ -> Number.prototype -> Number.prototype._proto_->
    Object.prototype -> Object.prototype._proto_ -> null

原型链.jpg

作者制定的四条路线

  • Function._proto_ = Function.prototype

  • Object._proto_ = Function.prototype

    原因:ObjectFunction作为函数,应该能使用bind()...等方法
    
  • Object.prototype._proto_ === null

    原因:避免对象原型链无限循环 
    
  • Function.prototype._proto_ = Object.prototype

    原因 :Function.prototype的值是一个函数,在JS中函数也是对象
          对象都应该能访问Object.prototype原型对象上的属性方法
    console.log( Function.prototype );// ƒ () { [native code] } 函数
    

原型链案例

    function fn(name, age) {
        this.name = name;
        this.age = age;
    }
    fn.prototype.sayHello = function() {
      console.log('Hello, my name is ' + this.name);
    };
    let f1 = new fn('Bob', 25); //实例对象
  // 原型链
  /* f1 -> f1._proto_ -> fn.prototype -> fn.prototype._proto_ ->
     Object.prototype -> Object.prototype._proto_ -> null
     
     Object._proto_ === Function.prototype
     Object.__proto__.constructor === Function
     Object.prototype.constructor === Object
     fn.__proto__ === Function.prototype
  */   
  // f1找不到Function.prototype
  
    Function.prototype.a = function(){
      console.log('a...');
    }
    Object.prototype.b = function(){
      console.log('b...');
    }
    f1.b() //b...
    f1.a() //报错