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

变量提升(Hoisting)

变量的声明和赋值

  1. var myName = '极客时间'
  2. var myName = undefined; // 声明
  3. myName = '极客时间' // 赋值

image.png

函数的声明和赋值

  1. function foo(){
  2. console.log('foo');
  3. }
  4. function foo = function(){
  5. console.log('foo');
  6. }
  7. ----------------------------------
  8. var bar = function(){
  9. console.log('bar');
  10. }
  11. var bar = undefined;
  12. bar = function(){
  13. console.log('bar');
  14. }

image.png

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

  1. showName()
  2. console.log(myname)
  3. var myname = '极客时间'
  4. function showName() {
  5. console.log('函数showName被执行');
  6. }

image.png
如果函数用关键字 function声明的,直接提升到顶部,如果函数用变量形式声明,则提升的是变量

  1. // 函数的声明提升
  2. sayHello() // 打印hello
  3. function sayHello(){
  4. console.log('hello');
  5. }
  6. sayNoFn() // 报错(sayNoFn is not a function) sayNoFn 此时是undefined
  7. var sayNoFn = function(){
  8. console.log('no');
  9. }

JavaScript 代码的执行流程

先编译,再执行

image.png

  1. showName()
  2. console.log(myname)
  3. var myname = '极客时间'
  4. function showName() {
  5. console.log('函数showName被执行');
  6. }

1. 编译阶段

image.png

  • 变量提升部分的代码
    1. var myName = undefined;
    2. function showName(){
    3. console.log('函数showName被执行');
    4. }
    1. VariableEnvironment: // 初始变量环境
    2. myName -> undefined
    3. showName -> function: console.log('函数showName被执行');

2. 执行阶段

  • 执行部分的代码

    1. showName(); // 变量环境中有showName的应用, 执行打印 '函数showName被执行'
    2. console.log(myName); // 变量环境中有myName, 执行打印为 undefined
    3. myName = '极客时间' // 查找变量环境中的myName, 赋值为‘极客时间’
    1. VariableEnvironment: // 执行后变量环境
    2. myName -> '极客时间'
    3. showName -> function: console.log('函数showName被执行');

    总结

  • JS代码在执行的过程中,需要先做变量提升,之所以需要变量提升,是因为JS代码在执行之前需要先编译

  • 编译 阶段,变量和函数会被存放在变量环境中,变量的默认值会被设置为undefined,在代码的执行阶段,JS引擎会从变量环境中去找自定义的变量和函数
  • 如果在编译阶段,存在两个完全相同的函数、变量,那么最终存放在变量环境中的是最后定义的那个(覆盖之前的)

思考时间

  1. showName()
  2. var showName = function() {
  3. console.log(2)
  4. }
  5. function showName() {
  6. console.log(1)
  7. }
  • 编译

    关于同名变量和函数的两点处理原则: 1:如果是同名的 函数 / 变量,JavaScript编译阶段会选择最后声明的那个。 2:如果变量和函数同名,那么在编译阶段,变量的声明会被忽略

  1. VariableEnvironment
  2. showName -> function: console.log(1)
  • 执行

    1. showName() // 此时变量环境中只有 showName -> function: console.log(1) 打印1

    执行到第二行,赋值,此时变量环境为:

    1. VariableEnvironment
    2. showName -> function: console.log(2)

    如果在下面再有函数调用showName,因为 showName函数的值被覆盖,此时打印 2

  • 函数和变量同名,变量声明被忽略

  • 当执行到赋值语句时,变量被重新赋值
    1. showName() // 1
    2. console.log('showName', showName); // showName ƒ showName() {console.log(1)}
    3. var showName = function() {
    4. console.log(2)
    5. }
    6. function showName() {
    7. console.log(1)
    8. }
    9. showName() // 2

08 | 调用栈:为什么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)
  1. 第一步,创建全局上下文,并将其压入栈底

image.png

  1. 第二步是调用 addAll 函数

image.png

  1. 第三步,当执行到 add 函数

image.png

  1. add 函数返回,该函数的执行上下文弹出

image.png
5.addAll函数返回,从函数栈中弹出
image.png

总结

  • 调用栈

    • 每调用一个函数,JavaScript 引擎会为其创建执行上下文,并把该执行上下文压入调用栈,然后 JavaScript 引擎开始执行函数代码。
    • 如果在一个函数 A 中调用了另外一个函数 B,那么 JavaScript 引擎会为 B 函数创建执行上下文,并将 B 函数的执行上下文压入栈顶。
    • 当前函数执行完毕后,JavaScript 引擎会将该函数的执行上下文弹出栈。当分配的调用栈空间被占满时,会引发“堆栈溢出”问题
  • 执行上下文

    • 全局执行上下文
    • 函数执行上下文
    • eval 执行上下文

      思考

      1. function runStack (n) {
      2. if (n === 0) return 100;
      3. return runStack( n- 2);
      4. }
      5. runStack(50000)
      如何解决“栈溢出”
      image.png
  • 改变代码的模式,避免使用递归

  • 使用定时器的方法来吧当前的任务拆分为很多小任务

    1. function runStack (n) {
    2. if (n === 0) return 100;
    3. sum +=n;
    4. return setTimeout(()=>{
    5. runStack( n- 2);
    6. },0);
    7. }
    8. runStack(50000)

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

    块级作用域就是通过词法环境的栈结构来实现的,而变量提升是通过变量环境来实现,通过这两者的结合,JS 支持块级作用域和变量提升同时存在

    作用域scope

    概念

    作用域是指程序中变量定义的区域,它决定了变量和函数的生命周期,通俗来讲,作用域就是变量和函数的可访问范围,它决定了变量和函数的可见性和生命周期

    分类

  • 全局作用域:在代码的任何地方都可以访问,声明周期伴随着页面的生命周期

  • 函数作用域:在函数内定义的变量或函数,只能在函数内访问,函数执行结束后会被销毁
  • 块级作用域:在一个块级(函数、判断语句、循环语句、{})定义的变量或函数,只能在块内访问

    变量提升所带来的问题

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

    1. var myname = "极客时间"
    2. function showName(){
    3. console.log(myname); // undefined
    4. if(0){
    5. var myname = "极客邦"
    6. }
    7. console.log(myname); // "极客邦"
    8. }
    9. showName()
  2. 本应销毁的变量没有被销毁

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

    ES6 是如何解决变量提升带来的缺陷

    通过引入let 和 const 声明块级作用域

    JavaScript 是如何支持块级作用域的

    通过变量环境实现变量提升,通过词法环境的栈结构实现块级作用域,通过这两者的结合,JavaScript 引擎也就同时支持了变量提升和块级作用域了

  1. variableEnvironment
  2. a --> 1
  3. function foo(){
  4. var a = 1
  5. let b = 2
  6. {
  7. let b = 3
  8. var c = 4
  9. let d = 5
  10. console.log(a) // 1
  11. console.log(b) // 3
  12. }
  13. console.log(b) // 2
  14. console.log(c) // 4
  15. console.log(d) // d is not defined
  16. }
  17. foo()
  1. 第一步是编译并创建执行上下文(块内通过let 声明的变量并没有存放到词法环境中)

image.png

  1. 第二步继续执行代码(当进入函数的作用域块时,作用域块中通过 let 声明的变量,会被存放在词法环境的一个单独的区域中,这个区域中的变量并不影响作用域块外面的变量)

image.png

  1. 执行语句时,沿着词法环境的栈顶向下查询,如果在词法环境中的某个块中查找到了,就直接返回给 JavaScript 引擎,如果没有查找到,那么继续在变量环境中查找。

image.png

  1. 当作用域块执行结束之后,其内部定义的变量就会从词法环境的栈顶弹出,最终执行上下文如下图所示

image.png

思考

  1. let myname= '极客时间'
  2. {
  3. console.log(myname) //Uncaught ReferenceError Cannot access 'myname' before initialization
  4. let myname= '极客邦'
  5. }

最终打印结果

VM6277:3 Uncaught ReferenceError: Cannot access ‘myname’ before initialization

分析原因

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

拓展

var的创建和初始化被提升,赋值不会被提升。
let的创建被提升,初始化和赋值不会被提升。
function的创建、初始化和赋值均会被提升。

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

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

分析

image.png
全局执行上下文:
变量环境

  • bar: undefined
  • foo: undefined
  • myName: ‘极客时间’

词法环境:无
执行代码 foo() — foo 函数执行上下文:
变量环境:

  • myName: ‘极客邦’
  • outer —> 全局

词法环境:无
执行 bar() — bar 函数执行上下文:
变量环境

  • outer —> 全局

    词法环境: 无

执行代码 console.log(myName)
在bar执行上下文查找 myName(无) —> 全局myName === 极客时间

作用域链

