首頁  >  文章  >  web前端  >  JavaScript 中的預解析

JavaScript 中的預解析

高洛峰
高洛峰原創
2017-02-04 16:46:221086瀏覽

在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程式碼執行的環境,稱為全域作用域。

基本資料型別依照值來操作,引用資料型別依照位址來操作。

根據上述原則,以上的程式碼在記憶體中的模型為:

JavaScript 中的预解析

記憶體模型.png

基本型別是直接儲存在堆疊記憶體中,而物件是儲存在堆疊記憶體中,變數只是持有該對象的位址。所以obj持有一個物件的位址oxff44,函數func持有一個位址oxff66。

在以上的程式碼的基礎上再執行:

console.log(func);
console.log(func());

第一行輸出的是整個函數的定義部分(函數本身):

JavaScript 中的预解析

第一行程式碼輸出結果.png

上面已經說明了,func儲存的是一個位址,該位址指向一塊堆內存,該堆內存就保留了函數的定義。

第二行輸出的是func函數的回傳結果:

JavaScript 中的预解析

第二行程式碼輸出結果.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);
}

JavaScript 中的预解析

輸出結果.png

第一次輸出num的時候,由於預解析的原因,只聲明了還沒有定義,所以會輸出undefined;第二次輸出num的時候,已經定義了,所以輸出24。

由於函數的宣告和定義是同時進行的,所以func()雖然是在func函數定義宣告處之前調用的,但是依然可以正常的調用,會正常輸出300。

JavaScript 中的预解析

記憶體模型.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;

JavaScript 中的预解析

输出结果.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中文网!

陳述:
本文內容由網友自願投稿,版權歸原作者所有。本站不承擔相應的法律責任。如發現涉嫌抄襲或侵權的內容,請聯絡admin@php.cn