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

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

1、 上下文与做用域

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

做用域(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
复制代码

2、 JavaScript的执行

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

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

JS代码的执行环境app

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

执行上下文的类型编程语言

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

3、 执行上下文

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

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

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

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

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

let color = 'blue';

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

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

  swapColors();
}

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

4、 词法做用域

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

在 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的值都没变。


5、 闭包的应用

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

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;
  };
})();
复制代码

参考文章:

相关文章
相关标签/搜索