在每个执行上下文的变量环境中,都包含了一个外部引用,用来指向外部的执行上下文,我们把这个外部引用称为 outer,当一段代码使用了一个变量时,JavaScript 引擎首先会在“当前的执行上下文”中查找该变量,如果在“当前执行上下文”没有找到,就继续向外层执行上下文查找,一直到全局作用域,如果仍然没有找到,则宣布找不到,返回undefined, 我们把这个查找的链路成为“作用域链

词法作用域

词法作用域就是指作用域是由代码中函数声明的位置来决定的,所以词法作用域是静态的作用域,通过它就能够预测代码在执行过程中如何查找标识符, 词法作用域是代码编译阶段就决定好的,和函数是怎么调用的没有关系(根据代码的位置来决定的)。
决定了函数如何按照作用域链查找变量(与函数怎么被调用没有关系,只跟代码的位置有关系)
如果一个函数在全局作用域,那如果再函数找不到变量的时候,就去全局找(即使他可能被其他函数在内部调用)
**
通过词法作用域,我们分析了在 JavaScript 的执行过程中,作用域链是已经注定好了,比如即使在 foo 函数中调用了 bar 函数,你也无法在 bar 函数中直接使用 foo 函数中的变量信息 ==> 看代码结构
image.png
foo 函数作用域—>bar 函数作用域—>main 函数作用域—> 全局作用域

块级作用域中的变量查找

  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
全局执行上下文
变量环境:
myName: “极客时间”
foo: ()
outer: null
词法环境
myAge: 10
test: 1
foo 执行上下文
变量环境:
myName: ‘极客帮’
outer: 全局
词法环境
test: 2
test: 3
bar 执行上下文
变量环境
myName: ‘极客世界’
outer: 全局
词法环境
test1: 100
myName: Chrome浏览器

执行 console.log(test) // 1

闭包

在 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())

分析

全局执行上下文:
变量环境
bar: undefined
outer: null
词法环境


foo函数执行上下文:
变量环境:
myName: ‘极客时间’
innerBar: function(){}
outer:
词法环境**
test1: 1
test2: 2
image.png


执行 var bar = foo() 时,foo()函数执行,返回 innerBar
image.png


执行 bar.setName(“极客邦”), setName 函数按照作用域链查找myName, setName —> foo闭包 —> 全局
bar.getName(), 打印test1 // 1 作用域链:getName —> foo闭包 —> 全局
console.log(bar.getName()) // 1 极客邦 作用域链:getName —> foo闭包 —> 全局
image.png
image.png

闭包是怎么回收的

  • 引用闭包的是全局变量:一直存在,直到页面关闭,可能导致内存泄漏
  • 引用闭包的是局部变量:函数销毁后,会被垃圾回收,释放内存

思考时间

  1. var bar = {
  2. myName:"time.geekbang.com",
  3. printName: function () {
  4. console.log(myName)
  5. }
  6. }
  7. function foo() {
  8. let myName = "极客时间"
  9. return bar.printName
  10. }
  11. let myName = "极客邦"
  12. let _printName = foo()
  13. _printName()
  14. bar.printName()

分析:
全局执行上下文:
变量环境:
bar:{
myName:”time.geekbang.com”,
printName: function () {
console.log(myName)
}
}
outer:null
词法环境:
myName: ‘极客邦’
_printName:foo

foo 执行上下文:
变量环境:
outer:window
词法环境:
myName: ‘极客时间’
查找 return bar.printName 查找 bar: foo 词法 —> foo 变量 —> outer 词法 —>
outer 变量找到 bar.printName

bar.printName执行上下文
变量环境:

outer: window
词法环境:

执行 console.log(myName) —> bar.printName 词法 —> bar.printName 变量 —>
全局词法找到myName: ‘极客邦’

执行bar.printName()
bar.printName执行上下文
变量环境:

outer: window
词法环境:

执行 console.log(myName) —> bar.printName 词法 —> bar.printName 变量 —>
全局词法找到myName: ‘极客邦’

11 | this:从JavaScript执行上下文的视角讲清楚this

JavaScript 中的 this 是什么

image.png

概念

this 是和上下文绑定的,每个执行上下文都有对应的this

分类

全局执行上下文中的this

  • 全局执行上下文中的this总是指向window

    函数执行上下文中的this

    调用一个函数,默认this也是指向window
    1. function foo(){
    2. console.log(this) // window
    3. }
    4. foo()

