web前端入门到实战:Javascript 中的「上下文」你只需要看这一篇

正文

上下文 是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()
web前端开发学习Q-q-u-n: 731771211,分享学习的方法和需要注意的小细节,不停更新最新的教程和学习方法(详细的前端项目实战教学视频,PDF)

如果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是函数自身内部的。

下面我们说一下闭包。

闭包

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

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

那闭包究竟是什么呢?

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

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

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

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

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

function add(p1){
   return function(p2){
     return p1 + p2;
  }
}
​
var a = add(1);
var b = add(2);
​
a(1) //2
b(1) // 3
web前端开发学习Q-q-u-n: 731771211,分享学习的方法和需要注意的小细节,不停更新最新的教程和学习方法(详细的前端项目实战教学视频,PDF)

在上面的例子里,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)
}
web前端开发学习Q-q-u-n: 731771211,分享学习的方法和需要注意的小细节,不停更新最新的教程和学习方法(详细的前端项目实战教学视频,PDF)

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

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

  1. [推荐] 使用 let
for (let i = 1; i <= 5; i++) {
  setTimeout(function timer() {
     console.log(i)
  }, i * 1000)
}

执行上下文

首先, 执行上下文是什么呢?

简单来说, 执行上下文就是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
web前端开发学习Q-q-u-n: 731771211,分享学习的方法和需要注意的小细节,不停更新最新的教程和学习方法(详细的前端项目实战教学视频,PDF)

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

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
web前端开发学习Q-q-u-n: 731771211,分享学习的方法和需要注意的小细节,不停更新最新的教程和学习方法(详细的前端项目实战教学视频,PDF)

总结

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

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

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

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 218,122评论 6 505
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 93,070评论 3 395
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 164,491评论 0 354
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 58,636评论 1 293
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 67,676评论 6 392
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,541评论 1 305
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,292评论 3 418
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 39,211评论 0 276
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,655评论 1 314
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,846评论 3 336
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,965评论 1 348
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,684评论 5 347
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 41,295评论 3 329
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,894评论 0 22
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 33,012评论 1 269
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 48,126评论 3 370
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,914评论 2 355

推荐阅读更多精彩内容