JS笔记(二)

本文详细解释了闭包的概念,包括变量作用域、闭包的形成条件(嵌套函数和引用外部变量)、常见应用场景(如返回函数和参数传递)以及其作用和优缺点。还提供了内存管理的注意事项和解决方法,以及实例分析和编程练习题解答。
摘要由CSDN通过智能技术生成

闭包

变量作用域

变量根据作用域的不同分为两种:全局变量和局部变量。

  • 函数内部可以使用全局变量
  • 函数外部不可以使用局部变量
  • 当函数执行完毕,本作用域内的局部变量就会销毁

如何产生闭包:

当一个嵌套的内部(子)函数引用了嵌套的外部(父)函数的变量(函数)时,就产生了闭包

闭包到底是什么

闭包是有权访问另一个函数作用域中变量的函数,简单理解就是一个作用域可以访问另外一个函数内部的局部变量。
闭包:我们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是为了获得父类的属性,继承获得父类的方法

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值