可以通过以下方式改变this的指向

  1. 通过函数的 call 方法设置

    1. let bar = {
    2. myName : "极客邦",
    3. test1 : 1
    4. }
    5. function foo(){
    6. this.myName = "极客时间"
    7. }
    8. foo.call(bar) // 将foo函数中的this更改为bar
    9. console.log(bar) // {myName : "极客时间",test1 : 1}
    10. console.log(myName) // myName is not defined
  2. apply

    1. let bar = {
    2. myName : "极客邦",
    3. test1 : 1
    4. }
    5. function foo(){
    6. this.myName = "极客时间"
    7. }
    8. foo.apply(bar) // 将foo函数中的this更改为bar
    9. console.log(bar) // {myName : "极客时间",test1 : 1}
    10. console.log(myName) // myName is not defined
  3. bind

    1. let bar = {
    2. myName : "极客邦",
    3. test1 : 1
    4. }
    5. function foo(){
    6. this.myName = "极客时间"
    7. }
    8. foo.bind(bar)(); // 将foo函数中的this更改为bar
    9. console.log(bar) // {myName : "极客时间",test1 : 1}
    10. console.log(myName) // myName is not defined
  4. 通过对象调用方法设置(使用对象来调用其内部的一个方法,该方法的 this 是指向对象本身的)

    1. var myObj = {
    2. name : "极客时间",
    3. showThis: function(){
    4. console.log(this)
    5. }
    6. }
    7. myObj.showThis() // ==> myObj.showThis().call(myObj)
  5. 通过构造函数中设置

    1. function CreateObj(){
    2. this.name = "极客时间"
    3. }
    4. var myObj = new CreateObj()

    eval执行上下文中的this

    不常用

    this 的设计缺陷以及应对方案

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

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

    函数 bar 中的 this 指向的是全局 window 对象,而函数 showThis 中的 this 指向的是 myObj 对象

解决方案:

  • 把this体系转化为变量作用域体系 - 声明一个变量 self 用来保存 this,然后在 bar 函数中使用 self

    1. var myObj = {
    2. name : "极客时间",
    3. showThis: function(){
    4. console.log(this)
    5. var self = this // 保存this
    6. function bar(){
    7. self.name = "极客邦"
    8. }
    9. bar()
    10. }
    11. }
    12. myObj.showThis()
    13. console.log(myObj.name)
    14. console.log(window.name)
  • 使用 ES6 中的箭头函数(ES6 中的箭头函数并不会创建其自身的执行上下文,所以箭头函数中的 this 取决于它的外部函数)

    1. var myObj = {
    2. name : "极客时间",
    3. showThis: function(){
    4. console.log(this)
    5. var bar = ()=>{
    6. this.name = "极客邦"
    7. console.log(this)
    8. }
    9. bar()
    10. }
    11. }
    12. myObj.showThis()
    13. console.log(myObj.name)
    14. console.log(window.name)
  1. 普通函数中的 this 默认指向全局对象 window

当函数被正常调用时

  • 在严格模式下,this 值是 undefined
  • 非严格模式下 this 指向的是全局对象 window

思考题

改变如下代码,使this指向正确的位置

  1. let userInfo = {
  2. name:"jack.ma",
  3. age:13,
  4. sex:male,
  5. updateInfo:function(){
  6. //模拟xmlhttprequest请求延时
  7. setTimeout(function(){
  8. this.name = "pony.ma"
  9. this.age = 39
  10. this.sex = female
  11. },100)
  12. }
  13. }
  14. userInfo.updateInfo()
  • 箭头函数
  • 存储this
  • call
  • bind
  • apply

