你不知道的javaScript(作用域和闭包)--作用域闭包

1、开门见山

1、对于闭包这个概念一定是建立在作用域的基础上的,这里假如你真的觉的自己真的对作用掌握的比较好的话,可以看下去,否则真的,下面的东西可能看起来比较费劲。

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

2、实质是什么?

1、我们下面会讲三个比较经典的三个案例,也可以认为是闭包经常发生的三种情况

2.1、函数返回内部函数对象本身(表面上是返回内部函数名)

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

var baz=foo();
baz();  //2,这就是闭包

1、代码怎么执行的?首先foo函数返回了bar这个函数对象本身,foo函数执行并将函数对象bar赋值给baz,最后调用baz这个函数引用,整个过程的实质就是通过bar和baz这两个不同名称的标识符去调用了foo内部的函数bar

2、为什么产生闭包?首先bar这个函数的当前词法作用域是哪里?很明显在foo里面,bar函数的词法作用域是foo,并且bar可以访问到foo里面所有的变量和函数,比如说a。但是bar执行的位置是foo外面,也就说它先满足了一个条件:函数执行的位置不在词法作用域中。再看,foo函数执行完是不是内部作用域就销毁了,那么为什么后面baz执行还能访问到a,说明bar这个函数对象本身还持有对自己当前作用域(即foo作用域)的引用,这就满足了第二个条件,bar函数记住并访问的到所在的词法作用域

3、所以上面整个段代码可以用一句话形容:bar函数在定义的词法作用域foo以外的地方被调用,bar手里持有的对foo作用域的引用使得bar可以继续访问定义时的词法作用域foo。

4、所以闭包又可以这样理解:对定义的词法作用域引用的持有

2.2、对内部函数直接传递

function foo(){
    var a=2;
    function baz(){
        console.log(a)
    }
    bar(baz);
}
function bar(fn){
    fn(); //baz函数在foo外执行,产生闭包
}
foo();

经过第一个例子,这个例子就好理解了,无论以何种方式对函数类型的值进行传递,当函数在别处被调用时都可以观察到闭包。

2.3、对内部函数的间接传递

var fn;
function foo(){
    var a=2;
    function baz(){
        console.log(a);
    }
    fn=baz; //将函数对象本身传给fn变量,fn就是baz函数对象的引用
}

function bar(){
    fn();   //实际还是baz函数在foo外部执行,产生了闭包
}

foo();
bar();

3、回调函数和闭包

1、在实际我们写代码的时候我们也不会经常写上面这些样子的代码,我们更多接触的就是回调函数,而实际上,只要使用了回调函数,实际上就在使用闭包

function wait(message){
    setTimeout(function time(){
        console.log(message);
},1000);
}

wait("hello world");

2、你看,在实际上wait函数执行完1秒后,time函数执行,还是能访问到message,还是持有对wait(....)这个作用域的闭包,那么你可能会问:为什么wait函数执行完不能自动的将wait(...)这个作用域销毁呢,它怎么知道别人还有自己的作用域的引用?

3、那实际上在引擎内部:内置的工具函数setTimeout持有对一个参数的引用,这个参数叫什么都无所谓,但是引擎会去调用这个函数。在上面的例子中就是time函数,所以词法作用域在引擎调用time的时候也保持了完整。这也是闭包!

4、循环和闭包

<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>菜鸟教程(runoob.com)</title>
</head>		
<body>

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

</body>
</html>

1、上面这段代码可以直接复制到一个txt文件中,然后保存为html文件,拿到浏览器去运行一下,打开控制台,看看到底是什么结果。

2、在最开始写这段代码的时候我觉的大家都会认为这段代码想写出的效果应该是分别输出1、2、3、4、5,然后是一秒一次,但是结果却是:每秒输出一次6,输出5次

3、(1)首先我们要想一下为什么是6,因为其中延迟函数的回调会在循环后进行,所以整个循环的主体执行完了,i已经从1变到了6,所以打印6。

(2)然后我们想想为什么会输出5次,这个很容易理解,每次循环都会告诉time函数你过一会再执行,所以就相当于,循环执行完毕有5个time才开始执行。

(3)最后我们想一下为什么5次打印出来都是6?我们原本是想每一次迭代中都要给time函数一个副本i,可是我们的i是用var定义的,所以i属于for外面的作用域,在上面这个例子中就是全局作用域,所以其中5次迭代5个time函数用的是同一个i,就是那个for中定义的var i。所以我们应该使用let去劫持i的作用域,使每次迭代中的i都只能存在于本次迭代的这个块作用域中。可以写下面的代码来改进:就把var改成let

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

(4)但是重要的不是这样的代码,而是这样的思想:在循环中使用let,这样的行为指出变量i在循环过程中不止被声明一次,每次迭代都会被声明,随后的每个迭代都会使用上一个迭代结束的值来初始化这个变量。(这个真的是重点!)

5、模块

5.1、模块的概念和条件:

1、依旧是开门见山:模块模式需要具备两个条件:

(1)必须有外部的封闭函数,该函数必须至少被调用一次(每次调用都会创建一个新的模块实例)

(2)封闭函数必须返回至少一个内部函数,这样内部函数才能在私有作用域中形成闭包,并且可以访问或者修改私有的状态

2、所以我们来看一个实例,看看什么是模块:

function CoolModule(){
    var something="cool";
    var anther=[1,2,3];
    function doSomething(){
        console.log(something);
    }
    function doAnother(){
        console.log(anther.join("!"));
    }
    return {
        doSomething:doSomething,
        doAnother:doAnother
    };
}
var foo=CoolModule();
foo.doSomething();   // cool
foo.doAnother();  // 1!2!3

结合我们开门见山说的模块的两个条件,首先,CoolModule是外部的封闭函数,这函数在外部被调用了,此时创建了CoolModule包裹的这个模块的实例,这个实例的引用就是foo。第二个就是CoolModule返回了内部的两个函数doSomething和doAnother,将函数对象本身的引用作为一个对象的属性返回了出去。那后面使用这个对象foo的属性就是在调用doSomething和doAnother这两个函数。

5.2、模块最常用的用法:

1、首先我们要知道模块实质是什么?模块的实质就是函数

2、模块最简单粗暴的用法就是命名将要作为公共API返回的对象。代码如下:

var foo=(function CoolModule(id){
    function change(){
        //修改公共的API
        publicAPI.identify=identify2;
    }
    function identify1(){
        console.log(id);
    }
    function identify2(){
        console.log(id.toUpperCase());
    }

    return var publicAPI={
        change:change,
        identify:identify1
    };
})("foo module")

foo.identify();// foo module
foo.change(); 
foo.identify();//FOO MODULE

3、上面这段代码反应出来两个东西,第一个就是我们在写代码的时候遇到的公共API可能都是这样的方法来写的,就是将foo暴露出来让你使用,可以通过foo去调用公共的API。第二个就是假如我们以后要写一些自己的模块,这种形式就是最常用的写法。

6、模块机制的变化

1、在es6之前,基于函数的模块并不是一个能被静态识别的模式,什么意思?就是说我们可以通过上面的例子中的那种形式去修改公共的API,所以这种公共的API并不是一个不变的方法,所以编译器是无法识别的,这种API只有在运行时才会考虑进来。

2、但是es6模块的API是静态的。所以编译器对导入模块的API成员的引用是否真实存在进行检查。不存在就编译不过去。es6的模块还有个特点就是一个文件一个模块,浏览器和引擎有默认的模块加载器可以在导入模块的同时同步加载模块文件。

//bar.js文件
function hello(who){
    return "let me introduce "+who;
}
export hello;

//foo.js
import hello from "bar";  //仅仅从bar模块中导入了hello
var hungry="hippo";
function awesome(){
    console.log(
        hello(hungry).toUpperCase()
    );
}
export awesome;

//baz.js文件
module foo from "foo";
module bar from "bar";
console.log(
    bar.hello("rhino")  //let me introduce rhino
)
foo.awesome();  //LET ME INTRODUCE HIPPO

3、import 可以将一个模块中的一个或者多个API导入到当前作用域中,并分别绑定在一个变量上。

4、module会将整个模块的API导入并绑定在一个变量上。

5、export会将当前模块的一个标识符导出去作为公共的API。

猜你喜欢

转载自blog.csdn.net/weixin_37968345/article/details/81710993