前言

到现在为止,我们应该知道作用域是做啥的了,以及词法作用域模式中,根据代码声明的位置和方式分配作用域的相关原理,。函数作用域和块作用域的行为是一样的,可以总结为:任何声明在某个作用域内的变量,都将附属于这个作用域。

但是作用域中的变量标识符和其声明的位置却有一些微妙的联系。

编译

a = 2;
var a;
console.log(a);

这段代码,其实会输出2;至于为什么,我们可以从编译的角度去理解。

我们知道,JavaScript在运行之前会进行一次编译,其中就有词法分析,并将词法与对应的作用域进行关联,于是会将声明先提取出去,于是乎上述的var a;会被先取走并丢入作用域中去。

当代码编译完毕后,进入运行阶段,此时运行阶段的代码为:

a = 2;
console.log(a);

引擎会通过LHS方式查询作用域中是否存在a,查到后进行赋值2的操作。

最后通过RHS的方式查询作用域中的a,得到2并进行打印输出。

我们再看一段代码:

console.log(a);
var a = 2;

此时在通过上述的方式我们转换成伪代码:

var a = undefined;

console.log(a);
a = 2;

变量在编译时会默认赋值undefined,所以我们在后续打印的时候并不会抛出ReferenceError的报错,而是得到一个undefined值,这个值是系统级赋值的,所以不会受全局作用域中的undefined变量所影响,所以我们会有利用这种特性的方式来修复全局undefined变量名被污染的问题。

所以我们可以确定一件事件,那就是先有声明,后有赋值,只是因为var的声明特性,它会在分析是提前存到作用域中去。

只有声明本身会被提升,而赋值或其他运行逻辑会留在原地。如果提升改变了代码执行的顺序,会造成非常严重的破坏。

foo();

function foo() {
    console.log(a); // undefined
    var a = 2;
}

foo函数中的a变量被提升了,因此它的log打印才能输出undefined。

需要注意的是,提升不是提升到整个全局最顶层去,而是提升到声明对应的作用域中,因此上述代码可以理解为如下:

function foo() {
    var a;
    console.log(a); // undefined
    a = 2;
}

函数提升

我们都知道,在js中,函数声明可以写在后面,但是却可以在上面调用。

test(); //1

function test() {
  console.log(1);
}

这是因为除了变量声明会被提升,函数声明也会被提升,而且函数会首先被提升,然后才是变量。

所以我们看下面这段代码:

foo(); // 不是 ReferenceError, 而是 TypeError!
var foo = function bar() {
    // ...
};

由于此时的foo不是函数声明,即便它的值是具名函数,但整个foo代码段其实是一个函数表达式,所以它不具备函数提升的特性,所以可以如下理解:

var foo

foo(); // 不是 ReferenceError, 而是 TypeError!

foo = function bar() {
    // ...
};

此时运行foo()时,先RHS查询到作用域中存在foo变量,但是由于不是函数,所以在通过函数运行是触发TypeError报错。

毕竟undefined不是一个函数。

需要注意的是,即便你将foo()换成具名函数bar(),也不会成功调用,在上一章的时候就已经说过了,此时的函数是一个部分,它的整体是foo代码段,不是一个声明函数,自然不会有函数提升,如果你不信,你最终会得到ReferenceError

命名重复

foo(); // 1
var foo;

function foo() {
    console.log(1);
}
foo = function() {
    console.log(2);
};

会输出 1 而不是 2 !这个代码片段会被引擎理解为如下形式:

function foo() {
    console.log(1);
}

foo(); // 1

foo = function() {
    console.log(2);
}

因为作用域中已经存在了foo;所以不会重新声明,重复的var声明会被忽略,所以还是会运行第一个foo,然后才被赋值改变了。

虽然var的重复声明会被忽略,但是函数的声明不会。

foo(); // 3

function foo() {
    console.log(1);
}

var foo = function() {
    console.log(2);
};

function foo() {
    console.log(3);
}

虽然这些听起来都是些无用的学院理论,但是它说明了在同一个作用域中进行重复定义是非常糟糕的,而且经常会导致各种奇怪的问题。

块级作用域中的函数提升

目前来看,还不支持函数提升只提升到块作用域内,但是不保证后续会有改动,我们先看个例子:

foo(); // "b"

var a = true;

if (a) {
    function foo() {
        console.log("a");
    }
} else {
    function foo() {
        console.log("b");
    }
}

由于函数声明的提升不受块级作用域的限制,所以在外部作用域还是可以访问到函数,函数被重复声明替换,运行只会得到最后那个函数的输出。

再看一个例子:

try {
    throw new Error("我是一个错误")
} catch (error) {
    function testError(e) {
        console.log(e);
    }
    testError(error);
}

//首先输出 我是一个错误

testError(111);  //111

是一样的结果,函数无法被块作用域限制。

总结

我们习惯将 var a = 2; 看作一个声明,而实际上 JavaScript 引擎并不这么认为。它将 var a和 a = 2 当作两个单独的声明,第一个是编译阶段的任务,而第二个则是执行阶段的任务。

这意味着无论作用域中的声明出现在什么地方,都将在代码本身被执行前首先进行处理。可以将这个过程形象地想象成所有的声明(变量和函数)都会被“移动”到各自作用域的最顶端,这个过程被称为提升。

声明本身会被提升,而包括函数表达式的赋值在内的赋值操作并不会提升。

要注意避免重复声明,特别是当普通的 var 声明和函数声明混合在一起的时候,否则会引起很多危险的问题!

分类: 你不知道的JavaScript 标签: 提示变量提升函数提升

评论

暂无评论数据

暂无评论数据

目录