call 、apply、bind的对比(https://juejin.cn/post/6844903567967387656

共同目标:改变函数执行上下文,也就是改变this的指向

  1. let obj1 = {
  2. name: 'wuyanbin',
  3. sayName: function(){
  4. console.log(this.name);
  5. }
  6. }
  7. let obj2 = {
  8. name: 'jiale'
  9. }
  10. obj1.sayName(); // wuyanbin
  11. obj1.sayName.call(obj2); // jiele
  12. obj1.sayName.apply(obj2); // jiale
  13. let bind = obj1.sayName.bind(obj2);
  14. bind(); // jiale

bind

  • 执行时机:事先把fn的this改变为我们要想要的结果,并且把对应的参数值准备好,以后要用到了,直接的执行即可

    apply

  • 执行时机:马上执行改变this后的结果

  • 参数:第一个是改变上下文的对象,第二个是以参数列表的形式传入

    call

  • 执行时机:马上执行改变this后的结果

  • 参数:第一个是改变上下文的对象,第二个是以数组的形式传入

应用场景

  • 转化伪数组

Array.prototype.slice.call(obj)

  1. let obj4 = {
  2. 0: 1,
  3. 1: 'thomas',
  4. 2: 13,
  5. length: 3 // 一定要有length属性
  6. };
  7. console.log(Array.prototype.slice.call(obj4)); // [1, "thomas", 13]
  • 判断变量类型

Object.prototype.toString.call('变量')

  1. let arr1 = [1,2,3];
  2. let str1 = 'string';
  3. let obj1 = {name: 'thomas'};
  4. //
  5. function isArray(obj) {
  6. return Object.prototype.toString.call(obj) === '[object Array]';
  7. }
  8. console.log(fn1(arr1)); // true
  9. // 判断类型的方式,这个最常用语判断array和object,null(因为typeof null等于object)
  10. console.log(Object.prototype.toString.call(arr1)); // [object Array]
  11. console.log(Object.prototype.toString.call(str1)); // [object String]
  12. console.log(Object.prototype.toString.call(obj1)); // [object Object]
  13. console.log(Object.prototype.toString.call(null)); // [object Null]
  • 数组拼接,添加 ``` let arr1 = [1,2,3]; let arr2 = [4,5,6];

//数组的concat方法:返回一个新的数组 let arr3 = arr1.concat(arr2); console.log(arr3); // [1, 2, 3, 4, 5, 6]

console.log(arr1); // [1, 2, 3] 不变 console.log(arr2); // [4, 5, 6] 不变 // 用 apply方法 [].push.apply(arr1,arr2); // 给arr1添加arr2 console.log(arr1); // [1, 2, 3, 4, 5, 6] console.log(arr2); // 不变

  1. - 使用callapply做继承

function Animal(name){
this.name = name;
this.showName = function(){
console.log(this.name);
}
}

function Cat(name){
Animal.call(this, name);
}

// Animal.call(this) 的意思就是使用this对象代替Animal对象,那么 // Cat中不就有Animal的所有属性和方法了吗,Cat对象就能够直接调用Animal的方法以及属性了 var cat = new Cat(“TONY”);
cat.showName(); //TONY

  1. - 多继承

function Class1(a,b) { this.showclass1 = function(a,b) { console.log(class1: ${a},${b}); } }

function Class2(a,b) { this.showclass2 = function(a,b) { console.log(class2: ${a},${b}); } }

function Class3(a,b,c) { Class1.call(this); Class2.call(this); }

let arr10 = [2,2]; let demo = new Class3(); demo.showclass1.call(this,1); // class1: 1,undefined demo.showclass1.call(this,1,2); // class1: 1,2 demo.showclass2.apply(this,arr10); // class2: 2,2

  1. > 手写bindapplycall
  2. <a name="cjuNe"></a>
  3. ### 普通函数和箭头函数
  4. [https://zhuanlan.zhihu.com/p/106675713](https://zhuanlan.zhihu.com/p/106675713)<br />**(1)箭头函数是匿名函数,不能作为构造函数,不能使用new。**<br />**(2)箭头函数不绑定arguments,取而代之用rest参数...解决**<br />**(3)this的作用域不同,箭头函数不绑定this,会捕获其所在的上下文的this值,作为自己的this值。**<br />**(4)箭头函数没有原型属性**<br />**(5)箭头函数不能当做Generator函数,不能使用yield关键字**
  5. <a name="ZPF74"></a>
  6. ### ***new 的过程
  7. [https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Operators/new](https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Operators/new)**`
  8. **`new`** 关键字会进行如下的操作:
  9. 1. 创建一个空的简单JavaScript对象(即`**{}**`);
  10. 2. 链接该对象(设置该对象的**constructor**)到另一个对象
  11. 3. 将步骤1新创建的对象作为`**this**`的上下文
  12. 4. 如果该函数没有返回对象,则返回`**this**`
  13. 当执行 new CreateObj() 的时候,JavaScript 引擎做了如下四件事:
  14. - 首先创建了一个空对象 tempObj
  15. - 接着调用 CreateObj.call 方法,并将 tempObj 作为 call 方法的参数,这样当 CreateObj 的执行上下文创建时,它的 this 就指向了 tempObj 对象;
  16. - 然后执行 CreateObj 函数,此时的 CreateObj 函数执行上下文中的 this 指向了 tempObj 对象;
  17. - 最后返回 tempObj 对象。
  18. 为了直观理解,我们可以用代码来演示下
  19. ```javascript
  20. var tempObj = {};
  21. CreateObj.call(tempObj);
  22. return tempObj

这样,我们就通过 new 关键字构建好了一个新对象,并且构造函数中的 this 其实就是新对象本身。