Skip to content

S05-02 JS高级-JS运行原理、JS内存管理、闭包、函数增强

[TOC]

JS 运行原理

V8 引擎原理

JS 代码的执行

JavaScript 代码下载好之后,是如何一步步被执行的呢?

我们知道,浏览器内核是由两部分组成的,以 webkit 为例:

  • WebCore:负责 HTML 解析、布局、渲染等等相关的工作;

  • JavaScriptCore:解析、执行 JavaScript 代码;

image-20230630174900800

另外一个强大的 JavaScript 引擎就是 V8 引擎。

V8 引擎-执行原理

我们来看一下官方对 V8 引擎的定义:

  • V8 是用 C ++编写的 Google 开源高性能 JavaScript 和 WebAssembly 引擎,它用于ChromeNode.js等。

  • 它实现ECMAScriptWebAssembly,并在 Windows 7 或更高版本,macOS 10.12+和使用 x64,IA-32,ARM 或 MIPS 处理器的 Linux 系统上运行。

  • V8 可以独立运行,也可以嵌入到任何 C ++应用程序中

总结: 高性能跨平台可独立运行

image-20230630175410491

V8 引擎-架构

V8 引擎本身的源码非常复杂,大概有超过100w 行 C++代码,通过了解它的架构,我们可以知道它是如何对 JavaScript 执行的:

Parse模块会将 JavaScript 代码转换成 AST(抽象语法树),这是因为解释器并不直接认识 JavaScript 代码;

Ignition是一个解释器,会将 AST 转换成 ByteCode(字节码)

  • 同时会收集 TurboFan 优化所需要的信息(比如函数参数的类型信息,有了类型才能进行真实的运算);

  • 如果函数只调用一次,Ignition 会解释执行 ByteCode;

  • Ignition 的 V8 官方文档:https://v8.dev/blog/ignition-interpreter

TurboFan是一个编译器,可以将字节码编译为 CPU 可以直接执行的机器码

  • 如果一个函数被多次调用,那么就会被标记为热点函数,那么就会经过 TurboFan转换成优化的机器码,提高代码的执行性能;

  • 但是,机器码实际上也会被还原为 ByteCode,这是因为如果后续执行函数的过程中,类型发生了变化(比如 sum 函数原来执行的是 number 类型,后来执行变成了 string 类型),之前优化的机器码并不能正确的处理运算,就会逆向的转换成字节码;

  • TurboFan 的 V8 官方文档:https://v8.dev/blog/turbofan-jit

V8 引擎-解析图(官方)

词法分析(英文 lexical analysis)

  • 将字符序列转换成 token 序列的过程。
  • scanner: 词法分析器(lexical analyzer,简称 lexer),也叫扫描器(scanner)
  • token: 是记号化(tokenization)的缩写

语法分析(英语:syntactic analysis,也叫 parsing)

  • parser: 语法分析器也可以称之为 。

image-20230701135913420

V8 引擎-解析图

image-20230630182031298

JS 执行上下文

ECMA 版本说明

在 ECMA 早期的版本中(ECMAScript3),代码的执行流程的术语和 ECMAScript5 以及之后的术语会有所区别:

  • 目前网上大多数流行的说法都是基于ECMAScript3版本的解析,并且在面试时问到的大多数都是 ECMAScript3 的版本内容。

  • 但是 ECMAScript3 终将过去, ECMAScript5必然会成为主流,所以最好也理解 ECMAScript5 甚至包括 ECMAScript6 以及更好版本的内容;

  • 事实上在TC39的最新描述中,和 ECMAScript5 之后的版本又出现了一定的差异;

那么我们课程按照如下顺序学习:

  • 通过ECMAScript3中的概念学习JavaScript 执行原理作用域作用域链闭包等概念;

  • 通过ECMAScript5中的概念学习块级作用域letconst等概念;

事实上,它们只是在对某些概念上的描述不太一样,在整体思路上都是一致的。

JS 执行原理

假如我们有下面一段代码,它在 JavaScript 中是如何被执行的呢?

image-20230701150710559

JS 执行流程

1、初始化全局对象 GO

2、事先存在一个执行上下文栈 ECS

3、执行全局代码:

  • 在 ECS 中创建一个全局执行上下文 GEC

  • 在 GEC 中创建 VO 对象,让它关联到 GO 对象

  • 变量的作用域提升:在转成 AST 树时,会将变量、函数加入到 GO 中,但不赋值

4、执行函数代码:

  • 在 ECS 中创建一个函数执行上下文 FEC
  • 在 FEC 中创建 VO 对象,让它关联到 AO 对象
  • 变量的作用域提升:在转成 AST 树时,会将变量、函数加入到 AO 中,但不赋值

JS 执行-初始化全局对象 GO

JS 引擎会在执行代码之前,在堆内存中创建一个全局对象 GO(Global Object)

GO 对象:

  • 该对象 所有的作用域(scope)都可以访问;

  • 里面会包含 Date、Array、String、Number、setTimeout、setInterval 等等;

  • 其中还有一个 window 属性指向自己;

image-20230701154627478

image-20230701160512239

JS 执行-执行上下文

JS 引擎内部有一个执行上下文栈 ECS(Execution Context Stack),它是用于执行代码的调用栈。

那么现在它要执行谁呢?执行的是全局的代码块:

  • 全局的代码块为了执行会构建一个 全局执行上下文 GEC(Global Execution Context)

  • GEC 会 被放入到 ECS 中 执行;

GEC 被放入到 ECS 中里面包含两部分内容:

  • **第一部分:**在代码执行前,在 parser 转成 AST 的过程中,会将全局定义的变量、函数等加入到 GlobalObject 中,但是并不会赋值;

    • 这个过程也称之为变量的作用域提升(hoisting)
  • **第二部分:**在代码执行中,对变量赋值,或者执行其他的函数;

image-20230701162227446

JS 执行-认识 VO 对象

每一个执行上下文会关联一个变量对象 VO(Variable Object),变量和函数声明会被添加到这个 VO 对象中。

image-20230701163412592

当全局代码被执行的时候,VO 就是 GO 对象了

image-20230701163907011

image-20230701164235848

全局代码执行过程

全局代码执行过程(执行前)

image-20230701164935556

全局代码执行过程(执行后)

image-20230701164949110

函数代码执行过程

函数如何被执行呢?

在执行的过程中执行到一个函数时,就会根据函数体创建一个函数执行上下文 FEC(Functional Execution Context),并且压入到 EC Stack 中。

因为每个执行上下文都会关联一个 VO,那么函数执行上下文关联的 VO 是什么呢?

  • 当进入一个函数执行上下文时,会创建一个AO 对象(Activation Object)

  • 这个 AO 对象会使用arguments作为初始化,并且初始值是传入的参数;

  • 这个 AO 对象会作为执行上下文的 VO 来存放变量的初始化;

image-20230701165001387

函数的执行过程(执行前)

image-20230701165033793

函数的执行过程(执行后)

image-20230701165021947

函数的多次执行

image-20230701180237508

函数代码相互调用

image-20230701182247973

image-20230701182257939

作用域和作用域链

全局变量的查找

image-20230705153222758

image-20230705153532860

函数代码变量的查找

1、函数中有定义自己的 message

image-20230705154212217

image-20230705154308436

2、函数中没有自己的 message

image-20230705154342637

作用域和作用域链

当进入到一个执行上下文时,执行上下文也会关联一个作用域链(Scope Chain)

  • 作用域链是一个对象列表,用于变量标识符的求值;

  • 当进入一个执行上下文时,这个作用域链被创建,并且根据代码类型,添加一系列的对象;

  • 函数的作用域链和函数的定义位置有关,与调用位置无关

image-20230701165050553

image-20230701165119807

image-20230705155729228

多层嵌套函数的作用域链

image-20230705161706522

image-20230705161603001

作用域提升面试题

image-20230701165134164

JS 内存管理

JS 内存管理

认识内存管理

不管什么样的编程语言,在代码的执行过程中都是需要给它分配内存的,不同的是某些编程语言需要我们自己手动的管理内存,某些编程语言会可以自动帮助我们管理内存:

不管以什么样的方式来管理内存,内存的管理都会有如下的生命周期

  • 第一步:分配申请你需要的内存(申请);

  • 第二步:使用分配的内存(存放一些东西,比如对象等);

  • 第三步:不需要使用时,对其进行释放

不同的编程语言对于第一步和第三步会有不同的实现:

  • 手动管理内存:比如 C、C++,包括早期的 OC,都是需要手动来管理内存的申请和释放的(malloc 和 free 函数);

  • 自动管理内存:比如 Java、JavaScript、Python、Swift、Dart 等,它们有自动帮助我们管理内存;

对于开发者来说,JavaScript 的内存管理是自动的、无形的。

  • 我们创建的原始值、对象、函数……这一切都会占用内存;

  • 但是我们并不需要手动来对它们进行管理,JavaScript 引擎会帮助我们处理好它;

JS 的内存管理

JavaScript 会在定义数据时为我们分配内存。

但是内存分配方式是一样的吗?

  • JS 对于原始数据类型内存的分配会在执行时,直接在栈空间进行分配;

  • JS 对于复杂数据类型内存的分配会在堆内存中开辟一块空间,并且将这块空间的指针返回值变量引用;

image-20230620103144430

垃圾回收机制算法

JS 的垃圾回收

因为内存的大小是有限的,所以当内存不再需要的时候,我们需要对其进行释放,以便腾出更多的内存空间。

手动管理内存的语言中,我们需要通过一些方式自己来释放不再需要的内存,比如 free 函数:

  • 但是这种管理的方式其实非常的低效,影响我们编写逻辑的代码的效率;

  • 并且这种方式对开发者的要求也很高,并且一不小心就会产生内存泄露

所以大部分现代的编程语言都是有自己的垃圾回收机制

  • 垃圾回收的英文是Garbage Collection,简称GC

  • 对于那些不再使用的对象,我们都称之为是垃圾,它需要被回收,以释放更多的内存空间;

  • 而我们的语言运行环境,比如 Java 的运行环境 JVM,JavaScript 的运行环境 js 引擎都会内存垃圾回收器;

  • 垃圾回收器我们也会简称为GC,所以在很多地方你看到 GC 其实指的是垃圾回收器;

但是这里又出现了另外一个很关键的问题:GC 怎么知道哪些对象是不再使用的呢?

  • 这里就要用到 GC 的实现以及对应的算法;

常见 GC 算法-引用计数

引用计数(Reference counting)

  • 当一个对象有一个引用指向它时,那么这个对象的引用就+1;

  • 当一个对象的引用为 0 时,这个对象就可以被销毁掉;

这个算法有一个很大的弊端就是会产生循环引用

image-20230620103215295

obj1=nullobj2=null时,依然会有obj1.info指向当前对象,引用计数为 1,所以无法销毁

必须通过obj1.info=null 才能取消引用

常见 GC 算法-标记清除

标记清除(mark-Sweep)

  • 标记清除的核心思路是可达性(Reachability)

  • 这个算法是设置一个根对象(root object),垃圾回收器会定期从这个根开始,找所有从根开始有引用到的对象,对于那些没有引用到的对象,就认为是不可用的对象;

  • 这个算法可以很好的解决循环引用的问题;

  • V8 使用的是该算法

image-20230620103229596

常见 GC 算法-其他算法优化补充

JS 引擎比较广泛的采用的就是可达性中的标记清除算法,当然类似于 V8 引擎为了进行更好的优化,它在算法的实现细节上也会结合一些其他的算法。

标记整理(Mark-Compact) 和“标记-清除”相似;

  • 不同的是,回收期间同时会将保留的存储对象搬运汇集到连续的内存空间,从而整合空闲空间,避免内存碎片化;

分代收集(Generational collection)——对象被分成两组:“新的”和“旧的”。

  • 许多对象出现,完成它们的工作并很快死去,它们可以很快被清理;

  • 那些长期存活的对象会变得“老旧”,而且被检查的频次也会减少;

增量收集(Incremental collection)

  • 如果有许多对象,并且我们试图一次遍历并标记整个对象集,则可能需要一些时间,并在执行过程中带来明显的延迟。

  • 所以引擎试图将垃圾收集工作分成几部分来做,然后将这几部分会逐一进行处理,这样会有许多微小的延迟而不是一个大的延迟;

闲时收集(Idle-time collection)

  • 垃圾收集器只会在 CPU 空闲时尝试运行,以减少可能对代码执行的影响。

