[深入01] 执行上下文 和 作用域

2,402 阅读17分钟

导航

[深入01] 执行上下文
[深入02] 原型链
[深入03] 继承
[深入04] 事件循环
[深入05] 柯里化 偏函数 函数记忆
[深入06] 隐式转换 和 运算符
[深入07] 浏览器缓存机制(http缓存机制)
[深入08] 前端安全
[深入09] 深浅拷贝
[深入10] Debounce Throttle
[深入11] 前端路由
[深入12] 前端模块化
[深入13] 观察者模式 发布订阅模式 双向数据绑定
[深入14] canvas
[深入15] webSocket
[深入16] webpack
[深入17] http 和 https
[深入18] CSS-interview
[深入19] 手写Promise
[深入20] 手写函数

[react] Hooks

[部署01] Nginx
[部署02] Docker 部署vue项目
[部署03] gitlab-CI

[源码-webpack01-前置知识] AST抽象语法树
[源码-webpack02-前置知识] Tapable
[源码-webpack03] 手写webpack - compiler简单编译流程
[源码] Redux React-Redux01
[源码] axios
[源码] vuex
[源码-vue01] data响应式 和 初始化渲染

前置知识

(1) 作用域 和 执行上下文的区别 ?

  • 2022.05.11
(一) 作用域
---

1
定义和作用
- 定义:作用域 指 ( 变量或函数 ) 存在的范围
- 作用:隔离变量

2
分类 ( 3种 )
- 全局作用域 -- 变量在整个程序中一直存在,其他任何地方都可以访问
- 函数作用域 -- 只在函数中存在,外部无法访问
- 块级作用域

3
形成时机
- 函数作用域:在函数创建时形成

4
函数本身的作用域
- 函数本身也是一个变量,也有自己的作用域
- 函数本身的作用域是,函数声明时所在的作用域,与函数调用时所在的作用域无关

5
局部变量 和 全局变量
- 函数内部的变量,是局变量,函数外部无法访问;此时如果有同名的局部变量和全局变量,局部变量将覆盖全局变量
- 全局变量在函数内部可以访问,即其他任何地方都可以访问到全局变量
(二) 执行上下文
---

1
本质
- 执行上下文 本质上是一个 ( 对象 )


2
分类
- 全局执行上下文对象
- 函数执行上下文对象
- eval

3
全局执行上下文
- 形成时机:全局代码执行前
- 详细
  - 在 ( 全局代码 ) 执行前,将 ( window ) 确定为 ( 全局执行上下文对象 )
  - 对 ( 全局数据 ) 进行 预处理
    - 变量提升,函数提升,并将全局变量和函数变量确定为window的属性
    - 将 this 指向 window
  
4
函数执行上下文
- 形成时机:函数调用时,并且在 ( 执行函数体之前 ),( 创建 ) 函数执行上下文对象
- 区别
  - 全局执行上下文 和 函数执行上下文 形成时机的区别
    - 全局执行上下文是把window确定为全局执行上下文,之前window就存在 ------ 之前就存在
    - 函数执行上下文在函数调用并且函数体执行前,新创建的函数执行上下文对象 --- 新创建
- 详细
  - 在 ( 函数调用,并且函数体执行前 ),新 ( 创建 ) ( 函数执行上下文对象 )
  - 形参赋值为实参
  - 变量提升,函数名提升
  - arguments 对象赋值
  - this 赋值为该函数执行上下文对象
(三) 作用域 和 执行上下文对象 的区别?
- 形成时机
  - 函数作用域:是在函数创建时形成
  - 函数执行上下文:是在函数调用,并且函数体执行前新创建的
- 动态和静态
  - 函数作用域:是静态的,函数创建后就不会再变
  - 函数执行上下文:是动态的,函数调用函数体执行前创建,调用结束后释放
- 联系
  - ( 执行上下文对象 ) 是从属于所在的 ( 作用域 )
  - ( 全局执行上下文对象 ) 从属于 ( 全局作用域 )
  - ( 函数执行上下文对象 ) 从属于 ( 函数作用域 )
  - 因为只有函数存在,才会去创建函数执行上下文对象
(四) 箭头函数中的this
- this:箭头函数中的this是,箭头函数定义时所在的作用域的 ( 上层作用域 ) 中的 this
- 作用域:全局作用域 函数作用域 块级作用域
- 特点
  - 箭头函数没有自己的this,箭头函数中的this是箭头函数定义时所在的作用域中的上层作用域中的this
  - 因为没有this,所以不能作为构造函数,不能使用 new 命令
  - 2023-01-25更新 因为没有this,所有 call bind apply 在箭头函数中,将失效
  - 没有 arguments 对象,可以使用 rest 参数代替
  - 不能使用 yield 命令,即不能作为generator函数
  

