React.js 面试真题
★★★ React 事件绑定原理
其实react并不是将click事件绑定到了div的真实DOM上,而是在document处监听了所有的事件,当事件发生并且冒泡到document处的时候,React将事件内容封装并交由真正的处理函数运行。这样的方式不仅仅减少了内存的消耗,还能在组件挂在销毁时统一订阅和移除事件。
/*
一、react并没有使用原生的浏览器事件,而是在基于Virtual DOM的基础上实现了合成事件,采用小驼峰命名法,默认的事件传播方式是冒泡,如果想改为捕获的话,直接在事件名后面加上Capture即可;事件对象event也不是原生事件对象,而是合成对象,但通过nativeEvent属性可以访问原生事件对象;
二、react合成事件主要分为以下三个过程:
1、事件注册
在该阶段主要做了两件事:document上注册、存储事件回调。所有事件都会注册到document上,拥有统一的回调函数dispatchEvent来执行事件分发,类似于document.addEventListener(“click”,dispatchEvent)。
register:
addEventListener-click
addEventListener-change
listenerBank:
{
click: {key1: fn1, key2: fn2},
change: {key1: fn3, key3: fn4}
}
2、事件合成
事件触发后,会执行一下过程:
(1)进入统一的事件分发函数dispatchEvent;
(2)找到触发事件的 ReactDOMComponent;
(3)开始事件的合成;
—— 根据当前事件类型生成指定的合成对象
—— 封装原生事件和冒泡机制
—— 查找当前元素以及他所有父级
—— 在listenerBank根据key值查找事件回调并合成到 event(合成事件结束)
3、批处理
批量处理合成事件内的回调函数
*/
★★★ React中的 setState 缺点是什么呢
/*
setState执行的时候可以简单的认为,隶属于原生js执行的空间,那么就是属于同步,被react处理过的空间属于异步,这其实也是一种性能的优化,如果多次使用setState修改值,那么在异步中会先进行合并,再进行渲染,降低了操作dom的次数,具体如下:
(1)setState 在合成事件和钩子函数中是“异步”的,在原生事件和 `setTimeout` 中都是同步的。
(2)setState的“异步”并不是说内部由异步代码实现,其实本身执行的过程和代码都是同步的,只是合成事件和钩子函数的调用顺序在更新之前,导致在合成事件和钩子函数中没法立马拿到更新后的值,形式了所谓的“异步”,当然可以通过第二个参数 setState(partialState, callback) 中的callback拿到更新后的结果。
(3)setState 的批量更新优化也是建立在“异步”(合成事件、钩子函数)之上的,在原生事件和setTimeout 中不会批量更新,在“异步”中如果对同一个值进行多次 setState, setState 的批量更新策略会对其进行覆盖,取最后一次的执行,如果是同时 setState 多个不同的值,在更新时会对其进行合并批量更新。
(4)正是由于setState存在异步的机制,如果setState修改值的时候依赖于state本身的值,有时候并不可靠,这时候我们需要传入一个回调函数作为其入参,这个回调函数的第一个参数为更新前的state值。
*/
★★★ React组件通信如何实现
/*
react本身:
(1)props——父组件向子组件通过props传参
(2)实例方法——在父组件中可以用 refs 引用子组件,之后就可以调用子组件的实例方法了
(3)回调函数——用于子组件向父组件通信,子组件调用props传递过来的方法
(4)状态提升——两个子组件可以通过父组件定义的参数进行传参
(5)Context上下文——一般用作全局主题
(6)Render Props——渲染的细节由父组件控制
状态管理:
(1) mobx/redux/dva——通过在view中触发action,改变state,进而改变其他组件的view
*/
★★★ 类组件和函数组件的区别
/*
(1)语法上:函数组件是一个函数,返回一个jsx元素,而类组件是用es6语法糖class定义,继承component这个类
(2)类组件中可以通过state进行状态管理,而在函数组件中不能使用setState(),在react16.8以后,函数组件可以通过hooks中的useState来模拟类组件中的状态管理;
(3)类组件中有一系列的生命周期钩子函数,在函数组件中也需要借助hooks来使用生命周期函数;
(4)类组件能够捕获最新的值(永远保持一致),这是因为当实例的props属性发生修改时,class组件能够直接通过this捕获到组件最新的props;而函数式组件是捕获渲染所使用的值,已经因为javascript闭包的特性,之前的props参数保存在内存之中,无法从外部进行修改。
*/
★★★ 请你说说React的路由是什么?
/*
路由分为前端路由和后端路由,后端路由是服务器根据用户发起的请求而返回不同内容,前端路由是客户端根据不同的URL去切换组件;在web应用前端开发中,路由系统是最核心的部分,当页面的URL发生改变时,页面的显示结果可以根据URL的变化而变化,但是页面不会刷新。
react生态中路由通常是使用react-router来进行配置,其主要构成为:
(1)Router——对应路由的两种模式,包括与;
(2)route matching组件——控制路径对应的显示组件,可以进行同步加载和异步加载,;
(3)navigation组件——用做路由切换和跳转,;
BrowserRouter与HashRouter的区别:
(1)底层原理不一样:BrowserRouter使用的是H5的history API,不兼容IE9及以下版本;HashRouter使用的是URL的哈希值;
(2)path表现形式不一样:BrowserRouter的路径中没有#,例如:localhost:3000/demo/test;HashRouter的路径包含#,例如:localhost:3000/#/demo/test;
(3)刷新后对路由state参数的影响:BrowserRouter没有任何影响,因为state保存在history对象中;HashRouter刷新后会导致路由state参数的丢失;
*/
★★★★★ React有哪些性能优化的手段?
/*
1、使用纯组件;
2、使用 React.memo 进行组件记忆(React.memo 是一个高阶组件),对于相同的输入,不重复执行;
3、如果是类组件,使用 shouldComponentUpdate(这是在重新渲染组件之前触发的其中一个生命周期事件)生命周期事件,可以利用此事件来决定何时需要重新渲染组件;
4、路由懒加载;
5、使用 React Fragments 避免额外标记;
6、不要使用内联函数定义(如果我们使用内联函数,则每次调用“render”函数时都会创建一个新的函数实例);
7、避免在Willxxx系列的生命周期中进行异步请求,操作dom等;
8、如果是类组件,事件函数在Constructor中绑定bind改变this指向;
9、避免使用内联样式属性;
10、优化 React 中的条件渲染;
11、不要在 render 方法中导出数据;
12、列表渲染的时候加key;
13、在函数组件中使用useCallback和useMemo来进行组件优化,依赖没有变化的话,不重复执行;
14、类组件中使用immutable对象;
*/
★★★★ React hooks 用过吗,为什么要用?
/*
Hooks 是React在16.8版本中出的一个新功能,本质是一种函数,可以实现组件逻辑复用,让我们在函数式组件中使用类组件中的状态、生命周期等功能,hooks的名字都是以use开头。
react:
1、useState——创建状态
接收一个参数作为初始值;返回一个数组,第一个值为状态,第二个值为改变状态的函数
2、useEffect——副作用(数据获取、dom操作影响页面——在渲染结束之后执行
(1)第一个参数为函数,第二个参数为依赖列表,只有依赖更新时才会执行函数;返回一个函数,当页面刷新的时候先执行返回函数再执行参数函数
(2)如果不接受第二个参数,那么在第一次渲染完成之后和每次更新渲染页面的时候,都会调用useEffect的回调函数
3、useRef
返回一个可变的ref对象,此索引在整个生命周期中保持不变。可以用来获取元素或组件的实例,用来做输入框的聚焦或者动画的触发。
4、useMemo——优化函数组件中的功能函数——在渲染期间执行
(1)接收一个函数作为参数,同样接收第二个参数作为依赖列表,返回值可以是任何,函数、对象等都可以
(2)这种优化有助于避免在每次渲染时都进行高开销的计算,仅会在某个依赖项改变时才重新计算
5、useContext——获取上下文注入的值
(1)接受一个context 对象,并返回该对象<MyContext.Provider> 元素的 value值;
const value = useContext(MyContext);
6、useLayoutEffect——有DOM操作的副作用——在DOM更新之后执行
和useEffet类似,但是执行时机不同,useLayoutEffect在DOM更新之后执行,useEffect在render渲染结束后执行,也就是说useLayoutEffect比useEffect先执行,这是因为DOM更新之后,渲染才结束或者渲染还会结束
7、useCallback——与useMemo类似
useMemo与useCallback相同,接收一个函数作为参数,也同样接收第二个参数作为依赖列表;useCallback是对传过来的回调函数优化,返回的是一个函数
react-router:
被route包裹的组件,可以直接使用props进行路由相关操作,但是没有被route包裹的组件只能用withRouter高阶组件修饰或者使用hooks进行操作
1、useHistory——跳转路由
2、useLocation——得到url对象
3、useParams——得到url上的参数
react-redux:
1、useSelector——共享状态——从redux的store中提取数据
2、useDispatch——共享状态——返回redux的store中对dispatch的引用
*/
★★★★ 虚拟DOM的优劣如何?实现原理?
/*
虚拟dom是用js模拟一颗dom树,放在浏览器内存中,相当于在js和真实dom中加了一个缓存,利用dom diff算法避免了没有必要的dom操作,从而提高性能。
优点:
(1)虚拟DOM具有批处理和高效的Diff算法,最终表现在DOM上的修改只是变更的部分,可以保证非常高效的渲染,优化性能;
(2)虚拟DOM不会立马进行排版与重绘操作,对虚拟DOM进行频繁修改,最后一次性比较并修改真实DOM中需要改的部分;
(3)虚拟 DOM 有效降低大面积真实 DOM 的重绘与排版,因为最终与真实 DOM 比较差异,可以只渲染局部;
缺点:
(1)首次渲染大量DOM时,由于多了一层虚拟DOM的计算,会比innerHTML插入慢;
React组件的渲染过程:
(1)使用JSX编写React组件后所有的JSX代码会通过Babel转化为 React.createElement执行;
(2)createElement函数对 key和 ref等特殊的 props进行处理,并获取 defaultProps对默认 props进行赋值,并且对传入的子节点进行处理,最终构造成一个 ReactElement对象(所谓的虚拟 DOM)。
(3)ReactDOM.render将生成好的虚拟 DOM渲染到指定容器上,其中采用了批处理、事务等机制并且对特定浏览器进行了性能优化,最终转换为真实 DOM。
虚拟DOM的组成——ReactElementelement对象结构:
(1)type:元素的类型,可以是原生html类型(字符串),或者自定义组件(函数或class)
(2)key:组件的唯一标识,用于Diff算法,下面会详细介绍
(3)ref:用于访问原生dom节点
(4)props:传入组件的props,chidren是props中的一个属性,它存储了当前组件的孩子节点,可以是数组(多个孩子节点)或对象(只有一个孩子节点)
(5)owner:当前正在构建的Component所属的Component
(6)self:(非生产环境)指定当前位于哪个组件实例
(7)_source:(非生产环境)指定调试代码来自的文件(fileName)和代码行数(lineNumber)
*/
★★★★ React 和 Vue 的 diff 时间复杂度从 O(n^3) 优化到 O(n) ,那么 O(n^3) 和 O(n) 是如何计算出来的?
/*
react的diff算法只需要O(n),这是因为react对树节点的比较做了一些前提假设,限定死了一些东西,不做过于复杂的计算操作,所以降低了复杂度。react和vue做了以下的假设,这样的话diff运算时只进行同层比较,每一个节点只遍历了一次。
(1)Web UI中DOM节点跨层级的移动操作特别少,可以忽略不计;
(2)拥有相同类的两个组件将会生成相似的树形结构,拥有不同类的两个组件将会生成不同的树形结构;
(3)对于同一层级的一组子节点,它们可以通过唯一 id 进行区分。
而传统的diff运算时间复杂度为O(n^3),这是因为传统的树节点要做非常完整的检查,首先需要节点之间需要两两比较,找到所有差异,这个对比过程时间复杂度为O(n^2),找到差异后还要计算出最小的转换方式,最终复杂度为O(n^3)
*/
★★★ 聊聊 Redux 和 Vuex 的设计思想
/*
Flux的核心思想就是数据和逻辑永远单向流动,由三大部分组成 dispatcher(负责分发事件), store(负责保存数据,同时响应事件并更新数据)和 view(负责订阅store中的数据,并使用这些数据渲染相应的页面),Redux和Vuex是flux思想的具体实现,都是用来做状态管理的工具,Redux主要在react中使用,Vuex主要在vue中使用。
Redux设计和使用的三大原则:
(1)单一的数据源:整个应用的 state被储存在唯一一个 store中;
(2)状态是只读的:Store.state不能直接修改(只读),必须调用dispatch(action) => store.reducer => return newState;action是一个对象,有type(操作类型)和payload(新值)属性;
(3)状态修改均由纯函数完成:在Redux中,通过纯函数reducer来确定状态的改变,因为reducer是纯函数,所以相同的输入,一定会得到相同的输出,同时也不支持异步;返回值是一个全新的state;
vuex由State + Muatations(commit) + Actions(dispatch) 组成:
(1)全局只有一个Store实例(单一数据源);
(2)Mutations必须是同步事务,不同步修改的话,会很难调试,不知道改变什么时候发生,也很难确定先后顺序,A、B两个mutation,调用顺序可能是A -> B,但是最终改变 State的结果可能是B -> A;
(3)Actions负责处理异步事务,然后在异步回调中触发一个或多个mutations,也可以在业务代码中处理异步事务,然后在回调中同样操作;
(4)模块化通过module方式来处理,这个跟Redux-combineReducer类似,在应用中可以通过namespaceHelper来简化使用;
*/
★★★ React中不同组件之间如何做到数据交互?
★★★ React中refs的作用是什么?
// ref是React提供的用来操纵React组件实例或者DOM元素的接口。主要用来做文本框的聚焦、触发强制动画等;
// 类组件
class Foo extends React.Component {
constructor(props) {
super(props)
this.myRef = React.createRef()
}
render() {
return
<input ref={ this.myRef } />
<button onClick = {()=>this.handle()}>聚焦
}
handle() {
// 通过current属性访问到当前元素
this.myRef.current.focus()
}
}
// 函数组件
function Foo() {
const inputEl = useRef(null)
const handle = () => {
inputEl.current.focus()
}
return
<input type=“text” ref={ inputEl }/>
聚焦
}
★★★★ 请列举react生命周期函数。
/*
第一阶段:装载阶段3
constructor()
render()
componentDidMount()
第二阶段:更新阶段2
[shouldComponentUpdate()]
render()
componentDidUpdate()
第三阶段:卸载阶段1
componentWillUnmount()
constructor生命周期:
(1)当react组件实例化时,是第一个运行的生命周期;
(2)在这个生命周期中,不能使用this.setState();
(3)在这个生命周期中,不能使用副作用(调接口、dom操作、定时器、长连接等);
(4)不能把props和state交叉赋值;
componentDidMount生命周期:
(1)相当于是vue中的mounted;
(2)它表示DOM结构在浏览器中渲染已完成;
(3)在这里可以使用任何的副作用;
shouldComponentUpdate(nextProps,nextState)生命周期:
(1)相当于一个开关,如果返回true则更新机制正常执行,如果为false则更新机制停止;
(2)在vue中是没有的;
(3)存在的意义:可以用于性能优化,但是不常用,最新的解决方案是使用PureComponent;
(4)理论上,这个生命周期的作用,用于精细地控制声明式变量的更新问题,如果变化的声明式变量参与了视图渲染则返回true,如果被变化的声明式变量没有直接或间接参与视图渲染,则返回false;
componentDidUpdate生命周期:
(1)相当于vue中的updated();
(2)它表示DOM结构渲染更新已完成,只发生在更新阶段;
(3)在这里,可以执行大多数的副作用,但是不建议;
(4)在这里,可以使用this.setState(),但是要有终止条件判断。
componentWillUnmount生命周期:
(1)一般在这里清除定时器、长连接等其他占用内存的构造器;
render生命周期:
(1)render是类组件中唯一必须有的生命周期,同时必须有return(return 返回的jsx默认只能是单一根节点,但是在fragment的语法支持下,可以返回多个兄弟节点);
(2)Fragment碎片写法: <React.Fragment></React.Fragment> 简写成<></>;
(3)return之前,可以做任意的业务逻辑,但是不能使用this.setState(),会造成死循环;
(4)render()在装载阶段和更新阶段都会运行;
(5)当render方法返回null的时候,不会影响生命周期函数的正常执行。
*/
★★★ 组件绑定和js原生绑定事件哪个先执行?
// 先执行js原生绑定事件,再执行合成事件,因为合成事件是发生在冒泡阶段
★★ fetch的延时操作
// fetch语法:fetch(resource, config).then( function(response) { … } );resource为要获取的资源,config是配置对象,包含method请求方法,headers请求头信息等
// 定义一个延时函数,返回一个promise
const delayPromise = (timeout=5000) => {
return new Promise((resolve, reject) => {
setTimeout(()=>{
reject(new Error(“网络错误”))
}, timeout)
})
}
// 定义一个fetch网络请求,返回一个promise
const fetchPromise = (resource, config) => {
return new Promise((resolve, reject)=>{
fetch(resource, config).then(res=>{
resolve(res)
})
})
}
// promise的race静态方法接受多个promise对象组成的数组,该数组中哪个promise先执行完成,race方法就返回这个promise的执行结果
const fetchRequest = (resource, config, timeout) => {
Promise.race([
delayPromise(timeout),
fetchPromise(resource,config)
])
}
★★ A 组件嵌套 B 组件,生命周期执行顺序
/*
父组件创建阶段的生命周期钩子函数 constructor
父组件创建阶段的生命周期钩子函数 render
子组件创建阶段的生命周期钩子函数 constructor
子组件创建阶段的生命周期钩子函数 render
子组件创建阶段的生命周期钩子函数 componentDidMount
父组件创建阶段的生命周期钩子函数 componentDidMount
*/
★★★ diff 和 Key 之间的联系
/*
diff算法即差异查找算法,对于DOM结构即为tree的差异查找算法,只有在React更新阶段才会有Diff算法的运用;react的diff运算为了降低时间复杂度,是按层比较新旧两个虚拟dom树的。diff运算的主要流程见下:
1、tree diff : 新旧两棵dom树,逐层对比的过程就是 tree diff, 当整棵DOM树逐层对比完毕,则所有需要被按需更新的元素,必然能够被找到。
2、component diff : 在进行tree diff的时候,每一层中,都有自己的组件,组件级别的对比,叫做 component diff。如果对比前后,组件的类型相同,则暂时认为此组件不需要更新;如果对比前后,组件的类型不同,则需要移除旧组件,创建新组件,并渲染到页面上。
React只会匹配类型相同的组件,也就是说如果<A>被<B>替换,那么React将直接删除A组件然后创建一个B组件;如果某组件A转移到同层B组件上,那么这个A组件会先被销毁,然后在B组件下重新生成,以A为根节点的树整个都被重新创建,这会比较耗费性能,但实际上我们很少跨层移动dom节点,一般都是同层横向移动;
3、element diff :在进行组件对比的时候,如果两个组件类型相同,则需要进行元素级别的对比,这叫做element diff。
对于列表渲染,react会在创建时要求为每一项输入一个独一无二的key,这样就能进行高效的diff运算了。比如我们要在b和c节点中间插入一个节点f,jquery会将f这个节点后面的每一个节点都进行更新,比如c更新成f,d更新成c,e更新成d,这样操作的话就会特别多,而加了key的react咋不会频繁操作dom,而是优先采用移动的方式,找到正确的位置去插入新节点;所以我们不能省略key值,因为在对比两个新旧的子元素是,是通过key值来精确地判断两个节点是否为同一个,如果没有key的话则是见到谁就更新谁,非常耗费性能。
当我们通过this.setState()改变数据的时候,React会将其标记为脏节点,在事件循环的最后才会重新渲染所有的脏节点以及脏节点的子树;另外我们可以使用shouldComponentUpdate这个生命周期来选择性的渲染子树,可以基于组件之前的状态或者下一个状态来决定它是否需要重新渲染,这样的话可以组织重新渲染大的子树。
*/
★★★ 虚拟 dom 和原生 dom
/*
(1)原生dom是浏览器通过dom树渲染的复杂对象,属性非常多;
(2)虚拟dom是存在于内存中的js对象,属性远少于原生的dom对象,它用来描述真实的dom,并不会直接在浏览器中显示;
(3)原生dom操作、频繁排版与重绘的效率是相当低的,虚拟dom则是利用了计算机内存高效的运算性能减少了性能的损耗;
(4)虚拟DOM进行频繁修改,然后一次性比较并修改真实DOM中需要改的部分,最后并在真实DOM中对修改部分进行排版与重绘,减少过多DOM节点排版与重绘损耗
*/
★★★★ 新出来两个钩子函数?和砍掉的will系列有啥区别?
// react16 中废弃了三个钩子
componentWillMount // 组件将要挂载的钩子
componentWillReceiveProps // 组件将要接收一个新的参数时的钩子
componentWillUpdate // 组件将要更新的钩子
// 新增了方法
getDerivedStateFromProps // 静态方法
getSnapshotBeforeUpdate
/*
在16.8版本以后,react将diff运算改进为Fiber,这样的话当我们调用setState方法进行更新的时候,在reconciler 层中js运算会按照节点为单位拆分成一个个小的工作单元,在render前可能会中断或恢复,就有可能导致在render前这些生命周期在进行一次更新时存在多次执行的情况,此时如果我们在里面使用ref操作dom的话,就会造成页面频繁重绘,影响性能。
所以废弃了这几个will系列的勾子,增加了 getDerivedStateFromProps这个静态方法,这样的话我们就不能在其中使用this.refs以及this上的方法了;getSnapshotBeforeUpdate 这个方法已经到了commit阶段,只会执行一次,给想读取 dom 的用户一些空间。
*/
★★★ react中如何打包上传图片文件
★★★ 对单向数据流和双向数据绑定的理解,好处?
/*
react的单向数据流是指只允许父组件向子组件传递数据,子组件绝对不能修改父组件传的数据,如果想要修改数据,则要在子组件中执行父组件传递过来的回调函数,提醒父组件对数据进行修改。数据单向流让所有的状态改变可以追溯,有利于应用的可维护性;
angular中实现了双向数据绑定,代码编写方便,但是不利于维护
*/
★★ React 组件中 props 和 state 有什么区别?
/*
1、props是从外部传入组件的参数,一般用于父组件向子组件通信,在组件之间通信使用;state一般用于组件内部的状态维护,更新组建内部的数据,状态,更新子组件的props等
2、props不可以在组件内部修改,只能通过父组件进行修改;state在组件内部通过setState修改;
*/
★★ react中组件分为那俩种?
// 类组件和函数组件
★★ react中函数组件和普通组件的区别?
见上
★★★★ react中 setState 之后做了什么?
/*
如果是在隶属于原生js执行的空间,比如说setTimeout里面,setState是同步的,那么每次执行setState将立即更新this.state,然后触发render方法,渲染数据;
如果是在被react处理过的空间执行,比如说合成事件里,此时setState是异步执行的,并不会立即更新this.state的值,当执行setState的时候,会将需要更新的state放入状态队列,在这个空间最后再合并修改this.state,触发render;
*/
★★★★ redux本来是同步的,为什么它能执行异步代码?中间件的实现原理是什么?
/*
当我们需要修改store中值的时候,我们是通过 dispatch(action)将要修改的值传到reducer中的,这个过程是同步的,如果我们要进行异步操作的时候,就需要用到中间件;中间件其实是提供了一个分类处理action的机会,在 middleware 中,我们可以检阅每一个流过的action,并挑选出特定类型的 action进行相应操作,以此来改变 action;
applyMiddleware 是个三级柯里化的函数。它将陆续的获得三个参数:第一个是 middlewares 数组,第二个是 Redux 原生的 createStore,最后一个是 reducer;然后applyMiddleware会将不同的中间件一层一层包裹到原生的 dispatch 之上;
redux-thunk 中间件的作用就是让我们可以异步执行redux,首先检查参数 action 的类型,如果是函数的话,就执行这个 action这个函数,并把 dispatch, getState, extraArgument 作为参数传递进去,否则就调用next让下一个中间件继续处理action。
*/
// redux-thunk部分源码
function createThunkMiddleware(extraArgument) {
return ({ dispatch, getState }) => next => action => {
if (typeof action === ‘function’) {
return action(dispatch, getState, extraArgument)
}
return next(action)
}
}
const thunk = createThunkMiddleware()
thunk.withExtraArgument = createThunkMiddleware
export default thunk
★★★★ 列举重新渲染 render 的情况
this.setState()
this.forceUpdate()
// 接受到新的props
// 通过状态管理,mobx、redux等
// 改变上下文
★★★ React 按需加载
// 1、使用React.lazy, 但是React.lazy技术还不支持服务端渲染
const OtherComponent = React.lazy(() => import(‘./OtherComponent’))
// 2、使用Loadable Components这个库
import loadable from ‘@loadable/component’
const OtherComponent = loadable(() => import(‘./OtherComponent’))
★★★ React 实现目录树(组件自身调用自身)
★★★ React组件生命周期按装载,更新,销毁三个阶段分别都有哪些?
见上
★★★★★ 调用this.setState之后,React都做了哪些操作?怎么拿到改变后的值?
/*
如果是在隶属于原生js执行的空间,比如说setTimeout里面,setState是同步的,那么每次执行setState将立即更新this.state,然后触发render方法;因为是同步执行,可以直接获取改变后的值;
如果是在被react处理过的空间执行,比如说合成事件里,此时setState是异步执行的,并不会立即更新this.state的值,当执行setState的时候,会将需要更新的state放入状态队列,在这个空间最后再合并修改this.state,触发render;setState接受第二个参数,是一个回调函数,可以在这里获取改变后的state值;
触发render执行后,会生成一个新的虚拟dom结构,然后触发diff运算,找到变化的地方,重新渲染;
*/
★★★ 如果我进行三次setState会发生什么
// 看情况,如果是在原生js空间,则会同步执行,修改三次state的值,调用三次render函数;如果是在react函数空间下,则会进行合并,只修改一次state的值,调用一次render。
★★★ 循环执行setState组件会一直重新渲染吗?为什么?
见上
★★★ 渲染一个react组件的过程
/*
1、babel编译
当我们对代码进行编译的时候,babel会将我们在组件中编写的jsx代码转化为React.createElement的表达式,createElement方法有三个参数,分别为type(元素类型)、attributes(元素所有属性)、children(元素所有子节点);
2、生成element
当render方法被触发以后,createElement方法会执行,返回一个element对象,这个对象描述了真实节点的信息,其实就是虚拟dom节点;
3、生成真实节点(初次渲染)
这时候我们会判断element的类型,如果是null、false则实例一个ReactDOMEmptyComponent对象; 是string、number类型的话则实例一个ReactDOMTextComponent对象; 如果element是对象的话,会进一步判断type元素类型,是原生dom元素,则实例化ReactDOMComponent; 如果是自定义组件,则实例化ReactCompositeComponentWrapper;
在这些类生成实例对象的时候,在其内部会调用 mountComponent方法,这个方法里面有一系列浏览器原生dom方法,可以将element渲染成真实的dom并插入到文档中;
4、生命周期
componentDidMount:会在组件挂载后(插入DOM树中) 立即调用。一般可以在这里请求数据;
componentDidUpdate:会在数据更新后立即调用,首次渲染不会执行此方法;可以在其中直接调用 setState,但必须用if语句进行判断,防止死循环;
conponentWillUnmount:会在组件卸载及销毁之前调用,在此方法中执行必要的清理操作,如清除timer;
static getDerivedStateFromProps(prps,state):这个生命周期函数代替了componentWillMount和componentWillUpdate生命周期;props和state发生改变则调用,在初始化挂载及后续更新时都会被调用,返回一个对象来更新state,如果返回null则不更新任何内容;
shouldComponentUpdate(nextProps,nextState):这个生命周期函数的返回值用来判断React组件是否因为当前 state 或 props 更改而重新渲染,默认返回值是true;这个方法在初始化渲染或使用forceUpdate()时不会调用;当将旧的state的值原封不动赋值给新的state(即不改变state的值,但是调用了setState)和 无数据交换的父组件的重新渲染都会导致组件重新渲染,这时候都可以通过shouldComponentUpdate来优化。
*/
★★★ React类组件,函数组件,在类组件修改组件对象会使用。
★★★★ 类组件怎么做性能优化?函数组件怎么做性能优化?
/*
类组件:
(1)使用shouldComponentUpdate:这个生命周期可以让我们决定当前状态或属性的改变是否重新渲染组件,默认返回ture,返回false时不会执行render,在初始化渲染或使用forceUpdate()时不会调用;如果在shouldComponentUpdate比较的值是引用类型的话,可能达不到我们想要的效果,因为引用类型指向同一个地址;
当将旧的state的值原封不动赋值给新的state(即不改变state的值,但是调用了setState)和 无数据交换的父组件的重新渲染都会导致组件重新渲染,这时候都可以通过shouldComponentUpdate来优化;
(2)React.PureComponent:基本上和Component用法一致,不同之处在于 PureComponent不需要开发者自己设置shouldComponentUpdate,因为PureComponent自带通过props和state的浅对比来实现 shouldComponentUpate;但是如果props和state对象包含复杂的数据结构,它可能会判断错误(表现为对象深层的数据已改变,视图却没有更新);
(4)使用Immutable:immutable是一种持久化数据,一旦被创建就不会被修改,修改immutable对象的时候返回新的immutable;也就是说在使用旧数据创建新数据的时候,会保证旧数据同时可用且不变;为了避免深度复制所有节点的带来的性能损耗,immutable使用了结构共享,即如果对象树中的一个节点发生变化,只修改这个节点和受他影响的父节点,其他节点仍然共享;
(5)bind函数:在react中改变this的指向有三种方法,a)constructor中用bind绑定; b)使用时通过bind绑定; 3)使用箭头函数;选择第一种只在组件初始化的时候执行一次,第二种组件在每次render都要重新绑定,第三种在每次render时候都会生成新的箭头函数,所以选择第一种;
函数组件:
(1)useCallback:接收一个函数作为参数,接收第二个参数作为依赖列表,返回值为函数,有助于避免在每次渲染时都进行高开销的计算,仅会在某个依赖项改变时才重新计算;可以使用useCallback把要传递给子组件的函数包裹起来,这样父组件刷新的时候,传递给子组件的函数指向不会发生改变,可以减少子组件的渲染次数;
const handleUseCallback=useCallback(handleClick,[])
(2)useMemo:useMemo的使用和useCallback差不多,只是useCallback返回的是一个函数,useMemo返回值可以是函数、对象等都可以;
两者都可使用:
(1)React.memo:React.memo 功能同React.PureComponent,但React.memo是高阶组件,既可以用在类组件中也可以用在函数组件中;memo还可以接收第二个参数,是一个可定制化的比较函数,其返回值与 shouldComponentUpdate的相反;
(2)使用key:在列表渲染时使用key,这样当组件发生增删改、排序等操作时,diff运算后可以根据key值直接调整DOM顺序,避免不必要的渲染而避免性能的浪费;
(3)不要滥用props:尽量只传需要的数据,避免多余的更新,尽量避免使用{…props};
*/
★★★ useEffect 和 useLayoutEffect 的区别
/*
2、useEffect和useLayout都是副作用hooks,两则非常相似,同样都接收两个参数:
(1)第一个参数为函数,第二个参数为依赖列表,只有依赖更新时才会执行函数;返回一个函数,当页面刷新的或销毁的时候执行return后的代码;
(2)如果不接受第二个参数,那么在第一次渲染完成之后和每次更新渲染页面的时候,都会调用useEffect的回调函数;
useEffect和 useLayout的主要区别就是他们的执行时机不同,在浏览器中js线程与渲染线程是互斥的,当js线程执行时,渲染线程呈挂起状态,只有当js线程空闲时渲染线程才会执行,将生成的 dom绘制。useLayoutEffect在js线程执行完毕即dom更新之后立即执行,而useEffect是在渲染结束后才执行,也就是说 useLayoutEffect比 useEffect先执行。
*/
★★★ hooks 的使用有什么注意事项
/*
(1)只能在React函数式组件或自定义Hook中使用Hook。
(2)不要在循环,条件或嵌套函数中调用Hook,必须始终在React函数的顶层使用Hook。这是因为React需要利用调用顺序来正确更新相应的状态,以及调用相应的钩子函数。一旦在循环或条件分支语句中调用Hook,就容易导致调用顺序的不一致性,从而产生难以预料到的后果。
*/
★★★ 纯函数有什么特点,副作用函数特点
/*
纯函数与外界交换数据只有一个唯一渠道——参数和返回值;函数从函数外部接受的所有输入信息都通过参数传递到该函数内部;函数输出到函数外部的所有信息都通过返回值传递到该函数外部。
纯函数的优点:无状态,线程安全;纯函数相互调用组装起来的函数,还是纯函数;应用程序或者运行环境可以对纯函数的运算结果进行缓存,运算加快速度。
函数副作用是指当调用函数时,除了返回函数值之外,还对主调用函数产生附加的影响。比如调接口、修改全局变量、抛出一个异常或以一个错误终止、打印到终端或读取用户输入、读取或写入一个文件等,所以说副作用是编程中最关键的部分,因为我们需要跟用户、跟数据进行交互。
*/
★★ React 中 refs 干嘛用的?如何创建 refs?
见上
★★★ 在构造函数调用 super
并将 props
作为参数传入的作用是啥?
/*
ES6 中在调用 super()方法之前,子类构造函数无法使用this引用,在react的类组件中也是如此;将 props 参数传递给 super() 调用的主要原因是在子构造函数中能够通过this.props来获取传入的 props。
*/
★★★ 如何 React.createElement ?
见上
★★★ 讲讲什么是 JSX ?
/*
JSX全称为JavaScript XML,是react中的一种语法糖,可以让我们在js代码中脱离字符串直接编写html代码;本身不能被浏览器读取,必须使用@babel/preset-react和webpack等工具将其转换为传统的JS。
主要有以下特点:
(1)类XML语法容易接受,结构清晰;
(2)增强JS语义;
(3)抽象程度高,屏蔽DOM操作,跨平台;
(4)代码模块化;
*/
★★★ 为什么不直接更新 state
呢?
// 如果试图直接更新 state ,则不会重新渲染组件;需要使用setState()方法来更新 state这样组件才会重新渲染;
★★★ React 组件生命周期有哪些不同阶段?React 的生命周期方法有哪些?
见上
★★★ 这三个点(…)在 React 干嘛用的?
// …是es6语法新出的规范,叫做展开运算符;在react中可以将对象或数组进行展开,让我们操作改变数据结构非常方便。
★★★ React 中的 useState()
是什么?
// useState是一个内置的React Hook,可以让我们在函数组件中像类组件一样使用state并且改变state的值。
★★★ React 中的StrictMode(严格模式)是什么?
/*
React的StrictMode是一种辅助组件,用包装组件,可以帮助我们编写更好的react组件,不会渲染出任何可见的ui;仅在开发模式下运行,它们不会影响生产构建,可以做以下检查:
(1)验证内部组件是否遵循某些推荐做法,如果没有,会在控制台给出警告;
(2)验证是否使用的已经废弃的方法,如果有,会在控制台给出警告;
(3)通过识别潜在的风险预防一些副作用。
*/
★★★ 为什么类方法需要绑定到类实例?
// 在 JS 中,this 值会根据当前上下文变化。在 React 类组件方法中,开发人员通常希望 this 引用组件的当前实例,因此有必要将这些方法绑定到实例。通常这是在构造函数中完成的:
★★★★ 什么是 prop drilling,如何避免?
/*
从一个外部组件一层层将prop传递到内部组件很不方便,这个问题就叫做 prop drilling;主要缺点是原本不需要数据的组件变得不必要地复杂,并且难以维护,代码看起来也变得冗余,不优雅;
为了避免prop drilling,一种常用的方法是使用React Context。通过定义提供数据的Provider组件,并允许嵌套的组件通过 Consumer组件或 useContext Hook 使用上下文数据。
*/
★★ 描述 Flux 与 MVC?
/*
传统的 MVC 模式在分离数据(Model)、UI(View和逻辑(Controller)方面工作得很好,但是 MVC 架构经常遇到两个主要问题:
数据流不够清晰——跨视图发生的级联更新常常会导致混乱的事件网络,难于调试。
缺乏数据完整性——模型数据可以在任何地方发生突变,从而在整个UI中产生不可预测的结果。
使用 Flux 模式的复杂用户界面不再遭受级联更新,任何给定的React 组件都能够根据 store 提供的数据重建其状态。Flux 模式还通过限制对共享数据的直接访问来加强数据完整性。
*/
★★★ 这段代码有什么问题吗?
this.setState((prevState, props) => {
return {
streak: prevState.streak + props.count
}
})
// 没有问题
★★★★ 什么是 React Context?
React Context
源码解析
What
Context提供了一个无需为每层组件手动添加props,就能在组件树间进行数据传递的功能
Why
某些全局属性,通过父子props传递太过繁琐,Context提供了一种组件之间共享此类值的方式,而不必显式的通过组件树逐层传递props
When
共享那些对于一个组件树而言是全局的数据,例如当前认证的用户、主题或者首选语言等
Where
Context应用场景在于很多不同层级的组件访问同样的数据,这样也使得组件的复用性变差。
如果你只是想避免层层传递一些属性,组件组合有时候是一个比Context更好的方案,也就是直接传递组件
所以一个技术方案的选定需要针对不同的场景具体分析,采取合适的方案
How
// ①创建
const ThemeContext = React.createContext(‘xxx’)
// ②注入—提供者 在入口或者你想要注入的父类中,且可以嵌套,里层覆盖外层
return (
<ThemeContext.Provider value=“yyy”>
{children}
<ThemeContext.Provider>
)
// ③使用—消费者 需要使用共享数据的子类中
// 方式一
static contextType = ThemeContext
// 方式二
Class.contextType = ThemeContext
render() {
let value = this.context
/* 基于这个值进行渲染工作 /
}
// 方式三
return(
<ThemeContext.Consumer>
{ value => / 基于 context 值进行渲染*/ }
</ThemeContext.Consumer>
)
More
动态Context—类似父子组件
// ①创建
const ThemeContext = React.createContext({
value: ‘xxx’,
changeFunc: () => {} //通过context传递这个函数,让consumers组件更新context
})
// ②注入
return (
<ThemeContext.Provider value=“yyy”>
<ThemeContext.Provider>
)
// ③消费
return(
<ThemeContext.Consumer>
{ ({value, changeFunc}) => /* 基于 context 值进行渲染,同时把changeFunc绑定*/ }
</ThemeContext.Consumer>
)
消费多个Context、注意事项等参考React中文网
★★★★★ 什么是 React Fiber?
What
React Fiber En
Fiber是React16中新的协调引擎,它的主要目的是使Virtual DOM可以进行增量式渲染,让界面渲染更流畅
一种流程控制原语,也称为协程,可以类比es6中的generator函数;React渲染的过程可以被中断,可以将控制权交回浏览器,让位给高优先级的任务,浏览器空闲后再恢复渲染。
一个执行单元,每次执行完一个“执行单元”,React就会检查现在还剩多少时间,如果没有时间就将控制权让出去。
目标
把可中断的工作拆分成小任务
对正在做的工作调整优先次序、重做、复用上次(做了一半的)成果
在父子任务之间从容切换(yield back and forth),以支持React执行过程中的布局刷新
支持render()返回多个元素
更好地支持error boundary
特性
增量渲染(把渲染任务拆分成块,匀到多帧)
更新时能够暂停,终止,复用渲染任务
给不同类型的更新赋予优先级
并发方面新的基础能力
★★★ 如何在 React 的 Props 上应用验证?
使用PropTypes进行类型检查
PropTypes自React v15.5起,请使用这个库prop-types
What & Why & When
随着应用的不断增长,也是为了使程序设计更加严谨,我们通常需要对数据的类型(值)进行一些必要的验证
出于性能方面的考虑,propTypes仅在开发模式下进行检测,在程序运行时就能检测出错误,不能使用到用户交互提醒用户操作错误等
也可以使用Flow或者TypeScript做类型检查,后期建议用typescript进行替代更好
Where
class组件
函数组件
React.memo高阶组件 可自行扩展
React.forwardRef组件 可自行扩展
How
我们在组件类下添加一个静态属性 propTypes (属性名不能更改),它的值也是一个对象,用来设置组件中props的验证规则,key 是要验证的属性名称,value 是验证规则。
// 类组件
import PropTypes from ‘prop-types’;
class Greeting extends React.Component {
render() {
return (
Hello, {this.props.name}
);
}
}
// 指定 props 的默认值:
Greeting.defaultProps = {
name: ‘Stranger’
};
// 类组件在这里做检测
Greeting.propTypes = {
// v15.4 and below
// name: React.PropTypes.string
name: PropTypes.string
};
// 函数组件
unction HelloWorldComponent({ name }) {
return (
)
}
// 函数组件在这里做检测
HelloWorldComponent.propTypes = {
name: PropTypes.string
}
export default HelloWorldComponent
更多检测的类型
More
限制单个元素 PropTypes.element
★★ 在 React 中使用构造函数和 getInitialState 有什么区别?
参考-stackoverflow
// ES6
class MyComponent extends React.Component {
constructor(props) {
super(props);
this.state = { /* initial state / };
}
}
// ES5
var MyComponent = React.createClass({
getInitialState() {
return { / initial state */ };
},
});
本质上其实是等价的?
区别在于ES6和ES5本身,getInitialState 是搭配 React.createClass 使用的, constructor 是搭配 React.Component 使用的
在React组件的生命周期中 constructor 先于 getInitialState
Life Cycle
★★★ 如何有条件地向 React 组件添加属性?
参考-stackoverflow
对于某些属性,React足够智能可以忽略该属性,比如值为boolean值属性的值
也可以写控制语句管理是否给组件添加属性
★★★★ Hooks 会取代 render props
和高阶组件吗?
官方简答
可以取代,但没必要
在Hook的渐进策略中也有提到,没有计划从React中移除class,在新的代码中同时使用Hook和class,所以这些方案目前还是可以有勇武之地
What
为什么要把这3种技术拿过来对比?
都在处理同一个问题,逻辑复用
高阶组件HOC—不是 React API 的一部分,是基于 React 的组合特性形成的设计模式。
高阶组件是参数为组件,返回值为新组件的函数(将组件转换为另一个组件,纯函数,无副作用)
Render Props
是指一种在 React 组件之间使用一个值为函数的 prop 共享代码的 简单技术?
Hooks
React16.8新增的特性,是一些可以让你在函数组件里“钩入”React state及生命周期等特性的函数,
Why
虽然 HOC & Render Props 能处理逻辑复用的问题,但是却存在各自的问题。
HOC 存在的问题
写法破坏了原来组件的结构,DevTools中组件会形成“嵌套地狱”
不要在 render 方法中使用 HOC 每次调用render函数会创建一个新的高阶组件导致该组件及其子组件的状态丢失
需要修复静态方法,即拷贝原组件的静态方法到高级组件中
如需传递Ref则需要通过React.forwardRef创建组件
Render Props 存在的问题
同样的写法会破坏原来组件的结构,DevTools中组件会形成“嵌套地狱”
与React.PureComponent组件使用有冲突
Hook 目前最优雅的实现,React为共享状态逻辑提供最好的原生途径
没有破坏性改动,完全可选,100%向后兼容
解决复杂组件,中逻辑状态、副作用和各种生命周期函数中逻辑代码混在一起,难以拆分,甚至形成bug的问题
处理class组件中
When
在函数组件中意识到要向其添加一些state—useState
有副作用的行为时
Where
只能在函数最外层调用Hook,不要在循环、条件判断或者子函数中调用
只能在函数组件或者自定义Hook中调用Hook
How
★★★ 如何避免组件的重新渲染?
Answer
当porps/state改变时组件会执行render函数也就是重新渲染
class组件中 使用shouldComponentUpdate钩子函数
PureComponent默认有避免重新渲染的功能
函数组件使用高阶组件memo处理
★★★ 什么是纯函数?
Answer
一个不会更改入参,且多次调用下相同的入参始终返回相同的结果
★★★★ 当调用setState
时,React render
是如何工作的?
Answer
调用setState()
检查上下文环境生成更新时间相关参数并判定事件优先级(fiber,currenttime,expirationtime等…)
根据优先级相关参数判断更新模式是sync(同步更新)或是batched(批量处理)
加入执行更新事件的队列,生成事件队列的链表结构
根据链表顺序执行更新
setState既是同步的,也是异步的。同步异步取决于setState运行时的上下文。且setState 只在合成事件和钩子函数中是“异步”的,在原生DOM事件和 setTimeout 中都是同步的
render如何工作
React在props或state发生改变时,会调用React的render方法,创建一颗不同的树
React需要基于这两颗不同的树之间的差别来判断如何有效的更新UI
diff算法,将两颗树完全比较更新的算法从O(n3),优化成O(n);
同层节点之间相互比较,不会跨节点比较
不同类型的节点,产生不同的树结构
设置key来指定节点在不同的渲染下保持稳定
★★★ 如何避免在React重新绑定实例?
Answer
将事件处理程序定义为内联箭头函数
使用箭头函数来定义方法
使用带有 Hooks 的函数组件
★★★ 在js原生事件中 onclick 和 jsx 里 onclick 的区别
Answer
js原生中
onclick添加事件处理函数是在全局环境下执行,污染了全局环境,
且给很多dom元素添加onclick事件,影响网页的性能,
同时如果动态的从dom树种删除了该元素,还要手动注销事件处理器,不然就可能造成内存泄露
jsx里的onClick
挂载的函数都控制在组件范围内,不会污染全局空间
jsx中不是直接使用onclick,而是采取了事件委托的方式,挂载最顶层DOM节点,所有点击事件被这个事件捕获,然后根据具体组件分配给特定函数,性能当然比每个onClick都挂载一个事件处理函数要高
加上React控制了组件的生命周期,在unmount的时候自然能够清楚相关的所有事件处理函数,内存泄露不再是一个问题
★★★★ diff复杂度原理及具体过程画图
Answer
React 通过制定大胆的 diff 策略,将 O(n3) 复杂度的问题转换成 O(n) 复杂度的问题;
React 通过分层求异的策略,对 tree diff 进行算法优化;
React 通过相同类生成相似树形结构,不同类生成不同树形结构的策略,对 component diff 进行算法优化;
React 通过设置唯一 key的策略,对 element diff 进行算法优化;
建议,在开发组件时,保持稳定的 DOM 结构会有助于性能的提升;
建议,在开发过程中,尽量减少类似将最后一个节点移动到列表首部的操作,当节点数量过大或更新操作过于频繁时,在一定程度上会影响 React 的渲染性能。
★★★★ shouldComponentUpdate的作用是什么?
shouldComponentUpdate的作用
What
不常用的生命周期方法,能影响组件是否重新渲染
在更新阶段,当有new props 或者 调用了 setState()方法,在render方法执行前会执行到,默认返回值为true,如果返回false则不刷新组件
Why & When & Where
如果你知道在什么情况下组件不需要更新,你可以让其返回值为false跳过整个渲染过程
次方法仅作为 性能优化方式 而存在,不要企图靠此方法来阻止渲染,
大部分情况下,使用PureComponent代替手写shouldComponentUpdate,仅浅层对比
不建议在shoulComponentUpdate中进行深层或者使用JSON.stringify(),这样非常影响效率和性能
Answer
作为React组件中不常用的生命周期函数,能影响组件是否重渲染
建议做浅层次的比较,来优化性能,当然这里也可以用PureComponent组件代替
如果有较深层次的比较则可能会导致更严重的性能问题,因此在这种情况下不要靠手动管理组件的重新渲染来优化性能,要找其他方式
比如?
★★★ React组件间信息传递
Answer
1.(父组件)向(子组件)传递信息 : porps传值
2.(父组件)向更深层的(子组件) 进行传递信息 : context
3.(子组件)向(父组件)传递信息:callback
4.没有任何嵌套关系的组件之间传值(比如:兄弟组件之间传值): 利用共同父组件context通信、自定义事件
5.利用react-redux进行组件之间的状态信息共享 : 组件间状态信息共享:redux、flux、mobx等
★★★ React状态管理工具有哪些?redux actionCreator都有什么?
Answer
简单状态管理:组件内部state、基*于Context API封装、
复杂状态管理:redux(单项数据流)、mobx(响应式数据流)、RxJS(stream)、dva
创建各种action,包含同步、异步,然后在组件中通过dispatch调用
★★★★ 什么是高阶组件、受控组件及非受控组件?都有啥区别
Answer
定义
高阶组件HOC—不是 React API 的一部分,是基于 React 的组合特性形成的设计模式。
高阶组件是参数为组件,返回值为新组件的函数(将组件转换为另一个组件,纯函数,无副作用)
受控组件
在表单元素中,state是唯一数据源,渲染表单的React组件控制着用户输入过程中表单发生的操作。被React以这种方式控制取值的表单输入元素叫做受控组件
非受控组件
表单数据由DOM节点来处理,而不是用state来管理数据,一般可以使用ref来从DOM节点中获取表单数据
区别
受控组件和非受控组件是表单中的组件,高阶组件相当于对某个组件注入一些属性方法
高阶组件是解决代码复用性问题产生的技术
受控组件必须要有一个value,结合onChange来控制这个value,取值为event.target.value/event.target.checked
非受控组件相当于操作DOM,一般有个defaultValue,通过onBlur触发响应方法
★★★ vuex 和 redux 的区别?
Answer
Redux
Redux 是 JavaScript 状态容器,提供可预测化的状态管理。
Redux
随着JS单页面应用日趋复杂,JS需要管理比任何时候都要多的state(服务器响应,缓存数据,本地生成尚未持久化到服务器的数据,UI状态等)
伴随的现象就是models和views相互影响,你难以弄清楚变化的源头
也就是变化和异步让我们的state变得一团糟
★★★ Redux遵循的三个原则是什么?
Answer
单一数据源
整个应用的state被存储在一棵object tree中,并且整个 object tree 只存在于唯一一个 store 中
State是只读的
唯一改变state的方法就是触发 action,action是一个描述已发生事件的普通对象
这样确保视图和网络请求不能直接修改state
使用纯函数来执行修改
为了描述action如何改变state tree,你需要编写reducers
★★★ React中的keys的作用是什么?
Answer
key 是用来帮助 react 识别哪些内容被更改、添加或者删除。key 需要写在用数组渲染出来的元素内部,并且需要赋予其一个稳定的值。稳定在这里很重要,因为如果 key 值发生了变更,react 则会触发 UI 的重渲染。这是一个非常有用的特性。
key 的唯一性
在相邻的元素间,key 值必须是唯一的,如果出现了相同的 key,同样会抛出一个 Warning,告诉相邻组件间有重复的 key 值。并且只会渲染第一个重复 key 值中的元素,因为 react 会认为后续拥有相同 key 的都是同一个组件。
key 值不可读
虽然我们在组件上定义了 key,但是在其子组件中,我们并没有办法拿到 key 的值,因为 key 仅仅是给 react 内部使用的。如果我们需要使用到 key 值,可以通过其他方式传入,比如将 key 值赋给 id 等
★★★ redux中使用setState不能立刻获取值,怎么办
Answer
setState 只在合成事件和钩子函数中是异步的,在原生事件和 setTimeout 中都是同步
①addeventListener添加的事件或者dom事件中触发
②setState接收的参数还可以是一个函数,在这个函数中可以拿先前的状态,并通过这个函数的返回值得到下一个状态。
this.setState((preState) => {
return {
xxx: preState.xxx + yyy
}
})
③async/await 异步调用处理
★★ 什么是JSX
Answer
当 Facebook 第一次发布 React 时,他们还引入了一种新的 JS 方言 JSX,将原始 HTML 模板嵌入到 JS 代码中。JSX 代码本身不能被浏览器读取,必须使用Babel和webpack等工具将其转换为传统的JS。很多开发人员就能无意识使用 JSX,因为它已经与 React 结合在一直了
是一个 JavaScript 的语法扩展
具有 JavaScript 的全部功能
可以生成 React “元素”
JSX 也是一个表达式
★★★ React新老版生命周期函数
生命周期
Answer
New Version
挂载:constructor --> getDerivedStateFromProps --> render --> componentDidMount
更新:
New props、setState() --> getDerivedStateFromProps --> shouldComponentUpdate --> render --> getSnapshotBeforeUpdate --> componentDidUpdate
forceUpdate() --> getDerivedStateFromProps --> render --> getSnapshotBeforeUpdate --> componentDidUpdate
卸载: componentWillUnmount
Old Version*
挂载:constructor --> getDerivedStateFromProps --> render --> ComponentDidMount
更新:
New props --> getDerivedStateFromProps --> shouldComponentUpdate --> render --> getSnapshotBeforeUpdate --> componentDidUpdate
setState() --> shouldComponentUpdate --> render --> getSnapshotBeforeUpdate --> componentDidUpdate
forceUpdate() --> render --> getSnapshotBeforeUpdate --> componentDidUpdate
卸载:componentWillUnmount
★★★★ vue react都怎么检测数据变化
Answer
React
React默认是通过比较引用的方式(diff)进行的,不精确监听数据变化,如果不优化可能导致大量不必要的VDOM重新渲染
16之前 componentWillReveiveProps 监听 props 变化
16之后 getDerivedStateFromProps 监听 props
Vue
vue监听变量变化依靠 watch Object.defineProperty,Vue通过“getter/setter”以及一些函数的劫持,能精确知道数据变化
★★★ React中怎么让 setState 同步更新?
Answer
setState 回调,setState,第二个参数是一个回调函数,可实现同步
引入 Promise 封装 setState,在调用时我们可以使用 Async/Await 语法来优化代码风格
setStateAsync(state) {
return new Promise((resolve) => {
this.setState(state, resolve)
});
}
传入状态计算函数, setState 的第一个参数,
this.setState((prevState, props) => ({
count: prevState.count + 1
}));
在 setTimeout 函数中调用 setState
more?
★★★★ 什么是 immutable?为什么要使用它?
Answer
immutable是一种持久化数据。一旦被创建就不会被修改。修改immutable对象的时候返回新的immutable。但是原数据不会改变。
在Rudux中因为深拷贝对性能的消耗太大了(用到了递归,逐层拷贝每个节点)。 但当你使用immutable数据的时候:只会拷贝你改变的节点,从而达到了节省性能。 总结:immutable的不可变性让纯函数更强大,每次都返回新的immutable的特性让程序员可以对其进行链式操作,用起来更方便。
因为在react中,react的生命周期中的setState()之后的shouldComponentUpdate()阶段默认返回true,所以会造成本组件和子组件的多余的render,重新生成virtual dom,并进行virtual dom diff,所以解决办法是我们在本组件或者子组件中的shouldComponentUpdate()函数中比较,当不需要render时,不render。
当state中的值是对象时,我们必须使用深拷贝和深比较!
如果不进行深拷贝后再setState,会造成this.state和nextState指向同一个引用,所以shouldComponentUpdate()返回值一定是false,造成state值改了,而组件未渲染(这里不管shouldComponentUpdate中使用的是深比较还是浅比较)。所以必须深拷贝。
如果不在shouldComponentUpdate中进行深比较,会造成即使state中的对象值没有改变,因为是不同的对象,而在shouldComponentUpdate返回true,造成不必要的渲染。
所以只能是深拷贝和深比较。
★★★ 为什么不建议在 componentWillMount 做AJAX操作
Answer
Fiber原因,React16之后,采用了Fiber架构,只有componentDidMount的生命周期函数确定会执行一次,其他像componentWillMount可能会执行多次
render 阶段 可能会被React暂停,中止或重启
★★★★ 如何在React中构建一个弹出的遮罩层
Answer
//css部分
.mask{
background: rgba(0,0,0,0.4) !important;
z-index: 10;
height: 100vh;
position: fixed;
width: 100vw;
}
.selectMask_box{
background: rgba(0,0,0,0);
transition: all .2s linear
}
//js部分
handleMask=()=>{
this.setState({
dateSelected: !this.state.dateSelected
})
}
//这里是待展示的内容,
//你可以自己设置dataSelected的初始值,同时请注意注意三元运算的顺序。
★★★★★ React中的Context的使用
1.调用React.createContext()创建Provider(提供数据)和Consumer(消费数据)两个组件
2.使用Provider组件作为父节点
3.设置value属性,表示要传递的数据
4.调用Consumer组件接收数据
如果两个组件嵌套多层 可以使用Context实现组件通讯
Context提供两个组件:Provider 和 Consumer
Provider :用来提供数据
Consumer:用来接收数据的
// ES6 中模块化语法
import React from 'react';
import ReactDOM from 'react-dom';
import './index.css';
// 创建context得到两个组件
const { Provider, Consumer } = React.createContext()
class App extends React.Component {
render() {
return (
<Provider value="pink">
<div className="app">
<Node />
</div>
</Provider>
)
}
}
const Node = props => {
return (
<div className="node">
<SubNode />
</div>
)
}
const SubNode = props => {
return (
<div className="subnode">
<Child />
</div>
)
}
const Child = props => {
return (
<div className="child">
<Consumer>
{/* 回调 */}
{
data => <span>{data}</span>
}
</Consumer>
</div>
)
}
ReactDOM.render(<App />, document.getElementById('root'))
★★★★ React路由懒加载的实现
Answer
原理
webpack代码分割
React利用 React.lazy与import()实现了渲染时的动态加载
利用Suspense来处理异步加载资源时页面应该如何显示的问题
1.React.lazy
通过lazy() api来动态import需要懒加载的组件
import的组件目前只支持export default的形式导出
Suspense来包裹懒加载的组件进行加载,可以设置fallback现实加载中效果
React.lazy可以结合Router来对模块进行懒加载。
import { BrowserRouter as Router, Route, Switch } from ‘react-router-dom’;
import { Suspense, lazy } from ‘react’;
const Home = lazy(() => import(‘./routes/Home’))
const AnyComponent = lazy(() => import(‘./routes/AnyComponent’))
…
return (
<Suspense fallback={
…
2.react-loadable
react-loadable是以组件级别来分割代码的,这意味着,我们不仅可以根据路由按需加载,还可以根据组件按需加载,使用方式和路由分割一样,只用修改组件的引入方式即可
// 路由懒加载(异步组件)
import Loadable from ‘react-loadable’;
//通用过场组件
const LoadingComponent = () => {
return (
)
}
…
export default (loader, loading=LoadingComponent) => {
return Loadable({
loader,
loading
})
}
//Route中调用
import { BrowserRouter, Route } from ‘react-router-dom’
const loadable from ‘./loadable’;
const AnyComponent = loadable(() => import(‘./AnyComponent’))
const Routes = () => (
);
export default Routes;
以下是老版中的方法
3.webpack配置中使用lazyload-loader
// webpack 配置中
module: {
rules: [
{
test: /.(js|jsx)$/,
use: [
‘babel-loader’,
‘lazyload-loader’
]
},
// 业务代码中
// 使用lazy! 前缀 代表需要懒加载的Router
import Shop from ‘lazy!./src/view/Shop’;
// Router 正常使用
4.import() webpack v2+
符合ECMAScript提议的import()语法,该提案与普通 import 语句或 require 函数的类似,但返回一个 Promise 对象
function component() {
return import( /* webpackChunkName: “lodash” / ‘lodash’).then(_ => {
var element = document.createElement(‘div’);
element.innerHTML = _.join([‘Hello’, ‘webpack’], ’ ');
return element;
}).catch(error => ‘An error occurred while loading the component’);
}
// 或者使用async
async function getComponent() {
var element = document.createElement(‘div’);
const _ = await import(/ webpackChunkName: “lodash” */ ‘lodash’);
element.innerHTML = _.join([‘Hello’, ‘webpack’], ’ ‘);
return element;
}
5.requre.ensure webpack v1 v2
require.ensure([], function(require){
var list = require(’./list’);
list.show();
,‘list’);
const Foo = require.ensure([], () => {
require(“Foo”);
}, err => {
console.error("We failed to load chunk: " + err);
}, “chunk-name”);
//react-router2 or 3
★★★★ React-router-dom内部是怎么样实现的,怎么做路由守卫?
Answer
内部实现
总
react-router-dom利用了Context API,通过上下文对象将当前路由信息对象注入到 Router 组件中,所以 Router 组件中 render() 渲染的内容就是 ContextAPI 提供的 Provider 组件,然后接收 Router 组件中的当前路由信息对象。 这样 Router 组件下的所有组件都能通过上下文拿到当前路由信息对象,即其中的Switch 、 Route 、 Link 、Redirect 等组件都可以拿到当前路由信息对象,然后通过改变当前路由信息来实现动态切换 Route 组件的渲染。
分
RouterContext:react-router使用context实现跨组件间数据传递,所以react-router定义了一个routerContext作为数据源,
Router:BrowserRouter和HashRouter将当前路由注入到上下文中,同时路由信息包含location、match、history
Route:路由规则,获取RouterContext的信息(location对象),获取path和component属性,判断path和当前的location是否匹配,如果匹配,则渲染component,否则返回null,不渲染任何内容
Switch:遍历所有子元素(Route),判断Route的path和location是否匹配,如果匹配,则渲染,否则不渲染
Redireact:未能配则重定向到指定页面
Link/NavLink: Link组件本质就是a标签,它修改了a标签的默认行为,当点击Link时,会导航到对应的路由,导致locaiton对象的改变,出发组件的更新
withRouter:对传入的组件进行加强,功能就是获取routerContext上面的信息,然后作为props传给需要加强的组件
怎么做路由守卫
路由里设置meta元字符实现路由拦截
React Router 4.0之前也像vue中一样有个钩子函数 onEnter 可实现
ReactRouter 4.0开始自己实现如下
// routerMap.js中
import Index from ‘./page/index’
export default [
{ path:‘/’, name: ‘App’, component:Index, auth: true },
…
]
//入口文件 app.js中
import { BrowserRouter as Router, Switch } from “react-router-dom”;
import FrontendAuth from “./FrontendAuth”;
import routerMap from “./routerMap”;
…
return (
)
// 高阶组件FrontendAuth 处理路由跳转,即路由守卫功能
//FrontendAuth.js
import React, { Component } from “react”;
import { Route, Redirect } from “react-router-dom”;
class FrontendAuth extends Component {
// eslint-disable-next-line no-useless-constructor
constructor(props) {
super(props);
}
render() {
const { routerConfig, location } = this.props;
const { pathname } = location;
const isLogin = sessionStorage.getItem(“username”);
console.log(pathname, isLogin);
console.log(location);
// 如果该路由不用进行权限校验,登录状态下登陆页除外
// 因为登陆后,无法跳转到登陆页
// 这部分代码,是为了在非登陆状态下,访问不需要权限校验的路由
const targetRouterConfig = routerConfig.find(
(item) => item.path === pathname
);
console.log(targetRouterConfig);
if (targetRouterConfig && !targetRouterConfig.auth && !isLogin) {
const { component } = targetRouterConfig;
return ;
}
if (isLogin) {
// 如果是登陆状态,想要跳转到登陆,重定向到主页
if (pathname === “/login”) {
return ;
} else {
// 如果路由合法,就跳转到相应的路由
if (targetRouterConfig) {
return (
);
} else {
// 如果路由不合法,重定向到 404 页面
return ;
}
}
} else {
// 非登陆状态下,当路由合法时且需要权限校验时,跳转到登陆页面,要求登陆
if (targetRouterConfig && targetRouterConfig.auth) {
return ;
} else {
// 非登陆状态下,路由不合法时,重定向至 404
return ;
}
}
}
}
export default FrontendAuth;
总结一下,实现路由守卫需要考虑到以下的问题:
未登录情况下,访问不需要权限校验的合法页面:允许访问
未登录情况下,访问需要权限校验的页面:禁止访问,跳转至登陆页
未登录情况下,访问所有的非法页面:禁止访问,跳转至 404
登陆情况下,访问登陆页面:禁止访问,跳转至主页
登陆情况下,访问除登陆页以外的合法页面:允许访问
登陆情况下,访问所有的非法页面:禁止访问,跳转至 404
★★★★ redux中sages和thunk中间件的区别,优缺点
Answer
区别
redux-thunk异步采取 async/await redux-saga采取generate函数
优缺点
redux-thunk
优点: 库小,代码就几行 缺点:代码臃肿,reducer不再是纯粹函数,直接返回对象,违背了当初的设计原则;action的形式不统一,异步操作太为分散,分散在了各个action中
这几行代码做的事情也很简单,判别action的类型,如果action是函数,就调用这个函数
thunk的缺点也是很明显的,thunk仅仅做了执行这个函数,并不在乎函数主体内是什么,也就是说thunk使得redux可以接受函数作为action,但是函数的内部可以多种多样。比如下面是一个获取商品列表的异步操作所对应的action
redux-saga
优点: 将异步与reducer区分开了,更加优雅,适合大量APi请求,而且每个请求之间存在复杂的以来关系 缺点:学习曲线比较陡,理解async await;而且库也比较大,即使发布的最小也有25kb,gzip压缩后也有7KB,React压缩后才45kb
优点:
(1)集中处理了所有的异步操作,异步接口部分一目了然
(2)action是普通对象,这跟redux同步的action一模一样
(3)通过Effect,方便异步接口的测试
(4)通过worker 和watcher可以实现非阻塞异步调用,并且同时可以实现非阻塞调用下的事件监听
(5) 异步操作的流程是可以控制的,可以随时取消相应的异步操作。
缺点:太复杂,学习成本较高
★★ 为什么说React是view(视图层)
Answer
react, 是 Facebook 推出的一个用来构建用户界面的 JavaScript 库. React 主要用于构建 UI
React被认为是视图层的框架是因为它是基于组件的,一切都是组件,而组件就是渲染页面的基础。不论组件中包含的jsx,methods,state,props,都是属于组件内部的
View(视图)是应用程序中处理数据显示的部分。视图层主要包括二个部分:1.视图层显示及交互逻辑;2.视图层的数据结构ViewObj, 包括React中的props和stats;
★★★ 怎么用useEffect模拟生命周期函数?
useEffect
Answer
①默认情况下,它在第一次渲染之后和每次更新之后都会执行,无需清除的effect
// 在函数式组件中 在 return之前
// Similar to componentDidMount and componentDidUpdate:
useEffect(() => {
// Update the document title using the browser API
document.title = You clicked ${count} times
;
});
②需要清除的effect:React 会在组件卸载的时候执行清除操作
useEffect(() => {
function handleStatusChange(status) {
setIsOnline(status.isOnline);
}
ChatAPI.subscribeToFriendStatus(props.friend.id, handleStatusChange);
// Specify how to clean up after this effect:
return function cleanup() {
ChatAPI.unsubscribeFromFriendStatus(props.friend.id, handleStatusChange);
};
});
③使用多个effect实现关注点的分离
把类组件中的分散在多个生命周期中的同一件事件的处理,合并到同一个effect中处理
④通过跳过effect进行性能优化
useEffect(() => {
document.title = You clicked ${count} times
;
}, [count]); // 仅在 count 更改时更新
★★★ useCallback是干什么的?使用useCallback有什么好处?
useCallback
Answer
把内联回调函数及依赖项数组作为参数传入 useCallback,它将返回该回调函数的 memoized(缓存)版本,该回调函数仅在某个依赖项改变时才会更新 好处 当你把回调函数传递给经过优化的并使用引用相等性去避免非必要渲染(例如 shouldComponentUpdate)的子组件时,它将非常有用
★★★ 能简单说一下redux-sage的使用流程吗?
redux-saga
Answer
redux-saga 是一个用于管理 Redux 应用异步操作的中间件(又称异步 action)。 redux-saga 通过创建 Sagas 将所有的异步操作逻辑收集在一个地方集中处理,可以用来代替 redux-thunk 中间件。
Reducers 负责处理 action 的 state 更新
Sagas 负责协调那些复杂或异步的操作
①connet to the store:本质是管理 Redux 应用异步操作的中间件
import { createStore, applyMiddleware } from ‘redux’
import createSagaMiddleware from ‘redux-saga’
import reducer from ‘./reducers’
import mySaga from ‘./sagas’
// Create the saga middleware
const sagaMiddleware = createSagaMiddleware()
// Mount it on the Store
const store = createStore(
reducer,
applyMiddleware(sagaMiddleware)
)
// Then run the saga
sagaMiddleware.run(mySaga)
// Render the application
②initiate a side effect:初始化副作用
import { call, put, takeEvery, takeLatest } from ‘redux-saga/effects’
import Api from ‘…’
// Worker saga will be fired on USER_FETCH_REQUESTED actions
function* fetchUser(action) {
try {
const user = yield call(Api.fetchUser, action.payload.userId);
yield put({type: “USER_FETCH_SUCCEEDED”, user: user});
} catch (e) {
yield put({type: “USER_FETCH_FAILED”, message: e.message});
}
}
// Starts fetchUser on each dispatched USER_FETCH_REQUESTED action
// Allows concurrent fetches of user
function* mySaga() {
yield takeEvery(“USER_FETCH_REQUESTED”, fetchUser);
}
③dispath an action:组件中使用
class UserComponent extends React.Component {
…
onSomeButtonClicked() {
const { userId, dispatch } = this.props
dispatch({type: ‘USER_FETCH_REQUESTED’, payload: {userId}})
}
…
}
④more:takeEvery、takeLatest、take、put、call、fork、select
★★★★ React复用组件的状态和增强功能的方法
Answer
①render props模式
创建Mouse组件,在组件中提供复用的状态逻辑代码
将要复用的状态作为 props.render(state) 方法的参数,暴露到组件外部
使用 props.render() 的返回值作为要渲染的内容
// 子组件
class Mouse extends React.Component{
// mouse本组件的数据
state={
x:0,
y:0
}
// mouse本组件的方法
handleMouse=(e)=>{
this.setState({
x:e.clientX,
y:e.clientY
})
}
componentDidMount(){
window.addEventListener(‘mousemove’,this.handleMouse)
};
render(){
// 在这里用props接收从父传过来的render函数,再把state数据作为实参传递出去
// 其实渲染的就是从父传过来的UI结构,只是公用了Mouse组件的数据和方法
return this.props.render(this.state)
}
}
// 父组件
class App extends React.Component{
render(){
return(
app组件:{this.props.name}
{/* 在使用mouse组件时,给mouse传递一个值(父传子),
只不过这里的props是函数,这个函数将要用形参接受从mouse组件传递过来的实参(state数据) */}
<Mouse render={(mouse)=>{
return (
我用的是Mouse的state和方法:X坐标{mouse.x}-Y坐标{mouse.y}
}} />
}
}
②高阶组件 HOC
const EnhancedComponent = withHOC(WrappedComponent)
// 高阶组件内部创建的类组件:
class Mouse extends React.Component {
render() {
return <WrappedComponent {…this.state} />
}
}
③hooks:自定义hook
// 自定义hook
function useFriendStatus(friendID) {
const [isOnline, setIsOnline] = useState(null);
// …
return isOnline;
}
// 使用
function FriendStatus(props) {
const isOnline = useFriendStatus(props.friend.id);
if (isOnline === null) {
return ‘Loading…’;
}
return isOnline ? ‘Online’ : ‘Offline’;
}
★★★ redux 和 mobx 的区别
edux的数据流向
触发action,匹配到对应的reducer, 修改store中的数据,store中的数据发生改变,触发render方法,重新渲染页面UI
Mobx的核心原理是通过action触发state的变化,进而触发state的衍生对象(computed value & Reactions)
触发action,在action中修改state,通过computed拿到state的计算值,自动触发对应的reactions,这里包含autorun,渲染视图等。有一点需要注意:相对于react来说,mobx没有一个全局的状态树,状态分散在各个独立的store中。mobx的工作原理非常简单,使用Object.defineProperty来拦截对数据的访问,一旦值发生变化,将会调用react的render方法来实现重新渲染视图的功能或者触发autorun
Answer
①Redux的编程范式是函数式的而Mobx是面向对象的
②因此数据上来说Redux理想的是immutable的,每次都返回一个新的数据,而Mobx从始至终都是一份引用。因此Redux是支持数据回溯的
③然而和Redux相比,使用Mobx的组件可以做到精确更新,这一点得益于Mobx的observable;对应的,Redux是用dispatch进行广播,通过Provider和connect来比对前后差别控制更新粒度,有时需要自己写SCU;Mobx更加精细一点
④Mobx-react vs React-rdux:
redux,采取Provider和connect方式,mobx采取Provider和inject、observer
redux将数据保存在单一的store中,mobx将数据保存在分散的多个store中
★★★ react中如何实现命名插槽
Answer
import React, { Component } from ‘react’;
import ReactDOM from ‘react-dom’;
class ParentCom extends React.Component {
constructor(props) {
super(props)
console.log(props);
}
render() {
return (
组件插槽
{this.props.children}
这是放在头部的内容
这是放在主体的内容
这是放在尾部的内容
)
}
}
class ChildCom extends React.Component {
render() {
console.log(this.props);
let headerCom, mainCom, footerCom
this.props.children.forEach((item,index) => {
if (item.props[‘data-position’] === ‘header’) {
headerCom = item
}else if (item.props[‘data-position’] === ‘main’) {
mainCom = item
}else {
footerCom = item
}
})
return (
{headerCom}
{mainCom}
{footerCom}
)
}
}
ReactDOM.render(
子组件1
子组件2
子组件3
,
document.getElementById(‘root’)
)
★★★ 简单说一下,如何在react中实现瀑布流加载?(左右两列的一个商品长列表)
Answer
瀑布流
根据红线,将数据分为两部分,然后根据两边的高度(哪边少往那边加内容)去渲染两个盒子,然后达到一个瀑布流的效果
import React, { Component,Fragment } from ‘react’;
import {connect} from’react-redux’
import Axios from ‘_axios@0.19.0@axios’;
class Waterfall extends Component {
constructor(props) {
super(props);
this.state = {
data:[],//整体的数据
leftData:[],//左边的数据
rightData:[]//右边的数据
}
}
getHW(data){
let heightDate = [0,0];//接收累计高度的容器数组
let rightData =[]//渲染右侧盒子的数组
let leftData = []//渲染左侧盒子的数组
data.forEach(item => {
let height = item.src.replace(‘http://dummyimage.com/’,‘’).substr(0,7).split(‘x’)[1]*1;//对url地址进行一个截取,拿到高度
let minNum = Math.min.apply(null,heightDate)// 从heighetData筛选最小项
let minIndex = heightDate.indexOf(minNum);// 获取 最小项的小标 准备开始进行累加
heightDate[minIndex] = heightDate[minIndex] + height;//从 heightData 中找到最小的项后进行累加,
if(minIndex===0){//[0]加到left [1]加到 right
leftData.push(item)
}else{
rightData.push(item)
}
})
this.setState({ leftData,rightData });//重新set state
}
render() {
let {leftData,rightData} = this.state;
console.log(leftData,rightData)
return (
{
leftData && leftData.map((item,index)=>{
return
})
}
{
rightData && rightData.map((item,index)=>{
return
})
}
);
}
componentDidMount(){
Axios.get(‘/api/data’).then(res=>{
this.props.dispatch({
type:‘SET_DATA’,
data:res.data.data
})
this.getHW(this.props.data) //调用
})
}
}
export default connect(
(state)=>{
return{
data:state.data,
}
}
)(Waterfall);
JavaScript面试真题
前端性能优化
1、减少请求数量 ,图片处理, 雪碧图 , Base64 , 使用字体图标来代替图片
减少重定向, 使用缓存,不使用css@import,避免使用空的src和href,
减少资源大小:html压缩,css压缩,js压缩与混乱,图片压缩
优化网络连接:使用CDN, 使用DNS预解析,持久连接
优化资源加载:
资源加载位置,
1、css文件放在head中,先外链,后本页
2、js文件放在body底部,先外连,后本页
3、处理页面、处理页面布局的js文件放在head中,如babel-polyfill.js文件、flexible.js文件
4、body中尽量不写style标签和script标签
资源加载时机:
1、异步script标签 2、模块按需加载
5、减少重绘回流
7、webpack性能优化
打包公共代码,动态导入和按需加载,删除无用的代码,长缓存优化,公共代码内联
1.★★ 介绍一下JS的内置类型有哪些?
- 空类型:null
- 未定义:undefined
- 布尔:boolean
- 数字:number
- 字符串:string
- 符号:symbol(ES6新增)
- 对象:object
除了对象之外,其他为基本类型.
2. ★★★★ 介绍一下 typeof 区分类型的原理
typeof原理: 不同的对象在底层都表示为二进制,在Javascript中二进制前(低)三位存储其类型信息。
000: 对象
010: 浮点数
100:字符串
110: 布尔
1: 整数
/----------------------------------------------/
typeof null 为"object", 原因是因为 不同的对象在底层都表示为二进制,在Javascript中二进制前(低)三位都为0的话会被判断为Object类型,null的二进制表示全为0,自然前三位也是0,所以执行typeof时会返回"object"
3. ★★★ 介绍一下类型转换
/-------------------显式转换---------------------/
-
toString() // 转化为字符串,不可以转null和underfined
-
Number() // 转换为数字,字符串中有一个不是数值的字符,返回NaN
-
parseInt() // 转换为数字,第一个字符不是数字或者符号就返回NaN
-
String() // 转换为字符串,
-
Boolean() // 转换为布尔值
/-------------------隐式转换(±)---------------------/
当 JavaScript 尝试操作一个 “错误” 的数据类型时,会自动转换为 “正确” 的数据类型 -
num + “” -> String
-
num + bool -> num
// 当加号运算符时,String和其他类型时,其他类型都会转为 String;其他情况,都转化为Number类型 -
string - num -> num
// 其他运算符时, 基本类型都转换为 Number,String类型的带有字符的比如: -
‘a1’ - num -> NaN
// 与undefined 一样。
/-------------------隐式转换(逻辑表达式)---------------------/
- 对象和布尔值比较
对象和布尔值进行比较时,对象先转换为字符串,然后再转换为数字,布尔值直接转换为数字
[] == true; //false []转换为字符串’',然后转换为数字0,true转换为数字1,所以为false - 对象和字符串比较
对象和字符串进行比较时,对象转换为字符串,然后两者进行比较。
[1,2,3] == ‘1,2,3’ // true [1,2,3]转化为’1,2,3’,然后和’1,2,3’, so结果为true; - 对象和数字比较
对象和数字进行比较时,对象先转换为字符串,然后转换为数字,再和数字进行比较。
[1] == 1; // true `对象先转换为字符串再转换为数字,二者再比较 [1] => ‘1’ => 1 所以结果为true - 字符串和数字比较
字符串和数字进行比较时,字符串转换成数字,二者再比较。
‘1’ == 1 // true - 字符串和布尔值比较
字符串和布尔值进行比较时,二者全部转换成数值再比较。
‘1’ == true; // true - 布尔值和数字比较
布尔值和数字进行比较时,布尔转换为数字,二者比较。
true == 1 // true
4. ★★★★ 说说你对 JavaScript 的作用域的理解。什么是作用域链?
在 JavaScript 中有两种作用域类型:
- 局部作用域:只能在函数内部访问它们
- 全局作用域:网页的所有脚本和函数都能够访问它
JavaScript 拥有函数作用域:每个函数创建一个新的作用域。
作用域决定了这些变量的可访问性(可见性)。
函数内部定义的变量从函数外部是不可访问的(不可见的)。
作用域链:
当查找变量的时候,会先从当前上下文的变量对象中查找,
如果没有找到,就会从父级(词法层面上的父级)执行上下文的变量对象中查找,一直找到全局上下文的变量对象,也就是全局对象。
这样由多个执行上下文的变量对象构成的链表就叫做作用域链
5. ★★ 解释下 let 和 const 的块级作用域
var 声明的变量,没有块的概念,可以块访问,不能跨域访问
使用var声明的变量会自动提升到函数作用域顶部
/------------let-----------/
- let声明的仅在块级作用域内有效,
- let不会发生变量提升的现象,所以一定要在定义后使用,否则报错。
- 暂时性死区:只要块级作用域内存在let命令,它所声明的变量就绑定这个区域,不再受外部影响。
- 不允许重复声明,let不允许在相同作用域内,重复声明同一个变量:
/-----------const----------/ - 声明一个只读的常量。一旦声明,常量的值就不能改变。
- 一旦声明,就要立即初始化,否则也报错。
- const命令声明的常量也不提升,同样存在暂时性死区,只能在声明的位置后使用。
- 也不可以重复声明。
在定义的函数名字和变量名相同的情况下,函数提升优先级高于变量的例子:
func(); // 1
var func;
function func() {
console.log(1);
}
func = function() {
console.log(2);
}
输出1,不会输出2。函数声明和变量声明都会被提升,但是需要注意的是函数会先被提升,然后才是变量。
变量提升的例子:
num = 6;
var num = 7;
var num;
console.log(num); // 不报错,输出7,也证明了变量只会声明一次,其他的会被忽略。
6. ★★★★ 说说你对执行上下文的理解
执行上下文有且只有三类,全局执行上下文,函数上下文,与eval上下文(eval一般不会使用)
-
全局执行上下文:
全局执行上下文只有一个,也就是我们熟知的window对象,我们能在全局作用域中通过this直接访问到它 -
函数执行上下文
函数执行上下文可存在无数个,每当一个函数被调用时都会创建一个函数上下文;
需要注意的是,同一个函数被多次调用,都会创建一个新的上下文。 -
执行上下文栈(下文简称执行栈)也叫调用栈,
执行栈用于存储代码执行期间创建的所有上下文,具有LIFO(Last In First Out后进先出,也就是先进后出)的特性。
JS代码首次运行,都会先创建一个全局执行上下文并压入到执行栈中,之后每当有函数被调用,都会创建一个新的函数执行上下文并压入栈内;由于执行栈LIFO的特性,所以可以理解为,JS代码执行完毕前在执行栈底部永远有个全局执行上下文。
7. ★★★ 对闭包的看法,为什么要用闭包?说一下闭包的原理以及应用场景?闭包的 this 指向问题?
闭包的作用:
- 在外部访问函数内部的变量
- 让函数内的局部变量可以一直保存下去
- 模块化私有属性和公共属性
闭包的原理:
全局变量生存周期是永久,局部变量生存周期随着函数的调用介绍而销毁。
闭包就是 在函数中定义且成为该函数内部返回的函数的自由变量 的变量,该变量不会随着外部函数调用结束而销毁。
(注:不光是变量,函数内声明的函数也可以形成闭包)
当函数可以记住并访问所在的词法作用域,即使函数是在当前词法作用域之外执行,这时就产生了闭包。
闭包的应用场景:
// 1. 返回值 最常见的一种形式
var fun_1 = function () {
var name = "limo";
return function () {
return name;
}
}
var fun_1 = function () {
var name = "limo";
return name;
}
var fu_1 = fun_1();
console.log("fu_1():" + fu_1());
// 2. 函数赋值 一种变形的形式是将内部函数赋值给一个外部变量
var f2;
var fun_2 = function () {
var name = "limop"
var a = function () {
return name;
}
f2 = a;
}
f2();
console.log(f2);
// 3. 函数参数 通过函数参数引用内部函数产生闭包
var fn_3 = function (f3) {
console.log(f3);
}
function fun_3() {
var name = "limo";
var a = function () {
return name;
}
fn_3(a)
}
fun_3();
// 4. IIFE(自执行函数)
var fn_4 = function (f4) {
console.log(f4);
};
(function fun_4() {
var name = "limo";
var a = function () {
return name;
}
fn_3(a)
})();
// 5. 循环赋值
function foo(){
var arr = [];
for(var i = 0; i < 10; i++){
arr[i] = (function(n){
return function(){
return n;
}
})(i)
}
return arr;
}
var bar = foo();
console.log(bar[3]());
// 6. getter和setter
// getter和setter函数来将要操作的变量保存在函数内部,防止暴露在外部
var getValue, setValue;
(function () {
var num = 0
getValue = function () {
return num
}
setValue = function (v) {
if (typeof v === 'number') {
num = v
}
}
})();
console.log(getValue()); //0
setValue(10);
console.log(getValue()) //10
// 7.迭代器(计数器)
var add = function(){
var num = 0;
return function(){
return ++num;
}
}();
console.log(add());
console.log(add());
function setUp(arr){
var i = 0;
return function(){
return arr[i++];
}
}
var next = setUp(['Steve','Alex','LingYi']);
console.log(next());
console.log(next());
console.log(next());
// 8.触发事件
window.onload = function (){
var btn = document.querySelector('.btn')
btn.onclick = function (){//事件相当于在全局触发
btn.style.color = 'red'//保持对上层作用域的引用 btn
console.log('abc')
// this
}
}
闭包的this指向问题:
var myNumber = {
value: 1,
add: function(i){
var helper = function(i){
console.log(this);
this.value += i;
}
helper(i);
}
}
myNumber.add(1);
1.this指向window对象(因为匿名函数的执行具有全局性,所以其this对象指向window);
2.不能实现value加1(每个函数在被调用时都会自动取得两个特殊变量,this和arguments,内部函数在搜索这两个对象时,只会搜索到其活动对象为止,所以不能实现访问外部函数的this对象);
3.修改代码实现正确功能
第一种解决方法:
var myNumber={
value:1,
add:function(i){
var that=this;//定义变量that用于保存上层函数的this对象
var helper=function(i){
console.log(that);
that.value+=i;
}
helper(i);
}
}
myNumber.add(1);
第二种解决方法:
var myNumber={
value:1,
add:function(i){
var helper=function(i){
this.value+=i;
}
helper.apply(this,[i]);//使用apply改变helper的this对象指向,使其指向myNumber对象
}
}
myNumber.add(1);
第三种解决方法
var myNumber={
value:1,
add:function(i){
var helper=function(i){
this.value+=i;
}.bind(this,i);//使用bind绑定,和apply相似,只是它返回的是对函数的引用,不会立即执行
helper(i);
}
}
myNumber.add(1);
8. ★★★ 简述闭包的问题以及优化
闭包的缺点:占用内层空间 大量使用闭包会造成 栈溢出
由于闭包会一直占用内存空间,直到页面销毁,我们可以主动将已使用的闭包销毁:
将闭包函数赋值为null 可以销毁闭包
9. ★★★ 如何确定 this 指向?改变 this 指向的方式有哪些?
this 指向:
- 全局上下文(函数外)
无论是否为严格模式,均指向全局对象。注意:严格模式下全局对象为undifined - 函数上下文(函数内)
默认的,指向函数的调用对象,且是最直接的调用对象:
简单调用,指向全局对象注意:严格模式下全局对象为undifined,某些浏览器未实现此标准也可能会是window
改变this指向的方式:
- 第一种: new关键字改变this指向
//构造函数版this
function Fn(){
this.user = “李某”;
}
var a = new Fn();
console.log(a.user); //李某
/----------------------------------------/
2. 第二种: call()
// 把b添加到第一个参数的环境中,简单来说,this就会指向那个对象
var a = {
user:“李某”,
fn:function(){
console.log(this.user); //李某
}
}
var b = a.fn;
b.call(a); //若不用call,则b()执行后this指的是Window对象
/----------------------------------------/
3. 第三种:apply()
// apply方法和call方法有些相似,它也可以改变this的指向,也可以有多个参数,但是不同的是,第二个参数必须是一个数组
var a = {
user:“李某”,
fn:function(){
console.log(this.user); //李某
}
}
var b = a.fn;
b.apply(a);
/----------------------------------------/
4. 第四种:bind()
// bind方法返回的是一个修改过后的函数,
// bind也可以有多个参数,并且参数可以执行的时候再次添加,但是要注意的是,参数是按照形参的顺序进行的。
var a = {
user:“李某”,
fn:function(){
console.log(this.user); //李某
}
}
var b = a.fn;
var c = b.bind(a);
c();
10. ★★★ 介绍箭头函数的 this
由于箭头函数不绑定this, 它会捕获其所在(即定义的位置)上下文的this值, 作为自己的this值
- 所以 call() / apply() / bind() 方法对于箭头函数来说只是传入参数,对它的 this 毫无影响。
- 考虑到 this 是词法层面上的,严格模式中与 this 相关的规则都将被忽略
作为方法的箭头函数this指向全局window对象,而普通函数则指向调用它的对象
11. ★★★ 谈一下你对原型链的理解,画一个经典的原型链图示
原型链:
因为每个对象和原型都有原型,对象的原型指向原型对象,
而父的原型又指向父的父,这种原型层层连接起来的就构成了原型链。
img
12. ★★★ ES5/ES6 的继承除写法以外还有什么区别?
- ES5 的继承实质上是先创建子类的实例对象,然后再将父类的方法添加 到 this 上(Parent.apply(this)).
- ES6 的继承机制完全不同,实质上是先创建父类的实例对象 this(所以必 须先调用父类的super()方法),然后再用子类的构造函数修改 this。
- ES5 的继承时通过原型或构造函数机制来实现。
- ES6 通过 class 关键字定义类,里面有构造方法,类之间通过 extends 关 键字实现继承。
- 子类必须在 constructor 方法中调用 super 方法,否则新建实例报错。因 为子类没有自己的 this对象,而是继承了父类的 this 对象,然后对其进行加工。 如果不调用 super 方法,子类得不到 this 对象。
- 注意 super 关键字指代父类的实例,即父类的 this 对象。 注意:在子类构造函数中,调用 super 后,才可使用 this关键字,否则报错
13. ★★★★ 你对事件循环有了解吗?说说看
Event Loop(事件循环)中,每一次循环称为 tick, 每一次tick的任务如下:
执行栈选择最先进入队列的宏任务(通常是script整体代码),如果有则执行
检查是否存在 Microtask,如果存在则不停的执行,直至清空 microtask 队列
更新render(每一次事件循环,浏览器都可能会去更新渲染)
重复以上步骤
宏任务 > 所有微任务 > 宏任务,如下图所示:
img
从上图我们可以看出:
- 将所有任务看成两个队列:执行队列与事件队列。
- 执行队列是同步的,事件队列是异步的,宏任务放入事件列表,微任务放入执行队列之后,事件队列之前。
- 当执行完同步代码之后,就会执行位于执行列表之后的微任务,然后再执行事件列表中的宏任务
14. ★★★★ 微任务和宏任务有什么区别?
宏任务(macrotask) 微任务(microtask)
谁发起的 宿主(Node、浏览器) JS引擎
具体事件 1. script (可以理解为外层同步代码) 2. setTimeout/setInterval 3. UI rendering/UI事件 4. postMessage,MessageChannel 5. setImmediate,I/O(Node.js) 1. Promise 2. MutaionObserver 3. Object.observe(已废弃;Proxy 对象替代) 4. process.nextTick(Node.js)
谁先运行 后运行 先运行
会触发新一轮Tick吗 会 不会
15. ★★★★★ 浏览器和 Node 事件循环的区别?
浏览器中的事件循环:
img
Node中的事件循环:
Node 中的 Event Loop 和浏览器中的是完全不相同的东西。Node.js 采用 V8 作为 js 的解析引擎,而 I/O 处理方面使用了自己设计的 libuv,libuv 是一个基于事件驱动的跨平台抽象层,封装了不同操作系统一些底层特性,对外提供统一的 API,事件循环机制也是它里面的实现(下文会详细介绍)。
img
Node.js 的运行机制如下:
V8 引擎解析 JavaScript 脚本。
解析后的代码,调用 Node API。
libuv 库负责 Node API 的执行。它将不同的任务分配给不同的线程,形成一个 Event Loop(事件循环),以异步的方式将任务的执行结果返回给 V8 引擎。
V8 引擎再将结果返回给用户。
16. ★★★ 异步解决方案有哪些?
解决方案:
/---------1.回调函数callback:----------/
被作为实参传入另一函数,并在该外部函数内被调用,用以来完成某些任务的函数。如setTimeOut,ajax请求,readFile等。
例:
function greeting(name) {
alert('Hello ’ + name);
}
function processUserInput(callback) {
var name = prompt(‘请输入你的名字。’);
callback(name);
}
processUserInput(greeting);
优点:
解决了异步的问题。
缺点:
回调地狱:多个回调函数嵌套的情况,使代码看起来很混乱,不易于维护。
/---------2.事件发布订阅:---------/
当一个任务执行完成后,会发布一个事件,当这个事件有一个或多个‘订阅者’的时候,会接收到这个事件的发布,执行相应的任务,这种模式叫发布订阅模式。如node的events,dom的事件绑定
例:
document.body.addEventListener(‘click’,function(){
alert(‘订阅了’);
},false);
document.body.click();
优点:
时间对象上的解耦。
缺点:
消耗内存,过度使用会使代码难以维护和理解
/---------3.Promise:---------/
Promise是es6提出的异步编程的一种解决方案。
Promise 对象有三种状态:
pending: 初始状态,既不是成功,也不是失败状态。
fulfilled: 意味着操作成功完成。
rejected: 意味着操作失败。
promise的状态只能从pending变成fulfilled,和pending变成rejected,状态一旦改变,就不会再改变,且只有异步操作的结果才能改变promise的状态。
例:
let promise = new Promise(function (resolve, reject) {
fs.readFile(‘./1.txt’, ‘utf8’, function (err, data) {
resolve(data)
})
})
promise
.then(function (data) {
console.log(data)
})
优点:
解决了回调地狱的问题,将异步操作以同步操作的流程表达出来。
缺点:
无法取消promise。如果不设置回调函数,Promise内部抛出的错误,不会反应到外部。当处于Pending状态时,无法得知目前进展到哪一个阶段(刚刚开始还是即将完成)。当执行多个Promise时,一堆then看起来也很不友好。
/---------4.Generator:---------/
Generator是es6提出的另一种异步编程解决方案,需要在函数名之前加一个*号,函数内部使用yield语句。Generaotr函数会返回一个遍历器,可以进行遍历操作执行每个中断点yield。
例:
function * count() {
yield 1
yield 2
return 3
}
var c = count()
console.log(c.next()) // { value: 1, done: false }
console.log(c.next()) // { value: 2, done: false }
console.log(c.next()) // { value: 3, done: true }
console.log(c.next()) // { value: undefined, done: true }
优点:
没有了Promise的一堆then(),异步操作更像同步操作,代码更加清晰。
缺点:
不能自动执行异步操作,需要写多个next()方法,需要配合使用Thunk函数和Co模块才能做到自动执行。
/---------5.async/await:---------/
async是es2017引入的异步操作解决方案,可以理解为Generator的语法糖,async等同于Generator和co模块的封装,async 函数返回一个 Promise。
例:
async function read() {
let readA = await readFile(‘data/a.txt’)
let readB = await readFile(‘data/b.txt’)
let readC = await readFile(‘data/c.txt’)
console.log(readA)
console.log(readB)
console.log(readC)
}
read()
优点:
内置执行器,比Generator操作更简单。async/await比*yield语义更清晰。返回值是Promise对象,可以用then指定下一步操作。代码更整洁。可以捕获同步和异步的错误。
17. ★★★ async 和 await 、promise的区别 和 这两个的本质
/---------Promise概念:---------/
Promise 是异步编程的一种解决方案,比传统的解决方案——回调函数和事件——更合理和更强大,简单地说,Promise好比容器,里面存放着一些未来才会执行完毕(异步)的事件的结果,而这些结果一旦生成是无法改变的
/---------async await概念:---------/
async await也是异步编程的一种解决方案,他遵循的是Generator 函数的语法糖,他拥有内置执行器,不需要额外的调用直接会自动执行并输出结果,它返回的是一个Promise对象。
两者的区别:
Promise的出现解决了传统callback函数导致的“地域回调”问题,但它的语法导致了它向纵向发展行成了一个回调链,遇到复杂的业务场景,这样的语法显然也是不美观的。而async await代码看起来会简洁些,使得异步代码看起来像同步代码,await的本质是可以提供等同于”同步效果“的等待异步返回能力的语法糖,只有这一句代码执行完,才会执行下一句。
async await与Promise一样,是非阻塞的。
async await是基于Promise实现的,可以说是改良版的Promise,它不能用于普通的回调函数。
18. ★★★ 简述 aync await 的好处
- async/await最重要的好处是同步编程风格
- async/await有本地浏览器支持。截至今天,所有主流浏览器 查看都完全支持异步功能。
- async关键字。它声明 getBooksByAuthorWithAwait()函数返回值确保是一个 promise,以便调用者可以安全调用 getBooksByAuthorWithAwait().then(…)或 await getBooksByAuthorWithAwait()
19. ★★★ 移动端点击事件 300ms 延迟如何去掉?原因是什么?
300毫秒原因:
当用户第一次点击屏幕后,需要判断用户是否要进行双击操作,于是手机会等待300毫秒,
解决方法:FastClick.js
FastClick 是 FT Labs 专门为解决移动端浏览器 300 毫秒点击延迟问题所开发的一个轻量级的库。FastClick的实现原理是在检测到touchend事件的时候,会通过DOM自定义事件立即出发模拟一个click事件,并把浏览器在300ms之后的click事件阻止掉。
20. ★★★ Cookie 有哪些属性?其中HttpOnly,Secure,Expire分别有什么作用?
Cookie属性:
name 字段为一个cookie的名称。
value 字段为一个cookie的值。
domain 字段为可以访问此cookie的域名。
path 字段为可以访问此cookie的页面路径。 比如domain是abc.com,path是/test,那么只有/test路径下的页面可以读取此cookie。
expires/Max-Age 字段为此cookie超时时间。若设置其值为一个时间,那么当到达此时间后,此cookie失效。不设置的话默认值是Session,意思是cookie会和session一起失效。当浏览器关闭(不是浏览器标签页,而是整个浏览器) 后,此cookie失效。
Size 字段 此cookie大小。
http 字段 cookie的httponly属性。若此属性为true,则只有在http请求头中会带有此cookie的信息,而不能通过document.cookie来访问此cookie。
secure 字段 设置是否只能通过https来传递此条cookie
1 secure属性
当设置为true时,表示创建的 Cookie 会被以安全的形式向服务器传输,也就是只能在 HTTPS 连接中被浏览器传递到服务器端进行会话验证,如果是 HTTP 连接则不会传递该信息,所以不会被窃取到Cookie 的具体内容。
2 HttpOnly属性
如果在Cookie中设置了"HttpOnly"属性,那么通过程序(JS脚本、Applet等)将无法读取到Cookie信息,这样能有效的防止XSS攻击。
3 Expire属性
设置Cookie的失效时间:
21. ★★★★ 如何实现函数的柯里化?比如 add(1)(2)(3)
/-----解决方法1:-----/
function add () {
var args = Array.prototype.slice.call(arguments);
var fn = function () {
var sub_arg = Array.prototype.slice.call(arguments);
// 把全部的参数聚集到参数的入口为一个参数: args.concat(sub_arg)
return add.apply(null, args.concat(sub_arg));
}
fn.valueOf = function () {
return args.reduce(function(a, b) {
return a + b;
})
}
return fn;
}
console.log(add(1,2)) // 3
console.log(add(1)(2)) // 3
console.log(add(1)(2)(3)) // 6
console.log(add(1,2,3)(4)) // 10
/-----解决方法2:-----/
function add () {
var args = Array.prototype.slice.call(arguments);
var fn = function () {
// 把参数都放在一个相当于全局变量的 args 里面
args.push(...arguments)
return fn;
}
fn.valueOf = function () {
return args.reduce(function(a, b) {
return a + b;
})
}
return fn;
}
console.log(add(1,2)) // 3
console.log(add(1)(2)) // 3
console.log(add(1)(2)(3)) // 6
console.log(add(1,2,3)(4)) // 10
22. ★★★★ 什么是反柯里化
在JavaScript中,当我们调用对象的某个方法时,其实不用去关心该对象原本是否被设计为拥有这个方法,这是动态类型语言的特点。可以通过反柯里化(uncurrying)函数实现,让一个对象去借用一个原本不属于他的方法。
23. ★★ 将 [1,2] 与 [3,[4]] 合并为 [1,2,3,[4]]
JS数组合并方法:
let arr3 = [1,2].concat([3,[4]]); //[1,2,3,[4]]
24. ★★ Array.forEach() 与 Array.map() 的区别,Array.slice() 与 Array.splice() 的区别?
/-----forEach-----/
forEach不支持return,对原来的数组也没有影响。但是我们可以自己通过数组的索引来修改原来的数组
var ary = [12,23,24,42,1];
var res = ary.forEach(function (item,index,input) {
input[index] = item10;
})
console.log(res);//–>undefined;
console.log(ary);//–>会对原来的数组产生改变;
/-----map-----*/
map支持return返回值,也不影响原数组,但是会返回一个新的数组
var ary = [12,23,24,42,1];
var res = ary.map(function (item,index,input) {
return item*10;
})
console.log(res);//–>[120,230,240,420,10];
console.log(ary);//–>[12,23,24,42,1];
array.slice(start,end)函数是取array数组中指定的一些元素:
根据数组下标start和end,两个参数为取值的开始和结束下标,取出的值不包括end位置的值,生成一个新数组作为返回值;
这里end可以为空,为空则取从start位置到数组结尾的元素,生成新数组。
array.splice(start,length,insert_one…)函数则是直接在原数组进行删除、添加、替换元素的操作:
start为数组删除元素的开始下标,
length为从start位置开始array删除元素的个数,
后面参数为删除之后array重新插入的数据内容,插入位置为删除位置,而非数组开头或末尾,
返回值为array删除的元素组成的数组。
显而易见,splice函数可以用来对数组元素进行替换。由splice操作后的数组array,数组中内容如果已经改变,就再也找不回array在splice之前的模样。
25. ★★ 将 1234567 转换为 1,234,567
function fun(n){
return String(n).replace(/(?!^)(?=(\d{3})+.)/g, “,”)
}
26. ★★★ bind 的作用是什么?
bind()方法主要就是将函数绑定到某个对象,
bind()会创建一个函数,函数体内的this对象的值会被绑定到传入bind()第一个参数的值
27. ★★ Promise.resolve(Promise.resolve(1)).then(console.log) 输出?
// 答案:1
28. ★★★ var let const的区别
- var声明的变量会挂载在window上,而let和const声明的变量不会
- var声明变量存在变量提升,let和const不存在变量提升
- let和const声明形成块作用域
- 同一作用域下let和const不能声明同名变量,而var可以
- 使用let/const声明的变量在当前作用域存在暂存死区
- const一旦声明必须赋值,不能使用null占位,声明后不能再修改,如果声明的是复合类型数据,可以修改其属性
29. ★★★ document load 和 documen ready的区别
DOM文档解析:
解析html结构
加载脚本和样式文件
解析并执行脚本
构造html的DOM模型 //ready
加载图片等外部资源文件
页面加载完毕 //load
document load:
load是当页面所有资源全部加载完成后(包括DOM文档树,css文件,js文件,图片资源等),执行一个函数,load方法就是onload事件。
documen ready:
构造html的DOM模型加载完毕后触发
30. ★★★ 如何自定义事件?
自定义事件
事件是与DOM交互的最常见的方式。通过实现自定义事件,可以让事件用于非DOM代码中。
思想:创建一个管理事件的对象,让其他对象监听那些事件。
基本模式:
function EventTarget(){
this.handlers = {};
}
EventTarget.prototype = {
constructor:EventTarget,
addHandler:function(type,handler){
if(typeof this.handlers[type] === “undefined”){
this.handlers[type] = [];
}
this.handlers[type].push(handler);
},
fire:function(event){
if(!event.target){
event.target = this;
}
if(this.handlers[event.type] instanceof Array){
const handlers = this.handlers[event.type];
handlers.forEach((handler)=>{
handler(event);
})
}
},
removeHandler:function(type,handler){
if(this.handlers[type] instanceof Array){
const handlers = this.handlers[type];
for(var i = 0,len = handlers.length; i < len; i++){
if(handlers[i] === handler){
break;
}
}
handlers.splice(i,1);
}
}
}
31. ★★★ 如何用 setTImeout 来实现 setInterval?
1.不去关心回调函数是否还在运行
在某些情况下,函数可能需要比间隔时间更长的时间去完成执行。比如说是用setInterval每隔5秒对远端服务器进行轮询,网络延迟,服务器无响应以及其他因素将会阻止请求按时按成。结果会导致返回一串无必要的排成队列请求。
2.忽视错误
因为某些原因,setInterval调用的代码中会出现一个错误,但是代码并不会中止执行而是继续执行错误的代码。
3.缺乏灵活性
除了前面提到的缺点之外,我非常希望setInterval方法能有一个表明执行次数的参数而不是无休止的执行下去。
function interval(func, w, t){
var interv = function(){
if(typeof t === “undefined” || t-- > 0){
setTimeout(interv, w);
try{
func.call(null);
}
catch(e){
t = 0;
throw e.toString();
}
}
};
setTimeout(interv, w);
};
32. ★★★ 如何判断 user 对象里有没有 a 这个属性?如果把user对象中所有的属性都输出出来?
(var user = {‘a’: ‘19’, ‘b’: ‘18’, ‘c’: ‘16’})
如何判断 user 对象里有没有 a 这个属性?
js对象的Object.hasOwnProperty()方法
返回一个布尔值,判断对象是否包含特定的自身(非继承)属性。
let obj = new Object();
obj.a = “123”;
console.log(obj.hasOwnProperty(‘a’)) // true
console.log(obj.hasOwnProperty(‘b’)) // false
把user对象中所有的属性都输出出来
for(item for user){
console.log(item)
}
33. ★★ 使用 setTimeout 模拟 setInterval 的功能做一个60秒的倒数计时
function setInter(s,fn){
let timeOut = (s,fn)=>{
setTimeout(()=>{
fn();
timeOut(s,fn);
},s)
}
timeOut(s,fn);
}
//调用上面的方法
setInter(60000,()=>{console.log(“hello world!”)})
34. ★★★ 实现一个函数 add(),运算结果可以满足如下预期结果
function add () {
var args = Array.prototype.slice.call(arguments);
var fn = function () {
var sub_arg = Array.prototype.slice.call(arguments);
// 把全部的参数聚集到参数的入口为一个参数: args.concat(sub_arg)
return add.apply(null, args.concat(sub_arg));
}
fn.valueOf = function () {
return args.reduce(function(a, b) {
return a + b;
})
}
return fn;
}
add(1,2,3)(10) //16
add(1)(2)(3,4)(5) //15
35. ★★★ 如何避免回调地狱?
- Promise 对象就是为了解决这个问题而提出的。它不是新的语法功能,而是一种新的写法,允许将回调函数的嵌套,改成链式调用。
promise只有两个状态resolve和reject,当它触发任何一个状态后,它会将当前的值缓存起来,并在有回调函数添加进来的时候尝试调用回调函数,如果这个时候还没有触发resolve或者reject,那么回调函数会被缓存,等待调用,如果已经有了状态(resolve或者reject),则立刻调用回调函数。并且所有回调函数在执行后都立即被销毁。
-
ES6 co/yield方案
yield: Generator 函数是协程在 ES6 的实现,而yield是 Generator关键字, 异步操作需要暂停的地方,都用yield语句注明。
co: co 模块是著名程序员 TJ Holowaychuk 于 2013 年 6 月发布的一个小工具,用于 Generator 函数的自动执行。 -
ES7 async/await 方案
async/await是es7的新标准,并且在node7.0中已经得到支持。
它就是 Generator 函数的语法糖,async函数就是将 Generator 函数的星号(*)替换成async,将yield替换成await,仅此而已。可以理解官方对co和Generator 封装方案。
36. ★★ 写一个 function,清除字符串前后的空格。(兼容所有的浏览器)
function trim(str) {
if (str && typeof str === “string”) {
return str.replace(/(^\s*)|(\s*)$/g,“”); //去除前后空白符
}
}
37. ★★ 使用正则表达式验证邮箱格式
function fChkMail(emailAddress){
var reg = new RegExp(“1+([._\-][a-z0-9])@([a-z0-9]+[-a-z0-9]*[a-z0-9]+.){1,63}[a-z0-9]+$”);
var bChk=reg.test(emailAddress);
return bChk;
}
38. ★★★ 简述同步和异步的区别
同步:
同步的思想是:所有的操作都做完,才返回给用户。这样用户在线等待的时间太长,给用户一种卡死了的感觉(就是系统迁移中,点击了迁移,界面就不动了,但是程序还在执行,卡死了的感觉)。这种情况下,用户不能关闭界面,如果关闭了,即迁移程序就中断了。
异步:
将用户请求放入消息队列,并反馈给用户,系统迁移程序已经启动,你可以关闭浏览器了。然后程序再慢慢地去写入数据库去。这就是异步。但是用户没有卡死的感觉,会告诉你,你的请求系统已经响应了。你可以关闭界面了。
同步和异步本身是相对的:
同步就相当于是 当客户端发送请求给服务端,在等待服务端响应的请求时,客户端不做其他的事情。当服务端做完了才返回到客户端。这样的话客户端需要一直等待。用户使用起来会有不友好。
异步就是,当客户端发送给服务端请求时,在等待服务端响应的时候,客户端可以做其他的事情,这样节约了时间,提高了效率。
存在就有其道理 异步虽然好 但是有些问题是要用同步用来解决,比如有些东西我们需要的是拿到返回的数据在进行操作的。这些是异步所无法解决的。
39. ★★ JavaScript 中 callee 和 caller 的作用
1.callee
callee是对象的一个属性,该属性是一个指针,指向参数arguments对象的函数
作用:就是用来指向当前对象
返回正被执行的 Function 对象,也就是所指定的 Function 对象的正文.
callee是arguments 的一个属性成员,它表示对函数对象本身的引用,这有利于匿名
函数的递归或者保证函数的封装性
2.caller
caller是函数对象的一个属性,该属性保存着调用当前函数的函数的引用(指向当前函数的直接父函数)
返回一个对函数的引用,该函数调用了当前函数。
functionName.caller
functionName 对象是所执行函数的名称。
注意:
对于函数来说,caller 属性只有在函数执行时才有定义。 如果函数是由 Javascript 程序的顶层调用的,那么 caller 包含的就是 null 。
40. ★★ 统计字符串中字母个数或统计最多的字母数
function count(str) {
var obj = {}; // 统计对象
var i = 0;
var len = str.length;
for (; i < len; i++){
var curChar = str.charAt(i);
// 如果结果对象存在该字符的属性,则自增,否则置为1
if (obj[curChar]) {
obj[curChar]++;
} else {
obj[curChar] = 1;
}
}
// 返回结果
return obj;
}
var str = “javaScript”;
console.log(count(str));
41. ★★★ jQuery 的事件委托方法 on,live,delegate之间有区别?
live 把事件委托交给了document(根节点),document 向下去寻找符合条件的元素(), 不用等待document加载结束也可以生效。
delegate可指定事件委托对象,相比于live性能更优,直接锁定指定选择器;
on事件委托对象选填,如果不填,即给对象自身注册事件,填了作用和delegate一致。
42. ★★★ 简述下 Promise 对象
Promise是异步编程的一种解决方案,比传统的解决方案(回调函数和事件)更合理更强大。
所谓Promise,简单说就是一个容器,里面保存着某个未来才会结束的事件 (通常是一个异步操作)的结果。从语法上说,Promise是一个对象,从它可以获取异步操作的消息。
Promise对象有以下2个特点:
1.对象的状态不受外界影响。Promise对象代表一个异步操作,有三种状态:Pending(进行中)、Resolved(已完成)和Rejected(已失败)。只有异步操作的结果,可以决定当前是哪一种状态,任何其他操作都无法改变这个状态。这也是Promise这个名字的由来,它的英语意思就是“承诺”,表示其他手段无法改变。
2.一旦状态改变,就不会再变,任何时候都可以得到这个结果。Promise对象的状态改变,只有两种可能:从Pending变为Resolved;从Pending变为Rejected。只要这两种情况发生,状态就凝固了,不会再变了,会一直保持这个结果。就算改变已经发生了,你再对Promise对象田静回调函数,也会立即得到这个结果。这与事件(Event)完全不同,事件的特点是,如果你错过了它,再去监听,是得不到结果的。
有了Promise对象,就可以把异步操作以同步操作的流程表达出来,避免了层层嵌套的回调函数。此外,Promise对象提供了统一的接口,使得控制异步操作更加容易。
43. ★★★ 数组扁平化,不用 api
function myFlat(arr){
let res = [];
for(let i=0; i<arr.length; i++){
if(arr[i] instanceof Array){
res = res.concat(myFlat(arr[i]));
}else {
res.push(arr[i]);
}
}
return res;
}
let arr = [1,[2,3,[4,5]]];
console.log(myFlat(arr))
44. ★★★ 用 JavaScript 实现观察者模式
function BusinessOne(name){
this.name = name;
//订阅者的集合
this.subscribers = new Array();
}
//订阅者的发送消息的方法(推模式)
BusinessOne.prototype.delive = function(news){
var self = this;
//给每一个订阅者发送消息
this.subscribers.forEach(
function(fn){
//调用接受者处理信息的函数
fn(news,self);
}
)
}
//扩展公共订阅的函数,和取消订阅的函数
Function.prototype.subscribe = function(publisher){
var that = this;
//some 访问数组度i型并且以参数的形式传回回调函数中
//只要至少有一次返回是true那么some就是true
var alreadyExists = publisher.subscribers.some(
function(el){
//处理不能重复订阅的功能
if(el == that){
return;
}
}
);
//没用订阅你就可以订阅
if(!alreadyExists){
publisher.subscribers.push(that);
}
return this;
}
//取消
Function.prototype.unsubscribe = function(publisher){
var that = this;
publisher.subscribers = publisher.subscribers.filter(
function(el){
if(el !== that){
return el;
}
}
);
return this;
};
45. ★★ 简述一下面象对象的六法则
- 单一职责原则:一个类只做它该做的事情
- 开闭原则:软件实体应当对扩展开放,对修改关闭
- 依赖倒转原则:面向接口编程
- 接口隔离原则:接口要小而专,绝不能大而全
- 合成聚合复用原则:优先使用聚合或合成关系复用代码
- 迪米特法则:迪米特法则又叫最少知识原则,一个对象应当对其他对象有尽可能少的了解(低耦合)
46. ★★★ 谈谈垃圾回收机制方法以及内存管理
垃圾回收方式
① 标记清除
工作原理:是当变量进入环境时,将这个变量标记为“进入环境”。当变量离开环境时,则将其标记为“离开环境”。标记“离开环境”的就回收内存。
② 引用计数
工作原理:跟踪记录每个值被引用的次数。一旦没有引用,内存就直接释放了。
内存管理
什么时候触发垃圾回收?
垃圾回收器周期性运行,如果分配的内存非常多,那么回收工作也会很艰巨,确定垃圾回收时间间隔就变成了一个值得思考的问题。
1、合理的GC方案:(1)、遍历所有可访问的对象; (2)、回收已不可访问的对象。
2、GC缺陷: (1)、停止响应其他操作;
3、GC优化策略: (1)、分代回收(Generation GC);(2)、增量GC
47. ★★★ 开发过程中遇到内存泄漏的问题都有哪些?
- 当页面中元素被移除或替换时,若元素绑定的事件仍没被移除,在IE中不会作出恰当处理,此时要先手工移除事件,不然会存在内存泄露。
- 由于是函数内定义函数,并且内部函数–事件回调的引用外暴了,形成了闭包。闭包可以维持函数内局部变量,使其得不到释放。
48. ★★★ 请编写获取当前窗口地址中查询参数name的值,当前窗口地址为:https://foo.com/?id=1&name=tom
function GetQueryString(name){
var reg = new RegExp(“(^|&)”+ name +“=([^&]*)(&|$)”);
var r = window.location.search.substr(1).match(reg);
if(r!=null)
return unescape(r[2]);
return null;
}
49. ★★★ 已知a,b两个构造函数,现在 let c = new a(),如何在c的存储地址不变的情况下,改变c的继承(c->a 转为 c->b)
- 改变原型链:通过改变C的prototype为b,实现内存地址不动,改变继承
50. ★★★ 浏览器有哪些兼容问题,你封装过什么插件
//1.滚动条到顶端的距离(滚动高度)
var scrollTop = document.documentElement.scrollTop || document.body.scrollTop;
//2.滚动条到左端的距离
var scrollLeft = document.documentElement.scrollLeft || document.body.scrollLeft;
//3. IE9以下byClassName
function byClassName(obj,className){
//判断是否支持byClassName
if(obj.getElementsByClassName){
//支持
return obj.getElementsByClassName(className);
}else{
//不支持
var eles = obj.getElementsByTagName(‘*’); //获取所有的标签
var arr = []; //空数组,准备放置找到的对象
//遍历所有的标签
for(var i = 0,len = eles.length;i < len;i ++){
//找出与我指定class名相同的对象
if(eles[i].className === className){
arr.push(eles[i]); //存入数组
}
}
return arr; //返回
}
}
//4. 获取非行内样式兼容 IE:currentStyle 标准:getComputedStyle
function getStyle(obj,attr){
return window.getComputedStyle ? getComputedStyle(obj,true)[attr] : obj.currentStyle[attr];
}
//div.style.width = ‘’;设置样式
//obj[‘属性’]: 对象是变量时,必须用对象[‘属性’]获取。
//5. 获取事件对象的兼容
evt = evt || window.event
//6. 获取鼠标编码值的兼容
function getButton(evt){
var e = evt || window.event;
if(evt){
return e.button;
}else if(window.event){
switch(e.button){
case 1 : return 0;
case 4 : return 1;
case 2 : return 2;
}
}
}
//7. 获取键盘按键编码值的兼容
var key = evt.keyCode || evt.charCode || evt.which;
//8. 阻止事件冒泡的兼容
e.stopPropagation ? e.stopPropagation() : e.cancelBubble = true;
//9. 阻止超链接的默认行为的兼容
evt.preventDefault ? evt.preventDefault() : evt.returnValue = false;
//10. 添加事件监听器的兼容
function addEventListener(obj,event,fn,boo){
if(obj.addEventListener){
obj.addEventListener(event,fn,boo);
}else if(obj.attachEvent){
obj.attachEvent(‘on’ + event,fn);
}
}
//11. 移除事件监听器的兼容
function removeEventListener(obj,event,fn,boo){
if(obj.removeEventListener){
obj.removeEventListener(event,fn,boo);
}else if(obj.detachEvent){
obj.detachEvent(‘on’ + event,fn);
}
}
//12. 获取事件源的兼容
var target = event.target || event.srcElement;
51. ★★★ 如何判断一个对象是否为数组,函数
方法一: instanceof:
var arr=[];
console.log(arr instanceof Array) //返回true
方法二: constructor:
console.log(arr.constructor == Array); //返回true
方法三: Array.isArray()
console.log(Array.isArray(arr)); //返回true
52. ★★★ 写一个函数,接受可变个数参数,且每个参数均为数字,返回参数的最大值
function myMax(){
return Math.max(arguments)
}
53. ★★★ 请写出 ES6 Array.isArray()
if (!Array.isArray){
Array.isArray = function(arg){
return Object.prototype.toString.call(arg) === ‘[object Array]’;
};
}
54. ★★★ 实现一个函数 clone,可以对 JavaScript 中的5种主要数据类型进行值复制
// 方法一:
Object.prototype.clone = function() {
var o = this.constructor === Array ? [] : {};
for (var e in this) {
o[e] = typeof this[e] === “object” ? this[e].clone() : this[e];
}
return o;
};
//方法二:
/**
* 克隆一个对象
* @param Obj
* @returns
*/
function clone(Obj) {
var buf;
if (Obj instanceof Array) {
buf = []; //创建一个空的数组
var i = Obj.length;
while (i–) {
buf[i] = clone(Obj[i]);
}
return buf;
} else if (Obj instanceof Object) {
buf = {}; //创建一个空对象
for (var k in Obj) {
//为这个对象添加新的属性
buf[k] = clone(Obj[k]);
}
return buf;
} else {
//普通变量直接赋值
return Obj;
}
}
55. ★★★ 假如A页面我定义了一个定时器,然后跳到B页面如果让A页面的定时器暂停
方法1:在beforeDestroy()等生命周期结束阶段内清除定时器:
beforeDestroy() {
clearInterval(this.timer);
this.timer = null;
}
方法2:通过$once这个事件侦听器器在定义完定时器之后的位置来清除定时器。
const timer = setInterval(() =>{
// 某些定时器操作
}, 500);
// 通过
o
n
c
e
来监听定时器,在
b
e
f
o
r
e
D
e
s
t
r
o
y
钩子可以被清除。
t
h
i
s
.
once来监听定时器,在beforeDestroy钩子可以被清除。 this.
once来监听定时器,在beforeDestroy钩子可以被清除。this.once(‘hook:beforeDestroy’, () => {
clearInterval(timer);
})
56. ★★★ promise的实现原理,如果我现在向服务器发送一个请求,但是我后悔了,不想让服务器返回数据,去实现一个delay
取消结束Promise的方法?
-
返回一个pending状态的Promise,原Promise链会终止
Promise.resolve().then(() => {
console.log(‘ok1’)
return new Promise(()=>{}) // 返回“pending”状态的Promise对象
}).then(() => {
// 后续的函数不会被调用
console.log(‘ok2’)
}).catch(err => {
console.log(‘err->’, err)
}) -
Promise.race竞速方法
let p1 = new Promise((resolve, reject) => {
resolve(‘ok1’)
})
let p2 = new Promise((resolve, reject) => {
setTimeout(() => {resolve(‘ok2’)}, 10)
})
Promise.race([p2, p1]).then((result) => {
console.log(result) //ok1
}).catch((error) => {
console.log(error)
})
- 当Promise链中抛出错误时,错误信息沿着链路向后传递,直至捕获
Promise.resolve().then(() => {
console.log(‘ok1’)
throw ‘throw error1’
}).then(() => {
console.log(‘ok2’)
}, err => {
// 捕获错误
console.log(‘err->’, err)
}).then(() => {
// 该函数将被调用
console.log(‘ok3’)
throw ‘throw error3’
}).then(() => {
// 错误捕获前的函数不会被调用
console.log(‘ok4’)
}).catch(err => {
console.log(‘err->’, err)
Axios如何取消请求?
第一种通过CancelToken.source工厂方法创建cancel token
var CancelToken = axios.CancelToken;
var source = CancelToken.source();
axios.get(‘/user/12345’, {
cancelToken: source.token
}).catch(function(thrown) {
if (axios.isCancel(thrown)) {
console.log(‘Request canceled’, thrown.message);
} else {
// 处理错误
}
});
// 取消请求(message 参数是可选的)
source.cancel(‘Operation canceled by the user.’);
第二种通过传递executor函数到CancelToken的构造函数来创建cancel token
var CancelToken = axios.CancelToken;
var cancel;
axios.get(‘/user/12345’, {
cancelToken: new CancelToken(function executor© {
// executor 函数接收一个 cancel 函数作为参数
cancel = c;
})
});
// 取消请求
cancel();
57. ★★★ CommonJS 和 RequireJS 的实现原理
commonjs是通过module.exports导出模块,用require引入一个模块,原理:闭包
requirejs是通过define定义导出模块,用require引入模块。
define(‘name’,[],function(){
return ‘requirejs’
})
define(‘say’,[‘name’].function(name){
return “my name is” + name
})
require([‘say’],function(text){
console.log(text)
})
58. ★★★ 面向对象编程与面向过程编程的区别?
面向过程的程序设计把计算机程序视为一系列的命令集合,即一组函数的顺序执行。为了简化程序设计,面向过程把函数继续切分为子函数,即把大块函数通过切割成小块函数来降低系统的复杂度。
而面向对象的程序设计把计算机程序视为一组对象的集合,而每个对象都可以接收其他对象发过来的消息,并处理这些消息,计算机程序的执行就是一系列消息在各个对象之间传递。
59. ★★★ eval 是做什么的?性能怎么样?安全如何?
它的功能是把对应的字符串解析成js代码并运行,
应该避免使用eval,因为不安全,非常耗性能(2次,一次解析成js语句,一次执行)
注意:在项目里写js代码的时候,禁止使用的,因为有安全因素。
60. ★★★★★ 函数节流、防抖。scroll resize 使用函数节流实现不要频繁触发事件的需求
// 答案:
防抖:
//scroll方法中的do somthing至少间隔500毫秒执行一次
window.addEventListener(‘scroll’,function(){
var timer;//使用闭包,缓存变量
return function(){
if(timer) clearTimeout(timer);
timer = setTimeout(function(){
console.log(‘do somthing’)
},500)
}
}());//此处()作用 - 立即调用return后面函数,形成闭包
节流:
//scroll方法中当间隔时间大于2s,do somthing执行一次
window.addEventListener(‘scroll’,function(){
var timer ;//使用闭包,缓存变量
var startTime = new Date();
return function(){
var curTime = new Date();
if(curTime - startTime >= 2000){
timer = setTimeout(function(){
console.log(‘do somthing’)
},500);
startTime = curTime;
}
}
}());//此处()作用 - 立即调用return后面函数,形成闭包
61. ★★★ 数据类型(判断,和=)堆栈、内存泄漏及垃圾回收机制
// 答案:
1,== 判断值是否相等; === 判断值和数据类型是否严格相等
2,Javascript堆栈和垃圾回收机制
堆栈溢出
当储存的数据导到某一限制时就会造成堆栈溢出
内存泄漏
当不断向堆中存储数据,而不进行清理,这就是内存泄漏
垃圾回收机制(清除孤儿机制)
语言当中一般分两种,一种是自动清理,一种是手动清理(GC),js中只有自动清理
垃圾回收机制就是将引用对中的地址的对象设置为null,并且将所有引用该地址的对象都设置为null,并且移除事件侦听
不会即时清除,垃圾回收车会根据内存的情况在适当的时候进行清除堆中的对象 内存到达一定程度了才会进行回收
62. ★★★★ 了解 ES6 的 Proxy 吗?
// 答案:
Proxy,代理,是ES6新增的功能,可以理解为代理器(即由它代理某些操作)。
Proxy 对象用于定义或修改某些操作的自定义行为,可以在外界对目标对象进行访问前,对外界的访问进行改写。
new Proxy()表示生成一个 Proxy 实例
-target:目标对象
-handler:一个对象,其属性是当执行一个操作时定义代理的行为的函数。
注意:要实现拦截操作,必须是对 Proxy 实例进行操作,而不是针对目标对象 target 进行操作。
63. ★★★★ 深拷贝是什么?项目哪里是用到了深拷贝?
// 答案:
1,在拷贝构造函数中假如只完成了数据成员本身的赋值则称为“浅拷贝”;编译器提供的默认拷贝构造函数就已经可以完成这个任务。
而假如要复制的数据除了属性值本身以外,还要复制附加在数据属性值上的额外内容,那就要自己来写拷贝构造函数了,来完成所谓的“深拷贝”。
举个例子:
若在构造函数中new了一个新的空间存放数据,并且用指针记录了首地址;若是浅拷贝,则在拷贝构造函数中指针值将复制给另一个数据成员,这样就会有两个指针指向同一个空间;这样的话在析构函数里将会对指针所指向的空间进行释放,由于两个指针指向的是同一个空间,在释放第一个指针指向的空间时不会出现什么问题,而释放第二个指针指向的空间时就会因为空间已经被解析过而导致解析的空间不存在的情况,就会造成程序无法终止。
而解决上面这种情况的办法就是使用“深拷贝”,深拷贝是在拷贝构造函数里再new一个新的空间。将数据复制在新空间里,并将拷贝的指针记录这个新空间的首地址,这样在析构函数里就不会有问题了。
2,在某些引用类型值不更新的情况下用深拷贝
64. ★★★ swiper 插件从后台获取数据没问题,css 代码啥的也没问题,但是图片不动,应该怎么解决?
// 答案:
主要原因:
swiper提前初始化了,而这个时候,数据还没有完全出来。
解决方法
从swiper 入手,在swiper中写 observer:true/observeParents:true
let myswiper = new Swiper(“.swiper-container” , {
autoplay: true,
loop: true,
// observer 修改swiper子元素时自动初始化swiper
observer:true,
// observeParents 包括当前父元素的swiper发生变更时也会初始化swiper
observeParents:true,
})
从 Vue 入手,vue中专门提供了提供了一个方法nextTick() 用于解决dom的先后执行问题。
mounted(){
this.$nextTick(function(){
// …操作
let myswiper = new Swiper(“.swiper-container” , {
autoplay: true,
loop: true
})
})
}
65. ★★★★ ES6 中,数组监测怎么实现的(代理)
// 通过ES6的关键字extends实现继承完成Array原型方法的重写
class NewArray extends Array {
constructor(…args) {
// 调用父类Array的constructor()
super(…args)
}
push (…args) {
console.log(‘监听到数组的变化啦!’);
// 调用父类原型push方法
return super.push(...args)
}
// …
}
let list3 = [1, 2];
let arr = new NewArray(…list3);
console.log(arr)
// (2) [1, 2]
arr.push(3);
// 监听到数组的变化啦!
console.log(arr)
// (3) [1, 2, 3]
66. ★★ jQuery 优点和缺点
// 答案:
优点
1.出色的浏览器兼容性
2、出色的DOM操作的封装,使他具备强大的选择器,可以进行快速的DOM元素操作
3、可靠的事件处理机制、jq在处理事件绑定的时候是相当的可靠
4、完善的ajax(对ajax的封装非常好,不需要考虑复杂的浏览器的兼容和XMLhttprequest对象的创建和使用)
5、支持链式操作(什么是链式操作?通过‘.’来操作)和隐士迭代
6、减少服务器的压力和带宽并且加快了加载速度(为什么这么说?原因就是:当你打开网页之前打开了其他的网页,并且该网页也用了cdn的方式来
加载相同版本的jq文件,那么,浏览器就不会加载第二次,为啥舍近求远呢,和生活中的道理一样一样的!)
7、支持丰富的插件,当然你也可以自定义插件,再加上jq的文档也很丰富,对于程序员来说,是一件非常美好的事情
缺点
1.不能向后兼容。
每一个新版本不能兼容早期的版本。举例来说,有些新版本不再支持某些selector,新版jQuery却没有保留对它们的支持,而只是简单的将其移除。这可能会影响到开发者已经编写好的代码或插件。
2.插件兼容性。
与上一点类似,当新版jQuery推出后,如果开发者想升级的话,要看插件作者是否支持。通常情况下,在最新版jQuery版本下,现有插件可能无法正常使用。开发者使用的插件越多,这种情况发生的几率也越高。我有一次为了升级到jQuery 1.3,不得不自己动手修改了一个第三方插件。
3.多个插件冲突。
在同一页面上使用多个插件时,很容易碰到冲突现象,尤其是这些插件依赖相同事件或selector时最为明显。这虽然不是jQuery自身的问题,但却又确实是一个难于调试和解决的问题。
4.jQuery的稳定性。
jQuery没有让浏览器崩溃,这里指的是其版本发布策略。jQuery 1.3版发布后仅过数天,就发布了一个漏洞修正版1.3.1。他们还移除了对某些功能的支持,可能会影响许多代码的正常运行。我希望类似修改不要再出现。
5.对动画和特效的支持差。
在大型框架中,jQuery核心代码库对动画和特效的支持相对较差。但是实际上这不是一个问题。目前在这方面有一个单独的jQuery UI项目和众多插件来弥补此点。
67. ★★★ ES6 class 关键字原理跟 function 什么区别?
// 答案:
function 可以用call apply bind 的方式 来改变他的执行上下文
但是class 却不可以 class 虽然本质上也是一个函数 但是 其内(babel)部做了一层代理 来禁止了这种行为
关于构造器constructor
在function定义的构造函数中,其prototype.constructor属性指向构造器自身
在class定义的类中,constructor其实也相当于定义在prototype属性上
重复定义
function会覆盖之前定义的方法
class会报错
原型或者类中方法的枚举
class中定义的方法不可用Object.keys(Point.prototype)枚举到
function构造器原型方法可被Object.keys(Point.prototype)枚举到,除过constructor
所有原型方法属性都可用Object.getOwnPropertyNames(Point.prototype)访问到
68. ★★★ iframe 跨域问题,页面之间怎么传值?
// 答案:
一般有两个解决方案,一个是建立一个代理页面,通过代理页面传值,
另一个方法是通过H5的postMessage方法传值,今天用的是第二种。
首先,在父页面A中建立一个iframe,其中src要写好子页面B的地址,然后在A页面中写如下方法:
var iframe = document.getElementById("onemap");
var msg = {loginName:'arcgis',loginPassword:'Esri1234'};
var childDomain = "https://geoplat.training.com";
iframe.contentWindow.postMessage(msg,childDomain);
记住,childDomain与A的iframe的src地址不一样,childDomain是域,而src是域中的一个页面
msg是传输的信息,可以是字符串,也可以是对象。
上面的方法一定要写在一个函数中,并通过点击事件调用,如果希望iframe开始为空,点击后在设置src,
可以在设置src之后,通过setTimeout设置一定时间后在传输信息。
在子页面B中,通过对window添加事件获取传输过来的信息:
window.addEventListener("message",function(obj){
var name = obj.data.loginName;
var password = obj.data.loginPassword;
login.iframeChildLogin(name,password);
},false);
这样就完成了从不同域的父页面向子页面传值的过程
69. ★★★ 简述 commonJS、AMD 和 CMD
// 答案:
CommonJS导出模块的方法是exports,导入模块的是require,具体规范如下
1)如果一个JS文件中存在exports或require,该JS文件是一个模块
2)模块内的所有代码均为隐藏代码,包括全局变量、全局函数,这些全局的内容均不应该对全局变量造成任何污染
3)如果一个模块需要暴露一些API提供给外部使用,需要通过exports导出,exports是一个空的对象,你可以为该对象添加任何需要导出的内容
4)如果一个模块需要导入其他模块,通过require实现,require是一个函数,传入模块的路径即可返回该模块导出的整个内容
【注】CommonJS只是一个规范,相当于告诉你按什么标准制造汽车,但是具体怎么制造还是得看生产商。因此,有了规范以后,nodejs就去实现模块化了
AMD
AMD 是 RequireJS 在推广过程中对模块定义的规范化产出。
AMD 推崇依赖前置。
CMD
CMD 是 SeaJS 在推广过程中对模块定义的规范化产出。
CMD 推崇依赖就近
70. ★★★ require.js 源码看过吗?怎么做到异步加载的
/**
- Creates the node for the load command. Only used in browser envs.
*/
req.createNode = function (config, moduleName, url) {
var node = config.xhtml ?
document.createElementNS(‘http://www.w3.org/1999/xhtml’, ‘html:script’) :
document.createElement(‘script’);
node.type = config.scriptType || ‘text/javascript’;
node.charset = ‘utf-8’;
node.async = true;
return node;
};
requirejs 导入模块的方式实际就是创建脚本标签,一切的模块都需要经过这个方法创建。requirejs 使用 onload 事件来处理回调函数:
71. ★★ jQuery,$() 能传什么参数? html 代码怎么解析? 传 function 呢?
这个函数接收一个包含 CSS 选择器的字符串,然后用这个字符串去匹配一组元素。
jQuery 的核心功能都是通过这个函数实现的。
jQuery中的一切都基于这个函数,或者说都是在以某种方式使用这个函数。这个函数最基本的用法就是向它传递一个表达式(通常由 CSS 选择器组成),然后根据这个表达式来查找所有匹配的元素。
默认情况下, 如果没有指定context参数,$()将在当前的 HTML document中查找 DOM 元素;如果指定了 context 参数,如一个 DOM 元素集或 jQuery 对象,那就会在这个 context 中查找。在jQuery 1.3.2以后,其返回的元素顺序等同于在context中出现的先后顺序。
72. ★★ AMD 怎么加载文件的?
AMD 即Asynchronous Module Definition,中文名是异步模块定义的意思。它是一个在浏览器端模块化开发的规范
由于不是JavaScript原生支持,使用AMD规范进行页面开发需要用到对应的库函数,也就是大名鼎鼎RequireJS,实际上AMD 是 RequireJS 在推广过程中对模块定义的规范化的产出
requireJS主要解决两个问题
多个js文件可能有依赖关系,被依赖的文件需要早于依赖它的文件加载到浏览器
js加载的时候浏览器会停止页面渲染,加载文件越多,页面失去响应时间越长
看一个使用requireJS的例子
// 定义模块 myModule.js
define([‘dependency’], function(){
var name = ‘Byron’;
function printName(){
console.log(name);
}
return {
printName: printName
};
});
// 加载模块
require([‘myModule’], function (my){
my.printName();
});
语法
requireJS定义了一个函数 define,它是全局变量,用来定义模块
define(id?, dependencies?, factory);
id:可选参数,用来定义模块的标识,如果没有提供该参数,脚本文件名(去掉拓展名)
dependencies:是一个当前模块依赖的模块名称数组
factory:工厂方法,模块初始化要执行的函数或对象。如果为函数,它应该只被执行一次。如果是对象,此对象应该为模块的输出值
在页面上使用require函数加载模块
require([dependencies], function(){});
require()函数接受两个参数
第一个参数是一个数组,表示所依赖的模块
第二个参数是一个回调函数,当前面指定的模块都加载成功后,它将被调用。加载的模块会以参数形式传入该函数,从而在回调函数内部就可以使用这些模块
require()函数在加载依赖的函数的时候是异步加载的,这样浏览器不会失去响应,它指定的回调函数,只有前面的模块都加载成功后,才会运行,解决了依赖性的问题。
73. ★★ jQuery 怎么找到事件源元素
$(“.btn”).click(function(e){
// e 就是事件对象
e.target; // 事件的目标 dom
e.currentTarget; // 事件处理程序正在处理事件的那个元素
e.srcElement; // 事件的目标 ie
});
74. ★★★★ 模板引擎原理
模板引擎是通过字符串拼接得到的
let template = ‘hello <% name %>!’
let template = 'hello ’ + name + ‘!’
字符串是通过new Function执行的
let name = ‘world’
let template = let str = 'hello ' + name + '!' return str
let fn = new Function(‘name’, template)
console.log(fn(name)) // hello world!
将模板转换为字符串并通过函数执行返回
let template = ‘hello <% name %>!’
let name = ‘world’
function compile (template) {
let html = template.replace(/<%([\s\S]+?)%>/g, (match, code) => {
return ' + ${code} + '
})
html = let str = '${html}'; return str
return new Function(‘name’, html)
}
let str = compile(template)
console.log(str(name)) // hello world!
函数只能接收一个name变量作为参数,功能太单一了,一般会通过对象来传参,with来减少变量访问。
with功能
let params = {
name: ‘张三’,
age: 18
}
let str = ‘’
with (params) {
str = 用户${name}的年龄是${age}岁
}
console.log(str) // 用户张三的年龄是18岁
实现简单的模板引擎
let template = ‘hello <% name %>!’
let name = ‘world’
function compile (template) {
let html = template.replace(/<%([\s\S]+?)%>/g, (match, code) => {
return ' + ${code.trim()} + '
})
html = '${html}'
html = let str = ''; with (params) { str = ${html}; } return str
return new Function(‘params’, html)
}
let str = compile(template)
console.log(str({ name })) // hello world!
75. ★★ map 和 foreach 的区别
forEach()方法不会返回执行结果,而是undefined。也就是说,forEach()会修改原来的数组。
map()方法会得到一个新的数组并返回。
76. ★★★★ ES6 的新特性
- const与let
- 模板字符串
- 解构赋值
- 对象简写法
- for…of循环
- 展开运算符
- 剩余参数(可变参数)
- ES6箭头函数
- 参数默认值
10.类和继承
11.模块化规范
77. ★★ 2018/01/01 转换成 2018年/1月/1日
function fun(str){
var date = new Date(str)
return date.getFullYear()+‘年/’+date.getMonth()+‘月/’+date.getDate()+‘日’
}
78. ★★★ 0.1+0.2 等不等于 0.3?自己封装一个让他们相等的方法
在正常的数学逻辑思维中,0.1+0.2=0.3这个逻辑是正确的,但是在JavaScript中0.1+0.2!==0.3,这是为什么呢?这个问题也会偶尔被用来当做面试题来考查面试者对JavaScript的数值的理解程度。
在JavaScript中的二进制的浮点数0.1和0.2并不是十分精确,在他们相加的结果并非正好等于0.3,而是一个比较接近的数字 0.30000000000000004 ,所以条件判断结果为false。
方法1:设置一个误差范围值,通常称为”机器精度“,而对于Javascript来说,这个值通常是2-52,而在ES6中,已经为我们提供了这样一个属性:Number.EPSILON,而这个值正等于2-52。这个值非常非常小,在底层计算机已经帮我们运算好,并且无限接近0,但不等于0,。这个时候我们只要判断(0.1+0.2)-0.3小于Number.EPSILON,在这个误差的范围内就可以判定0.1+0.2===0.3为true。
function numbersequal(a,b){
return Math.abs(a-b)<Number.EPSILON;
}
方法2:转为整数运算
79. ★★★ 跨域是什么?有哪些解决跨域的方法和方案?
什么是跨域?
所谓的同源是指,域名、协议、端口均为相同。
所谓的跨域,不同的域名、协议、端口皆为不同域
一个域与另一个域名、协议或者端口不同的域的之间访问都叫跨域
解决跨域的方法和方案:
1:通过服务端代理请求。如PHP,服务端语言php是没有跨域限制的,让服务器去别的网站获取内容然后返回给页面。
2:第二种:jsonp跨域
1. jsonp跨域就是利用script标签的跨域能力请求资源
2. jsonp与ajax没有半毛钱关系!!
3. 浏览器的同源策略限制了js的跨域能力,但没有限制link img iframe script 的跨域行为
实现方式:
1. 利用js创建一个script标签,把json的url赋给script的scr属性,
2. 把这个script插入到页面里,让浏览器去跨域获取资源
3. JS先声明好回调函数,插入页面后会代为执行该函数,并且传入json对象为其参数。
注意:
1. jsonp只针对get请求
2. script标签加载回来的资源会被当成js在全局执行
3:CORS 跨域资源共享(xhr2)
CORS是一个W3C标准,全称是"跨域资源共享"(Cross-origin resource sharing)
它允许浏览器向跨源服务器,发出XMLHttpRequest请求,从而克服了AJAX只能同源使用的限制
整个CORS通信过程,都是浏览器自动完成,不需要用户参与
对于开发者来说,CORS通信与同源的AJAX通信没有差别,代码完全一样
实现CORS通信的关键是服务器,只要服务器实现了CORS接口,就可以跨源通信
4:nginx代理跨域
通过nginx服务器转发跨域请求,达到跨域的目的
80. ★★★ 什么是函数式编程?什么的声明式编程?
函数式编程:
函数式编程和声明式编程是有所关联的,因为他们思想是一致的:即只关注做什么而不是怎么做。但函数式编程不仅仅局限于声明式编程。
函数式编程最重要的特点是“函数第一位”,即函数可以出现在任何地方,比如你可以把函数作为参数传递给另一个函数,不仅如此你还可以将函数作为返回值。
声明式编程:
声明式编程是以数据结构的形式来表达程序执行的逻辑。它的主要思想是告诉计算机应该做什么,但不指定具体要怎么做。
SQL 语句就是最明显的一种声明式编程的例子,例如:
SELECT * FROM collection WHERE num > 5
除了 SQL,网页编程中用到的 HTML 和 CSS 也都属于声明式编程。
特点:
1:是它不需要创建变量用来存储数据。
2:不包含循环控制的代码如 for, while。
81. ★★★ super() 是否必须执行?不执行怎么让它不报错?
非必须,
在 JavaScript 中,super 指的是父类的构造函数
如果想在构造函数中使用this,你必须首先调用super。 先让父类做完自己的事
不执行无法使用this,
不报错的方法:
1:不使用this
2:手动修正this
82. ★★★ eventloop 渲染在哪一步?
任务队列
所有的任务可以分为同步任务和异步任务,同步任务,顾名思义,就是立即执行的任务,同步任务一般会直接进入到主线程中执行;而异步任务,就是异步执行的任务,比如ajax网络请求,setTimeout 定时函数等都属于异步任务,异步任务会通过任务队列( Event Queue )的机制来进行协调。
同步和异步任务分别进入不同的执行环境,同步的进入主线程,即主执行栈,异步的进入 Event Queue 。主线程内的任务执行完毕为空,会去 Event Queue 读取对应的任务,推入主线程执行。 上述过程的不断重复就是我们说的 Event Loop (事件循环)。
在事件循环中,每进行一次循环操作称为tick,通过阅读规范可知,每一次 tick 的任务处理模型是比较复杂的,其关键的步骤可以总结如下:
在此次 tick 中选择最先进入队列的任务( oldest task ),如果有则执行(一次)
检查是否存在 Microtasks ,如果存在则不停地执行,直至清空Microtask Queue
更新 render
主线程重复执行上述步骤
那么,什么是 microtasks ?规范中规定,task分为两大类, 分别是 Macro Task (宏任务)和 Micro Task(微任务), 并且每个宏任务结束后, 都要清空所有的微任务,这里的 Macro Task也是我们常说的 task 。
(macro)task 主要包含:script( 整体代码)、setTimeout、setInterval、I/O、UI 交互事件、setImmediate(Node.js 环境)
microtask主要包含:Promise、MutaionObserver、process.nextTick(Node.js 环境)
整体 script 作为第一个宏任务进入主线程,遇到 console.log,输出 script start
遇到 setTimeout,其回调函数被分发到宏任务 Event Queue 中
遇到 Promise,其 then函数被分到到微任务 Event Queue 中,记为 then1,之后又遇到了 then 函数,将其分到微任务 Event Queue 中,记为 then2
遇到 console.log,输出 script end
至此,Event Queue 中存在三个任务,如下表:
宏任务 微任务
setTimeout then1
- then2
执行微任务,首先执行then1,输出 promise1, 然后执行 then2,输出 promise2,这样就清空了所有微任务
此时,所有的mircotask执行完毕,本轮事件循环结束,UI 开始 render,当 UI render 完毕,开始下一轮事件循环.
执行 setTimeout 任务,输出 setTimeout, 至此,输出的顺序是:script start, script end, promise1, promise2, setTimeout
UI渲染
根据HTML Standard,一轮事件循环执行结束之后,下轮事件循环执行之前开始进行 UI render。即:macro-task任务执行完毕,接着执行完所有的micro-task任务后,此时本轮循环结束,开始执行UI render。UI render完毕之后接着下一轮循环。
83. ★★★★ 图片懒加载怎么实现?
原理:随着滚轮滚动,底部的图片会被不断地加载,从而显示在页面上,按需加载,当页面需要显示图片的时候才进行加载,否则不加载
- 页面加载完成时记录每个img标签的src值的字符串,
- 用鼠标滚轮判断图片是否出现在屏幕,如果是,则把记录的src值赋值给src属性
- 然后让image的src来发起请求,获取对应的图片放置到DOM树的这个位置上,从而实现图片的页面渲染!
于是就可以知道,当进入页面的时候,其实我们已经把所有的图片的这个地址信息拿到了,图片懒加载的作用就是让这个图片的src按需发起请求,获取图片。
84. ★★ for-in 循环会遍历出原型上的属性吗?怎么避免遍历到原型上的属性
使用 for in 循环遍历对象的属性时,原型链上的所有属性都将被访问
只遍历对象自身的属性,而不遍历继承于原型链上的属性,需要使用hasOwnProperty 方法过滤一下。
Object.prototype.say=“cgl”;
var person ={
age: 18
};
for (var key in person) {
if(person.hasOwnProperty(key)){
console.log(key, eval(“person.”+key));
}
}
85. ★★★ 简述call、apply、bind,call 和 apply哪个性能更好?
1、call()
call() 方法调用一个函数, 其具有一个指定的 this值和分别地提供的参数(参数的列表)。 第一个参数:在 fun 函数运行时指定的 this 值;如果指定了 null 或者 undefined 则内部 this 指向 window,后面的参数:指定的参数列表
var fn = function(arg1, arg2) {
};
fn.call(this, arg1, arg2);
var numbers = [5, 458 , 120 , -215 ];
var maxInNumbers = Math.max.call(Math,5, 458 , 120 , -215); //获取数组中的最大值458
2、apply()
apply()方法调用一个函数, 其具有一个指定的 this 值,以及作为一个数组(或类似数组的对象)提供的参数。apply() 与 call() 非常相似,不同之处在于提供参数的方式。apply() 使用参数数组而不是一组参数列表。
var fn = function(arg1, arg2) {
};
fn.apply(this, [arg1, arg2])
var numbers = [5, 458 , 120 , -215 ];
//umber 本身没有 max 方法,但是 Math 有,我们就可以借助 call 或者 apply 使用其方法。
var maxInNumbers = Math.max.apply(Math, numbers), //获取数组中的最大值458
3、bind()
bind() 函数会创建一个新函数(称为绑定函数),新函数与被调函数(绑定函数的目标函数)具有相同的函数体(在 ECMAScript 5 规范中内置的call属性)。
当目标函数被调用时 this 值绑定到 bind() 的第一个参数,该参数不能被重写。绑定函数被调用时,bind() 也接受预设的参数提供给原函数。
一个绑定函数也能使用new操作符创建对象:这种行为就像把原函数当成构造器。提供的 this 值被忽略,同时调用时的参数被提供给模拟函数。
/* ------call 和 apply哪个性能更好?------ */
call的性能要比apply好一些,尤其是传递给函数的参数超过3个时所以后期开发的时候,可以使用call多一些
(传参数3个以内的话,call和apply性能差不多,超过3个以上call更好一些)
86. ★★ ES6 箭头函数和普通函数有什么差异?
- 相比普通函数更简洁的语法
- 没有this,捕获其所在上下文的 this 值,作为自己的 this 值
- 不能使用new,箭头函数作为匿名函数,是不能作为构造函数的,不能使用new
- 不绑定arguments,用rest参数…解决
let test3=(…a)=>{console.log(a[1])} //22 - 使用call()和apply()调用:由于 this 已经在词法层面完成了绑定,通过 call() 或 apply() 方法调用一个函数时,只是传入了参数而已,对 this 并没有什么影响:
- 箭头函数没有原型属性
- 不能简单返回对象字面量
let fun5 = ()=>({ foo: x }) //如果x => { foo: x } //则语法出错 - 箭头函数不能当做Generator函数,不能使用yield关键字
- 箭头函数不能换行
let a = ()
=>1; //SyntaxError: Unexpected token =>
87. ★★★ Promise 避免回调地狱的语法糖–实现链式调用的核心点是什么?
解决回调地狱的终极方法 async/await ES7的语法,可以通过 async/await让代码看起来像同步的
async异步 await等待
await 等待 就是当后面跟的是promise对象,就让他停止 ,先让里面的异步事情做完,在把结果返回给前面的新变量,在继续向后执行
他只生效当前作用域内部,也就是async函数内部。
实现链式调用的核心点:
在 then 中新创建的 Promise,它的状态变为 fulfilled 的节点是在上一个 Promise的回调执行完毕的时候。也就是说当一个 Promise 的状态被 fulfilled 之后,会执行其回调函数,而回调函数返回的结果会被当作 value,返回给下一个 Promise(也就是then 中产生的 Promise),同时下一个 Promise的状态也会被改变(执行 resolve 或 reject),然后再去执行其回调,以此类推下去…
88. ★★★ 进程线程区别是什么?
什么是进程?什么是线程?
进程是系统中正在运行的一个程序,程序一旦运行就是进程。
进程可以看成程序执行的一个实例。进程是系统资源分配的独立实体,每个进程都拥有独立的地址空间。一个进程无法访问另一个进程的变量和数据结构,如果想让一个进程访问另一个进程的资源,需要使用进程间通信,比如管道,文件,套接字等。
一个进程可以拥有多个线程,每个线程使用其所属进程的栈空间。线程与进程的一个主要区别是,统一进程内的一个主要区别是,同一进程内的多个线程会共享部分状态,多个线程可以读写同一块内存(一个进程无法直接访问另一进程的内存)。同时,每个线程还拥有自己的寄存器和栈,其他线程可以读写这些栈内存。
线程是进程的一个实体,是进程的一条执行路径。
线程是进程的一个特定执行路径。当一个线程修改了进程的资源,它的兄弟线程可以立即看到这种变化。
进程和线程的区别体现在以下几个方面:
1.地址空间和其他资源(如打开文件):进程间相互独立,同一进程的各线程间共享。某进程内的线程在其他进程内不可见。
2.通信:进程间通信IPC(管道,信号量,共享内存,消息队列),线程间可以直接独写进程数据段(如全局变量)来进程通信——需要进程同步和互斥手段的辅助,以保证数据的一致性。
3.调度和切换:线程上下文切换比进程上下文切换快得多。
4.在多线程OS中,进程不是一个可执行的实体。
89. ★★★ 禁止事件冒泡,禁止默认事件
/-----禁止事件冒泡:-----/
function stopBubble(e) {
//如果提供了事件对象,则这是一个非IE浏览器
if ( e && e.stopPropagation )
//因此它支持W3C的stopPropagation()方法
e.stopPropagation();
else
//否则,我们需要使用IE的方式来取消事件冒泡
window.event.cancelBubble = true;
}
/-----阻止浏览器的默认行为-----/
function stopDefault( e ) {
//阻止默认浏览器动作(W3C)
if ( e && e.preventDefault )
e.preventDefault();
//IE中阻止函数器默认动作的方式
else
window.event.returnValue = false;
return false;
}
90. ★★ import export commonJS 对比区别
ES6和commonJS的一些区别
从语法的角度上看,ES6模块化的import 和 export 是一个内置的标识,而commonJS的module.exports 和 require 分别是js对象和方法。其ES6模块化和commonJS的实现方式不同。
1.ES6是在编译的时候导入文件,而commonJS是编译完成后,在通过require方法导入,并读取文件导出的文件,并返回一个module.exports对象
2.在ES6模块的内部this是问undefined,而commonJS的this为一个空对象
3.ES6模块输出的是一个引用,而commonJS模块输出的是一个值的引用
91. ★★ 为什么 JavaScript 是单线程
js作为主要运行在浏览器的脚本语言,js主要用途之一是操作DOM。
举一个栗子,如果js同时有两个线程,同时对同一个dom进行操作,这时浏览器应该听哪个线程的,如何判断优先级?
为了避免这种问题,js必须是一门单线程语言
92. ★★★ 使用箭头函数应该注意什么?
- 不要在对象里面定义函数,对象里面的行数应该用传统的函数方法
- 不要在对原型对象上定义函数,在对象原型上定义函数也是遵循着一样的规则
- 不要用箭头定义构造函数
- 不要用箭头定义事件回调函数
93. ★★★ 你知道 ES6 中的 Generator 和 yiled 吗?在实际开发中使用过吗?
Generator 函数是 ES6 提供的一种异步编程解决方案
执行 Generator 函数会返回一个遍历器对象,可以依次遍历 Generator 函数内部的每一个状态
形式上,Generator函数是一个普通函数,但是有两个特征:
- function关键字与函数名之间有一个星号
- 函数体内部使用yield表达式,定义不同的内部状态
/-----利用Generator函数,在对象上实现Iterator接口-----/
function* iterEntries(obj) {
let keys = Object.keys(obj);
for (let i=0; i < keys.length; i++) {
let key = keys[i];
yield [key, obj[key]];
}
}
let myObj = { foo: 3, bar: 7 };
for (let [key, value] of iterEntries(myObj)) {
console.log(key, value);
}
94. ★★★ Cookie、storage 的区别?什么时候使用?
区别:
-
cookie数据始终在同源的http请求中携带(即使不需要),即cookie在浏览器和服务器间来回传递。localStorage不会自动把数据发给服务器,仅在本地保存。
-
cookie数据还有路径(path)的概念,可以限制cookie只属于某个路径下。
-
存储大小限制也不同,cookie数据不能超过4k,同时因为每次http请求都会携带cookie,所以cookie只适合保存很小的数据,如会话标识。localStorage 虽然也有存储大小的限制,但比cookie大得多,可以达到5M或更大。
-
数据有效期不同,
localStorage:始终有效,窗口或浏览器关闭也一直保存,因此用作持久数据;
cookie只在设置的cookie过期时间之前一直有效,即使窗口或浏览器关闭。 -
WebStorage 支持事件通知机制,可以将数据更新的通知发送给监听者。
-
WebStorage 的 api 接口使用更方便。
使用场景:
localStorage可以用来统计页面访问次数。
cookie一般存储用户名密码相关信息,一般使用escape转义编码后存储。
95. ★★★ map、fillter、reduce 各自有什么作用?
1:map 作用是生成一个新数组,遍历原数组,将每个元素拿出来做一些变换然后放入到新的数组中。
另外 map 的回调函数接受三个参数,分别是当前索引元素,索引,原数组
2:filter 的作用也是生成一个新数组,在遍历数组的时候将返回值为 true 的元素放入新数组,我们可以利用这个函数删除一些不需要的元素
和 map 一样,filter 的回调函数也接受三个参数,用处也相同。
3:reduce 可以将数组中的元素通过回调函数最终转换为一个值。
它接受两个参数,分别是回调函数和初始值,接下来我们来分解上述代码中 reduce 的过程
首先初始值为 0,该值会在执行第一次回调函数时作为第一个参数传入
回调函数接受四个参数,分别为累计值、当前元素、当前索引、原数组,后三者想必大家都可以明白作用,这里着重分析第一个参数
96. ★★ JS的基本数据类型判断有什么方法?
1.typeof:
typeof’';// string 有效
typeof 1;// number 有效
typeof Symbol();// symbol 有效
typeof true;//boolean 有效
typeof undefined;//undefined 有效
typeof null;//object 无效
typeof [] ;//object 无效
typeof new Function();// function 有效
typeof new Date();//object 无效
typeof new RegExp();//object 无效
2.instanceof
instanceof 是用来判断 A 是否为 B 的实例,表达式为:A instanceof B,如果 A 是 B 的实例,则返回 true,否则返回 false。 在这里需要特别注意的是:instanceof 检测的是原型
3.constructor
当一个函数 F被定义时,JS引擎会为F添加 prototype 原型,然后再在 prototype上添加一个 constructor 属性
4.toString
toString() 是 Object 的原型方法,调用该方法,默认返回当前对象的 [[Class]] 。这是一个内部属性,其格式为 [object Xxx] ,其中 Xxx 就是对象的类型。
对于 Object 对象,直接调用 toString() 就能返回 [object Object] 。而对于其他对象,则需要通过 call / apply 来调用才能返回正确的类型信息。
Object.prototype.toString.call(‘’) ; // [object String]
Object.prototype.toString.call(1) ; // [object Number]
Object.prototype.toString.call(true) ;// [object Boolean]
Object.prototype.toString.call(Symbol());//[object Symbol]
Object.prototype.toString.call(undefined) ;// [object Undefined]
Object.prototype.toString.call(null) ;// [object Null]
Object.prototype.toString.call(newFunction()) ;// [object Function]
Object.prototype.toString.call(newDate()) ;// [object Date]
Object.prototype.toString.call([]) ;// [object Array]
Object.prototype.toString.call(newRegExp()) ;// [object RegExp]
Object.prototype.toString.call(newError()) ;// [object Error]
Object.prototype.toString.call(document) ;// [object HTMLDocument]
Object.prototype.toString.call(window) ;//[object global] window 是全局对象 global 的引用
97. ★★★ 构造函数、实例对象、原型对象三者的关系是什么?
实例可以通过内部指针访问到原型对象,原型对象可以通过constructor找到构造函数。
构造函数创建实例对象,构造函数里面有个prototy属性,prototy属性就是原型对象,原型对象里有个构造器constructor,这个构造器就是指向自己的构造函数,
98. ★★★★★ JS中的常见设计模式以及应用场景?
1、单例模式
单例模式就是一个实例在整个网页的生命周期里只创建一次,后续再调用实例创建函数的时候,返回的仍是之前创建的实例。在实际开发中应用十分广泛,例如页面中的登录框,显示消息的提示窗
2、策略模式
策略模式是指将策略(算法)封装起来,策略的目的是将算法和使用分离开。
3、代理模式
代理模式很好理解,我们不能直接使用目标函数,而是通过调用代理函数来实现对目标函数的使用。
4、发布订阅模式
发布订阅模式在实际应用中非常常见,例如,我们在微信App上关注了某个公众号,当该公众号有新文章发布时,就会通知我们。
发布订阅模式定义了一种一对多的依赖关系,当“一”发生变化,通知多个依赖。
5、命令模式
所谓命令模式就是将下要执行的业务逻辑封装到一个函数或类中,不需要具体谁来执行该命令的
99. ★★ 介绍下事件代理,主要解决什么问题
- 绑定事件太多,浏览器占用内存变大,严重影响性能
- Ajax出现,局部刷新盛行,每次加载完,都要重新绑定事件
- 部分浏览器移除元素时,绑定的事件没有被及时移除,导致内存泄漏,严重影响性能
- Ajax中重复绑定,导致代码耦合性过大,影响后期维护
100. ★★★★ 异步的解决方案有哪些?
1.回调函数callback
2.事件发布订阅
3.Promise
4.Generator
5.async/await
101. ★★ new 的原理是什么?通过 new 的方式创建对象和通过字面量创建有什么区别?
new操作符的作用如下:
1.创建一个空对象
2.由this变量引用该对象
3.该对象继承该函数的原型
4.把属性和方法加入到this引用的对象中
5.新创建的对象由this引用,最后隐式地返回this。
区别:
字面量创建不会调用 Object构造函数, 简洁且性能更好;
102. ★★ 数组去重的方法
/*
方法一:
双层循环,外层循环元素,内层循环时比较值
如果有相同的值则跳过,不相同则push进数组
*/
Array.prototype.distinct = function(){
var arr = this,
result = [],
i,
j,
len = arr.length;
for(i = 0; i < len; i++){
for(j = i + 1; j < len; j++){
if(arr[i] === arr[j]){
j = ++i;
}
}
result.push(arr[i]);
}
return result;
}
var arra = [1,2,3,4,4,1,1,2,1,1,1];
arra.distinct(); //返回[3,4,2,1]
/*
方法二:利用splice直接在原数组进行操作
双层循环,外层循环元素,内层循环时比较值
值相同时,则删去这个值
注意点:删除元素之后,需要将数组的长度也减1.
*/
Array.prototype.distinct = function (){
var arr = this,
i,
j,
len = arr.length;
for(i = 0; i < len; i++){
for(j = i + 1; j < len; j++){
if(arr[i] == arr[j]){
arr.splice(j,1);
len–;
j–;
}
}
}
return arr;
};
var a = [1,2,3,4,5,6,5,3,2,4,56,4,1,2,1,1,1,1,1,1,];
var b = a.distinct();
console.log(b.toString()); //1,2,3,4,5,6,56
/*
优点:简单易懂
缺点:占用内存高,速度慢
方法三:利用对象的属性不能相同的特点进行去重
*/
Array.prototype.distinct = function (){
var arr = this,
i,
obj = {},
result = [],
len = arr.length;
for(i = 0; i< arr.length; i++){
if(!obj[arr[i]]){ //如果能查找到,证明数组元素重复了
obj[arr[i]] = 1;
result.push(arr[i]);
}
}
return result;
};
var a = [1,2,3,4,5,6,5,3,2,4,56,4,1,2,1,1,1,1,1,1,];
var b = a.distinct();
console.log(b.toString()); //1,2,3,4,5,6,56
/*
方法四:数组递归去重
运用递归的思想
先排序,然后从最后开始比较,遇到相同,则删除
*/
Array.prototype.distinct = function (){
var arr = this,
len = arr.length;
arr.sort(function(a,b){ //对数组进行排序才能方便比较
return a - b;
})
function loop(index){
if(index >= 1){
if(arr[index] === arr[index-1]){
arr.splice(index,1);
}
loop(index - 1); //递归loop函数进行去重
}
}
loop(len-1);
return arr;
};
var a = [1,2,3,4,5,6,5,3,2,4,56,4,1,2,1,1,1,1,1,1,56,45,56];
var b = a.distinct();
console.log(b.toString()); //1,2,3,4,5,6,45,56
//方法五:利用indexOf以及forEach
Array.prototype.distinct = function (){
var arr = this,
result = [],
len = arr.length;
arr.forEach(function(v, i ,arr){ //这里利用map,filter方法也可以实现
var bool = arr.indexOf(v,i+1); //从传入参数的下一个索引值开始寻找是否存在重复
if(bool === -1){
result.push(v);
}
})
return result;
};
var a = [1,1,1,1,1,1,1,1,1,2,2,2,2,2,2,3,3,3,3,3,3,3,2,3,3,2,2,1,23,1,23,2,3,2,3,2,3];
var b = a.distinct();
console.log(b.toString()); //1,23,2,3
方法六:利用ES6的set
Set数据结构,它类似于数组,其成员的值都是唯一的。
利用Array.from将Set结构转换成数组
function dedupe(array){
return Array.from(new Set(array));
}
dedupe([1,1,2,3]) //[1,2,3]
拓展运算符(…)内部使用for…of循环
1
2
3
let arr = [1,2,3,3];
let resultarr = […new Set(arr)];
console.log(resultarr); //[1,2,3]
103. ★★★★ 常见内存泄漏
1、静态集合类,如HashMap、LinkedList等等。如果这些容器为静态的,那么它们的生命周期与程序一致,则容器中的对象在程序结束之前将不能被释放,从而造成内存泄漏。简单而言,长生命周期的对象持有短生命周期对象的引用,尽管短生命周期的对象不再使用,但是因为长生命周期对象持有它的引用而导致不能被回收。
2、各种连接,如数据库连接、网络连接和IO连接等。在对数据库进行操作的过程中,首先需要建立与数据库的连接,当不再使用时,需要调用close方法来释放与数据库的连接。只有连接被关闭后,垃圾回收器才会回收对应的对象。否则,如果在访问数据库的过程中,对Connection、Statement或ResultSet不显性地关闭,将会造成大量的对象无法被回收,从而引起内存泄漏。
3、变量不合理的作用域。一般而言,一个变量的定义的作用范围大于其使用范围,很有可能会造成内存泄漏。另一方面,如果没有及时地把对象设置为null,很有可能导致内存泄漏的发生。
4、内部类持有外部类,如果一个外部类的实例对象的方法返回了一个内部类的实例对象,这个内部类对象被长期引用了,即使那个外部类实例对象不再被使用,但由于内部类持有外部类的实例对象,这个外部类对象将不会被垃圾回收,这也会造成内存泄露。
5、改变哈希值,当一个对象被存储进HashSet集合中以后,就不能修改这个对象中的那些参与计算哈希值的字段了,否则,对象修改后的哈希值与最初存储进HashSet集合中时的哈希值就不同了,在这种情况下,即使在contains方法使用该对象的当前引用作为的参数去HashSet集合中检索对象,也将返回找不到对象的结果,这也会导致无法从HashSet集合中单独删除当前对象,造成内存泄露
6、缓存泄漏
内存泄漏的另一个常见来源是缓存,一旦你把对象引用放入到缓存中,他就很容易遗忘,对于这个问题,可以使用WeakHashMap代表缓存,此种Map的特点是,当除了自身有对key的引用外,此key没有其他引用那么此map会自动丢弃此值
7、监听器和回调
内存泄漏第三个常见来源是监听器和其他回调,如果客户端在你实现的API中注册回调,却没有显示的取消,那么就会积聚。需要确保回调立即被当作垃圾回收的最佳方法是只保存他的若引用,例如将他们保存成为WeakHashMap中的键。
104. ★★★ promise 常见方法和 all 和 race的应用场景
Promise.race():
race的用法:谁跑的快,以谁为准执行回调。
race的使用场景:比如我们可以用race给某个异步请求设置超时时间,并且在超时后执行相应的操作
Promise.all():
all的用法:谁跑的慢,以谁为准执行回调。
在前端的开发实践中,我们有时会遇到需要发送多个请求并根据请求顺序返回数据的需求
105. ★★★ 介绍一下 ES6 中 Set, Map的区别?
Map
在JS中的默认对象的表示方式为{},即一组键值对,但是键必须是字符串。
为了使用Number或者其他数据类型作为键,ES6规范引入了新的数据类型Map。
Map是一组键值对的结构,具有极快的查找速度。初始化Map需要一个二维数组,或者直接初始化一个空Map。
Map 对象是键值对集合,和 JSON 对象类似,但是 key 不仅可以是字符串还可以是其他各种类型的值包括对象都可以成为Map的键
Set
Set也是一组key的集合,与Map类似。但是区别是Set不存储value,并且它的key不能重复。
创建一个Set,需要提供一个Array作为输入,或者直接创建一个空Set:
重复元素会在Set中自动被过滤
Set 对象类似于数组,且成员的值都是唯一的
106. ★★ 并行和并发的区别是什么?
并行意味着可以同时取得多个任务,并同时去执行所取得的这些任务。并行模式相当于将长长的一条队列,划分成了多条短队列,所以并行缩短了任务队列的长度
并发表示多个任务同时都要执行
107. ★★★ 为什么操作 dom 慢?
DOM对象本身也是一个js对象,所以严格来说,并不是操作这个对象慢,而是说操作了这个对象后,需要经过跨流程通信和渲染线程触发的重新渲染,导致DOM操作慢
JS引擎和和渲染引擎的模块化设计,使得它们可以独立优化,运行速度更快,但是这种设计带来的后果就是DOM操作会越来越慢
108. ★★★★ 插入几万个 dom ,如何实现页面不卡顿?
让创建插入节点的工作分批进行:
setTimeout(() => {
// 插入十万条数据
const total = 100000;
// 一次插入 20 条,如果觉得性能不好就减少
const once = 20;
// 渲染数据总共需要几次
const loopCount = total / once;
let countOfRender = 0
let ul = document.querySelector(“ul”);
function add() {
// 优化性能,插入不会造成回流
const fragment = document.createDocumentFragment();
for (let i = 0; i < once; i++) {
const li = document.createElement(“li”);
li.innerText = Math.floor(Math.random() * total);
fragment.appendChild(li);
}
ul.appendChild(fragment);
countOfRender += 1;
loop();
}
function loop() {
if (countOfRender < loopCount) {
window.requestAnimationFrame(add);
}
}
loop();
}, 0);
109. ★★★ js中的常用事件绑定方法
- 在DOM元素中直接绑定
- 在JavaScript代码中绑定
- 绑定事件监听函数
110. ★ 简述 src 和 href 的区别?
src用于替换当前元素,href用于在当前文档和引用资源之间确立联系
111. ★★★★ 你知道什么是原型吗?我们为什么要用原型呢?或者说原型为我们提供了什么?
什么是原型:
Javascript规定,每一个函数都有一个prototype对象属性,指向另一个对象(原型链上面的)。
prototype(对象属性)的所有属性和方法,都会被构造函数的实例继承。这意味着,我们可以把那些不变(公用)的属性和方法,直接定义在prototype对象属性上。
prototype就是调用构造函数所创建的那个实例对象的原型(proto)。
prototype可以让所有对象实例共享它所包含的属性和方法。也就是说,不必在构造函数中定义对象信息,而是可以直接将这些信息添加到原型中。
为什么要用原型:使用原型对象解决浪费内存
112. ★★★ 你了解原型链吗 你能说说 prototype 与 proto 的区别吗?
img
1.对象有属性__proto__,指向该对象的构造函数的原型对象。
2.方法除了有属性__proto__,还有属性prototype,prototype指向该方法的原型对象。
113. ★★★ ts 和 js 的区别
1.ts是静态类语言,可以做到声明即文档,js是动态类语言相对更灵活。
2.如用ts写一个button组件可以清晰的知道,ButtonProps如是否必传,可选,style是什么类型,disabled是什么类型,较js,ts更易于维护和拓展,可以做到代码即注释,避免一个月不见3,代码自己都忘记自己写了什么的尴尬,
4.ts对比js基础类型上,增加了 void/never/any/元组/枚举/以及一些高级类型
5.js没有重载概念,ts有可以重载
6.vscode/ide对ts有很友好的提示
7.ts更利于重构
114. ★★★ 简述原生 js 发 ajax 的步骤
1.创建XMLHTTPRequest对象
2.使用open方法设置和服务器的交互信息
3.设置发送的数据,开始和服务器端交互
4.注册事件
5.更新界面
115. ★★ 是否所有函数都有 prototype 一说?
-
使用Function.prototype.bind创建的函数对象
function abc(){console.log(‘abc’)}
var binded = abc.bind(null)
binded() //abc
console.log(binded.prototype) //undefined -
箭头函数也没有
var abc = ()=>{console.log(‘abc’)}
abc() //abc
console.log(abc.prototype) //undefined
116. ★★ 为什么 await 在 forEach 中不生效?如何解决?
lodash的forEach和[].forEach不支持await, forEach 只支持同步代码。
解决方法1:使用 for…of
解决方法2:使用 for循环
解决方法3:让orEach支持async await
forEach 在正常情况像下面这么写肯定是做不到同步的,程序不会等一个循环中的异步完成再进行下一个循环。原因很明显,在上面的模拟中,while 循环只是简单执行了 callback,所以尽管 callback 内使用了 await ,也只是影响到 callback 内部。
arr.myforeach(async v => {
await fetch(v);
});
要支持上面这种写法,只要稍微改一下就好
Array.prototype.myforeach = async function (fn, context = null) {
let index = 0;
let arr = this;
if (typeof fn !== ‘function’) {
throw new TypeError(fn + ’ is not a function’);
}
while (index < arr.length) {
if (index in arr) {
try {
await fn.call(context, arr[index], index, arr);
} catch (e) {
console.log(e);
}
}
index ++;
}
};
117. ★ a 标签中,如何禁用 href 跳转页面或定位链接?
118. ★★ 请描述一下 cookies,sessionStorage 和 localStorage 的区别?
不同点:
1.存储大小
cookie数据大小不能超过4k。
sessionStorage和localStorage 虽然也有存储大小的限制,但比cookie大得多,可以达到5M或更大。
2.有效时间
localStorage 存储持久数据,浏览器关闭后数据不丢失除非主动删除数据;
sessionStorage 数据在当前浏览器窗口关闭后自动删除。
cookie 设置的cookie过期时间之前一直有效,即使窗口或浏览器关闭
3. 数据与服务器之间的交互方式
cookie的数据会自动的传递到服务器,服务器端也可以写cookie到客户端
sessionStorage和localStorage不会自动把数据发给服务器,仅在本地保存。
119. ★★★ instanceof的原理是什么?
// instanceof 可以正确的判断对象的类型,是通过判断对象的原型链中是不是能找到类型的 prototype。
function fn(left, right) {
let prototype = right.prototype;
left = left.proto;
while (true) {
if (left === undefined || left === null) {
return false;
}
if (left === prototype) {
return true;
}
left = left.proto;
}
}
120. ★★★ 用多种方法实现 JavaScript 继承
1、原型链继承
核心: 将父类的实例作为子类的原型
特点:
1. 非常纯粹的继承关系,实例是子类的实例,也是父类的实例
2. 父类新增原型方法/原型属性,子类都能访问到
3. 简单,易于实现
缺点:
1. 要想为子类新增属性和方法,必须要在new Animal()这样的语句之后执行,不能放到构造器中
2. 无法实现多继承
3. 来自原型对象的所有属性被所有实例共享(来自原型对象的引用属性是所有实例共享的)(详细请看附录代码: 示例1)
4. 创建子类实例时,无法向父类构造函数传参
推荐指数:★★(3、4两大致命缺陷)
2、构造继承
核心:使用父类的构造函数来增强子类实例,等于是复制父类的实例属性给子类(没用到原型)
特点:
1. 解决了1中,子类实例共享父类引用属性的问题
2. 创建子类实例时,可以向父类传递参数
3. 可以实现多继承(call多个父类对象)
缺点:
1. 实例并不是父类的实例,只是子类的实例
2. 只能继承父类的实例属性和方法,不能继承原型属性/方法
3. 无法实现函数复用,每个子类都有父类实例函数的副本,影响性能
推荐指数:★★(缺点3)
3、实例继承
核心:为父类实例添加新特性,作为子类实例返回
特点:
1.不限制调用方式,不管是new 子类()还是子类(),返回的对象具有相同的效果
缺点:
1. 实例是父类的实例,不是子类的实例
2. 不支持多继承
推荐指数:★★
4、拷贝继承
特点:支持多继承
缺点:
1. 效率较低,内存占用高(因为要拷贝父类的属性)
2. 无法获取父类不可枚举的方法(不可枚举方法,不能使用for in 访问到)
推荐指数:★(缺点1)
5、组合继承
核心:通过调用父类构造,继承父类的属性并保留传参的优点,然后通过将父类实例作为子类原型,实现函数复用
特点:
1. 弥补了方式2的缺陷,可以继承实例属性/方法,也可以继承原型属性/方法
2. 既是子类的实例,也是父类的实例
3. 不存在引用属性共享问题
4. 可传参
5. 函数可复用
缺点:
1. 调用了两次父类构造函数,生成了两份实例(子类实例将子类原型上的那份屏蔽了)
推荐指数:★★★★(仅仅多消耗了一点内存)
6、寄生组合继承
核心:通过寄生方式,砍掉父类的实例属性,这样,在调用两次父类的构造的时候,就不会初始化两次实例方法/属性,避免的组合继承的缺点
特点:堪称完美
缺点:实现较为复杂
推荐指数:★★★★★
浏览器面试真题
★★ HTML、CSS、JAVASCRIPT 是如何变成页面的?
★★★★ chrome 仅仅打开一个页面,为什么有有4个进程?
★★★★★ 强缓存,协商缓存
★★ 浏览器内核有啥,咋解决兼容问题
万字详文:深入理解浏览器原理
常见的浏览器内核有以下四种:
Trident:IE浏览器引擎
Gecko:Firefox浏览器引擎
Presto:Opera浏览器引擎(现为Blink内核)
Webkit:Safari,Google Chrome浏览器引擎
网络面试真题
★★★★ url从输入到渲染页面的全过程
浏览器构建HTTP Request请求, DNS解析URL地址、生成HTTP请求报文、构建TCP连接、使用IP协议选择传输路线
将请求通过网络传输到服务端 从客户机到服务器需要通过许多网络设备,一般包括集线器、交换器、路由器等
服务器构建HTTP Response响应,响应客户端的请求
将响应体的数据通过网络传输返回给客户端
浏览器渲染页面 解析HTML、CSS、JS,生成RenderTree渲染页面
★★★★ tcp三次握手,四次挥手,可靠传输原理
三次握手
第一次握手:客户端发送syn报文,并发送seq为x序列号给服务端,等待服务端的确认。
第二次握手:服务端发送syn+ack报文,并发送seq为Y的序列号,在确认序列号为x+1
第三次握手:客户端发送ack报文,并发送seq序列号为z,在确认序列号为y+1
四次挥手
第一次挥手:先由客户端向服务器端发送一个FIN,请求关闭数据传输。
第二次挥手:当服务器接收到客户端的FIN时,向客户端发送一个ACK,其中ack的值等于FIN+SEQ
第三次挥手:然后服务器向客户端发送一个FIN,告诉客户端应用程序关闭。
第四次挥手:当客户端收到服务器端的FIN是,回复一个ACK给服务器端。其中ack的值等于FIN+SEQ
★★★ http200
和 302
的区别
http_200的意思是 成功处理了请求
http_302的意思是(临时移动)服务器目前从不同位置的网页响应请求, 但请求者应继续使用原有位置来进行以后的请求。
★★★ 你知道哪些http首部字段?
首部字段名 说明
Cache-Control 控制缓存的行为
Connection 逐跳首部、连接的管理
Date 创建报文的日期时间
Program 报文指令
Trailer 报文末端的首部一览
Transfer-Encoding 指定报文主体的传输编码方式
Upgrade 升级为其他协议
Via 代理服务器的相关信息
Warning 错误通知
Authorization Web认证信息
★★★★ 说一下http缓存策略,有什么区别,分别解决了什么问题?
浏览器每次发起请求时,先在本地缓存中查找结果以及缓存标识,根据缓存标识来判断是否使用本地缓存, 如果缓存有效,则使用本地缓存。
向服务器发起请求并携带缓存标识。根据是否需向服务器发起HTTP请求, 将缓存过程划分为两个部分:强制缓存和协商缓存,强缓优先于协商缓存。
强缓存,服务器通知浏览器一个缓存时间,在缓存时间内,下次请求,直接用缓存,不在时间内,执行比较缓存策略。
协商缓存,让客户端与服务器之间能实现缓存文件是否更新的验证、提升缓存的复用率, 将缓存信息中的Etag和Last-Modified,通过请求发送给服务器,由服务器校验,返回304状态码时,浏览器直接使用缓存。
解决问题
1.减少了冗余的数据传输
2.减少了服务器的负担,大大提升了网站的性能
3.加快了客户端加载网页的速度
avatar 详情
★★★ 为什么浏览器要限制TCP的最大个数
建立一个tcp连接需要:1,socket文件描述符;2,IP地址;3,端口;4,内存
1、内存资源: 一个tcp连接最小占用内存为4096+4096 = 8k, 那么对于一个8G内存的机器,在不考虑其他限制下, 最多支持的并发量为:810241024/8 约等于100万, 在实际中,由于linux kernel对一些资源的限制, 加上程序的业务处理,所以,8G内存是很难达到100万连接的
2、CPU资源
★★★ HTTP2.0的特点
HTTP2.0大幅度的提高了web性能,在HTTP1.1完全语意兼容的基础上,进一步减少了网络的延迟。
1、二进制分帧
2、多路复用
3、首部压缩
4、流量控制
5、请求优先级
6、服务器推送
★★★ 说一下HTTP2 多路复用原理,以及多路复用优势?
第一个:串行的文件传输。当请求a文件时,b文件只能等待,等待a连接到服务器、 服务器处理文件、服务器返回文件,这三个步骤。我们假设这三步用时都是1秒, 那么a文件用时为3秒,b文件传输完成用时为6秒,依此类推。 (注:此项计算有一个前提条件,就是浏览器和服务器是单通道传输)
第二个:连接数过多。我们假设Apache设置了最大并发数为300,因为浏览器限制, 浏览器发起的最大请求数为6,也就是服务器能承载的最高并发为50, 当第51个人访问时,就需要等待前面某个请求处理完成。
★★★ 简述https原理,以及与http的区别
HTTP协议工作在80端口,HTTPS协议工作在443端口
HTTPS需要申请证书(用于验证服务器身份)
HTTP在TCP三次握手建立连接之后即可开始传输数据;HTTPS协议则需要在建立TCP连接之后客户端与服务器在进行SSL加密,确定对话密钥,完成加密后才开始传输数据。
HTTPS协议传输是密文,HTTP协议传输是明文
★★★ CDN 是什么?描述下 CDN 原理?为什么要用 CDN?
CDN的全称是Content Delivery Network,即内容分发网络 共有云厂商在全世界各地都遍布不计其数都数据中心和服务器, CDN服务简单来讲就是这些厂商将你的服务器上面的文档分发到他们不同地区的服务器的当中, 每个地区可以称为一个节点,这样用户在访问你的网址时, 浏览器发送的请求就会优先绕去离客户最近的节点来获取数据, 这样方便客户更快的速度访问网站。 CDN是构建在现有网络基础之上的智能虚拟网络,依靠部署在各地的边缘服务器, 通过中心平台的负载均衡、内容分发、调度等功能模块,使用户就近获取所需内容, 降低网络拥塞,提高用户访问响应速度和命中率。CDN的关键技术主要有内容存储和分发技术。
★★★ DNS 查询的过程,分为哪两种,是怎么一个过程
1、分布域名解析 是指分在客户端上维护一个静态的文本文件,其中包含主机名和IP地址的映射。 随着网络规模的扩大,分布式分辨率的有效性越来越低。
2、集中式域名解析 要求网络中有多台DNS服务器,负责维护域名/IP地址映射数据库。 客户端从指定的服务器获取域名的地址信息。一旦客户端指定的DNS服务器不包含相应的数据, DNS服务器就会在网络中进行递归查询,并获取其他服务器上的地址信息。
★★★ 强缓存和协商缓存的区别
强缓存(本地缓存):直接使用本地的缓存,不用跟服务器进行通信
相关header字段
expires
一个未来时间,代表请求有效期,没有过期之前都使用当前请求。
cache-control
no-cache:不使用本地缓存。向浏览器发送新鲜度校验请求
pubilc:任何情况下都缓存(即使是HTTP认证的资源)
private:只能被终端用户的浏览器缓存,不允许CDN等中继缓存服务器对其缓存
no-store:禁止浏览器缓存数据,也禁止保存至临时文件中,每次都重新请求,多次设置 cache-control,优先级最高
协商缓存:将资源一些相关信息返回服务器,让服务器判断浏览器是否能直接使用本地缓存,整个过程至少与服务器通信一次
相关header字段
Last-Modified/If-Modified-Since(两个都是时间格式字符串)
浏览器第一次发请求,服务器在返回的 respone 的 header 加上 Last-Modified,表示资源的最后修改时间
再次请求资源,在 requset 的 header 加上 If-Modified-Since ,值就是上一次请求返回的 Last-Modified 值
服务器根据请求传过来的值判断资源是否有变化,没有则返回 304,有变化就正常返回资源内容,更新 Last-Modified 的值
304 从缓存加载资源,否则直接从服务器加载资源
Etag/If-None-Match
与 Last-Modified/If-Modified-Since 不同的是,返回 304 时,ETag 还是会重新生成返回至浏览器。
★★★ 为什么from表单提交没有跨域问题,但ajax有跨域问题
浏览器的策略本质是:一个域名下面的JS,没有经过允许是不能读取另一个域名的内容,但是浏览器不阻止你向另外一个域名发送请求。 所以form表单提交没有跨域问题,提交form表单到另外一个域名,原来页面是无法获取新页面的内容,或者说form提交后不需要返回,但是ajax是需要返回的。 而ajax是想要读取响应内容,浏览器是不允许你这么做的。 浏览器的安全策略限制的是js脚本,并不限制src,form表单提交之类的请求, 就是说form表单提交不存在安全问题,ajax提交跨域存在安全问题。
前端工程化面试真题
★★★ 什么是 bundle
, 什么是 chunk
,什么是module
?
什么是module
module是我们经常所说的模块,不管是esModule(import/export或export default)、CommonJs(require/module.exports)、AMD(依赖前置require([‘Ma’,‘Mb’],(a,b)=>_/define({key:value}))都属于module
什么是chunks
chunks是webpack依据module间的引用关系通过代码分割生成的不同chunk文件,同时wenpack也可对chunk文件做些一些操作(如加chunkname)
什么是bundle
bundle是webpack最终经过编译输出output的文件集合,他可以包含多个经过加载和编译后的chunk文件,由于bundle是经过加载和编译的最终源文件,所以直接可以在浏览器运
★★★ hash chunkhash contenthash三者区别
在了解这三者的区别之前首先需要清楚一个概念,构建项目为何需要缓存,因为webpack实现热更新后,每次更新都会将我们的业务代码和第三方库的代码同时打包更新,降低构建效率,所以需要使用webpack的optimization.splitChunks分离第三方库和业务代码,公共模块直接缓存就不要每次更新都消耗资源 > hash 是跟整个项目构建有关,只要项目的文件内容发生变化,整个项目构建的hash值都会更改,并且全部文件都共用相同的hash值,所以一旦修改任何一个文件,整个项目的文件缓存都将失效 > chunkhash 为避免整个项目的文件因为hash不断变化而导致缓存失效,需要chunkhash,chunkhash根据模块之间引入关系生成不同的chunk文件的同时生成对应的hash值,而不再是所有文件共用同一个hash值 如果需要对chunkhash做进一步优化就需要使用上述所说的optimization.splitChunks或者dllPlugin,可以将公共库以及业务代码抽离而直接缓存公共库,只要公共库只要不发生版本迭代,那么公共库的chunkhash就一直不会变。 >contenthash contenthash又是在chunkhash的进一步优化,webpack在根据模块引入关系生成对应的chunk文件,模块可能引入index.css以及index.js,那么当index.js变化而index.css没有变化,则项目更新时inedx.css也会被迫重新加载,导致资源损耗 + 优化方法: 我们可以使用extract-text-webpackplugin(webpack4)里的contenthash值,保证引入css文件所在的模块只要css文件内容不变,就不会重复构建css + 配置方法 js var extractTextPlugin = require('extract-text-webpack-plugin') plugins:[ new extractTextPlugin('../css/bundle.[name].[contenthash].css') ]
补充:webpack5已经使用mini-css-extract-plugin代替extractTextPlugin,感兴趣的了解一下
★★★ 你知道什么是脚手架吗?
脚手架的概念和作用:就是拥有完整的开发环境,帮助我们快速的生成一套既定的项目架构、文件、配置。使用者只需要专注自己的业务代码即可,不需要使用者单独配置 脚手架的构成:常见的脚手架的开发环境主要分为三种模式:生产模式,开发模式,测试模式。以及需要配置完整的路由系统(vue-router,react-router-dom),和状态管理系统(vuex,redux)才能保证开发环境的完整性 常见的脚手架:Vue-cli,Creat-React-app,umi-app
★★★ 你们公司有自己的脚手架工具么,他是怎么工作的?
我们公司有自己的脚手架工具
★★★ webpack的核心思想是什么
万物皆模块:在webpacck的世界中,其他任何资源都可以当做模块的方式引入 代码分割:webapp 的优化关键在于代码体积,当应用体积增大,实现代码的按需加载是刚需,这也是 webpack 出现的根本原因 可定制化:任何一个工具都不可能解决所有问题,提供解决方案才是最可行的,webpack基于可定制化的理念构建,通过插件系统,配置文件,可实现大型项目的定制需求
★★★ Loader和Plugin的区别
loader是一个转换器
1、用于对模块源码文件的预编译和转换,,loader描述了webpack如何处理非javascript模块。
2、没有loader,构建的打包过程无法顺利完成
3、loader作用在打包前
4、将A文件转换为B文件,操作的是文件,比如将A.scss转换为A.css,是单纯的文件转换过程
Plugin是插件扩展器
1、plugin构建过程更完整的补充和优化,如使用new UglifyJsPlugin(),new CssMinimizerPlugin()压缩js和css
2、没有plugin,文件的打包过程可以完成
3、plugin作用于整个打包过程,
4、针对webpack的打包过程,他不直接操作文件,而是基于事件机制工作,会监听webpack打包过程的事件钩子,执行任务,通过事件钩子拦截webpack的执行。
★★★ 有哪些常见的Loader和Plugin,简单聊一聊各自的作用
常用Loader
babel-loader使得webpack可以通过babel转译js代码
css-loader转译css文件,会对@import和url()进行处理,就像js解析import/require一样,
style-loadder把css-loader转译后的结果插入输出的脚本中显示样式效果
sass-loader/less-loader都是用来预编译和转换.sass/.less文件
file-loader将一个文件中的加载文件、图片import/require()解析为url,并且将文件发送到输出文件夹,并返回文件的publicURL,
url-loader是file-loader功能的扩展和封装,options选项中可以设置对引用图片大小的限制,如果大于等于限制,默认使用file-loader并传递所有参数,如果小于,则默认使用base64编码并返回输出,
// 示例
use: [
{
loader: ‘url-loader’,//等价写法use:‘url-loader’?limit=1024
options: {
limit: 8192,
}
},
]
常用Plugin
DllPlugin:作用和optimization.splitChunk的作用相似,都是用某种方法拆分bundles,可以大幅度提升构建速度
SplitChunksPlugin:分离公共的第三方模块以及业务代码
extract-text-webpackplugin: extract-text-webpackplugin(webpack4)里的contenthash值,保证引入css文件所在的模块只要css文件内容不变,就不会重复构建css
MiniCssExtractPlugin:抽离css文件,删除和压缩css代码,本插件是extract-text-webpackplugin插件的webpack5的升级用法,可以实现contenthash
HtmlWebpackPlugin:
1、为html文件动态引入外部资源给script、link添加compile(编译)后的hash,防止引用缓文件问题
2、可以生成创建html入口文件,比如单页面可以生成一个html文件入口,多页面生成多个html
UglifyjsWebpackPlugin:删除和压缩js文件
EslintWebpackPlugin:作用类似eslint-loader(eslint-loader已被弃用),审查代码是否符合编码规范和统一,审查代码是否存在语法错误
★★★ 说一下 Webpack 的热更新原理吧
首先热更新所更新的内容是基于hash值发生改变的文件
更新的原理方法是webpack-dev-server启动本地服务,源码在webpack-dev-server的package.json中的bin命令,可以找到命令的入口文件bin/webpack-dev-server.js。注:源码附在最后
这段代码主要完成了
一、webpack启动后,完成webpack所有编译工作,以及监听本地文件变化
二、使用express框架启动本地服务让浏览器可以请求本地的静态资源。
三、本地服务启动后,再启动websocket服务,通过websocket,可以建立本地服务和浏览器的双向通信,这样就可以实现本地文件变化,立马告知浏览器可以更新了
// node_modules/webpack-dev-server/bin/webpack-dev-server.js
// 生成webpack编译主引擎 compiler
let compiler = webpack(config);
// 启动本地服务
let server = new Server(compiler, options, log);
server.listen(options.port, options.host, (err) => {
if (err) {throw err};
});
// node_modules/webpack-dev-server/lib/Server.js
class Server {
constructor() {
this.setupApp();
this.createServer();
}
setupApp() {
// 依赖了express
this.app = new express();
}
createServer() {
this.listeningApp = http.createServer(this.app);
}
listen(port, hostname, fn) {
return this.listeningApp.listen(port, hostname, (err) => {
// 启动express服务后,启动websocket服务
this.createSocketServer();
}
}
}
★★★ 如何优化 Webpack 的构建速度
不要让loader做太多事情,以babel-loader为例
最常见的优化方式是,用include或exclude来帮助我们避免不必要的转译,除此之外,如果我们开启缓存转译结果缓存至文件系统,则至少可以提升两倍工作效率
use:{
exclude:/(node_module|bower_components)/,
loader:‘babel-loader?cacheDirectory=true’
}
不要放过第三方库,以node_modules为代表,庞大又不可或缺。推荐使用DllPlugin
用DllPlugin处理文件,要分两步走:
基于dll专属的配置文件,打包dll库 ```js const path = require(‘path’) const webpack = require(‘webpack’)
module.exports = { entry: { // 依赖的库数组 vendor: [ ‘prop-types’, ‘babel-polyfill’, ‘react’, ‘react-dom’, ‘react-router-dom’, ] }, output: { path: path.join(dirname, ‘dist’), filename: ‘[name].js’, library: ‘[name][hash]’, }, plugins: [ new webpack.DllPlugin({ // DllPlugin的name属性需要和libary保持一致 name: ‘[name][hash]’, path: path.join(dirname, ‘dist’, ‘[name]-manifest.json’), // context需要和webpack.config.js保持一致 context: __dirname, }), ], } // 编写完成之后,运行这个配置文件,我们的 dist 文件夹里会出现这样两个文件:
- 基于 webpack.config.js 文件,打包业务代码
const path = require(‘path’);
const webpack = require(‘webpack’)
module.exports = {
mode: ‘production’,
// 编译入口
entry: {
main: ‘./src/index.js’
},
// 目标文件
output: {
path: path.join(__dirname, ‘dist/’),
filename: ‘[name].js’
},
// dll相关配置
plugins: [
new webpack.DllReferencePlugin({
context: __dirname,
// manifest就是我们第一步中打包出来的json文件
manifest: require(‘./dist/vendor-manifest.json’),
})
]
}
其中SplitChunksPlugin也可以将公共模块和业务代码抽离成chunks,热更新时就不需要再更新公共模块的内容了,区别在于他每次构建时都会重新构建一次vender,而DllPlugin一次生成vender,终生使用,效率更高)
将loader由单进程转为多进程构建
Happypack–将loader由单进程转为多进程,进程是程序运行的数据集合,是系统资源分配基本单位,一般情况下单核cpu只有一个单进程,多核cpu在node的cpu利用率不足的情况下才会由单进程转为多进程。而Happypack帮我们做了这个事儿。多核并发,最大限度使用多核cpu的作用。大大提升效
可视化工具的使用,找出导致文件体积过大的原因
推荐使用webpack-bundule-analyzer,配置方法和普通的 plugin 无异,它会以矩形树图的形式将包内各个模块的大小和依赖关系呈现出来
★★★ 自己写过Loader和Plugin么
★★★ 代码分割的本质是什么?有什么意义呢?
- 代码分割的本质: 是能够把代码分离到不同的bundle中,避免出现大体积的代码包,然后可以按需加载或并行加载这些文件 + 代码分离的意义: 代码分离可以获取更小的bundle,以及控制资源加载优先级,合理使用可以极大的减少加载时间 + 代码分割的实现方式有三种:
入口起点:使用entry手动分离代码(不建议)
防止重复加载:使用optimization.splitChunks配置选项,可以将第三方公共模块和业务代码直接分离动态引入:使用import()方法来分离代码,原理是当 Webpack 解析到该语法时,会自动进行代码分割,分割出不同的chunks
> 语法:使用的时候再去下载对应的文件,返回一个Promise,当Promise成功后再去执行回调,(动态引入的另一种方式是require.ensure())js import("./math").then(math => { console.log(math.add(16, 26)); }); ``````js //所有可能匹配此模式的文件都会自动进行代码拆分。 const loadFile = file => import (` ./ $ { file } ` )
import动态语法实现异步加载的代码演示,js function getComponent(){ return import(./loadash).then(_=>{ const element = document.createElement('div'); element.innerHTML = _.join(['Hello', 'webpack'], ' '); return element; }) } getComponent().then(component=>{ document.body.appendChild(component); })
★★★ 说下 tree-shaking 的原理
作用:tree-shaking可以实现删除项目中未被引用的代码 原理:
//test.js
export const a =1;
export const b = 2;
// main.js
import {a} from ‘./test.js’
对于以上情况,test文件中的变量b如果没有在项目中使用的话,就不会被打包到文件中 使用: 在webpack4中,开启生产环境就会自动开启这个优化功能,也可以手动的引入ModuleConcatenationPlugin的支持
★★★ babel原理
★★★ linux部署和windows sever服务器区别?
性价比:Linux服务器性价比更高,Linux作为资源管理器和操作系统来说,是开源的,免费的,而正版windows的操作系统是收费的。
性能方面:相同配置的Linux服务器的性能比windows服务器好一些,Linux服务器占用的资源少一点
稳定性方面:Window系统用户量大,因而攻击者多一些,所以暴露了更多的系统安全漏洞。Linux是多用户多进程系统,意味着Linux能够一次性处理大量正在进行的进程,比windows处理的多
安全性方面:Linux系统开源软件的开发方式有助于暴露错误,集众人智慧解决问题,补丁更新更快。这是windows不具备的,Windows的另一个不利因素是其许多应用程序依靠远程过程调用,这就迫使Windows的防火墙没有Linux那样严格。而Linux远程过程调用是限制使用的。
★★★ 你们公司项目发布流程是什么样的?
★★★ 前端资源发布路径怎么实现非覆盖式发布(平滑升级)?
★★★ SSR项目是如何发布的
★★★ 你有发布过自己的npm包吗?流程是怎样的?
1.首先将自己的脚手架发布到githuhb仓库 然后在新建一个脚手架文件夹jcq-cli,并执行 2.npm init -y 生成package.json
3.安装我们需要的模块commander,download-git-repo. npm i commander download-git-repo
4.在项目根目录创建index.js文件,写入相应的处理逻辑 js #! /usr/bin/env node const program = require('commander'); const download = require('download-git-repo'); //version 版本号 //name 新项目名称 program.version('1.0.0', '-v, --version') .command('init ') .action((templateName, projectName) => { if (templateName === "vue") { console.log('clone template ...'); download('github:junkaicool/jkc-cli-vue-src', projectName, function (err) { console.log(err ? 'Error' : 'Success') }) } else if(templateName === "react") { console.log('clone template ...'); download('github:junkaicool/jkc-cli-react-src', projectName, function (err) { console.log(err ? 'Error' : 'Success') }) } else { console.error('A template name that does not exist') } }); program.parse(process.argv); // #! /usr/bin/env node是执行这个文件时使用node方式执行 // program.version是解析别人输入jkc-cli -v时输出的内容: 1.0.0 // command解析输入jkc-cli init vue my-vue-project,init后面两个参数,一个模板名,一个项目名 // action是根据上面的两个参数做相应的逻辑处理,判断模板名,去相应的git仓库下载代码。download的第一个参数下载地址不是填我们git的网址,按照我的格式填就行,第二个参数是生成的项目名,第三个参数是错误的回调执行函数。
5.在package.json文件中加入,这一步是我们在命令行jkc-cli的时候执行的文件。 js .... "bin": { "jkc-cli": "index.js" }, ....
登陆npm 注意一定要切回官方npm才能成功(因为你的账号是在官网注册登陆的) npm login --registry http://registry.npmjs.org
登陆成功显示 Logged in as ranbaojia on http://registry.npmjs.org/.
然后发布 也要在官方网站发布,发布的时候一定要确保你的包名不存在,并且发布前一定要确保邮箱验证npm才可以 npm publish
然后直接可以下载你在npm发布的包了,你定要区分清楚现在是处于镜像下载环境还是官方下载环境 npm i -g jcq-cli
如果下载失败,最好执行一下以下代码 npm config set registry http://registry.npmjs.org
生成你的脚手架 jcq-cli init vue my-first-npmpackage
★★★ 介绍下 npm 模块安装机制,为什么输入 pm install 就可以自动安装对应的模块?
i
★★★ 你会搭建私有的npm仓库吗?怎么搭建?
★★★ Webpack 为什么慢,如何进行优化
webpack为了进行资源的压缩和合并,在打包过程中会引入很多大型的第三方库,以babel、babel-loader为例,使得打包构建以及输出的过程都变得缓慢。具体的构建优化见★★★ 如何优化 Webpack 的构建速度
★★★ jenkins 上线流程
关于jekins上线流程,有一篇详细文章介绍
★★★ webpack 中 loader 和 plugins 的区别
loader是一个转换器
1、用于对模块源码文件的预编译和转换,,loader描述了webpack如何处理非javascript模块。
2、没有loader,构建的打包过程无法顺利完成
3、loader作用在打包前
4、将A文件转换为B文件,操作的是文件,比如将A.scss转换为A.css,是单纯的文件转换过程
Plugin是插件扩展器
1、plugin构建过程更完整的补充和优化,如使用new UglifyJsPlugin(),new CssMinimizerPlugin()压缩js和css
2、没有plugin,文件的打包过程可以完成
3、plugin作用于整个打包过程,
4、针对webpack的打包过程,他不直接操作文件,而是基于事件机制工作,会监听webpack打包过程的事件钩子,执行任务,通过事件钩子拦截webpack的执行。
★★★ 什么是长缓存,在webpack中如何做到长缓存优化?
浏览器在用户访问页面的时候,都会对静态资源进行存储,但是每次代码更新或者升级的时候,我们都需要浏览器去重新加载代码,最方便的方法就是以文件名的方式引入,只下载新的代码块,不加载旧的没有变化的代码块,这就是长缓存,
在webpack4中使用SplitChunkPlugin把第三方库和业务代码分离,由于第三方库的chunkHash未改变,所以只会对改变的业务代码的模块进行更新。而第三方库的代码块因为长缓存而不更新。
★★★ 使用git上传的时候,会出现那些冲突,怎么解决这些冲突?
★★★ 什么是组件?什么是模块化?有什么区别?
组件化
就是基础库或者基础组件,意思是把代码重复的部分提炼出一个个组件供给功能使用
模块化
就是业务框架或者业务模块,也可以理解为框架,意思是把功能进行划分,将同一类型的代码整合在一起,所以模块的功能相对复杂,都属于同一个业务。
区别:
使用:组件的使用能在不同项目(模块)重复应用的代码,而模块按照项目功能需求划分成不同类型的业务框架 目的:组件是复用,解耦,模块是为了隔离、封装 依赖:组件之间低依赖,比较独立,模块之间的依赖可通过路由进行耦合 架构定位:组件位于架构底层,被其它层所依赖,模块位于架构业务层
★★★ 你们公司有自己的脚手架工具么,他是怎么工作的?
有自己的脚手架工具
待解决问题 polyfills:提供api以方便兼容不同的浏览器 vendor:项目插件扩展 CommonsChunkPlugin 每次构建时都会重新构建一次 vendor? 原因就是因为CommonsChunkPlugin把polyfills和vendor进行了打包,polyfills和vendor中包含了mian所有需要的公共模块, 所以mian不再进行打包这些公共模块,只打包我们自己写的模块。
性能面试真题
★★★ 当修改一个数据时,不想整个页面都被重新渲染,只想要渲染变更数据的那一部分,怎么做?
选择合适的框架,vue和react都使用了虚拟dom加diff运行,找出最小化的差异并更新到真实dom中,只会改变发生变化的dom,不会渲染整个页面。并为列表中每一个节点添加唯一的key值,提高diff运算的速度。
让数据变为响应式的,如:vue中,当页面初始化时候,vue会遍历data对象所有的属性,并使用defineProperty或Proxy把这些属性全部转化为getter/setter,所以属性必须在 data 对象上存在才能让 Vue 转换它,这样才能让它是响应的。
vue计算属性是基于它们的响应式依赖进行缓存的。只在相关响应式依赖发生改变时它们才会重新求值。
Vue 不允许在已经创建的实例上动态添加新的根级响应式属性,可以使用 Vue.set(object, key, value) 方法将响应属性添加到嵌套的对象上,也可以使用 this.$forceUpdate() 方法,可以局部更新。迫使Vue实例重新渲染。它仅仅影响实例本身和插入插槽的子组件,而不是所有字组件
★★★ 页面是否可以快速加载
图片资源的预加载,懒加载,cdn加速,雪碧图/精灵图
减少HTTP请求,尽可能的合并脚本,CSS,图像,在用户的浏览器上缓存文件
压缩JS文件,图片,HTML文档,CSS文档等等。用户可以下载较小的文件,增加网页的加载速度,这样可以降低服务器的消耗
去除不必要的空格, 注释 减少文件的总尺寸,较小的页面可以获得更快的加载速度
把CSS放在顶端: css文件的加载不会阻塞dom树的解析,把CSS文件放在网站的顶端,可以让网站尽可能同时加载其他部件,如图片和文字。
把js放在底端: 在body闭合标签前插入js脚本,让这些脚本在后台加载的同时,用户先得到看似完整的页面。
async异步 或 defer 推迟 加载js脚本
避免使用CSS表达式,如:calc
避免重定向:无论是服务器端头重定向,js重定向,或者HTML元素重定向。你的网站都会加载空白的页面的头, 然后再加载新的一页,用户为了获得需要的页面花费越来越多的时间
使用Web Worker为 JavaScript 创造多线程环境,允许主线程创建 Worker 线程,将一些任务分配给Worker 线程运行。
★★★ 是否允许用户快速开始与之交互
会引起回流的css属性:width、height、min-height、border、border-width、padding、margin、display、position、left、right、top、bottom、float、clear、font-size、font-weight、font-family、line-height、text-align、overflow、overflow-y、white-space、vertical-align 减少以上属性的使用或用其他属性代替,如:使用 visibility 替换 display: none ,因为前者只会引起重绘,后者会引发回流
避免使用table布局,table中的单元格的宽度取决于最长的单元格,可能会引起多次回流。
避免标签的过多嵌套,CSS 选择符从右往左匹配查找,避免节点层级过多。
避免使用CSS表达式,动态的计算,可能会引起多次的回流
将频繁重绘或者回流的节点设置为图层,图层能够阻止该节点的渲染行为影响别的节点,例如will-change、video、iframe等标签,浏览器会自动将该节点变为图层。
将动画效果应用到position属性为absolute或fixed的元素上,避免影响其他元素的布局,这样只是一个重绘,而不是回流,同时,控制动画速度可以选择 requestAnimationFrame
避免频繁操作样式,最好一次性重写style属性,或者将样式列表定义为class并一次性更改class属性。
避免频繁操作DOM,创建一个documentFragment,在它上面应用所有DOM操作,最后再把它添加到文档中。
避免频繁读取会引发回流/重绘的属性,如果确实需要多次使用,就用一个变量缓存起来。
对具有复杂动画的元素使用绝对定位,使它脱离文档流,否则会引起父元素及后续元素频繁回流。
★★★ 怎么让滚动和动画流畅
前提:滚动和动画会频繁的引起回流与重绘,操作卡顿问题
防抖(debounce)与 节流(throttle)都是为了限制函数的执行频次,以优化函数触发频率过高导致的响应速度跟不上触发频率,出现延迟,假死或卡顿的现象。
window.requestAnimationFrame() 这个方法是用来在页面重绘之前,通知浏览器调用一个指定的函数,用于准确控制页面的帧刷新渲染,让动画效果更加流畅。频率是每秒 60 次,也就是 1000/60 ,触发频率大概是 16.7ms 。(当执行复杂操作时,当它发现无法维持 60fps 的频率时,它会把频率降低到 30fps 来保持帧数的稳定。)
避免在scroll 事件中修改样式属性或将样式操作从 scroll 事件中剥离,因为如果你在 scroll 事件的处理函数中做了修改样式属性的操作,那么这些操作会被浏览器暂存起来。然后在调用 requestAnimationFrame 的时候,如果你在一开始做了读取样式属性的操作,那么这将会导致触发浏览器的强制同步布局。
滑动过程中尝试使用 pointer-events: none 禁止鼠标事件,可用来提高滚动时的帧频。当滚动时,鼠标悬停在某些元素上,则触发其上的 hover 效果,多半导致滚动出现问题。
★★★ 怎么图片优化
使用base64编码代替图片:适用于图片小于2KB,页面引用图片不多的情况。将图片转换为base64编码字符串inline到CSS或页面中,减少http的请求次数。
合并图片sprite(雪碧图):任何用到页面图片的场景。将多个页面用到的背景图片合并成一个大的图片在页面中引用,可以有效地减少请求个数。
使用canvas代替图片:需要高性能的图片或动画,使用HTML5的canvas元素绘制图片,页面渲染性能较高。
响应式图片:不同终端对同一图片的需求不一样,根据终端加载不同的图片来节省不必要的流量。通过picture元素,picturefill或平台判断来为不同终端平台输出不同的图片。减少没必要的图片加载,灵活控制。
图片压缩:在不得不加载图片的前提下,进一步提高优化效果,通过有损或无损压缩所见图片的大小。减少图片加载流量,效果明显。
更好的图片格式:webp、bpg、sharpP等新图片格式具有更好的压缩比
字体图库代替图标:使用字体图库你不仅可以改变大小,而且还可以改变颜色。
图片懒加载:在页面图片非常多的情况下,可以使用懒加载。只加载第一屏的图片,当用户通过滚动访问后面的内容时再加载相应图片。
按照HTTP协议设置合理的缓存:具体的缓存策略(如永久缓存+重命名)
利用CDN加速
★★★ 骨架屏+合理的loading
骨架屏 Skeleton Screen Loading 也叫加载占位图,是近年流行的加载控件,通常表现形式是在界面上待加载区域填充灰色的占位图,与线框图的效果非常相似。Skeleton Screen本质上是界面加载过程中的过渡效果。
loading 在一定程度上限制了用户的操作,而骨架屏不干扰用户的操作,弱网络环境下极大的优化了用户体验。
异步加载样式表防止阻塞骨架屏的渲染
当前端渲染内容替换掉骨架屏内容时,必须保证此时样式表已经加载完毕,否则真正有意义的页面内容将出现 FOUC。所以必须要保证 Vue 实例在异步样式表加载完毕后进行挂载,如果此时样式还没有完成,我们把挂载方法放到全局,等到样式加载完成后再调用。
★★★ 长列表懒加载思路与性能优化
我们将需要渲染的内容限制在我们的给定固定容器中,给容器一个固定的高度与高度overflow:auto ,然后数据将在这个容器显示,我们只需要渲染视口容器中适合的数量节点
为了避免用户快速的滚动,导致频繁的渲染行,我们可以使用requestAnimationFrame来实现对渲染速率的控制,从而平滑的滚动
对节点进行缓存,在滚动时遇到缓存中节点时,可直接使用。
有时候数据操作会导致列表需要重新渲染,会导致全列表的重新渲染,所以我们需要做列表的回收,永远只保证可视区域的节点与上下预渲染的节点数量即可,超出这个范围的节点进行回收缓存。
★★★ 循环太多引起浏览器的卡顿如何处理
使用其他的循环代替for-in、for-of循环:for-in需要同时遍历实例和原型链,在遍历上的消耗更多,for-of需要去调用Symbol.iterator函数来构建一个迭代器,自动调用next(),且不向next()传入任何值,在接收到done:true之后停止
减少每次循环工作量,获取并存储数组的长度。在查找该值时在作用域链上查找的步数就会减少,所以能有效地减少性能的消耗
倒序循环,合并i和数组长度的比较和判断为true或false,以此来得到性能上的优化
减少循环次数:达夫设备实际上就是在一次循环中完成多次循环的事情,以达到减少循环次数的作用
基于函数的迭代,JavaScript本身封装了数组迭代方法,如:forEach、map、filter、some等。
使用Web Worker创造多线程环境,主线程创建 Worker 线程,将循环任务分配给Worker线程运行,防止主线程的卡顿,影响程序的执行。
安全面试真题
★★★ 如何防止XSS攻击
1,对输入和URL参数进行过滤(白名单和黑名单)
a,主要的思路就是将容易导致XSS攻击的边角字符替换成全角字符
b,对于每一个输入,在客户端和服务器端还要进行各种验证,验证是否合法字符,长度是否合法,格式是否正确
c,< 和 > 是脚本执行和各种html标签需要的,比如 \
手写代码面试真题
★★★ 手写代码:实现forEach map filter reduce
// 1. ------_forEach--------
// Array.prototype.forEach() 方法对数组的每个元素执行一次给定的函数。
// arr.forEach(callback(currentValue [, index [, array]])[, thisArg])
Array.prototype.forEach = function (fn, thisArg) {
if (typeof fn !== ‘function’) throw ‘参数必须为函数’
if (!Array.isArray(this)) throw ‘只能对数组使用此方法’
let arr = this
for (let i = 0; i < arr.length; i++) {
fn.call(thisArg, arr[i], i, arr) //用call来改变fn里面this的指向
}
}
//test
// window.value = 0
// let obj = { value: 1 }
// let arr = ["", “for”, “each”]
// arr._forEach(
// function (ele, index, arr) {
// console.log(“ele, index, arr”, ele, index, arr);
// console.log(“this.vaule”, this.value);
// }, obj)
// 2. ------_map--------
// Array.prototype.map() 方法创建一个新数组,其结果是该数组中的每个元素是调用一次提供的函数后的返回值。
// arr.map(callback(currentValue [, index [, array]])[, thisArg])
Array.prototype._map = function (fn, thisArg) {
if (typeof fn !== 'function') throw "参数必须是回调函数"
if (!Array.isArray(this)) throw "只能为数组使用此方法"
const arr = this //将调用者(实例化的arr)赋值给arr
const newArr = []
for (let i = 0, len = arr.length; i < len; i++) {
newArr.push(fn.call(thisArg, arr[i], i, arr))
//将每次调用的回调函数fn的返回值push到新数组中
}
return newArr //返回新数组
}
//test
// window.value = 0
// let obj = { value: 1 }
// let arr = ["_", "for", "each"]
// const newArr = arr._map(
// function (ele, index) {
// return ele + "你好" + index
// }, obj)
// console.log(newArr);
// 3. ------_filter--------
// Array.prototype.map() 方法创建一个新数组, 其包含通过所提供函数实现的测试的所有元素。
// arr.filter(callback(currentValue [, index [, array]])[, thisArg])
Array.prototype._filter = function (fn, thisArg) {
if (typeof fn !== 'function') throw "参数必须是回调函数"
if (!Array.isArray(this)) throw "只能为数组使用此方法"
const arr = this //将调用者(实例化的arr)赋值给arr
const newArr = []
for (let i = 0, len = arr.length; i < len; i++) {
if (fn.call(thisArg, arr[i], i, arr)) { //判断回调的返回值是否为true
newArr.push(arr[i])
}
}
return newArr //返回新数组
}
//test
// window.value = 0
// let obj = { value: 1 }
// let arr = ["_", "for", "each"]
// const newArr = arr._filter(
// function (ele, index) {
// return ele == "for"
// }, obj)
// console.log(newArr);
//???????传入的数组这个参数的作用是???????
// 4. ------_reduce--------
// Array.prototype.map() 方法对数组中的每个元素执行一个由您提供的reducer函数(升序执行),将其结果汇总为单个返回值。
// arr.reduce(callback(accumulator, currentValue[, index[, array]])[, initialValue])
Array.prototype._reduce = function (_reducer, initialValue) {
if (typeof _reducer !== 'function') throw "参数必须是回调函数"
if (!Array.isArray(this)) throw "只能为数组使用此方法"
if ((!this.length && !initialValue)) throw "请传入初始值或者给非空对象使用此方法"
let arr = this //将调用者(实例化的arr)赋值给arr
let result = initialValue || arr[0] //初始值
for (let i = 0, len = arr.length; i < len; i++) {
if (!initialValue && i == 0) continue // 如果提供了initialValue,则起始索引号为0,否则从索引1起始。
result = _reducer(result, arr[i], i, this)
}
return result //返回新数组
}
// //test
// let arr = [1, 3, 4]
// const Result = arr._reduce(function (accumulator, currentValue, index, array) {
// console.log(index);
// return accumulator + currentValue
// }, 2)
// console.log(Result);
★★★ 手写实现一个简易的 Vue Reactive
★★★ 手写代码,监测数组变化,并返回数组长度
//手写代码,监测数组变化,并返回数组长度
// 获取Array的原型,并创建一个新的对象指向这个原型
// const arrayMethods = Object.create(Array.prototype)
// 创建一个新的原型,这就是改造之后的数组原型
const ArrayProto = []
// 重新构建Array原型里面的虽有方法
Object.getOwnPropertyNames(Array.prototype).forEach(method => {
if (typeof Array.prototype[method] === “function”) {
ArrayProto[method] = function () {
console.log(“我已经监听到数组触发了” + method + “事件”)
let len = this.length
let result = Array.prototype[method].apply(this, arguments)
console.log(len, this.length);
if (len !== this.length) return this.length
return result
}
}
})
let list = [1, 2, 3]
// 将数组的原型链指向新构造的原型
list.proto = ArrayProto
// 执行push事件
console.log(list.push(2), list.pop(2), list.slice(2), list.unshift(2));
★★★ 手写原生继承,并说出局限性?
★★★★ 手写一个柯里化函数
//柯里化:固定部分参数,返回一个接受剩余参数的函数,也称为部分计算函数,目的是为了缩小适用范围,创建一个针对性更强的函数。核心思想是把多参数传入的函数拆成单参数(或部分)函数,内部再返回调用下一个单参数(或部分)函数,依次处理剩余的参数。
//好处: 入口单一,易于测试和复用
//缺点: 函数嵌套多、占内存
function Curry(fn,…args){
return (…_args)=>{
return fn(…args,…_args))
}
}
function V(l,w,h){
return lwh
}
const hcy =Curry(V,10)
hcy(5,8) //400
hcy(6,7) //420
★★★ 手写一个反柯里化函数
//反柯里化,从字面讲,意义和用法跟函数柯里化相比正好相反,扩大适用范围,创建一个应用范围更广的函数。使本来只有特定对象才适用的方法,扩展到更多的对象。
Function.prototype.unCurrying = function() {
const self = this
return function(…rest) {
return Function.prototype.call.apply(self, rest)
}
}
★★★★ 手写一个Promise
class Promise{
constructor(executor){
this.state = ‘pending’;
this.value = undefined;
this.reason = undefined;
this.onResolvedCallbacks = [];
this.onRejectedCallbacks = [];
try{ // 如果executor执行报错,直接执行reject
executor(this.resolve, this.reject);
} catch (err) {
reject(err);
}
}
//成功
resolv(value){
// state改变,resolve调用就会失败
if (this.state === 'pending') {
// resolve调用后,state转化为成功态
this.state = 'fulfilled';
// 储存成功的值
this.value = value;
}
};
//失败
reject(reason) {
// state改变,reject调用就会失败
if (this.state === 'pending') {
// reject调用后,state转化为失败态
this.state = 'rejected';
// 储存失败的原因
this.reason = reason;
}
};
//then方法
then(onFulfilled,onRejected) {
// 声明返回的promise2
let promise2 = new Promise((resolve, reject)=>{
if (this.state === 'fulfilled') {
let x = onFulfilled(this.value);
// resolvePromise函数,处理自己return的promise和默认的promise2的关系
resolvePromise(promise2, x, resolve, reject);
};
if (this.state === 'rejected') {
let x = onRejected(this.reason);
resolvePromise(promise2, x, resolve, reject);
};
if (this.state === 'pending') {
this.onResolvedCallbacks.push(()=>{
let x = onFulfilled(this.value);
resolvePromise(promise2, x, resolve, reject);
})
this.onRejectedCallbacks.push(()=>{
let x = onRejected(this.reason);
resolvePromise(promise2, x, resolve, reject);
})
}
});
// 返回promise,完成链式
return promise2;
}
}
★★★ 手写一个instanceOf
function instanceOf(left,right){
let proto = left.proto
let prototype = right.prototype
while(true){
if(proto === null) return false
if(proto === prototype) return true
proto = proto.proto;
}
}
★★★ 手写ajax
//1.创建对象
let xhq = new XMLHttpRequest()
//2.初始话http请求参数
xhq.open(methode, url, true)
//3.发送请求
xhq.send({
username:‘wzx’
})
//4.监听请求状态,执行对应的回调函数
xhq.onreadystatechange = function () {
if ( xhq.readystate == 4 && xhq.status == 200 ) {
// success 回调
success(xhq.responseText)
} else if (xhq.readyState == 4 && xhq.status !== 200) {
// error 回调
error()
}
}
//-----------完整实现-----------
function sendAjax(obj) {
function splicStr(data) {// get方式传入时,将内容进行data内容进行拼接
var str = ‘’
for (var i in data) {
str = i + ‘=’ + data[i]
}
return str
}
// 原生ajax实现 步骤分析
// 一、声明XMLHttpRequest, 为了兼容IE5、6需要使用ActiveXObject()
let xhq = new XMLHttpRequest() // 创建对象
// 二、初始化HTTP请求参数, 只初始化并不会发送
if (obj.method.toUpperCase() === ‘GET’) { // get方法
xhq.open(obj.method, obj.url + ‘?’ + splicStr(obj.data), typeof obj.async === ‘boolean’? obj.async : true) // 路径拼接
xhq.send()// 三、发送此次请求
}
else if (obj.method.toUpperCase() === ‘POST’) { // post方法
xhq.open(obj.method, obj.url, typeof obj.async === ‘boolean’? obj.async : true)
xhq.setRequestHeader(“content-type”,“application/x-www-form-urlencoded”) // 以表单提交
xhq.send(obj.data)// 三、发送此次请求
}
//四、监听发送
xhq.onreadystatechange = function () {
if ( xhq.readyState == 4 && xhq.status == 200 ) {
// success 回调
success(xhq.responseText)
} else if (xhq.readyState == 4 && xhq.status !== 200) {
// error 回调
error()
}
}
}
sendAjax({
url: ‘your url’,
method: ‘post’,
async: true,
data: {
username: ‘xiong’,
pwd: ‘123’
},
success: function (data) {
console.log(data)
},
error: function () {
console.log(‘发生了错误’)
}
})
★★★ 手写JSONP的原理和实现
function jsonp(url,data,callback){
var funcName = ‘jsonp_’+Date.now()+Math.random().toString().substr(2, 5)
//如果有其他参数需要拼接
if(typeof data===‘object’){
var tmpArr = []
for (let key in data){
let value =data[key]
tmpArr.push(key+‘=’+value)
}
data = tmpArr.join(‘&’)
}
let script = document.createElement(‘script’)
script.src = url + ‘?’ + data + '&callback= ’ + funcName
document.body.appendChild(script)
window[funcName]= function (data){
callback(data)
//清除标签
delete Window[funcName]
document.body.removeChild(script)
}
}
★★★★ 手写深拷贝
//乞丐版
function deepCopy(obj){
return obj.JSON.Parse(JSON.Stringify(obj))
}
//面试够用版
function deepCopy(obj){
if(typeof obj)
if(typeof obj ==‘object’){//判断是否为复杂数据源类型
var result = obj.constructor == Array?[]:{} //数组还是对象
for(let i in obj){
result[i]= typeof obj[i] ==‘object’? deepCopy(obj[i]):obj[i]
}
}else{
//简单数据类型
var result = obj;
}
return result
}
★★★ 手写浅拷贝
function clone(target) {
if(target === null ) {
return target
}
// 克隆 数组 和 对象
let cloneTarget = Array.isArray(target) ? [] : {}
for (const key in target) {
if (target.hasOwnProperty(key)) {//判断是否是本身的属性
cloneTarget[key] = target[key]
}
}
return cloneTarget
}
★★★★ 手写 bind
Function.prototype.myBind = function(context) {
if (typeof this !== ‘function’) {
throw new TypeError(‘Error’)
}
//返回一个绑定this的函数,这里我们需要保存this
const _this = this
const args = […arguments].slice(1)
//返回一个函数
return function F() {
//因为返回一个函数,我们可以new F()需要判断能当做构造函数吗
if (this instanceof F) { //实例是F这个构造函数造出来的
return new _this(…args, …arguments)
}
return _this.apply(context, args.concat(…arguments))
}
}
★★★★ 手写 call
Function.prototype.myCall = function(context) {
context=context||window //传入参数的话,就指定context为this指向,否则指定window
context.fn = this
const args = […arguments].slice(1) //入参删除context
const result = context.fn(…args)
delete context.fn
return result
}
★★★★ 手写 apply
Function.prototype.myApply = function(context) {
context = context || window
context.fn = this //给传入的上下文对象添加一个fn方法,这个fn方法即为myApply调用者
let result
if(arguments[1]){
result = context.fn(…arguments[1])
}else{
result = context.fn()
}
delete context.fn
return result
}
★★★ 手写模拟 object.create
function _create (obj){
function F(){} //创建一个构造函数
F.prototype = obj //将构造函数的原型对象赋值
return new F()
}
★★★ 手写模拟 Object.is
function _is(x, y) {
if (x === y) {
//运行到1/x === 1/y的时候x和y都为0,但是1/+0 = +Infinity, 1/-0 = -Infinity, 是不一样的
return x !== 0 || y !== 0 || 1 / x === 1 / y
} else {
//NaN=NaN是false,这是不对的,我们在这里做一个拦截,x ! x,那么一定是 NaN, y 同理
//两个都是NaN的时候返回true
return x !== x && y !== y
}
}
console.log(is(+0, -0))
console.log(is(NaN, NaN))
★★★ 手写 new
//new的实现过程(实际上就是调用这个构造函数,同时将构造函数的prototype上的属性方法挂上去。)
//1. 新建一个对象
//2. 对象 继承 构造函数的 原型链
//3. 将构造函数的this指向这个对象
//4. 根据构造函数的返回值的返回结构
function myNew(fn){
let obj = {} //定义空对象obj
obj = Object.create(fn.prototype) //将传入的构造函数的prototype属性方法复制到obj里面
let args = Array.prototype.slice.call(arguments,1)// 获取除去fn之外的参数
//或者 […arguments].slice(1)
let result = fn.call(obj,…args) // 调用传入的构造函数,矫正this为obj,并传入args
return typeof result === ‘object’||result instanceof Function? result : obj;
//如果构造函数返回引用类型,直接返回,否则返回obj
}
class Foo{
constructor(){
this.name = ‘ciel’
this.arg = arguments[0]
}
callname(){
console.log(this.name)
}
}
// 测试
let test = myNew(Foo, ‘hhh’, ‘123’, ‘saf’)
test.callName()
console.log(test.arg)
★★★ 手写对象扁平化
export function jsonFlatten(data) {
var result = {}
function recurse(cur, prop) {
if (Object(cur) !== cur) {
result[prop] = cur
} else if (Array.isArray(cur)) {
for (var i = 0, l = cur.length; i < l; i++) { recurse(cur[i], prop + ‘[’ + i + ‘]’) }
if (l === 0) { result[prop] = [] }
} else {
var isEmpty = true
for (var p in cur) {
isEmpty = false
recurse(cur[p], prop ? prop + ‘.’ + p : p)
}
if (isEmpty && prop) { result[prop] = {} }
}
}
recurse(data, ‘’)
return result
}
★★★ 手写数组扁平化
//第一种 正则表达式
function flatten(arr){
let str = JSON.stringify(arr);
return str.replace(/([]))/g,‘’).split(‘,’)
}
//第二种 递归
function flatten(arr, result = []) {
if (!Array.isArray(arr)) {
result.push(arr)
return result
}
for (let value of arr) {
flatten(value, result)
}
return result
}
//第三种 数组字符串方法
function flatten(arr) {
return arr.toString().split(‘,’).map(ele => parseInt(ele))
}
//第四种
function flatten(arr){
while(arr.some(item => Array.isArray(item))){
arr = [].concat(…arr);
}
return arr;
}
★★★ 手写数组去重
//1.ES6的Set
function unique(arr){
ruturn Array.from(new Set(arr))
}
//2. 双层for循环
function unique(arr) {
let result = []
for (let i = 0, len = arr.length; i < len; i++) { //第一层遍历
let flag = false //进行标记
for (let k = i + 1, len = arr.length; k < len; k++) {
if (arr[i] === arr[k]) { //如果后面有重复的就跳过
flag = true
k = arr.length
}
}
if (!flag) result.push(arr[i]) //没有重复的,添加到数组中
}
return result
}
//3. 利用数组的indexOf方法
function unique(arr) {
let result = []
for (let i = 0, len = arr.length; i < len; i++) {
if (result.indexOf(arr[i]) === -1) result.push(arr[i])
}
return result
}
//4. 利用类似桶排序的方法
function unique(arr) {
let result = []
let bucket = []; //创建一个桶
for (let i = 0, len = arr.length; i < len; i++) {
bucket[arr[i]] = 1 //将有些桶标记
}
for (key in bucket) { //取出有标记的桶的下标
console.log(key);
result.push(Number(key))
}
return result
}
//5. 利用filter方法
function unique(arr) {
return arr.filter((ele, index) => (arr.indexOf(ele) === index)) //过滤
}
//6. 利用map方法
function unique(arr) {
return arr.map((ele, index) => {
if (arr.indexOf(ele) === index) return ele
})
}
//7.排序后进行数组
function unique(arr) {
arr.sort()
let result = []
arr.forEach((ele, index) => {
if (ele !== arr[index + 1]) result.push(ele)
})
return result
}
★★★ 手写模拟实现 async/await
★★★★ 手写实现发布/订阅模式
class Subject { //定义被观察者
constructor() {
this.observers = []
}
addObserver(observer) { //订阅
this.observers.push(observer)
}
removerObserver(observer) {//取消订阅
let index = this.observers.indexOf(observer)
if (index !== -1) {
this.observers.splice(index, 1)
}
}
notify() { //通知
this.observers.forEach(observer => {
observer.update()
})
}
}
class Observer { // 定义观察者
update() {
console.log(‘subject更新了’);
}
subscribeTo(subject) {
subject.addObserver(this)
}
}
let subject = new Subject() //被观察者
let observer1 = new Observer() //观察者
observer1.subscribeTo(subject) //观察者进行订阅
let observer2 = new Observer() //观察者
observer2.subscribeTo(subject) //观察者进行订阅
subject.notify()
★★★★ 手写防抖
funtion debounce(fn,delay){
let timer = null
return ()=>{
clearTimeout(timer)
timer = setTimeout(()=>(fn())
,delay)
}
}
let a = debounce(()=>(console.log(‘防抖处理’)),500)
function 点击事件(){
a()
}
★★★★ 手写节流
function throttle(fn, delay = 500) {
let lastTime, time
return function(){
let context = this;
let args = [].slice.call(arguments);
time = Date.now()
if (!lastTime || time - lastTime > delay) {
fn.apply(context)
lastTime = time
}
}
}
function fn(){
console.log(‘节流’)
}
let a = throttle(fn, 1000)
function 点击事件(){
a()
}
css
1.盒模型
CSS 盒模型本质上是一个盒子,封装周围的HTML元素,它包括:边距,边框,填充,和实际内容
盒模型允许我们在其它元素和周围元素边框之间的空间放置元素
CSS 中组成一个块级盒子需要:
Content box(内容): 这个区域是用来显示内容,大小可以通过设置 width 和 height
Padding box(内边距): 包围在内容区域外部的空白区域; 大小通过 padding 相关属性设置
Border box(边框): 边框盒包裹内容和内边距。大小通过 border 相关属性设置
Margin box(外边距): 这是最外面的区域,是盒子和其他元素之间的空白区域。大小通过 margin 相关属性设置
可以通过 box-sizing 属性来设置盒子模型
box-sizing: border-box; /* IE盒模型 /
box-sizing: content-box; / 标准盒模型 */
讲讲react
vue对比react
http://caibaojian.com/vue-vs-react.html
react优缺点
https://www.cnblogs.com/qiqi715/p/10513195.html
react生命周期
https://www.jianshu.com/p/b331d0e4b398
Redux 流程
https://www.cnblogs.com/goodjobluo/p/9077010.html
git情景题
https://www.jianshu.com/p/34bf7e7ee694
Es6
https://www.cnblogs.com/ainyi/p/8537027.html
异步操作
https://www.jianshu.com/p/6f91e7696b91
虚拟dom diff算法
https://www.cnblogs.com/xiaohanga/p/11075861.html
setState
https://www.cnblogs.com/dreamcc/p/11409053.html
本地存储
https://zhuanlan.zhihu.com/p/25567678
react路由传参
https://www.jianshu.com/p/ad8cc02b9e6c
Eventloop 浏览器与Node的事件循环(Event Loop)有何区别
https://zhuanlan.zhihu.com/p/55511602
https://www.jianshu.com/p/5f1a8f586019
https://www.jianshu.com/p/deedcbf68880
Webpack
https://www.cnblogs.com/gaoht/p/11310365.html
Webpack优化
https://www.jianshu.com/p/773760145ea5
Eslint https://www.jianshu.com/p/f8d2ef372adf
node基于v8运行环境,非阻塞i/o
https://blog.youkuaiyun.com/banana960531/article/details/89043185
https://www.yuque.com/sunluyong/node
Ts
https://www.cnblogs.com/shangyixuan/p/11400619.html
js
1.函数节流防抖
(先讲下原理,再问问题:比如7s内点击100次,我用截流和防抖分别设置了3s,那么实际情况是分别执行了几次)
函数防抖:当事件被触发一段时间后再执行回调,如果在这段时间内事件又被触发,则重新计时。
在事件触发时,开始计时,在规定的时间(delay)内,若再次触发事件,将上一次计时(timer)清空,然后重新开始计时。保证只有在规定时间内没有再次触发事件之后,再去执行这个事件。 1次
函数节流(throttle):指定时间间隔内,若事件被多次触发,只会执行一次
在事件触发之后,开始计时,在规定的时间(delay)内,若再次触发事件,不对此事件做任何处理。保证在规定时间内只执行一次事件.3次
首先函数节流和函数防抖都是为了解决高频触发函数而带来的负荷问题
应用场景:防抖:向input框中一直输入文字
search搜索联想,用户在不断输入值时,用防抖来节约请求资源;
window触发resize的时候,不断的调整浏览器窗口大小会不断的触发这个事件,用防抖来让其只触发一次
节流:鼠标不断点击触发,mousedown(单位时间内只触发一次)
监听滚动事件,比如是否滑到底部自动加载更多,用throttle来判断
2.promise
(假设有个页面交互式:需要执行10次接口以后再渲染页面,看他怎么完成这个交互,他可以答得出来用promise.all。我再问他:有9个接口都是需要1s执行的,还有1个是需要2s执行的,那么总共大约会花多少时间)
2s
微任务
Promise 是异步编程的一种解决方案,其实是一个构造函数,自己身上有race,all、reject、resolve这几个方法,原型上有then、catch等方法。
Promise对象有以下两个特点。
(1)对象的状态不受外界影响。Promise对象代表一个异步操作,有三种状态:pending(进行中)、fulfilled(已成功)和rejected(已失败)。只有异步操作的结果,可以决定当前是哪一种状态,任何其他操作都无法改变这个状态。这也是Promise这个名字的由来,它的英语意思就是“承诺”,表示其他手段无法改变。
(2)一旦状态改变,就不会再变,任何时候都可以得到这个结果。Promise对象的状态改变,只有两种可能:从pending变为fulfilled和从pending变为rejected。只要这两种情况发生,状态就凝固了,不会再变了,会一直保持这个结果,这时就称为 resolved(已定型)。如果改变已经发生了,你再对Promise对象添加回调函数,也会立即得到这个结果。这与事件(Event)完全不同,事件的特点是,如果你错过了它,再去监听,是得不到结果的。
Promise.all
返回所有 Promise(p1,p2,p3) 实例的新 Promise。
当所有 Promise 实例都变成 fulfilled 状态,新 Promise 的状态才是 fulfilled 状态,返回所有 promise 实例的 resolve value 数组。
如果有一个 Promise 实例状态是 rejected 状态,则新 Promise 的状态是 rejected,返回第一个 promise reject 的 reason。
Promise.race
- 返回 p1,p2,p3 最先执行的 Promise 实例的 value 或者 reason,不论 fulfilled 或 rejected 状态
3.js原型链(讲一下你的理解)
实例对象与原型之间的连接,叫做原型链。_proto_( 隐式连接 )
JS在创建对象的时候,都有一个叫做__proto__的内置属性,用于指向创建它的函数对象的原型对prototype。
内部原型(_proto_)和构造器的原型(prototype)
1、每个对象都有一个__proto__属性,原型链上的对象正是依靠这个属性连结在一起.
2、[原型链的过程] 作为一个对象,当你访问其中的一个属性或方法的时候,如果这个对象中没有这个方法或属性,那么Javascript引擎将会访问这个对象的(__proto__)属性所指向上一个对象prototype,并在那个对象中查找指定的方法或属性,如果不能找到,那就会继续通过那个对象的(__proto_)属性指向的对象进行向上查找Object.prototype,直到这个链表结束。(null)
4.eventloop
Event Loop 即事件循环,是指浏览器或者Node 的一种解决JavaScript 单线程运行时不阻塞的一种机制,
单线程的是所有任务都在主线程上完成,任务太多的时候,页面卡死,
eventLoop可以解决单线程阻塞问题,程序中会有两个线程,一个主线程,一个eventLoop线程,负责主线程和其他进程之间的通信,
遇到I/O的时候,主线程会让eventLoop线程通知对应的程序,主线程的任务会继续往后执行,等I/O程序执行完了,eventLoop线程会把结果返回给主线程,主线程利用回调函数调用结果,完成任务
5.es6新特性
let let声明的变量有块作用域 不能重复声明变量,
const 常量声明,一旦声明,常量的值不能改
const定义一个对象,可以修改里面的属性,因为对象是引用类型 改变的是引用地址
解构赋值
扩展运算符 …
模板字符串
箭头函数 简洁,同时函数体内this对象,就是定义时所在的对象,而不是使用时所在的对象。This不会改变了 一定是匿名函数 不要在最外层定义箭头函数
Symbol类型 独一无二的值
Generators生成器函数 * 函数体内部使用yield表达式将进程分片暂停 next获得结果
class extends 类和继承
promise 异步编程的一种解决方法 三个状态pending-resolve-reject
状态从pending-resolve 成功返回一个promise对象,通过then传递
状态从pending-reject 失败走reject
promise.all 并发 promise.race速发
set和map forEach
不说set 数据结构Set new set()存储数据 set.size得到存储的数据长度
has()判断某个值是否存在set中 foreach遍历set
不说map : new map map.set map.get map.delete
都是用来存储数据用的,但是存储的数据格式不同
set 直接存储 任意类型数据
map 存储数据的时候,必须以key,value的形式,
set 使用forEach 遍历的时候,key和value值是一样的
而map 遍历的时候,key就是存进去的对象的key,value就是存在的值
for循环这种写法比较麻烦,因此数组提供内置的forEach方法。
forEach没有返回值,无法中途跳出forEach循环,break命令或return命令都不能奏效。
for…in循环主要是为遍历对象而设计的,不适用于遍历数组**
for…of循环相比上面几种做法,有一些显著的优点。
有着同for…in一样的简洁语法,但是没有for…in那些缺点。
不同于forEach方法,它可以与break、continue和return配合使用。
提供了遍历所有数据结构的统一操作接口。
6.宏任务和微任务
同步和异步任务分别进入不同的执行"场所",同步的进入主线程,异步的进入Event Table并注册函数。
当指定的事情完成时,Event Table会将这个函数移入Event Queue。
主线程内的任务执行完毕为空,会去Event Queue读取对应的函数,进入主线程执行。
上述过程会不断重复,也就是常说的Event Loop(事件循环)。
先执行同步代码 整个script都属于宏任务先执行script里的代码,然后微任务放在微任务队列中 宏任务放在宏任务队列中
宏任务和微任务是任务队列是两个任务队列中
同步执行完,执行微任务,再执行宏任务
宏任务 setTimout setInterval
微任务 promise process.nextTick
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-tg12ceSR-1657079140515)(/Users/huangrun/Library/Application Support/typora-user-images/image-20210903162213660.png)]
7.异步操作
回调函数 异步回调 函数作为参数传递给另外有一个函数 将函数内部的值通过回调函数传出来 解决同步的一种方式 多层嵌套 造成回调地狱
事件监听
发布/订阅 系统中有一个信号中心,当某个任务执行完成后向信号中心发布一个消息,其他任务就可以通过这个信号中心去订阅这个信号,从而知道什么时候直接可以开始执行 发布订阅模式和事件监听类似,可以查看消息中心有多少信号,每个信号有多少订阅者
promise
generator(ES6) 异步任务 通过yield关键字可以让任务在需要的地方暂停,每一步的值可以通过next获取
async/await(ES7) await得到的就是async异步返回值,底层原理还是promise中的resolve方法
8.本地存储 三种存储
首先总的来说,三者都是用于持久化数据存储的手段,都是存储在浏览器端,且同源.
localStorage和sessionStorage都是Web存储,大小5M左右,完全存储在客户端,它们是因为本地存储数据而存在.
cookies也是存储在浏览器端的,大小不超过4k,由服务器端存储在客户端。
localStorage属于永久性存储,数据存储量大,,而sessionStorage属于当会话结束的时候,存储的值会被清空,而cookie是通过设置过期时间expires来存储的。
9.表单只能输入数字
正则 type number
10.es2016-es2020
https://zhuanlan.zhihu.com/p/133658121
https://juejin.im/post/6844904021308735502
String.prototype.matchAll
String.prototype.matchAll比 match() 多得多的信息—它返回的迭代器不仅包括精确的匹配结果,还有全部的正则模式捕获结果。返回的结果更全面
而String.prototype 的 match() 方法仅返回完整的匹配结果,却不会返回特定正则表达式组的信息,
import() 按需获取的动态 import 返回了一个强大的 promise 函数,使得诸如按需引入
BigInt – 任意精度整数 或是在一个数值后添加 n 后缀,来将一个 number 转换为 bigint 类型
Promise.allSettled 它可以用在处理所有的 promise 无论结果是 fulfilled 还是 rejected,无需 catch
Promise.any()
globalThis 手动定义全局this指向,全局this指向:浏览器中它是 window, 在 worker 中它是 self, 在 Node.js 中它是 global
可选链 简化对象属性的长链式访问易出错又不易读
const title = data && data.article && data.article.title // 之前
const title = data?.article?.title // 之后
空值合并运算符 空值合并运算符 ?? 只会在左边的值严格等于 null 或 undefined 时起作用
11.Proxy和Reflect
Proxy用于修改某些操作的默认行为,即对编程语言层面进行修改,属于“元编程”,Proxy意思为“代理”,即在访问对象之前建立一道“拦截”,任何访问该对象的操作之前都会通过这道“拦截”,即执行Proxy里面定义的方法
let pro = new Proxy(target,handler)
其中 new Proxy相当于创建了一个Proxy实例,target为所要拦截的目标对象,handler也是一个对象,里面定义的是对拦截对象所要进行的拦截方法
Proxy也可以作为其他对象的原型对象使用
上述实例将pro作为obj的原型对象使用,虽然obj本身没有name这个属性,但是根据原型链,会在pro上读取到name属性,之后会执行相对应的拦截操作。
let pro = new Proxy(target,handler);
let obj = Object.create(pro);
Proxy常用的拦截方法
get(target,name,property)方法,用于拦截某个读取属性的操作,第一个参数为目标对象,第二个参数为属性名称,第三个属性为操作所针对的对象(可选参数)
set(target,name,value,property),用于拦截某个属性的赋值操作,第一个参数为目标对象,第二个参数为属性名,第三个参数为属性值,第四个参数为操作行为所针对的对象(可选参数)
has(target,key),用来拦截对象是否具有某个属性值的操作,第一个参数为目标对象,第二个参数为属性名
Reflect对象 :Reflect设计的目的是为了优化Object的一些操作方法以及合理的返回Object操作返回的结果,对于一些命令式的Object行为,Reflect对象可以将其变为函数式的行为
Reflect(target,name,property) Reflect.has(obj,“name”)
Reflect.get(target,name,property)
12.浏览器优化
减少请求数量
一.图片处理
1、雪碧图 - gulp Base64 使用字体图标来代替图片 - 自定义字体 @font-face{} 在安卓下可以使用webp格式的图片,它
二.减小资源大小 - grunt gulp webpack
1、HTML压缩2、CSS压缩3、JS压缩与混乱4、图片压缩
三.优化网络连接
cdnCDN即内容分发网络,它能够实时地根据网络流量和各节点的连接、负载状
用户可就近取得所需内容,解决 Internet网络拥挤的状况,提高用户访问网站的响应速度
四.优化资源加载
资源加载位置
1、CSS文件放在head中,先外链,后本页
2、JS文件放在body底部,先外链,后本页
3、body中间尽量不写style标签和script标签
资源加载时机
1、异步script标签
2、模块按需加载需要根据路由来加载当前页面需要的业务模块
3、资源懒加载与资源预加载
五.减少重绘回流
当render tree中的一部分(或全部)因为元素的规模尺寸,布局,隐藏等改变而需要重新构建。这就称为回流(reflow)。
当render tree中的一些元素需要更新属性,而这些属性只是影响元素的外观,风格,而不会影响布局的,比如background-color。则就叫称为重绘。
回流必将引起重绘,而重绘不一定会引起回流。
优化
减少回流和重绘
css3硬件加速(GPU加速)
六.【DOM优化】
1、缓存DOM2、减少DOM深度及DOM数量3、批量操作DOM4、批量操作CSS样式5、在内存中操作DOM6、DOM元素离线更新7、DOM读写分离8、事件代理9、防抖和节流10、及时清理环境
13.static有什么特性
new出来一个实例对象是否带有static属性 static用ES5怎么写
没有创建对象,也能使用属性和调用方法
用来形成静态代码块以优化程序性能。因为只会在类加载的时候执行一次。因此,很多时候会将一些只需要进行一次的初始化操作都放在static代码块中进行。
static块可以置于类中的任何地方,类中可以有多个static块。在类初次被加载的时候,会按照static块的顺序来执行每个static块,并且只会执行一次
被static修饰的变量或者方法是独立于该类的任何对象,也就是说,这些变量和方法不属于任何一个实例对象,而是被类的实例对象所共享。
类第一次加载初始化的时候就去加载static部分,后面可以重新赋值
static用ES5怎么写:静态是通过类名直接调用 class A staticB
直接用A.B 将B绑定在A上
14.async await
await后面有个接口 接口要2S才能完成 接口2S才会执行
await是同步还是异步:await同步 async异步
async和await有两个关键字,一个写在函数外面,一个写在函数里面,函数外面是异步的 函数里面是同步的 调用函数的那一行其实是异步的,下一行
函数里面转成阻塞的
async await promise改写
async函数中
let a=await promise 的a函数
let b=await promise 的b函数
promise.all改写 Promise.allSettled Promise.any
promise实现promise.all的方法
async 使用的时候报错,如何捕获try…catch
var test3 = async function () {
try {
await p1();
await p2();
p3();
} catch (e) {
console.log(‘p1失败了’, e)
}
}
15.$.ajax中async:false的阻塞和await这种阻塞有什么区别
没区别
16.怎么携带cookie发送给后端
设置请求头 请求头中携带cookie
17.项目上线怎么解决跨域
aginx 上线前设置好了 上线后不存在跨域
18.如何扩展数组,让他拥有新的方法
Array.prototype.
19.原型和原型链的关系
const a=new Fn a里面有什么属性指向Fn __proto a里面有没有属性值就是Fn
通过什么方法判断a的构造器就是Fn a.constructor==fn
prototype 原型对象中的constructor属性 指向的是prototype自己所在的构造函数
20.前端堆 栈 深拷贝浅拷贝
浅拷贝只能拷贝一层, obj的第一层a改变,newObj不变
浅拷贝: obj的第二层改变,newObj也会变化
Object.create(obj)
Object.assign({}, obj)
{ …obj }
map set
深拷贝,obj和newObj改变,互相不影响
手动封装递归实现深拷贝 loadsh const newObj = _.cloneDeep(obj);
JSON序列化 JSON.stringfy(state) JSON.parse() immutable
堆–引用类型地址传递
堆:动态分配的内存,大小不定,不会自动释放。存放引用类型的值 先进后出FILO
栈–基本类型值传递
栈:自动分配内存空间,系统自动释放,存放基本类型的值和引用类型的地址 先进先出FIFO
对象浅拷贝在react中用到哪些地方,为什么
为什么在react中要使用浅拷贝
redux中要求:状态是只读的,唯一且不可修改的,reducer必须是一个纯函数
-因为redux中数据不可更改,所以redux中的数据应该要拷贝 返回一个新值
21.class中 super作用是什么
es5 的继承是先创建子类的this,然后将父类的方法添加到子类的this上去;
es6 的继承是创建父类的this对象,然后再对this对象添加方法/属性。
而super方法就是用来创建父类this对象的。
实际上执行的是 super.sport.call(this);
22.闭包
1、闭包:打破了作用域链的规则 闭包就是能够读取其他函数内部变量的函数
2、用途:a:可以读取函数内部的局部变量 b:让这些变量始终保持在内存当中
3、注意:由于闭包会使得函数中的变量都被保存在内存当中,内存会消耗很大,所以不能够滥用
闭包,否则会造成网⻚性能的问题 闭包 内存泄漏 意外的全局变量 被遗忘的定时器 没有清除的dom应用 ,故要及时清除,清除内存泄漏方法有两种,一是标记清除,二便是引用计数清除。 4.使用闭包的好处是不会污染全局环境,方便进行模块化开发,减少形参个数,延长了形 参的生命周期,坏处就是不恰当使用会造成内存泄漏 5 闭包的常用场景有一是函数作为返回值,二是函数作为参数来传递。不适用于返回闭包的 函数是个特别大的函数,很多高级应用都要依靠闭包实现. 闭包经典使用场景一:通过循环给页面上多个dom节点绑定事件 闭包使用场景二:封装变量 闭包使用场景三:延续局部变量的寿命
23.普通构造函数constructor与class的区别?
传统的javascript中只有对象,没有类的概念。它是基于原 型的面向对象语言。原型对象特点就是将自身的属性共享给新对象。 ES6引入了Class(类)这个概念,通过class关键字可以 定义类。该关键字的出现使得其在对象写法上更加清晰,更像是一种面向对象的语言
constructor: constructor()方法是类的默认方法,通过new命令生成对象实例 时,自动调用该方法。一个类必须有constructor()方法,如果没有显式定义,一个空的 constructor()方法会被默认添加。
24.回流/重绘
当渲染树中的一部分或者全部因为元素的尺寸、布局、隐藏等改变而需要重新构建的时候,这时 候就会发生回流。 每个⻚面都至少发生一次回流,也就是⻚面第一次加载的时候。 在回流的时候,浏览器会使渲染树中受到影响的元素部分失效,并重新绘制这个部分的渲染树, 完成回流以后,浏览器会重新绘制受到影响的部分元素到屏幕中,这个过程就是重绘
React
1.父子组件传信
(父组件获取子组件有哪几种方式,子组件获取父组件有那几种方式)
父组件获取子组件:父组件定义获取数据的方法 ,绑定在子组件身上,子组件调用这个方法并传参
子组件获取父组件:
类组件中:子组件身上绑定属性
函数组件中:传props参数
跨组件通信:ref ref不能绑定函数组件 高阶组件
redux
2.react(生命周期,最新版本改动,知道哪有新特性)
----------------初始化—-----------
constructor:属性的继承 ,定义状态,绑定方法
类组件有constructor的 constructor执行顺序 class中 new实例对象的时候执行constructor
componentWillMount-17弃用
UNSAFE_componentWillMount:组件即将挂载,初始化事件,为挂载做准备,内部操作
render:解析this.state,this.props,将jsx型的vdom渲染为对象类型的vdom,render函数中不允许使用this.setState()
componentDidMount:1.组件挂载结束2.vdom->真实dom3.发送数据请求,赋值给state4.真实dom操作,第三方库实例化
--------------更新阶段—----------
componentWillReceiveProps-17弃用:1.用于接收新的props值2.这个钩子的实际作用:判断组件身上的props是否改变
这里面做哪些事情比较好 判断组件身上的props是否改变
shouldComponentUpdate:1.可以决定组件是否要重新渲染2.接收新旧状态,用于做对比(浅对比)
componentWillUpdate-17弃用:1.表示组件更新前的准备
render
componentDidUpdate
----------------销毁阶段----------------
componentWillUnmount:组件销毁,清除无用的实例,事件,定时器等
componentDidCatch:用于捕获组件throw的错误,然后显示回退UI,网络有延迟等
17版本 废弃componentWillMount componentWillReceiveProps componentWillUpdate
static getDerivedStateFromProps
1.这是一个静态方法,不能使用this
2.提供了一次修改state的机会
getSnapshotBeforeUpdate
1.快照
2.用于传递一个值给componentDidUpdate
15版本 getDefaultProps定义属性 , getInitialState定义状态
3.组件按需加载
**(实现方式,需要的依赖) **懒加载
1、vue异步组件技术
vue-router配置路由,使用vue的异步组件技术,可以实现按需加载。
但是,这种情况下一个组件生成一个js文件。
2.import()
3.webpack提供的require.ensure()
4.第三方库比如react-loadable
5.lazy-loader
**4.react新特性,hooks ** 16.8版本出 19年
useState:定义状态 修改状态!
useEffect:相当于componentDidMount , componentDidUpdate ,同useLayoutEffect
componentWillUnmount三个钩子的组合 DOM操作 第三方实例化可以做 清除无用实例和事件
useContext:跨组件通信 createContext创建一个组件 <numContext.Provider value={num}>
useDebugValue:自定义 hook 的标签 方便调试台查看
useMemo:记忆组件 动态缓存 新值和旧值一样,不重新渲染页面,优化作用,类似于shouldComponentUpdate useCallBack
useRef:返回一个可变的ref对象
useImperativeHandle:将组件中的方法放到外面使用 搭配React.forwardRef
自定义hook封装过组件吗
1.将Hello组件和App组件中共用的逻辑放在统一的自定义hook中写
2.自定义hook,hook名以use开头
3.其他组件通过import引入自定义hook,就可以使用了
5.是否看过react源码
看过 说一说setState redux原理
6.vue对比react
相同点:1.都使用了vdom虚拟dom,如果需要改变任何元素的状态,先改vdom,当有变化产生时,会创建新的vdom,通过diff算法对比新旧vdom的差异,只需要渲染差异部分就行
2.都使用组件化
不同点:1.react中有新语法jsx,vue用普通的html
2.vue中父组件和子组件有通信的时候,父组件数据改变引起子组件改变,子组件会重新渲染 如果父子组件没有通信,父组件改变子组件不会渲染
react中不管是否有数据通信,父组件改变子组件都会渲染
3.react:create-react-app vue : vue-cli
4.react中用redux管理状态,state通过setState更新
vue中数据由vuex管理
React Native
React Native能在手机上创建原生应用,React在这方面处于领先位置。使用JavaScript, CSS和HTML创建原生移动应用,这是一个重要的革新。Vue社区与阿里合作开发Vue版的React Native——Weex也很不错,但仍处于开发状态且并没经过实际项目的验证。
既拥有Native的用户体验、又保留React的开发效率
React Native与React.js的主要区别还是JSX,它使用XML标记的方式去直接声明界面,将HTML直接嵌入到JavaScript代码中
7.react优缺点
优点:1.vdom react速度更快 2.react不用考虑浏览器兼容问题 3.组件化 模块化 4.单向数据流:指父组件可以向子组件传递数据,但子组件不可以修改这些数据,否则会报错。
劣势: react只是视图view 需要引入很多第三方插件
**8.redux流程 ** redux的原理
1)首先先把redux相关工具安装好
yarn add redux react-redux redux-thunk redux-devtools-extension -D
2)通过创建一个store实例createStore,接收一个rootReducer和中间件执行函数 一个组件只能有一个store实例
3)创建分块的数据rootReducer,通过combineReducers打造rootReducer,里面放分块的数据
4)在组件中通过高阶组件connect函数,
接收store里的数据,
把ActionCreators里的方法绑定到组件身上,并且可以发送动作action给reducer
5)在reductor中根据action中的type动作类型,判断动作修改数据
redux-saga namespace state effects subscriptions事件订阅 监听路由变化 键盘变化鼠标变化 reducers
redux-saga
namespace命名空间 数据只在此组件中使用
state 定义状态
effects 调用数据请求 发送action给reducer
subscription 监听事件变化 键盘变化 鼠标变化 根据条件触发dispatch需要的action
reducers 修改状态
9.setState
**setState在合成事件和钩子函数中是异步的 **
在原生事件和setTimeout中是同步的
setState的“异步”并不是说内部由异步代码实现,其实本身执行的过程和代码都是同步的,只是合成事件和钩子函数的调用顺序在更新之前,导致在合成事件和钩子函数中没法立马拿到更新后的值,形式了所谓的“异步”,当然可以通过第二个参数 setState(partialState, callback) 中的callback拿到更新后的结果。
setState 的批量更新优化也是建立在“异步”(合成事件、钩子函数)之上的,在原生事件和setTimeout 中不会批量更新,在“异步”中如果对同一个值进行多次 setState , setState 的批量更新策略会对其进行覆盖,取最后一次的执行,如果是同时 setState 多个不同的值,在更新时会对其进行合并批量更新。
第一个参数可以是对象或者函数 是更新state 第二个参数获取最新的state,副作用操作 dom操作事件触发声明 数据获取
10.Alibaba fusion design
完整的中后台设计系统解决方案
Alibaba Fusion Design 最初来源于一个阿里巴巴内部的中台项目
antv
npm install @antv/g2
G2 G6关系数据可视化图表分析工具 F2移动端可视化 L7地图空间数据可视化
11.react路由传参
params:
优势 : 刷新地址栏,参数依然存在
缺点:只能传字符串,并且,如果传的值太多的话,url会变得长而丑陋。
query:
优势:传参优雅,传递参数可传对象;
缺点:刷新地址栏,参数丢失
state:同query this.props.location.query.state
search:同search this.props.location.search
12.虚拟dom和diff算法
1.虚拟dom是什么
所谓的virtual dom,也就是虚拟节点。它通过JS的Object对象模拟DOM中的节点,然后再通过特定的render方法将其渲染成真实的DOM节点
2.虚拟dom的使用基本流程(前四步骤)
1.获取数据
2.创建vdom
- 通过render函数解析jsx,将其转换成 vdom结构
4.将vdom渲染成真实dom
5.数据更改了
6.使用diff算法比对两次vdom,生成patch对象
7.根据key将patch对象渲染到页面中改变的结构上,而其他没有改变的地方是不做任何修改的( 虚拟dom的惰性原则 )
3.diff算法是什么
Diff算法是用于比较两个新旧vdom树的差异的,比较完之后会得到一个差异对象,我们称之为patch补丁对象
比较后会出现四种情况:
1、此节点是否被移除 -> 添加新的节点
2、属性是否被改变 -> 旧属性改为新属性
3、文本内容被改变-> 旧内容改为新内容
4、节点要被整个替换 -> 结构完全不相同 移除整个替换
4.diff算法运行结束后,返回是什么
返回一个key
13.react中的路由模式有哪些?有什么区别?
1.browerHistory (推荐) :它使用History API用于处理URL、创建像xxx.com/login/xxx这样的URL实例
2.hashHistory :使用URL中的hash(#)部分去创建形如xxx.com/#/login/xxx 类似于vue中hash路由
3.createMemoryHistory :不会在地址栏被操作或读取。和另外两种histoty的不同点是必须创建它,这种方法 便于测试
4、NativeRouter:经常配合ReactNative使用,多用于移动端
5、StaticRouter:设置静态路由,需要和后台服务器配合设置,比如设置服务端渲染时使用
14.mobx
改变了store中的值 发现页面没有更新,是为什么 如果想要更新怎么做
- 组成部分
actions->state->computed values->Reactions - 工作流
在mobx中, 数据是通过加 @observable 作为可监测的被观察者, 在view层中, 你可以通过添加@observer 将view作为观察者,对数据进行监测, 如果要触发改变数据,则使用@action, 事实上,你可以直接在view层改变数据, 但这种方式不便监控数据,因此不推荐直接改变数据。 而@computed可以用来计算数据, 也可以是计算多个数据之后返回新的数据, 如果其中数据改变, @computed也会触发改变 - 优点
不同于redux的单一数据流, mobx中,你可以同时在各个地方使用同一份state, 也可以在一个页面中使用多个store文件
15.react-router原理
react-router依赖基础 - history
history是一个独立的第三方js库,可以用来兼容在不同浏览器、不同环境下对历史记录的管理
老浏览器的history: 主要通过hash来实现,对应createHashHistory
高版本浏览器: 通过html5里面的history,对应createBrowserHistory
node环境下: 主要存储在历史记录memeory里面,对应createMemoryHistory
抽象了一个公共的文件createHistory:
此时的location跟浏览器原生的location是不相同的,最大的区别就在于里面多了key字段,history内部通过key来进行location的操作
原理:
1.1执行URL前进
createBrowserHistory: pushState、replaceState
createHashHistory: location.hash=*** location.replace()
createMemoryHistory: 在内存中进行历史记录的存储
1.2检测URL回退
createBrowserHistory: popstate
createHashHistory: hashchange
createMemoryHistory: 因为是在内存中操作,跟浏览器没有关系,不涉及UI层面的事情,所以可以直接进行历史信息的回退
1.3state的存储
为了维护state的状态,将其存储在sessionStorage里面:
基本原理:实现URL与UI可视化界面的同步。其中在react-router中,URL对应Location对象,而UI是由react components来决定的,这样就转变成location与components之间的同步问题。
在react-router中最主要的component是Router RouterContext Link,history库起到了中间桥梁的作用。
安装react-router-dom
-Link组件用于点击链接跳转其他页面,没有路由激活
-NavLink 用于有路由激活效果的
1.Switch:表示一次只渲染一个组件
2.Route:路由组件,用于展示一个组件 同router-view
3.Redirect:重定向
4.lazy + Suspense :实现路由懒加载
5.exact:路径完全匹配
6.fallback:组件切换时候的转场组件
react-router-dom和react-router 的区别
在 V3 中,我们 是将整个庞大的 router 直接丢给 DOM,而在 V4 中,除了 BrowserRouter, 我们丢给 DOM 的是我们的应用程序本身 另外,V4 中,我们不再使用 {props.children} 来嵌套组件了,替代的 ,当 route 匹配时,子组件会被渲染到 书写的地方 (router3需要进行集中式管 理,router4的话哪里需要在哪里配置就可以了)
16.react原理
1.1setState
setState在合成事件和钩子函数中是异步的
在原生事件和setTimeout中是同步的
setState的“异步”并不是说内部由异步代码实现,其实本身执行的过程和代码都是同步的,只是合成事件和钩子函数的调用顺序在更新之前,导致在合成事件和钩子函数中没法立马拿到更新后的值,形式了所谓的“异步”,
第一个参数可以是对象或者函数 是更新state 第二个参数获取最新的state,副作用操作 dom操作事件触发声明 数据获取
1.2 JSX语法的转化
JSX 仅仅是 createElement() 方法的语法糖(简化语法)
JSX 语法被 @babel/preset-react 插件编译为 createElement() 方法
react.createElement()
React 元素:是一个对象,用来描述你希望在屏幕上看到的内容
1.3组件更新机制
setState() 的两个作用: 1. 修改 state 2. 更新组件(UI)
过程:父组件重新渲染时,也会重新渲染子组件。但只会渲染当前组件子树(当前组件及其所有子组件)
1.4组件性能优化
减轻 state:只存储跟组件渲染相关的数据
避免不必要的重新渲染 : shouldComponentUpdate(nextProps, nextState) 通过返回值决定该组件是否重新渲染,返回 true 表示重新渲染,false 表示不重新渲染 起到优化作用
1.5纯组件 PureComponent
PureComponent 内部自动实现了 shouldComponentUpdate 钩子,不需要手动比较
纯组件内部通过分别 对比 前后两次 props 和 state 的值,来决定是否重新渲染组件
纯组件内部的对比是 shallow compare(浅层对比)
1.6虚拟 DOM 和 Diff 算法
数据改变视图更新
初次渲染时,React 会根据初始state(Model),创建一个虚拟 DOM 对象(树)。
根据虚拟 DOM 生成真正的 DOM,渲染到页面中。
当数据变化后(setState()),重新根据新的数据,创建新的虚拟DOM对象(树)。
与上一次得到的虚拟 DOM 对象,使用 Diff 算法 对比(找不同),生成patch补丁对象,得到需要更新的内容。
最终,React 只将变化的内容更新(patch)到 DOM 中,重新渲染到页面。
17.connect的实现, 如何在全局中取得store
连接React组件与 Redux store。
connect:connect函数的返回值是一个高阶组件,通过高阶组件来获取store中的数据 connect底层原理:是闭包
mapStateFromProps:从countReducer中解构出num数据,用来获取数据
mapDispatchFromProps:将ActionCreators中的方法绑定到组件上,并且发送action
connect实现原理:
首先connect之所以会成功,是因为Provider组件:
在原应用组件上包裹一层,使原来整个应用成为Provider的子组件
接收Redux的store作为props,通过context对象传递给子孙组件上的connect
那connect做了些什么呢?
它真正连接 Redux 和 React,它包在我们的容器组件的外一层,它接收上面 Provider 提供的 store 里面的 state 和 dispatch,传给一个构造函数,返回一个对象,以属性形式传给我们的容器组件。
关于它的源码
connect是一个高阶函数,首先传入mapStateFromProps、mapDispatchFromProps,然后返回一个生产Component的函数(wrapWithConnect),然后再将真正的Component作为参数传入wrapWithConnect,这样就生产出一个经过包裹的Connect组件,该组件具有如下特点:
通过props.store获取祖先Component的store
props包括stateProps、dispatchProps、parentProps,合并在一起得到nextState,作为props传给真正的Component
componentDidMount时,添加事件this.store.subscribe(this.handleChange),实现页面交互
shouldComponentUpdate时判断是否有避免进行渲染,提升页面性能,并得到nextState
componentWillUnmount时移除注册的事件this.handleChange
18.webpack打包vue,react,
与自己的源代码分离使用splitchunks
19.react的版本15和16的区别
1.1生命周期不同
15和16版本不一样的地方只有初始化阶段getDefaultProps,getInitialState,
用constructor 代替
20.react组件的时候,经常写class组件,class没有的时候怎么写
React.createClass 我们最早使用这个方法来构建一个组件“类”,它接受一个对象为参数,对象中必须声明一个render方法,render返回一个组件实例
21.生命周期 钩子函数用es5怎么定义
protoType.componentDidMount
22.为什么使用hook
不必写class组件就可以用state和其他的React特性;
自定义hook 实现组件的复用 用useEffects代替生命周期方法 代码更加简洁
23.useEffect
第二个参数可以支持数组中有几个函数吗 不可以
依赖性可以是一些props的属性
1)相当于componentDidMount , componentDidUpdate ,
componentWillUnmount三个钩子的组合
2)数据请求,DOM操作 第三方实例化可以做 清除无用实例和事件
3)参数 第一个参数是回调函数,用于执行副作用操作 第二个参数就是定义的state 返回值:是一个函数 return ()=>{} 用来清除无用的组件
4)数据更新触发这个函数
第二个参数是空数组[],数据更新只执行一次,相当于componentDidMount
第二个参数是[a],只有a数据更新才会触发这个函数,b数据改变不会触发
第二个参数不写,所有数据更新都触发这个函数,执行多次的,相当于componentDidUpdate+componentDidMount
24.react中的key
diff算法同层级对比需要根据key来
每一个key都是独一无二的,为的是方便React的和解过程中,方便元素树进行比对。key不仅可以使这个过程更有效率,如果没有key时,React不知道那个本地状态对应于移动中的哪个项目
组件a里面嵌套组件b 给组件b上加了key 组件a中state改变了 组件b会发生什么行为
如果a b有数据通信 b的数据也会改变 并且b下面的子组件都会重新渲染
25.组件销毁怎么做 componentWillUnmount
定义state flag:false 作为开关 定义方法if判断是否为1,改变state flag:true
将方法绑定组件上 可以用三目或者短路原则&&控制组件的显示隐藏
27antv中input输入框三个属性怎么实现
onChange输入框内容改变时候回调 value输入的内容 defaultValue输入框默认值
input默认值在input中绑定value 定义一个state 和input双向数据绑定 做成受控组件 定义一个事件 改变的时候获取e.target.value
28.生命周期应该做哪些事情完成异步的组件
29.函数组件和类组件区别
相同点:无论是使用函数或是类来声明一个组件,它决不能修改它自己的 props。
所有 React 组件都必须是纯函数,并禁止修改其自身 props 。
React是单项数据流,父组件改变了属性,那么子组件视图会更新。
属性 props 是外界传递过来的,状态 state 是组件本身的,状态可以在组件中任意修改
组件的属性和状态改变都会更新视图。
不同点:
函数组件的性能比类组件的性能要高,因为类组件使用的时候要实例化,而函数组件直接执行函数取返回结果即可。
类组件有this state定义state 修改state 生命周期 ES6的class创建组件
函数组件 没有 函数组件用hooks useState JS函数创建组件 函数组件必须有返回值
30.受控组件和非受控组件
如果组件或是元素并不是由自己来控制的,那么它就是受控组件
如果组件或是元素自己的状态自己管理,那么它就是非受控组件
- 子组件的渲染收到父组件的控制【父组件传递数据给子组件,子组件使用了这个数据】
- 在一个组件中,如果我们让这个组件的state来控制表单元素的值,那么这个组件就是受控组件【几乎所有的表单都会被控制的】
31.redux 中间件的实现
中间件的执行时机在aciton发出到reducer执行之间
redux中间件的实质是对redux中store.dispath函数的再封装
当中间件被rudex初始化完成后,每次调用store.dispatch都会执行一遍每个中间件,他们之间是链式关系
32.高阶组件
复用组件逻辑的一种高级技巧。
- 高阶组件是一个函数
- 高阶组件接收一个组件作为参数进行使用,返回值是一个类组件
- 高阶组件的目的是为了: 复用组件,将多个组件都要使用的类似逻辑放在同一个地方进行处理
- 高阶组件返回的一个类组件,而自定义Hook可以返回任何东西
- 高阶组件必须传递一个组件作为参数,而自定义Hook不需要
33.单页面应用这么实现
路由实现 react-router -dom react-router实现原理看题目
34.fetch和axios的区别
axios是一种对ajax的封装,fetch是一种浏览器原生实现的请求方式,跟ajax对等
Fetch:可以很好的实现分离原则。 也原生支持目前流行的异步流(promise) 模型,但是很多平台都不实现,不能进行复杂的io操作 Fetch携带cookie credentials: “include”
https://cloud.tencent.com/developer/article/1359550
axios怎么取消请求
原生中有一个取消的方法,可以调用XMLHttpRequest对象上的abort方法 在axios拦截器里, 查找axios的文档,发现 可以通过使用CancelToken来取消axios发起的请求
35.react合成事件
- 合成事件原理
如果DOM上绑定了过多的事件处理函数,整个页面响应以及内存占用可能都会受到影响。React为了避免这类DOM事件滥用,同时屏蔽底层不同浏览器之间的事件系统差异,实现了一个中间层——SyntheticEvent。
当用户在为onClick添加函数时,React并没有将Click时间绑定在DOM上面。
而是在document处监听所有支持的事件,当事件发生并冒泡至document处时,React将事件内容封装交给中间层SyntheticEvent(负责所有事件合成)
所以当事件触发的时候,对使用统一的分发函数dispatchEvent将指定函数执行。
- 与原生事件的区别
React合成事件一套机制:React并不是将click事件直接绑定在dom上面,而是采用事件冒泡的形式冒泡到document上面,然后React将事件封装给正式的函数处理运行和处理。
36.flux
flux 是 react 中的类似于 vuex 的公共状态管理方案,它是 Facebook 官方给出的应用架构,利用数据的单向流动的形式对公共状态进行管理。现已不推荐使用。但是为了能更好的理解 Redux 方案,还是有必要熟悉 flux 的工作流程滴~
使用 cnpm i flux -S 的方式进行安装。
flux的组成
flux 是 react 中的类似于 vuex 的公共状态管理方案,它是 Facebook 官方给出的应用架构,利用数据的单向流动的形式对公共状态进行管理。现已不推荐使用。但是为了能更好的理解 Redux 方案,还是有必要熟悉 flux 的工作流程滴~
View:视图层
Action:视图发出的消息
Dispatcher:派发者,用来接收Action,执行回调函数
Store:数据层,存放状态,一旦发生改动
flux 在进行数据更新时,会经历以下几步:
用户与 View 层交互,触发 Action
Action 使用 dispatcher.dispatch 将Action自己的状态发送给dispatcher
dispatcher 通过register注册事件,再通过Action传入的类型来触发对应的 Store 回调进行更新
Store 里进行相应的数据更新,并触发 View 层事件使试图也同步更新
View层 收到信号进行更新
37.redux与react-reudx的关系
redux是独立的应用状态管理工具。它是可以独立于react之外的。如果我们需要在react当中运用它,那么我们需要手动订阅store的状态变化,来对我们的react组件进行更新。那么react-reudx这个工具,就帮我们实现了这个功能,我们只需对store进行处理,react组件就会有相应的变化。
Redux的核心由三部分组成:Store, Action, Reducer。
Store : 是个对象,贯穿你整个应用的数据都应该存储在这里。
Action: 是个对象,必须包含type这个属性,reducer将根据这个属性值来对store进行相应的处理。除此之外的属性,就是进行这个操作需要的数据。
Reducer: 是个函数。接受两个参数:要修改的数据(state) 和 action对象。根据action.type来决定采用的操作,对state进行修改,最后返回新的state。
总结
Redux: store, action, reducer
store: getState, dispatch, subscribe
combineReducers
createStore
store ️ dispatch ️ action ️ reducer
react-redux:
connect: 将store作为props注入
Provider: 使store在子孙组件的connect中能够获取到
38.redux-thunk redux-saga
好处
可以进行前后端数据交互
缺点
将带有数据请求的action和没有带有数据请求的action混在一起了
缺点解决: 弃用redux-thunk,使用redux-saga
redux-saga可以将异步action和普通action区别开来
redux-saga可以将异步action和普通action区别开来,控制器与更优雅的异步处理
而redux-saga就是用Generator来处理异步。
redux-saga文档并没有说自己是处理异步的工具,而是说用来处理边际效应(side effects),这里的边际效应你可以理解为程序对外部的操作,比如请求后端,比如操作文件。
redux-saga同样是一个redux中间件,它的定位就是通过集中控制action,起到一个类似于MVC中控制器的效果。
同时它的语法使得复杂异步操作不会像promise那样出现很多then的情况,更容易进行各类测试。
这个东西有它的好处,同样也有它不好的地方,那就是比较复杂,有一定的学习成本。
39.DVA与CRA相比的优点
dva: dva 首先是一个基于 redux 和 redux-saga 的数据流方案,然后为了简化开发体验,dva 还额外内置了 react-router 和 fetch,所以也可以理解为一个轻量级的应用框架。— 来自官方。相比于cra只是多了内置的redux和redux-saga,帮我们处理了数据流这方面的需求而已。如果只是想要达到这个效果的话,直接在cra中增加dva-core的依赖也是可以做到的。
umi:是一个可插拔的企业级 react 应用框架。umi和cra都是应用框架,可能相比cra来说umi的功能点更多一些,只能说是功能性的话umi要相对来说更胜一筹
40.React新特性
render
支持返回这五类:React elements, 数组,Fragments,Portal,String/numbers,boolean/null。基础数据类型
Fiber
React Fiber的方法其实很简单——分片。把一个耗时长的任务分成很多小片,每一个小片的运行时间很短,虽然总时间依然很长,但是在每个小片执行完之后,都给其他任务一个执行的机会,这样唯一的线程就不会被独占,其他任务依然有运行的机会。
新的生命周期函数
由于异步渲染的改动,componentWillMount, componentWillReceiveProps,componentWillUpdate 三个函数将被废弃。
由于这是一个很大的改变会影响很多现有的组件,所以需要慢慢的去改。
目前react 16 只是会报warning,在react 17就只能在前面加UNSAFE_ 的前缀来使用
diff算法的作用计算出Virtual DOM中真 正变化的部分,并只针对该部分进行原生DOM操作,而非重新渲染整个页面
分层对比,把DOM树按层级拆分,简化了 复杂度,只比较同级元素,只匹配相同组件名字的组件,给列表结构的每个单元添加唯一的key 属性,方便比较(为什么不用index,如果用index的话追加在后面不会影响,如果追加到前面会 影响性能)
getDerivedStateFromProps
static getDerivedStateFromProps(props, state)在调用render方法之前调用,无论是在初始安装还是后续更新。它应返回一个对象来更新状态,或者返回null以不更新任何内容。
根据props更新state
这个生命周期可用于替代componentWillReceiveProps
getSnapshotBeforeUpdate()
getSnapshotBeforeUpdate(prevProps, prevState)在最近呈现的输出被提交到例如DOM之前调用。它使组件可以在可能更改之前从DOM捕获一些信息(例如滚动位置)。此生命周期返回的任何值都将作为参数传递给componentDidUpdate()。
hooks
lazy、suspense
lazy需要跟Suspence配合使用。
lazy实际上是帮助我们实现代码分割的功能。
由于有些内容,并不一定要在首屏展示,所以这些资源没有必要一开始就要去获取,那么这些资源就可以动态获取。
这样的话,相当于把不需要首屏展示的代码分割出来,减少首屏代码的体积,提升性能。
Suspence 很像Error Boundary,不同的是Error Boundary是用来捕获错误,显示相应的callback组件。而Suspence是用来捕获还没有加载好的组件,并暂停渲染,显示相应的callback。
41.component和pureComponent区别
PureComponent自带通过props和state的浅对比来实现shouldComponentUpdate(),而Component没有。这样不需要开发者实现shouldComponentUpdate,就可以使用PureComponnent来进行简单判断以提升性能。
-
Component适宜用在数据层级繁杂的地方;
-
PureComponent适宜用在小组件中(如模态框, 错误信息展示 etc)
使用pureComponent的条件
- props 和 state 必须是 immutable object
- props 和 state 最好不要有层级嵌套; 否则深层的数据改变不会更新组件(可以用 forceUpdate 强制更新组件)
- PureComponent 的子组件也应该是PureComponent
Webpack
webpack打包react vue,与自己的源代码分离使用splitchunks
webpack热更新:不用刷新浏览器而将新变更的模块替换掉旧的模块。
1.Webpack Loader Plugin
(loader , plugin分别什么作用,哪个配置可以把依赖包抽离出来,不打包进去)
【Loader】:用于对模块源码的转换,loader描述了webpack如何处理非javascript模块,并且在buld中引入这些依赖。loader可以将文件从不同的语言(如TypeScript)转换为JavaScript,或者将内联图像转换为data URL。比如说:CSS-Loader,Style-Loader等。babel-loader优雅降级配置ES高版本转成低版本
【Plugin】:是用于在webpack打包编译过程里,在对应的事件节点里执行自定义操作,比如资源管理、bundle文件优化等操作,
依赖包抽离 const ExtractTextWebapckPlugin= require(“extract-text-webpack-plugin”) module exclude node_modules
排除excloude排除node_modules
loader的使用很简单:
在webpack.config.js中指定loader。module.rules可以指定多个loader,对项目中的各个loader有个全局概览。
loader是运行在NodeJS中,可以用options对象进行配置。plugin可以为loader带来更多特性。loader可以进行压缩,打包,语言翻译等等。
loader从模板路径解析,npm install node_modules。也可以自定义loader,命名XXX-loader。
语言类的处理器loader:CoffeeScript,TypeScript,ESNext(Bable),Sass,Less,Stylus。任何开发技术栈都可以使用webpack。
webpack常用的loader
-
样式:style-loader、css-loader、less-loader、sass-loader等
-
文件:raw-loader、file-loader 、url-loader等
-
编译:babel-loader、coffee-loader 、ts-loader等
-
校验测试:mocha-loader、jshint-loader 、eslint-loader等
目的在于解决loader无法实现的其他事,从打包优化和压缩,到重新定义环境变量,功能强大到可以用来处理各种各样的任务。webpack提供了很多开箱即用的插件:CommonChunkPlugin主要用于提取第三方库和公共模块,避免首屏加载的bundle文件,或者按需加载的bundle文件体积过大,导致加载时间过长,是一把优化的利器。而在多页面应用中,更是能够为每个页面间的应用程序共享代码创建bundle。
webpack功能强大,难点在于它的配置文件,webpack4默认不需要配置文件,可以通过mode选项为webpack指定了一些默认的配置,mode分为:development/production,默认是production。
插件可以携带参数,所以在plugins属性传入new实例。
webpack常用的plugin
webpack内置UglifyJsPlugin
,压缩和混淆代码。
webpack内置CommonsChunkPlugin
,提高打包效率,将第三方库和业务代码分开打包。
ProvidePlugin
:自动加载模块,代替require和import
html-webpack-plugin可以根据模板自动生成html代码,并自动引用css和js文件
extract-text-webpack-plugin` 将js文件中引用的样式单独抽离成css文件
DefinePlugin` 编译时配置全局变量,这对开发模式和发布模式的构建允许不同的行为非常有用。
**【Mode】**可以在config文件里面配置,也可以在CLI参数中配置:webpack --mode=production(一般会选择在CLI,也就是npm scripts里面进行配置)。
在webpack4以下版本,webpack3.XX,通过plugins进行环境变量的配置。
**【resolve】**模块,resolver是个库,帮助webpack找到bundle需要引入的模块代码,打包时,webpack使用enhanced-resolve来解析路径。
2.webpack优化
多进程打包 安装插件thread-loader parallel-webpack HappyPack
多进程压缩 parallel-uglify-plugin terser-webpack-plugin
资源CDN 公用代码提取,使用 CDN 加载
动态polyfill 动态 polyfill 指的是根据不同的浏览器,动态载入需要的 polyfill。
3.版本号上加一个^是什么意思
升级到最新的版本
CSS Less
1.flex布局
(有个布局是:右边是固定宽度,左边是自适应,如何实现,如果是答了flex:1,那么是flex下面的那个属性设置1)
flex是由 flex-grow:1父容器在主轴上还有多少剩余空间,
flex-shrink当父元素的宽度大于所有子元素的宽度的和时(即父元素会有剩余空间),子元素如何分配父元素的剩余空间
flex-basis基准值组成
2.如果有个导航的吸顶效果,你这边如何实现
设置css:fixed固定定位
判断滚动条滚动的距离大于导航条距顶部的距离,来判断是否实现吸顶,然后addClass添加样式
3.less的hover怎么简写
a{
&:hover {} //这里&代表它的上一级就是a
}
4.css伪类 ::after,before的应用场景
::before和::after必须配合content属性来使用,content用来定义插入的内容,content必须有值,至少是空。默认情况下,伪类元素的display是默认值inline,可以通过设置display:block来改变其显示。
1.清除浮动 : 在浮动元素后面添加一个空的Div标签,然后在设置它的清除浮动要是,使用after伪元素
2.常见消息框 : 伪类content:’ ’ 伪类4条边必须宽度相同,而且其他三条边为transparent 可以通过设置定位元素left,top值为50%,translate(-50%,-50%) 来使任意宽高的元素居中。
div::before{
content:’ ';
3.阴影 : 通过设置before,after不同位置,不同旋转角度,要保证伪类的颜色及z-index
div.outer::before,div.outer::after{content:‘’;
z-index:1;
width:50%;
height:3px;
position:absolute;
left:10px;
bottom:7px;
background-color:transparent;
box-shadow:5px 5px 10px rgba(0,0,0,0.5);
-webkit-transform:rotate(-3deg);
4.做出各种图形效果
#star-five:before {
border-bottom: 80px solid red;
border-left: 30px solid transparent;
border-right: 30px solid transparent;
position: absolute;
height: 0;
width: 0;
top: -45px;
left: -65px;
content: ‘’;
transform: rotate(-35deg);
}
#star-five:after {
width: 0;
height: 0;
border-left: 100px solid transparent;
border-right: 100px solid transparent;
border-bottom: 70px solid yellow;
top: 7px;
left: -110px;
position: absolute;
display: block;
content: ‘’;
transform: rotate(-70deg);
}
5.弹性盒
display:flex 设置成弹性盒后,里面的子元素在一行显示
flex-direction:row水平 | row-reverse | column垂直 | column-reverse
flex-wrap:nowrap|wrap|wrap-reverse换行,第一行在下方
flex-flow:是flex-direction和flex-wrap的简写形式。默认值为:row nowrap。
justify-content:flex-start|flex-end|center|space-between|space-around; 水平方向
align-items:flex-start|flex-end|center|baseline|stretch; 垂直方向
flex: order排列顺序|flex-grow放大比例|flex-shrink缩小|flex-basis|flex|align-self
flex属性是flex-grow, flex-shrink 和 flex-basis的简写,默认值为0 1 auto。后两个属性可选 flex:1 flex-grow
align-self: auto | flex-start | flex-end | center | baseline | stretch;
6.css权重的计算
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-dzI5NtJt-1657079140517)(C:\Users\28913\AppData\Roaming\Typora\typora-user-images\1596852700371.png)]
权重叠加 0,0,0,5 + 0,0,0,5 =0,0,0,10 而不是 0,0, 1, 0, 所以不会存在10个div能赶上一个类选择器的情况
继承的权重是0 1)如果选中了,那么以上面的公式来计权重。谁大听谁的。
2) 如果没有选中,那么权重是0,因为继承的权重为0.
当选择器冲突时,权重高的生效;当权重相同时,写在后头的会把前面的覆盖。
7.盒模型
其他技术
1.跨域
(是什么策略的显示,讲一下这个策略,再说一下解决方式)
浏览器的同源策略
浏览器阻止从一个域去获取或者操作另外一个域上的文件
1.后端代理(后端不存在跨域)
2.CORS解决跨域(xhr2)
CORS是一个W3C标准,全称是"跨域资源共享"(Cross-origin resource sharing)。 它允许浏览器向跨源(协议 + 域名 + 端口)服务器,发出 XMLHttpRequest请求,从而克服了AJAX只能同源使用的限制。
需要服务器(提供接口的源码里面)添加下面两句话。
header(‘Access-Control-Allow-Origin:*’);
header(‘Access-Control-Allow-Method:POST,GET’);
3.jsonp(json with padding)----前端常用的
4.其他跨域介绍–nginx(反向代理)
2.WebSocket是否有了解, 和普通接口http有什么区别
socket是传输控制层协议,webSocket是应用层协议
WebSocket protocol 是HTML5一种新的协议。它实现了浏览器与服务器全双工通信(full-duplex)。一开始的握 手需要借助HTTP请求完成。
HTTP请求缺点:会导致过多不必要的请求,浪费流量和服务器资源,每一次请求、应答,都浪费了一定流量在 相同的头部信息上
然而WebSocket的出现可以弥补这一缺点。在WebSocket中,只需要服务器和浏览器通过HTTP协议进行一个握 手的动作,然后单独建立一条TCP的通信通道进行数据的传送。
原理:(webSocket)
WebSocket同HTTP一样也是应用层的协议,但是它是一种双向通信协议,是建立在TCP之上的。
- 浏览器、服务器建立TCP连接,三次握手。这是通信的基础,传输控制层,若失败后续都不执行。
- TCP连接成功后,浏览器通过HTTP协议向服务器传送WebSocket支持的版本号等信息。(开始前的HTTP握手)
- 服务器收到客户端的握手请求后,同样采用HTTP协议回馈数据。
- 当收到了连接成功的消息后,通过TCP通道进行传输通信。
WebSocket与HTTP的关系
相同点:
-
都是一样基于TCP的,都是可靠性传输协议。
- 都是应用层协议
不同点:
- WebSocket是双向通信协议,模拟Socket协议,可以双向发送或接受信息。HTTP是单向的。
- WebSocket是需要握手进行建立连接的。
3.错误调试工具
F12 断点 错误附近输出打印 火狐中的firebug IE开发者工具 Emmet
开发环境
1.eslint规则
**(你怎么修改某些规则)**https://www.jianshu.com/p/f8d2ef372adf
Eslint 是一个JavaScript验证工具,有了它可以让你的编辑器像ide一样进行一些静态的错误提示功能.
npm install eslint -g
某些文件关闭eslint检测 在文件的最顶端加上注释/eslint-disable/
关闭某一行代码的eslint检查 // eslint-disable-next-line
.eslintrc.json配置rules选项
2.git
(版本回退是什么命令,哪个命令查看已删除的提交commitId)
git-reset 版本回退
git reset --hard xxx 回到上一个版本
git reset --soft xxx 该命令将最近一次提交节点的提交记录回退到暂存区
git reset --mixed xxx 是将最近一次提交节点记录回退到工作区
git log 与 git reflog 查看历史记录(被删除的历史commit ID)
git场景问题
提交暂存区git add 出错 git reset HEAD <文件名> 回退
提交本地仓库 git commit出错 : 1.更改 commit 信息:git commit --amend -m“新提交消息” 2.漏提交 : git add missed-file // missed-file 为遗漏提交文件
git commit --amend --no-edit //–no-edit提交消息不会更改
3.git reset --hard commit_id git log查看提交的版本
git revert是提交一个新的版本
git fetch 将远程主机的更新全部放到本地中
git revert 和 git reset 的区别
(1)git revert是用一次新的commit来回滚之前的commit,git reset是直接删除指定的commit。
(2)git revert是用一次逆向的commit“中和”之前的提交 ,合并的时候回滚的变化不会出现
git reset是之间把某些commit在某个branch上删除 合并时候回滚的commit会被引入
3.nodejs,
1.Node不是一个后端语言,但是它可以做类似后端语言的功能
- Node是使用谷歌V8引擎
- Node是js的一个运行环境
- Node具有非阻塞I/O 特点
- Node采用了Common.js规范
node+koa2 node+express
用于快速构建Node.js项目
node.js 是一个基于 Chrome V8 引擎的 JavaScirpt 运行环境。
Node.js使用了一个事件驱动、非阻塞式I/O的模型,使其轻量又高效**
Node.js基于commonjs规范
事件驱动: 任务执行,发布者,订阅者,事件驱动 .
异步(非阻塞): 执行某一个任务的同时也可以执行其他任务
同步(阻塞): 执行某一个任务,这个任务如果没有执行完成,其他任务必须等待
I/O: 输入/输出( 数据库操作,文件系统操作等 ) - 服务器的环境
非阻塞I/O模型: 当使用Node.js来实现数据库操作、文件系统等操作时,要进行的异步操作,异步操作的核心传统实现方式就是回调函数和事件。
Node.js的包管理工具npm
优点:Node.js 使用了一个事件驱动、非阻塞式 I/O 的模型,异步编程,使其轻量又高效。
缺点:单进程,单线程,只支持单核cpu,不能充分的利用多核cpu服务器。一旦这个进程崩掉,那么整个web服务就崩掉了。
内置模块 http 是用于创建一个能够处理和响应 http 响应的服务
fs 用于对系统文件及目录进行读写操作。
path 提供了一些用于处理文件路径的小工具
Url:帮助我们对提交上来的url进行解析处理
querystring 提供用于解析和格式化 URL 查询字符串的工具。qs.parse() qs.stringify()
4.typescript语法
TypeScript具有类型系统,且是JavaScript的超集。 它可以编译成普通的JavaScript代码。 TypeScript支持任意浏览器,任意环境,任意系统并且是开源的。
npm install -g typescript
tsc --init 生成tsconfig.json配置文件
基础数据类型
* number、string、boolean、null 、undefined
* any 表示任意类型
* void 表示空类型
-
内置对象类型
- Array Boolean
- HTMLElement
- HTMLDivElement
-
自定义类型
-
接口 interface
-
类
-
泛型 未来的类型定义的时候不知道是什么类型,调用的时候才知道
-
枚举 enum类型是对JavaScript标准数据类型的一个补充
-
never类型表示的是那些永不存在的值的类型。
readonly :只读属性,不可修改
sex? :表示sex是一个可传属性,可以有也可以没有
[propName: string]: any; 表示新增的属性可以是任意类型
arr3: Array 数组类型定义
arr2: (number | string)[]
fn (a: number, b: number) : number 函数类型定义
-
5.linux命令
pwd:输入pwd命令,Linux会输出当前目录。
ls命令用来查看目录的内容。
cd命令用来改变所在目录。
cat命令可以用来合并文件,也可以用来在屏幕上显示整个文件的内容。
grep命令的最大功能是在一堆文件中查找一个特定的字符串。
touch命令用来创建新文件
cp命令用来拷贝文件
mv命令用来移动文件
rm命令用来删除文件。
mkdir 创建文件夹创建目录
http
1.http2.0,https连接,对称加密非对称加密
https://blog.youkuaiyun.com/qq_33203555/article/details/85211595
http概述:超文本传输协议,是互联网上应用最为广泛的一种网络协议
http的缺点
1.通信使用明文可能会被窃听。
2.不验证通信方的身份可能遭遇伪装。
3.无法证明报文的完整性,可能已遭篡改。
https就是在安全的传输层上发送的http。它在将http报文发送给TCP之前,先将其发送给了一个安全层 ,对其进行加密。http安全层是通过ssl及其现代替代协议TSL来实现的。
https的优点
(1)使用HTTPS协议可认证用户和服务器,确保数据发送到正确的客户机和服务器;
(2)HTTPS协议是由SSL+HTTP协议构建的可进行加密传输、身份认证的网络协议,要比http协议安全,可防止数据在传输过程中不被窃取、改变,确保数据的完整性。
https的缺点
但是https因为加了层ssl,所以在效率方面比较低,会使页面加载的时长延长近50%,也会增加10-20%的耗电。
需要安装证书,在一定基础上增加部署费用,并且报文加密解密对数据传递有一点的效率影响。
http/2.0的目标是改善用户加载页面的时候更快
HTTP/2采用二进制格式而非文本格式
HTTP/2是完全多路复用的,而非有序并阻塞的——只需一个连接即可实现并行
对称密钥加密是指加密和解密使用同一个密钥的方式 , 一方通过密钥将信息加密后,把密文传给另一方,另一方通过这个相同的密钥将密文解密,转换成可以理解的明文
非对称加密是加密和解密使用的是两个不同的密钥,所以这种算法叫作非对称加密算法。指使用一对非对称密钥,即公钥和私钥,公钥可以随意发布,但私钥只有自己知道。发送密文的一方使用对方的公钥进行加密处理,对方接收到加密信息后,使用自己的私钥进行解密。
项目
注册登录是怎么实现的
1.登陆注册要做成受控组件,组件定义state,和表单绑定
2.redux-saga调用数据请求,发送action修改数据,useEffect中dispatch发送数据请求,后端比对用户名是否重复,返回status
3.前端根据返回的信息成功跳转登陆页
4.登陆发送数据请求,数据库对比用户名密码是否正确 根据后端返回的结果进入首页
5.setCookie将用户登录名密码token存cookie中 通过JWT(Json web token)
6.免密登陆 getCookie获取token 发给后端对比 根据返回结果是否自动登陆
7.注册通过Ant Design ,validator中进行表单正则的验证
8.用户体验 注册的时候跳转其他页面的时候给用户提示是否需要跳转,避免因为跳转后导致注册信息没有了 用组件内后置守卫做
如果输入框都没有填信息,不拦截跳转 如果用户输入信息,弹窗提示,点确定,跳转,点取消,不跳转
项目中遇到什么困难,怎么解决的?
1.react中配置二级路由 地址变化 但是界面不更新
使用dva/router中的withRouter高阶组件
2.图表联动怎么实现
我们只需要把当前被选中图表的事件,直接发给其他图表即可,然后判断被选中的图表是哪个作为区分,功能即可实现
onTouchEvent(event) 普通事件传递
3.产品经理要求智能匹配产品
找网上类似功能的网站 查看源码 和主管讨论
-需要一个设计一个投资习惯和风险承受能力测试,
-从后端获取这个客户测试的结果 以及客户平常投资的习惯 生成不同的关键字
-根据关键字从数据库中匹配产品 展示界面
4.后台管理系统遇到遇到什么奇葩的需求
后台管理系统和普通App面向用户的区别
toB和toC的项目
面向企业内部和面向用户的项目的区别
后台管理系统权限比较细 App高并发比较多 做性能优化
a-z0-9 ↩︎