V8 引擎详细的内存图

事实上,V8 引擎为了提供内存的管理效率,对内存进行非常详细的划分:

image-20230620103313820

闭包

闭包-概念

又爱又恨的闭包

闭包是 JavaScript 中一个非常容易让人迷惑的知识点:

  • 有同学在深入 JS 高级的交流群中发了这么一张图片;

  • 并且闭包也是群里面大家讨论最多的一个话题;

image-20230620103330161

闭包确实是 JavaScript 中一个很难理解的知识点,接下来我们就对其一步步来进行剖析,看看它到底有什么神奇之处。

JavaScript 的函数式编程

在前面我们说过,JavaScript 是支持函数式编程的

在 JavaScript 中,函数是非常重要的,并且是一等公民:

  • 那么就意味着函数的使用是非常灵活的;

  • 函数可以作为另外一个函数的参数,也可以作为另外一个函数的返回值来使用;

所以 JavaScript 存在很多的高阶函数:

  • 自己编写高阶函数

  • 使用内置的高阶函数

目前在 vue3 和 react 开发中,也都在趋向于函数式编程:

  • vue3 composition api: setup 函数 -> 代码(函数 hook,定义函数);

  • react:class -> function -> hooks

闭包的定义

这里先来看一下闭包的定义,分成两个:在计算机科学中和在 JavaScript 中。

计算机科学中对闭包的定义(维基百科):

  • 闭包(Closure),又称词法闭包(Lexical Closure)函数闭包(function closures)

  • 是在支持头等函数的编程语言中,实现词法绑定的一种技术;

  • 闭包在实现上是一个结构体,它存储了一个函数和一个关联的环境(相当于一个符号查找表);

  • 闭包跟函数最大的区别在于,当捕捉闭包的时候,它的自由变量会在捕捉时被确定,这样即使脱离了捕捉时的上下文,它也能照常运行;

闭包的概念出现于 60 年代,最早实现闭包的程序是 Scheme,那么我们就可以理解为什么 JavaScript 中有闭包:

  • 因为 JavaScript 中有大量的设计是来源于 Scheme 的;

我们再来看一下MDN对 JavaScript 闭包的解释

  • 一个函数和对其周围状态(lexical environment,词法环境)的引用捆绑在一起(或者说函数被引用包围),这样的组合就是闭包(closure)

  • 也就是说,闭包让你可以在一个内层函数中访问到其外层函数的作用域

  • 在 JavaScript 中,每当创建一个函数,闭包就会在函数创建的同时被创建出来;

那么我的理解和总结

  • 一个普通的函数function,如果它可以访问外层作用域的自由变量,那么这个函数和周围环境就是一个闭包

  • 从广义的角度来说:JavaScript 中的函数都是闭包;

  • 从狭义的角度来说:JavaScript 中一个函数,如果访问了外层作用域的变量,那么它是一个闭包;

闭包-形成过程

闭包的访问过程

如果我们编写了如下的代码,它一定是形成了闭包的:

image-20230620103407835

image-20230620103420069

闭包的执行过程

那么函数继续执行呢?

  • 这个时候 makeAdder 函数执行完毕,正常情况下我们的 AO 对象会被释放;

  • 但是因为在 0xb00 的函数中有作用域引用指向了这个 AO 对象,所以它不会被释放掉;

image-20230620103435420

闭包-内存泄露

闭包的内存泄漏

那么我们为什么经常会说闭包是有内存泄露的呢?

  • 在上面的案例中,如果后续我们不再使用 add10 函数了,那么该函数对象应该要被销毁掉,并且其引用着的父作用域 AO 也应该被销毁掉;

  • 但是目前因为在全局作用域下 add10 变量对 0xb00 的函数对象有引用,而 0xb00 的作用域中 AO(0x200)有引用,所以最终会造成这些内存都是无法被释放的;

  • 所以我们经常说的闭包会造成内存泄露,其实就是刚才的引用链中的所有对象都是无法释放的;

那么,怎么解决这个问题呢?

  • 因为当手动将 add10 设置为 null时,就不再对函数对象 0xb00 有引用,那么对应的 AO 对象 0x200 也就不可达了;

  • 在 GC 的下一次检测中,它们就会被销毁掉;

