JavaScript ——闭包,this

关于this对象

this对象是在运行时基于函数的执行环境绑定的;全局函数中,this等于window,而当函数的执行环境具有全局性,this对象通常指向window。

var name = "The window";
var object = {
	name:"My Object",
	getNameFun:function(){
		return function(){
			return this.name;
		}
	}
}
alert(object.getNameFun());//"The Window"(非严格模式下)

为什么不是"My Objdect"?
每个函数在被调用时都会自动取得两个特殊变量:this和arguments内部函数在搜索这两个变量时,只会搜索到其活动对象为止,因此永远不可能直接访问外部函数中的这两个变量。

把外部作用域中的this对象保存在一个闭包能够访问到的变量里,就可以让闭包访问该对象了

var name = "The window";
var object = {
	name:"My Object",
	getNameFun:function(){
		return function(){
			var that = this; 
			return that.name;//"My Object"
		}
	}
}

特殊情况下,this值可能会改变

var name = "The window";
var object = {
	name:"My Object",
	getNameFun:function(){
		return function(){
			return this.name;
		}
	}
}
①object.getName();//"My Objedt"
②(object.getName())();//"My Object"
③(object.getName = object.getName)();//"The window"非严格模式下
③先执行一条赋值语句然后再调用赋值后的结果,这个赋值表达式的值是函数本身,所以this的值不能维持,"The window"

闭包

闭包是指有权访问另一个函数作用域中的变量的函数
常见的创建闭包的方法:在一个函数内部创建另一个函数
当某个函数被调用时,会创建一个一个执行环境及相应的作用域链。然后,使用arguments和其他命名参数的值来初始化函数的活动对象。

function compare(value1,value2){
	if(value1 < value2){
		return -1;
	}else if(value1 > value2){
		return 1;
	}else{
		return 0;
	}
}
  1. 后台的每个执行环境都有一个表示变量的对象 —— 变量对象

  2. 全局环境的变量对象始终存在,而像compare()函数这样的局部环境变量的对象,则只在函数执行的过程存在。

  3. 作用域链本质上是一个指向变量对象的指针列表,只是引用但不实际包含变量对象

  4. 函数中当我一个变量时,就会从作用域链中搜索具有相应名字的变量。

  5. 当函数执行完毕后,一般,局部活动对象就会被销毁,内存中仅保存全局作用域(全局执行环境的变量对象),但闭包情况不同。

  6. 在另一个函数内部定义的函数会将包含函数(外部函数)的活动对象添加到它的作用域链中。
    这样外部函数执行完毕之后,其活动对象也不会被销毁,因为内部函数的作用域链仍然在引用这个活动对象。外部函数的作用域链会被销毁,但活动对象仍会留在内存中;直至内部函数被销毁后,外部函数的活动对象才会被销毁。

     //创建函数
     var compareNamed = createCompareFunction("name");
     //调用函数
     var result = compareNames({name:"ABC"},{name:"DRF"});
     //解除对匿名函数的引用(以便释放内存)
     compareNames = null;
    

创建的比较函数被保存在变量compareNames中,通过将compareNames设置为等于null解除该函数的引用,就等于通知垃圾回收例程将其清除,随着匿名函数的作用域链被销毁,其他作用域(除了全局作用域)也都可以安全地销毁。

闭包只能取得包含函数中任何变量的最后一个值

function createFunction(){
	var result = new Array();
	for(var i=0;i<10;i++){
		result[i] = function(){
			return i;
		}
	}
	return result;
}

返回一个数组,但每个函数都返回10,。因为每个函数作用域中都保存着createFunction()函数的活动对象,引用都是同一个变量i,每个函数都引用这个保存变量的同一个变量对象,所以函数内容i都是10

通过创建另一个匿名函数强制让闭包的行为符合预期

function createFunction(){
	var result = new Array();
	for(var i=0;i<10;i++){
		result[i] = function(num){
			return function(){
				return num;
			};
		}(i);
	}
	return result;
}

定义了一个匿名函数,并将立即执行该匿名函数的结果赋给数组。在调用每个匿名函数时,我们传入变量i。
由于函数参数是按值传递的,所以就会将变量i的当前复制给参数num。而这个匿名函数内部,又创建并返回了一个访问num的闭包。result数组中的每个函数都有自己num变量的一个副本,因此就可以返回各自不同的数值。

内存泄漏

如果闭包的作用域链中保存着一个HTML元素,那么就意味着该元素将无法被销毁

function assignHandler(){
	var element = document.getElementById("someElement");
	element.onclick = function(){
		alert(element.id);
	}
}

匿名函数保存了assignHandler()的活动对象的引用,因此就会导致无法减少element的引用数。只要匿名函数存在,element的引用数至少也是1,因此它所占的内存就永远不会被回收

function assignHandler(){
	var element = document.getElementById("someElement");
	var id = element.id
	element.onclick = function(){
		alert(id);
	};
	element = null;
}

element.id的一个副本保存在一个变量中,并在闭包中引用该变量消除了循环引用,闭包会引用包含函数的整个活动对象,而其中包含element。
即使闭包不直接引用element,包含函数的活动对象中也仍然会保存一个引用。有必要把element变量设置为null,这样就能解除对DOM对象的引用顺利地减少,确保正常回收其占用的内存。

私有变量
JS中没有私有成员的概念;任何在函数中定义的变量,都可以认为是私有变量,因此不能再函数的外部访问这些变量,私有变量包括函数的参数,局部变量和在函数内部定义的其他函数。


来源于《JavaScript高级程序设计》


为什么要用this

this提供了一种更优雅的方式来隐式“传递”一个对象引用,因此可以将API设计得更加简洁并且易于复用。

指向自身

  1. 有一种传统的但是现在已经被启用和批判的用法,使用arguments.callee来引用当前正在运行的函数对象。这是唯一一种可以从匿名函数对象内部引用自身的方法。现在已被弃用,不该再使用他。

  2. 记录一下函数foo被调用的次数

     function foo(num){
         console.log("foo:" + num);
         //记录foo被调用的次数
         foo.count++;
     }
     foo.count = 0;
     var l;
     for(i=0;i<10;i++){
         if(i>5){ foo(i);}
     }
     //foo:6,foo:7,foo:8.foo:9
     //foo被调用了多少次?
     console.log(foo.count);//4
    

这种方法是使用foo标识符替代this来引用函数对象,这种方法同样回避了this的问题,并且完全依赖于foo的词法作用域。
3. 另一种方法是强制this指向函数对象

     function foo(num){
        console.log("foo:" + num);
        //记录foo被调用的次数
        //注意,在当前的调用方式下,this确实指向foo
        this.count++;
    }
    foo.count = 0;
    var l;
    for(i=0;i<10;i++){
        if(i>5){ 
        //使用call(..)可以确保this指向函数对象foo本身
        foo.call(foo,i);
        }
    }
    //foo:6,foo:7,foo:8.foo:9
    //foo被调用了多少次?
    console.log(foo.count);//4

this到底是什么

  1. this是在运行时进行绑定的,并不是在编写时绑定,它的上下文取决于函数调用时的各种条件。this的绑定和函数声明的位置没有任何关系,只取决于函数的调用方式。
  2. 当一个函数被调用时,会创建一个活动记录(有时候也称为执行上下文)。这个记录会被包含函数在哪里被调用(调用栈),函数的调用方式,传入的参数等信息。this就是这个记录的而一个属性,会在执行函数执行的过程中用到。

this全面解析

调用位置

  1. 首先要理解调用位置:调用位置就是函数在代码中被调用的位置(而不是声明的位置)

  2. 调用位置就在当前正在执行的函数的前一个调用中。

  3. 看看到底什么是调用栈和调用位置

     function baz(){
         //当前调用栈是:baz
         //因此,当前调用位置是全局作用域
         console.log("baz");
         bar();//bar的调用位置
     }
     function bar(){
         //当前调用栈是:baz->bar
         //因此,当前调用位置在baz中
         console.log("bar");
         foo();//foo的调用位置
     }
     function foo(){
         //当前调用栈是:baz->bar->foo
         //因此,当前调用位置在bar中
         console.log("foo");
     }
     baz();//baz的调用位置
    

默认绑定

1.最常用的函数调用类型:独立函数调用。可以理解为是无法应用其他规则时的默认规则

    function fpp(){
        console.log(this.a);
    }
    var a = 2;
    foo();//2
  • 声明在全局作用域中的变量(比如 var a = 2)就是全局对象的一个同名属性。本质上就是同一个东西,并不是通过复制得到的,就像一个硬币的两面一样。
  • 当调用foo()时,this.a被解析成了全局变量a。函数调用时应用了this的默认绑定,因此this指向全局对象。
  • 在代码中,foo()是直接使用不带任何修饰的函数引用进行调用的,因此只能使用默认绑定,无法应用其他规则。
  1. 虽然this的绑定规则完全取决于调用位置,但是只有foo()运行在非strict mode下时,默认绑定才能绑定到全局对象,在严格模式下调用foo()则不影响默认绑定。

隐式绑定

  1. 另一条需要考虑的规则时调用位置是否有上下文对象,或者说是否被某个对象拥有或者包含,不过这种说法可能会造成一些误导。

         function foo(){
             console.log(this.a);
         }
         var obj = { 
             a:2,  foo:foo
         }
         obj.foo();
    
  2. 首先需要注意的是foo()的声明方式,及其之后是如何被当作属性添加到obj中的,但是无论是直接在obj中定义还是先定义再添加为引用属性,这个函数严格来说都不属于obj对象。

  3. 然而,调用位置会使用obj上下文来引用函数,因此你可以说函数被调用时obj对象“拥有”或者“包含”它。

  4. 当函数引用有上下文对象时,隐式绑定规则会把函数调用中的this绑定到这个上下文对象,因为调用foo()时this被绑定到obj,因此this.a和obj.a是一样的。

隐式丢失

  1. 一个最常见的this绑定问题就是被隐式绑定的函数会丢失绑定对象,也就是说它会应用默认绑定,从而把this绑定到全局对象或者undefined上,取决于是否是严格模式。

      function foo(){
             console.log(this.a);
      }
      var obj = { 
         a:2,  foo:foo
      }
     var bar = obj.foo;//函数别名
     var a = "oops,global";//a是全局对象的属性
     bar();//oops,global
    
  2. 虽然bar是obj.foo的一个引用,但是实际上,它引用的是foo的函数本身,因此此时的bar()其实是一个不带任何修饰的函数调用,因此应用了默认绑定。

  3. 参数传递其实就是一种隐式赋值,因此我们传入函数时也会被隐式赋值。

  4. 如果把函数传入语言内置的函数而不是传入你自己声明的函数,结果还是变得。

  5. 回调函数丢失this绑定是非常常见的。调用回调函数的函数可能会修改this。

显示绑定

  1. 在分析隐式绑定时,我们必须在一个对象内部包含一个指向函数的属性,并通过这个属性间接引用函数,从而把this间接(隐式)绑定到这个对象上。

  2. javascript提供的绝大多数函数以及你自己创建的所有函数都可以使用call(…)和apply(…)方法。

  3. 它们的第一个参数是一个对象,是给this准备的,接着在调用函数时将其绑定到this。

         function foo(){
             console.log(this.a);
         }
         var obj = {
             a:2;
         }
         foo.call(obj);//2
    
  4. 通过foo.call(…),我们可以在调用foo时强制把它的this绑定到obj上。

  5. 如果你传入了一个原始值(字符串类型,布尔类型后者数字类型)来当作this的绑定对象,这个原始值会被转换成它的对象形式(也就是new String(…),new Boolean(…)或者new Number(…))。这通常被称为“装箱”。

硬绑定

        function foo(){
            console.log(this.a);
        }
        var obj = {
            a:2;
        }
        var bar = function(){
            foo.call(obj);
        }
        bar();//2
        setTimeout(bar,100);//2
        //硬绑定的bar不可能再修改它的this
        bar.call(window);//2
  1. 我们创建了函数bar(),并在它的内部手动调用了foo.call(obj)。因此强制把foo的this绑定到了obj。无论之后如何调用函数bar,它总会手动在obj上调用foo。这种绑定是一种显示的强制绑定,因此我们称为硬绑定。

  2. 硬绑定的典型应用场景就是创建一个包裹函数,负责接收参数并返回值

         function foo(something){
             console.log(this.a,something);
             return this.a + something;
         }
         var obj = {
             a:2
         };
         var bar = function(){
             return foo.apply(obj,arguments);
         };
         var b = bar(3);//2,3
         console.log(b);//5
    
  3. 另一种使用方法是创建一个可以重复使用的辅助函数

         function foo(something){
             console.log(this.a,something);
             return this.a + something;
         }
         //简单地辅助绑定函数
         function bind(fn,obj){
             return function(){
                 return fn.apply(obj,arguments);
             };
         }
         var obj = {
             a:2
         };
         var bar = bind(foo,obj);
         var b = bar(3);//2,3
         console.log(b);//5
    
  4. ES5提供了内置的方法function.ptototype.bind,它的用法如下:

         function foo(something){
             console.log(this.a,something);
             return this.a + something;
         }
          var obj = {
             a:2
         };
         var bar = bind(obj);
         var b = bar(3);//2,3
         console.log(b);//5
    

bind(…)会返回一个硬编码的新函数,它会把你指定的参数设置为this的上下文并调用原始函数。

API调用的“上下文”

  1. 第三方库的许多函数,以及javascript语言和宿主环境中许多新的内置函数,都提供了个可选的参数,通常被称为“上下文”,其作用域和bind(…)一样,确保你的回调函数使用指定的this。例:

         function foo(el){
             console.log(el,this.id);
         }
         var obj = {
             id:"awesome"
         };
         //调用foo(..)时把this绑定到obj
         [1,2,3].forEach(foo,obj);
         //1 awesome 2 awesome 3 awesome
    

new绑定

  1. 重新定义JavaScript中的“构造函数”,在javascript中,构造函数只是一些使用new操作符时被调用的函数。它们并不会属于某个类,也不会实例化一个类。实际上,它们甚至都不能说是一种特殊的函数类型,它们只是被new操作符调用的普通函数而已。
  2. 包括内置对象函数(比如Number(…))在内的所有函数都可以用new来调用,这种函数调用被称为构造函数调用。实际上并不存在所谓的“构造函数”,只有对于函数的“构造调用”。
  3. 使用new来调用函数,或者说发生构造函数调用时,会自动执行下面的操作。
    • 创建(或者说构造)一个全新的对象。
    • 这个新对象会执行[[Prototype]]连接
    • 这个新对象会绑定到函数调用的this
    • 如果函数没有返回其他对象,那么new表达式中的函数调用会自动返回这个新对象。

判断this

1.我们可以根据优先级来判断函数在某个调用位置应用的是哪条规则。

  • 函数是否在new中调用(new绑定)?如果是的话this绑定的是新的创建的对象。var bar = new foo();
  • 函数是否通过call,apply(显示绑定)或者硬绑定调用?如果是的话,this绑定的是指定的对象。var bar = foo.call(obj2);
  • 函数是否在某个上下文对象中调用(隐式绑定)?如果是的话,this绑定的是那个上下文对象。var bar = obj1.foo()
  • 如果都不是的话,使用默认绑定。如果在严格模式下,就绑定到undefined,否则绑定到全局对象。var bar = foo();

绑定例外

间接引用

    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
  1. 赋值表达式p.foo = o.foo的返回值是目标函数的引用,因此调用位置是foo()而不是p.foo()或者o.foo()。这里会应用默认绑定。
  2. 注意:对于默认绑定来说,决定this绑定对象的并不是调用位置是否处于严格模式,而是函数体是否处于严格模式。如果函数体处于严格模式,this会被绑定到undefined,否则this会绑定到全局对象。

软绑定

  1. 如果可以给默认绑定指定一个全局对象和undefined以外的值,那就可以实现和硬绑定相同的效果,同时保留隐式绑定或者显示绑定修改this的能力。

  2. 软绑定例:

         if(!Function.prototype.doftBind){
             Function.prototype.softBind = function(obj){
                 var fn = this;
                 //捕获所有curried参数
                 var curried = [].slice.call(arguments,1);
                 var bound = function(){
                     return fn.apply(
                         (!this || this === (window || global))?
                         obj:this,
                         curried.concat.apply(curried,arguments)
                     );
                 }
                 boun.prototype = Object.create(fn,prototype);
                 return bound;
                 };
             }
    
  3. 除了软绑定之外,softBind(…)的其他原理和ES5内置的bind(…)类似。它会对指定的函数进行封装,首先检查调用时的this,如果this绑定到全局对象或者undefined,那就把指定的默认对象obj绑定到this,否则不会修改this。此外,这段代码还支持可选的柯里化。

更安全的this

一定要注意:有些调用可能在无意之中使用默认绑定规则。如果想“更安全”地忽略this绑定,你可以使用一个DMZ对象,比如 空符号 = Object.create(null),以保护全局对象。

this词法

  1. 肩头函数并不是使用function关键字定义的,而是使用被称为“胖箭头”的操作符=>定义的。

  2. ES6中的箭头函数并不会使用四条标准的绑定规则,而是根据当前的词法作用域来决定this,具体来说,箭头函数会继承外层函数调用的this绑定(无论this绑定到什么)。这其实和ES6之前的代码中的self=this机制一样。

  3. 箭头函数最常用于回调函数中,例如时间处理器或者定时器:

         function foo(){
             setTimeout(() => {
             //这里的this在此法上继承来自foo()
             console.log(this.a);
             },100);
         }
         var obj = { 
             a:2;
         }
         foo.call(obj);//2
    
  4. 如果你经常编写this风格的代码,但是绝大部分都会使用self = this或者箭头函数来否定this机制,那你或许应当:

    • 只使用词法作用域并完全抛弃错误this风格的代码
    • 完全采用this风格,在必要时使用bind(…),尽量避免使用self = this和箭头函数。

作用域闭包

实质问题

  1. 当函数可以记住并访问所在的词法作用域时,就产生了闭包,即使函数是在当前词法作用域之外执行。

  2. 清晰展示一下闭包:

     function foo(){
         var a= 2;
         function bar(){
             console.log(a);
         }
         return bar;
     }
     var baz = foo(); baz();//2
    

函数bar()的词法作用域能够访问foo()的内部作用域。然后将bar()函数本身当作一个值类型进行传递。在foo()执行后,其返回值(也就是每部的bar()函数)赋值给变量baz并调用baz()。实际上只是通过不同的标识符引用调用了内部的函数bar()。bar()显然可以被执行。但是,它在自己定义的词法作用域以外的地方执行。
3. 在foo()执行后,通常会期待foo()的整个内部作用域都被销毁,因为我们知道引擎有垃圾回收器用来释放不再使用的内存空间。由于看上去foo()的内容不会再被使用,所以很自然地会考虑对其进行回收。
4. 而闭包可以阻止这件事情发生。事实上内部作用域依然存在。因此没有被回收。拜bar()所声明的位置所赐,它拥有涵盖foo()内部作用域的闭包,使得作用域能够一直存活,以供bar()在之后任何时间进行引用。bar()依然持有对该作用于的引用,而这个引用就叫做闭包。
5. 函数在定义时的词法作用域以外的地方被调用。闭包使得函数可以继续访问定义时的词法作用域。
6. 无论通过何种手段将内部函数传递到所在的词法作用域以外,它都会持有对原始定义作用域的引用,无论在何处执行这个函数都会使用闭包。
7. 只要使用了回调函数,实际上就是使用闭包。

