资讯专栏INFORMATION COLUMN

有关javascript强制转换不得不说的故事

xcold / 1668人阅读

摘要:我们首先了解一下中有关类型转换的知识。新增类型抛出异常从列表可以明显看到少了一个类型转换为的规则。这里要强调一点第二个表达式没有涉及到强制类型转换。如果文中有错误或者有某些强制转换的情形没有涉及到请及时留言告知,我会修改并补充进去。

javascript是一门非常奇特的语言,它有时候奇特的会让人怀疑人生。比如让我们看一下下面的一些奇葩例子:

 false == "0"           //true   "哇"
 false == 0             //true   "哦"
 false == ""            //true   "噢"
 false == []            //true   "啥?"
 
 0 == ""                //true   "what?"
 0 == []                //true   
 0 == "0"               //true   
 [] == "0"              //false  "why?"
 [] == ""               //true   
 
 //-----------更惊讶的是---------------

 [] == ![]              //true    "WTF!"
 [2] == 2               //true  
 "" == [null]           //true 
 0 == "
"              //true     我还能说什么呢?
 false == "
"          //true

还有许多可以列出来吓你一跳的例子,别怀疑我是随便编出来骗你的。当时我在浏览器运行这些时,我都怀疑我以前学得是假的js。如果要形容我当时的表情的话,你想一下黑人小哥的表情就能明白我当时是有多怀疑人生。
好,现在让我们先喝杯水压压惊,暂时忘记前面那些奇葩的例子。我们首先了解一下js中有关类型转换的知识。

类型转换

学过js的应该都了解js是一门弱类型语言。你在声明一个变量的时候没有告诉它是什么类型,于是在程序运行时,你可能不知不觉中就更改了变量的类型。可能有些是你故意改的,另一些可能并不是你的本意,但是不管怎样你都不可避免的会遇到类型转换(强制或隐含)。让我们看一下下面的列子:

  var a = "1";
  var b= Number(a);               // b=1; 
  +a;                             // 1;
  b + "";                         // "1";    
     

大家应该都知道答案,很多人在代码中或多或少都会用到这些方法,并且都明白其中发生了值的类型转换,但是你们是否有深入了解js内部在类型转换时做了哪些操作呢?

ToBoolean(argument)

我们首先来了解强制转换为Boolean类型时,发生了什么操作。在用调用Boolean(a)或者!a等操作将值转换为Boolean类型时,js内部会调用ToBoolean方法来进行转换,该方法定义了以下规则:

argument的类型 转换的结果
Undefined false
Null false
Boolean argument
Number 如果argument是 +0、-0、NaN, 返回false; 否则返回true.
String 如果arguments是空字符串(长度为0)返回false,否则返回true
Object true
Symbol(ES6新增类型) true

从这个列表中我们简单概括一下就是只要argument的值是(undefined、null、+0、-0、NaN、""(空字符串)以及false))这7个里的其中一个,那转换之后返回的是false,其他都为true。js专门把这7个值放到一个falsy列表中,其余值都放在truthy列表。

ToNumber(argument)

ToNumber顾名思义即把其它类型转换为Number类型(js内部调用的方法,外部无法访问到),ECMAScript官方也专门给出了转换规则:

argument的类型 转换的结果
Undefined NaN
Null +0
Boolean false为+0,true为1
Number 返回argument
Object 执行以下步骤:让primValue成为ToPrimitive(argument, hint Number)的返回值,再调用ToNumber(primValue)返回。
Symbol(ES6新增类型) 抛出TypeError异常.

从列表可以明显看到少了一个String类型转换为Number的规则。因为String转Number,js内部有非常复杂的判断,我这里面不详细说转换的细节,有兴趣的可以看一ECMAScript官方的说明。只要知道它与确定Number字面量值的算法相似,但是要注意一下细节:

一个空(empty)的或只包含空格的字符串被转换为+0。

StrWhiteSpace会转化为+0

StrNumericLiteral前后的StrWhiteSpace会被忽略。

StrNumericLiteral前面的多个0会被忽略。

不是StringNumericLiteral的扩展会变为NaN。

在这里特别说明一下
StrWhiteSpace:在js中StrWhiteSpace包含WhiteSpace(空白符)和LineTerminator(终止符)。
StrNumericLiteral:可以理解为包含Infinity和数字的字符串集合。
StringNumericLiteral:包含StrNumericLiteral和StrWhiteSpace的集合

