执行上下文/作用域/闭包/一等公民

1.什么时执行上下文?

执行上下文,它是比较抽象的概念,就是当前 JavaScript 代码被解析和执行时所在环境,所以,在 JavaScript 中运行任何的代码都是在执行上下文中运行的。

执行上下文有三种类型

  • 第一种类型:全局执行上下文,只有一个即一个程序中只有一个全局执行上下文,如果是在浏览器中,那么全局对象就是 window 对象, this 指向就是这个全局对象。
  • 第二种类型:函数执行上下文,函数执行上下文可以存在多个,甚至是无数个,只有在函数被调用时才会创建(函数执行上下文),每次调用完函数都会创建一个新的执行上下文。
  • 第三种类型:Eval 函数执行上下文,这个是运行在 eval 函数中的代码,只有在 eval 函数中的代码才有 eval 函数执行上下文。
2.执行栈

每个函数都有自己的执行环境,当执行流进入一个函数时,函数的环境就会被推入一个环境栈中,函数执行完,栈将其环境弹出,把控制权返回给之前的执行环境。

其实执行堆栈(调用堆栈)具有后进先出结构的堆栈,该结构用于存储在代码执行期间创建的所有执行上下文。

压栈出栈的过程–执行上下文栈

当 JavaScript 引擎运行 JavaScript 代码时它会创建一个全局执行上下文并将其 push 到当前执行 堆栈。(函数还没解析或是执行、调用)仅用在全局执行上下文,每当引擎发现函数调用时,引擎都会为该函数创建一个新的函数执行上下文,并将其推入到堆栈的顶部(当前执行栈的栈顶)

当引擎执行其执行上下文位于堆栈顶部的函数之后,将其对应的函数执行上下文将会从堆栈中弹出,并且控件到达当前堆栈中位于其下方的上下文(如果有下一个函数的话)

执行上下文的生命周期

  • 创建过程:1. 生成变量对象,2.建立作用域链 3.确定 this 的指向
  • 执行过程:1.变量赋值 2. 函数引用 3.执行其他代码
  • 销毁阶段:执行完毕后出栈,等待被回收
3.执行上下文的创建

执行上下文创建分为两个阶段
创建阶段-执行上下文

	确定 this 的指向,this 确定或设置的值

在全局执行上下文中, this 是指向全局对象,在浏览器中,this的值指向 window 对象,在 nodejs 中指向的是 module 对象。

在函数执行上下文中, this 的值取决于函数的调用方式(即如何被调用的)。当它被一个引用对象调用,则将值得 this 设置为该对象,否则 this 的值将指设置为全局对象或者 undefined(在严格模式下)。

抽象的,词汇环境在伪代码中看起来这样:

GlobalExectionContext = {
	// 全局执行上下文
	lexicalEnvironment:{
 		// 词法环境
 		EnvironmentRecord:{
		  // 环境记录
		  Type:"Object",
		  // 全局环境
		  // 标识符绑定在这里
		  outer:< null >
		  // 对外部环境的引用
		}
	
	}
}
FunctionExectionContext = {
	// 函数执行上下文
	LexicalEnvironment:{
		// 词法环境
		EnvironmentRecord:{
		 // 环境记录
		 Type:"Declarative".
		 // 函数环境
		 // 标识符绑定在这里
		 // 对外部环境的引用
		 outer:<Global or outer function environment reference>
		}
	}
	
}

首先看到词法环境?究竟什么事词法环境呢?这个名词概念如何理解?
那么首先上来就是,词法环境的定义:
官方规范对词法环境的说明,词法环境是一种规范类型,用于根据 ECMAScript 代码的词法嵌套解构来定义标识符与特定变量和函数的关联。

词法环境是保存标识符,变量映射的结构。(这里的标识符是指变量/函数的名称,而变量是对实际对象(包括函数对象和数组对象)或原始值得引用)

词法环境由一个环境记录和可能为空引用(null)的外部词法环境组成,通常,词法环境和 ECMAScript 代码的特定语法结构相关联。

	环境记录是在词法环境中存储变量和函数声明的地方。

