@panhonhang
2018-07-29T11:52:45.000000Z
字数 4232
阅读 522
javascript
当函数可以记住并访问所在的词法作用域时,就产生了闭包。即使函数是在当前词法作用域之外执行的。
我们来看一串代码:
function foo() {
var a =2;
function() {
console.log(a);
}
return bar;
}
var baz = foo();
baz();// 这里是2
以上代码就是闭包的效果,函数bar()的词法作用域可以访问foo()的内部作用域。然后我们把bar()这个函数当作一个值来传递。在这个例子中,我们将bar所引用的函数对象本身当作返回值。
在foo()执行了以后,在它内部的bar()函数赋值给变量baz并调用baz(),实际上只是通过不同的标识符引用调用了内部函数的bar();bar()显然可以被执行。但是在这个例子中,它在自己定义的词法作用域以外的地方执行。
在foo()执行了以后,通常会期待foo()的整个内部作用域被摧毁,因为我们知道引擎有垃圾回收器用来释放不再使用的内存空间。由于看上去foo()内容不会再使用,所以会考虑对它进行回收。
而闭包的作用就是可以阻止这种情况的发生,事实上内部作用域依然存在,因此没有被回收。这是因为bar()本身在使用,这是因为bar()所声明的位置,它拥有涵盖foo()内部作用域的闭包,使得该作用域能够一直存活,以供bar()在之后任何时间进行调用。
bar()依然持有对该作用域的引用,而这个引用叫做闭包
本质上无论在何时何地,如果将函数作为第一级的值类型并到处传递,你就会看见闭包在这些函数中应用。在定时器,事件监听器,Ajax请求,跨窗口通信,Web Workers或者任何其他的异步(或者)同步任务中,只要使用了回调函数,实际上就是在使用闭包。
循环和闭包
来看下面的一个例子:
for(var i = 1; i < = 5; i++) {
setTimeout(function() {
console.log(i);
},i*1000);
}
以上代码会以每秒一次的频率输出五次6;
那么我们会有两个问题:
1.为什么是每秒一次而不是一起输出?
2.为什么输出的是6.
先解决问题1.
首先这个循环的终止条件是i不再<=5,首次条件成立的时候i的值是6.所以显示的是循环结束的时候i的最终值。这是因为延迟函数回调会在循环结束的时候才执行。事实上,当定时器运行的时候即使每个迭代中执行的是setTimeout(...,0),所有的回调函数依然是在循环结束后才被执行,因此会每次都输出一个6出来。
比如下面这两段代码其实是等价的
代码1:
for(var i = 1; i < = 5; i++) {
setTimeout(function() {
console.log(i);
},1000);
}
代码2:
for(var i = 1; i < = 5; i++) {};
setTimeout(function() {console.log(i);},1000);
setTimeout(function() {console.log(i);},1000);
setTimeout(function() {console.log(i);},1000);
setTimeout(function() {console.log(i);},1000);
setTimeout(function() {console.log(i);},1000);
然后再来解决问题2:
首先我们来了解一下setTimeout()这个函数用法:setTimeout() 方法用于在指定的毫秒数后调用函数或计算表达式。
所以下面这两段代码其实是等价的:
代码1:
for(var i = 1; i < = 5; i++) {
setTimeout(function() {
console.log(i);
},i*1000);
}
代码2:
for(var i = 1; i < = 5; i++) {};
setTimeout(function() {console.log(i);},1000);
setTimeout(function() {console.log(i);},2000);
setTimeout(function() {console.log(i);},3000);
setTimeout(function() {console.log(i);},4000);
setTimeout(function() {console.log(i);},5000);
这就很好解释为什么每隔一秒输出一次6.
关于This
关于this我首先想讲它的几种绑定与优先级:
this 的四种绑定规则及优先级
创建一个全新的对象;
这个新对象会有一个 prototype 属性;
这个新对象会绑定到函数调用的this;
如果这个函数没有返回其他对象,那么new表达式中的函数调用会自动返回这个新对象
例如:
function foo(a){
this.a = a;
}
var bar = new foo(2);
console.log(bar.a); //2
使用new调用foo()时,this就会指向new调用函数新创建的对象。
2.显式绑定
用call() , apply() , bind() 方法,强制在某个对象上调用函数。
call()从第二个参数开始所有的参数都是 原函数的参数。
apply()只接受两个参数,且第二个参数必须是数组,这个数组代表原函数的参数列表。
bind() 会创建一个新函数(称为绑定函数),当新函数被调用时 this 值绑定到 bind() 的第一个参数,该参数不能被重写。
function foo(){
console.log(this.a);
}
var obj = {
a : 2
};
foo.call(obj); // 2
通过foo.call(),在调用foo时强制把它的this绑定到obj上。
如果传入的对象为 null 或 undefined ,this将会指向window
为了解决这个问题,我们可以创建一个空的非委托对象。
Object.create(null) 代替null
3.隐式绑定
隐式绑定会把函数调用的This绑定到这个上下文对象。
通过为对象添加属性,该属性的值即为要调用的函数,进而使用该对象调用函数:
function foo(){
console.log(this.a);
}
var obj = {
a : 2,
foo: foo,
obj2: obj2
};
var obj2 = {
a: 4,
foo: foo
};
obj.foo(); //2 this被绑定到obj
obj.obj2.foo(); //4
在这种链式关系,对象属性引用链只有上一层(紧挨)或最后一层在调用位置中起作用
4.默认绑定
当以上三条都不符合时,即为默认绑定。this指向window。
function foo(){
console.log(this.a);
}
var a=2; // a是全局对象的一个同名属性
foo(); //2
this的丢失
在隐式绑定时,有两种情况会造成this会绑定到全局对象window或undefined。(是否严格模式)
引用赋值丢失
function foo(){
console.log(this.a);
}
var obj = {
a : 2,
foo: foo
};
var bar = obj.foo;
var a = 4;
bar(); //4
bar 进行了一次引用赋值,引用 foo 函数本身。因此this指向window。
传参丢失
function foo(){
console.log(this.a);
}
var obj = {
a : 2,
foo: foo
};
function dofoo(f){
f();
}
var a = 4;
dofoo(obj.foo); //4
其实参数传递是一种隐式赋值,因此传入函数时也会丢失绑定对象。
关于如何解决丢失this绑定
1.bind() 方法(硬绑定)
function foo(){
console.log(this.a);
}
var obj={
a:2
};
var a=4;
var bar=foo.bind(obj);
console.log(bar()); //2
bind()会返回一个硬编码的新函数,它会把参数设置为this的上下文并调用原始函数。
1.2 软绑定
硬绑定存在一个问题,就是会降低函数的灵活性,并且在硬绑定之后无法再使用隐式绑定或者显式绑定来修改this的指向。软绑定既有硬绑定的效果,又可以使隐式绑定或者显式绑定修改this的效果
引用《你不知道的JS》
if(!Function.prototype.softBind){
Function.prototype.softBind=function(obj){
var fn=this;
var args=Array.prototype.slice.call(arguments,1);
var bound=function(){
return fn.apply(
(!this||this===(window||global))?obj:this,
args.concat.apply(args,arguments)
);
};
bound.prototype=Object.create(fn.prototype);
return bound;
};
}
2.间接引用
用一个定义对象的方法引用另一个对象存在的方法,这种情况下会使得this指向window
function foo(){
console.log(this.a);
}
var a = 2;
var o = {
a : 3,
foo: foo
};
var p = {
a : 4
};
o.foo(); //3
(p.foo = o.foo)(); //2 默认绑定
赋值 p.foo = o.foo 返回值是目标函数的引用。调用位置是全局,this 指向window。
3.箭头函数
ES6箭头函数是由 => 操作符定义的。它不适用上面的四种规则,而是根据外层作用域来决定this.
function foo(){
return (a) => { //返回一个箭头函数
console.log(this.a); //this 继承自foo()
};
}
var obj = {
a: 2
};
var obj2 = {
a: 3
};
var bar = foo.call(obj);
bar(); // 2
bar.call(obj2); // 2 箭头函数的绑定无法修改
foo()内部创建的箭头函数会捕获调用时foo()的 this。由于foo()的this绑定到obj,bar的this也到obj,箭头函数的绑定无法修改