博客
关于我
强烈建议你试试无所不能的chatGPT,快点击我
执行上下文、变量对象、作用域链、this,看这篇就够了!
阅读量:6280 次
发布时间:2019-06-22

本文共 5442 字,大约阅读时间需要 18 分钟。

不管是前端老司机,还是前端小白,看到标题中列举的这些概念,想必都是头大。其实你知道么?这些概念背后是有联系的,理清楚他们的关系,你才能准确且牢靠地记住他们。

也只有理清楚这些基本且重要的概念,你才能在前端的道路上越走越远。

好了,让我们开始吧。

执行上下文

执行上下文可以理解为函数运行的环境。每个函数执行时,都会给对应的函数创建这样一个执行环境。

JS运行环境大概包括三种情况:全局环境、函数环境、eval环境(不推荐使用,所以不讨论)。

一个JS程序中,必定会产生多个执行上下文,JS引擎会以栈的方式处理它们,这个栈,我们称之为函数调用栈。栈底永远都是全局上下文,栈顶就是当前正在执行的上下文。

由于栈是先进后出的结构,我们不难推出以下四点:

  • 只有栈顶的上下文处于执行中,其他上下文需要等待
  • 全局上下文只有唯一的一个,它在浏览器关闭时出栈
  • 函数的执行上下文的个数没有限制
  • 每次某个函数被调用,就会有个新的执行上下文为其创建。

当然,光知道这些还是不够,我们还必须了解执行上下文的生命周期。

执行上下文的生命周期

当调用一个函数时,一个新的执行上下文就会被创建。而一个执行上下文的生命周期可以分为两个阶段。

创建阶段

在这个阶段中,执行上下文会分别创建变量对象,建立作用域链,以及确定this的指向。

代码执行阶段

创建完成之后,就会开始执行代码,这个时候,会完成变量赋值,函数引用,以及执行其他代码。

至此,我们终于知道执行上下文跟变量对象、作用域链及this的关系。

接下来我们重点介绍这三个概念。

变量对象

当一个函数被调用时,执行上下文就创建了,执行上下文包含了函数所有声明的变量和函数,保存这些变量跟函数的对象,我们称之为变量对象。

变量对象的创建,依次经历了以下几个过程。

  • 建立arguments对象。检查当前上下文中的参数,建立该对象下的属性与属性值。
  • 检查当前上下文的函数声明,也就是使用function关键字声明的函数。在变量对象中以函数名建立一个属性,属性值为指向该函数所在内存地址的引用。如果函数名的属性已经存在,那么该属性将会被新的引用所覆盖。
  • 检查当前上下文中的变量声明,每找到一个变量声明,就在变量对象中以变量名建立一个属性,属性值为undefined。如果该变量名的属性已经存在,为了防止同名的函数被修改为undefined,则会直接跳过,原属性值不会被修改。
    举个反例,很多人对以下代码存在疑问,既然变量声明的foo遇到函数声明的foo会跳过,可是为什么最后foo的输出结果仍然是被覆盖了?
function foo() { console.log('function foo') }var foo = 20;console.log(foo); // 20复制代码

这是因为上面的三条规则仅仅适用于变量对象的创建过程。也就是执行上下文的创建过程。而foo = 20是在执行上下文的执行过程中运行的,输出结果自然会是20。对比下例。

console.log(foo); // function foofunction foo() { console.log('function foo') }var foo = 20;复制代码
// 上栗的执行顺序为// 首先将所有函数声明放入变量对象中function foo() { console.log('function foo') }// 其次将所有变量声明放入变量对象中,但是因为foo已经存在同名函数,因此此时会跳过undefined的赋值// var foo = undefined;// 然后开始执行阶段代码的执行console.log(foo); // function foofoo = 20;复制代码

再看一个例子:

// demo01function test() {    console.log(a);    console.log(foo());    var a = 1;    function foo() {        return 2;    }}test();复制代码

我们直接从test()的执行上下文开始理解。全局作用域中运行test()时,test()的执行上下文开始创建。为了便于理解,我们用如下的形式来表示

