词法作用域
js采用的是词法作用域,函数在定义的时候,函数的作用域就已经确定了
执行上下文栈
因为JavaScript代码的执行顺序:顺序执行但是他的是,一段一段执行。
例子
function fun3() {
console.log('fun3')
}
function fun2() {
fun3();
}
function fun1() {
fun2();
}
fun1();
我们假定,执行上下文栈是一个栈,假定为ECStrack = []
-
首先我们最先遇到的一定是全局代码,那么我们就会将全局执行上下文
globalStrack压入到ECStrack里面 -
这是我们首先遇到的是
fun1,压入ECStrack里面ECStrack = [globalStrack,fun1Strack] -
发现
fun1里面调用了fun2,那么我们也要将fun2也压入ECStrack = [globalStrack,fun1Strack,fun2Strack] -
发现
fun2里面调用了fun3,那么我们也要将fun3也压入ECStrack = [globalStrack,fun1Strack,fun2Strack,fun3Strack] -
fun3执行完毕了,那么我们就pop掉ECStrack = [globalStrack,fun1Strack,fun2Strack] -
因为
fun2只调用fun3,那么其实fun2也计算好了,pop掉ECStrack = [globalStrack,fun1Strack] -
fun1也是如此ECStrack = [globalStrack] -
最后只剩下了全局执行上下文
函数完成以后,他就会从执行栈里面pop掉
执行上下文
执行上下文栈会创建执行上下文,执行上下文里面又含有三个重要的属性
- 变量对象(
VO) - 作用域链
- this
全局上下文
就是定义在全局的那些东西,全局上下文里的变量对象其实就是全局对象
函数上下文
函数上下文,在函数上下文里面我们会涉及到,活动对象AO
活动对象在进入函数上下文的时候被创建,当函数代码执行时他会进行修改
执行过程
- 进入执行上下文
- 代码执行
进入执行上下文
一般他的AO有几个方便
arguments(形参)- 没有实参,属性值为
undefined - 有的话,就是那个传入的值
- 没有实参,属性值为
- 函数声明
- 由名称和对应值(函数对象
function-object)组成 - 如果有其他变量和函数变量同名,那么函数直接覆盖
- 由名称和对应值(函数对象
- 变量声明
- 值全部是
undefind
- 值全部是
举例:
function foo(a) {
var b = 2;
function c() {}
var d = function() {};
b = 3;
}
foo(1);
我们可以分析AO
AO = {
arguments: {
0: 1,
length: 1
},
a: 1,
b: undefined,
c: reference to function c(){},
d: undefined
}
因为我是存在参数的,所以,参数传过来的时候,我就已经知道了,那个值是多少
代码执行
代码执行的时候,我们会将初始化的AO进行改变
AO = {
arguments: {
0: 1,
length: 1
},
a: 1,
b: 3,
c: reference to function c(){},
d: reference to FunctionExpression 'd'
}
例子
console.log(foo);
function foo(){
console.log("foo");
}
var foo = 1;
其实我们打印的是这个函数
因为当函数和其他变量声明同名时,函数,先进行函数声明,然后再是变量声明
作用域链
当javascript在使用一个变量的时候,,他会现在当前的作用域中找,如果没找到,那么他回去上层的作用域找,一直到全局作用域为止,这样就构成了作用域链
函数内部有一个属性,[[scope]],当函数创建时,他就会保存在对应的父变量的对象中
举例
function foo() {
function bar() {
...
}
}
那么对应的[[scoped]]就是
foo.[[scope]] = [
globalContext.VO
];
bar.[[scope]] = [
fooContext.AO,
globalContext.VO
];
原因:
- 因为
foo是定义在全局的,所以他的scoped里面只有global.vo - 但是
bar是在foo,里面的,所以他还有一个foo,因为foo是一个函数,所以我们使用AO来表示变量对象fooContext.AO
当函数进入执行上下文时,就会将活动对象AO添加到作用链前端
例子
var scope = "global scope";
function checkscope(){
var scope2 = 'local scope';
return scope2;
}
checkscope();
-
首先是
checkscope函数被创建,保存作用域链到内部属性[[scoped]]checkscope.[[scope]] = [ global.vo ] -
执行
checkscope函数,创建checkscope函数执行上下文ECStack = [ checkscopeContext, globalContext ]; -
checkscope进入执行上下文- 复制
scope到作用域链Scope
checkscopeContext = { Scope: checkscope.[[scope]], }- 创建
AO
checkscopeContext = { AO: { arguments: { length: 0 }, scope2: undefined }, Scope: checkscope.[[scope]], }- 将活动对象压入到
checkscope作用域链Scope顶端
checkscopeContext = { AO: { arguments: { length: 0 }, scope2: undefined }, Scope: [AO, [[Scope]]] } - 复制
-
执行函数,将
AO进行更新checkscopeContext = { AO: { arguments: { length: 0 }, scope2: 'local scope' }, Scope: [AO, [[Scope]]] } -
函数执行完毕,那么
pop掉
例题分析
var scope = "global scope";
function checkscope(){
var scope = "local scope";
function f(){
return scope;
}
return f();
}
checkscope();
-
执行全局上下文,创建全局上下文,并将全局执行上下文压入栈中
ECStack = [ globalContext ]; -
全局上下文初始化
globalContext = { VO: [global], Scope: [globalContext.VO], this: globalContext.VO } -
checkscope函数被创建,保存作用域链到函数的内部属性[[scope]]checkscope.[[scope]] = [ globalContext.VO ]; -
执行
checkScope那么压入执行上下文栈ECStack = [ checkscopeContext, globalContext ]; -
将
checkScope进行初始化,包括复制[[scope]]到作用域链,AO等checkscopeContext = { AO: { arguments: { length: 0 }, scope: undefined, f: reference to function f(){} }, Scope: [AO, globalContext.VO],//将活动对象压入到作用域链顶部 this: undefined } -
将
f进行初始化fContext = { AO: { arguments: { length: 0 } }, Scope: [AO, checkscopeContext.AO, globalContext.VO], this: undefined } -
函数
f执行,顺带着寻找scope,f结束,pop出去 -
然后
checkScope也执行完成,pop -
最后只剩下了
globalContext
所以到最后,所说的作用域链,其实就是那个Scope,就是那个数组
闭包
定义:
- 即使他的上下文已经被销毁了,但是他依然存在了
- 在代码中存在自由变量
回答:
引入自做了一份前端面试复习计划,保熟~ - 掘金 (juejin.cn)
在某个内部函数的执行上下文创建时,会将父级函数的活动对象加到内部函数的
[[scope]]中,形成作用域链,所以即使父级函数的执行上下文销毁(即执行上下文栈弹出父级函数的执行上下文),但是因为其活动对象还是实际存储在内存中可被内部函数访问到的,从而实现了闭包。
常见题:
var data = [];
for (var i = 0; i < 3; i++) {
data[i] = function () {
console.log(i);
};
}
data[0]();
data[1]();
data[2]();
输出答案全部是3
分析:
这里为了方便,我只写出作用域链
global:
vo:{
data:[],
i:3
}
data[0]:
data[0].Scoped = [AO,global.VO]
data[0].AO = {
function () {}
}
同理,data[1]和data[2]
data[1].Scoped = [A0,data[0].AO,global.VO]
data[2].Scoped = [AO,data[1].A0,data[0].AO,global.VO]
data[1].AO = {
function () {}
}
data[2].AO = {
function () {}
}
所以一直到i = 3,就会从全局找,一直找到3
闭包在处理速度和内存消耗方面性能具有负面影响
例如:
function MyObject(name, message) {
this.name = name.toString();
this.message = message.toString();
this.getName = function() {
return this.name;
};
this.getMessage = function() {
return this.message;
};
}
其实可以避免这种写法,可以写为
function MyObject(name, message) {
this.name = name.toString();
this.message = message.toString();
}
MyObject.prototype.getName = function() {
return this.name;
};
MyObject.prototype.getMessage = function() {
return this.message;
};
参考链接:
做了一份前端面试复习计划,保熟~ - 掘金 (juejin.cn)
JavaScript深入之参数按值传递 · Issue #10 · mqyqingfeng/Blog (github.com)