本文重点是要梳理执行上下文的生命周期中的建立作用域链,在此之前,先回顾下关于作用域的一些知识。

1.什么是作用域(scope)?

在《JavaScritp高级程序设计》中并没有找到确切的关于作用域的定义,只是在“4.2执行环境及作用域”中简单说了下执行环境(execution context)的概念。而执行环境其实就是之前博客:js基础梳理-究竟什么是执行上下文栈(执行栈),执行上下文(可执行代码)?中的执行上下文。

而在《JavaScript权威指南》中,对作用域的描述为:

变量作用域:一个变量的作用域(scope)是程序源代码中定义这个变量的区域

在《你不知道的Javascript·上卷》中对作用域的描述则为:

负责收集并维护由所有生命的标识符(变量)组成的一系列查询,并实施一套非常严格的规则,确定当前执行的代码对这些标识符的访问权限。

简单来讲,作用域(scope)就是变量访问规则的有效范围

  • 作用域外,无法引用作用域内的变量;
  • 离开作用域后,作用域的变量的内存空间会被清除,比如执行完函数或者关闭浏览器
  • 作用域与执行上下文是完全不同的两个概念。我曾经也混淆过他们,但是一定要仔细区分。

JavaScript代码的整个执行过程,分为两个阶段,代码编译阶段与代码执行阶段。编译阶段由编译器完成,将代码翻译成可执行代码,这个阶段作用域规则会确定。执行阶段由引擎完成,主要任务是执行可执行代码,执行上下文在这个阶段创建。

说得很深奥的样子,其实上面这段话重点用函数作用域与函数执行上下文来区分是最好不过的了。函数作用域是在函数声明的时候就已经确定了,而函数执行上下文是在函数调用时创建的。假如一个函数被调用多次,那么它就会创建多个函数执行上下文,但是函数作用域显然不会跟着函数被调用的次数而发生什么变化。

1.1 全局作用域

var foo = 'foo';
console.log(window.foo); // => 'foo'

在浏览器环境中声明变量,该变量会默认成为window对象下的属性。

function foo() {
name = "bar"
}
foo();
console.log(window.name) // bar

在函数中,如果不加 var 声明一个变量,那么这个变量会默认被声明为全局变量,如果是严格模式,则会报错。

全局变量会造成命名污染,如果在多处对同一个全局变量进行操作,那么久会覆盖全局变量的定义。同时全局变量数量过多,非常不方便管理。

这也是为什么jquery要在全局建立变量 $,其余私有方法属性挂在 $ 下的原因。

1.2 函数作用域

假如在函数中定义一个局部变量,那么该变量只可以在该函数作用域中被访问。

function doSomething () {
var thing = '吃早餐';
}
console.log(thing); // Uncaught ReferenceError: thing is not defined

嵌套函数作用域:

function outer () {
var thing = '吃早餐';
function inner () {
console.log(thing);
}
inner();
} outer(); // 吃早餐

在外层函数中,嵌套一个内层函数,那么这个内层函数可以向上访问到外层函数中的变量。

既然内层函数可以访问到外层函数的变量,那如果把内层函数return出来会怎样?

function outer () {
var thing = '吃早餐'; function inner () {
console.log(thing);
} return inner;
} var foo = outer();
foo(); // 吃早餐

前面提到,函数执行完后,函数作用域的变量就会被垃圾回收。而这段代码看出当返回了一个访问了外部函数变量的内部函数,最后外部函数的变量得以保存。

这种当变量存在的函数已经执行结束,但扔可以再次被访问到的方式就是“闭包”。后期会继续对闭包进行梳理。

1.3 块级作用域

很多书上都有一句话,javascript没有块级作用域的概念。所谓块级作用域,就是{}包裹的区域。但是在ES6出来以后,这句话并不那么正确了。因为可以用 let 或者 const 声明一个块级作用域的变量或常量。

比如:

for (let i = 0; i < 10; i++) {
// ...
}
console.log(i); // Uncaught ReferenceError: i is not defined

发现这个例子就会和函数作用域中的第一个例子一样的错误提示。因为变量i只可以在 for循环的{ }块级作用域中被访问了。

扩散思考:

究竟什么时候该用let?什么时候该用const?

默认使用 const,只有当确实需要改变变量的值的时候才使用let。因为大部分的变量的值在初始化之后不应再改变,而预料之外的变量的修改是很多bug的源头。

1.4 词法作用域

词法作用域,也可以叫做静态作用域。意思是无论函数在哪里调用,词法作用域都只在由函数被声明时所处的位置决定。

既然有静态作用域,那么也有动态作用域。

而动态作用域的作用域则是由函数被调用时执行的位置所决定。

var a = 123;
function fn1 () {
console.log(a);
}
function fn2 () {
var a = 456;
fn1();
}
fn2(); // 123

以上代码,最后输出结果 a 的值,来自于 fn1 声明时所在位置访问到的 a 值 123。

所以JS的作用域是静态作用域,也叫词法作用域。

上面的1.1-1.3可以看做作用域的类型。而这一小节,其实跟上面三小节还是有差别的,并不属于作用域的类型,只是关于作用域的一个补充说明吧。

2. 什么是作用域链(scope chain)

在JS引擎中,通过标识符查找标识符的值,会从当前作用域向上查找,直到作用域找到第一个匹配的标识符位置。就是JS的作用域链。

var a = 1;
function fn1 () {
var a = 2;
function fn2 () {
var a = 3;
console.log(a);
}
fn2 ();
}
fn1(); // 3

console.log(a) 语句中,JS在查找 a变量标识符的值的时候,会从 fn2 内部向外部函数查找变量声明,它发现fn2内部就已经有了a变量,那么它就不会继续查找了。那么最终结果也就会打印3了。

3. 作用域链与执行上下文

在此前的博客:js基础梳理-究竟什么是执行上下文栈(执行栈),执行上下文(可执行代码)?中讲到执行上下文的生命周期:

3.执行上下文的生命周期

3.1 创建阶段

  • 生成变量对象(Variable object, VO)
  • 建立作用域链(Scope chain)
  • 确定this指向

3.2 执行阶段

  • 变量赋值
  • 函数引用
  • 执行其他代码

上面做了那么多铺垫,其实重点是想梳理这一小节。

下面,以一个函数的创建和激活两个时期来讲解作用域链是如何创建及变化的。

3.1函数创建阶段

上文中讲到,函数的作用域在函数定义的时候就决定了。

这是因为函数有一个内部属性[[scope]],当函数创建的时候,就会保存所有父变量对象到其中,但是注意:此时[[scope]]并不代表完整的作用域链,因为在创建阶段,它还没有包括自己的作用域。

举个栗子:

function foo () {
function bar () {
...
}
}

函数创建时,各自的[[scope]]为:

foo.[[scope]] = [
globalContext.VO
]; bar.[[scope]] = [
fooContext.AO,
globalContext.AO
];

3.2 函数激活阶段

当函数激活时,进入函数上下文,创建VO/AO后,就会将活动对象添加到作用域链的前端。

这时候执行上下文的作用域链,命名为 Scope:

Scope = [AO].concat([[scope]]);

至此,作用域链创建完毕。

3.3 举个栗子

以下面的例子为例,结合之前的变量对象,活动对象和执行上下文栈,总结一下函数执行上下文中作用域链和变量对象的创建过程:

var x = 10;

function foo() {
var y = 20; function bar() {
var z = 30;
console.log(x + y + z);
} bar();
} foo(); // 60

大家肯定都知道打印结果会是60。但是从第一行代码开始到最后一行代码结束,整个代码的执行上下文栈以及作用域链是怎样变化的呢?