image-20230620103450493

闭包的内存泄漏测试

image-20230620103508731

image-20230620103517953

AO 不使用的属性优化

我们来研究一个问题:AO 对象不会被销毁时,是否里面的所有属性都不会被释放?

  • 下面这段代码中 name 属于闭包的父作用域里面的变量;

  • 我们知道形成闭包之后 count 一定不会被销毁掉,那么 name 是否会被销毁掉呢?

  • 这里我打上了断点,我们可以在浏览器上看看结果;

image-20230620103528569

image-20230620103539085

函数增强

函数属性、arguments

函数对象的属性

我们知道 JavaScript 中函数也是一个对象,那么对象中就可以有属性和方法。

1、自定义函数属性

image-20230706163143994

2、函数内置属性

  • name:一个函数的名词我们可以通过 name 来访问;

image-20230620104054880

  • length:属性 length 用于返回函数形参的个数;

注意: rest 参数有默认值的参数是不参与参数的个数的;

image-20230620104102766

认识 arguments

arguments是一个对应于传递给函数的参数的类数组(array-like)对象

image-20230620104112227

array-like 意味着它不是一个数组类型,而是一个对象类型:

  • 但是它却拥有数组的一些特性,比如说 length,比如可以通过 index 索引来访问;

  • 但是它却没有数组的一些方法,比如 filter、map 等;

image-20230620104120469

arguments 转 Array

在开发中,我们经常需要将 arguments 转成 Array,以便使用数组的一些特性。

常见的转化方式如下:

*转化方式一:*遍历 arguments,添加到一个新数组中;

image-20230706165113737

转化方式二: 调用数组 slice 函数的 call 方法;较难理解(有点绕),了解即可

image-20230706165556164

*转化方式三:*ES6 中的两个方法

  • Array.from(arguments)

  • […arguments]

image-20230706165542818

箭头函数不绑定 arguments

箭头函数是不绑定 arguments 的,所以我们在箭头函数中使用 arguments 会去上层作用域查找:

1、箭头函数不绑定 arguments

image-20230620104156172

2、在箭头函数中使用 arguments 会去上层作用域查找

image-20230620104202683

函数的剩余(rest)参数

ES6 中引用了剩余参数(rest parameter),可以将不定数量的参数放入到一个数组中

  • 如果最后一个参数是 ... 为前缀的,那么它会将剩余的参数放到该参数中,并且作为一个数组;

image-20230620104213651

那么剩余参数arguments有什么区别呢?

  • 剩余参数只包含那些没有对应形参的实参,而 arguments 对象包含了传给函数的所有实参

  • arguments对象不是一个真正的数组,而rest 参数是一个真正的数组,可以进行数组的所有操作;

  • arguments 是早期的 ECMAScript 中为了方便去获取所有的参数提供的一个数据结构,而 rest 参数是 ES6 中提供并且希望以此来替代 arguments 的;

剩余参数必须放到最后一个位置,否则会报错。

纯函数

理解 JavaScript 纯函数

函数式编程中有一个非常重要的概念叫纯函数(Pure Function),JavaScript 符合函数式编程的范式,所以也有纯函数的概念;

  • react开发中纯函数是被多次提及的;

  • 比如react 中组件就被要求像是一个纯函数(为什么是像,因为还有 class 组件),redux 中有一个 reducer 的概念,也是要求必须是一个纯函数;

  • 所以掌握纯函数对于理解很多框架的设计是非常有帮助的;

纯函数的维基百科定义:

  • 在程序设计中,若一个函数符合以下条件,那么这个函数被称为纯函数:

  • 此函数在相同的输入值时,需产生相同的输出

  • 函数的输出和输入值以外的其他隐藏信息或状态无关,也和由 I/O 设备产生的外部输出无关。

  • 该函数不能有语义上可观察的函数副作用,诸如“触发事件”,使输出设备输出,或更改输出值以外物件的内容等。

当然上面的定义会过于的晦涩,所以我简单总结一下:

  • 确定的输入,一定会产生确定的输出

  • 函数在执行过程中,不能产生副作用

副作用概念的理解