// 1
var a = 100;
const obj = {
    a: 1,
    b: function () {
      console.log(this.a);
    },
    c: () => console.log(this.a),
};
obj.b(); // 1,普通函数中的this,在函数调用时确定指向,即this指代的是函数调用时所在的对象
obj.c();
// 100,
// 箭头函数中的this,是箭头函数定义时所在所用域的上层作用域中的this
// 1. c箭头函数定义时所在的作用域是 ( 块级作用域obj )
// 2. obj块级作用域的上层作用域是window,window.a=100,所以这里输出100


// 2
var A = {
    name: "A",
    sayHello: function () {
      var s = () => console.log(this.name);
      return s;
    },
};
var sayHello = A.sayHello(); // 不管外面如何调用,都不会影响箭头函数this的指向,和调用无关
sayHello();
// 输出 A
// 1. 箭头函数s定义时所在的作用域是:函数作用域 sayHello 函数
// 2. 上层作用域:是块级作用域A,this就指向A,A.name = 'A'



// 3
window.color = "red";
//let 声明的全局变量不具有全局属性,即不能用window.访问
let color = "green";
let obj = {
    color: "blue",
    getColor: () => {
        return this.color;//this指向window
    }
};
let sayColor = () => {
    return this.color;//this指向window
};
obj.getColor();//red
sayColor();//red

分析:
1. let
  - 首先 let 声明的变量是块级作用域,并且不和window挂钩
  - var 声明的全局变量会和 window 挂钩,即 var a=1, 则 window.a=1
2. 箭头函数没有自己的this,箭头函数中的this是,箭头函数定义时所在作用域的 ( 上层作用域 ) 中的 this
  - getColor 所在的作用域是 ( 块级作用域 ),所以上层作用域是 ( window ),所以 this 指向window
  - sayColor 同理
(五) 变量提升优先级
- 形参 > 函数声明 > 变量声明
- 函数名存在,新的覆盖旧的
- 变量名存在,直接略过变量的声明
---

- 案例 1
  function a(name, age) {
    console.log(name); // wang
    var name = 10;
    console.log(name); // 10
    console.log(age);  // undefined
  }
  a('wang')
  ---
  实际执行的代码如下:
  function a(name, age) {
    // 1. 变量提升:形参 > 函数声明 > 变量声明
    var name = 'wang' // 形参赋值实参
    var age = undefined // 未传实参,则将形参赋值为undefined
    // var name = undefined 变量提升,但是变量名已经存在,则直接略过变量的声明
    console.log(name) // 'wang'
    name = 10 // 从新赋值
    console.log(name) // 10
    console.log(age) // undefined
  }
  ---
  最终结果
  'wang' 10 undefined
------------------------------------------------------------------------------
 


  - 案例 2
  function a(name) {
    console.log(name); // function name() {.....}
    var name = 10;
    function name() { console.log('20') }; }
  a('wang')
  ---
  实际执行的代码如下:
  function a(name) {
    var name = 'wang'
    // var name = undefined 变量提升,但是变量名已经存在,直接略过变量的声明
    function name() { console.log('20')} // 函数提升,但是函数名已经存在,则新的覆盖旧的,即函数覆盖掉'wang'
    console.log(name) // 此处打印函数
    name = 10 // 从新赋值
  }
  ---
  最终结果
  function name() { console.log('20') };
------------------------------------------------------------------------------  
  
  
- 案例 3
window.color = "red";
//let 声明的全局变量不具有全局属性,即不能用window.访问
let color = "green";
let obj = {
    color: "blue",
    getColor: () => {
        return this.color;//this指向window
    }
};
let sayColor = () => {
    return this.color;//this指向window
};
obj.getColor();//red
sayColor();//red

分析:
1. let
  - 首先 let 声明的变量是块级作用域,并且不和window挂钩
  - var 声明的全局变量会和 window 挂钩,即 var a=1, 则 window.a=1
2. 箭头函数没有自己的this,箭头函数中的this是,箭头函数定义时所在作用域的 ( 上层作用域 ) 中的 this
  - getColor 所在的作用域是 ( 块级作用域 ),所以上层作用域是 ( window ),所以 this 指向window
  - sayColor 同理
(六) varlet 的区别 ( 5个区别 ) ?
- 作用域
  - let 声明的变量只在 ( 块级作用域 ) 中有效
  - var 声明的变量在代码块外也能访问