// 第一步:进入全局上下文,此时的执行上下文栈是这样:
ECStack = [
globalContext: {
VO: {
foo: <reference to function foo() {}>,
x: 10
}
}
]; // 第二步:foo函数被创建,此时的执行上下文栈没有变化,但是创建了foo函数的作用域,保存作用域链到内部属性[[scope]]。
ECStack = [
globalContext: {
VO: {
foo: <reference to function foo() {}>,
x: 10
}
}
];
foo.[[scope]] = [
globalContext.VO
]; // 第三步:foo函数执行,进入foo函数执行上下文的创建阶段
// 这个阶段它做了三件事:
// 1.复制之前的foo.[[scope]]属性到foo函数上下文下,创建foo函数的作用域链;
// 2. 创建foo函数上下文的变量对象,并初始化变量对象,依次加入形参,函数声明,变量声明
// 3. 把foo函数上下文的变量对象加入到第一步创建的foo函数作用域链的最前面。
// 最终,经过这三个步骤之后,整个执行上下文栈是这样 ECStack = [
globalContext: {
VO: {
foo: <reference to function foo() {}>,
x: 10
}
},
<foo>functionContext: {
VO: {
arguments: {
length: 0
},
bar: <reference to function bar() {}>,
y: undefined
},
Scope: [foo.VO, globalContext.VO]
}
]; foo.[[scope]] = [
foo.VO,
globalContext.VO
]; // 第四步:foo函数执行,进入foo函数执行上下文的执行阶段。
// 这个阶段又做了以下2件事:
// 1. 把foo执行上下文的变量对象VO改成了活动对象AO,并且修改AO中变量的值
// 2. 发现创建了一个 bar函数,就保存了bar函数的所有父变量对象到bar函数的[[scope]]属性上。 ECStack = [
globalContext: {
VO: {
foo: <reference to function foo() {}>,
x: 10
}
},
<foo>functionContext: {
AO: {
arguments: {
length: 0
},
bar: <reference to function bar() {}>,
y: 20
},
Scope: [foo.AO, globalContext.VO]
}
]; foo.[[scope]] = [
foo.AO,
globalContext.VO
]; bar.[[scope]] = [
foo.AO,
globalContext.VO
]; // 第五步,bar函数执行,进入bar函数执行上下文的创建阶段
// 与第三步类似,也做了三件事,只不过主体变成了bar
// 1.复制之前的bar.[[scope]]属性到bar函数上下文下,创建foo函数的作用域链;
// 2. 创建bar函数上下文的变量对象,并初始化变量对象,依次加入形参,函数声明,变量声明
// 3. 把bar函数上下文的变量对象加入到第一步创建的bar函数作用域链的最前面。
// 最终,经过这三个步骤之后,整个执行上下文栈是这样 ECStack = [
globalContext: {
VO: {
foo: <reference to function foo() {}>,
x: 10
}
},
<foo>functionContext: {
AO: {
arguments: {
length: 0
},
bar: <reference to function bar() {}>,
y: 20
},
Scope: [foo.AO, globalContext.VO]
},
<bar>functionContext: {
VO: {
arguments: {
length: 0
},
z: undefined
},
Scope: [bar.VO, foo.AO, globalContext.VO]
}
]; foo.[[scope]] = [
foo.AO,
globalContext.VO
]; bar.[[scope]] = [
bar.VO,
foo.AO,
globalContext.VO
]; // 第六步:bar函数执行,进入bar函数执行上下文的执行阶段
// 与第四步类似。不过此时bar函数里面不会再创建新的函数上下文了
// 1. 把bar执行上下文的变量对象VO改成了活动对象AO,并且修改AO中变量的值
ECStack = [
globalContext: {
VO: {
foo: <reference to function foo() {}>,
x: 10
}
},
<foo>functionContext: {
AO: {
arguments: {
length: 0
},
bar: <reference to function bar() {}>,
y: 20
},
Scope: [foo.AO, globalContext.VO]
},
<bar>functionContext: {
AO: {
arguments: {
length: 0
},
z: 30
},
Scope: [bar.AO, foo.AO, globalContext.VO]
}
]; foo.[[scope]] = [
foo.AO,
globalContext.VO
]; bar.[[scope]] = [
bar.AO,
foo.AO,
globalContext.VO
]; // 第七步:执行bar函数中的console.log(x + y +z),查找x, y, z三个标识符 - "x"
-- <bar>functionContext.AO // 没找到,继续到foo.AO中找
-- <foo>functionContext.AO // 还没找到,再往globalContext.VO中找
-- globalContext.VO // 找到了,值为 10 - "y"
-- <bar>functionContext.AO // 没找到,继续到foo.AO中找
-- <foo>functionContext.AO // 找到了,值为20 -- "z"
-- <bar>functionContext.AO // 找到了,值为 30 打印结果: 60。 // 第八步:bar函数执行完毕,将其从执行上下文栈中弹出,foo函数执行完毕,将其从执行上下文栈中弹出。最终,执行上下文栈,只剩下globalContext ECStack = [
globalContext: {
VO: {
foo: <reference to function foo() {}>,
x: 10
}
}
]

感觉其实可以简化理解一下,把第三四步,第五六步分别分成一个步骤。

打算每周定一个小主题,可能是基础知识巩固,也可能是自己学习新知识的记录。在下一篇博文中,将对this指向问题进行梳理。如果你也感兴趣,也可以去搜集下相关资料,到时候大家共同学习探讨一下。

