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

简介: 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)

目录
相关文章
|
3月前
|
机器学习/深度学习 JavaScript 前端开发
JS进阶教程:递归函数原理与篇例解析
通过对这些代码示例的学习,我们已经了解了递归的原理以及递归在JS中的应用方法。递归虽然有着理论升华,但弄清它的核心思想并不难。举个随手可见的例子,火影鸣人做的影分身,你看到的都是同一个鸣人,但他们的行为却能在全局产生影响,这不就是递归吗?雾里看花,透过其间你或许已经深入了递归的魅力之中。
142 19
|
5月前
|
JavaScript
JS实现多条件搜索函数
JS封装的多条件搜索
|
6月前
|
JavaScript 前端开发 开发者
JavaScript基础——JavaScript变量名称命名规范
JavaScript变量命名规范是编写高质量代码的重要部分。通过遵循基本规则、使用常见命名约定并应用最佳实践,可以提高代码的可读性和可维护性。希望本文能帮助开发者在日常编程中更好地理解和应用JavaScript变量命名规范,从而编写出更清晰、更可靠的代码。
339 11
|
7月前
|
JavaScript 前端开发
JavaWeb JavaScript ③ JS的流程控制和函数
通过本文的详细介绍,您可以深入理解JavaScript的流程控制和函数的使用,进而编写出高效、可维护的代码。
160 32
|
6月前
|
JavaScript 前端开发 Java
详解js柯里化原理及用法,探究柯里化在Redux Selector 的场景模拟、构建复杂的数据流管道、优化深度嵌套函数中的精妙应用
柯里化是一种强大的函数式编程技术,它通过将函数分解为单参数形式,实现了灵活性与可复用性的统一。无论是参数复用、延迟执行,还是函数组合,柯里化都为现代编程提供了极大的便利。 从 Redux 的选择器优化到复杂的数据流处理,再到深度嵌套的函数优化,柯里化在实际开发中展现出了非凡的价值。如果你希望编写更简洁、更优雅的代码,柯里化无疑是一个值得深入学习和实践的工具。从简单的实现到复杂的应用,希望这篇博客能为你揭开柯里化的奥秘,助力你的开发之旅! 只有锻炼思维才能可持续地解决问题,只有思维才是真正值得学习和分享的核心要素。如果这篇博客能给您带来一点帮助,麻烦您点个赞支持一
|
8月前
|
JavaScript 前端开发 容器
盘点JavaScript中所有声明变量的方式及特性
本文详细介绍了JavaScript中变量定义的多种方式,包括传统的`var`、`let`和`const`,以及通过`this`、`window`、`top`等对象定义变量的方法。每种方式都有其独特的语法和特性,并附有代码示例说明。推荐使用`let`和`const`以避免作用域和提升问题,谨慎使用`window`和`top`定义全局变量,不建议使用隐式全局变量。掌握这些定义方式有助于编写更健壮的JS代码。
154 11
|
10月前
|
前端开发 JavaScript
如何在 JavaScript 中访问和修改 CSS 变量?
【10月更文挑战第28天】通过以上方法,可以在JavaScript中灵活地访问和修改CSS变量,从而实现根据用户交互、页面状态等动态地改变页面样式,为网页添加更多的交互性和动态效果。在实际应用中,可以根据具体的需求和场景选择合适的方法来操作CSS变量。
348 12
|
10月前
|
前端开发 JavaScript 数据处理
CSS 变量的作用域和 JavaScript 变量的作用域有什么不同?
【10月更文挑战第28天】CSS变量和JavaScript变量虽然都有各自的作用域概念,但由于它们所属的语言和应用场景不同,其作用域的定义、范围、覆盖规则以及与其他语言特性的交互方式等方面都存在明显的差异。理解这些差异有助于更好地在Web开发中分别运用它们来实现预期的页面效果和功能逻辑。
180 11
|
10月前
|
前端开发 JavaScript UED
如何使用 JavaScript 动态修改 CSS 变量的值?
【10月更文挑战第28天】使用JavaScript动态修改CSS变量的值可以为页面带来更丰富的交互效果和动态样式变化,根据不同的应用场景和需求,可以选择合适的方法来实现CSS变量的动态修改,从而提高页面的灵活性和用户体验。
|
10月前
|
前端开发 JavaScript 开发者
除了 Generator 函数,还有哪些 JavaScript 异步编程解决方案?
【10月更文挑战第30天】开发者可以根据具体的项目情况选择合适的方式来处理异步操作,以实现高效、可读和易于维护的代码。