ホームページ > 記事 > ウェブフロントエンド > JavaScript はスコープchain_javascript スキルからのクロージャについて説明します
シェンマは閉鎖です
クロージャの概念に関しては、それは理にかなっています。
クロージャーは、別の関数のスコープ内の変数にアクセスできる関数を指します
この概念は少し複雑なので、詳しく見てみましょう。概念的には、クロージャには 2 つの特徴があります:
ES 6 より前の JavaScript には関数スコープの概念のみがあり、ブロックレベルのスコープの概念はありませんでした (ただし、catch でキャッチされた例外には catch ブロック内でのみアクセスできます) (IIFE はローカル スコープを作成できます)。各関数スコープは閉じられており、関数スコープ内の変数には外部からアクセスできません。
function getName() { var name = "美女的名字"; console.log(name); //"美女的名字" } function displayName() { console.log(name); //报错 }
しかし、美しい女性の名前を入手するために、諦めなかった独身男はコードを次のように変更しました:
function getName() { var name = "美女的名字"; function displayName() { console.log(name); } return displayName; } var 美女 = getName(); 美女() //"美女的名字"
今、美人は心を閉ざした女性であり、独身の男は好きなように遊ぶことができます。 (ただし、独身者が中国語で変数名を書くことはお勧めできませんので、学習しないでください)。
閉店に関して、さらに 3 つの点をお伝えしたいと思います。
1. クロージャは現在の関数の外部の変数にアクセスできます
function getOuter(){ var date = '815'; function getDate(str){ console.log(str + date); //访问外部的date } return getDate('今天是:'); //"今天是:815" } getOuter();
getDate はクロージャです。この関数が実行されると、変数 date は A に定義されませんが、親スコープで変数の定義を見つけることができます。
2. 外部関数が戻った場合でも、クロージャは外部関数
によって定義された変数に引き続きアクセスできます。function getOuter(){ var date = '815'; function getDate(str){ console.log(str + date); //访问外部的date } return getDate; //外部函数返回 } var today = getOuter(); today('今天是:'); //"今天是:815" today('明天不是:'); //"明天不是:815"
3. クロージャは外部変数の値を更新できます
function updateCount(){ var count = 0; function getCount(val){ count = val; console.log(count); } return getCount; //外部函数返回 } var count = updateCount(); count(815); //815 count(816); //816
スコープチェーン
クロージャが外部関数の変数にアクセスできるのはなぜですか?これはJavaScriptのスコープチェーンについてです。
Javascript には実行コンテキストの概念があり、変数または関数がアクセスできる他のデータを定義し、それぞれの動作を決定します。各実行環境には変数オブジェクトが関連付けられており、環境内で定義されたすべての変数と関数はこのオブジェクトに格納されます。 JavaScript ではこれを通常のオブジェクトとして扱うことができますが、プロパティの変更のみが可能であり、参照はできません。
変数オブジェクトにも親スコープがあります。変数にアクセスするとき、インタプリタはまず現在のスコープで識別子を検索します。見つからない場合は、変数の識別子が見つかるまで、または親スコープが存在しなくなるまで、親スコープに移動します。スコープチェーン。
スコープ チェーンはプロトタイプの継承に似ていますが、わずかな違いがあります。共通のオブジェクトのプロパティを検索し、現在のオブジェクトまたはそのプロトタイプで見つからない場合は、未定義のプロパティが返されます。スコープ チェーンに存在しない場合は、ReferenceError がスローされます。
スコープ チェーンの最上位はグローバル オブジェクトです。グローバル環境のコードの場合、スコープ チェーンにはグローバル オブジェクトという 1 つの要素のみが含まれます。したがって、グローバル環境で変数を定義すると、グローバル オブジェクトで変数が定義されます。関数が呼び出されるとき、スコープ チェーンには複数のスコープ オブジェクトが含まれます。
スコープチェーンについてもう少し詳しく説明します (レッドブックにはスコープと実行環境の詳細な説明があります)。簡単な例を見てみましょう:
// my_script.js "use strict"; var foo = 1; var bar = 2;
グローバル環境では、2 つの単純な変数が作成されます。前述したように、現時点では変数オブジェクトはグローバル オブジェクトです。
関数のネストなしで関数を作成するようにコードを変更します:
"use strict"; var foo = 1; var bar = 2; function myFunc() { //-- define local-to-function variables var a = 1; var b = 2; var foo = 3; console.log("inside myFunc"); } console.log("outside"); //-- and then, call it: myFunc();
当myFunc被定义的时候,myFunc的标识符(identifier)就被加到了当前的作用域对象中(在这里就是全局对象),并且这个标识符所引用的是一个函数对象(function object)。函数对象中所包含的是函数的源代码以及其他的属性。其中一个我们所关心的属性就是内部属性[[scope]]。[[scope]]所指向的就是当前的作用域对象。也就是指的就是函数的标识符被创建的时候,我们所能够直接访问的那个作用域对象(在这里就是全局对象)。
比较重要的一点是:myFunc所引用的函数对象,其本身不仅仅含有函数的代码,并且还含有指向其被创建的时候的作用域对象。
当myFunc函数被调用的时候,一个新的作用域对象被创建了。新的作用域对象中包含myFunc函数所定义的本地变量,以及其参数(arguments)。这个新的作用域对象的父作用域对象就是在运行myFunc时我们所能直接访问的那个作用域对象。
如前面所说,当函数返回没有被引用的时候,就会被垃圾回收器回收。但是对于闭包(函数嵌套是形成闭包的一种简单方式)呢,即使外部函数返回了,函数对象仍会引用它被创建时的作用域对象。
"use strict"; function createCounter(initial) { var counter = initial; function increment(value) { counter += value; } function get() { return counter; } return { increment: increment, get: get }; } var myCounter = createCounter(100); console.log(myCounter.get()); // 返回 100 myCounter.increment(5); console.log(myCounter.get()); // 返回 105
当调用createCounter(100)时,内嵌函数increment和get都有指向createCounter(100) scope的引用。如果createCounter(100)没有任何返回值,那么createCounter(100) scope不再被引用,于是就可以被垃圾回收。但是因为createCounter(100)实际上是有返回值的,并且返回值被存储在了myCounter中,所以对象之间的引用关系发生变化。
需要用点时间思考的是:即使createCounter(100)已经返回,但是其作用域仍在,并能且只能被内联函数访问。可以通过调用myCounter.increment() 或 myCounter.get()来直接访问createCounter(100)的作用域。
当myCounter.increment() 或 myCounter.get()被调用时,新的作用域对象会被创建,并且该作用域对象的父作用域对象会是当前可以直接访问的作用域对象。
当执行到return counter;时,在get()所在的作用域并没有找到对应的标示符,就会沿着作用域链往上找,直到找到变量counter,然后返回该变量,调用increment(5)则会更有意思。当单独调用increment(5)时,参数value会存贮在当前的作用域对象。函数要访问value,能马上在当前作用域找到该变量。但是当函数要访问counter时,并没有找到,于是沿着作用域链向上查找,在createCounter(100)的作用域找到了对应的标示符,increment()就会修改counter的值。除此之外,没有其他方式来修改这个变量。闭包的强大也在于此,能够存贮私有数据。
Similar function objects, different scope objects
对于上面的counter示例,再说点扩展的事。看代码:
//myScript.js "use strict"; function createCounter(initial) { /* ... see the code from previous example ... */ } //-- create counter objects var myCounter1 = createCounter(100); var myCounter2 = createCounter(200);
myCounter1 和 myCounter2创建之后,关系图是酱紫的:
在上面的例子中,myCounter1.increment和myCounter2.increment的函数对象拥有着一样的代码以及一样的属性值(name,length等等),但是它们的[[scope]]指向的是不一样的作用域对象。
这才有了下面的结果:
var a, b; a = myCounter1.get(); // a 等于 100 b = myCounter2.get(); // b 等于 200 myCounter1.increment(1); myCounter1.increment(2); myCounter2.increment(5); a = myCounter1.get(); // a 等于 103 b = myCounter2.get(); // b 等于 205
作用域和this
作用域会存储变量,但this并不是作用域的一部分,它取决于函数调用时的方式。关于this指向的总结,可以看这篇文章:JavaScript面试问题:事件委托和this