this的指向在函数定义的时候是确定不了的,只有函数执行的时候才能确定this到底指向谁,实际上this的最终指向的是那个调用它的对象
简单描述
稍微完整的描述
在 js 中变量的作用域属于函数作用域,在函数执行完后,作用域会被清理,内存会随之被回收,但是由于闭包函数式建立在函数内部的子函数,由于其可访问上级作用域,即使,上级函数执行完,作用域也不会随之销毁,这时的子函数(也就是闭包),便拥有了访问上级作用域中变量的权限,即使上级函数执行完毕后作用域内的值也不会被销毁。
闭包解决了什么问题
闭包的缺点
在JavaScript中是使用构造函数来新建一个对象的,每一个构造函数的内部都有一个 prototype 属性,它的属性值是一个对象,这个对象包含了可以由该构造函数的所有实例共享的属性和方法。当使用构造函数新建一个对象后,在这个对象的内部将包含一个指针,这个指针指向构造函数的 prototype 属性对应的值,在 ES5 中这个指针被称为对象的原型。一般来说不应该能够获取到这个值的,但是现在浏览器中都实现了 proto 属性来访问这个属性,但是最好不要使用这个属性,因为它不是规范中规定的。ES5 中新增了一个 Object.getPrototypeOf() 方法,可以通过这个方法来获取对象的原型。
当访问一个对象的属性时,如果这个对象内部不存在这个属性,那么它就会去它的原型对象里找这个属性,这个原型对象又会有自己的原型,于是就这样一直找下去,也就是原型链的概念。原型链的尽头一般来说都是 Object.prototype 所以这就是新建的对象为什么能够使用 toString() 等方法的原因。
递归方法:遍历对象、数组直到里边都是基本数据类型,然后再去复制,就是深度拷贝。
回收概念:JavaScript代码运行时,需要分配内存空间来储存变量和值。当变量不在参与运行时,就需要系统收回被占用的内存空间,这就是垃圾回收。
垃圾回收的方式:
//例子
function fun() {
let obj1 = {};
let obj2 = {};
obj1.a = obj2; // obj1 引用 obj2
obj2.a = obj1; // obj2 引用 obj1
}
//解决方法手动释放内存 obj1.a = null obj2.a = null
**优化垃圾回收的方式:**
1. 不再使用的值和对象可以赋值为null。 obj = null。
2. 对象object优化 可以进行逐个删除 delete obj[key]。
3. 数组array优化 讲length赋值为0 arr.length = 0。
4. function中在循环中的函数表达式,如果可以复用,尽量放在函数的外面。
<a name="MK6gJ"></a>
## 7.js中的内存泄漏
1. **闭包**:不合理的使用闭包,从而导致某些变量一直被留在内存当中。
2. **意外的全局变量**:由于使用未声明的变量,而意外的创建了一个全局变量,而使这个变量一直留在内存中无法被回收。
3. **被遗忘的计时器和回调函数**:设置了 setInterval 定时器,而忘记取消它,如果循环函数有对外部变量的引用的话,那么这个变量会被一直留在内存中,而无法被回收。
4. **脱离DOM的引用**:获取一个 DOM 元素的引用,而后面这个元素被删除,由于一直保留了对这个元素的引用,所以它也无法被回收。
5. **被遗忘的事件监听器**:window.addEventListener("resize", this.doSomething) 需要window.removeEventListener("resize", this.doSomething)
6. 如果用**发布订阅模式**做的类似于eventbus的函数需要on之后off取消监听它
7. **遗忘的Map和Set**,因为他们是对引用值的强引用,如果需要做内存优化则需要用weakMap和weakSet来替代他们,让GC回收他们
<a name="j6LrQ"></a>
## 8.Js中的evet loop(事件循环)
**事件循环**:会将**同步代码**按顺序排在某个地方(**执行栈**),然后**依次**执行里面的函数。当遇到**异步**任务的时候,就交给**其他线程**去处理,等待当前执行栈里面的**所有同步代码**执行**完成**后,从一个队列中去取出**已完成的异步任务的回调**,加入执行栈继续执行,遇到异步任务又交给其他线程,然后放入执行栈里面再来执行。通过这样的一个循环来执行完整个的代码,这就叫做**事件循环**,每一次的**循环**其实就是一个**事件周期,**或称作一次tick。
- 事件循环中**宏任务**、**微任务**:
- **宏任务(macro task)**:setTimeout,setInterval,setImmediate等主代码块,请求任务,常见的鼠标点击事件和键 盘按下事件。
- **特征:宏任务有明确异步任务需要执行和回调,需要其他异步线程去支持**。如**setTimeout**需要**定时器线程**去支持,**Ajax**的‘发送请求’任务,需要**Http线程**去支持执行。
- **微任务(micro task)**:promise.then()、promise.catch()、 new MutaionObserver()、process.nextTick()。
- **特征:微任务没有明确的异步任务需要执行,只有回调,不需要其他异步线程去支持。**
```javascript
console.log('同步代码1');
setTimeout(()=>{
console.log('setTimeout');//异步但为宏任务
},0)
new Promise((re)=>{
console.log('同步代码2')
re();
}).then(()=>{
console.log('promise.then')//异步但为微任务
})
console.log('同步代码3');
/**
最后输出:
同步代码1
同步代码2
同步代码3
promise.then
setTimeout
**/
防抖:将几次操作合并为一此操作进行。原理是维护一个计时器,规定在delay时间后触发函数,但是在delay(延迟)时间内再次触发的话,就会取消之前的计时器而重新设置。这样一来,只有最后一次操作能被触发。
// func是用户传入需要防抖的函数
// wait是等待时间
const debounce = (func, wait = 50) => {
// 缓存一个定时器id
let timer = 0;
// 这里返回的函数是每次用户实际调用的防抖函数
// 如果已经设定过定时器了就清空上一次的定时器
// 开始一个新的定时器,延迟执行用户传入的方法
return function (...args) {
if (timer) clearTimeout(timer)
timer = setTimeout(() => {
func.apply(this, args)
}, wait)
}
}
debounce(fun,500);
节流:使得一定时间内只触发一次函数。原理是通过判断是否到达一定时间来触发函数。
function throttle(fun, delay = 500) {
let last, deferTimer
return function (args) {
let that = this
let _args = arguments
let now = +new Date()
if (last && now < last + delay) {
clearTimeout(deferTimer)
deferTimer = setTimeout(function () {
last = now
fun.apply(that, _args)
}, delay)
}else {
last = now
fun.apply(that,_args)
}
}
}
throttle(ajax, 1000)
<a name="ukFrR"></a>
## 10.Js中的异步及Promise
<a name="uqYAW"></a>
### javascript中的异步实现
- **回调方式**:使用回调函数的方式有一个缺点是,多个回调函数嵌套的时候会造成回调函数地狱,上下两层的回调函数间的代码耦合度太高,不利于代码的可维护。
- **Promise**:使用 Promise 的方式可以将嵌套的回调函数作为链式调用。但是使用这种方法,有时会造成多个 then 的链式调用,可能会造成代码的语义不够明确。
- **generator**:它可以在函数的执行过程中,将函数的执行权转移出去,在函数外部还可以将执行权转移回来。当遇到异步函数执行的时候,将函数执行权转移出去,当异步函数执行完毕时再将执行权给转移回来。因此在 generator 内部对于异步操作的方式,可以以**同步**的顺序来书写。
```javascript
// 使用 * 表示这是一个 Generator 函数
// 内部可以通过 yield 暂停代码
// 通过调用 next 恢复执行
function* test() {
let a = 1 + 2;
yield 2;
yield 3;
}
let b = test();
console.log(b.next()); // > { value: 2, done: false }
console.log(b.next()); // > { value: 3, done: false }
console.log(b.next()); // > { value: undefined, done: true }
console.log('script start') //1. 打印 script start
setTimeout(function(){
console.log('settimeout') // 4. 打印 settimeout
}) // 2. 调用 setTimeout 函数,并定义其完成后执行的回调函数
console.log('script end') //3. 打印 script start
// 输出顺序:script start->script end->settimeout
对promise的理解:
Promise是异步编程的一种解决方案,它是一个对象,可以获取异步操作的消息,他的出现大大改善了异步编程的困境,避免了地狱回调,它比传统的解决方案回调函数和事件更合理和更强大。
promise的状态和过程:
当把一件事情交给promise时,它的状态就是Pending,任务完成了状态就变成了Resolved、没有完成失败了就变成了Rejected。
注意:一旦从进行状态变成为其他状态就永远不能更改状态了。
对promise的特点:
对promise的缺点:
Promise本身是同步的立即执行函数, 当在executor中执行resolve或者reject的时候, 此时是异步操作, 会先执行then/catch等,当主栈完成后,才会去调用resolve/reject中存放的方法执行。
console.log('script start')
setTimeout(function(){
console.log('settimeout')
})
let promise1 = new Promise(function (resolve) {
console.log('promise1')
resolve()
console.log('promise1 end')
}).then(function () {
console.log('promise2')
})
console.log('script end')
// 输出顺序: script start->promise1->promise1 end->script end->promise2->settimeout
当js主线程执行到异步任务时:
总结:
Promise 对象是异步编程的一种解决方案,最早由社区提出。Promise 是一个构造函数,接收一个函数作为参数,返回一个 Promise 实例。一个 Promise 实例有三种状态,分别是pending、resolved 和 rejected,分别代表了进行中、已成功和已失败。实例的状态只能由 pending 转变 resolved 或者rejected 状态,并且状态一经改变,就凝固了,无法再被改变了。
状态的改变是通过 resolve() 和 reject() 函数来实现的,可以在异步操作结束后调用这两个函数改变 Promise 实例的状态,它的原型上定义了一个 then 方法,使用这个 then 方法可以为两个状态的改变注册回调函数。这个回调函数属于微任务,会在本轮事件循环的末尾执行。
Promise的方法:
Promise有五个常用的方法:then()、catch()、all()、allSettled()、race()、finally。
//第一种普通书写方式:
const promise = new Promise(function(resolve, reject) {
// ... some code
if (/* 异步操作成功 */){
resolve(value);
} else {
reject(error);
}
});
promise.then(data=>{
console.log(data)
},err=>{
console.log(err)
}).finally(()=>{//不管最后状态如何,都会执行这个函数。
})
//等同于
promise.then(data=>{
console.log(data);
}).catch(err=>{
console.log(err);
}).finally(()=>{////不管最后状态如何,都会执行这个函数。
})
Promise.all和Promise.race的区别的使用场景
Promise.all可以将多个Promise实例包装成一个新的promise实例,同时,成功和失败的返回值是不同的,成功的时候返回的是一个结果数组,而失败返回的是最先被reject失败状态的值。
Promise.all中传入的是数组,返回的也是是数组,并且会将进行映射,传入的promise对象返回的值是按照顺序在数组中排列的,但是注意的是他们执行的顺序并不是按照顺序的,除非可迭代对象为空。
需要注意,Promise.all获得的成功结果的数组里面的数据顺序和Promise.all接收到的数组顺序是一致的,这样当遇到发送多个请求并根据请求顺序获取和使用数据的场景,就可以使用Promise.all来解决。
顾名思义,Promse.race就是赛跑的意思,意思就是说,Promise.race([p1, p2, p3])里面哪个结果获得的快,就返回那个结果,不管结果本身是成功状态还是失败状态。当要做一件事,超过多少时间就不做了,可以用这个方法来解决。
Promise.race([promise1,timeOutPromise(5000)]).then(res=>{})
Promise.allSettled()与Promise.all()的区别
//allSettled
0:
status: "fulfilled"
value: "p1"
__proto__: Object
1:
status: "fulfilled"
value: "p2"
__proto__: Object
length: 2
__proto__: Array(0)
//all ["p1", "p2"]
2. 如果是all()的话,如果有一个Promise对象报错了,则all()无法执行,会报错你的错误,无法获得其他成功的数据。则allSettled()方法是不管有没有报错,把所有的Promise实例的数据都返回回来,放入到一个对象中。如果是resolve的数据则status值为fulfilled,相反则为rejected。
<a name="hrv4U"></a>
#### (3) Async/Await
async/await其实就是generator的 语法糖,它能实现的效果都能用then链来实现,它是为优化then链而开发出来的。从字面上来看,async是“异步”的简写,await则为等待,所以很好理解async 用于申明一个 function 是异步的,而 await 用于等待一个异步方法执行完成。
async 函数返回一个 Promise 对象,当函数执行的时候,一旦遇到 await 就会先返回,等到触发的异步操作完成,再执行函数体内后面的语句。可以理解为,是让出了线程,跳出了 async 函数体。
await的含义为等待,也就是 async 函数需要s等待await后的函数执行完成并且有了返回结果(Promise对象)之后,才能继续执行下面的代码。await通过返回一个Promise对象来实现同步的效果。
**async/await对比Promise的优势**
- 代码阅读和书写起来更加同步代码化,会减轻阅读负担
- 传递中间值很方便,不像Promise.then一样麻烦
- 错误处理比较友好,可以用try catch
- 调试比较优化,更加容易打断点调试
<a name="zA7Zc"></a>
## 11.什么是函数柯里化?
能把一个带有**多个参数**的函数转换成**一系列的嵌套函数**。它**返回一个新函数**,这个**新函数期望传入下一个参数**。<br />它不断地返回新函数(像我们之前讲的,这个新函数期望当前的参数),直到所有的参数都被使用。参数会一直保持 alive (通过**闭包**),当柯里化函数链中最后一个函数被返回和调用的时候,它们会用于执行。<br />优点是:通常可用于在不侵入函数的前提下,为函数**预置通用参数**,供多次重复调用。
```javascript
const add = function add(x) {
return function (y) {
return x + y
}
}
const add1 = add(1)
add1(2) === 3
add1(20) === 21
1.
//利用绝对定位,先将元素的左上角通过 top:50%和 left:50%定位到页面的中心,
//然后再通过 translate 来调整元素的中心点到页面的中心。该方法需要考虑浏览器兼容问题。
.parent {
position: relative;
}
.child {
position: absolute;
left: 50%;
top: 50%;
transform: translate(-50%,-50%);
}
2.
//利用绝对定位,设置四个方向的值都为 0,并将 margin 设置为 auto,
//由于宽高固定,因此对应方向实现平分,可以实现水平和垂直方向上的居中。该
//方法适用于盒子有宽高的情况:
.parent {
position: relative;
}
.child {
position: absolute;
top: 0;
bottom: 0;
left: 0;
right: 0;
margin: auto;
}
3.
//利用绝对定位,先将元素的左上角通过 top:50%和 left:50%定位到页面的中心
//,然后再通过 margin 负值来调整元素的中心点到页面的中心。该方法适用于盒子宽高已知的情况
.parent {
position: relative;
}
.child {
position: absolute;
top: 50%;
left: 50%;
margin-top: -50px; /* 自身 height 的一半 */
margin-left: -50px; /* 自身 width 的一半 */
}
4.
//使用 flex 布局,通过 align-items:center 和 justify-content:center
//设置容器的垂直和水平方向上为居中对齐,然后它的子元素也可以实现垂直和水平的居中。
//该方法要**考虑兼容的问题**,该方法在移动端用的较多:
.parent {
display: flex;
justify-content:center;
align-items:center;
}
5.
//另外,如果父元素设置了flex布局,只需要给子元素加上`margin:auto;`
//就可以实现垂直居中布局
.parent{
display:flex;
}
.child{
margin: auto;
}
React并不是将click事件绑定到了div的真实DOM上,而是在document(react 17 是在root根节点上)处监听了所有的事件,当事件发生并且冒泡到document处的时候,React将事件内容封装并交由真正的处理函数运行(是由react自己实现的合成事件(SyntheticEvent))进行处理。这样的方式不仅仅减少了内存的消耗,还能在组件挂在销毁时统一订阅和移除事件。
兼容其他浏览器,更好的跨平台;
将事件统一存放在一个数组,避免频繁的新增与删除(垃圾回收)。
在React底层,主要对合成事件做了两件事:
事件委派:React会把所有的事件绑定到结构的最外层,使用统一的事件监听器,这个事件监听器上维持了一个映射来保存所有组件内部事件监听和处理函数。
自动绑定:React组件中,每个方法的上下文都会指向该组件的实例,即自动绑定this为当前组件。
在react中现在return false无法阻止默认事件了(官网有说),阻止默认事件e.preventDefault ,阻止冒泡事件e.stopPropagation。
阻止document(root)上的原生事件:e.nativeEvent.stopImmediatePropagation()
要想阻止所有的冒泡事件,只能通过ref获得dom节点监听,用 原生事件对象e的e.stopPropagation()去阻止冒泡(除非是本身dom节点的其他原生监听事件 比如:dom.addeventlister('click', ...))
e.target.matches方法也可以用来解决事件冒泡。
命名不一样,合成事件是小驼峰,原生是非驼峰
事件的值不同,合成事件是函数,原生是字符串
react 事件不能采用 return false 的方式来阻止浏览器的默认行为,而必须要地明确地调用preventDefault()
来阻止默认行为。
事件的执行顺序为原生事件先执行,合成事件后执行,合成事件会冒泡绑定到 document 上,所以尽量避免原生事件与合成事件混用,如果原生事件阻止冒泡,可能会导致合成事件不执行,因为需要冒泡到document 上合成事件才会执行。
https://juejin.cn/post/6939766434159394830
每个 hook 都会有一个 next 指针,hook 对象之间以单向链表的形式相互串联, 同时也能发现 useState 底层依然是 useReducer ,所以总结一下初始化阶段构建链表,更新阶段按照顺序去遍历之前构建好的链表,取出对应的数据信息进行渲染,(类似于数组索引值)当两次顺序不一样的时候就会造成渲染上的差异。
//这样,通过 _hooks['key'] 来查找,就无所谓前序的 Hook 出现的任何意外情况了。
const [value, setValue] = useState(0, "key2");
这样,通过 _hooks['key'] 来查找,就无所谓前序的 Hook 出现的任何意外情况了。
import React, { useState } from 'react'
// 子组件
function Child({ userInfo }) {
// render: 初始化 state
// re-render: 只恢复初始化的 state 值,不会再重新设置新的值
// 只能用 setName 修改
const [ name, setName ] = useState(userInfo.name)
return <div>
<p>Child, props name: {userInfo.name}</p>
<p>Child, state name: {name}</p>
</div>
}
function App() {
const [name, setName] = useState('双越')
const userInfo = { name }
return <div>
<div>
Parent
<button onClick={() => setName('慕课网')}>setName</button>
</div>
<Child userInfo={userInfo}/>
</div>
}
export default App
点击setName后:
只会改变props name 不会改变state name
re-render: 只恢复初始化的 state 值,不会再重新设置新的值,只能用 setName 修改
包括interval
import React, { useState, useRef, useEffect } from 'react'
function UseEffectChangeState() {
const [count, setCount] = useState(0)
// 模拟 DidMount
const countRef = useRef(0)
let countA = 0;
useEffect(() => {
console.log('useEffect...', count)
// 定时任务
const timer = setInterval(() => {
console.log('setInterval...', countRef.current)
setCount(count + 1)
//两种方法都可行
// setCount(++countRef.current)
// setCount(++countA)
}, 1000)
// 清除定时任务
return () => clearTimeout(timer)
}, []) // 依赖为 []
// 依赖为 [] 时: re-render 不会重新执行 effect 函数
// 没有依赖:re-render 会重新执行 effect 函数
return <div>count: {count}</div>
}
export default UseEffectChangeState
如果依赖项是[],一直返回的都是0,如果删掉[]依赖项或者[count]的话就是正常的累加
解决方法: