ES6 より前は、変数は var を使用して宣言されており、変数の事前解析が行われていました (関数にも事前解析がありました)。let と const を初めて学習したとき、多くの学生が事前解析に戸惑ったと思います。 ES6 で導入されましたが、現段階では ES6 は完全には普及しておらず、古いコードの多くは依然として ES5 標準、さらには ES3 標準に従って書かれています。
01 メモリ内の変数と関数の表示
JavaScript の変数の型は、基本データ型と参照データ型を含め、他の言語と同じです。基本的なデータ型には、未定義、null、ブール、文字列、数値が含まれます。参照データ型は主にオブジェクトです ({}、[]、/^$/、日付、関数など)。
var num = 24; var obj = {name:'iceman' , age:24}; function func() { console.log('hello world'); }
ブラウザが HTML ページをロードすると、まずグローバル スコープと呼ばれるグローバル JavaScript コード実行のための環境が提供されます。
基本データ型は値を操作し、参照データ型はアドレスを操作します。
上記の原則によれば、上記のコードのメモリ モデルは次のとおりです:
Memory model.png
基本型はスタック メモリに直接格納されますが、オブジェクトはヒープ メモリに格納され、変数は単に保持されますこのオブジェクトのアドレス。したがって、obj はオブジェクト oxff44 のアドレスを保持し、関数 func はアドレス oxff66 を保持します。
上記のコードを元に実行:
console.log(func); console.log(func());
出力の1行目は関数全体(関数自体)の定義部分です:
コードの1行目出力結果.png
となっています上で説明したように、 func はヒープ メモリの一部を指すアドレスを格納し、ヒープ メモリは関数の定義を保持します。
出力の 2 行目は func 関数の戻り結果です:
コードの 2 行目は result.png を出力します。func 関数には戻り値がないため、出力は未定義です。
注: return の後に記述された関数の戻り結果が戻り値になります。return がない場合、デフォルトの戻り値は未定義です。
02 事前解析
上記のメモリモデルを理解すると、事前解析のメカニズムをよりよく理解できるようになります。いわゆる事前解析とは、現在のスコープ内で、JavaScript コードが実行される前に、デフォルトでブラウザはまず var および function 宣言を使用してすべての変数を事前に宣言または定義します。
2.1. 宣言と定義
var num = 24;
この単純なコード行は、実際には宣言と定義の 2 つのステップです。
宣言: var num; は、変数が宣言されているだけで値が割り当てられていない場合、デフォルト値は未定義であることをブラウザーに伝えます。
定義: num = 12; 定義は変数に値を代入することです。
2.2. var で宣言された変数と事前解析時の function で宣言された関数の違い
var で宣言された変数と事前解析時の function で宣言された関数には違いがあります。事前解析中に、 function で宣言された関数が事前に宣言され、事前解析中に同時に定義されます。つまり、var で宣言された変数と function で宣言された関数の違いは、宣言と同時に定義されるかどうかです。
2.3. 事前解析は現在のスコープ内でのみ行われます
プログラムの開始時は、関数が実行されるときのみ、関数内の変数と関数が事前解析されます。 -解析されました。
console.log(num); var num = 24; console.log(num); func(100 , 200); function func(num1 , num2) { var total = num1 + num2; console.log(total); }
Output result.png
num が初めて出力されるとき、事前解析の理由により、それはまだ定義されていないと宣言されるだけなので、num が 2 回目に出力されるときは、unknown が出力されます。と定義されているため、出力は 24 になります。
関数の宣言と定義が同時に行われるため、func関数定義文の前にfunc()が呼ばれていますが、正常に呼び出すことができ、正常に300が出力されます。
Memory model.png
03 スコープチェーン
まず、次の 3 つの概念を理解します。
関数内のスコープはプライベート スコープとなり、ウィンドウが配置されているスコープはグローバル スコープと呼ばれます。
グローバル スコープで宣言された変数はグローバル変数です。 「プライベート スコープで宣言された変数」と「関数の仮パラメーター」は両方ともプライベート変数です。在私有作用域中,代码执行的时候,遇到了一个变量,首先需要确定它是否为私有变量,如果是私有变量,那么和外面的任何东西都没有关系,如果不是私有的,则往当前作用域的上级作用域进行查找,如果上级作用域也没有则继续查找,一直查找到window为止,这就是作用域链。
当函数执行的时候,首先会形成一个新的私有作用域,然后按照如下的步骤执行:
如果有形参,先给形参赋值;
进行私有作用域中的预解析;
私有作用域中的代码从上到下执行
函数形成一个新的私有的作用域,保护了里面的私有变量不受外界的干扰(外面修改不了私有的,私有的也修改不了外面的),这也就是闭包的概念。
console.log(total); var total = 0; function func(num1, num2) { console.log(total); var total = num1 + num2; console.log(total); } func(100 , 200); console.log(total);
以上代码执行的时候,第一次输出total的时候会输出undefined(因为预解析),当执行func(100,200)的时候,会执行函数体里的内容,此时func函数会形成一个新的私有作用域,按照之前描述的步骤:
先给形参num1、num2赋值,分别为100、200;
func中的代码进行预解析;
执行func中的代码
因为在func函数内进行了预解析,所以func函数里面的total变量会被预解析,在函数内第一次输出total的时候,会输出undefined,接着为total赋值了,第二次输出total的时候就输出300。 因为函数体内有var声明的变量total,函数体内的输出total并不是全局作用域中的total。
最后一次输出total的时候,输出0,这里输出的是全局作用域中的total。
console.log(total); var total = 0; function func(num1, num2) { console.log(total); total = num1 + num2; console.log(total); } func(100 , 200); console.log(total);
将代码作小小的变形之后,func函数体内的total并没有使用var声明,所以total不是私有的,会到全局作用域中寻找total,也就说说这里出现的所有total其实都是全局作用域下的。
04全局作用域下带var和不带var的区别
在全局作用域中声明变量带var可以进行预解析,所以在赋值的前面执行不会报错;声明变量的时候不带var的时候,不能进行预解析,所以在赋值的前面执行会报错。
console.log(num1); var num1 = 12; console.log(num2); num2 = 12;
输出结果.png
num2 = 12; 相当于给window增加了一个num2的属性名,属性值是12;
var num1 = 12; 相当于给全局作用域增加了一个全局变量num1,但是不仅如此,它也相当于给window增加了一个属性名num,属性值是12;
问题:在私有作用域中出现一个变量,不是私有的,则往上级作用域进行查找,上级没有则继续向上查找,一直找到window为止,如果window也没有呢?
获取值:console.log(total); --> 报错 Uncaught ReferenceError: total is not defined
设置值:total= 100; --> 相当于给window增加了一个属性名total,属性值是100
function fn() { // console.log(total); // Uncaught ReferenceError: total is not defined total = 100; } fn(); console.log(total);
注意:JS中,如果在不进行任何特殊处理的情况下,上面的代码报错,下面的代码都不再执行了
05预解析中的一些变态机制
5.1 不管条件是否成立,都要把带var的进行提前的声明
if (!('num' in window)) { var num = 12; } console.log(num); // undefined
JavaScript进行预解析的时候,会忽略所有if条件,因为在ES6之前并没有块级作用域的概念。本例中会先将num预解析,而预解析会将该变量添加到window中,作为window的一个属性。那么 'num' in window 就返回true,取反之后为false,这时代码执行不会进入if块里面,num也就没有被赋值,最后console.log(num)输出为undefined。
5.2 只预解析“=”左边的,右边的是指,不参与预解析
fn(); // -> undefined(); // Uncaught TypeError: fn is not a function var fn = function () { console.log('ok'); } fn(); -> 'ok' function fn() { console.log('ok'); } fn(); -> 'ok'
建议:声明变量的时候尽量使用var fn = ...的方式。
5.3 自执行函数:定义和执行一起完成
(function (num) { console.log(num); })(100);
自治性函数定义的那个function在全局作用域下不进行预解析,当代码执行到这个位置的时候,定义和执行一起完成了。
补充:其他定义自执行函数的方式
~ function (num) {}(100) + function (num) {}(100) - function (num) {}(100) ! function (num) {}(100)
5.4 return下的代码依然会进行预解析
function fn() { console.log(num); // -> undefined return function () { }; var num = 100; } fn();
函数体中return下面的代码,虽然不再执行了,但是需要进行预解析,return中的代码,都是我们的返回值,所以不进行预解析。
5.5 名字已经声明过了,不需要重新的声明,但是需要重新的赋值
var fn = 13; function fn() { console.log('ok'); } fn(); // Uncaught TypeError: fn is not a function
经典题目
fn(); // -> 2 function fn() {console.log(1);} fn(); // -> 2 var fn = 10; // -> fn = 10 fn(); // -> 10() Uncaught TypeError: fn is not a function function fn() {console.log(2);} fn();
更多JavaScript 中的预解析相关文章请关注PHP中文网!