一文搞清-Javascript-中的上下文

8次阅读

共计 4547 个字符,预计需要花费 12 分钟才能阅读完成。

背景

本文是「2019 年,看了这一份,再也不怕前端面试了」中的一部分:

参考了之前写过的 博客 和额外的 资料 ,分享给大家,希望能给大家带来一些 启发和帮助

如需转载,请联系作者获得许可。

正文

上下文 是 Javascript 中的一个比较重要的概念,可能很多朋友对这个概念并不是很熟悉,那换成「 作用域 」和「 闭包」呢?是不是就很亲切了。

「作用域」「闭包」 都是和「执行上下文」 密切相关的两个概念。

在解释「执行上下文」是什么之前,我们还是先回顾下「作用域」和「闭包」。

作用域

首先,什么是作用域呢?

域,即是 范围

作用域,其实就是某个变量或者函数的 可访问范围

它控制着变量和函数的 可见性 生命周期

作用域也分为:「全局作用域 」和「 局部作用域」。

全局作用域:

如果一个对象在任何位置都能被访问到,那么这个对象,就是一个全局对象,拥有一个全局作用域。

拥有全局作用域的对象可以分为以下几种情况:

  • 定义在最外层的变量
  • 全局对象的属性
  • 任何地方隐式定义的变量(即:未定义就直接赋值的变量)。隐式定义的变量都会定义在全局作用域中。

局部作用域:

JavaScript 的作用域是通过 函数 来定义的。

在一个函数中定义的变量,只对此函数 内部可见

这类作用域,称为局部作用域。

还有一个概念和作用域联系密切,那就是 作用域链

作用域链

作用域链是一个 集合 ,包含了一系列的对象,它可以用来 检索 上下文中出现的各类 标识符(变量,参数,函数声明等)。

函数在定义的时候, 会把父级的变量对象 AO/VO 的集合保存在内部属性 [[scope]] 中,该集合称为作用域链。

  • AO : Activation Object 活动对象
  • VO : Variable object 变量对象

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/20…

下面我们说一下闭包。

闭包

闭包也是面试中经常会问到的问题,考察的形式也很灵活,譬如:

  • 描述下什么是闭包
  • 写一段闭包的代码
  • 闭包有什么用
  • 给你一个闭包的例子,让你修改,或者看输出

那闭包究竟是什么呢?

说白了,闭包其实也就是 函数 ,一个可以访问 自由变量 的函数。

自由变量:不在函数内部声明的变量。

很多所谓的代码规范里都说,不要滥用闭包,会导致性能问题,我当然是不太认同这种说法的,不过这个说法被人提出来,也是有一些原因的。

毕竟,闭包里的自由变量会 绑定 在代码块上,在离开创造它的环境下依旧生效,而使用代码块的人可能无法察觉。

闭包里的自由变量的形式有很多,先举个简单例子。

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

解决办法还是很多的,就简单说两个常见的。

  1. 用闭包解决
for (var i = 1; i <= 5; i++) {;(function(j) {setTimeout(function timer() {console.log(j)
    }, j * 1000)
  })(i)
}

使用 立即执行函数 将 i 传入函数内部。

这个时候值就被固定在了参数 j 上面不会改变,当下次执行 timer 这个闭包的时候,就可以使用外部函数的变量 j,从而达到目的。

  1. [推荐] 使用 let
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 解释器在初始化执行代码时,会创建一个全局执行上下文到栈中,接着随着每次函数的调用都会创建并压入一个新的执行 上下文栈

函数执行后,该执行上下文被弹出。

执行上下文建立的步骤:

  1. 创建阶段
  2. 初始化作用域链
  3. 创建变量对象
  4. 创建 arguments
  5. 扫描函数声明
  6. 扫描变量声明
  7. 求 this
  8. 执行阶段
  9. 初始化变量和函数的引用
  10. 执行代码

this

this 是 Javascript 中一个很重要的概念, 也是很多初级开发者容易搞混到的一个概念。

今天我们就好好说道说道。

首先,this 是 运行时 才能确认的,而非 定义时 确认的。

在函数执行时,this 总是指向 调用该函数 的对象。

要判断 this 的指向,其实就是判断 this 所在的函数 属于谁

this 的执行,会有不同的指向情况,大概可以分为:

  • 指向调用对象
  • 指向全局对象
  • 用 new 构造就指向新对象
  • apply/call/bind, 箭头函数

我们一个个来看。

1. 指向调用对象

function foo() {console.log( this.a);
}
​
var obj = {
  a: 2,
  foo: foo
};
​
obj.foo(); // 2

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 不是 doFooa 呢?

难道是 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

结果证实了我们上面的结论,并不存在什么优先。

3. 用 new 构造就指向新对象

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 返回的对象里

4. apply/call/bind

这个大家应该都很熟悉了。

令 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

5. 箭头函数

箭头函数比较特殊,它 没有自己的 this。它使用 封闭执行上下文 (函数或是 global) 的 this 值:

var x=11;
var obj={
 x:22,
 say() {console.log(this.x); //this 指向 window
 }
}
​
obj.say();// 11
obj.say.call({x:13}) // 11
​
x = 14
obj.say() // 14
​
// 对比一下
var obj2={
 x:22,
 say() {console.log(this.x); //this 指向 obj2
 }
}
obj2.say();// 22
obj2.say.call({x:13}) // 13

总结

以上我们系统的介绍了 上下文 ,以及与之相关的 作用域 闭包 this 等相关概念。

介绍了他们的作用,使用场景以及区别和联系。

希望能对大家有所帮助,文中若有纰漏,欢迎指正, 谢谢。

最后

如果觉得内容有帮助可以关注下我的公众号「前端 e 进阶」,了解最新动态。

也可以联系我加入微信群,群里有诸多大佬坐镇,可以一起探讨技术,一起摸鱼。一起学习成长!

正文完
 0