执行上下文
当 JavaScript 代码执行一段可执行代码(全局代码、函数代码、eval代码)时,会创建对应的执行上下文
- 对于每个执行上下文,都有三个重要属性:变量对象(Variable object,VO)、 作用域链、 this
- 执行上下文栈(先进后出,全局执行上下文最先进入,最后弹出)
js引擎创建了执行上下文栈,来管理执行上下文,初始化的时候首先就会向栈内压入一个全局执行上下文,当执行一个函数时,会创建它的执行上下文压入栈内,在函数执行完后它的执行上下文才会从栈中弹出,整个应用程序结束的时候,全局执行上下文才会被弹出,栈被清空
变量对象:
存储了在执行上下文中定义的变量和函数声明。
- 预编译(函数调用时且在具体的函数代码运行之前)
- 函数预编译 - (函数被调用时,变量对象VO被激活为活动对象AO)
1. 创建VO对象
2. 找形参和变量声明,将变量和形参作为VO的属性名值为undefined
3. 将实参值和形参统一
4. 在函数内部找函数声明, 值赋予函数体
- 全局预编译 - 关于GO
1. 创建GO == window对象
2. 找变量声明
3. 找函数声明赋值函数体
作用域链(Scope chain)
当查找变量的时候,会先从当前上下文的变量对象中查找,如果没有找到,就会从父级(词法层面上的父级)执行上下文的变量对象中查找,一直找到全局上下文的变量对象,也就是全局对象。这样由多个执行上下文的变量对象构成的链表就叫做作用域链。
-
作用域:规定了当前执行代码对变量的访问权限,可以隔离变量,不同作用域下同名变量不会冲突。
-
JavaScript 采用的是词法(静态)作用域,函数的作用域在函数定义的时候就决定了,而与词法作用域相对的是动态作用域,函数的作用域是在函数调用的时候才决定的。
var value = 1; function foo() { console.log(value); } function bar() { var value = 2; foo(); } bar(); // 1
总结一下函数执行上下文中作用域链和变量对象的创建过程:
var scope = "global scope";
function checkscope() {
var scope2 = 'local scope';
return scope2;
}
checkscope();
执行过程如下:
1.当函数创建时,会有一个名为 [[scope]] 的内部属性保存所有父变量对象到其中。
当函数执行时,会创建一个执行环境,然后通过复制函数的 [[scope]] 属性中的对象
构建起执行环境的作用域链,然后,变量对象 VO 被激活生成 AO 并添加到作用域链的前端,
完整作用域链创建完成:
checkscope.[[scope]] = [
globalContext.VO
];
2. 开始做准备工作,第一步:复制函数[[scope]]属性创建作用域链
checkscopeContext = {
Scope: checkscope.[[scope]],
}
3. 开始预编译 创建AO
checkscopeContext = {
AO: {
arguments: { length: 0 },
scope2: undefined
},
Scope: checkscope.[[scope]],
}
4. 预编译完成后将AO压入 checkscope 作用域链顶端
checkscopeContext = {
AO: {
arguments: { length: 0 },
scope2: undefined
},
Scope: [AO, [[Scope]]]
}
5. 开始执行函数,创建 checkscope 函数执行上下文,checkscope 函数执行上下文被压入执行上下文栈,
随着函数的执行,修改 AO 的属性值
ECStack = [
checkscopeContext,
globalContext
];
checkscopeContext = {
AO: {
arguments: { length: 0 },
scope2: 'local scope'
},
Scope: [AO, [[Scope]]]
}
7. 函数执行完毕,函数上下文从执行上下文栈中弹出
ECStack = [
globalContext
];
this 可以简单的理解为最后调用函数的对象, ECMAScript规范解读this
- 手写
// new
function myNew(fn, args) {
const obj = Object.create(fn.prototype)
const result = fn.call(obj, args)
// 如果函数没有返回对象类型`Object`(包含`Functoin`, `Array`, `Date`, `RegExg`, `Error`),那么`new`表达式中的函数调用会自动返回这个新的对象。
return ['object', 'function'].includes(typeof result) ? result : obj
}
// call
Function.prototype.myApply = function (target, args) {
target = target ? Object(target) : window
target.fn = this
return target.fn(args)
}
// bind
Function.prototype.myBind = function (target, args) {
// 保存原函数的原型,避免共用一个原型的话互相影响(箭头函数没原型)
const func = function () {}
func.ptototype = this.prototype
const self = this
function bindFunc (args1) {
// this instanceof bindFunc , 判断是否使用 new 来调用 bindFunc,如果是this的指向就是其实例
return self.myApply(this instanceof bindFunc ? this : target, [...args, ...args1])
}
bindFunc.prototype = myNew(func)
return bindFunc
}
原型
闭包
理论角度:闭包是指那些能够访问自由变量的函数
自由变量是指在函数中使用的,但既不是函数参数也不是函数的局部变量的变量。
实践角度:以下函数才算是闭包:
- 即使创建它的上下文已经销毁,它仍然存在(比如,内部函数从父函数中返回)
- 在代码中引用了自由变量
优点 自由变量不会被垃圾回收机制回收
缺点 内存泄漏
解决内存泄漏
function a (){
let x = 1
return function b () { x++ }
}
var c = a()
c = null
作用 4. 实现公有变量: 函数累加器 5. 可以做缓存(存储结构) 6. 可以实现封装,属性私有化 7. 模块化开发,防止污染全局变量
EventLoop
js是单线程的脚本语言
js单线程任务被分为同步任务和异步任务,同步任务会在执行栈中按照顺序等待主线程依次执行,异步任务会在有了结果后, 将注册的回调函数放到任务队列中等待主线程空闲的时候(执行栈被清空),被读取到栈内等待执行。
- 上面说的任务一般都指宏任务, 在当前宏任务执行过程中也会有一些微任务,这些微任务的回调会被注册到微任务队列里,js会在把将当前微任务队列里的微任务执行完(更新渲染页面),再执行下一个宏任务,我将其理解为eventloop。
Promise
手写
const PENDING = 'PENDING'
const FULFILLED = 'FULFILLED'
const REJECTED = 'REJECTED'
const resolvePromise = (promise, x, resolve, reject) => {
if (x === promise) return reject(new Error('循环引用,x不能与promise为同一对象'))
let only = false
if (x && ['object, function'].includes(typeof x) && x.then instanceof Function) {
try {
const then = x.then
then.call(x, y => {
if (only) return
only = true
resolvePromise(promise, y, resolve, reject)
}, r => {
if (only) return
only = true
reject(e)
})
} catch (e) {
if (only) return
only = true
reject(e)
}
} else {
resolve(x)
}
}
class Promise {
constructor(executor) {
this.status = PENDING
this.value = undefined
this.reason = undefined
this.resolveCallback = []
this.rejectCallback = []
const resolve = val => {
if (this.status === PENDING) {
this.status = FULFILLED
this.value = val
this.resolveCallback.forEach(fn => fn())
}
}
const reject = val => {
if (this.status === PENDING) {
this.status = REJECTED
this.reason = val
this.rejectCallback.forEach(fn => fn())
}
}
try {
executor(resolve, reject)
} catch (e) {
reject(e)
}
}
then(onFulfilled, onRejected) {
onFulfilled = onFulfilled instanceof Function ? onFulfilled : v => v
onRejected = onRejected instanceof Function ? onRejected : err => { throw err }
const promise = new Promise((resolve, reject) => {
const next = (isResolve = true) => setTimeout(() => {
try {
resolvePromise(promise,
isResolve ? onFulfilled(this.value) : onRejected(this.reason),
resolve,
reject)
} catch(e) {
reject(e)
}
},0)
if (this.status === FULFILLED) next()
if (this.status === REJECTED) next(false)
if (this.status === PENDING) {
this.resolveCallback.push(next)
this.rejectCallback.push(() => next(false))
}
})
return promise
}
}