最近在读《你不知道的 Javascript》,在变量部分,觉得内容很是有趣,工作中其实每天都在使用变量,但有些骚操作(奇技淫巧)看到后还是想写出来与各位分享一下。
let a = 3;
let b = [3];
a == b; // 这个语句会输出什么?
上面这个语句如果在实际开发中,相信各位肯定不会用 == 来进行判断。
==
和 ===
的区别是: ==
会对比较类型进行强制的类型转换,而 ===
不会,所以平时我们总是说 ==
是宽松的比较,===
是严格的相等。
let a = "45";
let b = 45;
a == b; // true
a === b; // false
这里再说明一点,==
是怎样的转换逻辑?我们知道 ==
比较会自动进行强制类型转换,这种转换遵循的是什么规则?
摘录一下书中的引用
如果 Type(x) 是数字,Type(y) 是字符串,则返回 x == ToNumber(y) 的结果 如果 Type(x) 是字符串,Type(y) 是数字,则返回 ToNumber(x) == y 的结果 如果 Type(x) 是字符串/数字,Type(y) 是对象,则返回 x == ToPrimitive(y) 的结果 如果 Type(y) 是对象,Type(x) 是字符串/数字,则返回 ToPrimitive(x) == y 的结果
所以总结来说,如果比较的是基本数据类型,会先转换为数字进行比较,如果是对象比较,则会通过 ToPrimitive
进行隐式转换。这里提到的 ToNumber
在书中被称为「抽象操作」,在「抽象操作」中,不仅有 ToNumber
,还有 ToString
, ToBoolean
。话已到此,我们再开一个分支先来讲讲这些抽象操作的作用及用法。
在 ES5 中定义了一些「抽象操作」,其作用是内部“虚拟”的一些操作或者方法,无法直接调用这些行为,是一种隐式的类型转换,接下来会介绍几种常见的抽象操作。
let o = {};
let a = {
b: 42,
c: o,
d: function () {},
};
o.e = a; // 创建循环引用,此时 a 对象中包含了「非安全」值,此时 JSON.stringify() 会报错
a.toJSON = function () {
return { b: this.b };
};
JSON.stringify(a); // '{"b": 42}'
valueOf
方法,如果没有则检查是否含有 toString
方法,然后对其中某个函数返回的值进行强制类型转换,如果这两个方法都没有返回的「基本类型」值,则会输出 TypeError
错误。 var a = {
valueOf: () => {
return "45";
},
};
Number(a); // 45
undefined
, null
, false
, +0
, -0
, NaN
, ""
。对象一般都会被强制转换为 true,也有例外。document.all
是 JS 中的一个假值对象,因为它在现代浏览器中已经被废除,经常通过 if (document.all)
来判读是否在 IE 浏览器环境。所以我们在来检查之前的问题,下述判断会输出什么?
let a = 3;
let b = [3];
a == b; // 这个语句会输出什么?
我们知道非严格相等 ==
会进行隐式的类型转换(抽象操作),所以对于上述比较,左侧 a
就是数字无需转换,右侧 b
是数组,对于数组来说,会进行 ToNumber
的隐式转换,调用数组的 ToPrimitive
方法(先检查 valueOf
,再检查 toString
)对于数组来说,toString
方法被重新定义过了,故等式右侧会被转换为 3。所以上述比较返回了 true
。
let a = [1, 2, 3];
String(a); // '1,2,3'
所以我们之前提到的 ToPrimitive
的含义是什么?即 ECMASCript 规范中定义的「抽象操作」,按照字面理解即返回原始值。在执行 ToPrimitive
操作时,遵循下面的顺序
valueOf
方法,则优先调用该方法返回原始值toString
方法,调用该方法返回原始值ToPrimitive
实质是担任了将对象转换为原始值的作用,这里就不得不提「拆箱」和「装箱」的概念了。「装箱」:boxing,顾名思义,就是打包的意思,这里可以理解为将属性和方法打包
「拆箱」:unboxing,同理,拆除包装,还原为之前的状态
这两种行为我们在日常开发中经常用到,概括来说:
「装箱」即将基本类型的值转换为对应的对象
「拆箱」则是将对象转换为基本类型
let a = "123"; // JS 会自动为 a 进行装箱操作,这样可以像「字符串对象」一样使用变量 a
a.length; // 3
a.toUpperCase(); // "ABC"
上面的例子中,JS 会自动帮我们进行装箱操作,那么我们可以自己进行装箱吗?
let a = new Boolean(false);
if (!a) {
console.log("可以执行到这儿么?");
}
上面代码是无法进行到分支中的,我们自己对 false 进行了装箱操作,a 此刻变成了一个对象,在 if 条件中进行了隐式的类型转换,而此时 Boolean(a) === true
。
所以在实际开发中,我们应该优先使用 JS 自动的装箱逻辑,相对于自己进行装箱,JS 的装箱性能也较优,也可以规避一些不太明显的错误。
对于「拆箱」实质就是 ToPrimitive
的过程,对于要使用到基本类型的地方,都会进行隐式的「拆箱」过程。
思考如下代码:
let a = "abc";
let b = Object(a);
a == b; // true
a === b; // false
这里 Object
函数的作用类似「装箱」,将变量 a
封装为对象,所以在宽松比较 ==
时,会进行 ToPrimitive
逻辑,此时 b 会被隐式转换为 'abc',所以 a == b
等式成立。
null
没有对象的包装对象,所以 b
此刻是一个普通的对象,a == b
等式不成立。所以同理可以推断 undefined
, NaN
这种特殊的字段也会是相同结果。我们在日常开发中,经常会使用 ||
的方式设置默认值
let a = c || "default value";
上述语句中,如果 c
的 ToPrimitive
为 false/undefined/null
等假值时,就会使用 ||
右侧的值来为 a
赋值。
let a = c && b;
// 等效为
if (c) {
b;
}
对于 &&
来说,相当于是逻辑判断的简略写法。一些压缩代码工具压缩后,常见的逻辑判断会被转换为 xx && yy
。
JS 中由于灵活的类型声明,导致了隐式转换几乎遍布于整个代码中,日常开发有些驾轻就熟的使用技巧多理解些原理也会更加从容,有些知识是常看常新的。
欢迎大家互相交流。
本文中例子大部分引用于《你不知道的 JavaScript》(中)。
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。