青训营第⑧课(深入理解JS)

88 阅读11分钟

深入理解JS

JS的基本概念

构成:

  • 借鉴C语言的基本语法
  • 借鉴Java语言的数据类型和内存管理
  • 借鉴Scheme语言,将函数提升到“第一等公民”(frist class)的地位
  • 借鉴Self语言,使用基于原型(prototype)的继承机制

特点:

  • 单线程
  • 动态、弱类型

动态语言(弱类型语言): 是运行时才确定数据类型的语言,变量在使用之前无需申明类型。

 const company="qzfrato";

静态语言(强类型语言): 是编译时变量的数据类型就需要确定的语言

 String company = "qzfrato";
  • 面向对象、函数式

    参数->结果 副作用、纯函数 jQuery、Ramda

    原型、继承、封装

  • 解释类语言、JIT

  • 安全、性能差

  • ……

数据类型:

image-20230509124500342.png

作用域:

变量的可访问性和可见性

静态作用域,通过它就能够预测代码在执行过程中如何查找标识符

image-20230509125752071.png

变量提升:

image-20230509131014308.png

JS执行:

JavaScript是一种解释性语言,它在运行时被解释器逐行执行。当浏览器加载一个包含JS代码的页面时,它会创建一个解释器,然后按照代码的顺序逐行执行。

JavaScript代码的执行过程大致分为以下几个步骤:

  1. 语法解析:解释器会首先对代码进行语法解析,检查代码是否符合语法规范,并生成相应的数据结构。
  2. 预编译:解释器会对代码进行预编译,包括变量声明、函数声明等操作。在预编译阶段,解释器会将变量和函数的声明提升到作用域的顶部,这也被称为“提升”。
  3. 执行:解释器会按照代码的顺序逐行执行,执行过程中会根据需要进行变量赋值、函数调用等操作。
  4. 垃圾回收:解释器会定期进行垃圾回收,清除不再使用的内存空间,以避免内存泄漏等问题。

当 JS 引擎解析到可执行代码片段(通常是函数调用)的时候,就会先做一些执行前的准备工作,这个准备工作,就叫做“执行上下文”,也叫执行环境。

语法环境: 基于ECMAScript 代码的词法嵌套结构来定义 标识符和具体变量和函数的关联。一个词法环境由环境记录器和一个可能的引用外部词法环境的空值组成。

变量环境: 变量环境和词法环境的一个不同就是前者被用来存储函数声明和变量(let 和 const)绑定,而后者只用来存储var变量绑定。

Outer: 指向外部变量环境的一个指针。

JS的进阶概念

闭包:

闭包是指在一个函数内部定义的函数可以访问该函数的变量和参数,即使该函数已经返回了。换句话说,闭包可以访问在其外部定义的变量和函数,这些变量和函数被保存在内存中,不会被垃圾回收机制清除,因此可以在函数外部继续使用。

闭包的实现方式是将内部函数作为外部函数的返回值返回,这样就可以在外部函数调用后继续访问内部函数及其所引用的变量。

简单的闭包示例:

 function outerFunction() {
   var outerVariable = "Hello, ";
 ​
   function innerFunction(name) {
     console.log(outerVariable + name);
   }
 ​
   return innerFunction;
 }
 ​
 var innerFunc = outerFunction();
 innerFunc("world"); // 输出 "Hello, world"

在这个示例中,innerFunction 是在 outerFunction 内部定义的函数,它可以访问 outerFunction 中定义的变量 outerVariable。当 outerFunction 被调用时,它返回 innerFunction,这样就可以在外部调用 innerFunction 并传递参数 "world",从而输出 "Hello, world"

闭包可以用于许多场景,例如模块化开发、事件处理等。但需要注意的是,由于闭包会占用内存,如果使用不当会导致内存泄漏等问题。因此,在使用闭包时需要注意内存管理问题。

this:

在 JavaScript 中,this 关键字用于指向当前执行代码的对象。具体来说,this 的值取决于函数的调用方式,有以下四种情况:

  1. 作为函数调用:当函数作为普通函数调用时,this 指向全局对象(在浏览器中通常是 window 对象)。
 function foo() {
   console.log(this);
 }
 ​
 foo(); // 输出 window 对象
  1. 作为方法调用:当函数作为对象的方法调用时,this 指向该对象。
 var obj = {
   name: "John",
   sayName: function() {
     console.log(this.name);
   }
 };
 ​
 obj.sayName(); // 输出 "John"
  1. 作为构造函数调用:当函数作为构造函数调用时,this 指向新创建的对象。
 function Person(name) {
   this.name = name;
 }
 ​
 var john = new Person("John");
 console.log(john.name); // 输出 "John"
  1. 使用 callapply 方法调用:通过 callapply 方法调用函数时,可以指定 this 的值。
 function greet() {
   console.log("Hello, " + this.name);
 }
 ​
 var person = { name: "John" };
 greet.call(person); // 输出 "Hello, John"

需要注意的是,当函数被嵌套调用时,this 的值会发生改变。在这种情况下,可以使用 bind 方法来绑定 this 的值。

 var obj = {
   name: "John",
   greet: function() {
     console.log("Hello, " + this.name);
   },
   nested: {
     name: "Mary",
     greet: function() {
       console.log("Hello, " + this.name);
     }
   }
 };
 ​
 obj.greet(); // 输出 "Hello, John"
 obj.nested.greet(); // 输出 "Hello, Mary"
 ​
 var greetMary = obj.nested.greet.bind(obj);
 greetMary(); // 输出 "Hello, John"

在这个例子中,bind 方法可以将 obj 绑定到 nested.greet 方法中,使其 this 指向 obj,从而输出 "Hello, John"。

垃圾回收:

image-20230511205300971.png

JavaScript 的垃圾回收是自动进行的,开发者不需要手动释放内存。当一个对象不再被引用时,垃圾回收器会自动将其标记为可回收对象,并在适当的时候进行回收。

垃圾回收器通过标记-清除算法来回收内存。具体来说,垃圾回收器会在堆内存中标记所有被引用的对象,然后清除所有未被标记的对象。这个过程可以分为以下几个阶段:

  1. 标记阶段:垃圾回收器遍历堆内存中的所有对象,并标记所有被引用的对象。
  2. 清除阶段:垃圾回收器清除所有未被标记的对象,并回收它们占用的内存。

需要注意的是,垃圾回收器并不会立即回收内存,而是等待一段时间后才进行回收。这是因为垃圾回收器需要等待所有正在运行的 JavaScript 代码执行完毕,才能准确地判断哪些对象可以被回收。因此,在编写 JavaScript 代码时,需要注意避免创建过多的全局变量和闭包,以减少内存占用。

另外,JavaScript 中还有一种常见的内存泄漏情况,即当一个对象不再需要时,仍然被其他对象引用,导致垃圾回收器无法回收它。这种情况可以通过手动解除引用来避免,例如将对象的引用设置为 null

事件循环:

JavaScript 的事件循环是一种机制,用于处理异步操作。它的基本原理是,将所有的异步任务放入一个任务队列中,然后按照顺序逐个执行。当任务队列为空时,事件循环会不断地轮询任务队列,直到有新的任务加入。

事件循环的过程可以分为以下几个步骤:

  1. 执行同步任务:JavaScript 引擎会先执行当前线程的同步任务,直到执行完毕。
  2. 处理异步任务:当遇到异步任务时,JavaScript 引擎会将其放入任务队列中,然后继续执行同步任务。
  3. 轮询任务队列:当同步任务执行完毕后,JavaScript 引擎会不断地轮询任务队列,查看是否有新的任务加入。
  4. 执行异步任务:如果任务队列中有任务,JavaScript 引擎会按照顺序逐个执行,直到执行完毕或者遇到需要等待的异步任务。
  5. 等待异步任务完成:如果遇到需要等待的异步任务,JavaScript 引擎会将其放入等待队列中,并继续轮询任务队列。
  6. 回到第 3 步:当等待队列中的异步任务完成后,JavaScript 引擎会将其放回任务队列中,然后继续轮询任务队列。

