闭包
变量作用域
变量根据作用域的不同分为两种:全局变量和局部变量。
- 函数内部可以使用全局变量
- 函数外部不可以使用局部变量
- 当函数执行完毕,本作用域内的局部变量就会销毁
如何产生闭包:
当一个嵌套的内部(子)函数引用了嵌套的外部(父)函数的变量(函数)时,就产生了闭包
闭包到底是什么
闭包是有权访问另一个函数作用域中变量的函数,简单理解就是一个作用域可以访问另外一个函数内部的局部变量。
闭包:我们fun这个函数作用域访问了另一个函数fn里面的局部变量num,被访问的变量所在的函数称为闭包函数,因此fn1就称为闭包函数。
function fn1(){
var a = 2;
function fn2(){
console.log(a);
}
return fn2;
}
fn1()
理解一:闭包是嵌套的内部函数
理解二:包含被引用变量(函数)的对象
注意:闭包存在于嵌套的内部函数中。
产生闭包的条件
- 函数嵌套
- 内部函数引用了外部函数的数据(变量/函数)
- 执行外部函数(只有执行外部函数才可以执行内部函数定义,执行内部函数定义就会产生闭包。注意:只是执行定义,不是调用)
常见的闭包
1.将一个函数作为另一个函数的返回值。
fn1外面的作用域访问了fn1内部的作用域。
function fn1() {
var a = 2;//在这个时候闭包已经产生了,因为有函数提升
function fn2(){
a++;
console.log(a)
}
return fn2;
}
var f = fn1();//这个时候产生闭包,因为执行了fn2的函数定义
f();//3
f();//4 第二次执行f()时,闭包还是存在的
在这个过程中,虽然调用了两次,但是只产生了一次闭包,因为执行内部函数定义就会产生闭包,而我们定义了一次内部函数;如果想多次产生闭包,只需要多次调用外部函数即可,因为每次调用外部函数都会执行内部函数定义,就会产生闭包。
闭包中就只有a这一个变量,而fn2不在闭包中,说明被释放掉了,但是由于var f = fn1()
,用一个变量f指向了fn2所对应的函数对象,而函数对象关联闭包(闭包中有变量a),return 的是fn2,但实际上return 的是fn2保存的地址值,所以,fn2这个变量被释放掉了,但是并不代表fn2对应的函数对象成为垃圾对象,因为它被f引用了。
因此,函数执行完成后,函数内部声明的局部变量一般是不存在的,但是存在于闭包中变量是可能存在的;在函数外部一般不能直接访问函数内部的变量,但是通过闭包是可以让外部操作内部数据的。
一般直接return一个匿名函数即可
function fn1() {
var a = 2;
return function(){
a++;
console.log(a)
};
}
var f = fn1();
f();//3
f();//4
如果没有闭包 var f = fn1()执行完以后,a就自动释放了,在调用f时就会报错;
向外部暴露的是function这个匿名函数
函数执行完以后a依然存在,也就是说var f = fn1()
执行完毕之后,a依然存在,因为它在闭包中
2.将函数作为实参传递给另一个函数调用
msg作为了引用变量,闭包中有msg没有time(闭包中包含被引用的变量)
function showDelay(msg,time){
setTimeout (function(){
alert(msg);
},time)
}
showDelay('ss',2000)
闭包的作用
主要作用:延伸了变量的作用域(使函数内部的变量在函数执行完后,仍然存活在内存中;让函数外部可以操作(读写)到函数内部的数据)
在上述例子中只有等到所有函数都调用完毕之后,引用的变量才会销毁;也即f执行完毕后才会销毁
闭包的案例
案例一
<body>
<button>测试1</button>
<button>测试2</button>
<button>测试3</button>
<script>
var btns = document.getElementsByTagName('button');
for(var i = 0,length = btns.length;i<length;i++) {
//循环了3次,相当于创建了3个立即执行函数
(function (i) {//这里的i是局部变量,接受全局变量传过来的i
btns[i].onclick = function () {//这里访问了立即执行函数的局部变量i,因此闭包产生
alert('第'+(i+1)+'个');
}
})(i)//这里的i使用的全局变量
}
</script>
</body>
立即执行函数也称为小闭包,因为立即执行函数里面的任何一个函数都可以使用它的i这一变量。
但是闭包不一定就是最优的,比如上述例子中,匿名函数被创建了3次,内存空间占用更多,而且一般来说立即函数执行完毕后,变量就会立即销毁,但是里面有个点击事件使用到了这个i,因此必须要等到点击事件结束后才会销毁,如果一直不点击,就一直不会销毁,就会特别占用内存。甚至会造成内存泄漏。
案例二
<body>
<ul class="nav">
<li>榴莲</li>
<li>臭豆腐<li>
<li>鲱鱼罐头</li>
<li>大猪蹄子</li>
</ul>
<script>
//3秒后打印所有li元素内容
var lis = document.querySelector('.nav').querySelectorAll('li');
for(var i = 0;i<lis.length;i++){
(function (i) {
setTimeout(function () {
console.log(lis[i].innerHTML)
},3000)
})(i)
}
//定时器里的任务也是个异步任务,
</script>
</body>
案例三
<script>
//打车起步价13(3公里内),之后每多一公里增加5块钱,用户输入公里数就可以计算打车价格
//如果有拥堵情况,总价格多收取10快钱的拥堵费
// function fn() {
//
// }
// fn()
var car = (function () {
var start = 13;
var total = 0;
return {
price:function (n) {
if(n<=3){
total = start;
}else{
total = (n-3)*5 + start
}
return total;
},//正常的总价
yd:function (flag) {
return flag ? total+10:total;
}//拥堵的费用
}
})();
car.price(5);
car.yd(true);
</script>
闭包的生命周期
产生:在嵌套内部函数定义执行完成时就产生了
死亡:在嵌套的内部函数成为垃圾对象时
function fn1() {
var a = 2;
return function(){
a++;
console.log(a)
};
}
var f = fn1();
f();//3
f();//4
f = null//闭包死亡
包含闭包的函数对象成为垃圾对象,之所以成为垃圾对象就是引用它的变量不在引用它了。
闭包的缺点及解决
缺点
- 函数执行完成后,函数内的局部变量没有释放,占用内存时间会变长
- 容易造成内存泄漏
function fn1(){
var arr = new Array[100000];
function fn2() {
console.log(arr.length);
}
return fn2;
}
var f = fn1();
f()
//解决方法
f = null
因为f一直在,因此闭包就一直存在,闭包会导致arr数组一直存在,没有被释放。
解决方法:让内部函数成为垃圾对象,进而去回收闭包
解决
- 能不用闭包就不用闭包
- 及时释放
内存溢出和内存泄露
内存溢出;
- 一种程序运行出现的错误
- 当程序运行需要的内存超过了剩余的内存时,就会抛出内存溢出的错误
var obj = {};
for(var i = 0;i<1000;i++){
obj[i] = new Array(100000000)//说明obj这个对象中的属性都是0,1,3....999这样的数字,相当于一个伪数组
}
内存泄露
- 占用内存没有及时释放
- 内存泄露积累多了就容易导致内存溢出
常见的内存泄露
- 意外的全局变量
function fn(){
a = 3;
console.log(a);
}
fn()
正常来说,如果a是一个局部变量,fn已调用完以后,局部变量a就会被释放;但是现在a却没有被释放
- 没有及时清理的计时器或回调函数
setInterval(function(){//启动循环定时器后不清理
console.log('----')
},1000)
- 闭包
练习题
var name = 'The Window';
var object = {
name:'My Object',
getNameFunc : function(){
return function(){
return this.name
}
}
}
alert(object.getNameFunc()());
结果:The WIndow
解析:object.getNameFunc()调用以后得到一个函数function(){return this.name}
;执行该函数得到this.name
。直接执行函数,函数体里的this是window,因此为The WIndow。没有闭包
var name = 'The Window';
var object = {
name:'My Object',
getNameFunc : function(){
var that = this;
return function(){
return that.name
}
}
}
alert(object.getNameFunc()());
结果:My Object
解析:有闭包。object.getNameFunc()调用以后得到一个函数function(){return that.name}
;在执行该函数,这个函数中的this也是window,但是我们用的是that,that是object调用getNameFunc()时的this,这个时候谁调用,谁就是this,因此外层this是object;所以相当于return object.name
function fun(n,o){
console.log(o);
return {
fun:function(m){
return fun(m,n);
}
}
}
var a = fun(0); a.fun(1); a.fun(2); a.fun(3);//undefined 0 0 0
var b = fun(0).fun(1).fun(2).fun(3);//undefined 0 1 2
var c = fun(0).fun(1);c.fun(2);c.fun(3);//undefined 0 1 1
调用外部函数时才会产生新的闭包,调用内部函数时不会产生新的闭包,不产生新的闭包,闭包里的数据就不会变。
这道题是因为调用了n,因此才会产生闭包,之后又将n传给了o
var a = fun(0);此时n=0;o=undefined;输出undefined;返回function(m){ return fun(m,n);}
a.fun(1):m=1 返回function(m){ return fun(m,n);}
闭包仍然存在,因此n=0;所以o = 0;输出0
a.fun(2):m=2 n=0 o=0;
a.fun(3): m=3 n=0 o=0;
var b = fun(0).fun(1).fun(2).fun(3);
对象创建模式
构造函数
先创建Object空对象,在动态的添加属性、方法。
var p = new Object();
p.name = 'Tom';
p.age = 12;
p.setName = function (name){
this.name = name;
}
使用场景:起始时不确定对象内部数据
缺点:语句太多
对象字面量
使用{}创建对象,同时指定属性、方法
var p = {
name : 'Tom';
age : 12;
setName : function (name){
this.name = name;
}
}
适用场景:起始时对象内部数据是确定的
问题:如果创建多个对象,有重复代码
工厂模式
通过工厂函数动态创建对象并返回
工厂函数:返回一个对象的函数都能称为工厂函数
function createPerson(name,age) {
var obj = {
name: name,
age: age,
setName : function (name){
this.name = name;
}
}
return obj;
}
var p1 = createPerson('Tom',12);
var p2 = createPerson('Bob',18);
使用场景:需要创建多个对象
问题:对象没有一个具体的类型,都是object
自定义构造函数
自定义构造函数,通过new创建对象
function Person(name,age) {
this.name = name;
this.age = age;
this.setName = function (name) {
this.name = name;
}
}
var p1 = new Person('Tom',12);
var p2 = new Person('Jack',24);
p1.setName('Jack');
console.log(p1 instanceof Person);
console.log(p1,p2);
function Student(name,price) {
this.name = name;
this.price= price;
}
var s = new Student('Bob',13000)
console.log(s instanceof Student);
使用场景:需要创建多个类型确定的对象
问题:每个对象都有相同的数据(方法),浪费内存
构造函数+原型的组合模式
自定义构造函数,属性在函数中初始化,方法添加到原型上
function Person(name,age) {
this.name = name;
this.age = age;
}
Person.prototype.setName = function (name) {
this.name = name;
}
var p1 = new Person('Tom',12);
var p2 = new Person('Jack',24);
console.log(p1,p2);
原型链继承
方式一:子类型的原型为父类型的一个实例对象
//父类型
function Supper(){
this.supProp = 'Supper property'
}
Supper.prototype.showSupperProp = function(){
console.log(this.supProp)
}
//子类型
function Sub(){
this.subProp = 'Sub property'
}
//关键:子类型的原型为父类型的一个实例对象
Sub.prototype = new Supper();
//让子类型的原型的constructor指向子类型。
Sub.prototype.constructor = Sub
Sub.prototype.showSubProp = function(){
console.log(this.supProp)
}
var sub = new SUb()
sub.showSupperProp()
sun.toString()//sub.proto = Sub.prototype;Sub.prototype.proto = Object.prototype
console.log(sub.constructor);//查看这个实例对象sub他的构造函数是谁
查看这个实例对象sub他的构造函数是谁console.log(sub.constructor);
打印出来不是Sub而是Supper。constructor这个属性存在于原型对象中,原型对象中,实例对象也能看见,但是此时因为Sub.prototype = new Supper();
,所以原型对象为Supper的实例。故而,sub的构造函数是Supper,这样是不符合常理的。因此,使用Sub.prototype.constructor = Sub
将构造函数修改为Sub
方式二:借用构造函数继承(实际没有继承)
在子类型构造函数中通用call()调用父类型构造函数
function Person(name,age) {
this.name = name;
this.age = age;
}
function Student(name,age,price) {
Person.call(this,name,age)//相当于this.Person(name,age)
//this.name = name;
//this.age = age
this.price = price;
var s = new Student('Tom',20,14000);
console.log(s.name,s.age,s.price)
}
方式三:组合继承
function Person(name,age) {
this.name = name;
this.age = age;
}
Person.prototype.setName = function(name){
this.name = name
}
function Student(name,age,price) {
//call方法为了初始化属性
Person.call(this,name,age)//相当于this.Person(name,age)
//this.name = name;
//this.age = age
this.price = price;
}
//真正的产生继承
Student.prototype = new Person();
//修正constructor属性
Student.prototype.constructor = Student
Student.prototype.setPrice = function (price) {
this.price = price;
}
var s = new Student('Tom',24,15000);
s.setName('Bob');
s.setPrice(16000);
console.log(s.name,s.age,s.price);//Bob 24 16000
方法在原型上,call是为了获得父类的属性,继承获得父类的方法