首页
Search
1
解决visual studio code (vscode)安装时没有选择安装路径问题
320 阅读
2
如何在 Clash for Windows 上配置服务
215 阅读
3
Linux 下 Bash 脚本 bad interpreter 报错的解决方法
150 阅读
4
Arch Linux 下解决 KDE Plasma Discover 的 Unable to load applications 错误
149 阅读
5
uniapp打包app提示通讯录权限问题,如何取消通讯录权限
113 阅读
clash
服务器
javascript
全部
游戏资讯
登录
Search
加速器之家
累计撰写
1,195
篇文章
累计收到
0
条评论
首页
栏目
clash
服务器
javascript
全部
游戏资讯
页面
搜索到
1195
篇与
的结果
2024-10-20
10 分钟内了解判断数组中存在元素的多种方式
在前端开发中,经常会遇到要判断数组中是否存在某个元素。其实判断的方式有很多种,我们一个一个来了解下。我们先来定义一个数组:const arr = [ 13, false, 'abcd', undefined, 13, null, NaN, [1, 2], { a: 123 }, () => Date.now(), new Date('2021/03/04'), new RegExp('abc', 'ig'), Symbol('sym'), ]; 在这个数组中,我们包含了好几种类型:number, boolean, string, undefined, null, array, object, Date, Symbol 等。其中数字 13 出现了 2 次。 1. indexOf # 我们最熟悉的就是indexOf了,毕竟他出现的早,兼容性也好,使用起来也很方便。如果存在该元素,则返回第一次出现的索引值;若整个数组不存在该元素,则返回-1。 1.1 使用方式 # 只要判断返回的数据是不是-1,就能知道数组中是否包含该元素。arr.indexOf(13) >= 0; // true, indexOf返回0 arr.indexOf(2) >= 0; // false, indexOf返回-1 与 indexOf 对应的是 lastIndexOf,从最后往前查找元素,若存在该元素,则返回在数组中的最后一个的索引;若不存在该元素,则返回-1。arr.lastIndexOf(13) >= 0; // true, lastIndexOf返回4, 最后一次出现的索引 两个方法在判断变量是否存在时,调用方式是一样的。 1.2 第 2 个可选参数 # indexOf 和 lastIndexOf 还有第 2 个可选参数 fromIndex,用来表示从哪个索引开始进行搜索。在 indexOf 中,若 fromIndex 超过数组的长度,则直接返回-1,若为负数,则从最后往前数几个索引(arr.length-Math.abs(fromIndex)),然后开始往后搜索。在 lastIndexOf 中,若 fromIndex 达到或超过数组的长度,则搜索整个数组;若为负数,则从最后往前数几个索引(arr.length-Math.abs(fromIndex)),然后开始往前搜索,若负数的绝对值超过了数组的长度,则直接返回-1。arr.indexOf(13, 2); // 4, 从索引值2开始往后查找,首先找到的13的索引值为4 arr.indexOf(13, -10); // 4, 从索引值1(11-10)开始往后检索 arr.lastIndexOf(13, 2); // 0, 从索引值2往前开始搜索 arr.lastIndexOf(13, -2); // 4, 从索引值9(11-2)开始往前搜索 而且 indexOf 和 lastIndexOf 中采用的是严格相等的方式(===)来判断的。arr.indexOf(null); // 5, 在null的前面有几个假值false和undefined,也能准确找到null的索引值 2. includes # indexOf 主要是为了查找元素所在的索引值,只是我们可以用返回的索引值来间接判断数组中是否存在该元素。而在 ES7(ES2016)中添加的includes方法,就是专门用来判断元素是否存在的。返回值为 true 或者 false,true 表示存在,false 表示不存在,简单明了。arr.includes(13); // true arr.includes('abc'); // false arr.includes(false); // true, 存在false元素 同时,includes 方法中也存在第 2 个可选参数 fromIndex,fromIndex 的用法与 indexOf 中的一样。若 fromIndex 超过数组的长度,则直接返回-1,若为负数,则从最后往前数几个索引(arr.length-Math.abs(fromIndex)),然后开始往后搜索。arr.includes(13, 5); // false, 从索引值5开始往后检索,没检索到 到目前为止,后面的几种类型,例如 Array, Object, Date 和 Symbol,我们都没判断呢。我们现在来判断下后面的几个元素:// 使用indexOf判断 arr.indexOf(NaN); // -1 arr.indexOf([1, 2]); // -1 arr.indexOf({ a: 123 }); // -1 arr.indexOf(() => Date.now()); // -1 arr.indexOf(new Date('2021/03/04')); // -1 arr.indexOf(new RegExp('abc', 'ig')); // -1 arr.indexOf(Symbol('sym')); // -1 // 使用includes判断 arr.includes(NaN); // false arr.includes([1, 2]); // false arr.includes({ a: 123 }); // false arr.includes(() => Date.now()); // false arr.includes(new Date('2021/03/04')); // false arr.includes(new RegExp('abc', 'ig')); // false arr.includes(Symbol('sym')); // false 结局很惨,这几种元素在数组中都没有检索到。可是实际上在数组中都是真实存在的。这是因为 indexOf 和 includes 都是采用严格相等的方式(===)来判定的。NaN === NaN; // false, 两个NaN永远也不会相等 [1, 2] === [1, 2]; // false, 每个声明出来的数组都有单独的存储地址 {a: 123} === {a: 123}; // false, 同数组 new Date('2021/03/04')===new Date('2021/03/04'); // false, 看着日期是相同的,但是用new出来的对象进行比较的,肯定是不相等的 Symbol('sym')===Symbol('sym'); // Symbol类型的出现就是为了避免冲突创造出来的类型,括号里的属性仅是为了方便描述而已 针对这些无法被检索的类型,我们就需要自己写函数来判断特殊的类型了。 3. find 和 findIndex # find()和 findIndex()允许我们通过回调函数,来自定义判断的方式。 3.1 find 方法 # find() 方法返回数组中满足提供的测试函数的第一个元素的值。否则返回 undefined。find()方法无法检测数组中的 undefined 元素。因为不存在和存在 undefined 元素,find()方法都会返回 undefined。这里我们就要考虑其他方式了,稍后再讲。arr.find((item) => item === 13); // 13, 找到了元素13 arr.find((item) => item === 3); // undefined, 没找到元素3 arr.find((item) => item === undefined); // undefined, 也不知道是找到了还是没找到 对于上面稍微复杂点的类型,我们就需要特殊的判断了:arr.find((item) => typeof item === 'number' && isNaN(item)); // NaN // array和object类型进行比较时,情况很复杂,因为每个元素的类型都无法确定 // 如果确定都是基本类型,如string, number, boolean, undefined, null等,可以将其转为字符串再比较 // 转字符串的方式也很多,如JSON.stringify(arr), arr.toString(), arr.split('|')等 // 复杂点的,只能一项一项比较,或者使用递归 arr.find((item) => item.toString() === [1, 2].toString()); // [1, 2] arr.find((item) => JSON.stringify(item) === JSON.stringify({ a: 123 })); // {a: 123} arr.find((item) => { if (typeof item === 'function') { return item.toString() === (() => Date.now()).toString(); } return false; }); // () => Date.now() arr.find((item) => { if (item instanceof Date) { return item.toString() === new Date('2021/03/04').toString(); } return false; }); // Thu Mar 04 2021 00:00:00 GMT+0800 arr.find((item) => { if (item instanceof RegExp) { return item.toString() === new RegExp('abc', 'ig').toString(); } return false; }); // /abc/gi // Symbol确实没法比较,只能比较描述是否一样 arr.find((item) => { if (typeof item === 'symbol') { return item.toString() === Symbol('sym').toString(); } return false; }); // Symbol(sym) 上面的判断代码在后面的方法也将会使用到。 3.2 两个元素进行比较 # 我们在上面对比了多种类型元素的比较,稍微来总结下。先来定义一个函数:const compare = (x, y) => {}; 3.2.1 基本类型 # 对于元素是 string, number, boolean, undefined, null 等基本类型的,可以直接进行比较:const compare = (x, y) => { return x === y; }; 3.2.2 NaN 数据 # NaN 用 typeof 来判断是 number 类型,但 NaN 不与任何数字相等,包括它自己。const compare = (x, y) => { if (typeof x === 'number' && isNaN(x) && typeof y === 'number' && isNaN(y)) { return true; } return x === y; }; 3.2.3 Function 与 Date 与 RegExp # 这些类型的,可以将变量转为字符串进行比较:const compare = (x, y) => { if (typeof x === 'number' && isNaN(x) && typeof y === 'number' && isNaN(y)) { return true; } if ( (typeof x === 'function' && typeof y === 'function') || (x instanceof Date && y instanceof Date) || (x instanceof RegExp && y instanceof RegExp) || (x instanceof String && y instanceof String) || (x instanceof Number && y instanceof Number) ) { return x.toString() === y.toString(); } return x === y; }; 对于 object 类型和 array 的,我们可以将每一项拆开,然后利用上面的方式再挨个儿比较。 3.3 findIndex 方法 # 如果还要判断数组中是否存在 undefined,我们可以使用findIndex()方法。 findIndex() 方法返回数组中满足提供的测试函数的第一个元素的索引。若没有找到对应元素则返回-1。 arr.findIndex((item) => item === undefined); // 3 arr.findIndex((item) => item === 3); // -1, 没有找到数字3 其他数据格式的判断,与上面的 find()一样。 4. some # some() 方法测试数组中是不是至少有 1 个元素通过了被提供的函数测试。它返回的是一个 Boolean 类型的值。 注意:如果用一个空数组进行测试,在任何情况下它返回的都是 false。 some()方法与 find()方法的使用方式一样,只不过 some()方法返回的是 boolean 类型的数据。arr.some((item) => item === false); // true arr.some((item) => item === undefined); // true arr.some((item) => typeof item === 'number' && isNaN(item)); // true arr.some((item) => item === 3); // false, 不存在数字3 arr.some((item) => { if (item instanceof Date) { return item.toString() === new Date('2021/03/04').toString(); } return false; }); // true 5. filter # filter() 方法创建一个新数组, 其包含通过所提供函数实现的测试的所有元素。无论找到几个元素或者没有元素,filter()方法都是会返回一个数组,数组中的数据就是我们想要的元素。arr.filter((item) => item === false); // 1 arr.filter((item) => item === undefined); // 1 arr.filter((item) => typeof item === 'number' && isNaN(item)); // 1 arr.filter((item) => item === 13); // 2 arr.filter((item) => item === 3); // 0 arr.filter((item) => { if (item instanceof Date) { return item.toString() === new Date('2021/03/04').toString(); } return false; }); // 1 因此我们可以通过该数组的长度,来判断原数组是否包含我们想要的元素。 6. 总结 # 查找数组中元素的方式有很多,我们可以数组中元素的格式,来选择更合适的方式。如果都是一些基本类型,建议优先选择使用includes()方法;如果格式比较复杂的,建议选择使用some()方法。这两个方法都是直接返回 boolean 类型,无需更多的转换即可直接使用方法的结果。
2024年10月20日
5 阅读
0 评论
0 点赞
2024-10-20
一个操作 cookie 的原生方法 cookieStore
我们平时对 cookie 的增删改查等操作,都是在操作 document.cookie,这里我们介绍一个新方法cookieStore。 1. 平时如何操作 cookie # document.cookie 能获取到当前域所有的 cookie 字符串。每个 cookie 用分号进行隔开:document.cookie; // "a=1; b=2; c=wenzi" 操作 cookie,均是在操作 document.cookie。如下面就是我常用的一段代码:/** * 写cookies * @param {string} name 写cookie的key * @param {string|number} value 写cookie的值 * @param {number} day 存储的时间,默认30天 */ export const setCookie = (name: string, value: string | number, day = 30): void => { const exp = new Date(); exp.setTime(exp.getTime() + day * 24 * 60 * 60 * 1000); document.cookie = `${name}=${escape(value.toString())};path=/;expires=${exp.toUTCString()}`; }; /** * 读取cookies * @param {string} name 要获取的cookie名称 * @param {number|boolean} type 是否直接获取对应的值,若存入真值,则直接返回,否则进行解码 */ export const getCookie = (name: string): string | null => { const reg = new RegExp(`(^| )${name}=([^;]*)(;|$)`); const arr = document.cookie.match(reg); if (arr) { return unescape(arr[2]); } return null; }; /** * 删除cookie * @param name 删除的cookie名称 */ export const delCookie = (name: string) => { if (!name) return; const ex: Date = new Date(); ex.setTime(ex.getTime() - 1); document.cookie = `${name}=; expires=${ex.toUTCString()};path=/`; }; 可以看到设置、获取和删除 cookie,都是在 document.cookie 上进行操作的。 2. 新方式 cookieStore # 现在 Chrome 有了更方便操作 cookie 的方法了cookieStore,这个方法是在 Chrome87 版本加入的,兼容性还不太好。下图是当前日期 2021/03/15 的兼容性概览,可以发现仅仅是 Chrome 体系支持了 cookieStore。不过我们可以先来了解它的用法。 cookieStore 现在只能在https 协议下的域名才能访问的到;其他 http 协议的域名里会提示 cookieStore 为 undefined,或者设置失败。 2.1 基本方法 # cookieStore 是一个类似localStorage的 object 类型变量。可以看到 cookieStore 主要有 5 个方法: set: 设置 cookie,可以是 set(name, value),也可以是 set({name, value}); get: 获取 cookie,可以是 get(name),或者 get({name}); getAll: 获取所有的 cookie; delete: 删除 cookie; onchange: 监听 cookie 的变化; 前 4 个方法天然支持 Promise。接下来我们一个个来了解下。 2.2 设置 cookie # cookieStore.set 方法可以设置 cookie,并返回一个 Promise 状态,表示是否设置成功。cookieStore .set('username', 'wenzi') .then(() => console.log('设置username成功')) .catch(() => console.error('设置username失败')); 如果我们想要设置更多的属性,例如过期时间,可以传入一个 Object 类型:cookieStore .set({ name: 'age', value: 18, expires: new Date().getTime() + 24 * 60 * 60 * 1000, }) .then(() => console.log('设置age成功')) .catch(() => console.error('设置age失败')); value 中所有的数据都会默认先执行toString(),然后再进行存储,因此有些非基本类型的数据,最好先转换好。上面都是我们设置 cookie 成功的情况,那么什么时候会设置失败呢?在本地 localhost 环境会设置失败。本地 localhost,我们是能获取到cookieStore这个全局变量,也能执行相应的方法,但无法设置成功:cookieStore.set('username', 'wenzi'); 浏览器会发出提示,无法在不安全的域名下通过 CookieStore 中的 set 设置 cookie:Uncaught (in promise) TypeError: Failed to execute 'set' on 'CookieStore': Cannot modify a secure cookie on insecure origin 添加 catch 后,就能捕获到这个错误:cookieStore .set('username', 'wenzi') .then(() => console.log('设置username成功')) .catch(() => console.error('设置username失败')); 因此在想使用 cookieStore 时,不能直接通过下面的方式判断,还得新增一个页面 url 的协议来判断:typeof cookieStore === 'object'; // 判断不准确,本地localhost也会存在 应当使用:const isSupportCookieStore = typeof cookieStore === 'object' && location.protocol === 'https:'; // 只有在https协议下才使用cookieStore 2.3 获取 cookie # cookieStore.get(name)方法可以获取 name 对应的 cookie,会以 Promise 格式返回所有的属性:await cookieStore.get('username'); get()方法还可以接收一个 Object 类型,测试后发现,key 的值只能是 name:await cookieStore.get({ name: 'username' }); 当获取的 cookie 不存在时,则返回一个 Promise。 2.4 获取所有的 cookie # cookieStore.getAll()方法可以获取当前所有的 cookie,以 Promise的形式返回的形式返回,数组中的每一项与通过 get()方式获取到的格式一样;若当前域没有 cookie,或者获取不到指定的 cookie,则为空数组;await cookieStore.getAll(); getAll()方法也可以传入一个 name,用来获取对应的 cookie:await cookieStore.getAll('username'); await cookieStore.getAll({ name: 'username' }); 2.5 删除 cookie # cookieStore.delete(name)用来删除指定的 cookie:cookieStore .delete('age') .then(() => console.log('删除age成功')) .catch(() => console.error('删除age失败')); 删除成功后则会提示删除成功。即使删除一个不存在的 cookie,也会提示删除成功。因此,当再次执行上面的代码时,还是会正常提示。同样的,在 localhost 环境下会提示删除失败。 2.6 监听 cookie 的变化 # 我们可以通过添加change事件,来监听 cookie 的变化,无论是通过 cookieStore 操作,还是直接操作 document.cookie,都能监听。添加监听事件:cookieStore.addEventListener('change', (event) => { const type = event.changed.length ? 'change' : 'delete'; const data = (event.changed.length ? event.changed : event.deleted).map((item) => item.name); console.log(`刚才进行了 ${type} 操作,cookie有:${JSON.stringify(data)}`); }); 这里面有 2 个重要的字段changed数组和deleted数组,当设置 cookie 时,则 changed 数组里为刚才设置的 cookie;当删除 cookie 时,则 deleted 数组里为刚才删除的 cookie。 2.6.1 设置操作 # 当调用 set()方法时,会触发 change 事件,同时影响的 cookie 会放在event.changed数组中。通过 document.cookie 设置或者删除的 cookie,均认为是在修改 cookie,而不是删除。 每次设置 cookie 时,即使两次的 name 和 value 完全一样,也会触发change事件。 cookieStore.set('math', 90); 2.6.2 删除操作 # 通过 delete()方法删除一个存在的 cookie 时,会触发 change 事件,被删除的 cookie 会放在event.deleted数组中。如果删除一个不存在的 cookie,则不会触发 change 事件。cookieStore.delete('math'); 可以看到,当第二次删除 name 为 math 的 cookie 时,就没有触发 change 事件。 3. 总结 # 我们整篇了解了下cookieStore的使用和操作,要比我们直接操作 cookie 简便的多,而且还可以通过自身的 change 事件来监听 cookie 的变化。
2024年10月20日
4 阅读
0 评论
0 点赞
2024-10-20
第8页
文字与元素居中的方式我们经常会让元素进行上下左右的居中,这里提供几种方法供大家使用➥Read Moreposted @2019/10/15十大经典排序算法(javascript实现)排序在我们日常开发工作中经常要用到,那么都有哪些排序算法呢,这些排序算法各有什么特点呢➥Read Moreposted @2019/10/13nextjs 如何将静态资源发布到 CDNnextjs 是基于 react 的服务端同构指出框架,在使用的过程中也多多少少遇到过几个问题,其中最大的问题就是静态资源的发布了➥Read Moreposted @2019/09/17JavaScript:如何获取某一天所在的星期如何获取今天或者某一天所在星期的开始和结束日期,或者如何获取整个星期的日期➥Read Moreposted @2019/09/05前端:形成自己的方法论在前端工程化的体系建设中,如何形成自己的一套方法论➥Read Moreposted @2019/09/05如何减少函数参数的输入针对常用的函数,如何能减少必要的参数输入呢?➥Read Moreposted @2019/06/03Can’t perform a React state update on an unmounted componentWarning: Can’t perform a React state update on an unmounted component. This is a no-op, but it indicates a memory leak in your application. To fix, cancel all subscriptions and asynchronous tasks in t➥Read Moreposted @2019/06/03高考毕业10年后的我活成了...岁月走过不小心踩伤了受了伤的心尖,痛、始终不曾明灭的萦绕着苍白憔悴的思念。是否前世我淡薄寡怜了你一世的情愫?岁月走过光阴的沧海,遗落了一路的伤碎桑田心愿,在年岁风烛蚕食里是否还有痕迹?➥Read Moreposted @2019/06/03记一次项目中分享图片的过程从其他客户端分享到微信朋友圈的步骤是什么呢?又遇到了什么问题呢?➥Read Moreposted @2019/05/21前端中的事件循环eventloop机制js是单线程执行的,那么setTimeout和Promise.resolve等异步机制是如何执行的呢?➥Read Moreposted @2019/05/14能用js重写的都会被typescript重写TypeScript从今天数以百万计的JavaScript开发者所熟悉的语法和语义开始。使用现有的JavaScript代码,包括流行的JavaScript库,并从JavaScript代码中调用TypeScript代码➥Read Moreposted @2019/05/13实现react组件的递归调用项目中有侧边栏的层级菜单或者需要根据接口生成DOM树等操作时,如何实现组件的递归调用呢?➥Read Moreposted @2019/05/12React:实现一个带有loading效果的按钮组件在react中如何实现一个带有loading效果的按钮组件呢?➥Read Moreposted @2019/05/10基于观察者模式实现一个EventEmitter类在实现一些基础类库时,我们是经常需要用到`EventEmitter`模块的,比如在微信中下载某个APP时,前端需要了解当前下载处在哪个状态:刚开始下载、在在下载中、下载的进度、下载已完成、下载失败等等,都是需要我们向发布事件的➥Read Moreposted @2019/05/09页面中长列表滚动的优化当我们有10万条或者更多的数据需要展示在页面中,会出现哪些问题呢,我们应该怎样处理和优化呢?➥Read Moreposted @2019/04/23实现Promise的first等各种变体通过ES6中Promise提供的几个方法,来实现诸如first、last、none、any等各种变体方法➥Read Moreposted @2019/04/09js打乱数组的实战应用在js中,能把数组随机打乱的方法有很多,每个方法都有自己的哪些特点呢,同时若想随机取出数组中的几个元素,怎么办呢?➥Read Moreposted @2019/04/06博客里评论系统的前端总结本篇文章主要是从前端的角度来讲解下本博客评论系统的实现➥Read Moreposted @2019/03/31js中parseInt与Math.floor的区别js中parseInt与Math.floor在使用的过程中,有哪些细致的区别呢?➥Read Moreposted @2019/03/14getScript在前端中请求js文件和jsonpgetScript在前端中使用的非常广泛,那么这里也分享下自己使用的ts版本的getScript,可以用来请求js文件和进行jsonp的请求➥Read Moreposted @2019/02/27
2024年10月20日
6 阅读
0 评论
0 点赞
2024-10-20
js 中你不知道的各种循环测速
在前端 js 中,有着多种数组循环的方式: for 循环; while 和 do-while 循环; forEach、map、reduce、filter 循环; for-of 循环; for-in 循环; 那么哪种循环的执行速度最快呢,我们今天来看一看。在测试循环速度之前,我们先来创建一个有 100 万数据的数组:const len = 100 * 10000; const arr = []; for (let i = 0; i < len; i++) { arr.push(Math.floor(Math.random() * len)); } 测试环境为: 电脑:iMac(10.13.6); 处理器:4.2 GHz Intel Core i7; 浏览器:Chrome(89.0.4389.82) 1. for 循环 # for 循环是我们最常用的一种循环方式了,最大的好处就是结构清晰,能够随时 break 停止。我们先用 10 次的测试来看下:console.log('test for'); for (let k = 0; k < 10; k++) { console.time('for'); let sum = 0; for (let i = 0; i < len; i++) { sum += arr[i] % 100; } console.timeEnd('for'); } 最终得到的结果:在第 1 次和第 2 次时耗时比较多,从第 3 次开始就一直维持在 1.25ms 左右。 2. while 循环和 do-while 循环 # 这两个放在一起,也是他们的结构足够像,而且也能够随时 break 停止。console.log('\ntest while'); for (let k = 0; k < 10; k++) { console.time('while'); let sum = 0; let i = 0; while (i < len) { sum += arr[i] % 100; i++; } console.timeEnd('while'); } console.log('\ntest do-while'); for (let k = 0; k < 10; k++) { console.time('do-while'); let sum = 0; let i = 0; do { sum += arr[i] % 100; i++; } while (i < len); console.timeEnd('do-while'); } while 循环和 do-while 循环的结果几乎一样,我们只看下 while 循环在浏览器上运行的结果:跟 for 循环的速度不相上下。 3. forEach、map 和 reduce 循环 # 接下来来到我们常用的数组三剑客了:forEach, map, reduce 等,这 3 个方法都是在 ES6 标准上新加的语法。 3.1 forEach 的简要介绍 # 这几种方法是无法停止循环的,无论使用break还是return,都无法停止整个循环。我们可以做一个测试,例如我想当遇到 3 的倍数时,即停止循环[1, 2, 3, 4, 5].forEach((item) => { console.log(`before return: ${item}`); if (item % 3 === 0) { return; } console.log(`after return: ${item}`); }); 运行结果如下:从运行的结果可以看到,我们的 return 只是没有执行当时循环时后面的语句,但并没有停止整个循环,后面的 4 和 5 依然正常输出。那循环是否真的像炫迈一样停不下来吗?并不,还有一种方式,可以停止循环。那就是抛出异常:try { [1, 2, 3, 4, 5].forEach((item) => { console.log(`before return: ${item}`); if (item % 3 === 0) { throw new Error('break forEach'); } console.log(`after return: ${item}`); }); } catch (e) {} 在 forEach 中抛出异常后,就可以停止该循环,然后再使用try-catch捕获异常,避免整个服务被挂掉。虽然可以停止 forEach 的循环,但实现起来麻烦了不少。因此若没有停止整个循环的需求,可以使用 forEach, map 等循环方式;否则还是要使用其他的循环方式。 3.3 forEach 等的测速 # 好的,接下来我们就要测试这 3 个循环方式的循环速度了。// forEach 的测试: console.log('\ntest forEach'); for (let k = 0; k < 10; k++) { console.time('forEach'); let sum = 0; arr.forEach((item) => { sum += item % 100; }); console.timeEnd('forEach'); } // map 的测试: console.log('\ntest map'); for (let k = 0; k < 10; k++) { console.time('map'); let sum = 0; arr.map((item) => { sum += item % 100; }); console.timeEnd('map'); } // reduce 的测试: console.log('\ntest reduce'); for (let k = 0; k < 10; k++) { console.time('reduce'); let sum = 0; arr.reduce((_, item) => { sum += item % 100; }, 0); console.timeEnd('reduce'); } 因这 3 个循环的时间差不多,我这里就只截取了 forEach 的测试结果。执行 10 次循环后,forEach 的执行时间差不多在 10.8ms 左右,比上面的 for 循环和 while 循环高了将近 10 倍的运行时间。 4. for-of # ES6 借鉴 C++、Java、C# 和 Python 语言,引入了 for...of 循环,作为遍历所有数据结构的统一的方法。 4.1 for-of 的简要介绍 # 一个数据结构只要部署了 Symbol.iterator 属性,就被视为具有 iterator 接口,就可以用 for...of 循环遍历它的成员。也就是说,for...of 循环内部调用的是数据结构的 Symbol.iterator 方法。for...of 循环可以使用的范围包括数组、Set 和 Map 结构、某些类似数组的对象(比如 arguments 对象、DOM NodeList 对象)、后文的 Generator 对象,以及字符串。for-of 拿到的就是 value 本身,而 for-in 则拿到的是 key,然后通过 key 再获取到当前数据。const fruits = ['apple', 'banana', 'orange', 'lemon']; for (const value of fruits) { console.log(value); // 'apple', 'banana', 'orange', 'lemon' } 4.2 for-of 的循环测速 # 测试 for-of 循环速度的代码:console.log('\ntest for-of'); for (let k = 0; k < 10; k++) { console.time('for-of'); let sum = 0; for (const value of arr) { sum += value % 100; } console.timeEnd('for-of'); } 测试结果:在多次重复同一个循环时,前 2 次的 for-of 循环时间会比较长,得在 15ms 以上。但后续的执行,循环速度就下降到 1.5ms 左右了,与 for 循环的时间差不多。 5. for-in 循环 # for-in 通常用于 object 类型的循环,但也可以用来循环数组,毕竟所有数据类型的祖先都是 object 类型。console.log('\ntest for-in'); for (let k = 0; k < 10; k++) { console.time('for-in'); let sum = 0; for (let key in arr) { sum += arr[key] % 100; } console.timeEnd('for-in'); } 测试结果:for-in 循环的测速数据很惊人,简直是独一档的存在了,最好的时候也至少需要 136ms 的时间。可见 for-in 的循环效率真的很低。数组类型的数据还是不要使用采用 for-in 循环了;Object 类型的可以通过Object.values()先获取到所有的 value 数据,然后再使用 forEach 循环:const obj = {}; for (let i = 0; i < len; i++) { obj[i] = Math.floor(Math.random() * len); } for (let k = 0; k < 10; k++) { console.time('forEach-values'); let sum = 0; Object.values(obj).forEach((item) => { sum += item % 100; }); console.timeEnd('forEach-values'); } 即使多了一步操作,循环时间也大概在 14ms 左右,要比 for-in 快很多。 6. 总结 # 我们把所有的循环数据放到一起对比一下,我们这里将每个循环的测试次数调整为 100 次,横轴是循环的次数,数轴是循环的时间: for 循环、while 循环和 d-while 循环的时间最少; for-of 循环的时间稍长; forEach 循环、map 循环和 reduce 循环 3 者的数据差不多,但比 for-of 循环的时长更长一点; for-in 循环所需要的时间最多; 每种循环的时长不一样,我们在选择循环方式时,除了考虑时间外,也要考虑到语义化和使用的场景。
2024年10月20日
6 阅读
0 评论
0 点赞
2024-10-20
带你深入领略 Proxy 的世界
Proxy 是 es2015 标准规范加入的语法,很可能你只是听说过,但并没有用过,毕竟考虑到兼容的问题,不能轻易地使用 Proxy 特性。但现在随着各个浏览器的更新迭代,Proxy 的支持度也越来越高:而且使用 Proxy 进行代理和劫持,也渐渐成为了趋势。Vue3 已经用 Proxy 代替了 Object.defineProperty 实现响应式,mobx 也从 5.x 版本开始使用 Proxy 进行代理。 1. Proxy 的基本结构 # Proxy 的基本使用方式:/** * target: 表示要代理的目标,可以是object, array, function类型 * handler: 是一个对象,可以编写各种代理的方法 */ const proxy = new Proxy(target, handler); 例如我们想要代理一个对象,可以通过设置 get 和 set 方法来代理获取和设置数据的操作:const person = { name: 'wenzi', age: 20, }; const personProxy = new Proxy(person, { get(target, key, receiver) { console.log(`get value by ${key}`); return target[key]; }, set(target, key, value) { console.log(`set ${key}, old value ${target[key]} to ${value}`); target[key] = value; }, }); Proxy 仅仅是一个代理,personProxy 上有 person 所有的属性和方法。我们通过personProxy获取和设置 name 时,就会有相应的 log 输出:personProxy.name; // "wenzi" // log: get value by name personProxy.name = 'hello'; // log: set name, old value wenzi to hello 并且通过 personProxy 设置数据时,代理的原结构里的数据也会发生变化。我们打印下 person,可以发现字段 name 的值 也变成了hello:console.log(person); // {name: "hello", age: 20} Proxy 的第 2 个参数 handler 除了可以设置 get 和 set 方法外,还有更多丰富的方法: 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)。 如我们通过 delete 删除其中一个元素时,可以通过deleteProperty()方法来拦截这个操作。还是上面代理 person 的代码,我们添加一个 deleteProperty:const person = { name: 'wenzi', age: 20, }; const personProxy = new Proxy(person, { // 忽略get和set方法,与上面一样 // ... deleteProperty(target, key, receiver) { console.log(`delete key ${key}`); delete target[key]; }, }); 当执行 delete 操作时:delete personProxy['age']; // log: delete key age 2. Proxy 与 Reflect # Proxy 与 Reflect 可以说形影不离了,Reflect 里所有的方法和使用方式与 Proxy 完全一样。例如上面 Proxy 里的 get(), set()和 deleteProperty()方法我们都是直接操作原代理对象的,这里我们改成使用Reflect来操作:const personProxy = new Proxy(person, { get(target, key, receiver) { console.log(`get value by ${key}`); return Reflect.get(target, key, receiver); }, set(target, key, value, receiver) { console.log(`set ${key}, old value ${target[key]} to ${value}`); return Reflect.set(target, key, value, receiver); }, deleteProperty(target, key, receiver) { console.log(`delete key ${key}`); return Reflect.deleteProperty(target, key, receiver); }, }); 可以发现完美地实现这些功能。 3. 代理数组 # 我们在之前的文章 Vue 中对数组特殊的操作 中,讨论过 Vue 为什么没有使用Object.defineProperty来劫持数据,而是重写了 Array 原型链上的几个方法,通过这几个方法来实现 Vue 模板中数据的更新。但若 Proxy 的话,就可以直接代理数组:const arr = [1, 2, 3, 4]; const arrProxy = new Proxy(arr, { get(target, key, receiver) { console.log('arrProxy.get', target, key); return Reflect.get(target, key, receiver); }, set(target, key, value, receiver) { console.log('arrProxy.set', target, key, value); return Reflect.set(target, key, value, receiver); }, deleteProperty(target, key) { console.log('arrProxy.deleteProperty', target, key); return Reflect.deleteProperty(target, key); }, }); 现在我们再来操作一下代理后的数组 arrProxy 看下:arrProxy[2] = 22; // arrProxy.set (4)?[1, 2, 3, 4] 2 22 arrProxy[3]; // arrProxy.get (4)?[1, 2, 22, 4] 3 delete arrProxy[2]; // arrProxy.deleteProperty (4)?[1, 2, 22, 4] 2 arrProxy.push(5); // push操作比较复杂,这里进行了多个get()和set()操作 arrProxy.length; // arrProxy.get (5)?[1, 2, empty, 4, 5] length 可以看到无论获取、删除还是修改数据,都可以感知到。还有数组原型链上的一些方法,如: push() pop() shift() unshift() splice() sort() reverse() 也都能通过 Proxy 中的代理方法劫持到。concat()方法比较特殊的是,他是一个赋值操作,并不改变原数组,因此在调用 concat()方法操作数组时,如果没有赋值操作,那么这里只有 get()拦截到。 4. 代理函数 # Proxy 中还有一个apply()方法,是表示自己作为函数调用时,被拦截的操作。const getSum = (...args) => { if (!args.every((item) => typeof item === 'number')) { throw new TypeError('参数应当均为number类型'); } return args.reduce((sum, item) => sum + item, 0); }; const fnProxy = new Proxy(getSum, { /** * @params {Fuction} target 代理的对象 * @params {any} ctx 执行的上下文 * @params {any} args 参数 */ apply(target, ctx, args) { console.log('ctx', ctx); console.log(`execute fn ${getSum.name}, args: ${args}`); return Reflect.apply(target, ctx, args); }, }); 执行 fnProxy:// 10, ctx为undefined, log: execute fn getSum, args: 1,2,3,4 fnProxy(1, 2, 3, 4); // ctx为undefined, Uncaught TypeError: 参数应当均为number类型 fnProxy(1, 2, 3, '4'); // 10, ctx为window, log: execute fn getSum, args: 1,2,3,4 fnProxy.apply(window, [1, 2, 3, 4]); // 6, ctx为window, log: execute fn getSum, args: 1,2,3 fnProxy.call(window, 1, 2, 3); // 6, ctx为person, log: execute fn getSum, args: 1,2,3 fnProxy.apply(person, [1, 2, 3]); 5. 一些简单的应用场景 # 我们知道 Vue3 里已经用 Proxy 重写了响应式系统,mobx 也已经用了 Proxy 模式。在可见的未来,会有更多的 Proxy 的应用场景,我们这里也稍微讲解几个。 5.1 统计函数被调用的上下文和次数 # 这里我们用 Proxy 来代理函数,然后函数被调用的上下文和次数。const countExecute = (fn) => { let count = 0; return new Proxy(fn, { apply(target, ctx, args) { ++count; console.log('ctx上下文:', ctx); console.log(`${fn.name} 已被调用 ${count} 次`); return Reflect.apply(target, ctx, args); }, }); }; 现在我们来代理下刚才的getSum()方法:const getSum = (...args) => { if (!args.every((item) => typeof item === 'number')) { throw new TypeError('参数应当均为number类型'); } return args.reduce((sum, item) => sum + item, 0); }; const useSum = countExecute(getSum); useSum(1, 2, 3); // getSum 已被调用 1 次 useSum.apply(window, [2, 3, 4]); // getSum 已被调用 2 次 useSum.call(person, 3, 4, 5); // getSum 已被调用 3 次 5.2 实现一个防抖功能 # 基于上面统计函数调用次数的功能,也给我们实现一个函数的防抖功能添加了灵感。const throttleByProxy = (fn, rate) => { let lastTime = 0; return new Proxy(fn, { apply(target, ctx, args) { const now = Date.now(); if (now - lastTime > rate) { lastTime = now; return Reflect.apply(target, ctx, args); } }, }); }; const logTimeStamp = () => console.log(Date.now()); window.addEventListener('scroll', throttleByProxy(logTimeStamp, 300)); logTimeStamp()至少需要 300ms 才能执行一次。 5.3 实现观察者模式 # 我们在这里实现一个最简单类 mobx 观察者模式。const list = new Set(); const observe = (fn) => list.add(fn); const observable = (obj) => { return new Proxy(obj, { set(target, key, value, receiver) { const result = Reflect.set(target, key, value, receiver); list.forEach((observer) => observer()); return result; }, }); }; const person = observable({ name: 'wenzi', age: 20 }); const App = () => { console.log(`App -> name: ${person.name}, age: ${person.age}`); }; observe(App); person就是使用 Proxy 创建出来的代理对象,每当 person 中的属性发生变化时,就会执行 App()函数。这样就实现了一个简单的响应式状态管理。 6. Proxy 与 Object.defineProperty 的对比 # 上面很多例子用Object.defineProperty也都是可以实现的。那么这两者都各有什么优缺点呢? 6.1 Object.defineProperty 的优劣 # Object.defineProperty的兼容性可以说比 Proxy 要好很多,出特别低的 IE6,IE7 浏览器外,其他浏览器都有支持。但 Object.defineProperty 支持的方法很多,并且主要是基于属性进行拦截的。因此在 Vue2 中只能重写 Array 原型链上的方法,来操作数组。 6.2 Proxy 的优劣 # Proxy与上面的正好相反,Proxy 是基于对象来进行代理的,因此可代理更多的类型,例如 Object, Array, Function 等;而且代理的方法也多了很多。劣势就是兼容性不太好,即使用 polyfill,也无法完美的实现。 7. 总结 # Proxy 能实现的功能还有很多,后面我们也会继续进行探索,并且尽可能去了解下基于 Proxy 实现的类库,例如 mobx5 的源码和实现原理等。
2024年10月20日
6 阅读
0 评论
0 点赞
1
...
70
71
72
...
239