ES 5、ES 6变量和函数声明以及作用域总结

变量提升

JavaScript引擎在执行的时候,会把所有变量的声明都提升到当前作用域的最前面。当前作用域内的声明都会提升到作用域的最前面,包括变量和函数的声明。

var v = "hello";
(function(){
  console.log(v);
  var v = "world";
})();

这段代码运行的结果是:undefined
第一,function作用域里的变量v遮盖了上层作用域变量v。

var v = "hello";
if(true){
  console.log(v);
  var v = "world";
}

代码做少些变动,输出结果为”hello”,说明ES 5是没有块级作用域的。函数是ES 5中唯一拥有自身作用域的结构。

第二,在function作用域内,变量v的声明被提升了。所以最初的代码相当于:

var v = "hello";
(function(){
  var v; //declaration hoisting
  console.log(v);
  v = "world";
})();
(function(){
  var a = "1";
  var f = function(){};
  var b = "2";
  var c = "3";
})();

变量a,f,b,c的声明会被提升到函数作用域的最前面,类似如下:

(function(){
  var a,f,b,c;
  a = "1";
  f = function(){};
  b = "2";
  c = "3";
})();

下面的代码中函数声明f2被提升,所以在前面调用f2是没问题的。虽然变量f1也被提升,但f1提升后的值为undefined,其真正的初始值是在执行到函数表达式处被赋予的。所以只有声明是被提升的。

(function(){
  //var f1,function f2(){}; //hoisting,被隐式提升的声明

  f1(); //ReferenceError: f1 is not defined
  f2();

  var f1 = function(){};
  function f2(){}
})();

ES5 有var和没var声明

ES5中只有全局作用域函数作用域,我们依次分析。

函数作用域

var i=100;  //显示申明

i=100;  //隐式申明

在函数内部,有var和没var声明的变量是不一样的。在函数中使用var关键字进行显式申明的变量是做为局部变量,而没有用var关键字,使用直接赋值方式声明的是全局变量,所以可以借此向外暴露接口。

JavaScript中变量声明有var和没var的区别,所以ES5中经常见到避免全局变量污染的方法是匿名立即执行函数:

(function(){ 
 // ... 
})();

全局作用域

在全局作用域内声明变量时,有var 和没var看起来都一样,声明的全局变量,就是window的属性。但其实还是有差别的,我们用delete删除属性来验证下,配置性为false的属性无法删除。也就是通过变量var声明全局对象的属性无法删除,我们还会发现和函数声明创建的全局对象属性也无法删除。

var fff = 2; 
window.ffa = 3; 
ffb = 4; 
this.ffc = 4; 
var ffftx = Object.getOwnPropertyDescriptor(window, 'fff'); //configurable:false,enumerable:true,value:2,writable:true 
var ffatx = Object.getOwnPropertyDescriptor(window, 'ffa'); //configurable:true,enumerable:true,value:2,writable:true 
var ffbtx = Object.getOwnPropertyDescriptor(window, 'ffb'); //configurable:true,enumerable:true,value:2,writable:true 
var ffctx = Object.getOwnPropertyDescriptor(window, 'ffc'); //configurable:true,enumerable:true,value:2,writable:true
delete fff; // 无法删除 
delete ffa; // 可删除 
delete ffb; // 可删除 
delete ffc; // 可删除

使用var声明变量,是在当前域中声明变量. 如果在方法中声明,则为局部变量;如果是在全局域中声明,则为全局变量。而num = 1,事实上是对属性赋值操作。首先,它会尝试在当前作用域链(如在方法中声明,则当前作用域链代表全局作用域和方法局部作用域etc。。。)中解析 num; 如果在任何当前作用域链中找到num,则会执行对num属性赋值; 如果没有找到num,它才会在全局对象(即当前作用域链的最顶层对象,如window对象)中创造num属性并赋值。

注意!没有用var关键字,本质上并不是声明了一个全局变量,而是创建了一个全局对象的属性。

函数定义两种方式的区别

函数定义有两种方式,一种是函数定义表达式,一种是函数声明语句。

// 函数定义表达式 
var fns = function (){ 
// ... 
}; 
// 函数声明语句 
function fns(){ 
// ... 
}

由于声明是会被提前的,所以函数声明语句“被提前”到外部脚本或外部函数作用域的顶部,所以以这种方式声明的函数,可以被再它定义之前出现的代码所调用。而函数定义表达式中,变量的声明被提前了,但是给变量的赋值是不会提前的,所以,以表达式方式定义的函数在函数定义之前无法调用。

(function() { 
    testa();            // 打印出testa 
    testb();            // 报错:提示undefined is not a function 
    console.log(testc); //undefined,如果移到上面就可以了 
    function testa() { 
    console.log("testa"); 
    } 
    var testb = function() { 
    console.log("tesb"); 
    } 
    var testc = "testc"; 
})();

ES 6对块级作用域的支持

ES 5没有块级作用域

,经常容易遇到两类问题。

  • 内层变量可能会覆盖外层变量。

    var tmp = new Date();
    
    function f(){
      console.log(tmp);
      if (false){
        var tmp = "hello world";
      }
    }
    
    f() // undefined

    上面代码中,函数f执行后,输出结果为undefined,原因在于变量提升,导致内层的tmp变量覆盖了外层的tmp变量。

  • 用来计数的循环变量泄露为全局变量。

    var s = 'hello';
    
    for (var i = 0; i < s.length; i++){
      console.log(s[i]);
    }
    
    console.log(i); // 5