- 变量提升
  - var 声明的变量存在 变量提升
  - let 不存在变量提升,即 ( 不能先访问再声明 )
- 暂时性死区
  - let 声明的变量存在暂时性死区
  - var 不存在暂时性死区
- 重复声明
  - let 不能重复声名同一个变量
  - var 可以重复声名
- 是否和 ( 顶层对象window,浏览器环境 ) 挂钩
  - let 声明的全局变量不会和window挂钩
  - var 会和window挂钩,即 var a=1 声明的全局变量,可以通过 window.a 来访问
---

1
重复声明
var a = 1
var a = 2 // 不报错
-
let bb = 1
let bb = 2 // 报错
VM933:2 Uncaught SyntaxError: Identifier 'bb' has already been declared

2
暂时性死区
var tmp = 123;
if (true) {
  tmp = 'abc'; // ReferenceError
  let tmp;
}

执行上下文的概念

执行上下文是:javaScript代码解析和执行时所在的环境,javascript中运行的所有代码都在执行上下文中执行

需要理解的一些名词

  • VO: 变量对象 variable object
  • AO: 活动对象 active object
  • EC: 执行上下文 execution context
  • ECS: 执行上下文栈 execution context stack
  • Scope Chain: 作用域链
  • Lexical Environment: 词法环境
  • Variable Environment: 变量环境
  • execution:执行
  • lexical:词汇的

执行上下文的类型

执行上下文分为三种类型:

全局执行上下文:

  • js代码运行起来,首先进入该环境。不在任何函数中的js代码都位于全局执行上下文中
  • 一个程序中只能存在一个全局上下文,位于执行上下文栈的最底部
  • 全局执行上下文会做两件事:(1)创建一个全局对象 (2)将this指向这个全局对象
  • 在浏览器环境全局对象是window,在node环境全局对象是global

函数执行上下文:

  • 特点:每次调用一个函数,都会生成一个新的函数执行上下文
  • 创建时机:每个函数都有自己的函数执行上下文,但只有函数被调用时才会被创建
  • 生命周期:函数执行上下文的生命周期分为两个阶段,创建阶段执行阶段

Eval执行上下文:

  • eval函数执行时产生的执行上下文,不建议使用

执行上下文栈(函数调用栈)

  • 栈是一个后进先出的结构,用于储存 在js代码执行期间产生的所有执行上下文
  • 具体过程:
    • 在javascript刚开始运行时,首先产生全局执行上下文,压入栈,位于栈底
    • 每当发生一个函数被调用,则产生函数执行上下文,压入栈,位于栈顶
    • 当这个函数执行完后,会从执行上下文栈中弹出
    • 引擎会继续去执行位于栈顶的函数

执行上下文的生命周期

  • 执行上下文分为两个阶段:创建阶段-预编译阶段 和 执行阶段

1. 创建阶段:(预编译阶段)

  • 执行上下文会创建: 变量对象VO(arguments,形参,变量,函数),作用域链,this
  • 2021/10/20 更新
  • 总结:
    • 创建阶段一共会做三件事情
      • 创建VO变量对象:arguments,函数形参,变量声明,函数声明
      • 创建ScopeChain作用域链
      • 确定 this 指向

2. 执行阶段:

  • 变量赋值
  • 函数引用
  • 以及执行其他代码
  • 当(执行上下文)执行完毕后,就会出栈,等待被js垃圾回收机制回收

变量对象

变量对象VO ,活动对象AO

变量对象的分类

  • 不同执行上下文环境的变量对象有不同的表现
  • 全局执行上下文中的 变量对象
  • 函数执行上下文中的 变量对象

全局执行上下文中的变量对象

  • 全局执行上下文中的变量对象就是:全局对象
  • 什么是全局对象
    • 进入任何执行上下文之前就建立的对象,只有一份,在程序任何地方都能访问,生命周期终止于程序退出时
  • 全局对象初始化:
    • 初始化一系列的原始属性:Math、String、Date、parseInt、window等
    • 浏览器中,window对象引用全局对象自身,全局环境中this也能引用自身

函数执行上下文中的变量对象

  • 在函数执行上下文中,变量对象VO 用活动对象AO来表示
  • AO是在进入函数执行上下文时(创建阶段 - 即预编译阶段)被创建的,通过argument对象进行初始化
  • 复习下执行上下文的生命周期:(1)创建阶段即预编译阶段 (2)执行阶段。

(1) 函数执行上下文的,进入函数执行上下文阶段(预编译阶段)代码还未真正执行,此时AO被创建,此时包含的属性有:

  • arguments对象
  • 所有形参
    • 形参名称和形参的值,组成了AO对象的属性。
    • 传递实参,则该形参的值被赋值为实参
    • 没有传递实参,值是undefined
  • 所有函数声明
  • 所有变量声明
  • this
函数执行上下文
- 创建阶段(进入执行上下文阶段,或者说预编译阶段),即进入函数执行上下文时,AO被创建,代码并未真正执行
- 此时AO中的属性有:arguments, 形参,函数声明,变量声明,this
- 代码示例:


function a(name, age) {
    var b = 1
    function c() {}
    var d = function()()
    (function e () {}); // 注意e函数的写法
    var f = function g(){}
}
a('woow') // 注意这里没有传实参age


------------
预编译阶段的AO如下:
AO(a function execution context) = {
    arguments: {
        ......
    },
    name: 'woow',
    age: undefined,
    b: undefined,
    c: reference to function c(){},
    d: undefined
    f: undefined
}
// 注意:
// e函数表达式不在AO中
// g函数不在AO中


------------
执行阶段的AO如下:
AO(a function execution context) = {
    arguments: {
        ......
    },
    name: 'woow',
    age: undefined,
    b: 1,
    c: reference to function c(){},
    d: reference to FunctionExpression "d",
    f: reference to FunctionExpression "f"
}

(2)活动对象AO和变量对象VO的区别:

  • 变量对象:是规范或引擎实现层面的,在js的环境中无法访问,在进入函数执行上下文阶段(预编译阶段),VO才被激活,成为AO
  • 活动对象:只有成为了活动对象,变量对象的属性和方法才能被访问

变量提升

优先级: 函数形参 > 函数声明 > 变量声明

函数名已经存在,则新的覆盖旧的

变量名已经存在,直接跳过变量声明

- 变量提升,实际上是在函数执行上下文中:进入函数执行上下文时,生成的活动对象AO的属性的赋值过程
变量提升
- 优先级:形参 > 函数声明 > 变量声明 
- 函数名已经存在,则新的覆盖旧的
- 变量名已经存在,则跳过变量声明(注意:只是跳过变量的声明,赋值是正常的赋值)

(例1function a(name, age) {
    console.log(name) // -------------------------- 'wang'
    console.log(age) // --------------------------- undefined
    var name = 'woow_wu7'
    console.log(name) // -------------------------- 'woow_wu7'
}
a('wang')

实际执行的代码如下:
function a(name, age) {
    var name = 'wang'
    var age = undefined // ------------------------ 优先级:形参 > 变量声明,所以形参声明并被赋值
    // var name = undefined // -------------------- 变量名已经存在了,跳过变量声明,即这行代码不执行
    console.log(name) // -------------------------- 'wang'
    console.log(age) // --------------------------- undefined, 未传入实参
    name = 'woow_wu7' // -------------------------- 执行阶段被重新赋值
    console.log(name) // -------------------------- 'woow_wu7'
}
a('wang')

(例2function a(name, age) {
    console.log(name) // -------------------------- function name(){....}
    console.log(age) // --------------------------- undefined
    var name = 'woow_wu7'
    function name() {
        console.log('name()')
    }
    console.log(name) // --------------------------- 'woow_wu7'
}
a('wang')

实际执行的代码如下:
function a(name, age) {
    var name = 'wang' // -------------------------- 优先级:形参 > 函数声明 > 变量声明
    function name() {...} // ---------------------- 优先级:函数声明 > 变量声明;函数名name已经存在,则新的覆盖旧的
    // var name = undefined // -------------------- 变量名已经存在,则跳过变量声明,相当于该行不执行
    console.log(name) // -------------------------- function name(){....}
    console.log(age) // --------------------------- undefined
    name = 'woow_wu7' // -------------------------- 执行时重新被赋值
    function name() {
        console.log('name()')
    }
    console.log(name) // --------------------------- 'woow_wu7'
}
a('wang')

2021/10/20 复习更新

  • 综合案例
// 执行上下文
function execute(name, age) {
  console.log("1", name);
  var name = "woow_wu7";
  var name = function () {};
  function name() {}
  console.log("2", name);
}
execute("wang", 10);


// 实际执行的是
function execute(name, age) {
  var name = "wang"; // 形参 - 声明并赋值实参
  var age = 10; // 形参 - 声明并赋值实参
  // var name = undefined // 变量 - 变量提升 - 变量名存在,直接略过变量的声明
  // var name = undefined; // 变量 - 变量提升 - 变量名存在,直接略过变量的声明
  // function name() {} // 函数 - 函数提升 - 是整个函数一起提升 - 函数名已经存在,新的覆盖旧的
  // 注意:
  // 1. var name = function () {} 变量提升
  // 2. function name() {} 函数提升
  console.log("1", name); // --- function name() {}
  name = "woow_wu7";
  name = function () {};
  console.log("2", name); // --- 'function () {}'
}

我的语雀:www.yuque.com/woowwu/msyq… juejin.im/post/684490…
juejin.im/post/684490…
juejin.im/post/684490…
juejin.im/post/684490…
www.jianshu.com/p/330b1505e…

复习

手写new

  • 构造函数
    • 构造函数的首字母通常大写
    • this指向的是实例对象
    • 调用时通过new命令
    • 构造函数也可以接收参数
  • new命令
    • 执行构造函数,返回实例对象
    • new命令本身就可以执行构造函数,所以函数后面的那对括号可以不要。比如:new fn 或者 new fn() 都可以
  • 构造函数调用忘记使用new命令?
    • 构造函数忘记使用new命令调用的话,构造函数就成了普通函数,函数中的this就要在调用时才能确定指向
    • 如何避免构造函数忘记使用new命令调用:
    • (1)在构造函数内使用严格模式'use strict',这样忘记使用new就是报错
    • (2)就是判断是否使用了new,如何判断?
    • !(this instanceof Fubar) => 如果this不是Fubar的实例,就用new命令调用return new Fubar()
  • new命令的原理
    • 创建一个空对象,作为将要返回的实例对象
    • 将这个空对象的原型指向构造函数的prototype属性
    • 将这个空对象赋值给函数内部的this关键字
    • 执行构造函数内部的代码
    • 如果构造函数的返回值是一个对象,就返回这个对象。否则返回这个空对象即this对象
  • 构造函数中的return
    • 构造函数中,如果return后面跟着一个对象,new命令就会返回这个对象
    • 构造函数中,如果return后面跟的不是一个对象,或者没有返回值,就是返回this对象
    • 普通函数如果用new命令调用,则会返回一个空对象

手写new
- new命令执行构造函数,返回实例对象
- 构造函数中有return,如果后面跟一个对象new命令会返回这个对象,如果后面不是对象,就会返回this对象
- new命令生成的实例对象可以继承构造函数的prototype属性 => 即实例可以访问构造函数的prototype上的属性和方法
- new命令生成的实例可以访问构造函数中的属性和函数


过程:
1. 新建一个空对象
2. 将空对象的隐式原型指向构造函数的显示原型 => 空对象就能访问构造函数的prototype上的属性和方法
3. 将构造函数中的this指向这个空对象 => this上绑定的属性和方法实际上就绑在了空对象上
4. 执行构造函数 => 如果有return,并返回一个对象,就返回这个对象,如果不是对象,就返回这个空对象
5. 如果构造函数的返回值是一个对象,就返回这个对象。否则返回这个空对象即this对象


代码:
function Constructor(name) {
  this.name = name
  this.age = 20
  return this
}
Constructor.prototype.address = 'chongqing'

function _new() {
  const obj = {}
  // 还可以通过 const obj = new Object()来生成

  const paramsConstructor = Array.prototype.shift.call(arguments)
  // 获取传入new函数的构造函数
  // 等于 [].prototype.shift.call(arguments)
  // 等于 [].prototype.shifig.apply(arguments)

  obj.__proto__ = paramsConstructor.prototype
  // 将空对象的隐式原型指向构造函数的显示原型
  // 这样paramsConstructor.prototype就成了obj的原型对象,obj可以访问原型对象上的属性和方法
  // 注意:Object.create() 该方法可以以参数对象为原型,返回一个实例对象
  // 所以:空对象的生成,加空对象的隐式原型的绑定可以一行代码搞定:
  //!!!!!!!可以:const obj = Object.create(paramsConstructor.prototype)
  //!!!! 还可以:const obj = Object.setPrototypeOf({}, paramsConstructor.prototype)

  const res = paramsConstructor.apply(obj, arguments)
  // 将构造函数中的this绑定在空对象上,并执行构造函数
  // 注意:这里arguments是shift后剩下的值,因为apply可以自己转化类数组的对象,所以无需额外操作

  return Object.prototype.toString.call(res).includes('Object') ? res : obj
  // 如果构造函数返回的是一个对象,就返回这个对象,否则返回这个空对象
  // 等于 typeof res === 'object' ? res : obj
}
const res = _new(Constructor, 'woow_wu7')
console.log(res, 'res11')