js基础梳理-如何理解作用域和作用域链?的更多相关文章

  1. JS基础-该如何理解原型、原型链?

    JS的原型.原型链一直是比较难理解的内容,不少初学者甚至有一定经验的老鸟都不一定能完全说清楚,更多的"很可能"是一知半解,而这部分内容又是JS的核心内容,想要技术进阶的话肯定不能对 ...

  2. js基础梳理-关于this常见指向问题的分析

    首先,依然回顾<js基础梳理-究竟什么是执行上下文栈(执行栈),执行上下文(可执行代码)?>中的 3.执行上下文的生命周期 3.1 创建阶段 生成变量对象(Variable object, ...

  3. js基础梳理-究竟什么是执行上下文栈(执行栈),执行上下文(可执行代码)?

    日常在群里讨论一些概念性的问题,比如变量提升,作用域和闭包相关问题的时候,经常会听一些大佬们给别人解释的时候说执行上下文,调用上下文巴拉巴拉,总有点似懂非懂,不明觉厉的感觉.今天,就对这两个概念梳理一 ...

  4. js基础梳理-究竟什么是变量对象,什么是活动对象?

    首先,回顾下上篇博文中js基础梳理-究竟什么是执行上下文栈(执行栈),执行上下文(可执行代码)?的执行上下文的生命周期: 3.执行上下文的生命周期 3.1 创建阶段 生成变量对象(Variable o ...

  5. 前端【JS】,深入理解原型和原型链

    对于原型和原型链,相信有很多伙伴都说的上来一些,但有具体讲不清楚.但面试的时候又经常会碰到面试官的死亡的追问,我们慢慢来梳理这方面的知识! 要理解原型和原型链的关系,我们首先需要了解几个概念:1.什么 ...

  6. 通过作用域链解析js函数一些难以理解的的作用域问题

    基本原理 js函数在执行时,系统会创建一个隐式的属性scope,scope中存储的是函数的作用域链. 通过对这个scope的分析,就能解释JavaScript中许多难以理解的问题: 例1: funct ...

  7. 【 js 基础 】【读书笔记】作用域和闭包

    一.编译过程 常见编译性语言,在程序代码执行之前会经历三个步骤,称为编译. 步骤一:分词或者词法分析 将由字符组成的字符串分解成有意义的代码块,这些代码块被称为词法单元. 例子:  var a = 2 ...

  8. js基础梳理-内存空间

    我估计有很多像我这样非计算机专业的人进入到前端之后,总是在写业务代码,思考什么什么效果如何实现,导致很多基础概念型的东西都理解得并不太清楚.经常一碰到群里讨论的些笔试题什么的,总觉得自己像是一个假前端 ...

  9. js知识梳理1:理解对象的属性特性

    1.数据属性 数据属性的4个特性: Configurable:①表示能否通过delete删除属性从而重新定义,②能否修改属性的特性,③能否把属性修改为访问器属性.对象直接量里默认值true. Enum ...

随机推荐

  1. Confluence 6 数据库支持的驱动

    数据库 驱动已捆绑? JDBC 驱动 备注 更多信息 PostgreSQL 9.4-1202 JDBC 41 driver download 我们推荐你使用 JDBC 4 的驱动. 如果你希望使用更新 ...

  2. 基于DBUtils实现数据库连接池

    小知识: 1.子类继承父类的三种方式 class Dog(Animal): #子类 派生类 def __init__(self,name,breed, life_value,aggr): # Anim ...

  3. list的add()方法与addAll()方法简介

    简单描述:月读别人的代码,发现了一个有意思的东西,list的一个方法,addAll(),然后就去度娘了一下,发现这个还挺有用的. 吐槽一下:为什么自己没发现这个方法呢?因为平时自己写list的时候,基 ...

  4. 【ftp】主动模式和被动模式

    来自:http://blog.csdn.net/liuhelong12/article/details/50218311 原博主不让转载全文,不过下面这部分是原博主转载别人的,所以我拿过来应该没问题吧 ...

  5. kali linux DVWA config 问题解决方案

    1.下载DVWA之后解压到 var/www/html目录下 unzip DVWA-master.zip -d /usr/www/html 2.配置 打开终端,执行以下命令: 将apache2停止:se ...

  6. Nginx详解二十五:Nginx架构篇之Nginx常见的问题

    Nginx常见的问题 1.相同server_name多个虚拟主机优先级访问,是按读取文件的优先级来排序 在/opt/app/下准备3个code文件夹,下面放入3个html文件,里面的内容分别是code ...

  7. npm 如何查看一个包的版本信息?

    转载. https://blog.csdn.net/cvper/article/details/79543262 有了npm 我们能够简单的一段代码就下载我们需要的包,但是包是不断更新的, 所以我们要 ...

  8. win10ssh连接ubuntu服务器并本地绘图

    update @ 2018-11-07 00:36:38 用xrdp+tigervnc等的组合,可以使用原生unity桌面.具体教程见ubuntu日常使用指南 工具准备 win10上: xshell, ...

  9. C#Enum用Tuple保存值绑定到前端的CheckBox

    //把数字转成枚举 public static T[] NumStringsToEnums<T>(string enumNumString ) //where T:Enum { if (s ...

  10. 树递归写法ref实现

    using System; using System.Collections.Generic; using System.Linq; namespace ConsoleAppTest { class ...