环境记录主要使用两种环境记录:声明性环境记录和对象环境记录。环境记录分别是声明式环境记录,对象环境记录和全局环境记录。(全局环境记录在逻辑上是单个记录,但是它被指定为封装对象环境记录和声明性环境记录的组合)
声明性环境记录**(绑定了包含在其作用域内声明定义的标识符集),就是它存储变量和函数声明**,功能代码的词法环境包含一个声明性环境记录。
对象环境记录(绑定对象),全局代码的词法环境包含一个客观环境记录,除了变量和函数声明外,对象环境记录还存储全局绑定对象。所以,对于每个绑定对象的属性,将在记录中创建一个新的条目。

当代码在一个环境中执行时,会创建变量对象的一个作用域链。**作用域链式保证对执行环境有权访问的所有变量和函数的有序访问。
**作用域链的前端,始终都是当前执行的代码所在环境的变量对象,如果这个环境是函数,则将其**活动对象**作为变量对象。活动
对象在最开始时只包含一个变量,即 arguments 对象。

所以,对于功能代码来说,环境记录中包含了一个 arguments 对象,该对象包含传递给该函数的索引和参数与传递给该函数的参数的长度之间的映射。
如下代码:

function foo(a,b){
	let c = a + b;
}
foo(1,2);
// 参数对象 {0:1,1:2, 长度:2}
对外部环境的引用,意味着它可以访问其外部词法环境,如果在当前词法环境中找不到变量,则 JavaScript 引擎可以在外部环境中查找变量。

词法环境有两个组成部分:

  • 环境记录,记录相应环境中的形参,函数声明变量声明等(存储变量和函数声明的实际位置)
  • 对外部环境的引用,可以访问其外部词法环境

用伪代码表示:

function LexicalEnvironment(){
	this.EnvironmentRecord = undefined;
	this.outer = undefined;
	// outer Environment Reference
}

环境记录记录了在其关联的词法环境作用域内创建的标识符绑定。
其实词法环境就是描述环境的对象,先确定当前环境的外部引用,环境记录初始化,就是常遇到的声明提前,全局代码执行之前,先初始化全局环境;函数代码执行之前,先初始化函数环境。

  1. 全局环境(用于表示在共同领域中处理所有共享最外层作用域的 ECMAScript script 元素)是一个没有外部环境的词法环境,所以,全局环境的外部环境引用为 null。
  2. 模块环境是一个包含模块顶层声明绑定的词法环境,它的外部环境是一个全局环境。
  3. 函数环境是一个对应于 ECMAScript 函数对象调用的词法环境。

现在用代码表示词法环境:

var a = 1;
var b = 2;
function foo(){
	console.log('chenxishen');
}
// 这段代码的词法环境表示:
lexicalEnvironment = {
	a:1,
	b:2,
	foo:<ref. to foo function>
}

执行阶段-执行上下文
在此阶段,将完成对所有这些变量的分配,最后执行代码。

VarableEnvironmnet(变量环境)组件已创建

在 ES6 中,词法组件和变量环境组件之间的区别是前者用于存储函数声明和变量绑定( let 和 const),而后者仅用于存储变量 var 绑定。

说说变量提升的原因,在创建阶段,函数声明存储在环境中,而变量会被设置为 undefined 或 保持未初始化。
所以,这就是为什么可以在声明之前访问 var 定义的变量,但如果在声明之前访问 let 和 const 定义的变量就会提示引用错误的原因。

现在举个例子:

let data1,data2 = 1;
function foo(){
	var data3,data4;
}
foo();

JS 在执行这段代码时,创建了一个词法环境(global environment-ge),确定 (ge) 的环境记录,里面包含了 data1,data2,foo 等标识符的记录,设置外部词法环境的引用,因为 (ge) 以及在最外面了,所以,外部词法环境引用就是 null,到此(ge) 就确立完毕了。

接着执行代码,当执行到 foo() , JS 调用了 foo 函数,foo 函数是一个函数声明, JS 开始执行函数创建了一个新的词法环境表示为 (ge2),设置 (ge2) 的外部词法环境引用,很明显就是 (ge), (ge2)的环境记录(data3,data4)。

