变量提升:JavaScript代码是按顺序执行的吗?

变量提升(Hoisting)

变量提升是指在js代码执行过程中,js引擎把变量的声明部分和函数的声明部分提升到代码开头的“行为”。变量被提升后,会给变量设置默认值,这个默认值就是 undefined

声明与赋值 的 概念

  • 变量
    1. var myname = "极客时间"
    =》会执行下面的两步操作
    1. var myname
    2. myname = "极客时间"

image.png

  • 函数 ```javascript //无赋值操作 function foo(){ console.log(“foo”)
    }

//先声明变量bar,在给bar赋值 var bar = function(){ console.log(“bar”) }

  1. ![image.png](https://cdn.nlark.com/yuque/0/2020/png/618347/1605160640514-b36a7231-f182-44f9-bd5f-a8d613c73041.png#align=left&display=inline&height=545&margin=%5Bobject%20Object%5D&name=image.png&originHeight=545&originWidth=820&size=71485&status=done&style=none&width=820)
  2. ![image.png](https://cdn.nlark.com/yuque/0/2020/png/618347/1605160959844-03038212-ae24-4f67-a6ee-d1d0743d8052.png#align=left&display=inline&height=466&margin=%5Bobject%20Object%5D&name=image.png&originHeight=466&originWidth=711&size=111993&status=done&style=none&width=711)
  3. <a name="kxahF"></a>
  4. ### javascript 代码的执行流程
  5. > 实际上变量和函数声明在代码里的位置是不会改变的,而且是在编译阶段被javascript引擎放入内存中,编译完成后在进入执行阶段。
  6. <a name="4DqOi"></a>
  7. #### 1.编译阶段
  8. 第一部分:变量提升部分代码<br />第二部分:执行部分的代码<br />![image.png](https://cdn.nlark.com/yuque/0/2020/png/618347/1605161151767-5b070710-5c55-49df-98ad-8b445255eff4.png#align=left&display=inline&height=493&margin=%5Bobject%20Object%5D&name=image.png&originHeight=493&originWidth=742&size=158357&status=done&style=none&width=742)<br />**执行上下文 **是 js 执行一段代码时的运行环境,在执行上下文中存在一个变量环境的对象(Viriable Environment)该对象保存了变量提升的内容。
  9. <a name="KvvwM"></a>
  10. #### 2.执行阶段
  11. <a name="fuJKX"></a>
  12. #### 代码中出现相同的变量或者函数怎么办?
  13. ```javascript
  14. function showName(){
  15. console.log("chu")
  16. }
  17. showName() //初
  18. function showName(){
  19. console.log("初")
  20. }
  21. showName() //初

总结:一段代码如果定义了两个相同名字的函数,那么最终生效的是最后一个函数

思考:当变量名与函数名重复时,谁的优先级较高

  1. showName()
  2. var showName = function () {
  3. console.log(2)
  4. }
  5. function showName() {
  6. console.log(1)
  7. }
  8. // 1

规则

  1. 如果是同名的函数,js编译阶段会选择最后声明的那个
  2. 如果是变量和函数同名,那么在编译阶段,变量的声明会被忽略

调用栈:为什么javascript代码会出现栈溢出?

哪些情况下代码会在执行之前就进行编译并创建执行上下文:

  1. 当 JavaScript 执行全局代码的时候,会编译全局代码并创建全局执行上下文,而且在
    整个页面的生存周期内,全局执行上下文只有一份。
  2. 当调用一个函数的时候,函数体内的代码会被编译,并创建函数执行上下文,一般情况
    下,函数执行结束之后,创建的函数执行上下文会被销毁。
  3. 当使用 eval 函数的时候,eval 的代码也会被编译,并创建执行上下文。

image.png

在js中有很多函数,经常会出现在一个函数中调用另外一个函数的情况,调用栈就是用来管理函数调用关系的一种数据结构。

什么是函数调用

函数调用就是运行一个函数,具体使用方式是使用函数名称跟着一对小括号。

  1. var a = 2
  2. function add() {
  3. var b = 10
  4. return a + b
  5. }
  6. add()

image.png

javascript 引擎通过一种叫栈的数据结构来管理 这些执行上下文。

