本文原创:dongdezhangjavascript
你们都据说过execution context(执行上下文或执行环境)
,在操做系统中也有相似的上下文概念,它指得是存储在各寄存器中的中间数据,还有context switchs(上下文切换)等概念,那js中的context跟os中的context相比,是否是也有殊途同归之妙呐?前端
看个题目,试想下浏览器是如何执行这段代码?java
let a = 1
var b = 2
function jad() {
var a = 2
jdzw()
}
function jdzw() {
console.log(a, b)
}
jad() //1 2
复制代码
首先要明确的是执行上下文跟做用域链是两个不一样的概念node
定义:执行上下文是一个"环境"的抽象概念,在这个“环境”中Javascript代码被分析和执行。任何代码在JavaScript中运行时,都是在执行上下文中运行的。chrome
简单理解就是遵循 LIFO
的栈结构,结合图解和动图感觉一下上述函数的执行过程,目前咱们只须要关注图中call stack 和 scope下的内容便可,图片是在chrome调试下截取,关于chrome调试工具使用能够参考浏览器
具体的执行过程:缓存
上图中咱们发现它 scope 中存在 local 和 global 这两个变量,local 中保存有当前函数能够访问到的变量名,而 global 其实就是 window,保存全局的一些变量,这其实就是一个简单的做用域链。闭包
上下文的生命周期分两个阶段:建立阶段和执行阶段app
在这期间会建立 LexicalEnvironment 和 VariableEnvironment 两部分,用伪代码表示以下:ide
ExecutionContext = {
LexicalEnvironment: {},
VariableEnvironment: {},
}
复制代码
简单理解为是**标识符-变量的映射(identifier-variable mapping)**标识符指的是函数或变量的名称,变量指得是真正的引用对象或基本类型数据。它包含三个结构:
对于 Environment Record,又分为 Declarative environment record(声明式环境记录)和Object environment record(对象式环境记录)两种,
声明式环境记录主要用在函数上下文中,包含变量、函数和 arguments 对象;相反对象式环境记录则用于全局上下文中,主要用于记录全局的对象,函数和变量。
outer environment 指向外部的 LexicalEnvironment,这样能够获取到外部的数据。全局上下文的outer老是指向null。
this绑定依赖于函数的执行方式,全局环境中this指向global object。关于this的理解推荐查看以前的文章
其实它也是一种 LexicalEnvironment,只不过是用来保存用 var 声明的变量,与let、const声明的变量区分开。
举个例子🌰
let a = 1
var b = 2
var h
function jad() {
var c = 2
let d = 3
return c + d
}
h = jad(1,2)
复制代码
上述代码的全局上线文建立过程以下:
GlobalExectionContext = {
LexicalEnvironment: {
EnvironmentRecord: {
Type: "Object", // 对象式环境记录用于全局上下文中
a: <uninitialized>, // let声明在建立时期uninitialized
jad: <function>
}
outer: <null>,
this: <global object>
},
VariableEnvironment: {
EnvironmentRecord: {
Type: "Object",
b: undefined, // var 声明undefined
h: undefined
}
outer: <null>,
this: <global object>
}
}
复制代码
jad 函数执行时,函数上下文的建立过程以下
JadFunctionExectionContext = {
LexicalEnvironment: {
EnvironmentRecord: {
Type: "Declarative", // 函数上下文的环境记录为Declarative类型
d: <uninitialized>,
Arguments: {0: 1, 1: 2, length: 2// 函数上下文包含Arguments对象
}
outer: <GlobalExectionContext>, // 指向上层上下文
this: <global>
},
VariableEnvironment: {
EnvironmentRecord: {
Type: "Declarative",
c: undefined,
}
outer: <GlobalExectionContext>,
this: <global>
}
}
复制代码
能够观察到 let 声明的变量在建立过程当中是未初始化的,而 var 声明的变量是 undefined,这也就是为何在 var 以前访问变量会输出 undefined,而 let 会报错。这其实就是 var 变量提高
和 let 的TDZ(临时性死区)
,而且函数的变量提高优先级高,所以会出现变量覆盖,另外函数表达式并不会提高
但若是在 let 声明以后访问,虽然没赋值,引擎也会默认undefined
var a = 1
function a() {
}
var b = 2
var b = function(){}
if(true) {
console.log(c) // ReferenceError: c is not defined
let c
}
console.log(a, b) // 1 function
复制代码
当进入执行阶段,上述代码的全局上下文会更新以下:
GlobalExectionContext = {
LexicalEnvironment: {
EnvironmentRecord: {
Type: "Object",
a: 1, // 执行完 a = 1
jad: <function>
}
outer: <null>,
this: <global object>
},
VariableEnvironment: {
EnvironmentRecord: {
Type: "Object",
b: 2, // 执行完 b =2
h: undefined
}
outer: <null>,
this: <global object>
}
}
复制代码
jad执行时,函数上下文的更新以下
JadFunctionExectionContext = {
LexicalEnvironment: {
EnvironmentRecord: {
Type: "Declarative", // 函数上下文的环境记录Declarative
d: 3, // 执行let d = 3
Arguments: {0: 1, 1: 2, length: 2
}
outer: <GlobalExectionContext>,
this: <global>
},
VariableEnvironment: {
EnvironmentRecord: {
Type: "Declarative",
c: 2, // 执行 var c = 2
}
outer: <GlobalExectionContext>,
this: <global>
}
}
复制代码
当 jad
函数执行完后,将返回值更新到变量 h
。
在ES6以前,上下文能够简单理解为下面这种结构:
// 上下文在建立阶段
Context = {
vo: {
a: undefined // 各类变量 undefined
},
outer:<other context or null>
this:<global or undefined>
}
// 上下文在执行阶段
Context = {
ao: {
a: 1 // 变量赋值
},
outer:<other context or null>
this:<global or undefined>
}
复制代码
做用域简单分为全局做用域,函数做用域和块级做用域
let a = 1
var b = 2 // 全局做用域
function jad() {
var a = 2 // 函数做用域
jdzw()
if(true) { // 块级做用域
let f = 3
let a = 4
console.log(f, a)// 3 4
}
}
function jdzw() {
console.log(a, b)// 1 2
}
jad()
复制代码
在上图中的scope看到的local global block等属性,这其实就是当前函数的做用域链。local其实能够理解为函数做用域。本质上是在执行上下文建立的过程当中,outer指向的外部环境,这样就构成了相似链表式的结构,做用域链的终点始终是global object,在浏览器中也就是window对象。
做用域链保证了函数对执行环境有权访问的全部变量和函数的有序访问
js中的某些语句能够延长做用域链
function url() {
var s = '?debug=true'
with(location) {
console.log(s + href)// 此处能够访问到href
}
}
try {
throw new Error('error')
} catch(err) { // catch并非一个函数,执行到此处时 err被放到当前做用域链的最前端
console.log(err) // error
}
复制代码
闭包指的是有权访问另外一个函数做用域中的变量的函数。
闭包为何能达到这样的效果?
究其缘由是由于闭包函数定义在父级函数的内部,所以闭包函数的执行上下文在建立的过程当中,天然将outer指向父级函数的执行上下文。
以下 c
函数就是一个闭包,固然建立的过程也可以使用匿名函数
闭包这一特色能够用来缓存数据
。好比函数柯理化
等,也让函数执行过程变得复杂多变,这也是js做为弱语言吸引人的特色之一。❤️
let a = 1
var b = 2
function jad() {
var a = 2
function jdzw() {
console.log(a, b) //2 2 可以访问到jad中的a
}
return jdzw
}
let c = jad()
c() //2 2 可以访问到jad中的a
复制代码
但愿读完以后,可以对js的执行上下文,做用域,闭包及变量提高有所收获。(我的感受跟os中的context的概念大同小异)
let a = 'hello'
(function (callback) {
let b = 'world'
callback()
})(function () {
console.log(a)
console.log(b)
})
// hello world or hello error?
复制代码