《JavaScript高级程序设计》笔记

147 阅读20分钟

语言基础

变量

let、const、var

var:

  • 声明的范围是函数作用域
  • 存在声明提升,会自动挂载到最近上下文中,如果未经声明就被初始化就会挂载到全局上下文。
  • 重复声明会被忽略。

let:

  • 声明的范围是块级作用域,作用域由最近的一对花括号界定。
  • 存在暂时性死区,在声明之前不能以任何方式来引用未声明的变量。
  • 同一作用域内不能声明两次。
  • 可以重新赋值。

const

  • 声明的范围是块级作用域,存在暂时性死区,同一作用域内不能声明两次。
  • 声明同时必须初始化,生命周期中不能再重新赋值,但引用对象的属性可以被修改。

命名规则

  • 只能是数字、英文字母、美元符、下划线组成。
  • 开头不能是数字。

变量提升

  • var 关键字声明或初始化的变量,会将声明语句“提升”到当前作用域的顶部。
  • 函数声明会使函数提升,但函数表达式声明不会。

数据类型

JavaScript 是动态类型语言,可以为变量赋予任意类型。

也是弱类型语言,允许显式和隐式的类型转换。

基本数据类型

即原始值,表示定义在语言最低层面的基础类型,值不可以被改变。

Null和Undefined的区别:

Null表示一个空的对象指针,通常表示对象的缺失。Undefined通常表示值的缺失,出现在变量已经声明,但没有赋值时。

Boolean:包括true和 false,常用于条件运算。

Number

表示数字的原始值,超出会自动被转换为Infinity。

  • 为什么0.1+0.2 ! == 0.3,如何让其相等:Number的存储方式是双精度浮点数,会先进行二进制计算再转为十进制,而小数部分的计算在这个过程中会产生误差。解决方法:①先转换为整数再除。②使用Number对象上的toFixed方法截去多余的小数位。

BigInt:表示数字的原始值,可以表示任意大小的整数,但不能表示小数。允许超越 Number 的安全整数限制

String

表示文本数据的原始值,一旦字符串被创建,就不可能被修改。

  • substring 和 substr 的区别:参数不同,substring接收 开始和结束的索引,substr接收 开始索引和需要截取的长度。

Symbol

  • 可以生成一个唯一并且不可变的原始值。
  • 直接使用 Symbol() 创建新的 symbol 类型,也可以用一个字符串作为其描述。
  • 使用 Symbol.for() 方法可以拿到已创建的Symbol 对象。
  • 可以用来作为对象属性的键,也可以用来加密对象私有属性,防止被修改。
引用数据类型
Object

引用数据类型即Object的定义是内存中可以被变量引用的一块区域。

对象也可以被看作是一组属性的集合,属性值可以是任何类型的值,包括其他对象。

属性类型:对象的属性有两种类型,数据属性和访问器属性

  • 数据属性:描述对象中键与值的关系,其中有:
    • value:存储值。
    • writable:控制可否赋值。
    • enumerable:控制能否枚举。
    • configurable:表示属性能否被控制,如被删除、更改特性等。
  • 访问器属性:控制对该对象的访问。其中有:
    • get:可以绑定一个方法,对应属性被访问时自动调用。
    • set:可以绑定一个方法,对应属性被初始化时自动调用。具有真实值的属性不能设置setter。
  • 判断对象是否相等:===操作符 和 Object.is。其中 Object.is 可以 正确判断NaN 和 区分+0、-0。Object.is: 判断地址是否一致。要判断对象中内容是否一致需要手动深度遍历所有属性(递归)。
  • 深浅拷贝的区别:主要在于对引用对象的拷贝,浅拷贝只复制了引用对象的引用,指向的内存区域有被其他引用被修改的风险。常见浅拷贝:扩展运算符、Object.assign、=操作符、Array.from等。如何实现深拷贝:递归函数拷贝属性、 JSON 序列化与反序列化。
Array

连续内存中相同类型数据的集合。

  • 判断数组的方式:① Object.prototype.toString.call ② instanceof ③ Array.isArray
  • 什么是类数组,如何转换为数组:document.children、arguments等具有数组相似特性,但不能调用数组方法的对象。可以用Array.from方法转换为数组。
  • forEach和map方法区别:
    • forEach()对数据的操作会改变原数组,该方法没有返回值。
    • map()方法不会改变原数组的值,返回一个新数组,新数组中的值为原数组调用函数处理之后的值。
  • 多维数组降维:falt方法(参数是需要降维的层数)或手动创建递归函数。
Map

基本使用:

  • 在 初始化时 或 通过set()方法 添加键值对。
  • 使用 get() 和 has() 进行查询。
  • 通过 size 属性获取映射中的键值对的数量。
  • 使用 delete() 和 clear() 删除值。

Map和Object的差异:

  • map可以使用任何 JavaScript 数据类型作为键。
  • map在自己的内容或属性被修改时仍然保持不变。
  • Map内存占用少于Object。
  • Map的查找和删除操作性能优于Object。
WeakMap
  • WeakMap中的键只能是对象 或 Symbol 值
  • 所引用的键都是弱引用:不属于正式的引用,不会阻止垃圾回收。如果所有键都被回收,WeakMap本身作为空映射也会被回收。
  • 不可迭代性:因为弱引用任何时候都可能被销毁,所以没必要提供迭代其键/值对的能力。

使用场景:

WeakMap的特性可以实现关联一些对象或数据,且不用对这些数据的销毁进行管理。可以用于:

  • 保存私有数据,作为对象的私有属性。
  • 缓存计算结果,将计算结果和对象相关联。
Set

类似于数组,但是成员的值都是唯一的,没有重复的值。

因为不会添加重复的值,可以作为字符数组或数字数组去重的一种方法。

基本使用:

  • 在初始化时或可以使用 add()增加值。
  • 使用has()进行查询。
  • 通过 size 属性获取元素的数量。
  • 使用 delete()和 clear()删除值。
WeakSet
  • WeakSet 的成员只能是对象和 Symbol 值。
  • 所引用的对象是弱引用。
  • 不可迭代性。
判断数据类型
  • typeof:返回字符串,常用于区分基本数据类型。无法区分Null和Object,也无法区分Object下的细分类目,如date、error等。
  • instanceof和isPrototypeOf:返回布尔值,检测构造函数的 prototype 属性是否出现在某个实例对象的原型链上。
  • Object.prototype.toString.call:对于Object类型的实例进行类型判断,返回它的class类名。

手写instanceof:

function myInstance(L, R) {
 // 实例原型对象
 let left = L.__proto__
 // 原型对象
 let right = R.prototype

 while (left !== null) {
     if (left === right) {
         return true
     }
     // 说明要么不是继承关系 要么left还有父级 所以继续迭代
     left = left.__proto__
 }
 return false
}
类型转换

主要分 显式类型转换 和 隐式类型转换 两种。

  • 显式类型转换:

开发者使用Boolean、Number、String等函数进行数据转换。

Boolean:只有空值、false、undefined、null、0、NaN、空字符串会转换为false,其他都为true。

Number:如果参数无法被转换为数字,则返回NaN。但一般使用更加灵活的 parseInt 和 parseFloat 进行转换。

String:等同于调用 toString方法,会返回一个反映参数的字符串。在转换对象时,不同的类有不同规则。

  • 隐式类型转换

JS在使用特定操作符时自动进行的类型转换,如 if判断用 == 比较等。

最常见的转换规则是:布尔值会被转换为数字0或1,数字或字符串可能会被转换为Boolean类型等。

作用域与内存

执行上下文与作用域

执行上下文:就是变量或函数执行环境,变量或函数的上下文决定了它们可以访问哪些数据,以及它们的行为。

全局上下文:最外层的上下文,根据执行环境不同也会更改,如浏览器一般都是window 对象。

变量对象:每个上下文都有一个关联的变量对象,而这个上下文中定义的所有变量和函数都存在于这个对象上。

执行上下文的生命周期

  1. 创建阶段:当代码执行流进入函数时,函数的上下文被推到一个上下文栈上。这个阶段确定了this值、词法环境和变量环境。
  2. 执行阶段:负责变量赋值、代码执行,为找不到的变量分配 undefined 值。
  3. 回收阶段:在函数执行完之后,上下文栈会弹出该函数上下文,将控制权返还给之前的执行上下文。
  4. 上下文在其所有代码都执行完毕后会被销毁,包括定义在它上面的所有变量和函数。

作用域链:

  • 作用域链决定了当前上下文中的代码在访问变量和函数时的顺序。
  • 上下文中的代码被执行时,会创建变量对象的作用域链。
  • 正在执行的上下文的变量对象始终位于作用域链的最前端。
  • 当前变量对象之后的就是当前变量对象的父级上下文,也叫包含上下文,并以此类推直到全局上下文。

作用域链增强:使用 try/catch语句的catch块with 语句 都可以在作用域链前端临时添加一个上下文。

垃圾回收

垃圾回收就是JS的执行环境(V8引擎)负责在代码执行时管理内存。

基本思路:每隔一段时间就判断一遍哪个变量不会再使用,然后释放它占用的内存。

V8引擎的分代式垃圾回收机制

因为垃圾回收的算法性能消耗较大,V8引擎选择将存活时间较短的对象和较长的对象进行分区。

新生代区

在新生代区中,V8引擎会将开辟两块内存空间,每次垃圾回收,会将空间A中仍处于活跃状态的对象复制一份到空间B,然后删除整块空间A,下次执行就复制到空间A,清空空间B。

对象晋升:当一个对象在经过多次复制之后依旧存活,它会被认为是一个生命周期较长的对象,在下一次进行垃圾回收被直接转移到老生代中。

老生代区

对于生命周期长的对象,V8引擎采用是标记整理策略。分为两个阶段:

  1. 在标记阶段,会遍历堆中的所有对象,然后标记活跃的对象。
  2. 在清除阶段,会将死亡的对象进行清除,将活动的对象往堆内存的一端进行移动,移动完成后再清理掉边界外的全部内存。

增量标记:为了减少垃圾回收带来的停顿时间,在进行标记时,会先标记堆内存中的一部分对象,然后暂停,将执行权重新交给JS主线程,待主线程任务执行完毕后再从原来暂停标记的地方继续标记,直到全部标记完成。

如何避免内存泄漏

  • 少用闭包
  • 少用全局对象
  • 手动清除定时器
  • 使用 removeChild 手动清除DOM引用

类与面向对象编程

原型链
  • 原型链:常用于实现继承。每个构造函数都有 prototype属性指向原型对象,原型上有constructor属性指回构造函数,而所有实例内部有都一个 __ proto __ 指向原型对象。
  • 原型对象:可以挂载所有实例共享的属性和方法。
  • Js实现继承有哪几种方式?
    1. 原型链继承:将父类的实例作为子类的原型。(将子类prototype指向父类实例)
  • 缺点:①无法实现多继承。②原型上的属性是共享的,属性可以被其他实例修改。
    1. 构造继承:在子类构造函数中执行父类的构造函数,通过call函数设置this的指向。缺点:只能继承父类的实例属性,不能继承原型属性、方法。
    2. 实例继承、拷贝继承:都是为父类实例添加新特性,作为子类实例。
    3. 组合继承:原型链继承和构造继承组合使用。通过原型链继承父类方法,通过call调用构造函数继承属性。
    4. 寄生组合式继承:同组合继承,唯一不同是用Object.create继承父类方法,避免两次调用父类构造函数。
function Parent(name) {
  this.name = name;
  this.colors = ['red', 'green', 'blue'];
}
Parent.prototype.sayName = function() {
  console.log(this.name);
};
function Child(name, age) {
  // 执行父类构造函数
  Parent.call(this, name);
  this.age = age;
}
// 将子类的原型  指向父类
Child.prototype = Object.create(Parent.prototype);
// 此时的构造函数为父类的 需要指回自己
Child.prototype.constructor = Child;

Child.prototype.sayAge = function() {
  console.log(this.age);
};
var child1 = new Child('Tom', 18);
child1.sayName(); // 'Tom'
child1.sayAge(); // 18
new操作符的执行流程
  1. 创建一个新对象,将this绑定到新创建的对象。
  2. 使用传入的参数调用构造函数。
  3. 将创建的对象的__proto__指向构造函数的prototype。
  4. 如果构造函数没有显式返回一个对象,则返回创建的新对象,否则返回显式返回的对象。
function mynew(Func, ...args) {
    // 1.创建一个新对象
    const obj = {}
    // 2.新对象原型指向构造函数原型对象
    obj.__proto__ = Func.prototype
    // 3.将构建函数的this指向新对象
    let result = Func.apply(obj, args)
    // 4.根据返回值判断
    return result instanceof Object ? result : obj
}

函数

bind()、call()和apply()的区别
  • 参数传入方式不同:bind 和 call 需要将参数作为一个个单独的值传入,而 apply 允许传递一个数组作为参数。
  • 返回值不同:bind 方法返回一个绑定后的新函数,而 call 和 apply 则直接执行原始函数并返回执行结果。
匿名函数
  • 匿名函数通常作为只用一次,不需要在其他地方使用的回调函数。
  • 也可以在 IIFE 中使用,来封装局部作用域内的代码,以便其声明的变量不会暴露到全局作用域。
尾递归

递归:函数在定义中使用函数自身。

一般来说,递归需要有 边界条件、递归前进阶段 和 递归返回阶段。当边界条件不满足时,递归前进;当边界条件满足时,递归返回。

尾递归:一个函数中所有递归形式的调用都出现在函数的末尾。

意义:一般递归在递归调用过程中,系统会为每一层的返回点、局部量等开辟栈来存储,递归次数过多容易造成栈溢出。而尾递归只需要存储一个调用记录,可以避免栈溢出。

DOM

常见操作
  • 创建节点:createElement。
  • 获取节点:querySelector、getElementById。
  • 修改节点:改变InnerHTML、style等。
  • 添加节点:appendChild。
  • 删除节点:removeChild。
BOM

就是浏览器对象模型,主要提供与浏览器的交互。

常用对象:window、location(操作路由)、history(路由记录)、navigator(浏览器信息)、screen(显示器信息)。

实现上拉加载,下拉刷新
  • 第三方库
  • 通过判断dom对象的scrollTop(视窗高度)+clientHeight(可视区域高度)和scrollHeight(页面元素总高度)的大小。来判断当前对象是否被上拉。
  • 通过touchstart和touchmove记录用户是否在顶部,且用户是否下拉超过指定高度。以此判断是否触发下拉刷新。

事件

事件与事件流

通常指HTML文档或者浏览器中发生的交互操作。 常见的有加载事件、鼠标事件、自定义事件等。

事件流

事件流是描述页面中接收事件的顺序,在多个节点绑定事件时,存在同时被触发的顺序问题。

事件流的三个阶段(从子到父触发):1. 捕获阶段。2. 目标阶段。 3 .冒泡阶段。

事件模型

事件模型就是 JS实现的通过绑定监听函数对事件做出反应。

事件模型的可以分为三种:原始事件模型、标准事件模型和IE事件模型。

原始事件模型:

绑定方式:HTML代码中绑定 和 JS代码中直接将函数赋值给dom对象中对应属性。

特点:1. 仅能绑定一次,重复绑定会覆盖。2. 仅支持冒泡,不支持捕获。

标准事件模型:

使用 addEventListener 和 removeEventListener 管理绑定事件。

特点:

  1. 通过参数useCapture可以控制是冒泡阶段执行还是捕获阶段执行。
  2. 可以在同个dom上多次绑定。
事件代理

把一个或者一组元素的事件委托到它的父层或者更外层元素上,真正绑定事件的是外层元素,而不是目标元素。

如果我们有一个列表,列表之中有大量的列表项,我们需要在点击列表项的时候响应一个事件。

如果给每个列表项一一都绑定一个函数,那对于内存消耗是非常大的。

这时候就可以事件委托,把点击事件绑定在父级元素ul上面,然后执行事件的时候再去匹配目标元素。

Worker

定义:是HTML5 标准定义的一套 API,允许开发者创建多线程任务。

意义:

  • 在有大量运算任务时,js线程可能会被长时间阻塞,甚至造成页面卡顿,影响用户体验。
  • 而Worker 线程能与 js 主线程同时运行,互不阻塞。
  • 把运算任务交给 Worker 线程去处理,js 主线程就只需要专注处理业务逻辑,增加工作效率。

worker环境和浏览器环境差别:没有window、document等。

主线程开启:使用onmessage监听接收消息,使用onerror监听错误信息。使用postmessage发送消息。

worker线程:使用onmessage监听接收消息。使用postmessage发送消息。

关闭worker线程:主线程调用 worker.terminate 或 worker线程调用self.close。

ES6

Promise的状态及其方法

Promise对象的三种状态:Pending(进行中)、Fulfilled(已成功)和Rejected(已失败)。

Promise对象的主要方法:

  1. then:用来添加Pending或Fulfilled的回调,每次返回一个新的 Promise 对象。
  2. catch:捕获错误。
  3. all:接收Promise数组作为参数,当数组内所有方法都执行完且没有报错才会返回结果。
  4. race:接收Promise数组作为参数,返回第一个执行完毕的方法的结果。
async/await 的实现原理
  1. 利用 Generator生成器Promise期约 实现将异步函数转为同步执行。
  2. Generator的特殊机制是可以定义多个内部状态,调用next方法来更新状态,用于实现代码的暂停执行。
  3. 配合Promise方法,实现将每个异步任务都划分为一个状态,当方法体内异步任务执行并回调,才进入下一个状态,执行后面的任务。
