Better

Ethan的博客,欢迎访问交流

阅读笔记:JavaScript 黑话

一些利用语言的特征使用的一些不常见的奇淫技巧,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.floorparseInt,且效率更高

+

变量前使用 + 的本意是将变量转换为数字。

使用 + 也可以作为立即执行函数:+function() {}(),等效于 (function(){})()

& 与 &&

& 表示按位与,对应位均为 1 才为 1,其他情况为 0。需要两个数组并返回一个数字。如果不是数字,则会转换为数字。具体步骤

  1. 转换为 2 进制
  2. 比较结果
  3. 转回十进制

&& 表示逻辑与,但需要注意的是,&& 并不是单纯的返回 true 或者 false

  • 若第一个表达式为 false,则返回第一个表达式;
  • 若第一个表达式为 true,返回第二个表达式。

除此以外,它还经常被作为短路逻辑使用:若前面表达式不是 truthy,则不会继续执行之后的表达式。如在取一个对象的属性,我们需要先判断是否为空才能进行取值,否则会抛出 Uncaught TypeError,这种情况下一般我们也会通过逻辑或,给与表达式一个默认值:

const value = obj && obj.value || false

| 与 ||

它们与 &&& 使用方法很相似,不同的是它们表示的是逻辑或,因此使用 | 会进行按位或运算(对应位有 1 则为 1,否则为 0),而 || 会返回第一个 Truthy 值。

使用 || 进行默认值赋值在 JavaScript 中十分常见,这样可以省略很多不必要的 if 语句

== 与 ===

这个想必是比较熟悉的,就不多啰嗦了。简单来说,== 用于判断值是否相等, === 判断值与类型是否都相等。

针对于 undefinednullundefinednull 互等,与其余任意对象都不相等

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 可直接写为 .5px0.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[原码]

来源



留言