1. 程式人生 > >js 變量聲明易混淆的幾點知識

js 變量聲明易混淆的幾點知識

內部 可能 決定 archive fire for 作用域鏈 時機 聲明

這是我 JavaScript 學習過程中遇到的一些容易混淆的地方,趁著有時間,做了一個整理。

變量提升

變量與函數名提升優先級

js 作用域內有變量,這個很好理解,但有一些細節需要註意。

console.log(foo);  // 函數
function foo(){
    console.log("函數聲明");
}
console.log(foo); // 函數
var foo = "變量";
console.log(foo); // 變量

當變量名與函數名同名,且都提升上去了,那最終結果是哪個聲明起作用呢?

有兩個知識點:

  1. var foo;並不會覆蓋之前的變量
  2. 函數提升優先級比變量提升要高,且不會被變量聲明覆蓋,但是會被變量賦值覆蓋,所以上面的代碼實際上是
function foo(){ // 優先級最高,提升到最前面
    console.log("函數聲明");
}
var foo; // 只提升聲明,不提升賦值,且不能覆蓋函數聲明
console.log(foo); 
console.log(foo); 
foo = "變量"; // 可以覆蓋函數聲明
console.log(foo); 

連等賦值的變量提升

var num1 = 1;  
function   fn(num3){  
    console.log(num1);    //output    undefined  
    console.log(num3);    //output  4  
console.log(num4); //throw error “num4 is not defined” console.log(num2); //throw error “num2 is not defined” var num1 = num4 = 2; // js 連等賦值 num4 不會被提升 num2 = 3; // 沒有 var 會掛載到全局作用域,但不會提升,所以之前會報錯 var num3= 5; } fn(4);

if 判斷內變量提升

if (true) {  
    function fn
(){ return 1; } }else { if(false){ function fn(){ return 2; } } } console.log(fn.toString()); console.log(fn())

以下是從找到這個例子的原文中摘抄的內容:

chrome和ie一均為function fn(){ return 2;},而firefox中依然報錯。
可見三者處理並不相同。ff中會提前變量的聲明,但不會提前塊級作用域中的函數聲明。而chrome和ie下就會提前塊級作用域中的函數聲明,而且後面的聲明會覆蓋前面的聲明。

我寫這篇文章時,測試的結果 IE10 及以下,返回2,IE11和谷歌,火狐新版,返回1

var fn;
console.log(fn); // undefined

if (true) {  
    function fn(){ return 1; }  
}else {  
     if(false){  
       function fn(){ return 2; }  
     }  
}  
console.log(fn.toString());  // 函數 1
console.log(fn()) // 1

if判斷內函數並沒有提升

對有名函數賦值

function test5() {
  var fn = function fn1() {
    log(fn === fn1); // true
    log(fn == fn1); // true
  }
  fn();
  log(fn === fn1); // fn1 is not defined
  log(fn == fn1);  // fn1 is not defined
}
test5();

在function內部,fn完全等於fn1

在function外面,fn1則是 not defined

!兼容

b(); 
var a = function b() {alert(‘this is b‘)}; 

在ie8及以下瀏覽器是可以執行b的. 說明不同瀏覽器在處理函數表達式細節上是有差別的.

return 後聲明的提升

函數 return 語句後的變量、函數聲明提升

function text6() {
   var a = 1;
   function b() {
     a = 10;
      return;
      function a() {}
    }
    b();
    console.log(a);  // 1
}
text6();

函數的作用域內賦值

在js中,提到變量賦值,就要先說作用域,而作用域,在es6之前,只有函數才會形成獨立的作用域,然後函數的嵌套形成了 js 的作用域鏈。子作用域內可以訪問父級作用域內的元素。函數的作用域在函數確定的時候就已經確定,與調用無關。

// test1
var x = 1;
function foo(x) {
  var x = 3;
  var y = function() { 
    x = 2;
    console.log(x)
  }
  y();
  console.log(x);
  return y
}
var z = foo() // 2 2
z() // 2

這段函數會輸出三個 2 ,指向同一個 x,甚至,將 x 改為對象,就更明顯了

// test2
var x = "abc";
function foo(x) {
  var x = c;
  var y = function() { 
    return x;
  }
  return y;
}
var c = {a:1}
var z = foo(); 
var b = z();
console.log(b === c); // true

上面例子中,foo 函數執行後,返回 y 函數並賦值給 z,z 指向 y 函數(函數體),此時,z 並不在 foo 函數的作用域內,在此作用域不能訪問到 x,但 z 只是引用類型數據的一個指針,只是同 x 指向了同一個對象而已。而執行 z 函數,則會返回 x 的值,這個值是函數 y 作用域內訪問到的 x 的值,是根據函數的書寫位置確定的作用域,並不會因為調用位置不同,而改變變量的指向。

但是同時要註意,雖然函數作用域在函數寫出來時就已經確定,但具體的值卻跟調用的時機有關。

// test3
var x = "abc";
function foo(x) {
  var x = c;
  var y = function() {
    x.a++;
    return x;
  }
  return y
}
var c = {a:1}
var z = foo(); 
console.log(z()) // {a: 2}
console.log(z()) // {a: 3}
console.log(z()) // {a: 4}

這個例子中,輸出的三次都是同一個對象,但輸出的值不同,這是因為輸出的時候的值不同,這就和調用時的實際值有關了。

同時,函數的另一個值也與函數的調用情況相關,就是 this。函數的 this 指向函數的調用者,與函數在哪裏定義沒有關系。

var x = "abc";
var b = {x:"def"}
function foo(x) {
  var x = c;
  var y = function() {
    x.a++;
    return this.x;
  }
  console.log(y()); // abc
  return y
}
var c = {a:1}
var z = foo(); 
console.log(z()) // abc
b.a = z;
console.log(b.a()) // def
b.b = {
    x: "hig",
    z: z
}
console.log(b);
console.log(b.b.z()); // hig

上面的例子就說明了,函數內的 this 始終指向本次調用函數的對象,如果沒有,就指向全局對象,而如果指向的對象本身是另一個對象的屬性,這並不影響,甚至,將上個例子裏的 b.b 的 x 屬性刪除,最後調用時 b.b.z() 的 this 依然指向 b.b 所指的對象,即使該對象對象沒有 x 值,會返回 undefined,而不會向上級尋找。

function fn(){
    var array=[];
    var b = {a:1}
    for(var i=0;i<10;i++){
        array[i]=function(){
            var abc = {def: "ghi"};
            return b;
        }
    }
    return array;
}
var a = fn();
console.log(a) //[?, ?, ?, ?, ?, ?, ?, ?, ?, ?]
console.log(a[0].toString()) //f
console.log(a[1] === a[2]) // false
console.log(a[1] == a[2]) // false
console.log(a[1].toString() === a[2].toString()) // true
console.log(a[1]() === a[2]()) // true

兩個函數相等的比較:兩個函數的作用域相同並且函數內容相同,那麽,比較結果為這兩個函數相等,否則,不相等!不是我們直接用 == 去比較,而是按照這個規則去進行比較。

函數是對象,復合型的值。一般比較引用,同一個引用,就相等;否則不等。比較toString幾乎沒任何意義:因為對於函數調用來說,作用域只是一個不可見的透明的規則

對 arguments 賦值

function fn(a, b) {
    var a = 1;
    var b = 2;
    console.log(arguments[0]);
    console.log(arguments[1]);
    arguments[0] = 3;
    arguments[1] = 4;
    console.log(a);
    console.log(b);
}

fn(5,6) // 1 2 3 4 傳入實參,實參和形參指向相同
fn() // unde unde 1 2 沒有傳入實參,arguments 指向 undefined,形參與實參不再相關
function fn(a, b) {
    ‘use strict‘
    var a = 1;
    var b = 2;
    console.log(arguments[0]);
    console.log(arguments[1]);
    arguments[0] = 3;
    arguments[1] = 4;
    console.log(a);
    console.log(b);
}

fn(5,6) // 5 6 1 2 嚴格模式下 arguments 指向實參,與形參不相關
fn() // unde unde 1 2

javascript中Arguments對象是函數的實際參數,arguments對象的長度是由實參個數而不是形參個數決定的。形參是函數內部重新開辟內存空間存儲的變量,但是其與arguments對象內存空間並不重疊。

在es5規範下的非嚴格模式下,函數調用時傳入實參,函數內部使用 形參 與 arguments 指向相同, arguments 的具體屬性改變, 形參的值也會改變,但如果沒有傳入實參,arguments 與 形參的指向就沒有關系了。

es5規範的嚴格模式下,形參與arguments 分別存放,不會相關

但自es6之後,非嚴格模式下效果也同es5 嚴格模式一樣,即arguments和參數符號分別存放。

而es6的嚴格模式下,修改arguments會報錯。

函數內 形參、變量、函數 同名的問題

function aa(a,b,c) {
    console.log(arguments); // {0: 函數a, 1: 2, 2: 3} 通過更改形參的值,可以更改 arguments 
    function a(){console.log(a)}
    console.log(a); // 函數 a
    console.log(aa); // undefined, var aa 被提升
    console.log(arguments); // 同上
    var a = "ee";
    var aa = "444";
    arguments = 6;
    console.log(a); // ee
    console.log(aa); // 444
    console.log(arguments) // 6
}
aa(1,2,3)
function aa(a,b,c) {
    ‘use strict‘ // 調用嚴格模式
    console.log(arguments); // 指向arguments 對象,{0:1,1:2,2:3}
    function a(){console.log(a)}
    console.log(a); // 函數a
    console.log(aa); // undefined
    console.log(arguments); // 同上
    var a = "ee";
    var aa = "444";
    arguments[0] = 6;
    // arguments = 6; 嚴格模式 不能直接對 arguments 進行賦值
    console.log(a); // ee
    console.log(aa); // 444
    console.log(arguments) // // 指向arguments 對象,{0:6,1:2,2:3}
}
aa(1,2,3)

從網上找到的一些解釋

填充變量的順序是: 函數的形參 -> 函數聲明->變量聲明, 當變量聲明遇到VO中已經有同名的時候,不會影響已經存在的屬性。
函數形參————由名稱和對應值組成的一個變量對象的屬性被創建;沒有傳遞對應參數的話,那麽由名稱和undefined值組成的一種變置對象的屬性性也將被創建
函數聲明————由名稱和對應值(函數對象(function-object))組成一個變量對象的屬性被創建;如果變置對象已經存在相同名稱的厲性,則完全替換這個屬性
變量聲明————由名稱和對應值(undefined〉組成一個變量對象的屬性被創建;如果變量名稱跟已經聲明的形式參數或函數相同,則變量聲明不會幹擾已經存在的屬性

一般情況下,會按照四種方式依次解析
語言內置:
形式參數:
函數聲明:
變量聲明:
也有例外:
內置的名稱arguments表現得很奇怪,看起來應該是聲明在形參之後,但是卻在聲明之前。這是說,如果形參裏面有arguments,它會比內置的那個優先級高。所以盡可能不要在形參裏面使用arguments;
在任何地方定義this變量都會出語法錯誤
如果多個形式參數擁有相同的名稱,最後的那個優先級高,即便是實際運行的時候它的值是undefined;

資料:

https://segmentfault.com/q/1010000006727575?_ea=1111028

http://www.cnblogs.com/sharpxiajun/archive/2011/09/16/2179010.html

https://www.cnblogs.com/zhouyongtao/archive/2012/11/22/2783089.html

https://www.cnblogs.com/luqin/p/5164132.html

https://segmentfault.com/q/1010000006135524

https://www.cnblogs.com/Eric1997/p/7499819.html

js 變量聲明易混淆的幾點知識