(1) 範囲
変数のスコープとは、プログラムのソースコード内で定義された変数の領域です。
1. JS で字句スコープが使用されます
関数内で宣言されていない変数 (関数内で var が省略されている場合もグローバルとみなされます) は、グローバル変数 (グローバル スコープ) と呼ばれます
関数内で宣言された変数は関数スコープを持ち、ローカル変数です
ローカル変数はグローバル変数よりも優先されます
var name="one"; function test(){ var name="two"; console.log(name); //two } test();
関数内で var を省略すると、実際にはグローバル変数に書き換えられるため、グローバル変数に影響します
var name="one"; function test(){ name="two"; } test(); console.log(name); //two
関数スコープ、つまり関数はスコープの基本単位です。js には if for などの c/c のようなブロックレベルのスコープがありません。
function test(){ for(var i=0;i<10;i++){ if(i==5){ var name = "one"; } } console.log(name); //one } test(); //因为是函数级作用域,所以可以访问到name="one"
もちろん、js では高階関数も使用されており、実際には入れ子関数として理解できます
function test1(){ var name = "one"; return function (){ console.log(name); } } test1()();
test1()の後、外側の関数が呼び出され、内側の関数が返され、Continue()すると、それに応じて内側の関数が呼び出されて実行されるため、「one」
が出力されます。
ネストされた関数にはクロージャが含まれます。これについては後で説明します。ここで、内側の関数は外側の関数で宣言された変数名にアクセスできます。これにはスコープ チェーン メカニズムが関係します
2. 事前にJSで宣言
js の関数スコープは、関数内で宣言されたすべての変数が関数本体内で常に表示されることを意味します。また、宣言前に変数を使用することもできます。この状況をホイスティング
と呼びます。
ヒント: 事前宣言は、JS エンジンのプリコンパイル時に行われます。事前宣言は、コードが実行される前に発生します。
たとえば
var name="one"; function test(){ console.log(name); //undefined var name="two"; console.log(name); //two } test();
上記により次のような効果が得られます
var name="one"; function test(){ var name; console.log(name); //undefined name="two"; console.log(name); //two } test();
var をもう一度削除してみますか?これはグローバル変数になった関数内の名前なので、未定義ではなくなりました
var name="one"; function test(){ console.log(name); //one name="two"; console.log(name); //two } test();
3. テストにパラメータがある場合は、上記のパラメータが渡されないことに注意してください。
function test(name){ console.log(name); //one name="two"; console.log(name); //two } var name = "one"; test(name); console.log(name); // one
前に述べたように、基本型は値によって渡されるため、テストに渡される名前は実際には単なるコピーであり、関数が返された後にこのコピーはクリアされます。
関数内の name="two" がグローバル名を変更するとは考えないでください。これらは 2 つの独立した名前であるためです
(2) スコープチェーン
上記の高度な機能にはスコープ チェーンが含まれます
function test1(){ var name = "one"; return function (){ console.log(name); } } test1()();
1. 大きな段落で説明します。
js コードの各部分 (グローバル コードまたは関数) には、スコープ チェーンが関連付けられています。
このスコープ チェーンは、オブジェクトのリストまたはリンクされたリストであり、このオブジェクトのグループは、このコードの「スコープ内」の変数を定義します。
js が変数 x の値を見つける必要がある場合 (このプロセスは変数解決と呼ばれます)、このオブジェクトに x という名前の属性がある場合、この属性の値は次のようになります。最初のオブジェクトに x という名前の属性がない場合、js はチェーン内の次のオブジェクトの検索を続けます。 2 番目のオブジェクトに x という名前の属性がまだない場合は、引き続き次のオブジェクトの検索が続きます。スコープ チェーン内のオブジェクトに属性 x が含まれていない場合、x はこのコードのスコープ チェーンに存在しないとみなされ、最終的に ReferenceError 例外がスローされます。
2. スコープチェーンの例:
js のトップレベル コード (つまり、関数定義が含まれていないコード) では、スコープ チェーンはグローバル オブジェクトで構成されます。
ネストを含まない関数本体には、スコープ チェーン上に 2 つのオブジェクトがあります。1 つ目は関数パラメータとローカル変数を定義するオブジェクトで、2 つ目はグローバル オブジェクトです。
ネストされた関数本体では、スコープ内に少なくとも 3 つのオブジェクトがあります。
3. スコープチェーン作成ルール:
関数が定義されると (定義されたときに関数が開始されることに注意してください)、実際にはスコープ チェーンが保存されます。
この関数が呼び出されると、そのパラメーターまたはローカル変数を格納するための新しいオブジェクトが作成され、そのオブジェクトがそのスコープ チェーンに追加され、関数呼び出しスコープの新しいより長い表現「チェーン」が作成されます。
入れ子関数の場合は、状況が再び変わります。外部関数が呼び出されるたびに、内部関数が再定義されます。外部関数が呼び出されるたびにスコープ チェーンが異なるためです。内部関数は定義されるたびに微妙に異なる必要があります。内部関数のコードは外部関数が呼び出されるたびに同じであり、このコードに関連付けられたスコープ チェーンも異なります。
(ヒント: 上記の 3 つのポイントをよく理解して、覚えておいてください。自分の言葉で言うのが最善です。そうしないと、面接官が直接「スコープ チェーンについて説明してください」と質問するため、暗記する必要があります。 )
スコープチェーンの実際的な例:
var name="one"; function test(){ var name="two"; function test1(){ var name="three"; console.log(name); //three } function test2(){ console.log(name); // two } test1(); test2(); } test();
上边是个嵌套函数,相应的应该是作用域链上有三个对象
那么在调用的时候,需要查找name的值,就在作用域链上查找
当成功调用test1()的时候,顺序为 test1()->test()->全局对象window 因为在test1()上就找到了name的值three,所以完成搜索返回
当成功调用test1()的时候,顺序为 test2()->test()->全局对象window 因为在test2()上没找到name的值,所以找test()中的,找到了name的值two,就完成搜索返回
还有一个例子有时候我们会犯错的,面试的时候也经常被骗到。
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd"> <html xmlns="http://www.w3.org/1999/xhtml"> <head> <script type="text/javascript"> function buttonInit(){ for(var i=1;i<4;i++){ var b=document.getElementById("button"+i); b.addEventListener("click",function(){ alert("Button"+i); //都是 Button4 },false); } } window.onload=buttonInit; </script> </head> <body> <button id="button1">Button1</button> <button id="button2">Button2</button> <button id="button3">Button3</button> </body> </html>
为什么?
根据作用域链中变量的寻找规则:
b.addEventListener("click",function(){ alert("Button"+i); },false);
这里有一个函数,它是匿名函数,既然是函数,那就在作用域链上具有一个对象,这个函数里边使用到了变量i,它自然会在作用域上寻找它。
查找顺序是 这个匿名函数 -->外部的函数buttonInit() -->全局对象window
匿名函数中找不到i,自然跑到了buttonInit(), ok,在for中找到了,
这时注册事件已经结束了,不要以为它会一个一个把i放下来,因为函数作用域之内的变量对作用域内是一直可见的,就是说会保持到最后的状态
当匿名函数要使用i的时候,注册事件完了,i已经变成了4,所以都是Button4
那怎么解决呢?
给它传值进去吧,每次循环时,再使用一个匿名函数,把for里边的i传进去,匿名函数的规则如代码
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd"> <html xmlns="http://www.w3.org/1999/xhtml"> <head> <script type="text/javascript"> function buttonInit(){ for(var i=1;i<4;i++){ (function(data_i){ var b=document.getElementById("button"+data_i); b.addEventListener("click",function(){ alert("Button"+data_i); },false); })(i); } } window.onload=buttonInit; </script> </head> <body> <button id="button1">Button1</button> <button id="button2">Button2</button> <button id="button3">Button3</button> </body> </html>
这样就可以 Button1..2..3了
4.上述就是作用域链的基本描述,另外,with语句可用于临时拓展作用域链(不推荐使用with)
语法形如:
with(object)
statement
这个with语句,将object添加到作用域链的头部,然后执行statement,最后把作用域链恢复到原始状态
简单用法:
比如给表单中各个项的值value赋值
一般可以我们直接这样
var f = document.forms[0]; f.name.value = ""; f.age.value = ""; f.email.value = "";
引入with后(因为使用with会产生一系列问题,所以还是使用上面那张形式吧)
with(document.forms[0]){ f.name.value = ""; f.age.value = ""; f.email.value = ""; }
另外,假如 一个对象o具有x属性,o.x = 1;
那么使用
with(o){ x = 2; }
就可以转换成 o.x = 2;
假如o没有定义属性x,它的功能就只是相当于 x = 2; 一个全局变量罢了。
因为with提供了一种读取o的属性的快捷方式,但他并不能创建o本身没有的属性。
要理解变量的作用域范围就得先理解作用域链
用var关键字声明一个变量时,就是为该变量所在的对象添加了一个属性。
作用域链:由于js的变量都是对象的属性,而该对象可能又是其它对象的属性,而所有的对象都是window对象的属性,所以这些对象的关系可以看作是一条链
链头就是变量所处的对象,链尾就是window对象
看下面的代码: