JavaScript执行上下文之作用域链,闭包和this(四)

许多人发现下面的概念是js中复杂的部分:

  • 作用域链
  • 闭包
  • this

其实,这些概念要比它们看起来更加容易理解,尤其是了解了执行上下文相关的知识。

这三个部分有什么共同点?它们都与变量查找有关-js引擎查找变量的方法。

变量查找

在下面的例子中,变量查找可能是令人迷惑的。

JavaScript执行上下文之作用域链,闭包和this(四)_第1张图片

JavaScript执行上下文之作用域链,闭包和this(四)_第2张图片

当执行isApple函数时,执行栈中有三个堆叠的执行上下文。

  • 全局执行上下文
  • isBanana 函数执行上下文
  • isApple函数执行上下文

接下来console开始查找apple变量。

直观来说,我们可以通过调用栈从上到下的流程来分析链式查找,控制台将会打印“banana”,因为它在isBanana函数的执行上下文中查找apple变量。

恰恰相反,控制台实际打印的是在全局执行环境的apple变量。

为什么?

 

Outer

我们链式查找时遗漏了另一个执行上下文的关键组件-外部

outer定义了js引擎如何处理链式查找,也就是众所周知的作用域链。

如果我们研究一下isApple的执行上下文,就会发现它的outer指向全局执行上下文。

在这个例子中,js引擎在iSApple执行上下文中查找apple变量失败后,立即会去全局执行上下文中查找。

困惑解决了吗?

不完全是,outer概念引出了另一个问题

为什么isApple执行上下文中的outer指向的时全局执行上下文而不是isBanana执行上下文?

毕竟,isApple函数是在isBanana函数内部调用的,作用域链不应该跟随调用栈吗?

另外,js的作用域是被词法作用域定义的,从来不是受调用栈影响的。

从两步过程的角度,作用域是定义在编译阶段,而不是在执行阶段。

为了更好地回答这个问题,我们需要了解js如何设计它的词法作用域。

词法作用域(Lexical scope)

js有一个规则:就是词法作用域定义由函数的出生地决定

让我们从词法作用域的角度看下面的例子

JavaScript执行上下文之作用域链,闭包和this(四)_第3张图片

在这个例子中,isApple和isBanana函数是在全局作用被声明,因此,它们的词法作用域是在全局作用域。

当js引擎编译脚本时,所有函数执行上下文里的outer都指向全局执行上下文。

为了更好的理解这个特点,让我们来另一个例子。我们将每一个函数缩进到前一个函数的里面,而不是将所有函数都声明在全局作用域。

JavaScript执行上下文之作用域链,闭包和this(四)_第4张图片

在这个例子里:

  • 函数priceA是定义在全局作用域
  • 函数priceB是定义在函数priceA作用域
  • 函数priceC是定义在函数priceB作用域

在词法作用域的基础上,我们可以推理出每个执行上下文的outer

  • 在priceC执行上下文,outer指向priceB的执行上下文
  • 在priceB执行上下文,outer指向priceA的执行上下文
  • 在priceA执行上下文,outer指向全局执行上下文

执行结束后,控制台打印30

闭包

闭包理解起来比听起来更加直观,让我们来看一个例子

JavaScript执行上下文之作用域链,闭包和this(四)_第5张图片

 

在返回util并将其分配给price变量之前,我们有以下调用堆栈。

在返回util之后,applePrice函数执行结束,并且它的执行上下文被移除。

与此同时,变量环境和词法环境消失,并且它们内部的变量应该被销毁。

此时,js的一个规则起作用了—一个内函数总是可以访问它外函数的变量

在这里,内涵数是getPricesetPrice,外函数是applePrice

getPrice函数使用两个变量fruit 和 price,而setPrice函数使用price变量

根据这个规则,fruit和price变量应该被保存在一个分离的区域,这是一个仅仅只能被getPrice和setPrice函数访问的独立的区域,也被称为闭包

与此同时,discount 变量被销毁,因为没有方法有引用指向它。

接下来,继续执行代码,并调用setPrice函数,js引擎浏览作用域链,并在闭包中定位到price变量。price的值被设置为“20”。

在最后一行,getPrice 函数被调用,通过同样的链式查找,js引擎在闭包中找到fruitprice变量,并打印“apple”和“20”。

执行结束。

通过在chrome中运行这个例子的代码,你可以在它的开发者工具中看到闭包

“This”不是作用域链的一部分

我们已经接触了执行上下文的三个组成部分

  • 变量环境
  • 词法环境
  • outer

最后一个是this

每个作用域都有自己的this

如果我们在全局作用域中打印this,我们将会收到一个window对象。

window对象是this和作用域概念联系的唯一元素,因为它是位于作用域链的根部,作为全局作用域的一部分。

函数作用域中的this是怎样的呢?

JavaScript执行上下文之作用域链,闭包和this(四)_第6张图片JavaScript执行上下文之作用域链,闭包和this(四)_第7张图片

this是指applePrice函数吗?

令人意外的是,控制台会打印window对象,跟它在全局作用域中运行一样。

this跟任何作用域没有关联。

但是this是什么?它是否总是指向window对象?

this是什么?

让我们看下一个例子

JavaScript执行上下文之作用域链,闭包和this(四)_第8张图片

在这个例子中,getPrice函数打印“10”,getThis函数打印apple对象。

所以我们找到了答案:谁调用这个方法说就是this。

outer是在编译阶段被定义,而this是由执行阶段决定。

当一个函数被声明,它被附属到window对象上,当你执行一个函数,window对象正是函数的调用者,因此,this指向window对象。

我们可以通过改变调用者来重置this

JavaScript执行上下文之作用域链,闭包和this(四)_第9张图片

在最后一行,我们通过call函数来改变this指向banana对象

当js引擎执行最后一行代码时,正是banana对象在调用函数,因此this是banana,控制台打印“20”。

this转换称作用域概念

尽管this跟作用域没有任何关系,但我们可以简单地将它转换称作用域概念

下面的例子展示了一个使用this时的经典问题

JavaScript执行上下文之作用域链,闭包和this(四)_第10张图片

谁调用了discount 函数?

乍一看,像是getPrice 函数在调用它,然而,控制台却打印window对象

到目前位置,我们知道一个函数(或方法)不是由函数调用,而是由window或者一个对象调用,在这个例子中,是window对象调用了discount函数。

这是js设计的一个缺陷,this不能够从外部作用域继承,因为它从来都不是作用域的一部分。

我们可以通过将this赋值给一个局部变量来很快修复这个问题

JavaScript执行上下文之作用域链,闭包和this(四)_第11张图片

通过这么做,作用域链可以正常工作。

自从ES6后,我们可以通过使用箭头函数来避免使用一个多余的self变量。

JavaScript执行上下文之作用域链,闭包和this(四)_第12张图片

箭头函数没有将this转换称作用域概念,而是,它没有创建执行上下文,并且共享外部函数相同的this。

总结:

  • outer定义了变量的链式查找,又称作作用域链
  • 词法作用域定义了outer,并且函数定义在哪里,词法作用域就设置在哪里
  • 作用域链是在编译阶段被决定的,不是在执行阶段。因此,一个函数的调用发生在执行阶段,不会影响它的作用域
  • 闭包出现是由于词法作用域规则,一个内涵数总是可以访问它外函数的变量,它是独立的,对函数保有变量的引用
  • this不是作用域概念,谁调用函数,this就是谁

你可能感兴趣的:(js,前端)