重学前端-JavaScript(五)

290 阅读10分钟

这是我参与11月更文挑战的第29天,活动详情查看:2021最后一次更文挑战

重学前端-JavaScript(1)

重学前端-JavaScript(2)

重学前端-JavaScript(3)

重学前端-JavaScript(4)

重学前端-JavaScript(5)

重学前端-JavaScript(五)

1, 前言

当拿到一段 JavaScript 代码时,浏览器或者 Node 环境首先要做的就是;传递给 JavaScript 引擎,并且要求它去执行。

然而,执行 JavaScript 并非一锤子买卖,宿主环境当遇到一些事件时,会继续把一段代码传递给 JavaScript 引擎去执行,此外,我们可能还会提供 API 给 JavaScript 引擎,比如 setTimeout 这样的 API,它会允许 JavaScript 在特定的时机执行。

所以,我们首先应该形成一个感性的认知:一个 JavaScript 引擎会常驻于内存中,它等待着我们(宿主)把 JavaScript 代码或者函数传递给它执行。

我们把宿主发起的任务称为宏观任务,把 JavaScript 引擎发起的任务称为微观任务。 在上一节中我们已经分析过了宏观和微观任务,如果要更细粒度的分析,那就要说到:函数调用, 今天主要分析一下:

  • 闭包;
  • 作用域链;
  • 执行上下文;
  • this 值。

1638198106547.png

2, 闭包

​ 在计算机领域,它就有三个完全不相同的意义:编译原理中,它是处理语法产生式的一个步骤;计算几何中,它表示包裹平面点集的凸多边形(翻译作凸包);而在编程语言领域,它表示一种函数。

​ 我们可以这样简单理解一下,闭包其实只是一个绑定了执行环境的函数,闭包与普通函数的区别是,它携带了执行的环境,就像人在外星中需要自带吸氧的装备一样,这个函数也带有在程序中生存的环境。

  • 闭包包含两个部分

1, 环境部分:包括

环境:函数的词法环境(执行上下文的一部分)
标识符列表:函数中用到的未声明的变量

2,表达式部分: 包括

函数体

​ 我们可以认为,JavaScript 中的函数完全符合闭包的定义。它的环境部分是函数词法环境部分组成,它的标识符列表是函数中用到的未声明变量,它的表达式部分就是函数体。

  • 执行上下文:执行的基础设施

相比普通函数,JavaScript 函数的主要复杂性来自于它携带的“环境部分”

JavaScript 标准把一段代码(包括函数),执行所需的所有信息定义为:“执行上下文”。

3, 执行上下文在 ES3 中,包含三个部分。

#1, scope:作用域,也常常被叫做作用域链。
#2, variable object:变量对象,用于存储变量的对象。
#3, this value:this 值。

在 ES5 中,我们改进了命名方式,把执行上下文最初的三个部分改为下面这个样子。

#1,lexical environment:词法环境,当获取变量时使用。
#2,variable environment:变量环境,当声明变量时使用。
#3, this value:this 值。

在 ES2018 中,执行上下文又变成了这个样子,this 值被归入 lexical environment,但是增加了不少内容

1,lexical environment:词法环境,当获取变量或者 this 值时使用。v
2,ariable environment:变量环境,当声明变量时使用。
3,code evaluation state:用于恢复代码执行位置。
4, Function:执行的任务是函数时使用,表示正在被执行的函数。
5,ScriptOrModule:执行的任务是脚本或者模块时使用,表示正在被执行的代码。
6,Realm:使用的基础库和内置对象实例。
7, Generator:仅生成器上下文有这个属性,表示当前生成器。

如果是我们自己使用,我建议统一使用最新的 ES2018 中规定的术语定义。

比如,我们看以下的这段 JavaScript 代码:

var b = {}
let c = 1
this.a = 2;

1,通常我们认为它声明了 b,并且为它赋值为 1,var 声明作用域函数执行的作用域。也就是说,var 会穿透for 、if等语句。

