变量提升:JavaScript代码是按顺序执行的吗?
变量提升(Hoisting)
所谓的变量提升,是指在 JavaScript 代码执行过程中,JavaScript 引擎把变量的声明部分和函数的声明部分提升到代码开头的“行为”。变量被提升后,会给变量设置默认值 undefined。-
JavaScript 代码的执行流程
- 编译阶段
输入一段代码,经过编译后,会生成两部分内容:执行上下文(Execution context)和可执行代码,执行上下文是 JavaScript 执行一段代码时的运行环境。 - 执行阶段
JavaScript 引擎开始执行“可执行代码”,按照顺序一行一行地执行。
- 编译阶段
代码中出现相同的变量或者函数怎么办?
一段代码如果定义了两个相同名字的函数,那么最终生效的是最后一个函数。总结
JavaScript 代码执行过程中,需要先做变量提升,而之所以需要实现变量提升,是因为 JavaScript 代码在执行之前需要先编译。
在编译阶段,变量和函数会被存放到变量环境中,变量的默认值会被设置为 undefined;在代码执行阶段,JavaScript 引擎会从变量环境中去查找自定义的变量和函数。
如果在编译阶段,存在两个相同的函数,那么最终存放在变量环境中的是最后定义的那个,这是因为后定义的会覆盖掉之前定义的。
调用栈:为什么JavaScript代码会出现栈溢出?
-
哪些情况下代码才算是“一段”代码,才会在执行之前就进行编译并创建执行上下文?
- 当 JavaScript 执行全局代码的时候,会编译全局代码并创建全局执行上下文,而且在整个页面的生存周期内,全局执行上下文只有一份。
- 当调用一个函数的时候,函数体内的代码会被编译,并创建函数执行上下文,一般情况下,函数执行结束之后,创建的函数执行上下文会被销毁。
- 当使用 eval 函数的时候,eval 的代码也会被编译,并创建执行上下文。
什么是栈?
栈就是类似于一端被堵住的单行线,车子类似于栈中的元素,栈中的元素满足后进先
出的特点。什么是 JavaScript 的调用栈?
在执行上下文创建好后,JavaScript 引擎会将执行上下文压入栈中,通常把这种用来管理执行上下文的栈称为执行上下文栈,又称调用栈。
如果在一个函数 A 中调用了另外一个函数 B,那么 JavaScript 引擎会为 B 函数创建执行上下文,并将 B 函数的执行上下文压入栈顶。当前函数执行完毕后,JavaScript 引擎会将该函数的执行上下文弹出栈。栈溢出(Stack Overflow)
调用栈是一种用来管理执行上下文的数据结构,符合后进先出的规则。调用栈是有大小的,当入栈的执行上下文超过一定数目,JavaScript 引擎就会报错,我们把这种错误叫做栈溢出。
块级作用域:var缺陷以及为什么要引入 let 和 const?
正是由于 JavaScript 存在变量提升这种特性,从而导致了很多与直觉不符的代码,这也是 JavaScript 的一个重要设计缺陷。
作用域(scope)
作用域是指在程序中定义变量的区域,该位置决定了变量的生命周期。通俗地理解,作用域就是变量与函数的可访问范围,即作用域控制着变量和函数的可见性和生命周期。-
变量提升所带来的问题
- 变量容易在不被察觉的情况下被覆盖掉
- 本应销毁的变量没有被销毁
ES6 是如何解决变量提升带来的缺陷
引入了 let 和 const 关键字,作用块内声明的变量不影响块外面的变量。JavaScript 是如何支持块级作用域的
块级作用域就是通过词法环境的栈结构来实现的,而变量提升是通过变量环境来实现,通过这两者的结合,JavaScript 引擎也就同时支持了变量提升和块级作用域了。
作用域链和闭包 :代码中出现相同的变量,JavaScript引擎是如何选择的?
- 作用域链
bar 函数和 foo 函数的 outer 都是指向全局上下文的,这也就意味着如果在 bar 函数或者 foo 函数中使用了外部变量,那么 JavaScript 引擎会去全局执行上下文中查找。
我们把通过作用域查找变量的链条称为作用域链;作用域链是通过词法作用域来确定的,而词法作用域反映了代码的结构。
词法作用域
词法作用域就是指作用域是由代码中函数声明的位置来决定的,所以词法作用域是静态的作用域,通过它就能够预测代码在执行过程中如何查找标识符。
词法作用域是代码阶段就决定好的,和函数是怎么调用的没有关系。块级作用域中的变量查找
首先是在 bar 函数的执行上下文中查找,但因为 bar 函数的执行上下文中没有定义 test 变量,所以根据词法作用域的规则,下一步就在 bar 函数的外部作用域中查找,也就是全局作用域。
- 闭包(closure)
在 JavaScript 中,根据词法作用域的规则,内部函数总是可以访问其外部函数中声明的变量,当通过调用一个外部函数返回一个内部函数后,即使该外部函数已经执行结束了,但是内部函数引用外部函数的变量依然保存在内存中,我们就把这些变量的集合称为闭包。比如外部函数是 foo,那么这些变量的集合就称为 foo 函数的闭包。
- 闭包使用原则
如果该闭包会一直使用,那么它可以作为全局变量而存在;但如果使用频率不高,而且占用内存又比较大的话,那就尽量让它成为一个局部变量。
this:从JavaScript执行上下文的视角讲清楚this
this 是和执行上下文绑定的,也就是说每个执行上下文中都有一个 this。
执行上下文主要分为三种——全局执行上下文、函数执行上下文和 eval 执行上下文,所以对应的 this 也只有这三种——全局执行上下文中的 this、函数中的 this 和 eval 中的 this。
全局执行上下文中的 this
全局执行上下文中的 this 是指向 window 对象的。这也是 this 和作用域链的唯一交点,作用域链的最底端包含了 window 对象,全局执行上下文中的 this 也是指向 window 对象。-
函数执行上下文中的 this
通过函数的 call、bind 和 apply 方法设置
this 由参数决定。通过对象调用方法设置
在全局环境中调用一个函数,函数内部的 this 指向的是全局变量 window。
通过一个对象来调用其内部的一个方法,该方法的执行上下文中的 this 指向对象本身。通过构造函数中设置
首先 new 关键字会创建一个空的对象,然后会自动调用一个函数 apply 方法,将 this 指向这个空对象,这样的话函数内部的 this 就会被这个空的对象替代。
(1)若无 return 语句,则默认返回 this 即构造函数的实例。
(2)若有 return 语句,如果 return 了一个基本数据类型,则最终返回 this
(3)若有 return 语句,如果 return 了一个对象,则最终返回这个对象
-
this 的设计缺陷以及应对方案
-
嵌套函数中的 this 不会从外层函数中继承
var myObj = { name: " 极客时间 ", showThis: function () { console.log(this) // myObj function bar() { console.log(this) } bar() // Window } } myObj.showThis()
解决方案:
第一种是把 this 保存为一个 self 变量,再利用变量的作用域机制传递给嵌套函数。
第二种是继续使用 this,但是要把嵌套函数改为箭头函数,因为箭头函数没有自己的执行上下文,所以它会继承调用函数中的 this。 普通函数中的 this 默认指向全局对象 window
在严格模式下,默认执行一个函数,其函数的执行上下文中的 this 值是 undefined
-