那么这里又有一个概念,叫做副作用,什么又是副作用呢?

  • *副作用(side effect)*其实本身是医学的一个概念,比如我们经常说吃什么药本来是为了治病,可能会产生一些其他的副作用;

  • 在计算机科学中,也引用了副作用的概念,表示在执行一个函数时,除了返回函数值之外,还对调用函数产生了附加的影响,比如修改了全局变量修改参数或者改变外部的存储

纯函数在执行的过程中就是不能产生这样的副作用:

  • 副作用往往是产生 bug 的 “温床”

image-20230706174015236

纯函数的案例

我们来看一个对数组操作的两个函数:

  • slice:slice 截取数组时不会对原数组进行任何操作,而是生成一个新的数组;

  • splice:splice 截取数组, 会返回一个新的数组, 也会对原数组进行修改;

slice 就是一个纯函数,不会修改数组本身,而 splice 函数不是一个纯函数;

image-20230620104245290

判断下面函数是否是纯函数?

image-20230620104258310

image-20230620104303820

image-20230620104311203

纯函数的作用和优势

作用:

为什么纯函数在函数式编程中非常重要呢?

  • 因为你可以安心编写和安心的使用

  • 你在写的时候保证了函数的纯度,只是单纯实现自己的业务逻辑即可,不需要关心传入的内容是如何获得的或者依赖其他的外部变量是否已经发生了修改;

  • 你在用的时候,你确定你的输入内容不会被任意篡改,并且自己确定的输入,一定会有确定的输出;

React中就要求我们无论是函数还是 class 声明一个组件,这个组件都必须像纯函数一样保护它们的 props 不被修改

image-20230620104325225

柯里化

柯里化概念的理解

柯里化(Currying)也是属于函数式编程里面一个非常重要的概念。

  • 是一种关于函数的高阶技术;

  • 它不仅被用于 JavaScript,还被用于其他编程语言;

我们先来看一下维基百科的解释:

  • 在计算机科学中,柯里化,又译为卡瑞化或加里化;

  • 把接收多个参数的函数,变成接受一个单一参数(最初函数的第一个参数)的函数,并且返回接受余下的参数,而且返回结果的新函数的技术;

  • 柯里化声称 “如果你固定某些参数,你将得到接受余下参数的一个函数”;

维基百科的解释非常的抽象,我们这里做一个总结

  • 只传递给函数一部分参数来调用它,让它返回一个函数去处理剩余的参数,这个过程就称之为柯里化;

柯里化是一种函数的转换,将一个函数从可调用的 f(a, b, c) 转换为可调用的 f(a)(b)(c)

  • 柯里化不会调用函数。它只是对函数进行转换。

柯里化的代码转换

那么柯里化到底是怎么样的表现呢?

1、普通函数转柯里化函数

image-20230620104408652

2、柯里化函数的箭头函数写法

image-20230620104415502

柯里化优势一-函数的职责单一

那么为什么需要有柯里化呢?

  • 在函数式编程中,我们其实往往希望一个函数处理的问题尽可能的单一,而不是将一大堆的处理过程交给一个函数来处理;

  • 那么我们是否就可以将每次传入的参数在单一的函数中进行处理,处理完后在下一个函数中再使用处理后的结果;

比如上面的案例我们进行一个修改:传入的函数需要分别被进行如下处理

  • 第一个参数 + 2

  • 第二个参数 * 2

  • 第三个参数 ** 2

image-20230620104436458

柯里化优势二-函数的参数复用

另外一个使用柯里化的场景是可以帮助我们可以复用参数逻辑

  • makeAdder 函数要求我们传入一个 num(并且如果我们需要的话,可以在这里对 num 进行一些修改);

  • 在之后使用返回的函数时,我们不需要再继续传入 num 了;

image-20230620104447419

柯里化案例练习

这里我们在演示一个案例,需求是打印一些日志:

  • 日志包括时间、类型、信息;

普通函数的实现方案如下:

image-20230620104500528

image-20230620104512380

柯里化高级 - 手写自动柯里化函数▸

目前我们有将多个普通的函数,转成柯里化函数:

image-20230708115746002

组合函数

组合函数概念的理解