// 创建过程testEC = {    // 变量对象    VO: {},    scopeChain: {}}// 因为本文暂时不详细解释作用域链,所以把变量对象专门提出来说明// VO 为 Variable Object的缩写,即变量对象VO = {    arguments: {...},  //注:在浏览器的展示中,函数的参数可能并不是放在arguments对象中,这里为了方便理解,我做了这样的处理    foo: 
// 表示foo的地址引用 a: undefined}复制代码

未进入执行阶段之前,变量对象中的属性都不能访问!但是进入执行阶段之后,变量对象转变为了活动对象,里面的属性都能被访问了,然后开始进行执行阶段的操作。

变量对象和活动对象其实都是同一个对象,只是处于执行上下文的不同生命周期。不过只有处于函数调用栈栈顶的执行上下文中的变量对象,才会变成活动对象。

// 执行阶段VO ->  AO   // Active ObjectAO = {    arguments: {...},    foo: 
, a: 1, this: Window}复制代码

因此,上面的例子demo1,执行顺序就变成了这样

function test() {    function foo() {        return 2;    }    var a;    console.log(a);    console.log(foo());    a = 1;}test();复制代码

作用域链与闭包

变量对象讲完了,接着是作用域链,这里就不得不先提下作用域。

作用域

作用域最大的用处就是隔离变量,不同作用域下同名变量不会有冲突。

JavaScript中只有全局作用域与函数作用域。言外之意是:javascript除了全局作用域之外,只有函数可以创建的作用域

JavaScript代码的整个执行过程,分为两个阶段,代码编译阶段代码执行阶段

编译阶段由编译器完成,将代码翻译成可执行代码,这个阶段作用域规则会确定。

执行阶段由引擎完成,主要任务是执行可执行代码,执行上下文在这个阶段创建。

理解这点很重要,我们面试过程中,经常会被问到“自由变量”的取值问题。

什么是“自由变量”?先看个例子:

var x = 10;function fn() {    var b = 20;    console.log(x+b); // x在这里就是一个自由变量}复制代码

取x的值时,需要到另一个作用域中取,x就被称作“自由变量”。

“自由变量”的取值,难倒一片的人,不信,看看下面这个例子:

var x = 10;function fn() {    console.log(x);}function show(f){    var x = 20;    (function () {        f(); // 这里输出什么???    })();}show(fn);复制代码

你的第一反应是不是20?答案是10!!

其实这个问题很简单,自由变量要到创建这个函数的那个作用域中取值——是“创建”,而不是“调用”

为什么呢?因为作用域是在代码编译过程就确定下来的,然后就不会改变,这就是所谓的“静态作用域”。

本例中,在fn函数取自由变量x的值时,要到哪个作用域中取?——要到创建fn函数的那个作用域中取——无论fn函数将在哪里调用。fn明显是在全局环境下创建的,x明显就是10。

作用域链

上面的例子,只是跨一个作用域去寻找。

如果跨了一步,还没找到呢?——接着跨!——一直跨到全局作用域为止。要是在全局作用域中都没有找到,那就是真的没有了。

这个一步一步“跨”的路线,我们称之为——作用域链。

我们拿文字总结一下取自由变量时的这个“作用域链”过程:(假设a是自由量)

第一步,现在当前作用域查找a,如果有则获取并结束。如果没有则继续;

第二步,如果当前作用域是全局作用域,则证明a未定义,结束;否则继续;

第三步,(不是全局作用域,那就是函数作用域)将创建该函数的作用域作为当前作用域;

第四步,跳转到第一步。

闭包

闭包是一种特殊的对象。

它由两部分组成。执行上下文(代号A),以及在该执行上下文中创建的函数(代号B)。

当B执行时,如果访问了A中变量对象中的值,那么闭包就会产生。

// demo01function foo() {    var a = 20;    var b = 30;    function bar() {        return a + b;    }    return bar;}var bar = foo();bar();复制代码

上面的例子,首先有执行上下文foo,在foo中定义了函数bar,而通过对外返回bar的方式让bar得以执行。当bar执行时,访问了foo内部的变量a,b。因此这个时候闭包产生。

闭包的应用场景

除了面试,在实践中,闭包有两个非常重要的应用场景。分别是模块化与柯里化。

this

this或许是最让初学者头疼的概念了吧。this难就难在指向上。

请记住:this的指向,是在函数被调用的时候确定的,在函数执行过程中,this一旦被确定,就不可更改了

我们来看看几种情况:

全局对象中的this

全局环境中的this,指向它本身。

函数中的this

在一个函数上下文中,this由调用者提供,由调用函数的方式来决定。如果调用者函数,被某一个对象所拥有,那么该函数在调用时,内部的this指向该对象。如果函数独立调用,那么该函数内部的this,则指向undefined。但是在非严格模式中,当this指向undefined时,它会被自动指向全局对象。切记,函数执行过程中,this一旦被确定,就不可更改。

'use strict';var a = 20;function foo () {    var a = 1;    var obj = {        a: 10,        c: this.a + 20,        fn: function () {            return this.a;        }    }    return obj.c;}console.log(foo());    // ?console.log(window.foo());  // ?复制代码

执行foo()时,函数独立调用,所以this指向undefined(因为是严格模式),所以执行this.a时报错。

执行window.foo()时,this.a = 20,结果为40.

function foo() {    console.log(this.a)}function active(fn) {    fn(); // 真实调用者,为独立调用}var a = 20;var obj = {    a: 10,    getA: foo}active(obj.getA); // 20复制代码

使用call,apply显示指定this

call与applay

构造函数与原型方法上的this

function Person(name, age) {    // 这里的this指向了谁?    this.name = name;    this.age = age;   }Person.prototype.getName = function() {    // 这里的this又指向了谁?    return this.name;}// 上面的2个this,是同一个吗,他们是否指向了原型对象?var p1 = new Person('Nick', 20);p1.getName();复制代码

this,是在函数调用过程中确定,因此,搞明白new的过程中到底发生了什么就变得十分重要。

通过new操作符调用构造函数,会经历以下4个阶段。

  • 创建一个新的对象;
  • 将构造函数的this指向这个新对象;
  • 指向构造函数的代码,为这个对象添加属性,方法等;
  • 返回新对象。

因此,当new操作符调用构造函数时,this其实指向的是这个新创建的对象,最后又将新的对象返回出来,被实例对象p1接收。因此,我们可以说,这个时候,构造函数的this,指向了新的实例对象,p1。

而原型方法上的this就好理解多了,根据上边对函数中this的定义,p1.getName()中的getName为调用者,他被p1所拥有,因此getName中的this,也是指向了p1。

写在最后

本文提到的概念,都是JavaScript中相对晦涩的,平时开发过程中,要多思考其原理,这是一个必经的阶段,只要不断加深理解,我们才能真正掌握这些概念,也只有掌握好这些概念,我们才能在前端的道理上越走越远。

转载地址:http://hisva.baihongyu.com/

你可能感兴趣的文章
BOOT.INI文件参数
查看>>
vmstat详解
查看>>
新年第一镖
查看>>
unbtu使用笔记
查看>>
OEA 中 WPF 树型表格虚拟化设计方案
查看>>
Android程序开发初级教程(一) 开始 Hello Android
查看>>
使用Gradle打RPM包
查看>>
“我意识到”的意义
查看>>
淘宝天猫上新辅助工具-新品填表
查看>>
再学 GDI+[43]: 文本输出 - 获取已安装的字体列表
查看>>
nginx反向代理
查看>>
操作系统真实的虚拟内存是什么样的(一)
查看>>
hadoop、hbase、zookeeper集群搭建
查看>>
python中一切皆对象------类的基础(五)
查看>>
modprobe
查看>>
android中用ExpandableListView实现三级扩展列表
查看>>
%Error opening tftp://255.255.255.255/cisconet.cfg
查看>>
java读取excel、txt 文件内容,传到、显示到另一个页面的文本框里面。
查看>>
《从零开始学Swift》学习笔记(Day 51)——扩展构造函数
查看>>
python多线程队列安全
查看>>