变量提升

当执行 JS 代码时,会生成执行环境,只要代码不是写在函数中的,就是在全局执行环境中,函数中的代码会产生函数执行环境。在生成执行环境时,会有两个阶段。第一个阶段是创建的阶段,JS 解释器会找出需要提升的变量和函数,并且给他们提前在内存中开辟好空间,函数的话会将整个函数存入内存中,变量只声明并且赋值为 undefined,所以在第二个阶段,也就是代码执行阶段,我们可以直接提前使用。var 会产生很多错误,所以在 ES6中引入了 let。let不能在声明前使用,但是这并不是常说的 let 不会提升,let提升了,在第一阶段内存也已经为他开辟好了空间,但是因为这个let声明的变量存在暂时性死区的特性导致了并不能在声明前使用。

执行顺序

JavaScript 代码执行过程中,会先进行编译,然后再按顺序执行,在编译阶段,会生成执行上下文和可执行代码。变量和函数会被存放到执行上下文中;在代码执行阶段,JavaScript 引擎会从执行上下文中去查找自定义的变量和函数。

执行上下文

执行上下文是 JavaScript 执行一段代码时的运行环境,它由变量环境词法环境、this组成,比如调用一个函数,就会进入这个函数的执行上下文,确定该函数在执行期间用到的诸如 this、变量、对象以及函数等。

  1. 当 JavaScript 执行全局代码的时候,会编译全局代码并创建全局执行上下文,而且在整个页面的生存周期内,全局执行上下文只有一份。
  2. 当调用一个函数的时候,函数体内的代码会被编译,并创建函数执行上下文,一般情况下,函数执行结束之后,创建的函数执行上下文会被销毁。

变量环境: 存放着使用var 声明的变量和函数外部的执行上下文的引用。
词法环境: 存放着使用let 声明的变量和函数等。

作用域(scope)

作用域是指在程序中定义变量的区域,该位置决定了变量和函数的可访问范围(可见性)和生命周期。
ES 的作用域有三种:全局作用域、函数作用域、块级作用域。

  • 全局作用域中的对象在代码中的任何地方都能访问,其生命周期伴随着页面的生命周期。
  • 函数作用域就是在函数内部定义的变量或者函数,并且定义的变量或者函数只能在函数内部被访问。函数执行结束之后,函数内部定义的变量会被销毁。

在 ES6 之前,JavaScript 只支持这两种作用域,相较而言,其他语言则都普遍支持块级作用域。块级作用域就是使用一对大括号包裹的一段代码,比如函数、判断语句、循环语句,甚至单独的一个{}都可以被看作是一个块级作用域。

作用域链

每个函数执行上下文的变量环境中,都包含了一个指向外部执行上下文的外部引用(在函数声明式确定的), 当访问一个变量时,假如变量当前执行上下文中找不到的时候,就会去外部的执行上下文中查找,我们把这个查找的链条就称为作用域链。
注: 词法作用域就是指作用域是由代码中函数声明的位置来决定的

闭包

闭包其实就是一个可以访问其他函数内部变量的函数。
当通过调用一个外部函数返回一个内部函数后,即使该外部函数已经执行结束了,但是内部函数引用外部函数的变量依然保存在内存中,所以就产生了闭包。

  • 用途1 使用这种方法来创建私有变量,避免污染全局作用域(模块化)。
  • 用途2 是使已经运行结束的函数上下文中的变量对象继续留在内存中,可以用闭包实现防抖、节流。

原型

原型是一个对象,它包含了该构造函数的所有实例共享的属性和方法。当我们使用构造函数新建实例对象后,在这个对象的内部将包含一个指针,这个指针指向构造函数的 prototype 属性对应的值,这个指针被称为对象的原型。

原型链

原型链是由原型对象组成,每个对象都有 proto 属性,指向了创建该对象的构造函数的原型,proto 将对象连接起来组成了原型链。是一个用来实现继承和共享属性的有限的对象链。

继承

https://www.php.cn/js-tutorial-473770.html

  1. Class继承: 使用 extend 关键字
  2. 原型链继承: 新实例的原型等于父类的实例;
  • 所有新实例都会共享父类实例的属性
  1. 构造函数继承:
  • 只继承了父类构造函数的属性,没有继承父类原型的属性。
  1. 组合继承(组合原型链继承和构造函数继承)

可以不了解

  1. 寄生式继承;
  2. 寄生组合式继承;

模块化

模块化方案: CommonJS、AMD、CMD、ES6

  • 第一种是 CommonJS 方案,它通过 require 来导入模块,通过 module.exports 导出。这种模块加载方案是服务器端的解决方案,它是以同步的方式来引入模块的,因为在服务端文件都存储在本地磁盘,所以读取非常快,所以以同步的方式加载没有问题。但如果是在浏览器端,由于模块的加载是使用网络请求,因此使用异步加载的方式更加合适。
  • 第二种是 AMD 方案,这种方案采用异步加载的方式来加载模块,模块的加载不影响后面语句的执行,所有依赖这个模块的语句都定义在一个回调函数里,等到加载完成后再执行回调函数。require.js 实现了 AMD 规范
  • 第三种是 CMD 方案,这种方案和 AMD 方案都是为了解决异步模块加载的问题,sea.js 实现了 CMD 规范。它和require.js的区别在于模块定义时对依赖的处理不同和对依赖模块的执行时机的处理不同。
  • 第四种方案是 ES6 提出的方案,异步加载的方式来加载模块,使用 import 和 export 的形式来导入导出模块。

CommonJS和ES6模块化的区别

  1. CommonJS支持动态导入(也就是 require(${path}/xx.js)),ES6 目前不支持。
  2. CommonJS是同步导入、ES6是异步导入
    • CommonJS是同步导入,因为用于服务端,文件都在本地,同步导入即使卡住主线程影响也不大
    • ES6 是异步导入,因为用于浏览器,需要下载文件,如果也采用同步导入会对渲染有很大影响
  3. CommonJS 导入是值拷贝,导出值变化不会影响导入值, ES6 导入指向内存地址,导入值会随导出值而变化。

浏览器中的Event loop

为了避免多线程同时修改 dom 的同步问题,JS 被设计成了单线程,但是遇到定时逻辑、网络请求等异步任务又会被阻塞住,又为了解决单线程的阻塞问题,所以加了一层调度逻辑,也就是 Loop 循环和 任务队列,JS主线程在执行代码是会把异步任务放到其他线程跑(定时器触发线程、网络请求线程、事件触发线程),异步任务执行完毕之后会被放入任务队列中,主线程就不断从任务队列中去任务执行, 然后为了支持高优先级的任务调度,又引入了微任务队列,这就是浏览器的 Event Loop 机制:每次执行一个宏任务,然后执行所有微任务。

  • 宏任务包括:setTimeout、setInterval、requestAnimationFrame、Ajax、fetch、script(浏览器会先执行一个宏任务,接下来有异步代码的话就先执行微任务) 标签的代码
  • 微任务包括:Promise.then、MutationObserver、Object.observe、process.nextTick

requestAnimationFrame 会把每一帧中的所有DOM操作集中起来,在一次重绘或回流中就完成.
MutationObserver 是一个可以监听DOM结构变化的接口。
Object.observe 可以监听对象变化,当该对象有新增或更新或删除等操作,就会触发。