[JS] You Don't Know JavaScript [Scope & Closures]

前言

在What is Scope与Lexical Scope对辞法範围做具体的定义与基本概念介绍,也了解作用域的嵌套与找寻变量的像上搜寻,在本章节中将介绍什么是作用域链,可以把它想像为是连接着嵌套作用域之间的通道,而这个作用域链是固定方向的,它意味着变量的查询只能往上/外层作用域移动。

"Lookup" is Conceptual

http://img2.58codes.com/2024/20124767K5alkLKbYA.png
我们在前两章中使用了这个程式来区分作用域,若我们在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只能访问全域中的变数而不能访问非全域的上一层作用域


即使是全域也只能访问到varfunction

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


关于作者: 网站小编

码农网专注IT技术教程资源分享平台,学习资源下载网站,58码农网包含计算机技术、网站程序源码下载、编程技术论坛、互联网资源下载等产品服务,提供原创、优质、完整内容的专业码农交流分享平台。

热门文章