javascript进制转换_你不知道的三大 JavaScript “黑话”!

  • Post author:
  • Post category:java


a0f94fa48f6560edf52ee01f3273dbd6.gif
f927b7f2dee815ecfc8b5b9413c63c92.png

作者 | 斯年

责编 | 伍杏玲

因为球是圆的,所以不论发生什么都有可能,对这点我是深信不疑的,但最近我总是在怀疑,JavaScript也是圆的!下面我们一起来看看JavaScript有啥黑话:

8b5bea0fa4647f72a1380c4d7e1df056.png


“算术”

算术中的位运算已被作者列为禁术,因此希望你在工程中使用位运算时,请确保你有充足的理由使用,并在需要时写好Hack注释。


!与!!

!为逻辑非操作符,可以应用于ECMAScript中的任何值,无论这个值是什么类型,它会被强制转化为一个布尔值变量,再对其值取反。

!!只是单纯的将操作数执行两次逻辑非,它能将任意类型的值转化为相应的布尔值,它包含的步骤为:

  1. 将一个值转化为布尔值

  2. 将其取反

  3. 再次取反。

假设你需要通过一个布尔型变量表示是否有ID值,以下写法推荐你使用最后一种方式来进行转化:


const enable1 = !!id;



const enable2 = id ? true : false;



const enable3 = Boolean(id);



~ 与 ~~

~表示按位取反,~5的运行步骤为:

  1. 转为一个字节的二进制表示:00000101

  2. 按位取反:11111010

  3. 取其反码:10000101

  4. 取其补码:10000110

  5. 转化为十进制:-6

~~它代表双非按位取反运算符,如果你想使用比Math.floor更快的方法,那就是它了。需要注意,对于正数,它向下取整;对于负数,向上取整;非数字取值为0,它具体的表现形式为:


~~; // => 0



~~undefined; // => 0



~~Infinity; // => 0



--NaN; // => 0



~~0; // => 0



~~{}; // => 0



~~; // => 0



~~(1/0); // => 0



~~false; // => 0



~~true; // => 1



~~1.9; // => 1



~~-1.9; // => -1



+

在变量值前使用+的本意是将变量转换为数字,在一个函数接受数字类型的参数时特别有用:


+'1' // 1



+'-1' // '-1



+ // 0



+{} // NaN


根据观察,+a与a * 1结果类似。除此之外,使用+也可以作为立即执行函数:+function {},等效于(function{})。

字符串与数字相加时会将数值默认转为字符串,因此有了一下将数字转为字符串的快捷方法:” + 1。


& 与 &&

如何你是从类C语言过来的话,请抛弃之前的刻板印象:&可以充当逻辑操作符号。在JavaScript中,&只能进行位运算。


&,它表示按位与,

此运算符需要两个数字并返回一个数字。如果它们不是数字,则会转换为数字。如果执行7 & 3, 则会经过以下步骤:

  1. 先转换为2进制: 111 & 11

  2. 比较结果为:011

  3. 将二进制转回十进制,因此:7 & 3 = 3

它也可用于基偶数判断:const isOdd = num => !!(num & 1)。


&&,表示逻辑与

,通常用于if条件判断,可跟你想象的不太一样,&&并不是单纯的返回true或者false,而是依据:

  1. 若第一个表达式为false,则返回第一个表达式;

  2. 若第一个表达式为true,返回第二个表达式。 在这里举几个例子:


0 && false 0 (both are false-y, but 0 is the first)



true && false false (second one is false-y)



true && true true (both are true-y)



true && 20 20 (both are true-y)


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


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


当JavaScript压缩工具遇到if判断时,也会使用&&短路逻辑从而节省内存空间:


// before



if (test) { alert('hello') }



// after



test && alert('hello')



| 与 ||

它们与&和&&使用方法很相似,不同的是它们表示的是逻辑或,因此使用|会进行按位或运算,而||会返回第一个Truthy值。

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


// before



let res;



if (a) {



res = a;



} else if (b) {



res = b;



} else if (c) {



res = c;



} else {



res = 1;



}



// after



const res = a || b || c || 1;



== 与 ===

==为相等运算符,操作符会先将左右两边的操作数强制转型,转换为相同的操作数,再进行相等性比较。

===为全等运算符,它除了在比较时不会将操作数强制转型,其余相等判断与==一致。

简单而言,==用于判断值是否相等,===判断值与类型是否都相等,因此使用全等运算符判断操作数会更准确,新手也在学习JavaScript接收到的前几条Tips就是避免使用相等运算符,真的是这样吗?没错,这样能确保在你不彻底熟悉语言的情况下,尽可能的去避免犯错,但是我们也应该清楚在哪些情况下应该使用相等运算符,规则往往只针对于新手,而对聪明的你来说,最重要的是要清楚自己在做什么。

