Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

1.1 作用域 Scope #131

Open
6 tasks done
EthanLin-TWer opened this issue Mar 6, 2017 · 0 comments
Open
6 tasks done

1.1 作用域 Scope #131

EthanLin-TWer opened this issue Mar 6, 2017 · 0 comments
Assignees

Comments

@EthanLin-TWer
Copy link
Owner

EthanLin-TWer commented Mar 6, 2017

本节需要读者具备基本的 JavaScript 预备知识 #130

Scope 作用域

  • 为什么先讲 作用域 scope 和 闭包 closure ?因为它对接下来要学习的 OO JavaScript 很重要;
  • 支持 OO JavaScript 的两个关键技术是:this 和 原型 prototype;

Lexical scope 词法作用域

作用域是什么呢?它指的是你的变量和函数运行到某个地方的代码处能否被访问到。为什么需要作用域呢?为什么要限制变量的访问性而非全部暴露到公共域下呢?这是计算机科学中最基本的概念和理念:隔离性(The Principle of Least Access)。为了职责明确,你只能刚好访问到你需要的所有东西,不多也不少。附带地,它带来了模块化、命名空间等好处,让你写出更易阅读、更易维护的代码。可以说,作用域是许多现代编程语言都从语言层面支持的一个特性。

而词法作用域,指的是一个变量,你可以通过变量名引用之,而不发生引用错误(Lexical Scoping defines how variable names are resolved in nested functions)。它本质上是 静态作用域 static scopes

有什么类型的作用域呢?

  • 全局作用域:不定义在任何函数以内的变量或函数都位于全局作用域下。当然,这个全局作用域其实也是有边界/上下文的,比如 NodeJS 中不同文件之间的全局变量不能互相访问,因为每个全局对象 global 的上下文仅限于一个文件;比如浏览器中 不同 tab 之间的全局变量也是不能互相访问的,因为每个标签的全局对象 window 的上下文也仅限于一个 tab 中
  • 函数作用域:任何定义在函数内的变量或函数都处于函数作用域下,这些变量无法在函数以外被引用到
  • 块作用域:ES6之前是没有这个东西的,也就是说,定义在 { } 大括号对 及 for (i = 0; i < 10; i++) { ... } 循环结构中的变量统统都会跑到全局作用域下去。这就很违反直觉了。ES6 通过 letconst 关键字修复了这个问题,并且赋予了一般的块 block 以块作用域

在函数或块作用域中声明的变量或函数若发生嵌套,则又有了嵌套下的词法作用域规则。相对地,位于一个函数/块内部的变量或函数称位于内层作用域 inner scope;前者相对地称其位于外层的作用域 outer scope。内外之间的变量访问性规则为:

  • 外层作用域(全局作用域是最外层的作用域)下的变量及函数可以在内层作用域中被访问获取
  • 内层作用域 可以访问到外层作用域的变量,可以访问自身的变量,但不能被外层作用域引用

作用域什么时候生成呢?

  • 全局作用域:没声明就使用的代码,默认全跑到全局作用域中。可以使用 use strict 模式禁止
  • 函数作用域:函数声明开始时 function() { ... },自动生成一个词法作用域
  • 块作用域:ES6 前,{ } 大括号对 及 for (i = 0; i < 10; i++) { ... } 循环结构 均不生成词法作用域。也就是说,块里面声明的变量全部都会跑到当前的全局作用域里去。这是前 JS 时代的一个坑。ES6 以后 letconst 关键字都修复了这个问题,它们会生成一个块作用域,或可称为 condition/loop lexical scope

因此 ES6 时代后,可以这样理解:除了函数作用域和块作用域,其他的全是全局作用域。

if (true) {
  dragon = 'dragon' // used without declaration
}

// output: 'dragon'
console.log(dragon) 
'use strict'

if (true) {
  dragon = 'dragon'
}

// output: ReferenceError: dragon is not defined
if (true) {
  var varVariable = 'var'
  let letVariable = 'let'
  const constVariable = 'const'
}

console.log(varVariable)   // output: var
console.log(letVariable)   // output: ReferenceError: letVariable is not defined
console.log(constVariable) // output: ReferenceError: constVariable is not defined

