前言
在What is Scope与Lexical Scope对辞法範围做具体的定义与基本概念介绍,也了解作用域的嵌套与找寻变量的像上搜寻,在本章节中将介绍什么是作用域链
,可以把它想像为是连接着嵌套作用域之间的通道
,而这个作用域链是固定方向的,它意味着变量的查询只能往上/外层作用域移动。
"Lookup" is Conceptual
我们在前两章中使用了这个程式来区分作用域,若我们在for...loop中呼叫了students
这个变量,我们在Lexical Scope提到了lookup的概念,JS引擎会先在当前的作用域中查询使否有宣告这个变量,若没有会持续向上层作用域中查询直到全域作用域中,这个查询的过程有助于我们了解作用域个概念,但在正常的情况下JS通常是不这么做的
。
通常在编译的过程中就会决定好变量的scope,基于这个原理,所以变量的作用域是不会根据程式运行时发生变化,由于scope是在编译中得知并且不可变的,所以这个scope的讯息有可能与AST储存在一起,然后当程式运行的时候使用这个讯息。
换句话说,其实JS并不需要像Lexical Scope说的要一一遍历作用域来找到这个变量在哪个作用域中宣告,因为这个讯息其实在编译期间就知道了,透过不需要花费时间进行搜寻,它可以使我们的程序更高效能的运行,
不过凡事都有个例外,若我们使用了一个从未在任何地方宣告个变数,因为其实每一个专案对于JS来说都是独立的,所以就算在这个专案中没有办法找这个变量的宣告,但是他也有可能被宣告在共享的全域範围中,所以不一定会是个错误。
因此可能将这个结果(变量準确宣告的位置)需要推迟到运行的时候才知道,所以当JS还没编译到其他专案的这个期间,这个变量都使处于无法确定作用域的情况,而延迟查找最终才会将这个变量宣告(有可能是全域中)但是这个动作只需要进行一次,因为作用域是无法被更改的。
Shadowing
当两个或多个变量(每个同名的变量都存在于不同作用域中)时,具有不同lexical scope的问题就变个更重要,值得注意的是,当你在同一个作用域中,你不能同时宣告同一个名称的变量,他只能接受一个名字一个宣告,因此若需要使用两个或多个同名的变量则需要使用嵌套作用域。
var studentName = "Suzy";function printStudent(studentName) { studentName = studentName.toUpperCase(); console.log(studentName);}printStudent("Frank"); // FRANKprintStudent(studentName); // SUZYconsole.log(studentName); // Suzy
在上面这段程式中我们在第一行中(全域作用域)宣个了一个变量studentName
,而相同名字的变量我们也宣告了一个,但是他是存在于printStudent(...)作用域中。
藉由lookup的概念,当我们使用了一个变量时,他会从当前的作用中往上查询,一但找到匹配的变量就会停止,所以当function中的console.log(...)使用到studentName这个变量,他先在当前作用域中(printfStudent(...))寻找,发现在当前作用域中就找到了这个变量的宣告,那么他就会停止收寻并直接使用这个变量(不会考虑全域作用域中的studentName)。
这个概念便是lexcal scope中的shadowing
,printfStudent(...)作用域中的studentName shadowing了全域作用域中的studentName,若是你需要使用到外部作用域的变量,那么你就不能在自身作用域中宣告一个相同名字的变量,因为这样做他将永远只会使用自身作用域的变量。
Global Unshadowing Trick
注意!这不是一个好的方法,他可能造成你的程式发生非预期的错误
。
其实有方法可以在被变量shadowing的情况下使用到全域中的变量。
var studentName = "Suzy";function printStudent(studentName) { console.log(studentName); console.log(window.studentName); //uee global}// "Frank"// "Suzy"
可以使用window.studentName
这样的方法来访问到全域中的studentName,这是唯一能够在shadowing情况下访问到外部作用域变数的方法。
这个技巧只适用于访问全域作用域中的变量(不能访问到非全域的上一层作用域)。
var special = 42;function lookingFor(special) { function keepLooking() { var special = 3.141592; console.log(special); console.log(window.special); } keepLooking();}lookingFor(112358132134);// 3.141592// 42
在全域中宣告了一个special,也在lookingFor(...)和keepLooking(...)中宣告了
一样名字的变数,在keepLooking(...)中使用了special变数,可是因为他自身的作用域中就有宣告这个变数,所以就直接使而不会使用lookingFor(...)和全域的,而下一行使用window
则会直接访问到全域作用域中的变数,所以使用window只能访问全域中的变数而不能访问非全域的上一层作用域
。
即使是全域也只能访问到var
与function
。
var one = 1;let notOne = 2; //use letconst notTwo = 3; //use constclass notThree {} //objectconsole.log(window.one); // 1console.log(window.notOne); // undefinedconsole.log(window.notTwo); // undefinedconsole.log(window.notThree); // undefined
Copying Is Not Accessing
var special = 42;function lookingFor(special) { var another = { // use object to copy value of arguments to another.special special: special }; function keepLooking() { var special = 3.141592; console.log(special); console.log(another.special); // Ooo, tricky! console.log(window.special); } keepLooking();}lookingFor(112358132134);// 3.141592// 112358132134// 42
也可以透过这种方法访问到上层作用域,这个概念是将原本被shadowing的变数複製给另一个变量
,那么这个被複製的变量就可以被访问到(除非他也被shadowing),但是这个行为并不代表是特殊的访问方法,他只是藉由另一个容器(拥有被shadowing的变量的值)(有点饶舌)
让我们可以访问到他,它意味着我们访问的并不是lookingFor(...)中的special。
Illegal Shadowing
不是所有宣告都能够shadowing上一层作用域的变数,let可以shadowing var
,但var不能shadowing let
function something() { var special = "JavaScript"; { let special = 42; // totally fine shadowing }}function another() { { let special = "JavaScript"; { var special = "JavaScript"; // ^^^ Syntax Error } }}
因为var宣告会试图跨越同名的let宣告範围,换句话说使用let宣告的这个special会阻止var的hoasting,所以就导致了错误。
Funciton Name Scope
var askQuestion = function ofTheTeacher() { console.log(ofTheTeacher);};askQuestion(); // function ofTheTeacher()...console.log(ofTheTeacher); // ReferenceError: ofTheTeacher is not defined
当我们呼叫askQuestion时,有成功console出ofTheTeacher(...),但是直接console ofTheTeacher却无法成功,是因为这是一个funciton expression
,对于ofTheTeacher(...)来说,他只存活在赋予askQuestion这段时间,一但他将自身赋予给了askQuestion后他便会消失,所以在其他地方呼叫他才会是not defined。
Arrow Functions
在ES6中加入了箭头函数
var askQuestion = () => { //...}
使用=>
让我们可以不需要对这个function命名,由于这特性,意味没办法透过function名称来呼叫他,虽然箭头函数是匿名的形式,但是他的辞法规则与正常的function一样,无论带不带有{...}的箭头函数都会创建一个单独的内部作用域,这个作用域的规则与function一样。
参考文献:
You Don't Know JavaScript