js closure


JS变量声明

JS中变量申明分显式申明和隐式申明。

  • vari=100;//显式申明
  • i=100;//隐式申明

JS作用域 与变量(scope)

  • 全局变量 - global:在函数外部定义的变量,可以在函数内部使用
  • 局部变量 - local :在函数内部定义的变量,只能在函数内部使用
    • 其中,在函数内部定义的变量,如果不写var,也是全局变量。在外部使用前,需先执行这个函数(不推荐)
  • 如果全局变量与局部变量有冲突,使用局部变量。(作用域近的)
  • 块级作用域 Function scope/Block scope: 函数内部 或者 { } 内部
    • for while if ,块级作用域可通过新增命令 let 和 const 声明,所声明的变量在指定块的作用域外无法被访问。
    • 基本上可以用 let 来代替 var 进行变量声明,只有块级变量,没有块级函数
  • 作用域链会先从自身开始查找作用域内的变量,有就执行,没有就往上一层作用域链查找,直到顶端为止,如果顶端还是没有就抛出异常。
  • 自由变量 : 当前作用域没有定义的变量

Javascript函数内部可以直接读取全局变量。

基本作用域例子

最后输出的结果为 2, 4, 12

  • 泡泡 1 是全局作用域,有标识符 foo;
  • 泡泡 2 是作用域 foo,有标识符 a,bar,b;
  • 泡泡 3 是作用域 bar,仅有标识符 c。

外部读取局部变量

  1. 在函数的内部,再定义一个函数
  2. 将内部函数值返回
 function f1(){
    var n=999;
    function f2(){
      alert(n); // 999
    }
  }

Javascript语言特有的”链式作用域”结构(chain scope),子对象会一级一级地向上寻找所有父对象的变量。所以,父对象的所有变量,对子对象都是可见的,反之则不成立。

function f1(){
    var n=999;
    function f2(){
      alert(n);
    }
    return f2;
  }
  var result=f1();
  result(); // 999

函数作用域查找

1、定义说明

1)函数当前作用域查找不到,可以访问外层函数作用域的活动对象(参数、局部变量、定义在外层函数体里的函数)
2)外层的外层函数。。。一直到全局

2、原理

执行环境、作用域链、作用域、活动对象
1)调用内层函数,会创建一个执行环境,执行环境会关联一个作用域链
2)调用内层函数时,所有的外层函数都已经调用完毕或者外层函数调用中,所有只要把所用外层函数作用域(包括最外层全局)的活动对象,关联到当前内层函数的作用域链上。
3)最后创建内层函数作用域的活动对象,并且关联到作用域链的最前端。

活动对象注释:
函数参数,函数体里面定义的局部变量,函数体里面定义的函数

3、例子

var str1 = '全局变量';
function func(arg) {
  var str2 = '外层局部变量';
  function funcInner1() {
    console.log('外层函数的其他函数');
  }
  function funcInner2(argInner2) {
    var str3 = '内层函数变量';
    console.log(str3);
    console.log(argInner2);

    console.log(arg);
    console.log(str2);
    funcInner1();
    console.log(str1);
  }
  return funcInner2;
}
var result = func('外层函数参数')
result('内层函数参数');
/*
执行结果:
内层函数变量
内层函数参数
外层函数参数
外层局部变量
外层函数的其他函数
全局变量*/

自测

var n=999;
function f1(){
  alert(n); 
  }
f1(); // 999 

//外部函数之间的作用域是分开的
function loo(){
    vargoo=1;moo();}
function moo(){
    console.log(goo);}
loo();
//UncaughtReferrenceError:goo is not defined

//注意下面的区别
(function(){
  var n=999;
  })();
console.log(n);//undefined

var n=999;
(function f1(){
  alert(n); //999
  })();


var salary="653.582";
(function(){
    console.log("original salary"+salary);// undefined
    var salary="789";//如果这里没有,那么上面不是undefined;但是内部有的 先看内部
})();


{
  const hello = 'Hello CSS-Tricks Reader!'
  console.log(hello) // 'Hello CSS-Tricks Reader!'
  function f1(){ console.log("this is inside")} //这个全局函数
   let f= function(){};//块级变量
}

