1、手动实现防抖
和节流
防抖:事件被触发 n 秒后再执行回调,如果在这 n 秒内事件又被触发,则重新计时,,防止在短时间内过于频繁的执行相同的任务。当短时间内的频繁是不必要的时候,就可以考虑去抖动,避免资源浪费,或造成不友好的体验。
节流:规定一个单位时间,在这个单位时间内,只能有一次触发事件的回调函数执行,如果在同一个单位时间内某事件被触发多次,只有一次能生效
防抖函数的应用场景:
- 按钮提交场景:防⽌多次提交按钮,只执⾏最后提交的⼀次
- 服务端验证场景:表单验证需要服务端配合,只执⾏⼀段连续的输⼊事件的最后⼀次,还有搜索联想词功能类似⽣存环境请⽤lodash.debounce
节流函数的适⽤场景:
- 拖拽场景:固定时间内只执⾏⼀次,防⽌超⾼频次触发位置变动
- 缩放场景:监控浏览器resize
- 动画场景:避免短时间内多次触发动画引起性能问题
/** * debounce 防抖 * @param fn [function] 需要防抖的函数 * @param delay [number] 毫秒,防抖期限值 */ function debounce(fn, delay) { let timer = null; //借助闭包 return function (...arg) { if (timer) { //进入该分支语句,说明当前正在一个计时过程中,并且又触发了相同事件。 // 所以要取消当前的计时,重新开始计时 clearTimeout(timer); } timer = setTimeout(() => { fn.apply(this, arg); // 使用apply将fn函数的this指向修改为return后的function }, delay); // 进入该分支说明当前并没有在计时,那么就开始一个计时 }; } /** * debounce 节流 * @param fn [function] 需要节流的函数 * @param delay [number] 毫秒 */ function throttle(fn, delay) { let valid = false; // 节流阀 return function (...arg) { if (valid) { //休息时间 暂不接客 return; } // 工作时间,执行函数并且在间隔期内把状态位设为无效 valid = true; setTimeout(() => { fn.apply(this, arg); valid = false; }, delay); }; }
2、let、const、var 的区别
1.是否存在变量提升?
var
声明的变量存在变量提升(将变量提升到当前作用域的顶部)。即变量可以在声明之前调用,值为undefined
let
和const
不存在变量提升。即它们所声明的变量一定要在声明后使用,否则报ReferenceError
错2.是否存在暂时性死区?
let和const存在暂时性死区
。即只要块级作用域内存在 let 命令,它所声明的变量就“绑定”(binding)这个区域,不再受外部的影响3.是否允许重复声明变量?
var
允许重复声明变量。let
和const
在同一作用域不允许重复声明变量。4.是否存在块级作用域?
- var 不存在块级作用域。
- let 和 const 存在块级作用域
- 块作用域由
{ }
包括,if
语句和for
语句里面的{ }
也属于块作用域5. 是否能修改声明的变量?
var
和let
可以。const
声明一个只读的常量。一旦声明,常量的值就不能改变。const
声明的变量不得改变值,这意味着,const
一旦声明变量,就必须立即初始化,不能留到以后赋值。
3、箭头函数与普通函数区别
1.箭头函数是匿名函数,不能作为构造函数,不能使用 new
2.箭头函数内没有
arguments
,可以用展开运算符...
解决3.箭头函数的 this,始终指向父级上下文(箭头函数的
this
取决于定义位置父级的上下文
,跟使用位置没关系,普通函数this
指向调用的那个对象)4.箭头函数不能通过
call() 、 apply() 、bind()
方法直接修改它的 this 指向。(call、apply、bind
会默认忽略第一个参数,但是可以正常传参)5.箭头函数没有原型属性
4、Promise
- 状态
- 进行中:
pending
- 已成功:
resolved
- 已失败:
rejected
- 特点
- 对象的状态不受外界影响
- 一旦状态改变就不会再变,任何时候都可得到这个结果
- 声明:
new Promise((resolve, reject) => {})
- 出参
- resolve:将状态从
未完成
变为成功
,在异步操作成功时调用,并将异步操作的结果作为参数传递出去 - reject:将状态从
未完成
变为失败
,在异步操作失败时调用,并将异步操作的错误作为参数传递出去
① 什么是 Promise?
- Promise,简单说就是一个
容器
,包含异步操作结果的对象 - 从语法上说,promise 是一个
对象
,从它可以获取异步操作的的最终状态(成功或失败)。 - Promise 是一个
构造函数
,对外提供统一的 API,自己身上有 all、reject、resolve 等方法,原型上有 then、catch 等方法。
② Promise 有什么用?
解决回调地狱
ps:什么是回调地狱?回调地狱,其实简单来说就是异步回调函数的嵌套
③Promise 有哪些方法
- then():分别指定
resolved状态
和rejected状态
的回调函数
- 第一参数:状态变为
resolved
时调用
- 第二参数:状态变为
rejected
时调用(可选) - 链式调用 promise.then():
then
方法返回一个 Promise 对象,其允许方法链,从而创建一个 promise 链
- catch():指定发生错误时的回调函数
- Promise.resolve():将对象转为 Promise 对象 等价于
new Promise(resolve => resolve())
- Promise 实例:原封不动地返回入参
- thenable 对象:
thenable
对象指的是具有then
方法的对象Promise.resolve
方法会将这个对象转为 Promise 对象,然后就立即执行thenable
对象的then
方法- 不具有 then()的对象:将此对象转为 Promise 对象并返回,状态为
resolved
- 不带参数:返回 Promise 对象,状态为
resolved
- Promise.reject():将对象转为状态为
rejected
的 Promise 对象(等价于new Promise((resolve, reject) => reject())
)
Promise.all():
并发,发起多个并发请求,将多个实例包装成一个新实例(数组形式),然后在所有 promise 都被解决后执行一些操作(齐变更再返回)
- 成功:只有全部实例状态变成
fulfilled
( 成功 ),最终状态才会变成fulfilled
- 失败:其中一个实例状态变成
rejected
,最终状态就会变成rejected
- 每一个 promise 成功的值,会按照传入的顺序返回数组内
- 用 all 方法进行接口请求,就算其中有失败的情况,别的请求也会进行,但最后的状态还是
rejected
- Promise.race():赛跑机制将多个实例包装成一个新实例,返回全部实例状态优先变更后的结果(先变更先返回)
- 成功失败:哪个实例率先改变状态就返回哪个实例的状态
- Promise.finally():指定不管最后状态如何都会执行的回调函数
- **Promise.allSettled()**⭐:将多个实例包装成一个新实例,返回全部实例状态变更后的状态数组(齐变更再返回)
- 成功:成员包含
status
和value
,status
为fulfilled
,value
为返回值 - 失败:成员包含
status
和value
,status
为rejected
,value
为错误原因
- Promise.any():将多个实例包装成一个新实例,返回全部实例状态变更后的结果数组(齐变更再返回)
- 成功:其中一个实例状态变成
fulfilled
,最终状态就会变成fulfilled
- 失败:只有全部实例状态变成
rejected
,最终状态才会变成rejected
- Promise.try():不想区分是否同步异步函数,包装函数为实例,使用
then()
指定下一步流程,使用catch()
捕获错误
常见的错误
Uncaught TypeError: undefined is not a promise
如果在控制台中收到 Uncaught TypeError: undefined is not a promise
错误,则请确保使用 new Promise()
而不是 Promise()
UnhandledPromiseRejectionWarning
这意味着调用的 promise 被拒绝,但是没有用于处理错误的 catch。 在 then 之后添加 catch 则可以正确地处理
扩展:手写 Promise
5、数据类型
原始数据类型(基本类型):按值访问,可以操作保存在变量中实际的值。
- 空值(
null
):用于未知的值 —— 只有一个null
值的独立类型。 - 未定义(
undefined
): 用于未定义的值 —— 只有一个undefined
值的独立类型。 - 布尔值(
boolean
):用于true
和false
。 - 数字(
number
):用于任何类型的数字:整数或浮点数,在±(253-1)
范围内的整数。 - 字符串(
string
):用于字符串:一个字符串可以包含 0 个或多个字符,所以没有单独的单字符类型。 - 符号(
symbol
):用于唯一的标识符。 - BigInt:一种数字类型,可以表示任意精度格式的整数
引用类型(复杂数据类型):引用类型的值是保存在内存中的对象。
- 对象(Object)
- 数组对象(Array)
- 函数对象(Function)
- 布尔对象(Boolean)
- 数字对象(Number)
- 字符串对象(String)
- 日期对象(Date)
- 正则对象(RegExp)
- 错误对象(Erro
⚠️ 注意: 与其他语言不同的是,JavaScript 不允许直接访问内存中的位置,也就是说不能直接操作对象的内存空间。在操作对象时,实际上是在操作对象的引用而不是实际的对象。所以引用类型的值是按引用访问的。
6、检测数据类型的常用方法
typeof、instanceof、constructor、Object.prototype.toString.call()
1 .typeof
console.log( typeof 100, //"number" typeof undefined, //"undefined" typeof null, //"object" typeof function () { console.log("aaa"); }, //"function" typeof new Number(100), //'object' typeof new String("abc"), // 'string' typeof new Boolean(true) //'boolean' );
typeof 可以正常检测出:number、boolean、string、object、function、undefined、symbol、bigint
- 检测基本数据类型,null 会检测 object,因为 null 是一个空的引用对象
- 检测复杂数据类型,除 function 外,均为 object
2 . instanceof
instanceof
运算符需要指定一个构造函数,或者说指定一个特定的类型,用来判断这个构造函数的原型是否在给定对象的原型链上
基本数据类型中:Number,String,Boolean。字面量值不可以用 instanceof 检测,但是构造函数创建的值可以
注意:null 和 undefined 都返回了 false,这是因为它们的类型就是自己本身,并不是 Object 创建出来它们,所以返回了 false。
console.log( 100 instanceof Number, //false undefined instanceof Object, //false [1, 2, 3] instanceof Array, //true new Error() instanceof Error //true );
3 .constructor
constructor 是 prototype 对象上的属性,指向构造函数。根据实例对象寻找属性的顺序,若实例对象上没有实例属性或方法时,就去原型链上寻找,因此,实例对象也是能使用 constructor 属性的。可以检测出字面量方式创建的对象类型
如果输出一个类型的实例的 constructor,就如下所示:
console.log(new Number(123).constructor); //ƒ Number() { [native code] }
可以看到它指向了 Number 的构造函数,因此,可以使用num.constructor === Number
来判断一个变量是不是 Number 类型的
除了 undefined 和 null 之外,其他类型都可以通过 constructor 属性来判断类型。
var num = 123; var str = "abcdef"; var bool = true; var arr = [1, 2, 3, 4]; // undefined和null没有constructor属性 console.log( num.constructor === Number, str.constructor === String, bool.constructor === Boolean, arr.constructor === Array ); //所有结果均为true
4 . 使用 Object.prototype.toString.call()检测对象类型⭐
const toString = Object.prototype.toString; toString.call(123); //"[object Number]" toString.call(undefined); //"[object Undefined]" toString.call(null); //"[object Null]" toString.call(/^[a-zA-Z]{5,20}$/); //"[object RegExp]" toString.call(new Error()); //"[object Error]"
可以使用Object.prototype.toString.call(obj).slice(8,-1)
来判断并截取
使用Object.prototype.toString.call()
的方式来判断一个变量的类型是最准确的方法
5 . 自己封装函数
function getType(obj) { const type = typeof obj; if (type !== "object") { return type; } //如果不是object类型的数据,直接用typeof就能判断出来 //如果是object类型数据,准确判断类型必须使用Object.prototype.toString.call(obj)的方式才能判断 return Object.prototype.toString.call(obj).replace(/^\[object (\S+)]$/, "$1"); }
6、isArray
isArray 可以检测出是否为数组
const arr = []; Array.isArray(arr); // true
7、数组的常用方法有哪些
一、操作方法
增
下面前三种是对原数组产生影响的增添方法,第四种则不会对原数组产生影响
push( ) unshift( ) splice( ) concat( )
push()
push()
方法接收任意数量的参数,并将它们添加到数组末尾,返回数组的最新长度
unshift()
unshift()在数组开头添加任意多个值,然后返回新的数组长度
splice
传入三个参数,分别是开始位置、0(要删除的元素数量)、插入的元素,返回的是空数组
concat()
合并
首先会创建一个当前数组的副本,然后再把它的参数添加到副本末尾,最后返回这个新构建的数组,不会影响原始数组
let colors = ["red"].concat("yellow", ["black"]);// ["red", "yellow", "black"]
删
下面三种都会影响原数组,最后一项不影响原数组:
pop() shift() splice() slice()
pop()
pop()
方法用于删除数组的最后一项,同时减少数组的length
值,返回被删除的项
shift()
shift()
方法用于删除数组的第一项,同时减少数组的length
值,返回被删除的项
splice()
传入两个参数,分别是开始位置,删除元素的数量,返回包含删除元素的数组
slice()
slice(开始索引, 结束索引) ,返回一个新数组,不会影响原始数组
改
即修改原来数组的内容,常用splice
splice()
传入三个参数,分别是(开始位置,要删除元素的数量,要插入的任意多个元素),返回删除元素的数组
会改变原数组
查
查找元素,返回元素坐标或者元素值
indexOf( ) includes( ) find( )
indexOf()
返回要查找的元素在数组中的位置,如果没找到则返回 -1
includes()
返回要查找的元素在数组中的位置,找到返回true
,否则false
find()
返回第一个匹配的元素
二、排序方法
reverse()sort()
reverse()
翻转
sort()
排序
function sortArr(a, b) { return a - b; // 升序 return b - a; // 降序 }
三、转换方法
join()
join() 方法接收一个参数,即字符串分隔符,返回包含所有项的字符串,转为字符串
四、迭代方法
常用来迭代数组的方法(除 forEach 外其他都不会对空数组进⾏检测、不会改变原始数组)有如下:
some() every() forEach() filter() map()
some()
对数组每一项都运行传入的测试函数,如果至少有 1 个元素返回 true ,则这个方法返回 true
every()
对数组每一项都运行传入的测试函数,如果所有元素都返回 true ,则这个方法返回 true
forEach()
对数组每一项都运行传入的函数,没有返回值
filter()
对数组每一项都运行传入的函数,函数返回 true
的项会组成数组之后返回
map()
映射 对数组每一项都运行传入的函数,返回由每次函数调用的结果构成的数组
8、深浅拷贝
基本类型数据保存在在栈内存中
引用类型数据保存在堆内存中,引用数据类型的变量是一个指向堆内存中实际对象的引用,存在栈中
深拷贝和浅拷贝的区别
1.浅拷贝: 将原对象或原数组的引用直接赋给新对象,新数组,新对象/数组只是原对象的一个引用。
如果属性是基本类型,拷贝的就是基本类型的值。如果属性是引用类型,拷贝的就是内存地址,即浅拷贝是拷贝一层,深层次的引用类型则共享内存地址
2.深拷贝: 创建一个新的对象和数组,将原对象的各项属性的“值”(数组的所有元素)拷贝过来,是“值”而不是“引用”
浅拷贝
数组浅拷贝:
// 直接遍历 function shallowCopy(arr) { const newArr = []; arr.forEach((item) => newArr.push(item)); return newArr; } // slice const arr2 = [1, 2, 3, 4, 5]; const newArr2 = arr2.slice(); // concat() 合并空数组实现 const arr3 = [11, 22, 33, 44, 55]; const newArr3 = arr3.concat([]);
对象浅拷贝:
// 直接遍历 function shallowCopy(obj) { const newObj = {}; for (let item in obj) { newObj[item] = obj[item]; } return newObj; } // 使用拓展运算符 const obj2 = { name: "Bob", age: 17 }; const newObj2 = { ...obj2 };
深拷贝
用深拷贝最后要递归到全部是基本值,不然可能会陷入死循环/循环引用,导致栈溢出
TODO(待理解)⭐: 处理过的数据使用 map 结构缓存起来 >> 递归的时候碰到相同的数据 >> 直接使用缓存里面的
1. 先转换成字符串,在转换成(数组/对象) JSON.parse(JSON.stringify(XXXX))
有一个缺点 里面的函数不能拷贝
const array = [{ number: 1 }, { number: 2 }, { number: 3 }]; const str = JSON.stringify(array); const copyArray = JSON.parse(str);
2、递归实现简单的深拷贝:
function deepClone(obj = {}) { if (typeof obj !== "object" || obj == null) { // obj 是 null ,或者不是对象和数组,直接返回 return obj; } // 初始化返回结果 let result; obj instanceof Array ? (result = []) : (result = {}); for (let key in obj) { // 保证 key 不是原型的属性 if (obj.hasOwnProperty(key)) { // 递归调用!!! result[key] = deepClone(obj[key]); } } // 返回结果 return result; }
3、解构赋值法实现一层拷贝
4、Object.assign() 实现一层深拷贝
5、 lodash第三方库
小结
前提为拷贝类型为引用类型的情况下:
- 浅拷贝是拷贝一层,属性为对象时,浅拷贝是复制,两个对象指向同一个地址
- 深拷贝是递归拷贝深层次,属性为对象时,深拷贝是新开栈,两个对象指向不同的地址
9、闭包
什么是闭包
通俗地讲闭包就是在一个函数里边再定义一个函数,这个内部函数一直保持有对外部函数中作用域的访问权限(小房间一直可以有大房子的访问权限)
闭包的作用
- 访问其他函数内部变量
- 保护变量不被 JS 的垃圾回收机制回收
- 避免全局变量被污染 方便调用上下文的局部变量 加强封装性
闭包的优点
(一)变量长期驻扎在内存中
(二)另一个就是可以重复使用变量,并且不会造成变量污染
① 全局变量可以重复使用,但是容易造成变量污染。不同的地方定义了相同的全局变量,这样就会产生混乱。
② 局部变量仅在局部作用域内有效,不可以重复使用,不会造成变量污染。
③ 闭包结合了全局变量和局部变量的优点。可以重复使用变量,并且不会造成变量污染
闭包的缺点
由于闭包会使得函数中的变量都被保存在内存中,内存消耗很大,所以不能滥用闭包,否则会造成网页的性能问题,在 IE 中可能导致内存泄露。解决方法是,在退出函数之前,将不使用的局部变量全部删除。