相等操作符对于不同类型的值,进行的比较如下图所示:

c12905358c649a611fd38379065d56b3.png

针对于undefined与:undefined与互等,与其余任意对象都不相等,因此在某些lib里,你可能会看到如下写法:


if (VAR == undefined) {}



if (VAR == ) {}


它等效于:

if (VAR === undefined || VAR === ) {}


对于 ”, false, 0而言,他们都属于Falsy类型,通过Boolean对象都会转换为假值,而通过==判断三者的关系,他们总是相等的,因为在比较值时它们会因为类型不同而都被转换为false值:

console.log((false == 0) && (0 == '') && ('' == false)) // true


或者有时候我们希望利用强转特性比较字符串与数字:


console.log(11 == '11') // trueconsole.log(11 === '11') // false



^

按位异或运算符,对比每一个比特位,当比特位不相同时则返回1,否则返回0。很少人在Web开发中使用此运算符吧,除了传说中的一种场景:交换值。

若要交换a与b的值,如果可以的话推荐你使用:

[a, b] = [b, a];


或者新建一个c,用于存储临时变量,如果你遇到有人这样书写:


// 异或运算,相同位取0,不同位取1,a ^ b ^ b = a, a ^ a ^ b = b



a = a ^ b



b = a ^ b



a = a ^ b


这样通过异或运算进行交换两个数字型变量,请原谅他并忽视它,他只可能是一个醉心于魔法的初心者,并祝愿他早日发现,简洁易读的函数才是最佳实践。


..

在JavaScipt整数和浮点数都属于Number类型,所有数字都以64位浮点数的形式储存,因此在解析语句时允许数组后面跟着一个小数点(1. === 1),可这样其实会引发一个问题,解释器无法解析1.toString这样的语句,会抛出:Uncaught SyntaxError,此时表达式中的.并没有视为属性访问器,而是与1结合为浮点数1.,所以程序会报错,1.toString等同于1toString。

为了更便于理解,可以记住这个规则:在解释器眼中,Number型表达式的出现的第一个.为浮点数的小数分隔符号,第二个.为属性访问器。比如1.0.toString与1..toString这样的语法都能正常执行。需要注意的是变量与表达式的区别,若将Number型表达式赋值给变量,通过变量是可以直接调用原型方法的,因为此时的.没有歧义。

这样的松散类型结构确实很使人产生误解,在程序中我们都应该规避这样的歧义性语句,通过括号消除数值表达式的歧义(1).toString,而不是为了耍酷使用1..toString。


void

根据MDN中的定义:void对给定的表达式进行求值,然后返回undefined,我们可以有很多种方式去理解这句话。

首先它可以作为undefined的替代品,由于undefined不是保留字,它其实是一个全局变量值,因此我们可以对其进行改变,程序可能会出现不稳定的状态,在ES5中已经是一个只读属性了,但是在局部作用域中,还是有被重载的可能(你可能也有被害妄想症):


(function {



const undefined = 'hello';



console.log(undefined); // hello



});


其次,我们可以在函数前面加上void关键字,表示函数没有返回值,但是不必在每一个函数都加上,这不符合JavaScript的代码风格,利用此特性我们可以用于执行IIFE(立即执行函数),让我们来看以下示例:


const arrs =



(function {



console.log('hello')



})


若你不习惯于写分号,那就极有可能遇到过这种报错:Uncaught TypeError: is not a function,这是由于编辑器在进行minify的时候无法进行正确的分词,这时通过void就可以解决此类问题,解决了分词的问题,也使立即执行函数调用更加优雅:


const arrs =



void function {



console.log('hello')



}


在有时我们不希望a标签进行跳转,以下是一些常用方法:


hello



hello


当我们给href值设置为undefined,也可以避免a标签的默认跳转行为:


hello



数值表示法


3e9

科学计数法是一种数学术语,将一个数表示为a乘以10的n次方,如光速30万公里每秒,在计算中通常将米做单位,则记为:300000000m/s,而在JavaScript中我们可使用科学计数法 3e9表示。

在这里举几个科学计数法的示例:


1e5; // 100000



2e-4; // 0.0002



-3e3; // -3000


Number对象有toExponential(fractionDigits)方法以科学计数法返回该数值的字符串表示形式,参数fractionDigits可选,用于用来指定小数点后有几位数字,例如:(179000).toExponential; // “1.79e+5″。

以下情况JavaScript会自动将数值转为科学计数法表示:

  1. 小数点前的数字多于21位。

  2. 数值小于1且小数点后的零多于5个,如0.0000001。


.5px

通常某些人习惯省略0.开头的数字,常见于数值计算、css属性中,比如0.5px可直接写为.5px,0.2 * 0.3可写为: .2 * .3


