[关闭]
@panhonhang 2018-07-29T11:52:45.000000Z 字数 4232 阅读 511

闭包与this

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 的四种绑定规则及优先级

  1. new绑定
    new方式是优先级最高的一种调用方式,只要是使用new方式来调用一个构造函数,this一定会指向new调用函数新创建的对象。
    使用new 调用函数,或者 构造函数调用时,会自动执行以下操作

创建一个全新的对象;
这个新对象会有一个 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,箭头函数的绑定无法修改

添加新批注
在作者公开此批注前,只有你和作者可见。
回复批注