JavaScript深度剖析之变量、函数提升:从表面到本质

本文涉及的产品
全局流量管理 GTM,标准版 1个月
云解析 DNS,旗舰版 1个月
公共DNS(含HTTPDNS解析),每月1000万次HTTP解析
简介: JavaScript深度剖析之变量、函数提升:从表面到本质

JavaScript深度剖析之变量、函数提升:从表面到本质


前言


  • • 想要彻底理解提升这篇文章,除非你已经理解了作用域、词法作用域、动态作用域、编译器、引擎 之间的联系,否则建议你先从之前的文章读起。
  • • 在前几篇文章中提到的作用域中的变量声明出现的位置有着某种微妙的联系,而这个联系就是本篇文章所讨论的内容。


先有鸡还是先有蛋


  • • 在我们的直觉上 JavaScript 代码在执行时是一行一行执行的,其实并不完全正确,有一种情况会导致这个假设是错误的。
  • • 考虑以下代码:
a = 2;
var a;
console.log(a); // ?这里会输出什么呢?
  • • 可能会有人认为会输出 undefined,因为 var a 声明是在 a = 2; 赋值之后的,他们会自然而然地认为变量被重新赋值了,因为会被赋予默认值 undefined。但正确的输出结果为 2;
  • • 再考虑另外一段代码:
console.log(a); // ?这里会输出什么呢?
var a = 2;
  • • 鉴于上一个代码片段所表现出的某种非自上而下的行为特点,你可能会认为这段代码会输出 2。或者还有人可能认为,由于变量 a 在使用前没有事先被声明过,会抛出 ReferenceError 异常。然而,两种猜测都不会,正确的输出结果为 undefined
  • 那到底还是先有鸡还是先有蛋?到底是声明(蛋)在前,还是赋值(鸡)在前?,让我们带着这个问题接着向下看。


编译器阶段


  • • 根据前面分享的几篇文章我们可得知,引擎会在解释 JavaScript 代码之前会首先对其进行编译。而编译阶段中的一部分工作就是先找到所有的声明,并用合适的作用域将他们关联起来。因此,包括变量和函数在内的所有声明都会在任何代码被执行前首先被处理。
  • • 当你看到 var a = 2; 时,你可能会认为这是一个声明。但 JavaScript 会将他们看成两个声明。var aa = 2;第一个定义声明是在编译阶段进行的第二个赋值声明会被留在原地等待执行阶段
  • • 第一段代码的解析过程:
var a; // 被提升后的声明
a = 2;
// var a;  // 注意, var a 会被提升到顶部, 也就是上面提到的声明
console.log(a); // 2
  • • 第二段代码的解析过程:
// var a;
console.log(a); // undefined
var a = 2;
  • • 因此,这个过程就好像变量和函数声明从他们的代码中出现的位置被"移动"到了最上面,这个过程就叫做提升。
  • • 换句话说,先有蛋(声明)后有鸡(赋值)
  • 只有声明本身会被提升,而赋值或其他运行逻辑会留在原地。如果提升改变了代码的执行顺序,会造成非常严重的破坏。
  • • 考虑以下代码:
foo();
function foo() {
    console.log(a);
    var a = 2;
}
  • • 根据上面两个示例代码,先不要看答案。你可以试着将上面这段代码的解析后的结果写出来,巩固实践一下。
function foo() {
    // var a; 提升后的声明
    console.log(a); // undefined
    var a = 2;
}
foo(); // foo 函数的声明也被隐含地提升了,因此第一行在调用 foo 可正常执行。
  • • 另外,需要注意的是,每个作用域都会进行提升操作。这里的 foo(...) 函数自身也会在内容对 var a 进行提升(并不是提升到这个程序的最上方)。
  • • 再考虑以下代码:
foo(); // 会输出 success 吗?
var foo = function bar(){
    console.log('success');
}
  • • 其实并不会,知道为什么吗?可以先自己想一下,再看下面的答案:
var foo;
foo(); // TypeError: foo is not a function
foo = function bar() {
    console.log("success");
};
/**
你可能会疑惑为什么不是 ReferenceError?
    因为后面的 var foo = ... 对 foo 进行提升,默认值为 undefined。因为并不会抛出 ReferenceError。
为什么会抛出 TypeError?
    在前面几篇文章中我们说过,对变量进行一些不合规的操作时则会抛出 undefined, 因此,这里对 undefined 进行函数调用,则抛出 TypeError。
 */
  • 因此,从上面的代码中得知,函数声明会被提升,但函数表达式并不会被提升。
  • • 再考虑以下代码:
foo();
bar();
var foo = function bar() {
    console.log("success");
};
  • • 自己可以先试着写出这段代码的解析后的结果,在查看答案:
var foo;
foo(); // TypeError: foo is not a function
bar(); // ReferenceError: bar is not defined
foo = function {
    var bar = ...self...
};

函数优先


  • 函数声明和变量声明都会被提升,但出现有多个 "重复" 声明的代码中是函数首先会被提升,然后才是变量。
  • • 考虑以下代码:
foo(); // ?会输出什么呢?
var foo;
function foo() {
    console.log(1);
}
foo = function () {
    console.log(2);
}
  • • 自己可以先试着写出这段代码的解析后的结果,再查看答案:
function foo() {
    console.log(1);
}
foo(); // 1
// var foo; 尽管 var foo; 声明出现在 function foo(...) 之前,但他还是重复声明,因此会被忽略。因为函数声明会被提升到普通变量之前。
// 此处函数表达式并不会被提升
foo = function () {
    console.log(2);
}
  • • 再考虑以下代码:
foo(); // ?这里会输出什么呢?
function foo() {
    console.log(1);
}
var foo = function () {
    console.log(2);
}
function foo() {
    console.log(3);
}
  • • 和之前一样,可先试试自己写出解析后的结果,再查看答案:
foo(); // 3
// 尽管重复的 var 声明会被忽略掉,但出现在后面的函数声明还是可以覆盖前面的函数声明的。
function foo() {
    console.log(1);
}
var foo = function () {
    console.log(2);
}
// 会使用这个函数的结果
function foo() {
    console.log(3);
}
  • • 从上面代码可以看出,在同一个作用域内重复定义是很糟糕的,经常会导致各种奇怪的问题。
  • 小测试
  • • 考虑以下代码:
foo(); // 这里会调用那个函数?
var a = true;
if (a) {
    function foo() { console.log("a"); }
}
else {
    function foo() { console.log("b"); }
}
  • • 自己先写出解析后的结果后,再来看看自己的答案是否正确:
foo(); // TypeError: foo is not a function
/**
    为什么会抛出 TypeError 而不是 ReferenceError?
        其实 foo(); 这段调用函数的代码会被解析成以下代码:
        var foo;
        foo();
        看到这里,你应该明白,为什么会抛出 TypeError 异常了吧。如果还是没理解,建议你从头重新读起。
 */
var a = true;
if (a) {
    function foo() { console.log("a"); }
}
else {
    function foo() { console.log("b"); }
}

小结

  1. 1. 先有鸡(声明),后有蛋(赋值)。
  2. 2. 记住如 var a = 2; 这段代码看起来是一个声明,但 JavaScript 引擎并不这么认为,它会将这段代码当做 var aa = 2; 两个单独的声明来处理,第一个是在编译阶段执行的任务,第二个是在执行阶段执行的任务。
  3. 3. 重复定义的函数声明后面的会覆盖前面的。
  4. 4. 函数声明会被提升,但函数表达式并不会被提升。
  5. 5. 只有声明本身会被提升,而包括函数表达式的赋值在内的赋值操作并不会被提升。



特殊字符描述


问题标注 Q:(question)答案标注 R:(result)注意事项标准:A:(attention matters)详情描述标注:D:(detail info)总结标注:S:(summary)分析标注:Ana:(analysis)提示标注:T:(tips)

相关文章
|
2月前
|
JavaScript 前端开发
JavaScript如何判断变量undefined
JavaScript如何判断变量undefined
|
3天前
|
JavaScript 前端开发 Java
[JS]同事:这次就算了,下班回去赶紧补补内置函数,再犯肯定被主管骂
本文介绍了JavaScript中常用的函数和方法,包括通用函数、Global对象函数以及数组相关函数。详细列出了每个函数的参数、返回值及使用说明,并提供了示例代码。文章强调了函数的学习应结合源码和实践,适合JavaScript初学者和进阶开发者参考。
13 2
[JS]同事:这次就算了,下班回去赶紧补补内置函数,再犯肯定被主管骂
|
4天前
|
前端开发 JavaScript
如何在 JavaScript 中访问和修改 CSS 变量?
【10月更文挑战第28天】通过以上方法,可以在JavaScript中灵活地访问和修改CSS变量,从而实现根据用户交互、页面状态等动态地改变页面样式,为网页添加更多的交互性和动态效果。在实际应用中,可以根据具体的需求和场景选择合适的方法来操作CSS变量。
|
4天前
|
前端开发 JavaScript 数据处理
CSS 变量的作用域和 JavaScript 变量的作用域有什么不同?
【10月更文挑战第28天】CSS变量和JavaScript变量虽然都有各自的作用域概念,但由于它们所属的语言和应用场景不同,其作用域的定义、范围、覆盖规则以及与其他语言特性的交互方式等方面都存在明显的差异。理解这些差异有助于更好地在Web开发中分别运用它们来实现预期的页面效果和功能逻辑。
|
4天前
|
前端开发 JavaScript UED
如何使用 JavaScript 动态修改 CSS 变量的值?
【10月更文挑战第28天】使用JavaScript动态修改CSS变量的值可以为页面带来更丰富的交互效果和动态样式变化,根据不同的应用场景和需求,可以选择合适的方法来实现CSS变量的动态修改,从而提高页面的灵活性和用户体验。
|
2天前
|
前端开发 JavaScript 开发者
除了 Generator 函数,还有哪些 JavaScript 异步编程解决方案?
【10月更文挑战第30天】开发者可以根据具体的项目情况选择合适的方式来处理异步操作,以实现高效、可读和易于维护的代码。
|
16天前
|
JavaScript 前端开发
JavaScript 函数语法
JavaScript 函数是使用 `function` 关键词定义的代码块,可在调用时执行特定任务。函数可以无参或带参,参数用于传递值并在函数内部使用。函数调用可在事件触发时进行,如用户点击按钮。JavaScript 对大小写敏感,函数名和关键词必须严格匹配。示例中展示了如何通过不同参数调用函数以生成不同的输出。
|
18天前
|
存储 JavaScript 前端开发
JS函数提升 变量提升
【10月更文挑战第6天】函数提升和变量提升是 JavaScript 语言的重要特性,但它们也可能带来一些困惑和潜在的问题。通过深入理解和掌握它们的原理和表现,开发者可以更好地编写和维护 JavaScript 代码,避免因不了解这些机制而导致的错误和不一致。同时,不断提高对执行上下文等相关概念的认识,将有助于提升对 JavaScript 语言的整体理解和运用能力。
|
27天前
|
JavaScript 前端开发
js教程——函数
js教程——函数
24 4
|
26天前
|
存储 JavaScript 前端开发
js中函数、方法、对象的区别
js中函数、方法、对象的区别
15 2