一文搞清 Javascript 中的「上下文」

javavs.jpg

背景

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

image.png

参考了以前写过的博客和额外的资料, 分享给你们, 但愿能给你们带来一些启发和帮助前端

如需转载,请联系做者得到许可。java

正文

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

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

在解释「执行上下文」是什么以前, 咱们仍是先回顾下「做用域」 和 「闭包」。微信

做用域

首先, 什么是做用域呢?闭包

域, 便是范围app

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

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

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

全局做用域:

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

拥有全局做用域的对象能够分为如下几种状况:

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

局部做用域:

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);
 }
}
​
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等相关概念。

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

但愿能对你们有所帮助, 文中如有纰漏, 欢迎指正, 谢谢。

最后

若是以为内容有帮助能够关注下个人公众号 「 前端e进阶 」,了解最新动态。

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

clipboard.png

相关文章
相关标签/搜索