ES 6对块级作用域的支持

上面的函数有两个代码块,都声明了变量n,运行后输出5。这表示外层代码块不受内层代码块的影响。如果使用var定义变量n,最后输出的值就是10。

function f1() {
  let n = 5;
  if (true) {
    let n = 10;
  }
  console.log(n); // 5
}

ES6允许块级作用域的任意嵌套。外层作用域无法读取内层作用域的变量。内层作用域可以定义外层作用域的同名变量。

{{{{
  {let insane = 'Hello World'}
  console.log(insane); // 报错
}}}};
{{{{
  let insane = 'Hello World';
  {let insane = 'Hello World';}
}}}};

ES6也规定,函数本身的作用域,在其所在的块级作用域之内。

function f() { console.log('I am outside!'); }
(function () {
  if(false) {
    // 重复声明一次函数f
    function f() { console.log('I am inside!'); }
  }

  f();   //  *
}());

上面代码在ES5中运行,会得到“I am inside!”,但是在ES6中运行,会得到“I am outside!”。这是因为ES5存在函数提升,不管会不会进入 if代码块,函数声明都会提升到当前作用域的顶部,得到执行;而ES6支持块级作用域,不管会不会进入if代码块作用域,其内部声明的函数皆不会影响到作用域的外部,即*所在行的函数不会受if代码块内部的影响。

块级作用域外部,无法调用块级作用域内部定义的函数。如果确实需要调用,就要像下面这样处理。值得注意的是,有的浏览器可能并不认为{ }形成了一个块级作用域。

{
  let a = 'secret';
  function f() {
    return a;
  }
}
f() // 报错

如果确实需要调用,就要像下面这样处理。

let f;
{
  let a = 'secret';
  f = function () {
    return a;
  }
}
f() // "secret"

var和let、const的区别

作用域

  • let和const命令所声明的变量,只在let命令所在的代码块内有效。

    {
      let a = 10;
      var b = 1;
    }
    
    a // ReferenceError: a is not defined.
    b // 1

    for循环的计数器,就很合适使用let命令。

    for(let i = 0; i < arr.length; i++){}
    
    console.log(i)
    //ReferenceError: i is not defined

用var声明的变量i,在全局范围内都有效。所以每一次循环,新的i值都会覆盖旧值,导致最后输出的是最后一轮的i的值。

如果使用let,声明的变量仅在块级作用域内有效,最后输出的是6。

    var a = [];
    for (var i = 0; i < 10; i++) {
      a[i] = function () {
        console.log(i);
      };
    }
    a[6](); // 10

用let声明的变量i,当前的i只在本轮循环有效,所以每一次循环的i其实都是一个新的变量,所以最后输出的是6。

    var a = [];
    for (let i = 0; i < 10; i++) {
      a[i] = function () {
        console.log(i);
      };
    }
    a[6](); // 6

const命令声明的变量不允许修改

对于复合类型的变量,变量名不指向数据,而是指向数据所在的地址。const命令只是保证变量名指向的地址不变,并不保证该地址的数据不变,所以将一个对象声明为常量必须非常小心。

const foo = {};
foo.prop = 123;

foo.prop
// 123

foo = {} // TypeError: "foo" is read-only

上面代码中,常量foo储存的是一个地址,这个地址指向一个对象。不可变的只是这个地址,即不能把foo指向另一个地址,但对象本身是可变的,所以依然可以为其添加新属性。

const a = [];
a.push("Hello"); // 可执行
a.length = 0;    // 可执行
a = ["Dave"];    // 报错

如果真的想将对象冻结,应该使用Object.freeze方法。

const foo = Object.freeze({});

// 常规模式时,下面一行不起作用;
// 严格模式时,该行会报错
foo.prop = 123;

上面代码中,常量foo指向一个冻结的对象,所以添加新属性不起作用,严格模式时还会报错。

除了将对象本身冻结,对象的属性也应该冻结。下面是一个将对象彻底冻结的函数。

var constantize = (obj) => {
  Object.freeze(obj);
  Object.keys(obj).forEach( (key, value) => {
    if ( typeof obj[key] === 'object' ) {
      constantize( obj[key] );
    }
  });
};

变量提升

let和const没有“变量提升”现象。所以,变量一定要在声明后使用,否则报错。

console.log(foo); // 输出undefined
console.log(bar); // 报错ReferenceError

var foo = 2;
let bar = 2;

重复声明

使用var语句重复声明语句是合法且无害的。如果重复声明且带有赋值,那么就和一般的赋值语句没差别。如果尝试读取没有声明过的变量,Js会报错。

但是ES 6中新增的let、constlet不允许在相同作用域内,重复声明同一个变量。

// 报错
function () {
  let a = 10;
  var a = 1;
}

// 报错
function () {
  let a = 10;
  let a = 1;
}

因此,不能在函数内部重新声明参数。

function func(arg) {
  let arg; // 报错
}

function func(arg) {
  {
    let arg; // 不报错
  }
}
  • 3
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 3
    评论
评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值