导航
[深入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] 手写函数
[部署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 同理
(六) var 和 let 的区别 ( 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的属性的赋值过程
变量提升
- 优先级:形参 > 函数声明 > 变量声明
- 函数名已经存在,则新的覆盖旧的
- 变量名已经存在,则跳过变量声明(注意:只是跳过变量的声明,赋值是正常的赋值)
(例1)
function 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')
(例2)
function 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')