所有的创建词法环境以及环境记录都是不可见的,在编译器内完成

实例词法环境:

 // 全局 词法环境,源文件代码,就是一个词法环境
 // 函数代码 eval 词法环境, with解构 catch结构
 
 // 全局的词法环境
 var a = 1;
 function da1(){
	// 函数 da1 的词法环境
	var b = 2;
	function da2(){
		// 函数 da2 的词法环境
		return a*b;
	}
	return da2();
 }
 with({c:3,d:5}){
	// with 声明的词法环境
	console.log(this.c);
 }
 try{
	var e = da1();
 }catch(e){
	 // catch块声明的词法环境
	 console.log('chenxishen')}
4. JavaScript 执行上下文栈过程

思考,JavaScript 引擎并非一行一行的分析和执行程序,而是一段一段地分析执行,如何管理创建那么多执行上下文?
JavaScript 引擎创建了执行上下文栈来管理。

5. 面试题

比较下面两段代码,解释一下两段代码的不同之处

 //  A----
 var scope = "global scope";
 function checkscope(){
	var scope = "local scope";
    function f(){
        return scope;
    }
    return f();
 }
 checkscope();

// B---------------------------
var scope = "global scope";
function checkscope(){
    var scope = "local scope";
    function f(){
        return scope;
    }
    return f;
}
checkscope()();

解释:因为JavaScript采用的是词法作用域,函数的作用域基于函数创建的位置。

JavaScript 函数的执行用到了作用域链,这个作用域链式在函数定义的时候创建的,嵌套的函数 f() 定义在这个作用域链里,
其中的变量 scope 一定是局部变量,不管何时何地执行函数 f(),这种绑定在执行 f() 时仍然有效。
  • 第一个内部函数 f 在初始化时,会建立一个活动对象,它会添加一个 属性名为 scope 的属性,会给它建立一个隐藏属性 [[scope]] ,这个就是用于指向父级活动对象的。在到这个函数执行时, scope 会被赋值,顺着它的 [[scope]]就可以找到父级的值,返回一个代指的变量,继续返回到函数外部,输出 local scope
  • 第二个内部函数 f 在初始化的时候也是建立一个活动对象,这个活动对象上会添加一个属性名为 scope 属性,也会建立一个指向父级活动对象的 [[scope]] 隐藏属性。在 checkscope 第一次执行进入 checksocpe 函数体的时候返回的是 f 指针值(对内部函数的一个引用),而非第一个返回的直接就是个原始值变量,第二次执行才进入 f 函数体,内部活动对象及 [[scope]] 私有属性已建立,它便顺着这条链查找 scope 变量的值,并返回,形成闭包。

对于函数对象来说,当外层函数执行完就该销毁所有变量的,但此时一个函数指针被返回了,就意味着外部跟函数建立了联系,这个指针指向函数内部区域,它无法销毁,作用域链还在,所以,内部那个函数就可以访问到私有变量了。

变量对象,每一个执行上下文都会分配一个变量对象,变量对象的属性由变量和函数声明构成,在函数上下文情况下,参数列表也会被加入到变量对象中作为属性,变量对象与当前作用域相关。

不同作用域的变量对象互不相同,它保存了当前作用域的所有函数和变量。

执行环境定义了变量或函数有权访问其他的数据,决定了他们各自的行动,每个执行环境都有一个与之关联的变量对象,环境中定义的所有变量和函数都保存在这个对象中。

只有函数声明会被加入到变量对象中,而函数表达式不会。

// 函数声明
function da(){}
console.log(typeof da); // "function"

// 函数表达式
var da2 = function da1(){};
console.log(typeof da2); // "function"
console.log(typeof da1); // "undefined" 

当 JS 编辑器开始执行的时,会初始化一个全局对象用于关联全局的作用域,对于全局环境而言,全局对象就是变量对象。

之前提到了变量对象对于程序而言是不可以读的,只有编译器才有权访问变量对象。在浏览器端,全局对象被具象成 window对象,即全局对象 等于 window 等于 全局环境的 variable object。