WhiteSpace
Unicode Code Point name
U+0009 制表符
U+000B 垂直方向的制表符
U+000C 换页符
U+0020 空格符
U+00A0 不换行空格符
U+FEFF 零宽度不换行空格符
其他种类的“Zs”(分隔符,空白) Unicode “Space_Separator”

ECMAScript WhiteSpace有意排除具有Unicode“White_Space”属性但未在类别“Space_Separator”(“Zs”)中分类的所有代码点。

Zs列表
我这边列出了Unicode其它“Zs”的列表,感兴趣的可以了解一下:

Unicode Code Point name
U+1680 OGHAM SPACE MARK
U+180E MONGOLIAN VOWEL SEPARATOR
U+2000 EN QUAD
U+2001 EM QUAD
U+2002 EN SPACE
U+2003 EM SPACE
U+2004 THREE-PER-EM SPACE
U+2005 FOUR-PER-EM SPACE
U+2006 SIX-PER-EM SPACE
U+2007 FIGURE SPACE
U+2008 PUNCTUATION SPACE
U+2009 THIN SPACE
U+200A NARROW NO-BREAK SPACE
U+202F FIGURE SPACE
U+205F MEDIUM MATHEMATICAL SPACE  
U+3000 IDEOGRAPHIC SPACE
LineTerminator
Unicode Code Point name
U+000A 换行符
U+000D 回车
U+2028 行分隔符
U+2029 段分隔符

上面的过程说的很抽象,不是很容易理解,我们来看一下具体的列子:

Number("");                   //0  empty
Number("    ");               //0  多个空格
Number("u0009");             //0  制表符也可以用Number("	")表示
Number(
);                            //0  换行符也可以用Number("
")或Number("u000A")表示
Number("000010");             //10 1前面的多个0被忽略
Number("    10    ");         //10 string前后多个StrWhiteSpace
Number("u000910u0009");     //10 string前后有制表符
Number("ab");                 //NaN 

StrNumericLiteral中的其它进制的数字与十进制有相似的规则,但转化的Number值是十进制下的值:

Number("0b10");               //2   (二进制)
Number("0o17");               //15  (八进制)
Number("0xA");                //10  (十六进制)

还有说明一点是十进制下数字的科学计数法显示的字符串也能通过ToNumber转换为Number类型:

Number("1.2e+21");            //1.2e+21
Number("1.2e-21");            //1.2e-21 
ToString(argument)

转换为String类型的规则如下:

argument的类型 转换的结果
Undefined "undefined"
Null "null"
Boolean false为"false",true为true"
String argument
Object 执行以下步骤:让primValue成为ToPrimitive(argument, hint String)的返回值,再调用ToString(primValue)返回。
Symbol(ES6新增类型) 抛出TypeError异常.

同样的在表中我也没有列出Number类型转换为String类型的规则,Number转String并不是简单的在数字前后加上‘或“就行了(即使看起来是这样),里面涉及到了复杂的数学算法,我不细说(好吧主要是我没有特别理解,具体算法可以看文档),在这里我只列出几种特殊情况:

假设Number的值为m:

如果m是NaN,返回String "NaN"。

如果m是+0或-0,返回String "0"。

如果m小于0, 返回字符串连接符"-"和ToString(-m)。

如果m是+∞,返回String "Infinity"。

ToPrimitive(input [ , PreferredType ])

我们在上面ToNumber和ToString方法中注意到Object类型转换为Number和String时都会调用ToPrimitive方法。该方法接受一个input输入参数和一个可选的PreferredType参数。PreferredType是用来决定当某个对象能够转换为多个基本类型时该返回什么类型。可是ToPrimitive内部究竟是如何操作来返回Number或String类型的呢?如果要深入探究其具体的操作步骤可能花大半天也不能完全理清,里面包含了各种方法的调用以及复杂的逻辑判断还有各种安全检测,我不仔细深入下去。我这边假设所有的判断都按正常流程走,所有安全机制都通过不报错误,那么一个对象转换为Number或String就可以概括为以下几个判断:

一个对象上是否有@@toPrimitive方法定义,如果有调用该方法返回结果。

对象上如果没有定义@@toPrimitive方法,则沿着该对象的原型链向上查找,直到找到或者[[Prototype]]为空。

如果该对象和其原型链上都没有定义@@toPrimitive方法,则调用OrdinaryToPrimitive(O,hint);

hint有PreferredType决定,如果PreferredType是hint Number,hint为"number",PreferredType是hint String,hint为"string",如果没定义,默认hint为"number",O就是input对象。

