一些利用语言的特征使用的一些不常见的奇淫技巧,JavaScript 的语法是十分简单灵活的,但在项目中建议大家遵从 ESLint 规范编写可维护性的代码。
算数
算术中的位运算已被作者列为禁术,因此希望你在工程中使用位运算时,请确保你有充足的理由使用,并在需要时写好 Hack 注释。
! 与 !!
!
为逻辑非操作符,!
强制转化为一个布尔值变量,在对其取反
!!
只是单纯的将操作数执行两次逻辑非,!!
任意类型的值转化为相应的布尔值
以下写法推荐你使用最后一种方式来进行转化:
const enable1 = !!id;
const enable2 = id ? true : false;
const enable3 = Boolean(id)
~ 与 ~~
~
表示按位非运算符,~5
步骤为
- 转为一个字节的二进制表示:00000101
- 按位取反:11111010
- 取其反码:10000101(符号位为 1 表示负数,将除符号位之外的其他数字取反)
- 末尾加 1 得其补码:10000110
- 转化为十进制:-6
针对负数的操作是为了统一加法和减法,在计算机中,减法会变成加一个负数,而负数会以补码的形式存储。取反加一
得到补码形式
如果只想按位取反,而不是附带补码的按位取反,让全 1 的数据和当前数据做按位异或即可,比如:0xFFFF ^ a
简单理解,对任意数字按位非操作的结果为 -(x + 1)
。
~~
就表示按位双非运算符了,那么 ~~x
就为 -(-(x + 1) + 1)
~value
的使用,判断数组中是否有某元素
if(arr.indexOf(ele) > -1) {} // 已读
if(~arr.indexOf(ele)) {} // 简洁
~~value
常用来代替 Math.floor
或 parseInt
,且效率更高
+
变量前使用 +
的本意是将变量转换为数字。
使用 +
也可以作为立即执行函数:+function() {}()
,等效于 (function(){})()
。
& 与 &&
& 表示按位与,对应位均为 1 才为 1,其他情况为 0。需要两个数组并返回一个数字。如果不是数字,则会转换为数字。具体步骤
- 转换为 2 进制
- 比较结果
- 转回十进制
&& 表示逻辑与,但需要注意的是,&& 并不是单纯的返回 true 或者 false
- 若第一个表达式为 false,则返回第一个表达式;
- 若第一个表达式为 true,返回第二个表达式。
除此以外,它还经常被作为短路逻辑使用:若前面表达式不是 truthy,则不会继续执行之后的表达式。如在取一个对象的属性,我们需要先判断是否为空才能进行取值,否则会抛出 Uncaught TypeError,这种情况下一般我们也会通过逻辑或,给与表达式一个默认值:
const value = obj && obj.value || false
| 与 ||
它们与 &
和 &&
使用方法很相似,不同的是它们表示的是逻辑或,因此使用 |
会进行按位或运算(对应位有 1 则为 1,否则为 0),而 ||
会返回第一个 Truthy 值。
使用 ||
进行默认值赋值在 JavaScript 中十分常见,这样可以省略很多不必要的 if
语句
== 与 ===
这个想必是比较熟悉的,就不多啰嗦了。简单来说,==
用于判断值是否相等, ===
判断值与类型是否都相等。
针对于 undefined
与 null
:undefined
与 null
互等,与其余任意对象都不相等
if (a == undefined) {}
if (a == null) {}
// 等效于
if(a === undefined || a === null) {}
对于 ''
, false
, 0
而言,他们都属于 Falsy 类型,通过 Boolean 对象都会转换为假值,而通过 ==
判断三者的关系,他们总是相等的,因为在比较值时它们会因为类型不同而都被转换为 false
值
^
按位异或运算符,对比每一个比特位,当比特位不相同时则返回 1,否则返回 0。很少人在 Web 开发中使用此运算符吧,除了传说中的一种场景:交换值。
若要交换 a 与 b 的值,如果可以的话推荐你使用:
[a, b] = [b, a]
如果有人这样写
a = a ^ b
b = a ^ b
a = a ^ b
但请忘记这种写法,简洁易读的函数才是最佳实践。
数值表示
数字几种你可能不知道的数值表达
科学计数法
科学计数法是一种数学术语,将一个数表示为 a 乘以 10 的 n 次方,例子如下
1e5; // 100000
2e-4; // 0.0002
-3e3; // -3000
Number 对象有 toExponential(fractionDigits)
方法以科学计数法返回该数值的字符串表示形式,参数 fractionDigits
可选,用于用来指定小数点后有几位数字
以下情况JavaScript会自动将数值转为科学计数法表示:
- 小数点前的数字多于21位。
- 小数点后的零多于5个。
0.x 小数
通常某些人习惯省略 0.
开头的数字,常见于数值计算、css 属性中,比如 0.5px
可直接写为 .5px
,0.2 * 0.3
可写为:.2 * .3
0x、0o 和 0b
JavaScript 提供了以下进制的表示方法:
- 二进制:只用 0 和 1 两个数字,前缀为 0b,十进制 13 可表示为 0b1101
- 八进制:只用 0 到7 八个数字,前缀为 0o,十进制 13 可表示为 0o15、015
- 十六进制:只用 0 到 9 的十个数字,和 a 到 f 六个字母,前缀为 0x,十进制 13 可表示为 0xd
默认情况下,JavaScript 内部会自动将八进制、十六进制、二进制转为十进制再进行运算。从十进制转其他进制请查阅 toString 方法,从其他进制转十进制请查阅 parseInt 方法,从其他进制转其他进制请先转为十进制再转为其他方法。
话术
你可能你知道函数技巧
Array.prototype.sort
默认根据字符串的 Unicode 编码进行排序,具体算法取决于实现的浏览器,在 v8 引擎中,若数组长度小于 10 则使用从插入排序,大于 10 使用的是快排。
sort 支持传入一个 compareFunction(a, b) 的参数,其中 a、b 为数组中进行比较的两个非空对象(所有空对象将会排在数组的最后),具体比较规则为:
- 返回值小于0,a排在b的左边
- 返回值等于0,a和b的位置不变
- 返回值大于0,a排在b的右边
打乱数组的方法
[1, 2, 3, 4].sort(() => .5 - Math.random())
这里你需要知道的是,以上实现并不是完全随机,究其原因,还是因为排序算法的不稳定性,导致一些元素没有机会进行比较,在抽奖程序中若要实现完全随机,请使用 Fisher–Yates shuffle 算法,以下是简单实现:
function shuffle(arrs) {
for(let i = arrs.length - 1; i > 0; i-= 1) {
const random = Math.floor(Math.random() * (i + 1))
[arrs[random], arrs[i]] = [arrs[i], arrs[random]]
}
}
Array.prototype.concat.apply
apply 接收数组类型的参数来调用函数,值得注意的是,concat 可接收原始型或数组的多个参数,利用这个特性,可用来打平一个数组
Array.prototype.concat.apply([, [1, [2, 3], [4]]])
通过此方法也可以写一个深层次遍历的方法
function flattenDeep(arrs) {
let result = Array.prototype.concat.apply([], arrs)
while(result.some(item => item instanceof Array)) {
result = Array.prototype.concat.apply([], result)
}
return result;
}
Array.prototype.push.apply
如果要对数组进行拼接操作,习惯于使用 concat 函数,比如
let arrs = [1, 2, 3]
arrs = arrs.concat([4, 5, 6])
利用 apply 方法的数组传参特性,可以更简洁
const arrs = [1, 2, 3]
arrs.push.apply(arrs, [4, 5, 6])
// 在 ES6 中,可以更简单
arrs.push(...arrs)
Array.prototype.length
它通常用于返回数组的长度,但是也是一个包含有复杂行为的属性,首先需要说明的是,它并不是用于统计数组中元素的数量,而是代表数组中最高索引的值。
const arrs = []
arrs[5] = 1
console.log(arrs.length) // 6
length 长度随着数组的变化而变化,但是这种变化仅限于:子元素最高索引值的变化,假如使用 delete 方法删除最高元素,length 是不会变化的,因为最高索引值也没变
const arrs = [1, 2, 3]
delete arrs[2] // 长度依然为 3
length 还有一个重要的特性,那就是允许你修改它的值,若修改值小于数组本身的最大索引,则会对数组进行部分截取,若赋予的值大于当前最大索引,则会得到一个稀疏数组。
在对length进行修改的时候,还需要注意:
- 值需要为正整数
- 传递字符串会被尝试转为数字类型
Object.prototype.toString.call
每个对象都有一个 toString()
,用于将对象以字符串方式引用时自动调用,如果此方法未被覆盖,toString
则会返回 [object type]
,因此 Object.prototype.toString.call
只是为了调用原生对象上未被覆盖的方法,call
将作用域指向需要判断的对象,这样一来就可以通过原生的 toString
方法打印对象的类型字符串,利用这个特性,可以较为精确的实现类型判断。
Object.create(null)
用于创建无“副作用”的对象,也就是说,它创建的是一个空对象,不包含原型链与其他属性。
使用 const map = {}
创建出来的对象相当于 Object.create(Object.prototype)
,它继承了对象的原型链。
JSON.parse(JSON.stringify(Obj))
很常用的一种深拷贝对象的方式,将对象进行JSON字符串格式化再进行解析,即可获得一个新的对象,要注意它的性能不是特别好,而且无法处理闭环的引用
这样通过 JSON 解析的方式其实性能并不高,若对象可通过浅拷贝复制请一定使用浅拷贝的方式,不管你使用 {...obj}
还是 Object.assign({}, obj)
的方式,而如果对性能有要求的情况下,请不要再造轮子了,直接使用 npm:clone
这个包或是别的吧。
理论
Truthy 与 Falsy
对每一个类型的值来讲,它每一个对象都有一个布尔型的值,Falsy 表示在 Boolean 对象中表现为 false 的值,在条件判断与循环中,JavaScript 会将任意类型强制转化为 Boolean 对象。以下这些对象在遇到 if 语句时都表现为 Falsy:
false
null
undefined
0
NaN
''
""
document.all
其中 document.all
属于历史遗留原因,所以为 false,它违背了 JavaScript 的规范,可以不管它。
原码, 反码, 补码
基本规律
- 正数的原码、反码、补码都是它本身
- 负数的反码:在其原码的基础上, 符号位不变,其余各个位取反
- 负数的补码:负数的反码 + 1
位运算就是用计算机底层电路所有运算的基础,为了让计算机的运算更加简单,而不用去辨别符号位,所有值都采用加法运算,因此,人们设计了原码,通过符号位来标识数字的正负
1 = 0000 0001
-1 = 1000 0001
假如计算机要对两个数相加:1 + (-1),使用原码相加的运算结果为:10000010,很明显-2并不是我们想要的结果,因此出现了反码,若使用反码进行运算会有什么结果呢,让我们来看一下:
1[反码] + (-1)[反码] = 0000 0001 + 1111 1110 = 1111 1111[反码] = 1000 0000[原码]
此时运算结果是正确的,可是这样还存在一个问题,有两个值可以表示0:1000 0000、0000 0000,对于计算机来说,0 带符号是没有任何意义的,人们为了优化 0 的存在,设计出了补码:
1[补码] + (-1)[补码] = 0000 0001 + 1111 1111 = 0000 0000[原码]