console.log(hello) // Error, hello is not defined
f1();//"this is inside"

另一方面,在函数外部自然无法读取函数内的局部变量。

 function f1(){
    var n=999;
  }
  alert(n); // error

这里有一个地方需要注意,函数内部声明变量的时候,一定要使用var命令。如果不用的话,你实际上声明了一个全局变量!

function f1(){
    n=999;
  }
  f1();
  alert(n); // 999

关于自由变量取值

var x = 10;
function fn() {
    console.log(x);
}
function show(f) {
    var x = 20(function() {
        f(); //10,而不是20
    })();
}
show(fn);

在 fn 函数中,取自由变量 x 的值时,要到哪个作用域中取?——要到创建这个函数的那个域”。—其实这就是所谓的”静态作用域”

var a = 10;
function fn() {
    var b = 20;
    function bar() {
        console.log(a + b); //30
    }
    return bar;
}
var x = fn(),
    b = 200;
x(); //bar()

fn()返回的是 bar 函数,赋值给 x。执行 x(),即执行 bar 函数代码。取 b 的值时,直接在 fn 作用域取出。取 a 的值时,试图在 fn 作用域取,但是取不到,只能转向创建 fn 的那个作用域中去查找,结果找到了,所以最后的结果是 30

闭包实际概念

一个函数和对其周围状态(lexical environment,词法环境)的引用捆绑在一起(或者说函数被引用包围),这样的组合就是闭包closure)。

闭包就是能够读取其他函数内部变量的函数。

由于在Javascript语言中,只有函数内部的子函数才能读取局部变量,因此可以把闭包简单理解成”定义在一个函数内部的函数”。

所以,在本质上,闭包就是将函数内部和函数外部连接起来的一座桥梁。

闭包可以用在许多地方。它的最大用处有两个,一个是前面提到的可以读取函数内部的变量,另一个就是让这些变量的值始终保持在内存中。使用闭包十分容易造成浏览器的内存泄露,严重情况下会是浏览器挂死

基本例子

function fn(){
    var num=100;
    console.log(num);
    num++
}
fn();//100
fn();//100


function aa(){
    var n =1;//常驻变量 不会被销毁
    return function(){return n++;}
}
var a1=aa(),
var a2=aa();
    console.log(a1());//1 n=2
    console.log(a1());//2 n=3
    console.log(a2());//1 n=2

a1、a2中的变量n是独立的,存储在各自的作用域里,互不干涉

匿名函数 - IIFE - immediately Invoked Function Expression

匿名函数最大的用途是创建闭包(这是JavaScript语言的特性之一),并且还可以构建命名空间,以减少全局变量的使用

var oEvent = {}; 

(function(){ 
    var addEvent = function(){ /*代码的实现省略了*/ }; 
    function removeEvent(){} 
    oEvent.addEvent = addEvent; 
    oEvent.removeEvent = removeEvent; 
})();

在这段代码中函数addEvent和removeEvent都是局部变量,但我们可以通过全局变量oEvent使用它,这就大大减少了全局变量的使用,增强了网页的安全性。

oEvent.addEvent(document.getElementById('box') , 'click' , function(){});
var rainman = (function(x , y){ 
return x + y; 
})(2 , 3); 
/** 
* 也可以写成下面的形式,因为第一个括号只是帮助我们阅读,但是不推荐使用下面这种书写格式。 
* var rainman = function(x , y){ 
* return x + y; 
* }(2 , 3);
**/

闭包中的匿名函数

下面的情况是很熟悉的

"use strict";
var a=[];
for(var i=0;i<10;i++){
        a[i]=function(){
            return i;
        }
}
console.log(a[0]());
console.log(a[1]());
console.log(a[2]());

如果使用闭包能改变吗?

"use strict";
var a=[];
for(var i=0;i<10;i++){
    (function(){
        a[i]=function(){
            return i;
        }
    })()
}
console.log(a[0]());//10
console.log(a[1]());//10
console.log(a[2]());//10

