js相关知识复习(上)
写在前面:
这篇博文偏应用,改天再写篇概念性的。该篇幅内容包括:减少 DOM 操作,时间委托,冒泡事件,Promise 相关, 微任务宏任务,作用域,变量提升,闭包,基本数据类型和检测方法,深浅拷贝,原型和作用域链后续争取把 js 重点都记录上,深入浅出。
DOM 操作
- 使用文档碎片减少 DOM 操作
- 冒泡事件:stopPropagation();阻止向上冒泡
- 事件委托 :减少 DOM 请求次数
使用文档碎片减少 DOM 操作
假设在一个 ul 中,如果持续添加一百次插入 li 的 DOM 操作,那这个过程无疑是非常消耗内存的,这时不妨使用文档碎片 fragment
,将插入的 100 次操作存储在这,结束后在一并添加到 ul 中,这样 DOM 操作从原本的 100 次变为一次。
1 | <ul id="list"></ul> |
1 | const list = document.getElementById('list') |
阻止冒泡事件
依旧举个栗子:当点击一个 li , 他没有绑定事件处理函数,那么他会往父级元素(一直往上找直到 body)看有没有事件处理函数,有的话同样触发。
这样可以简便的为子元素绑定处理事件,但弊端也很明显:如果父元素有处理函数,即使你给子元素绑定了处理事件,那么在触发子元素的处理事件同时,还会向上冒泡在触发父元素的处理函数!!这就需要到 stopPropagation();
阻止冒泡行为。
1 | <div id="one"> |
1 | const p3 = document.getElementById('p3'); |
在没有绑定阻止冒泡时,给了 body,第一个父元素和第三个子元素绑定处理事件,点击第三个会触发第三个和 body 的,点击第一个会触发父元素和 body 的。如果只想触发第三个事件绑定,可将 p3 的触发函数修改为:传入 event,利用 event 下的 stopPropagation();
方法。
1 | bindEvent(p3, 'click', function (event) { |
事件委托
事件指的是:不在要发生的事件(直接 dom)上设置监听函数,而是在其父元素上设置监听函数,通过事件冒泡,监听子元素,来做出不同的响应。
例如在 ul 中有这么五个小 li ,要给他们绑定点击事件,可获取他们标签名后,利用 slice 方法对 li 的类数组转化为数组,在对其中的 li 添加注册事件。
1 | <ul id="list"> |
1 | const lis = document.getElementsByTagName('li') |
要是有 500 个 li 呢,那岂不是遍历绑定 500 次,这产生的事件监听器非常消耗内存。这时可以找到其父元素,设置事件监听器,利用 target
给 li 绑定事件监听。
1 | // 绑定事件处理函数 |
这样,即使不循环遍历 ul 下的 li ,也能通过对 ul 的绑定打印出子元素 li 的信息。
异步
- 使用 promise 加载一张图片
- then,catch 改变 promise 的状态
- async , await , 和 Promise
- 微任务和宏任务
使用 promise 加载一张图片
考验对 promise 的基本使用情况。思路:实例化一个 promise 对象,对图片的加载分别成功与否用resolve, reject
处理。
1 | function loadImage(src) { |
使用 then,catch 改变 promise 的状态
先说结论:
then 和 catch 正常返回的时候,Promise 状态是 fulfilled ,抛出错误的时候,Promise 状态是 rejected。
fulfilled 状态的 Promise 执行 then 的回调,而 rejected 则执行 catch 的回调。
怎么理解?看下面例子:
1 | const p1 = Promise.resolve() |
首先定义一个 resolve 返回值的 promise 对象,打印显示状态为 fulfilled ,保存当前信息在打印走 catch 还是 then;然后在手动跑出个错误,打印当前状态,在判断走 catch 还是 then。结果显示:
再回过头看先前的结论,发现也不难理解。reject 类型的演示也差不多,结论见上。
async , await , 和 Promise
这里也先说这三者的结论:
执行 async 函数返回的都是 Promise 对象;promise.then 的情况对应 await ;promise.catch 异常情况对应 try…catch。
1 | // async结论 :async返回的都是Promise对象 |
例子比较通俗易懂,配合结果应该能更清晰的理解结论。
微任务和宏任务
宏任务有:setTimeout , setInterval , Dom 事件,AJAX 请求
微任务有:Promise ,async / await
微任务会比宏任务先执行,具体可搜该类型的答应题目进行深入理解。
作用域,变量提升,和闭包
关于作用域,有全局和局部(函数作用域,块级作用域 ES6 才有),局部能访问全局,全局不能访问局部。在查找变量的时候,变量的父子级关系或者查找的过程就是一条作用域链。
来个设计之处的小 BUG:按理来说全局作用下不能访问局部变量,但事实好像是:JS 在设计之初的好像压根就没把块级作用域的事情考虑进去,直到后来 ES6 出 let 和 const 代替 var 关键字来解决这一现象。
1 | if (true) { |
上述乱使用 var 会找出变量污染,在 let 和 const 没出之前,用函数声明作用域可解决上述现象。实际业务中基本不会使用 var,取而代之的 const 和 let。这也是JS 模块化的体现。
问题来了,如果业务中就是要引用某局部作用域的变量呢,那怎么办?这时候闭包的作用就体现了,往下看
作用域声明里,还有个知识点:变量提升,这有关 JS 编译执行过程,挺有意思,要理解这得多看几道作用域输出变量的题(下面这张我是去牛客网刷 js 专项训练截来的)闭包和匿名函数不要搞混!那么什么是闭包,红宝书说:闭包是有权访问另一个函数作用域的变量函数。也就是说,局部作用域之间并非是不能互相访问的,可以通过闭包来访问局部的变量。下面举俩个小例子:
1 | // 函数作为返回值,在定义的地方向上找变量,而不是再调用的地方 |
就一句话,闭包调用的函数,变量在定义的地方向上找,而不是再调用的地方
为什么要使用闭包: 闭包可以缓存结果不释放变量内存,方便下次调用直接拿取。 因为正常情况下,函数执行完存储在内存的变量会被销毁,再次调用该函数需要重新计算。假设这函数计算量大且耗时,每次调用都得重新计算,那将会浪费内存,影响用户体验,这时就需要道闭包。
闭包的弊端:由于闭包的变量被保存在内存中,所以内存消耗很大,容易导致内存泄漏;容易改变父函数内部变量的值。
变量类型,深浅拷贝
变量类型有:基本类型(String、Number、Boolean、Symbol、Undefined、Null,BigInt),和引用类型(object , array ,function )。原始类型的存储改变发生在桟中执行,使用具体数值直接存储;引用类型则在堆中,且存储的数据是地址
而非具体数值。
顺带说下类型的判断:
- typeof:分不清普通对象(typeof null =object)还是数组对象( typeof []=object)
- instanceof:用来测 A 是不是 B 的原型的,能清楚判断对象类型但也只能测对象类型
- constructor: 利用创建时产生的原型链追溯类型,null 和 undefined 是无效的对象,所以测不了
- Object.prototype.toString.call():都能测
1 | //类型判断 |
在上面的例子中,原始类型的变量有私有内存各自存储数值,一方的改变不会牵动自身;引用类型 d 指向 c 的存储地址,c 值改变后,d 用的依旧是 c 的地址所以输出 21。
上面其实引出了深拷贝和浅拷贝的概念,浅拷贝:复制的同时一方变另一放跟着变(修改的是堆内存中的同一个值),深拷贝,完全复制了一个对象,一方的改变不会牵动另一方(修改堆的不同的值)。
如何实现浅拷贝?
如果是数组,可以使用slice,concat,Array.from , 展开运算符
,来返回一个新数组的特性实现拷贝。但是如果数组嵌套了其他引用类型, concat
方法将复制不完整。
原理:
1 | function copy(obj) { |
例子:
1 | var obj = { |
深拷贝才是重点:JSON.parse(JSON.stringify())
(如果原对象中有undefined、Symbol、函数
时,会导致该键值被丢失,有正则
,会被转换为空对象{}
,有Date
,会被转换成字符串)
_.cloneDeep
: lodash 的 Api,直接克隆
1 | var new_arr = JSON.parse(JSON.stringify(old_arr)) |
原理是 JOSN 对象中的 stringify 可以把一个 js 对象序列化为一个 JSON 字符串,parse 可以把 JSON 字符串反序列化为一个 js 对象,通过这两个方法,也可以实现对象的深复制,但是这个方法不能够拷贝函数 。
手写一个深拷贝(巩固理解):
1 | function cloneDeep(obj){ |
深拷贝是拷贝对象各个层级的属性,看对象的属性是否是对象或数组类型,进行相应属性的一一复制。
原型和原型链
原型:每一个对象都会从原型继承属性或方法
官话:每一个 JavaScript 对象在创建的时候就会预制管理另一个对象,这个对象就是我们所说的原型。
大白话:
构造函数内部有个 prototype 属性,通过这个属性能访问到原型,这里的原型就是指构造函数.prototype 。
前面也说了,原型是预制管理一个对象,那么那个对象是谁呢?其实是构造函数 new 出来的实例对象,好了,现在有三个角色:构造函数,原型,实例。
它们是怎么联系的呢?官方设定,实例不能直接访问构造函数,要不设计原型干嘛。
1 | function Person() {} |
说的再多不如结合图看遍代码。
构造函数通过原型的方式添加属性,成功调用实例,说明 1.实例确实是通过原型访问到构造函数的属性或方法的。2.通过 new 出来的实例可以继承原型方法,instanceof 足以说明两者关系。
现在,你知道上述基础类型判断里的instanceof
和 constructor
为什么能判断对象类型了吧,还是因为原型!
清楚了原型和实例的关系后,更能透彻理解 new 实例化的过程中发生了什么,具体可见我的 js 复习下篇。
原型链
每个对象都有属于自己的隐式原型 __proto__
,每个函数或方法都有自己的显示原型 prototype
。实例对象继承使用原型方法的时候通过隐式原型向上查找,如果在原型的显示原型中找到,就不用在通过原型的隐式原型再向上找。
这么说有点拗口,其实蛮好理解的。下面通过一张图说明:
如图所示,实例 teacher 要想调用 teach 方法,首先通过显示原型查找有没有 teach 这个方法,发现没有就通过 _proto_
隐式原型查找他的上一级原型(Teacher.prototype),看他有没有。Teacher 同样先看自己的显示原型,发现有就返回。同理,如果要找 drink 方法,则还要通过 _proto_
再向上找原型(Person.prototype)看他有没有,找到就层层返回。
如果找到尽头都没有那个方法,就回返回 null。这样,一条完整的方法查找路径就是原型链。
ES6 新增特性
let 和 const(看变量提升,块级作用域,能否重定义),以及 Symbol,Bigint 变量。
解构赋值:let arr = [1,2,3,4,5]; const [a,b,c,d,e] = arr; 业务中很常用
模块化:export default { 暴露的对象 } 导出; import { a } from ‘暴露的对象’ 导入
拓展运算符(…):可以将多个数组或对象解构形成一个新元素(在去重,浅拷贝,解构很常用) :
const c = [...new Set([...a,...b])];
箭头函数:()=>{} ,箭头函数和普通函数的区别(this,实例化,argument)
扁平化数组:**Object.values(数组).flat(Infinity)**,Infinity 为数组的维度,flat 不支持 IE 浏览器
模版字符串:let str3=
我爱中国的${str2}
数组新增的 api:原来的:传送门 新增:(includes,startsWith,endsWith)
双问号
??
:作用就是判断该对象有没有默认值,没有就采取问号后面的值,比如输入框非空判断if((value??'') !== '')
,?.
则是判断该对象有无该属性或方法obj?.eat
,没有就返回 undefined,不会报错,在业务中算是常用),(在 TS 中,还有!.
就告诉编译器,一定有这属性,别给我报这属性可能不存在的错)