1 Star 0 Fork 0

hjc / hjc0930

加入 Gitee
与超过 1200万 开发者一起发现、参与优秀开源项目,私有仓库也完全免费 :)
免费加入
该仓库未声明开源许可证文件(LICENSE),使用请关注具体项目描述及其代码上游依赖。
克隆/下载
content.json 545.62 KB
一键复制 编辑 原始数据 按行查看 历史
hjc 提交于 2023-06-17 10:52 . Site updated: 2023-06-17 10:52:16
{"posts":[{"title":"HTML笔记","text":"1.HTML XHTML XML的区别 HTML: 超文本标记语言 XHTML:可扩展的超文本标记语言,基于XML,作用与HTML类似,但语法更加严格 XML:可扩展标记语言,主要用于存储数据和结构 XHTML标签名必须手写,元素必须以双标签形式存在,元素必须被正确嵌套,元素必须有根元素 2.HTML5和HTML的区别HTML5是HTML的新标准,其主要目标是无需任何额外的插件就可以传输所有内容,它包括了动画,视频等丰富的图形界面 从文档声明 HTML的文档声明是很长的一段代码,而HTML5 只需要在文档头部使用<!DOCTYPE html>标签即可声明 从语义结构 HTML4.0没有体现语义化的标签,而HTML5加入了很多语义化标签,如header main footer acticle等 3.DOCTYPE标签<!DOCTYPE html>的作用就是让浏览器进入标准模式,使用最新的W3C标准来解析渲染页面,若文档头部不写则浏览器会使用兼容模式来解析和渲染页面 标准模式:指浏览器按照W3C标准解析文档 兼容模式:浏览器通常会为了兼容老旧站点而不使用最新的W3C标准来解析文档 4.块元素 行内元素 行内块元素 块元素 独占一行 可以设置宽高,不设置宽度情况下默认继承父元素的宽度 常见的块元素:div p h1~h6 ul ol table form 行内元素 相邻的元素会排列在同一行 无法设置宽高,其大小由内容决定 可以设置水平方向的margin padding的值,但无法使用auto属性居中 常见的行内元素:span a strong b em i label等 行内块元素 不独占一行 可以设置宽高,默认大小由内容决定 可以设置margin padding等属性,但无法使用auto属性居中 常见的行内块元素:button input img iframe等 5.Link和@import导入样式的区别 link是HTML标签,@import用于CSS文件中导入另一个CSS文件 link标签在页面加载时就会被加载,@import引用会等到页面加载完在加载 link标签权重高于@import引用 @import引用有兼容性问题,而link标签无兼容问题 6.label标签label标签用来定义表单控制间的关系,当用户与该标签发生交互的时候,浏览器会自动对焦到绑定的表单标签上 12<label for="Name">Number:</label><input type=“text“ name="Name" id="Name"/> 7.标签上的title和alt属性的区别title属性用于为该元素设置建议性信息,在鼠标移到该元素上面时会显示 alt属性用于在图片未能正常显示时给予文字说明 8.语义化的好处 便于开发者阅读和写出更加优雅的代码 有利于SEO:让浏览器爬虫更好地解析,爬虫依赖于标签来确定上下文和各个关键字的权重 方便其它设备(如移动设备)解析文档 9.iframe的优缺点 优点 跨域通信 无刷新文件上传 可以用于加载一些第三方图标或广告等 缺点 会阻塞主页面onload事件 无法被一些搜索引擎识别 会产生很多页面,不利于管理 10.src与href的区别 href: 指向网络资源所在的位置,并建立该资源和当前元素(锚点)或当前文档(链接)之间的链路,用于超链接 src:指向外部资源的位置,指向的内容将会嵌入到文档中当前标签所在的位置;在请求src资源时会将其指向的资源下载并应用到文档内,例如js脚本,img图片,iframe等。 11.HTML5新特性 Canvas SVG:用于绘图的元素 video audio:用于播放视频和音频的媒体 Drag,Drop:用于拖拽的元素 Geolocation:用于获取地理位置 LocalStorage SessionStorage:用于本地离线存储 web Worker:运行在后台的JavaScript脚本 webSocket:基于TCP的全双工通信协议 语义化标签:header main footer nav section等 新的表单控件:date time url email search等 12.标准模式和怪异模式的区别 盒模型:标准模式中一个元素的宽高是它内容的宽高,怪异模式下元素的宽高还包含了padding和border 行内元素宽高:标准模式下行内元素无法设置宽高,怪异模式下则可以 水平居中:标准模式下margin: 0 auto可以使元素水平居中,怪异模式下则不行 13.标准盒模型和怪异盒模型HTML中每一个元素都可以看作一个盒模型,一个盒模型由content + padding + border + margin组成 标准盒模型:设置盒模型的width和height属性其实是设置内容的宽高,盒模型的宽度等于width + padding + border + margin 怪异盒模型:设置盒模型的的width和height属性其实是设置了content + padding + border的值。例如设置width为100px,padding为10px,那么此时内容区域的宽度只有80px(100 - 20 * 2) box-sizing:content-box|border-box|inherit 14.前端结构样式和行为分离结构(HTML)相当于人的骨架,样式(CSS)相当于人的装饰,行为(JavaScript)相当于人的动作,前端将这三者分离开,各自负责各自的内容,各部分可以通过引用进行使用 在分离的基础上,我们需要做到代码的精简,重用,有序 分离的好处 代码分离,利于团队的开发和后期的维护; 减少维护成本,提高可读性和更好的兼容性; 15.如何对网站的文件和资源进行优化 文件合并,减少http请求 文件压缩(gzip压缩需要的css和js文件) 使用缓存 使用cdn托管资源 网站外链接优化 meta标签优化,设置title keywords description优化等 16.渐进增强和优雅降级的区别 渐进增强:优先考虑低版本浏览器的兼容,在保证基本功能可以使用的情况下,再考虑对高级浏览器进行效果,交互等方面的优化 优雅降级:一开始就构建完整的功能,然后再针对低版本浏览器进行兼容 17.为什么HTML5不需要DTDHTML5中没有使用SGML或XHTML,不需要参考DTD 18.form表单关闭自动完成(自动联想)功能设置autocomplete=off 19.几种图片格式的区别 png:图片背景透明,可以支持很多颜色 jpg:图片背景不透明,静态图,可压缩 gif:动态图,支持颜色较少 20.meta标签meta标签常用于定义页面的说明,关键字等元数据,这些数据一般服务于浏览器,搜索引擎,并不会直接向用户展示 charset:规定HTML文档的字符编码 http-equiv:一般用于设置一些与http请求头相关的信息,例如content-Type refresh等 X-UA-Compatible:一般用于设置浏览器兼容 keywords:设置网页关键字 description:设置网页的描述内容 viewport:用于移动端的显示优化","link":"/HTML/"},{"title":"高阶函数","text":"前言高阶函数是对其它函数进行操作的函数,可以将它们作为参数或者返回它们;简单来说,高阶函数是一个函数,它接收函数作为参数或将函数作为输出返回 常用的高阶函数memozition缓存函数含义: 缓存函数的作用是将上次的计算结果缓存起来,当下次调用时,如果遇到相同的参数,则取出缓存结果返回 1234567891011const memozition = (fn) => { const cache = Object.create(null); return (...args) => { if(!cache[args]) { cache[args] = fn(...args); } return cache[args] }} 案例: 求斐波那切数列 1234567891011121314151617181920212223242526272829303132// 记录执行的次数let count = 0let fibonacci = function(n) { count++ return n < 2 ? n : fibonacci(n - 1) + fibonacci(n - 2)}for (let i = 0; i <= 10; i++) { fibonacci(i)}console.log(count) // 不使用缓存函数的情况下,函数会被执行453次const memoize = (fn) => { const cache = Object.create(null); return (...args) => { if(!cache[args]) { cache[args] = fn(...args); } return cache[args] }}fibonacci = memoize(fibonacci)for (let i = 0; i <= 10; i++) { fibonacci(i)}console.log(count) // 使用缓存函数的情况,函数会被调用12次 缓存函数能应付大量重复计算,或者大量依赖之前的结果的运算场景 curry柯里化函数含义: 在数学和计算机科学中,柯里化是一种将使用多个参数的一个函数转换成一些列使用一个参数的函数技术(把接受多个参数的函数转换成几个单一参数的函数) 示例: 123456789101112131415// 没有柯里化的函数function girl(name,age,single) { return `${name}${age}${single}`}girl('张三',180,'单身')// 柯里化的函数function girl(name) { return function (age){ return function (single){ return `${name}${age}${single}` } }}girl('张三')(180)('单身') 检测字符串是否有空格: 1234567891011121314// 封装函数去检测let matching = (reg, str) => reg.test(str)matching(/\\s+/g, 'hello world') // truematching(/\\s+/g, 'abcdefg') // false// 柯里化let curry = reg => { return str => { return reg.test(str) }}let hasSpace = curry(/\\s+/g)hasSpace('hello word') // truehasSpace('abcdefg') // false 偏函数含义: 固定函数的某一个或几个参数,返回一个新的函数来接收剩下的变量参数","link":"/%E9%AB%98%E9%98%B6%E5%87%BD%E6%95%B0/"},{"title":"React State","text":"前言state,可以理解为组件的内存,React项目中UI的改变来源于state的改变,在目前React的多种模式下,state的更新会有不同的表现,本文主要是让大家了解React更新流程,以及归纳总结了类组件setState和函数组件useState的诸多细节问题 类组件中的state类组件我们使用setState方法来合并更新state,基本用法如下: 1setState(obj, callback) 第一个参数:当 obj 为一个对象,则为即将合并的 state;如果 obj 是一个函数,那么当前组件的 state 和 props 将作为参数,返回值用于合并新的 state。 第二个参数 callback:callback 为一个函数,函数执行上下文中可以获取当前 setState 更新后的最新 state 的值,可以作为依赖 state 变化的副作用函数,可以用来做一些基于 DOM 的操作。 1234567891011121314/* 第一个参数为function类型 */this.setState((state,props)=>{ return { number:1 }})/* 第一个参数为object类型 */this.setState( { number:1 }, ()=>{ console.log(this.state.number) //获取最新的number }) 首先,setState会产生当前更新的优先级(老版本用expirationTime ,新版本用lane)。 接下来React会从fiber Root根部fiber向下调和子节点,调和阶段将对比发生更新的地方,更新对比expirationTime,找到发生更新的组件,合并state,然后触发render函数,得到新的UI视图层,完成render阶段。 接下来到commit阶段,commit阶段,替换真实DOM,完成此次更新流程。 此时仍然在commit阶段,会执行setState中callback函数,如上的()=>{ console.log(this.state.number) },到此为止完成了一次setState全过程。 请记住一个主要任务的先后顺序,这对于弄清渲染过程可能会有帮助: render阶段render函数执行 -> commit阶段真实DOM替换 -> setState回调函数执行callback。 类组件如何限制state更新视图对于类组件如何限制 state 带来的更新作用的呢? pureComponent 可以对 state 和 props 进行浅比较,如果没有发生变化,那么组件不更新 shouldComponentUpdate 生命周期可以通过判断前后 state 变化来决定组件需不需要更新,需要更新返回true,否则返回false setState原理揭秘setState底层实际上是调用了类组件在初始化时绑定的了负责更新的Updater对象,对于如果调用 setState 方法,实际上是 React 底层调用Updater对象上的enqueueSetState方法。 12345678910enqueueSetState(){ /* 每一次调用`setState`,react 都会创建一个 update 里面保存了 */ const update = createUpdate(expirationTime, suspenseConfig); /* callback 可以理解为 setState 回调函数,第二个参数 */ callback && (update.callback = callback) /* enqueueUpdate 把当前的update 传入当前fiber,待更新队列中 */ enqueueUpdate(fiber, update); /* 开始调度更新 */ scheduleUpdateOnFiber(fiber, expirationTime);} batchUpdate的更新时机正常 state 更新、UI 交互,都离不开用户的事件,比如点击事件,表单输入等,React 是采用事件合成的形式,每一个事件都是由 React 事件系统统一调度的,那么 State 批量更新正是和事件系统息息相关的 12345/* 在`legacy`模式下,所有的事件都将经过此函数同一处理 */function dispatchEventForLegacyPluginEventSystem(){ // handleTopLevel 事件处理函数 batchedEventUpdates(handleTopLevel, bookKeeping);} 其中batchedEventUpdates函数就是处理批量更新的关键函数 123456789101112function batchedEventUpdates(fn,a){ /* 开启批量更新 */ isBatchingEventUpdates = true; try { /* 这里执行了的事件处理函数, 比如在一次点击事件中触发setState,那么它将在这个函数内执行 */ return batchedEventUpdatesImpl(fn, a, b); } finally { /* try 里面 return 不会影响 finally 执行 */ /* 完成一次事件,批量更新 */ isBatchingEventUpdates = false; } } 123456789101112function batchedEventUpdates(fn, a) { /* 开启批量更新 */ isBatchingEventUpdates = true; try { /* 这里执行了的事件处理函数, 比如在一次点击事件中触发setState,那么它将在这个函数内执行 */ return batchedEventUpdatesImpl(fn, a, b); } finally { /* try 里面 return 不会影响 finally 执行 */ /* 完成一次事件,批量更新 */ isBatchingEventUpdates = false; }} 如上可以分析出流程,在 React 事件执行之前通过 isBatchingEventUpdates=true 打开开关,开启事件批量更新,当该事件结束,再通过 isBatchingEventUpdates = false; 关闭开关,然后在 scheduleUpdateOnFiber 中根据这个开关来确定是否进行批量更新。 而在异步回调函数中,这个规则会被打破。但可以通过unstable_batchedUpdates方法,手动指定要批量更新的setState。 12345678910setTimeout(()=>{ unstable_batchedUpdates(()=>{ this.setState({ number:this.state.number + 1 }) console.log(this.state.number) this.setState({ number:this.state.number + 1}) console.log(this.state.number) this.setState({ number:this.state.number + 1 }) console.log(this.state.number) })}) 函数组件中的stateReact-hooks 正式发布以后, useState 可以使函数组件像类组件一样拥有 state,也就说明函数组件可以通过 useState 改变 UI 视图。那么 useState 到底应该如何使用,底层又是怎么运作的呢,首先一起看一下useState。 useState用法1const [state, dispatch] = useState(initState) state,目的提供给 UI ,作为渲染视图的数据源。 dispatch 改变 state 的函数,可以理解为推动函数组件渲染的渲染函数。 initData 有两种情况,第一种情况是非函数,将作为 state 初始化的值。 第二种情况是函数,函数的返回值作为 useState 初始化的值。 对于dispatch也有两种情况: 第一种非函数情况,此时将作为新的值,赋予给state,作为下一次渲染使用; 第二种是函数的情况,如果dispatch的参数为一个函数,这里可以称它为reducer,reducer参数,是上一次返回最新的state,返回值作为新的state。 监听state变化类组件 setState 中,有第二个参数 callback 或者是生命周期componentDidUpdate 可以检测监听到 state 改变或是组件更新。 那么在函数组件中,如何怎么监听 state 变化呢?这个时候就需要 useEffect 出场了,通常可以把 state 作为依赖项传入 useEffect 第二个参数 deps ,但是注意 useEffect 初始化会默认执行一次。 useState注意事项在使用 useState 的 dispatchAction 更新 state 的时候,记得不要传入相同的 state,这样会使视图不更新。比如下面这么写: 123456789101112export default function Index(){ const [state, dispatchState] = useState({ name:'alien' }); const handleClick = ()=>{ // 点击按钮,视图没有更新。 state.name = 'Alien' dispatchState(state) // 直接改变 `state`,在内存中指向的地址相同。 } return <div> <span>{ state.name }</span> <button onClick={handleClick}>changeName++</button> </div>} setState和useState的异同相同点 setState和 useState 更新视图,底层都调用了 scheduleUpdateOnFiber 方法,而且事件驱动情况下都有批量更新规则。 不同点 在不是 pureComponent 组件模式下, setState 不会浅比较两次 state 的值,只要调用 setState,在没有其他优化手段的前提下,就会执行更新。但是 useState 中的 dispatchAction 会默认比较两次 state 是否相同,然后决定是否更新组件。 setState 有专门监听 state 变化的回调函数 callback,可以获取最新state;但是在函数组件中,只能通过 useEffect 来执行 state 变化引起的副作用。 setState 在底层处理逻辑上主要是和老 state 进行合并处理,而 useState 更倾向于重新赋值。","link":"/React/React%20State/"},{"title":"React Component","text":"前言在React中,一切皆组件,React的组件可以分为两种,类组件和函数式组件,它们本质上是类和函数,同样拥有原型链,继承,静态属性等特性;但与常规的类和函数不同的是,React组件承载了渲染视图的UI和更新视图的setState、useState 等方法。 React对组件处理的流程在React调和渲染fiber节点的时候,如果发现fiber tag是ClassComponet=1,则按照类组件逻辑处理,如果是FunctionComponet=0则按照函数组件逻辑处理。当然React也提供了一些内置的组件,比如说Suspense、Profiler等。 类组件的执行,是在react-reconciler/src/ReactFiberClassComponent.js中: 12345678function constructClassInstance( workInProgress, // 当前正在工作的 fiber 对象 ctor, // 我们的类组件 props // props){ /* 实例化组件,得到组件实例 instance */ const instance = new ctor(props, context)} 函数式组件的执行,是在react-reconciler/src/ReactFiberHooks.js中: 1234567891011function renderWithHooks( current, // 当前函数组件对应的 `fiber`, 初始化 workInProgress, // 当前正在工作的 fiber 对象 Component, // 我们函数组件 props, // 函数组件第一个参数 props secondArg, // 函数组件其他参数 nextRenderExpirationTime, //下次渲染过期时间){ /* 执行我们的函数组件,得到 return 返回的 React.element对象 */ let children = Component(props, secondArg);} 类组件类组件执行执行构造函数过程中会在实例上绑定props和context,初始化置空的refs属性,原型链上绑定setState、forceUpdate方法。对于updater,React在实例化组件之后会单独绑定update对象。 1234567891011121314function Component(props, context, updater) { this.props = props; //绑定props this.context = context; //绑定context this.refs = emptyObject; //绑定ref this.updater = updater || ReactNoopUpdateQueue; //上面所属的updater 对象}/* 绑定setState 方法 */Component.prototype.setState = function(partialState, callback) { this.updater.enqueueSetState(this, partialState, callback, 'setState');}/* 绑定forceupdate 方法 */Component.prototype.forceUpdate = function(callback) { this.updater.enqueueForceUpdate(this, callback, 'forceUpdate');} 类组件的主要组成部分 12345678910111213141516class Index extends React.Component{ constructor(...arg){ super(...arg) /* 执行 react 底层 Component 函数 */ } state = {} /* state */ static number = 1 /* 内置静态属性 */ handleClick= () => console.log(111) /* 方法: 箭头函数方法直接绑定在this实例上 */ componentDidMount(){ /* 生命周期 */ console.log(Index.number,Index.number1) // 打印 1 , 2 } render(){ /* 渲染函数 */ return <div style={{ marginTop:'50px' }} onClick={ this.handerClick } >hello,React!</div> }}Index.number1 = 2 /* 外置静态属性 */Index.prototype.handleClick = ()=> console.log(222) /* 方法: 绑定在 Index 原型链的 方法*/ 函数组件函数组件最开始又称无状态组件,在React v16.8之前只能渲染一些纯UI视图,或者通过context获取共享的状态 123456function Index(){ console.log(Index.number) // 打印 1 const [ message , setMessage ] = useState('hello,world') /* hooks */ return <div onClick={() => setMessage('let us learn React!') } > { message } </div> /* 返回值 作为渲染ui */ } Index.number = 1 /* 绑定静态属性 */ 通过上面的源码我们知道,React底层在执行函数组件时,是直接采用执行函数的方法,不是采用new的方法,因此不要在函数组件的原型链上绑定属性和方法。 类组件和函数组件的本质区别 类组件底层只需要实例化一次,实例中保存了组件的state等状态,对于每一次更新只需要调用render方法以及对于的生命周期就可以了 函数组件每一次更新都是一个新的函数执行,里面的变量都会重新声明,所以才有了React Hooks,用于保存函数组件中的状态,执行一些副作用钩子等。 组件通信方式React组件通信方式主要有以下五种: props和callback方式 ref方式 context上下文方式 Redux、Mobx等状态管理的方式 Event Bus事件总线 组件强化方式 类组件继承 React的类组件,有着良好的继承性,可以先针对一些基础组件,实现一部分功能,再针对项目要求进行改造,强化,添加额外功能 1234567891011121314151617181920212223242526272829303132/* 人类 */class Person extends React.Component{ constructor(props){ super(props) console.log('hello , i am person') } componentDidMount(){ console.log(1111) } eat(){ /* 吃饭 */ } sleep(){ /* 睡觉 */ } ddd(){ console.log('打豆豆') /* 打豆豆 */ } render(){ return <div> 大家好,我是一个person </div> }}/* 程序员 */class Programmer extends Person{ constructor(props){ super(props) console.log('hello , i am Programmer too') } componentDidMount(){ console.log(this) } code(){ /* 敲代码 */ } render(){ return <div style={ { marginTop:'50px' } } > { super.render() } { /* 让 Person 中的 render 执行 */ } 我还是一个程序员! { /* 添加自己的内容 */ } </div> }}export default Programmer 函数组件自定义Hooks HOC高阶组件","link":"/React/react%E7%BB%84%E4%BB%B6/"},{"title":"text-transform","text":"text-transform属性指定如何将元素的文本设置为大写,可以全部大写或全部小写,也可以单独对每一个单词进行操作 属性 含义 capitalize 首个字符大写 uppercase 所有字符大写 lowercase 所有字符小写 none 禁止转换 full-width 按照一般东亚文字(如中文或日文)对齐","link":"/text-transform/"},{"title":"work-break","text":"work-break用于设置单词在行内的断行行为 123456789/* 关键字值 */word-break: normal;word-break: break-all; /* 允许在单词内换行 */word-break: keep-all; /* 只能在半角空格或连字符处换行 *//* 全局值 */word-break: inherit;word-break: initial;word-break: unset;","link":"/word-break/"},{"title":"面试小结(20230410)","text":"前言依然还是遇到的面试题总结。 一共三轮面试,第一轮是笔试;第二轮是技术面;第三轮基本上是和领导以及开发的小伙伴聊天,还是很愉快的~ 第二轮技术面主要是针对一面的笔试题和简历上的内容进行展开提问 题目(不分顺序先后) TS和JS的区别,什么情况下是JS可以实现而TS不能实现的 TS是JS的超集,包含了JS所有的语法,除原生开发只能使用JS外,工程化的脚手架中的功能均可以使用TS进行实现 你的笔试源码中报了一个TS异常,这是为什么?如何解决? 不允许隐式any类型,应该是tsconfig设置了严格类型导致的,将它关闭,或者显示指定变量的类型即可 你的笔试源码中有做了前端本地Table表格导出Excel数据到本地,这样写合适吗? 前端JS做Excel导出会有性能问题,解决方案有以下几种: 生成表格交给后端去做 将生成Excel函数的执行过程交给Web Worker去做 利用requestAnimationFrame/requestIdleCallback对生成Excel的函数进行分片执行 你的笔试源码中有使用了less当作变量进行引入,它是用来做什么的? CSS Module,主要是用来做变量隔离,防止组件的变量污染全局变量 CSS Module有什么优缺点?是否会影响到页面的性能呢? 需要额外的脚手架配置,目前没有遇到性能问题 如果一个组件中引用了同一个样式选择器,那CSS Module会不会生成不同的类名 不会 除了CSS Module,你还了解哪些样式隔离方案 css in js 你的简历中有提到一个性能优化的场景,展开讲讲 还是那个图表的问题~ 项目中组件懒加载是通过什么实现的? 使用IntersectionObserver异步监听元素是否出现在可视范围内 对页面性能的指标的了解 你的简历上写了了解React18,可以说说它的新特性吗? 更强的并发渲染机制,提供了一些Hooks让我们手动指定渲染,如useTransition、startTransition 更完善的状态更新批处理机制 SSR流式渲染,将HTML进行分段渲染 React18对你现在项目开发有哪些帮助呢? 目前应该新的批处理机制影响比较大 跨域的解决方案 cors proxy nginx反向代理 JSONP … 有阅读过Antd的源码吗? 了解过 三面 你有做过职业规划?或者短期内的一些打算吗? 你对开源技术怎么看? 说说你在写项目时给你带来了哪些成长? 总结四月十号的一场面试,总体来说聊的过程还是很愉快的,期间也发现了很多自己的不足,后面也会再接再厉提升自己!","link":"/%E9%9D%A2%E8%AF%95%E5%B0%8F%E7%BB%93(20230410)%20/"},{"title":"WebSocket","text":"1.Websocket Websocket是一种基于TCP的全双工通讯协议,现在的业务场景中可能会遇到一些需要实时请求数据去刷新页面的需求,在ws协议出来之前,我们常用的方式有三种,轮询,长轮询和数据流,但这三种方式都十分的占用服务器资源。WebSocket协议基于TCP,只要一次握手后,就可以保持连接不断开,并且服务端可以主动发送数据给客户端,减轻了服务器的压力 2.特点 建立在TCP协议的基础上,双向通信,服务端也可以主动向客户端推送数据 兼容HTTP协议,在握手时使用的就是HTTP协议 数据格式轻量,可以发送文本,也可以发送二进制数据 没有同源限制 3.通信原理WebSocket在请求阶段使用的是HTTP协议,在协议的请求头中,会有两个标识,一是客户端标识key,二是Upgrade字段,表示告诉服务器要进行协议的升级。服务器在拿到请求头后会进行判断,如果支持,则会在响应头中,带上需要升级的Upgrade和客户端标识key","link":"/JavaScript/27.websocket/"},{"title":"防抖和节流","text":"防抖所谓防抖,就是指触发事件后 n 秒后才执行函数,如果在 n 秒内又触发了事件,则会重新计算函数执行时间。 12345678910const debounce = (fn, wait) => { let timer = 0; return (args) => { if(timer) window.clearTimeout(timer); timer = window.setTimeout(() => { fn(args); }, wait) }} 节流所谓节流,就是指连续触发事件但是在 n 秒中只执行一次函数。 节流会稀释函数的执行频率。 123456789101112const throttle = (fn, wait) => { let timer = 0; return (args) => { if(timer) return; timer = window.setTimeout(() => { fn(args); timer = 0; }, wait) }}","link":"/JavaScript/5.%E9%98%B2%E6%8A%96%E5%92%8C%E8%8A%82%E6%B5%81/"},{"title":"JavaScript中的数据类型","text":"前言在JavaScript中,数据类型可以分为两种: 基本类型 复杂类型 两种类型的区别是:存储位置不同 一、基本类型基本类型主要为以下6种: Number String Boolean Symbol BigInt Null Undefined Number数值最常见的整数类型格式为十进制,也可以设置八进制(零开头)和十六进制(0x开头) 123let intNum = 55;let num1 = 070;let hexNum1 = 0xA; 浮点类型则在数值汇总必须包含小数点,还可通过科学计数法表示 1234let floatNum1 = 1.1;let floatNum2 = 0.1;let floatNum3 = .1; // 有效,但不推荐let floatNum = 3.125e7; // 等于31250000 在数值类型中,存在一个特殊值NaN,意为“不是数值”,用于表示本来要返回数值的操作失败了(而不是抛出错误) 12console.log(0/0) // NaNconsole.log(-0/+0) // NaN Undefinedundefined类型只有一个值,就是特殊值undefined。当使用code或let声明了变量但没有初始化时,就相当于给变量赋予了undefined值 12let message;console.log(message === undefined); 包含undefined值的变量跟未定义变量是有区别的 1234let message; // 这个变量被声明了,只是值为undefinedconsole.log(message);console.log(age) // 没有声明这个变量,报错 String字符串可以使用双引号(“)、单引号(‘)或反引号(`)标示 123let firstName = "John";let lastName = "Jacob";let lastName = `Jingleheimerschmidt`; 字符串是不可变的,意思是一旦创建,它们的值就不能变了 12let lang = "Java";lang = lang + "Script" // 先创建再销毁 NullNull类型同样只有一个值,即特殊值null 逻辑上讲,null值表示一个空指针对象,这也是给typeof传一个null会返回object的原因 12let car = null;console.log(typeof car) // object undefined值是由null值派生而来 1console.log(null == undefined); 只要变量保存对象,而当时又没有那个对象可保存,就可以用 null 填充该变量 Boolean布尔类型有两个字面值:true和 false,通过 Boolean 可以将其它类型的数据转化为布尔值 规则如下: 数据类型 转为true 转为false String 非空字符串 “” Number 非零数值 0、NaN Object 任意对象 N/A null N/A null Undefined N/A undeinfed SymbolSymbol(符号)是原始值,且符号实例是唯一、不可变的。符号的用途是确保对象属性使用唯一标识,不会发生属性冲突的危险 1234567let genericSymbol = Symbol();let otherGenericSymbol = Symbol();console.log(genericSymbol == otherGenericSymbol); // falselet fooSymbol = Symbol('foo');let otherFooSymbol = Symbol('foo');console.log(fooSymbol == otherFooSymbol); // false 二、引用类型复杂类型统称为 Object,我们这里主要讲述下面三种: Object Array Function Object创建object常用方式为对象字面量表示法,属性名可以是字符串或数值 12345let person = { name: "Nicholas", age: 29, 5: true} ArrayJavaScript数组是一组有序的数据,但跟其他语言不同的是,数组中每个槽位可以存储任意类型的数据,并且,数组也是动态大小的,会随着数据添加而自动增长 12let colors = ['red', 2, {age: 20}]colors.push(2) Function函数实际是对象,每个函数都是 Function 类型的实例,而 Function 也有属性和方法,跟其他引用类型一样 函数存在三种常见的表达方式: 函数声明 123function sum(num1, num2) { return num1 + num2;} 函数表达式 123const sum = function(num1, num2) { return num1 + num2;} 箭头函数 123const sum = (num1, num2) => { return num1 + num2;} 其他引用类型除了上述说的三种之外,还包括 Date、RegExp、Map、Set等…… 三、存储区别基本数据类型和引用数据类型存储在内存中的位置不同: 基本数据类型存储在栈中 引用类型的对象存储于堆中 当我们把变量赋值给另一个变量时,解析器首先要确认的就是这个值是基本类型还是引用类型 举个例子 基本类型1234let a = 10;let b = a;b = 20;console.log(a); // 10值 a的值为一个基本类型,是存储在栈中,将 a的值赋给 b,虽然两个变量的值相等,但是两个变量保存了两个不同的内存地址 引用类型123456let obj1 = {};let obj2 = obj1;obj2.name = 'xxx';console.log(obj1.name); // xxx 引用类型数据存放在堆中,每个堆内存对象都有对应的引用地址指向它,引用地址存放在栈中。 obj1是一个引用类型,在赋值操作过程中,实际是将堆内存对象在栈内存的引用地址复制了一份给了 obj2,实际上它们共同指向同一个堆内存对象,所以更改 obj2会对 obj1 产生影响 小结 声明变量时不同的内存地址分配: 简单类型的值存放在栈内存中 引用类型的值存放在堆内存中,栈内存中存放的是指向堆内存的地址 不同的类型数据导致赋值变量方式不同: 简单类型赋值,是生成相同的值,两个对象对应不同的地址 引用类型赋值,是将保存对象的内存地址赋值给另一个变量。也就是两个变量指向堆内存中同一个对象","link":"/JavaScript/JavaScript%E6%95%B0%E6%8D%AE%E7%B1%BB%E5%9E%8B%E5%92%8C%E5%AD%98%E5%82%A8%E4%B8%8A%E7%9A%84%E5%B7%AE%E5%BC%82/"},{"title":"闭包","text":"1.作用域 在JavaScript中,变量的可访问性由作用域管理。作用域由函数或代码块创建。变量只能在定义它的函数或代码块内使用。超出范围则不可访问 变量隔离 我们可以把作用域理解为一种空间策略,每个函数拥有自己的私有作用域,它对变量进行了隔离,控制了变量的可访问性,因此不同的作用域可以具有相同名称的变量而不冲突。 作用域嵌套 我们可以把innerFunc()嵌套在外部函数outerFunc()中,外部作用域中的outerVar变量在内部作用域中是可访问的。 小结 作用域可以嵌套 外部作用域的变量可以在内部作用域内部访问 如果外部作用域中也找不到,最终会去全局作用域中查找,从内至外层层查找形成作用域链 扩展: 我们把内部作用域可以访问到外部作用域,而外部无法访问内部的行为叫做作用域继承,可以把全局作用域比喻成一个大房子,里面有很多层小房间(函数作用域或者块作用域),各自拥有自己的钥匙🔑。可以从里面访问外面,但是无法从外面访问里面,并且相互之间无法访问 2.闭包示例: 1234567891011function outerFunc() { let outerVar = 'I am outside!'; function innerFunc() { console.log(outerVar); // "I am outside!" } innerFunc();}outerFunc(); 现在我们知道在innerFunc()作用域内可以从词法作用域访问变量outerVar,这里innerFunc()调用发生在其词法作用域(outerFunc()的作用域)内。 把innerFunc()修改到其词法作用域之外(outerFunc()之外)调用。innerFunc()还能访问outerVar吗? 让我们对代码片段进行调整: 123456789101112function outerFunc() { let outerVar = 'I am outside!'; function innerFunc() { console.log(outerVar); // "I am outside!" } return innerFunc;}const myInnerFunc = outerFunc();myInnerFunc(); 现在innerFunc()在其词法作用域之外执行,但是innerFunc()仍然可以从其词法作用域访问outerVar,即使是在词法作用域之外执行。也就是说innerFunc()从其词法作用域捕获(又称记忆)变量outerVar。 换句话说,innerFunc()是一个闭包,因为它在词法作用域内捕获了变量outerVar。 正常来说,当outerFunc函数执行完毕之后,其作用域是会被销毁的,然后垃圾回收器会释放那段内存空间。而闭包却很神奇的将outerFunc的作用域存活了下来,innerFunc依然持有该作用域的引用,这个引用就是闭包。 闭包是一个函数,它从定义它的地方记住变量,形成一个私有的作用域,保护里边的私有变量不受外界的干扰,除了保护私有变量外,还可以存储一些内容,而不管它以后在哪里执行,所以无论通过哪种方式将内部的函数传递到所在的词法作用域以外,它都会持有对原始作用域的引用,无论在何处执行这个函数都会使用闭包。 小结某个函数在定义时的词法作用域之外的地方被调用,闭包可以使该函数极限访问定义时的词法作用域。 扩展:通俗地讲闭包就是在一个函数里边再定义一个函数。这个内部函数一直保持有对外部函数中作用域的访问权限(小房间一直可以有大房子的访问权限)。 3.闭包的作用 访问其他函数内部变量 保护变量不被内存回收机制回收 避免全局变量被污染 方便调用上下文的局部变量 加强封装性 4.总结作用域决定了JavaScript中变量的可访问性。主要包括函数作用域和块作用域。 词法作用域允许函数作用域从外部作用域静态访问变量。 最后,闭包是从其词法作用域捕获变量的函数。用简单的话来说,闭包会记住从定义它的地方开始的变量,无论它在哪里执行。 闭包捕获事件处理程序,回调中的变量。它们用于函数式编程。","link":"/JavaScript/16.%E5%87%BD%E6%95%B0%E7%9A%84%E9%97%AD%E5%8C%85/"},{"title":"事件循环机制","text":"1.JavaScript执行机制 同步:在执行到同步代码时,会按照顺序执行直接输出 异步:在执行到异步代码时,会将异步代码的回调函数存入事件队列中,等所有同步代码执行完成后再执行异步代码 1234567891011console.log(1) // 同步setTimeout(() => { console.log(2) // 异步}, 2000); // 注意延迟的时间console.log(3) // 同步setTimeout(() => { console.log(4) // 异步}, 0);console.log(5) // 同步// 输出 : 1 3 5 4 2 2.宏任务&&微任务异步任务分为宏任务和微任务,微任务执行时机先于宏任务 宏任务 # 浏览器 Node I/O ✅ ✅ setTimeout ✅ ✅ setInterval ✅ ✅ setImmediate ❌ ✅ requestAnimationFrame ✅ ❌ 微任务 # 浏览器 Node Promise.prototype.then catch finally ✅ ✅ process.nextTick ❌ ✅ MutationObserver ✅ ❌ 执行流程 3.例子12345678910console.log(1) // 同步setTimeout(() => { console.log(2) // 异步:宏任务});console.log(3) // 同步Promise.resolve().then(()=>{ // 异步:微任务 console.log(4)})console.log(5) // 同步// 1 3 5 4 2 例11234567891011121314151617181920console.log(1)setTimeout(() => { console.log(2) Promise.resolve().then(() => { console.log(3) })});console.log(4)new Promise((resolve,reject) => { console.log(5) resolve()}).then(() => { console.log(6) setTimeout(() => { console.log(7) })})console.log(8)// 1 4 5 8 6 2 3 7 例2123456789101112131415setTimeout(() => { console.log(1)}, 0)console.log(2)const p = new Promise((resolve) => { console.log(3) resolve()}).then(() => { console.log(4)}).then(() => { console.log(5)})console.log(6)// 2 3 6 4 5 1 遇到Promise.then.then这种时,如果有点懵逼的同学,可以转换一下 12345678910111213141516setTimeout(() => { // 异步:宏任务 setTimeout console.log(1)}, 0)console.log(2) // 同步const p = new Promise((resolve) => { // p 是 then1 执行返回的新 Promise console.log(3) // 同步 resolve()}).then(() => { // 异步:微任务 then1 console.log(4) // 拿着 p 重新 then p.then(() => { // 异步:微任务 then2 console.log(5) })})console.log(6) // 同步 6// 2 3 6 4 5 1 例312345678910111213141516171819202122232425262728293031323334353637new Promise((resolve,reject)=>{ console.log(1) resolve()}).then(()=>{ console.log(2) new Promise((resolve,reject)=>{ console.log(3) resolve() }).then(()=>{ console.log(4) }).then(()=>{ console.log(5) })}).then(()=>{ console.log(6)})// 转换后的代码const p1 = new Promise((resolve,reject)=>{ console.log(1) resolve()}).then(()=>{ console.log(2) const p2 = new Promise((resolve,reject)=>{ console.log(3) resolve() }).then(()=>{ console.log(4) p2.then(()=>{ console.log(5) }) }) p1.then(()=>{ console.log(6) })})// 1 2 3 4 6 5 例412345678910111213141516171819new Promise((resolve, reject) => { console.log(1) resolve()}).then(() => { console.log(2) // 多了个return return new Promise((resolve, reject) => { console.log(3) resolve() }).then(() => { console.log(4) }).then(() => { // 相当于return了这个then的执行返回Promise console.log(5) })}).then(() => { console.log(6)})// 1 2 3 4 5 6 例512345678910111213141516171819202122232425new Promise((resolve, reject) => { console.log(1) resolve()}).then(() => { console.log(2) new Promise((resolve, reject) => { console.log(3) resolve() }).then(() => { console.log(4) }).then(() => { console.log(5) })}).then(() => { console.log(6)})new Promise((resolve, reject) => { console.log(7) resolve()}).then(() => { console.log(8)})// 1 7 2 3 8 4 6 5 例61234567891011121314151617181920async function async1() { console.log(1); await async2(); console.log(2);}async function async2() { console.log(3);}console.log(4);setTimeout(function () { console.log(5);});async1()new Promise(function (resolve, reject) { console.log(6); resolve();}).then(function () { console.log(7);});console.log(8); 转换后的代码 12345678910111213141516171819202122console.log(4);setTimeout(function () { console.log(5);});console.log(1);new Promise((resolve, reject) => { console.log(3);}).then(() => { console.log(2);})new Promise(function (resolve, reject) { console.log(6); resolve();}).then(function () { console.log(7);});console.log(8);// 4 1 3 6 8 2 7 5 例7123456789101112131415161718192021222324252627282930313233343536373839async function async1() { console.log(1); await async2(); console.log(2);}async function async2() { console.log(3);}new Promise((resolve, reject) => { setTimeout(() => { resolve() console.log(4) }, 1000);}).then(() => { console.log(5) new Promise((resolve, reject) => { setTimeout(() => { async1() resolve() console.log(6) }, 1000) }).then(() => { console.log(7) }).then(() => { console.log(8) })}).then(() => { console.log(9)})new Promise((resolve, reject) => { console.log(10) setTimeout(() => { resolve() console.log(11) }, 3000);}).then(() => { console.log(12)}) 转换后 123456789101112131415161718192021222324252627282930313233343536373839new Promise((resolve, reject) => { setTimeout(() => { resolve() console.log(4) }, 1000);}).then(() => { console.log(5) new Promise((resolve, reject) => { setTimeout(() => { console.log(1); new Promise((resolve, reject) => { console.log(3); resolve(); }).then(() => { console.log(2); }) resolve() console.log(6) }, 1000) }).then(() => { console.log(7) }).then(() => { console.log(8) })}).then(() => { console.log(9)})new Promise((resolve, reject) => { console.log(10) setTimeout(() => { resolve() console.log(11) }, 3000);}).then(() => { console.log(12)})// 10 4 5 9 1 3 6 2 7 8 11 12 例8123456789101112131415161718192021222324252627282930async function async1() { console.log('async1 start') await async2() console.log('async1 end')}async function async2() { console.log('async start') return new Promise((resolve, reject) => { resolve() console.log('async2 promise') })}console.log('script start')setTimeout(() => { console.log('setTimeout')}, 0);async1()new Promise((resolve) => { console.log('promise1') resolve()}).then(() => { console.log('promise2')}).then(() => { console.log('promise3')})console.log('script end') 转换后 1234567891011121314151617181920212223242526272829303132333435363738console.log('script start')setTimeout(() => { console.log('setTimeout')}, 0);console.log('async1 start')new Promise((resolve,reject) => { console.log('async start') return new Promise((resolve, reject) => { resolve() console.log('async2 promise') })}).then(() => { console.log('async1 end')})new Promise((resolve) => { console.log('promise1') resolve()}).then(() => { console.log('promise2')}).then(() => { console.log('promise3')})console.log('script end')// script start// async1 start// async start// async2 promise// promise1// script end// promise2// promise3// async1 end// setTimeout 123456789101112131415161718Promise.resolve().then(() => { console.log(0); return Promise.resolve(4);}).then((res) => { console.log(res)})Promise.resolve().then(() => { console.log(1);}).then(() => { console.log(2);}).then(() => { console.log(3);}).then(() => { console.log(5);}).then(() =>{ console.log(6);})","link":"/JavaScript/3.%E4%BA%8B%E4%BB%B6%E5%BE%AA%E7%8E%AF/"},{"title":"Math的扩展","text":"1.Math.trunc()用于去除一个数值的小数部分,返回整数部分 12345Math.trunc(4.1) // 4Math.trunc(4.9) // 4Math.trunc(-4.1) // -4Math.trunc(-4.9) // -4Math.trunc(-0.1234) // -0 对于非数值,会先调用Number方法将其转为数值 123456789Math.trunc('123.456') // 123Math.trunc(true) //1Math.trunc(false) // 0Math.trunc(null) // 0Math.trunc(NaN); // NaNMath.trunc('foo'); // NaNMath.trunc(); // NaNMath.trunc(undefined) // NaN 对于没有部署这个方法的环境,可以用下面的代码模拟。 123Math.trunc = Math.trunc || function(x) { return x < 0 ? Math.ceil(x) : Math.floor(x);}; 2.Math.sign()用来判断一个数到底是正数,负数,还是零。对于非数值,会先将其转换为数值 它会返回五种值 参数为正数,返回+1; 参数为负数,返回-1; 参数为 0,返回0; 参数为-0,返回-0; 其他值,返回NaN。 12345Math.sign(-5) // -1Math.sign(5) // +1Math.sign(0) // +0Math.sign(-0) // -0Math.sign(NaN) // NaN 如果参数是非数值,会自动转为数值。对于那些无法转为数值的值,会返回NaN。 12345678Math.sign('') // 0Math.sign(true) // +1Math.sign(false) // 0Math.sign(null) // 0Math.sign('9') // +1Math.sign('foo') // NaNMath.sign() // NaNMath.sign(undefined) // NaN 3.Math.cbrt()用于计算一个数的立方根,对于非数值,会先调用Number方法将其转为数值 1234567Math.cbrt(-1) // -1Math.cbrt(0) // 0Math.cbrt(1) // 1Math.cbrt(2) // 1.2599210498948732Math.cbrt('8') // 2Math.cbrt('hello') // NaN 4.Math.clz32()将参数转为 32 位无符号整数的形式,然后返回这个 32 位值里面有多少个前导 0。 12345Math.clz32(0) // 32Math.clz32(1) // 31Math.clz32(1000) // 22Math.clz32(0b01000000000000000000000000000000) // 1Math.clz32(0b00100000000000000000000000000000) // 2 上面代码中,0 的二进制形式全为 0,所以有 32 个前导 0;1 的二进制形式是0b1,只占 1 位,所以 32 位之中有 31 个前导 0;1000 的二进制形式是0b1111101000,一共有 10 位,所以 32 位之中有 22 个前导 0。 clz32这个函数名就来自”count leading zero bits in 32-bit binary representation of a number“(计算一个数的 32 位二进制形式的前导 0 的个数)的缩写。 左移运算符(<<)与Math.clz32方法直接相关。 12345Math.clz32(0) // 32Math.clz32(1) // 31Math.clz32(1 << 1) // 30Math.clz32(1 << 2) // 29Math.clz32(1 << 29) // 2 对于小数,Math.clz32方法只考虑整数部分。 12Math.clz32(3.2) // 30Math.clz32(3.9) // 30 对于空值或其他类型的值,Math.clz32方法会将它们先转为数值,然后再计算。 12345678Math.clz32() // 32Math.clz32(NaN) // 32Math.clz32(Infinity) // 32Math.clz32(null) // 32Math.clz32('foo') // 32Math.clz32([]) // 32Math.clz32({}) // 32Math.clz32(true) // 31 5.Math.imul()Math.imul方法返回两个数以 32 位带符号整数形式相乘的结果,返回的也是一个 32 位的带符号整数。 123Math.imul(2, 4) // 8Math.imul(-1, 8) // -8Math.imul(-2, -2) // 4 如果只考虑最后 32 位,大多数情况下,Math.imul(a, b)与a * b的结果是相同的,即该方法等同于(a * b)|0的效果(超过 32 位的部分溢出)。之所以需要部署这个方法,是因为 JavaScript 有精度限制,超过 2 的 53 次方的值无法精确表示。这就是说,对于那些很大的数的乘法,低位数值往往都是不精确的,Math.imul方法可以返回正确的低位数值。 1(0x7fffffff * 0x7fffffff)|0 // 0 上面这个乘法算式,返回结果为 0。但是由于这两个二进制数的最低位都是 1,所以这个结果肯定是不正确的,因为根据二进制乘法,计算结果的二进制最低位应该也是 1。这个错误就是因为它们的乘积超过了 2 的 53 次方,JavaScript 无法保存额外的精度,就把低位的值都变成了 0。Math.imul方法可以返回正确的值 1。 1Math.imul(0x7fffffff, 0x7fffffff) // 1 6.Math.fround()Math.fround方法返回一个数的32位单精度浮点数形式。 对于32位单精度格式来说,数值精度是24个二进制位(1 位隐藏位与 23 位有效位),所以对于 -224 至 224 之间的整数(不含两个端点),返回结果与参数本身一致。 123Math.fround(0) // 0Math.fround(1) // 1Math.fround(2 ** 24 - 1) // 16777215 如果参数的绝对值大于 224,返回的结果便开始丢失精度。 12Math.fround(2 ** 24) // 16777216Math.fround(2 ** 24 + 1) // 16777216 Math.fround方法的主要作用,是将64位双精度浮点数转为32位单精度浮点数。如果小数的精度超过24个二进制位,返回值就会不同于原值,否则返回值不变(即与64位双精度值一致)。 12345678// 未丢失有效精度Math.fround(1.125) // 1.125Math.fround(7.25) // 7.25// 丢失精度Math.fround(0.3) // 0.30000001192092896Math.fround(0.7) // 0.699999988079071Math.fround(1.0000000123) // 1 对于 NaN 和 Infinity,此方法返回原值。对于其它类型的非数值,Math.fround 方法会先将其转为数值,再返回单精度浮点数。 12345678Math.fround(NaN) // NaNMath.fround(Infinity) // InfinityMath.fround('5') // 5Math.fround(true) // 1Math.fround(null) // 0Math.fround([]) // 0Math.fround({}) // NaN 对于没有部署这个方法的环境,可以用下面的代码模拟。 123Math.fround = Math.fround || function (x) { return new Float32Array([x])[0];}; 7.Math.hypot()Math.hypot方法返回所有参数的平方和的平方根。 1234567Math.hypot(3, 4); // 5Math.hypot(3, 4, 5); // 7.0710678118654755Math.hypot(); // 0Math.hypot(NaN); // NaNMath.hypot(3, 4, 'foo'); // NaNMath.hypot(3, 4, '5'); // 7.0710678118654755Math.hypot(-3); // 3 上面代码中,3 的平方加上 4 的平方,等于 5 的平方。 如果参数不是数值,Math.hypot方法会将其转为数值。只要有一个参数无法转为数值,就会返回 NaN。 8.对数方法ES6 新增了 4 个对数相关方法。 (1) Math.expm1() Math.expm1(x)返回 ex - 1,即Math.exp(x) - 1。 123Math.expm1(-1) // -0.6321205588285577Math.expm1(0) // 0Math.expm1(1) // 1.718281828459045 对于没有部署这个方法的环境,可以用下面的代码模拟。 123Math.expm1 = Math.expm1 || function(x) { return Math.exp(x) - 1;}; (2)Math.log1p() Math.log1p(x)方法返回1 + x的自然对数,即Math.log(1 + x)。如果x小于-1,返回NaN。 1234Math.log1p(1) // 0.6931471805599453Math.log1p(0) // 0Math.log1p(-1) // -InfinityMath.log1p(-2) // NaN 对于没有部署这个方法的环境,可以用下面的代码模拟。 123Math.log1p = Math.log1p || function(x) { return Math.log(1 + x);}; (3)Math.log10() Math.log10(x)返回以 10 为底的x的对数。如果x小于 0,则返回 NaN。 12345Math.log10(2) // 0.3010299956639812Math.log10(1) // 0Math.log10(0) // -InfinityMath.log10(-2) // NaNMath.log10(100000) // 5 对于没有部署这个方法的环境,可以用下面的代码模拟。 123Math.log10 = Math.log10 || function(x) { return Math.log(x) / Math.LN10;}; (4)Math.log2() Math.log2(x)返回以 2 为底的x的对数。如果x小于 0,则返回 NaN。 1234567Math.log2(3) // 1.584962500721156Math.log2(2) // 1Math.log2(1) // 0Math.log2(0) // -InfinityMath.log2(-2) // NaNMath.log2(1024) // 10Math.log2(1 << 29) // 29 对于没有部署这个方法的环境,可以用下面的代码模拟。 123Math.log2 = Math.log2 || function(x) { return Math.log(x) / Math.LN2;}; 9.双曲函数方法ES6 新增了 6 个双曲函数方法。 Math.sinh(x) 返回x的双曲正弦(hyperbolic sine) Math.cosh(x) 返回x的双曲余弦(hyperbolic cosine) Math.tanh(x) 返回x的双曲正切(hyperbolic tangent) Math.asinh(x) 返回x的反双曲正弦(inverse hyperbolic sine) Math.acosh(x) 返回x的反双曲余弦(inverse hyperbolic cosine) Math.atanh(x) 返回x的反双曲正切(inverse hyperbolic tangent) 10.BigIntJavaScript 原生提供BigInt函数,可以用它生成 BigInt 类型的数值。转换规则基本与Number()一致,将其他类型的值转为 BigInt。 1234BigInt(123) // 123nBigInt('123') // 123nBigInt(false) // 0nBigInt(true) // 1n BigInt()函数必须有参数,而且参数必须可以正常转为数值,下面的用法都会报错。 12345new BigInt() // TypeErrorBigInt(undefined) //TypeErrorBigInt(null) // TypeErrorBigInt('123n') // SyntaxErrorBigInt('abc') // SyntaxError 上面代码中,尤其值得注意字符串123n无法解析成 Number 类型,所以会报错。 参数如果是小数,也会报错。 12BigInt(1.5) // RangeErrorBigInt('1.5') // SyntaxError BigInt 继承了 Object 对象的两个实例方法。 BigInt.prototype.toString() BigInt.prototype.valueOf() 它还继承了 Number 对象的一个实例方法。 BigInt.prototype.toLocaleString() 此外,还提供了三个静态方法。 BigInt.asUintN(width, BigInt): 给定的 BigInt 转为 0 到 2width - 1 之间对应的值。 BigInt.asIntN(width, BigInt):给定的 BigInt 转为 -2width - 1 到 2width - 1 - 1 之间对应的值。 BigInt.parseInt(string[, radix]):近似于Number.parseInt(),将一个字符串转换成指定进制的 BigInt。 12345678const max = 2n ** (64n - 1n) - 1n;BigInt.asIntN(64, max)// 9223372036854775807nBigInt.asIntN(64, max + 1n)// -9223372036854775808nBigInt.asUintN(64, max + 1n)// 9223372036854775808n 上面代码中,max是64位带符号的 BigInt 所能表示的最大值。如果对这个值加1n,BigInt.asIntN()将会返回一个负值,因为这时新增的一位将被解释为符号位。而BigInt.asUintN()方法由于不存在符号位,所以可以正确返回结果。 如果BigInt.asIntN()和BigInt.asUintN()指定的位数,小于数值本身的位数,那么头部的位将被舍弃。 1234const max = 2n ** (64n - 1n) - 1n;BigInt.asIntN(32, max) // -1nBigInt.asUintN(32, max) // 4294967295n 上面代码中,max是一个64位的 BigInt,如果转为32位,前面的32位都会被舍弃。 下面是BigInt.parseInt()的例子。 12345// Number.parseInt() 与 BigInt.parseInt() 的对比Number.parseInt('9007199254740993', 10)// 9007199254740992BigInt.parseInt('9007199254740993', 10)// 9007199254740993n 上面代码中,由于有效数字超出了最大限度,Number.parseInt方法返回的结果是不精确的,而BigInt.parseInt方法正确返回了对应的 BigInt。 对于二进制数组,BigInt 新增了两个类型BigUint64Array和BigInt64Array,这两种数据类型返回的都是64位 BigInt。DataView对象的实例方法DataView.prototype.getBigInt64()和DataView.prototype.getBigUint64(),返回的也是 BigInt。 转换规则可以使用Boolean()、Number()和String()这三个方法,将 BigInt 可以转为布尔值、数值和字符串类型。 1234Boolean(0n) // falseBoolean(1n) // trueNumber(1n) // 1String(1n) // "1" 上面代码中,注意最后一个例子,转为字符串时后缀n会消失。 另外,取反运算符(!)也可以将 BigInt 转为布尔值。 12!0n // true!1n // false 数学运算数学运算方面,BigInt 类型的+、-、*和**这四个二元运算符,与 Number 类型的行为一致。除法运算/会舍去小数部分,返回一个整数。 129n / 5n// 1n 几乎所有的数值运算符都可以用在 BigInt,但是有两个例外。 不带符号的右移位运算符>>> 一元的求正运算符+ 上面两个运算符用在 BigInt 会报错。前者是因为>>>运算符是不带符号的,但是 BigInt 总是带有符号的,导致该运算无意义,完全等同于右移运算符>>。后者是因为一元运算符+在 asm.js 里面总是返回 Number 类型,为了不破坏 asm.js 就规定+1n会报错。 BigInt 不能与普通数值进行混合运算。 11n + 1.3 // 报错 上面代码报错是因为无论返回的是 BigInt 或 Number,都会导致丢失精度信息。比如(2n**53n + 1n) + 0.5这个表达式,如果返回 BigInt 类型,0.5这个小数部分会丢失;如果返回 Number 类型,有效精度只能保持 53 位,导致精度下降。 同样的原因,如果一个标准库函数的参数预期是 Number 类型,但是得到的是一个 BigInt,就会报错。 12345// 错误的写法Math.sqrt(4n) // 报错// 正确的写法Math.sqrt(Number(4n)) // 2 上面代码中,Math.sqrt的参数预期是 Number 类型,如果是 BigInt 就会报错,必须先用Number方法转一下类型,才能进行计算。 asm.js 里面,|0跟在一个数值的后面会返回一个32位整数。根据不能与 Number 类型混合运算的规则,BigInt 如果与|0进行运算会报错。 11n | 0 // 报错 其他运算BigInt 对应的布尔值,与 Number 类型一致,即0n会转为false,其他值转为true。 123456if (0n) { console.log('if');} else { console.log('else');}// else 上面代码中,0n对应false,所以会进入else子句。 比较运算符(比如>)和相等运算符(==)允许 BigInt 与其他类型的值混合计算,因为这样做不会损失精度。 123450n < 1 // true0n < true // true0n == 0 // true0n == false // true0n === 0 // false BigInt 与字符串混合运算时,会先转为字符串,再进行运算。 1'' + 123n // "123"","link":"/JavaScript/12.Math%E7%9A%84%E6%89%A9%E5%B1%95/"},{"title":"事件模型","text":"Javascript事件使得Html网页具备互动性, 常见的加载事件、鼠标事件、自定义事件等 1.事件模型JavaScript事件流是为了描述父子元素之间事件触发的顺序 Javascript事件模型一共有三种分别是 原始事件模型(DOM0级) 所有的浏览器都支持的一种事件模型, 没有事件流,事件一旦发生马上进行处理,有两种方式可以实现原始事件模型 以on开头的标签属性 123456789<p onclick="console.log('p')"> <span onclick="console.log('span')">clieck me</span></p><!--标签属性值是事件点击后要执行的javascript代码;显示与行为没有分离;没法为同一个事件绑定多个函数事件发生在冒泡阶段(当点击span标签时,console依次打印span、p)--> 元素节点对象的事件属性 123456789101112 var pDom = document.getElementById("p");pDom.onclick = sayHello;function sayHello() { console.log("hello world") } /** 节点对象的事件属性值是函数 没法为同一个事件绑定多个函数,后者会覆盖前者 事件依然发生在冒泡阶段 */ 标准事件模型(DOM2级) 标准事件模型是W3C组织制定的标准事件模型,现代浏览器(IE6~8之外)都支持,该模型将事件分为三个阶段: 捕获阶段 当某个事件触发时,事件会从window对象至上而下传播直至事件发生的目标元素,默认在这个过程中相应的事件监听函数不会触发。 目标阶段 当事件传播到目标元素之后,执行目标元素上该事件的监听函数,如果没有就不执行。 冒泡阶段 事件再从目标元素开始逐层向上传播, 如果途中有该事件的监听函数就执行;所有事件都有捕获阶段,但是只有部分事件才有冒泡阶段。 依次给下面的div、ul、li、p、span元素添加一个click事件,并给该事件绑定两个监听函数(函数相同,但是调用阶段不同,一个在捕获阶段触发, 一个在冒泡阶段触发) 1234567891011121314151617181920212223242526272829303132333435<!DOCTYPE html><html lang="en"><head> <meta charset="UTF-8"> <meta http-equiv="X-UA-Compatible" content="IE=edge"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>Document</title></head><body> <div> <p> <span>clieck me</span> </p> </div> <script> var elArray = ['div', 'p', 'span'] var domArray = elArray.map(item => { return document.querySelector(item) }) domArray.forEach(item => { item.addEventListener('click', hello, false) item.addEventListener('click', hello, true) }) function hello(event) { console.log(` target:${event.target.nodeName.toLowerCase()} currentTarget: ${event.currentTarget.nodeName.toLowerCase()} eventPhase: ${["not happen", "capture", "target", "bubble"][event.eventPhase]}`) } </script></body></html> 当点击span时,事件从window对象至上而下传播, 先触发捕获阶段的事件,当事件传播到目标元素时触发目标元素的两个监听事件发生,所以在target阶段console打印了两次, 接着冒泡阶段事件由里向外传播,依次触发对应事件。 IE事件模型 在IE老版本浏览器中(IE6~8),事件对象不作为函数传入, 而是作为window全局对象的一个属性传入 window.event 现已基本不用了,太老了。 2.EventTargetDOM的事件操作(监听和触发),都定义在EventTarget接口, 所有节点对象都部署了这个接口(window, document, element等对象) 该接口主要提供三个实例方法: addEventListener : 添加事件监听器 removeEventListener : 移除事件监听器 dispatchEvent : 派发事件 1.1 addEventListeneraddEventListener(type, fn, useCapture) 方法为事件添加对应的处理函数,在事件触发时调用。 type: 事件名称(click、dubleclick、keydown…) fn: 事件触发时的回调函数 useCapture: 指定回调函数是在捕获阶段调用还是在冒泡阶段调用, 默认值为false在冒泡阶段调用。 可以通过该方法为同一个事件添加多个监听函数; 可以手动控制事件发生是在捕获阶段还是在冒泡阶段触发; 可以将子元素上的事件统一委托给父元素代为处理 事件对象event以监听函数参数的形式出现, 其常用属性: target: 监听事件所在的节点对象, 只会出现在目标阶段 currentTarget: 事件传播过程中当前所在的节点对象, 会发生在捕获、目标、冒泡三阶段 eventPhase: 事件发生的阶段: 0: 事件未发生; 1:捕获阶段;2:目标阶段; 3:冒泡阶段 3.事件代理通过事件冒泡机制,统一将添加在子元素上的事件,委托给父元素代为处理,这样的好处: 减少元素上的事件绑定、减少内存占用 统一在父元素上代为处理, 即便后面再动态添减元素,上面的事件依然有效","link":"/JavaScript/13.%E4%BA%8B%E4%BB%B6%E6%A8%A1%E5%9E%8B/"},{"title":"Set和Map数据结构","text":"1.SetES6提供了新的数据结构Set。它类似于数组,但成员的值都是唯一的,没有重复的值 注意:Set结构没有键名,或者说键名和键值是同一个值 Set本身是一个构造函数,用来生成Set数据结构 12345678const s = new Set();[2, 3, 5, 4, 5, 2, 2].forEach(x => s.add(x));for (let i of s) { console.log(i);}// 2 3 5 4 上面的代码通过add()方法向Set结构加入成员,结果表明Set结构不会添加重复的值 Set函数可以接受一个数组(或者具有iterator接口的其他数据结构),作为参数,用来初始化 12345678910111213141516171819// 例一const set = new Set([1, 2, 3, 4, 4]);[...set]// [1, 2, 3, 4]// 例二const items = new Set([1, 2, 3, 4, 5, 5, 5, 5]);items.size // 5// 例三const set = new Set(document.querySelectorAll('div'));set.size // 56// 类似于const set = new Set();document .querySelectorAll('div') .forEach(div => set.add(div));set.size // 56 上面代码中,例一和例二都是Set函数接受数组作为参数,例三是接受类似数组的对象作为参数。 12// 去除数组的重复成员[...new Set(array)] 向 Set 加入值的时候,不会发生类型转换,所以5和"5"是两个不同的值。Set 内部判断两个值是否不同,使用的算法叫做“Same-value-zero equality”,它类似于精确相等运算符(===),主要的区别是向 Set 加入值时认为NaN等于自身,而精确相等运算符认为NaN不等于自身。 123456let set = new Set();let a = NaN;let b = NaN;set.add(a);set.add(b);set // Set {NaN} 上面代码向 Set 实例添加了两次NaN,但是只会加入一个。这表明,在 Set 内部,两个NaN是相等的。 另外,两个对象总是不相等的。 1234567let set = new Set();set.add({});set.size // 1set.add({});set.size // 2 上面代码表示,由于两个空对象不相等,所以它们被视为两个值。 2.Set实例的属性和方法Set结构的实例有以下属性 Set.prototype.constructor:构造函数,默认就是Set函数 Set.prototype.size:返回Set实例的成员个数 Set 实例的方法分为两大类:操作方法(用于操作数据)和遍历方法(用于遍历成员)。下面先介绍四个操作方法。 Set.prototype.add(value):添加某个值,返回 Set 结构本身。 Set.prototype.delete(value):删除某个值,返回一个布尔值,表示删除是否成功。 Set.prototype.has(value):返回一个布尔值,表示该值是否为Set的成员。 Set.prototype.clear():清除所有成员,没有返回值。 下面是一个对比,看看在判断是否包括一个键上面,Object结构和Set结构的写法不同。 12345678910111213141516171819// 对象的写法const properties = { 'width': 1, 'height': 1};if (properties[someName]) { // do something}// Set的写法const properties = new Set();properties.add('width');properties.add('height');if (properties.has(someName)) { // do something} Array.from方法可以将 Set 结构转为数组。 12const items = new Set([1, 2, 3, 4, 5]);const array = Array.from(items); 这就提供了去除数组重复成员的另一种方法。 12345function dedupe(array) { return Array.from(new Set(array));}dedupe([1, 1, 2, 3]) // [1, 2, 3] 遍历操作Set 结构的实例有四个遍历方法,可以用于遍历成员。 Set.prototype.keys():返回键名的遍历器 Set.prototype.values():返回键值的遍历器 Set.prototype.entries():返回键值对的遍历器 Set.prototype.forEach():使用回调函数遍历每个成员 key() value() forEach()keys方法、values方法、entries方法返回的都是遍历器对象(详见《Iterator 对象》一章)。由于 Set 结构没有键名,只有键值(或者说键名和键值是同一个值),所以keys方法和values方法的行为完全一致。 12345678910111213141516171819202122let set = new Set(['red', 'green', 'blue']);for (let item of set.keys()) { console.log(item);}// red// green// bluefor (let item of set.values()) { console.log(item);}// red// green// bluefor (let item of set.entries()) { console.log(item);}// ["red", "red"]// ["green", "green"]// ["blue", "blue"] 上面代码中,entries方法返回的遍历器,同时包括键名和键值,所以每次输出一个数组,它的两个成员完全相等。 Set 结构的实例默认可遍历,它的默认遍历器生成函数就是它的values方法。 12Set.prototype[Symbol.iterator] === Set.prototype.values// true 这意味着,可以省略values方法,直接用for...of循环遍历 Set。 12345678let set = new Set(['red', 'green', 'blue']);for (let x of set) { console.log(x);}// red// green// blue forEach()Set 结构的实例与数组一样,也拥有forEach方法,用于对每个成员执行某种操作,没有返回值。 12345let set = new Set([1, 4, 9]);set.forEach((value, key) => console.log(key + ' : ' + value))// 1 : 1// 4 : 4// 9 : 9 上面代码说明,forEach方法的参数就是一个处理函数。该函数的参数与数组的forEach一致,依次为键值、键名、集合本身(上例省略了该参数)。这里需要注意,Set 结构的键名就是键值(两者是同一个值),因此第一个参数与第二个参数的值永远都是一样的。 另外,forEach方法还可以有第二个参数,表示绑定处理函数内部的this对象。 遍历的应用扩展运算符(...)内部使用for...of循环,所以也可以用于 Set 结构。 123let set = new Set(['red', 'green', 'blue']);let arr = [...set];// ['red', 'green', 'blue'] 扩展运算符和 Set 结构相结合,就可以去除数组的重复成员。 123let arr = [3, 5, 2, 2, 5, 5];let unique = [...new Set(arr)];// [3, 5, 2] 而且,数组的map和filter方法也可以间接用于 Set 了。 1234567let set = new Set([1, 2, 3]);set = new Set([...set].map(x => x * 2));// 返回Set结构:{2, 4, 6}let set = new Set([1, 2, 3, 4, 5]);set = new Set([...set].filter(x => (x % 2) == 0));// 返回Set结构:{2, 4} 因此使用 Set 可以很容易地实现并集(Union)、交集(Intersect)和差集(Difference)。 1234567891011121314let a = new Set([1, 2, 3]);let b = new Set([4, 3, 2]);// 并集let union = new Set([...a, ...b]);// Set {1, 2, 3, 4}// 交集let intersect = new Set([...a].filter(x => b.has(x)));// set {2, 3}// (a 相对于 b 的)差集let difference = new Set([...a].filter(x => !b.has(x)));// Set {1} 如果想在遍历操作中,同步改变原来的 Set 结构,目前没有直接的方法,但有两种变通方法。一种是利用原 Set 结构映射出一个新的结构,然后赋值给原来的 Set 结构;另一种是利用Array.from方法。 123456789// 方法一let set = new Set([1, 2, 3]);set = new Set([...set].map(val => val * 2));// set的值是2, 4, 6// 方法二let set = new Set([1, 2, 3]);set = new Set(Array.from(set, val => val * 2));// set的值是2, 4, 6 上面代码提供了两种方法,直接在遍历操作中改变原来的 Set 结构。 3.WeakSetWeakSet 结构与 Set 类似,也是不重复的值的集合。但是,它与 Set 有两个区别: WeakSet 的成员只能是对象,而不能是其他类型的值。 WeakSet 中的对象都是弱引用,即垃圾回收机制不考虑 WeakSet 对该对象的引用,也就是说,如果其他对象都不再引用该对象,那么垃圾回收机制会自动回收该对象所占用的内存,不考虑该对象还存在于 WeakSet 之中。 12345const ws = new WeakSet();ws.add(1)// TypeError: Invalid value used in weak setws.add(Symbol())// TypeError: invalid value used in weak set WeakSet 结构有以下三个方法。 **WeakSet.prototype.add(value)**:向 WeakSet 实例添加一个新成员。 **WeakSet.prototype.delete(value)**:清除 WeakSet 实例的指定成员。 **WeakSet.prototype.has(value)**:返回一个布尔值,表示某个值是否在 WeakSet 实例之中。 WeakSet 没有size属性,没有办法遍历它的成员。 12345ws.size // undefinedws.forEach // undefinedws.forEach(function(item){ console.log('WeakSet has ' + item)})// TypeError: undefined is not a function 上面代码试图获取size和forEach属性,结果都不能成功。 WeakSet 不能遍历,是因为成员都是弱引用,随时可能消失,遍历机制无法保证成员的存在,很可能刚刚遍历结束,成员就取不到了。WeakSet 的一个用处,是储存 DOM 节点,而不用担心这些节点从文档移除时,会引发内存泄漏。 4.MapJavaScript 的对象(Object),本质上是键值对的集合(Hash 结构),但是传统上只能用字符串当作键。这给它的使用带来了很大的限制。 12345const data = {};const element = document.getElementById('myDiv');data[element] = 'metadata';data['[object HTMLDivElement]'] // "metadata" 上面代码原意是将一个 DOM 节点作为对象data的键,但是由于对象只接受字符串作为键名,所以element被自动转为字符串[object HTMLDivElement]。 为了解决这个问题,ES6 提供了 Map 数据结构。它类似于对象,也是键值对的集合,但是“键”的范围不限于字符串,各种类型的值(包括对象)都可以当作键。也就是说,Object 结构提供了“字符串—值”的对应,Map 结构提供了“值—值”的对应,是一种更完善的 Hash 结构实现。如果你需要“键值对”的数据结构,Map 比 Object 更合适。 123456789const m = new Map();const o = {p: 'Hello World'};m.set(o, 'content')m.get(o) // "content"m.has(o) // truem.delete(o) // truem.has(o) // false 上面的例子展示了如何向 Map 添加成员。作为构造函数,Map 也可以接受一个数组作为参数。该数组的成员是一个个表示键值对的数组。 12345678910const map = new Map([ ['name', '张三'], ['title', 'Author']]);map.size // 2map.has('name') // truemap.get('name') // "张三"map.has('title') // truemap.get('title') // "Author" 上面代码在新建 Map 实例时,就指定了两个键name和title。 Map构造函数接受数组作为参数,实际上执行的是下面的算法。 12345678910const items = [ ['name', '张三'], ['title', 'Author']];const map = new Map();items.forEach( ([key, value]) => map.set(key, value)); 事实上,不仅仅是数组,任何具有 Iterator 接口、且每个成员都是一个双元素的数组的数据结构(详见《Iterator》一章)都可以当作Map构造函数的参数。这就是说,Set和Map都可以用来生成新的 Map。 12345678910const set = new Set([ ['foo', 1], ['bar', 2]]);const m1 = new Map(set);m1.get('foo') // 1const m2 = new Map([['baz', 3]]);const m3 = new Map(m2);m3.get('baz') // 3 上面代码中,我们分别使用 Set 对象和 Map 对象,当作Map构造函数的参数,结果都生成了新的 Map 对象。 如果对同一个键多次赋值,后面的值将覆盖前面的值。 1234567const map = new Map();map.set(1, 'aaa').set(1, 'bbb');map.get(1) // "bbb" 上面代码对键1连续赋值两次,后一次的值覆盖前一次的值。 如果读取一个未知的键,则返回undefined。 12new Map().get('asfddfsasadf')// undefined 上面代码的set和get方法,表面是针对同一个键,但实际上这是两个不同的数组实例,内存地址是不一样的,因此get方法无法读取该键,返回undefined。 同理,同样的值的两个实例,在 Map 结构中被视为两个键。 1234567891011const map = new Map();const k1 = ['a'];const k2 = ['a'];map.set(k1, 111).set(k2, 222);map.get(k1) // 111map.get(k2) // 222 上面代码中,变量k1和k2的值是一样的,但是它们在 Map 结构中被视为两个键。 由上可知,Map 的键实际上是跟内存地址绑定的,只要内存地址不一样,就视为两个键。这就解决了同名属性碰撞(clash)的问题,我们扩展别人的库的时候,如果使用对象作为键名,就不用担心自己的属性与原作者的属性同名。 如果 Map 的键是一个简单类型的值(数字、字符串、布尔值),则只要两个值严格相等,Map 将其视为一个键,比如0和-0就是一个键,布尔值true和字符串true则是两个不同的键。另外,undefined和null也是两个不同的键。虽然NaN不严格相等于自身,但 Map 将其视为同一个键。 123456789101112131415let map = new Map();map.set(-0, 123);map.get(+0) // 123map.set(true, 1);map.set('true', 2);map.get(true) // 1map.set(undefined, 3);map.set(null, 4);map.get(undefined) // 3map.set(NaN, 123);map.get(NaN) // 123 实例属性和操作方法Map 结构的实例有以下属性和操作方法。 (1)size 属性 size属性返回 Map 结构的成员总数。 12345const map = new Map();map.set('foo', true);map.set('bar', false);map.size // 2 (2)Map.prototype.set(key, value) set方法设置键名key对应的键值为value,然后返回整个 Map 结构。如果key已经有值,则键值会被更新,否则就新生成该键。 12345const m = new Map();m.set('edition', 6) // 键是字符串m.set(262, 'standard') // 键是数值m.set(undefined, 'nah') // 键是 undefined set方法返回的是当前的Map对象,因此可以采用链式写法。 1234let map = new Map() .set(1, 'a') .set(2, 'b') .set(3, 'c'); (3)Map.prototype.get(key) get方法读取key对应的键值,如果找不到key,返回undefined。 123456const m = new Map();const hello = function() {console.log('hello');};m.set(hello, 'Hello ES6!') // 键是函数m.get(hello) // Hello ES6! (4)Map.prototype.has(key) has方法返回一个布尔值,表示某个键是否在当前 Map 对象之中。 12345678910const m = new Map();m.set('edition', 6);m.set(262, 'standard');m.set(undefined, 'nah');m.has('edition') // truem.has('years') // falsem.has(262) // truem.has(undefined) // true (5)Map.prototype.delete(key) delete方法删除某个键,返回true。如果删除失败,返回false。 123456const m = new Map();m.set(undefined, 'nah');m.has(undefined) // truem.delete(undefined)m.has(undefined) // false (6)Map.prototype.clear() clear方法清除所有成员,没有返回值。 1234567let map = new Map();map.set('foo', true);map.set('bar', false);map.size // 2map.clear()map.size // 0 遍历方法Map 结构原生提供三个遍历器生成函数和一个遍历方法。 Map.prototype.keys():返回键名的遍历器。 Map.prototype.values():返回键值的遍历器。 Map.prototype.entries():返回所有成员的遍历器。 Map.prototype.forEach():遍历 Map 的所有成员。 123456789101112131415161718192021222324252627282930313233343536const map = new Map([ ['F', 'no'], ['T', 'yes'],]);for (let key of map.keys()) { console.log(key);}// "F"// "T"for (let value of map.values()) { console.log(value);}// "no"// "yes"for (let item of map.entries()) { console.log(item[0], item[1]);}// "F" "no"// "T" "yes"// 或者for (let [key, value] of map.entries()) { console.log(key, value);}// "F" "no"// "T" "yes"// 等同于使用map.entries()for (let [key, value] of map) { console.log(key, value);}// "F" "no"// "T" "yes" Map 结构转为数组结构,比较快速的方法是使用扩展运算符(...)。 1234567891011121314151617const map = new Map([ [1, 'one'], [2, 'two'], [3, 'three'],]);[...map.keys()]// [1, 2, 3][...map.values()]// ['one', 'two', 'three'][...map.entries()]// [[1,'one'], [2, 'two'], [3, 'three']][...map]// [[1,'one'], [2, 'two'], [3, 'three']] 5.WeakMapWeakMap结构与Map结构类似,也是用于生成键值对的集合。 123456789101112// WeakMap 可以使用 set 方法添加成员const wm1 = new WeakMap();const key = {foo: 1};wm1.set(key, 2);wm1.get(key) // 2// WeakMap 也可以接受一个数组,// 作为构造函数的参数const k1 = [1, 2, 3];const k2 = [4, 5, 6];const wm2 = new WeakMap([[k1, 'foo'], [k2, 'bar']]);wm2.get(k2) // "bar" WeakMap与Map的区别有两点。 首先,WeakMap只接受对象作为键名(null除外),不接受其他类型的值作为键名。 1234567const map = new WeakMap();map.set(1, 2)// TypeError: 1 is not an object!map.set(Symbol(), 2)// TypeError: Invalid value used as weak map keymap.set(null, 2)// TypeError: Invalid value used as weak map key 上面代码中,如果将数值1和Symbol值作为 WeakMap 的键名,都会报错。 其次,WeakMap的键名所指向的对象,不计入垃圾回收机制。 WeakMap的设计目的在于,有时我们想在某个对象上面存放一些数据,但是这会形成对于这个对象的引用。请看下面的例子。 123456const e1 = document.getElementById('foo');const e2 = document.getElementById('bar');const arr = [ [e1, 'foo 元素'], [e2, 'bar 元素'],]; 面代码中,e1和e2是两个对象,我们通过arr数组对这两个对象添加一些文字说明。这就形成了arr对e1和e2的引用。 一旦不再需要这两个对象,我们就必须手动删除这个引用,否则垃圾回收机制就不会释放e1和e2占用的内存。 1234// 不需要 e1 和 e2 的时候// 必须手动删除引用arr [0] = null;arr [1] = null; 上面这样的写法显然很不方便。一旦忘了写,就会造成内存泄露。 WeakMap 就是为了解决这个问题而诞生的,它的键名所引用的对象都是弱引用,即垃圾回收机制不将该引用考虑在内。因此,只要所引用的对象的其他引用都被清除,垃圾回收机制就会释放该对象所占用的内存。也就是说,一旦不再需要,WeakMap 里面的键名对象和所对应的键值对会自动消失,不用手动删除引用。 基本上,如果你要往对象上添加数据,又不想干扰垃圾回收机制,就可以使用 WeakMap。一个典型应用场景是,在网页的 DOM 元素上添加数据,就可以使用WeakMap结构。当该 DOM 元素被清除,其所对应的WeakMap记录就会自动被移除。 123456const wm = new WeakMap();const element = document.getElementById('example');wm.set(element, 'some information');wm.get(element) // "some information" 上面代码中,先新建一个 WeakMap 实例。然后,将一个 DOM 节点作为键名存入该实例,并将一些附加信息作为键值,一起存放在 WeakMap 里面。这时,WeakMap 里面对element的引用就是弱引用,不会被计入垃圾回收机制。 也就是说,上面的 DOM 节点对象除了 WeakMap 的弱引用外,其他位置对该对象的引用一旦消除,该对象占用的内存就会被垃圾回收机制释放。WeakMap 保存的这个键值对,也会自动消失。 总之,WeakMap的专用场合就是,它的键所对应的对象,可能会在将来消失。WeakMap结构有助于防止内存泄漏。 注意,WeakMap 弱引用的只是键名,而不是键值。键值依然是正常引用。 12345678const wm = new WeakMap();let key = {};let obj = {foo: 1};wm.set(key, obj);obj = null;wm.get(key)// Object {foo: 1} 上面代码中,键值obj是正常引用。所以,即使在 WeakMap 外部消除了obj的引用,WeakMap 内部的引用依然存在。 WeakMap 的语法WeakMap 与 Map 在 API 上的区别主要是两个,一是没有遍历操作(即没有keys()、values()和entries()方法),也没有size属性。因为没有办法列出所有键名,某个键名是否存在完全不可预测,跟垃圾回收机制是否运行相关。这一刻可以取到键名,下一刻垃圾回收机制突然运行了,这个键名就没了,为了防止出现不确定性,就统一规定不能取到键名。二是无法清空,即不支持clear方法。因此,WeakMap只有四个方法可用:get()、set()、has()、delete()。 123456const wm = new WeakMap();// size、forEach、clear 方法都不存在wm.size // undefinedwm.forEach // undefinedwm.clear // undefined 6.WeakRefWeakSet 和 WeakMap 是基于弱引用的数据结构,ES2021 更进一步,提供了 WeakRef 对象,用于直接创建对象的弱引用。 12let target = {};let wr = new WeakRef(target); 上面示例中,target是原始对象,构造函数WeakRef()创建了一个基于target的新对象wr。这里,wr就是一个 WeakRef 的实例,属于对target的弱引用,垃圾回收机制不会计入这个引用,也就是说,wr的引用不会妨碍原始对象target被垃圾回收机制清除。 WeakRef 实例对象有一个deref()方法,如果原始对象存在,该方法返回原始对象;如果原始对象已经被垃圾回收机制清除,该方法返回undefined。 1234567let target = {};let wr = new WeakRef(target);let obj = wr.deref();if (obj) { // target 未被垃圾回收机制清除 // ...} 上面示例中,deref()方法可以判断原始对象是否已被清除。 弱引用对象的一大用处,就是作为缓存,未被清除时可以从缓存取值,一旦清除缓存就自动失效。 12345678910111213141516function makeWeakCached(f) { const cache = new Map(); return key => { const ref = cache.get(key); if (ref) { const cached = ref.deref(); if (cached !== undefined) return cached; } const fresh = f(key); cache.set(key, new WeakRef(fresh)); return fresh; };}const getImageCached = makeWeakCached(getImage); 上面示例中,makeWeakCached()用于建立一个缓存,缓存里面保存对原始文件的弱引用。 注意,标准规定,一旦使用WeakRef()创建了原始对象的弱引用,那么在本轮事件循环(event loop),原始对象肯定不会被清除,只会在后面的事件循环才会被清除。","link":"/JavaScript/21.Set%E5%92%8CMap%E6%95%B0%E6%8D%AE%E7%BB%93%E6%9E%84/"},{"title":"Proxy","text":"1.概述Proxy用于修改某些操作的默认行为,等同于在语言层面做出修改,所以属于一种”元编程”,即对编程语言进行编程 Proxy可以理解成,在目标对象之前架设一层”拦截”,外界对该对象的访问,都必须先通过这层拦截,因此提供了一种机制,可以对外界访问进行过滤和改写。Proxy这个词的原意是代理,用在这里表示由他来代理某些操作 123456789const obj = new Proxy({}, { get: function(target, propKey, receiver) { return Reflect.get(target, propKey, receiver); }, set: function(target, propKey, value, receiver) { console.log(`setting ${propKey}!`); return Reflect.set(target, propKey, value, receiver); }}) 上面代码对一个空对象架设了一层拦截,重定义了属性的读取(get)和设置(set)行为。这里暂时先不解释具体的语法,只看运行结果。对设置了拦截行为的对象obj,去读写它的属性,就会得到下面的结果。 123456obj.count = 1// setting count!++obj.count// getting count!// setting count!// 2 上面代码说明,Proxy 实际上重载(overload)了点运算符,即用自己的定义覆盖了语言的原始定义。 ES6 原生提供 Proxy 构造函数,用来生成 Proxy 实例。 1var proxy = new Proxy(target, handler); 注意,要使得Proxy起作用,必须针对Proxy实例(上例是proxy对象)进行操作,而不是针对目标对象(上例是空对象)进行操作。 如果handler没有设置任何拦截,那就等同于直接通向原对象。 12345var target = {};var handler = {};var proxy = new Proxy(target, handler);proxy.a = 'b';target.a // "b" 上面代码中,handler是一个空对象,没有任何拦截效果,访问proxy就等同于访问target。 下面是 Proxy 支持的拦截操作一览,一共 13 种。 **get(target, propKey, receiver)**:拦截对象属性的读取,比如proxy.foo和proxy['foo']。 **set(target, propKey, value, receiver)**:拦截对象属性的设置,比如proxy.foo = v或proxy['foo'] = v,返回一个布尔值。 **has(target, propKey)**:拦截propKey in proxy的操作,返回一个布尔值。 **deleteProperty(target, propKey)**:拦截delete proxy[propKey]的操作,返回一个布尔值。 **ownKeys(target)**:拦截Object.getOwnPropertyNames(proxy)、Object.getOwnPropertySymbols(proxy)、Object.keys(proxy)、for...in循环,返回一个数组。该方法返回目标对象所有自身的属性的属性名,而Object.keys()的返回结果仅包括目标对象自身的可遍历属性。 **getOwnPropertyDescriptor(target, propKey)**:拦截Object.getOwnPropertyDescriptor(proxy, propKey),返回属性的描述对象。 **defineProperty(target, propKey, propDesc)**:拦截Object.defineProperty(proxy, propKey, propDesc)、Object.defineProperties(proxy, propDescs),返回一个布尔值。 **preventExtensions(target)**:拦截Object.preventExtensions(proxy),返回一个布尔值。 **getPrototypeOf(target)**:拦截Object.getPrototypeOf(proxy),返回一个对象。 **isExtensible(target)**:拦截Object.isExtensible(proxy),返回一个布尔值。 **setPrototypeOf(target, proto)**:拦截Object.setPrototypeOf(proxy, proto),返回一个布尔值。如果目标对象是函数,那么还有两种额外操作可以拦截。 **apply(target, object, args)**:拦截 Proxy 实例作为函数调用的操作,比如proxy(...args)、proxy.call(object, ...args)、proxy.apply(...)。 **construct(target, args)**:拦截 Proxy 实例作为构造函数调用的操作,比如new proxy(...args)。 2.Proxy实例的方法get()get方法用于拦截某个属性的读取操作,可以接受三个参数,依次为目标对象、属性名和 proxy 实例本身(严格地说,是操作行为所针对的对象),其中最后一个参数可选。 get方法的用法,上文已经有一个例子,下面是另一个拦截读取操作的例子。 12345678910111213141516var person = { name: "张三"};var proxy = new Proxy(person, { get: function(target, propKey) { if (propKey in target) { return target[propKey]; } else { throw new ReferenceError("Prop name \\"" + propKey + "\\" does not exist."); } }});proxy.name // "张三"proxy.age // 抛出一个错误 上面代码表示,如果访问目标对象不存在的属性,会抛出一个错误。如果没有这个拦截函数,访问不存在的属性,只会返回undefined。 get方法可以继承。 123456789let proto = new Proxy({}, { get(target, propertyKey, receiver) { console.log('GET ' + propertyKey); return target[propertyKey]; }});let obj = Object.create(proto);obj.foo // "GET foo" 上面代码中,拦截操作定义在Prototype对象上面,所以如果读取obj对象继承的属性时,拦截会生效。 下面的例子使用get拦截,实现数组读取负数的索引。 123456789101112131415161718function createArray(...elements) { let handler = { get(target, propKey, receiver) { let index = Number(propKey); if (index < 0) { propKey = String(target.length + index); } return Reflect.get(target, propKey, receiver); } }; let target = []; target.push(...elements); return new Proxy(target, handler);}let arr = createArray('a', 'b', 'c');arr[-1] // c 上面代码中,数组的位置参数是-1,就会输出数组的倒数第一个成员。 利用 Proxy,可以将读取属性的操作(get),转变为执行某个函数,从而实现属性的链式操作。 12345678910111213141516171819202122var pipe = function (value) { var funcStack = []; var oproxy = new Proxy({} , { get : function (pipeObject, fnName) { if (fnName === 'get') { return funcStack.reduce(function (val, fn) { return fn(val); },value); } funcStack.push(window[fnName]); return oproxy; } }); return oproxy;}var double = n => n * 2;var pow = n => n * n;var reverseInt = n => n.toString().split("").reverse().join("") | 0;pipe(3).double.pow.reverseInt.get; // 63 上面代码设置 Proxy 以后,达到了将函数名链式使用的效果。 下面的例子则是利用get拦截,实现一个生成各种 DOM 节点的通用函数dom。 123456789101112131415161718192021222324252627282930const dom = new Proxy({}, { get(target, property) { return function(attrs = {}, ...children) { const el = document.createElement(property); for (let prop of Object.keys(attrs)) { el.setAttribute(prop, attrs[prop]); } for (let child of children) { if (typeof child === 'string') { child = document.createTextNode(child); } el.appendChild(child); } return el; } }});const el = dom.div({}, 'Hello, my name is ', dom.a({href: '//example.com'}, 'Mark'), '. I like:', dom.ul({}, dom.li({}, 'The web'), dom.li({}, 'Food'), dom.li({}, '…actually that\\'s it') ));document.body.appendChild(el); 下面是一个get方法的第三个参数的例子,它总是指向原始的读操作所在的那个对象,一般情况下就是 Proxy 实例。 123456const proxy = new Proxy({}, { get: function(target, key, receiver) { return receiver; }});proxy.getReceiver === proxy // true 上面代码中,proxy对象的getReceiver属性是由proxy对象提供的,所以receiver指向proxy对象。 12345678const proxy = new Proxy({}, { get: function(target, key, receiver) { return receiver; }});const d = Object.create(proxy);d.a === d // true 上面代码中,d对象本身没有a属性,所以读取d.a的时候,会去d的原型proxy对象找。这时,receiver就指向d,代表原始的读操作所在的那个对象。 如果一个属性不可配置(configurable)且不可写(writable),则 Proxy 不能修改该属性,否则通过 Proxy 对象访问该属性会报错。 123456789101112131415161718const target = Object.defineProperties({}, { foo: { value: 123, writable: false, configurable: false },});const handler = { get(target, propKey) { return 'abc'; }};const proxy = new Proxy(target, handler);proxy.foo// TypeError: Invariant check failed set()set方法用来拦截某个属性的赋值操作,可以接受四个参数,依次为目标对象、属性名、属性值和 Proxy 实例本身,其中最后一个参数可选。 假定Person对象有一个age属性,该属性应该是一个不大于 200 的整数,那么可以使用Proxy保证age的属性值符合要求。 123456789101112131415161718192021222324let validator = { set: function(obj, prop, value) { if (prop === 'age') { if (!Number.isInteger(value)) { throw new TypeError('The age is not an integer'); } if (value > 200) { throw new RangeError('The age seems invalid'); } } // 对于满足条件的 age 属性以及其他属性,直接保存 obj[prop] = value; return true; }};let person = new Proxy({}, validator);person.age = 100;person.age // 100person.age = 'young' // 报错person.age = 300 // 报错 上面代码中,由于设置了存值函数set,任何不符合要求的age属性赋值,都会抛出一个错误,这是数据验证的一种实现方法。利用set方法,还可以数据绑定,即每当对象发生变化时,会自动更新 DOM。 有时,我们会在对象上面设置内部属性,属性名的第一个字符使用下划线开头,表示这些属性不应该被外部使用。结合get和set方法,就可以做到防止这些内部属性被外部读写。 12345678910111213141516171819202122const handler = { get (target, key) { invariant(key, 'get'); return target[key]; }, set (target, key, value) { invariant(key, 'set'); target[key] = value; return true; }};function invariant (key, action) { if (key[0] === '_') { throw new Error(`Invalid attempt to ${action} private "${key}" property`); }}const target = {};const proxy = new Proxy(target, handler);proxy._prop// Error: Invalid attempt to get private "_prop" propertyproxy._prop = 'c'// Error: Invalid attempt to set private "_prop" property 上面代码中,只要读写的属性名的第一个字符是下划线,一律抛错,从而达到禁止读写内部属性的目的。 下面是set方法第四个参数的例子。 123456789const handler = { set: function(obj, prop, value, receiver) { obj[prop] = receiver; return true; }};const proxy = new Proxy({}, handler);proxy.foo = 'bar';proxy.foo === proxy // true 上面代码中,set方法的第四个参数receiver,指的是原始的操作行为所在的那个对象,一般情况下是proxy实例本身,请看下面的例子。 123456789101112const handler = { set: function(obj, prop, value, receiver) { obj[prop] = receiver; return true; }};const proxy = new Proxy({}, handler);const myObj = {};Object.setPrototypeOf(myObj, proxy);myObj.foo = 'bar';myObj.foo === myObj // true 上面代码中,设置myObj.foo属性的值时,myObj并没有foo属性,因此引擎会到myObj的原型链去找foo属性。myObj的原型对象proxy是一个 Proxy 实例,设置它的foo属性会触发set方法。这时,第四个参数receiver就指向原始赋值行为所在的对象myObj。 注意,如果目标对象自身的某个属性不可写,那么set方法将不起作用。 12345678910111213141516const obj = {};Object.defineProperty(obj, 'foo', { value: 'bar', writable: false});const handler = { set: function(obj, prop, value, receiver) { obj[prop] = 'baz'; return true; }};const proxy = new Proxy(obj, handler);proxy.foo = 'baz';proxy.foo // "bar" 上面代码中,obj.foo属性不可写,Proxy 对这个属性的set代理将不会生效。 注意,set代理应当返回一个布尔值。严格模式下,set代理如果没有返回true,就会报错。 1234567891011'use strict';const handler = { set: function(obj, prop, value, receiver) { obj[prop] = receiver; // 无论有没有下面这一行,都会报错 return false; }};const proxy = new Proxy({}, handler);proxy.foo = 'bar';// TypeError: 'set' on proxy: trap returned falsish for property 'foo' 上面代码中,严格模式下,set代理返回false或者undefined,都会报错。 apply()apply方法拦截函数的调用、call和apply操作。 apply方法可以接受三个参数,分别是目标对象、目标对象的上下文对象(this)和目标对象的参数数组。 12345var handler = { apply (target, ctx, args) { return Reflect.apply(...arguments); }}; 下面是一个例子。 1234567891011var target = function () { return 'I am the target'; };var handler = { apply: function () { return 'I am the proxy'; }};var p = new Proxy(target, handler);p()// "I am the proxy" 上面代码中,变量p是 Proxy 的实例,当它作为函数调用时(p()),就会被apply方法拦截,返回一个字符串。 下面是另外一个例子。 123456789101112var twice = { apply (target, ctx, args) { return Reflect.apply(...arguments) * 2; }};function sum (left, right) { return left + right;};var proxy = new Proxy(sum, twice);proxy(1, 2) // 6proxy.call(null, 5, 6) // 22proxy.apply(null, [7, 8]) // 30 上面代码中,每当执行proxy函数(直接调用或call和apply调用),就会被apply方法拦截。 另外,直接调用Reflect.apply方法,也会被拦截。 1Reflect.apply(proxy, null, [9, 10]) // 38 has()has()方法用来拦截HasProperty操作,即判断对象是否具有某个属性时,这个方法会生效。典型的操作就是in运算符。 has()方法可以接受两个参数,分别是目标对象、需查询的属性名。 下面的例子使用has()方法隐藏某些属性,不被in运算符发现。 1234567891011var handler = { has (target, key) { if (key[0] === '_') { return false; } return key in target; }};var target = { _prop: 'foo', prop: 'foo' };var proxy = new Proxy(target, handler);'_prop' in proxy // false 上面代码中,如果原对象的属性名的第一个字符是下划线,proxy.has()就会返回false,从而不会被in运算符发现。 如果原对象不可配置或者禁止扩展,这时has()拦截会报错。 12345678910var obj = { a: 10 };Object.preventExtensions(obj);var p = new Proxy(obj, { has: function(target, prop) { return false; }});'a' in p // TypeError is thrown 上面代码中,obj对象禁止扩展,结果使用has拦截就会报错。也就是说,如果某个属性不可配置(或者目标对象不可扩展),则has()方法就不得“隐藏”(即返回false)目标对象的该属性。 值得注意的是,has()方法拦截的是HasProperty操作,而不是HasOwnProperty操作,即has()方法不判断一个属性是对象自身的属性,还是继承的属性。 另外,虽然for...in循环也用到了in运算符,但是has()拦截对for...in循环不生效。 12345678910111213141516171819202122232425262728293031323334let stu1 = {name: '张三', score: 59};let stu2 = {name: '李四', score: 99};let handler = { has(target, prop) { if (prop === 'score' && target[prop] < 60) { console.log(`${target.name} 不及格`); return false; } return prop in target; }}let oproxy1 = new Proxy(stu1, handler);let oproxy2 = new Proxy(stu2, handler);'score' in oproxy1// 张三 不及格// false'score' in oproxy2// truefor (let a in oproxy1) { console.log(oproxy1[a]);}// 张三// 59for (let b in oproxy2) { console.log(oproxy2[b]);}// 李四// 99 上面代码中,has()拦截只对in运算符生效,对for...in循环不生效,导致不符合要求的属性没有被for...in循环所排除。 3.this 问题虽然 Proxy 可以代理针对目标对象的访问,但它不是目标对象的透明代理,即不做任何拦截的情况下,也无法保证与目标对象的行为一致。主要原因就是在 Proxy 代理的情况下,目标对象内部的this关键字会指向 Proxy 代理。 1234567891011const target = { m: function () { console.log(this === proxy); }};const handler = {};const proxy = new Proxy(target, handler);target.m() // falseproxy.m() // true 上面代码中,一旦proxy代理target,target.m()内部的this就是指向proxy,而不是target。 下面是一个例子,由于this指向的变化,导致 Proxy 无法代理目标对象。 12345678910111213141516const _name = new WeakMap();class Person { constructor(name) { _name.set(this, name); } get name() { return _name.get(this); }}const jane = new Person('Jane');jane.name // 'Jane'const proxy = new Proxy(jane, {});proxy.name // undefined 上面代码中,目标对象jane的name属性,实际保存在外部WeakMap对象_name上面,通过this键区分。由于通过proxy.name访问时,this指向proxy,导致无法取到值,所以返回undefined。 此外,有些原生对象的内部属性,只有通过正确的this才能拿到,所以 Proxy 也无法代理这些原生对象的属性。 123456const target = new Date();const handler = {};const proxy = new Proxy(target, handler);proxy.getDate();// TypeError: this is not a Date object. 上面代码中,getDate()方法只能在Date对象实例上面拿到,如果this不是Date对象实例就会报错。这时,this绑定原始对象,就可以解决这个问题。 123456789101112const target = new Date('2015-01-01');const handler = { get(target, prop) { if (prop === 'getDate') { return target.getDate.bind(target); } return Reflect.get(target, prop); }};const proxy = new Proxy(target, handler);proxy.getDate() // 1 另外,Proxy 拦截函数内部的this,指向的是handler对象。 1234567891011121314151617181920const handler = { get: function (target, key, receiver) { console.log(this === handler); return 'Hello, ' + key; }, set: function (target, key, value) { console.log(this === handler); target[key] = value; return true; }};const proxy = new Proxy({}, handler);proxy.foo// true// Hello, fooproxy.foo = 1// true 上面例子中,get()和set()拦截函数内部的this,指向的都是handler对象。","link":"/JavaScript/22.Proxy/"},{"title":"Reflect","text":"1.ReflectReflect对象与Proxy对象一样,也是ES6为了操作对象而提供的新API。Reflect对象设计的目的有这样几个 将Object对象的一些明显属于语言内部的方法(比如Object.defineProperty),放到Reflect对象上。现阶段,某些方法同时在Object和Reflect对象上部署,未来的新方法将只部署在Reflect对象上。也就是说,从Reflect对象上可以拿到语言内部的方法。 修改某些Object方法的返回结果,让其变得更合理。比如,Object.defineProperty(obj, name, desc)在无法定义属性时,会抛出一个错误,而Reflect.defineProperty(obj, name, desc)则会返回false。 1234567891011121314// 老写法try { Object.defineProperty(target, property, attributes); // success} catch (e) { // failure}// 新写法if (Reflect.defineProperty(target, property, attributes)) { // success} else { // failure} 让Object操作都变成函数行为,某些Object操作是命令式,比如name in obj和delete obj[name],而Reflect.has(obj, name)和Reflect.deleteProperty(obj, name)让它们变成了函数行为。 12345// 老写法'assign' in Object // true// 新写法Reflect.has(Object, 'assign') // true Reflect对象的方法与Proxy对象的方法一一对应,只要是Proxy对象的方法,就能在Reflect对象上找到对应的方法。这就让Proxy对象可以方便地调用对应的Reflect方法,完成默认行为,作为修改行为的基础。也就是说,不管Proxy怎么修改默认行为,你总可以在Reflect上获取默认行为 123456789Proxy(target, { set: function(target, name, value, receiver) { var success = Reflect.set(target, name, value, receiver); if (success) { console.log('property ' + name + ' on ' + target + ' set to ' + value); } return success; }}); 上面代码中,Proxy方法拦截target对象的属性赋值行为。它采用Reflect.set方法将值赋值给对象的属性,确保完成原有的行为,然后再部署额外的功能 1234567891011121314var loggedObj = new Proxy(obj, { get(target, name) { console.log('get', target, name); return Reflect.get(target, name); }, deleteProperty(target, name) { console.log('delete' + name); return Reflect.deleteProperty(target, name); }, has(target, name) { console.log('has' + name); return Reflect.has(target, name); }}); 上面代码中,每一个Proxy对象的拦截操作(get、delete、has),内部都调用对应的Reflect方法,保证原生行为能够正常执行。添加的工作,就是将每一个操作输出一行日志。 有了Reflect对象以后,很多操作会更易读。 12345// 老写法Function.prototype.apply.call(Math.floor, undefined, [1.75]) // 1// 新写法Reflect.apply(Math.floor, undefined, [1.75]) // 1 2.静态方法Reflect对象一共有13个静态方法 Reflect.apply(target, thisArg, args) Reflect.construct(target, args) Reflect.get(target, name, receiver) Reflect.set(target, name, value, receiver) Reflect.defineProperty(target, name, desc) Reflect.deleteProperty(target, name) Reflect.has(target, name) Reflect.ownKeys(target) Reflect.isExtensible(target) Reflect.preventExtensions(target) Reflect.getOwnPropertyDescriptor(target, name) Reflect.getPrototypeOf(target) Reflect.setPrototypeOf(target, prototype) 上面这些方法的作用,大部分与Object对象的同名方法的作用都是相同的,而且它与Proxy对象的方法是一一对应的。下面是对它们的解释。 Reflect.get(target, name, reciver)Reflect.get方法查找并返回target对象的name属性的值,如果没有该属性,则返回undefined。 1234567891011var myObject = { foo: 1, bar: 2, get baz() { return this.foo + this.bar; },}Reflect.get(myObject, 'foo') // 1Reflect.get(myObject, 'bar') // 2Reflect.get(myObject, 'baz') // 3 Reflect.set(target, name, value, receiver)Reflect.set方法设置target对象的name属性等于value 1234567891011121314var myObject = { foo: 1, set bar(value) { return this.foo = value; },}myObject.foo // 1Reflect.set(myObject, 'foo', 2);myObject.foo // 2Reflect.set(myObject, 'bar', 3)myObject.foo // 3 如果name属性设置了赋值函数,则赋值函数的this绑定receiver。 1234567891011121314var myObject = { foo: 4, set bar(value) { return this.foo = value; },};var myReceiverObject = { foo: 0,};Reflect.set(myObject, 'bar', 1, myReceiverObject);myObject.foo // 4myReceiverObject.foo // 1 注意,如果 Proxy对象和 Reflect对象联合使用,前者拦截赋值操作,后者完成赋值的默认行为,而且传入了receiver,那么Reflect.set会触发Proxy.defineProperty拦截。 12345678910111213141516171819let p = { a: 'a'};let handler = { set(target, key, value, receiver) { console.log('set'); Reflect.set(target, key, value, receiver) }, defineProperty(target, key, attribute) { console.log('defineProperty'); Reflect.defineProperty(target, key, attribute); }};let obj = new Proxy(p, handler);obj.a = 'A';// set// defineProperty 上面代码中,Proxy.set拦截里面使用了Reflect.set,而且传入了receiver,导致触发Proxy.defineProperty拦截。这是因为Proxy.set的receiver参数总是指向当前的 Proxy实例(即上例的obj),而Reflect.set一旦传入receiver,就会将属性赋值到receiver上面(即obj),导致触发defineProperty拦截。如果Reflect.set没有传入receiver,那么就不会触发defineProperty拦截。 123456789101112131415161718let p = { a: 'a'};let handler = { set(target, key, value, receiver) { console.log('set'); Reflect.set(target, key, value) }, defineProperty(target, key, attribute) { console.log('defineProperty'); Reflect.defineProperty(target, key, attribute); }};let obj = new Proxy(p, handler);obj.a = 'A';// set 如果第一个参数不是对象,Reflect.set会报错。 12Reflect.set(1, 'foo', {}) // 报错Reflect.set(false, 'foo', {}) // 报错 Reflect.has(obj, name)Reflect.has方法对应name in obj里面的in运算符。 123456789var myObject = { foo: 1,};// 旧写法'foo' in myObject // true// 新写法Reflect.has(myObject, 'foo') // true 如果Reflect.has()方法的第一个参数不是对象,会报错。 Reflect.deleteProperty(obj, name)Reflect.deleteProperty方法等同于delete obj[name],用于删除对象的属性。 1234567const myObj = { foo: 'bar' };// 旧写法delete myObj.foo;// 新写法Reflect.deleteProperty(myObj, 'foo'); 该方法返回一个布尔值。如果删除成功,或者被删除的属性不存在,返回true;删除失败,被删除的属性依然存在,返回false。 如果Reflect.deleteProperty()方法的第一个参数不是对象,会报错。 Reflect.construct(target, args)Reflect.construct方法等同于new target(...args),这提供了一种不使用new,来调用构造函数的方法。 123456789function Greeting(name) { this.name = name;}// new 的写法const instance = new Greeting('张三');// Reflect.construct 的写法const instance = Reflect.construct(Greeting, ['张三']); 如果Reflect.construct()方法的第一个参数不是函数,会报错。 Reflect.getPrototypeOf(obj)Reflect.getPrototypeOf方法用于读取对象的__proto__属性,对应Object.getPrototypeOf(obj)。 1234567const myObj = new FancyThing();// 旧写法Object.getPrototypeOf(myObj) === FancyThing.prototype;// 新写法Reflect.getPrototypeOf(myObj) === FancyThing.prototype; Reflect.getPrototypeOf和Object.getPrototypeOf的一个区别是,如果参数不是对象,Object.getPrototypeOf会将这个参数转为对象,然后再运行,而Reflect.getPrototypeOf会报错。 12Object.getPrototypeOf(1) // Number {[[PrimitiveValue]]: 0}Reflect.getPrototypeOf(1) // 报错 Reflect.setPrototypeOf(obj, newProto)Reflect.setPrototypeOf方法用于设置目标对象的原型(prototype),对应Object.setPrototypeOf(obj, newProto)方法。它返回一个布尔值,表示是否设置成功。 123456789const myObj = {};// 旧写法Object.setPrototypeOf(myObj, Array.prototype);// 新写法Reflect.setPrototypeOf(myObj, Array.prototype);myObj.length // 0 如果无法设置目标对象的原型(比如,目标对象禁止扩展),Reflect.setPrototypeOf方法返回false。 1234Reflect.setPrototypeOf({}, null)// trueReflect.setPrototypeOf(Object.freeze({}), null)// false 如果第一个参数不是对象,Object.setPrototypeOf会返回第一个参数本身,而Reflect.setPrototypeOf会报错。 12345Object.setPrototypeOf(1, {})// 1Reflect.setPrototypeOf(1, {})// TypeError: Reflect.setPrototypeOf called on non-object 如果第一个参数是undefined或null,Object.setPrototypeOf和Reflect.setPrototypeOf都会报错。 12345Object.setPrototypeOf(null, {})// TypeError: Object.setPrototypeOf called on null or undefinedReflect.setPrototypeOf(null, {})// TypeError: Reflect.setPrototypeOf called on non-object Reflect.apply(func, thisArg, args)Reflect.apply方法等同于Function.prototype.apply.call(func, thisArg, args),用于绑定this对象后执行给定函数。 一般来说,如果要绑定一个函数的this对象,可以这样写fn.apply(obj, args),但是如果函数定义了自己的apply方法,就只能写成Function.prototype.apply.call(fn, obj, args),采用Reflect对象可以简化这种操作。 1234567891011const ages = [11, 33, 12, 54, 18, 96];// 旧写法const youngest = Math.min.apply(Math, ages);const oldest = Math.max.apply(Math, ages);const type = Object.prototype.toString.call(youngest);// 新写法const youngest = Reflect.apply(Math.min, Math, ages);const oldest = Reflect.apply(Math.max, Math, ages);const type = Reflect.apply(Object.prototype.toString, youngest, []); Reflect.defineProperty(target, propertyKey, attributes)Reflect.defineProperty方法基本等同于Object.defineProperty,用来为对象定义属性。未来,后者会被逐渐废除,请从现在开始就使用Reflect.defineProperty代替它。 12345678910111213function MyDate() { /*…*/}// 旧写法Object.defineProperty(MyDate, 'now', { value: () => Date.now()});// 新写法Reflect.defineProperty(MyDate, 'now', { value: () => Date.now()}); 如果Reflect.defineProperty的第一个参数不是对象,就会抛出错误,比如Reflect.defineProperty(1, 'foo')。 这个方法可以与Proxy.defineProperty配合使用。 1234567891011const p = new Proxy({}, { defineProperty(target, prop, descriptor) { console.log(descriptor); return Reflect.defineProperty(target, prop, descriptor); }});p.foo = 'bar';// {value: "bar", writable: true, enumerable: true, configurable: true}p.foo // "bar" 上面代码中,Proxy.defineProperty对属性赋值设置了拦截,然后使用Reflect.defineProperty完成了赋值。 Reflect.getOwnPropertyDescriptor(target, propertyKey)Reflect.getOwnPropertyDescriptor基本等同于Object.getOwnPropertyDescriptor,用于得到指定属性的描述对象,将来会替代掉后者。 1234567891011var myObject = {};Object.defineProperty(myObject, 'hidden', { value: true, enumerable: false,});// 旧写法var theDescriptor = Object.getOwnPropertyDescriptor(myObject, 'hidden');// 新写法var theDescriptor = Reflect.getOwnPropertyDescriptor(myObject, 'hidden'); Reflect.getOwnPropertyDescriptor和Object.getOwnPropertyDescriptor的一个区别是,如果第一个参数不是对象,Object.getOwnPropertyDescriptor(1, 'foo')不报错,返回undefined,而Reflect.getOwnPropertyDescriptor(1, 'foo')会抛出错误,表示参数非法。 Reflect.isExtensible (target)Reflect.isExtensible方法对应Object.isExtensible,返回一个布尔值,表示当前对象是否可扩展。 1234567const myObject = {};// 旧写法Object.isExtensible(myObject) // true// 新写法Reflect.isExtensible(myObject) // true 如果参数不是对象,Object.isExtensible会返回false,因为非对象本来就是不可扩展的,而Reflect.isExtensible会报错。 12Object.isExtensible(1) // falseReflect.isExtensible(1) // 报错 Reflect.preventExtensions(target)Reflect.preventExtensions对应Object.preventExtensions方法,用于让一个对象变为不可扩展。它返回一个布尔值,表示是否操作成功。 1234567var myObject = {};// 旧写法Object.preventExtensions(myObject) // Object {}// 新写法Reflect.preventExtensions(myObject) // true 如果参数不是对象,Object.preventExtensions在 ES5 环境报错,在 ES6 环境返回传入的参数,而Reflect.preventExtensions会报错。 12345678// ES5 环境Object.preventExtensions(1) // 报错// ES6 环境Object.preventExtensions(1) // 1// 新写法Reflect.preventExtensions(1) // 报错 Reflect.ownKeys (target)Reflect.ownKeys方法用于返回对象的所有属性,基本等同于Object.getOwnPropertyNames与Object.getOwnPropertySymbols之和。 1234567891011121314151617var myObject = { foo: 1, bar: 2, [Symbol.for('baz')]: 3, [Symbol.for('bing')]: 4,};// 旧写法Object.getOwnPropertyNames(myObject)// ['foo', 'bar']Object.getOwnPropertySymbols(myObject)//[Symbol(baz), Symbol(bing)]// 新写法Reflect.ownKeys(myObject)// ['foo', 'bar', Symbol(baz), Symbol(bing)] 如果Reflect.ownKeys()方法的第一个参数不是对象,会报错 3.实例:使用 Proxy 实现观察者模式观察者模式(Observer mode)指的是函数自动观察数据对象,一旦对象有变化,函数就会自动执行。 12345678910111213const person = observable({ name: '张三', age: 20});function print() { console.log(`${person.name}, ${person.age}`)}observe(print);person.name = '李四';// 输出// 李四, 20 上面代码中,数据对象person是观察目标,函数print是观察者。一旦数据对象发生变化,print就会自动执行。 下面,使用 Proxy 写一个观察者模式的最简单实现,即实现observable和observe这两个函数。思路是observable函数返回一个原始对象的 Proxy 代理,拦截赋值操作,触发充当观察者的各个函数。 12345678910const queuedObservers = new Set();const observe = fn => queuedObservers.add(fn);const observable = obj => new Proxy(obj, {set});function set(target, key, value, receiver) { const result = Reflect.set(target, key, value, receiver); queuedObservers.forEach(observer => observer()); return result;} 上面代码中,先定义了一个Set集合,所有观察者函数都放进这个集合。然后,observable函数返回原始对象的代理,拦截赋值操作。拦截函数set之中,会自动执行所有观察者。","link":"/JavaScript/23.Reflect/"},{"title":"JavaScript笔记","text":"1.深拷贝和浅拷贝 浅拷贝:创建一个对象,这个对象有着原始对象的一份精确拷贝。如果属性是基本数据类型,拷贝的就是基本类型的值,如果是引用类型,拷贝的就是内存地址,所以如果其中一个对象改变了这个地址,就会影响到另一个对象 深拷贝:将一个对象从一个内存中完整地拷贝出来,从堆内存中开辟一个新的区域存放这个新对象,新对象的修改不会影响原对象 浅拷贝实现 Object.assign() 扩展运算符(…) Array.prototype.slice() 深拷贝实现 JSON.parase(JSON.stringify(obj)) 会忽略undefined Symbol 不能序列化函数 不能解决循环引用的对象 不能正确处理 new Date() 不能处理正则 手写 123456789101112131415function deepClone(obj) { let res; if(Object.prototype.toString.call(obj).slice(8,-1) === 'Object') { res = {} } else if(Array.isArray(obj)) { res = [] } else { return obj } for(const key in obj) { res[key] = deepClone(obj[key]) } return res;} 2.跨域1.同源策略跨越的本质其实就是指两个地址不同源,同源指的是:两个URL的协议,域名和端口号都相同,则就是两个同源的URL 1234567// 非同源http://www.baidu.comhttps://www.baidu.com// 同源http://www.baidu.comhttp://www.baidu.com?query=1 同源策略是一个重要的安全策略,它用于限制一个origin的文档或者它加载的脚本如何能与另一个源的资源进行交互。其主要目的是为了保护用户信息的安全,防止恶意网站窃取数据,是浏览器在Web页面层面做的安全保护 2.同源策略的表现同源策略主要的限制有三个层面:DOM层面,数据层面和网络层面 DOM层面同源策略限制了来自不同源的JavaScript脚本对当前源的DOM对象进行读和写的操作 数据层面同源策略限制了不同源站点读取当前站点的Cookie,IndexDB,LocalStorage等数据 网络层面同源策略限制了通过XMLHttpRequest等方式将站点的数据发送给不同源的站点 3.跨域分类同源策略虽然保证了浏览器的安全,但有时候我们需要访问不同源的数据等,因此有时我们需要进行跨越操作 1.DOM层面片段标识符 片段标识符的核心原理就是通过监听url中hash的改变来实现数据的传递 123456789101112131415161718192021// 父页面parentHtml.html<!DOCTYPE html><html lang="zh"> <head> <title></title> </head> <body> 我是父页面 <button id='btn'>父传给子</button> <iframe src="./childHtml.html" id="childHtmlId"></iframe> </body> <script> window.onhashchange = function() { console.log(decodeURIComponent(window.location.hash)); }; document.getElementById('btn').addEventListener('click', () => { const iframeDom = document.getElementById('childHtmlId'); iframeDom.src += '#父传给子'; }); </script></html> 1234567891011121314151617181920// 子页面childHtml.html<!DOCTYPE html><html lang="zh"> <head> <title></title> </head> <body> 我是子页面 <button id='btn'>子传给父</button> </body> <script> window.onhashchange = function() { console.log(decodeURIComponent(window.location.hash)); }; document.getElementById('btn').addEventListener('click', () => { parent.location.href += '#子传给父'; }); </script></html> window.name 浏览器窗口有window.name属性,这个属性最大的特点就是,无论是否同源,只要在同一个窗口里面,前一个网页设置的属性后一个网页就可以读取它。如果需要实现父页面和跨域的子页面之间的通信,需要一个和父页面同源的子页面作为中介,将跨域的子页面中的信息传递过来。 document.domain document.domain是存放文档的服务器主机名,可通过手动设置将其设置成当前域名或者上级域名,当具有同document.domain的页面就相当于处于同域名的服务器上,如果其域名和端口号相同,就可以实现跨越访问资源 postMessage postMessage是HTML5新增的跨文档通信API 通过监听message事件来接受数据 通过contentWindow.postMessage()函数来发生数据 123456789101112131415161718192021// 父页面<!DOCTYPE html><html lang="zh"> <head> <title></title> </head> <body> 我是父页面 <button id='btn'>父传给子</button> <iframe src="http://127.0.0.1:5500/024/childHtml.html" id="childHtmlId"></iframe> </body> <script> window.addEventListener('message', function(event) { console.log('父页面接收到信息', event.data); }); document.getElementById('btn').addEventListener('click', () => { const iframeDom = document.getElementById('childHtmlId'); iframeDom.contentWindow.postMessage('我是执鸢者1', 'http://127.0.0.1:5500/024/childHtml1.html'); }); </script></html> 1234567891011121314151617181920// 子页面<!DOCTYPE html><html lang="zh"> <head> <title></title> </head> <body> 我是子页面 <button id='btn'>子传给父</button> </body> <script> window.addEventListener('message', function(event) { console.log('子页面接收到信息', event.data); }); document.getElementById('btn').addEventListener('click', () => { parent.postMessage('我是执鸢者2', 'http://127.0.0.1:5500/024/parentHtml1.html'); }); </script></html> 2.网络层面同源策略对网络层面的限制主要在于不允许通过XMLHttpRequest等方式访问非同源站点的资源,目前主要的解决方法有三种 通过代理实现 同源策略主要是浏览器为了安全而制定的策略,而服务端之间不存在这样的限制,因此可以先将请求发送到同源的服务器上,然后通过同源服务器代理至最终服务器,从而实现跨域访问资源,比如Node中间件代理,Nginx方向代理等 NodeJS代理 123456789101112131415161718192021222324252627282930313233343536// server1.js 代理服务器(http://localhost:3000)const http = require('http')// 第一步:接受客户端请求const server = http.createServer((request, response) => { // 代理服务器,直接和浏览器直接交互,需要设置CORS 的首部字段 response.writeHead(200, { 'Access-Control-Allow-Origin': '*', 'Access-Control-Allow-Methods': '*', 'Access-Control-Allow-Headers': 'Content-Type' }) // 第二步:将请求转发给服务器 http.request( { host: '127.0.0.1', port: 4000, url: '/', method: request.method, headers: request.headers }, serverResponse => { // 第三步:收到服务器的响应 var body = '' serverResponse.on('data', chunk => { body += chunk }) serverResponse.on('end', () => { console.log('The data is ' + body) // 第四步:将响应结果转发给浏览器 response.end(body) }) } ).end()})server.listen(3000, () => { console.log('The proxyServer is running at http://localhost:3000')}) JSONP JSONP的原理其实就是利用script标签不会被同源策略限制的特点,通过监听一个回调函数,将这个回调函数的函数名作为参数发送给服务端,服务端直接运行这个函数并将数据通过形参的方式传回即可 script标签特点:src属性能够访问任何URL资源,不会受到同源策略的限制。如果访问的资源包含JavaScript代码,其会在下载后自动执行 CORS 跨域共享资源,主要的原理是服务端设置Access-Control-Allow-Origin等响应头,携带这个响应头的http请求,并不会被浏览器拦截 简单请求 请求方式仅限于GET POST HEAD Content-Type仅限于text/plain mutipart/form-data application/x-www-form-urlencoded 非简单请求 PUT DELETE方法 发送json格式 携带自定义请求头 3.原型和原型链 原型 在JavaScript中每个函数都有一个prototype属性(注意是函数,普通对象是没有的),指向一个实例原型对象,每个对象在创建的时候,都会有一个__proto__属性关联它们构造函数的实例原型对象,这个就是该对象的原型 实例原型没有指向实例的属性,但有指向构造函数的属性,每个实例原型通过constructor属性指向对应的构造函数 原型链 当我们要读取对象上的一个属性时,JavaScript引擎会先在该对象上寻找,如果找不到,就会沿着__proto__属性到它构造函数的实例原型上寻找,若实例原型上找不到,就会沿着实例原型的原型(实例原型本质上是一个对象)去Object()这个构造函数对应的实例原型上寻找,而对象的构造函数的实例原型的__proto__属性指向null,原型链就是__proto__的检索路径 3.声明变量关键字var 使用var关键字声明的变量会被直接绑定到window对象上 12var a = 2;console.log(window.a); // 2 使用var关键字声明的变量存在变量提升,也就是说即使在声明之前输出该变量也不会报错 12console.log(a) // undefinedvar a = 2; 可以重复声明变量 123var a = 1;var a = 2;console.log(a); // 2 let和const 使用let/const关键字声明的变量不存在变量提升 12console.log(a); // Errorlet a; 使用let/const关键字声明的变量存在暂时性死区 12345let a = 2;{ console.log(a); // Error let a = 3;} 使用let/const关键字声明的变量无法重复声明 123let a = 1;let a = 2;console.log(a); // Error 使用let/const关键字声明的变量,存在块级作用域 123456let a = 1;{ let a = 2; console.log(a); // 2}console.log(a); // 1 使用const关键字声明变量时,必须指定初始值 12const a;console.log(a); // Error 4.作用域作用域,指的是变量存在的范围。在JavaScript中,一共有三种作用域,分别是全局作用域,函数作用域和块级作用域 全局作用域在全局声明的变量存在于全局作用域中 12345var a = 1;function fn() { console.log(a); // 1} 函数作用域在函数内声明的变量,无法在函数外获取 123456function fn() { var a = 1; console.log(a); // 1}fn();console.log(a); // Error 对于var关键字来说,局部变量只能在函数内部声明,在其它区块中声明,一律都是全局变量 1234if (true) { var x = 5;}console.log(x); // 5 函数内部的变量提升与全局作用域一样,函数作用域内部也会产生变量提升现象 12345678910111213function fn() { console.log(x); // undefined if (false) { var x; }}fn();// 等同于function fn() { var x; console.log(x); // undefined} 函数本身的作用域函数本身也是一个值,也有自己的作用域,它的作用域和其它变量一样,就是声明时所在的作用域,与其运行时所在的作用域无关 123456789101112var a = 1;function x() { console.log(a);}function f() { var a = 2; x();}f() // 1 即使传入一个回调函数,其作用域也是绑定在其定义时所在的作用域 123456789101112var a = 1;function x() { console.log(a);}function f(fn) { var a = 2; fn();}f(x) // 1 同样的,如果在函数内部定义的函数,其作用域就是绑定在函数内部 12345678910111213var a = 1;function fn() { var a = 2; return function () { console.log(a); }}var x = fn();x(); // 2 5.PromisePromise是异步编程的一种解决方案,提供统一的API用来处理各种异步操作。简单来说,Promise就是一个容器,里面保存着某个未来才会结束的事件(通常是异步操作)的结果;从语法上来讲,Promise是一个对象,从它可以获取异步操作的消息。 Promise对象有两个特点: 一是对象的状态不受外界的影响。它有三种状态,pending(进行中),fulfilled(已完成)和rejected(已失败),只有异步操作的结果,才能决定当前是哪一种状态 二是一旦状态改变,就不会再变,Promise对象状态的改变只有两种可能:从pending变为fulfilled和从pending变为rejected Promise对象的缺点: 无法取消,一旦新建它就会立即执行。 如果不设置回调函数,Promise内部抛出的错误,不会反应到外部 当处于pending状态时,无法得知目前进展到哪一个阶段(刚刚开始还是即将完成) 构造函数上的方法 Promise.all():接受一个数组,数组中的每个成员都是一个Promise实例,返回一个新的Promise实例 只有成员中所有状态都变成resolve,新实例的状态才会变成resolve,只要有一个成员的状态变成reject,新实例的状态就会变成reject 如果成员有自己的catch方法,那么当该成员状态变成reject时,新实例的状态并不会变为reject Promise.allSettled() 只有所有成员的状态发生了改变,包装实例才会结束 返回一个Promise对象,对象包裹着一个对象数组,每个对象有status和value字段,如果status的值为rejected,则返回reason字段 如果成员有自己的catch方法,则不会触发allSettled()的reject状态 Promise.race() race意为赛跑,只要有一个成员的状态率先改变,那么race的状态就直接发生改变并接受那个成员的返回值 如果成员有自己的catch方法,则不会触发race状态的改变 Promise.any() 与race类似,只要有一个参数实例变成resolve状态,包装器就会变为fulfilled状态,如果所有参数实例都变成rejected状态,包装器实例就会变成rejected状态 Promise.resolve():将一个对象转为Promise对象 参数分为四种情况,分别如下: 参数是一个Promise实例,则原封不动地返回这个实例 参数是一个thenable对象,则会将这个对象转为Promise对象,并立即执行该对象的then方法 参数不是具有then方法的对象,或根本不是对象,Promise.resolve()会返回一个新的Promise对象,并且状态为resolve 没有参数时,直接返回一个resolve状态的Promise对象 Promise.reject():返回一个状态为rejected的Promise对象 实例上的方法 then():是Promise实例状态发生改变时的回调,第一个参数是resolve状态的回调函数,第二个参数是rejected状态的回调函数,then方法返回值是一个Promise实例,这也是Promise可以进行链式书写的原因 catch():catch方法是then方法第二个回调函数的别名,用于指定发生错误时的回调函数 finally():用于指定不管Promise对象最后状态如何,都会执行的操作 6.继承 原型链继承 直接将父类的实例赋值给子类的原型对象,缺点是所有继承的属性和方法都会在子类实例对象之间共享,无法做到属性私有化 12345678910111213function Parent() { this.name = 'Parent';}Parent.prototype.getName = function() { return this.name;}function Child() {}Child.prototype = new Parent();const c = new Child();const c2 = new Child2(); 盗用构造函数继承 在子类构造函数中使用call(或apply)方法调用父类构造函数,缺点是子类无法使用父类原型对象上的属性和方法 1234567891011121314function Parent() { this.name = 'Parent';}Parent.prototype.getName = function() { return this.name;}function Child() { Parent.call(this)}const c = new Child();const c2 = new Child2(); 组合式继承 结合原型链继承和盗用构造函数继承,缺点是父类的构造函数会被实例化两次,造成性能浪费 12345678910111213141516function Parent() { this.name = 'Parent';}Parent.prototype.getName = function() { return this.name;}function Child() { // 第二次调用父类 Parent.call(this)}Child.prototype = new Parent();// 第一次调用父类Child.prototype.constructor = Child;const c = new Child();const c2 = new Child2(); 原型式继承 原型式继承可以无需明确定义构造函数而实现继承。使用Object.create()方法,对现有的普通对象进行一份浅拷贝,优点是无需调用构造函数,缺点是对象中的引用值共享同一内存,很可能造成值的篡改 12345678910const parent = { name: 'parent', age: 18, getName: function() { return this.name; }}const child1 = Object.create(parent);const child2 = Object.create(parent); 寄生式继承 和原型式继承类型,多了一个用于继承的函数,在函数中会先基于原对象创建一个新的对象,然后再增强这个新对象,最后返回新对象 1234567891011121314151617function _extend(parent) { const object = Object.create(object); object.prototype.getAge = function() { return this.age; } return object;}const parent = { name: 'parent', age: 18, getName: function() { return this.name; }}const child = _extend(parent); 寄生组合式继承 在组合继承的基础上加入寄生式继承,减少一次父类的调用 12345678910111213141516171819function _extend(parent, child) { const object = Object.create(parent.prototype); object.constructor = child; // 手动指定原型对象上的constructor指向子类 child.prototype = object;}function Parent() { this.name = 'Parent';}Parent.prototype.getName = function() { return this.name;}function Child() { Parent.call(this);}_extend(Parent, Child);const c = new Child(); 7.手写new 创建一个新对象 将该对象的原型指向构造函数的原型对象 调用call(或apply)方法,将构造函数的this指向该对象 判断构造函数的返回值是否是对象,若是则直接返回该对象,否则就返回这个创建的临时对象 123456function _new(tarent, ...rest) { const object = {}; Object.setPrototypeOf(object, target.prototype); const result = tarent.apply(object, rest); return (result instanceOf Object) ? result : object;} 8.闭包一个函数和其周围状态的引用捆绑在一起,这样的组合就是闭包,闭包让你可以在一个内层函数中,访问到外层函数的作用域。 在JavaScript中,任何闭包的使用场景基本上包含两点:创建私有变量,延长变量的生命周期 柯里化函数:柯里化的目的在于避免频繁调用具有相同参数函数的同时,又能轻松的复用。 函数的防抖和节流 9.函数防抖和节流 防抖:一个事件在n秒后执行一次,若在n秒内被重复触发,则重新计时 12345678910111213141516171819202122232425262728293031323334353637function debounce(fn: Function,wait: number) { let timer = 0; return function(...args) { if(timer) { window.clearTimeout(timer); } timer = window.setTimeout(() => { fn.apply(this, args); }, wait) }}// 立即执行版本function debounce(fn: Function, wait: number, immdiately = false) { let timer = 0; return function (...args: unknown[]) { if (timer) { window.clearTimeout(timer); } if (immdiately) { let callNow = !timer; timer = window.setTimeout(() => { timer = 0; }, wait); if (callNow) { fn.apply(this, args); } } else { timer = window.setTimeout(() => { fn.apply(this, args); }, wait); } };} 节流:一个事件在n秒内只执行一次,若在n秒内重复触发,只有一次生效 12345678910function throttled(fn: Function, delay: number) { let timer = 0; return function(...args) { if(timer) return; timer = window.setTimeout(() => { fn.apply(this, args); timer = 0; }, delay) }} 10.async awaitasync函数是generator和Promise的语法糖,它可以让我们以同步的形式去处理异步问题,async函数返回的一定是一个Promise对象,内部可以使用await关键字,返回异步信息的结果,await关键字后面通常也是跟着一个Promise对象,也可以跟一个基本值 手写实现async awiat函数的效果 12345678910111213141516171819202122232425262728function asyncGenerator(generatorFunc) { return function () { const gen = generatorFunc.apply(this, arguments); return new Promise((resolve, reject) => { function step(key, arg) { let generatorResult; try { generatorResult = gen[key](arg); } catch (error) { reject(error) } const { value, done } = generatorResult; if (done) { return resolve(value) } else { return Promise.resolve(value).then( (val) => step('next', val), (err) => step('throw', err) ) } } step('next'); }) }} 11.异步编程的实现方案 回调函数:最常见的异步编程解决方式,缺点是多个回调函数嵌套会造成回调地狱,不利于维护 Promise:使用Promise可以将嵌套的回调函数作为链式调用,但多个then的链式调用,可能会造成代码语义不够明确 generator:generator函数可以在函数执行过程中,将函数的执行权转移出去,在函数外部我们还可以将执行权转移回来,当我们遇到异步函数执行的时候,将函数执行权转移出去,当异步函数执行完毕的时候我们再将执行权转移回来。因此我们在generator内部对于异步操作的方式,可以以同步的顺序来写。只需要我们考虑何时将函数执行权转移回来。所以我们需要一个自动执行generator的机制,比如co模块等方式来实现generator的自动执行 async函数:async函数其实就是generator和promise实现的一个自动执行的语法糖,它内部自带执行器,当函数内部执行到一个await语句时,如果语句返回一个promise对象,那么函数将会等待promise对象的状态变为resolve后再继续向下执行。因此我们可以将异步逻辑转换为同步的顺序来书写。 12.co模块的原理Generator函数在处理异步操作时,需要一种自动执行的机制,当异步操作有了结果,能够自动交回执行权,两种方法可以做到这一点: 回调函数:将异步操作包装成Thunk函数,在回调函数里面交回执行权 Promise对象,将异步操作包装成Promise对象,用then方法交回执行权 co模块其实就是将这两种自动执行权器包转成的一个模块,所以使用co的前提条件是,Generator函数的yield命令后面,只能是Thunk函数或Promise对象 13.观察者模式和发布订阅模式 观察者模式:一个对象(观察者)订阅另一个对象(主题),当主题被激活的时候,触发观察者里面的事件 发布订阅模式:订阅者把自己想要的事件注册到调度中心,当发布者触发事件时,由调度中心统一调度订阅者注册到调度中心的代码 14.判断数据类型的方法 typeof:可以判断除null之外的基本数据类型和函数,其余的引用数据类型全部返回object instanceof:判断某个引用数据类型是否是对应构造函数的实例 1234567891011121314151617181920212223// 手写instanceoffunction _instanceof(left, right) { // 如果是基本数据类型,则直接返回false if (typeof left !== 'object' || left === null) { return false; } // 取右侧的实例原型 let rightProto = right.prototype; // 取左侧的原型 let leftProto = Object.getPrototypeOf(left); while (true) { if (leftProto === null) { return false; } if (leftProto === rightProto) { return true; } leftProto = Object.getPrototypeOf(leftProto) }} Object.prototype.toString.call():因为toString()是Object实例原型上的方法,而Array,Function等类型作为Object的实例,都重写了toString方法,不同的对象类型调用toString方法,返回的值是不相同的,Array返回元素组成的字符串,Function返回函数体等,因此想要返回具体的类型,必须直接调用Object实例原型上的方法 constructor:每个对象的原型都指向其构造函数的实例原型,而实例原型上的constructor属性又直接指向对应的构造函数,因此直接调用constructor可以判断该对象是否是某个构造函数的实例","link":"/JavaScript/JS%E9%9A%8F%E8%AE%B0/"},{"title":"字符串常用方法","text":"一、前言我们可以将字符串常用的方法归纳为增、删、改、查,需要知道字符串的特点是一旦创建了,就不可变 二、操作方法增用于将一个或多个字符串拼接成一个新字符串。除了常用的 + 和 ${} 外,还可以使用 concat进行操作 concat1234let stringValue = "hello ";let result = stringValue.concat("world");console.log(result); // "hello world"console.log(stringValue); // "hello" 删常见的删除操作有: slice() substr() substring() 这三个方法都返回调用它们的字符串和一个子字符串,而且都接收一或两个参数 1234567let stringValue = "hello world";console.log(stringValue.slice(3)); // "lo world"console.log(stringValue.substring(3)); // "lo world"console.log(stringValue.substr(3)); // "lo world"console.log(stringValue.slice(3, 7)); // "lo w"console.log(stringValue.substring(3,7)); // "lo w"console.log(stringValue.substr(3, 7)); // "lo worl" 改常见的修改方法: trim()、trimLeft()、trimRight() repeat() padStart()、padEnd() toLowerCase()、 toUpperCase() trim()、trimLeft()、trimRight()删除前、后或前后所有空格,再返回新的字符串 1234let stringValue = " hello world ";let trimmedStringValue = stringValue.trim();console.log(stringValue); // " hello world "console.log(trimmedStringValue); // "hello world" repeat()接收一个整数参数,表示要将字符串复制多少次,然后返回拼接所有副本后的结果 12let stringValue = "na ";let copyResult = stringValue.repeat(2) // na na padEnd()复制字符串,如果小于指定长度,则在相应一边填充字符,直至满足长度条件 123let stringValue = "foo";console.log(stringValue.padStart(6)); // " foo"console.log(stringValue.padStart(9, ".")); // "......foo" toLowerCase()、 toUpperCase()大小写转化 123let stringValue = "hello world";console.log(stringValue.toUpperCase()); // "HELLO WORLD"console.log(stringValue.toLowerCase()); // "hello world" 查除了通过索引的方式获取字符串的值,还可通过: chatAt() indexOf() startWith() includes() chatAt()返回给定索引位置的字符,由传给方法的整数参数指定 12const message = "abcde";console.log(message.chatAt(2)) // "c" indexOf()从字符串开头去搜索传入的字符串,并返回位置(如果没找到,则返回-1) 12const stringValue = "hello world";console.log(stringValue.indexOf("o")); // 4 startWith()查询字符串是否是已传入的字符串开头,返回布尔值 1234const message = "foobarbaz";console.log(message.startsWith("foo")) // trueconsole.log(message.startsWith("bar")); // false includes()从字符串中搜索传入的字符串,并返回一个表示是否包含的布尔值 1234const message = "foobarbaz";console.log(message.includes("bar")); // trueconsole.log(message.includes("qux")); // false 二、转换方法split把字符串按照指定的分割符,拆分成数组中的每一项 12const str = "12+23+34"const arr = str.split("+") // [12,23,34] 三、模板匹配方法针对正则表达式,字符串设计了几个方法用于匹配: match() search() replace() match()接收一个参数,可以是一个正则表达式字符串,也可以是一个 RegExp 对象,返回数组 1234const text = "cat, bat, sat, fat";const pattern = /.at/;const matches = text.match(pattern);console.log(matches[0]); // "cat" search()接收一个参数,可以是一个正则表达式字符串,也可以是一个 RegExp 对象,找到则返回匹配索引,否则返回 -1 123const text = "cat, bat, sat, fat";const pos = text.search(/at/);console.log(pos); replace()接收两个参数,第一个参数为匹配的内容,第二个参数为替换的元素(可用函数) 123const text = "cat, bat, sat, fat";const result = text.replace("at", "ond");console.log(result); // "cond, bat, sat, fat"","link":"/JavaScript/%E5%AD%97%E7%AC%A6%E4%B8%B2%E5%B8%B8%E7%94%A8%E6%96%B9%E6%B3%95/"},{"title":"字符串新增语法","text":"1.字符的Unicode表示法(了解)ES6增强了对Unicode的支持,允许采用\\uxxxx形式表示一个字符 上面代码表示,如果直接在\\u后面跟上超过0xFFFF的数值(比如\\u20BB7),JavaScript 会理解成\\u20BB+7。由于\\u20BB是一个不可打印字符,所以只会显示一个空格,后面跟着一个7。 ES6 对这一点做出了改进,只要将码点放入大括号,就能正确解读该字符。 1234567891011"\\u{20BB7}"// "𠮷""\\u{41}\\u{42}\\u{43}"// "ABC"let hello = 123;hell\\u{6F} // 123'\\u{1F680}' === '\\uD83D\\uDE80'// true 上面代码中,最后一个例子表明,大括号表示法与四字节的 UTF-16 编码是等价的。 有了这种表示法之后,JavaScript 共有 6 种方法可以表示一个字符。 12345'\\z' === 'z' // true'\\172' === 'z' // true'\\x7A' === 'z' // true'\\u007A' === 'z' // true'\\u{7A}' === 'z' // true 2.字符串遍历接口ES6为字符串添加了遍历接口(Iterator),使得字符串可以被for...of循环遍历,也可以被数组形式的解构识别 12345678910111213const str = 'hello';const [a, b, c, d, e] = 'hello';for (const i of str) { console.log(i); // h e l l o}console.log(a); // hconsole.log(b); // econsole.log(c); // lconsole.log(d); // lconsole.log(e); // o 3.JSON.stringify()的改造根据标准,JSON 数据必须是 UTF-8 编码。但是,现在的JSON.stringify()方法有可能返回不符合 UTF-8 标准的字符串。 具体来说,UTF-8 标准规定,0xD800到0xDFFF之间的码点,不能单独使用,必须配对使用。比如,\\uD834\\uDF06是两个码点,但是必须放在一起配对使用,代表字符𝌆。这是为了表示码点大于0xFFFF的字符的一种变通方法。单独使用\\uD834和\\uDF06这两个码点是不合法的,或者颠倒顺序也不行,因为\\uDF06\\uD834并没有对应的字符。 JSON.stringify()的问题在于,它可能返回0xD800到0xDFFF之间的单个码点。 1JSON.stringify('\\u{D834}') // "\\u{D834}" 为了确保返回的是合法的 UTF-8 字符,ES2019 改变了JSON.stringify()的行为。如果遇到0xD800到0xDFFF之间的单个码点,或者不存在的配对形式,它会返回转义字符串,留给应用自己决定下一步的处理。 12JSON.stringify('\\u{D834}') // ""\\\\uD834""JSON.stringify('\\uDF06\\uD834') // ""\\\\udf06\\\\ud834"" 4.模板字符串模板字符串,是增强版的字符串,用反引号(`)标识,它可以当作普通字符串使用,页可以用来定义多行字符串,或者在字符串中嵌入变量 12345678910111213// 普通字符串`In JavaScript '\\n' is a line-feed.`// 多行字符串`In JavaScript this is not legal.`console.log(`string text line 1string text line 2`);// 字符串中嵌入变量let name = "Bob", time = "today";`Hello ${name}, how are you ${time}?` 上面代码中的模板字符串,都是用反引号表示。如果在模板字符串中需要使用反引号,则前面要用反斜杠转义。 1let greeting = `\\`Yo\\` World!`; 模板字符串中嵌入变量,需要将变量名写在${}之中。 123456789101112function authorize(user, action) { if (!user.hasPrivilege(action)) { throw new Error( // 传统写法为 // 'User ' // + user.name // + ' is not authorized to do ' // + action // + '.' `User ${user.name} is not authorized to do ${action}.`); }} 由于模板字符串的大括号内部,就是执行 JavaScript 代码,因此如果大括号内部是一个字符串,将会原样输出。 12`Hello ${'World'}`// "Hello World" 模板字符串甚至还能嵌套。 12345678const tmpl = addrs => ` <table> ${addrs.map(addr => ` <tr><td>${addr.first}</td></tr> <tr><td>${addr.last}</td></tr> `).join('')} </table>`; 上面代码中,模板字符串的变量之中,又嵌入了另一个模板字符串,使用方法如下。 123456789101112131415const data = [ { first: '<Jane>', last: 'Bond' }, { first: 'Lars', last: '<Croft>' },];console.log(tmpl(data));// <table>//// <tr><td><Jane></td></tr>// <tr><td>Bond</td></tr>//// <tr><td>Lars</td></tr>// <tr><td><Croft></td></tr>//// </table> 如果需要引用模板字符串本身,在需要时执行,可以写成函数。 12let func = (name) => `Hello ${name}!`;func('Jack') // "Hello Jack!" 上面代码中,模板字符串写成了一个函数的返回值。执行这个函数,就相当于执行这个模板字符串了。 5.字符串的新增方法String.fromCodePoint()ES6提供了String.fromCodePoint()方法,可以识别大于0xFFFF的字符并返回对应的字符串 1234String.fromCodePoint(0x20BB7)// "𠮷"String.fromCodePoint(0x78, 0x1f680, 0x79) === 'x\\uD83D\\uDE80y'// true 上面代码中,如果String.fromCodePoint方法有多个参数,则它们会被合并成一个字符串返回。 注意,fromCodePoint方法定义在String对象上,而codePointAt方法定义在字符串的实例对象上。 String.raw()ES6 还为原生的 String 对象,提供了一个raw()方法。该方法返回一个斜杠都被转义(即斜杠前面再加一个斜杠)的字符串,往往用于模板字符串的处理方法。 12345String.raw`Hi\\n${2+3}!`// 实际返回 "Hi\\\\n5!",显示的是转义后的结果 "Hi\\n5!"String.raw`Hi\\u000A!`;// 实际返回 "Hi\\\\u000A!",显示的是转义后的结果 "Hi\\u000A!" 如果原字符串的斜杠已经转义,那么String.raw()会进行再次转义。 1234String.raw`Hi\\\\n`// 返回 "Hi\\\\\\\\n"String.raw`Hi\\\\n` === "Hi\\\\\\\\n" // true String.raw()方法可以作为处理模板字符串的基本方法,它会将所有变量替换,而且对斜杠进行转义,方便下一步作为字符串来使用。 String.raw()本质上是一个正常的函数,只是专用于模板字符串的标签函数。如果写成正常函数的形式,它的第一个参数,应该是一个具有raw属性的对象,且raw属性的值应该是一个数组,对应模板字符串解析后的值。 123// `foo${1 + 2}bar`// 等同于String.raw({ raw: ['foo', 'bar'] }, 1 + 2) // "foo3bar" 上面代码中,String.raw()方法的第一个参数是一个对象,它的raw属性等同于原始的模板字符串解析后得到的数组。 作为函数,String.raw()的代码实现基本如下。 12345678910String.raw = function (strings, ...values) { let output = ''; let index; for (index = 0; index < values.length; index++) { output += strings.raw[index] + values[index]; } output += strings.raw[index] return output;} 实例方法:codePointAt()JavaScript 内部,字符以 UTF-16 的格式储存,每个字符固定为2个字节。对于那些需要4个字节储存的字符(Unicode 码点大于0xFFFF的字符),JavaScript 会认为它们是两个字符。 1234567var s = "𠮷";s.length // 2s.charAt(0) // ''s.charAt(1) // ''s.charCodeAt(0) // 55362s.charCodeAt(1) // 57271 上面代码中,汉字“𠮷”(注意,这个字不是“吉祥”的“吉”)的码点是0x20BB7,UTF-16 编码为0xD842 0xDFB7(十进制为55362 57271),需要4个字节储存。对于这种4个字节的字符,JavaScript 不能正确处理,字符串长度会误判为2,而且charAt()方法无法读取整个字符,charCodeAt()方法只能分别返回前两个字节和后两个字节的值。 ES6 提供了codePointAt()方法,能够正确处理 4 个字节储存的字符,返回一个字符的码点。 123456let s = '𠮷a';s.codePointAt(0) // 134071s.codePointAt(1) // 57271s.codePointAt(2) // 97 codePointAt()方法的参数,是字符在字符串中的位置(从 0 开始)。上面代码中,JavaScript 将“𠮷a”视为三个字符,codePointAt 方法在第一个字符上,正确地识别了“𠮷”,返回了它的十进制码点 134071(即十六进制的20BB7)。在第二个字符(即“𠮷”的后两个字节)和第三个字符“a”上,codePointAt()方法的结果与charCodeAt()方法相同。 总之,codePointAt()方法会正确返回 32 位的 UTF-16 字符的码点。对于那些两个字节储存的常规字符,它的返回结果与charCodeAt()方法相同。 codePointAt()方法返回的是码点的十进制值,如果想要十六进制的值,可以使用toString()方法转换一下。 1234let s = '𠮷a';s.codePointAt(0).toString(16) // "20bb7"s.codePointAt(2).toString(16) // "61" 你可能注意到了,codePointAt()方法的参数,仍然是不正确的。比如,上面代码中,字符a在字符串s的正确位置序号应该是 1,但是必须向codePointAt()方法传入 2。解决这个问题的一个办法是使用for...of循环,因为它会正确识别 32 位的 UTF-16 字符。 123456let s = '𠮷a';for (let ch of s) { console.log(ch.codePointAt(0).toString(16));}// 20bb7// 61 另一种方法也可以,使用扩展运算符(...)进行展开运算。 123456let arr = [...'𠮷a']; // arr.length === 2arr.forEach( ch => console.log(ch.codePointAt(0).toString(16)));// 20bb7// 61 codePointAt()方法是测试一个字符由两个字节还是由四个字节组成的最简单方法。 123function is32Bit(c) { return c.codePointAt(0) > 0xFFFF;} 实例方法:normalize()许多欧洲语言有语调符号和重音符号。为了表示它们,Unicode 提供了两种方法。一种是直接提供带重音符号的字符,比如Ǒ(\\u01D1)。另一种是提供合成符号(combining character),即原字符与重音符号的合成,两个字符合成一个字符,比如O(\\u004F)和ˇ(\\u030C)合成Ǒ(\\u004F\\u030C)。 这两种表示方法,在视觉和语义上都等价,但是 JavaScript 不能识别。 1234'\\u01D1'==='\\u004F\\u030C' //false'\\u01D1'.length // 1'\\u004F\\u030C'.length // 2 上面代码表示,JavaScript 将合成字符视为两个字符,导致两种表示方法不相等。 ES6 提供字符串实例的normalize()方法,用来将字符的不同表示方法统一为同样的形式,这称为 Unicode 正规化。 12'\\u01D1'.normalize() === '\\u004F\\u030C'.normalize()// true normalize方法可以接受一个参数来指定normalize的方式,参数的四个可选值如下。 NFC,默认参数,表示“标准等价合成”(Normalization Form Canonical Composition),返回多个简单字符的合成字符。所谓“标准等价”指的是视觉和语义上的等价。 NFD,表示“标准等价分解”(Normalization Form Canonical Decomposition),即在标准等价的前提下,返回合成字符分解的多个简单字符。 NFKC,表示“兼容等价合成”(Normalization Form Compatibility Composition),返回合成字符。所谓“兼容等价”指的是语义上存在等价,但视觉上不等价,比如“囍”和“喜喜”。(这只是用来举例,normalize方法不能识别中文。) NFKD,表示“兼容等价分解”(Normalization Form Compatibility Decomposition),即在兼容等价的前提下,返回合成字符分解的多个简单字符。 12'\\u004F\\u030C'.normalize('NFC').length // 1'\\u004F\\u030C'.normalize('NFD').length // 2 上面代码表示,NFC参数返回字符的合成形式,NFD参数返回字符的分解形式。 不过,normalize方法目前不能识别三个或三个以上字符的合成。这种情况下,还是只能使用正则表达式,通过 Unicode 编号区间判断。 实例方法:includes(), startsWith(), endsWith()传统上,JavaScript 只有indexOf方法,可以用来确定一个字符串是否包含在另一个字符串中。ES6 又提供了三种新方法。 **includes()**:返回布尔值,表示是否找到了参数字符串。 **startsWith()**:返回布尔值,表示参数字符串是否在原字符串的头部。 **endsWith()**:返回布尔值,表示参数字符串是否在原字符串的尾部。 12345let s = 'Hello world!';s.startsWith('Hello') // trues.endsWith('!') // trues.includes('o') // true 这三个方法都支持第二个参数,表示开始搜索的位置。 12345let s = 'Hello world!';s.startsWith('world', 6) // trues.endsWith('Hello', 5) // trues.includes('Hello', 6) // false 上面代码表示,使用第二个参数n时,endsWith的行为与其他两个方法有所不同。它针对前n个字符,而其他两个方法针对从第n个位置直到字符串结束。 实例方法:repeat()repeat方法返回一个新字符串,表示将原字符串重复n次。 123'x'.repeat(3) // "xxx"'hello'.repeat(2) // "hellohello"'na'.repeat(0) // "" 参数如果是小数,会被取整。 1'na'.repeat(2.9) // "nana" 如果repeat的参数是负数或者Infinity,会报错。 1234'na'.repeat(Infinity)// RangeError'na'.repeat(-1)// RangeError 但是,如果参数是 0 到-1 之间的小数,则等同于 0,这是因为会先进行取整运算。0 到-1 之间的小数,取整以后等于-0,repeat视同为 0。 1'na'.repeat(-0.9) // "" 参数NaN等同于 0。 1'na'.repeat(NaN) // "" 如果repeat的参数是字符串,则会先转换成数字。 12'na'.repeat('na') // ""'na'.repeat('3') // "nanana" 实例方法:padStart(),padEnd()ES2017 引入了字符串补全长度的功能。如果某个字符串不够指定长度,会在头部或尾部补全。padStart()用于头部补全,padEnd()用于尾部补全。 12345'x'.padStart(5, 'ab') // 'ababx''x'.padStart(4, 'ab') // 'abax''x'.padEnd(5, 'ab') // 'xabab''x'.padEnd(4, 'ab') // 'xaba' 上面代码中,padStart()和padEnd()一共接受两个参数,第一个参数是字符串补全生效的最大长度,第二个参数是用来补全的字符串。 如果原字符串的长度,等于或大于最大长度,则字符串补全不生效,返回原字符串。 12'xxx'.padStart(2, 'ab') // 'xxx''xxx'.padEnd(2, 'ab') // 'xxx' 如果用来补全的字符串与原字符串,两者的长度之和超过了最大长度,则会截去超出位数的补全字符串。 12'abc'.padStart(10, '0123456789')// '0123456abc' 如果省略第二个参数,默认使用空格补全长度。 12'x'.padStart(4) // ' x''x'.padEnd(4) // 'x ' padStart()的常见用途是为数值补全指定位数。下面代码生成 10 位的数值字符串。 123'1'.padStart(10, '0') // "0000000001"'12'.padStart(10, '0') // "0000000012"'123456'.padStart(10, '0') // "0000123456" 另一个用途是提示字符串格式。 12'12'.padStart(10, 'YYYY-MM-DD') // "YYYY-MM-12"'09-12'.padStart(10, 'YYYY-MM-DD') // "YYYY-09-12" 实例方法:trimStart(),trimEnd()ES2019对字符串实例新增了trimStart()和trimEnd()这两个方法。它们的行为与trim()一致,trimStart()消除字符串头部的空格,trimEnd()消除尾部的空格。它们返回的都是新字符串,不会修改原始字符串。 12345const s = ' abc ';s.trim() // "abc"s.trimStart() // "abc "s.trimEnd() // " abc" 上面代码中,trimStart()只消除头部的空格,保留尾部的空格。trimEnd()也是类似行为。 除了空格键,这两个方法对字符串头部(或尾部)的 tab 键、换行符等不可见的空白符号也有效。 浏览器还部署了额外的两个方法,trimLeft()是trimStart()的别名,trimRight()是trimEnd()的别名 实例方法:matchAll()matchAll()方法返回一个正则表达式在当前字符串的所有匹配,详见《正则的扩展》的一章。 实例方法:replaceAll()历史上,字符串的实例方法replace()只能替换第一个匹配。 12'aabbcc'.replace('b', '_')// 'aa_bcc' 上面例子中,replace()只将第一个b替换成了下划线。 如果要替换所有的匹配,不得不使用正则表达式的g修饰符。 12'aabbcc'.replace(/b/g, '_')// 'aa__cc' 正则表达式毕竟不是那么方便和直观,ES2021 引入了replaceAll()方法,可以一次性替换所有匹配。 12'aabbcc'.replaceAll('b', '_')// 'aa__cc' 它的用法与replace()相同,返回一个新字符串,不会改变原字符串。 1String.prototype.replaceAll(searchValue, replacement) 上面代码中,searchValue是搜索模式,可以是一个字符串,也可以是一个全局的正则表达式(带有g修饰符)。 如果searchValue是一个不带有g修饰符的正则表达式,replaceAll()会报错。这一点跟replace()不同。 12345// 不报错'aabbcc'.replace(/b/, '_')// 报错'aabbcc'.replaceAll(/b/, '_') 上面例子中,/b/不带有g修饰符,会导致replaceAll()报错。 replaceAll()的第二个参数replacement是一个字符串,表示替换的文本,其中可以使用一些特殊字符串。 $&:匹配的字符串。 $ `:匹配结果前面的文本。 $':匹配结果后面的文本。 $n:匹配成功的第n组内容,n是从1开始的自然数。这个参数生效的前提是,第一个参数必须是正则表达式。 $$:指代美元符号$。 下面是一些例子。 12345678910111213141516171819202122232425// $& 表示匹配的字符串,即`b`本身// 所以返回结果与原字符串一致'abbc'.replaceAll('b', '$&')// 'abbc'// $` 表示匹配结果之前的字符串// 对于第一个`b`,$` 指代`a`// 对于第二个`b`,$` 指代`ab`'abbc'.replaceAll('b', '$`')// 'aaabc'// $' 表示匹配结果之后的字符串// 对于第一个`b`,$' 指代`bc`// 对于第二个`b`,$' 指代`c`'abbc'.replaceAll('b', `$'`)// 'abccc'// $1 表示正则表达式的第一个组匹配,指代`ab`// $2 表示正则表达式的第二个组匹配,指代`bc`'abbc'.replaceAll(/(ab)(bc)/g, '$2$1')// 'bcab'// $$ 指代 $'abc'.replaceAll('b', '$$')// 'a$c' replaceAll()的第二个参数replacement除了为字符串,也可以是一个函数,该函数的返回值将替换掉第一个参数searchValue匹配的文本。 12'aabbcc'.replaceAll('b', () => '_')// 'aa__cc' 上面例子中,replaceAll()的第二个参数是一个函数,该函数的返回值会替换掉所有b的匹配。 这个替换函数可以接受多个参数。第一个参数是捕捉到的匹配内容,第二个参数捕捉到是组匹配(有多少个组匹配,就有多少个对应的参数)。此外,最后还可以添加两个参数,倒数第二个参数是捕捉到的内容在整个字符串中的位置,最后一个参数是原字符串。 123456789const str = '123abc456';const regex = /(\\d+)([a-z]+)(\\d+)/g;function replacer(match, p1, p2, p3, offset, string) { return [p1, p2, p3].join(' - ');}str.replaceAll(regex, replacer)// 123 - abc - 456 上面例子中,正则表达式有三个组匹配,所以replacer()函数的第一个参数match是捕捉到的匹配内容(即字符串123abc456),后面三个参数p1、p2、p3则依次为三个组匹配。","link":"/JavaScript/10.%E5%AD%97%E7%AC%A6%E4%B8%B2%E7%9A%84%E6%89%A9%E5%B1%95/"},{"title":"对象的扩展","text":"1.属性的简洁表示法ES6允许在大括号里面,直接写入变量和函数,作为对象的属性和方法 1234567const foo = 'bar';const baz = {foo};baz // {foo: "bar"}// 等同于const baz = {foo: foo}; 属性的赋值器(setter)和取值器(getter),事实上也是采用这种写法。 1234567891011121314const cart = { _wheels: 4, get wheels () { return this._wheels; }, set wheels (value) { if (value < this._wheels) { throw new Error('数值太小了!'); } this._wheels = value; }} 2.属性表达式JavaScript 定义对象的属性,有两种方法。 12345// 方法一obj.foo = true;// 方法二obj['a' + 'bc'] = 123; 上面代码的方法一是直接用标识符作为属性名,方法二是用表达式作为属性名,这时要将表达式放在方括号之内。 但是,如果使用字面量方式定义对象(使用大括号),在 ES5 中只能使用方法一(标识符)定义属性。 1234var obj = { foo: true, abc: 123}; ES6 允许字面量定义对象时,用方法二(表达式)作为对象的属性名,即把表达式放在方括号内。 123456let propKey = 'foo';let obj = { [propKey]: true, ['a' + 'bc']: 123}; 3.属性的可枚举性和遍历对象的每个属性都有一个描述对象(Descriptor),用来控制该属性的行为。Object.getOwnPropertyDescriptor方法可以获取该属性的描述对象。 12345678let obj = { foo: 123 };Object.getOwnPropertyDescriptor(obj, 'foo')// {// value: 123,// writable: true,// enumerable: true,// configurable: true// } 目前,有四个操作会忽略enumerable为false的属性。 for...in循环:只遍历对象自身的和继承的可枚举的属性。 Object.keys():返回对象自身的所有可枚举的属性的键名。 JSON.stringify():只串行化对象自身的可枚举的属性。 Object.assign(): 忽略enumerable为false的属性,只拷贝对象自身的可枚举的属性。 这四个操作之中,前三个是 ES5 就有的,最后一个Object.assign()是 ES6 新增的。其中,只有for...in会返回继承的属性,其他三个方法都会忽略继承的属性,只处理对象自身的属性。实际上,引入“可枚举”(enumerable)这个概念的最初目的,就是让某些属性可以规避掉for...in操作,不然所有内部属性和方法都会被遍历到。比如,对象原型的toString方法,以及数组的length属性,就通过“可枚举性”,从而避免被for...in遍历到。 12345Object.getOwnPropertyDescriptor(Object.prototype, 'toString').enumerable// falseObject.getOwnPropertyDescriptor([], 'length').enumerable// false 上面代码中,toString和length属性的enumerable都是false,因此for...in不会遍历到这两个继承自原型的属性。 另外,ES6 规定,所有 Class 的原型的方法都是不可枚举的。 12Object.getOwnPropertyDescriptor(class {foo() {}}.prototype, 'foo').enumerable// false 总的来说,操作中引入继承的属性会让问题复杂化,大多数时候,我们只关心对象自身的属性。所以,尽量不要用for...in循环,而用Object.keys()代替。 可以通过Object.defineProperty()来创建对象属性的描述 1234Object.defineProperty({}, 'invisible', { enumerable: false, value: 'hello'}) 4.属性的遍历ES6 一共有 5 种方法可以遍历对象的属性。 (1)for…in for...in循环遍历对象自身的和继承的可枚举属性(不含 Symbol 属性)。 (2)Object.keys(obj) Object.keys返回一个数组,包括对象自身的(不含继承的)所有可枚举属性(不含 Symbol 属性)的键名。 (3)Object.getOwnPropertyNames(obj) Object.getOwnPropertyNames返回一个数组,包含对象自身的所有属性(不含 Symbol 属性,但是包括不可枚举属性)的键名。 (4)Object.getOwnPropertySymbols(obj) Object.getOwnPropertySymbols返回一个数组,包含对象自身的所有 Symbol 属性的键名。 (5)Reflect.ownKeys(obj) Reflect.ownKeys返回一个数组,包含对象自身的(不含继承的)所有键名,不管键名是 Symbol 或字符串,也不管是否可枚举。 以上的 5 种方法遍历对象的键名,都遵守同样的属性遍历的次序规则。 首先遍历所有数值键,按照数值升序排列。 其次遍历所有字符串键,按照加入时间升序排列。 最后遍历所有 Symbol 键,按照加入时间升序排列。 12Reflect.ownKeys({ [Symbol()]:0, b:0, 10:0, 2:0, a:0 })// ['2', '10', 'b', 'a', Symbol()] 上面代码中,Reflect.ownKeys方法返回一个数组,包含了参数对象的所有属性。这个数组的属性次序是这样的,首先是数值属性2和10,其次是字符串属性b和a,最后是 Symbol 属性。 5.Object.is()ES5 比较两个值是否相等,只有两个运算符:相等运算符(==)和严格相等运算符(===)。它们都有缺点,前者会自动转换数据类型,后者的NaN不等于自身,以及+0等于-0。JavaScript 缺乏一种运算,在所有环境中,只要两个值是一样的,它们就应该相等。 ES6 提出“Same-value equality”(同值相等)算法,用来解决这个问题。Object.is就是部署这个算法的新方法。它用来比较两个值是否严格相等,与严格比较运算符(===)的行为基本一致。 1234Object.is('foo', 'foo')// trueObject.is({}, {})// false 不同之处只有两个:一是+0不等于-0,二是NaN等于自身。 12345+0 === -0 //trueNaN === NaN // falseObject.is(+0, -0) // falseObject.is(NaN, NaN) // true 6.Object.assign()Object.assign()方法用于对象的合并,将源对象(source)的所有可枚举属性,复制到目标对象(target)。 1234567const target = { a: 1 };const source1 = { b: 2 };const source2 = { c: 3 };Object.assign(target, source1, source2);target // {a:1, b:2, c:3} Object.assign()方法的第一个参数是目标对象,后面的参数都是源对象。 注意,如果目标对象与源对象有同名属性,或多个源对象有同名属性,则后面的属性会覆盖前面的属性。 1234567const target = { a: 1, b: 1 };const source1 = { b: 2, c: 2 };const source2 = { c: 3 };Object.assign(target, source1, source2);target // {a:1, b:2, c:3} 7.Object.getOwnPropertyDescriptors()ES2017 引入了Object.getOwnPropertyDescriptors()方法,返回指定对象所有自身属性(非继承属性)的描述对象。 12345678910111213141516const obj = { foo: 123, get bar() { return 'abc' }};Object.getOwnPropertyDescriptors(obj)// { foo:// { value: 123,// writable: true,// enumerable: true,// configurable: true },// bar:// { get: [Function: get bar],// set: undefined,// enumerable: true,// configurable: true } } 8.__proto__,Object.setPrototypeOf(),Object.getPrototypeOf()__proto__属性__proto__属性(前后各两个下划线),用来读取或设置当前对象的原型对象(prototype)。目前,所有浏览器(包括 IE11)都部署了这个属性。 123456789// es5 的写法const obj = { method: function() { ... }};obj.__proto__ = someOtherObj;// es6 的写法var obj = Object.create(someOtherObj);obj.method = function() { ... }; 该属性没有写入 ES6 的正文,而是写入了附录,原因是__proto__前后的双下划线,说明它本质上是一个内部属性,而不是一个正式的对外的 API,只是由于浏览器广泛支持,才被加入了 ES6。标准明确规定,只有浏览器必须部署这个属性,其他运行环境不一定需要部署,而且新的代码最好认为这个属性是不存在的。因此,无论从语义的角度,还是从兼容性的角度,都不要使用这个属性,而是使用下面的Object.setPrototypeOf()(写操作)、Object.getPrototypeOf()(读操作)、Object.create()(生成操作)代替。 Object.setPrototypeOf()Object.setPrototypeOf方法的作用与__proto__相同,用来设置一个对象的原型对象(prototype),返回参数对象本身。它是 ES6 正式推荐的设置原型对象的方法。 12345// 格式Object.setPrototypeOf(object, prototype)// 用法const o = Object.setPrototypeOf({}, null); 该方法等同于下面的函数。 1234function setPrototypeOf(obj, proto) { obj.__proto__ = proto; return obj;} 下面是一个例子。 12345678910let proto = {};let obj = { x: 10 };Object.setPrototypeOf(obj, proto);proto.y = 20;proto.z = 40;obj.x // 10obj.y // 20obj.z // 40 上面代码将proto对象设为obj对象的原型,所以从obj对象可以读取proto对象的属性。 如果第一个参数不是对象,会自动转为对象。但是由于返回的还是第一个参数,所以这个操作不会产生任何效果。 123Object.setPrototypeOf(1, {}) === 1 // trueObject.setPrototypeOf('foo', {}) === 'foo' // trueObject.setPrototypeOf(true, {}) === true // true 由于undefined和null无法转为对象,所以如果第一个参数是undefined或null,就会报错。 12345Object.setPrototypeOf(undefined, {})// TypeError: Object.setPrototypeOf called on null or undefinedObject.setPrototypeOf(null, {})// TypeError: Object.setPrototypeOf called on null or undefined Object.getPrototypeOf()该方法与Object.setPrototypeOf方法配套,用于读取一个对象的原型对象。 1Object.getPrototypeOf(obj); 下面是一个例子。 123456789101112function Rectangle() { // ...}const rec = new Rectangle();Object.getPrototypeOf(rec) === Rectangle.prototype// trueObject.setPrototypeOf(rec, Object.prototype);Object.getPrototypeOf(rec) === Rectangle.prototype// false 9.Object.keys(),Object.values(),Object.entries()ES5 引入了Object.keys方法,返回一个数组,成员是参数对象自身的(不含继承的)所有可遍历(enumerable)属性的键名。 123var obj = { foo: 'bar', baz: 42 };Object.keys(obj)// ["foo", "baz"] ES2017 引入了跟Object.keys配套的Object.values和Object.entries,作为遍历一个对象的补充手段,供for...of循环使用。 1234567891011121314let {keys, values, entries} = Object;let obj = { a: 1, b: 2, c: 3 };for (let key of keys(obj)) { console.log(key); // 'a', 'b', 'c'}for (let value of values(obj)) { console.log(value); // 1, 2, 3}for (let [key, value] of entries(obj)) { console.log([key, value]); // ['a', 1], ['b', 2], ['c', 3]} 10.Object.fromEntries()Object.fromEntries()方法是Object.entries()的逆操作,用于将一个键值对数组转为对象。 12345Object.fromEntries([ ['foo', 'bar'], ['baz', 42]])// { foo: "bar", baz: 42 } 该方法的主要目的,是将键值对的数据结构还原为对象,因此特别适合将 Map 结构转为对象。 12345678910111213// 例一const entries = new Map([ ['foo', 'bar'], ['baz', 42]]);Object.fromEntries(entries)// { foo: "bar", baz: 42 }// 例二const map = new Map().set('foo', true).set('bar', false);Object.fromEntries(map)// { foo: true, bar: false } 该方法的一个用处是配合URLSearchParams对象,将查询字符串转为对象。 12Object.fromEntries(new URLSearchParams('foo=bar&baz=qux'))// { foo: "bar", baz: "qux" }","link":"/JavaScript/18.%E5%AF%B9%E8%B1%A1%E7%9A%84%E6%89%A9%E5%B1%95/"},{"title":"原型与原型链","text":"1.prototypeJavaScript中,每一个函数都有一个prototype属性,这个属性是与生俱来的特质,特别强调,是函数,普通对象是没有这个属性的 1234567891011function Person() {}// 虽然写在注释里面,但是需要注意的是// prototype 是函数才会有的属性 (哈哈哈,看来在JavaScript中函数果然是有特权的……)Person.prototype.name = "Kevin";var person1 = new Person();var person2 = new Person();console.log(person1.name) // Kevinconsole.log(person2.name) // Kevin 上面的代码中我们创建了一个构造函数Person,并且在实例原型上面添加了一个name属性赋值为"Kevin"; 然后分别创建了两个实例对象:person1、person2; 当我们打印两个实例对象上name属性时均输出了Kevin(可以亲自试一下)。 我们不禁疑惑,这个Person.prototype到底是什么,为什么在上面添加属性,在 构造函数的实例化对象上都能访问到呢? 其实 Person这个函数的prototype属性指向了一个对象(即:Person.prototype也是一个对象)。这个对象正是调用该构造函数而创建的实例的原型。也就是这个例子中的person1和person2的原型(即实例化的对象的__proto__属性指向构造函数的原型)。 那么什么是原型呢?即: 每个JavaScript对象(null除外),在创建的时候都会与之关联另外一个对象,这个对象就是我们所说的原型,并且每一个对象都会从原型继承属性 上面的代码中我们并没有直接在person1和person2中添加name属性 但是这两个对象 却能够访问name属性,就是这个道理。 我们用一张图表示构造函数和实例原型之间的关系: 小结 在JavaScript中,每一个函数都会有一个prototype属性,这个属性指向它的实例原型对象,而每一个对象,在创建的时候,就会关联另一个对象,这个关联的对象,就是我们所说的原型 tips:new关键字的内部执行机制 创建一个空对象 将这个空对象的原型对象指向构造函数的原型 将构造函数的this指向这个空对象并执行 返回这个空对象 手写一个new 123456789function myNew(...newArgs) { const [Fn,...arg] = newAtgs; // 创建一个空对象并将这个对象的原型指向构造函数的实例原型 const obj = Object.create(Fn.prototype); // 将构造函数的this指向空对象并执行构造函数 Fn.apply(obj, arg); // 返回这个对象 return obj;} 2.__proto__每一个JavaScript对象(null除外),都有一个__proto__属性,作为实例对象和实例原型之间链接的桥梁 这里强调,是对象,同样,因为函数也是对象,所以函数也有这个属性 1234567function Person() {}var person = new Person();console.log(person.__proto__ === Person.prototype); //true; 有了第二个属性的帮助,我们就能更加全面的理解这张关系图了: 通过上面的关系图我们可以看到,构造函数Person 和实例对象person 分别通过 prototype和__proto__ 和实例原型Person.prototype进行关联,根据箭头指向 我们不禁要有疑问:实例原型是否有属性指向构造函数或者实例呢? 这时候该请出我们的第三个属性了:constructor 3.constructor实例原型指向实例的属性倒是没有,因为一个构造函数可能会生成很多个实例,但是原型指向构造函数的属性倒是有的,这就是我们的constructor——每一个原型都有一个constructor属性指向关联的构造函数。 我们再来看一个示例: 1234function Person() {}console.log(Person === Person.prototype.constructor); // true 好了到这里我们再完善下关系图: 4.实例与原型当读取实例的属性时,如果找不到,就会查找与对象关联的原型中的属性,如果还查不到,就去找原型的原型,一直找到最顶层为止。 我们再举一个例子: 1234567891011121314 function Person() { } Person.prototype.name = 'Kevin'; var person = new Person(); person.name = 'Daisy'; console.log(person.name) // Daisy delete person.name; console.log(person.name) // Kevin复制代码 在上面这个例子中,我们给实例person添加了name 属性,当我们打印person.name的时候,结果自然为Daisy 但是当我们删除了person下面的name属性后,读取person.name,依然能够成功输出Kevin,实际情况是从 person 对象中找不到 name 属性就会从 person 的原型也就是 person.__proto__ ,也就是 Person.prototype中查找,幸运的是我们找到了 name 属性,结果为 Kevin。 但是我们不禁有疑问,如果万一没有找到该怎么办? 我们来看下一层的关系 原型的原型 5.原型与原型链我们前面提到过,原型也是一个对象,那么既然是对象,那肯定有创建它的构造函数,这个构造函数就是Object() 123const obj = new Object()obj.name = 'jiacheng';console.log(obj.name) // jiacheng 其实原型对象就是通过Object构造函数生成的,结合之前我们所说的,实例__proto__指向构造函数的 prototype 所以我们再丰富一下我们的关系图; 到了这里我们对于 构造函数、实例对象、实例原型之间的关系又有了进一步的认识。 说了这么多,终于可以介绍原型链了。 6.原型链那Object.prototype的原型呢?Object是根节点对象,再往上查找就是null 1console.log(Object.prototype.__proto__ === null) // true 然而 null 究竟代表了什么呢? 引用阮一峰老师的 《undefined与null的区别》 就是: null 表示“没有对象”,即该处不应该有值。 所以 Object.prototype.proto 的值为 null 跟 Object.prototype 没有原型,其实表达了一个意思。 所以查找属性的时候查到 Object.prototype 就可以停止查找了。 我们可以将null 也加入最后的关系图中,这样就比较完整了。 上图中相互关联的原型组成的链状结构就是原型链,也就是红色的这条线 换句话来说,原型链就是__proto__这个属性连接的路径 7.补充最后,补充三点大家可能不会注意到的地方: constructor首先是constructor,我们看一个例子: 123456function Person() {}var person = new Person();console.log(person.constructor === Person); // true 当获取person.constructor时,其实 person 中并没有constructor 属性,当不能读取到constructor属性时,会从 person 的原型也就是 Person.prototype中读取,正好原型中有该属性,所以: 1person.constructor === Person.prototype.constructor __proto__其次是 proto ,绝大部分浏览器都支持这个非标准的方法访问原型,然而它并不存在于 Person.prototype 中,实际上,它是来自于 Object.prototype ,与其说是一个属性,不如说是一个 getter/setter,当使用 obj.proto 时,可以理解成返回了 Object.getPrototypeOf(obj)。 真的是继承吗?最后是关于继承,前面我们讲到“每一个对象都会从原型‘继承’属性”,实际上,继承是一个十分具有迷惑性的说法,引用《你不知道的JavaScript》中的话,就是: 继承意味着复制操作,然而 JavaScript 默认并不会复制对象的属性,相反,JavaScript 只是在两个对象之间创建一个关联,这样,一个对象就可以通过委托访问另一个对象的属性和函数,所以与其叫继承,委托的说法反而更准确些。 构造函数的原型指向我们知道构造函数的prototype属性指向它的实例原型,但构造函数同样是通过Function()这个构造函数创建出来的,所以它的__proto__属性指向Function.prototype 1console.log(fn.__proto__ === Function.prototype); // true 同样的Object和Function的__proto__属性也是指向Function.prototype 12console.log(Object.__proto__ === Function.prototype); // trueconsole.log(Function.__proto__ === Function.prototype); // true 8.练习题第一题1234567891011121314151617var F = function() {};Object.prototype.a = function() { console.log('a');};Function.prototype.b = function() { console.log('b');}var f = new F();f.a(); // af.b(); // errorF.a(); // aF.b(); // b 第二题1234567891011121314var A = function() {};A.prototype.n = 1;var b = new A();A.prototype = { n: 2, m: 3}var c = new A();console.log(b.n); // 1console.log(b.m); // undefinedconsole.log(c.n); // 2console.log(c.m); // 3 第三题12345678910var foo = {}, F = function(){};Object.prototype.a = 'value a';Function.prototype.b = 'value b';console.log(foo.a); // value aconsole.log(foo.b); // undefinedconsole.log(F.a); // value aconsole.log(F.b); // value b 第四题12345678910111213141516function A() {}function B(a) { this.a = a; // 有实例属性时,访问会优先调用实例属性,然后调用原型上的属性}function C(a) { if (a) { this.a = a; }}A.prototype.a = 1;B.prototype.a = 1;C.prototype.a = 1;console.log(new A().a); // 1console.log(new B().a); // undefinedconsole.log(new C(2).a); // 2 第五题1console.log(123['toString'].length + 123) // 124","link":"/JavaScript/19.%E5%8E%9F%E5%9E%8B%E4%B8%8E%E5%8E%9F%E5%9E%8B%E9%93%BE/"},{"title":"Generator","text":"1.特性·function关键字与函数名之间有一个星号 内部通过yield表达式,定义不同的内部状态 返回的遍历器对象,使用next()方法依次遍历函数内部的状态 2.yield表达式Generator函数内部通过yield表达式来暂停执行后面的操作,只有通过next()方法才能继续往下执行 next方法的运行逻辑: 遇到yield表达式,就暂停执行后面的操作,并将紧跟在yield后面的那个表达式的值,作为返回的对象的value属性值 再次调用next方法,就继续往下执行,直到遇到下一个yield表达式,就暂停执行并将后面的值赋给value返回 如果没有遇到新的yield表达式,就一直运行到函数结束,将return语句后面的表达式作为对象的value值返回,如果没有return语句,则返回undefined 3.与Iterator的关系Generator函数就是遍历器的生成函数,可以把Generator赋值给对象的Symbol.Iterator属性,从而使得该对象可以被Iterator接口相关的语句遍历 123456789101112const obj = {}obj[Symbol.iterator] = function * () { yield 1; yield 2; yield 3;}[...obj]for(const i of obj) { console.log(i)} 4. next方法参数yield表达式本身没有返回值,或者说返回值是undefined。next方法可以带一个参数,该参数就会被当作上一个yield表达式的返回值 123456789101112131415function* foo(x) { var y = 2 * (yield (x + 1)); var z = yield (y / 3); return (x + y + z);}var a = foo(5);a.next() // Object{value:6, done:false}a.next() // Object{value:NaN, done:false}a.next() // Object{value:NaN, done:true}var b = foo(5);b.next() // { value:6, done:false }b.next(12) // { value:8, done:false }b.next(13) // { value:42, done:true } 上面代码中,第二次运行next方法的时候不带参数,导致 y 的值等于2 * undefined(即NaN),除以 3 以后还是NaN,因此返回对象的value属性也等于NaN。第三次运行next方法的时候不带参数,所以z等于undefined,返回对象的value属性等于5 + NaN + undefined,即NaN。 如果向next方法提供参数,返回结果就完全不一样了。上面代码第一次调用b的next方法时,返回x+1的值6;第二次调用next方法,将上一次yield表达式的值设为12,因此y等于24,返回y / 3的值8;第三次调用next方法,将上一次yield表达式的值设为13,因此z等于13,这时x等于5,y等于24,所以return语句的值等于42。 5.next() throw() return() 都是让Generator函数恢复执行,并且使用不同的语句替换yield表达式 next()是将上一个yield表达式替换成一个值 1234567891011const g = function* (x, y) { let result = yield x + y; return result;};const gen = g(1, 2);gen.next(); // Object {value: 3, done: false}gen.next(1); // Object {value: 1, done: true}// 相当于将 let result = yield x + y// 替换成 let result = 1; throw()是将yield表达式替换成一个throw语句 123gen.throw(new Error('出错了')); // Uncaught Error: 出错了// 相当于将 let result = yield x + y// 替换成 let result = throw(new Error('出错了')); return()相当于将yield表达式替换成一个return语句 123gen.return(2); // Object {value: 2, done: true}// 相当于将 let result = yield x + y// 替换成 let result = return 2; 6.yield*yield*表达式用来在一个Generator函数里面执行另一个Generator函数 1234567891011121314function * foo() { yield 'a'; yield 'b'}function * bar() { yield 'x'; yield foo(); yield 'y'}for(let item of bar()) { console.log(item) // x a b y} 任何数据结构只要有iterator接口,就能被yield*遍历 12345function * gen() { yield * [1,2,3]}gen().next() 7.处理异步传统的编程语言,早有异步编程的解决方案(其实是多任务的解决方案)。其中有一种叫做”协程”(coroutine),意思是多个线程互相协作,完成异步任务。 协程的执行流程 协程A开始执行 协程A执行到一半,进入暂停,执行权转移到协程B 一段时间后,协程B交还执行权并恢复协程A的执行 协程的Generator函数实现Generator函数是协程在ES6的实现,最大的特点就是可以交出函数的执行权(暂停执行)。 整个Generator函数就是一个封装的异步任务,或者说是异步任务的容器,异步操作需要暂停的地方,都使用yield语句注明 1234function * gen(x) { var y = yield x + 2; return y} Generator函数数据的交换和错误处理Generator函数所具有的特性,使它成为异步编程的解决方案 可以暂停执行和恢复执行,这是它能封装异步任务的根本原因 函数体内外的数据交换和错误处理机制,使它可以作为异步编程的完整解决方案 调用next()方法返回值的value属性,是Generator函数向外输出数据,而在调用next()方法还可以接受参数,向Generator函数体内输入数据 12345678function* gen(x){ var y = yield x + 2; return y;}var g = gen(1);g.next() // { value: 3, done: false }g.next(2) // { value: 2, done: true } Generator函数内部还可以部署错误处理代码,捕获函数体外抛出的错误 12345678910111213function* gen(x){ try { var y = yield x + 2; } catch (e){ console.log(e); } return y;}var g = gen(1);g.next();g.throw('出错了');// 出错了 上面代码的最后一行,Generator 函数体外,使用指针对象的throw方法抛出的错误,可以被函数体内的try...catch代码块捕获。这意味着,出错的代码与处理错误的代码,实现了时间和空间上的分离,这对于异步编程无疑是很重要的。 异步任务的封装下面看看如何使用 Generator 函数,执行一个真实的异步任务。 1234567var fetch = require('node-fetch');function* gen(){ var url = 'https://api.github.com/users/github'; var result = yield fetch(url); console.log(result);} 上面代码中,Generator 函数封装了一个异步操作,该操作先读取一个远程接口,然后从 JSON 格式的数据解析信息。就像前面说过的,这段代码非常像同步操作,除了加上了yield命令。 执行这段代码的方法如下。 12345678var g = gen();var result = g.next();result.value.then(function(data){ return data.json();}).then(function(data){ g.next(data);}); 上面代码中,首先执行 Generator 函数,获取遍历器对象,然后使用next方法(第二行),执行异步任务的第一阶段。由于Fetch模块返回的是一个 Promise 对象,因此要用then方法调用下一个next方法。 可以看到,虽然 Generator 函数将异步操作表示得很简洁,但是流程管理却不方便(即何时执行第一阶段、何时执行第二阶段)。 自动执行Generator 封装一个基于Promise的自动执行机制 1234567891011121314151617var fs = require('fs');var readFile = function (fileName){ return new Promise(function (resolve, reject){ fs.readFile(fileName, function(error, data){ if (error) return reject(error); resolve(data); }); });};var gen = function* (){ var f1 = yield readFile('/etc/fstab'); var f2 = yield readFile('/etc/shells'); console.log(f1.toString()); console.log(f2.toString());}; 手动执行上面的Generator函数 1234567var g = gen();g.next().value.then(function(data){ g.next(data).value.then(function(data){ g.next(data); });}); 手动执行其实就是用then方法,层层添加回调函数。理解了这一点,就可以写出一个自动执行器。 123456789101112131415function run(gen){ var g = gen(); function next(data){ var result = g.next(data); if (result.done) return result.value; result.value.then(function(data){ next(data); }); } next();}run(gen);","link":"/JavaScript/25.Generator/"},{"title":"ESM","text":"ESM是ES6引入的JS标准模块化规范,它的设计思想是尽量的静态化,使得编译时就能确定模块的依赖关系和输入和输出的变量。 功能:模块功能主要由两个命令构成:export和import。export命令用于规定模块的对外接口,import命令用于输入其他模块提供的功能。 特性 编译时确定模块的依赖关系和输入输出变量 自动采用严格模式 一个模块就是一个文件,该模块内部的所有变量,外部无法获取 好处 使得静态分析成了可能,比如类型检验 不再需要UMD模块格式 将来浏览器的新 API 就能用模块格式提供,不再必须做成全局变量或者navigator对象的属性。 不再需要对象作为命名空间(比如Math对象),未来这些功能可以通过模块提供。 export如果你希望外部能够读取模块内部的某个变量,必须通过export关键字输出该变量 输出变量 1234567891011// profile.jsexport const a = 1;export const b = 'aaa';export const c = 2021;// 另一种写法const a = 1;const b = 'aaa';const c = 2021;export {a, b, c} 上面的这两种写法的等价的,但推荐使用第二种写法,因为这样可以在模块的末尾一眼看出该模块输出了哪些变量 输出函数 1234567891011export function fn() { console.log()}// 另一种写法function fn1(){}function fn2() {}export {fn1, fn2}// 给函数重命名export {fn1 as st1, fn2 as st2, fn2 as st3} 上面代码使用as关键字,重命名了函数v1和v2的对外接口。重命名后,v2可以用不同的名字输出两次。 注意:export命令规定的是模块对外的接口,必须与模块内部的变量建立一一对应关系 例如以下的写法会报错 123export 1; // 报错const a = 1;export a; // 报错 上面两种写法都会报错,因为没有提供对外的接口。第一种写法直接输出 1,第二种写法通过变量m,还是直接输出 1。1只是一个值,不是接口。 正确的写法 12345678910// 写法1export const a = 1;// 写法2const a = 1;export { a }// 写法3const a = 1;export {a as m} 上面三种写法都是正确的,规定了对外的接口a。其他脚本可以通过这个接口,取到值1。它们的实质是,在接口名与模块内部变量之间,建立了一一对应的关系。 函数和类同样必须遵守上面的规则 1234567function fn() {}export fn; // 报错export function fn() {} // 正确function fn() {}export {fn} // 正确 export语句输出的接口,与其对应的值是动态绑定关系,即通过该接口,获取模块内部实时的值 12export let foo = 'bar';setTimeout(() => foo = 'baz', 500); 上面的代码输出时是bar,500ms后会变为baz export只能出现在模块的顶层,不能在块级作用域中 export default:用于模块的默认导出,一个模块中只能存在一个export default 123456// index.jsexport default function() { ...}// 加载,可以为该匿名函数指定任意名字import aaa from './index.js' importimport命令用于输入其他模块提供的功能 123456import { a, b, c } from './index.js';// 直接使用console.log(a);console.log(b);console.log(c); import命令也可以给加载的模块重命名 1import { a as st1, b as st2, c as st3 } from './index.js'; import命令输入的变量都是只读的,因为它本质是输入接口,也就是说,不允许在加载模块的脚本里面,改写接口 123import {a} from './xxx.js'a = {}; // Syntax Error : 'a' is read-only; 上面代码中,脚本加载了变量a,对其重新赋值就会报错,因为a是一个只读的接口。但是,如果a是一个对象,改写a的属性是允许的。 123import { a } from './xxx.js'a.foo = 'hello'; // 合法操作 import后面的from指定模块文件的位置,可以是相对路径,也可以是绝对路径。如果不带有路径,只是一个模块名,那么必须有配置文件,告诉 JavaScript 引擎该模块的位置。 import命令具有提升效果,会提升到整个模块的头部,首先执行。 由于import是静态执行,所以不能使用表达式和变量,这些只有在运行时才能得到结果的语法结构。 12345678910111213// 报错import { 'f' + 'oo' } from 'my_module';// 报错let module = 'my_module';import { foo } from module;// 报错if (x === 1) { import { foo } from 'module1';} else { import { foo } from 'module2';} import语句在没有from的情况下会执行所加载的模块,如果多次执行同一个模块的import,那么只会执行一次 12import 'lodash';import 'lodash'; 如果执行的import语句对应的是同一个模块,等同于只加载一次 12345import { foo } from 'my_module';import { bar } from 'my_module'; // 不建议这么写// 等同于import { foo, bar } from 'my_module'; 复合写法如果在一个模块之中,先输入后输出同一个模块,import语句可以与export语句写在一起。 12345export { foo, bar } from 'my_module';// 可以简单理解为import { foo, bar } from 'my_module';export { foo, bar }; import()ES2020提案 引入import()函数,支持动态加载模块,并且返回一个Promise对象。 1import(module) 和CommonJs的差异 CommonJS 模块输出的是一个值的拷贝,ESM 模块输出的是值的引用。 CommonJS 模块是运行时加载,ESM 模块是编译时输出接口。 CommonJS 模块的require()是同步加载模块,ES6 模块的import命令是异步加载,有一个独立的模块依赖的解析阶段。 CommonJS模块,都是只能在运行时确定这些东西。 12345678// CommonJS模块let { stat, exists, readfile } = require('fs');// 等同于let _fs = require('fs');let stat = _fs.stat;let exists = _fs.exists;let readfile = _fs.readfile; 上面的代码实质是整体加载fs模块,生成一个对象,然后从这三个对象上面读取三个方法,这种加载称为运行时加载,因为只有在运行时才能得到这个对象,导致完全无法在编译时做”静态优化” NodeJs从13.2版本开始支持ESM模块 CommonJS 模块加载 ESM 模块CommonJs不能通过require()命令加载ESM模块,只能通过import()命令加载 12345( async () => { await import(modlue) })() require()不支持 ESM模块的一个原因是,它是同步加载,而 ES6 模块内部可以使用顶层await命令,导致无法被同步加载。 ESM模块加载CommonjsESM模块的import命令可以加载CommonJs模块,但只能整体加载,不能单独加载单一的输出项","link":"/JavaScript/4.ESM/"},{"title":"迭代器","text":"1.Iterator(遍历器)的概念JavaScript原有的表示集合的数据,主要是数组和对象,ES6又添加了Map和Set结构,这样就有了四种数据集合 这样就需要一种统一的接口机制,来处理所有不同的数据结构 遍历器(Iterator)就是这样的一种机制,它是一个接口,为不同的数据结构提供统一的访问机制。任何数据结构只要部署了Iterator接口,就可以完成遍历操作(即依次处理该数据结构的所有成员) Iterator 的作用有三个:一是为各种数据结构,提供一个统一的、简便的访问接口;二是使得数据结构的成员能够按某种次序排列;三是 ES6 创造了一种新的遍历命令for...of循环,Iterator 接口主要供for...of消费。 遍历过程 创建一个指针对象,指向当前数据结构的起始位置,也就是说,遍历器的本质是一个指针对象 不断调用这个指针对象的next方法,返回数据结构的当前成员信息,具体来说,就是返回一个包含value和done两个属性的对象,其中,value就是当前成员的值,done属性是一个布尔值,表示遍历是否结束 12345678910111213141516var it = makeIterator(['a', 'b']);it.next() // { value: "a", done: false }it.next() // { value: "b", done: false }it.next() // { value: undefined, done: true }function makeIterator(array) { var nextIndex = 0; return { next: function() { return nextIndex < array.length ? {value: array[nextIndex++], done: false} : {value: undefined, done: true}; } };} 2.默认Iterator接口Iterator接口的目的,就是为所有的数据结构,提供一种统一的访问机制,即for...of循环 一种数据结构只要部署了 Iterator 接口,我们就称这种数据结构是“可遍历的”(iterable) ES6 规定,默认的 Iterator 接口部署在数据结构的Symbol.iterator属性,或者说,一个数据结构只要具有Symbol.iterator属性,就可以认为是“可遍历的”(iterable)。 ES6 的有些数据结构原生具备 Iterator 接口(比如数组),即不用任何处理,就可以被for...of循环遍历。原因在于,这些数据结构原生部署了Symbol.iterator属性(详见下文),另外一些数据结构没有(比如对象)。凡是部署了Symbol.iterator属性的数据结构,就称为部署了遍历器接口。调用这个接口,就会返回一个遍历器对象。 原生具备 Iterator 接口的数据结构如下。 Array Map Set String TypedArray 函数的 arguments 对象 NodeList 对象 1234567let arr = ['a', 'b', 'c'];let iter = arr[Symbol.iterator]();iter.next() // { value: 'a', done: false }iter.next() // { value: 'b', done: false }iter.next() // { value: 'c', done: false }iter.next() // { value: undefined, done: true } 对象(Object)之所以没有默认部署 Iterator 接口,是因为对象的哪个属性先遍历,哪个属性后遍历是不确定的,需要开发者手动指定。本质上,遍历器是一种线性处理,对于任何非线性的数据结构,部署遍历器接口,就等于部署一种线性转换。不过,严格地说,对象部署遍历器接口并不是很必要,因为这时对象实际上被当作 Map 结构使用,ES5 没有 Map 结构,而 ES6 原生提供了 3.调用 Iterator 接口的场合有一些场合会默认调用 Iterator 接口(即Symbol.iterator方法),除了下文会介绍的for...of循环,还有几个别的场合。 (1)解构赋值 对数组和 Set 结构进行解构赋值时,会默认调用Symbol.iterator方法。 1234567let set = new Set().add('a').add('b').add('c');let [x,y] = set;// x='a'; y='b'let [first, ...rest] = set;// first='a'; rest=['b','c']; (2)扩展运算符 扩展运算符(…)也会调用默认的 Iterator 接口。 12345678// 例一var str = 'hello';[...str] // ['h','e','l','l','o']// 例二let arr = ['b', 'c'];['a', ...arr, 'd']// ['a', 'b', 'c', 'd'] 上面代码的扩展运算符内部就调用 Iterator 接口。 实际上,这提供了一种简便机制,可以将任何部署了 Iterator 接口的数据结构,转为数组。也就是说,只要某个数据结构部署了 Iterator 接口,就可以对它使用扩展运算符,将其转为数组。 1let arr = [...iterable]; (3)yield* yield*后面跟的是一个可遍历的结构,它会调用该结构的遍历器接口。 1234567891011121314let generator = function* () { yield 1; yield* [2,3,4]; yield 5;};var iterator = generator();iterator.next() // { value: 1, done: false }iterator.next() // { value: 2, done: false }iterator.next() // { value: 3, done: false }iterator.next() // { value: 4, done: false }iterator.next() // { value: 5, done: false }iterator.next() // { value: undefined, done: true } (4)其他场合 由于数组的遍历会调用遍历器接口,所以任何接受数组作为参数的场合,其实都调用了遍历器接口。下面是一些例子。 for…of Array.from() Map(), Set(), WeakMap(), WeakSet()(比如new Map([['a',1],['b',2]])) Promise.all() Promise.race()","link":"/JavaScript/9.Iterator%E5%92%8Cfor...of%E5%BE%AA%E7%8E%AF/"},{"title":"浏览器事件循环机制","text":"前言事件循环是JS的重要执行机制,掌握事件循环,有助于我们在前端编程中更上一层楼 浏览器多进程模型 浏览器是多进程模型,其中最主要的进程包括: 浏览器主进程:主要负责界面显示,用户交互,子进程的管理等。浏览器进程内部会启动多个线程处理不同的任务。 网络进程:负责加载网络资源。网络进程内部会启动多个子线程来处理不同的网络任务。 渲染进程: 渲染进程启动后,会开启一个渲染主线程,主要负责执行 HTML、CSS、JS代码,默认情况下,浏览器会为每一个标签页开启一个新的渲染进程,以保证不同的标签页之间不相互影响。 渲染主线程需要处理的任务渲染主线程是和我们打交道最多的线程,需要它处理的任务包括但不限于: 解析HTML 解析CSS 计算样式、布局 处理图层、渲染页面 执行JS代码 执行事件处理函数 执行计时器回调函数等 渲染主线程的工作方式 最开始的时候,渲染主线程会进入一个无限循环 每一次循环会检查事件队列中是否有任务存在。如果有,就取出第一个任务执行,执行完一个后进入下一次循环;如果没有,则进入休眠状态。 其他所有线程,可以随时向事件队列添加任务。新任务会加到队列的末尾。在添加新任务时,如果主线程是休眠状态,则会唤醒以继续循环拿取任务。 这样一来,就可以让每个任务有条不紊的,持续进行下去了。 整个过程,被称之为事件循环 什么是异步代码在执行过程中,会遇到一些无法立即执行的任务,比如: 计时完成后需要执行的任务 – setTimeout、setInterval 网络通信完成后需要执行的任务 – XMLHttpRequest、Fetch 用户操作后需要执行的任务 – addEventListener 如果让渲染主线程等待这些任务的时机到达,就会导致主线程长期处于「阻塞」状态,从而导致浏览器「卡死」。所以浏览器选择了异步,不阻塞渲染主线程的方式来执行这些无法立即执行的任务 如何理解JS的异步 JS是一门单线程语言,因为浏览器渲染进程中执行JS代码的线程只有一个 如果使用同步的方式,就极有可能导致主线程产生阻塞,从而导致消息队列中的很多其他任务无法得到执行。这样一来,一方面会导致繁忙的主线程白白地消耗时间,另一方面会导致页面无法及时更新,给用户造成卡死的现象。 所以浏览器采用异步的方式来避免。具体做法是当某些任务发生时,比如计时器、网络、事件监听,JS线程将任务交给其它线程去处理,自身立即结束任务的执行,转而执行后续的代码。当其它线程完成时,将事先传递的回调函数包转成任务,加入到消息队列的末尾排队,等待主线程调度执行。 在这种异步模式下,最大限度地保证了单线程的流畅运行。 任务的优先级任务没有优先级,先进先出,但事件队列是有优先级的 根据W3C的最新解释: 每个任务都有一个任务类型,同一个任务必须在一个队列,不同类型的任务可以分属于不同的队列。在一次事件循环中,浏览器可以根据实际情况从不同的队列中取出任务执行 浏览器必须准备好一个微队列,微队列中的任务优先于所有其它任务队列 随着浏览器的复杂度急剧提升,W3C不再使用宏队列的说法 在目前chrome的实现中,至少包含了下面的队列: 延时队列:用于存放计时器到达后的回调任务,优先级「中」 交互队列:用于存放用户操作后产生的事件处理任务,优先级「高」 微队列:用于存放需要最快执行的任务,优先级「最高」 添加任务到微队列的主要方式是使用 Promise、MutationObserver等 结尾阐述一下JS的事件循环事件循环又叫消息循环,是浏览器渲染主线程的工作方式。 在Chrome的源码中,它开启一个不会结束的for循环,每次循环从消息队列中取出一个任务执行,其它线程只需要在合适的时候将任务加入到队列末尾即可。 过去把消息队列简单分为宏队列和微队列,这种说法目前已经无法满足复杂的浏览器环境,取而代之的是一种更加灵活多变的处理方式。 根据W3C官方的解释,每个任务有不同的类型,同类型的任务必须在同一个队列,不同的任务可以属于不同的队列。不同任务队列有不同的优先级,在一次事件循环中,由浏览器自行决定取哪一个队列的任务。但浏览器必须有一个微队列,微队列的任务一定具有最高的优先级,必须优先调度执行 JS中的计时器能做到精确计时吗?为什么?不行,因为: 操作系统的计时函数本身就有偏差,由于JS的计时器最终调用的是操作系统的函数,也就携带了这些偏差 按照W3C的标准,浏览器实现计时器时,如果嵌套层级超过5层,则会带有4ms的最小时间,这样在计时器少于4ms时又带来了偏差。 受事件循环的影响,计时器的回调函数只能在主线程空闲时运行,因此又带来了偏差。","link":"/JavaScript/JavaScript%E4%BA%8B%E4%BB%B6%E5%BE%AA%E7%8E%AF/"},{"title":"字符串和数组常用方法","text":"1.Unicode表示法12//es6中将编码放入{}即可解析console.log('hell\\u{6f}'); 2.字符串常用方法 常规方法 charAt(index):返回指定索引的字符串 indexOf(item):返回指定字符串第一次出现的位置 lastIndexOf(item):返回指定字符串最后一次出现的位置 substring[start,end):提取指定区间的字符串,索引不能为负值 slice[start,end):提取指定区间的字符串(索引可以为负值,-1就是倒数第二位) substr(start,length):返回指定长度的字符串,可以从末尾开始取数据(-2就是取后面两位) concat(str1,str2…):字符串拼接方法,连接两个或多个字符串,返回连接后的新字符串 toString():返回字符串对象方法 toLowerCase(),把字符串转换成小写的 toUpperCase(),把字符串转换成大写的 String.fromCharCode(ASCII):将对应的ASCII值转为字符串 String.fromCodePoint(ASCII):将对应的ASCII值转为字串串,可处理高位编码 charCodeAt():字符串转为对应的ASCII值 与正则表达式有关的方法 match(正则表达式):在字符串中匹配是否有符合正则表达式的字符串,返回匹配到的字符串数组,未匹配到则返回null replace(oldStr/正则,newStr):字符串替换方法,用新的子串将旧的子串替换掉,返回替换后的字符串 search(子串/正则):字符串查询方法,找到子串第一次出现的位置并返回,未找到则返回-1 split(分割字符/正则):字符串分割方法,传入分割符,把字符串分割为子字符串数组,返回分割后的数组 es6新增方法 includes(item):返回布尔值,判断字符串是否包含某串字符。接受两个参数,第二个参数是起始的位置 startsWith(item):返回布尔值,判断字符串开头是否包含某串字符 endsWith(item):回布尔值,判断字符串结尾是否包含某串字符 repeat:返回一个新的字符串,将原字符串重复 n 次 padStart:返回新的字符串,在字符串前面补全,接受两个参数,第一个参数补全字符串长度,第二个参数补全的字符 padEnd:返回新的字符串,在字符串后面补全,接受两个参数,第一个参数补全字符串长度,第二个参数补全的字符 trimStart:去除前面空格 tiemEnd:去除后面空格 matchAll:返回一个正则表达式在当前字符串的所有匹配 1234567891011121314151617181920let str = "How are you Are are are";// console.log(str.charAt(4)); //返回指定索引的字符串// console.log(str.indexOf("are")); // 返回指定字符串第一次出现的索引// console.log(str.lastIndexOf("are")); // 返回指定字符串最后一次出现的索引// console.log(str.substring(0,9)); // 返回指定区间的字符串// console.log(str.slice(0,0)); // 返回指定区间的字符串,索引可以为负值// console.log(str.substr(0,3)); // 返回指定长度的子串// console.log(str.concat(" yes"," no"));/* let num = 123;console.log(num.toString()); */// console.log(str.toLocaleLowerCase());// console.log(str.toLocaleUpperCase());// console.log(str.match(/are/));// console.log(str.replace(/are/g,"at"));// console.log(str.split(/are/ig));// console.log(str.includes("are"));// console.log(str.endsWith("e"));// console.log(str.repeat(2));// console.log(str.match(/are/g)); 数组常用方法 常规方法 push():从数组末尾插入数据,返回插入完成后数组的长度 pop():从数组末尾取下数据,返回取下的数据 unshift():从数组头部插入数据,返回插入完成后数组的长度 shift():从数组头部取下数据,返回取下的数据 splice():实现数组的插入,删除,替换: 插入:3个参数,起始位置,0,插入的项。 删除:2个参数,起始位置,删除的项数 替换:任意参数,起始位置,删除的项数,插入任意数量的项数 sort():数组排序方法,默认按照ASCII值进行升序排序 fill():使用一个固定值替换数组中的元素 reverse():逆序输出数组 at():接收一个整数值并返回该索引的项目,允许负数 indexOf(item):返回对应元素的下标 concat(arr2,数据1,数据2…):拷贝原数组生成新数组。合并数组 slice[start,end):提取数组中指定区间的数据 join(分割符):数组分割方法 copyWithin(target,[start,end)):在当前数组内部,将指定位置的成员复制到其他位置(会覆盖原有成员),然后返回当前数组 高阶函数 forEach(callback):遍历数组 map(callback):遍历数组,可返回对数组的item和index进行一定操作后的结果。 filter(callback):返回数组中全部符合条件的元素 find(callbakc):返回数组中第一个符合条件的元素 findIndex(callback):返回数组中第一个符合条件的元素的下标 some(callback):返回布尔值,查找数组中是否有符合条件的元素(找到第一个符合条件的元素便会停止循环) every(callbakc):返回布尔值,遍历数组中的每一个元素是否都符合条件 reduce(callback(prev, curr, index, array),initValue):归并,为数组中的每一个元素依次执行回调函数,不包括数组中被删除或从未被赋值的元素 12345678910111213141516171819202122232425262728293031323334353637383940414243444546// 各值的累加const arr = [1,2,3,4,5];const result = arr.reduce((acc, cur) => acc + cur);arr.forEach(item => console.log(item));// 数组降维const result = [[1,2],[3,4],[5,6]].reduce((acc, cur) => [...acc, ...cur], []);console.log(result);// 计算数组中每个元素出现的次数const arr = ["1", "2", "3", "4", "2", "1", "1"];const result = arr.reduce((acc, cur) => { if( cur in acc ) { acc[cur]++; } else { acc[cur] = 1; } return acc;}, {});// 按属性对object分类const people = [ { name: 'Alice', age: 21 }, { name: 'Max', age: 20 }, { name: 'Jane', age: 20 },]function groupBy( objectArray, property ) { return objectArray.reduce( (acc, obj) => { const key = obj[property]; if (!acc[key]) { acc[key] = []; } acc[key].push(obj); return acc; }, {});}const groupedPeople = groupBy(people, 'age');console.log(groupedPeople);","link":"/JavaScript/0.%E5%AD%97%E7%AC%A6%E4%B8%B2%E5%92%8C%E6%95%B0%E7%BB%84%E5%B8%B8%E7%94%A8%E6%96%B9%E6%B3%95/"},{"title":"数值的扩展","text":"1.二进制和八进制表示发ES6提供了二进制和八进制数值的新写法,二进制使用0b(0B)八进制使用0o(0O)来表示 120b111110111 === 503 // true0o767 === 503 // true 如果要将0b和0o前缀的字符串数值转为十进制,要使用Number方法。 12Number('0b111') // 7Number('0o10') // 8 2.数值分隔符ES2021中,允许JavaScript的数值使用下划线(_)作为分隔符 1let num = 1_000_000_000_000 这个数值分隔符没有指定间隔的位数,也就是说,可以每三位添加一个分隔符,也可以每一位、每两位、每四位添加一个。 1234123_00 === 12_300 // true12345_00 === 123_4500 // true12345_00 === 1_234_500 // true 小数和科学计数法也可以使用数值分隔符。 12345// 小数0.000_001// 科学计数法1e10_000 数值分隔符有几个使用注意点。 不能放在数值的最前面(leading)或最后面(trailing)。 不能两个或两个以上的分隔符连在一起。 小数点的前后不能有分隔符。 科学计数法里面,表示指数的e或E前后不能有分隔符。 下面三个将字符串转成数值的函数,不支持数值分隔符。主要原因是语言的设计者认为,数值分隔符主要是为了编码时书写数值的方便,而不是为了处理外部输入的数据。 Number() parseInt() parseFloat() 12Number('123_456') // NaNparseInt('123_456') // 123 3.Number.isFinite()和Number.isNaN()ES6 在Number对象上,新提供了Number.isFinite()和Number.isNaN()两个方法。 Number.isFinite()用来检查一个数值是否为有限的(finite),即不是Infinity。 12345678Number.isFinite(15); // trueNumber.isFinite(0.8); // trueNumber.isFinite(NaN); // falseNumber.isFinite(Infinity); // falseNumber.isFinite(-Infinity); // falseNumber.isFinite('foo'); // falseNumber.isFinite('15'); // falseNumber.isFinite(true); // false 注意,如果参数类型不是数值,Number.isFinite一律返回false。 Number.isNaN()用来检查一个值是否为NaN。 1234567Number.isNaN(NaN) // trueNumber.isNaN(15) // falseNumber.isNaN('15') // falseNumber.isNaN(true) // falseNumber.isNaN(9/NaN) // trueNumber.isNaN('true' / 0) // trueNumber.isNaN('true' / 'true') // true 如果参数类型不是NaN,Number.isNaN一律返回false。 它们与传统的全局方法isFinite()和isNaN()的区别在于,传统方法先调用Number()将非数值的值转为数值,再进行判断,而这两个新方法只对数值有效,Number.isFinite()对于非数值一律返回false, Number.isNaN()只有对于NaN才返回true,非NaN一律返回false。 12345678910isFinite(25) // trueisFinite("25") // trueNumber.isFinite(25) // trueNumber.isFinite("25") // falseisNaN(NaN) // trueisNaN("NaN") // trueNumber.isNaN(NaN) // trueNumber.isNaN("NaN") // falseNumber.isNaN(1) // false 4.Number.parseInt(), Number.parseFloat()ES6 将全局方法parseInt()和parseFloat(),移植到Number对象上面,行为完全保持不变。 1234567// ES5的写法parseInt('12.34') // 12parseFloat('123.45#') // 123.45// ES6的写法Number.parseInt('12.34') // 12Number.parseFloat('123.45#') // 123.45 这样做的目的,是逐步减少全局性方法,使得语言逐步模块化。 12Number.parseInt === parseInt // trueNumber.parseFloat === parseFloat // true 5.Number.isInteger()Number.isInteger()用来判断一个数值是否为整数。 12Number.isInteger(25) // trueNumber.isInteger(25.1) // false JavaScript 内部,整数和浮点数采用的是同样的储存方法,所以 25 和 25.0 被视为同一个值。 12Number.isInteger(25) // trueNumber.isInteger(25.0) // true 如果参数不是数值,Number.isInteger返回false。 1234Number.isInteger() // falseNumber.isInteger(null) // falseNumber.isInteger('15') // falseNumber.isInteger(true) // false 注意,由于 JavaScript 采用 IEEE 754 标准,数值存储为64位双精度格式,数值精度最多可以达到 53 个二进制位(1 个隐藏位与 52 个有效位)。如果数值的精度超过这个限度,第54位及后面的位就会被丢弃,这种情况下,Number.isInteger可能会误判。 1Number.isInteger(3.0000000000000002) // true 上面代码中,Number.isInteger的参数明明不是整数,但是会返回true。原因就是这个小数的精度达到了小数点后16个十进制位,转成二进制位超过了53个二进制位,导致最后的那个2被丢弃了。 类似的情况还有,如果一个数值的绝对值小于Number.MIN_VALUE(5E-324),即小于 JavaScript 能够分辨的最小值,会被自动转为 0。这时,Number.isInteger也会误判。 12Number.isInteger(5E-324) // falseNumber.isInteger(5E-325) // true 上面代码中,5E-325由于值太小,会被自动转为0,因此返回true。 总之,如果对数据精度的要求较高,不建议使用Number.isInteger()判断一个数值是否为整数。 6.Number.EPSILON和Number.isSafeInteager() 最小精度值 ES6在Number对象上面,新增了一个极小的常量Number.EPSILON它表示1与大于1的最小浮点数之间的差 对于 64 位浮点数来说,大于 1 的最小浮点数相当于二进制的1.00..001,小数点后面有连续 51 个零。这个值减去 1 之后,就等于 2 的 -52 次方。 123456Number.EPSILON === Math.pow(2, -52)// trueNumber.EPSILON// 2.220446049250313e-16Number.EPSILON.toFixed(20)// "0.00000000000000022204" 引入一个这么小的量的目的,在于为浮点数计算,设置一个误差范围。我们知道浮点数计算是不精确的。 123456780.1 + 0.2// 0.300000000000000040.1 + 0.2 - 0.3// 5.551115123125783e-175.551115123125783e-17.toFixed(20)// '0.00000000000000005551' 上面代码解释了,为什么比较0.1 + 0.2与0.3得到的结果是false。 10.1 + 0.2 === 0.3 // false Number.EPSILON可以用来设置“能够接受的误差范围”。比如,误差范围设为 2 的-50 次方(即Number.EPSILON * Math.pow(2, 2)),即如果两个浮点数的差小于这个值,我们就认为这两个浮点数相等。 125.551115123125783e-17 < Number.EPSILON * Math.pow(2, 2)// true 因此,Number.EPSILON的实质是一个可以接受的最小误差范围。 123456789function withinErrorMargin (left, right) { return Math.abs(left - right) < Number.EPSILON * Math.pow(2, 2);}0.1 + 0.2 === 0.3 // falsewithinErrorMargin(0.1 + 0.2, 0.3) // true1.1 + 1.3 === 2.4 // falsewithinErrorMargin(1.1 + 1.3, 2.4) // true 上面的代码为浮点数运算,部署了一个误差检查函数。 安全整数 JavaScript 能够准确表示的整数范围在-2^53到2^53之间(不含两个端点),超过这个范围,无法精确表示这个值。 ES6 引入了Number.MAX_SAFE_INTEGER和Number.MIN_SAFE_INTEGER这两个常量,用来表示这个范围的上下限。 123456789Number.MAX_SAFE_INTEGER === Math.pow(2, 53) - 1// trueNumber.MAX_SAFE_INTEGER === 9007199254740991// trueNumber.MIN_SAFE_INTEGER === -Number.MAX_SAFE_INTEGER// trueNumber.MIN_SAFE_INTEGER === -9007199254740991// true Number.isSafeInteger()则是用来判断一个整数是否落在这个范围之内。 注意:实际使用这个API时,必须注意验证运算的结果是否落在安全整数的范围内,不要只验证运算结果,而要同时验证参与运算的每个值 123456789Number.isSafeInteger(9007199254740993)// falseNumber.isSafeInteger(990)// trueNumber.isSafeInteger(9007199254740993 - 990)// true9007199254740993 - 990// 返回结果 9007199254740002// 正确答案应该是 9007199254740003 上面代码中,9007199254740993不是一个安全整数,但是Number.isSafeInteger会返回结果,显示计算结果是安全的。这是因为,这个数超出了精度范围,导致在计算机内部,以9007199254740992的形式储存。","link":"/JavaScript/11.%E6%95%B0%E5%80%BC%E7%9A%84%E6%89%A9%E5%B1%95/"},{"title":"函数柯里化","text":"柯里化说柯里化之前,首先抛出一个疑问,如何实现一个add函数,使得这个add函数可以灵活调用和传参,支持下面的调用示例呢? 123456add(1, 2, 3) // 6add(1) // 1add(1)(2) // 3add(1, 2)(3) // 6add(1)(2)(3) // 6add(1)(2)(3)(4) // 10 要解答这样的疑问,还是要先明白什么是柯里化。 在计算机科学中,柯里化(Currying)是把接受多个参数的函数变换成接受一个单一参数(最初函数的第一个参数)的函数,并且返回接受余下的参数且返回结果的新函数的技术。 这段解释看着还是挺懵逼的,不如举个例子: 本来有这么一个求和函数dynamicAdd(),接受任意个参数。 12345function dynamicAdd() { return [...arguments].reduce((prev, curr) => { return prev + curr }, 0)} 现在需要通过柯里化把它变成一个新的函数,这个新的函数预置了第一个参数,并且可以在调用时继续传入剩余参数。 看到这,我觉得有点似曾相识,预置参数的特性与bind很相像。那么我们不如用bind的思路来实现。 123456789function curry(fn, firstArg) { // 返回一个新函数 return function() { // 新函数调用时会继续传参 var restArgs = [].slice.call(arguments) // 参数合并,通过apply调用原函数 return fn.apply(this, [firstArg, ...restArgs]) }} 接着我们通过一些例子来感受一下柯里化。 123456789// 柯里化,预置参数10var add10 = curry(dynamicAdd, 10)add10(5); // 15// 柯里化,预置参数20var add20 = curry(dynamicAdd, 20);add20(5); // 25// 也可以对一个已经柯里化的函数add10继续柯里化,此时预置参数10即可var anotherAdd20 = curry(add10, 10);anotherAdd20(5); // 25 可以发现,柯里化是在一个函数的基础上进行变换,得到一个新的预置了参数的函数。最后在调用新函数时,实际上还是会调用柯里化前的原函数。 并且柯里化得到的新函数可以继续被柯里化,这看起来有点像俄罗斯套娃的感觉。 实际使用时也会出现柯里化的变体,不局限于只预置一个参数。 1234567891011function curry(fn) { // 保存预置参数 var presetArgs = [].slice.call(arguments, 1) // 返回一个新函数 return function() { // 新函数调用时会继续传参 var restArgs = [].slice.call(arguments) // 参数合并,通过apply调用原函数 return fn.apply(this, [...presetArgs, ...restArgs]) }} 其实Function.protoype.bind就是一个柯里化的实现。不仅如此,很多流行的库都大量使用了柯里化的思想。 实际应用中,被柯里化的原函数的参数可能是定长的,也可能是不定长的。 参数定长的柯里化假设存在一个原函数fn,fn接受三个参数a, b, c,那么函数fn最多被柯里化三次(有效地绑定参数算一次)。 1234567891011function fn(a, b, c) { return a + b + c}var c1 = curry(fn, 1);var c2 = curry(c1, 2);var c3 = curry(c2, 3);c3(); // 6// 再次柯里化也没有意义,原函数只需要三个参数var c4 = curry(c3, 4);c4();复制代码 也就是说,我们可以通过柯里化缓存的参数数量,来判断是否到达了执行时机。那么我们就得到了一个柯里化的通用模式。 12345678910111213141516171819function curry(fn) { // 获取原函数的参数长度 const argLen = fn.length; // 保存预置参数 const presetArgs = [].slice.call(arguments, 1) // 返回一个新函数 return function() { // 新函数调用时会继续传参 const restArgs = [].slice.call(arguments) const allArgs = [...presetArgs, ...restArgs] if (allArgs.length >= argLen) { // 如果参数够了,就执行原函数 return fn.apply(this, allArgs) } else { // 否则继续柯里化 return curry.call(null, fn, ...allArgs) } }} 这样一来,我们的写法就可以支持以下形式。 123456789function fn(a, b, c) { return a + b + c;}var curried = curry(fn);curried(1, 2, 3); // 6curried(1, 2)(3); // 6curried(1)(2, 3); // 6curried(1)(2)(3); // 6curried(7)(8)(9); // 24 参数不定长的柯里化解决了上面的问题,我们难免会问自己,假设原函数的参数不定长呢,这种情况如何柯里化? 首先,我们需要理解参数不定长是指函数声明时不约定具体的参数,而在函数体中通过arguments获取实参,然后进行运算。就像下面这种。 123456function dynamicAdd() { return [...arguments].reduce((prev, curr) => { return prev + curr }, 0)}复制代码 回到最开始的问题,怎么支持下面的所有调用形式? 1234567add(1, 2, 3) // 6add(1) // 1add(1)(2) // 3add(1, 2)(3) // 6add(1)(2)(3) // 6add(1)(2)(3)(4) // 10复制代码 思考了一阵,我发现在参数不定长的情况下,要同时支持1~N次调用还是挺难的。add(1)在一次调用后可以直接返回一个值,但它也可以作为函数接着调用add(1)(2),甚至可以继续add(1)(2)(3)。那么我们实现add函数时,到底是返回一个函数,还是返回一个值呢?这让人挺犯难的,我也不能预测这个函数将如何被调用啊。 而且我们可以拿上面的成果来验证下: 12curried(1)(2)(3)(4);复制代码 运行上面的代码会报错:Uncaught TypeError: curried(…)(…)(…) is not a function,因为执行到curried(1)(2)(3),结果就不是一个函数了,而是一个值,一个值当然是不能作为函数继续执行的。 所以如果要支持参数不定长的场景,已经柯里化的函数在执行完毕时不能返回一个值,只能返回一个函数;同时要让JS引擎在解析得到的这个结果时,能求出我们预期的值。 大家看了这个可能还是不懂,好,说人话!我们实现的curry应该满足: 经curry处理,得到一个新函数,这一点不变。 123// curry是一个函数var curried = curry(add);复制代码 新函数执行后仍然返回一个结果函数。 123// curried10也是一个函数var curried10 = curried(10);var curried30 = curried10(20); 结果函数可以被Javascript引擎解析,得到一个预期的值。 1curried10; // 10 好,关键点在于3,如何让Javascript引擎按我们的预期进行解析,这就回到Javascript基础了。在解析一个函数的原始值时,会用到toString。 我们知道,console.log(fn)可以把函数fn的源码输出,如下所示: 1234console.log(fn)ƒ fn(a, b, c) { return a + b + c;} 那么我们只要重写toString,就可以巧妙地实现我们的需求了。 12345678910111213141516function curry(fn) { // 保存预置参数 const presetArgs = [].slice.call(arguments, 1) // 返回一个新函数 function curried () { // 新函数调用时会继续传参 const restArgs = [].slice.call(arguments) const allArgs = [...presetArgs, ...restArgs] return curry.call(null, fn, ...allArgs) } // 重写toString curried.toString = function() { return fn.apply(null, presetArgs) } return curried;} 这样一来,魔性的add用法就都被支持了。 12345678function dynamicAdd() { return [...arguments].reduce((prev, curr) => { return prev + curr }, 0)}var add = curry(dynamicAdd);add(1)(2)(3)(4) // 10add(1, 2)(3, 4)(5, 6) // 21 柯里化总结柯里化是一种函数式编程思想,实际上在项目中可能用得少,或者说用得不深入,但是如果你掌握了这种思想,也许在未来的某个时间点,你会用得上! 大概来说,柯里化有如下特点: 简洁代码:柯里化应用在较复杂的场景中,有简洁代码,可读性高的优点。 参数复用:公共的参数已经通过柯里化预置了。 延迟执行:柯里化时只是返回一个预置参数的新函数,并没有立刻执行,实际上在满足条件后才会执行。 管道式流水线编程:利于使用函数组装管道式的流水线工序,不污染原函数。","link":"/JavaScript/15.%E5%87%BD%E6%95%B0%E6%9F%AF%E9%87%8C%E5%8C%96/"},{"title":"Promise","text":"1.Promise Promise 是异步编程的一种解决方案: 从语法上讲,promise是一个对象,从它可以获取异步操作的消息; 从本意上讲,它是承诺,承诺它过一段时间会给你一个结果。 promise有三种状态:**pending(等待态),fulfiled(成功态),rejected(失败态)**;状态一旦改变,就不会再变。创造promise实例后,它会立即执行。 Promise对象有以下两个特点: 对象的状态不受外界影响,Promise对象代表一个异步操作,有三种状态:pending(进行中),fulfilled(已成功)和rejected(已失败),只有异步操作的结果,可以决定当前是哪一种状态,任何其他操作都无法改变这个状态,这也是Promise这个名字的由来。 一旦状态改变,就不会再变,任何时候都可以得到这个结果,Promise对象状态改变,只有两种情况,从pending变为fufilled和从pending变为rejected,只要这两种情况发生,状态就凝固了,不会再变了,这时就称为resolved。如果改变已经发生了,你再对Promise对象添加回调函数,也会立即得到这个结果。这与事件(Event)完全不同,事件的特点是,如果你错过了它,再去监听,是得不到结果的。 Promise也有一些缺点。首先,无法取消Promise,一旦新建它就会立即执行,无法中途取消。其次,如果不设置回调函数,Promise内部抛出的错误,不会反应到外部。第三,当处于pending状态时,无法得知目前进展到哪一个阶段(刚刚开始还是即将完成)。 2.实例方法 Promise.prototype.then() 为Promise实例添加状态改变时的回调函数,第一个参数是resolve状态的回调函数,第二个参数是reject状态的回调函数 then方法会返回一个新的Promise实例,因此可以采用链式写法,即then后面再调用一个then 1fetch('/api/xxx').then(res => res.json()).then(data => console.log(data)) Promise.prototype.catch() catch方法是.then(_,rejection)方法的别名,用于指定发生错误时的回调函数 catch方法不仅可以捕获reject函数的回调,也能捕获异步运行时抛出的异常 1234567const promise = new Promise(function(resolve, reject) { throw new Error('test');});promise.catch(function(error) { console.log(error);});// Error: test 上面代码中,promise抛出一个错误,就被catch()方法指定的回调函数捕获。注意,上面的写法与下面两种写法是等价的。 12345678910111213141516171819// 写法一const promise = new Promise(function(resolve, reject) { try { throw new Error('test'); } catch(e) { reject(e); }});promise.catch(function(error) { console.log(error);});// 写法二const promise = new Promise(function(resolve, reject) { reject(new Error('test'));});promise.catch(function(error) { console.log(error);}); Promise.prototyp.finally() finally方法用于指定不管Promise对象最后状态如何,都会执行操作。 1234promise.then(result => {···}).catch(error => {···}).finally(() => {···}); 3.静态方法 Promise.all() Promise.all()方法用于将多个Promise实例,包装成一个新的Promise实例,并且只有当所有Promise实例状态都变成fulfilled,该实例的状态才会变成fulfilled,并返回实例返回值组成的一个数组。 该方法常用于多个接口的并发请求 1Promise.all(api1, api2, api3).then(res => console.log(res)) // 会返回三个接口数据组成的数组 如果有一个实例的状态变为rejected,那么该实例的状态就会变为rejected Promise.race() 该方法同样是将多个Promise实例,包装成一个新的Promise实例 只要被包裹的Promise实例中,有一个状态变为fulfilled,那么该实例的状态就直接变为fulfilled 如果有一个被包裹的Promise状态变为rejected,那么Promise状态就会变为reject Promise.allSettled() Promise.allSettled()方法,用来确定一组异步组件操作是否都结束了,只有当被包裹的对象都发生了状态变更,返回的Promise对象才会发生状态变更 Promise.any() ES2021引入的新方法,只要被包裹的实例有一个变成fulfilled状态,包装实例就会变成fulfilled,如果所有参数实例都变成rejected,包装实例就会变成rejected状态 Promise.resolve() Promise.resolve方法将一个对象转为Promise对象,其参数分为四种情况 参数是一个Promise实例 如果参数是一个Promise实例,将不做修改,原封不动地返回这个实例 参数是一个thenable实例 thenable对象是指具有then方法的对象,Promise.resolve方法会将这个对象转为Promise对象,然后执行thenable对象的then方法 12345678910let thenable = { then: function(resolve, reject) { resolve(42); }};let p1 = Promise.resolve(thenable);p1.then(function (value) { console.log(value); // 42}); 参数不具有then()方法的对象,或者根本就不是对象 参数不是具有then()方法的对象,或根本就不是对象 如果参数是一个原始值,或者是一个不具有then()方法的对象,则Promise.resolve()方法返回一个新的 Promise 对象,状态为resolved。 123456const p = Promise.resolve('Hello');p.then(function (s) { console.log(s)});// Hello 不带任何参数 Promise.resolve()方法允许调用时不带参数,直接返回一个resolved状态的 Promise 对象。 所以,如果希望得到一个 Promise 对象,比较方便的方法就是直接调用Promise.resolve()方法。 12345const p = Promise.resolve();p.then(function () { // ...}); 需要注意的是,立即resolve()的 Promise 对象,是在本轮“事件循环”(event loop)的结束时执行,而不是在下一轮“事件循环”的开始时。 12345678910111213setTimeout(function () { console.log('three');}, 0);Promise.resolve().then(function () { console.log('two');});console.log('one');// one// two// three 上面代码中,setTimeout(fn, 0)在下一轮“事件循环”开始时执行,Promise.resolve()在本轮“事件循环”结束时执行,console.log('one')则是立即执行,因此最先输出。 Promise.reject() Promise.reject(reason)方法也会返回一个新的Promise实例,该实例的状态为rejected 12345678const p = Promise.reject('出错了');// 等同于const p = new Promise((resolve, reject) => reject('出错了'))p.then(null, function (s) { console.log(s)});// 出错了 4.应用 对事件监听处理异步的二次封装 Pormise在实际业务开发中常用于事件监听解决异步的二次封装 例如将图片转为Base64编码格式时,需要实例化一个FileReader对象,并且添加load事件监听 如果不用Promise处理,则需要传入一个回调函数获取 123456789101112function getBase64(img, callback) { const reader new FileReader(); reader.addEventListener('load', () => callback(reader.result)); reader.readAsDataURL(img);}// 使用getBase64(img, baseUrl => this.setState({ baseUrl }),); 如果使用Promise进行处理,则可以这么写 1234567891011121314function getBase65(img) { return new Promise(resolve => { const reader new FileReader(); reader.addEventListener('load', () => resolve(reader.result)); reader.readAsDataURL(img); })}// 使用getBase64(img).then(baseUrl => { this.setState({ baseUrl }),})","link":"/JavaScript/24.Promise/"},{"title":"import.meta属性","text":"import.metaimport.meta是一个给JavaScript模块暴露特定上下文的元数据属性的对象。有ECMAScript实现,原型是null,并且默认带有一个url对象,这个对象可扩展,它的属性都是可写,可配置,可枚举的 1<script type="module" src="./index.js"></script> // 必须是module 1console.log(import.meta); // {url: 'http://127.0.0.1:5500/index.js'}","link":"/JavaScript/26.import.meta/"},{"title":"let和const关键字","text":"1.letES6新增的声明变量关键字,所声明的变量,只在let命令所在的代码块内有效 块级作用域1234567{ let a = 10; var b = 10;}console.log(a);// ReferenceError: a is not defined.console.log(b);// 10 for循环计数器,就很合适使用let命令 123456789101112for (let i = 0; i < 10; i++) { // ...}console.log(i);// ReferenceError: i is not defined// 如果使用var,最后会输出10for (var i = 0; i < 10; i++) { // ...}console.log(i);// 10 不存在变量提升var关键字声明的变量,可以在声明之前使用,let关键字声明的变量,一定要在声明后使用,否则报错 12345console.log(foo); // undefinedvar foo = 2;console.log(bar); // 报错ReferenceErrorlet bar = 2; 上面代码中,变量foo用var命令声明,会发生变量提升,即脚本开始运行时,变量foo已经存在了,但是没有值,所以会输出undefined。变量bar用let命令声明,不会发生变量提升。这表示在声明它之前,变量bar是不存在的,这时如果用到它,就会抛出一个错误。 暂时性死区只要块级作用域内存在let关键字声明的变量,它所声明的变量就被绑定在这个区域,不再受到外部的影响 123456var a = 10;{ console.log(a); // 报错ReferenceError let a;} 有些“死区”比较隐蔽,不太容易发现。 12345function bar(x = y, y = 2) { return [x, y];}bar(); // 报错 上面代码中,调用bar函数之所以报错(某些实现可能不报错),是因为参数x默认值等于另一个参数y,而此时y还没有声明,属于“死区”。如果y的默认值是x,就不会报错,因为此时x已经声明了。 1234function bar(x = 2, y = x) { return [x, y];}bar(); // [2, 2] 另外,下面的代码也会报错,与var的行为不同。 123456// 不报错var x = x;// 报错let x = x;// ReferenceError: x is not defined 上面代码报错,也是因为暂时性死区。使用let声明变量时,只要变量在还没有声明完成前使用,就会报错。上面这行就属于这个情况,在变量x的声明语句还没有执行完成前,就去取x的值,导致报错”x 未定义“。 ES6 规定暂时性死区和let、const语句不出现变量提升,主要是为了减少运行时错误,防止在变量声明前就使用这个变量,从而导致意料之外的行为。这样的错误在 ES5 是很常见的,现在有了这种规定,避免此类错误就很容易了。 总之,暂时性死区的本质就是,只要一进入当前作用域,所要使用的变量就已经存在了,但是不可获取,只有等到声明变量的那一行代码出现,才可以获取和使用该变量。不允许重复声明 不允许重复声明let不允许在相同作用域内,重复声明同一个变量。 1234567891011// 报错function func() { let a = 10; var a = 1;}// 报错function func() { let a = 10; let a = 1;} 因此,不能在函数内部重新声明参数。 1234567891011function func(arg) { let arg;}func() // 报错function func(arg) { { let arg; }}func() // 不报错 constconst声明一个只读的常量。一旦声明,常量的值就不能改变。 12345const PI = 3.1415;PI // 3.1415PI = 3;// TypeError: Assignment to constant variable. 上面代码表明改变常量的值会报错。 const声明的变量不得改变值,这意味着,const一旦声明变量,就必须立即初始化,不能留到以后赋值。 12const foo;// SyntaxError: Missing initializer in const declaration 上面代码表示,对于const来说,只声明不赋值,就会报错。 const的作用域与let命令相同:只在声明所在的块级作用域内有效。 12345if (true) { const MAX = 5;}MAX // Uncaught ReferenceError: MAX is not defined const命令声明的常量也是不提升,同样存在暂时性死区,只能在声明的位置后面使用。 1234if (true) { console.log(MAX); // ReferenceError const MAX = 5;} 上面代码在常量MAX声明之前就调用,结果报错。 const声明的常量,也与let一样不可重复声明。 123456var message = "Hello!";let age = 25;// 以下两行都会报错const message = "Goodbye!";const age = 30; 本质const实际上是为了保证,变量指向的那个内存地址所保存的数据不能改动。对于基本数据类型来说,值就是保存在变量指向的那个内存地址中。但对于引用数据类型来说,变量指向的内存地址所保存的数据只是指针,const只能保证这个指针是固定的,至于它的数据结果是不是固定的就不可控制了。 如果真想要冻结对象,应该使用Object.freeze方法 12345const foo = Object.freeze({});// 常规模式时,下面一行不起作用;// 严格模式时,该行会报错foo.prop = 123;","link":"/JavaScript/7.let%E5%92%8Cconst/"},{"title":"数组常用的方法","text":"前言数组常见的操作方法可分为以下四种: 操作方法 排序方法 转换方法 迭代方法 一、操作方法数组基本操作可以归纳为增、删、改、查,需要留意的是哪些方法会对原数组产生影响,哪些方法不会 增下面前三种是对原数组产生影响的增加方法,第四种则不会对原数组产生影响 push() unshift() splice() concat() push()push 方法接收任意数量的参数,并将它们添加到数组末尾,返回数组的最新长度 1234const colors = [];const count = colors.push('red', 'green'); // 推入两项console.log(count) // 2 unshift()unshift() 在数组开头添加任意多个值,然后返回新的数组长度 123let colors = new Array(); // 创建一个数组let count = colors.unshift("red", "green"); // 从数组开头推入两项alert(count); // 2 splice()可以接收三个参数,分别是开始位置,要删除的元素数量,插入的元素;返回值是被删除的数组元素 1234const colors = ["red", "green", "blue"];const removed = colors.splice(1, 0, "yellow", "orange")console.log(colors) // red,yellow,orange,green,blueconsole.log(removed) // [] concat()首先会创建一个当前数组的副本,然后再把它的参数添加到副本末尾,最后返回这个新构建的数组,不会影响原始数组 1234let colors = ["red", "green", "blue"];let colors2 = colors.concat("yellow", ["black", "brown"]);console.log(colors); // ["red", "green","blue"]console.log(colors2); // ["red", "green", "blue", "yellow", "black", "brown"] 删下面三种都会影响原数组,最后一项不影响原数组: pop() shift() splice() slice() 改即修改原来数组的内容,常用 splice 查即查找元素,返回元素坐标或者元素值 indexOf() includes() find() 二、排序方法数组有两个方法可以用来对元素重新排序: reverse() sort() 三、转换方法常见的转换方法有: join() 四、迭代方法常用来迭代数组的方法(都不改变原数组)有如下几个: some() every() forEach() filter() map()","link":"/JavaScript/%E6%95%B0%E7%BB%84%E5%B8%B8%E7%94%A8%E7%9A%84%E6%96%B9%E6%B3%95%E6%9C%89%E5%93%AA%E4%BA%9B/"},{"title":"基础语法整理","text":"1.数据类型JavaScript数据类型包括7种基本类型和1种引用类型 基本类型 Number JavaScript的Number类型是采用IEEE754格式来表示整数和浮点的值,Number类型的值一般默认是十进制表示,当然也可以直接表示八进制和十六进制,八进制以0为首位,十六进制以0x为首位,但无论是几进制,在运算时都是以十进制的方式进行运算 双精度64位浮点格式 数值范围:-2^53-1~2^53-1 浮点数值:浮点数值最高精度是17位,超出的位数会被截掉,这也是为什么0.1+0.2 !== 0.3,在运算时,先将数值转换为二进制,运算结束后转换回来,最后得到的值是0.300000000004,所以不相等 NaN:NaN用于表示一个本来要返回数值的操作数未返回数值的情况,以防止抛出错误,NaN与任何值都不相等,包括其本身。isNaN()这个函数可以判断一个参数是否是数值,若不是数值,则会尝试转换为数值,无法转换的情况会返回false,这个函数适用于对象,在参数是对象的时候,会首先调用对象的valueof()方法,如果不能转为数值,则会调用toString()方法 数值的转换:这里指显示转换,主要通过三个方法进行转换:Number() parseInt(),第一个参数是要转换的值,第二个参数是进制,parseFloat()只会转换为十进制 String 字符串是非常常用的类型,主要需要注意两个地方,一个是字符串是不可变的,如果需要改变一个字符串的值,那么就需要销毁原来的字符串,然后再创建一个新的字符串。第二个是在转换为字符串的时候,有两个方法,toString()和String(),后者其实是调用前者 Boolean Boolean只有两个值,即true和false,该类型的字面值是区分大小写的,只有全小写才是boolean的值,其他的都是标识符。Boolean类型本身包含一个Boolean()方法,可以将其他类型转换为Boolean类型。 Symbol(ES6) ES6新增的基础数据类型,可以避免对象属性命名冲突,多个模块共享独一的属性 BigInt(ES2020) 用于表示对任意长度整数的支持 创建BigInt:在一个整数字面量后面加n或者调用BigInt函数 不要把BigInt和Number数字类型混用 BigInt不支持一元加法 Null null的主要作用是用来判断一个值是否存储了对象的引用 使用typeof操作符对null值进行检测时,会返回一个object值,这是因为null从逻辑的角度来说表示一个空指针对象。 在创建一个变量用来保存对象的时候,建议将变量初始化为null undefined 用来表示一个已经声明但没有被初始化的变量 引用类型 object 有一句话叫做JavaScript一切皆对象,对象是JavaScript的基础 所具有的属性和方法: constructor:执向对应的构造函数 hasOwnProperty():用来检查当前对象实例(不是原型)是否具有某个属性 isPrototypeOf():检测一个对象是否在另一个对象的原型链中 prototypeIsEnumberable():是否允许for-in枚举 toLocalString() toString() valueOf() 包括了Array Function Date RegExp Error 类型判断 typeof():除了null以外的基础类型都能判断,引用类型可以判断function Object.prototype.toString.call():可以检测出所有数据类型 instanceof:查找该属性是否属于原型链上的某个构造函数 constructor:通过构造函数去判断 2.深拷贝与浅拷贝 深拷贝层层拷贝,浅拷贝只拷贝第一层,深层只是引用 在深拷贝中,新对象中的更改不会影响原始对象。而浅拷贝新对象中的更改原始对象也会跟着更改 深拷贝中,原始对象不与新对象共享相同的属性,浅拷贝中,他们具有相同的属性 3.闭包闭包是一个能读取其他函数内部变量的函数 优点:使外部能访问到局部的变量 缺点:使用不当容易造成内存泄漏问题 1234567891011function a () { let num = 0 // 这是个闭包 return function () { return ++num }}const b = a()console.log(b()) // 1console.log(b()) // 2 4.变量提升使用var关键字声明的变量存在变量提升 12345678console.log(name) // undefinedvar name = "hjc"if(false) { var age = 23;}console.log(age) // undefined 函数提升 1234567console.log(fun) // function fun() {}function fun() {}if (false) { function fun2(){}}console.log(fun2) // undefined 不会报错 优先级:函数提升 > 变量提升 5.isNaN和Number.isNaN()的区别 isNaN:除了判断NaN为true,还会把不能转成数字的判断为true Number.isNaN:只有判断NaN时为true,其余情况都为false 6.遍历对象时,如何避免遍历出原型上的属性 使用hasOwnProperty() 1234567891011121314function Person(name) { this.name = name;}Person.prototype.age = 22;const person = new Person('jiacheng');for(const key in person) { console.log(key) // name age}for(const key in person) { person.hasoOwnProperty(key) && console.log(key) // name} 7.valueof与toString valueOf偏向于运算,toString偏向于显示 对象转换时,会优先调用toString 强转字符串优先调用toString,强转数字优先调用valueOf 正常情况下优先调用toString,运算操作符情况下优先调用valueOf 调用valueOf 调用者 返回值 返回值类型 Array 数组本身 Array Boolean 布尔值 Boolean Date 毫秒数 Number Function 函数本身 Function Number 数字值 Number Object 对象本身 Object String 字符串 String 调用toString 调用者 返回值 返回值类型 Array 数组转字符串,相当于Array.join() String Boolean 转字符串’true’、’false’ String Date 字符串日期,如’Fri Dec 23 2016 11:24:47 GMT+0800 (中国标准时间)’ String Number 数字字符串 String Object ‘[object Object]’ String String 字符串 String 8.JavaScript变量在内存中的具体存储形式 基本数据类型:存在栈内存中 引用数据类型:指针存在栈内存中,指向堆内存中一块地址,内容存在堆内存中 9.JavaScript装箱和拆箱装箱:把基本数据类型转化为对应的引用数据类型的操作 在声明一个基本数据类型时,若要调用对应引用数据类型上面的api,则直接调用即可 123456789var a = 'jiacheng'var index = a.indexOf('j')console.log(index) // 0// 装箱操作 1.创建String类型的一个实例;2.在实例上调用指定的方法;3.销毁这个实例var temp = new String('jiacheng');var index = temp.indexOf('j');temp = null;console.log(index) 拆箱:将引用数据类型转化为对应的基本数据类型的操作 通过valueOf和toString方法实现拆箱操作 123456789var objNum = new Number(123);var objStr =new String("123");console.log( typeof objNum ); //objectconsole.log( typeof objStr ); //objectconsole.log( typeof objNum.valueOf() ); //numberconsole.log( typeof objStr.valueOf() ); //stringconsole.log( typeof objNum.toString() ); // stringconsole.log( typeof objStr.toString() ); // string 注意:从ES6开始语法禁止显示地实例化基本数据类型所对应的包装类,因此Symbol和BigInt的构造函数无法被new关键字实例化 10. null和undefined的异同点相同的 都是空变量 对应的布尔值都为false null == undefined为true 不同点 typeof判断null为object,判断undefined为undefined null转为number类型为0,undefined转成number类型为NaN null表示一个对象未初始化,undefined表示初始化了但为赋值 null === undefined为false 11.如何判断数据类型 typeof xxx:能判断出number,string,undefined,boolean,symbol,bigint,object,function(null是object) Object.prototype.toString.call(xxx):能判断出大部分类型 Array.isArray(xxx):判断是否为数组 12.为什么typeof null是object不同的数据类型在底层都是通过二进制来表示的,二进制的前三位全是0的会被判断为object,而null的底层全是0,所以会被判断为object 13、== 与 === 的区别? ==:在比较过程中会存在隐式转换 ===:需要类型相同,值相同,才能为true 14、JavaScript的隐式转换规则? 1、转成string类型: +(字符串连接符) 2、转成number类型:++/–(自增自减运算符) + - * / %(算术运算符) > < >= <= == != === !== (关系运算符) 3、转成boolean类型:!(逻辑非运算符) 15.双等号左右两边的转换规则 null == undefined为true 如果有一个为boolean,则在比较之前会先转换成number类型再比较,true转为1,false转为0 如果一个操作数是字符串,另一个操作数是数值,在比较相等性之前先将字符串转换为数值 如果一个操作数是对象,另一个操作数不是,则调用对象的toString()方法,用得到的基本类型值按照前面的规则进行比较 16. undefined >= undefined为什么是false按照隐式转换规则,可以转为NaN >=NaN,所以是false 17.null >= null为什么是true按照隐式转换规则,可以转为 0 >= 0 ,所以为true 18.[] == ![] 为什么是true按照双等号左右两边的转换规则 ! 优先级高于 ==,[]不是假值,所以先转换成 ![] == false 右边为布尔值,false先转数字0,所以可转换为[] == 0 左边为对象,[]调用toString转为 '',转换为'' == 0 左边为字符串,''转换为0,最终为 0 == 0 19.0.1 + 0.2 === 0.3,对吗?不对,JavaScript的计算存在精度丢失问题 原因:JavaScript中小数是浮点数,需转二进制进行运算,有些小数无法用二进制表示,所以只能取近似值,所以造成误差 解决方法: 先变成整数运算,然后再变回小数 toFixed() 性能不好,不推荐 20.什么是匿名函数就是没有函数名的函数 21.绑定点击事件有几种方式 xxx.onclick = function (){} <xxx onclick=""></xxx> xxx.addEventListence('click', function(){}, false) 22. addEventListence的第三个参数是干嘛的第三个变量传一个布尔值,是否要阻止冒泡,默认是false,不阻止冒泡 23.函数声明和函数表达式的区别 函数声明:享受函数提升 函数表达式:归于类变量声明,享受变量提升 函数提升优先级 > 变量提升优先级 123456console.log(fun) // fun () {}// 函数表达式var fun = function(name) {}// 函数声明function fun () {}console.log(fun) // fun (name) {} 24.JavaScript事件流模型 事件冒泡:由具体的元素接收,并往上传播 事件捕获:由最不具体的元素接收,并往下传播 DOM事件流:捕获阶段 -> 目标阶段 -> 冒泡阶段 25.Ajax Axios Fetch Ajax:是对XMLHttpRequest对象(XHR)的封装 Axios:是基于Promise对XHR对象的封装 Fetch:是window的一个方法,也是基于Promise,但是与XHR无关,不支持IE 26.load、$(document).ready、DOMContentLoaded的区别?DOM文档加载的步骤为: 解析HTML结构。 加载外部脚本和样式表文件。 解析并执行脚本代码。 DOM树构建完成。// DOMContentLoaded触发、$(document).ready触发 加载图片等外部文件。 页面加载完毕。// load触发 27.阻住事件冒泡1234567function stopBubble(e) { if (e.stopPropagation) { e.stopPropagation() } else { window.event.cancelBubble = true; }} 28.阻住事件默认行为 比如说链接a,input type为submit等 123456function stopDefault(e) { if (e.preventDefault) { e.preventDefault(); } else { window.event.returnValue = false;} 29.事件委托当所有子元素都需要绑定相同的事件的时候,可以把事件绑定在父元素上,这就是事件委托,优点有: 绑定在父元素上只需要绑定一次,节省性能 子元素不需要每个都去绑定同一事件 如果后续又有新的子元素添加,会由于事件委托的原因,自动接收到父元素的事件监听 30.如何实现数组去重1234// 使用 Set 去重function quchong (arr) { return [...new Set(arr)]} 31.NaN是什么,有什么特点 NaN不等于自身,也就是 NaN === NaN 为 false NaN为假值,转布尔值为false NaN本质是一个number,typeof NaN === number 32.处理异步的方法有哪些 回调函数 Promise 事件监听 发布订阅 Generator(async/await) 33.创建一个对象new Object创建12const obj = new Object()obj.name = 'Sunshine_Lin' 字面量创建1const obj = { name: 'Sunshin_Lin' } 工厂模式创建123456function createObj(name) { const obj = new Object() obj.name = name return obj}const obj = createObj('Sunshine_Lin') 构造函数创建1234function Person(name) { this.name = name}const person = new Person('Sunshine_Lin') 34.this指向的四种情况 指向new操作符创建的实例 12345function Person(name) { this.name = name; console.log(this)}const person = new Person('jiacheng') // this指向当前person的实例对象 指向window 12345function() { console.log(this);}fn() // 指向window 指向对象调用方法 1234567const target = { fn: function () { console.log(this) }}target.fn() // targetconst fn = target.fnfn() // 浏览器window,node里global call apply bind改变this 123456789101112131415161718const obj1 = { name: '黄家程', sayName: function() { console.log(this.name) }}const obj2 = { name: 'jiacheng_huang'}// 改变sayName的this指向obj2obj1.sayName.call(obj2) // jiacheng_huang// 改变sayName的this指向obj2obj1.sayName.apply(obj2) // jiacheng_huang// 改变sayName的this指向obj2const fn = obj1.sayName.bind(obj2)fn() // jiacheng_huang 35、数组的常用方法有哪些? 方法 作用 是否影响原数组 push 在数组后添加元素,返回数组长度 ✅ pop 删除数组最后一项,返回被删除项 ✅ shift 删除数组第一项,并返回数组 ✅ unshift 数组开头添加元素,返回添加的元素 ✅ reserve 反转一个数组,返回修改后的数组 ✅ sort 排序一个数组,返回修改后的数组 ✅ splice 截取数组,返回被截取的区间 ✅ join 将一个数组所有元素连接成字符串并返回这个字符串 ❌ concat arr1.concat(arr2, arr3) 连接数组 ❌ map 操作数组每一项并返回一个新数组 ❌ forEach 遍历数组,没有返回值 ❌ find 对数组进行筛选,返回第一个符合条件的元素 ❌ filter 对数组所有项进行判断,返回符合规则的新数组 ❌ every 数组每一项都符合规则才返回true ❌ some 数组有符合规则的一项就返回true ❌ reduce 接收上一个return和数组的下一项 ❌ flat 数组扁平化 ❌ slice 截取数组,返回被截取的区间 ❌ 36、Math的常用方法有哪些? 方法 作用 Math.max(arr) 取arr中的最大值 Math.min(arr) 取arr中的最小值 Math.ceil(小数) 小数向上取整 Math.floor(小数) 小数向下取整 Math.round(小数) 小数四舍五入 Math.sqrt(num) 对num进行开方 Math.pow(num, m) 对num取m次幂 Math.random() * num 取[0,num)的随机数 37、哪些因素导致内存泄漏?如何解决?请看我这篇文章哪是大神?只是用他人七夕约会时间,整理「JS避免内存泄漏」罢了 38、讲讲JavaScript的垃圾回收机制看我这篇文章:赠你13张图,助你20分钟打败了「V8垃圾回收机制」 39、JS中有哪些不同类型的弹出框?在JS中有三种类型的弹出框可用,分别是: Alert Confirm Prompt 40. 如何将 JS 日期转换为ISO标准toISOString() 方法用于将js日期转换为ISO标准。 它使用ISO标准将js Date对象转换为字符串。如: 12345var date = new Date();var n = date.toISOString();console.log(n);// YYYY-MM-DDTHH:mm:ss.sssZ复制代码 41、如何在JS中编码和解码 URLencodeURI() 函数用于在JS中对URL进行编码。它将url字符串作为参数并返回编码的字符串。 注意: encodeURI()不会编码类似这样字符: / ? : @ & = + $ #,如果需要编码这些字符,请使用encodeURIComponent()。 用法: 12var uri = "my profile.php?name=sammer&occupation=pāntiNG";var encoded_uri = encodeURI(uri); decodeURI() 函数用于解码js中的URL。它将编码的url字符串作为参数并返回已解码的字符串,用法: 123var uri = "my profile.php?name=sammer&occupation=pāntiNG";var encoded_uri = encodeURI(uri);decodeURI(encoded_uri); 42、什么是BOM?有哪些api?BOM就是browser object model,浏览器对象模型 api 作用 代表方法或属性 window.history 操纵浏览器的记录 history.back() history.go(-1) window.innerHeight 获取浏览器窗口的高度 window.innerWidth 获取浏览器窗口的宽度 window.location 操作刷新按钮和地址栏 location.host:获取域名和端口 location.hostname:获取主机名 location.port:获取端口号 location.pathname:获取url的路径 location.search:获取?开始的部分 location.href:获取整个url location.hash:获取#开始的部分 location.origin:获取当前域名 location.navigator:获取当前浏览器信息 43、BOM 和 DOM 的关系BOM全称Browser Object Model,即浏览器对象模型,主要处理浏览器窗口和框架。 DOM全称Document Object Model,即文档对象模型,是 HTML 和XML 的应用程序接口(API),遵循W3C 的标准,所有浏览器公共遵守的标准。 JS是通过访问BOM(Browser Object Model)对象来访问、控制、修改客户端(浏览器),由于BOM的window包含了document,window对象的属性和方法是直接可以使用而且被感知的,因此可以直接使用window对象的document属性,通过document属性就可以访问、检索、修改XHTML文档内容与结构。因为document对象又是DOM的根节点。 可以说,BOM包含了DOM(对象),浏览器提供出来给予访问的是BOM对象,从BOM对象再访问到DOM对象,从而js可以操作浏览器以及浏览器读取到的文档。 44、JS中的substr()和substring()函数有什么区别substr() 函数的形式为substr(startIndex,length)。 它从startIndex返回子字符串并返回’length’个字符数。 12var s = "hello";( s.substr(1,4) == "ello" ) // true substring() 函数的形式为substring(startIndex,endIndex)。 它返回从startIndex到endIndex - 1的子字符串。 123var s = "hello";( s.substring(1,4) == "ell" ) // true复制代码 45、解释一下 “use strict” ?“use strict”是Es5中引入的js指令。 使用“use strict”指令的目的是强制执行严格模式下的代码。 在严格模式下,咱们不能在不声明变量的情况下使用变量。 早期版本的js忽略了“use strict”。 46.ES5继承方式 定义一个类 123456789101112// 定义一个类function Animal(name) { this.name = name || 'Animal'; // 实例方法 this.sleep = function() { console.log(this.name + '正在睡觉'); }}// 原型方法Animal.prototype.eat = function(food) { console.log(this.name + '正在吃' + food)} 1.原型链继承核心:将父类的实例作为子类的原型 优点 实例是子类的实例,也是父类的实例 父类新增原型方法/属性,子类都能访问到 简单,易于实现 缺点 子类要想新增属性和方法,必须要在new Animal()这样的语句之后,不能放在构造器中 原型对象的所有属性被所有实例共享 实现子类实例时,无法向父类构造函数传参 不支持多继承 12345678910111213function Cat() { }Cat.prototype = new Animal();Cat.prototype.name = 'cat';var cat = new Cat();console.log(cat.name);cat.eat('fish'); // cat正在吃fishcat.sleep(); // cat正在睡觉console.log(cat instanceof Animal); // trueconsole.log(cat instanceof Cat); // true 2.构造继承核心:使用父类的构造器来增强子类实例,等于是复制父类的实例属性给子类(没用到原型) 优点 解决了原型链继承中,子类实例共享父类引用属性的问题 创建子类实例时,可以向父类传递参数 可以实现多继承(call多个父类对象) 缺点 实例并不是父类实例,只是子类的实例 只能继承父类的实例属性和实例方法,不能继承父类原型上的属性和方法 无法实现函数复用,每个子类都有父类实例函数的副本,影响性能 123456789101112function Cat(name) { Animal.call(this); this.name = name || 'Tom';}var cat = new Cat();console.log(cat.name); // Tomcat.sleep(); // Tom正在睡觉cat.eat('fish'); // 报错console.log(cat instanceof Animal); // falseconsole.log(cat instanceof Cat); // true 3.实例继承核心:为父类实例添加新特性,作为子类实例返回 优点: 不限制调用方式,不管是new 子类()还是子类(),返回的对象具有相同效果 缺点: 实例是父类的实例,不是子类的实例 不支持多继承 123456789101112131415161718function Cat(name) { var instance = new Animal(); instance.name = name || 'Tom'; return instance;}var cat = new Cat();console.log(cat.name); // Tomcat.sleep(); // Tom正在睡觉cat.eat('fish'); // Tom正在吃fishconsole.log(cat instanceof Animal); // trueconsole.log(cat instanceof Cat); // false 4.拷贝继承核心:一个一个拷贝 优点 支持多继承 缺点 因为要一个一个拷贝,所以效率很低 无法获取父类的不可枚举方法 12345678910111213141516function Cat(name) { var animal = new Animal(); for (var p in animal) { Cat.prototype[p] = animal[p]; } this.name = name || 'Tom'}var cat = new Cat();console.log(cat.name); // Tomcat.sleep() // Tom正在睡觉!cat.eat('fish'); // Tom正在吃fishconsole.log(cat instanceof Animal); // falseconsole.log(cat instanceof Cat); // true 5.组合继承(原型链继承+构造继承)核心:通过父类构造,继承父类的属性并保留传参的优点,然后通过将父类实例作为子类原型,实现函数复用 优点 弥补了构造继承的缺陷,可以继承实例的属性/方法,也可以继承原型上的属性/方法 既是子类的实例,也是父类的实例 可以向父类传参 函数可以复用 缺点 调用了两次父类的构造函数,生成了两份实例 123456789101112131415function Cat(name) { Animal.call(this); this.name = name || 'Tom';}Cat.prototype = new Animal();Cat.prototype.constructor = Cat;var cat = new Cat();console.log(cat.name); // Tomcat.sleep() // Tom正在睡觉!cat.eat('fish'); // Tom正在吃fishconsole.log(cat instanceof Animal); // trueconsole.log(cat instanceof Cat); // true 6.寄生组合继承核心:通过寄生方式,砍掉父类的实例属性,这样,在调用两次父类的构造时,就不会初始化两次实例方法/属性,避免继承组合的缺点 优点 几乎完美 缺点 实现复杂 123456789101112131415161718function Cat(name) { Animal.call(this); this.name = name || 'Tom';}// 创建一个没有实例方法的构造函数var Super = function () { }Super.prototype = Animal.prototype;// 将实例作为子类的原型Cat.prototype = new Super();// Test Codevar cat = new Cat();console.log(cat.name); // Tomcat.sleep() // Tom正在睡觉!cat.eat('fish') // // Tom正在吃fishconsole.log(cat instanceof Animal); // trueconsole.log(cat instanceof Cat); //true","link":"/JavaScript/1.%E8%AF%AD%E6%B3%95%E6%95%B4%E7%90%86/"},{"title":"数组的扩展","text":"1.扩展运算符扩展运算符(spread)是三个点(…)。它好比rest参数的逆运算,将一个数组用逗号分隔的参数序列 12345678console.log(...[1, 2, 3])// 1 2 3console.log(1, ...[2, 3, 4], 5)// 1 2 3 4 5[...document.querySelectorAll('div')]// [<div>, <div>, <div>] 替代apply方法由于扩展运算符可以展开数组,所以不再需要apply方法,将数组转为函数的参数了。 12345678910111213// ES5 的写法function f(x, y, z) { // ...}var args = [0, 1, 2];f.apply(null, args);// ES6的写法function f(x, y, z) { // ...}let args = [0, 1, 2];f(...args); 扩展运算符的应用(1)复制数组 数组是复合的数据类型,直接复制的话,只是复制了指向底层数据结构的指针,而不是克隆一个全新的数组。 12345const a1 = [1, 2];const a2 = a1;a2[0] = 2;a1 // [2, 2] 上面代码中,a2并不是a1的克隆,而是指向同一份数据的另一个指针。修改a2,会直接导致a1的变化。 ES5 只能用变通方法来复制数组。 12345const a1 = [1, 2];const a2 = a1.concat();a2[0] = 2;a1 // [1, 2] 上面代码中,a1会返回原数组的克隆,再修改a2就不会对a1产生影响。 扩展运算符提供了复制数组的简便写法。 12345const a1 = [1, 2];// 写法一const a2 = [...a1];// 写法二const [...a2] = a1; 上面的两种写法,a2都是a1的克隆。 (2)合并数组 扩展运算符提供了数组合并的新写法。 1234567891011const arr1 = ['a', 'b'];const arr2 = ['c'];const arr3 = ['d', 'e'];// ES5 的合并数组arr1.concat(arr2, arr3);// [ 'a', 'b', 'c', 'd', 'e' ]// ES6 的合并数组[...arr1, ...arr2, ...arr3]// [ 'a', 'b', 'c', 'd', 'e' ] 不过,这两种方法都是浅拷贝,使用的时候需要注意。 12345678const a1 = [{ foo: 1 }];const a2 = [{ bar: 2 }];const a3 = a1.concat(a2);const a4 = [...a1, ...a2];a3[0] === a1[0] // truea4[0] === a1[0] // true 上面代码中,a3和a4是用两种不同方法合并而成的新数组,但是它们的成员都是对原数组成员的引用,这就是浅拷贝。如果修改了引用指向的值,会同步反映到新数组。 (3)与解构赋值结合 扩展运算符可以与解构赋值结合起来,用于生成数组。 1234// ES5a = list[0], rest = list.slice(1)// ES6[a, ...rest] = list 下面是另外一些例子。 1234567891011const [first, ...rest] = [1, 2, 3, 4, 5];first // 1rest // [2, 3, 4, 5]const [first, ...rest] = [];first // undefinedrest // []const [first, ...rest] = ["foo"];first // "foo"rest // [] 如果将扩展运算符用于数组赋值,只能放在参数的最后一位,否则会报错。 12345const [...butLast, last] = [1, 2, 3, 4, 5];// 报错const [first, ...middle, last] = [1, 2, 3, 4, 5];// 报错 (4)字符串 扩展运算符还可以将字符串转为真正的数组。 12[...'hello']// [ "h", "e", "l", "l", "o" ] 上面的写法,有一个重要的好处,那就是能够正确识别四个字节的 Unicode 字符。 12'x\\uD83D\\uDE80y'.length // 4[...'x\\uD83D\\uDE80y'].length // 3 上面代码的第一种写法,JavaScript 会将四个字节的 Unicode 字符,识别为 2 个字符,采用扩展运算符就没有这个问题。因此,正确返回字符串长度的函数,可以像下面这样写。 12345function length(str) { return [...str].length;}length('x\\uD83D\\uDE80y') // 3 凡是涉及到操作四个字节的 Unicode 字符的函数,都有这个问题。因此,最好都用扩展运算符改写。 1234567let str = 'x\\uD83D\\uDE80y';str.split('').reverse().join('')// 'y\\uDE80\\uD83Dx'[...str].reverse().join('')// 'y\\uD83D\\uDE80x' 上面代码中,如果不用扩展运算符,字符串的reverse操作就不正确。 (5)实现了 Iterator 接口的对象 任何定义了遍历器(Iterator)接口的对象(参阅 Iterator 一章),都可以用扩展运算符转为真正的数组。 12let nodeList = document.querySelectorAll('div');let array = [...nodeList]; 上面代码中,querySelectorAll方法返回的是一个NodeList对象。它不是数组,而是一个类似数组的对象。这时,扩展运算符可以将其转为真正的数组,原因就在于NodeList对象实现了 Iterator 。 123456789Number.prototype[Symbol.iterator] = function*() { let i = 0; let num = this.valueOf(); while (i < num) { yield i++; }}console.log([...5]) // [0, 1, 2, 3, 4] 上面代码中,先定义了Number对象的遍历器接口,扩展运算符将5自动转成Number实例以后,就会调用这个接口,就会返回自定义的结果。 对于那些没有部署 Iterator 接口的类似数组的对象,扩展运算符就无法将其转为真正的数组。 123456789let arrayLike = { '0': 'a', '1': 'b', '2': 'c', length: 3};// TypeError: Cannot spread non-iterable object.let arr = [...arrayLike]; 上面代码中,arrayLike是一个类似数组的对象,但是没有部署 Iterator 接口,扩展运算符就会报错。这时,可以改为使用Array.from方法将arrayLike转为真正的数组。 (6)Map 和 Set 结构,Generator 函数 扩展运算符内部调用的是数据结构的 Iterator 接口,因此只要具有 Iterator 接口的对象,都可以使用扩展运算符,比如 Map 结构。 1234567let map = new Map([ [1, 'one'], [2, 'two'], [3, 'three'],]);let arr = [...map.keys()]; // [1, 2, 3] Generator 函数运行后,返回一个遍历器对象,因此也可以使用扩展运算符。 1234567const go = function*(){ yield 1; yield 2; yield 3;};[...go()] // [1, 2, 3] 上面代码中,变量go是一个 Generator 函数,执行后返回的是一个遍历器对象,对这个遍历器对象执行扩展运算符,就会将内部遍历得到的值,转为一个数组。 如果对没有 Iterator 接口的对象,使用扩展运算符,将会报错。 12const obj = {a: 1, b: 2};let arr = [...obj]; // TypeError: Cannot spread non-iterable object 2.Array.from()Array.from()方法用于将两类对象转为真正的数组:类似数组的对象(array-like object)和可遍历(iterable)的对象(包括 ES6 新增的数据结构 Set 和 Map) 下面是一个类似数组的对象,Array.from将它转为真正的数组。 123456789101112let arrayLike = { '0': 'a', '1': 'b', '2': 'c', length: 3};// ES5的写法var arr1 = [].slice.call(arrayLike); // ['a', 'b', 'c']// ES6的写法let arr2 = Array.from(arrayLike); // ['a', 'b', 'c'] 实际应用中,常见的类似数组的对象是 DOM 操作返回的 NodeList 集合,以及函数内部的arguments对象。Array.from都可以将它们转为真正的数组。 1234567891011// NodeList对象let ps = document.querySelectorAll('p');Array.from(ps).filter(p => { return p.textContent.length > 100;});// arguments对象function foo() { var args = Array.from(arguments); // ...} 值得提醒的是,扩展运算符(...)也可以将某些数据结构转为数组。 1234567// arguments对象function foo() { const args = [...arguments];}// NodeList对象[...document.querySelectorAll('div')] 扩展运算符背后调用的是遍历器接口(Symbol.iterator),如果一个对象没有部署这个接口,就无法转换。Array.from方法还支持类似数组的对象。所谓类似数组的对象,本质特征只有一点,即必须有length属性。因此,任何有length属性的对象,都可以通过Array.from方法转为数组,而此时扩展运算符就无法转换。 12Array.from({ length: 3 });// [ undefined, undefined, undefined ] 上面代码中,Array.from返回了一个具有三个成员的数组,每个位置的值都是undefined。扩展运算符转换不了这个对象。 对于还没有部署该方法的浏览器,可以用Array.prototype.slice方法替代。 123const toArray = (() => Array.from ? Array.from : obj => [].slice.call(obj))(); 对于还没有部署该方法的浏览器,可以用Array.prototype.slice方法替代。 123const toArray = (() => Array.from ? Array.from : obj => [].slice.call(obj))(); 3.Array.of()Array.of()方法用于将一组值,转换为数组。 123Array.of(3, 11, 8) // [3,11,8]Array.of(3) // [3]Array.of(3).length // 1 Array.of()总是返回参数值组成的数组。如果没有参数,就返回一个空数组。 Array.of()方法可以用下面的代码模拟实现。 123function ArrayOf(){ return [].slice.call(arguments);} 这个方法的主要目的,是弥补数组构造函数Array()的不足。因为参数个数的不同,会导致Array()的行为有差异。 123Array() // []Array(3) // [, , ,]Array(3, 11, 8) // [3, 11, 8] 上面代码中,Array()方法没有参数、一个参数、三个参数时,返回的结果都不一样。只有当参数个数不少于 2 个时,Array()才会返回由参数组成的新数组。参数只有一个正整数时,实际上是指定数组的长度。 Array.of()基本上可以用来替代Array()或new Array(),并且不存在由于参数不同而导致的重载。它的行为非常统一。 4.数组实例copyWithin()数组实例的copyWithin()方法,在当前数组内部,将指定位置的成员复制到其他位置(会覆盖原有成员),然后返回当前数组。也就是说,使用这个方法,会修改当前数组。 1Array.prototype.copyWithin(target, start = 0, end = this.length) 它接受三个参数。 target(必需):从该位置开始替换数据。如果为负值,表示倒数。 start(可选):从该位置开始读取数据,默认为 0。如果为负值,表示从末尾开始计算。 end(可选):到该位置前停止读取数据,默认等于数组长度。如果为负值,表示从末尾开始计算。 这三个参数都应该是数值,如果不是,会自动转为数值。 12[1, 2, 3, 4, 5].copyWithin(0, 3)// [4, 5, 3, 4, 5] 上面代码表示将从 3 号位直到数组结束的成员(4 和 5),复制到从 0 号位开始的位置,结果覆盖了原来的 1 和 2。 下面是更多例子。 123456789101112131415161718192021// 将3号位复制到0号位[1, 2, 3, 4, 5].copyWithin(0, 3, 4)// [4, 2, 3, 4, 5]// -2相当于3号位,-1相当于4号位[1, 2, 3, 4, 5].copyWithin(0, -2, -1)// [4, 2, 3, 4, 5]// 将3号位复制到0号位[].copyWithin.call({length: 5, 3: 1}, 0, 3)// {0: 1, 3: 1, length: 5}// 将2号位到数组结束,复制到0号位let i32a = new Int32Array([1, 2, 3, 4, 5]);i32a.copyWithin(0, 2);// Int32Array [3, 4, 5, 4, 5]// 对于没有部署 TypedArray 的 copyWithin 方法的平台// 需要采用下面的写法[].copyWithin.call(new Int32Array([1, 2, 3, 4, 5]), 0, 3, 4);// Int32Array [4, 2, 3, 4, 5] 5.数组实例find()和findIndex()数组实例的find方法,用于找出第一个符合条件的数组成员。它的参数是一个回调函数,所有数组成员依次执行该回调函数,直到找出第一个返回值为true的成员,然后返回该成员。如果没有符合条件的成员,则返回undefined。 12[1, 4, -5, 10].find((n) => n < 0)// -5 上面代码找出数组中第一个小于 0 的成员。 123[1, 5, 10, 15].find(function(value, index, arr) { return value > 9;}) // 10 上面代码中,find方法的回调函数可以接受三个参数,依次为当前的值、当前的位置和原数组。 数组实例的findIndex方法的用法与find方法非常类似,返回第一个符合条件的数组成员的位置,如果所有成员都不符合条件,则返回-1。 123[1, 5, 10, 15].findIndex(function(value, index, arr) { return value > 9;}) // 2 这两个方法都可以接受第二个参数,用来绑定回调函数的this对象。 12345function f(v){ return v > this.age;}let person = {name: 'John', age: 20};[10, 12, 26, 15].find(f, person); // 26 上面的代码中,find函数接收了第二个参数person对象,回调函数中的this对象指向person对象。 另外,这两个方法都可以发现NaN,弥补了数组的indexOf方法的不足。 12345[NaN].indexOf(NaN)// -1[NaN].findIndex(y => Object.is(NaN, y))// 0 上面代码中,indexOf方法无法识别数组的NaN成员,但是findIndex方法可以借助Object.is方法做到。 6.数组实例fill()fill方法使用给定值,填充一个数组。 12345['a', 'b', 'c'].fill(7)// [7, 7, 7]new Array(3).fill(7)// [7, 7, 7] 上面代码表明,fill方法用于空数组的初始化非常方便。数组中已有的元素,会被全部抹去。 fill方法还可以接受第二个和第三个参数,用于指定填充的起始位置和结束位置。 12['a', 'b', 'c'].fill(7, 1, 2)// ['a', 7, 'c'] 上面代码表示,fill方法从 1 号位开始,向原数组填充 7,到 2 号位之前结束。 注意,如果填充的类型为对象,那么被赋值的是同一个内存地址的对象,而不是深拷贝对象。 123456789let arr = new Array(3).fill({name: "Mike"});arr[0].name = "Ben";arr// [{name: "Ben"}, {name: "Ben"}, {name: "Ben"}]let arr = new Array(3).fill([]);arr[0].push(5);arr// [[5], [5], [5]] 7.数组实例entries(),keys()和values()ES6 提供三个新的方法——entries(),keys()和values()——用于遍历数组。它们都返回一个遍历器对象(详见《Iterator》一章),可以用for...of循环进行遍历,唯一的区别是keys()是对键名的遍历、values()是对键值的遍历,entries()是对键值对的遍历。 12345678910111213141516for (let index of ['a', 'b'].keys()) { console.log(index);}// 0// 1for (let elem of ['a', 'b'].values()) { console.log(elem);}// 'a'// 'b'for (let [index, elem] of ['a', 'b'].entries()) { console.log(index, elem);}// [0, 'a']// [1, 'b'] 8.数组实例的 includes()Array.prototype.includes方法返回一个布尔值,表示某个数组是否包含给定的值,与字符串的includes方法类似。ES2016 引入了该方法。 123[1, 2, 3].includes(2) // true[1, 2, 3].includes(4) // false[1, 2, NaN].includes(NaN) // true 该方法的第二个参数表示搜索的起始位置,默认为0。如果第二个参数为负数,则表示倒数的位置,如果这时它大于数组长度(比如第二个参数为-4,但数组长度为3),则会重置为从0开始。 12[1, 2, 3].includes(3, 3); // false[1, 2, 3].includes(3, -1); // true 没有该方法之前,我们通常使用数组的indexOf方法,检查是否包含某个值。 123if (arr.indexOf(el) !== -1) { // ...} indexOf方法有两个缺点,一是不够语义化,它的含义是找到参数值的第一个出现位置,所以要去比较是否不等于-1,表达起来不够直观。二是,它内部使用严格相等运算符(===)进行判断,这会导致对NaN的误判。 12[NaN].indexOf(NaN)// -1 includes使用的是不一样的判断算法,就没有这个问题。 12[NaN].includes(NaN)// true 下面代码用来检查当前环境是否支持该方法,如果不支持,部署一个简易的替代版本。 123456const contains = (() => Array.prototype.includes ? (arr, value) => arr.includes(value) : (arr, value) => arr.some(el => el === value))();contains(['foo', 'bar'], 'baz'); // => false 另外,Map 和 Set 数据结构有一个has方法,需要注意与includes区分。 Map 结构的has方法,是用来查找键名的,比如Map.prototype.has(key)、WeakMap.prototype.has(key)、Reflect.has(target, propertyKey)。 Set 结构的has方法,是用来查找值的,比如Set.prototype.has(value)、WeakSet.prototype.has(value)。 9.数组实例的 flat(),flatMap()数组的成员有时还是数组,Array.prototype.flat()用于将嵌套的数组“拉平”,变成一维的数组。该方法返回一个新数组,对原数据没有影响。 12[1, 2, [3, 4]].flat()// [1, 2, 3, 4] 上面代码中,原数组的成员里面有一个数组,flat()方法将子数组的成员取出来,添加在原来的位置。 flat()默认只会“拉平”一层,如果想要“拉平”多层的嵌套数组,可以将flat()方法的参数写成一个整数,表示想要拉平的层数,默认为1。 12345[1, 2, [3, [4, 5]]].flat()// [1, 2, 3, [4, 5]][1, 2, [3, [4, 5]]].flat(2)// [1, 2, 3, 4, 5] 上面代码中,flat()的参数为2,表示要“拉平”两层的嵌套数组。 如果不管有多少层嵌套,都要转成一维数组,可以用Infinity关键字作为参数。 12[1, [2, [3]]].flat(Infinity)// [1, 2, 3] 如果原数组有空位,flat()方法会跳过空位。 12[1, 2, , 4, 5].flat()// [1, 2, 4, 5] flatMap()方法对原数组的每个成员执行一个函数(相当于执行Array.prototype.map()),然后对返回值组成的数组执行flat()方法。该方法返回一个新数组,不改变原数组。 123// 相当于 [[2, 4], [3, 6], [4, 8]].flat()[2, 3, 4].flatMap((x) => [x, x * 2])// [2, 4, 3, 6, 4, 8] flatMap()只能展开一层数组。 123// 相当于 [[[2]], [[4]], [[6]], [[8]]].flat()[1, 2, 3, 4].flatMap(x => [[x * 2]])// [[2], [4], [6], [8]] 上面代码中,遍历函数返回的是一个双层的数组,但是默认只能展开一层,因此flatMap()返回的还是一个嵌套数组。 flatMap()方法的参数是一个遍历函数,该函数可以接受三个参数,分别是当前数组成员、当前数组成员的位置(从零开始)、原数组。 123arr.flatMap(function callback(currentValue[, index[, array]]) { // ...}[, thisArg]) flatMap()方法还可以有第二个参数,用来绑定遍历函数里面的this。 10.数组的空位ES6明确规定将空位转为undefined Array.from方法会将数组的空位,转为undefined,也就是说,这个方法不会忽略空位。 12Array.from(['a',,'b'])// [ "a", undefined, "b" ] 扩展运算符(...)也会将空位转为undefined。 12[...['a',,'b']]// [ "a", undefined, "b" ] copyWithin()会连空位一起拷贝。 1[,'a','b',,].copyWithin(2,0) // [,"a",,"a"] fill()会将空位视为正常的数组位置。 1new Array(3).fill('a') // ["a","a","a"] for...of循环也会遍历空位。 123456let arr = [, ,];for (let i of arr) { console.log(1);}// 1// 1 上面代码中,数组arr有两个空位,for...of并没有忽略它们。如果改成map方法遍历,空位是会跳过的。 11.Array.prototype.sort() 的排序稳定性排序稳定性(stable sorting)是排序算法的重要属性,指的是排序关键字相同的项目,排序前后的顺序不变。 1234567891011121314const arr = [ 'peach', 'straw', 'apple', 'spork'];const stableSorting = (s1, s2) => { if (s1[0] < s2[0]) return -1; return 1;};arr.sort(stableSorting)// ["apple", "peach", "straw", "spork"] 上面代码对数组arr按照首字母进行排序。排序结果中,straw在spork的前面,跟原始顺序一致,所以排序算法stableSorting是稳定排序。 1234567const unstableSorting = (s1, s2) => { if (s1[0] <= s2[0]) return -1; return 1;};arr.sort(unstableSorting)// ["apple", "peach", "spork", "straw"] 上面代码中,排序结果是spork在straw前面,跟原始顺序相反,所以排序算法unstableSorting是不稳定的。 常见的排序算法之中,插入排序、合并排序、冒泡排序等都是稳定的,堆排序、快速排序等是不稳定的。不稳定排序的主要缺点是,多重排序时可能会产生问题。假设有一个姓和名的列表,要求按照“姓氏为主要关键字,名字为次要关键字”进行排序。开发者可能会先按名字排序,再按姓氏进行排序。如果排序算法是稳定的,这样就可以达到“先姓氏,后名字”的排序效果。如果是不稳定的,就不行。 早先的 ECMAScript 没有规定,Array.prototype.sort()的默认排序算法是否稳定,留给浏览器自己决定,这导致某些实现是不稳定的。ES2019 明确规定,Array.prototype.sort()的默认排序算法必须稳定。这个规定已经做到了,现在 JavaScript 各个主要实现的默认排序算法都是稳定的。","link":"/JavaScript/17.%E6%95%B0%E7%BB%84%E7%9A%84%E6%89%A9%E5%B1%95/"},{"title":"Symbol关键字","text":"1.概述ES5 的对象属性名都是字符串,这容易造成属性名的冲突。比如,你使用了一个他人提供的对象,但又想为这个对象添加新的方法(mixin 模式),新方法的名字就有可能与现有方法产生冲突。如果有一种机制,保证每个属性的名字都是独一无二的就好了,这样就从根本上防止属性名的冲突。这就是 ES6 引入Symbol的原因。 ES6 引入了一种新的原始数据类型Symbol,表示独一无二的值。它是 JavaScript 语言的第七种数据类型,前六种是:undefined、null、布尔值(Boolean)、字符串(String)、数值(Number)、对象(Object)。 Symbol 值通过Symbol函数生成。这就是说,对象的属性名现在可以有两种类型,一种是原来就有的字符串,另一种就是新增的 Symbol 类型。凡是属性名属于 Symbol 类型,就都是独一无二的,可以保证不会与其他属性名产生冲突。 123let s = Symbol();console.log(s); // 'symbol' 注意,Symbol函数前不能使用new命令,否则会报错。这是因为生成的 Symbol 是一个原始类型的值,不是对象。也就是说,由于 Symbol 值不是对象,所以不能添加属性。基本上,它是一种类似于字符串的数据类型。 Symbol函数可以接受一个字符串作为参数,表示对 Symbol 实例的描述,主要是为了在控制台显示,或者转为字符串时,比较容易区分。 12345678let s1 = Symbol('foo');let s2 = Symbol('bar');s1 // Symbol(foo)s2 // Symbol(bar)s1.toString() // "Symbol(foo)"s2.toString() // "Symbol(bar)" 上面代码中,变量s就是一个独一无二的值。typeof运算符的结果,表明变量s是 Symbol 数据类型,而不是字符串之类的其他类型。 不能使用new命令注意,Symbol函数前不能使用new命令,否则会报错。这是因为生成的 Symbol 是一个原始类型的值,不是对象。也就是说,由于 Symbol 值不是对象,所以不能添加属性。基本上,它是一种类似于字符串的数据类型。 1var sym = new Symbol(); // TypeError 这会阻止创建一个显式的 Symbol 包装器对象而不是一个 Symbol 值。围绕原始数据类型创建一个显式包装器对象从 ECMAScript 6 开始不再被支持。 然而,现有的原始包装器对象,如 new Boolean、new String以及new Number,因为遗留原因仍可被创建。 也就是说,Symbol无法被new命令创建是语法故意这么做的,从ES6开始不再支持围绕原始数据类型创建一个显示包装器对象 如果你真的想创建一个 Symbol 包装器对象 (Symbol wrapper object),你可以使用 Object() 函数 1234var sym = Symbol("foo");typeof sym; // "symbol"var symObj = Object(sym);typeof symObj; // "object" Symbol函数可以接受一个字符串作为参数,表示对 Symbol 实例的描述,主要是为了在控制台显示,或者转为字符串时,比较容易区分。 12345678let s1 = Symbol('foo');let s2 = Symbol('bar');s1 // Symbol(foo)s2 // Symbol(bar)s1.toString() // "Symbol(foo)"s2.toString() // "Symbol(bar)" 上面代码中,s1和s2是两个 Symbol 值。如果不加参数,它们在控制台的输出都是Symbol(),不利于区分。有了参数以后,就等于为它们加上了描述,输出的时候就能够分清,到底是哪一个值。 如果 Symbol 的参数是一个对象,就会调用该对象的toString方法,将其转为字符串,然后才生成一个 Symbol 值。 1234567const obj = { toString() { return 'abc'; }};const sym = Symbol(obj);sym // Symbol(abc) 注意,Symbol函数的参数只是表示对当前 Symbol 值的描述,因此相同参数的Symbol函数的返回值是不相等的。 1234567891011// 没有参数的情况let s1 = Symbol();let s2 = Symbol();s1 === s2 // false// 有参数的情况let s1 = Symbol('foo');let s2 = Symbol('foo');s1 === s2 // false 上面代码中,s1和s2都是Symbol函数的返回值,而且参数相同,但是它们是不相等的。 Symbol 值不能与其他类型的值进行运算,会报错。 123456let sym = Symbol('My symbol');"your symbol is " + sym// TypeError: can't convert symbol to string`your symbol is ${sym}`// TypeError: can't convert symbol to string 但是,Symbol 值可以显式转为字符串。 1234let sym = Symbol('My symbol');String(sym) // 'Symbol(My symbol)'sym.toString() // 'Symbol(My symbol)' 另外,Symbol 值也可以转为布尔值,但是不能转为数值。 12345678910let sym = Symbol();Boolean(sym) // true!sym // falseif (sym) { // ...}Number(sym) // TypeErrorsym + 2 // TypeError 2.Symbol.prototype.description创建 Symbol 的时候,可以添加一个描述。 1const sym = Symbol('foo'); 上面代码中,sym的描述就是字符串foo。 但是,读取这个描述需要将 Symbol 显式转为字符串,即下面的写法。 1234const sym = Symbol('foo');String(sym) // "Symbol(foo)"sym.toString() // "Symbol(foo)" 上面的用法不是很方便。ES2019提供了一个实例属性description,直接返回 Symbol 的描述。 123const sym = Symbol('foo');sym.description // "foo" 3.作为属性名由于每一个Symbol值都不相等,这意味着Symbol值可以作为标识符,用于对象的属性名,就能保证不会出现同名的属性,这对于一个对象由多个模块构成的情况非常有用,能防止某一个键不小心改写或覆盖 1234567891011121314151617let mySymbol = Symbol();// 第一种写法let a = {};a[mySymbol] = 'Hello!';// 第二种写法let a = { [mySymbol]: 'Hello!'};// 第三种写法let a = {};Object.defineProperty(a, mySymbol, { value: 'Hello!' });// 以上写法都得到同样结果a[mySymbol] // "Hello!" 注意:Symbol值作为对象属性名时,不能使用.运算符 123456const mySymbol = Symbol();const a = {};a.mySymbol = 'Hello!';a[mySymbol] // undefineda['mySymbol'] // "Hello!" Symbol 类型还可以用于定义一组常量,保证这组常量的值都是不相等的。 123456789const log = {};log.levels = { DEBUG: Symbol('debug'), INFO: Symbol('info'), WARN: Symbol('warn')};console.log(log.levels.DEBUG, 'debug message');console.log(log.levels.INFO, 'info message'); 下面是另外一个例子。 12345678910111213const COLOR_RED = Symbol();const COLOR_GREEN = Symbol();function getComplement(color) { switch (color) { case COLOR_RED: return COLOR_GREEN; case COLOR_GREEN: return COLOR_RED; default: throw new Error('Undefined color'); }} 常量使用 Symbol 值最大的好处,就是其他任何值都不可能有相同的值了,因此可以保证上面的switch语句会按设计的方式工作。 还有一点需要注意,Symbol 值作为属性名时,该属性还是公开属性,不是私有属性。 4.属性名的遍历Symbol 作为属性名,遍历对象的时候,该属性不会出现在for...in、for...of循环中,也不会被Object.keys()、Object.getOwnPropertyNames()、JSON.stringify()返回。 但是,它也不是私有属性,有一个Object.getOwnPropertySymbols()方法,可以获取指定对象的所有 Symbol 属性名。该方法返回一个数组,成员是当前对象的所有用作属性名的 Symbol 值。 1234567891011const obj = {};let a = Symbol('a');let b = Symbol('b');obj[a] = 'Hello';obj[b] = 'World';const objectSymbols = Object.getOwnPropertySymbols(obj);objectSymbols// [Symbol(a), Symbol(b)] 上面代码是Object.getOwnPropertySymbols()方法的示例,可以获取所有 Symbol 属性名。 下面是另一个例子,Object.getOwnPropertySymbols()方法与for...in循环、Object.getOwnPropertyNames方法进行对比的例子。 1234567891011const obj = {};const foo = Symbol('foo');obj[foo] = 'bar';for (let i in obj) { console.log(i); // 无输出}Object.getOwnPropertyNames(obj) // []Object.getOwnPropertySymbols(obj) // [Symbol(foo)] 上面代码中,使用for...in循环和Object.getOwnPropertyNames()方法都得不到 Symbol 键名,需要使用Object.getOwnPropertySymbols()方法。 另一个新的 API,Reflect.ownKeys()方法可以返回所有类型的键名,包括常规键名和 Symbol 键名。 12345678let obj = { [Symbol('my_key')]: 1, enum: 2, nonEnum: 3};Reflect.ownKeys(obj)// ["enum", "nonEnum", Symbol(my_key)] 5.Symbol.for()和Symbol.keyFor()Symbol.for()方法接受同一个字符串作为参数,然后搜索有没有以该参数作为名称的Symbol值,然后搜索有没有以该参数作为名称的Symbol值,如果有,就返回这个Symbol值,否则就新建一个以该字符串为名称的Symbol值,并将其注册到全局 1234let s1 = Symbol.for('foo');let s2 = Symbol.for('foo');s1 === s2 // true Symbol.keyFor()方法返回一个已登记的 Symbol 类型值的key。 12345let s1 = Symbol.for("foo");Symbol.keyFor(s1) // "foo"let s2 = Symbol("foo");Symbol.keyFor(s2) // undefined 上面代码中,变量s2属于未登记的 Symbol 值,所以返回undefined。 注意,Symbol.for()为 Symbol 值登记的名字,是全局环境的,不管有没有在全局环境运行。 6.理解Symbol这个数据类型引入的原本目的是防止对象的属性名命名冲突。因为对象的属性名只能是字符串,所以有可能命名冲突 Symbol的值由Symbol函数进行生成,对象的属性原本只能是字符串,现在可以是Symbol类型,表示独一无二 123456789let name = Symbol();console.log(name);let obj = { [name]: 'jiacheng'} 创建Symbol变量时可以传入一个描述,如果要读取这个描述,可以使用ES2019新增的实例方法Symbol.prototype.description 12345let foo = Symbol('foo');console.log(foo.description);console.log(foo.toString()); 如果一个对象中有Symbol类型的属性名,那么它在遍历时不会出现在for...in Object.key Object.getOwnPropertyName()JSON.stringify()中 可以通过Object.getOwnPropertySymbols()获取对象中所有的Symbol对象或者使用Reflect.ownKeys()","link":"/JavaScript/20.Symbol/"},{"title":"ES6运算符的扩展","text":"1.指数运算符ES2016新增的指数运算符** 1234562** 2 // 42 ** 3 // 8// 相当于Math.pow(2,2)Math.pow(2,3) 指数运算符的特点是右结合 1232 ** 3 ** 2 // 512//相当于 2 ** (3 ** 2) 指数运算符可以和等号结合 123let a = 3;a **= 3; // 相当于 a = a ** 3; 2.链式判断运算符当我们想要读取变量中的某个属性并进行一定操作时,往往需要判断一下对象中的属性是否存在 12345678// 错误的写法const firstName = message.body.user.firstName || 'default';// 正确的写法(需要判断四次)const firstName = (message && message.body && message.body.user && message.body.user.firstName) || 'default'; 或者使用三元运算符 12const fooInput = myForm.querySelector('input[name=foo]')const fooValue = fooInput ? fooInput.value : undefined ES2020引入了链判断运算符,来简化这些判断?. 12const firstName = message?.body?.user?.firstName || 'default';const fooValue = myForm.querySelector('input[name=foo]')?.value 使用链判断运算符的语句,直接在调用时会判断左侧的对象是否为null或undefined,如果是,则停止往下执行并返回undefined 常见写法123456789101112131415a?.b// 等同于a == null ? undefined : a.ba?.[x]// 等同于a == null ? undefined : a[x]a?.b()// 等同于a == null ? undefined : a.b()a?.()// 等同于a == null ? undefined : a() 本质上,?.运算符相当于一种短路机制,只要不满足条件,就不再往下执行 如果属性链中带有括号,那么链判断运算符对括号外部没有影响 123(a?.b).c; // 等价于 (a == null ? undefined : a.b).c 禁止使用的场景123456789101112// 构造函数new a?.()new a?.b()// 右侧是模板字符串a?.`${b}`;// 左侧是supersuper?.foo()// 在赋值的左侧a?.b = c; 3.空值合并运算符读取对象属性时,如果这个属性是null或者undefined,有时候需要为它们指定默认值,常见的做法是通过|| 1const a?.b || 'a'; 但||使用运算符会导致只要左侧转换后的布尔值是false,就会执行右侧 而在js中,false "" NaN 0 null undefined等都会被判断为false 为了了避免这种情况,ES2020引入了一个新的null判断运算符??,只有在左侧是null或者undefined时才会返回右侧的值 1const a?.b ?? 'a'; 优先级??本质上是逻辑运算符,与||和&&一起使用时存在优先级问题,必须使用括号来表示哪个优先级更高 1234567891011(lhs && middle) ?? rhs;lhs && (middle ?? rhs);(lhs ?? middle) && rhs;lhs ?? (middle && rhs);(lhs || middle) ?? rhs;lhs || (middle ?? rhs);(lhs ?? middle) || rhs;lhs ?? (middle || rhs); 4.逻辑赋值运算符ES2021引入了三个逻辑赋值运算符 12345x ||= y // 相当于 x || (x = y)x ??= y // 相当于 x ?? (x = y)x &&= y // 相当于 x && (x = y) 它们的一个用途是,为变量或属性设置默认值。 12345// 老的写法user.id = user.id || 1;// 新的写法user.id ||= 1; 上面示例中,user.id属性如果不存在,则设为1,新的写法比老的写法更紧凑一些。 下面是另一个例子。 1234function example(opts) { opts.foo = opts.foo ?? 'bar'; opts.baz ?? (opts.baz = 'qux');} 上面示例中,参数对象opts如果不存在属性foo和属性baz,则为这两个属性设置默认值。有了“Null 赋值运算符”以后,就可以统一写成下面这样。 1234function example(opts) { opts.foo ??= 'bar'; opts.baz ??= 'qux';}","link":"/JavaScript/2.%E8%BF%90%E7%AE%97%E7%AC%A6%E7%9A%84%E6%89%A9%E5%B1%95/"},{"title":"JavaScript类机制","text":"1.ES5 继承ES5中没有类的概念,通常通过声明一个构造函数来模拟类 1234567891011121314151617181920function Person(name) { this.name = name; this.sayName1 = function () { console.log(this.name + '在工作'); }}// 原型链上的方法和属性会被多个实例共享,构造函数中的则不会Person.prototype.sayName2 = function () { console.log(this.name + '在学习');}// 静态方法Person.sayName3 = () => { console.log(this.name + '在运动')}var person = new Person('Tom');person.sayName1(); // Tom在工作person.sayName2(); // Tom在学习 原型方法和实例方法的区别: 写在原型中的方法可以被所有的实例共享, 实例化的时候不会在实例内存中再复制一份,占有的内存消耗少。 js中每个函数都有一个prototype属性,这个属性指向一个对象(所有属性的集合:默认constructor属性,值指向这个函数本身。) 每个原型对象都属于对象,所以它也有自己的原型,而它自己的原型对象又有自己的原型,所以就形成了原型链。 一个对象的隐式原型指向构造这个对象的构造函数的显式原型,所以这个对象可以访问构造函数的属性和方法。(new一个实例) js的继承也就是通过原型链来实现的,当访问一个对象的属性,如果这个对象本身不存在,则沿着__proto__依次往上查找,如果有则返回值,没有则一直到查到Object.prototype的__proto__的值为null. 继承ES5实现继承的方式有原型链继承,构造继承,实例继承,拷贝继承,组合继承,寄生组合继承这六种 原型链继承 优点 实例是子类的实例,也是父类的实例 可以调用父类的实例属性和方法,也可以调用父类原型链上的属性和方法 缺点 子类无法在构造器中新增属性或者方法,必须要在new Person()之后 在子类实例化时,无法向父类传参 父类原型对象的所有属性被所有实例共享 1234567// 原型链继承function Cat() { }Cat.prototype = new Person('Pt'); // 只能在这里向父类传参,或者下面代码那样var cat = new Cat();cat.sayName1(); 构造继承 优点 解决了原型链继承中,子类实例共享父类引用属性的问题 在创建子类实例时,可以向父类传参 缺点 实例并不是父类的实例,只是子类的实例 只能继承父类构造函数中的属性和方法,不能继承父类原型链中的属性和方法 无法实现函数的复用,每个子类都有父类实例函数的副本,影响性能 1234567function Cat(name) { Person.call(this, name);}var cat = new Cat('Tom');cat.sayName1(); 实例继承 缺点 无法实现多继承 子类实例化出来的对象是父类类型,不是子类类型 123456789// 实例继承function Cat(name) { var instance = new Person(name); return instance;}var cat = new Cat('Tom');cat.sayName1() 组合继承 优点 弥补了构造继承的缺陷,可以继承实例的属性/方法,也可以继承原型上的属性/方法 既是子类的实例,也是父类的实例 可以向父类传参 函数可以复用 缺点 调用了两次父类的构造函数,生成了两份实例 1234567891011121314151617function Cat(name, age) { Person.call(this, name); this.age = age;}Cat.prototype = new Person();var cat = new Cat('Tom', 18);console.log(cat.name);cat.sayName1();cat.sayName2();console.log(cat.age); 寄生组合继承1234567891011function Cat(name) { Person.call(this, name);}var Temp = Object.create(Person.prototype); // 创建对象,创建父类原型的一个副本Temp.constructor = Cat; // 增强对象,弥补因重写原型而失去的默认的constructor 属性Cat.prototype = Temp; // 指定对象,将新创建的对象赋值给子类的原型var cat = new Cat('Tom');cat.sayName1() 2.new的时候都做了什么? 创建一个新对象 把这个新对象的__proto__属性指向你要new 的那个对象的prototype 让构造函数里面的this指向新的对象,然后执行构造函数 返回这个新对象 123456function _new(constructor, ...args) { const obj = Object.create(constructor.prototype); const result = constructor.call(obj, ...args); return result instanceof Object ? result : obj;} 3.ES6 ClassES6提供了更接近传统的写法,基本上可以看作只是一个语法糖,它的绝大部分功能,ES5都可以做到 12345678910class Person { constructor(name) { this.name = name; } sayName() { console.log(this.name + '在学习') }}const p = new Person('jiacheng') 构造函数上的prototype属性,在ES6类上面继续存在,事实上,类的所有方法都是定义在类的prototype属性上面的 1234567891011121314151617181920class Point { constructor() { // ... } toString() { // ... } toValue() { // ... }}// 等同于Point.prototype = { constructor() {}, toString() {}, toValue() {},}; 因此,在类的实例上面调用方法,其实就是调用原型上的方法 12345class B{}const b = new B();b.constructor === B.prototype.constructor // true 所以,使用Object.assin()方法可以很方便地一次向类添加多个方法 12345678910class Point { constructor(){ // ... }}Object.assign(Point.prototype, { toString(){}, toValue(){}}); prototype对象的constructor属性,直接指向类本身,这与ES5的行为是一致的 1Point.prototype.constructor === Point // true 另外,类内部所有定义的方法,都是不可枚举的 1234567891011121314class Point { constructor(x, y) { // ... } toString() { // ... }}Object.keys(Point.prototype)// []Object.getOwnPropertyNames(Point.prototype)// ["constructor","toString"] constructorconstructor方法是类的默认方法,通过new命令生成对象实例时,自动调用该方法,一个类即使没有显示定义constructor,也会被默认添加一个空的constructor 123456class Person{}// 相当于class Person { constructor(){}} constructor默认返回实例对象,完全可以指定返回另一个对象 1234567class Person { constructor() { return Object.create(null) }}new Person instanceof Person // false 类必须使用new调用,否则会直接报错 12345class Person {}Person()// TypeError: Class constructor Foo cannot be invoked without 'new' 类的实例直接通过new命令生成一个类的实例,与ES5不同的是,直接调用类会报错 123456789class Point { // ...}// 报错var point = Point(2, 3);// 正确var point = new Point(2, 3); 与 ES5 一样,实例的属性除非显式定义在其本身(即定义在this对象上),否则都是定义在原型上(即定义在class上) 12345678910111213141516171819202122//定义类class Point { constructor(x, y) { this.x = x; this.y = y; } toString() { return '(' + this.x + ', ' + this.y + ')'; }}var point = new Point(2, 3);point.toString() // (2, 3)point.hasOwnProperty('x') // truepoint.hasOwnProperty('y') // truepoint.hasOwnProperty('toString') // falsepoint.__proto__.hasOwnProperty('toString') // true 类的所有实例共享一个原型对象 12345var p1 = new Point(2,3);var p2 = new Point(3,2);//truep1.__proto__ === p2.__proto__ 这也意味着,可以通过实例的__proto__属性为“类”添加方法。 __proto__ 并不是语言本身的特性,这是各大厂商具体实现时添加的私有属性,虽然目前很多现代浏览器的 JS 引擎中都提供了这个私有属性,但依旧不建议在生产中使用该属性,避免对环境产生依赖。生产环境中,我们可以使用 Object.getPrototypeOf 方法来获取实例对象的原型,然后再来为原型添加方法/属性。 12345678910var p1 = new Point(2,3);var p2 = new Point(3,2);p1.__proto__.printName = function () { return 'Oops' };p1.printName() // "Oops"p2.printName() // "Oops"var p3 = new Point(4,2);p3.printName() // "Oops" 上面代码在p1的原型上添加了一个printName()方法,由于p1的原型就是p2的原型,因此p2也可以调用这个方法。而且,此后新建的实例p3也可以调用这个方法。这意味着,使用实例的__proto__属性改写原型,必须相当谨慎,不推荐使用,因为这会改变“类”的原始定义,影响到所有实例。 getter和setter与 ES5 一样,在“类”的内部可以使用get和set关键字,对某个属性设置存值函数和取值函数,拦截该属性的存取行为。 12345678910111213141516171819class MyClass { constructor() { // ... } get prop() { return 'getter'; } set prop(value) { console.log('setter: '+value); }}let inst = new MyClass();inst.prop = 123;// setter: 123inst.prop// 'getter' 存值函数和取值函数是设置在属性的 Descriptor 对象上的。 1234567891011121314151617181920class CustomHTMLElement { constructor(element) { this.element = element; } get html() { return this.element.innerHTML; } set html(value) { this.element.innerHTML = value; }}var descriptor = Object.getOwnPropertyDescriptor( CustomHTMLElement.prototype, "html");"get" in descriptor // true"set" in descriptor // true 注意点 严格模式 类和模块的内部,默认就是严格模式,不需要使用’use strict’指定运行模式 不存在提升 类不存在变量提升 Generator方法 如果某个方法前加上*,就表示该方法是一个Generator函数 12345678910111213141516class Foo { constructor(...args) { this.args = args; } * [Symbol.iterator]() { for (let arg of this.args) { yield arg; } }}for (let x of new Foo('hello', 'world')) { console.log(x);}// hello// world 上面代码中,Foo类的Symbol.iterator方法前有一个星号,表示该方法是一个 Generator 函数。Symbol.iterator方法返回一个Foo类的默认遍历器,for...of循环会自动调用这个遍历器。 this的指向 类方法内部如果含有this,它默认指向类的实例,但该方法无法单独使用 12345678910111213class Logger { printName(name = 'there') { this.print(`Hello ${name}`); } print(text) { console.log(text); }}const logger = new Logger();const { printName } = logger;printName(); // TypeError: Cannot read property 'print' of undefined 上面代码中,因为printName方法中的this,默认指向Logger类的实例,但如果将这个方法单独提取出来使用,this会指向该方法运行时所在的环境,而由于class内部是严格模式,所以这时候this直接指向undefined,导致报错 解决方法: 在构造方法中绑定this,这样就不会找不到print方法了。 12345678910111213class Logger { constructor() { this.printName = this.printName.bind(this); } printName(name = 'there') { this.print(`Hello ${name}`); } print(text) { console.log(text); }} 使用箭头函数 箭头函数内部的this总是指向定义时所在的对象。上面代码中,箭头函数位于构造函数内部,它的定义生效的时候,是在构造函数执行的时候。这时,箭头函数所在的运行环境,肯定是实例对象,所以this会总是指向实例对象 123456789101112131415class Person { printName = () => { this.print() } print() { console.log('111'); }}var p = new Person();var { printName } = p;printName() 静态方法类相当于实例的原型,所有在类中定义的方法,都会被实例继承,如果在一个方法前面加上static关键字,就表示该方法不会被实例继承,而是通过类来调用,这被称为静态方法 1234567891011class Foo { static classMethod() { return 'hello'; }}Foo.classMethod() // 'hello'var foo = new Foo();foo.classMethod()// TypeError: foo.classMethod is not a function 如果静态方法中包含this关键字,这个this指向的是类,而不是实例 12345678910111213class Foo { static bar() { this.baz(); } static baz() { console.log('hello'); } baz() { console.log('world'); }}Foo.bar() // hello 父类的静态方法,可以被子类继承 静态方法也可以从super对象上调用 12345678910111213class Foo { static classMethod() { return 'hello'; }}class Bar extends Foo { static classMethod() { return super.classMethod() + ', too'; }}Bar.classMethod() // "hello, too" 静态属性静态属性是指class本身的属性,而不是定义在实例对象上的属性 123456789101112class Foo {}Foo.prop = 1;// 或者class Foo{ static name = 'jiacheng'}Foo.name; // jiacheng 4.继承class通过extends关键字来实现继承 123class Person{}class ColorPoint extends Person {} super super关键字表示父类的构造函数,用来新建父类的this对象 子类必须在constructor中调用super方法,否则会报错 12345678910class ColorPoint extends Point { constructor(x, y, color) { super(x, y); // 调用父类的constructor(x, y) this.color = color; } toString() { return this.color + ' ' + super.toString(); // 调用父类的toString() }} 5.和ES5继承的区别 ES5里的构造函数是一个普通函数,可以使用new调用,也可以直接调用,且存在变量提升。ES6的class必须使用new操作符调用,且不存在变量提升 ES5子类的原型是指向Function.prototype,而ES6子类的原型是指向父类的 ES5的原型方法和静态方法是可枚举的,而class的默认不可枚举,但可以使用Object.getOwnPropertyNames方法获取 ES5的继承,实质是先创造一个子类实例对象的this,然后再执行父类构造函数给它添加实例方法和属性(不执行也无所谓);ES6的继承机制则相反,先将父类的属性和方法,加到一个空对象上,然后再将该对象作为子类的实例,即”继承在前,实例在后”","link":"/JavaScript/6.%E7%B1%BB/"},{"title":"解构赋值","text":"解构赋值ES6允许按照一定的模式,从数组和对象中提取值,并赋值给对应的对象,这被称为解构赋值 解构赋值本质上属于模式匹配,也就是说,只要等式两边的模式相同,左边的变量就会被赋予对应的值 如果解构不成功,变量的值就会被赋予undefined 解构赋值分为数组形式的解构赋值和对象形式的解构赋值,两者实现的原理不同 1.数组形式的解构赋值数组形式的解构赋值本质上是调用可迭代数据结构原型链上的iterator接口,遍历出对应的数据并进行赋值 1234567891011121314151617181920let [foo, [[bar], baz]] = [1, [[2], 3]];foo // 1bar // 2baz // 3let [ , , third] = ["foo", "bar", "baz"];third // "baz"let [x, , y] = [1, 2, 3];x // 1y // 3let [head, ...tail] = [1, 2, 3, 4];head // 1tail // [2, 3, 4]let [x, y, ...z] = ['a'];x // "a"y // undefinedz // [] 如果右边不是数组(或者严格来说,不是可遍历的结构),那么将会报错 对于Set结构,也可以使用数组进行解构赋值 1let [x,y,z] = new Set([1,2,3]) 事实上,只要某种数据结构具有Iterator接口,都可以采用数组的形式解构赋值 1234567891011function* fibs() { let a = 0; let b = 1; while (true) { yield a; [a, b] = [b, a + b]; }}let [first, second, third, fourth, fifth, sixth] = fibs();sixth // 5 默认值 解构赋值允许指定默认值 12345let [foo = true] = [];foo // truelet [x, y = 'b'] = ['a']; // x='a', y='b'let [x, y = 'b'] = ['a', undefined]; // x='a', y='b' 注意:ES6内部默认使用严格相等运算符判断一个位置是否有值,所以,只有当要解构的数组成员默认等于undefined时,默认值才会生效 12345let [x = 1] = [undefined];x // 1let [x = 1] = [null]x // null 如果默认值是一个表达式,那么这个表达式是惰性求值的,即只有在用到的时候,才会求值。 12345function f() { console.log('aaa');}let [x = f()] = [1]; 默认值可以引用解构赋值的其他变量,但该变量必须已经声明。 1234let [x = 1, y = x] = []; // x=1; y=1let [x = 1, y = x] = [2]; // x=2; y=2let [x = 1, y = x] = [1, 2]; // x=1; y=2let [x = y, y = 1] = []; // ReferenceError: y is not defined 字符串的解构赋值字符串也可以解构赋值。这是因为字符串的包装类也是一个带有iterator的可迭代对象 123456const [a, b, c, d, e] = 'hello';a // "h"b // "e"c // "l"d // "l"e // "o" 函数参数的解构赋值函数参数也可以使用解构赋值 12345function add([x, y]){ return x + y;}add([1, 2]); // 3 2.对象的解构赋值对象的解构赋值和数组有所不同,数组的解构赋值变量名是按照数组的元素顺序一一赋值的,而对象解构赋值内部的运行机制其实就是直接查找同名的属性进行赋值,只要属性名正确,就能取到值,否则为undefined 123456let { bar, foo } = { foo: 'aaa', bar: 'bbb' };foo // "aaa"bar // "bbb"let { baz } = { foo: 'aaa', bar: 'bbb' };baz // undefined 对象的解构赋值极大地方便了我们将现有对象的某个方法,单独取出来使用,并且支持重命名 1234567let { foo: baz } = { foo: 'aaa', bar: 'bbb' };baz // "aaa"let obj = { first: 'hello', last: 'world' };let { first: f, last: l } = obj;f // 'hello'l // 'world' 默认值 对象的解构赋值也可以指定默认值 123456789101112131415var {x = 3} = {};x // 3var {x, y = 5} = {x: 1};x // 1y // 5var {x: y = 3} = {};y // 3var {x: y = 3} = {x: 5};y // 5var { message: msg = 'Something went wrong' } = {};msg // "Something went wrong" 默认值生成的条件与数组相同,对象的属性必须严格等于undefined 12345var {x = 3} = {x: undefined};x // 3var {x = 3} = {x: null};x // null 3.解构中的装箱一些基本数据类型也可以进行解构取值。准确地来说,是因为在解构中发生了装箱操作,可以取到其对应包装类型上的方法 例如: 取字符串所对应String类中的length12let { length } = 'hello';length // 5 数值和布尔值的解构赋值同样可以取到对应包装类型上的方法 12345let {toString: s} = 123;s === Number.prototype.toString // truelet {toString: s} = true;s === Boolean.prototype.toString // true null和undefined没有对应的包装类型,无法转为对象,因此解构会直接报错 12let { prop: x } = undefined; // TypeErrorlet { prop: y } = null; // TypeError 4.用途变量的解构赋值用途很多。 (1)交换变量的值 1234let x = 1;let y = 2;[x, y] = [y, x]; 上面代码交换变量x和y的值,这样的写法不仅简洁,而且易读,语义非常清晰。 (2)从函数返回多个值 函数只能返回一个值,如果要返回多个值,只能将它们放在数组或对象里返回。有了解构赋值,取出这些值就非常方便。 12345678910111213141516// 返回一个数组function example() { return [1, 2, 3];}let [a, b, c] = example();// 返回一个对象function example() { return { foo: 1, bar: 2 };}let { foo, bar } = example(); (3)函数参数的定义 解构赋值可以方便地将一组参数与变量名对应起来。 1234567// 参数是一组有次序的值function f([x, y, z]) { ... }f([1, 2, 3]);// 参数是一组无次序的值function f({x, y, z}) { ... }f({z: 3, y: 2, x: 1}); (4)提取 JSON 数据 解构赋值对提取 JSON 对象中的数据,尤其有用。 12345678910let jsonData = { id: 42, status: "OK", data: [867, 5309]};let { id, status, data: number } = jsonData;console.log(id, status, number);// 42, "OK", [867, 5309] 上面代码可以快速提取 JSON 数据的值。 (5)函数参数的默认值 1234567891011jQuery.ajax = function (url, { async = true, beforeSend = function () {}, cache = true, complete = function () {}, crossDomain = false, global = true, // ... more config} = {}) { // ... do stuff}; 指定参数的默认值,就避免了在函数体内部再写var foo = config.foo || 'default foo';这样的语句。 (6)遍历 Map 结构 任何部署了 Iterator 接口的对象,都可以用for...of循环遍历。Map 结构原生支持 Iterator 接口,配合变量的解构赋值,获取键名和键值就非常方便。 123456789const map = new Map();map.set('first', 'hello');map.set('second', 'world');for (let [key, value] of map) { console.log(key + " is " + value);}// first is hello// second is world 如果只想获取键名,或者只想获取键值,可以写成下面这样。 123456789// 获取键名for (let [key] of map) { // ...}// 获取键值for (let [,value] of map) { // ...} (7)输入模块的指定方法 加载模块时,往往需要指定输入哪些方法。解构赋值使得输入语句非常清晰。 1const { SourceMapConsumer, SourceNode } = require("source-map");","link":"/JavaScript/8.%E5%8F%98%E9%87%8F%E7%9A%84%E8%A7%A3%E6%9E%84%E8%B5%8B%E5%80%BC/"},{"title":"前端组件化思考","text":"前言组件化对于任何一个业务场景复杂的前端应用以及经过多次迭代之后的产品来说都是必经之路。组件化要做的不仅仅是表面上看到的模块拆分解耦,其背后还有很多工作来支撑组件化的进行,例如结合业务特性的模块拆分策略、模块间的交互方式和构建系统等等 组件化并不是前端独有的,当今前端生态里面,React、Angular和Vue三分天下。虽然这三个框架的定位各有不同,但是它们有一个核心的共同点,那就是提供了组件化的能力。 前端实施组件化的目的 从发展的角度来看 随着技术的发展,开发的复杂度也越来越高,传统开发模式总是存在着开发效率低,维护成本高等弊端 传统开发方式效率低以及维护成本高的主要原因在于很多时候是将一个系统做成了整块应用,而且往往随着业务的增长或者变更,系统的复杂度会呈现指数级的增长,经常出现的情况就是一个小小的改动或者一个小功能的增加可能会引起整体逻辑的修改,造成牵一发而动全身 我们希望一个大且复杂的场景能够被分解成几个小的部分,这些小的部分彼此之间互不干扰,可以单独开发,单独维护,而且他们之间可以随意的进行组合 从效率的角度思考 迭代速度慢,公共代码相互耦合,需要全量回归 多人协作是极其困难的一件事 代码冲突多,每次提交代码可能需要解决冲突 版本风险高,修改会影响很多需求之外的功能 从技术的角度思考 代码整体结构混乱、缺少层次 优秀的代码应该是高内聚,低耦合 龟速编译,开发体验极差 无法很好地支持A/BTest 每次发版在QA回归上耗时很久 什么是组件化前端的组件化,其实是对项目进行自上而下的拆分,把通用的、可复用的功能以黑盒的形式封装到一个组件中,然后暴露一些开箱即用的函数和属性配置供外部组件调用,实现与业务逻辑的解耦,来达到代码间的高内聚、低耦合,实现功能模块的可配置、可复用、可扩展。 组件化的演变组件化经历了:函数化编程思想、模块化编程思想和组件化编程思想三个阶段 函数化编程思想: 以函数(方法)来分离行为 每一个函数仅在做一件事情 模块化编程思想: 以模块(js文件)来分离行为 每个模块负责一类事情 组件化编程思想: 以组件来分离行为 每个组件拥有独立的结构、视图和行为,代表一个完整的个体 组件的职能划分组件最大的不稳定性来自于展现层,一个组件只做一件事,基于功能做好职责划分: 基础组件 容器型组件 展示型组件 业务组件 通用组件 容器型组件一个容器性质的组件,一般当作一个业务子模块的入口,比如一个路由指向的组件;容器型组件需要知道如何获取子组件所需数据,以及这些数据的处理逻辑,并把数据和逻辑通过props提供给子组件使用。容器型组件一般是有状态组件,因为它们需要管理页面所需数据。 展示型组件主要表现为组件是怎样渲染的,就像一个简单的模版渲染; 只通过props接受数据和回调函数,不充当数据源; 通常用props.children(react) 或者slot(vue)来包含其他组件; 可以有状态,在其生命周期内可以操纵并改变其内部状态,职责单一,将不属于自己的行为通过回调传递出去,让父级去处理。 业务组件通常是根据最小业务状态抽象而出,业务组件虽然也具有一定的复用性,但大多数是一次性组件 通用组件可以在一个或多个项目内通用的组件 组件化在现代项目中的职责组件化开发方案主要关注的是在迭代开发阶段的对团队效率的提升。 1.单一职责 单一职责强调一个组件具备一项“能力”。 单一职责可以保证组件是最细的粒度,且有利于复用。但太细的粒度有时又会造成组件的碎片化。因此单一职责组件要建立在可复用的基础上,对于不可复用的单一职责组件,我们仅仅作为独立组件的内部组件即可。 单一职责同时也具备简化组件的能力,遵守该原则在一定程度上能够使代码足够简单,意味着易读、易维护。 2.封装 良好的组件封装应该隐藏内部细节和实现意义,并通过props来控制行为和输出。同时还要具备减少访问全局变量能力,因为访问全局变量会打破封装,创造了不可预测的行为。 封装能够将不用逻辑代码分离,能够帮助开发中快速定位问题。 3.可配置性 一个组件,要明确它的输入和输出分别是什么。 组件除了要展示默认的内容,还需要做一些动态的适配,比如:一个组件内有一段文本,一个图片和一个按钮;字体的颜色、图片的规则、按钮的位置、按钮点击事件的处理逻辑等,都是可以做成可配置的。 4.组合 单一责任原则描述了如何将需求拆分为组件,封装描述了如何组织这些组件,组合描述了如何将整个系统粘合在一起; 具有多个功能的组件,应该转换为多个单一职责的小组件,这些小的组件又可以组合成为一个职责更大、功能单一的组件。 5.复用 通常来说我们进行组件设计的目的有两种: 抽取公共功能部分,方便复用; 复杂设计/功能分解,便于代码管理和提高代码阅读性。 提高组件的复用性,使得一处代码的封装能够在各个不同的地方使用。复用性能够使代码的修改/编辑更加方便,只需要修改组件代码,各个引用的地方会同步进行修改和更新。 6.可测试 现在前端开发过程中一直都在强调单元测试,一个完成的项目单测是不可缺少的一部分,单测可以保证代码正确性、一部分依赖的正确性、以及减少调试时间等。 单元测试的目的不是为了减少代码覆盖率,而是为了减少bug出现的概率,以及防止bug回归。 总结组件化并非一蹴而就,而是一个持续的过程。在沉淀业务组件的同时还需考虑组件包的大小,不能因为组件包的体积大而导致页面加载过慢,以及组件发布前的测试等。但可以通过一些方法和规范去解决挑战,让组件化设计更好的服务于系统。所以,理解组件化可以帮助开发者更好地使用框架进行工作内容的拆分和维护,才能在实际开发中结合具体的业务场景,设计出合理的组件,实现真正的前端组件化。","link":"/%E5%89%8D%E7%AB%AF%E7%BB%84%E4%BB%B6%E5%8C%96%E6%80%9D%E8%80%83/"},{"title":"跨域","text":"1.什么是同源策略同源策略就是指协议,域名,端口号三者相同,浏览器就认为这个请求地址是同源的,有一个不同,则认为非同源,同源策略限制访问以下内容: Cookie LocationStore SessionStore等资源不共享 无法访问DOM节点 Ajax请求的结果会被浏览器拦截 特别说明: -如果是协议和端口造成的跨域问题,前端是无能为力的 -在跨域问题上,浏览器只通过url首部(协议+域名+端口号)来识别,而不会根据域名所对应的IP地址是否相同来判断 -跨域状态下Ajax请求是可以发出去的,只不过请求的响应被浏览器拦截了 2.跨域解决方案1.JSONP利用<script>标签没有跨域限制的漏洞,可以在请求地址中传入一个回调函数名,通过window监听这个回调函数,后端执行这个函数并把参数以形参的形式传入发给前端 12345678const script = document.createElement('script');script.src = 'http://127.0.0.1:3000/say?callback=show';window.show = function(data) { console.log(data)}document.body.appendChaild(script) Nodejs 12345678910const express = require('express');const app = express();app.get('/say', (req, res) => { const { callback } = req.query; res.send(`${callback}('222')`)})app.listen(3000) 缺点:仅支持get方法 2.CORSCORS需要浏览器和后端同时支持,IE8和IE9需要通过XDomainRequest来实现 浏览器会自动进行CORS通信,实现CORS通信的关键在后端,需要设置Access-Control-Allow-Origin就可以开启CORS,该属性表示哪些域名可以访问资源 使用CORS解决跨域问题,会在发送请求时出现两种情况,分别为简单请求和复杂请求 简单请求 同时满足两大条件: 1.请求方法为GET POST HEAD之一 2.Content-Type的值仅限三种:text/plain multipart/form-data application/x-www-form-urlcodeed 请求中的任意 XMLHttpRequestUpload 对象均没有注册任何事件监听器; XMLHttpRequestUpload 对象可以使用 XMLHttpRequest.upload 属性访问。 复杂请求 在发送复杂请求时,会增加一次HTTP查询请求,称为”预检”请求,该请求是使用OPTION方法,通过该请求来知道服务端是否允许跨域请求 后端配置 1234567891011121314151617181920212223const express = require('express')const app = express()const whitList - ['http://127.0.0.1:5500'] // 配置白名单app.all('*',function (req, res, next) { res.header('Access-Control-Allow-Origin','http://localhost:3001'); //当允许携带cookies此处的白名单不能写’*’ res.header('Access-Control-Allow-Headers','content-type,Content-Length, Authorization,Origin,Accept,X-Requested-With'); //允许的请求头 res.header('Access-Control-Allow-Methods', 'POST, GET, OPTIONS, PUT'); //允许的请求方法 res.header('Access-Control-Allow-Credentials',true); //允许携带cookies next();});app.put('/getData', function(req, res) { console.log(req.headers) res.setHeader('name', 'jw') //返回一个响应头,后台需设置 res.end('111')})app.get('/getData', function(req, res) { console.log(req.headers) res.end('222')})app.use(express.static(__dirname))app.listen(4000) 3.postMessageposeMessage用于解决以下方面的问题: -页面和其打开页面的新窗口的数据传递 -多窗口之间的消息传递 -页面与嵌套的iframe消息传递 -跨域数据传递 123456789101112<iframe src="http://127.0.0.1:4000/b.html" frameborder="0" id="frame" onload="load()"></iframe> //等它加载完触发一个事件<script> window.onload = function() { const frame = document.getElementById('frame'); frame.contentWindow.postMessage('111', 'http://127.0.0.1:4000/b.html') // 发送数据 window.onMessage = function(e) { // 监听并接收返回的数据 console.log(e.data) } }</script> 12345// b.htmlwindow.onmessage = function(e) { console.log(e.data) //111 e.source.postMessage('222', e.origin)} 4.webSocketWebsocket是HTML5的一个全双工双向通信协议,在建立连接之后,WebSocket的服务端与客户端都能主动向对方发送或接收数据 12345678910111213// socket.html<script> // 实例化socket const socket = new WebSocket('ws://127.0.0.1:3000'); // 发送请求 socket.onopen = function () { socket.send('111'); } // 监听回调 socket.onmessage = function (e) { console.log(e); }</script> 1234567891011// server.jsconst WebSocket = require('ws');//记得安装ws// 实例化webSocket,监听3000端口const wss = new WebSocket.Server({ port: 3000 });// 建立连接wss.on('connection', function (ws) { ws.on('message', function (data) { console.log(data); ws.send('222') });}) 5.Node中间件代理 接受客户端请求 将请求转发给服务器 拿到服务器响应的数据 将响应转发给客户端 123456789101112131415161718192021222324252627282930313233343536// server1.js 代理服务器(http://localhost:3000)const http = require('http')// 第一步:接受客户端请求const server = http.createServer((request, response) => { // 代理服务器,直接和浏览器直接交互,需要设置CORS的首部字段 response.writeHead(200, { 'Access-Control-Allow-Origin': '*', 'Access-Control-Allow-Methods': '*', 'Access-Control-Allow-Headers': 'Content-Type' }) // 第二步:将请求转发给服务器 http.request( { host: '127.0.0.1', port: 4000, url: '/', method: request.method, headers: request.headers }, serverResponse => { // 第三步:收到服务器的响应 var body = '' serverResponse.on('data', chunk => { body += chunk }) serverResponse.on('end', () => { console.log('The data is ' + body) // 第四步:将响应结果转发给浏览器 response.end(body) }) } ).end()})server.listen(3000, () => { console.log('The proxyServer is running at http://localhost:3000')}) 6.Nginx反向代理实现原理类似于Node中间件代理,通过Nginx配置一个代理服务器(域名与domain1相同),反向代理访问domain2接口,并且可以顺便修改cookie中的domain信息,方便当前域cookie写入,实现跨域登录 1234567891011121314// proxy服务器server { listen 81; server_name www.domain1.com; location / { proxy_pass http://www.domain2.com:8080; #反向代理 proxy_cookie_domain www.domain2.com www.domain1.com; #修改cookie里域名 index index.html index.htm; # 当用webpack-dev-server等中间件代理接口访问nignx时,此时无浏览器参与,故没有同源限制,下面的跨域配置可不启用 add_header Access-Control-Allow-Origin http://www.domain1.com; #当前端只跨域不带cookie时,可为* add_header Access-Control-Allow-Credentials true; }}","link":"/%E8%B7%A8%E5%9F%9F/"},{"title":"函数的扩展","text":"1.参数默认值ES6允许为函数的参数设置默认值,直接写在参数定义的后面 1234567function log(x, y = 'World') { console.log(x, y);}log('Hello') // Hello Worldlog('Hello', 'China') // Hello Chinalog('Hello', '') // Hello 这种写法比ES5简洁了很多,而且可以让阅读代码的人可以立刻意识到哪些参数是可以省略的,提高了代码的可维护性 注意:参数变量是默认声明的,所以不能在函数作用域中使用let或const再次声明 1234function foo(x = 5) { let x = 1; // error const x = 2; // error} 使用参数默认值时,函数不能有同名参数 12345678910// 不报错function foo(x, x, y) { // ...}// 报错function foo(x, x, y = 1) { // ...}// SyntaxError: Duplicate parameter name not allowed in this context 另外,一个容易忽略的地方是,参数默认值不是传值的,而是每次都重新计算默认值表达式的值。也就是说,参数默认值是惰性求值的。 123456789let x = 99;function foo(p = x + 1) { console.log(p);}foo() // 100x = 100;foo() // 101 上面代码中,参数p的默认值是x + 1。这时,每次调用函数foo,都会重新计算x + 1,而不是默认p等于 100。 与解构赋值结合使用参数默认值可以与解构赋值的默认值,结合起来使用。 12345678function foo({x, y = 5}) { console.log(x, y);}foo({}) // undefined 5foo({x: 1}) // 1 5foo({x: 1, y: 2}) // 1 2foo() // TypeError: Cannot read property 'x' of undefined 上面代码只使用了对象的解构赋值默认值,没有使用函数参数的默认值。只有当函数foo的参数是一个对象时,变量x和y才会通过解构赋值生成。如果函数foo调用时没提供参数,变量x和y就不会生成,从而报错。通过提供函数参数的默认值,就可以避免这种情况。 12345function foo({x, y = 5} = {}) { console.log(x, y);}foo() // undefined 5 上面代码指定,如果没有提供参数,函数foo的参数默认为一个空对象。 下面是另一个解构赋值默认值的例子。 123456789function fetch(url, { body = '', method = 'GET', headers = {} }) { console.log(method);}fetch('http://example.com', {})// "GET"fetch('http://example.com')// 报错 上面代码中,如果函数fetch的第二个参数是一个对象,就可以为它的三个属性设置默认值。这种写法不能省略第二个参数,如果结合函数参数的默认值,就可以省略第二个参数。这时,就出现了双重默认值。 123456function fetch(url, { body = '', method = 'GET', headers = {} } = {}) { console.log(method);}fetch('http://example.com')// "GET" 上面代码中,函数fetch没有第二个参数时,函数参数的默认值就会生效,然后才是解构赋值的默认值生效,变量method才会取到默认值GET。 参数默认值的位置通常情况下,定义了默认值的参数,应该是函数的尾参数。因为这样比较容易看出来,到底省略了哪些参数。如果非尾部的参数设置默认值,实际上这个参数是没法省略的。 12345678910111213141516171819// 例一function f(x = 1, y) { return [x, y];}f() // [1, undefined]f(2) // [2, undefined]f(, 1) // 报错f(undefined, 1) // [1, 1]// 例二function f(x, y = 5, z) { return [x, y, z];}f() // [undefined, 5, undefined]f(1) // [1, 5, undefined]f(1, ,2) // 报错f(1, undefined, 2) // [1, 5, 2] 上面代码中,有默认值的参数都不是尾参数。这时,无法只省略该参数,而不省略它后面的参数,除非显式输入undefined。 如果传入undefined,将触发该参数等于默认值,null则没有这个效果。 123456function foo(x = 5, y = 6) { console.log(x, y);}foo(undefined, null)// 5 null 上面代码中,x参数对应undefined,结果触发了默认值,y参数等于null,就没有触发默认值。 2.作用域一旦设置了参数的默认值,函数进行声明初始化时,参数会形成一个单独的作用域(context)。等到初始化结束,这个作用域就会消失。这种语法行为,在不设置参数默认值时,是不会出现的。 1234567var x = 1;function f(x, y = x) { console.log(y);}f(2) // 2 上面代码中,参数y的默认值等于变量x。调用函数f时,参数形成一个单独的作用域。在这个作用域里面,默认值变量x指向第一个参数x,而不是全局变量x,所以输出是2。 再看下面的例子。 12345678let x = 1;function f(y = x) { let x = 2; console.log(y);}f() // 1 上面代码中,函数f调用时,参数y = x形成一个单独的作用域。这个作用域里面,变量x本身没有定义,所以指向外层的全局变量x。函数调用时,函数体内部的局部变量x影响不到默认值变量x。 如果此时,全局变量x不存在,就会报错。 123456function f(y = x) { let x = 2; console.log(y);}f() // ReferenceError: x is not defined 下面这样写,也会报错。 1234567var x = 1;function foo(x = x) { // ...}foo() // ReferenceError: Cannot access 'x' before initialization 上面代码中,参数x = x形成一个单独作用域。实际执行的是let x = x,由于暂时性死区的原因,这行代码会报错。 3.rest参数ES6 引入 rest 参数(形式为...变量名),用于获取函数的多余参数,这样就不需要使用arguments对象了。rest 参数搭配的变量是一个数组,该变量将多余的参数放入数组中。 1234567function fn(x,...args){ for(const item of args) { console.log(item) }}fn(1,2,3,4) // 2, 3, 4 arguments对象不是数组,而是一个类似数组的对象。所以为了使用数组的方法,必须使用Array.from先将其转为数组。rest 参数就不存在这个问题,它就是一个真正的数组,数组特有的方法都可以使用。 注意:如果指定了rest参数,那么它后面就不能跟其他的参数,否则会报错 4.严格模式从 ES5 开始,函数内部可以设定为严格模式。 1234function doSomething(a, b) { 'use strict'; // code} ES2016 做了一点修改,规定只要函数参数使用了默认值、解构赋值、或者扩展运算符,那么函数内部就不能显式设定为严格模式,否则会报错。 12345678910111213141516171819202122232425// 报错function doSomething(a, b = a) { 'use strict'; // code}// 报错const doSomething = function ({a, b}) { 'use strict'; // code};// 报错const doSomething = (...a) => { 'use strict'; // code};const obj = { // 报错 doSomething({a, b}) { 'use strict'; // code }}; 这样规定的原因是,函数内部的严格模式,同时适用于函数体和函数参数。但是,函数执行的时候,先执行函数参数,然后再执行函数体。这样就有一个不合理的地方,只有从函数体之中,才能知道参数是否应该以严格模式执行,但是参数却应该先于函数体执行。 12345// 报错function doSomething(value = 070) { 'use strict'; return value;} 5.name属性函数的name属性,返回该函数的函数名 函数的name属性,返回该函数的函数名。 12function foo() {}foo.name // "foo" 这个属性早就被浏览器广泛支持,但是直到 ES6,才将其写入了标准。 需要注意的是,ES6 对这个属性的行为做出了一些修改。如果将一个匿名函数赋值给一个变量,ES5 的name属性,会返回空字符串,而 ES6 的name属性会返回实际的函数名。 1234567var f = function () {};// ES5f.name // ""// ES6f.name // "f" 如果将一个具名函数赋值给一个变量,则 ES5 和 ES6 的name属性都返回这个具名函数原本的名字。 1234567const bar = function baz() {};// ES5bar.name // "baz"// ES6bar.name // "baz" Function构造函数返回的函数实例,name属性的值为anonymous。 1(new Function).name // "anonymous" bind返回的函数,name属性值会加上bound前缀。 1234function foo() {};foo.bind({}).name // "bound foo"(function(){}).bind({}).name // "bound " 6.箭头函数ES6 允许使用“箭头”(=>)定义函数。 123456var f = v => v;// 等同于var f = function (v) { return v;}; 如果箭头函数不需要参数或需要多个参数,就使用一个圆括号代表参数部分。 123456789var f = () => 5;// 等同于var f = function () { return 5 };var sum = (num1, num2) => num1 + num2;// 等同于var sum = function(num1, num2) { return num1 + num2;}; 如果箭头函数的代码块部分多于一条语句,就要使用大括号将它们括起来,并且使用return语句返回。 1var sum = (num1, num2) => { return num1 + num2; } 由于大括号被解释为代码块,所以如果箭头函数直接返回一个对象,必须在对象外面加上括号,否则会报错。 12345// 报错let getTempItem = id => { id: id, name: "Temp" };// 不报错let getTempItem = id => ({ id: id, name: "Temp" }); 下面是一种特殊情况,虽然可以运行,但会得到错误的结果。 12let foo = () => { a: 1 };foo() // undefined 上面代码中,原始意图是返回一个对象{ a: 1 },但是由于引擎认为大括号是代码块,所以执行了一行语句a: 1。这时,a可以被解释为语句的标签,因此实际执行的语句是1;,然后函数就结束了,没有返回值。 如果箭头函数只有一行语句,且不需要返回值,可以采用下面的写法,就不用写大括号了。 1let fn = () => void doesNotReturn(); 箭头函数可以与变量解构结合使用。 123456const full = ({ first, last }) => first + ' ' + last;// 等同于function full(person) { return person.first + ' ' + person.last;} 箭头函数使得表达更加简洁。 12const isEven = n => n % 2 === 0;const square = n => n * n; 上面代码只用了两行,就定义了两个简单的工具函数。如果不用箭头函数,可能就要占用多行,而且还不如现在这样写醒目。 箭头函数的一个用处是简化回调函数。 1234567// 普通函数写法[1,2,3].map(function (x) { return x * x;});// 箭头函数写法[1,2,3].map(x => x * x); 另一个例子是 1234567// 普通函数写法var result = values.sort(function (a, b) { return a - b;});// 箭头函数写法var result = values.sort((a, b) => a - b); 下面是 rest 参数与箭头函数结合的例子。 123456789const numbers = (...nums) => nums;numbers(1, 2, 3, 4, 5)// [1,2,3,4,5]const headAndTail = (head, ...tail) => [head, tail];headAndTail(1, 2, 3, 4, 5)// [1,[2,3,4,5]] 注意点 箭头函数没有自己的this对象 不可以当作构造函数 不可以使用arguments对象,该对象在函数体内不存在。如果要用,可以用 rest 参数代替。 不可以使用yield命令,因此箭头函数不能用作 Generator 函数。 最重要的一点,普通函数this是指向函数运行时所在的对象,而箭头函数没有自己的this,内部的this就是定义时上层作用域的this,也就是说,箭头函数内部的this指向是固定的,相比之下,普通函数的this指向是可变的。 12345678910function foo() { setTimeout(() => { console.log('id:', this.id); }, 100);}var id = 21;foo.call({ id: 42 });// id: 42 上面代码中,setTimeout()的参数是一个箭头函数,这个箭头函数的定义生效是在foo函数生成时,而它的真正执行要等到 100 毫秒后。如果是普通函数,执行时this应该指向全局对象window,这时应该输出21。但是,箭头函数导致this总是指向函数定义生效时所在的对象(本例是{id: 42}),所以打印出来的是42。 下面例子是回调函数分别为箭头函数和普通函数,对比它们内部的this指向。 1234567891011121314151617function Timer() { this.s1 = 0; this.s2 = 0; // 箭头函数 setInterval(() => this.s1++, 1000); // 普通函数 setInterval(function () { this.s2++; }, 1000);}var timer = new Timer();setTimeout(() => console.log('s1: ', timer.s1), 3100);setTimeout(() => console.log('s2: ', timer.s2), 3100);// s1: 3// s2: 0 上面代码中,Timer函数内部设置了两个定时器,分别使用了箭头函数和普通函数。前者的this绑定定义时所在的作用域(即Timer函数),后者的this指向运行时所在的作用域(即全局对象)。所以,3100 毫秒之后,timer.s1被更新了 3 次,而timer.s2一次都没更新。 箭头函数实际上可以让this指向固定化,绑定this使得它不再可变,这种特性很有利于封装回调函数。下面是一个例子,DOM 事件的回调函数封装在一个对象里面。 123456789101112var handler = { id: '123456', init: function() { document.addEventListener('click', event => this.doSomething(event.type), false); }, doSomething: function(type) { console.log('Handling ' + type + ' for ' + this.id); }}; 上面代码的init()方法中,使用了箭头函数,这导致这个箭头函数里面的this,总是指向handler对象。如果回调函数是普通函数,那么运行this.doSomething()这一行会报错,因为此时this指向document对象。 总之,箭头函数根本没有自己的this,导致内部的this就是外层代码块的this。正是因为它没有this,所以也就不能用作构造函数。 下面是 Babel 转箭头函数产生的 ES5 代码,就能清楚地说明this的指向。 123456789101112131415// ES6function foo() { setTimeout(() => { console.log('id:', this.id); }, 100);}// ES5function foo() { var _this = this; setTimeout(function () { console.log('id:', _this.id); }, 100);} 上面代码中,转换后的 ES5 版本清楚地说明了,箭头函数里面根本没有自己的this,而是引用外层的this。 请问下面的代码之中,this的指向有几个? 123456789101112131415function foo() { return () => { return () => { return () => { console.log('id:', this.id); }; }; };}var f = foo.call({id: 1});var t1 = f.call({id: 2})()(); // id: 1var t2 = f().call({id: 3})(); // id: 1var t3 = f()().call({id: 4}); // id: 1 答案是this的指向只有一个,就是函数foo的this,这是因为所有的内层函数都是箭头函数,都没有自己的this,它们的this其实都是最外层foo函数的this。所以不管怎么嵌套,t1、t2、t3都输出同样的结果。如果这个例子的所有内层函数都写成普通函数,那么每个函数的this都指向运行时所在的不同对象。 除了this,以下三个变量在箭头函数之中也是不存在的,指向外层函数的对应变量:arguments、super、new.target。 12345678function foo() { setTimeout(() => { console.log('args:', arguments); }, 100);}foo(2, 4, 6, 8)// args: [2, 4, 6, 8] 上面代码中,箭头函数内部的变量arguments,其实是函数foo的arguments变量。 另外,由于箭头函数没有自己的this,所以当然也就不能用call()、apply()、bind()这些方法去改变this的指向。 123456(function() { return [ (() => this.x).bind({ x: 'inner' })() ];}).call({ x: 'outer' });// ['outer'] 上面代码中,箭头函数没有自己的this,所以bind方法无效,内部的this指向外部的this。 长期以来,JavaScript 语言的this对象一直是一个令人头痛的问题,在对象方法中使用this,必须非常小心。箭头函数”绑定”this,很大程度上解决了这个困扰。 7.尾调用优化什么是尾调用尾调用,是函数式编程的一个重要概念,就是指某个函数的最后一步是调用另一个函数 123function f(x){ return g(x);} 上面代码中,函数f的最后一步是调用函数g,这就叫尾调用。 以下三种情况,都不属于尾调用。 123456789101112131415// 情况一function f(x){ let y = g(x); return y;}// 情况二function f(x){ return g(x) + 1;}// 情况三function f(x){ g(x);} 上面代码中,情况一是调用函数g之后,还有赋值操作,所以不属于尾调用,即使语义完全一样。情况二也属于调用后还有操作,即使写在一行内。情况三等同于下面的代码。 1234function f(x){ g(x); return undefined;} 尾调用不一定出现在函数尾部,只要是最后一步操作即可。 123456function f(x) { if (x > 0) { return m(x) } return n(x);} 上面代码中,函数m和n都属于尾调用,因为它们都是函数f的最后一步操作。 尾调用优化尾调用之所以与其他调用不同,就在于它的特殊的调用位置。 我们知道,函数调用会在内存形成一个“调用记录”,又称“调用帧”(call frame),保存调用位置和内部变量等信息。如果在函数A的内部调用函数B,那么在A的调用帧上方,还会形成一个B的调用帧。等到B运行结束,将结果返回到A,B的调用帧才会消失。如果函数B内部还调用函数C,那就还有一个C的调用帧,以此类推。所有的调用帧,就形成一个“调用栈”(call stack)。 尾调用由于是函数的最后一步操作,所以不需要保留外层函数的调用帧,因为调用位置、内部变量等信息都不会再用到了,只要直接用内层函数的调用帧,取代外层函数的调用帧就可以了。 123456789101112131415function f() { let m = 1; let n = 2; return g(m + n);}f();// 等同于function f() { return g(3);}f();// 等同于g(3); 上面代码中,如果函数g不是尾调用,函数f就需要保存内部变量m和n的值、g的调用位置等信息。但由于调用g之后,函数f就结束了,所以执行到最后一步,完全可以删除f(x)的调用帧,只保留g(3)的调用帧。 这就叫做“尾调用优化”(Tail call optimization),即只保留内层函数的调用帧。如果所有函数都是尾调用,那么完全可以做到每次执行时,调用帧只有一项,这将大大节省内存。这就是“尾调用优化”的意义。 注意,只有不再用到外层函数的内部变量,内层函数的调用帧才会取代外层函数的调用帧,否则就无法进行“尾调用优化”。 1234567function addOne(a){ var one = 1; function inner(b){ return b + one; } return inner(a);} 上面的函数不会进行尾调用优化,因为内层函数inner用到了外层函数addOne的内部变量one。 注意,目前只有 Safari 浏览器支持尾调用优化,Chrome 和 Firefox 都不支持。 尾递归函数调用自身,称为递归。如果尾调用自身,就称为尾递归。 递归非常耗费内存,因为需要同时保存成千上百个调用帧,很容易发生“栈溢出”错误(stack overflow)。但对于尾递归来说,由于只存在一个调用帧,所以永远不会发生“栈溢出”错误。 123456function factorial(n) { if (n === 1) return 1; return n * factorial(n - 1);}factorial(5) // 120 上面代码是一个阶乘函数,计算n的阶乘,最多需要保存n个调用记录,复杂度 O(n) 。 如果改写成尾递归,只保留一个调用记录,复杂度 O(1) 。 123456function factorial(n, total) { if (n === 1) return total; return factorial(n - 1, n * total);}factorial(5, 1) // 120 还有一个比较著名的例子,就是计算 Fibonacci 数列,也能充分说明尾递归优化的重要性。 非尾递归的 Fibonacci 数列实现如下。 123456789function Fibonacci (n) { if ( n <= 1 ) {return 1}; return Fibonacci(n - 1) + Fibonacci(n - 2);}Fibonacci(10) // 89Fibonacci(100) // 超时Fibonacci(500) // 超时 尾递归优化过的 Fibonacci 数列实现如下。 123456789function Fibonacci2 (n , ac1 = 1 , ac2 = 1) { if( n <= 1 ) {return ac2}; return Fibonacci2 (n - 1, ac2, ac1 + ac2);}Fibonacci2(100) // 573147844013817200000Fibonacci2(1000) // 7.0330367711422765e+208Fibonacci2(10000) // Infinity 由此可见,“尾调用优化”对递归操作意义重大,所以一些函数式编程语言将其写入了语言规格。ES6 亦是如此,第一次明确规定,所有 ECMAScript 的实现,都必须部署“尾调用优化”。这就是说,ES6 中只要使用尾递归,就不会发生栈溢出(或者层层递归造成的超时),相对节省内存。 递归函数的改写尾递归的实现,往往需要改写递归函数,确保最后一步只调用自身。做到这一点的方法,就是把所有用到的内部变量改写成函数的参数。比如上面的例子,阶乘函数 factorial 需要用到一个中间变量total,那就把这个中间变量改写成函数的参数。这样做的缺点就是不太直观,第一眼很难看出来,为什么计算5的阶乘,需要传入两个参数5和1? 两个方法可以解决这个问题。方法一是在尾递归函数之外,再提供一个正常形式的函数。 12345678910function tailFactorial(n, total) { if (n === 1) return total; return tailFactorial(n - 1, n * total);}function factorial(n) { return tailFactorial(n, 1);}factorial(5) // 120 上面代码通过一个正常形式的阶乘函数factorial,调用尾递归函数tailFactorial,看起来就正常多了。 函数式编程有一个概念,叫做柯里化(currying),意思是将多参数的函数转换成单参数的形式。这里也可以使用柯里化。 1234567891011121314function currying(fn, n) { return function (m) { return fn.call(this, m, n); };}function tailFactorial(n, total) { if (n === 1) return total; return tailFactorial(n - 1, n * total);}const factorial = currying(tailFactorial, 1);factorial(5) // 120 上面代码通过柯里化,将尾递归函数tailFactorial变为只接受一个参数的factorial。 第二种方法就简单多了,就是采用 ES6 的函数默认值。 123456function factorial(n, total = 1) { if (n === 1) return total; return factorial(n - 1, n * total);}factorial(5) // 120 上面代码中,参数total有默认值1,所以调用时不用提供这个值。 总结一下,递归本质上是一种循环操作。纯粹的函数式编程语言没有循环操作命令,所有的循环都用递归实现,这就是为什么尾递归对这些语言极其重要。对于其他支持“尾调用优化”的语言(比如 Lua,ES6),只需要知道循环可以用递归代替,而一旦使用递归,就最好使用尾递归。 8.Function.prototype.toString()ES2019 对函数实例的toString()方法做出了修改。 toString()方法返回函数代码本身,以前会省略注释和空格。 1234function /* foo comment */ foo () {}foo.toString()// function foo() {} 上面代码中,函数foo的原始代码包含注释,函数名foo和圆括号之间有空格,但是toString()方法都把它们省略了。 修改后的toString()方法,明确要求返回一模一样的原始代码。 1234function /* foo comment */ foo () {}foo.toString()// "function /* foo comment */ foo () {}" 9.catch 命令的参数省略JavaScript 语言的try...catch结构,以前明确要求catch命令后面必须跟参数,接受try代码块抛出的错误对象。 12345try { // ...} catch (err) { // 处理错误} 上面代码中,catch命令后面带有参数err。 很多时候,catch代码块可能用不到这个参数。但是,为了保证语法正确,还是必须写。ES2019 做出了改变,允许catch语句省略参数。 12345try { // ...} catch { // ...}","link":"/JavaScript/14.%E5%87%BD%E6%95%B0%E7%9A%84%E6%89%A9%E5%B1%95/"},{"title":"Grid布局","text":"1.什么是Grid布局Flex布局是轴线布局,只能指定”项目”针对轴线的位置,可以看作是一维布局,Grid布局则是将容器划分成”行”和”列”,产生单元格,然后指定项目所在的单元格,可以看作是二维布局,Grid布局远比Flex布局强大 常用的三种布局 传统布局:利用position+display+float属性布局,兼容性最好,但效率低 FlexBox:一维布局方案,有自己的一套属性,效率高,学习成本低,兼容性强 Grid布局:网格布局是最强大的CSS布局方案,但是知识点较多,学习成本相对困难些,目前兼容性不如FlexBox 兼容性 2.基本概念 容器:有容器属性 项目:有项目属性 3.容器属性 grid-template-columns grid-template-rows row-gap column-gap gap(上面两个的简写) grid-template-areas grid-auto-flow justify-items align-items place-items(上面两个的简写) justify-content align-content place-content(上面两个的简写) grid-auto-columns grid-auto-rows3.1.grid-template-*设置Grid布局的行列属性(几行,几列)123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081<!DOCTYPE html><html lang="en"><head> <meta charset="UTF-8"> <meta http-equiv="X-UA-Compatible" content="IE=edge"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>Grid</title> <style> * { margin: 0; padding: 0; } .root { width: 600px; height: 600px; border: 1px solid cadetblue; display: grid; grid-template-columns: 100px 100px 100px; grid-template-rows: 100px 100px 100px 100px; } .item1 { background-color: red; } .item2 { background-color: gold; } .item3 { background-color: aqua; } .item4 { background-color: beige; } .item5 { background-color: blueviolet; } .item6 { background-color: brown; } .item7 { background-color: green; } .item8 { background-color: blanchedalmond; } .item9 { background-color: violet; } .item10 { background-color: thistle; } </style></head><body> <div class="root"> <div class="item item1">1</div> <div class="item item2">2</div> <div class="item item3">3</div> <div class="item item4">4</div> <div class="item item5">5</div> <div class="item item6">6</div> <div class="item item7">7</div> <div class="item item8">8</div> <div class="item item9">9</div> <div class="item item10">10</div> </div></body></html> repeat():第一个参数是重复次数,第二个参数是需要重复的值12grid-template-columns: repeat(3, 100px); // 重复三次,每个项目100pxgrid-template-rows: repeat(4, 100px); // 重复四次,每个项目100px auto-fill:有时候项目大小是固定的,但是容器大小不确定,这个属性就会自动填充1grid-template-columns: repeat(auto-fill, 100px) fr:为了方便表示比例关系,网格布局提供fr关键字(意为片段)1grid-template-columns: repeat(4, 1fr) // 宽度被平均分成4份 minmax():函数产生一个长度范围,表示长度就在这个范围之中,它接受两个参数,分别为最小值和最大值1grid-template-columns: 1fr minmax(15px, 1fr) auto:表示由浏览器自己决定宽度(自适应)1grid-template-columns: 100px auto 100px; // 两端固定,中间自适应 网格线:可以用方括号定义网格线名称,方便以后引用1grid-template-columns: [c1]100px [c2]100px [c3]100px [c4] 3.2.*-gap规定网格中行列之间的间距 column-gap row-gap grid-gap(row-gap | column-gap) 12345display: grid;grid-template-columns: repeat(2, 1fr);column-gap: 20px;row-gap: 20px;gap: 20px 10px; 3.3.grid-template-area用来划分模板的区域 1grid-template-areas: "a b c" "d e f" "g h i" "j"; 3.4.grid-auto-flow规定容器中项目的排列顺序如果出现如下图位置的情况,为了充分利用空间,可以加上dense属性 3.5.justify-items/align-items设置单元格内容的水平和垂直的对齐方式 12justify-items: start | end | center | stretch;align-items: start | end | center | stretch; place-itemsplace-items属性是align-items属性和justify-items属性的合并简写 1place-items: <align-items> <justify-items> 3.6.justify-content/align-content设置整个内容区域的水平和垂直对齐方式 12justify-content: start | end | center | stretch | space-around | space-between | space-evenly;align-content: start | end | center | stretch | space-around | space-between | space-evenly; place-contentplace-contend属性是align-contend属性和justify-contend属性的合并简写 1place-content: <align-content> <justify-content> 3.7.grid-auto-columns/grid-auto-rows 用来设置多出来的项目的宽和高 4.项目属性 grid-column-start grid-column-end grid-column(上面两个的简写) grid-row-start grid-row-end grid-row(上面两个的简写) grid-area justify-self align-self place-self(上面两个的简写) 4.1.grid-column-start/grid-column-end/grid-row-start/grid-row-end用来指定item在网格线中的开始网线和结束网线 简写 12grid-column: 1 / 3grid-row: 1 / 3 span写法 规定项目直接占据的行/列数量 12grid-column: span 3; // 跨越3列grid-row: span 1; // 跨越1行 4.2.grid-area指定项目放在哪一个区域 配合容器属性grid-template-area使用 用做上面四个属性的简写 grid-area属性可以用作grid-row-start,grid-column-start,grid-row-end,grid-column-end的合并简写形式,直接指定项目的位置 12/* grid-area: <row-start> / <column-start> / <row-end> / <column-end> */grid-areaL 1 / 1 / 3 / 3; 4.3.justify-self/align-selfjustify-self属性用来单独设置单元格内容的水平位置,跟justify-items属性的用法完全一致align-self属性用来单独设置单元格内容的垂直位置,跟align-items属性的用法完全一致 place-selfplace-self属性是align-self属性和justify-self属性的合并简写形式 1place-self: <align-self> <justify-self>","link":"/Grid%E5%B8%83%E5%B1%80/"},{"title":"min max clamp函数","text":"padding和父元素宽度的关系子元素不设置宽高,它的padding-bottom的比例是以父元素的宽度为参照的,相当于可以使用子元素来撑开父元素,实现元素的宽高等比效果 1234567.parent { width: 600px}.parent > .child { padding: 0 0 50% 0; // 相当于父元素宽度的50%} min()min函数用来设置元素的最小返回值,例如min(50%, 500px),浏览器会在50%和500px中取一个最小值,当视口宽度的50%大于500px时,取500px,否则就使用50% 1234width: min(50%, 500px)// 相当于width: 50%max-width: 500px; max()max()函数用来设置元素的最大返回值,例如max(50%, 600px),当视口宽度的50%大于600px时,取50%的值,否则取600px 1234width: max(50%, 600px)// 相当于width: 50%min-width: 600px clamp()设置一个区间范围值,即最小值,首选值和最大值 1width: clamp(600px, 80%, 1200px)","link":"/min%20clamp%E5%87%BD%E6%95%B0/"},{"title":"一道扁平数据转树结构面试题","text":"一、前言扁平数据转树状结构,是前端工程师必备的功能之一,这篇文章记录一道相关的面试题 二、相关知识点扁平数据转树状结构的要点有两个: 确定父子关系如何确认数据之间的父子关系是功能的重点,找到确认方法就可以事半功倍 浅拷贝在转换的过程中,我们会用到浅拷贝的知识 三、题目和解法12345678910111213141516171819202122232425262728293031323334353637383940414243444546const arr = [ { id: '1.1', label: '武汉市' }, { id: '1.1.2', label: '武汉市洪山区' }, { id: '1.3.1', label: '孝感市孝南区' }, { id: '1.5.1', label: '黄石市黄石港区' }, { id: '1.2', label: '宜昌市' }, { id: '1', label: '湖北省' }, { id: '1.1.1', label: '武汉市江夏区' }, { id: '1.2.1', label: '宜昌市伍家岗区' }, { id: '1.3', label: '孝感市' }]/** * * @param {Array} originArr */const arrayToTree = (originArr) => { const result = []; // 存储要返回的数据 const resultIdsSet = new Set(); // 存储命中的ID originArr.forEach(item => { if (item.id.length === 1) { result.push(item) } item.children = originArr.filter(originItem => { // 将ID根据.分割成数组 const originItemIdArr = originItem.id.split('.'); const itemIdArr = item.id.split('.'); /** * 父子关系说明:1 父ID的位数只比子ID少一位 2 匹配的时候,父ID的位数上的每个值可以和子ID位数上的值匹配上即可确定关系 */ if (itemIdArr.length + 1 === originItemIdArr.length && itemIdArr.every((i, k) => i === originItemIdArr[k])) { resultIdsSet.add(item.id); resultIdsSet.add(originItem.id); return tree; } return false; }) }) // 最后需要找出未命中的ID放在数组后面返回 return [...result, ...arr.filter(item => !resultIdsSet.has(item.id))];}console.log(arrayToTree(arr))","link":"/%E4%B8%80%E9%81%93%E6%89%81%E5%B9%B3%E6%95%B0%E6%8D%AE%E8%BD%AC%E6%A0%91%E7%BB%93%E6%9E%84%E9%9D%A2%E8%AF%95%E9%A2%98/"},{"title":"移动端开发记录","text":"h5适配计算什么是viewport 早期移动端的viewport与pc的viewport是一个概念,导致小屏体验不佳,后来苹果引入可视视窗(visual viewport)和布局视窗(layout viewport)。这两个视窗是透视的效果,想象下layout viewport是一张大的不能改变大小和角度的图片。我们透过visual viewport对layout viewport进行观察。观察距离远点(用户的缩小页面功能)就可以一次性看到这个大图。或者近点(用户的放大页面功能)可以看到一部分。你能改变这个透视框的方向,但这张大图片的大小和形状都不会改变。 我们在<meta name="viewport" /> 设置的其实是layout-viewport,使得layout viewport==visual viewport,达到ideal viewport效果,使得viewport刚好完美覆盖屏幕,因此适配方案的时候,这一句最重要。 px rem vw的转换 默认情况下根元素的font-size为 1rem = 16px,但为了方便换算,我们通常设置 1rem = 100px 以750px的设计稿为例,则可以得出 750px = 7.5rem 相当于100vw=7.5rem那么1rem = 100vw / 7.5 = 13.3333vw,所以设置根元素的font-size为13.3333vw 而在页面样式中,直接将设计稿中的px除以100就是对应的rem值 若要兼容旧浏览器,则需要写入响应式布局,例如:12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576// 相当于 320 / 7.5 = 42.667px@media screen and (max-width: 320px) { html { font-size: 42.667px; font-size: 13.3333vw; }}// 相当于375 / 7.5 = 48px,以下同理@media screen and (min-width: 321px) and (max-width: 375px) { html { font-size: 48px; font-size: 13.3333vw; }}@media screen and (min-width: 376px) and (max-width:393px) { html { font-size: 52.4px; font-size: 13.3333vw }}@media screen and (min-width: 394px) and (max-width:412px) { html { font-size: 54.93px; font-size: 13.3333vw }}@media screen and (min-width: 413px) and (max-width:414px) { html { font-size: 55.2px; font-size: 13.3333vw }}@media screen and (min-width: 415px) and (max-width:480px) { html { font-size: 64px; font-size: 13.3333vw }}@media screen and (min-width: 481px) and (max-width:540px) { html { font-size: 72px; font-size: 13.3333vw }}@media screen and (min-width: 541px) and (max-width:640px) { html { font-size: 85.33px; font-size: 13.3333vw }}@media screen and (min-width: 641px) and (max-width:720px) { html { font-size: 96px; font-size: 13.3333vw }}@media screen and (min-width: 721px) and (max-width:768px) { html { font-size: 102.4px; font-size: 13.3333vw }}@media screen and (min-width: 769px) and (max-width:852px) { html { font-size: 113.4px; font-size: 13.3333vw }}@media screen and (min-width: 853px) { html { font-size: 130.4px; font-size: 13.3333vw }} 不同的设计稿,可以参考下面的表格 设计稿大小(单位 px) html 的 font-size(单位 vw) 375 26.666666 750 13.333333 320 31.25 640 15.625 viewport缩放比例设置1<meta name="viewport" content="width=device-width,initial-scale=1.0,minimum-scale=1.0,maximum-scale=1.0,user-scalable=no" /> 其他可能使用到的meta标签配置123456<meta name="screen-orientation" content="portrait"> //Android 禁止屏幕旋转<meta name="full-screen" content="yes"> //全屏显示<meta name="browsermode" content="application"> //UC应用模式,使用了application这种应用模式后,页面讲默认全屏,禁止长按菜单,禁止收拾,标准排版,以及强制图片显示。<meta name="x5-orientation" content="portrait"> //QQ强制竖屏<meta name="x5-fullscreen" content="true"> //QQ强制全屏<meta name="x5-page-mode" content="app"> //QQ应用模式 电话号码识别在 iOS Safari (其他浏览器和 Android 均不会)上会对那些看起来像是电话号码的数字处理为电话链接,比如: 7 位数字,形如:1234567 带括号及加号的数字,形如:(+86)123456789 双连接线的数字,形如:00-00-00111 11 位数字,形如:13800138000 关闭识别 1<meta name="format-detection" content="telephone=no" /> 开启识别 1<a href="tel:123456">123456</a> 邮箱识别(Android)安卓上会对符合邮箱格式的字符串进行识别,我们可以通过如下的 meta 来管别邮箱的自动识别 1<meta content="email=no" name="format-detection" /> 同样地,我们也可以通过标签属性来开启长按邮箱地址弹出邮件发送的功能: 1<a mailto:dooyoe@gmail.com">dooyoe@gmail.com</a> chrome实机调试 电脑上访问chrome://inspect/#devices 在手机中安装Chrome浏览器 若是调试微信内置网页,需要先在手机中打开这个地址:http://debugxweb.qq.com/?inspector=true 安卓手机开启USB调试,通过USB连接电脑即可 底部使用固定定位遮住内容给底部内容设置padding-bottom撑开 判断页面所在的环境判断是否移动端1234567function isMobile(){ if(window.navigator.userAgent.match(/(phone|pad|pod|iPhone|iPod|ios|iPad|Android|Mobile|BlackBerry|IEMobile|MQQBrowser|JUC|Fennec|wOSBrowser|BrowserNG|WebOS|Symbian|Windows Phone)/i)) { return true; // 移动端 }else{ return false; // PC端 }} 判断是否微信环境12345function getIsWxClient() { const ua = navigator.userAgent.toLowerCase(); const reg = /MicroMessenger/i; return reg.test(ua);} 判断是否横屏/竖屏123456789101112// 判断是否横屏竖屏function hengshuping() { // 竖屏 if (window.orientation == 180 || window.orientation == 0) { alert('竖屏') } // 横屏 if (window.orientation == 90 || window.orientation == -90) { alert('横屏') }}window.addEventListener('onorientationchange' in window ? 'orientationchange' : 'resize',hengshuping,false); 判断是否IOS环境1234function isIos() { const u = navigator.userAgent; return !!u.match(/\\(i[^;]+;( U;)? CPU.+Mac OS X/);} 判断浏览器环境12345678910111213141516171819202122232425262728293031323334let isOpera = (!!window.opr && !!opr.addons) || !!window.opera || navigator.userAgent.indexOf(' OPR/') >= 0;// Firefox 1.0+let isFirefox = typeof InstallTrigger !== 'undefined';// Safari 3.0+ "[object HTMLElementConstructor]"let isSafari = /constructor/i.test(window.HTMLElement) || (function (p) { return p.toString() === "[object SafariRemoteNotification]"; })(!window['safari'] || (typeof safari !== 'undefined' && safari.pushNotification));// Internet Explorer 6-11let isIE = /*@cc_on!@*/false || !!document.documentMode;// Edge 20+let isEdge = !isIE && !!window.StyleMedia;// Chrome 1 - 79let isChrome = !!window.chrome && (!!window.chrome.webstore || !!window.chrome.runtime);// Edge (based on chromium) detectionlet isEdgeChromium = isChrome && (navigator.userAgent.indexOf("Edg") != -1);// Blink engine detectionlet isBlink = (isChrome || isOpera) && !!window.CSS;let output = 'Detecting browsers by ducktyping:<hr>';output += 'isFirefox: ' + isFirefox + '<br>';output += 'isChrome: ' + isChrome + '<br>';output += 'isSafari: ' + isSafari + '<br>';output += 'isOpera: ' + isOpera + '<br>';output += 'isIE: ' + isIE + '<br>';output += 'isEdge: ' + isEdge + '<br>';output += 'isEdgeChromium: ' + isEdgeChromium + '<br>';output += 'isBlink: ' + isBlink + '<br>';document.body.innerHTML = output; iphone字体重叠问题在iphone8下设置字体容器的宽度,发现字体会重叠,将容器宽度取消,问题解决 移动端获取scrollTop高度不同手机的浏览器获取scrollTop存在兼容问题,需要取几个可能取到的属性的最大值 12const scrollTop = Math.max(document.documentElement.scrollTop,document.body.scrollTop,window.scrollY);const scrollHeight = Math.max(document.documentElement.scrollHeight || document.body.scrollHeight) 关于小米手机自带浏览器背景图加载失败自带浏览器可能对一些字段进行了屏蔽(例如广告之类的),在对图片进行命名时,尽量简单命名,避开某些关键字 scss less移动端函数转换1234// scss@function pxToRem($size) { @return calc($size / 100) * 1rem} 123456789// less.pxToRem(@px) { @var: unit(@px / 100) @rem: ~'@{var}rem'}// 使用.box { width: .pxToRem(300px)[@rem]}","link":"/%E7%A7%BB%E5%8A%A8%E7%AB%AF%E5%BC%80%E5%8F%91%E8%AE%B0%E5%BD%95/"},{"title":"打包图片资源","text":"1.打包图片资源 webpack打包图片资源需要下载两个loader: url-loader file-loader url-loader 依赖于file-loader,在配置时只需要引入一个loader即可 1234567891011121314151617181920212223const dirs = require('./dirs');module.exports = { module: { rules: [ /** 处理图片资源,需要 url-loader file-loader */ { test: /\\.(png|jpe?g|gif|svg)(\\?.*)?$/, use: [ { loader: 'url-loader', options: { limit: 5 * 1000, include: dirs.src, // 打包后的图片路径以及命名 name: 'images/[path][name].[ext]' } } ] } ] }}","link":"/webpack/5.%E6%89%93%E5%8C%85%E5%9B%BE%E7%89%87%E8%B5%84%E6%BA%90/"},{"title":"devServer","text":"1.devServer基本配置 开发环境下自动编译,自动打开浏览器 123456789101112131415{ module.exports = { // 开发服务器, 在开发过程中热加载项目 devServer: { // 构建后的项目运行的目录 contentBase: path.resolve(dirs.build), // 启动gzip压缩 compress: true, // 端口号 port: 8100, // 自动打开浏览器 open: false, } }}","link":"/webpack/6.devServer/"},{"title":"JavaScript类型转换","text":"一、前言JS有七种简单数据类型:undefined、null、boolean、string、number、symbol、bigint,以及引用类型 object 但由于JavaScript是弱类型语言,所以只有在运行期间才会确定当前类型 1const x = y ? 1 : a; 上面代码中,x的值在编译阶段是无法获取的,只有等到程序运行时才能知道 虽然变量的数据类型是不确定的,但是各种运算符对于数据类型是有要求的,如果运算符的类型与预期不符合,就会触发类型转换机制 常见的类型转换有: 强制转换(显示转换) 自动转换(隐式转换)","link":"/JavaScript%E7%B1%BB%E5%9E%8B%E8%BD%AC%E6%8D%A2/"},{"title":"Webpack简介","text":"1.简介 Webpack是一种前端资源构建工具,一个静态模块打包器(module bundler) 在Webpack看来,前端的所有资源文件(js/json/css/img/less/…)都会作为模块处理 它将根据模块的依赖关系进行静态分析,打包生成对应的静态资源(bundle) 2.原理当前端需要引入模块(js文件,样式资源,图片,字体等其他资源)时,webpack会将这些资源交给构建工具去处理,构建工具会从webpack指定的入口文件作为起点开始打包,会将每一个模块记录好,形成依赖关系结构图,然后根据依赖关系图的先后顺序将这些模块引入,形成一个chunk代码块,最后将这个代码块中的模块(ts,less,scss…)进行编译打包,形成浏览器所能识别的文件,最后将处理好的资源进行输出,输出的文件称为bundle 3.五个核心概念 Entry 入口(Entry)指示,Webpack以哪个文件为入口起点开始打包,分析构建内部依赖图 Output 输出(Output)指示Webpack打包后的资源bundles输出到哪里去,以及如何命名 Loader(loader放在module模块的rules数组中) Loader让Webpack能够去处理那些非JavaScript文件(Webpack自身只能理解JavaScript) Plugins 插件(Plugins)可以用于执行范围更广的任务,插件的范围包括,从打包优化和压缩,一直到重新定义环境中的变量等。 Mode 打包的模式(development production)","link":"/webpack/1.webpack%E7%AE%80%E4%BB%8B/"},{"title":"Webpack初始化","text":"1.初始化webpack12$ yarn init -y$ yarn webpack webpack-cli webpack-dev-server -D package.json 1234"scripts": { "dev": "webpack server --config ./scripts/webpack.config.js --mode=development", // 开发时的启动服务命令 "build": "webpack --config ./scripts/webpack.config.js --mode=production" // 编译命令}, webpack.config.js**(注意webpack配置文件的路径,需要和package.json中启动命令的路径对应)** 123456789101112131415161718// /script/webpack.config.jsconst path = require('path');module.exports = { // 入口 entry: path.resolve(__dirname, '../src/index.js'), // 出口 output: { filename: 'index.js', path: path.resolve(__dirname, '../dist'), }, // loader module: [], // plugins plugins: [],} 2.配置目录文件 在scripts文件夹中新建dirs.js,此文件中专门用于存储各种需要用到的路径 123456789101112131415161718192021const path = require('path');const root = path.resolve(__dirname, '../../');const dirs = { // 根目录 root, // 源码目录 src: path.resolve(root, './src'), // 输出目录 dist: path.resolve(root, './dist'), // 第三方库 lib: path.resolve(root, './lib'), // 构建脚本目录 build: path.resolve(root, './webpack'), // modules modules: path.resolve(root, './node_modules'), // package package: path.resolve(root, './package.json')};module.exports = dirs;","link":"/webpack/2.%E5%88%9D%E5%A7%8B%E5%8C%96/"},{"title":"打包样式资源","text":"1.打包样式资源 常用的样式资源有 css less (sa|sc)ss,这些分别需要引入不同的loader 固定依赖: style-loader css-loader前者将打包好的资源引入到根文件的<head></head>标签内,后者将css样式文件编译成commjs可识别的字符串 引入less: less less-loader 引入(sa|sc)ss:sass sass-loader 1234567891011121314151617181920212223242526272829303132const { resolve } = require('path');module.exports = { ... // loader配置 module: { rules: [ { // 匹配哪些文件 test: /\\.css$/, // 使用哪些loader 执行顺序是数组降序(从下往上) use: [ // 创建style标签,将js中的样式资源插入到资源中进行,并将标签添加到页面head标签中 'style-loader', // 将 css文件变成commonjs模块加载到js中,里面的内容是字符串 'css-loader' ] }, /** 支持引入less 需要 less和less-loader依赖 */ { test: /\\.less$/, use: ['style-loader','css-loader','less-loader'] }, /** 支持引入(sa|sc)ss 需要sass和sass-loader依赖 */ { test: /\\.(sa|sc)ss$/, use: ['style-loader','css-loader','sass-loader'] } ] }, ...} 2.支持css module写法 只需在对应的css-loader中加入modules配置即可 12345678910111213141516171819202122232425262728293031const { resolve } = require('path');module.exports = { ... // loader配置 module: { rules: [ ... /** 支持引入(sa|sc)ss 需要sass和sass-loader依赖 */ { test: /\\.(sa|sc)ss$/, use: [ 'style-loader', { loader: 'css-loader', options: { importLoaders: true, // 支持 css module写法 modules: { localIdentName: '[local]__[name]-[hash:base64:4]' } } }, 'sass-loader' ] } ... ] }, ...}","link":"/webpack/3.%E6%89%93%E5%8C%85%E6%A0%B7%E5%BC%8F%E8%B5%84%E6%BA%90/"},{"title":"打包HTML资源","text":"1.打包html文件 下载plugin: html-webpack-plugin 使用 12345678910const HtmlWebpackPlugin = require('html-webpack-plugin');module.exports = { plugins: [ new HtmlWebpackPlugin({ // 复制指定的html文件,并自动引入打包后的所有资源 template: path.resolve(__dirname, '../src/index.html') }), ]}","link":"/webpack/4.%E6%89%93%E5%8C%85html%E8%B5%84%E6%BA%90/"},{"title":"模块联邦","text":"一、前言微前端的概念相信大家都不陌生,其本质是一种架构设计思想,将一个完整的项目拆分成若干个小项目,项目的开发和部署互不影响,上线后再聚合成一个完整的项目。webpack5的模块联邦其功能与这个类似,可以用来加载远程模块 二、三个概念模块联邦有三个重要的概念: webpack构建,一个独立项目通过webpack打包编译而产生资源包 remote,一个暴露模块提供其它 webpack构建 消费的 webpack构建 host,一个消费其他 remote 模块的 webpack构建 一言以蔽之,一个webpack构建可以是remote–即服务的提供方,也可以是host–即服务的消费方,也可以同时扮演服务提供方和服务消费者,完全看项目的架构。 需要指出的是,任何一个webpack构建既可以作为host消费方,也可以作为remote提供方,区别在于职责和webpack配置的不同 三、项目实战一共有三个微应用: lib-app、component-app、main-app,角色分别是: lib-app as remote,暴露了两个模块 react和 react-dom component-app as remote and host,依赖lib-app,暴露了一些组件供main-app消费 main-app as host,依赖lib-app和component-app lib-app暴露模块123456789101112131415//webpack.config.jsmodule.exports = { //...省略 plugins: [ new ModuleFederationPlugin({ name: "lib_app", filename: "remoteEntry.js", exposes: { "./react":"react", "./react-dom":"react-dom" } }) ], //...省略} component-app的配置依赖 lib-app,暴露三个模块组件 Button、Dialog、Logo 123456789101112131415161718//webpack.config.jsmodule.exports = { //...省略 plugins:[ new ModuleFederationPlugin({ name: "component_app", filename: "remoteEntry.js", exposes: { "./Button":"./src/Button.jsx", "./Dialog":"./src/Dialog.jsx", "./Logo":"./src/Logo.jsx" }, remotes:{ "lib-app":"lib_app@http://localhost:3000/remoteEntry.js" } }), ]} 三个暴露的组件 12345//Button.jsximport React from 'lib-app/react';export default function(){ return <button style={{color: "#fff",backgroundColor: "#409eff",borderColor: "#409eff"}}>按钮组件</button>} 1234567891011121314151617181920212223242526//Dialog.jsximport React from 'lib-app/react';export default class Dialog extends React.Component { constructor(props) { super(props); } render() { if(this.props.visible){ return ( <div style={{position:"fixed",left:0,right:0,top:0,bottom:0,backgroundColor:"rgba(0,0,0,.3)"}}> <button onClick={()=>this.props.switchVisible(false)} style={{position:"absolute",top:"10px",right:"10px"}}>X</button> <div style={{ marginTop:"20%",textAlign:"center"}}> <h1> What is your name ? </h1> <input style={{fontSize:"18px",lineHeight:2}} type="text" /> </div> </div> ); }else{ return null; } }} 123456// Logo.jsximport React from 'lib-app/react';import pictureData from './MF.jpeg'export default function(){ return <img src={pictureData} style={{width:"500px",borderRadius:"10px"}}/>} main-app的配置main-app依赖两个项目 lib-app和 component-app 1234567891011121314151617///webpack.config.jsmodule.exports = { //省略... plugins: [ new ModuleFederationPlugin({ name: "main_app", remotes:{ "lib-app":"lib_app@http://localhost:3000/remoteEntry.js", "component-app":"component_app@http://localhost:3001/remoteEntry.js" }, }), new HtmlWebpackPlugin({ template: "./public/index.html", }) ] //省略...}; 使用在 main-app 中引入 component-app 暴露出来的模块,即可使用 12345678910111213141516import React from 'lib-app/react';import Button from 'component-app/Button'import Dialog from 'component-app/Dialog'import Logo from 'component-app/Logo'export default class App extends React.Component{ constructor(props) { super(props) //省略... } //省略... render(){ return (<div> //省略... </div>) }}","link":"/webpack/%E6%A8%A1%E5%9D%97%E8%81%94%E9%82%A6/"},{"title":"归纳一下几个常用的CSS滚动属性","text":"Scroll SnapScroll Snaps可以用于创建平滑滚动的容器,旨在为开发者提供良好控制的滚动体验 scroll-behaviorscroll-behavior可以让容器进行锚点定位时,或者 JS 设置 scrollLeft/scrollTop 滚动距离时候表现为平滑定位。 123.scroll-container { scroll-behavior: smooth;} overscroll-behavioroverscroll-behavior属性可以让滚动嵌套时父滚动不触发,比方说弹框中的滚动滚动到底部的时候,页面中的滚动条不会移动 auto:默认值 contain:默认滚动边界不变,临近滚动区域不会被滚动链影响到,比如对话框后方的页面不会滚动 none:临近滚动区域不受到滚动链影响,而且默认的滚动到边界的表现也被阻止。 overflow-anchor1overflow-anchor: auto | none 用于指定浏览器滚动锚定的行为,通常表现为执行滚动锚定;overflow-anchor:none则表示禁止滚动锚定的行为。 scrollbar-widthscrollbar-width 可以用来自定义滚动条的宽度,不过不能指定具体数值,只能是正常(17px),细(8px)和没有,语法如下: 1scrollbar-width: auto | thin | none; 主要针对 windows 系统下的 Firefox 浏览器,因为 Mac OS X 或iOS 操作系统滚动条默认就不占据宽度,没必要使用这个,而 Chrome 浏览器可以使用 -webkit-scrollbar 伪元素自定义滚动条的宽度,用不到 scrollbar-width 设置。 scrollbar-colorscrollbar-color 可以设置滚动条的颜色,和 scrollbar-width 一样,仅 Firefox 浏览器支持,语法如下: 1scrollbar-color: auto | 滑杆颜色 轨道颜色; 和 scrollbar-width 属性一起,填补了 Firefox 浏览器滚动条样式无法自定义的空白。 例如: 1234.container { scrollbar-width: thin; scrollbar-color: #0009 transparent;} scrollbar-gutterscrollbar-gutter 可以让滚动条出现的时候内容不晃动。 实现原理是把滚动条的位置提前预留好(空白) ::-webkit-scrollbar用于调整滚动条样式的属性 ::-webkit-scrollbar——整个滚动条。 ::-webkit-scrollbar-button——滚动条上的按钮(上下箭头)。 ::-webkit-scrollbar-thumb——滚动条上的滚动滑块。 ::-webkit-scrollbar-track——滚动条轨道。 ::-webkit-scrollbar-track-piece——滚动条没有滑块的轨道部分。 ::-webkit-scrollbar-corner——当同时有垂直滚动条和水平滚动条时交汇的部分。通常是浏览器窗口的右下角。 ::-webkit-resizer——出现在某些元素底角的可拖动调整大小的滑块。","link":"/CSS%E6%BB%9A%E5%8A%A8%E5%B1%9E%E6%80%A7/"},{"title":"Chrome DevTools简介","text":"Chrome DevTools分为两部分,backend和frontend backend和Chrome集成,负责把Chrome的网页运行时状态通过调试协议暴露出来。 frontend是独立的,负责对接调试协议,做UI的展示和交互 两者之间的调试协议叫做Chrome DevTools Protocol,简称CDP。传输协议数据的方式叫做信道(message channel),有很多种,比如Chrome DevTools嵌入在Chrome里时,两者通过全局的函数通信;当Chrome DevTools远程调试某个目标代码时,两者通过WebSocket通信。frontend、backend、调试协议(CDP)、信道,这是Chrome DevTools的4个组成部分。 backend 可以是 Chromium,也可以是 Node.js 或者 V8,这些 JS 的运行时都支持 Chrome DevTools Protocol。这就是 Chrome DevTools 的调试原理。除了 Chrome DevTools 之外,VSCode Debugger 也是常用的调试工具 调试就是通过某种信道(比如 WebSocket)把运行时信息传递给开发工具,做 UI 的展示和交互,辅助开发者排查问题、了解代码运行状态等。","link":"/Chrome%20DevTools/"},{"title":"Git常用命令","text":"1.Git简介Git是一个开源的分布式版本控制系统,用于敏捷高效地处理任何大小型项目 Git有四个区,分别为工作区,暂存区,本地仓库和远程仓库 Workspace:工作区,就是平时写代码的地方 Index:暂存区,用于存放临时的改动 Repository:版本库,就是本地仓库,保存所有的版本数据,其中HEAD指向最新的版本 Remote:远程仓库,托管代码的服务器,我们可以在一些网站(GitHub,Gitee等)建立远程仓库来托管我们的代码 工作流程Git的工作流程一般是这样的: 1.在工作区中编写,修改代码 2.通过git add命令将需要跟踪的文件放入暂存区 3.通过git commit命令将暂存区中的文件存入本地仓库,此时会创建一条Git提交记录 4.通过git push命令将本地仓库的版本推送到远程仓库保存 因此,Git管理的文件一般有三种状态:已修改(modified),已暂存(staged),已提交(committed) 2.基本命令1.工作区命令 初始化仓库1git init 添加文件追踪通过git add <filename>命令可以将一个文件存入暂存区1git add <fileName> 也可以通过git add .命令将工作区中的文件全部存入暂存区 查看工作区文件状态通过git status可以查看当前工作区的文件状态 撤销提交通过git reset命令可以撤销存储在暂存区中的文件1234// 撤销一个文件git reset <fileName>// 撤销所有文件git reset 删除存储库中未提交的更改1git reset --hard HEAD 2.暂存区命令 将暂存区的文件提交到本地仓库1git commit -m <commitLog> 撤销上次的提交1234// 撤销提交至工作区git reset HEAD^// 撤销提交至暂存区git reset --soft HEAD^ 版本回滚1git reset --hard <logName> 3.本地仓库命令 查看所有远程仓库1git remote -v 添加一个远程仓库1git remote add <remoteName> <remoteUrl> 删除一个远程仓库1git remote rm <remoteName> 修改一个远程仓库名1git remote rename <oldName> <newName> 将本地仓库代码推送到远程仓库1git push <remoteNmae> <branchName> 可以加上-f(--force)命令强制推送可以加上-u命令默认追踪一个分支 克隆一个仓库到本地1git clone <remoteUrl> 更新远程仓库代码到本地仓库1git fetch 更新远程仓库代码到本地仓库并合并到工作区1git pull(git fetch + git merge) 3.分支命令 查看本地分支1git branch 查看远程分支1git branch -r 查看所有分支1git branch -a 新建一个分支1git branch <branchName> 重命名分支1git branch -m oldName newName 切换到指定分支1git checkout <branchName> 新建并切换到该分支1git checkout -b <branchName> 删除本地分支(不能在该分支下)1git branch -d <branchName> 强制删除本地分支(不能在该分支下)1git branch -D <branchName> 删除远程分支1git push -d <remoteName> <branchName> 合并分支1git merge <branchName> 分支变基1git rebase <branchName> 1.分支变基分支变基可以将当前分支的起点变更到目标分支的终点,一般用于同分支拉取代码时的合并 变基 1git rebase <branchName> 拉取远程代码时同时变基 1git pull --rebase <remoteName>/<branchName>(git fetch + git rebase <remoteName>/<branchName>) 冲突解决 放弃合并,回退到rebase操作之前的状态,之前的提交不会丢弃 1git rebase --abort 将引起冲突的commits直接丢弃(慎用) 1git rebase --skip 解决冲突如果在拉取代码时产生冲突,我们手动解决冲突后,先执行git add和git commit命令提交冲突的文件,然后执行git rebase --continue命令进行分支的合并 1git rebase --continue cherry-pick 用于合并指定的commit到当前分支 1git cherry-pick <commit id> 4.存储 git stash: 将当前工作区中所有已追踪的文件存入堆栈 git stash save :将当前工作区中所有已追踪的文件存入堆栈,并可以写上备注 git stash save -u:将当前工作区中所有文件存入堆栈(包括未追踪文件) git stash list:查询堆栈中的存储 git stash show:查看堆栈中最新保存的stash和当前目录的差异 git stash pop:将堆栈中最新的存储弹出至工作区,并且删除堆栈中对应的存储 git stash apply:将堆栈中最新的存储弹出至工作区,该操作不会删除堆栈中对应的存储 git stash drop :移除堆栈中指定的存储 git stash clear:清除堆栈中的所有存储","link":"/Git%E5%B8%B8%E7%94%A8%E5%91%BD%E4%BB%A4/"},{"title":"适配器模式","text":"适配器模式主要用于解决开发中接口之间的不兼容问题,比如说axios,它可以同时在浏览器环境和node环境中使用,且用户在调用它的api时,入参是相同的,这里使用的就是适配器模式来兼容简单来说,适配器模式就是对外统一入参,出参和规则。 例子现代浏览器提供了Fetch API来简化Ajax请求,比如说我们封装了一个请求类 12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758export default class HttpUtils { // get方法 static get(url) { return new Promise((resolve, reject) => { // 调用fetch fetch(url) .then(response => response.json()) .then(result => { resolve(result) }) .catch(error => { reject(error) }) }) } // post方法,data以object形式传入 static post(url, data) { return new Promise((resolve, reject) => { // 调用fetch fetch(url, { method: 'POST', headers: { Accept: 'application/json', 'Content-Type': 'application/x-www-form-urlencoded' }, // 将object类型的数据格式化为合法的body参数 body: this.changeData(data) }) .then(response => response.json()) .then(result => { resolve(result) }) .catch(error => { reject(error) }) }) } // body请求体的格式化方法 static changeData(obj) { var prop, str = '' var i = 0 for (prop in obj) { if (!prop) { return } if (i == 0) { str += prop + '=' + obj[prop] } else { str += '&' + prop + '=' + obj[prop] } i++ } return str }} 如果此时项目中存在使用XMLHttpRequest来请求ajax的接口,我们可以封装一个适配器,用来将所有XHR请求转化为新的Fetch请求 12345678910111213141516171819202122232425// Ajax适配器函数,入参与旧接口保持一致async function AjaxAdapter(type, url, data, success, failed) { const type = type.toUpperCase() let result try { // 实际的请求全部由新接口发起 if(type === 'GET') { result = await HttpUtils.get(url) || {} } else if(type === 'POST') { result = await HttpUtils.post(url, data) || {} } // 假设请求成功对应的状态码是1 result.statusCode === 1 && success ? success(result) : failed(result.statusCode) } catch(error) { // 捕捉网络错误 if(failed){ failed(error.statusCode); } }}// 用适配器适配旧的Ajax方法async function Ajax(type, url, data, success, failed) { await AjaxAdapter(type, url, data, success, failed)}","link":"/%E9%80%82%E9%85%8D%E5%99%A8%E6%A8%A1%E5%BC%8F/"},{"title":"面试小结(20230406)","text":"前言准备换工作了,毕业以来一直在一家中小型企业写了一年多的React,现在无论是状态还是学习的动力都不是最巅峰的时候,是时候准备开启新的篇章了 面试项目相关首先是常规的自我介绍,接下来就是聊项目,这边印象比较深的是聊到了一个性能优化的场景 我的回答是: 观察到页面比较卡顿,然后使用Chrome的火焰图定位到了是用于处理图表数据到两个方法出现了性能问题,之后具体的优化方法是:处理X轴的方法中改用了Set集合来存取,处理Y轴数据的方法减少里面的for循环,降低时间复杂度来达到优化的效果 答的不好,听到后面面试官还问了几个相关的问题,我归纳了一下,觉得可以这样回答: 首先这是一个表格模块,里面有一列使用的是Echarts图表来展示数据,表格采用滚动分页,每页请求10条表格数据,其中每个图表数据平均是5万条左右。当时观察到这个页面会比较卡顿,于是使用Chrome火焰图去分析了一下这个模块的事件循环大概的执行时间,发现了一个长任务,点击这个任务的调用树去定位到了一个副作用里面的两个处理数据的方法执行时间非常长,于是就针对这两个方法进行了一个优化。 首先是X轴数据的处理方法,它的功能主要是从原始数据里面获取日期数据,原本用的是数组,最后给它改为Set结构,Set底层的实现会相对纯粹一些,性能较好。Y轴数据处理的方法是用于补全数据的,里面的循环比较多,这边着手的重点就是减少里面的循环时间,降低时间复杂度 最后还对这个图表组件进行了懒加载处理 这是当时进行性能处理的方案,如果是现在去处理的话,应该还有更多的优化空间,例如开启多线程渲染,例如利用事件循环机制中的requestAnimationFrame或者requestIdentCallback来调度任务的执行 JavaScript基础题高阶函数 高阶函数是指至少满足下列条件之一的函数: 可以被当做参数传递 可以被当做返回值输出 应用: 作为回调函数 闭包 防抖 节流 柯里化 去柯里化 偏函数 Trunk函数 … let和const的区别 let和const都能形成块作用域 const声明时必须初始化变量的值,let则不用 let声明的变量时可以改变的,const声明的基础数据类型不可改变,引用数据类型引用地址不可变,里面的值时可以变的 箭头函数和普通函数的区别 普通函数可以用作构造函数,被new实例化,箭头函数则不可以 普通函数的this是动态变化的,箭头函数的this在声明会跟着上下文的this,并且不可被call,apply,bind这些方法所改变 深拷贝和浅拷贝 浅拷贝和深拷贝只针对引用数据类型 如果引用数据类型的属性时基本类型,那拷贝的就是基本类型的值,如果是引用类型,拷贝的就是内存地址,即浅拷贝只拷贝一层,深层次的引用类型则共享内存地址 深拷贝就是层层拷贝,会开辟一个新的内存空间,将数据中属性全部都拷贝一遍 实现浅拷贝的方法: Object.assign slice() concat() 拓展运算符 实现深拷贝的方法: JSON.stringify() 弊端:会忽略undefined、symbol和函数 _.cloneDeep() jquery.extend() 手写循环递归 1234567891011121314151617181920function deepClone(obj, hash = new WeakMap()) { if(obj === null || obj === undefined) return obj; if(obj instanceof Date) return new Date(obj); if(obj instanceof RegExp) return new RegExp(obj); // 可能是普通的值或者函数 if(typeof obj !== 'object') return obj; // 是对象的话就要进行深拷贝 if(hash.get(obj)) return hash.get(obj); let cloneObj = new obj.constructor(); hash.set(obj, cloneObj); for(let key in obj) { if(obj.hasOwnProperty(key)){ // 循环递归 cloneObj[key] = deepClone(obj[key], hash); } } return cloneObj;} LocalStorage、SessionStorage和cookie的区别 存储大小:cookie数据大小不能超过4k,sessionStorage和localStorage虽然也有存储大小的限制,但比cookie大得多,可以达到5M左右 有效时间:localStorage存储持久数据,浏览器关闭后数据不会丢失,除非你主动去删除数据;sessionStorage数据在当前浏览器窗口关闭后自动删除;cookie设置的cookie过期时间之前一直有效 数据与服务之间的交互方式,cookie的数据会自动传递到服务器,服务端也可以写cookie到客户端;sessionStorage和localStorage不会自动把数据发给服务器,仅存在本地 map和forEach的区别map遍历数组,然后将数组里面的数据通过一定的操作再返回,会形成一个新数组,forEach就是单纯的遍历数组方法,且遍历时不能被break,continue等中断。 事件流标准事件流分为了三个阶段,事件捕获,目标阶段和事件冒泡阶段 阻止事件冒泡:event.stoppropagation(),Vue可以用.stop事件修饰符 Vue面试题 Vue生命周期有哪些 NextTick Vue响应式的原理 $set是用来干嘛的 Router有哪些模式,传参方式都有哪些,如何设置参数 介绍一下VueX key的作用 computed和watch的区别 总结整体来说这次的面试准备的不是很充足,还有很多可以改进的地方,很多基础问题答的不是很好,继续改进吧","link":"/%E9%9D%A2%E8%AF%95%E5%B0%8F%E7%BB%93%EF%BC%8820230406%EF%BC%89/"},{"title":"CSS随记","text":"1.FlexFlex布局是将元素进行水平或者垂直排列的一维布局方案,通过设置display为flex或inline-flex进行开启。采用Flex布局的容器,默认存在两根轴,水平的主轴和垂直的交叉轴,容器内的子元素默认成为容器的成员,称为项目。主轴的开始位置与边框的交叉点称为main start,结束位置称为end start,交叉轴的开始位置叫cross start,结束位置叫cross end Flex项目默认沿主轴排列。单个项目占据的主轴空间叫做main size,占据的交叉轴空间叫做cross size。 Flex属性分为两部分,一部分作于于容器,另一部分作于于容器内的项目 容器属性 属性 功能 flex-direction 决定主轴的方向 `row flex-wrap 决定主轴上的项目排列不下时,是否换行排列 `nowrap flex-flow flex-direction和flex-wrap的简写形式。默认值:row nowrap justify-content 定义了项目在主轴上的对齐方式和额外空间的分配方式 `flex-start align-item 决定项目在交叉轴上的对齐方式 `stretch align-content 定义了多根轴线的对齐方式,设置了flex-wrap属性为wrap后align-content属性才能生效 `stretch 项目属性 属性 功能 order 定义项目的排列顺序,数值越小,排列越靠前,默认为0,可以是负数 flex-grow 扩展规则,规定flex容器中剩余的空间应该拿出多少分配给项目,默认为0,最大值是1,超过1按照1来扩展 flex-shrink 规定了flex项目的收缩规则,flex项目仅在默认宽度之和大于容器的时候才会发生收缩,默认值是1 flex-basis 指定了子项在容器主轴方向上的初始大小,优先级高于自身的宽度width,默认值是auto flex 该属性是flex-grow flex-shrink flex-basis的简写,默认值是0 1 auto,后两个值可选 align-self 该属性用于设置单个项目在交叉轴的对齐方式,可覆盖align-item属性 `stretch 2.CSS选择器 选择器 格式 优先级权重 id选择器 #id 100 类选择器 #class 10 伪类选择器 li:last-child 10 属性选择器 a[href=”aaa”] 10 标签选择器 div 1 伪元素选择器 li::after 1 相邻兄弟选择器 h1+p 0 子元素选择器 ul > li 0 后代选择器 li a 0 通配符选择器 * 0 对于样式的优先级: !important:优先级最高 内联样式:1000 id选择器:100 类,伪类,属性选择器:10 元素选择器,伪元素选择器:1 通配符选择器,后代选择器,兄弟选择器:0 3.规则 @namespace:告诉CSS引擎必须考虑XML命名空间 @media:媒体查询 @page:描述打印文档时布局的变化 @font-face:描述将下载的外部字体 @keyframes:描述CSS动画关键帧 @import:用于告诉CSS引擎引入一个外部样式表 link和@import的区别 link是HTML标签,除了能导入CSS外,还可以导入其他资源,比如图片,脚本和字体等;而@import是CSS语法,只能用来导入CSS link导入的样式会在页面加载时同时加载,@import导入的样式需要等页面加载完成后再加载 link没有兼容性问题,@import不兼容ie5以下 link可以通过js操作访问 4.继承性 可继承属性:font-family font-style font-size font-weight color 不可继承属性:weigth height margin padding 5.清除浮动浮动的元素会脱离文档流,导致父元素高度塌陷 通过BFC清除浮动 123.parent { overflow: hidden;} 通过clear清除浮动 12345678.clearfix { zoom: 1;}.clearfix::after { content: ""; display: block; clear: both;} 6.消除浏览器默认样式12345678910111213141516171819202122232425262728293031323334353637383940414243html, body, div, span, applet, object, iframe,h1, h2, h3, h4, h5, h6, p, blockquote, pre,a, abbr, acronym, address, big, cite, code,del, dfn, em, img, ins, kbd, q, s, samp,small, strike, strong, sub, sup, tt, var,b, u, i, center,dl, dt, dd, ol, ul, li,fieldset, form, label, legend,table, caption, tbody, tfoot, thead, tr, th, td,article, aside, canvas, details, embed,figure, figcaption, footer, header, hgroup,menu, nav, output, ruby, section, summary,time, mark, audio, video { margin: 0; padding: 0; border: 0; font-size: 100%; font: inherit; vertical-align: baseline;}/* HTML5 display-role reset for older browsers */article, aside, details, figcaption, figure,footer, header, hgroup, menu, nav, section { display: block;}body { line-height: 1;}ol, ul { list-style: none;}blockquote, q { quotes: none;}blockquote:before, blockquote:after,q:before, q:after { content: ''; content: none;}table { border-collapse: collapse; border-spacing: 0;} 7.长文本处理 字符超出部分换行 1overflow-wrap: break-word; 字符超出部分使用连接字符 1hyphens: auto; 单行文本超出省略 123overflow: hidden;text-overflow: ellipsis;white-space: nowrap; 多行文本超出省略 12345overflow: hidden;text-overflow: ellipsis;display: -webkit-box-;-webkit-line-clamp: 2;-webkit-box-orient: vertical; 8.line-height和height的区别height就是元素的高度值 line-height是每一行文字的高度,如果文字换行,则整个格子高度会增大 9.BFC块级格式化上下文,它是一个独立容器,决定了元素如何对其内容进行定位,以及与其他元素的关系和相互作用。 创建规则: 根元素 float不是none的元素 绝对定位的元素(position为absolute或fixed) display取值为inline-block table-cell table-caption inline-flex之一的元素 overflow不是visible的元素 作用: 清除浮动 阻止父子元素的margin折叠 10.居中方式单行的文本、inline 或 inline-block 元素 水平居中 1text-align: center 垂直居中 123456789.single-line { padding-top: 10px; padding-bottom: 10px;}// 或.single-line { height: 100px; line-height: 100px;} 固定宽高的块级盒子 absolute+负margin 1234567891011.parent { position: relative;}.child { width: 100px; height: 100px; position: absolute; left: 50%; top: 50%; margin: -50px 0 0 -50px} absolute + margin auto 12345678910111213.parent { position: relative;}.child { width: 100px; height: 100px; position: absolute; left: 0; top: 0; right: 0; bottom: 0; margin: auto;} absolute + calc 12345678910.parent { position: relative;}.child { width: 100px; height: 100px; position: absolute; left: calc(50% - 50px); top: calc(50% - 50px)} 不固定宽高的块级盒子 absolute + transform 123456789.praent { position: relative;}.child { position: absolute; left: 50%; top: 50%; transform: translate(-50%,-50%)} line-height + vertical-align 123456789.parent { line-height: 150px; text-aligin: center;}.child { display: inline-block; line-height: initial; vertical-align: middle;} writing-mode 12345678910111213.parent { writing-mode: vertical-lr; text-align: center;}.middle { display: inline-block; writing-mode: horizontal-tb; width: 100%}.child { display: inline-block;} table-cell 12345678.parent { display: table-cell; vertical-aligin:middle; text-align: center;}.child { display: inline-block;} flex 12345.parent { display: flex; justify-content: center; align-items: center} grid 1234567.parent { display: grid;}.child { justify-self: center; align-self: center;} 11.常用布局双栏布局(边栏定宽主栏自适应) float+overflow(BFC原理) 1234567aside { float: left; width: 200px}main { overflow: hidden;} float+margin 1234567aside { float: left; width: 200px;}main { margin-left:200px} flex 123456789layout { display: flex;}aside { width:200px}main { flex-grow: 1} grid 1234layout { display: grid; grid-template-columns: 200px auto;} 三栏布局(两侧定宽主栏自适应) 圣杯布局 123456789101112131415161718192021layout { padding: 0 200px;}main { float: left; width: 100%;}aside { float: left; width: 200px;}left { position: relative; left: -200px; margin-left: -100%;}right { position: relative; right: -200px; margin-left: -200px} 双飞翼布局 1234567891011121314151617main { float: left; width: 100%;}.inner { margin: 0 200px;}aside { float: left; width: 200px;}.left { margin-left: -100%;}.right { margin-left: -200px;} float+overflow 123456789101112aside { width: 200px;}left { float: left;}right { float: right;}main { overflow: hidden;} flex 123456789layout { display: flex;}aside { width: 200px;}main { flex-grow: 1;} grid 1234layout { display: grid; grid-template-columns: 200px auto 200px} 12.Grid给<div>这类块状元素元素设置display:grid或者给<span>这类内联元素设置display:inline-grid,Grid布局即创建 此时该div就是grid容器,其子元素称为grid子项 容器属性 子项属性 grid-template-columns grid-column-start grid-template-rows grid-column-end grid-template-areas grid-row-start grid-template grid-row-end grid-column-gap grid-column grid-gap grid-row justify-items grid-area align-items justify-self place-items align-self justify-content place-self align-content place-content grid-auto-columns grid-auto-rows grid-auto-flow grid 13.nth-of-type和nth-child的区别 nth-of-type::nth-of-type(n) 选择器匹配属于父元素的特定类型的第 N 个子元素的每个元素。n 可以是数字、关键词或公式 12345678910111213141516<style>div>p:nth-of-type(2){ color:red;}</style><div> <p>我是第1个段落</p> <p>我是第2个段落</p><!--符合条件:1、是特定元素类型<p>,2、是父元素<div>的第二个<p>元素。这里被选择,会变成红色--> <p>我是第3个段落</p></div><div> <p>我是第1个段落</p> <blockquote>第1个引用</blockquote> <p>我是第2个段落</p><!--符合条件:1、是特定元素类型<p>,2、是父元素<div>的第二个<p>元素。这里被选择,会变成红色--> <p>我是第3个段落</p></div> nth-child::nth-child(n) 选择器匹配属于其父元素的第 N 个子元素,不论元素的类型。n 可以是数字、关键词或公式。 注意:如果第 N 个子元素与选择的元素类型不同则样式无效! 123456789101112131415<style>div>p:nth-child(2){ color:red;}</style><div> <p>我是第1个段落</p> <p>我是第2个段落</p><!--符合条件:1、是<p>元素,2、父元素<div>的第二个元素。这里被选择,会变成红色。--> <p>我是第3个段落</p></div><div> <p>我是第1个段落</p> <span>我是第1个文本</span><!--不符合条件:不是<p>元素,没有被选择--> <p>我是第2个段落</p></div> 毛玻璃特效可以使用 CSS 中的 backdrop-filter 属性来实现毛玻璃特效:backdrop-filter 属性可以为一个元素后面区域添加图形效果(如模糊或颜色偏移)。因为它适用于元素_背后_的所有元素,为了看到效果,必须使元素或其背景至少部分透明。 123.login { backdrop-filter: blur(5px);} 将文本设为大写或小写大写或小写字母可以不必在 HTML中设置。可以在 CSS 中使用text-transform属性来强制任何文本为大写或小写。text-transform 属性专门用于控制文本的大小写,当值为uppercase时会将文本转为大写,当值为capitalize时会将文本转化为小写,当值为capitalize时会将每个单词以大写字母开头。 123456789/* 大写 */.upper { text-transform: uppercase;}/* 小写 */.lower { text-transform: lowercase;} 实现首字母下沉可以使用::first-letter来实现文本首字母的下沉::first-letter选择器用来指定元素第一个字母的样式,它仅适用于在块级元素中。 1234p.texts:first-letter { font-size: 200%; color: #8A2BE2;} 图片文字环绕shape-outside 是一个允许设置形状的 CSS 属性。它还有助于定义文本流动的区域: 12345.any-shape { width: 300px; float: left; shape-outside: circle(50%);} 背景混合模式使用 background-blend-mode 来实现元素背景的混合: 123456789101112131415.blend-1 { background-image: url(https://duomly.nyc3.digitaloceanspaces.com/articles/coding/alps-lake.jpg); width: 100vw; height: 500px; background-size: cover;}.blend-2 { background-image: url(https://duomly.nyc3.digitaloceanspaces.com/articles/coding/alps-lake.jpg); width: 100vw; height: 500px; background-color: #20126f; background-size: cover; background-blend-mode: overlay;}","link":"/CSS%E9%9A%8F%E8%AE%B0/"},{"title":"JavaScript中单继承","text":"ES5 继承ES5中没有类的概念,通常通过声明一个构造函数来模拟类 1234567891011121314151617181920function Person(name) { this.name = name; this.sayName1 = function () { console.log(this.name + '在工作'); }}// 原型链上的方法和属性会被多个实例共享,构造函数中的则不会Person.prototype.sayName2 = function () { console.log(this.name + '在学习');}// 静态方法Person.sayName3 = () => { console.log(this.name + '在运动')}var person = new Person('Tom');person.sayName1(); // Tom在工作person.sayName2(); // Tom在学习 原型方法和实例方法的区别: 写在原型中的方法可以被所有的实例共享, 实例化的时候不会在实例内存中再复制一份,占有的内存消耗少。 js中每个函数都有一个prototype属性,这个属性指向一个对象(所有属性的集合:默认constructor属性,值指向这个函数本身。) 每个原型对象都属于对象,所以它也有自己的原型,而它自己的原型对象又有自己的原型,所以就形成了原型链。 一个对象的隐式原型指向构造这个对象的构造函数的显式原型,所以这个对象可以访问构造函数的属性和方法。(new一个实例) js的继承也就是通过原型链来实现的,当访问一个对象的属性,如果这个对象本身不存在,则沿着proto依次往上查找,如果有则返回值,没有则一直到查到Object.prototype的proto的值为null. 继承ES5实现继承的方式有原型链继承,构造继承,实例继承,拷贝继承,组合继承,寄生组合继承这六种 原型链继承 优点 实例是子类的实例,也是父类的实例 可以调用父类的实例属性和方法,也可以调用父类原型链上的属性和方法 缺点 子类无法在构造器中新增属性或者方法,必须要在new Person()之后 在子类实例化时,无法向父类传参 父类原型对象的所有属性被所有实例共享 1234567// 原型链继承function Cat() { }Cat.prototype = new Person('Pt'); // 只能在这里向父类传参,或者下面代码那样var cat = new Cat();cat.sayName1(); 构造继承 优点 解决了原型链继承中,子类实例共享父类引用属性的问题 在创建子类实例时,可以向父类传参 缺点 实例并不是父类的实例,只是子类的实例 只能继承父类构造函数中的属性和方法,不能继承父类原型链中的属性和方法 无法实现函数的复用,每个子类都有父类实例函数的副本,影响性能 1234567function Cat(name) { Person.call(this, name);}var cat = new Cat('Tom');cat.sayName1(); 实例继承 缺点 无法实现多继承 子类实例化出来的对象是父类类型,不是子类类型 123456789// 实例继承function Cat(name) { var instance = new Person(name); return instance;}var cat = new Cat('Tom');cat.sayName1() 组合继承 优点 弥补了构造继承的缺陷,可以继承实例的属性/方法,也可以继承原型上的属性/方法 既是子类的实例,也是父类的实例 可以向父类传参 函数可以复用 缺点 调用了两次父类的构造函数,生成了两份实例 1234567891011121314151617function Cat(name, age) { Person.call(this, name); this.age = age;}Cat.prototype = new Person();var cat = new Cat('Tom', 18);console.log(cat.name);cat.sayName1();cat.sayName2();console.log(cat.age); 寄生组合继承1234567891011function Cat(name) { Person.call(this, name);}var Temp = Object.create(Person.prototype); // 创建对象,创建父类原型的一个副本Temp.constructor = Cat; // 增强对象,弥补因重写原型而失去的默认的constructor 属性Cat.prototype = Temp; // 指定对象,将新创建的对象赋值给子类的原型var cat = new Cat('Tom');cat.sayName1() new的时候都做了什么? 创建一个新对象 把这个新对象的__proto__属性指向你要new 的那个对象的prototype 让构造函数里面的this指向新的对象,然后执行构造函数 返回这个新对象 123456function _new(constructor, ...args) { const obj = Object.create(constructor.prototype); const result = constructor.call(obj, ...args); return result instanceof Object ? result : obj;} ES6 ClassES6提供了更接近传统的写法,基本上可以看作只是一个语法糖,它的绝大部分功能,ES5都可以做到 12345678910class Person { constructor(name) { this.name = name; } sayName() { console.log(this.name + '在学习') }}const p = new Person('jiacheng') 构造函数上的prototype属性,在ES6类上面继续存在,事实上,类的所有方法都是定义在类的prototype属性上面的 1234567891011121314151617181920class Point { constructor() { // ... } toString() { // ... } toValue() { // ... }}// 等同于Point.prototype = { constructor() {}, toString() {}, toValue() {},}; 因此,在类的实例上面调用方法,其实就是调用原型上的方法 12345class B{}const b = new B();b.constructor === B.prototype.constructor // true 所以,使用Object.assin()方法可以很方便地一次向类添加多个方法 12345678910class Point { constructor(){ // ... }}Object.assign(Point.prototype, { toString(){}, toValue(){}}); prototype对象的constructor属性,直接指向类本身,这与ES5的行为是一致的 1Point.prototype.constructor === Point // true 另外,类内部所有定义的方法,都是不可枚举的 1234567891011121314class Point { constructor(x, y) { // ... } toString() { // ... }}Object.keys(Point.prototype)// []Object.getOwnPropertyNames(Point.prototype)// ["constructor","toString"] constructorconstructor方法是类的默认方法,通过new命令生成对象实例时,自动调用该方法,一个类即使没有显示定义constructor,也会被默认添加一个空的constructor 123456class Person{}// 相当于class Person { constructor(){}} constructor默认返回实例对象,完全可以指定返回另一个对象 1234567class Person { constructor() { return Object.create(null) }}new Person instanceof Person // false 类必须使用new调用,否则会直接报错 12345class Person {}Person()// TypeError: Class constructor Foo cannot be invoked without 'new' 类的实例直接通过new命令生成一个类的实例,与ES5不同的是,直接调用类会报错 123456789class Point { // ...}// 报错var point = Point(2, 3);// 正确var point = new Point(2, 3); 与 ES5 一样,实例的属性除非显式定义在其本身(即定义在this对象上),否则都是定义在原型上(即定义在class上) 12345678910111213141516171819202122//定义类class Point { constructor(x, y) { this.x = x; this.y = y; } toString() { return '(' + this.x + ', ' + this.y + ')'; }}var point = new Point(2, 3);point.toString() // (2, 3)point.hasOwnProperty('x') // truepoint.hasOwnProperty('y') // truepoint.hasOwnProperty('toString') // falsepoint.__proto__.hasOwnProperty('toString') // true 类的所有实例共享一个原型对象 12345var p1 = new Point(2,3);var p2 = new Point(3,2);//truep1.__proto__ === p2.__proto__ 这也意味着,可以通过实例的__proto__属性为“类”添加方法。 __proto__ 并不是语言本身的特性,这是各大厂商具体实现时添加的私有属性,虽然目前很多现代浏览器的 JS 引擎中都提供了这个私有属性,但依旧不建议在生产中使用该属性,避免对环境产生依赖。生产环境中,我们可以使用 Object.getPrototypeOf 方法来获取实例对象的原型,然后再来为原型添加方法/属性。 12345678910var p1 = new Point(2,3);var p2 = new Point(3,2);p1.__proto__.printName = function () { return 'Oops' };p1.printName() // "Oops"p2.printName() // "Oops"var p3 = new Point(4,2);p3.printName() // "Oops" 上面代码在p1的原型上添加了一个printName()方法,由于p1的原型就是p2的原型,因此p2也可以调用这个方法。而且,此后新建的实例p3也可以调用这个方法。这意味着,使用实例的__proto__属性改写原型,必须相当谨慎,不推荐使用,因为这会改变“类”的原始定义,影响到所有实例。 getter和setter与 ES5 一样,在“类”的内部可以使用get和set关键字,对某个属性设置存值函数和取值函数,拦截该属性的存取行为。 12345678910111213141516171819class MyClass { constructor() { // ... } get prop() { return 'getter'; } set prop(value) { console.log('setter: '+value); }}let inst = new MyClass();inst.prop = 123;// setter: 123inst.prop// 'getter' 存值函数和取值函数是设置在属性的 Descriptor 对象上的。 1234567891011121314151617181920class CustomHTMLElement { constructor(element) { this.element = element; } get html() { return this.element.innerHTML; } set html(value) { this.element.innerHTML = value; }}var descriptor = Object.getOwnPropertyDescriptor( CustomHTMLElement.prototype, "html");"get" in descriptor // true"set" in descriptor // true 注意点 严格模式 类和模块的内部,默认就是严格模式,不需要使用’use strict’指定运行模式 不存在提升 类不存在变量提升 Generator方法 如果某个方法前加上*,就表示该方法是一个Generator函数 12345678910111213141516class Foo { constructor(...args) { this.args = args; } * [Symbol.iterator]() { for (let arg of this.args) { yield arg; } }}for (let x of new Foo('hello', 'world')) { console.log(x);}// hello// world 上面代码中,Foo类的Symbol.iterator方法前有一个星号,表示该方法是一个 Generator 函数。Symbol.iterator方法返回一个Foo类的默认遍历器,for...of循环会自动调用这个遍历器。 this的指向 类方法内部如果含有this,它默认指向类的实例,但该方法无法单独使用 12345678910111213class Logger { printName(name = 'there') { this.print(`Hello ${name}`); } print(text) { console.log(text); }}const logger = new Logger();const { printName } = logger;printName(); // TypeError: Cannot read property 'print' of undefined 上面代码中,因为printName方法中的this,默认指向Logger类的实例,但如果将这个方法单独提取出来使用,this会指向该方法运行时所在的环境,而由于class内部是严格模式,所以这时候this直接指向undefined,导致报错解决方法: 在构造方法中绑定this,这样就不会找不到print方法了。 12345678910111213class Logger { constructor() { this.printName = this.printName.bind(this); } printName(name = 'there') { this.print(`Hello ${name}`); } print(text) { console.log(text); }} 使用箭头函数箭头函数内部的`this`总是指向定义时所在的对象。上面代码中,箭头函数位于构造函数内部,它的定义生效的时候,是在构造函数执行的时候。这时,箭头函数所在的运行环境,肯定是实例对象,所以`this`会总是指向实例对象 123456789101112131415class Person { printName = () => { this.print() } print() { console.log('111'); }}var p = new Person();var { printName } = p;printName() 静态方法类相当于实例的原型,所有在类中定义的方法,都会被实例继承,如果在一个方法前面加上static关键字,就表示该方法不会被实例继承,而是通过类来调用,这被称为静态方法 1234567891011class Foo { static classMethod() { return 'hello'; }}Foo.classMethod() // 'hello'var foo = new Foo();foo.classMethod()// TypeError: foo.classMethod is not a function 如果静态方法中包含this关键字,这个this指向的是类,而不是实例 12345678910111213class Foo { static bar() { this.baz(); } static baz() { console.log('hello'); } baz() { console.log('world'); }}Foo.bar() // hello 父类的静态方法,可以被子类继承 静态方法也可以从super对象上调用 12345678910111213class Foo { static classMethod() { return 'hello'; }}class Bar extends Foo { static classMethod() { return super.classMethod() + ', too'; }}Bar.classMethod() // "hello, too" 静态属性静态属性是指class本身的属性,而不是定义在实例对象上的属性 123456789101112class Foo {}Foo.prop = 1;// 或者class Foo{ static name = 'jiacheng'}Foo.name; // jiacheng 继承class通过extends关键字来实现继承 123class Person{}class ColorPoint extends Person {} super super关键字表示父类的构造函数,用来新建父类的this对象 子类必须在constructor中调用super方法,否则会报错 12345678910class ColorPoint extends Point { constructor(x, y, color) { super(x, y); // 调用父类的constructor(x, y) this.color = color; } toString() { return this.color + ' ' + super.toString(); // 调用父类的toString() }} ES6继承和ES5继承的区别 ES5里的构造函数是一个普通函数,可以使用new调用,也可以直接调用,且存在变量提升。ES6的class必须使用new操作符调用,且不存在变量提升 ES5子类的原型是指向Function.prototype,而ES6子类的原型是指向父类的 ES5的原型方法和静态方法是可枚举的,而class的默认不可枚举,但可以使用Object.getOwnPropertyNames方法获取 ES5的继承,实质是先创造一个子类实例对象的this,然后再执行父类构造函数给它添加实例方法和属性(不执行也无所谓);ES6的继承机制则相反,先将父类的属性和方法,加到一个空对象上,然后再将该对象作为子类的实例,即”继承在前,实例在后”","link":"/JavaScript%E4%B8%AD%E5%8D%95%E7%BB%A7%E6%89%BF/"},{"title":"accept属性","text":"accept限制上传文件类型1<input type="file" accept="" /> accept的值为: 格式 accept类型 *.3gpp audio/3gpp, video/3gpp *.ac3 audio/ac3 *.asf allpication/vnd.ms-asf *.au audio/basic *.css text/css *.csv text/csv *.doc application/msword *.dot application/msword *.dtd application/xml-dtd *.dwg image/vnd.dwg *.dxf image/vnd.dxf *.gif image/gif *.htm text/html *.html text/html *.jp2 image/jp2 *.jpe image/jpeg *.jpeg image/jpeg *.jpg image/jpeg *.js text/javascript, application/javascript *.json application/json *.mp2 audio/mpeg, video/mpeg *.mp3 audio/mpeg *.mp4 audio/mp4, video/mp4 *.mpeg video/mpeg *.mpg video/mpeg *.mpp application/vnd.ms-project *.ogg application/ogg, audio/ogg *.pdf application/pdf *.png image/png *.pot application/vnd.ms-powerpoint *.pps application/vnd.ms-powerpoint *.ppt application/vnd.ms-powerpoint *.rtf application/rtf, text/rtf *.svf image/vnd.svf *.tif image/tiff *.tiff image/tiff *.txt text/plain *.wdb application/vnd.ms-works *.wps application/vnd.ms-works *.xhtml application/xhtml+xml *.xlc application/vnd.ms-excel *.xlm application/vnd.ms-excel *.xls application/vnd.ms-excel *.xlt application/vnd.ms-excel *.xlw application/vnd.ms-excel *.xml text/xml, application/xml *.zip aplication/zip .xlsx application/vnd.openxmlformats-officedocument.spreadsheetml.sheet .xltx application/vnd.openxmlformats-officedocument.spreadsheetml.template .potx application/vnd.openxmlformats-officedocument.presentationml.template .ppsx application/vnd.openxmlformats-officedocument.presentationml.slideshow .pptx application/vnd.openxmlformats-officedocument.presentationml.presentation .sldx application/vnd.openxmlformats-officedocument.presentationml.slide .docx application/vnd.openxmlformats-officedocument.wordprocessingml.document .dotx application/vnd.openxmlformats-officedocument.wordprocessingml.template .xlsm application/vnd.ms-excel.addin.macroEnabled.12 .xlsb application/vnd.ms-excel.sheet.binary.macroEnabled.12","link":"/accept%E7%B1%BB%E5%9E%8B/"},{"title":"pointer-events","text":"pointer-events CSS 属性指定在什么情况下 (如果有) 某个特定的图形元素可以成为鼠标事件的target;pointer-events 属性值 auto | none | inherit => HTML visiblePainted | visibleFill | visibleStroke | visible | painted | fill | stroke | all => SVG 针对HTML元素 none:该元素永远不会成为鼠标事件的 target。但是,当其后代元素的 pointer-events 属性指定其他值时,鼠标事件可以指向后代元素,在这种情况下,鼠标事件将在捕获或冒泡阶段触发父元素的事件侦听器 (鼠标的动作将不能被该元素及其子元素所捕获,但是能够被其父元素所捕获)。 auto:默认值,表示指针事件已启用;此时元素会响应指针事件,阻止这些事件在其下面的元素上触发。对于 SVG 内容,该值与 visiblePainted 效果相同。 inherit:将使用 pointer-events 元素的父级的值。 注意:使用pointer-events来阻止元素成为鼠标事件目标不一定意味着元素上的事件侦听器永远不会触发。如果元素后代明确指定了pointer-events属性并允许其成为鼠标事件的目标。","link":"/pointer-events/"},{"title":"抽象工厂模式","text":"抽象工厂模式是开放封闭原则应用的典型,即:对拓展开发,对修改封闭。准确来说,软件实体(类、模块、函数)可以扩展,但不可修改。例如我们要一个生产手机的工厂,首先需要创建一个手机抽象类,用来约束手机流水线通用能力 12345678910class MobilePhoneFactory { // 提供操作系统的接口 createOS(){ throw new Error("抽象工厂方法不允许直接调用,你需要将我重写!"); } // 提供硬件的接口 createHardWare(){ throw new Error("抽象工厂方法不允许直接调用,你需要将我重写!"); }} 抽象工厂不干活,具体工厂来干活 1234567891011// 具体工厂继承自抽象工厂class FakeStarFactory extends MobilePhoneFactory { createOS() { // 提供安卓系统实例 return new AndroidOS() } createHardWare() { // 提供高通硬件实例 return new QualcommHardWare() }} 这里我们在提供安卓系统的时候,调用了两个构造函数:AndroidOS 和 QualcommHardWare,它们分别用于生成具体的操作系统和硬件实例。像这种被我们拿来用于 new 出具体对象的类,叫做具体产品类(ConcreteProduct)。具体产品类往往不会孤立存在,不同的具体产品类往往有着共同的功能,比如安卓系统类和苹果系统类,它们都是操作系统,都有着可以操控手机硬件系统这样一个最基本的功能。因此我们可以用一个抽象产品(AbstractProduct)类来声明这一类产品应该具有的基本功能 1234567891011121314151617181920// 定义操作系统这类产品的抽象产品类class OS { controlHardWare() { throw new Error('抽象产品方法不允许直接调用,你需要将我重写!'); }}// 定义具体操作系统的具体产品类class AndroidOS extends OS { controlHardWare() { console.log('我会用安卓的方式去操作硬件') }}class AppleOS extends OS { controlHardWare() { console.log('我会用🍎的方式去操作硬件') }}// ... 硬件类产品同理: 123456789101112131415161718192021// 定义手机硬件这类产品的抽象产品类class HardWare { // 手机硬件的共性方法,这里提取了“根据命令运转”这个共性 operateByOrder() { throw new Error('抽象产品方法不允许直接调用,你需要将我重写!'); }}// 定义具体硬件的具体产品类class QualcommHardWare extends HardWare { operateByOrder() { console.log('我会用高通的方式去运转') }}class MiWare extends HardWare { operateByOrder() { console.log('我会用小米的方式去运转') }}// ... 好了,如此一来,当我们需要生产一台FakeStar手机时,我们只需要这样做: 12345678910// 这是我的手机const myPhone = new FakeStarFactory()// 让它拥有操作系统const myOS = myPhone.createOS()// 让它拥有硬件const myHardWare = myPhone.createHardWare()// 启动操作系统(输出‘我会用安卓的方式去操作硬件’)myOS.controlHardWare()// 唤醒硬件(输出‘我会用高通的方式去运转’)myHardWare.operateByOrder() 总结抽象工厂和简单工厂的共同点,在于都尝试去分离一个系统中变与不变的部分。它们的不同在于场景的复杂度。在简单工厂的使用场景里,处理的对象是类,并且是一些非常好对付的类——它们的共性容易抽离,同时因为逻辑本身比较简单,故而不苛求代码可扩展性。抽象工厂本质上处理的其实也是类,但是是一帮非常棘手、繁杂的类,这些类中不仅能划分出门派,还能划分出等级,同时存在着千变万化的扩展可能性——这使得我们必须对共性作更特别的处理、使用抽象类去降低扩展的成本,同时需要对类的性质作划分,于是有了这样的四个关键角色: 抽象工厂(抽象类,它不能被用于生成具体实例): 用于声明最终目标产品的共性。在一个系统里,抽象工厂可以有多个(大家可以想象我们的手机厂后来被一个更大的厂收购了,这个厂里除了手机抽象类,还有平板、游戏机抽象类等等),每一个抽象工厂对应的这一类的产品,被称为“产品族”。 具体工厂(用于生成产品族里的一个具体的产品): 继承自抽象工厂、实现了抽象工厂里声明的那些方法,用于创建具体的产品的类。 抽象产品(抽象类,它不能被用于生成具体实例): 上面我们看到,具体工厂里实现的接口,会依赖一些类,这些类对应到各种各样的具体的细粒度产品(比如操作系统、硬件等),这些具体产品类的共性各自抽离,便对应到了各自的抽象产品类。 具体产品(用于生成产品族里的一个具体的产品所依赖的更细粒度的产品): 比如我们上文中具体的一种操作系统、或具体的一种硬件等。 抽象工厂模式的定义,是围绕一个超级工厂创建其他工厂。本节内容对一些工作年限不多的同学来说可能不太友好,但抽象工厂目前来说在JS世界里也应用得并不广泛,所以大家不必拘泥于细节,只需留意以下三点: 学会用 ES6 模拟 JAVA 中的抽象类; 了解抽象工厂模式中四个角色的定位与作用; 对“开放封闭原则”形成自己的理解,知道它好在哪,知道执行它的必要性。","link":"/%E6%8A%BD%E8%B1%A1%E5%B7%A5%E5%8E%82%E6%A8%A1%E5%BC%8F/"},{"title":"拖拽","text":"1.draggable属性现代浏览器中,图片标签是可以被长按拖拽的,如果需要让自定义DOM节点可以被拖拽,需要配置draggable全局标签属性当元素设置的draggable属性,就可以被自由拖拽了 2.Drag & Drop事件HTML 的 drag & drop 使用了“DOM Event”和从“Mouse Event”继承而来的“drag event”一个典型的拖拽操作: 用户选中一个可拖拽的(draggable)元素,并将其拖拽(鼠标按住不放)至一个可放置的(droppable)元素上,然后松开鼠标。在拖动元素期间,一些与拖放相关的事件会被触发,像 drag 和 dragover 类型的事件会被频繁触发。除了定义拖拽事件类型,每个事件类型还赋予了对应的事件处理器 事件类型 事件处理器 触发时机 绑定元素 dragstart ondragstart 当开始拖动一个元素时 拖拽 drag ondrag 当元素被拖动期间按一定频率触发 拖拽 dragend ondragend 当拖动的元素被释放(🖱️松开、按键盘 ESC)时 拖拽 dragenter ondragenter 当拖动元素到一个可释放目标元素时 放置 dragover ondragover 当元素被拖到一个可释放目标元素上时(100 ms/次) 放置 dragexit ondragexit 当元素变得不再是拖动操作的选中目标时 放置 dragleave ondragleave 当拖动元素离开一个可释放目标元素 放置 drop ondrop 当拖动元素在可释放目标元素上释放时 放置 各个事件的时机可以用下面这个图简单表示:⚠️注意: dragOver 事件的默认行为是:“Reset the current drag operation to “none””。也就是说,如果不阻止放置元素的 dragOver 事件,则放置元素不会响应“拖动元素”的“放置行为” 3.DataTransfer在上述的事件类型中,不难发现,放置元素和拖动元素分别绑定了自己的事件,可如何将拖拽元素和放置元素建立联系以及传递数据?这就涉及到 DataTransfer 对象:DataTransfer 对象用于保存拖动并放下(drag and drop)过程中的数据。它可以保存一项或多项数据,这些数据项可以是一种或者多种数据类型。 —— DataTransfer - MDN (1) 属性 属性 说明 dropEffect 获取当前选定的拖放操作类型或者设置的为一个新的类型。值为:none、copy、link、move effectAllowed 提供所有可用的操作类型。值是:none、copy、copyLink、copyMove、link、linkMove、move、all、uninitialized files 包含数据传输中可用的所有本地文件的列表。如果拖动操作不涉及拖动文件,则此属性为空列表 items (只读) 提供一个包含所有拖动数据列表的 DataTransferItemList 对象 types (只读) 提供一个 dragstart 事件中设置的格式的 strings 数组。 (2) 方法 属性 说明 setData(format, value) 设置给定类型的数据。如果该类型的数据不存在,则将其添加到末尾,以便类型列表中的最后一项将是新的格式。如果该类型的数据已经存在,则在相同位置替换现有数据。 getData(format) 检索给定类型的数据,如果该类型的数据不存在或 data transfer 不包含数据,则返回空字符串 clearData([format]) 删除与给定类型关联的数据。类型参数是可选的。如果类型为空或未指定,则删除与所有类型关联的数据。如果指定类型的数据不存在,或者 data transfer 中不包含任何数据,则该方法不会产生任何效果。 setDragImage(img,element, xOffset, yOffset) 设置自定义的拖动图像,注意图像需要提前加载,否则会无效 4.手写一个拖拽列表vue123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126<script setup lang="ts">import { ref } from 'vue'const MOCK_LIST_DATA = new Array(10).fill(0).map((_, index) => ({ id: Math.random().toString(36).slice(-6), val: `初始顺序: ${index}`}))const mockListData = ref(MOCK_LIST_DATA);const dragId = ref("");// 替换数据const move = (dragId: string, dropId: string) => { if (!dragId || !dropId) return; const dragIndex = mockListData.value.findIndex(item => item.id === dragId); const dropIndex = mockListData.value.findIndex(item => item.id === dropId); const originData = mockListData.value.splice(dragIndex, 1)[0]; mockListData.value.splice(dropIndex, 0, originData);}// 拖拽开始时const onDragStart = (e) => { dragId.value = e.currentTarget.dataset.id;}// 拖拽进行中const onDrag = (e) => { e.currentTarget.style.opacity = "0";}// 拖拽结束const onDragEnd = (e) => { e.currentTarget.style.opacity = "1"}// 当移动到可以被放置的元素时const onDragOver = (e) => { const dropId = e.currentTarget.dataset.id; if (dragId === dropId) return; const dragIndex = mockListData.value.findIndex(item => item.id === dragId.value); const dropIndex = mockListData.value.findIndex(item => item.id === dropId); e.currentTarget.classList.remove("drop-up", "drop-down"); if (dragIndex < dropIndex) { e.currentTarget.classList.add("drop-down"); } else if (dragIndex > dropIndex) { e.currentTarget.classList.add("drop-up"); } move(dragId.value, dropId);}</script><template> <div class="sortable-page"> <h1>基于 HTML5 原生拖拽事件的拖拽列表</h1> <div className="list-container"> <div @dragstart="onDragStart" @drag="onDrag" @dragend="onDragEnd" @dragover.prevent="onDragOver" class="item" v-for="item in mockListData" :key="item.id" :data-id="item.id" draggable="true"> <div>ID: {{ item.id }}</div> <div> {{ item.val }}</div> </div> </div> </div></template><style scoped lang="scss">.sortable-page { position: relative; width: 100%; min-height: 100vh; background-color: #ececec; display: flex; align-items: center; flex-direction: column; justify-content: center; &>.list-container { margin: 40px auto; width: 400px; min-height: 600px; padding: 10px 20px; background-color: #fff; border-radius: 4px; box-shadow: 0 5px 10px rgba(0, 0, 0, 0.1); display: flex; flex-direction: column; .item { position: relative; box-sizing: border-box; height: 50px; padding: 8px; display: flex; justify-content: space-between; align-items: center; background-color: #fff; cursor: grab; user-select: none; &.drop-up { animation: dropUp 0.3s ease-in-out forwards; } &.drop-down { animation: dropDown 0.3s ease-in-out forwards; } &.border-top { border-top: 2px solid red; } } }}@keyframes dropUp { 100% { transform: translateY(5px); }}@keyframes dropDown { 100% { transform: translateY(-5px); }}</style> react123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120import React, { useState } from "react";import { cloneDeep, findIndex, isEqual } from "lodash-es";import update from "immutability-helper";import "./App.less";const MOCK_LIST_DATA = new Array(10).fill(0).map((_, idx) => ({ id: Math.random().toString(36).slice(-6), val: `初始顺序:${idx}`,}));interface ItemProps { index: string; val: string; handleDragStart: React.DragEventHandler<HTMLDivElement>; handleDragOver: React.DragEventHandler<HTMLDivElement>; handleDrag: React.DragEventHandler<HTMLDivElement>; handleDragEnd: React.DragEventHandler<HTMLDivElement>;}const Item: React.FC<ItemProps> = React.memo((props) => { const { index, val, handleDrag, handleDragEnd, handleDragOver, handleDragStart, } = props; return ( <div className="item" draggable onDragStart={handleDragStart} onDragOver={handleDragOver} onDrag={handleDrag} onDragEnd={handleDragEnd} onDragLeave={(e) => { e.currentTarget.classList.remove("border-top"); }} data-index={index} > <div>ID: {index}</div> <div>{val}</div> </div> );});const SortableListPage = () => { const [listData, setListData] = useState(MOCK_LIST_DATA); const [dragId, setDragId] = useState<string | undefined>(""); const move = (dragId?: string, dropId?: string) => { if (!dragId || !dropId) return; const dragIndex = findIndex(listData, (i) => i.id === dragId); const dropIndex = findIndex(listData, (i) => i.id === dropId); const originItem = listData.splice(dragIndex, 1)[0]; listData.splice(dropIndex, 0, originItem); setListData([...listData]); }; // 源对象开始拖拽 const handleDragStart = (e: React.DragEvent<HTMLDivElement>) => { e.dataTransfer.effectAllowed = "move"; setDragId(e.currentTarget.dataset.index); }; // 源对象在目标对象上方时 const handleDragOver = (e: React.DragEvent<HTMLDivElement>) => { e.preventDefault(); // 允许放置,阻止默认事件 // 设置动画 const dropId = e.currentTarget.dataset.index; if(dragId === dropId) return; const dragIndex = findIndex(listData, (i) => i.id === dragId); const dropIndex = findIndex(listData, (i) => i.id === dropId); e.currentTarget.classList.remove("drop-up", "drop-down"); if (dragIndex < dropIndex) { e.currentTarget.classList.add("drop-down"); } else if (dragIndex > dropIndex) { e.currentTarget.classList.add("drop-up"); } move(dragId, dropId); }; // 源对象被拖拽过程中 const handleDrag = (e: React.DragEvent<HTMLDivElement>) => { e.currentTarget.style.opacity = "0"; }; // 源对象被放置完成时 const handleDragEnd = (e: React.DragEvent<HTMLDivElement>) => { e.currentTarget.style.opacity = "1"; }; return ( <div className="sortable-page"> <h1>基于 HTML5 原生拖拽事件的拖拽列表</h1> <div className="list-container"> {listData.map((i) => ( <Item key={i.id} index={i.id} val={i.val} handleDragStart={handleDragStart} handleDragOver={handleDragOver} handleDrag={handleDrag} handleDragEnd={handleDragEnd} /> ))} </div> </div> );};export default SortableListPage; 1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859.sortable-page { position: relative; width: 100%; min-height: 100vh; background-color: #ececec; display: flex; align-items: center; flex-direction: column; justify-content: center; & > .list-container { margin: 40px auto; width: 400px; min-height: 600px; padding: 10px 20px; background-color: #fff; border-radius: 4px; box-shadow: 0 5px 10px rgba(0, 0, 0, 0.1); display: flex; flex-direction: column; .item { position: relative; box-sizing: border-box; height: 50px; padding: 8px; display: flex; justify-content: space-between; align-items: center; background-color: #fff; cursor: grab; user-select: none; &.drop-up { animation: dropUp 0.3s ease-in-out forwards; } &.drop-down { animation: dropDown 0.3s ease-in-out forwards; } &.border-top { border-top: 2px solid red; } } }}@keyframes dropUp { 100% { transform: translateY(5px); }}@keyframes dropDown { 100% { transform: translateY(-5px); }} React-dnd1yarn add react-dnd react-dnd-html5-backend 12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485import { DndProvider } from 'react-dnd';import { HTML5Backend } from 'react-dnd-html5-backend';// App.tsxfunction App() { return ( <DndProvider backend={ HTML5Backend }> <Demo /> </DndProvider> )}// Demo.tsximport React, { useState, useRef } from "react";import { useDrag, useDrop } from "react-dnd";const mockData = new Array(10).fill(0).map((_, index) => ({ id: Math.random().toString(36).slice(-6), val: `初始位置: ${index}`,}));const Item = ({ val, id, move }: any) => { const ref = useRef<HTMLDivElement>(null); const [, drag, dragPreview] = useDrag({ type: "Item", item: () => ({ id, }), }); const [, drop] = useDrop({ accept: "Item", hover: (item: any, monitor) => { const dragId = item.id; const dropId = id; if (dragId === dropId) return; move(dragId, dropId) }, }); dragPreview(drag(drop(ref))); return ( <div ref={ref} style={{ width: "200px", lineHeight: "40px", cursor: "grab", display: "flex", alignItems: "center", justifyContent: "space-between", }} > <div>ID: {id}</div> <div>{val}</div> </div> );};function Demo() { const [dataList, setDataList] = useState(mockData); const move = (dragId: string, dropId: string) => { const dragIndex = dataList.findIndex((item) => item.id === dragId); const dropIndex = dataList.findIndex((item) => item.id === dropId); const originData = dataList.splice(dragIndex, 1)[0]; dataList.splice(dropIndex, 0, originData); setDataList([...dataList]); }; return ( <> {dataList.map((item) => ( <Item key={item.id} id={item.id} val={item.val} move={move} /> ))} </> );}export default Demo;","link":"/%E6%8B%96%E6%8B%BD/"},{"title":"","text":"编写一个简单的Golang程序 12345678package mainimport "fmt"func main() { fmt.Println("Hello Go")} 注意点: Golang中每行代码结尾可以加分号也可以不加 函数的{必须和函数名在同一行,否则报错 import语法不同的写法: 1234567import "fmt"import "time"import ( "fmt" "time") 声明变量的四种方式12345678910111213141516171819202122232425package mainimport "fmt"func main() { // 声明变量的四种方式 // 方法一 声明一个变量 指定数据类型 var a int fmt.Println(a) // 方法二 声明一个变量 初始化一个值 var b int = 100 fmt.Println(b) // 方法三:声明一个变量,不指定数据类型(Go可以自动推导类型) var c = 100 fmt.Println(c) // 方法四:省去var关键字 直接自动匹配(推荐) // := 声明的方式只能够在函数体内使用 d := 100 fmt.Println(d)}","link":"/Golang/Golang%E8%AF%AD%E6%B3%95%E6%B3%A8%E6%84%8F%E7%82%B9/"},{"title":"CSS属性初始化","text":"123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108/*css 属性初始化 */html,body,ul,li,ol,dl,dd,dt,p,h1,h2,h3,h4,h5,h6,form,fieldset,legend,img { margin: 0; padding: 0;}fieldset,img,input,button { /*fieldset组合表单中的相关元素*/ border: none; padding: 0; margin: 0; outline-style: none;}ul,ol { list-style: none;}input { padding-top: 0; padding-bottom: 0; font-family: "SimSun", "宋体";}select,input { vertical-align: middle;}select,input,textarea { margin: 0;}textarea { resize: none;}/*防止多行文本框拖动*/img { border: 0; vertical-align: middle;}/* 去掉图片低测默认的3像素空白缝隙*/table { border-collapse: collapse;}.clearfix:before,.clearfix:after { content: ""; display: table;}.clearfix:after { clear: both;}.clearfix { *zoom: 1;}a { text-decoration: none;}h1,h2,h3,h4,h5,h6 { text-decoration: none; font-weight: normal; font-size: 100%;}s,i,em { font-style: normal; text-decoration: none;} 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133* { margin: 0; padding: 0;}html,body,div,span,applet,object,iframe,h1,h2,h3,h4,h5,h6,p,blockquote,pre,a,abbr,acronym,address,big,cite,code,del,dfn,em,img,ins,kbd,q,s,samp,small,strike,strong,sub,sup,tt,var,b,u,i,center,dl,dt,dd,ol,ul,li,fieldset,form,label,legend,table,caption,tbody,tfoot,thead,tr,th,td,article,aside,canvas,details,embed,figure,figcaption,footer,header,hgroup,menu,nav,output,ruby,section,summary,time,mark,audio,video { margin: 0; padding: 0; border: 0;}/* HTML5 display-role reset for older browsers */article,aside,details,figcaption,figure,footer,header,hgroup,menu,nav,section { display: block;}body { line-height: 1;}ol,ul { list-style: none;}blockquote,q { quotes: none;}blockquote:before,blockquote:after,q:before,q:after { content: ''; content: none;}table { border-collapse: collapse; border-spacing: 0;}","link":"/CSS%E5%B1%9E%E6%80%A7%E5%88%9D%E5%A7%8B%E5%8C%96/"},{"title":"Volta使用指北","text":"前言Volta可以用来管理你电脑上的node、npm、yarn、pnpm版本 安装mac用户直接进入volta官网复制以下命令即可完成安装: 1curl https://get.volta.sh | bash 这将在~/.volta目录下安装volta库,并更新您的~/.bashrc,~/.zshrc和~/.config/fish 脚本会将bin目录添加到路径中,比如: 12export VOLTA_HOME="$HOME/.volta"export PATH="$VOLTA_HOME/bin:$PATH" 卸载必须手动从系统中删除volta引用。 从bash或zsh配置(~/.bashrc和/或~/.zshrc)的路径中删除volta 删除了~/.volta文件夹。rm -rf ~/.volta 使用安装命令很简单,直接volta install xxx就可以安装软件,可以选择安装默认版本,或者指定版本安装 12345678910111213141516171819# nodevolta install node# orvolta install node@14# npmvolta install npm# orvolta install npm@6# yarnvolta install yarn# orvolta install yarn@1.10.0# pnpmvolta install pnpm# orvolta install pnpm@8 固定项目中的运行环境版本在项目目录中执行volta pin xxx命令,可以固定你的node/npm/yarn/pnpm版本,这样如果团队中其他成员也使用volta,就可以避免环境版本不同带来的冲突问题","link":"/Volta%E4%BD%BF%E7%94%A8%E6%8C%87%E5%8C%97/"},{"title":"aspect-ratio","text":"用来定义容器的宽高比 示例 1234567891011121314151617181920212223<!DOCTYPE html><html lang="en"><head> <meta charset="UTF-8"> <meta http-equiv="X-UA-Compatible" content="IE=edge"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>Document</title> <style> .box { width: 200px; background-color: aqua; height: auto; aspect-ratio: 1/2; } </style></head><body> <div class="box"></div></body></html>","link":"/aspect-ratio/"},{"title":"简单工厂模式","text":"简单工厂模式主要在于区分变与不变,例如Coder 和 ProductManager 两个工种的员工,是不是仍然存在都拥有 name、age、career、work 这四个属性这样的共性?它们之间的区别,在于每个字段取值的不同,以及 work 字段需要随 career 字段取值的不同而改变。 1234567891011121314151617181920212223242526272829303132class User { /** * @param {String} name * @param {Number} age * @param {String} career * @param {String} work */ constructor(name, age, career, work) { this.name = name; this.age = age; this.career = career; this.work = work; }}function Factory(name, age, career) { let work switch(career) { case 'coder': work = ['写代码','写系分', '修Bug'] break case 'product manager': work = ['订会议室', '写PRD', '催更'] break case 'boss': work = ['喝茶', '看报', '见客户'] case 'xxx': // 其它工种的职责分配 ... return new User(name, age, career, work)} 小结工厂模式的简单之处,在于它的概念相对好理解:将创建对象的过程单独封装,这样的操作就是工厂模式。同时它的应用场景也非常容易识别:有构造函数的地方,我们就应该想到简单工厂;在写了大量构造函数、调用了大量的 new、自觉非常不爽的情况下,我们就应该思考是不是可以掏出工厂模式重构我们的代码了","link":"/%E7%AE%80%E5%8D%95%E5%B7%A5%E5%8E%82%E6%A8%A1%E5%BC%8F/"},{"title":"面试小结(20230407)","text":"前言这次是上一次的复试,废话不说直接看题吧 面试题Echarts图表本身针对大数据渲染的优化配置这个属于项目中性能优化话题里的一个问题,当时并没有考虑到Echarts本身的优化,后面查了一下可以通过sampling: average、large: true以及取消dataZoom的实时刷新这几个方法来实现优化的效果 数组方法的排序给定一个数组对象,里面提供了a和b两个属性进行排序,如果a属性相等,则采用b属性,否则使用a属性 使用sort方法 123456789const arr = [{ a: 1, b: 3 }, { a: 2, b: 4 }, { a: 3, b: 2 }, { a: 1, b: 5 }];arr.sort((prev, curr) => { if (prev.a === curr.a) { return prev.b - curr.b; } return prev.b - curr.a})console.log(arr); 封装一个排序方法 过滤数组里面的某一项过滤出数组中a不等于1的项 不会改变原数组的方法: 使用filter方法进行过滤 1const result = arr.filter(item => item.a !== 1); 需要在原数组上做更改的方法: 使用delete关键字 使用Reflect.deleteProperty() 缺点是过滤完的数据会出现特殊的empty属性,需要再过滤一遍 1234567891011const arr = [{ a: 1, b: 3 }, { a: 2, b: 4 }, { a: 3, b: 2 }, { a: 1, b: 5 }];for (let i = 0; i < arr.length; i++) { if (arr[i].a === 1) { Reflect.deleteProperty(arr, i); // or delete arr[i] }}// 去除empty属性console.log(arr.filter(Boolean)); 移动端适配方案移动端适配方案都有哪些? 采用rem进行适配 采用vw/vh进行适配 场景题给定一个性能监控的对象数组,里面包括了函数开始时间,执行时间,子函数的关系等等,他们的关系是通过深度优先搜索来决定,要求实现一个性能火焰图 整体应该是为了考察你的思路,面对新需求是否会主动提问等等,这里罗列一下面试官问的问题: 如何确定父函数和子函数之间的调用关系 确定了调用关系,如何在页面中绘制出来?使用原生DOM?canvas?还是第三方插件 假如这个时候你绘制出来了,canvas中每个函数的执行时间要如何换算成像素在页面上展示出来? 你刚刚提到了使用clientWidth获取屏幕宽度,那你还知道有其它哪些属性吗?他们的区别是什么? 你有做过哪些有难度的项目回答了一个简历中没有写的浏览器插件项目 总结复试给我的个人感觉不是很顺利,尤其是那个场景题基本上没有回答出来,总之再接再厉吧!","link":"/%E9%9D%A2%E8%AF%95%E5%B0%8F%E7%BB%93(20230407)/"},{"title":"h5适配计算","text":"1.什么是viewport 早期移动端的viewport与pc的viewport是一个概念,导致小屏体验不佳,后来苹果引入可视视窗(visual viewport)和布局视窗(layout viewport)。这两个视窗是透视的效果,想象下layout viewport是一张大的不能改变大小和角度的图片。我们透过visual viewport对layout viewport进行观察。观察距离远点(用户的缩小页面功能)就可以一次性看到这个大图。或者近点(用户的放大页面功能)可以看到一部分。你能改变这个透视框的方向,但这张大图片的大小和形状都不会改变。 我们在<meta name="viewport" /> 设置的其实是layout-viewport,使得layout viewport==visual viewport,达到ideal viewport效果,使得viewport刚好完美覆盖屏幕,因此适配方案的时候,这一句最重要。 2.计算方案1.px rem vw的转换 默认情况下根元素的font-size为 1rem = 16px,但为了方便换算,我们通常设置 1rem = 100px 以750px的设计稿为例,则可以得出 750px = 7.5rem 相当于100vw=7.5rem那么1rem = 100vw / 7.5 = 13.3333vw,所以设置根元素的font-size为13.3333vw 而在页面样式中,直接将设计稿中的px除以100就是对应的rem值 若要兼容旧浏览器,则需要写入响应式布局,例如: 12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576// 相当于 320 / 7.5 = 42.667px@media screen and (max-width: 320px) { html { font-size: 42.667px; font-size: 13.3333vw; }}// 相当于375 / 7.5 = 48px,以下同理@media screen and (min-width: 321px) and (max-width: 375px) { html { font-size: 48px; font-size: 13.3333vw; }}@media screen and (min-width: 376px) and (max-width:393px) { html { font-size: 52.4px; font-size: 13.3333vw }}@media screen and (min-width: 394px) and (max-width:412px) { html { font-size: 54.93px; font-size: 13.3333vw }}@media screen and (min-width: 413px) and (max-width:414px) { html { font-size: 55.2px; font-size: 13.3333vw }}@media screen and (min-width: 415px) and (max-width:480px) { html { font-size: 64px; font-size: 13.3333vw }}@media screen and (min-width: 481px) and (max-width:540px) { html { font-size: 72px; font-size: 13.3333vw }}@media screen and (min-width: 541px) and (max-width:640px) { html { font-size: 85.33px; font-size: 13.3333vw }}@media screen and (min-width: 641px) and (max-width:720px) { html { font-size: 96px; font-size: 13.3333vw }}@media screen and (min-width: 721px) and (max-width:768px) { html { font-size: 102.4px; font-size: 13.3333vw }}@media screen and (min-width: 769px) and (max-width:852px) { html { font-size: 113.4px; font-size: 13.3333vw }}@media screen and (min-width: 853px) { html { font-size: 130.4px; font-size: 13.3333vw }} 不同的设计稿,可以参考下面的表格: 设计稿大小(单位 px) html 的 font-size(单位 vw) 375 26.666666 750 13.333333 320 31.25 640 15.625 2.viewport缩放比例设置1<meta name="viewport" content="width=device-width,initial-scale=1.0,minimum-scale=1.0,maximum-scale=1.0,user-scalable=no" /> 3.其他可能使用到的meta标签配置123456<meta name="screen-orientation" content="portrait"> //Android 禁止屏幕旋转<meta name="full-screen" content="yes"> //全屏显示<meta name="browsermode" content="application"> //UC应用模式,使用了application这种应用模式后,页面讲默认全屏,禁止长按菜单,禁止收拾,标准排版,以及强制图片显示。<meta name="x5-orientation" content="portrait"> //QQ强制竖屏<meta name="x5-fullscreen" content="true"> //QQ强制全屏<meta name="x5-page-mode" content="app"> //QQ应用模式 电话号码识别在 iOS Safari (其他浏览器和 Android 均不会)上会对那些看起来像是电话号码的数字处理为电话链接,比如: 7 位数字,形如:1234567 带括号及加号的数字,形如:(+86)123456789 双连接线的数字,形如:00-00-00111 11 位数字,形如:13800138000 关闭识别 1<meta name="format-detection" content="telephone=no" /> 开启识别 1<a href="tel:123456">123456</a> 邮箱识别(Android)安卓上会对符合邮箱格式的字符串进行识别,我们可以通过如下的 meta 来管别邮箱的自动识别: 1<meta content="email=no" name="format-detection" /> 同样地,我们也可以通过标签属性来开启长按邮箱地址弹出邮件发送的功能: 1<a mailto:dooyoe@gmail.com">dooyoe@gmail.com</a> 4.css 篇0.5px细线移动端 H5 项目越来越多,设计师对于 UI 的要求也越来越高,比如 1px 的边框。在高清屏下,移动端的 1px 会很粗。 那么为什么会产生这个问题呢?主要是跟一个东西有关,DPR(devicePixelRatio) 设备像素比,它是默认缩放为 100%的情况下,设备像素和 CSS 像素的比值。目前主流的屏幕 DPR=2(iPhone 8),或者 3(iPhone 8 Plus)。拿 2 倍屏来说,设备的物理像素要实现 1 像素,而 DPR=2,所以 css 像素只能是 0.5。 下面介绍最常用的方法 1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586/* 底边框 */.b-border { position: relative;}.b-border:before { content: ''; position: absolute; left: 0; bottom: 0; width: 100%; height: 1px; background: #d9d9d9; -webkit-transform: scaleY(0.5); transform: scaleY(0.5); -webkit-transform-origin: 0 0; transform-origin: 0 0;}/* 上边框 */.t-border { position: relative;}.t-border:before { content: ''; position: absolute; left: 0; top: 0; width: 100%; height: 1px; background: #d9d9d9; -webkit-transform: scaleY(0.5); transform: scaleY(0.5); -webkit-transform-origin: 0 0; transform-origin: 0 0;}/* 右边框 */.r-border { position: relative;}.r-border:before { content: ''; position: absolute; right: 0; bottom: 0; width: 1px; height: 100%; background: #d9d9d9; -webkit-transform: scaleX(0.5); transform: scaleX(0.5); -webkit-transform-origin: 0 0; transform-origin: 0 0;}/* 左边框 */.l-border { position: relative;}.l-border:before { content: ''; position: absolute; left: 0; bottom: 0; width: 1px; height: 100%; background: #d9d9d9; -webkit-transform: scaleX(0.5); transform: scaleX(0.5); -webkit-transform-origin: 0 0; transform-origin: 0 0;}/* 四条边 */.setBorderAll { position: relative; &:after { content: ' '; position: absolute; top: 0; left: 0; width: 200%; height: 200%; transform: scale(0.5); transform-origin: left top; box-sizing: border-box; border: 1px solid #e5e5e5; border-radius: 4px; }} 屏蔽用户选择禁止用户选择页面中的文字或者图片 12345678div { -webkit-touch-callout: none; -webkit-user-select: none; -khtml-user-select: none; -moz-user-select: none; -ms-user-select: none; user-select: none;} 清除输入框内阴影在 iOS 上,输入框默认有内部阴影,以这样关闭: 123div { -webkit-appearance: none;} 如何禁止保存或拷贝图像代码如下 123img { -webkit-touch-callout: none;} 输入框默认字体颜色设置 input 里面 placeholder 字体的颜色 123456789101112input::-webkit-input-placeholder,textarea::-webkit-input-placeholder { color: #c7c7c7;}input:-moz-placeholder,textarea:-moz-placeholder { color: #c7c7c7;}input:-ms-input-placeholder,textarea:-ms-input-placeholder { color: #c7c7c7;} 用户设置字号放大或者缩小导致页面布局错误设置字体禁止缩放 12345body { -webkit-text-size-adjust: 100% !important; text-size-adjust: 100% !important; -moz-text-size-adjust: 100% !important;} android系统中元素被点击时产生边框部分android系统点击一个链接,会出现一个边框或者半透明灰色遮罩, 不同生产商定义出来额效果不一样。去除代码如下 1234a,button,input,textarea{ -webkit-tap-highlight-color: rgba(0,0,0,0) -webkit-user-modify:read-write-plaintext-only;} iOS 滑动不流畅ios 手机上下滑动页面会产生卡顿,手指离开页面,页面立即停止运动。整体表现就是滑动不流畅,没有滑动惯性。 iOS 5.0 以及之后的版本,滑动有定义有两个值 auto 和 touch,默认值为 auto。 解决方案 在滚动容器上增加滚动 touch 方法 123.wrapper { -webkit-overflow-scrolling: touch;} 设置 overflow 设置外部 overflow 为 hidden,设置内容元素 overflow 为 auto。内部元素超出 body 即产生滚动,超出的部分 body 隐藏。 123456body { overflow-y: hidden;}.wrapper { overflow-y: auto;} 5.js 篇移动端click屏幕产生200-300 ms的延迟响应移动设备上的web网页是有300ms延迟的,往往会造成按钮点击延迟甚至是点击失效。解决方案: fastclick可以解决在手机上点击事件的300ms延迟 zepto的touch模块,tap事件也是为了解决在click的延迟问题 触摸事件的响应顺序 ontouchstart ontouchmove ontouchend onclick audio 和 video 在 ios 和 andriod 中自动播放这个不是bug,由于自动播放网页中的音频或视频,会给用户带来一些困扰或者不必要的流量消耗,所以苹果系统和安卓系统通常都会禁止自动播放和使用 JS 的触发播放,必须由用户来触发才可以播放。加入自动触发播放的代码 123$('html').one('touchstart', function() { audio.play()}) iOS 上拉边界下拉出现空白手指按住屏幕下拉,屏幕顶部会多出一块白色区域。手指按住屏幕上拉,底部多出一块白色区域。 在 iOS 中,手指按住屏幕上下拖动,会触发 touchmove 事件。这个事件触发的对象是整个 webview 容器,容器自然会被拖动,剩下的部分会成空白。 解决方案 1234567891011document.body.addEventListener( 'touchmove', function(e) { if (e._isScroller) return // 阻止默认事件 e.preventDefault() }, { passive: false }) ios 日期转换 NAN 的问题将日期字符串的格式符号替换成’/‘ 1'yyyy-MM-dd'.replace(/-/g, '/') 6.软键盘问题IOS 键盘弹起挡住原来的视图 可以通过监听移动端软键盘弹起 Element.scrollIntoViewIfNeeded(Boolean)方法用来将不在浏览器窗口的可见区域内的元素滚动到浏览器窗口的可见区域。 如果该元素已经在浏览器窗口的可见区域内,则不会发生滚动。 true,则元素将在其所在滚动区的可视区域中居中对齐。 false,则元素将与其所在滚动区的可视区域最近的边缘对齐。 根据可见区域最靠近元素的哪个边缘,元素的顶部将与可见区域的顶部边缘对准,或者元素的底部边缘将与可见区域的底部边缘对准。 1234567891011121314window.addEventListener('resize', function() { if ( document.activeElement.tagName === 'INPUT' || document.activeElement.tagName === 'TEXTAREA' ) { window.setTimeout(function() { if ('scrollIntoView' in document.activeElement) { document.activeElement.scrollIntoView(false) } else { document.activeElement.scrollIntoViewIfNeeded(false) } }, 0) }}) onkeyUp 和 onKeydown 兼容性问题IOS 中 input 键盘事件 keyup、keydown、等支持不是很好, 用 input 监听键盘 keyup 事件,在安卓手机浏览器中没有问题,但是在 ios 手机浏览器中用输入法输入之后,并未立刻相应 keyup 事件 IOS12 输入框难以点击获取焦点,弹不出软键盘定位找到问题是 fastclick.js 对 IOS12 的兼容性,可在 fastclick.js 源码或者 main.js 做以下修改 12345678910111213141516FastClick.prototype.focus = function(targetElement) { var length if ( deviceIsIOS && targetElement.setSelectionRange && targetElement.type.indexOf('date') !== 0 && targetElement.type !== 'time' && targetElement.type !== 'month' ) { length = targetElement.value.length targetElement.setSelectionRange(length, length) targetElement.focus() } else { targetElement.focus() }} IOS 键盘收起时页面没用回落,底部会留白通过监听键盘回落时间滚动到原来的位置 123456789101112131415window.addEventListener('focusout', function() { window.scrollTo(0, 0)})//input输入框弹起软键盘的解决方案。var bfscrolltop = document.body.scrollTop$('input') .focus(function() { document.body.scrollTop = document.body.scrollHeight //console.log(document.body.scrollTop); }) .blur(function() { document.body.scrollTop = bfscrolltop //console.log(document.body.scrollTop); }) IOS 下 fixed 失效的原因软键盘唤起后,页面的 fixed 元素将失效,变成了 absolute,所以当页面超过一屏且滚动时,失效的 fixed 元素就会跟随滚动了。不仅限于 type=text 的输入框,凡是软键盘(比如时间日期选择、select 选择等等)被唤起,都会遇到同样地问题。 解决方法: 不让页面滚动,而是让主体部分自己滚动,主体部分高度设为 100%,overflow:scroll 1234567891011121314151617181920212223242526<head> ... <style> .warper { position: absolute; width: 100%; left: 0; right: 0; top: 0; bottom: 0; overflow-y: scroll; -webkit-overflow-scrolling: touch; /* 解决ios滑动不流畅问题 */ } .fix-bottom { position: fixed; bottom: 0; width: 100%; } </style></head><body> <div class='warper'> <div class='main'></div> <div> <div class="fix-bottom"></div></body>","link":"/3.h5%E9%80%82%E9%85%8D%E8%AE%A1%E7%AE%97/"},{"title":"min max clamp函数","text":"overscroll-behavior CSS 属性是 overscroll-behavior-x 和 overscroll-behavior-y 属性的合并写法,让你可以控制浏览器过度滚动时的表现——也就是滚动到边界。 123456789101112/* 关键字的值 */overscroll-behavior: auto; /* 默认 */overscroll-behavior: contain;overscroll-behavior: none;/* 使用 2 个值 */overscroll-behavior: auto contain;/* Global values */overflow: inherit;overflow: initial;overflow: unset;","link":"/overscroll-behavior/"},{"title":"compose函数","text":"compose函数含义: 将需要嵌套执行的函数平铺执行 嵌套执行指的是一个函数的返回值将作为另一个函数的参数 作用: 实现函数式编程中的 Pointfree,使我们专注于转换而不是数据(Pointfree 不使用所有处理的值,只合成运算过程,即我们所指的无参数分割) 实现12345678910111213141516171819let add = x => x + 10let multiply = y => y * 10const compose = function() { let args = [].slice.call(arguments) return function(x) { return args.reduceRight(function(total, current) { return current(total) }, x) }}// es6实现const compose = (...args) => x => args.reduceRight((res, cb) => cb(res), x)// 调用let calculate = compose(multiply, add)calculate(10) // 200 pipe函数pipe 函数 compose 类似,只不过从左往右执行","link":"/Pointfree/"},{"title":"图片响应式布局","text":"可以通过media属性和picture标签来实现响应式布局中图片的自适应 12345<picture> <source media="(max-width: 300px)" srcset="img"> <source media="(max-width: 500px)" srcset="img2"> <img src="img2" alt=""></picture> 也可以使用img标签本身的属性 123456789<img src="imgSrc" srcset=" image1 1240w, image2 600w, image3 300w " sizes=" (max-width: 400px) 300px, (max-width: 900px) 600px, 1240px " alt="">","link":"/%E5%9B%BE%E7%89%87%E5%93%8D%E5%BA%94%E5%BC%8F%E5%B8%83%E5%B1%80/"},{"title":"fs模块的使用","text":"1.fs模块功能介绍 1.fs.stat()检查是文件还是目录 12345678910111213141516171819202122const fs = require('fs');/* fs.stat() 第一个参数:要检测的路径 第二个参数:回调函数:err:返回错误信息,data:返回检测结果*/fs.stat('./html',(err,data)=>{ // 返回错误信息 if(err){ console.log(err); return; } // 如果data.isFile()为true则为文件,如果 data.isDirectory()为true则为目录 if(data.isFile()){ console.log("是文件"); }else if(data.isDirectory()){ console.log("是目录"); }}) 2.fs.mkdir()创建目录 1234567891011121314/* fs.mkdir() path:要创建目录的路径 mode:目录读写权限,可不写,默认为777 callback:回调函数,传递异常参数err*/fs.mkdir('./css',(err)=>{ if(err){ console.log(err); return; } console.log("创建成功");}) 3.fs.writeFile()创建写入文件 1234567fs.writeFile('./html/index.html','hello',(err)=>{ if(err){ console.log(err); return; } console.log("创建成功");}) 4.fs.appendFile()追加文件 123456789// fs.appendFile()fs.appendFile('./css/commit.css','body{color:red}\\nh2{font-size:16px}\\n*{margin:0}',(err)=>{ if(err){ console.log(err); return; } console.log("appendFile成功");}) 5.fs.readFile()读取文件 12345678910// fs.readFile()fs.readFile('./html/index.html',(err,data)=>{ if(err){ console.log(err); return; } console.log(data);//Buffer console.log(data.toString());}) 6.fs.readdir()读取目录 12345678// fs.readdirfs.readdir('./html',(err,files)=>{ if(err){ console.log(err); return; } console.log(files);}) 7.fs.rename()重命名/移动文件 1234567891011121314151617// fs.rename 功能1:重命名文件,功能2:移动文件//1.重命名fs.rename('./css/aaa.css','./css/bbb.css',(err)=>{ if(err){ console.log(err); return; } console.log("rename成功");})//2.移动fs.rename('./css/bbb.css','./html/bbb.css',(err)=>{ if(err){ console.log(err); return; } console.log("rename成功");}) 8.fs.unlink(),fs.rmdir()删除文件,删除目录 1234567891011121314151617// fs.unlink() fs.rmdir()//删除文件fs.unlink('./html/bbb.css',(err)=>{ if(err){ console.log(err); return; } console.log("删除文件成功");})//删除目录fs.rmdir('./bbb',(err)=>{ if(err){ console.log(err); return; } console.log("删除目录成功");}) 【注】如果目录下有文件,则必须删除文件后才能删除目录,直接删除会失败 2.案例1.创建upload目录判断服务器中是否有upload目录,若没有,则创建它,若有,则不做任何操作 12345678910111213141516171819202122232425262728293031323334const fs = require('fs');let path = './upload';fs.stat(path,(err,data)=>{ //判断目录/文件是否已存在,若不存在则直接创建 if(err){ mkdir(path); return; } //判断已存在的文件/目录属性,若是文件则先删除文件在创建,若是目录则不用创建 if(data.isFile()){ fs.unlink(path,(err)=>{ if(!err){ mkdir(path); return; } }); }else if(data.isDirectory()){ console.log("该目录已存在"); }})//创建文件方法function mkdir(path){ fs.mkdir(path,(err)=>{ if(err){ console.log(err); return; }else{ console.log("创建成功"); } })} 通过第三方模块mkdirp创建 1234/* cnpm i mkdirp --save */const mkdirp = require('mkdirp');mkdirp('./upload').then(made =>console.log(`made directories, starting with ${made}`)) 2.判断wwwroot目录下有几个为目录12345678910111213141516171819202122// 判断wwwroot文件夹下所有的文件,若是文件夹,则存到一个数组中const fs = require('fs');let dirArr = [];let path = './wwwroot';fs.readdir(path,(err,data)=>{ if(err){ console.log(err); return; } (function getDir(i){ if(i == data.length){ console.log(dirArr); return; } fs.stat(path+'/'+data[i],(error,state)=>{ if(state.isDirectory()){ dirArr.push(data[i]); } getDir(++i); }) })(0)})","link":"/NodeJs/5.fs%E6%A8%A1%E5%9D%97%E7%9A%84%E4%BD%BF%E7%94%A8/"},{"title":"ES6常用语法","text":"1.let和const用let声明的变量具备块级作用域,用const声明的变量为常量 12345678910111213var a = 1;{ var a = 2;}console.log(a); //2let a = 1;{ let a = 2;}console.log(a); //1const PI = 3.14; //是常量,不能改变其值 2.ES6中语法的简写对象属性的简写 12345//如果对象属性名和某个要赋值的变量名相同,则可以直接写对象属性名name = "小明"let obj = { name;} 对象方法的简写 123456789//写对象方法时,可以省略functionlet obj = { a: 1, b: 2, add(){ return this.a + this.b; }}console.log(obj.add()); 3.获取异步方法中的值es6之前,可以通过回调函数来获取异步方法中的值 12345678910function getData(callbck){ setTimeout(function(){ let name = "小明"; callbck(name); },1000);}getData(function(a){ console.log(a);}) es6之后,可以通过Promise来获取 1234567891011121314151617181920212223242526//写法一let p = new Promise((resolve,reject)=>{ setTimeout(function(){ let name = "小明"; resolve(name); },1000);})//通过then方法获取resolve中的值p.then(function(data){ console.log(data);})//写法二//定义一个函数,传入参数resolve和rejectfunction getData(resolve,reject){ setTimeout(function(){ let name = "小明"; resolve(name); },1000);}//将该函数传给Promise对象let p = new Promise(getData);//通过then来获取值p.then(function(data){ console.log(data);}) 通过async和await来获取异步数据 123456789101112//1.通过async声明的方法为异步方法,返回值为Promise对象async function test(){ return "Hello World";}console.log(test()); // Promise{"Hello world"}//2.可以通过await来直接获取异步方法中的内容async function getData(){ let d = await test(); console.log(d);}getData(); 案例async和await配合Promise使用 12345678910111213function test(){ return new Promise((res,rej)=>{ setTimeout(function(){ let name = "小明"; res(name); },1000); })}async function getData(){ let data = await test(); console.log(data);} 通过async_await判断wwwroot下有几个目录 12345678910111213141516171819202122232425262728293031323334353637// 判断wwwroot文件夹下所有的文件,若是文件夹,则存到一个数组中const fs = require('fs');function getDir(path){ return new Promise((res,rej)=>{ fs.stat(path,(err,data)=>{ if(err){ console.log(err); rej(err); return; } if(data.isDirectory()){ res(true); }else{ res(false); } }) })}function main(){ let path = './wwwroot'; let dirArr = []; fs.readdir(path,async (err,data)=>{ if(err){ console.log(err); return; } for(let i = 0; i < data.length; i++){ if(await getDir(path + '/' + data[i])){ dirArr.push(data[i]); } } console.log(dirArr); })}main();","link":"/NodeJs/6.async.await%E5%92%8Ces6%E4%B8%AD%E5%B8%B8%E7%94%A8%E7%9A%84%E8%AF%AD%E6%B3%95/"},{"title":"静态服务器的路由实现","text":"1.封装web静态服务器1.在routes.js中重新暴露一个方法,名为static12345678910111213141516171819202122232425262728293031323334353637const fs = require('fs');const path = require('path');const url = require('url');// 私有方法let getMime = (extName) => { let data = fs.readFileSync('./data/mime.json'); let mimeObj = JSON.parse(data.toString()); return mimeObj[extName];}//封装的web静态服务exports.static = (req, res, staticPath) => { /* / /favicon.ico */ //获取url中的路径(pathname) let pathname = url.parse(req.url).pathname; // 获取路径中的后缀名 let extName = path.extname(pathname); pathname = pathname == '/' ? '/index.html' : pathname; // 获得正确的路径 if (pathname != '/favicon.ico') { try { // 读取对应文件夹下的文件并传给浏览器 let data = fs.readFileSync('./' + staticPath + pathname); if (data) { // 获取对应后缀名的响应头 let mimeName = getMime(extName); res.writeHead(200, {'Content-Type':''+mimeName+';charset=utf-8'}); res.end(data); } } catch (error) { } }} 2.在app.js中调用该方法并且实现简单的路由访问123456789101112131415161718192021222324252627282930const http = require('http');const routes = require('./module/routes');const url = require('url');http.createServer((req,res)=>{ // 创建静态web服务 routes.static(req,res,'state'); // 路由 let pathname = url.parse(req.url).pathname; if(pathname == '/login'){ res.writeHead(200,{'Content-Type':'text/html;charset=utf-8'}); res.end("执行登录"); }else if(pathname == '/register'){ res.writeHead(200,{'Content-Type':'text/html;charset=utf-8'}); res.end("执行注册"); }else if(pathname == '/admin'){ res.writeHead(200,{'Content-Type':'text/html;charset=utf-8'}); res.end("执行后端操作"); }else{ res.writeHead(404,{'Content-Type':'text/html;charset=utf-8'}); res.end("页面不存在"); }}).listen(8080);console.log("http://127.0.0.1:8080");","link":"/NodeJs/9.node%E9%9D%99%E6%80%81%E6%9C%8D%E5%8A%A1%E5%99%A8%E5%AE%9E%E7%8E%B0%E8%B7%AF%E7%94%B1/"},{"title":"封装一个类似express的路由,静态web服务","text":"1.route模块12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697const url = require('url');const path = require('path');const fs = require('fs');// 扩展reslet changeRes = (res)=>{ res.send = (data)=>{ res.writeHead(200,{'Content-Type':'text/html;charset="utf-8"'}); res.end(data); }}// 根据后缀名获取文件类型let getMime = (extName) => { let data = fs.readFileSync('./data/mime.json'); let mimeObj = JSON.parse(data.toString()); return mimeObj[extName];}//静态web服务方法let static = (req, res, staticPath) => { /* / /favicon.ico */ //获取url中的路径(pathname) let pathname = url.parse(req.url).pathname; // 获取路径中的后缀名 let extName = path.extname(pathname); // pathname = pathname == '/' ? '/index.html' : pathname; // 获得正确的路径 try { // 读取对应文件夹下的文件并传给浏览器 let data = fs.readFileSync('./' + staticPath + pathname); if (data) { // 获取对应后缀名的响应头 let mimeName = getMime(extName); res.writeHead(200, {'Content-Type':''+mimeName+';charset=utf-8'}); res.end(data); } } catch (error) { }}let server = ()=>{ // 全局对象G let G = { _get: {}, _post: {}, staticPath: 'static' }; let app = (req,res)=>{ // 扩展res changeRes(res); // 配置静态web服务 static(req,res,G.staticPath); let pahtname = url.parse(req.url).pathname; //获得请求方式 let method = req.method.toLowerCase(); if(G['_'+method][pahtname]){ if(method == "get"){ G['_'+method][pahtname](req,res); //执行方法 }else{ let postData = ''; req.on('data',(chunk)=>{ postData += chunk; }) req.on('end',()=>{ req.body = postData; G['_'+method][pahtname](req,res); //执行方法 }) } }else{ res.writeHead(404,{'Content-Type':'text/html;charset="utf-8"'}); res.end("页面不存在"); } } // get方法 app.get = function(str,cb){ // 注册方法 G._get[str] = cb; } // post方法 app.post = function(str,cb){ // 注册方法 G._post[str] = cb; } // 配置静态web服务目录 app.static = function(staticPath){ G.staticPath = staticPath; } return app;}module.exports = server(); 2.server模块1234567891011121314151617181920212223242526272829const http = require('http');const app = require('./module/route');const ejs = require('ejs');http.createServer(app).listen(3000);console.log("http://127.0.0.1:3000");// 配置静态web目录app.static('static');// 配置路由app.get('/',(req,res)=>{ res.send("首页");})app.get('/login',(req,res)=>{ ejs.renderFile('./view/login.ejs',{},(err,data)=>{ res.send(data); })})app.post('/doLogin',(req,res)=>{ console.log(req.body); res.send(req.body);})app.get('/news',(req,res)=>{ res.send("新闻页面");}) 3.ejs页面123456789101112131415161718192021<!DOCTYPE html><html lang="en"><head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>Document</title> <link rel="stylesheet" href="./css/commit.css"></head><body> <h2>登录页面</h2> <form action="/doLogin" method="POST"> <input type="text" name="uername" value="admin"> <br> <br> <input type="password" name="password" value="123456"> <br> <br> <input type="submit" value="提交"> </form></body></html> 4.目录结构","link":"/NodeJs/13.%E5%B0%81%E8%A3%85%E4%B8%80%E4%B8%AA%E7%B1%BB%E4%BC%BCexpress%E7%9A%84%E8%B7%AF%E7%94%B1%EF%BC%8C%E9%9D%99%E6%80%81web%E6%9C%8D%E5%8A%A1/"},{"title":"url模块","text":"url模块的使用nodejs中的url模块是用来解析url地址的,可以获取对应的查询字符串 1234567891011121314const url = require('url');let api = "https://www.baidu.com?username='张三'&password='123456'";/* 1.url.parse()中传入对应的url地址; 2.第二个参数传入true后会将查询字符串转为js对象; 3.query就是对应的查询字符串对象*/let getValue = url.parse(api,true).query;let {username,password} = getValue;console.log(username+","+password); url模块的使用例子 1234567891011121314151617181920212223const http = require('http');const url = require('url');http.createServer((request,response)=>{ // 响应头 response.writeHead(200, {'Content-Type': 'text/html; charset="utf-8"'}); // 防止页面乱码 response.write('<head><meta charset="utf-8"></head>'); // 判断url是否为正常的url if(request.url != "/favicon.ico"){ let {username,pwd} = url.parse(request.url,true).query; // console.log(request.url); console.log(`姓名:${username},密码:${pwd}`); } response.write("你好 Node.js"); // 结束响应 response.end();}).listen(8080); //端口console.log("http://127.0.0.1:8080");","link":"/NodeJs/2.url%E6%A8%A1%E5%9D%97/"},{"title":"commonJS","text":"一.自定义模块的两种暴露方式1.exports(1)自定义一个方法,然后通过exports暴露12345function formateApi(api){ return "http://127.0.0.1:8080/"+api;}exports.formateApi = formateApi; 123//在另一个js文件中引入该模块即可const tools = require('./module/tools');tools.formateApi(); (2)直接通过exports暴露函数1234567exports.get = function(){ console.log("get");}exports.post = function(){ console.log("post");} 123456//使用const request = require('./module/request');request.get();request.post(); 2.module.exportsmodule.exports适合暴露封装好的对象 123456789let obj = { get: function(){ console.log("get"); }, post: function(){ console.log("post"); }}module.exports = obj; 123456//使用const request = require('./module/request');request.get();request.post(); 二.node_modules的使用 1.如果不指定相对路径,Node.js会默认引入node_modules中指定文件夹下文件名为index.js的文件 12345// NodeJs会默认引入node_modules文件夹中index.js模块const axios = require('axios');axios.post();axios.get(); 2.如果node_modules中指定的文件夹中的文件名不是index.js,则要在该文件夹中生成一个package.json文件后才能正常引入 123// 如果db文件夹中的js文件不是index.js,则会报错,解决方法是在该文件夹下生成一个package.json(npm init -y)的配置文件const db = require('db');db.add(); 【注】一般第三方模块都是放在node_modules文件夹中的","link":"/NodeJs/3.%E8%87%AA%E5%AE%9A%E4%B9%89%E6%A8%A1%E5%9D%97%E7%9A%84%E4%BD%BF%E7%94%A8/"},{"title":"管道流的使用","text":"1.读取流 2.写入流 3.管道流管道流值将一个文件读取并写入到另一个文件中的流程","link":"/NodeJs/7.fs%E4%B8%AD%E7%9A%84%E6%B5%81%E4%BB%A5%E5%8F%8A%E7%AE%A1%E9%81%93%E6%B5%81/"},{"title":"使用node搭建简单的静态web服务器","text":"1.说明1.获取url中的pathname属性路径有两种,要获得正确的路径 12345/* / /favicon.ico*///获取url中的路径(pathname)let pathname = url.parse(req.url).pathname; 2.通过fs中readFile()方法来获得对应的文件并传回浏览器12345678910111213// 读取对应文件夹下的文件并传给浏览器fs.readFile('./state'+pathname,async (err,data)=>{ if(err){ console.log(err); res.writeHead(404,{'Content-Type':'text/html;charset:utf-8'}); res.end('404'); return; } // 获取对应后缀名的响应头 let mimeName = await common.getMime(extName); res.writeHead(200,{'Content-Type':''+mimeName+';charset:utf-8'}); res.end(data);}) 3.在获得文件的过程中同时要确定相应的响应头12// 获取对应后缀名的响应头let mimeName = await common.getMime(extName); 4.获取响应头的模块12345678910111213141516// 引入一个后缀名和响应头映射的文件来返回响应头的格式exports.getFileMime = (extname) =>{ //使用Promise对象来获得异步方法中的数据 return new Promise((resolve,reject)=>{ //调用readFile()方法,查询mime.json中是否有传入的后缀名对应的响应头 fs.readFile('./data/mime.json',(err,data)=>{ if(err){ console.log(err); reject(err); return; } let mimeObj = JSON.parse(data.toString()); resolve(mimeObj[extname]); }) })} 4.1.也可以使用fs.readFileSync()方法来获取,该方法为同步方法 2.服务器代码123456789101112131415161718192021222324252627282930313233343536const http = require('http');const fs = require('fs');const path = require('path');const url = require('url');const common = require('./module/common');http.createServer((req,res)=>{ /* / /favicon.ico */ //获取url中的路径(pathname) let pathname = url.parse(req.url).pathname; // 获取路径中的后缀名 let extName = path.extname(pathname); pathname = pathname=='/' ? '/index.html':pathname; // 获得正确的路径 if(pathname != '/favicon.ico'){ // 读取对应文件夹下的文件并传给服务器 fs.readFile('./state'+pathname,async (err,data)=>{ if(err){ console.log(err); res.writeHead(404,{'Content-Type':'text/html;charset:utf-8'}); res.end('404'); return; } // 获取对应后缀名的响应头 let mimeName = await common.getMime(extName); res.writeHead(200,{'Content-Type':''+mimeName+';charset:utf-8'}); res.end(data); }) }}).listen(8080);console.log("http://127.0.0.1:8080"); 3.查询响应头代码12345678910111213141516// 引入一个后缀名和响应头映射的文件来返回响应头的格式exports.getFileMime = (extname) =>{ //使用Promise对象来获得异步方法中的数据 return new Promise((resolve,reject)=>{ //调用readFile()方法,查询mime.json中是否有传入的后缀名对应的响应头 fs.readFile('./data/mime.json',(err,data)=>{ if(err){ console.log(err); reject(err); return; } let mimeObj = JSON.parse(data.toString()); resolve(mimeObj[extname]); }) })}","link":"/NodeJs/8.%E4%BD%BF%E7%94%A8node%E6%90%AD%E5%BB%BA%E7%AE%80%E5%8D%95%E7%9A%84%E9%9D%99%E6%80%81web%E6%9C%8D%E5%8A%A1%E5%99%A8/"},{"title":"package.json文件的说明","text":"1.文件结构 name:文件名 version:版本号 description:描述 2.关于第三方模块版本的说明 “^2.3.0”:代表更新时,第一位版本号不变,后面两位更新到最新版本 “~2.3.0”:代表更新时,前两位版本号不变,最后一位更新到最新版本 “*2.3.0”:代表更新时,全部更新到最新版本 “2.3.0”:代表更新时,全部不更新,指定该版本号进行安装 【注】使用npm命令进行模块安装时,最后不加’-g’,则默认为本地安装","link":"/NodeJs/4.package.json%E6%96%87%E4%BB%B6%E7%9A%84%E8%AF%B4%E6%98%8E/"},{"title":"封装路由","text":"1.路由的封装1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162const fs = require('fs');const path = require('path');const url = require('url');const ejs = require('ejs');// 私有方法let getMime = (extName) => { let data = fs.readFileSync('./data/mime.json'); let mimeObj = JSON.parse(data.toString()); return mimeObj[extName];}// 封装路由let app = { static(req, res, staticPath) { //获取url中的路径(pathname) let pathname = url.parse(req.url).pathname; // 获取路径中的后缀名 let extName = path.extname(pathname); pathname = pathname == '/' ? '/index.html' : pathname; // 获得正确的路径 if (pathname != '/favicon.ico') { try { // 读取对应文件夹下的文件并传给浏览器 let data = fs.readFileSync('./' + staticPath + pathname); if (data) { // 获取对应后缀名的响应头 let mimeName = getMime(extName); res.writeHead(200, { 'Content-Type': '' + mimeName + ';charset=utf-8' }); res.end(data); } } catch (error) { } } }, login(req,res){ res.end("login"); }, news(req,res){ res.end("news"); }, form(req,res){ ejs.renderFile('./views/form.ejs',{},(err,data)=>{ res.writeHead(200,{'Content-Type':'text/html,charset="utf-8"'}); res.end(data); }) }, doLogin(req,res){ let postData = ''; req.on('data',(chunk)=>{ postData += chunk; }) req.on('end',()=>{ console.log(postData); res.end(postData); }) }, error(req,res){ res.end("error"); }}module.exports = app; 2.使用123456789101112131415161718192021const http = require('http');const routes = require('./module/routes');const url = require('url');http.createServer((req,res)=>{ // 创建静态web服务 routes.static(req,res,'state'); // 获取请求类型 // console.log(req.method); // 路由 let pathname = url.parse(req.url).pathname.replace("/",""); try { routes[pathname](req,res); } catch (error) { routes['error'](req,res); }}).listen(8080);console.log("http://127.0.0.1:8080");","link":"/NodeJs/11.%E5%B0%81%E8%A3%85%E8%B7%AF%E7%94%B1/"},{"title":"EJS","text":"1.下载ejs12//本地安装ejscnpm i ejs --save 2.引入ejs,并创建ejs页面1const ejs = require('ejs'); 1234567891011121314151617<!DOCTYPE html><html lang="en"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>Document</title> </head> <body> <h2>这是一个登录页面</h2> <h3><%=msg%></h3> <ul> <%for(let i = 0; i < list.length; i++){%> <li><%=list[i].msg%></li> <%}%> </ul> </body></html> 3.模拟将数据库中的数据传回ejs页面并进行渲染12345678910111213141516171819//将数据传去ejs页面if(pathname == '/login'){ let msg = "数据库中获取的数据"; let list = [ {msg:"新闻111"}, {msg:"新闻222"}, {msg:"新闻333"}, {msg:"新闻444"}, {msg:"新闻555"} ]; //ejs渲染页面 ejs.renderFile('./views/login.ejs',{msg:msg,list:list},(err,data)=>{ res.writeHead(200,{'Content-Type':'text/html;charset=utf-8'}); res.end(data); })} 1234567//ejs上渲染<h3><%=msg%></h3><ul> <%for(let i = 0; i < list.length; i++){%> <li><%=list[i].msg%></li> <%}%></ul> 2.get和post1.获取get传值get传值是拼接在url后面的查询字符串,可以直接通过url模块提供的parse解析获取 123456else if(pathname == '/news'){ // 获取get传值 let {page,title} = url.parse(req.url,true).query; console.log(page+","+title); res.end(page+","+title);} 2.获取post传值1.新建一个form页面,里面有简单的form表单 123456789101112131415<!DOCTYPE html><html lang="en"><head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>Document</title></head><body> <form action="/doLogin" method="POST"> <input type="text" name="username" placeholder="用户名" autofocus="off"><br> <input type="password" name="password" placeholder="密码"><br> <input type="submit" value="提交"> </form></body></html> 2.在路由中配置访问form页面的路由 123456else if(pathname == '/form'){ // 获取post传值 ejs.renderFile('./views/form.ejs',{},(err,data)=>{ res.end(data); })} 3.post请求提交到的路由地址为’/doLogin’,在这里面解析post请求 1234567891011else if(pathname == '/doLogin'){ // 获取POST传值 let postData = ''; req.on('data',(chunk)=>{ postData += chunk; }) req.on('end',()=>{ console.log(postData); res.end(postData); })} 【注】一般加载页面用的都是get请求,post请求大多数在提交数据时使用 可以通过req.method查看 12// 获取请求类型console.log(req.method);","link":"/NodeJs/10.EJS,Get,Post/"},{"title":"封装一个类似express的路由","text":"1.实现步骤1.定义一个app方法 123let app = (req,res)=>{} 2.定义一个app的私有方法app.get() 1234//str:传入的路径,cb:即callback,回调函数app.get = (str,cb)=>{} 3.注册方法 12345678910111213141516//1.定义一个G空对象let G = {};//2.在app.get()方法中注册app.get = (str,cb)=>{ G[str] = cb;}//3.调用该方法let app = (req,res)=>{ if(G['/login']){ G['/login'](req,res); }}//4.执行方法app.get('/login',(req,res)=>{ console.log("执行登录");}) 4.封装成通用方法,并暴露该方法 1234567891011121314151617181920const url = require('url');let G = {};let app = function(req,res){ let pahtname = url.parse(req.url).pathname; if(G[pahtname]){ G[pahtname](req,res); }else{ res.writeHead(404,{'Content-Type':'text/html;charset="utf-8"'}); res.end("页面不存在"); }}app.get = function(str,cb){ // 注册方法 G[str] = cb;}//暴露module.exports = app; 5.使用http模块实现调用路由 12345678910111213141516171819const http = require('http');const app = require('./module/route');http.createServer(app).listen(3000);console.log("http://127.0.0.1:3000");// 配置路由app.get('/',(req,res)=>{ res.writeHead(200,{'Content-Type':'text/html;charset="utf-8"'}); res.end("首页");})app.get('/login',(req,res)=>{ res.writeHead(200,{'Content-Type':'text/html;charset="utf-8"'}); res.end("登录页");})app.get('/news',(req,res)=>{ res.writeHead(200,{'Content-Type':'text/html;charset="utf-8"'}); res.end("新闻页面");}) 2.加入POST路由,并通过req.body获取数据,并封装一个res.send方法1.封装一个res.send()方法12345678function changeRes(res){ res.send = (data)=>{ res.writeHead(200,{'Content-Type':'text/html;charset="utf-8"'}); res.end(data); }}//在app中调用该方法changeRes(res); 2.改造server方法,封装一个post方法123456789101112131415161718192021222324252627282930313233343536373839404142434445let server = ()=>{ let G = {}; G._get = {}; G._post = {}; let app = function(req,res){ // 扩展res changeRes(res); let pahtname = url.parse(req.url).pathname; //获得请求类型 let method = req.method.toLowerCase(); if(G['_'+method][pahtname]){ if(method == "get"){ //get执行如下方法 G['_'+method][pahtname](req,res); //执行方法 }else{ //post执行如下方法 let postData = ''; req.on('data',(chunk)=>{ postData += chunk; }) req.on('end',()=>{ //将获得的post数据放入req.body中 req.body = postData; G['_'+method][pahtname](req,res); //执行方法 }) } }else{ res.writeHead(404,{'Content-Type':'text/html;charset="utf-8"'}); res.end("页面不存在"); } } // get方法 app.get = function(str,cb){ // 注册方法 G._get[str] = cb; } app.post = function(str,cb){ // 注册方法 G._post[str] = cb; } return app;} 3.调用方法123456789101112131415161718192021222324const http = require('http');const app = require('./module/route');const ejs = require('ejs');http.createServer(app).listen(3000);console.log("http://127.0.0.1:3000");/*配置路由*/app.get('/',(req,res)=>{ res.send("首页");})app.get('/login',(req,res)=>{ ejs.renderFile('./view/login.ejs',{},(err,data)=>{ res.send(data); })})app.post('/doLogin',(req,res)=>{ console.log(req.body); res.send(req.body);})app.get('/news',(req,res)=>{ res.send("新闻页面");})","link":"/NodeJs/12.%E5%B0%81%E8%A3%85%E4%B8%80%E4%B8%AA%E7%B1%BB%E4%BC%BCexpress%E7%9A%84%E8%B7%AF%E7%94%B1/"},{"title":"Egg、Midway系列相关笔记","text":"1.mysql 安装 1npm install egg-mysql -S 配置 123456789101112131415161718192021222324252627// /config/plugin.tsmysql: { enable: true, package: 'egg-mysql',},//config/config.default.ts// mysqlconfig.mysql = { // 单数据库信息配置 client: { // host host: '127.0.0.1', // 端口号 port: '3306', // 用户名 user: 'root', // 密码 password: '123456', // 数据库名 database: 'test', }, // 是否加载到 app 上,默认开启 app: true, // 是否加载到 agent 上,默认关闭 agent: false,}; 2.cors 安装 1npm i egg-cors -S 配置 123456789101112131415161718192021// /config/plugin.tscors: { enable: true, package: 'egg-cors',},//config/config.default.ts // 跨域 config.security = { csrf: { //关闭csrf enable: false, ignoreJSON: true, }, domainWhiteList: [ 'http://127.0.0.1:8000','http://127.0.0.1:5500' ], // 配置白名单 }; config.cors = { // origin: '*', //允许所有跨域访问,注释掉则允许上面 白名单 访问 credentials: true, // 允许 Cookie 跨域 allowMethods: 'GET,HEAD,PUT,POST,DELETE,PATCH,OPTIONS', }; 3.csrf12345//config/config.default.ts//单独关闭csrfconfig.security = { csrf: false,};","link":"/NodeJs/%E6%A1%86%E6%9E%B6/Egg_Midway/1.%E5%B8%B8%E7%94%A8%E6%8F%92%E4%BB%B6%E7%9A%84%E9%85%8D%E7%BD%AE/"},{"title":"express连接mongoDB实例","text":"1.创建连接12345678910111213141516171819202122232425262728// utils/mongoConfig.jsconst mongoose = require('mongoose');// 创建连接实例const mongoUrl = 'mongodb://admin:123456@127.0.0.1:27017/express-test?authSource=admin';mongoose.connect(mongoUrl, { useNewUrlParser: true, useUnifiedTopology: true });// 创建User集合连接const UserSchema = mongoose.Schema({ name: String, age: Number});const User = mongoose.model('User', UserSchema, 'user');// 创建student集合连接const StudentSchema = mongoose.Schema({ name: String, grade: Number, class: String});const Student = mongoose.model('Student', StudentSchema, 'student');module.exports = { User, Student}; 2.使用12345678910111213141516171819// routers/index.jsrouter.get('/mongo', async (req, res) => { const result = await User.aggregate([ { $lookup: { from: 'student', localField: 'name', foreignField: 'name', as: 'item' } }, { $project: {'name':1,'age':1,'item.grade':1,'item.class':1} }, { $group: { _id:"$item.class", count:{$sum: 1} } } ]) res.send(result);})","link":"/NodeJs/%E6%A1%86%E6%9E%B6/Express/5.express%E8%BF%9E%E6%8E%A5mongoDB%E5%AE%9E%E4%BE%8B/"},{"title":"mysql的使用","text":"mysql 安装 1npm i mysql --save 配置 123456789101112131415161718192021222324252627const mysql = require('mysql');// 连接数据库配置const config = { host: 'localhost', port: '3306', user: 'root', password: '123456', database: 'test'}// 创建数据库连接池module.exports = { sqlConnection: (sql,sqlArr,callBack) => { const pool = mysql.createPool(config); pool.getConnection((err,conn) => { if(err) { console.log(err); return; } // 事件驱动回调 conn.query(sql,sqlArr,callBack); // 关闭连接池 conn.release(); }) }}","link":"/NodeJs/%E6%A1%86%E6%9E%B6/Express/2.%E4%BD%BF%E7%94%A8mysql/"},{"title":"express编写接口","text":"routes1234567891011121314151617181920212223242526272829303132333435363738394041424344454647const express = require('express');const router = express.Router();const mysql = require('../utils/dbconfig');/* GET home page. */router.get('/', (req,res) => { mysql.sqlConnection({ sql: 'select * from user', sqlArr: [], callBack: (err,data) => { if(err) { console.log(err); return; } res.send(data); } });});router.get('/user',(req,res) => { const { id } = req.query; mysql.sqlConnection({ sql: 'select * from user where id=?', sqlArr: [id], callBack: (err,data) => { if(err) { console.log(err); return; } res.send(data); } });});router.post('/userpost', (req,res) => { const {name,password} = req.body; console.log(name+'---'+password);});router.post('/userpost/:id',(req,res) => { const {id} = req.params; console.log(id);});module.exports = router; app.js1234567891011121314151617181920212223const express = require('express');const path = require('path');const cookieParser = require('cookie-parser');const logger = require('morgan');const cors = require('cors');// 导入路由const indexRouter = require('./routes/index');const app = express();app.use(logger('dev'));app.use(express.json());app.use(express.urlencoded({ extended: false }));app.use(cookieParser());app.use(express.static(path.join(__dirname, 'public')));app.use(cors());// 使用路由app.use('/', indexRouter);module.exports = app;","link":"/NodeJs/%E6%A1%86%E6%9E%B6/Express/3.%E7%BC%96%E5%86%99%E6%8E%A5%E5%8F%A3/"},{"title":"express连接mysql实例","text":"1.express安装1npm i express express-generator -g 2.nodemon安装1npm i nodemon --save 修改package.json 12345{ "script": { "start": "nodemon ./bin/www" }} 3.操作mysql 安装mysql1npm i mysql --save 配置123456789101112131415161718192021222324252627282930// utils/dbConfig.jsconst mysql = require('mysql');// 数据库连接配置const config = { host: '127.0.0.1', port: '3306', user: 'root', password: '123456', database: 'test'}// 使用连接池连接mysqlmodule.exports = { sqlConnection: ({sql,sqlArr,callBack}) => { // 创建连接池 const pool = mysql.createPool(config); pool.getConnection((err,conn) => { if(err) { console.log(err); return; } // 事件驱动回调 conn.query(sql,sqlArr,callBack); // 释放连接 conn.release(); }) }} 操作数据库1234567891011121314151617181920212223242526272829303132333435363738394041424344454647// routes/index.jsconst express = require('express');const router = express.Router();const mysql = require('../utils/dbConfig');/** * 获取所有信息 */router.get('/', (req,res) => { mysql.sqlConnection({ sql: 'select * from user', sqlArr: [], callBack: (err,data) => { if(err) { console.log(err); return; } res.send(data); } })});/** * 通过用户名和密码获取信息 * @body name * @body password */router.post('/user',(req,res) => { const { name, password } = req.body; if(name && password) { mysql.sqlConnection({ sql: 'select * from user where name=? and password=?', sqlArr: [name,password], callBack: (err,data) => { if(err) { console.log(err); return; } res.send(data); } }); }});module.exports = router; 4.接口测试 VS Code安装REST Client插件 新建 .http文件 测试12345678910111213@url=http://127.0.0.1:3000@json=Content-Type: application/json###Get {{url}}/###POST {{url}}/user{{json}}{ "name": "小明", "password": "123456"} 5.优化配置 数据库配置优化123456789101112131415161718192021222324252627282930313233343536// utlis/dbConfig.jsconst mysql = require('mysql');// 数据库连接配置const config = { host: '127.0.0.1', port: '3306', user: 'root', password: '123456', database: 'test'}// 使用连接池连接mysqlmodule.exports = { sqlConnection: (sql, sqlArr = []) => { // 创建Promise对象 return new Promise((resolve, reject) => { // 创建连接池 const pool = mysql.createPool(config); // 运行结果操作 pool.getConnection((err,conn) => { // 返回错误信息 err && reject(err); // 事件驱动回调 conn.query(sql, sqlArr, (err, result) => { // 返回错误信息 err && reject(err); // 返回查询结果 resolve(result); }) // 释放连接 conn.release(); }) }) }} 使用优化123456789101112131415// router/index.jsconst express = require('express');const router = express.Router();// const userController = require('../controller/user');const mysql = require('../utils/dbConfig');router.get('/', async (req, res) => { const sql = 'select * from user'; const result = await mysql.sqlConnection(sql); res.send(result);});module.exports = router; 6.分层操作 service1234567891011121314// service/userServiceconst mysql = require('../utils/dbConfig');class userService { getUser = async () => { const sql = 'select * from user'; const result = await mysql.sqlConnection(sql); return result; }}module.exports = new userService(); controller123456789101112// controller/userControllerconst userService = require('../service/user');class UserController { getUser = async (req, res) => { const result = await userService.getUser(); res.send(result); }}module.exports = new UserController(); router123456789// routerconst express = require('express');const router = express.Router();const userController = require('../controller/user');router.get('/', userController.getUser);module.exports = router;","link":"/NodeJs/%E6%A1%86%E6%9E%B6/Express/4.express%E8%BF%9E%E6%8E%A5mysql%E5%AE%9E%E4%BE%8B/"},{"title":"使用mathjs进行高精度运算","text":"前言mathjs是用于JavaScript和NodeJS的数学库。它内置大量函数与常量,并提供集成解决方案来处理不同的数据类型,如数字,大数字,复数,分数,单位和矩阵等 使用 安装 1npm i mathjs 基本使用format用于格式化输出,其中precision参数用于指定格式化精度的位数 123456789101112131415161718192021222324252627import { create, all } from "mathjs";// 创建mathjs实例const mathjs = create(all, { number: "BigNumber", precision: 20,});// addconst add = (num1, num2) => +mathjs.format(mathjs.add(1, 2), { precision: 16,})// subtractconst subtract = (num1, num2) => +mathjs.format(mathjs.subtract(1, 2), { precision: 16,})// multiplyconst multiply = (num1, num2) => +mathjs.format(mathjs.multiply(1, 2), { precision: 16,})// divideconst multiply = (num1, num2) => +mathjs.format(mathjs.divide(1, 2), { precision: 16,}) 链式计算支持输入一个初始值进行链式计算 123456789import { create, all } from "mathjs";const mathjs = create(all, { number: "BigNumber", precision: 20,});// 111console.log(mathjs.format(mathjs.chain(1.11).multiply(100).done(), {precision: 16})); 配置mathjs支持配置来创建实例 12345678910import {} from 'mathjs';const maht = create(all, { epsilon: 1e-12, matrix: 'Matrix', number: 'number', precision: 64, predictable: false, randomSeed: null}) 支持的配置有 epsilon,用于测试两个比较值之间是否相等的最小相对差异。所有关系功能都使用该值。默认值为 1e-12 matrix,函数的矩阵输出的默认类型。可用值为:( 'Matrix'默认值)或'Array'。在可能的情况下,函数的矩阵输出类型取决于函数输入:将数组作为输入将返回数组,将矩阵作为输入将返回矩阵。如果没有矩阵作为输入,则输出类型由option决定matrix。对于混合矩阵输入,将始终返回矩阵。 number,指定实例的输入输出类型,默认值为number,可选值为:number | BigNumber precision,BigNumbers的最大有效位数。此设置仅适用于BigNumbers,不适用于数字。默认值为 64。 predictable,功能的可预测输出类型。如果为true,则输出类型仅取决于输入类型。如果为false(默认),则输出类型可以根据输入值而变化。例如,当predictable为false时 math.sqrt(-4) 返回 complex('2i'),为true NaN时返回。以编程方式处理计算结果时,可能需要可预测的输出,但对于评估动态方程式的用户可能不方便。 randomSeed,将此选项设置为种子伪随机数生成,使其具有确定性。每次设置此选项时,都会使用提供的种子重置伪随机数生成器。例如,将其设置为每次设置该选项'a' 后将导致在第一次呼叫时 math.random() 返回 0.43449421599986604。设置为 null 使用随机种子为伪随机数生成器提供种子。默认值为 null。","link":"/%E4%BD%BF%E7%94%A8mathjs%E8%BF%9B%E8%A1%8C%E9%AB%98%E7%B2%BE%E5%BA%A6%E8%BF%90%E7%AE%97/"},{"title":"http模块","text":"Node.js自动的http模块的使用123456789101112131415161718192021// 引入http模块const http = require('http');/* request 获取url传过来的信息 response 给浏览器响应信息 */http.createServer(function (request, response) { console.log(request.url); // 设置响应头 response.writeHead(200, {'Content-Type': 'text/html;charset="utf-8"'}); // 防止页面乱码 response.write("<head><meta charset='UTF-8'></head>"); response.write("你好 Node"); // 结束响应 response.end();}).listen(8080); //端口console.log('Server running at http://127.0.0.1:8080/');","link":"/NodeJs/1.http%E6%A8%A1%E5%9D%97/"},{"title":"Corepack","text":"1.CorepackCorepack是Nodejs在16.9.0中加入的实验性阶段工具,用于协助管理你本机package manager的版本 简单来说,Corepack 会成为 Node.js 官方的内置 CLI,用来管理『包管理工具(npm、yarn、pnpm、cnpm)』,用户无需手动安装,即『包管理器的管理器』 2.体验 升级你的Nodejs版本到16.9.0。如果使用nvm管理node版本,则需要先升级nvm版本1.1.8以上才行 在你项目的package.json文件中添加packageManager属性 1234{ "name": "test", "packageManager": "pnpm@6.26.0",} 运行corepack enable命令即可激活 1234567# 激活$ corepack enable# 使用$ pnpm i# 用非声明的包管理器,则会报错$ yarnUsage Error: This project is configured to use pnpm","link":"/NodeJs/14.Corepack/"},{"title":"JavaScript内存管理","text":"js内存机制JS的内存空间分为栈内存(stack)和堆内存(heap) 栈内存:所有原始数据类型都存储在栈内存中,如果删除一个栈原始数据,遵循先进后出; 堆内存:引用数据类型会在堆内存中开辟一个空间,并且会有一个十六进制的内存地址,在栈内存中声明的变量的值就是十六进制的内存地址。 垃圾回收机制我们平常创建的所有数据类型都需要内存,所谓的垃圾回收机制就是找出那些不再使用的变量,释放出他们所占用的内存,垃圾回收会按照固定的时间间隔,周期性地执行这一操作。 垃圾回收是一把双刃剑垃圾回收机制的优势在于可以大幅简化我们开发时编写的代码,我们在大多数情况下不需要去关心声明的变量是否会有内存泄露的问题,但这也造成了我们无法掌控内存,JS没有暴露任何操作内存的API,无法对内存管理进行干预 垃圾回收的方式1.引用计数跟踪记录每个值被引用的次数,如果一个值引用次数是 0,就表示这个值不再用到了,因此可以将这块内存释放原理:每次引用加 1,被释放减 1,当这个值的引用次数变成 0 时,就将其内存空间释放。 1234let obj = { a: 10 } // 引用+1let obj1 = { a: 10 } // 引用+1obj = {} //引用减1obj1 = null //引用为0 2.标记清除标记清除指的是当变量进入环境时,这个变量标记为进入环境,而当变量离开环境时,则将其标记为“离开环境”,最后垃圾收集器完成内存清除工作,销毁那些带标记的值并回收它们所占用的内存空间(所谓的环境就是执行环境) 全局执行环境 最外围的执行环境 根据宿主环境的不同表示的执行环境的对象也不一样,在浏览器中全局执行环境被认为是 window 对象 全局变量和函数都是作为 window 对象的属性和方法创建的 某个执行环境中的所有代码执行完毕后,该环境被销毁,保存在其中的所有变量和函数定义也随之销毁(全局执行环境只有当关闭网页的时候才会被销毁) 环境栈(局部) 每个函数都有自己的执行环境,当执行流进入一个函数时,函数的环境就会被推入一个环境栈中。而在函数执行之后,栈将其环境弹出,把控制权返回给之前的执行环境,ECMAScript 程序中的执行流正是由这个方便的机制控制着 V8内存管理机制V8引擎限制内存的原因 V8 最初为浏览器设计,不太可能遇到大量内存的使用场景(表层原因) 防止因为垃圾回收所导致的线程暂停执行的时间过长(深层原因,按照官方的说法以 1.5G 的垃圾回收为例,v8 做一次小的垃圾回收需要 50 毫秒以上,做一次非增量的垃圾回收需要 1 秒以上,这里的时间是指 javascript 线程暂停执行的时间,这是不可接受的, v8 直接限制了内存的大小,如果说在 node.js 中操作大内存的对象,可以通过去修改设置去完成,或者是避开这种限制,1.7g 是在 v8 引擎方面做的限制,我们可以使用 buffer 对象,而 buffer 对象的内存分配是在 c++层面进行的,c++的内存不受 v8 的限制) V8的回收策略 v8 采用可一种分代回收的策略,将内存分为两个生代;新生代和老生代 v8 分别对新生代和老生代使用不同的回收算法来提升垃圾回收效率 新生代回收from 和 to 组成一个Semispace(半空间)当我们分配对象时,先在 from 对象中进行分配,当垃圾回收运行时先检查 from 中的对象,当obj2需要回收时将其留在 from 空间,而ob1分配到 to 空间,然后进行反转,将 from 空间和 to 空间进行互换,进行垃圾 回收时,将 to 空间的内存进行释放,简而言之 from 空间存放不被释放的对象,to 空间存放被释放的对象,当垃圾回收时将 to 空间的对象全部进行回收 新生代对象的晋升(新生代中用来存放,生命较短的对象,老生代存放生命较长的对象) 在新生代垃圾回收的过程中,当一个对象经过多次复制后依然存活,它将会被认为是生命周期较长的对象,随后会被移动到老生代中,采取新的算法进行管理 在 From 空间和 To 空间进行反转的过程中,如果 To 空间中的使用量已经超过了 25%,那么就将 From 中的对象直接晋升到老生代内存空间中 老生代垃圾回收(有 2 种回收方法)老生代内存空间是一个连续的结构 标记清除(Mark Sweep) Mark Sweep 是将需要被回收的对象进行标记,在垃圾回收运行时直接释放相应的地址空间,红色的区域就是需要被回收的 标记合并(Mark Compact) Mark Compact 将存活的对象移动到一边,将需要被回收的对象移动到另一边,然后对需要被回收的对象区域进行整体的垃圾回收","link":"/JavaScript%E5%86%85%E5%AD%98%E7%AE%A1%E7%90%86/"},{"title":"line-break","text":"用于设置中日韩三种语言遇到标点符号时的换行规则 12345678910111213.class { /* Keyword values */ line-break: auto; /* 使用默认的断行规则分解文本 */ line-break: loose; /* 使用尽可能松散(least restrictive)的断行规则分解文本。一般用于短行的情况,如报纸 */ line-break: normal; /* 使用最一般(common)的断行规则分解文本。 */ line-break: strict; /* 使用最严格(stringent)的断行原则分解文本。 */ line-break: anywhere; /* 在每个印刷字符单元(typographic character unit)的周围,都有一个自动换行(soft wrap)的机会,包括任何标点符号(punctuation character)或是保留的空白字符(preserved white spaces),或是单词之间。但忽略任何用于阻止换行的字符,即使是来自 GL、WJ 或 ZWJ 字符集的字符,或是由 word-break 属性强制的字符。不同的换行机会拥有相同的优先级。也不应用断字符(hyphenation,可能是 "-")*/ /* Global values */ line-break: inherit; line-break: initial; line-break: unset;}","link":"/line-break/"},{"title":"随记","text":"1.鼠标滚轮事件wheel在鼠标滚轮滚动时触发,可以用来判断页面是向上滚动还是向下滚动 123456789useEffect(() => { const DOMMouseScroll = (e: WheelEvent) => { // 滚轮事件中的deltaY属性可以用来判断滚轮是向上滚动还是向下滚动 const log: string = e.deltaY > 0 ? '向下滚动' : '向上滚动' console.log(log); }; window.addEventListener('wheel', DOMMouseScroll); return () => removeEventListener('wheel', DOMMouseScroll);}) 2. cross-envcross-env用于配置多个开发环境 安装1yarn add cross-env --dev 使用1234"scripts": { "start": "cross-env PORT=8080 nodemon ./bin/www", "start:test": "cross-env PORT=8080 API_ENV=dev nodemon ./bin/www"} 导入配置的变量12const PORT: string = process.env.PORTconst API_ENV: string = process.env.API_ENV; 3. export和export default的区别 导出方式 export可以先定义再导出,也可以在导出时再定义变量 export在一个模块中可以出现多次 export default是模块的默认导出方式,只能导出已经定义好的变量 export default在模块中只能出现一次 导入方式 export导出的对象,可以使用解构的方式进行导入 1import { a, b } from 'aaa'; export default导出的对象,要使用变量名的方式导入 1import '变量名' from 'aaa'; 4.exports和module.exports的区别区别在于导出方式的不同,exports导出的是模块函数,module.exports导出的是一个对象 导出1234567891011const API_ENV = process.env.API_ENV;const PORT = process.env.PORT;exports.PORT = PORT;exports.API_ENV = API_ENV;module.exports = { PORT, API_ENV,} 导入1const { PORT, API_ENV } = require('../config.js'); 5.git pull冲突问题解决合并修改123git stash #封存修改git pull origin mastergit stash pop #把修改还原 注: git stash:备份当前工作区内容,从最近的一次提交中读取相关内容,让工作区保证和上次提交的内容一致。同时,将当前工作区内容保存到Git栈中 git pull:拉取服务器上当前分支代码 git stash pop:从Git栈中读取最近一次保存的内容,恢复工作区相关内容。同时,用户可能进行多次stash操作,需要保证后stash的最先被取到,所以用栈(先进后出)来管理;pop取栈顶的内容并恢复 git stash list:显示Git栈内的所有备份,可以利用这个列表来决定从那个地方恢复。 git stash clear:清空Git栈放弃本次修改(不建议使用)12git reset --hardgit pull origin master 6.CSS文本省略 单行文本省略12345width: 200px;display: block;overflow: hidden;text-overflow: ellipsis;white-space: nowrap; 多行文本省略12345width: 100px;display: -webkit-box;-webkit-box-orient: vertical;-webkit-line-clamp: 3;overflow: hidden; 7.git无法切换到刚创建的远程分支123git fetch --allgit reset --hard origin/mastergit fetch 8.JavaScript几个特殊运算符的用法注:JavaScript中null,undefined,false,"",NaN都会被判定为false ||和??的区别 ||和??主要用于默认值的赋值 12345// || 运算符const vaule = a || b; // 当左侧为 null,undefined,"",false,NaN时,右侧的值会生效// ?? 运算符const value = a ?? b; // 当且仅当左侧的值为 null 或 undefined时,右侧的值才会生效 &&运算符 &&运算符一般用于短路判断,也可用于默认值的赋值 123a && b; // 当且仅当 a的判断值为true时,才会执行b语句const value = a && b; // 当且仅当 a的判断值为true时,才会将b赋值给value,否则将a赋值给value !!运算符 !!运算符可以将值快速转换为它对应的判断值,可以在需要返回布尔值的数据遍历函数中使用(filter some find findIndex 等) 12const a = null;console.log(!!a); // 输出 false 9.connect-multiparty 可以用于express编写框架上传文件接口1234567891011121314151617181920212223242526272829303132333435const express = require('express');const multipart = require('connect-multiparty');const path = require('path');const Oss = require('ali-oss');const fs = require('fs');const app = express();app.use(express.json());const multipartMiddleware = multipart({ uploadDir: path.resolve(__dirname, 'static')});const client = new Oss({ endpoint: 'You endpint', accessKeyId: 'You AccessKeyId', accessKeySecret: 'You AccessKeySecret', bucket: 'You Bucket'});app.post('/test', multipartMiddleware, async (req, res) => { const file = Object.values(req.files)[0]; console.log(file); try { const result = await client.put(`express-test/${file.name}`, file.path); res.send(result.url); } catch (error) { res.send(error); throw error; } finally { fs.unlinkSync(file.path); };});app.listen(3000, () => console.log('http//127.0.0.1:3000')); 10.midway.js上传图片接口demo 具体可以参考egg官网的上传图片demo 下载egg-oss 如果采用Stream模式上传文件,还需要下载stream-wormhole依赖 1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859import { Controller, Post, Provide, Inject } from '@midwayjs/decorator';import { Context } from 'egg';import * as path from 'path';const sendToWormhole = require('stream-wormhole');@Provide()@Controller()export class ClientController { @Inject() ctx: Context; /** * file方式图片上传 * */ @Post('/test/file') async testClient() { const file = this.ctx.request.files[0]; const name = `egg-multipart-test/${path.basename(file.filename)}`; let result: any; try { result = await this.ctx.oss.put(name, file.filepath); } catch (error) { return Promise.reject(error); } finally { fs.unlinkSync(file.filepath); } return { code: 200, message: '图片上传成功', data: result.url, }; } /** * Stream方式图片上传 * */ @Post('/test/file') async testClient() { const stream = await this.ctx.getFileStream(); const name = 'egg-multipart-test/' + path.basename(stream.filename); let result; try { result = await this.ctx.oss.putStream(name, stream); } catch (err) { await sendToWormhole(stream); throw new Error(err); } return { code: 200, message: '图片上传成功', data: result.url, }; }} 11.React对props.children的操作 children类似与Vue中的插槽语法,但会比后者灵活得多,在TypeScript中,children的类型一般被定义为React.ReactNode | React.ReactNode[] React提供了相应的Api来操作children 12345678{ React.Children.map(props.children, (childItem, index) => ( <> {childItem} {index !== props.children.length - 1 && <Divider type="vertical" />} </> ))} 1234{React.Children.map(props.children, (childItem, i) => { if (i < 1) return return child})} 12.BigInt BigInt是一种新的数据类型,用于当整数值大于Number数据类型支持的范围时。这种数据类型允许我们安全地对大整数执行算术操作,表示高分辨率的时间戳,使用大整数id,等等,而不需要使用库 创建 bigint 的方式有两种:在一个整数字面量后面加 n 或者调用 BigInt 函数,该函数从字符串、数字等中生成 bigint。123const bigint = 1234567890123456789012345678901234567890n;// orconst sameBigint = BigInt("1234567890123456789012345678901234567890"); 运算 BigInt大多数情况下可以像常规数字类型一样使用,计算后的返回值也是BigInt12alert(1n + 2n); // 3alert(5n / 2n); // 2 BigInt不能和number类型混用1alert(1n + 2); // Error: Cannot mix BigInt and other types 13.qs axios paramsSerializer axios在发起get请求时如果遇到数组可以使用paramsSerializer和qs进行参数的序列化 可以通过yarn add qs进行安装qs依赖包123456axios.get(url, { params, paramsSerializer: (params) => { return qs.stringify(params, {arrayFormat: 'repeat'}) }}) 其他示例12345678//形式: ids=1&ids=2&id=3qs.stringify({ids: [1, 2, 3]}, { indices: false })//形式: ids[0]=1&aids1]=2&ids[2]=3qs.stringify({ids: [1, 2, 3]}, {arrayFormat: ‘indices‘})//形式:ids[]=1&ids[]=2&ids[]=3qs.stringify({ids: [1, 2, 3]}, {arrayFormat: ‘brackets‘})//形式: ids=1&ids=2&id=3qs.stringify({ids: [1, 2, 3]}, {arrayFormat: ‘repeat‘}) 14.滚动加载相关业务 三要素: scrollHeight scrollTop innerHeight和底部的高度123456789101112131415161718192021222324252627useEffect(() => { const scroll = () => { const scrollTop = Math.max(document.documentElement.scrollTop, document.body.scrollTop, window.scrollY); const scrollHeight = Math.max(document.documentElement.scrollHeight, document.body.scrollHeight); // 判断滚动条是否接触底部 if (scrollHeight - window.innerHeight - scrollTop < 294) { setAnchorAffix({ affix: true, top: anchorAffix.affix ? anchorAffix.top : (scrollTop - 35), // 144 是顶部菜单栏的高度 }); } else { setAnchorAffix({ affix: false, top: 0, }); } // 判断右侧菜单是否开启固定定位 if (scrollTop) { setIsFixed(!anchorAffix.affix); setOffsetLeft(getOffsetLeft()); } else { setIsFixed(false); } }; window.addEventListener('scroll', scroll); return () => { window.removeEventListener('scroll', scroll); }; }, [anchorAffix.affix]); 15.a链接下载导出文件 a链接的download属性可以指定下载的文件名,仅在同源策略下有效1234567const blob = await fetch(Object.values(res?.data)?.[0] as string).then(res => res.blob())const aDownLoad = document.createElement('a');const downLoadUrl = window.URL.createObjectURL(blob); // 将下载到的文件使用createObjectURL实例化成临时的urlaDownLoad.href = downLoadUrlaDownLoad.download = "name";aDownLoad.click();window.URL.revokeObjectURL(downLoadUrl); // 下载完毕时清除临时的url 16.倒计时12345678910111213141516171819202122232425262728293031import React, { useState, useEffect, useRef } from 'react';import { Button } from 'antd';export default () => { const [text, setText] = useState('获取验证码'); const timerRef = useRef<number>(); const handleClick = () => { timerRef.current && clearInterval(timerRef.current); let time = 10; timerRef.current = window.setInterval(() => { if (time === 0) { clearInterval(timerRef.current); timerRef.current = undefined; setText('重新获取验证码'); time = 10; } else { time -= 1; setText(`${time}后重新获取`) } }, 1000); } return ( <div> <Button onClick={handleClick} disabled={!!timerRef.current}>{text}</Button> </div> );}; 17.锚点平滑滚动 使用css的scroll-behavior属性 在滚动条所在的父元素中加入该属性,就可以实现锚点平滑滚动效果12345678910111213141516171819202122232425262728293031323334353637383940414243444546import React, { useState } from 'react';import style from './style.scss';export default () => { const [arr] = useState([1, 2, 3, 4, 5, 6, 7, 8, 9, 10]); return ( <div className={style.wrap}> <ul className={style.ul1}> {arr.map((item, index) => ( <li key={index} style={{ marginBottom: 200 }} id={`${item}`}> <h1>{item}</h1> </li> ))} </ul> <ul className={style.sidebar}> {arr.map((item, index) => ( <li key={index}> <h1> <a href={`#${item}`}>{item}</a> </h1> </li> ))} </ul> </div> );};// css.wrap { display: flex; justify-content: space-between; .sidebar { position: fixed; top: 0; right: 0; } .ul1 { width: 400px; height: 400px; overflow-y: scroll; scroll-behavior: smooth; }} 18.使用dayjs替换antd组件中默认的momentantd日期组件默认集成了momentjs这个日期处理库,这会使得打包的组件体积过大,可以使用dayjs替换antd组件中的moment 下载dayjs: npm i datjs -S 下载antd-dayjs-webpack-plugin: npm i antd-dayjs-webpack-plugin -D 1.修改webpack配置 参考: https://github.com/ant-design/antd-dayjs-webpack-plugin 1234567const AntdDayjsWebpackPlugin = require('antd-dayjs-webpack-plugin');module.exports = { // ... plugins: [ new AntdDayjsWebpackPlugin() ]} 2.国际化123456789101112131415161718// ...import { ConfigProvider } from 'antd';import zhCN from 'antd/lib/locale/zh_CN';import dayjs from 'dayjs';import dayjsLocal from 'dayjs/locale/zh-cn';// ...// dayjs国际化dayjs.locale(dayjsLocal)const App = () => { return ( /* antd国际化 */ <ConfigProvider locale={zhCN}> <Routes /> </ConfigProvider> )};render(<App />, document.getElementById('root')); 3. TypeScript声明修改 参考: https://github.com/ant-design/antd-dayjs-webpack-plugin/issues/23 若是使用TypeScript进行开发,还需要修改修改antd组件中默认的Moment声明,否则会报类型检测错误 在你项目的xxx.d.ts中添加以下代码 123456789// global.d.tsdeclare module 'moment' { import { Dayjs } from 'dayjs' namespace moment { type Moment = Dayjs } export = moment export as namespace moment} 19.ts-node-dev 可以实时运行TypeScript文件 安装 npm install ts-node-dev -D12345// package.json"scripts": { "dev": "cross-env tsnd -P ./tsconfig.json --respawn ./src/index.ts", "build": "cross-env tsc" }, 12345678910111213141516171819// tsconfig.json{ "include": ["src/**/*"], "exclude": ["node_modules"], "compilerOptions": { "target": "ES6", "module": "CommonJS", "allowSyntheticDefaultImports": true, "esModuleInterop": true, "sourceMap": true, "baseUrl": "./", "outDir": "./dist", "declaration": true, "declarationDir": "./dist", "moduleResolution": "Node", "noImplicitAny": false, "downlevelIteration": true, }} 20.移动端适配方案px rem vw的转换 默认情况下根元素的font-size为 1rem = 16px,但为了方便换算,我们通常设置 1rem = 100px 以750px的设计稿为例,则可以得出 750px = 7.5rem 相当于100vw=7.5rem那么1rem = 100vw / 7.5 = 13.3333vw,所以设置根元素的font-size为13.3333vw 若要兼容旧浏览器,则需要写入响应式布局 例如12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576// 相当于 320 / 7.5 = 42.667px@media screen and (max-width: 320px) { html { font-size: 42.667px; font-size: 13.3333vw; }}// 相当于375 / 7.5 = 48px@media screen and (min-width: 321px) and (max-width: 375px) { html { font-size: 48px; font-size: 13.3333vw; }}@media screen and (min-width: 376px) and (max-width:393px) { html { font-size: 52.4px; font-size: 13.3333vw }}@media screen and (min-width: 394px) and (max-width:412px) { html { font-size: 54.93px; font-size: 13.3333vw }}@media screen and (min-width: 413px) and (max-width:414px) { html { font-size: 55.2px; font-size: 13.3333vw }}@media screen and (min-width: 415px) and (max-width:480px) { html { font-size: 64px; font-size: 13.3333vw }}@media screen and (min-width: 481px) and (max-width:540px) { html { font-size: 72px; font-size: 13.3333vw }}@media screen and (min-width: 541px) and (max-width:640px) { html { font-size: 85.33px; font-size: 13.3333vw }}@media screen and (min-width: 641px) and (max-width:720px) { html { font-size: 96px; font-size: 13.3333vw }}@media screen and (min-width: 721px) and (max-width:768px) { html { font-size: 102.4px; font-size: 13.3333vw }}@media screen and (min-width: 769px) and (max-width:852px) { html { font-size: 113.4px; font-size: 13.3333vw }}@media screen and (min-width: 853px) { html { font-size: 130.4px; font-size: 13.3333vw }} 而在页面样式中,直接将测量出的px除以100就是对应的rem值 viewport缩放比例设置1<meta name="viewport" content="width=device-width,initial-scale=1.0,minimum-scale=1.0,maximum-scale=1.0,user-scalable=no"> 21.常用正则123456789101112131415161718/** 网址正则 */export const addressReg = /^(https?):\\/\\/[\\w-]+(\\.[\\w-]+)+([\\w-.,@?^=%&:/~+#]*[\\w\\-@?^=%&/~+#])?$/;/** 邮箱 */export const emailReg = /^[A-Za-z0-9\\u4e00-\\u9fa5]+@[a-zA-Z0-9_-]+(\\.[a-zA-Z0-9_-]+)+$/;/** 普通固定电话验证 */export const phoneNumberCnReg = /^((\\d{3,4})|\\d{3,4}-|\\s)?\\d{7,14}$/;/** 中国手机 */export const phoneCnReg = /^1[3456789]\\d{9}$/;/** 汉字正则 */export const regCn = /[\\u4E00-\\u9FFF]+/g;/** 排序序号为整数 */export const intReg = /^-?[1-9]?[0-9]{0,8}$/;/** 版本号 */export const versionReg = /^([0-9]|\\d\\d)(.([0-9]|\\d\\d)){2}$/;/** 银行卡号 */export const validateBankCard = /^([1-9]{1})(\\d{14}|\\d{18})$/; 22.可能会用到的网站ES5https://wangdoc.com/javascript/ ES6https://es6.ruanyifeng.com/ 在线工具https://oktools.net/ 刷题https://fe.ecool.fun/ 23.通过nvm安装Nodejs和管理包的全局化配置1.nvm下载nvm是node.js的版本管理工具,使用nvm安装node,可以实现node版本的快速切换 windows电脑下载nvm-setup.zip的安装包即可 下载地址nvm-download nvm-setup.zip(v1.1.7) 2.nvm常用的几个命令 命令 说明 nvm list available 显示可以安装的所有node.js的版本 nvm list 显示所有已安装的node.js版本 nvm use 切换到指定的nodejs版本 nvm install 安装指定版本的node.js,例如:nvm install 8.12.0 nvm uninstall 卸载指定版本的node.js,例如:nvm uninstall 8.12.0 nvm on 启用node.js版本管理 nvm off 禁用node.js版本管理(不卸载任何东西) 修改nvm下载源 在nvm目录中找到setting.txt添加以下代码 12node_mirror: https://npm.taobao.org/mirrors/node/npm_mirror: https://npm.taobao.org/mirrors/npm/ 3.npm管理使用 nvm 时,默认的 prefix 是当前激活的 Node.js 版本的安装路径。 带来一个问题是:切换版本之后,之前安装全局命令模块需要重新安装,非常不方便。 解决方案是配置统一的全局模块安装路径。 新建npm_global和npm_cache文件夹,分别用于npm包的全局安装路径和全局cache路径 npm查看各种全局路径的命令 查看当前npm包的全局安装路径 1npm prefix -g 查看当前npm包的全局cache路径 1npm config get cache 查看配置列表 1npm config ls 查看配置列表的全部信息 1npm config ls -l 【注】每次使用nvm切换node版本,最好都查看一下npm全局配置路径是否失效 npm修改全局路径命令 修改npm的包的全局安装路径 1npm config set prefix "E:\\NodeJs\\npm\\npm_global" 修改npm的包的全局cache位置 1npm config set cache "E:\\NodeJs\\npm\\npm_cache" 配置环境变量将npm包全局安装路径配置在环境变量中 此电脑 -> 属性 -> 高级系统设置 -> 环境变量 -> 系统变量 -> path ->编辑 - > 新增路径 -E:\\NodeJs\\npm\\npm_global(路径可以根据npm prefix -g查看) 4.yarn管理 安装yarn 12npm install yarn -gyarn -v 【注】如果首次安装yarn后,运行yarn -v不能显示yarn的版本,可以重启一下终端再尝试 yarn的默认缓存和存储包的路径都在C盘,所以最好在安装后也进行修改 yarn查看各种路径命令 查看 yarn 全局bin位置(prefix) 1yarn global bin 查看 yarn 全局安装位置(folder) 1yarn global dir 查看 yarn 全局cache位置(cache) 1yarn cache dir 查看配置列表 1yarn config list yarn修改路径命令 改变 yarn 全局bin位置(prefix) 1yarn config set prefix "E:\\NodeJs\\npm\\yarn_bin" 改变 yarn 全局安装位置(folder) 1yarn config set global-folder "E:\\NodeJs\\npm\\yarn_dir" 改变 yarn 全局cache位置(cache) 1yarn config set cache-folder "E:\\NodeJs\\npm\\yarn_cache" 配置环境变量将E:\\NodeJs\\npm\\yarn_bin填加到环境变量的path变量中,主要该目录下是否有自动生成的bin目录,若有,则添加E:\\NodeJs\\npm\\yarn_bin\\bin 5.nrmnrm是npm的下载源管理工具 安装 1npm i nrm -g 使用 命令 说明 nrm ls 查看当前源 nrm use <源名称> 切换下载源 nrm test 测试哪个源下载最快 使用nrm可能出现的错误在windows中使用nrm可能会出现process.env无法识别的情况,则修改错误提示的第四行中的文件 1const NRMRC = path.join(process.env[(process.platform == 'win32') ? 'USERPROFILE' : 'HOME'], '.nrmrc'); 24.Vue新脚手架1npm init vue@next 25.滚动条样式修改1234567891011121314151617// 整个滚动条&::-webkit-scrollbar { width: 6px; height: 6px;}// 滚动条轨道::-webkit-scrollbar-track { height: 3px; background: rgba(51, 51, 51, 0.3); border-radius: 3px;}// 滚动条上的滚动滑块&::-webkit-scrollbar-thumb { width: 6px; background: rgba(51, 51, 51, 0.3); border-radius: 3px;} 在修改滚动条样式时,需要在设置了&::-webkit-scrollbar的基础上进行修改 26.CSS文字环绕效果问题当字体为中文时,文字可以默认环绕,当字体为英文时,需要设置word-break: break-all work-break属性规定了自动换行的处理方式 值 描述 normal 使用浏览器默认的换行规则。 break-all 允许在单词内换行。 keep-all 只能在半角空格或连字符处换行。 27.Element.getBoundingClientRect() Element.getClientRects()Element.getBoundingClientRect() 返回元素的大小以及相对于视口的位置 返回值是一个 DOMRect 对象,这个对象是由该元素的 getClientRects() 方法返回的一组矩形的集合,就是该元素的 CSS 边框大小。返回的结果是包含完整元素的最小矩形,并且拥有left, top, right, bottom, x, y, width, 和 height这几个以像素为单位的只读属性用于描述整个边框。除了width 和 height 以外的属性是相对于视图窗口的左上角来计算的。 Element.getClientRects() 返回一个指向客户端中每一个盒子的边界矩形的矩形集合 返回值是ClientRect对象集合,该对象是与该元素相关的CSS边框。每个ClientRect对象包含一组描述该边框的只读属性——left、top、right和bottom,单位为像素,这些属性值是相对于视口的top-left的。即使当表格的标题在表格的边框外面,该标题仍会被计算在内。 28.获取当前设备信息JavaScript通过navigator.userAgent属性可以获取当前设备信息,包括浏览器信息,设备是PC还是Mobile等 123456// 例如判断页面是否在钉钉中打开function isDingTalk() { var ua = navigator.userAgent.toLowerCase(); if (/dingtalk/gi.test(ua) && /android/gi.test(ua) && /mobile/gi.test(ua)) return true; return false;} 29.JavaScript剪贴板操作Clipboard APIClipboard API 是下一代的剪贴板操作方法,它的所有操作都是异步的,返回Promise对象,不会造成页面的卡顿,并且可以将任意内容放入剪贴板 navigator.clipboard属性返回 Clipboard 对象,所有操作都通过这个对象进行。 1const clipboardObj = navigator.clipboard; 如果navigator.clipboard属性返回undefined,就说明当前浏览器不支持这个 API。 由于用户可能把敏感数据(比如密码)放在剪贴板,允许脚本任意读取会产生安全风险,所以这个 API 的安全限制比较多。 首先,Chrome 浏览器规定,只有 HTTPS 协议的页面才能使用这个 API。不过,开发环境(localhost)允许使用非加密协议。 其次,调用时需要明确获得用户的许可。权限的具体实现使用了 Permissions API,跟剪贴板相关的有两个权限:clipboard-write(写权限)和clipboard-read(读权限)。”写权限”自动授予脚本,而”读权限”必须用户明确同意给予。也就是说,写入剪贴板,脚本可以自动完成,但是读取剪贴板时,浏览器会弹出一个对话框,询问用户是否同意读取。 Clipboard 对象提供了四个方法,用来读写剪贴板。它们都是异步方法,返回 Promise 对象 Clipboard.readText():用于复制剪贴板里面的文本数据 1234567document.body.addEventListener( 'click', async (e) => { const text = await navigator.clipboard.readText(); console.log(text); }) Clipboard.read():用于复制剪贴板里面的数据,可以是文本数据,也可以是二进制数据(比如图片)。该方法需要用户明确给予许可。 12345678910111213async function getClipboardContents() { try { const clipboardItems = await navigator.clipboard.read(); for (const clipboardItem of clipboardItems) { for (const type of clipboardItem.types) { const blob = await clipboardItem.getType(type); console.log(URL.createObjectURL(blob)); } } } catch (err) { console.error(err.name, err.message); }} Clipboard.writeText():用于将文本内容写入剪贴板。 1await navigator.clipboard.writeText('要复制的内容') Clipboard.write(): 用于将任意数据写入剪贴板,可以是文本数据,也可以是二进制数据。 12345678910111213try { const imgURL = 'https://dummyimage.com/300.png'; const data = await fetch(imgURL); const blob = await data.blob(); await navigator.clipboard.write([ new ClipboardItem({ [blob.type]: blob }) ]); console.log('Image copied.');} catch (err) { console.error(err.name, err.message);} 30.React onChange和onInput的区别从表现形式上看,React中onChange和onInput是没有区别的,都是在用户持续输入时调用,它们的区别在于事件对象的类型不同,onChange的事件对象属于ChangeEvent事件,onInput的事件对象属于FormEvent事件类型。 31.Object-fit图片裁剪属性,可取值: fill:默认值,内容拉伸填满整个content box,不保证保持原有的比例 contain:保持原有尺寸比例。长度和高度中短的那条边跟容器大小一致,长的那条边等比缩放,可能会有留白 cover:保持原有尺寸比例,宽度和高度中长的那条边和容器大小一致,短的那条等比缩放。可能会有部分区域不可见 none:保留原有元素内容的长度和宽度 scale-down: 保持原有尺寸比例。内容的尺寸与 none 或 contain 中的一个相同,取决于它们两个之间谁得到的对象尺寸会更小一些。","link":"/%E9%9A%8F%E8%AE%B0/"}],"tags":[{"name":"HTML","slug":"HTML","link":"/tags/HTML/"},{"name":"CSS","slug":"CSS","link":"/tags/CSS/"},{"name":"Chrome调试","slug":"Chrome调试","link":"/tags/Chrome%E8%B0%83%E8%AF%95/"},{"name":"Git","slug":"Git","link":"/tags/Git/"},{"name":"JavaScript","slug":"JavaScript","link":"/tags/JavaScript/"},{"name":"前端","slug":"前端","link":"/tags/%E5%89%8D%E7%AB%AF/"},{"name":"设计模式","slug":"设计模式","link":"/tags/%E8%AE%BE%E8%AE%A1%E6%A8%A1%E5%BC%8F/"},{"name":"浏览器","slug":"浏览器","link":"/tags/%E6%B5%8F%E8%A7%88%E5%99%A8/"},{"name":"面试","slug":"面试","link":"/tags/%E9%9D%A2%E8%AF%95/"},{"name":"nodejs","slug":"nodejs","link":"/tags/nodejs/"},{"name":"React","slug":"React","link":"/tags/React/"},{"name":"前端工程化","slug":"前端工程化","link":"/tags/%E5%89%8D%E7%AB%AF%E5%B7%A5%E7%A8%8B%E5%8C%96/"}],"categories":[{"name":"前端","slug":"前端","link":"/categories/%E5%89%8D%E7%AB%AF/"},{"name":"CSS","slug":"CSS","link":"/categories/CSS/"},{"name":"CSS","slug":"前端/CSS","link":"/categories/%E5%89%8D%E7%AB%AF/CSS/"},{"name":"Git","slug":"Git","link":"/categories/Git/"},{"name":"JavaScript","slug":"前端/JavaScript","link":"/categories/%E5%89%8D%E7%AB%AF/JavaScript/"},{"name":"HTML","slug":"前端/HTML","link":"/categories/%E5%89%8D%E7%AB%AF/HTML/"},{"name":"框架","slug":"前端/框架","link":"/categories/%E5%89%8D%E7%AB%AF/%E6%A1%86%E6%9E%B6/"},{"name":"设计模式","slug":"前端/设计模式","link":"/categories/%E5%89%8D%E7%AB%AF/%E8%AE%BE%E8%AE%A1%E6%A8%A1%E5%BC%8F/"},{"name":"DOM","slug":"前端/JavaScript/DOM","link":"/categories/%E5%89%8D%E7%AB%AF/JavaScript/DOM/"},{"name":"nodejs","slug":"nodejs","link":"/categories/nodejs/"},{"name":"React","slug":"前端/React","link":"/categories/%E5%89%8D%E7%AB%AF/React/"},{"name":"React","slug":"React","link":"/categories/React/"},{"name":"前端工程化","slug":"前端/前端工程化","link":"/categories/%E5%89%8D%E7%AB%AF/%E5%89%8D%E7%AB%AF%E5%B7%A5%E7%A8%8B%E5%8C%96/"}],"pages":[]}
马建仓 AI 助手
尝试更多
代码解读
代码找茬
代码优化
1
https://gitee.com/hjc0930/hjc0930.git
git@gitee.com:hjc0930/hjc0930.git
hjc0930
hjc0930
hjc0930
master

搜索帮助

344bd9b3 5694891 D2dac590 5694891