javascript之作用域二(作用域的特点)

430 阅读5分钟

二、作用域的特点

我们上一篇讲解了作用域,现在看一下作用域的特点

看一下这段代码吧

定义变量时,如果不写var,比如 i=0,则会被定义为全局变量,作用域为全局作用域,否则为局部变量,作用域为函数作用域。上面第一行的var i=0,之所以说它是全局变量,是因为它已经是在全局区申明的了,并不在函数范围内,因此跟 i=0 是一样的。

1 声明形式

变量声明:

var x; //局部变量声明
var x=1; //局部变量声明并赋值
x=1; //定义全局变量并赋值

函数声明:

function fn(){}//函数声明并定义
var fn=function(){}//定义局部变量fn和一个匿名函数,并赋值给fn

2 变量提升

引出一个问题

下面这段代码会输出什么内容?

多数人都说输出的是日期。但真实的结果是undefined。为什么是这样呢?这里就引出了一个概念--hoisting,中文的意思就是变量提升。

因为变量声明是在任意代码执行前处理的,在代码区中任意地方声明变量和在最开始(最上面)的地方声明是一样的。也就是说,看起来一个变量可以在声明之前被使用!这种行为就是所谓的“hoisting”,也就是变量提升,看起来就像变量的声明被自动移动到了函数或全局代码的最顶上。

注意:仅仅是申明提升了,定义并不会被提升。

如此,上面这段代码其实就是下面的形式:

所以,这样就应该理解了,console输出的时候,tmp变量仅仅是申明了但未定义,所以输出应该是undefined。

推荐的做法是在声明变量的时候,将所用的变量都写在作用域(全局作用域或函数作用域)的最顶上,这样代码看起来就会更清晰,更容易看出来那个变量是来自函数作用域的,哪个又是来自作用域链

3 重复声明

上面的输出其实是:1 2 2。 虽然看起来里面x申明了两次,但上面说了,js的var变量只有全局作用域和函数作用域两种,且申明会被提升,因此实际上x只会在最顶上开始的地方申明一次,var x=2的申明会被忽略,仅用于赋值。也就是说上面的代码实际上跟下面是一致的。

4 函数和变量同时提升的问题

如果是函数和变量类型同时申明定义了,会发生什么事情呢?看下面的代码

上面的输出结果其实是: function foo(){} ,也就是函数内容。

而如果是这样的形式呢

它的输出却变成:undefined

为什么会这样呢?

  1. 原来函数提升分为两种情况: 一种:函数申明。就是function foo(){}这种形式。另一种:函数表达式。就是var foo=function(){}这种形式
  2. 第二种形式其实就是var变量的声明定义,因此上面第二个图输出结果为undefined应该就能理解了。
  3. 而第一种函数申明的形式,在提升的时候,会被整个提升上去,包括函数定义的部分!因此跟下面的这种方式是等价的!

原因是因为:

  1、函数声明被提升到最顶上;
  2、申明只进行一次,因此后面var foo='i am text'的申明会被忽略。
  3、并且函数申明的优先级优于变量申明,

所以以下形式的输出,同样是函数内容:

5 ES6的块级作用域

ES5只有全局作用域和函数作用域,没有块级作用域,会带来下面问题:

ES6引入了块级作用域,明确允许在块级作用域中声明函数,let和const命令都涉及块级作用域。

块作用域的用处:变量的声明应该距离使用的地方越近越好。并最大限度的本地化。避免污染。

块级作用域允许声明函数只在使用大括号的情况下成立,如果未使用大括号,会报错。

if (true) {
function func1() {} // 不报错
}

if (true)
function func2() {} // 报错
1.let

let定义的变量具有以下的特点:

  1. let隐形的创建块作用域({...}),可以将变量绑定到所在的任意作用域中
  2. let声明的变量不能进行变量提升,因此只能先定义,后使用

(如下代码)

{
    console.log(bar); // ReferenceError
    let bar = 2; //此处不会被提升。
}

let一个典型的应用就是在for循环里

我们看下面两个例子:

// 每秒输出一个5
for( var i = 0; i < 5 ; i++ ) {
    setTimeout(() => {
        console.log( i );
    }, i *1000)
}

// 依次输出0,1,2,3,4,时间间隔位1秒
for( let i = 0; i < 5 ; i++ ) {
    setTimeout(() => {
        console.log( i );
    }, i *1000)
}

其原因就是let形成了5个块作用域,使每次输出的变量都从本次循环的块作用域中获取。

2.const

固定常量,不可被修改,否则报错。

var foo = true;
if (foo) { 
    var a=2;
    const b = 3; // 包含在 if 中的块作用域常量

    a=3;//正常!
    b=4;//错误! 
}

console.log( a ); // 3
console.log( b ); // ReferenceError!

6 总结

要彻底理解JS的作用域和Hoisting,只要记住以下三点即可:

  1. 所有申明都会被提升到作用域的最顶上
  2. 同一个变量申明只进行一次,并且因此其他申明都会被忽略
  3. 函数声明的优先级优于变量申明,且函数声明会连带定义一起被提升