2,立即执行的函数表达式(IIFE),通过创建一个函数,并且立即执行,来构造一个新的域,从而控制 var 的范围。

3 ,函数

1, 第一种,普通函数:用 function 关键字定义的函数。

function foo(){
    // code
}

2, 第二种,箭头函数:用 => 运算符定义的函数。

const foo = () => {
    // code
}

3,第三种,方法:在 class 中定义的函数。

class C {
    foo(){
        //code
    }
}

4,第四种,生成器函数:用 function * 定义的函数。

function* foo(){
    // code
}

5, 第五种,类:用 class 定义的类,实际上也是函数。

class Foo {
    constructor(){
        //code
    }
}

6, 异步函数:普通函数、箭头函数和生成器函数加上 async 关键字


async function foo(){
    // code
}
const foo = async () => {
    // code
}
async function foo*(){
    // code
}

对普通变量而言,这些函数并没有本质区别,都是遵循了“继承定义时环境”的规则,它们的一个行为差异在于 this 关键字。

this 关键字的行为

this 是 JavaScript 中的一个关键字,它的使用方法类似于一个变量(但是 this 跟变量的行为有很多不同,上一节课我们讲了一些普通变量的行为和机制,也就是 var 声明和赋值、let 的内容)。

this 是执行上下文中很重要的一个组成部分。同一个函数调用方式不同,得到的 this 值也不同,我们看一个例子


function showThis(){
    console.log(this);
}

var o = {
    showThis: showThis
}

showThis(); // global
o.showThis(); // o

注意:调用函数时使用的引用,决定了函数执行时刻的 this 值。

改成箭头函数


const showThis = () => {
    console.log(this);
}

var o = {
    showThis: showThis
}

showThis(); // global
o.showThis(); // global

注意:改为箭头函数后,不论用什么引用来调用它,都不影响它的 this 值。

改成方法:看看下面的代码


class C {
    showThis() {
        console.log(this);
    }
}
var o = new C();
var showThis = o.showThis;

showThis(); // undefined
o.showThis(); // o

不难验证出:生成器函数、异步生成器函数和异步普通函数跟普通函数行为是一致的,异步箭头函数与箭头函数行为是一致的。

this 关键字的机制

函数能够引用定义时的变量,如上文分析,函数也能记住定义时的 this,因此,函数内部必定有一个机制来保存这些信息。

在 JavaScript 标准中,为函数规定了用来保存定义时上下文的私有属性[[Environment]]。

当一个函数执行时,会创建一条新的执行环境记录,记录的外层词法环境(outer lexical environment)会被设置成函数的[[Environment]]。

JavaScript 用一个栈来管理执行上下文,这个栈中的每一项又包含一个链表。如下图所示:

1638199558096.png

当函数调用时,会入栈一个新的执行上下文,函数调用结束时,执行上下文被出栈。而 this 则是一个更为复杂的机制,JavaScript 标准定义了 [[thisMode]] 私有属性。[[thisMode]] 私有属性有三个取值。

1, lexical:表示从上下文中找 this,这对应了箭头函数。
2, global:表示当 thisundefined 时,取全局对象,对应了普通函数。
3, strict:当严格模式时使用,this 严格按照调用时传入的值,可能为 null 或者 undefined

操作 this 的内置函数

Function.prototype.call 和 Function.prototype.apply 可以指定函数调用时传入的 this 值,示例如下:


function foo(a, b, c){
    console.log(this);
    console.log(a, b, c);
}
foo.call({}, 1, 2, 3);
foo.apply({}, [1, 2, 3]);

这里 call 和 apply 作用是一样的,只是传参方式有区别。此外,还有 Function.prototype.bind 它可以生成一个绑定过的函数,这个函数的 this 值固定了参数:


function foo(a, b, c){
    console.log(this);
    console.log(a, b, c);
}
foo.bind({}, 1, 2, 3)();

