你不知道的JS上卷第一部分

147 阅读7分钟

你不知道的JS(上卷)第一部分 作用域和闭包

1.变量赋值操作的剖析

变量的赋值操作会执行两个动作,首先编译器会在当前的作用域生成一个变量(如果之前没有声明过),然后运行时引擎会在该作用域中查找该变量,如果能找到就会对他进行赋值。

2.LHS和RHS

一道练习题帮你理解回顾LHS和RHS

function foo(a){
  var b = a
  return a+b
}
var c = foo(2)

(其中LHS执行了三次!RHS执行了四次!)

3.什么时候出现ReferenceError和TypeError异常?

RHS在所有的嵌套作用域中都没有查找到所需的变量,引擎就会抛出一个ReferenceError异常。相较之下,当引擎执行RHS查询时,如果在嵌套作用域中没有找到所需的变量,就会在全局作用域中创建一个具有这个名字的变量,然后将这个变量返回给引擎,前提是程序运行在非严格模式下。严格模式下是禁止自动或者隐试的创建全局变量。严格模式下也会报错ReferenceError。当RHS成功查询但是对这个变量进行非正当的使用的时候(比如对一个非函数类型的值进行函数执行,应用一个null或者更undefined变量的属性方法)就会返回TypeError。ReferenceError和作用域的判别失败相关,TypeError则代表了作用域判别成功了,但是对结果的 操作是非法的或者不合理的。

4.词法作用域是什么?

词法作用域意味着作用域是由书写代码时的位置来决定的,无论函数在哪里被调用,或者它如何被调用,它的词法作用域都只由函数被声明时所处的位置决定。编译的词法分析阶段基本能知道全部的标识符在哪里以及是如何声明的,从而能预测在执行过程中如何对它们进行查找。JavaScript中有两个机制可以欺骗词法作用域:eval()和with。使用其中任何一个机制都会导致代码的运行速度变慢,不要使用它们。

5.如何解决全局作用域污染问题?

方法:在想要隐藏的代码片段外面加上一层函数的封装,这样封装的内容就只能在这个函数内部访问,这个函数内部也就有了自己的作用域,就算这个函数作用域中有和外部相同名称的变量,也是相互不回影响的,相当于与世隔绝。代码如下:

var a = 2
function foo () {
  var a = 10
}
foo()
console.log(a)//2

但是这样做还是有瑕疵的,因为foo这个函数也在全局作用域中,而且也会在全局作用域中被调用,那么有没有更好的方式呢?答案是肯定的,进阶代码如下:

var a = 2
;(function foo (b) {
  var a = b
})(10)//传参
foo()//ReferenceError: foo is not defined
console.log(a)//2

这里使用的是函数表达式的方式来解决的,函数表达式的好处就是让foo函数没有在全局作用域中出现,全局作用域中也不能调用foo这个函数。这样这个问题就完美解决了!

6.块级作用域

一般情况下的JS是只有函数作用域而没有块级作用域的。比如

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

其实这个i我们只是想在for循环内部使用而不是在全局作用域中使用,但是我们这样写依然可以在全局作用域中使用i。解决这个问题的方法很简单,就是用let代替var,因为let是有块级作用域的!

还有一种很少见到的情况就是try/catch中的catch分句会创建一个块级作用域,其中声明的变量仅在catch内部有效。

7.什么是提升,为什么会有提升?

回忆一下,引擎会在执行JS代码之前首先对代码进行编译,编译阶段一部分工作就是找到所有的声明,明确这些声明的位置,并且把这些声明绑定到相关的作用域中。由此我们很容易发现,编译器比引擎先开始工作,所以在执行JS代码之前,所有的声明就已经都找到了,这样的结果就打破了平时认为JS代码是一行一行执行的观念,就出现了所谓的 “提升” 。

概要:先有声明,后有赋值

对比如下代码,有助于理解:

//代码片段1:
a = 2
var a//提升
console.log(a) //输出2


//代码片段2:
console.log(a) //输出undefined
var a = 2
//声明可以提升 var a 是可以提升的,但是赋值不可以提升,a=2是不能提升的。

变量有提升,函数声明也是有提升的,而且函数的声明的提升是高于变量的提升的!在此要注意函数表达式是不会有提升的。

8.闭包是什么?

当函数可以记住并访问所在的词法作用域时,就产生了闭包,即使函数是在当前的词法作用域之外执行。闭包使得函数可以继续访问定义时的词法作用域。如果函数在当前的词法作用域内执行的话,我们习惯上将它理解为RHS,其实这也是闭包。当函数在当前词法作用域之外执行的时候,我们就可以名正言顺的叫它闭包了!

下面是一个闭包的例子:

function foo(){
  var a = 2
  function baz(){
    console.log(a)
  }
  bar(baz)
}
function bar (fn){
  fn()//妈妈快看,这就是闭包!
}
foo()

在foo函数内部,声明了一个baz函数,这个函数可以访问foo内部的作用域,然后将baz函数传递给bar函数,并且在bar函数内部调用了baz函数(也就是fn函数),此时baz函数的调用已经脱离了定义时的词法作用域,但是依旧是可以访问定义时词法作用域的变量a,这就是传说中的闭包!

闭包和循环--学了一年JS至今才明白的问题

首先看这段代码:

for(var i=0;i<5;i++){
  setTimeout(function(){
    console.log(i)
  },1000*i)
}
//输出结果是5个5

延迟函数的回调会在循环结束的时候才执行。所以输出5个5是意料之中的结果。但是我们需要深入剖析一下这个问题,虽然每次迭代的时候都会定义一个函数,但是这5个函数都是使用了一个词法作用域,其实这五个函数中的i都是同一个i,这个i就是共享词法作用域(全局作用域)中的那个i。

如果我们的需求是依次输出0到4,该怎么办?

代码如下:

for(var i=0;i<5;i++){
  (function (){
    var k = i
    setTimeout(function(){
      console.log(k)
    },1000*k)
  })()
}
//我们在for循环内部加了一层词法作用域,就是我们看到的IIFE,在IIFE中我们定义了一个变量,k,这个k可以存储每次迭代过程中,i的值,每一个迭代都会创建一个新的IIFE所以,一共有五个IIFE,互不干扰,这样的话,我们再次输出k的值,都会在各自的IIFE中查找,找到之后就不会再到最外层的那个全局作用域中找。

这段代码还可以简化:

for(var i=0;i<5;i++){
  (function (k){
    setTimeout(function(){
      console.log(k)
    },1000*k)
  })(i)
}
//或者
for(let i=0;i<5;i++){
    setTimeout(function(){
      console.log(i)
    },1000*i)
}

第一段代码比较好理解,第二段代码中let本身就可以形成块级作用域。至此,问题完美解决!

9.闭包在模块中的应用

当通过返回一个含有属性引用的对象的方式来将函数传递到词法作用域外部时,我们就已经创造了可以观察和实践闭包的条件!

看一个模块的例子:

function CoolModule() {
  var something = 'cool'
  var another = [1,2,3]
  function doSomething(){
    console.log(something)
  }
  function doAnother (){
    console.log(another.join('!'))
  }
  return {
    doSomething: doSomething,
    doAnother: doAnother
  }
}

var fn = CoolModule()
fn.doAnother()
fn.doSomething()

CoolModule封装了两个变量和两个函数,并且这两个函数可以访问这两个变量,然后将这两个函数返回(暴露),这样在其他的作用域中也就可以访问CoolModule中的两个变量。

如果要更简单的描述,模块模式需要具备两个必要的条件:

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

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