0x、0o和0b

在十进制的世界里呆久了,请不要忘记还有其他进制的存在,在计算机中它们是同地位的。JavaScript提供了以下进制的表示方法:

  • 二进制:只用0和1两个数字,前缀为0b,十进制13可表示为0b1101

  • 八进制:只用0到7八个数字,前缀为0o、0,十进制13可表示为0o15、015

  • 十六进制:只用0到9的十个数字,和a到f六个字母,前缀为0x,十进制13可表示为0xd

默认情况下,JavaScript 内部会自动将八进制、十六进制、二进制转为十进制再进行运算。从十进制转其他进制请查阅toString方法,从其他进制转十进制请查阅parseInt方法,从其他进制转其他进制请先转为十进制再转为其他方法。

e186850bdd4c7bba5d31ebf83c065b53.png


“话术”


Array.prototype.sort

Array.prototype.sort默认根据字符串的Unicode编码进行排序,具体算法取决于实现的浏览器,在v8引擎中,若数组长度小于10则使用从插入排序,大于10使用的是快排。

而sort支持传入一个compareFunction(a, b)的参数,其中a、b为数组中进行比较的两个非空对象(所有空对象将会排在数组的最后),具体比较规则为:

  • 返回值小于0,a排在b的左边

  • 返回值等于0,a和b的位置不变

  • 返回值大于0,a排在b的右边

因此利用sort即可写一个打乱数组的方法:


[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;



}


经过测试,效率与lodash对比如下:

f06b5db6f3b88ddfae96c4bca51d893f.png

对上述方法中的Array.prototype.concat.apply([], target)亦可以写成:.concat(…target)。


Array.prototype.push.apply

在es5中,若想要对数组进行拼接操作,我们习惯于使用数组中的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]);



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还有一个重要的特性,那就是允许你修改它的值,若修改值小于数组本身的最大索引,则会对数组进行部分截取:


const arrs = [1, 2, 3, 4];



arrs.length = 2; // arrs = [1, 2]



arrs.length = 0; // arrs =


若赋予的值大于当前最大索引,则会得到一个稀疏数组:


const arrs = [1, 2];



arrs.length = 5; // arrs = [1, 2,,,,]


若将值赋为0,则执行了清空数组的操作:


const arrs = [1, 2, 3, 4];



arrs.length = 0; // arrs =


使用此方法会将数组中的所有索引都删除掉,因此也会影响其他引用此数组的值,这点跟使用arrs = 有很大的区别:


let a = [1,2,3];



let b = [1,2,3];



let a1 = a;



let b1 = b;



a = ;



b.length = 0;



console.log(a, b, a1, b1); // , , [1, 2, 3],


在对length进行修改的时候,还需要注意:

  • 值需要为正整数

  • 传递字符串会被尝试转为数字类型


Object.prototype.toString.call

每个对象都有一个toString,用于将对象以字符串方式引用时自动调用,如果此方法未被覆盖,toString则会返回[object type],因此Object.prototype.toString.call只是为了调用原生对象上未被覆盖的方法,call将作用域指向需要判断的对象,这样一来就可以通过原生的toString方法打印对象的类型字符串: Object.prototype.toString.call([]) => “[object Array]”,利用这个特性,可以较为精确的实现类型判断。

在ES3中,获取到的type为内部属性[[Class]]属性,它可以用来判断一个原生属性属于哪一种内置的值;在ES5中新增了两条规则:若this值为、undefined分别返回: [object ]、[object Undefined];在ES6中不存在[[Class]]了,取而代之的是一种内部属性:[[NativeBrand]],它是一种标记值,用于区分原生对象的属性,具体的判断规则为:


19.1.3.6Object.prototype.toString



When the toString method is called, the following steps are taken:



If the this value is undefined, return "[object Undefined]".



If the this value is , return "[object ]".



Let O be ! ToObject(this value).



Let isArray be ? IsArray(O).



If isArray is true, let builtinTag be "Array".



Else if O is a String exotic object, let builtinTag be "String".



Else if O has a [[ParameterMap]] internal slot, let builtinTag be "Arguments".



Else if O has a [[Call]] internal method, let builtinTag be "Function".



Else if O has an [[ErrorData]] internal slot, let builtinTag be "Error".



Else if O has a [[BooleanData]] internal slot, let builtinTag be "Boolean".



Else if O has a [[NumberData]] internal slot, let builtinTag be "Number".



Else if O has a [[DateValue]] internal slot, let builtinTag be "Date".



Else if O has a [[RegExpMatcher]] internal slot, let builtinTag be "RegExp".



Else, let builtinTag be "Object".



Let tag be ? Get(O, @@toStringTag).



If Type(tag) is not String, set tag to builtinTag.



Return the string-concatenation of "[object