js 变量声明易混淆的几点知识

时间: 2018-05-15阅读: 133标签: 变量

这是我 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;