在ES6之前,變數使用var聲明,會存在變數的預解析(函數也有預解析),我相信很多同學在剛開始學JavaScript的時候被預解析搞得團團轉,雖然在ES6的時候引入let和const ,但是現階段ES6並沒有完全普及,而且很多比較老的代碼都還是按照ES5的標準甚至是ES3的標準來書寫的。
01變數和函數在記憶體中的展示
JavaScript中的變數類型和其他語言一樣,有基本資料型別和引用資料型別。基本資料型別包括:undefined、null、boolean、String、Number;引用資料型別主要是物件(包括{}、[]、/^$/、Date、Function等)。
var num = 24; var obj = {name:'iceman' , age:24}; function func() { console.log('hello world'); }
當瀏覽器載入html頁面的時候,首先會提供一個供全域JavaScript程式碼執行的環境,稱為全域作用域。
基本資料型別依照值來操作,引用資料型別依照位址來操作。
根據上述原則,以上的程式碼在記憶體中的模型為:
記憶體模型.png
基本型別是直接儲存在堆疊記憶體中,而物件是儲存在堆疊記憶體中,變數只是持有該對象的位址。所以obj持有一個物件的位址oxff44,函數func持有一個位址oxff66。
在以上的程式碼的基礎上再執行:
console.log(func); console.log(func());
第一行輸出的是整個函數的定義部分(函數本身):
第一行程式碼輸出結果.png
上面已經說明了,func儲存的是一個位址,該位址指向一塊堆內存,該堆內存就保留了函數的定義。
第二行輸出的是func函數的回傳結果:
第二行程式碼輸出結果.png
由於func函數沒有回傳值,所以輸出undefined。
注意:函數的回傳結果,return後面寫的是什麼,回傳值就是什麼,如果沒有return,預設回傳值是undefined。
02預解析
有了以上的記憶體模型的理解之後,就能更好的了解預解析的機制了。所謂的預解析就是:在目前作用域中,JavaScript程式碼執行之前,瀏覽器首先會預設的把所有帶有var和function宣告的變數進行提前的宣告或定義。
2.1. 聲明和定義
var num = 24;
這行簡單的程式碼其實是兩個步驟:宣告和定義。
宣告:var num; 告訴瀏覽器在全域作用域中有一個num變數了,如果一個變數只是宣告了,但是沒有賦值,預設值是undefined。
定義:num = 12; 定義就是將變數賦值。
2.2. var宣告的變數和function宣告的函數在預解析的區別
var宣告的變數和function宣告的函數在預解析的時候有區別,var宣告的變數在預解析的時候只是事先的宣告,function宣告的函數在預先解析的時候會事先宣告並且會同時定義。也就是說var宣告的變數和function宣告的函數的差別是在宣告的同時有沒同時定義。
2.3. 預解析只發生在目前的作用域下
程式最開始的時候,只對window下的變數和函數進行預解析,只有函數執行的時候才會對函數中的變數很函數進行預解析。
console.log(num); var num = 24; console.log(num); func(100 , 200); function func(num1 , num2) { var total = num1 + num2; console.log(total); }
輸出結果.png
第一次輸出num的時候,由於預解析的原因,只聲明了還沒有定義,所以會輸出undefined;第二次輸出num的時候,已經定義了,所以輸出24。
由於函數的宣告和定義是同時進行的,所以func()雖然是在func函數定義宣告處之前調用的,但是依然可以正常的調用,會正常輸出300。
記憶體模型.png
03 作用域鏈
先理解以下三個概念:
函數裡面的作用域成為私有作用域,window所在的作用域稱為全局作用域;在全域作用域下宣告的變數是全域變數;
在「私有作用域中宣告的變數」和「函數的形參」都是私有變數;
在私有作用域中,代码执行的时候,遇到了一个变量,首先需要确定它是否为私有变量,如果是私有变量,那么和外面的任何东西都没有关系,如果不是私有的,则往当前作用域的上级作用域进行查找,如果上级作用域也没有则继续查找,一直查找到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中文网!