循环和闭包

    for(var i=1;i<=5;i++){
        setTimeout(function timer(){
            console.log(i);
        },i*1000);
    }

我们对这段代码代码行为的预期是分别输出1~5,每一秒一次,一次一个。但是实际上输出5次6.

  1. 首先解释6是从哪来的,这个循环的终止条件是不再<=5,条件首次成立时i的值是6,因此,输出显示的是循环结束时i的最终值。延迟函数的回调会在循环结束时才执行。事实上当定时器运行时即使每个迭代中执行的是setTimeout(…0),所有的回调函数依然是再循环结束后才被执行,因此会每次输出一个6来。

  2. 这里缺陷是我们试图假设循环中的每个迭代在运行时都会给自己“捕获”一个i的副本。但是,实际情况是尽管循环中的五个函数是在各个迭代中分别定义的,但是它们都被封闭在一个共享的全局作用域中,因此实际上只有一个i。我们需要更多的闭包作用域,特别是在循环的过程中每个迭代都需要一个闭包作用域。

     for(var i=1;i<=5;i++){
         (function(j){
             setTimeout(function timer(){
                 console.log(j);
             },j*1000);
         })(i);
     }
    
  3. 在迭代内使用IIFE会为每个迭代都生成一个新的作用域,使得延迟函数的回调可以将新的作用域封闭在每个迭代内部,每个迭代中都会含有一个具有正确值得变量供我们访问。

重返块作用域

  1. 我们使用IIFE在每次迭代时都创建一个新的作用域。换句话说,每次迭代我们都需要一个块作用域。let声明,可以用来劫持块作用域,并且在这个块作用域中声明一个变量。本质上这是将一个块转换成一个可以被关闭的作用域。

     for(let i=1;i<=5;i++){
         setTimeout(function timer(){
              console.log(i);
         },i*1000);
     }
    

模块

    function CoolModule(){
        var something = "cool";
        var another = [1,2,3];
        function dosomething(){
            consloe.log(something);
        }
        function doAnother(){
            consloe.log(another.join(" ! ");
        }
        return {
            dosomething:dosomething;
            doAnother:doAnother;
        };
    }
    var foo = CoolModule;
    foo.dosomething();//cool
    foo.doAnother();//1 ! 2 ! 3
  1. 这个模式在javascript中被称为模块。最常见的现实模块模式的方法通常被称为模块暴露,这里展现的是其变体。

  2. 模块模式需要具备两个必要条件:

    • 必须有外部的封闭函数,该函数必须至少被调用一次(每次调用都会创建一个新的模块实例)。
    • 封闭函数必须返回至少一个内部函数,这样内部函数才能在私有作用域中形成闭包,并且可以访问或者修改私有的状态。
  3. 一个具备函数属性的对象本身并不是真正的模块。一个从函数调用所返回的,只有数据属性而没有闭包函数的对象并不是真正的模块。

  4. 模块模式另一个简单但很强大的用法是命名将要作为公共API返回的对象。

      var foo = (function CoolModule(id){
         function cahnge(){
             //修改公共API
             publicAPI.identify = identfy2;
         }
         function identify1(){
             consloe.log(id);
         }
         function identify2(){
             consloe.log(id.toUpperCase());
         }
         var publicAPI = {
             cahnge:cahnge;
             identify:identify1;
         };
         return publicAPI;
     })("foo module");
     foo.identify();//foo module
     foo.change();
     foo.identify();//FOO MODULE
    
  5. 通过在模块实例的内部保留对公共API对象的内部引用,可以从内部对模块实例进行修改,包括添加或删除方法和属性,以及修改它们的值。


来源于《你不知道的JavaScript》

你可能感兴趣的:(程序猿)