OrdinaryToPrimitive方法的判断是:如果hint为"string",在O上调用« "toString", "valueOf" »。意思是在O以及原型链上先查找"toString"方法,找到第一个toString方法就调用toString返回结果,如果没有就查找”valueOf“方法来返回结果。

如果hint为"number",在O上调用« "valueOf", "toString" »。

@@toPrimitive、« "toString", "valueOf" »和« "valueOf", "toString" »方法调用返回一个Object类型时可能会报TypeError错误

@@toPrimitive是Symbol类型,是Symbol.toPrimitive的简写,ES6之前没有Symbol类型,所以只需判断toString和valueOf方法。

我这边用几个例子来解释ToPrimitive的运行过程

var a = {
    [Symbol.toPrimitive]: (hint)=>{
        if(hint==="number"){
            return 1;
        }else if(hint==="string"){
            return "Symbol.toPrimitive";
        }else if(hint==="default"){
            return 2;
        }else{
            throw TypeError("不能转换为String和Number之外的类型值");        //防止内部出现错误     
        }
    },
    toString: () => "toString",
    valueOf: () => 3
}; 
Number(a);                    //1         hint为"number"
String(a);                    //"Symbol.toPrimitive"  hint为"string"
a + "1";                      //"21"      a在进行+操作符时hint为"default",因为程序不知道你是做字符串相加还是数值相加
a + 1;                        //3 
+a;                           //1         此时hint为"number",为什么hint不是"default",+a实际上内部进行ToNumber转换,-、*、/操作符类似  

//删除a中Symbol.toPrimitive属性                            
delete a[Symbol.toPrimitive];
Number(a);                    //3         调用valueOf方法
String(a);                    //"toString"   调用toString方法
a + 1;                        //4        结果不是"toString1"是因为js内部先判断valueOf方法
//删除a中valueOf属方法 
delete a["valueOf"];
Number(a);                    //NaN     返回的"toString"不能转换为有效数字
String(a);                    //"toString"
1 + a;                        //"1toString"
//重写a中的toString方法
a.toString = () = > a;        //返回了a对象
Number(a);                    //TypeError
String(a);                    //TypeError
1 + a;                        //TypeError

上面例子看出Object类型在转换为String和Number时有可能会出现各种各样的情况。为此我们最好永远不要重写对象中的valueOf或者toString方法,以防出现意想不到的结果,如果你重写了方法那么你就要格外小心了。

Object.prototype.toString= () => 1;
1 + {};        //2   看到了吗?永远不要重写Object中的内置方法,最好也不要在子对象中覆盖Object的内置方法。

在此我们对js中强制转换时发生的过程基本捋了一遍,接下来我们来了解一下相等操作符两边发生了什么。

Abstract Equality Comparison

ECMAScript官方对(==)操作的说法是Abstract Equality Comparison(抽象的相等比较),它对x==y定义了下面一些规则:

如果x和y是同一类型,进行Strict Equality Comparison x === y。

如果x是null,y是undefined,返回true。

如果x是undefined,y是null,返回true。

如果x的类型是Number,y的类型是String,进行x==ToNumber(y)。

如果x的类型是String,y的类型是Number,进行ToNumber(x)==y。

如果x的类型是Boolean,进行ToNumber(x)==y。

如果y的类型是Boolean,进行x==ToNumber(y)。

如果x的类型是String、Number或者Symbol,y的类型是Object,进行x==ToPrimitive(y)。

如果x的类型是Object,y的类型是String、Number或者Symbol,进行ToPrimitive(x)==y。

其他返回false

Strict Equality Comparison

Strict Equality Comparison(严格的相等比较)对x===y定义下列规则:

如果x和y是不是同一类型, 返回false。

如果x的类型是Number:

- 如果x或y是NaN,返回false。
- 如果x和y数值相同,返回true。
- 如果x是+0,y是-0,返回true。
- 如果x是-0,y是+0,返回true。
- 其他返回false。

如果x是Undefined类型,返回true。

如果x是Null类型,返回true。

如果x是String类型,x和y是完全相同的代码单元序列返回true,否则false。

如果x是Boolean类型,x和y都是true或都是false,返回true,否则返回false。

如果x是Symbol类型,x和y是相同的Symbol值,返回true,否则返回false。

如果x和y是相同的对象,返回true,否则返回false。

提到(===)操作符,我们不等不说一个方法Object.is(a,b),该方法也是比较两个值是否一样,但它比(===)更严格。它们之间的区别在于如果x和y是NaN,返回true。如果x是+0,y是-0,返回false,如果x是-0,y是+0,返回false。

验证