当函数被调用,那么一个活动对象就会被创建并分配给执行上下文,则将其活动对象作为变量对象,活动对象由特殊对象 arguments 初始化。

arguments对象,这个对象在全局作用域是不存在的。

实例如下:


function dada(name, love){
    var job = 'it',
    function dada1(){}
}
dada('Jeskson',girl);

dada 被调用时,在dada的执行上下文会创建一个活动对象 AO,并且被初始化为 AO = [arguments],随后 AO 被当做变量对象variable object,vo 进行变量初始化,此时 VO = [arguments].concat([name,love,jog])。

词法作用域,词,单词,法,语法,就是单词(标识符,原始值,操作符等),语法就是JavaScript中的各种语法规则,
所以,词法作用域在js中,一种全局,一种函数。

作用域控制着变量和参数的可见性以及生命周期,在一块代码块中定义的所有变量在代码块的外部是不可见的 ,定义在代码块中的变量在代码块执行结束后会释放。在函数中的参数和变量在函数外部是不可见的,在一个函数内部任何定义的变量,在该函数内部都是可见的。

JavaScript采用词法作用域,也就是静态作用域,函数的作用域在函数定义的时候就决定了。

6. 动态作用域

动态作用域,函数的作用域是在函数调用的时候才决定的。

bash 就是动态作用域,不信的话,把下面的脚本存成例如 scope.bash,然后进入相应的目录,用命令 执行 bash ./scope.bash ,看看打印的值是多少。

总而言之,作用域的好处是内部函数可以访问定义他们的外部函数的参数和变量,除 this 和 arguments。

综上,每个执行上下文,都有变量对象,作用域链,this。

7. 作用域链

作用域链:当查找某个变量时,会先在当前上下文的变量对象中查找,如果没有找到,就会从父级执行上下文的变量的对象中查找,一直找到全局上下文的变量对象,也就是全局对象。(即由多个执行上下文的变量构成)

函数内部有一个内部属性[[scope]],当函数创建时,会保存所有父变量到这个属性中,[[scope]]为所有父变量对象的层级链,不代表全部完整的作用域链。

8. 闭包

第一:如何使用闭包;第二:什么是闭包;第三:闭包是什么时候被创建的;第四:什么时候被销毁的。

面试题

for(var i=0; i<5; i++) {// 从0-4
    setTimeout(function(){
        console.log(new Date, i);
    },1000);
}
console.log(new Date, i);

使用闭包让其输出5 -> 0,1,2,3,4

for (var i = 0; i < 5; i++) {
    (function(j) {  // j = i
        setTimeout(function() {
            console.log(new Date, j);
        }, 1000);
    })(i);
}

console.log(new Date, i);

优化:

var output = function(i) {
    setTimeout(function(){
        console.log(new Date, i);
    },1000);
};
// 变量赋值匿名函数
for(var i=0; i<5; i++){
    output(i); // 传递i值
}
console.log(new Date, i);

// 这段代码最后的i在运行时会报错
for(let i=0; i<5; i++) {
    setTimeout(function(){
        console.log(new Date, i);
    },1000);
}
console.log(new Date, i);

for(var i = 0; i<5; i++){
    function(j){
        setTimeout(function(){
            console.log(new Date, j);
        }, 1000 * j);
    })(i);
}

setTimeout(function(){
    console.log(new Date, i);
},1000 * i);

使用es6编写:

const tasks = []; // 存放异步操作promise
const output = (i) => new Promise((resolve) => {
    setTimeout(()=>{
        console.log(new Date, i);
        resolve();
    }, 1000 * i);
});
for(var i=0; i<5; i++){
    tasks.push(output(i));
}

Promise.all(tasks).then(()=>{
    setTimeout(()=>{
        console.log(new Data, i);
    },1000);
});

// async/await
const sleep = (timeountMS) => new Promise((resolve) => {
    setTimeout(resolve, timeountMS);
});

(async () => {  
    for (var i = 0; i < 5; i++) {
        if (i > 0) {
            await sleep(1000);
        }
        console.log(new Date, i);
    }

    await sleep(1000);
    console.log(new Date, i);
})();

