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
作用域链
绘制作用域链的规则
将这个script标签的全局作用域定义为0级作用域链,将全局作用域上的所有数据(变量、对象、函数),绘制在这条链上
由于在词法作用域中,只有函数可以分割作用域,那么只要遇到函数就再引申出新的作用域链,级别为当前链级别+1,将数据绘制到新链上
重复步骤二,直到没有遇到函数为止
以下面的函数举例来绘制作用域链:
var n = 123;
function f(){
var n = 12;
function f1(){
var n = 1;
function f2(){
var n = 0;
}
function f3(){
var n = 0;
}
}
}
变量的搜索原则
当访问一个变量时,首先在当前变量所处的作用域上查找,如果找到就直接使用,并停止查找
如果没有找到就向上一级链(T-1)上去查找,如果找到就直接使用并停止查找
如果没有找到就继续向上一级链查找,直到0级链
如果没有找到就报错
如果访问的变量不存在,会搜索整个作用域链(不仅性能低,而且抛出异常)
在实际开发不推崇所有数据都写在全局上。尽量使用局部变量,推荐使用沙箱。
如果在开发中,所有js变量都写在全局上,会造成全局污染
- 同级别的链上的变量互不干扰
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实现使用的都是标记清除的垃圾回收策略,只不过时间间隔不相同。
沙箱
变量不写在全局上,但又想达到写在全局的目的,就用沙箱
特点:
能分割作用域,不会污染全局(函数)
在分割后的作用域的内部的代码要自执行。(匿名函数)
// 结构:
(function(){
//代码块
}());
// 经典的沙箱模式:
var n = 2
(function () {
// 这个n不会污染外部的n。所以这样能保证自己的代码安全执行(别人也污染不了我),也不会污染全局变量或其他作用域的变量
var n = 1;
function foo () {
console.log(n);
}
//window.fn 相当于设定了一个全局变量
window.fn = foo;
}())
fn();