闭包
关于作用域的问题
var n = 999;
function f1() {
console.log(n);
}
f1() // 999
函数内部可以直接读取全局变量,函数 f1 可以读取全局变量 n。
但是,在函数外部无法读取函数内部声明的变量。
function f1() {
var n = 99;
}
f1();
console.log(n);
有时我们却需要在函数外部访问函数内部的变量;
正常情况下,这是办不到的,只有通过变通方法才能实现。
那就是在函数的内部,再定义一个函数。
function f1() {
var n = 999;
var f2 = function() {
console.log(n);
}
return f2;
}
var f = f1();
f();
上面代码中,函数f2就在函数f1内部,这时f1内部的所有局部变量,对f2都是可见的。
但是反过来就不行,f2内部的局部变量,对f1就是不可见的。
这就是JavaScript语言特有的”链式作用域”结构(chain scope),子级会一层一层地向上寻找所有父级的变量。
所以,父级的所有变量,对子级都是可见的,反之则不成立。
既然f2可以读取f1的局部变量,那么只要把f2作为返回值,我们不就可以在f1外部读取它的内部变量了吗!
闭包就是函数f2,即能够读取其他函数内部变量的函数。
由于在JavaScript语言中,只有函数内部的子函数才能读取内部变量,
因此可以把闭包简单理解成“定义在一个函数内部的函数”。
闭包最大的特点,就是它可以“记住”诞生的环境,比如f2记住了它诞生的环境f1,所以从f2可以得到f1的内部变量。
在本质上,闭包就是将函数内部和函数外部连接起来的一座桥梁;
闭包(closure)是 Javascript 语言的一个难点,也是它的特色,很多高级应用都要依靠闭包实现。
理解闭包,首先必须理解变量作用域。
关于JS垃圾回收机制的问题
function f1() {
var n = 99;
console.log(++n);
}
f1(); //100
f1(); //100
当我们在函数内部引入一个变量或函数时,系统都会开辟一块内存空间;还会将这块内存的引用计数器进行初始化,初始化值为0如果外部有全局变量或程序引用了这块空间,则引用计数器会自动进行+1操作当函数执行完毕后,变量计数器重新归零,系统会运行垃圾回收,将函数运行产生的数据销毁;如计数器不是 0 ,则不会清楚数据;
这个过程就称之为 “JS的垃圾回收机制” ;
如果将上节的代码,改为闭包形式:
function f1() {
var n = 99;
function f2(){
console.log(++n);
}
return f2;
}
var f = f1();
f(); //100
f(); //101
运行代码发现,函数调用一次,其变量 n 变化一次;
因 函数f1被调用时,返回的结果是f2函数体,也就是说,f2函数被当作值返回给f1的调用者,
但是f2函数并没有在此时被调用执行,
所以整个 f1 函数体,无法判断子函数f2会对其产生何种影响,无法判断 变量n是否会被使用;
即使f1函数被调用结束,整个f1函数始终保留在内存中,不会被垃圾回收机制回收;
闭包的最大用处有两个,一个是可以读取函数内部的变量,另一个就是让这些变量始终保持在内存中,
即闭包可以使得它诞生环境一直存在;
注意,外层函数每次运行,都会生成一个新的闭包,而这个闭包又会保留外层函数的内部变量,所以内存消耗很大。
因此不能滥用闭包,否则会造成网页的性能问题。
闭包小案例:缓存随机数
需求:获取随机数,随机数一旦生成,会在程序中多次使用,所以,在多次使用中不变;
function f1(){
var num = parseInt(Math.random()*100)+1;
return function (){
console.log(num);
}
}
var f2 = f1();
f2();
f2();
f2();
闭包点赞案例:
<ul>
<li><img src="images/ly.jpg" alt=""><br/><input type="button" value="赞(1)"></li>
<li><img src="images/lyml.jpg" alt=""><br/><input type="button" value="赞(1)"></li>
<li><img src="images/fj.jpg" alt=""><br/><input type="button" value="赞(1)"></li>
<li><img src="images/bd.jpg" alt=""><br/><input type="button" value="赞(1)"></li>
</ul>
<script>
function f1() {
var value=1;
return function () {
this.value="赞("+(++value)+")";
}
}
//获取所有的按钮
var btnObjs=document.getElementsByTagName("input");
for(var i=0;i<btnObjs.length;i++){
var ff=f1();
//将 闭包函数 绑定给事件
btnObjs[i].onclick=ff;
}
</script>
思考以下两段代码的运行结果:
var name = "The Window";
var object = {
name: "My Object",
getNameFunc: function () {
return function () {
console.log(this.name);
};
}
};
object.getNameFunc()()
var name = "The Window";
var object = {
name: "My Object",
getNameFunc: function () {
var that = this;
return function () {
console.log(that.name);
};
}
};
object.getNameFunc()();
通过分析代码得知,其中的 this
是关键,那么this到底是什么?
this 到底是谁
this的不同指向
this
关键字是一个非常重要的语法点。毫不夸张地说,不理解它的含义,大部分开发任务都无法完成。
记住一点:this
不管在什么地方使用:它永远指向一个对象。
下面是一个实际的例子。
var person = {
name: '张三',
describe: function () {
console.log('姓名:'+ this.name);
}
};
person.describe()
// "姓名:张三"
上面代码中,this.name
表示name
属性所在的那个对象。由于this.name
在describe
方法中调用,而describe
方法所在的当前对象是person
,因此this
指向person
,this.name
就是person.name
。
由于对象的属性可以赋给另一个对象,所以属性所在的当前对象是可变的,即this
的指向是可变的。
var A = {
name: '张三',
describe: function () {
console.log('姓名:'+ this.name);
}
};
var B = {
name: '李四'
};
B.describe = A.describe;
B.describe()
// "姓名:李四"
上面代码中,A.describe
属性被赋给B
,于是B.describe
就表示describe
方法所在的当前对象B
,所以this.name
就指向B.name
。
稍稍重构这个例子,this
的动态指向就能看得更清楚。
function f() {
console.log('姓名:'+ this.name);
}
var A = {
name: '张三',
describe: f
};
var B = {
name: '李四',
describe: f
};
A.describe() // "姓名:张三"
B.describe() // "姓名:李四"
上面代码中,函数f
内部使用了this
关键字,随着f
所在的对象不同,this
的指向也不同。
只要函数被赋给另一个变量,this
的指向就会变。
var A = {
name: '张三',
describe: function () {
console.log('姓名:'+ this.name);
}
};
var name = '李四';
var f = A.describe;
f(); // "姓名:李四"
上面代码中,A.describe
被赋值给变量f
,的内部this
就会指向f
运行时所在的对象(本例是顶层对象)。
案例:判断数值合法性
<input type="text" name="age" size=3 onChange="validate(this, 10, 20);">
<script>
function validate(obj, x, y){
if ((obj.value < x) || (obj.value > y)){
console.log('合法');
}else{
console.log('不合法');
}
}
</script>
上面代码是一个文本输入框,每当用户输入一个值,鼠标离开焦点就会触发onChange事件,并执行validate
回调函数,验证这个值是否在指定范围。浏览器会向回调函数传入当前对象,因此this
就代表传入当前对象(即文本框),就然后可以从this.value
上面读到用户输入的值。
JavaScript语言之中,一切皆对象,运行环境也是对象,所以函数都是在某个对象下运行的,
this
就是函数运行时所在的对象(环境)。这本来并不会让我们糊涂,但是JavaScript支持运行环境动态切换,也就是说,this
的指向是动态的,没有办法事先确定到底指向哪个对象,这才是最初初学者感到困惑的地方。
使用场合
(1)全局环境
全局环境使用this
,它指的就是顶层对象window
。
this === window // true
function f() {
console.log(this === window);
}
f() // true
(2)构造函数
构造函数中的this
,指的是实例对象。
function F(){
this.hello=function(){
console.log('Hello'+this.name);
}
}
var f1 = new F();
f1.name = '张三';
f1.hello();
var f2 = new F();
f2.name = '刘能';
f2.hello();
(3)对象的方法
方法在哪个对象下,this就指向哪个对象。
var o1 = {
s1:'123',
f1:function (){
console.log(this.s1)
}
}
var o2 = {
s1:'456',
f1:o1.f1
}
o2.f1();
(4)定时器中的this
// 第一个参数 有两种写法
// 1:可执行的JS代码(字符串)
// 2:函数
var age = 20;
var o = {
age:18,
gets:function(){
console.log(this.age);
}
}
var l = o.gets;
setInterval(function(){},2000);
setInterval('o.gets',2000); //
setInterval('o.gets()',2000); //18
setInterval(undefined,2000); //
使用this时的注意事项
(1) 避免包含多层this
var o = {
f1: function () {
console.log(this);
var f2 = function () {
console.log(this);
}
f2();
}
}
o.f1()
// Object
// Window
如果要在内层函数中使用外层的this指向,一般的做法是:
var o = {
f1: function () {
console.log(this);
var that = this;
var f2 = function () {
console.log(that);
}
f2();
}
}
o.f1()
// Object
(2)不在循环数组中使用this
var ar = ['a','b','c'];
ar.forEach(function(v,k,ar){
console.log(this[k])
})
this
的动态切换,固然为JavaScript创造了巨大的灵活性,但也使得编程变得困难和模糊。
有时,需要把this
固定下来,避免出现意想不到的情况;JavaScript提供了call
,apply
,bind
这三个方法,来切换/固定this
的指向。
call()方法、apply()方法、bind()方法
call()方法
var lisi = {names:'lisi'};
var zs = {names:'zhangsan'};
function f(age){
console.log(this.names);
console.log(age);
}
f(23);//undefined
f.call(zs,32);//zhangsan
call方法使用的语法规则
函数名称.call(obj,arg1,arg2…argN);
参数说明:
obj:函数内this要指向的对象,
arg1,arg2…argN :参数列表,参数与参数之间使用一个逗号隔开
apply()方法
函数名称.apply(obj,[arg1,arg2…,argN])
参数说明:
obj :this要指向的对象
[arg1,arg2…argN] : 参数列表,但是要求格式为数组
var lisi = {name:'lisi'};
var zs = {name:'zhangsan'};
function f(age,sex){
console.log(this.name+age+sex);
}
f.apply(zs,[23,'nan']);
bind()方法
bind
方法用于将函数体内的this
绑定到具体的某个对象上
this.x = 9;
var module = {
x: 81,
getX: function() {
return this.x;
}
};
module.getX(); // 返回 81
var retrieveX = module.getX;
retrieveX(); // 返回 9, 在这种情况下,"this"指向全局作用域
// 创建一个新函数,将"this"绑定到module对象
// 新手可能会被全局的x变量和module里的属性x所迷惑
var boundGetX = retrieveX.bind(module);
boundGetX(); // 返回 81