什么是栈(stack):后进先出

什么是javascript的调用栈: 把用来管理执行上下文的栈称为执行上下文栈,又称调用栈


分析代码

  1. var a = 2
  2. function add(b, c) {
  3. return b + c
  4. }
  5. function addAll(b, c) {
  6. var d = 10
  7. result = add(b, c)
  8. return a + result + d
  9. }
  10. addAll(3, 6)

第一步:创建全局上下文 并将其压入栈底
image.png

image.png

第二步 调用addAll函数

image.png

第三步: 当执行到add函数调用时,同样会为其创建执行上下文,并将其压入调用栈
image.png

image.png

image.png

如何利用浏览器查看调用栈的信息
开发者工具 source 或者 console.trace()

image.png

块级作用域:var缺陷以及为什么要引入let和const

作用域(scope)

作用域是指在程序中定义变量的区域,该位置决定了变量的的生命周期。简单来说作用域就是变量与函数的可访问范围,即作用域控制着变量和函数的可见性和生命周期。

  • 全局作用域:中的对象在代码中的任何地方都能访问,其生命周期伴随着页面的生命周期。
  • 函数作用域:就是在函数内部定义的变量或者函数,并且定义的变量或者函数只能在函数内 部被访问。函数执行结束之后,函数内部定义的变量会被销毁。
  • 块级作用域:块级作用域就是使用一对大括号包裹的一段代码,比如函数、判断语句、循环语句, 甚至单独的一个{}都可以被看作是一个块级作用域。
  1. //if块
  2. if(1){}
  3. //while
  4. while(1){}
  5. //函数块
  6. function foo(){}
  7. //for 循环块
  8. for(let i=0;i<100;i++){}
  9. //单独一个块
  10. {}

变量提升带来的问题

1.变量容易在不被察觉的情况下被覆盖掉

??? 疑惑:赋值之后 为什么下面的console 还是依旧输出 undefiend

  1. var myname = " 极客时间 "
  2. function showName() {
  3. console.log(myname); //undefiend
  4. if (0) {
  5. var myname = " 极客邦 "
  6. }
  7. console.log(myname); //undefiend
  8. }
  9. showName()

2.本应销毁的变量没有被销毁

  1. function foo(){
  2. for(var i =0;i<7;i++){
  3. }
  4. console.log(i) //7
  5. }
  6. foo()

ES6中如何解决

  1. let x = 5
  2. const y = 6
  3. x = 7
  4. y = 9 //报错 const声明的变量不可修改

使用 let 关键字声明的变量是可以被改变的,而const 声明的变量其值是不可以被改变的。

javascript 是如何支持块级作用域

  1. function foo(){
  2. var a = 1
  3. let b = 2
  4. {
  5. let b = 3
  6. var c = 4
  7. let d = 5
  8. console.log(a)
  9. console.log(b)
  10. }
  11. console.log(b)
  12. console.log(c)
  13. console.log(d)
  14. }
  15. foo()

第一步:编译并创建执行上下文
image.png
函数内部通过var声明的变量,在编译阶段全都被存放到 变量环境 里
通过let声明的变量,在编译阶段会被存放到词法环境中
在函数的作用域内部,通过let声明的变量并没有被存放到词法环境中

image.png

image.png

思考:

  1. let myname = "极客时间"
  2. {
  3. console.log(myname)
  4. let myname= "ccc"
  5. }

Uncaught SyntaxError: Identifier 'myname' has already been declared

在块级作用域内,let声明的变量被提升,但变量只是创建被提升,初始化并没有提升,在初始化之前使用变量,就会形成一个暂时性死区。

作用域链和闭包: 代码中出现相同的变量,javascript引擎如何选择?

  1. function bar(){
  2. console.log(myName)
  3. }
  4. function foo(){
  5. var myName = "ccc"
  6. bar()
  7. }
  8. var myName = "极客时间"
  9. foo() // 极客时间

作用域链

一级一级查找的链条就称为作用域链

词法作用域
指作用域是由代码中函数声明的位置来决定的,所以词法作用域是静态的作用域,通过它就能够预测代码在执行过程中如何查找标识符。
注意:词法作用域就是代码阶段就决定好的,和函数是怎么调用没有任何关系

