从执行上下文,到作用域闭包

1,268

上下文与作用域之间有什么样的关系? 这一概念看似简单,但很多人都讲不清楚之间的关系。上下文和作用域都是编译原理的知识,具体编程语言有具体的实现规则,本文关注的是 JavaScript 语言的实现。

一、 上下文与作用域

上下文(context)是一段程序运行所需要的最小数据集合。我们可以从上下文交换(context switch)来理解上下文,在多进程或多线程环境中,任务切换时首先要中断当前的任务,将计算资源交给下一个任务。因为稍后还要恢复之前的任务,所以中断的时候要保存现场,即当前任务的上下文,也可以叫做环境。

作用域(scope)是标识符(变量)在程序中的可见性范围。作用域规则是按照具体规则维护标识符的可见性,以确定当前执行的代码对这些标识符的访问权限。作用域是在具体的作用域规则之下确定的。

上下文、环境有时候也称作用域,即这两个概念有时候是混用的;不过,上下文指代的是整体环境,作用域关注的是标识符(变量)的可访问性(可见性)。上下文确定了,根据具体编程语言的作用域规则,作用域也就确定了。这就是上下文与作用域的关系。

function callWithContext(fn, context) {
  return fn.call(context);
}

let name = 'Banana';

const apple = {
  name: "Apple"
};
const orange = {
  name: "Orange"
};

function echo() {
  console.log(this.name);
}

echo(); // Banana
callWithContext(echo, apple);  // Apple
callWithContext(echo, orange); // Orange
var a = 1;
function foo(){
    // 返回一个箭头函数
    return () => {
        // this 继承自 foo()
        console.log( this.a );
    };
}
var obj1 = {
    a:2
};
var obj2 = {
    a:3
};

foo()() // 1
var bar = foo.call( obj1 ); // 调用位置
bar.call( obj2 ); // 2
foo.call( obj2 )(); // 3

二、 JavaScript的执行

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

当JavaScript代码执行进入一个环境时,就会为该环境创建一个执行上下文,它会在你运行代码前做一些准备工作,如确定作用域,创建局部变量对象等。

JS代码的执行环境

  1. 全局环境
  2. 函数环境
  3. eval函数环境(不推荐使用)

执行上下文的类型

  1. 全局执行上下文
  2. 函数执行上下文
  3. eval函数执行上下文

三、 执行上下文

JavaScript运行时首先会进入全局环境,对应会生成全局上下文。程序代码中基本都会存在函数,那么调用函数,就会进入函数执行环境,对应就会生成该函数的执行上下文。

函数编程中,代码中会声明多个函数,对应的执行上下文也会存在多个。在JavaScript中,通过栈的存取方式来管理执行上下文,我们可称其为执行栈,或函数调用栈(Call Stack)。栈底永远都是全局上下文,而栈顶就是当前正在执行的上下文。

程序执行进入一个执行环境时,它的执行上下文就会被创建,并被推入执行栈中(入栈);程序执行完成时,它的执行上下文就会被销毁,并从栈顶被推出(出栈),控制权交由下一个执行上下文。栈结构

因为JS执行中最先进入全局环境,所以处于"栈底的永远是全局环境的执行上下文"。而处于"栈顶的是当前正在执行函数的执行上下文",当函数调用完成后,它就会从栈顶被推出。

"全局环境只有一个,对应的全局执行上下文也只有一个,只有当页面被关闭之后它才会从执行栈中被推出,否则一直存在于栈底"

let color = 'blue';

function changeColor() {
  let anotherColor = 'red';

  function swapColors() {
      let tempColor = anotherColor;
      anotherColor = color;
      color = tempColor;
  }

  swapColors();
}

changeColor();
  • 全局上下文只有唯一的一个,它在浏览器关闭时出栈
  • 函数的执行上下文的个数没有限制
  • 每次某个函数被调用,就会有个新的执行上下文为其创建,即使是调用的自身函数,也是如此

四、 词法作用域

