美文网首页
面向对象三、作用域

面向对象三、作用域

作者: Gary23 | 来源:发表于2017-07-03 10:16 被阅读0次

    title: 面向对象三、作用域
    date: 2017-06-17 10:10:13
    tags: javascript笔记


    instanceof 运算符

    语法

    object instanceof fn

    如果运算符后面的函数的prototype属性引用的对象出现在运算符面前对象的原型链上的话就返回true,否则返回false。

    function foo (){
    
    }
    var f = new foo;
    console.log(f instanceof foo);    // 返回true  判断f是不是foo函数的实例
    console.log(f instanceof Object);    // 返回true  f也是在Object的原型链上
    
    function fn (){
    
    }
    foo.prototype = new fn;
    var ff = new foo;
    console.log(ff instanceof fn)     // true  因为fn创建的对象就是foo.prototype,所以foo.prototype的原型就是fn.prototype。
    console.log(ff instanceof Object)     // true
    //ff -> foo.prototype -> fn.prototype -> Object.prototype -> null
    

    作用域链

    绘制作用域链的规则

    1. 将这个script标签的全局作用域定义为0级作用域链,将全局作用域上的所有数据(变量、对象、函数),绘制在这条链上

    2. 由于在词法作用域中,只有函数可以分割作用域,那么只要遇到函数就再引申出新的作用域链,级别为当前链级别+1,将数据绘制到新链上

    3. 重复步骤二,直到没有遇到函数为止

    以下面的函数举例来绘制作用域链

    var n = 123;
    function f(){
      var n = 12;
      function f1(){
        var n = 1;
        function f2(){
          var n = 0;
        }
        function f3(){
          var n = 0;
        }
      }
    }
    
    image

    变量的搜索原则

    1. 当访问一个变量时,首先在当前变量所处的作用域上查找,如果找到就直接使用,并停止查找

    2. 如果没有找到就向上一级链(T-1)上去查找,如果找到就直接使用并停止查找

    3. 如果没有找到就继续向上一级链查找,直到0级链

    4. 如果没有找到就报错

    5. 如果访问的变量不存在,会搜索整个作用域链(不仅性能低,而且抛出异常)

    • 在实际开发不推崇所有数据都写在全局上。尽量使用局部变量,推荐使用沙箱。

    • 如果在开发中,所有js变量都写在全局上,会造成全局污染

    1. 同级别的链上的变量互不干扰
    function f (a){
      var a ;
      function a (){
        console.log(a);
      }
      a();
      a = 10;
      console.log(a);
    }
    f(100);
    // 在这个题中 var a 不会覆盖a的参数100,但是function会改变,a=10这个赋值操作也会覆盖,因为都相当于赋值。
    
    

    补充

    在函数执行时候,会创建一个执行的环境,这个环境包括:activeObject(活动对象)以及作用域链

    activeObject存储的是所有在函数内部定义的变量,以及函数的形参;

    会将变量名字以及形参名字作为该对象的属性来存储,比如有个变量a,那么就等于有了a这个属性,这时a的属性值就是100;

    因为之前已经传了a这个参数,传了参数也相当于在函数内声明了a这个变量,也就是说此时在activeObject中已经有了a这个属性,所以这时在函数内声明a就不管用了,只有赋值才管用。只能改属性值但属性不会再创建。上述代码先将函数赋值给了a,又将100赋值给了a

    查找对象也是在activeObject中查找,也就是查找里边的属性和属性值,没有的话就找上一级函数的activeObject。直到找到为止,没有找到就报错。

    闭包

    定义

    • 指一个函数有权去访问另一个函数内部的参数和变量。

    • 创建闭包的最常见的方式就是在一个函数内创建另一个函数,通过另一个函数访问这个函数的局部变量。

    • 应用闭包主要是为了设计私有的方法和变量。

    • 一般函数执行完毕后,局部活动对象就被销毁,内存中仅仅保存全局作用域,但是闭包的情况不同,不会被垃圾回收机制回收。

    • 为了防止闭包导致的内存泄漏,用完闭包之后手工赋值为null,就会被回收。

    • 闭包结构和闭包引用写在同一个函数里,出了函数就自动删除该缓存了。

    缺点

    • 闭包会造成函数内部的数据常驻内存,会增大内存使用量,从而引发内存泄漏问题。每创建一个闭包都会创建一个缓存数据,这样就会造成内存泄漏(内存满了后其他数据写不进去)

    • 闭包会使变量始终保存在内存中,如果不当使用会增大内存消耗。

    
    function fn (){
      var n = Math.random();
      function getN (){
        return n;  // 这个作用域中没有n所以会向上寻找。
      }
      return getN;  //这里是要返回整个getN函数,所以不加括号。
    }
    
    var ff = fn();  // 这个ff就是闭包,通过它可以访问fn内部的数据。
    var nn = ff();
    var mm = ff();  // fn()实际上是getN这个函数体,那么ff()就是调用了getN这个函数,这样会返回n。
    console.log(nn);
    console.log(mm);  // nn和mm的数是相同的
    console.log(nn === mm); //true,
    
    ff = null; // n被回收
    
    

    优点

    • 希望一个变量长期驻扎在内存中

    • 避免全局变量的污染

    • 私有成员的存在

    闭包的应用

    下面通过几个案例来了解闭包的优点:

    统计某个构造函数创建多少个对象,变量可以长驻内存

    //统计某个构造函数创建多少个对象
    function counter() {
      var n = 0;
      return {
        add:function(){
            n+=1;
        },
        getCounts:function(){
            return n;
        }
      }
    }
    
    // 创建一个闭包,相当于初始化计时器,因为重新调用会让n=0.
    // 然后创建闭包时,n=0和return的对象会被缓存。
    // 那么为什么闭包环境能缓存数据呢:
    // 因为 var n = 0相当于n进入环境,在局部作用域创建了一个对象和n 最后把对象和n返回给外部作用域,相当于已出执行环境,通过全局变量就能找到返回的对象,通过返回的对象就能找到n,通过这个路径就能找到变量n,
    // 所以得出结论因为在函数内部有方法(函数)对其有引用,并且又将其返回到外部作用域上的一个变量接收。创建之后就缓存了,这时再通过这个变量访问闭包里的环境,那么只会访问该变量的缓存区域。
    var PresonCount = counter();
    
    function Preson(){
      PresonCount.add();
    }
    
    //用Preson这个构造函数创建对象,每创建一次都相当于调用了一次该构造函数。
    var p = new Preson()
    var p1 = new Preson()
    var p2 = new Preson()
    var p3 = new Preson()
    console.log(PresonCount.getCounts());       // 打印4
    
    

    局部变量的累加,怎样做到变量a即是局部变量又可以累加

    // 1、全局变量
    var a = 1
    function abc(){
      a++
      console(a)
    }
    
    abc()  // 2
    abc()  // 3
    // 可以累加但问题是a是全局变量  容易被污染
    
    // 2、局部变量
    function abc () {
      var a = 1;
      a++;
      console(a);
    }
    abc() // 2
    abc() // 2
    // 放到局部里又不能累加,因为每次执行函数都相当于把a重新声明
    
    // 3、局部变量的累加
    function outer () {
      var a = 1;
      return function () {
        a++;
        console.log(a);
      }
    }
    
    var y = outer();
    y()  // 2
    y()  // 3
    // 这样即实现了累加,又能把变量a藏起来。
    

    模块化代码,减少全局变量的污染。a是局部变量,全局变量有a也没关系

    var abc = (function () {
      var a = 1;
      return function(){
        a++
        console(a)
      }
    }());   // 函数在这里自调用一次,所以abc得到的是abc里返回的函数
    
    abc();  // 2
    abc();  // 3
    

    函数的私有成员调用

    var aaa = (function(){
      var a = 1;
      function bbb(){
        a++;
        console.log(a);
      }
      function ccc(){
        a++;
        console.log(a);
      }
      return {
        b:bbb,
        c:ccc     // json格式,也就是返回一个对象。b是bbb的函数体
      }
    }());    // 自调用一下,这样aaa就是函数体内的返回值,也就是那个json格式的对象
    
    aaa.b();  //2
    aaa.c();  //3
    
    

    在循环中直接找到对应元素的索引

    //这是以前的写法
    var lis = document.getElementsByTagName('li');
    for (var i = 0; i < lis.length; i++) {
      lis[i].onclick = function(){
      console.log(i);   // 由于进入函数时i已经循环完毕,所以i变为常量4
    }
    
    // 用闭包的方式来写
    for (var i = 0; i < lis.length; i++) {
      (function(i){
        lis[i].onclick = function(){
          console.log(i);   
        }
      }(i))  //在这里调用一次,将i作为参数传进去,这时里边的i就不会是执行完之后的i值
    }
    

    内存泄漏问题

    由于IE的js对象和DOM对象使用不同的垃圾收集方法,因此闭包在IE中会导致内存泄露问题,也就是无法销毁驻留在内存中的元素

    function closure(){
      var oDiv = document.getElementById('oDiv');    //用完之后会一直待在内存中
      var test = oDiv.innerHTML;
      oDiv.onclick = function () {
        alert(test);    // 这里用oDiv导致内存泄漏
      };
      oDiv = null;    //最后应该将oDiv解除来避免内存泄漏
    }
    

    多闭包结构

    像上边的案例只需要一个n的值一个闭包就可以解决,而很多时候需要返回的变量大于1。

    如下需要访问函数内部的多个变量n和m,就需要多个闭包。闭包的实质就是一个函数。

    function foo(){
      var n = 1,m = {age:20}; // n是变量,m是对象
      function getN(){
        return n;   
      }
      function getM(){
        return m;
      }
      return {getM:getM,getN:getN}; // :前的是属性名,:后的是属性值也就是函数体。
    }
    
    var obj = foo(); // 这就是一次闭包
    obj.getM().age = 22;
    console.log(obj.getM().age);    // 22
    console.log(obj.getN());    // 1
    
    var obj1 = foo(); // 这是第二次闭包,每闭包一次就是重新调用一次。不会被上次obj闭包调用并且更改属性值而改变函数本身的值,这和原型的不可变特性比较像。
    
    console.log(obj1.getM().age);   // 20
    
    

    对象的私有属性

    // 用下面这个案例来说明构造函数的问题。
    function Preson (name,age) {
      this.name = name;
      this.age = age;
    }
    // 这是创建对象并且传参姓名
    var xiaohong = new Preson("小红",20)
    // 这时如果一不小心,就能随意将姓名改成小绿了。
    xiaohong.name = "小绿"
    
    // ---------------------------------------------------------------------------------------
    
    // 为了解决这个问题,可以用这种写法
    function Preson (name,age) {
      return {
        getName:function(){
            return name;
        },
        getAge:function(){
            return age;
        }
        // name通常不能更改,但是age 可以改,给了这样一个接口就可以直接改了
        setAge:function(val){
            age = val;
        }
      }  
    }
    // 还是创建对象并且传参
    // 这样就没法随意更改了,除非更改构造函数的函数。
    var xiaohong = new Preson("小红",20)
    
    xiaohong.serAge(19);
    xiaohong.getAge();     // 先传一个参数19,让age改为19.再调用一下getAge函数。就将年龄属性改为了19
    
    
    // 但是还有个问题,那就是通过下面的语句可以创建一个name的属性。这样也是不太好的
    xiaohong.name = "小绿"
    //通过下面这个属性可以解决。但是要写在上面创建属性的语句的前面
    Object.preventExtenions(xiaohong)
    xiaohong.name = "小绿"  
    console.log(xiaohong.name)  // 这时就返回undefined了。
    
    

    用闭包来解决递归函数性能问题

     // 利用闭包可以缓存数据的特性,改善递归性能
     // 这个函数是为了缓存
    var fib = (function() {
      var cache = [];
      // 这个函数是求fib的第n项值
      return function(n) {
        if (n < 1) {
          return undefined;
        }
        // 1、看缓存里有没有
        // 如果有,直接返回值
        if (cache[n]) {
          return cache[n]
        } else
        // 如果没有重新计算
        if (n === 1 || n === 2) {
          cache[n] = 1;
        } else {
          cache[n] = arguments.callee(n - 1) + arguments.callee(n - 2);
        }
        return cache[n];
      }
    }())
    
    console.log(fib(10));
    
    

    垃圾回收机制

    定义

    GC(Garbage Collection),专门负责一些无效的变量所占有的内存回收销毁。

    原理

    垃圾收集器会定期(周期性)找出那些不在继续使用的变量,然后释放其内存。但这个过程不是实时的,因为其开销比较大,所以垃圾回收器会照固定的时间间隔周期性的执行。

    为什么闭包会造成内存常驻,并且让垃圾回收机制不能回收

    不再使用的变量(生命周期结束的变量),当然只可能是局部变量,全局变量的生命周期直至浏览器卸载页面才会结束。局部变量只在函数的执行过程中存在,而在这个过程中会为局部变量在栈或堆上分配相应的空间,以存储它们的值,然后在函数中使用这些变量,直至函数结束,而闭包中由于内部函数的原因,外部函数并不能算是结束。

    function fn1 () {
      // body...
      var obj = {
        name:'tom',
        age:20
      }
    }
    
    function fn2 () {
      // body...
      var obj = {
        name:'tom',
        age:20
      }
      return obj
    }
    var a = fn1();
    var b = fn2();
    

    当fn1被调用时,进入fn1环境,会开辟一块内存存放对象obj,而当调用结束后,出了fn1的环境,那么该块内存会被js引擎中的垃圾回收器自动释放,

    而在fn2被调用的过程中,返回的对象被全局变量b所指向,所以该块内存并不会被释放。那么问题出现了:到底哪个变量是没有用的?所以垃圾收集器必须跟踪到底哪个变量没用,对于不再有用的变量打上标记,以备将来收回其内存。用于标记的无用变量的策略可能因实现而有所区别,通常情况下有两种实现方式:计数清除和引用清除。

    引用计数法

    跟踪记录每个值被引用的次数,如果一个变量被另外一个变量引用了, 那么该变量的引用计数+1,如果同一个值又被赋值给另一个变量,则引用次数再+1。相反,当这个变量不再引用该变量时,这个变量的引用计数-1;GC会在一定时间间隔去查看每个变量的计数,如果为0就说明没有办法再访问这个值了就将其占用的内存回收。

    function test () {
      var a = {};   // a的引用次数为0
      var b = a ;   // a的引用次数+1,为1
      var c = a ;   // a的引用次数再+1, 为2
      var b = {}    // a的引用次数减1,为1
    }
    

    引用计数的缺点

    function test () {
      var a = {};
      var b = {};
      a.pro = b;
      b.pro = a;
    }
    

    以上代码a和b的引用次数都是2,fn()执行完毕后,两个对象已经离开环境,在标记清除方式下是没问题,但在引用计数策略下,因为a和b的引用次数不为0,所以不会被垃圾回收器回收内存,如果fn函数被大量调用,就会造成内存泄漏。只能手动让a和b=null才能被识别并回收

    window.onload=function outerFunction(){
      var obj = document.getElementById("element");
      obj.onclick=function innerFunction(){};
    };
    

    这段代码看起来没什么问题,但是obj引用了document.getElementById("element")而document.getElementById("element")的onclick方法会引用外部环境值中的变量,自然也包括obj。
    解决办法:自己手工解除循环引用。

    window.onload=function outerFunction(){
      var obj = document.getElementById("element");
      obj.onclick=function innerFunction(){};
      obj = null;
    };
    

    将变量设置为null意味着切断变量与它此前引用的值之间的连接。当垃圾回收器下次运行时,就会删除这些值并回收它们占用的内存。

    标记清除法

    从当前文档的根部(window对象)找一条路径,如果能到达该变量,那么说明此变量有被其他变量引用,也就说明该变量不应该被回收掉,反之,应该被回收其所占的内存

    当变量进入某个执行环境(例如,在函数中声明一个变量),那么给其标记为“进入环境”,此时不需要回收,但是如果上述执行环境执行完毕,便被销毁,那么该环境内的所有变量都被标记为“已出环境”,如果被标记为已出环境,就会被回收掉其占用的内存空间。

    function test() {
      var a = 10;  // 被标记,进入环境
      var b = 20;  // 被标记,进入环境
    }
    test()   // 执行完毕后,a,b被标记离开环境,被回收。
    

    垃圾回收器在运行时会给存储在内存中的所有变量都加上标记,然后,它会去掉环境中的变量以及环境中的变量引用的变量的标记(闭包)。而在此之后再被加上标记的变量将被视为准备删除的变量,原因是环境中的变量已经无法访问到这些变量了。最后,垃圾回收器完成内存清除工作,销毁那些带标记的值并回收它们所占用的内存空间。目前IE,Firefox,Opera,Chrome,Safari的js实现使用的都是标记清除的垃圾回收策略,只不过时间间隔不相同。

    沙箱

    变量不写在全局上,但又想达到写在全局的目的,就用沙箱

    特点:

    1. 能分割作用域,不会污染全局(函数)

    2. 在分割后的作用域的内部的代码要自执行。(匿名函数)

    // 结构:
    (function(){
      //代码块
    }());
    
    // 经典的沙箱模式:
    var n = 2
    (function  () {
      // 这个n不会污染外部的n。所以这样能保证自己的代码安全执行(别人也污染不了我),也不会污染全局变量或其他作用域的变量
      var n = 1;
      function foo () {
        console.log(n);
      }
      //window.fn 相当于设定了一个全局变量
      window.fn = foo;
    }())
    fn();
    

    相关文章

      网友评论

          本文标题:面向对象三、作用域

          本文链接:https://www.haomeiwen.com/subject/kajacxtx.html