箭头函数
  • 是es6的语法糖。
  • 箭头函数没有独立的 this,arguments,和prototype。
  • 不能作为构造函数。
for...in和for...of的区别
  • for…of遍历获取对象的键值,for…in 获取的是对象的键名
  • for… in 会遍历对象的整个原型链,性能比较差。
for...of
  • 允许遍历一个含有 iterator 接口的数据结构(数组、对象等)并且返回各项的
  • 要遍历对象可以手动添加一个 Symbol.iterator 属性并指向一个迭代器。

JavaScript特性

为什么JS是单线程的

防止多个线程同时操作DOM,带来渲染冲突问题。

闭包

定义:当一个函数在内部访问了此函数的父级及父级以上的作用域变量

常见情况就是在一个函数内创建另一个函数,被创建的函数中仍可以访问到父函数的局部变量。

闭包的优缺点:

  1. 优点:①延长数据生命周期,保存数据。②封装数据,避免全局污染。
  2. 缺点:①可能导致内存泄漏。

使用场景:

  1. 函数柯里化。(一种将多个参数的函数转换为一系列接受单个参数的函数的过程)
  2. 实现链式调用。(返回this)
  3. 发布-订阅模式。(保存订阅事件)
this对象
  • this 关键字是函数运行时自动生成的一个内部对象,只能在函数内部使用。
  • 总是指向调用它的对象
  • 可以使用apply、call、bind等方法可以更改函数的this指向。
正则表达式

通常用于匹配字符串。

使用场景:①表单校验,如判断手机号、设备编号等。② 解析url。

本地存储

cookie:

  • 主要用于辨别用户身份。
  • 每次请求都会被发送,在使用http时容易被窃取。
  • 数据存储量较小不能超过4k。

localStorage:

  • 持久化的本地存储,除非主动删除数据,否则永不过期。
  • 可以在同一域中共享,多页面共享。
  • 只能存入字符串。

sessionStorage:

  • 一旦会话关闭,就会清空数据。
  • 其他同localStorage。

IndexedDB:

  • 支持存储大量结构化数据。
  • 支持储存JS对象。
  • 操作都是异步的,性能优于localStorage。
函数式编程

主要的编程范式有三种:命令式编程,声明式编程和函数式编程。

纯函数:单一职责的、无副作用的函数。有明确的输入类型和返回类型,且函数体内不进行作用域外的修改。

高阶函数:接收一个函数作为参数或返回一个函数,即将函数看作一种映射关系。常用高阶函数:foreach,map等。

柯里化:将一个接收多参数的函数转换为接收单个函数的嵌套。可以纯化函数。

组合与管道:使用compose函数将多个函数组合成一个新的函数并返回,类似于对函数进行组合。

函数式编程:

  • 相比命令式编程,函数式编程更加强调程序执行的结果而非执行的过程。
  • 倡导利用若干简单的执行单元让计算结果不断渐进,逐层推导复杂的运算,而非设计一个复杂的执行过程。
  • 通过把过程逻辑分割为一个个函数,并定义好输入参数。
  • 优点:方便复用、减少状态管理。
防抖和节流

都是通过减少调用频率 来 避免函数被高频触发。

常用于处理 scroll、keypress、mousemove 等事件或用户搜索框连续输入,表单实时验证等场景。

节流: n 秒内只运行一次,若在 n 秒内重复触发,只有一次生效。

防抖: n 秒后在执行该事件,若在 n 秒内被重复触发,则重新计时。

function throttled2(fn, delay = 500) {
    let timer = null
    return function (...args) {
        if (!timer) {
            timer = setTimeout(() => {
                fn.apply(this, args)
                timer = null
            }, delay);
        }
    }
}
function debounce(func, wait) {
    let timeout;
    return function () {
        let context = this; // 保存this指向
        let args = arguments; // 拿到event对象

        clearTimeout(timeout)
        timeout = setTimeout(function(){
            func.apply(context, args)
        }, wait);
    }
}

function debounce(func, wait, immediate) {
    let timeout;
    return function () {
        let context = this;
        let args = arguments;
        if (timeout) clearTimeout(timeout); // timeout 不为null
        if (immediate) {
            let callNow = !timeout; // 第一次会立即执行,以后只有事件执行后才会再次触发
            timeout = setTimeout(function () {
                timeout = null;
            }, wait)
            if (callNow) {
                func.apply(context, args)
            }
        }
        else {
            timeout = setTimeout(function () {
                func.apply(context, args)
            }, wait);
        }
    }
}