需要注意的是,事件循环中的异步任务分为宏任务和微任务两种类型。宏任务包括 setTimeout、setInterval、I/O 操作等,而微任务包括 Promise、async/await 等。在事件循环中,微任务会优先于宏任务执行,即当当前宏任务执行完毕后,会先执行所有的微任务,然后再执行下一个宏任务。这是因为微任务通常比宏任务更加高优先级,可以更快地响应用户的操作,提高用户体验。

微任务:

在 JavaScript 的事件循环机制中,微任务是一种高优先级的异步任务,它会在当前宏任务执行完毕后立即执行,而不需要等待下一个宏任务。常见的微任务包括 Promise 的回调函数、MutationObserver 的回调函数等。

微任务的执行顺序比较特殊,它会优先于宏任务执行。具体来说,当一个宏任务执行完毕后,JavaScript 引擎会先执行所有的微任务,然后再执行下一个宏任务。这种执行顺序可以保证微任务能够更快地响应用户的操作,提高用户体验。

需要注意的是,微任务可能会产生新的微任务,这种情况下会形成一个微任务队列,直到队列中的所有微任务执行完毕才会继续执行下一个宏任务。因此,在编写 JavaScript 代码时,需要注意避免出现过多的嵌套微任务,以避免影响性能。

下面是一个示例代码,演示了微任务的执行顺序:

 console.log('start');
 ​
 setTimeout(() => {
   console.log('timeout');
 }, 0);
 ​
 Promise.resolve().then(() => {
   console.log('promise');
 });
 ​
 console.log('end');

输出结果为:

 start
 end
 promise
 timeout

可以看到,微任务 Promise 的回调函数会先于宏任务 setTimeout 的回调函数执行。

宏任务:

在 JavaScript 的事件循环机制中,宏任务是一种低优先级的异步任务,它会在当前宏任务执行完毕后,等待下一个宏任务执行。常见的宏任务包括 setTimeout、setInterval、I/O 操作等。

宏任务的执行顺序比较简单,它会按照顺序依次执行。当一个宏任务执行完毕后,JavaScript 引擎会检查任务队列中是否有新的宏任务,如果有则执行下一个宏任务,否则继续等待。

需要注意的是,宏任务的执行时间可能会比较长,因此会影响 JavaScript 引擎的性能和响应速度。为了避免这种情况,可以使用微任务来优化代码,提高性能和用户体验。

下面是一个示例代码,演示了宏任务的执行顺序:

 console.log('start');
 ​
 setTimeout(() => {
   console.log('timeout');
 }, 0);
 ​
 console.log('end');

输出结果为:

 start
 end
 timeout

可以看到,宏任务 setTimeout 的回调函数会在当前宏任务执行完毕后执行。

ECMAScript符合性

ECMA-262阐述了什么是ECMAScript符合性。要成为ECMAScript是实现,必须满足以下条件:

  • 支持ECMA-262中描述的所有“类型、值、对象、属性、函数,以及程序语法和语义”。

  • 支持Unicode字符标准。

  • 增加EMCA-262中未提及的“额外的类型、值、对象、属性和函数”。ECMA-262所说的这些额外内容主要指规范中未给出的新对象或对象的新属性。

  • 支持ECMA-262中没有定义的“程序和正则表达式语法”(允许修改地址和拓展内置的正则表达式特征)。

    以上条件为实现开发者基于ECMAScript开发语言提供了极大的权限和灵活度。

总结:

  1. JS是单线程的,但是Render进程里面有多个线程
  2. JS线程和GUI线程互斥,执行大的计算任务会导致页面卡顿
  3. 基础数据类型在栈上,复杂数据类型存在堆上
  4. const,let没有变量提升,提前使用会报错
  5. JS也有编译的过程,执行之前会执行上下文
  6. 一个执行上下文包括变量环境、词法环境、this
  7. 变量环境里面有一个指向外部函数执行上下文的指针,形成了作用域链
  8. 全局执行上下文只有一份
  9. this和执行上下文绑定