这一册回答这样两个问题:
类型和值真正的工作方式 强制转换的角落和缝隙、强制转换值得花时间学习&合理的地方
主要内容:回答问题
谈谈类型和值真正的工作方式
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:
// 永远不会运行到这里
}
复制代码