到这里类型转换和相等比较的介绍就告一段落了,现在我们重新回过头去看一下最开始的几个奇特例子,你会发现它们之间的关系比较是如此的正常。我就拿([] == ![])进行讲解,按照操作符优先级比较,先运行![],它的值为false,这时等式变成([] == false);按(==)的规则7对false进行ToNumber操作,值变为0,这时等式变为([] == 0);按(==)的规则9对[]进行ToPrimitive操作,调用Array上的toString方法,返回"",这时等式变为("" == 0);按(==)的规则5对""进行ToNumber操作,值变为0,这时等式是(0==0)。我们最终得出结论([] == ![])是对的。

补充

我们看一下下面的例子:

1 + {};               //"1[object object]"
{} + 1;               //1
({} + 1);             //"[object object]1"

我们发现第一和第三个表达式按照我们预期的值输出了,但是第二个表达式却没有。这里要强调一点:第二个表达式没有涉及到强制类型转换。他把这个表达式看成了两个,一个是块{},还有一个是+1,把{}丢弃l,所以输出的值1。至于1+{},js把他看成一个表达式,所以{}被强制转换为"[object object]";第三个表达式加了(),使js认为{}+1是一个整体,所以{}也被强制转换了。

结束

到这里我想说的基本就结束了。如果文中有错误或者有某些强制转换的情形没有涉及到请及时留言告知,我会修改并补充进去。

文章版权归作者所有,未经允许请勿转载,若此文章存在违规行为,您可以联系管理员删除。

转载请注明本文地址:https://www.ucloud.cn/yun/89630.html

相关文章

  • JavaScript数据类型和他背后不得不说故事

    摘要:基本概念中有种简单数据类型也称为基本数据类型,存放在栈中和。在使用声明变量但未对其加以初始化时,这个变量的值就是,例如类型是第二个只有一个值的数据类型,这个特殊的值是。类型阿拉伯数字的八进制十进制十六进制整数浮点数。 基本概念 ECMAScript 中有 5 种简单数据类型(也称为基本数据类型,存放在栈中):Undefined、Null、Boolean、Number 和String。还...

    ASCH 评论0 收藏0
  • JavaScript数据类型和他背后不得不说故事

    摘要:基本概念中有种简单数据类型也称为基本数据类型,存放在栈中和。在使用声明变量但未对其加以初始化时,这个变量的值就是,例如类型是第二个只有一个值的数据类型,这个特殊的值是。类型阿拉伯数字的八进制十进制十六进制整数浮点数。 基本概念 ECMAScript 中有 5 种简单数据类型(也称为基本数据类型,存放在栈中):Undefined、Null、Boolean、Number 和String。还...

    avwu 评论0 收藏0
  • js 学习笔记(一)

    摘要:前言网上其实已经有非常多的学习资料了,但是每个人都有自己的基础,所以往往是有的人讲的深一点,有的人说的浅一点。讲述的人们因为害怕洪水的再次到来,而准备联合起来修建一座直通天际的高塔以传扬聚集四散的人类。 前言 网上其实已经有非常多的js学习资料了,但是每个人都有自己的基础,所以往往是有的人讲的深一点,有的人说的浅一点。 就我自身而言,想要匹配自己水平的找些资料,往往是十分的零碎,所以可...

    xiguadada 评论0 收藏0
  • 谈谈 var, let, const. var 也是一种选择

    摘要:谈谈也是一种选择历史故事在之前是一门被称为没有块级作用域的语言看看代码输出结果权威解析这是因为被声明在当前函数的作用域内不管你声明在函数的什么位置在函数执行之前解析器会扫描当前函数作用域并将以和开头的语句的变量名添加到当前函数作用域内这意味 谈谈 var, let, const. var 也是一种选择 历史故事 在 ES6 之前, JavaScript 是一门被称为没有块级作用域的语言...

    BoYang 评论0 收藏0
  • 好记性不如烂笔头,极光向你发出征文邀请函

    摘要:注参与奖任选其一,参与即有。参与征文的小伙伴均可参与一次抽奖,极光精美周边随心抽。这么多产品,肯定有一款与你产生过一些不得不说的故事,如果不巧还没有,那么我希望现在就是我们故事的开端。 由极光 1举办的征文大赛 :writing_hand:️ ——「我和极光的那些事儿」第三届如约而至! 过完双十一和双十二,是不是特想剁掉那只买买买的手?千万别 手觉得 ta 还能抢救一下 留着 ta 参...

    Terry_Tai 评论0 收藏0

发表评论

0条评论

最新活动
阅读需要支付1元查看
<