1、看起来一样的字符串却不相等
第一次遇到这种情况的同学可能会一脸懵,其实这主要是字符编码不同造成的
(这种情况可能发生在字符串来自不同地方的时候,如 文件的名称,html中的textContent,js等)
通过 encodeURIComponent 将两个字符串进行编码,你会发现编码结果不同:
console.log(str1, str2, encodeURIComponent(str1), encodeURIComponent(str2), str1 === str2);
经过测试,可以使用正则去掉空白类字符(也许尽管看起来没有空白字符)再比较,或者使用 localeCompare 比较两个字符串:str1.localeCompare(str2)===0
2、Storage的页面共享问题
通常,用户登录后会将token等登录信息缓存在sessionStorage中,这样用户关闭网页清理token,而刷新网页可以不用登录。似乎很完美。但:(注:以下新页面均是同源页面)
window.open() 打开新的页面;a标签 跳转新页面;ctrl + 鼠标右键点击 打开新页面。
这三种情况,新页面会复制已有的sessionStorage——但仅仅是复制,如果原页面sessionStorage变了,新页面是不会同步变的。那么,如果用户在原页面退出了登录,新页面却还保留了token(甚至有可能是合法的token),是不是很糟糕!
鼠标右键点击连接,弹出上下文菜单,再点击“新窗口(或标签页)打开链接”,打开新页面; 复制原页面网址,打开一个新的空的标签页,然后粘贴此网址,打开新页面。
这两种情况,sessionStorage不会被复制。也就是你需要再新页面重新登录!
另外Electron 将前端代码打包成桌面应用,window.open打开新标签页,也不会被复制。这是更糟糕的情况。
采用 localStorage,这是所有页面共享的,无论以何种方式打开的不同页面,无论以何种方式修改了localStorage,所有页面都会同步变化。
你可测试,几乎所有的知名网站 用户登录信息都是所有页面同步共享的(这个页面切换用户了,另一个页面也会变化),所以不要再用sessionStorage保存用户信息了!!
注意localStorage和sessionStorage的这个区别,将有助于你更好的做决策。
3、Map 和 Object互转问题
Map类型 和 普通Object 类型,都是键值对的形式,他们之间需要互转的场景也很多。如果你还在用for循环互转,那么这些技巧能帮你简化代码:
- 键值对entry,实际上就是一个 [ key , value ] 这样的数组
- new Map( iterableEntries ) 可以直接创建一个map,传入的参数是每个元素都是entry的可迭代对象,如数组,set,其他map 等。
- Object.fromEntries( iterableEntries ) 可以直接创建一个对象。传入的参数和上面的完全一致
- Object.entries(obj) 和 map.entries() 返回都是可迭代对象(每个元素都是entry的)
于是就可以很优雅的实现两者的互转:
// map 转 obj: const obj = Object.fromEntries(map);
const obj = Object.fromEntries(map.entries()) // obj 转 map: const map = new Map(Object.entries(obj));
以及更多其他的互转:
// 数组 转 map或obj const arr = [['a',1],['b',2],['c',3]] const map = new Map(arr) const obj = Object.fromEntries(arr) // for of 循环的解构赋值 for(const [k,v] of map){ console.log(k,v) }
4、window.parent
当前窗口的父窗口对象(如果没有父窗口,就是自身。)
window.parent === window 表明当前窗口不是在 iframe object frame这些标签下【参考】,这在避免自己的网站被恶意引用(如劫持类的攻击)时很有用。
5、多重循环中的label
在循环语句中,有两个重要的控制语句 break 和 continue,表示 退出当前循环 和 跳过本次循环进行下一个。
在多重循环中,使用label 控制非常方便。如果你还在用一些中间flag来传递控制,知道这个技巧尤为重要。举个例子,还好感受一下吧:
loop1: for (i = 0; i < 3; i++) { loop2: for (j = 0; j < 3; j++) { if (i == 1 && j == 1) { break loop1; // 这里直接退出外层循环 // continue loop1; // 这里退出内层循环,继续外层的下一条循环 } console.log("i = " + i + ", j = " + j); } }
6、try 语句块中的异步
直接上代码:
try { Promise.reject(new Error('test error')) } catch (err) { console.log('catch eror:' + err.message) } finally { console.log('finally') }
这段代码,只会打印出 finally,然后提示异常未捕获。这里的try语句只能捕获同步的异常
(async function () { try { await Promise.reject(new Error('test error')) } catch (err) { console.log('catch eror:' + err.message) } finally { console.log('finally') } })()
这段代码能和你期望的一样:先打印出catch eror:test error,再打印出 finally。能成功捕获异步异常!
7、?? 和 ?.
这两个都是判断一个变量是否存在,是否为nullish(null或undefined),但结果是相反的。??是nullish时为true,?.不是nullish时才为true:
?. 又叫可选链操作符; ?? 控制合并运算符; ??= 逻辑空赋值。示例:
console.log(obj?.a) // obj不是nullish时 打印obj.a,否则打印undefined obj.dog?.['name'] // dog属性存在的话,取其name(注意写法,不是obj.dog?['name'], ?.是一体的),[]里面是表达式,表达式的结果作为key的name。 // 如果dog不存在,后面的表达式将不会计算,类似于if else 直接跳过! obj.add?.(1,2,3) // 有add这个方法的,就调用这个方法。注意 如果存在add,但add不是方法,这会报错。()表示作为方法来调用它。 // ?.不能用于赋值 obj?.name = 'Jack' 这是错误的!
let foo = null ?? 'default string'; // ?? 则表示是nullish时,取后面的结果否则取前面的结果! const baz = 0 ?? 42; // 结果0 (0 不是 nullish) console.log( B() ?? C() ); // B()返回的不是undefined或null, C方法将不会调用! (null || undefined ) ?? "foo"; // ||或&&或关系比较 都不能与??直接连用,需要借助括号 a.duration ??= 10; // a.duration是nullish时才赋值,等价于 a.duration??(a.duration=10)
在复杂表达式中为了避免空值引起的异常,基本都用&&或||的技巧,但这任然很麻烦,而且无法很好的规避0,false,NaN这些合法的值,现在有了?和?? 让一起再次简单了起来!
高级示例:
// 当变量age存在时(存在包括0,NaN的字符串),调用验证函数validateAge: age?.[validateAge()] // 当age不存在时,弹出提示: age??alert("年龄不存在")
8、基础类型添加属性的问题
基础类型如:number、string、boolean、bigint、symbol
通过new创建的Number、String、Boolean 是对象,所以可以添加任意的属性。
let a = 5 a.name="a" // 严格模式下报错,非严格模式下静默失败! console.log(a,a.name)
更多冷知识,正在收录中……