作用域链,是由当前环境与上层环境的一系列变量对象组成,它保证了当前执行环境对符合访问权限的变量和函数的有序访问。

在 JavaScript 中,这个具体的作用域规则就是词法作用域(lexical scope),也就是 JavaScript 中的作用域链的规则。词法作用域是的变量在编译时(词法阶段)就是确定的,所以词法作用域又叫静态作用域(static scope),与之相对的是动态作用域(dynamic scope)。

let a = 2;

function foo() {
  console.log(a);
  // 会输出2还是3?
}

function bar() {
  let a = 3;
  foo();
}

bar();

前面说过,词法作用域也叫静态作用域,变量在词法阶段确定,也就是定义时确定。虽然在 bar 内调用,但由于 foo 是闭包函数,即使它在自己定义的词法作用域以外的地方执行,它也一直保持着自己的作用域。所谓闭包函数,即这个函数封闭了它自己的定义时的环境,形成了一个闭包。(即闭包是由函数以及创建该函数的词法环境组合而成。这个环境包含了这个闭包创建时所能访问的所有局部变量)所以 foo 并不会从 bar 中寻找变量,这就是静态作用域的特点。

而动态作用域并不关心函数和作用域是如何声明以及在何处声明的,只关心它们从何处调用。换句话说,作用域链是基于调用栈的,而不是代码中的作用域嵌套。

词法作用域是在写代码或者定义时确定的,而动态作用域是在运行时确定的。词法作用域关注函数在何处声明,而动态作用域关注函数从何处调用。

function foo() {
  let a = 0;
  function bar() {
    console.log(a);
  }
  return bar;
}

let a = 1;
let sub = foo();

sub(); // 0;

一旦设置了参数的默认值,函数进行声明初始化时,参数会形成一个单独的作用域(context)。等到初始化结束,这个作用域就会消失。这种语法行为,在不设置参数默认值时,是不会出现的。

var x = 1;
function foo(x, y = function() { x = 2; }) {
  var x = 3;
  y();
  console.log(x);
}

foo() // 3
x // 1

上面代码中,函数foo的参数形成一个单独作用域。这个作用域里面,首先声明了变量x,然后声明了变量yy的默认值是一个匿名函数。这个匿名函数内部的变量x,指向同一个作用域的第一个参数x。函数foo内部又声明了一个内部变量x,该变量与第一个参数x由于不是同一个作用域,所以不是同一个变量,因此执行y后,内部变量x和外部全局变量x的值都没变。


五、 闭包的应用

模块化、柯里化、模拟块级作用域、命名空间、缓存数据

const tar = (function () {
    let num = 0;
    return {
        addNum: function () {
            num++;
        },
        showNum: function () {
            console.log(num);
        }
    }
})()
tar.addNum();
tar.showNum();
let add = function(x){
  return function(y){
    return x + y
  }
}
console.log(add(2)(4)) // 6
for (var i = 1; i < 5; i++) {
  setTimeout(function timer() {
    console.log(i);
  }, 0);
}

function func(){
  for(var i = 0; i < 5; i++){  
    + (i => { setTimeout(() => console.log(i),300) })(i)
  }
}
func()
var MyNamespace = {};  

MyNamespace.doSomething = function (){  
    //使用闭包产生的私有类变量  
    var label,  icon;  
  
    //可访问私有变量,但不可被外部访问的私有方法  
    function setLabel(){  
      // do something...
    }  
    //可访问私有变量,也可被外部访问的方法  
    this.getLabel = function(){  
      // do something...
    };
}

// 该方法可被外部访问,却只能通过取/赋值器访问私有类变量  
MyNamespace.TreeItem.prototype = {  
    print: function(){  
        console.log( this.getLabel() );  
    }
}  
import {readFileSync, readdirSync} from 'fs';

var readContent = (function(){
  let contentCache = {};

  return (bookName)=>{
    let content = contentCache[bookName];
    if (!content){
      content = readFileSync(bookName+".txt", "utf8");
      contentCache[bookName] = content;
    }
    return content;
  };
})();

参考文章: