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

1、函数中的作用域

1、函数作用域的含义是:属于这个函数的全部变量都可以在整个函数的范围内使用和复用

1.2、隐藏内部实现

1、颠覆对传统函数的认知:传统的函数我们认为它就是围墙,我们总是先造好围墙,然后在围墙里面写东西,但是我们现在这样想:函数不就是从一段代码中挑选一段代码把它包裹起来么?给予这一段代码新的作用域,实现对这一段代码的隐藏

2、为什么是隐藏,因为给这段代码划分新的作用域foo(假如函数名是foo),就是在原来代码中,foo就是新的标识符,而foo里面的变量和函数现在在原来的代码中都不能直接找到,即从原来的作用域消失了,出现在子作用域中。

3、为什么这样做有好处?首先我们要了解最小授权最小暴露原则,就是在软件设计中我们应该最小限度的去暴露必要的内容,其他内容应该隐藏起来。就好比一个变量a,只在一个函数里使用,却在全局进行了声明,你能保证在别的地方a不会被修改和违法访问么?换句话说,我们应该尽量去缩小变量的作用域

4、举例:

function foo(a){
    b=a+doSomething(a);
    console.log(b);
}
function dosomething(c){
    return c+1;
}
var b;
foo(a)

就像这样,我们的b和函数doSomething都在foo中只用了一次,为什么要公开到全局的作用域?有两种坏处:

(1)JS引擎执行foo,要跑到外层作用域去找b和doSomething,虽然在编译的时候,两者作用域早就确定了,但肯定还是略慢一点。

(2)全局作用域的其他地方可能会对b有异常操作,会使用到dosomething等危险动作。

假如其他地方都不用这两样,我们应该写成这样:

function foo(a){
  function dosomething(c){
    return c+1;
  } 
  var b;
  b=a+doSomething(a);
  console.log(b);
}

foo(a)

1.3、规避冲突

1、下面这段代码大家仔细看,在bar中的i和在for循环中的i本来不是一个东西,但是bar中的i没有声明,跑到外层的for作用域中找到了i,那么i=3,就修改了for中i的值,也就说bar中的i=3意外的覆盖了for中的i,这样i永远=3。

function foo(){
    function bar(a){
        i=3;
        console.log(a+i)
    }
    
    for(var i=0;i<10;i++){
        bar(i*2)
    }
}

foo();

2、那么你会说,这个好办的很,我们bar中的i改成j不久行了,但是这样做没有j的声明,虽然会帮你自动声明j,但是在是全局作用域生成,而且你还看不见,那么全局作用域其他地方使用j怎么办?你又会说,那就写成var j=3,这种虽然可以,但是假如条件苛刻到就要使用i,不用i我的强迫症就犯了怎么办!好办!这样写:(现在还没有说到let,我们先用var)

function foo(){
    function bar(a){
        var i=3;
        console.log(a+i)
    }
    
    for(var i=0;i<10;i++){
        bar(i*2)
    }
}

foo();

3、这样做就把我们bar中的i声明和赋值,但最重要的还是我们把i的作用域缩小到了bar中,避免了与其他外层同名变量的冲突

1.4、解决变量冲突

1、解决变量冲突有两个方法:全局命名空间模块管理

2、全局命名空间这个很好理解,我们在全局作用域中声明一个独一无二的变量,通常是一个对象,叫做命名空间,所有要暴露给外界的都是这个对象的属性,这样也就避免了不小心将某些变量声明到全局作用域当中了。比如下面的代码:

var MyReallyCoolLibrary={
    awesome:"stuff",
    function A(){ //...},
    function B(){ //...},
    function C(){ //...},
}

3、模块管理我们后面再说。

1.5、函数作用域和立即执行函数(IIFE)

1.5.1立即执行函数的用法

1、前面你以为我们只要用函数去把代码包裹起来就完事了?你错了,程序员就是追求极致!尤其是优化的时候

2、用函数去隐藏代码有两个问题:(1)声明的具名函数会‘污染作用域。(2)必须显示的调用函数,这个函数才会执行

3、解决方法:使用立即执行函数将函数声明写成函数表达式

4、比如说下面的代码

function foo(){
    var a=3;
    console.log(a);
}
foo();

使用立即执行函数的方式将上面这段代码写成函数表达式:

//第一种形式:(function foo(){...})()
(function foo(){
    var a=3;
    console.log(a);
})();

//第二种形式:(function foo(){...}())
(function foo(){
    var a=3;
    console.log(a);
}());

立即执行函数的好处是:foo被绑定在函数表达式自身的函数中而不是所在作用域,这样foo变量名就被隐藏,不会污染外部作用域

1.5.2、立即执行函数的进阶用法

1、当做函数调用并传递参数进去:

(function IIFE(global){
    var a=3;
    console.log(a);
    console.log(global.a);
})(window);

2、倒置代码的运行顺序:将要运行的函数放在第二位,在IIFE执行的时候作为参数传进去:

(function IIFE(def){
    def(window);
})(
    function def(global){
        var a=3;
        console.log(a);
        console.log(global.a);
    }
)

2、块作用域

1、在了解作用域之前我们来看看之前的一段代码,很简单,但也是你最容易理解错误的代码:

for(var i=0;i<10;i++){
    console.log(i);
}

看完后,先回答一下i这变量被绑定在哪个作用域当中?你可能会说:欸?这不是块作用域么?i变量就绑定在这个块作用域上啊,i只能在for中使用。

2、好的,可以告诉你,你真的错了,虽然你只想在for循环的上下文中使用i,但是使用var声明变量的时候,写在整个作用域的哪里都一样,因为它会被绑定在外城作用域。也就说for的外面也能找到i。我们可以去运行一下下面的代码:

for(var i=0;i<10;i++){
}
var b=i;
console.log(b);

结果是10,可见i是被绑定在外部的作用域

3、那么你会想,我会java,估计java和javaScript差不多,用{}把for包起来不就行了,这样除了for其他都用不到i。像下面这样的代码:

{
	for(var i=0;i<10;i++){
	}
}
var b=i;
console.log(b);

听完你的这想法,那我只能告诉你,假如你不深入去研究一下JS,你完全想不到:其实JS根本没有块作用域的相关功能。所以上面的代码结果依旧是10。得知了这个结果你会想:JS真垃圾。但是事实也并非如此,在es6之前,with和try...catch...是有块作用域的功能。

4、with就不说了,看看try...catch吧,下面这段代码

try{
    undefined();  //强行制造一个错误
}catch(err){
    console.log(err);
}

console.log(err); //ReferfenceError

很奇怪,err居然有了块作用域,err并没有被绑定在外部作用域,而实际上在es6没有出来之前,块作用域的功能都是被用try..catch写的,比如下面这样:

try{throw 2}catch(a){
    console.log(a) //2
}
console.log(a) //ReferenceError

看完这个你会惊讶:这个是人能读懂的代码么?卧槽。但是真的是这样,现在谷歌维护的一个叫Traceur的项目,就是用这样的方式去兼容es6之前的环境。现在有没有就不知道了。

3、let与垃圾收集

3.1、let

1、有了let,let可以将变量绑定在所在的任意作用域中(通常是{...}内部):我们之前的代码就可以简单的写成下面这样,就是用let去代替var:

for(let i=0;i<10;i++){
}
var b=i;
console.log(b);

我也不骗大家,截图来看看代码的运行结果:

2、特别注意的是:let将变量附加在一个已经存在的作用域上的行为是隐式的

3、只要声明是有效的,我们就能用java中写块的方式给javaScript中的let 声明的变量创建一个用于绑定的块

for(let i=0;i<10;i++){
}
{
    console.log(b);
    let b=10; //b就绑在这个{}作用域中,{}外面一律访问不到b
}
console.log(b); //结果会报ReferenceError的错误

4、但是,你以为这样就完了,还没!真实的情况是在上面的代码中块中的console.log(b)找不到b的声明,你会又懵了:什么鬼,b的声明和赋值不就在下一行代码中么,不是之前说函数用域中,变量和函数能在整个范围内找到就行了么?不会去考虑写代码的先后顺序么?

5、let不做提升:回答你的问题需要用let不做提升来回答,因为最初你写代码的时候和其他语言一样,哪里定义的变量就只能从哪里之后找到和使用,但是JS帮我们做了提升,提升是指声明会被视为存在于其出现的作用域的整个范围内。但是巧了,使用let的声明不会在块作用域中提升,声明的代码在运行之前声明并不存在。下面的代码所示:

{
	console.log(b); //ReferenceError:b没有被定义
	let b=10;
}

6、当你真的了解到这些后,其实你就开始不觉得JS是表面上那么简单了。

3.2垃圾收集

1、垃圾回收这个我们到会面的闭包中详细的去说,暂不介绍。

4、小结

1、函数是javaScript中最常见的作用域单元,本质上,声明在一个函数内部的变量或函数会在所处的作用域中隐藏起来,这是有有意为之的良好软件的设计规则。

2、但是函数不是唯一的作用域单元。块作用域指的是变量和函数不仅可以属于所处的作用域,也可是属于某个代码块

3、ES6中引入了let关键字,用来在任意代码块中声明变量。简单的说下面的代码中:let 为声明的a变量隐式的劫持了if(){....}这个块,并将a添加到了if(){....}这块当中

if(...){
    let a=2;
}

猜你喜欢

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