块级作用域中的变量查找

  1. function bar() {
  2. var myName = " 极客世界 "
  3. let test1 = 100
  4. if (1) {
  5. let myName = "Chrome 浏览器 "
  6. console.log(test)
  7. }
  8. }
  9. function foo() {
  10. var myName = " 极客邦 "
  11. let test = 2;
  12. {
  13. let test = 3
  14. bar()
  15. }
  16. }
  17. var myName = " 极客时间 "
  18. let myAge = 10
  19. let test = 1
  20. foo()

image.png
**

闭包

在javascript中,根据词法作用域的规则,内部函数总是可以访问其外部函数中声明的变量,当通过调用一个外部函数返回一个内部函数后,即使该外部函数已经执行结束了,但是内部函数引用外部函数的变量依然保存在内存中,把这些变量的集合称为闭包,比如 外部函数是foo,那么这些变量的集合就称为foo函数的闭包。

  1. function foo() {
  2. var myName = " 极客时间 "
  3. let test1 = 1
  4. const test2 = 2
  5. var innerBar = {
  6. getName: function () {
  7. console.log(test1)
  8. return myName
  9. },
  10. setName: function (newName) {
  11. myName = newName
  12. }
  13. }
  14. return innerBar
  15. }
  16. var bar = foo()
  17. bar.setName(" 极客邦 ")
  18. bar.getName()
  19. console.log(bar.getName())
  20. //1
  21. //1
  22. // 极客邦

image.png

this: 从javasvript 执行上下文的视角讲清楚this

image.png

this 是和执行上下文绑定的,也就是说每个执行上下文中都有 一个 this

全局执行上下文中的this
window 对象

函数执行上下文
**
1.通过函数的 call 方法
以及 bind(需要自己执行) apply(参数传递为数组 )

  1. let bar = {
  2. myName:"极客邦",
  3. test1:1
  4. }
  5. function foo(){
  6. this.myName = "极客时间"
  7. }
  8. foo.call(bar)
  9. console.log(bar) //极客时间

2.通过对象调用方法设置**

  1. var myObj ={
  2. name:"极客时间",
  3. showThis:function(){
  4. console.log(this)
  5. }
  6. }
  7. myObj.showThis() // myObj 这个对象
  8. 相当于转换为
  9. myObj.showThis.call(myObj)
  1. var myObj ={
  2. name:"极客时间",
  3. showThis:function(){
  4. this.name = "极客邦"
  5. console.log(this)
  6. }
  7. }
  8. var foo = myObj.showThis // myObj 这个对象
  9. foo() //window

结论:
在全局环境中调用一个函数,函数内部的this指向的是全局变量 window
使用对象来调用其内部的一个方法,该方法的this是指向对象本身


3.通过构造函数中设置
当执行 new CreateObj()

  1. 首先创建一个空对象 tempObj
  2. 接着调用 CreateObj.call 方法,并将tempObj作为call方法的参数,这样当CreateObj 的执行上下文创建时,它的this就指向了tempObj 对象
  3. 然后执行 CreateObj 函数,此时CreateObj函数执行上下文中的this指向了tempObj
  4. 最后返回tempObj对象
  1. function CreateObj(){
  2. this.name = "极客时间"
  3. }
  4. var myObj = new CreateObj()
  5. 代码演示
  6. var tempObj = {}
  7. CreateObj.call(tempObj)
  8. return tempObj


存在的问题

1.嵌套函数中的this不会从外层函数中继承

  1. var myObj = {
  2. name:"极客时间",
  3. showThis:function(){
  4. console.log(this) //myObj
  5. function bar(){
  6. console.log(this) //window
  7. }
  8. bar()
  9. }
  10. }
  11. myObj.showThis()


解决:
1.在 showThis 函数中声明一个变量 self 用来保存 this**

  1. var myObj = {
  2. name:"极客时间",
  3. showThis:function(){
  4. console.log(this) //myObj
  5. var self = this
  6. function bar(){
  7. console.log(self) //myObj
  8. }
  9. bar()
  10. }
  11. }
  12. myObj.showThis()

2.使用箭头函数
因为箭头函数没有自己的执行上下文,所以它会继承调用函数中的this
**