这一册回答这样两个问题:
类型和值真正的工作方式 强制转换的角落和缝隙、强制转换值得花时间学习&合理的地方
主要内容:回答问题
谈谈类型和值真正的工作方式
JavaScript中定义的7种内建类型?
首先要说明的是,变量没有类型,变量持有有类型的值。
其中内建类型有:null、undefined、string、number、boolean、object、symbol(ES6新加);
使用typeof可以准确测出除null外的6个,还有function:tyepof demo;如果要测试null:
//唯一一个typeof是object但是是fasly的值
var a = null;
(!a && typeof a === "object"); // true
Array、String、Number和特殊值认识补充
类Array是什么?有例子吗?如何转换为Array?
类Array指长得很像Array但是没有链接到Array.prototype的数据结构,比如DOM查询操作返回的DOM元素列表(链接到NodeList.prototype)、函数内参数值暴露的arguments对象(链接到Object.prototype,在ES6被废弃)。
如何转换有两种方式:
Array.prototype.slice.call(***)Array.from(***)
对String和Array有肤浅的相似性这样的说法怎么看?
肤浅的相似性体现在它们都有一个length属性,一个indexOf方法和一个concat方法:demo
说是肤浅说明两者更大的是区别。String是不可变的,String.prototype上挂载的方法操作字符串,不是原地修改内容,而是创建返回一个新的字符串;Array是相当可变的,尽管Array.prototype上挂载的方法有创建返回一个新数组的非变化方法,也有原地修改内容的变化方法。
对于String操作,有些Array方法是有用的,我们可以对string"借用"非变化的array方法:demo;也有著名的反转字符串的例子是做了String=>Array=>String的转换:demo。当然如果频繁借用Array方法,那为什么最开始不直接设为Array类型。
0.1+0.2===0.3为什么是false?安全整数是什么?
第一个问题,0.1和0.2在js中的二进制形式表达是不精确的,相加结果不是0.3,而是接近0.3的一个值,所以是false。为了靠谱地做数值的比较,引入一个容差/机械极小值,在ES6中这个值是Number.EPSILON,只要差值的绝对值小于容差,认为是数值相等。
第二个问题,JavaScript能表示的最大最小值是Number.MAX_VALUE和Number.MIN_VALUE,但是对于整数而言有一个安全范围,在这个安全范围内,整数是无误表示,这个范围是Number.MIN_SAFE_INTEGER到Number.MAX_SAFE_INTEGER。
NaN是什么?如何判定一个值是NaN?
使用不同为number的值做操作等到的就是一个NaN,它是一个哨兵值,可以理解为不合法的数字/失败的数字,试着进行数学操作但是失败了,这就是失败的number结果。
NaN值的判定不能直接和NaN比较,因为NaN是唯一一个自己不等于自己的。ES6给了一个Number.isNaN(..),如要要填补的话,类型是数字+自己不等于自己两个条件。
ES6提供了Object.is(..),用来测试两个值的绝对等价性,没有任何例外:demo。Object.is(..)的定位是这样的:
Object.is(..)可能不应当用于那些==或===已知 安全 的情况(见第四章“强制转换”),因为这些操作符可能高效得多,并且更惯用/常见。Object.is(..)很大程度上是为这些特殊的等价情况准备的。
谈谈原生类型构造器/内建函数
内部[[Class]]是什么?
typeof结果是object的值被额外地打上一个内部标签属性,就是[[Class]]。这个属性不能直接访问,但可以间件通过Object.prototype.toString.call(..)访问。
比如:demo ,注意简单基本类型string、number、boolean发生封箱行为;比如结果[Object Array]后面的Array反应的是原生类型构造器/内建函数这个信息;
特殊的是null和undefined也能工作,结果是[Object Null] [Object Undefined],虽然没有Null和Undefined;
Object.prototype.toString.call(..)也被用来测值类型,比typeof更完善。
谈谈封箱与开箱?自动封箱有什么优势?
封箱与开箱是两个相反的过程,封箱将基本类型值包装进对象,开箱从对象中取出基本类型值。封箱有手动封箱(比如new String("aaa"))和自动封箱两种,在基本类型值上访问对象的属性和方法就会发生自动封箱,比如var a = "zzz";a.length。自动封箱,即让封箱在需要的地方隐含发生,这是推荐的,一个是方便另一个是浏览器对于.length等常见的情况有性能优化。开箱也分手动开箱(valueOf)和自动开箱两种,当以一种查询基本类型值的方式使用对象包装时,自动开箱发生,即开箱隐含发生,给个demo。
这么多的原生构造器...表个态?
Array、Object、Function、RegExp
这四种的话使用字面形式创建值几乎是更好的选择。
(使用Array构造器的一个用途:快速创建undefined * n的数组)
(使用Object构造器,得一个一个去添加属性,麻烦!)
(Function构造器用于动态定义函数,这篇文章里有一个demo,但是一般也挺少用的)
使用RegExp有一个用途是字面形式没有的——用于创建动态的正则表达式
Date、Error
这两种的话没有字面形式,只能使用原生类型构造器。
使用Date构造器给一个demo,获取当前时间时间戳比较常用
Error手动使用比较少,一般是自动被抛出,但是有些像message、type、stack的属性可能会有用
Symbol
使用Symbol(...)构造器生成的数据是简单的基本标量(注意不是object),Symbol主要为特殊的ES6结构的内建行为设计。可以自定义,也有预定义的Symbol值。(这里简单带过了,以后实际遇到了再研究)
谈谈强制转换的角落和缝隙、强制转换值得花时间学习&合理的地
抽象操作ToString、ToNumber、ToBoolean分别是?和强制转换的关系?
强制转换最后都是转为boolean、string、number这样的基本标量,而抽象操作ToString、ToNumber和ToBoolean规定了这样的转换。
首先是ToString,转换为string,给个demo,注意ToString可以被明确地调用,也可以在一个需要String的上下文环境中自动地调用;另外想补充一下JSON.stringify(..)字符串化(本身和ToString的抽象操作没有关系,只要是对简单类型的字符串化行为比较像),JSON字符串化分安全值和非法值,非法值主要是因为不能移植到其他消费JSON值的语言,非法值包括undefined、function、symbol和带有循环引用的object。object可以定义一个toJSON方法,该方法在JSON.stringify(..)调用时首先被调用,主要用途是将JSON非法值转换为安全值,另外JSON.stringify(..)也提供第二个参数是替换器——过滤object属性、第三个参数填充符。
然后说说ToNumber,转换为number,给个demo,注意object/array首先是ToPrimitive抽象操作。
最后是ToBoolean,转换为boolean,这个比较简单,falsy列表中的转换为false:null、undefined、""、+0、-0、NaN、false。
明确地String<-->Number
最明确的当然是不带new的String(..)和Number(..);另外x.toString()也被认为是明确转为字符串,尽管背后隐含地发生了封箱;一元操作符+也被认为是明确转为数字,但是如果情况是多个+/-连着,是增加困惑的。
和String-->Number似乎有点关联的是解析数字字符串的行为(比如parseInt、parseFloat),解析数字字符串和类型转换的一个最大区别是,解析数字字符串是容忍错误的,比如parseInt("42px")结果是"42",而类型转换是零容忍的。关于parseInt有个网红面试题。
明确地 * --> Boolean
明确地转为布尔值类型有两种方式:不带new的Boolean(a)和两个否定符号!!a。Boolean(a)非常明确,但是不常见,!!a更加明确。
有用的隐含强制转换
隐含地 Strings <--> Numbers
Numbers --> Strings
两个操作数的+操作符是这样工作的:如果两个操作数之一是一个string(或者object/array通过ToPrimitive抽象操作成为一个string),做string连接;其他情况做数字加法。使用在隐含地Numbers转换为Strings上面是,number + ""。
Strings --> Numbers
这里使用-操作符,-操作符仅为数字操作定义,所以两个操作数都会被转为number,即发生ToNumber抽象操作。使用在隐含地Strings转换为Numbers上面是,string - 0。
隐含地 Booleans --> Numbers
两个操作数的+操作符在没有string的情况是做数字加法,也适用这里,比如true+true===2 ,这里就隐含地发生了Booleans转换为Numbers。这个点有个有意思的用途,比如有3个条件,在仅一个为true情况做后续操作:
const cond1 = ....;
const cond2 = ....;
const cond3 = ....;
if((cond1&&!cond2&&!cond3)||(!cond1&&cond2&&!cond3)||(!cond1&&!cond2&&cond3)){
//后续操作...
}
这里的boolean逻辑就很复杂,但可使用前面的方法用数字加法做简化:
if((!!cond1+!!cond2+!!cond3)===1){//后续操作...}
隐含地 * --> Booleans
有些表达式操作会做一个隐含地强制转换为boolean值:
if(..)for(;;)第二个子句while(..)、do{..}while(..)?:三元表达式的第一个子句||、&&
其中||和&&被称为逻辑或和逻辑与,但也可以认为是操作数选择器操作符,因为:
a || b; //大致相当于 a?a:b
a && b; //大致相当于 a?b:a
另外JS压缩器会把if(a){foo();}转换为a&&foo(),因为代码更短,但是很少有人这么做(我有这么做过...)
怎么选择宽松等价与严格等价?宽松等价的抽象等价性比较算法可以说一说吗?
对待宽松等价与严格等价的态度
比较同类型的值时,它们算法是相同的,做的工作也基本是一致的,所以这里是随意的;在比较不同类型的值,宽松等价允许做强制转换,如果想要强制转换用宽松等价,否则用严格等价。 宽松不等价与严格不等价是在前面两个的基础上取反。
宽松等价的“抽象等价性比较算法”
同类型的值是简单自然地比较;
不同类型的值:
- 比较:
string与number
如果Type(x)是Number而Type(y)是String, 返回比较x == ToNumber(y)的结果。
如果Type(x)是String而Type(y)是Number, 返回比较ToNumber(x) == y的结果。
- 比较:任何东西与
boolean
如果Type(x)是Boolean, 返回比较 ToNumber(x) == y 的结果。
如果Type(y)是Boolean, 返回比较 x == ToNumber(y) 的结果。
这里挂个demo,提醒千万不要==true或者==false
- 比较:
null与undefined
如果x是null而y是undefined,返回true。
如果x是undefined而y是null,返回true。
null与undefined是互相等价的,而不等价于其他值(唯一宽松等价)。这里给一个伪代码:
var a = doSomething();
if (a == null) {
// ..
}
无论a是null还是undefined,检查都是通过的,而不需要再去做a===undefined||a===null,更加简洁。
- 比较:
object与非object
- 如果Type(x)是一个String或者Number而Type(y)是一个Object, 返回比较 x == ToPrimitive(y) 的结果。
- 如果Type(x)是一个Object而Type(y)是String或者Number, 返回比较 ToPrimitive(x) == y 的结果
宽松等价的坏列表?
宽松等价的坏列表指的是比较难理解的比较情况,发生的也是隐含强制转换,demo。
为了避免坏列表,给出了可以遵循的规则:
- 如果比较的任意一边可能出现
true或者false值,那么就永远,永远不要使用==。 - 如果比较的任意一边可能出现
[],"",或0这些值,那么认真地考虑不使用==。
在这些情况,使用===比==好
a<b时发生了什么?说说抽象关系比较算法
大致流程:
- 如果
a和b的值有一个object类型,首先是ToPrimitive抽象操作处理,使得比较双方都是基础类型值 - 如果这时候两个比较对象都是
string类型,按照简单的字典顺序比较(数字在字母前面) - 如果不满足2,两个比较对象做ToNumber抽象操作处理,再进行数字比较
在这个例子中a<b、a==b、a>b结果都是false,奇怪的是a>=b、a<=b都是true。因为大于等于、小于等于更贴切的翻译是不小于、不大于,对比较操作取反。
比较没有像等价那样的“严格的关系型比较”,所以如果不允许隐含强制转换发生,在比较之前做好类型的强制转换
主要内容之外
undefined和undeclared概念
给个代码片段就知道这两者的区别:在这里a就是undefined,b就是undeclared。
var a;
a; // undefined
b; // ReferenceError: b is not defined
typeof 在对待 undeclared的变量时不会报错,这是一种安全防卫行为。它有什么用呢?
- 假设
debug.js文件中有一个DEBUG的全局变量,只有在开发环境下导入全局,生产环境不会,在其他的程序代码中可能会去检查这个DEBUG变量:demo - 再比如想象一个你想要其他人复制-粘贴到他们程序中或模块中的工具函数,在它里面你想要检查包含它的程序是否已经定义了一个特定的变量:demo
第二个实际case没有遇到过,另外也在链接下面挂出了依赖注入的解决方式。
获取当前时间戳有哪些方法?
除了之前提到的使用Date构造器的两种方法(new Date().getTime()和Date.now())以外,一元操作符+也可以:+new Date/+new Date(),但是使用一元操作符+转换的方式不够明确,所以不是很推荐。
位操作符~发生了什么?有什么用途?
位操作符|会发生ToInt32(转换为32位整数),像0|x一般被用于转换为32位整数。位操作符~首先第一步也是ToInt32的转换,其次是按位取反,给个demo。用途有两个:
- 使用
arr#indexOf、arr#findIndex、str#indexOf时如果找不到结果返回-1,-1是一个错误情况的哨兵值,使用>=-1/==-1这样的判断,是一种“抽象泄露”,而如果使用~x //0当x=-1这样的判断更加优雅 - 截断比特位,
~~截断数字的小数部分,当然是ToInt32的结果
操作符优先级(更紧密的绑定)和结合性
例如分析下面表达式的执行结果:
a && b || c ? c || b ? a : c && b : a
工作分两步,首先是确定更紧密的绑定,因为&&优先级比||高,而||优先级比? :高。所以更紧密的绑定使得上述表达式等价于:
((a && b) || c) ? (c || b) ? a : (c && b) : a
第二步是在同等优先级的操作符情况下,是从左发生分组(左结合)还是从右发生分组(右结合),比如?:是右结合的,所以a?b:c?d:e是等价于a?b:(c?d:e),而不是(a?b:c)?d:e,所以上述表达式等价于:
((a && b) || c) ? ((c || b) ? a : (c && b)) : a
掘金上有看到问true || true && fn()表达式中,fn会不会执行。
按照之前更紧密的绑定分析,等价于表达式true||(true&&fn()),但是执行顺序依然是从左往右,因为||的短接,所以在第一个选择数为true的情况,不会去执行第二个选择数,也就不会执行fn函数。
涉及TDZ(时间死区)的两种情况
let和const声明的变量是在TDZ中的,所以必须得在声明之后使用,而不会发生提升;- 参数默认值
var b = 3;
function foo( a = 42, b = a + b + 5 ) {
// ..
}
在赋值中的b引用将在参数b的TDZ中发生(不会被拉到外面的b引用),所以它会抛出一个错误。然而,赋值中的a是没有问题的,因为那时参数a的TDZ已经过去了。
switch每个case是严格等价还是宽松等价?
答案是严格等价,但是宽松等价是可以模拟的:
var a = "42";
switch (true) {
case a == 10:
console.log( "10 or '10'" );
break;
case a == 42:
console.log( "42 or '42'" );
break;
default:
// 永远不会运行到这里
}