上下文 是Javascript 中的一个比较重要的概念, 可能很多朋友对这个概念并不是很熟悉, 那换成「作用域」 和 「闭包」呢?是不是就很亲切了。
「作用域」和「闭包」 都是和「执行上下文」密切相关的两个概念。
在解释「执行上下文」是什么之前, 我们还是先回顾下「作用域」 和 「闭包」。
首先, 什么是作用域呢?
域, 即是范围。
作用域,其实就是某个变量或者函数的可访问范围。
它控制着变量和函数的可见性和生命周期。
作用域也分为: 「全局作用域 」和 「局部作用域」。
如果一个对象在任何位置都能被访问到, 那么这个对象, 就是一个全局对象, 拥有一个全局作用域。
拥有全局作用域的对象可以分为以下几种情况:
JavaScript的作用域是通过函数来定义的。
在一个函数中定义的变量, 只对此函数内部可见。
这类作用域,称为局部作用域。
还有一个概念和作用域联系密切, 那就是作用域链。
作用域链是一个集合, 包含了一系列的对象, 它可以用来检索上下文中出现的各类标识符(变量, 参数, 函数声明等)。
函数在定义的时候, 会把父级的变量对象AO/VO的集合保存在内部属性 [[scope]] 中,该集合称为作用域链。
Javascript 采用了词法作用域(静态作用域),函数运行在他们被定义的作用域中,而不是他们被执行的作用域。
看个简单的例子 :
var a = 3;
function foo () {
console.log(a)
}
function bar () {
var a = 6
foo()
}
bar()
如果js采用动态作用域,打印出来的应该是6而不是3.
这个例子说明了javasript是静态作用域。
此函数作用域链的伪代码:
function bar() {
function foo() {
// ...
}
}
bar.[[scope]] = [
globalContext.VO
];
foo.[[scope]] = [
barContext.AO,
globalContext.VO
];
函数在运行激活的时候,会先复制 [[scope]] 属性创建作用域链,然后创建变量对象VO,然后将其加入到作用域链。
executionContextObj: {
VO: {},
scopeChain: [VO, [[scope]]]
}
总的来说, VO要比AO的范围大很多, VO是负责把各个调用的函数串联起来的。
VO是外部的, 而AO是函数自身内部的。
与AO, VO 密切相关的概念还有GO, EC , 感兴趣的朋友可以参考:https://blog.nixiaolei.com/
下面我们说一下闭包。
闭包也是面试中经常会问到的问题, 考察的形式也很灵活, 譬如:
那闭包究竟是什么呢?
说白了, 闭包其实也就是函数, 一个可以访问自由变量的函数。
自由变量: 不在函数内部声明的变量。
很多所谓的代码规范里都说, 不要滥用闭包, 会导致性能问题, 我当然是不太认同这种说法的, 不过这个说法被人提出来,也是有一些原因的。
毕竟,闭包里的自由变量会绑定在代码块上,在离开创造它的环境下依旧生效,而使用代码块的人可能无法察觉。
闭包里的自由变量的形式有很多,先举个简单例子。
function add(p1){
return function(p2){
return p1 + p2;
}
}
var a = add(1);
var b = add(2);
a(1) //2
b(1) // 3
在上面的例子里,a 和 b这两个函数,代码块是相同的,但若是执行a(1)和b(1)的结果却是不同的,原因在于这两者所绑定的自由变量是不同的,这里的自由变量其实就是函数体里的 p1 。
自由变量的引入,可以起到和OOP里的封装同样作用,我们可以在一层函数里封装一些不被外界知晓的自由变量,从而达到相同的效果, 很多模块的封装, 也是利用了这个特性。
然后说一下我遇到的真实案例, 是去年面试腾讯QQ音乐的一道笔试题:
for (var i = 1; i <= 5; i++) {
setTimeout(function timer() {
console.log(i)
}, i * 1000)
}
这段代码会输出一堆 6, 让你改一下, 输出 1, 2, 3, 4, 5
解决办法还是很多的, 就简单说两个常见的。
for (var i = 1; i <= 5; i++) {
;(function(j) {
setTimeout(function timer() {
console.log(j)
}, j * 1000)
})(i)
}
使用立即执行函数将 i 传入函数内部。
这个时候值就被固定在了参数 j 上面不会改变,当下次执行 timer 这个闭包的时候,就可以使用外部函数的变量 j ,从而达到目的。
for (let i = 1; i <= 5; i++) {
setTimeout(function timer() {
console.log(i)
}, i * 1000)
}
const , let 的原理和相关细节可以参考我的另一篇:[第13期] 掌握前端面试基础系列一:ES6
解释完这两个概念,就回到我们的主题, 上下文。
首先, 执行上下文是什么呢?
简单来说, 执行上下文就是Javascript 的执行环境。
当javascript执行一段可执行代码的时候时,会创建对应的执行上下文。
组成如下:
executionContextObj = {
this,
VO,
scopeChain: 作用域链,跟闭包相关
}
由于Javavscript是单线程的,一次只能处理一件事情,其他任务会放在指定上下文栈中排队。
Javascript 解释器在初始化执行代码时,会创建一个全局执行上下文到栈中,接着随着每次函数的调用都会创建并压入一个新的执行上下文栈。
函数执行后,该执行上下文被弹出。
执行上下文建立的步骤:
this 是Javascript中一个很重要的概念, 也是很多初级开发者容易搞混到的一个概念。
今天我们就好好说道说道。
首先, this 是运行时才能确认的, 而非定义时确认的。
在函数执行时,this 总是指向调用该函数的对象。
要判断 this 的指向,其实就是判断 this 所在的函数属于谁。
this 的执行,会有不同的指向情况, 大概可以分为:
我们一个个来看。
function foo() {
console.log( this.a );
}
var obj = {
a: 2,
foo: foo
};
obj.foo(); // 2
这种情况最容易考到, 也最容易迷惑人。
先看个简单的例子:
var a = 2;
function foo() {
console.log( this.a );
}
foo(); // 2
没什么疑问。
看个稍微复杂点的:
function foo() {
console.log( this.a );
}
function doFoo(fn) {
this.a = 4
fn();
}
var obj = {
a: 2,
foo: foo
};
var a = 3
doFoo( obj.foo ); // 4
对比:
function foo() {
this.a = 1
console.log( this.a );
}
function doFoo(fn) {
this.a = 4
fn();
}
var obj = {
a: 2,
foo: foo
};
var a = 3
doFoo(obj.foo); // 1
发现不同了吗?
你可能会问, 为什么下面的 a 不是 doFoo 的a呢?
难道是foo里面的a被优先读取了吗?
打印foo和doFoo的this,就可以知道,他们的this都是指向window的。
他们的操作会修改window中的a的值。并不是优先读取foo中设置的a。
简单验证一下:
function foo() {
setTimeout(() => this.a = 1, 0)
console.log( this.a );
}
function doFoo(fn) {
this.a = 4
fn();
}
var obj = {
a: 2,
foo: foo
};
var a = 3
doFoo(obj.foo); // 4
setTimeout(obj.foo, 0) // 1
结果证实了我们上面的结论,并不存在什么优先。
var a = 4
function A() {
this.a = 3
this.callA = function() {
console.log(this.a)
}
}
A() // 返回undefined, A().callA 会报错。callA被保存在window上
a = new A()
a.callA() // 3, callA在 new A 返回的对象里
这个大家应该都很熟悉了。
令this指向传递的第一个参数,如果第一个参数为null,undefined或是不传,则指向全局变量。
var a = 3
function foo() {
console.log( this.a );
}
var obj = {
a: 2
};
foo.call(obj); // 2
foo.call(null); // 3
foo.call(undefined); // 3
foo.call(); // 3
var obj2 = {
a: 5,
foo
}
obj2.foo.call() // 3,不是5
//bind返回一个新的函数
function foo(something) {
console.log(this.a, something);
return this.a + something;
}
var obj =
a: 2
};
var bar = foo.bind(obj);
var b = bar(3); // 2 3
console.log(b); // 5
箭头函数比较特殊,它没有自己的this。它使用封闭执行上下文(函数或是global)的 this 值:
var x=11;
var obj={
x:22,
say: () => {
console.log(this.x);
}
}
obj.say(); // 11
obj.say.call({x:13}) // 11
x = 14
obj.say() // 14
//对比一下
var obj2={
x:22,
say() {
console.log(this.x);
}
}
obj2.say();// 22
obj2.say.call({x:13}) // 13
以上我们系统的介绍了上下文, 以及与之相关的作用域, 闭包, this等相关概念。
介绍了他们的作用,使用场景以及区别和联系。
执行上下文,Execution Context,下面简称EC。当函数执行时,会创建一个称为执行上下文的内部对象(可理解为作用域)。一个执行上下文定义了一个函数执行时的环境。
在有些 CSS 相互影响作用下,对元素设置的 z-index 并不会按实际大小叠加,一直不明白其中的原理,最近特意查了一下相关资料,做一个小总结。层叠上下文(stacking content)是 HTML 中的三维概念,也就是元素z轴。
代码运行是在一定的环境之中运行的,这个运行环境我们就成为执行环境,也就是执行上下文,按照执行环境不同,我们可以分为三类:全局执行环境:代码首次执行时候的默认环境
如果你是或者想成为一名Javascript开发者,那就必须要知道Javascript内部是如何执行的。正确的理解Javascript中的执行上下文和执行栈对于理解其它Javascript概念(比如变量提升,作用域,闭包等)至关重要。
层叠上下文(stacking context),是HTML中一个三维的概念。在CSS2.1规范中,每个盒模型的位置是三维的,分别是平面画布上的X轴,Y轴以及表示层叠的Z轴。一般情况下,元素在页面上沿X轴Y轴
网上关于执行上下文的文章有很多,关于什么是执行上下文,很多文章说得很清晰。我说一下自己的理解。执行上下文包含三个东西:
内容以共享、参考、研究为目的,不存在任何商业目的。其版权属原作者所有,如有侵权或违规,请与小编联系!情况属实本人将予以删除!