面向对象三、作用域


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变量都写在全局上,会造成全局污染

  6. 同级别的链上的变量互不干扰

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();
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值