【JavaScript学习笔记】Part 1-作用域和闭包

1.作用域

  • 引擎 - 负责整个JS程序的编译以及执行过程。
  • 编译器 - 负责语法分析及代码生成。
  • 作用域 - 负责收集并维护由所有声明的标识符(变量)组成的一系列查询,并实施一套严格的规则,确定当前执行的代码对这些标识符的访问权限。所以,作用域就是更具名称查找变量的一套规则。

1.1 查找类型

LHS查询:查找的目的是对变量进行赋值。

RHS查询:目的是获取变量的值。

当引擎执行LHS查询时,如果在全局作用域中也无法找到目标变量,就在在全局作用域中创建一个具有该名称的变量,前提是在非严格模式下

1.2 异常

RefernceError和作用域判别失败相关。

TypeError代表作用域判别成功了,但对结果的操作是不合法或不合理的。

2.词法作用域

定义在词法阶段的作用域,词法作用域是由你在写代码时将变量和块作用域写在哪里来决定的,因此当词法分析器处理代码时会保持作用域不变。

function foo(a){
    
    
    var b = a * 2
    function bar(c){
    
    
        console.log(a,b,c)
    }
    bar(b * 3)
}
foo(2)

这里就有三个作用域:

  • 全局作用域,其中有一个标识符为foo
  • foo所创建的作用域,有三个标识符:a,bar,b
  • bar所创建的作用域,有一个标识符为c

作用域查找会在找到第一个匹配的标识符时停止,在多层嵌套作用域中可以定义同名的标识符,称为遮蔽效应

2.1 欺骗词法

  1. eval()中若包含一个或多个生命,就会对其所处的词法作用域进行修改;在严格模式下,eval()在运行时有自己的词法作用域,意味着其中的声明无法修改所在的作用域。
  2. with()根据传递给它的对象凭空创建一个全新的词法作用域。

用这两个东西的话引擎无法在编译时对作用域查找进行优化,因此不要使用他们。

3.函数作用域和块作用域

若function关键词是声明中的第一个词,那么就是一个函数声明,否则就是一个函数表达式。

函数作用域的含义是指,属于这个函数的全部变量都可以在整个函数的范围内使用及服用。

3.1 隐藏内部实现

function dosomething(a){
    
    
    function dosomethingelse(a){
    
    
        return a - 1
    }
    var b
    b = a + dosomethingelse(a*2)
    console.log(b*3)
}
dosomething(2)

bdosomethingelse()都无法从外界被访问,而只能被dosomething()所控制,在设计上将具体内容私有化了。

3.2 规避冲突

隐藏作用域中的变量和函数可以避免同名标识符之间的冲突。

3.3 块作用域

块作用域是一个用来对最小授权原则进行扩展的工具,将代码从在函数中隐藏信息扩展为在块中隐藏信息。

3.4 let

let关键字可以将变量绑定到所在的任意作用域中,换句话说,let为其声明的变量隐式地劫持了所在的块作用域。

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

该声明的变量劫持了if的{}块,并且将变量添加到这个块中。

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

let不仅将i绑定到for循环的块中,同时它将其重新绑定到了循环的每一个迭代中,确保使用上一个循环迭代结束时的值重新进行赋值,这一点在后面闭包会体现出来其作用。

4.提升

引擎会在解释js代码之前先对其进行编译,编译阶段中的一部分工作就是找到所有的声明并用合适的作用域将他们关联起来。so,包含变量和函数在内的所有声明都会在任何代码被执行之前首先被处理。

a = 2
var a
console.log(a) //输出2
===>
var a
a = 2
console.log(a)
//---------------------
console.log(a)
var a = 2 //undifine
===>
var a 
console.log(a)
a = 2

将所有变量和函数的声明提升到最上面了,且只有声明本身会被提升。

每个作用域都会进行提升操作。

函数声明先被提升,然后才是变量:

foo()  //输出1
var foo
function foo(){
    
    
    console.log(1)
}
foo = function(){
    
    
    console.log(2)
}

尽量避免在块内部声明函数。

5.作用域闭包

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

function foo(){
    
    
    var a = 2
    function bar(){
    
    
        console.log(a)
    }
    return bar
}
var baz = foo()
baz()
//baz所在的全局作用域可以访问到foo作用域的a
function foo(){
    
    
    var a = 2
    function baz(){
    
    
        console.log(a)
    }
    bar(baz)
}
function bar(fn){
    
    
    fn()
}
foo()
//内部函数baz传递给了bar,调用baz,即fn时候,其涵盖的foo()内部作用域的闭包就可以观察到了,因为此时他能访问到a

当内部函数传递到所在的词法作用域外,它都会持有对原始定义作用域的引用,无论在何处执行这个函数都会使用闭包

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

5.1 循环和闭包

for(var i=1;i<=5;i++){
    
    
    setTimeout(function timer(){
    
    
        console.log(i)
    },i*1000)
}
//全部打印6

延迟函数的回调会在循环结束时才会执行,根据作用域的工作原理,虽然这五个回调函数是在各个迭代中分别定义的,但它们都被封闭在一个共享的全局作用域中,这里只有一个i。也就是说,因为只有一个i,所以前面迭代定义的函数所使用的i=1,被最后一次迭代的i=6覆盖了,最终所有的time()都是打印值为6的这个i。

如果想让他正常打印,那么就要对每个迭代都有一个闭包作用域,可以使用IIFE

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

这样一来,每次迭代都会生成一个新的作用域,使得延迟函数的回调可以将新的作用域封闭在每个迭代的内部,所以这里就相当于有了5个i,每个i的值都不一样,每次迭代中都有含有一个正确的i供我们访问。

注意:每个作用域都要有自己的变量用来存储i的值,才能正常工作,也就是要有var j = i

当然可以写成下面这样:

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

前面说到的let,可以用来劫持块作用域,并在这个块作用域中声明一个变量,那么就可以使用let的特性来正确低循环输出了:

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

这里最后实现的原理和上面用IIFE是一样的,都给每次迭代创建了一个块级作用域。

然而let在循环中还有一个NB的行为,就是变量在循环过程中不止被声明一次,每次迭代都会声明,那么回到最初的写法,只要把var改成let就完事了(ES6大法好!)

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

5.2 模块

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

  1. 必须有外部的封闭函数,该函数必须至少被调用一次(每次调用都会创建一个新的模块实例)
  2. 封闭函数必须返回至少一个内部函数,这样内部函数才能在私有作用域中形成闭包,并且可以访问或者修改私有的状态。

一个从函数调用返回的,只有数据属性而没有闭包函数的对象不是真正的模块。

ES6的模块没有”行内“格式,必须被定义在独立的文件中(一个文件一个模块)。

  • import可以将一个模块中的一个或多个API导入到当前作用域中,并分别绑定在一个变量上。
  • module会将整个模块的API导入并绑定到一个变量上
  • export会将当前模块的一个标识符导出为公共API

猜你喜欢

转载自blog.csdn.net/weixin_40764047/article/details/110951203