window.setTimeout

setTimeout() 方法设置一个定时器,该定时器在定时器到期后执行一个函数或指定的一段代码。
var id = setTimeout(fn, delay) 启动单个计时器,该计时器将在延迟后调用指定的功能,返回一个唯一的id,以后可以使用该id取消计时器。
而 var id = setInterval(fn, delay) 类似于 setTimeout 但连续调用该函数,直到被取消。clearInterval(id), clearTimeout(id),接收计算器 id,并停止计算器回调。

不能保证计算器的延迟,由于浏览器中所有JavaScript都在单线程上执行,so,异步事件仅在执行中存在空缺时才运行。

由于JavaScript一次只能执行一段代码,因此这些代码块中的每一个都“阻塞”了其他异步事件的过程,当发生异步事件时,它将排队等待稍后执行。

setTimeout 和 setInterval:

setTimeout(function(){
    setTimeout(arguments.callee, 10);
},10);
setInterval(function(){
    
},10);
// setTimeout代码在上一次执行回调之后将始终至少有10ms的延迟,最终可能会更多,但是不会少,
// 而setInterval无论最后一次执行回调的时间如何,都会尝试每10ms执行

转载

Promise
Promise 对象用于表示一个异步操作的最终完成或失败,以及其结果值。
示例:

const promise1 = new Promise((resolve,reject)=>{
    setTimeout(()=>{
        resolve('foo');
    },200);
});

promise1.then((value)=>{
    console.log(value);
});
console.log(promise1);

Promise 对象是一个代理对象,被代理的值在 Promise 对象创建时可能是未知的。它允许你为异步操作的成功和失败分别绑定相应的处理方法。这让异步方法可以像同步方法那样返回值,但并不是立即返回最终执行结果,而是一个能代表未来出现的结果的 Promise 对象。
一个 Promise 有几种状态:

  • pending 初始状态,即不是成功,也不是失败状态;

  • fulfilled 表示操作成功完成;

  • rejected 表示操作失败。

    Promise.prototype.then 和 Promise.prototype.catch 方法返回 Promise 对象,所以它们可以被链式调用。

方法:
Promise.all(iterable),这个方法返回一个新的 Promise 对象,该 Promise 对象在 Iterable 参数对象里所有的 Promise对象都成功才会触发成功,一旦有任何一个 Iterable 里面的 Promise 对象失败则立即触发该 Promise 对象的失败。这个新的 Promise 对象在触发成功状态后,会把一个包含 Iterable 里所有 Promise 返回值得数组作为成功回调的返回值,顺序跟 Iterable 里第一个触发失败的 Promise 对象的错误信息作为它的失败错误信息。Promise.all 方法常用于处理多个 Promise 对象的状态集合。

Promise.race(iterable),当 Iterable 参数里的任意一个子 Promise 被成功会失败后,父 Promise 马上也会用子 Promise 的成功返回值或失败详情作为参数调用父 Promise 绑定的相应句柄,并返回该 Promise 对象。

Promise.prototype.catch(onRejected) 添加一个拒绝回调到当前 Promrise,返回一个新的 Promise。

Promise.prototype.finally(onFinally)添加一个事件处理回调于当前promise对象,并且在原promise对象解析完毕后,返回一个新的promise对象。
示例:

function myAsyncFunction(url) {
    return new Promise((resolve, reject) => {
        const xhr = new XMLHttpRequest();
        xhr.open('GET',url);
        xhr.onload = () => resolve(xhr.responseText);
        xhr.onerror = () => reject(xhr.statusText);
        xhr.send();
    }
}

async function

async function 用来定义一个返回 AsyncFunction 对象的异步 hash。
示例:

function resolveAfter2Seconds() {
  return new Promise(resolve => {
    setTimeout(() => {
      resolve('resolved');
    }, 2000);
  });
}

async function asyncCall() {
  console.log('calling');
  const result = await resolveAfter2Seconds();
  console.log(result);
  // expected output: 'resolved'
}

asyncCall();

//  "calling"
//  "resolved"

一个async异步函数可以包含await指令,该指令会暂停异步函数的执行,并等待 Promise 执行,然后继续执行异步函数,并返回结果。

await 关键字只在异步函数内有效。如果你在异步函数外使用它,会抛出语法错误。
9. 强大的闭包

示例:

"use strict";
var dada = (function outerFunction(){
    var da = 1;
    return {
        inc: function innerFunction(){
            return da++;
        }
    };
}());
dada.inc(); // 1
dada.inc(); // 2
dada.inc(); // 3

全局环境中运行的代码:

// my_script.js
"use strict";

var foo = 1;
var bar = 2;

没有被嵌套的函数:

"use strict";
var foo = 1;
var bar = 2;

function myFunc() {
  var a = 1;
  var b = 2;
  var foo = 3;
  console.log("inside myFunc");
}

console.log("outside");

myFunc();

当 myFunc 被执行的时候,对象之间的关系如下图所示

转载
闭包是同时含有对函数对象以及作用域对象引用的最想,实际上,所有 JavaScript 对象都是闭包。所以,当你定义一个函数的时候,你就定义了一个闭包。当闭包不被任何其他的对象引用时,会被销毁。

嵌套作用域:

(function dada(){
    let a = 1;
    function dada1() {
        console.log(a);
    }
    dada1();
})();
// dada1函数就是一个闭包
// 可以通过在一个函数内部或者{}块里面定义一个函数来创建闭包

内部函数可以访问外部函数:

(function autorun(da){
    let da1 = 1;
    setTimeout(function log(){
      console.log(da1);//1
      console.log(da);//6
    }, 10000);
})(6);

词法作用域是指内部函数在定义的时候就决定了其外部作用域,闭包的外部作用域是在其定义的时候就决定了。

示例:

(function dada(){
    let a = 1;
    function da(){
      console.log(a);
    };
    
    function run(fn){
      let a = 100;
      fn();
    }
    
    run(da);//1
})();

dada() 的函数作用域是 da() 函数的词法作用域。

外部作用域执行完毕后,内部函数还在(在其他地方被引用),闭包才真正发挥作用。

(function dadaqianduan(){
    let a = 1;
    setTimeout(function log(){
      console.log(a);
    }, 1000);
})();

(function dada(){
    let a = 1;
    $("#btn").on("click", function log(){
      console.log(a);
    });
})();

(function dada(){
    let ax = 1;
    fetch("http://").then(function log(){
      console.log(a);
    });
})();

闭包只存储外部变量的引用,而不会拷贝这些外部变量的值,注意,这玩意用多了内存泄漏了就不好了。

闭包可以引用函数外部变量,并且会沿着原型链向上查找,闭包引用的变量在闭包存在时不会被回收,函数的词法作用域在函数声明的时候已经决定了,所以闭包可引用的外部变量只能是父级。
在垃圾回收中,局部变量会随着函数的执行完毕而被销毁,除非还有指向他们的引用。当闭包本身被垃圾回收后,闭包中的私有状态随后也会被垃圾回收。

函数是一等公民

您是不是常常听到-“函数是一等公民”这样的描述,在编程中,一等公民可以作为函数参数,可以作为函数返回值,也可以赋值给变量。
例如,字符串在几乎所有编程语言中都是一等公民,字符串可以做为函数参数,可以作为函数返回值,也可以赋值给变量。

所以,函数在 JavaScript 中是一等公民。一等公民具有最高的优先权,当函数被看作是“一等公民”, 就是函数优先。

转载
转载

  • 函数可以存储到变量中
  • 函数可以存储为数组的一个元素
  • 函数可以作为对象的成员变量
  • 函数与数字一样可以在使用时直接创建出来
  • 函数可以被传递给另一个函数
  • 函数可以被另一个函数返回

转载

参考文献:
How do JavaScript closures work under the hood
Understanding Execution Context and Execution Stack in Javascript
How JavaScript Timers Work
**前端面试(80% 应聘者不及格系列):从闭包说起
JavaScript高级程序设计(第3版)
JavaScript权威指南

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章