许多人发现下面的概念是js中复杂的部分:
其实,这些概念要比它们看起来更加容易理解,尤其是了解了执行上下文相关的知识。
这三个部分有什么共同点?它们都与变量查找有关-js引擎查找变量的方法。
变量查找
在下面的例子中,变量查找可能是令人迷惑的。
当执行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有一个规则:就是词法作用域定义由函数的出生地决定
让我们从词法作用域的角度看下面的例子
在这个例子中,isApple和isBanana函数是在全局作用被声明,因此,它们的词法作用域是在全局作用域。
当js引擎编译脚本时,所有函数执行上下文里的outer都指向全局执行上下文。
为了更好的理解这个特点,让我们来另一个例子。我们将每一个函数缩进到前一个函数的里面,而不是将所有函数都声明在全局作用域。
在这个例子里:
在词法作用域的基础上,我们可以推理出每个执行上下文的outer
执行结束后,控制台打印30
闭包
闭包理解起来比听起来更加直观,让我们来看一个例子
在返回util并将其分配给price变量之前,我们有以下调用堆栈。
在返回util之后,applePrice函数执行结束,并且它的执行上下文被移除。
与此同时,变量环境和词法环境消失,并且它们内部的变量应该被销毁。
此时,js的一个规则起作用了—一个内函数总是可以访问它外函数的变量
在这里,内涵数是getPrice
和setPrice,外函数是
applePrice
getPrice函数使用两个变量fruit
和 price,而
setPrice函数使用price变量
根据这个规则,fruit和price变量应该被保存在一个分离的区域,这是一个仅仅只能被getPrice和setPrice函数访问的独立的区域,也被称为闭包。
与此同时,discount
变量被销毁,因为没有方法有引用指向它。
接下来,继续执行代码,并调用setPrice函数,js引擎浏览作用域链,并在闭包中定位到price变量。price的值被设置为“20”。
在最后一行,getPrice
函数被调用,通过同样的链式查找,js引擎在闭包中找到fruit和
price变量,并打印“apple”和“20”。
执行结束。
通过在chrome中运行这个例子的代码,你可以在它的开发者工具中看到闭包
“This”不是作用域链的一部分
我们已经接触了执行上下文的三个组成部分
最后一个是this
每个作用域都有自己的this
如果我们在全局作用域中打印this,我们将会收到一个window对象。
window对象是this和作用域概念联系的唯一元素,因为它是位于作用域链的根部,作为全局作用域的一部分。
函数作用域中的this是怎样的呢?
this是指applePrice函数吗?
令人意外的是,控制台会打印window对象,跟它在全局作用域中运行一样。
this跟任何作用域没有关联。
但是this是什么?它是否总是指向window对象?
this是什么?
让我们看下一个例子
在这个例子中,getPrice函数打印“10”,getThis函数打印apple对象。
所以我们找到了答案:谁调用这个方法说就是this。
outer是在编译阶段被定义,而this是由执行阶段决定。
当一个函数被声明,它被附属到window对象上,当你执行一个函数,window对象正是函数的调用者,因此,this指向window对象。
我们可以通过改变调用者来重置this
在最后一行,我们通过call函数来改变this指向banana对象
当js引擎执行最后一行代码时,正是banana对象在调用函数,因此this是banana,控制台打印“20”。
this转换称作用域概念
尽管this跟作用域没有任何关系,但我们可以简单地将它转换称作用域概念
下面的例子展示了一个使用this时的经典问题
谁调用了discount
函数?
乍一看,像是getPrice
函数在调用它,然而,控制台却打印window对象
到目前位置,我们知道一个函数(或方法)不是由函数调用,而是由window或者一个对象调用,在这个例子中,是window对象调用了discount函数。
这是js设计的一个缺陷,this不能够从外部作用域继承,因为它从来都不是作用域的一部分。
我们可以通过将this赋值给一个局部变量来很快修复这个问题
通过这么做,作用域链可以正常工作。
自从ES6后,我们可以通过使用箭头函数来避免使用一个多余的self变量。
箭头函数没有将this转换称作用域概念,而是,它没有创建执行上下文,并且共享外部函数相同的this。
总结: