语言基础
变量
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 对象。
变量对象:每个上下文都有一个关联的变量对象,而这个上下文中定义的所有变量和函数都存在于这个对象上。
执行上下文的生命周期:
- 创建阶段:当代码执行流进入函数时,函数的上下文被推到一个上下文栈上。这个阶段确定了this值、词法环境和变量环境。
- 执行阶段:负责变量赋值、代码执行,为找不到的变量分配 undefined 值。
- 回收阶段:在函数执行完之后,上下文栈会弹出该函数上下文,将控制权返还给之前的执行上下文。
- 上下文在其所有代码都执行完毕后会被销毁,包括定义在它上面的所有变量和函数。
作用域链:
- 作用域链决定了当前上下文中的代码在访问变量和函数时的顺序。
- 上下文中的代码被执行时,会创建变量对象的作用域链。
- 正在执行的上下文的变量对象始终位于作用域链的最前端。
- 当前变量对象之后的就是当前变量对象的父级上下文,也叫包含上下文,并以此类推直到全局上下文。
作用域链增强:使用 try/catch语句的catch块 或 with 语句 都可以在作用域链前端临时添加一个上下文。
垃圾回收
垃圾回收就是JS的执行环境(V8引擎)负责在代码执行时管理内存。
基本思路:每隔一段时间就判断一遍哪个变量不会再使用,然后释放它占用的内存。
V8引擎的分代式垃圾回收机制
因为垃圾回收的算法性能消耗较大,V8引擎选择将存活时间较短的对象和较长的对象进行分区。
新生代区:
在新生代区中,V8引擎会将开辟两块内存空间,每次垃圾回收,会将空间A中仍处于活跃状态的对象复制一份到空间B,然后删除整块空间A,下次执行就复制到空间A,清空空间B。
对象晋升:当一个对象在经过多次复制之后依旧存活,它会被认为是一个生命周期较长的对象,在下一次进行垃圾回收被直接转移到老生代中。
老生代区:
对于生命周期长的对象,V8引擎采用是标记整理策略。分为两个阶段:
- 在标记阶段,会遍历堆中的所有对象,然后标记活跃的对象。
- 在清除阶段,会将死亡的对象进行清除,将活动的对象往堆内存的一端进行移动,移动完成后再清理掉边界外的全部内存。
增量标记:为了减少垃圾回收带来的停顿时间,在进行标记时,会先标记堆内存中的一部分对象,然后暂停,将执行权重新交给JS主线程,待主线程任务执行完毕后再从原来暂停标记的地方继续标记,直到全部标记完成。
如何避免内存泄漏
- 少用闭包
- 少用全局对象
- 手动清除定时器
- 使用 removeChild 手动清除DOM引用
类与面向对象编程
原型链
- 原型链:常用于实现继承。每个构造函数都有 prototype属性指向原型对象,原型上有constructor属性指回构造函数,而所有实例内部有都一个 __ proto __ 指向原型对象。
- 原型对象:可以挂载所有实例共享的属性和方法。
- Js实现继承有哪几种方式?
-
- 原型链继承:将父类的实例作为子类的原型。(将子类prototype指向父类实例)
- 缺点:①无法实现多继承。②原型上的属性是共享的,属性可以被其他实例修改。
-
- 构造继承:在子类构造函数中执行父类的构造函数,通过call函数设置this的指向。缺点:只能继承父类的实例属性,不能继承原型属性、方法。
- 实例继承、拷贝继承:都是为父类实例添加新特性,作为子类实例。
- 组合继承:原型链继承和构造继承组合使用。通过原型链继承父类方法,通过call调用构造函数继承属性。
- 寄生组合式继承:同组合继承,唯一不同是用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操作符的执行流程
- 创建一个新对象,将this绑定到新创建的对象。
- 使用传入的参数调用构造函数。
- 将创建的对象的__proto__指向构造函数的prototype。
- 如果构造函数没有显式返回一个对象,则返回创建的新对象,否则返回显式返回的对象。
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 管理绑定事件。
特点:
- 通过参数useCapture可以控制是冒泡阶段执行还是捕获阶段执行。
- 可以在同个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对象的主要方法:
- then:用来添加Pending或Fulfilled的回调,每次返回一个新的 Promise 对象。
- catch:捕获错误。
- all:接收Promise数组作为参数,当数组内所有方法都执行完且没有报错才会返回结果。
- race:接收Promise数组作为参数,返回第一个执行完毕的方法的结果。
async/await 的实现原理
- 利用 Generator生成器 和 Promise期约 实现将异步函数转为同步执行。
- Generator的特殊机制是可以定义多个内部状态,调用next方法来更新状态,用于实现代码的暂停执行。
- 配合Promise方法,实现将每个异步任务都划分为一个状态,当方法体内异步任务执行并回调,才进入下一个状态,执行后面的任务。
箭头函数
- 是es6的语法糖。
- 箭头函数没有独立的 this,arguments,和prototype。
- 不能作为构造函数。
for...in和for...of的区别
- for…of遍历获取对象的键值,for…in 获取的是对象的键名;
- for… in 会遍历对象的整个原型链,性能比较差。
for...of
- 允许遍历一个含有 iterator 接口的数据结构(数组、对象等)并且返回各项的值。
- 要遍历对象可以手动添加一个 Symbol.iterator 属性并指向一个迭代器。
JavaScript特性
为什么JS是单线程的
防止多个线程同时操作DOM,带来渲染冲突问题。
闭包
定义:当一个函数在内部访问了此函数的父级及父级以上的作用域变量。
常见情况就是在一个函数内创建另一个函数,被创建的函数中仍可以访问到父函数的局部变量。
闭包的优缺点:
- 优点:①延长数据生命周期,保存数据。②封装数据,避免全局污染。
- 缺点:①可能导致内存泄漏。
使用场景:
- 函数柯里化。(一种将多个参数的函数转换为一系列接受单个参数的函数的过程)
- 实现链式调用。(返回this)
- 发布-订阅模式。(保存订阅事件)
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);
}
}
}