*组合函数(Compose Function)*是在 JavaScript 开发过程中一种对函数的使用技巧、模式:

  • 比如我们现在需要对某一个数据进行函数的调用,执行两个函数 fn1 和 fn2,这两个函数是依次执行的;

  • 那么如果每次我们都需要进行两个函数的调用,操作上就会显得重复;

  • 那么是否可以将这两个函数组合起来,自动依次调用呢?

  • 这个过程就是对函数的组合,我们称之为组合函数;

image-20230620104535226

image-20230620104543121

手写组合函数▸

刚才我们实现的 compose 函数比较简单

我们需要考虑更加复杂的情况:比如传入了更多的函数,在调用 compose 函数时,传入了更多的参数:

image-20230620104559724

with、eval

with 语句的使用

with语句扩展一个语句的作用域链

image-20230620104610973

不建议使用 with 语句,因为它可能是混淆错误和兼容性问题的根源。

eval 函数

内建函数 eval 允许执行一个代码字符串

  • eval 是一个特殊的函数,它可以将传入的字符串当做 JavaScript 代码来运行

  • eval 会将最后一句执行语句的结果,作为返回值

image-20230620104621473

不建议在开发中使用 eval

  • eval 代码的可读性非常的差(代码的可读性是高质量代码的重要原则);

  • eval 是一个字符串,那么有可能在执行的过程中被刻意篡改,那么可能会造成被攻击的风险;

  • eval 的执行必须经过 JavaScript 解释器,不能被 JavaScript 引擎优化

严格模式

认识严格模式

JavaScript 历史的局限性:

  • 长久以来,JavaScript 不断向前发展且并未带来任何兼容性问题;

  • 新的特性被加入,旧的功能也没有改变,这么做有利于兼容旧代码;

  • 但缺点是 JavaScript 创造者的任何错误或不完善的决定也将永远被保留在 JavaScript 语言中

在 ECMAScript5 标准中,JavaScript 提出了*严格模式(Strict Mode)*的概念:

  • 严格模式很好理解,是一种具有限制性的 JavaScript 模式,从而使代码隐式的脱离了 ”懒散(sloppy)模式“;

  • 支持严格模式的浏览器在检测到代码中有严格模式时,会以更加严格的方式对代码进行检测和执行;

严格模式对正常的 JavaScript 语义进行了一些限制

  • 严格模式通过 抛出错误 来消除一些原有的静默(silent)错误

  • 严格模式让JS 引擎执行代码时可以进行更多的优化(不需要对一些特殊的语法进行处理);

  • 严格模式禁用了在ECMAScript 未来版本中可能会定义的一些语法

开启严格模式

那么如何开启严格模式呢?严格模式支持粒度话迁移

  • 可以支持在js 文件中开启严格模式;

  • 也支持对某一个函数开启严格模式;

image-20230708160252353

严格模式通过在文件或者函数开头使用 use strict 来开启。

image-20230620104646340

image-20230620104653655

注意:

  • 没有类似于 "no use strict" 这样的指令可以使程序返回默认模式。

  • 现代 JavaScript 支持 “class” 和 “module” ,它们会自动启用 use strict

严格模式限制

JavaScript 被设计为新手开发者更容易上手,所以有时候本来错误语法,被认为也是可以正常被解析的;但是这种方式可能给带来留下来安全隐患;在严格模式下,这种失误就会被当做错误,以便可以快速的发现和修正;

这里我们来说几个严格模式下的严格语法限制

1、无法意外的创建全局变量

image-20230708160725518

2、严格模式会使引起静默失败(silently fail,注:不报错也没有任何效果)的赋值操作抛出异常

image-20230708161133304

3、严格模式下试图删除不可删除的属性

image-20230708161320154

4、严格模式不允许函数参数有相同的名称

image-20230708161411338

5、不允许 0 的八进制语法,要使用 0o

image-20230708161516401

6、在严格模式下,不允许使用 with

7、在严格模式下,eval 不能为上层引用(创建)变量

image-20230708161953968

8、严格模式下,this 绑定不会默认转成对象,也不会绑定 window,而是 undefined

image-20230708162104689

手写 apply、call、bind 函数实现(原型后)

接下来我们来实现一下 apply、call、bind 函数:

  • 注意:我们的实现是练习函数、this、调用关系,不会过度考虑一些边界情况

image-20230620104713267

image-20230620104725055