实际上闭包是不能的,一定注意先循环后面函数被call时候,函数才被运行,实际解决办法

"use strict";
var a=[];
for(var i=0;i<10;i++){
    console.log(i);
        a[i]=(function(){
            console.log(i);
            return i;
        })();

}

//或者
"use strict";
var a=[];
for(var i=0;i<10;i++){
    (function(){
        var y=i;//只有这样下层return才能知道i是多少
        a[i]=function(){
            return y;
        }
    })()
}
console.log(a[0]());
console.log(a[1]());
console.log(a[2]());

//实际上面的写法 按照匿名函数,最好的写法是:
"use strict";
var a=[];
for(var i=0;i<10;i++){
    (function(y){
        console.log("this is y : "+y);
        a[y]=function(){
            return y;
        }
    })(i)
}

实际应用 (跳过)

在定时器、事件监听器、 Ajax 请求、跨窗口通信、Web Workers 或者任何其他的异步(或者同步)任务中,只要使用了回调函数,实际上就是在使用闭包!

定时器闭包案例:

function wait(message) {
setTimeout( function timer() {
console.log( message );
}, 1000 );
}
wait( "Hello, closure!" );

事件监听闭包案例:

function setupBot(name, selector) {
$(selector).click( function activator() {
console.log( "Activating: " + name );
});
}
setupBot( "Closure Bot 1", "#bot_1" );
setupBot( "Closure Bot 2", "#bot_2" );

上面的案例中,有个相同的特点:先定义函数,后执行函数时能够调用到函数中的私有变量或者实参。这便是闭包的特点吧

Currying

//Un-curried function
function unCurried(x, y) {
  return x + y;
}

//Curried function
function curried(x) {
  return function(y) {
    return x + y;
  }
}
//Alternative using ES6
const curried = x => y => x + y

curried(1)(2) // Returns 3
function add(x) {
  // Only change code below this line
return function(y) {
    return function(z) {
      return x + y + z;
    };
  };
  // Only change code above this line
}
add(10)(20)(30);

实际应用中经典例子

/** 
* <body> 
* <ul> 
* <li>one</li> 
* <li>two</li> 
* <li>three</li> 
* <li>one</li> 
* </ul> 
*/
var lists = document.getElementsByTagName('li'); 
for(var i = 0 , len = lists.length ; i < len ; i++){ 
    lists[ i ].onmouseover = function(){ 
        alert(i); 
    }; 
}

当mouseover事件调用监听函数时,首先在匿名函数( function(){ alert(i); })内部查找是否定义了 i,结果是没有定义;因此它会向上查找,查找结果是已经定义了,并且i的值是4(循环后的i值);所以,最终每次弹出的都是4。

解决方法一:

var lists = document.getElementsByTagName('li'); 
for(var i = 0 , len = lists.length ; i < len ; i++){     
    (function(index){         
        lists[ index ].onmouseover = function(){             
            alert(index);         
        };     
    })(i); //这是一种自执行函数的格式,前一个括号是匿名函数,解析器执行后返回一个函数对象然后调用后面一个括号(i),所以后面一个括号就是函数参数}

解决办法二:

function eventListener(list, index){ 
    list.onmouseover = function(){ 
        alert(index); 
    }; 
} 
var lists = document.getElementsByTagName('li'); 
for(var i = 0 , len = lists.length ; i < len ; i++){ 
eventListener(lists[ i ] , i); 
}

自测

注意构造函数中的this,主要看是哪个obj调用这个function

  var name = "The Window";
  var object = {
    name : "My Object",
    getNameFunc : function(){
      return function(){
        return this.name;
      };
    }
  };
  alert(object.getNameFunc()());
 var name = "The Window";
  var object = {
    name : "My Object",
    getNameFunc : function(){
      var that = this;
      return function(){
        return that.name;
      };
    }
  };
  alert(object.getNameFunc()());

Reference:

https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Closures

http://www.ruanyifeng.com/blog/2009/08/learning_javascript_closures.html


Author: Savannah
Reprint policy: All articles in this blog are used except for special statements CC BY 4.0 reprint polocy. If reproduced, please indicate source Savannah !
  TOC