作用域对于编程模型的重要意义之一,即是其体现的模块概念。你只需要关注模块内与自己相关的变量和代码,而不需考虑代码库中其他任何代码。这样你可以专注在当前代码片段上,减少编写时大脑的思考负担,也减少了维护阅读时的理解负担。这样的编程模型很符合单一职责原则(SRP),大大提高了工作效率 。是人性的。

执行引擎生成词法作用域这一块的代码能看到吗?(肯定能)在哪里?

Execution context 执行上下文

跟讲 作用域&词法作用域 的套路一样,讲执行上下文前,我们先来了解了解上下文。

上下文与作用域是非常不同的概念。作用域是静态的分析概念,它在你代码写完后就唯一确定下来了,指示变量对所有代码的可见性;而上下文是动态的运行时概念,它指的是代码运行时同个作用域下 this 值的指向。

执行上下文,是 ECMA-262 规范定义的一个概念,用于描述一段可执行代码的类型和状态。但规范对其实现模型及数据结构均未做规定,由 ECMA 引擎各自实现。这里“执行上下文”中的上下文,偏指作用域,而非一般的“上下文”概念(也即 this 指向),它也是程序运行时的概念,可以通俗理解为它是代码运行起来后,作用域概念的内存描述

Execution context (abbreviated form — EC) is the abstract concept used by ECMA-262 specification for typification and differentiation of an executable code.

The standard does not define accurate structure and kind of EC from the technical implementation viewpoint; it is a question of the ECMAScript-engines implementing the standard.

Logically, set of active execution contexts forms a stack. The bottom of this stack is always a global context, the top — a current (active) execution context. The stack is modified (pushed/popped) during the entering and exiting various kinds of EC.

可以看到反过来,词法作用域 是 执行上下文的静态描述。同一个函数的词法作用域,在运行时可能会生成零个、一个或多个执行上下文,取决于它被调用多少次。虽是同一函数,但每次调用的执行上下文是分割的,不可互相访问。执行上下文即是运行时用来描述词法作用域所表示的变量对不同函数、代码可见性的一段内存和数据结构,简称环境。话有点绕,但需要深刻体会

实现上,普通的 key-value 键值对即可做到,这与一般的 object 很像。不过,一是执行上下文有其“业务需求”——即服务于实现词法作用域——决定了这个键值对可能不会使用普通 JS 对象上所具备的所有操作;二是,它们位于截然不同的世界中,词法作用域是静态的、文本的分析概念(因为它又称静态作用域),执行上下文是运行时的实现模型。

image

看一下 这一节 的视频,特别赞,使用了模型、颜色来模拟运行时的内存情况以解释执行上下文。

执行上下文栈 Execution Context Stack

由于 JavaScript 是个单线程模型的编程语言,因此任一时刻,正在运行的执行上下文只能有一个,称为 active EC;其他的 EC 则依它们被调用的先后次序,形成了一个后入先出的栈结构,简称 EC Stack。最地下的 EC 通常是 Global EC。

EC 的创建与闭包实现的关键:作用域链 Scope Chain

每个函数执行时,都会生成一个 EC。EC 的生成过程分为两个阶段:

  1. 环境准备阶段(也称创建阶段)
  2. 代码执行阶段(也称执行阶段)

执行阶段简单,不讲;环境准备阶段,引擎会准备并填充三个变量,保存到该创建的执行上下文中。里面的这些变量是代码执行过程所需要的所有变量的集合。话说回来,这三个变量分别为:

  • Variable Object 变量对象。主要包含了一些函数参数啊、函数定义的内部变量及其他声明等
  • Scope Chain 作用域链。它是词法作用域的静态描述,是实现闭包的关键要素之一!!太神奇了
  • this 关键字的值。填充好该函数的 this 的值

简单来说,一个 EC 经过创建阶段后,概念模型上是长这样的:

executionContextObject = {
    'variableObject': {}, // 包含了函数参数、内部变量定义及一些其他的声明等
    'scopeChain': {}, // 包含了该 EC 自身及其所有外部函数的 variableObject。是实现闭包的关键性数据结构 
    'this': valueOfThis
}

EC 是对编程模型-现实模型映射的作用,还是语法层面的作用,还是代码应用层面的作用?它是编程语言实现的一个普遍方面,还是 JS 有其特有的 EC 设计?

拓展阅读

我视为最佳的材料:

其他材料:

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests

1 participant