4, new 与 this

  • 以构造器的 prototype 属性(注意与私有字段[[prototype]]的区分)为原型,创建新对象;
  • 将 this 和调用参数传给构造器,执行;
  • 如果构造器返回的是对象,则返回,否则返回第一步创建的对象。

显然,通过 new 调用函数,跟直接调用的 this 取值有明显区别,如下图

1638199805724.png

5, 语句

0, Completion类型

为了了解 JavaScript 语句有哪些特别之处,首先我们要看一个不太常见的例子,我会通过这个例子,来向你介绍 JavaScript 语句执行机制涉及的一种基础类型:Completion 类型。

这一机制的基础正是 JavaScript 语句执行的完成状态,我们用一个标准类型来表示:Completion Record(我在类型一节提到过,Completion Record 用于描述异常、跳出等语句执行过程)。

Completion Record 表示一个语句执行完之后的结果,它有三个字段:

  • [[type]] 表示完成的类型,有 break continue return throw 和 normal 几种类型;
  • [[value]] 表示语句的返回值,如果语句没有,则是 empty;
  • [[target]] 表示语句的目标,通常是一个 JavaScript 标签(标签在后文会有介绍)。

​ JavaScript 正是依靠语句的 Completion Record 类型,方才可以在语句的复杂嵌套结构中,实现各种控制。接下来我们要来了解一下 JavaScript 使用 Completion Record 类型,控制语句执行的过程。

首先我们来看看语句有几种分类。

1638199939152.png 1, 普通的语句

在 JavaScript 中,我们把不带控制能力的语句称为普通语句。普通语句有下面几种

#声明类语句
  1,var 声明
  2,const 声明
  3,let 声明
  4,函数声明
  5,类声明
#表达式语句
#空语
#句debugger 语句

这些语句在执行时,从前到后顺次执行(我们这里先忽略 var 和函数声明的预处理机制),没有任何分支或者重复执行逻辑。

普通语句执行后,会得到 [[type]] 为 normal 的 Completion Record,JavaScript 引擎遇到这样的 Completion Record,会继续执行下一条语句。

2, 语句块

语句块就是拿大括号括起来的一组语句,它是一种语句的复合结构,可以嵌套。

语句块本身并不复杂,我们需要注意的是语句块内部的语句的 Completion Record 的[[type]] 如果不为 normal,会打断语句块后续的语句执行。

3, 控制型语句

控制型语句带有 if、switch 关键字,它们会对不同类型的 Completion Record 产生反应。

控制类语句分成两部分,一类是对其内部造成影响,如 if、switch、while/for、try。

另一类是对外部造成影响如 break、continue、return、throw,这两类语句的配合,会产生控制代码执行顺序和执行逻辑的效果,这也是我们编程的主要工作。

一般来说, for/while - break/continue 和 try - throw 这样比较符合逻辑的组合,是大家比较熟悉的,但是,实际上,我们需要控制语句跟 break 、continue 、return 、throw 四种类型与控制语句两两组合产生的效果。

1638200263095.png

因为 finally 中的内容必须保证执行,所以 try/catch 执行完毕,即使得到的结果是非 normal 型的完成记录,也必须要执行 finally。

而当 finally 执行也得到了非 normal 记录,则会使 finally 中的记录作为整个 try 结构的结果。

4, 带标签的语句

实际上,任何 JavaScript 语句是可以加标签的,在语句前加冒号即可:

 firstStatement: var i = 1;

大部分时候,这个东西类似于注释,没有任何用处。唯一有作用的时候是:与完成记录类型中的 target 相配合,用于跳出多层循环。

 outer: while(true) {
      inner: while(true) {
          break outer;
      }
    }
    console.log("finished")

​ break/continue 语句如果后跟了关键字,会产生带 target 的完成记录。一旦完成记录带了 target,那么只有拥有对应 label 的循环语句会消费它。

6, 总结

JS基础底层的知识,阅读起来很犯困,在学习的过程中,跟着老师的思路进行理解,也能够探索出一二,很多知识都是跟着老师做的学习笔记,希望自己能够坚持下来,学完整个重学前端课程。