面向对象三、作用域


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();
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 202,607评论 5 476
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,047评论 2 379
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 149,496评论 0 335
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,405评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,400评论 5 364
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,479评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,883评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,535评论 0 256
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,743评论 1 295
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,544评论 2 319
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,612评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,309评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,881评论 3 306
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,891评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,136评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,783评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,316评论 2 342

推荐阅读更多精彩内容