JS设计模式
单例模式、原型模式、观察者模式/订阅者模式等;
原型和原型链
原型和原型链的理解
原型:对象的基本模型,用于开发程序的部分结构。在JS中,每个对象都有一个与之相关的原型对象,可以从中继承属性和方法;E获取对象原型:
Object.getPrototypeOf()
原型链:是JS中对象继承机制中的一部分,允许对象通过其原型对象访问属性和方法。原型链是一个链式结构,其中每个对象都可链接到其原型对象(通过
__proto__
属性实现),从而形成一个从对象到Object.prototype
的继承链;
重写、修改原型
重写和修改对象的原型(prototype),用于扩展对象的功能或修改其默认行为。但是一般情况建议修改自己定义的对象原型哦~
原型链指向
由于Object是构造函数,所以原型链的终点的
Object.prototype.__proto__
,而Object.prototype.__proto__ ===null
输出true,即原型链的终点是null。
如何获取对象非原型链上的属性
hasOwnProperty()
:判断属性是否属于原型链的属性
JS数据类型
数据类型种类
8种数据类型:Undefined、Null、Boolean、Number、String、Object、Symbol、BigInt;
- Symbol:创建后独一无二的数据类型;
- BigInt:数字类型数据,可以表示任意精度格式的整数;
分类
- 基本数据类型:栈存储;
- 引用数据类型:堆存储;
类型检测
- typeof:数组、对象、null都会判断为object;
- instanceof:测试一个对象在其原型链中是否存在一个构造函数的
prototype
属性,只能正确判断引用数据类型。([] instanceof Array
); - constructor:用于判断数据类型;对象实例可以根据constructor访问它的构造函数; -Object.prototype.toString.call():使用Object对象的原型方法toString来判断数据类型;
JS的类型转换机制
当JS尝试将对象转换为原始值时,首先会调用valueOf
方法,如果还未转换为原始值,则会尝试调用toString()
- 隐式类型转换:+、==/===、&/|/**^**等操作符处理;
- 显示类型转换:Number()、String()/toString()、**Boolean()**等;
[] == ![]
![] == true
[] == false
'' == false
0 == 0
为什么obj.toString()和Object.prototype.toString()的结果不一样
toString是Object的原型方法,而Array、function等类型作为Object的实例,都重写了toString方法。不同的对象类型调用toString方法时,根据原型链原理,调用的都是重写以后的toString方法。
判断数组的方法
Object.prototype.toString.call()
obj.__proto__ === Array.prototype
Array.isArray()
obj instance of Array
Array.prototype.isPrototypeOf(obj)
set
和map
在JavaScript中,`Set`和`Map`是两种内置的数据结构。
new Set()
:主要用于创建集合(元素唯一)。作用:去重、存储不同类型元素、数组与Set转换;new Map()
:主要用于创建映射关系(键值对)。作用:遍历对象、简化数组操作等;
JS数组方法
forEach
...等常用的;splice(index, delNum, ...items)
:从index开始,删除delNum个元素,在当前位置插入元素;slice(start, end)
:复制从start
到end
的元素,返回一个新数组;reduce(func, initial)
:通过为每个元素调用func计算数组上的单个值,并在调用之间传递中间结果;array.fill(value, startm end)
:从start到end用value重复填充数据;array.copyWithin(target, start, end)
:将其元素从start到end在target位置复制到本身(覆盖所有;)array.from()
:将伪数组转换为真数组;array.of()
:将一组值转换为数组;
类数组和数组的区别
类数组和数组都是处理数据集合的概念。区别:
- 数据结构:
- 数组用于存储一系列元素,允许通过索引访问,长度固定,可动态调整;
- 类数组像数组,可通过索引访问连续元素,但不是真正的数组,
length
属性不可写;
- 内置方法:
- 数组有许多内置方法用于操作增删改查、映射、过滤等;
- 类数组由于不是真正的数组,仅可使用
Arrary.prototype
上的方法,可通过Array.prototype.slice.call()
或者扩展运算符[...array]
转化为数组;
- 来源:
- 数组是通过JS创建的;
- 类数组是由DOM操作(
document.querySelectorAll()
返回的NodeList)或者函数参数(arguments
对象在es6之前的函数中是类数组对象)、其它API调用产生的;
- 属性:
- 数组除元素外,还具有
length
属性,表示数组中元素数量;类数组也有一个length
属性,但并不一定等同于元素数量(NodeList中表示列表中的节点数量);
- 数组除元素外,还具有
- 类型检查:
- 数组可使用多方法迭代;
- 类数组仅可支持使用
for
循环进行迭代;
var、let、const的区别
- 块级作用域;
- 变量提升、暂时性死区;
- 重复声明;
- 给全局添加属性;
- 初始值设置:var、let可以不用设置初始值,但是const必须设置;
深拷贝和浅拷贝
- 浅拷贝:拷贝数据,复制引用,双向影响
Object.creat(x)
、Object.assign(target, ...sources)
[].Concat(x)
、[...array]
- 深拷贝:独立存在,互不影响
JSON.stringify(obj)
:不能拷贝特殊类型,不能处理循环引用;structuredClone()
// 手写深拷贝
const user ={
name:{
firstName: "牛",
lastName: "蜗"
},
age:18
}
function deep(obj){
let newObj = {};
for(let key in obj){
if(obj.hasOwnProperty(key)){//只拷贝显示具有的属性
if(obj[key] instanceof Object){//obj[key]是不是对象
newObj[key]=deep(obj[key]);//递归拷贝---深拷贝最核心的部分
}else{
newObj[key]=obj[key];
}
}
}
return newObj;
}
const newUser = deep(user);
user.name.firstName = "牛牛";
console.log(newUser)
new 操作符的原理
- 创建一个新的空对象;
- 设置原型:将对象的原型设置为函数的prototype 对象;
- 让函数的this指向这个对象,执行构造函数的代码(添加属性);
- 判断函数的返回值类型,如果是值类型,则返回创建的对象;如果是引用类型,则返回这个引用类型的对象;
function objectFactory() {
let newObject = null;
let constructor = Array.prototype.shift.call(arguments);
let result = null;
// 判断参数是否是一个函数
if (typeof constructor !== "function") {
console.error("type error");
return;
}
// 新建一个空对象,对象的原型为构造函数的 prototype 对象
newObject = Object.create(constructor.prototype);
// 将 this 指向新建对象,并执行函数
result = constructor.apply(newObject, arguments);
// 判断返回对象
let flag = result && (typeof result === "object" || typeof result === "function");
// 判断返回结果
return flag ? result : newObject;
}
// 使用方法
objectFactory(构造函数, 初始化参数);
对执行上下文的理解
JS引擎使用执行上下文栈来管理执行上下文;
分类:
- 全局执行上下文:一个程序中只有一个;
- 函数执行上下文
eval
函数执行上下文
执行步骤 解析 → 创建 → 执行:
- 全局上下文:变量定义,函数声明
- 函数上下文:变量定义,函数声明,
this
,arguments
this
指向问题
this
:执行上下文中的一个属性,指向最后一次调用这个方法的对象
箭头函数与普通函数有什么区别
- 箭头函数更简洁;
- 箭头函数没有自己的
this
,只在在自己作用域的上一层继承this
; - 箭头函数继承来的
this
指向永远不会改变; call
、apply
、bind
不能改变函数中的this指向;- 箭头函数不能作为构造函数使用;
- 箭头函数没有自己的arguments;
- 箭头函数没有prototype;
实现call、apply、bind函数
call和apply作用一样,且传参中,第一个参数均表示函数体内this
指向,仅在第二个参数不同:apply接受数组或者类数组;call接受枚举值,依次传入;
- call实现
Function.prototype.myCall = function(context) {
// 判断调用对象
if (typeof this !== "function") {
console.error("type error");
}
// 获取参数
let args = [...arguments].slice(1),
result = null;
// 判断 context 是否传入,如果未传入则设置为 window
context = context || window;
// 将调用函数设为对象的方法
context.fn = this;
// 调用函数
result = context.fn(...args);
// 将属性删除
delete context.fn;
return result;
};
- apply实现
Function.prototype.myApply = function(context) {
// 判断调用对象是否为函数
if (typeof this !== "function") {
throw new TypeError("Error");
}
let result = null;
// 判断 context 是否存在,如果未传入则为 window
context = context || window;
// 将函数设为对象的方法
context.fn = this;
// 调用方法
if (arguments[1]) {
result = context.fn(...arguments[1]);
} else {
result = context.fn();
}
// 将属性删除
delete context.fn;
return result;
};
- bind实现
Function.prototype.myBind = function(context) {
// 判断调用对象是否为函数
if (typeof this !== "function") {
throw new TypeError("Error");
}
// 获取参数
var args = [...arguments].slice(1),
fn = this;
return function Fn() {
// 根据调用方式,传入不同绑定值
return fn.apply(
this instanceof Fn ? this : context,
args.concat(...arguments)
);
};
};
闭包
闭包:指有权访问另一个函数作用域中变量的函数。闭包实际就是函数和声明函数的词法环境组合。
创建闭包最常用的方式就是在一个函数内部创建另一个函数,创建的函数可以访问到当前函数的局部变量。
作用:
- 创建私有变量;
- 延长变量的生命周期
缺点:
- 会造成内存泄漏
作用
- 使在外部能够访问到函数内部的变量,可以使用这种方法来创建私有变量;
- 使已经运行结束的函数上下文中,变量对象继续留在内存中(因为闭包函数保留了这个变量对象的引用,所以这个变量对象不会被回收)。
- 实现回调函数和高阶函数
—— 循环中使用闭包解决var定义函数问题
// setTimeout是异步函数,所以会先执行循环,所以会输出一堆5
for(var i = 0; i < 5; i++) {
setTimeout(function timer(){
console.log(i)
}, i*1000)
}
// 使用闭包解决
for(var i = 0; i < 5; i++) {
(function(){
setTimeout(function timer() {
console.log(i)
}, i * 1000)
})(i); // 立即执行函数,将当前的i值作为参数传入
}
解决办法:
- 使用闭包
- 使用
setTimeout
的第三个函数,这个参数会被当成timer参数传入; - 使用
let
操作符
对作用域、作用域链的理解
作用域
- 全局作用域
- 函数作用域
- 块级作用域
作用域链
本质就是一个指向变量对象的指针列表。在当前作用域中查找所需变量,找不到,则表示该变量是只有变量,即可向上查找,直到访问到window对象后终止。
作用:保证对执行环境有权访问的所有变量和函数的有序访问。通过作用域链,可以访问到外层环境的变量和函数。
浏览器的垃圾回收机制
浏览器的垃圾回收,即JS运行代码时,需要分配内存空间来储存变量和值。当变量不再参与运行,系统就会回收被占用的内存空间。
回收机制:
- 自动回收:定期对不再使用的变量、对象所占用的内存进行释放;
- 全局变量的生命周期会持续到页面卸载;局部变量声明在函数体呢,声明周期从函数执行开始到结束,函数执行结束后即释放,除了闭包情况;
垃圾回收的方式:
- 标记清楚:
- 引用计数
减少垃圾回收
- 优化数组:清空,使用length=0处理;
- 优化对象:不再使用则设置为null;
- 函数优化:可复用函数,外置封装;
内存泄漏
常见内存泄漏情况:
- 意外的全局变量;
- 被遗忘的计时器或回调函数清除;
- 脱离DOM的引用:获取一个DOM,而后元素被删除,但是一直保留了对该元素的引用,导致无法被回收;
- 不合理使用闭包
for...in和for...of(ES6新增)的区别
for...in
获取的是对象的键名,for...of
获取的是对象的键值;for...in
会遍历对象的整个原型链,性能很差;for...of
只遍历当前对象;for...in
会返回数组中所有可枚举的属性,for...of
只返回数组的下标对应的属性值。
综上,for...in
只适用遍历对象,for...of
可用来遍历数组、类数组对象、字符串、Map等;
AMD 和 CommonJS的区别
ES6模块与CommonJS模块有什么共同点和区别
异步编程的实现方法和优缺点
方法:
- 回调函数:缺点:可能会造成回调低于;
- Promise:同步的立即执行函数。缺点:有时会造成多个
then
的链式调用,导致代码的语意不明确; - generator:允许函数执行过程中挂起和恢复;
- async函数:将异步逻辑转换为同步顺序来写(await通过返回一个Promise来实现同步的效果),可以自动执行;
优点
- 非阻塞:提高了程序的响应性和吞吐量;
- 更好的资源利用;
- 代码简洁
缺点
- 错误处理有些复杂;
- 调试困难;
- 性能开销:比如创建和销毁Promise对象等;
promise的理解和优缺点
Promise:是JS中用来处理异步操作的的对象,表示了一个异步操作的最终完成状态和结果值。Promise能够简化异步编程的复杂性,当涉及多个异步操作时需要按照特定顺序执行则可考虑该对象使用(使用
.all()
或.race()
方法)。
状态:一旦改变,就不会再变
- pending:进行中
- fullfilled:已成功
- rejected:已失败
优点
- 解决回调地狱;
- 错误处理:使用
.catch()
,轻松捕获; - 可组合性;
- 状态不可变
缺点
- 错误容易被忽略;
- 不易于调试;
- 滥用可能导致性能问题;
- 不支持取消;
- 代码冗余。
Promise.all和Promise.race的区别和使用场景
Promise.all()
- 将多个Promise实例包装成一个新的Promise实例,传入的Promise顺序和返回成功的结果数组的顺序是一致的,但是失败则返回最先reject失败状态的值;
- 使用场景:发送多个请求,且要求请求顺序获取和使用数据时;
Promise.race()
- 赛跑。
Promise.race([p1, p2, p3])
里面哪个结果获得的快,就返回那个结果,不管结果本身是成功状态还是失败状态。 - 使用场景:当做某个请求,超时则取消,则使用该方法。
- 赛跑。
注意: Promise.resolve(x)
可以看作是 new Promise(resolve => resolve(x))
的简写,可以用于快速封装字面量对象或其他对象,将其封装成 Promise 实例。
事件循环
事件循环是一个单线程循环,用于监视调用堆栈并检查是否有工作即将在任务队列中完成。
如果调用堆栈为空并且任务队列中有回调函数时,则将回调函数调出并推送到调用堆栈中执行。
localStorage和sessionStorage的异同
localStorage和sessionStorage都是本地存储的方法。localStorage的存取是同步操作哦~
相同点
- 键值对存储;
- 数据类型:都存储字符串类型的数据;
- 容量限制:通常为5MB或更多;
- 同源策略:都受同源策略的限制;
不同点
- 生命周期:localStorage的数据没有过期时间,除非主动删除,所以常用于存储需要长期保存的数据;sessionStorage的数据在页面会话期间存在,当页面关闭时则会被清除。所以常用于存储与会话相关的临时数据。
- 存储位置:具体情况因浏览器而异;
- 事件监听:两者都支持事件监听,如
storage
事件,当存储区域中的数据发生变化时,可以触发该事件。 - 安全性:两者都支持事件监听,如
storage
事件,当存储区域中的数据发生变化时,可以触发该事件。但是,由于sessionStorage
的数据在页面会话结束后会被清除,因此它可能在某些情况下比localStorage
更安全一些(例如,当用户离开敏感页面时)。 - 数据共享:只读的
localStorage
属性允许你访问一个Document
源(origin)的对象Storage
,同一网站多个标签页中数据都可以共享;sessionStorage
在浏览器打开期间一直保持,并且重新加载或恢复页面仍会保持原来的页面会话,但sessionStorage 不能在多个窗口或标签页之间共享数据,但是当通过 window.open 或链接打开新页面时(不能是新窗口),新页面会复制前一页的 sessionStorage。
ES6和ES7的新特性有哪些?
ES6新特性主要有:
- 块级作用域:
let
、const
关键字;- 箭头函数:
=>
语法自动绑定了定义时的this
指向;- 解构赋值:允许从数组或对象中提取值,并将它们赋给一系列变量;
- 默认参数:允许在函数定义时为参数提供默认值;
- 扩展运算符:
...
;- 类和模块:引入了
class
关键字,支持extends
关键字实现继承,super
关键字调用父类方法,引入了模块化的概念,通过import
和export
方便导入导出模块;Promise
对象Symbol
数据类型,用于创建唯一的标识符;Proxy
和Reflect
:提供了在对象上设置陷阱和反射性的操作对象的能力;
ES7新特性:
- 指数运算符:
**
,用于数的幂计算(2**
3===8);Array.prototype,includes()
方法:判断包含,返回布尔值;Object.values()
、Object.entries()
等
数据结构算法有哪些?
- 查找算法:
- 线性查找:从开始到结束,逐个检查;
- 二分查找:要求数据结构已排序,每次查找都取中间元素与目标比较,根据结果决定左右继续;
- 哈希查找:利用哈希函数将键映射到数组中的索引;
- 排序算法:
- 冒泡排序:重复遍历,调整顺序;
- 插入排序:在已排序序列中国,从后向前扫描,找到相应位置插入;
- 选择排序:最大/最小值抽取,重复继续;
- 快速排序
- 归并排序
- 插入、删除、更新算法
- 遍历算法
- 深度有限搜索(DFS):用于遍历或搜索树或图。会尽可能深的搜索树的分支;
- 广度优先搜索(BFS):用于遍历或搜索树或图。从根节点开始,探索最近的邻居节点;
- 递归算法:通过函数调用自身来解决问题;
- 动态规划算法
- 图算法
- 最小生成树算法:在图的所有子集中找到一棵包含所有顶点的树,且所有边的权值之和最小;
- 最短路径算法:在图中找到从一个顶点到另一个顶点的最短路径;
- 字符串匹配算法
数组和链表有哪些区别?
数据和链表是两种常用的数据结构,区别如下:
- 存储方式:
- 数组:在内存中是一段连续的存储空间,大小在创建时候就固定了;
- 链表:在内存中不是连续存储的,每个节点都包含了一个数据域和一个指向下一个节点的指针域;在运行中大小可改变;
- 访问方式
- 数组:索引访问,复杂度O(1);
- 链表:从头节点开始,直到找到目标节点,复杂度O(n);
- 插入和删除:
- 数组:插入删除操作需要移动大量元素,复杂度O(n);
- 链表:插入删除只需要修改相关节点,复杂度O(1)
- 空间利用率
- 数组:只存储数据本身,利用率较高,但是设置过大容易导致内存浪费;
- 链表:利用率较低,但是由于可以动态分配内存空间,避免了内存浪费;
- 应用场景
- 数组:适用于需要频繁访问元素且元素数量固定不变的;
- 链表:适用于需要频繁进行插入和删除操作的元素、数据可能动态变化的场景。
全局函数有哪些?
min()
、max()
、abs()
、pow()
、range()
、parseInt()
、format()``escape()
和 unescape()
等;
location是怎么匹配的,匹配规则有哪些
location /
:匹配所有请求,因为所有的地址都以/开头location /documents/
:匹配任何以/documents/开头的地址,匹配符合后可能还会继续搜索;location ^~/images/
:匹配任何以/images/开头的地址,匹配符合后停止搜索正则;location ~* .(gif|jpg|jpeg)$
:匹配所有以gif,jpg或jpeg结尾的请求;- location的匹配顺序:先普通,再正则。即普通的location之间的匹配顺序按最大前缀匹配,正则location之间的匹配顺序按配置文件中的物理顺序匹配。
单项数据流和双向数据绑定的原理,区别
- 单项数据流:是指数据在应用程序中的流动方向是单项的,通常是父组件流向子组件,而不会反向流动:
- 数据更新只能通过父组件对子组件的传递;
- 明确、可控,使数据流向易于追踪和调试;
- 双向数据绑定:是指数据在视图和模型之间可以双向同步更新:
- ViewModel包含Observe个Compiler,监听器对所有数据的属性进行监听,解析器对每个元素节点的指令进行扫描和解析,视图或数据模型发生变化时,将同步另一方的自动更新;
- 方便,但也可能导致数据变更来源不明确;
前端跨域处理
前端跨域处理是解决前端开发过程中由于浏览器的同源策略,而无法直接访问不同源(协议、域名、端口)的资源的问题。
解决方法
-
JSONP:利用
<script>
标签不受同源策略限制的特点,通过动态插入<script>
标签来请求跨域资源。- 实现:
- 客户端,创建一个
<script>
标签,将跨域请求的URL作为src
属性。 - 服务器端:返回的数据需要以函数调用的形式包裹,客户端定义相应的回调函数来接收数据。
- 客户端,创建一个
- 缺点:只支持GET请求。
- 实现:
-
跨域资源共享(CORS):通过服务器端设置响应头信息来实现跨域请求。
- 服务器端
Access-Control-Allow-Origin
:指定允许跨域请求的源,可以是具体的域名或通配符*
;Access-Control-Allow-Methods
:指定允许使用的请求方法,如GET、POST等;Access-Control-Allow-Headers
:指定允许在请求中使用的头部字段;
- 客户端:浏览器会自动处理;
- 优点:支持所有类型的HTTP请求,是W3C推荐的标准跨域解决方案。
- 服务器端
-
代理(Proxy):通过在服务器端设置一个代理服务器,前端与代理服务器通信,代理服务器与目标服务器通信,从而绕过浏览器的同源策略限制。
- 实现:使用NodeJS的中间件或者Nginx等服务器软件来设置代理
- 优点:灵活性强,可以配置复杂的路由规则;
-
PostMessage:使用window对象的postMessage方法在不同源的窗口之间发送和接收数据。。
- 实现:
- 发送方:使用postMessage方法发送数据,并指定接收方窗口的引用。
- 接收方:监听message事件来接收数据。
- 场景:适用于两个页面之间的跨域通信,如iframe内的页面与父页面之间的通信
- 实现:
-
WebSocket:网络通信协议,可以在单个TCP连接上进行全双工通信。
- 跨域处理:WebSocket不受同源策略限制,可直接使用;
- 优点:支持实时通信,适合需要频繁传输数据的应用场景
-
服务器端反向代理:服务器端设置。优点:对前端透明,无需修改前端配置;
-
document.domain + iframe:通过设置
document.domain
属性使不同子域的页面具有相同的域,从而可以相互访问。- 场景:适用于主域相同、子域不同的跨域应用场景
-
location.hash + iframe
- 原理:通过iframe和
location.hash
来实现跨域通信。 - 实现步骤:
- 在一个页面中嵌入一个iframe,并设置其
src
属性为另一个域的页面。 - 通过修改iframe的
location.hash
值来传递数据,监听hashchange事件来接收数据。
- 在一个页面中嵌入一个iframe,并设置其
JS继承
在JS中,继承通常不是一个直接内置在语言中的特性,可以通过原型链、构造函数、组合继承、原型式继承、寄生式继承、寄生组合式继承等方式模拟实现。
- 原型链继承:当试图访问一个对象的属性时,如果该对象本身没有这个属性,那就回在原型对象中查找;
- 构造函数继承:在子类型构造函数的内部调用超类型构造函数。即可以使用
call
或apply
,将父对象的构造函数绑定在子对象上。
function Parent() {
this.name = 'parent';
}
function Child() {
Parent.call(this);
// 继承Parent
this.age = 'child';
}
let child = new Child();
console.log(child.name); // 输出'parent'
- 原型式继承:借助原型可以基于已有对象创建新对象。
function object(o) {
function F() {}
F.prototype = o;
return new F();
}
let person = {
isHuman: true,
printIntroduction: function() {
console.log("My name is " + this.name);
}
};
let me = object(person);
等