Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

JavaScript中的类型转换 #5

Open
lznbuild opened this issue Dec 26, 2019 · 0 comments
Open

JavaScript中的类型转换 #5

lznbuild opened this issue Dec 26, 2019 · 0 comments

Comments

@lznbuild
Copy link
Owner

lznbuild commented Dec 26, 2019

JavaScript是弱类型 动态编程语言?

          (隐式类型转换)       (同一个变量保存不同类型的值)

支持隐式类型转换的语言称为弱类型语言,不支持隐式类型转换的语言称为强类型语言。

在运行过程中需要检查数据类型的语言称为动态语言。比如我们所讲的 JavaScript 就是动态语言,因为在声明变量之前并不需要确认其数据类型。

弱类型,意味着你不需要告诉 JavaScript 引擎这个或那个变量是什么数据类型,JavaScript 引擎在运行代码的时候自己会计算出来。
动态,意味着你可以使用同一个变量保存不同类型的数据。

常见3种方式判断数据类型

  • typeOf,适用于基本数据类型。

  • instanceOf,适用于引用数据类型(基于原型链的查找)。

  • Object.prototype.toString.call() 可以精准判断( 基于所有对象上的[[class]]属性 )。

typeOf

用 typeOf 判断null,返回 Object ,《高程》上表示null是一个空对象指针,《你不知道的JavaScript》上表示,这是一个存在已久的BUG,这里就仁者见仁智者见智了,不纠结到底是什么说法了。

那就不能用typeOf去判断null的类型了吗??

当然有方法

!a && typeof a === "object"

只要满足这两个条件,就能断定a为null类型。

其实不光用typeOf去判断null比较特殊,判断undefined也有它的特殊之处。

大多数开发者倾向于将 undefined 等同于 undeclared(未声明),但在 JavaScript 中它们完全是两回事。

已在作用域中声明但还没有赋值的变量,是undefined类型,默认值是Undefined。相反,还没有在作用域中声明过的变量,是 undeclared 的。

var a;
a; // undefined
b; // ReferenceError: b is not defined

浏览器对这类情况的处理很让人抓狂。上例中,“b is not defined”容易让人误以为是“b is
undefined”。这里再强调一遍,“undefined”和“is not defined”是两码事。此时如果浏览器
报错成“b is not found”或者“b is not declared”会更准确。
更让人抓狂的是 typeof 处理 undeclared 变量的方式更加让人混淆这两种情况。

var a;
typeof a; // "undefined"
typeof b; // "undefined"

对于 undeclared(或者 not defined)变量,typeof 照样返回 "undefined"。请注意虽然 b 是
一个 undeclared 变量,但 typeof b 并没有报错。这是因为 typeof 有一个特殊的安全防范
机制,就是这个机制,容易让人有误解,需要注意。

instanceOf

instanceof 操作符的左边不是对象,直接返回false,右边不是构造函数,抛类型错误。

var obj = {}
console.log(obj instanceof Object)  // true
obj.__proto__ == Object.prototype // true,这是上面返回true的原因,基于原型链的查找

但是instanceof也不是完全可信的,可以通过Symbol.hasInstance自定义instanceof行为。

class PrimitiveString {
  static [Symbol.hasInstance](x) {
    return typeof x === "string"
  }
}

console.log('hello' instanceof PrimitiveString) // true

Object.prototype.toString.call()

原理就是借用Object.prototype上的toString方法,这个方法会返回对象的[[class]]属性

[Object [[class]] ]

这里有坑,就是
Object.prototype.toString() 和 Function.prototype.
toString(),Array.prototype.toString()都不一样。

Object.prototype.toString.call({}) // [Object Object]
Object.prototype.toString.call([]) // [Object Array]
Object.prototype.toString.call(function(){}) // [Object Function]

function fn(){}

fn.toString() // 'function fn(){}',这里的toString是Function.prototype.toString

var arr = [1,2,3];
arr.toString() // 1,2,3,这里的toString是Array.prototype.toString

Array.prototype上的 toString 方法是经过重写的,跟 join 方法类似。

隐式类型转换

说了这么多,终于到正题了。

JavaScript中的强制类型转换不在本文的介绍范围内,这里只总结隐式类型转换的一些规则。

类型转换是多数JavaScript 开发人员最头疼的问题之一,它常被诟病为语言设计上的一个
缺陷,比如在实际开发中不建议使用 == ,而是使用===,拒绝隐式强制类型转换。

宽松相等(loose equals)== 和严格相等(strict equals)=== 都用来判断两个值是否“相
等”,但是它们之间有一个很重要的区别,特别是在判断条件上。

常见的误区是“== 检查值是否相等,=== 检查值和类型是否相等”。听起来蛮有道理,然而还不够准确。很多 JavaScript 的书籍和博客也是这样来解释的,但是很遗憾他们都错了。

正确的解释是:“== 允许在相等比较中进行类型转换,而 === 不允许。”

我们来看一看两种解释的区别。

根据第一种解释(不准确的版本),=== 似乎比 == 做的事情更多,因为它还要检查值的
类型。第二种解释中 == 的工作量更大一些,因为如果值的类型不同还需要进行类型转换。

有人觉得 == 会比 === 慢,实际上虽然类型转换确实要多花点时间,但仅仅是微秒级
(百万分之一秒)的差别而已。

如果进行比较的两个值类型相同,则 == 和 === 使用相同的算法,所以除了 JavaScript 引擎
实现上的细微差别之外,它们之间并没有什么不同。
如果两个值的类型不同,我们就需要考虑有没有强制类型转换的必要,有就用 ==,没有就用 ===,不用在乎性能。

(以上摘自《你不知道的JavaScript》)

先上类型转换的对照表格

转换为字符串 转换为数字 转换为布尔值
undefined 'undefined' NaN false
null 'null' 0 false
''(空字符串) 0 false
'3'(非空,数字字符串) 3 true
'one'(非数字的字符串) NaN true
[ ] '' 0 true
[8] '8' 8 true
[5,'6'] '5,6' NaN true
function(){} 'function(){}' NaN true
{a:1} [object,object] NaN true

注意:

那些以数字表示的字符串可以直接转换为数字,也允许在开始和结尾处带有空格。但在开始和结尾处的任意非空格字符都不会被当成是数字的一部分,都为NaN。

例子:

parseFloat('3') // 3
parseFloat('X3') // NaN
parseFloat(' 3') // 3

对象转换为原始值的规则:

对于对象,转换为其原始值,会调用valueOf(), toString()方法。

先说一下valueOf方法,如果存在任意原始值,就将对象转换为原始值,如果不存在,返回原对象。

举例子:

new String('234').valueOf()
//"234",new String('234')是一个字符串对象,调用valueOf方法后,转换为'234','234'是基本数据类型,也就是说,new String('234')的原始值就是'234'

new Date().valueOf()
//1576022432100

toString方法就很容易理解了,就是将调用对象转换为字符串。

说完了这两个方法,再说一下这两个方法先调用那个,这就又引出了一个新的方法。

Symbol.toPrimitive 是一个内置的 Symbol 值,它是作为对象的函数值属性存在的。

规范指出,类型转换的内部实现是通过 ToPrimitive ( input ,[ PreferredType ] )方法进行转换的,这个方法的作用就是将input转换成一个非对象类型。

参数 preferredType 是可选的,它的作用是,指出了input被期待转成的类型。
input为日期时,preferredType 为 string,input 为其他值时preferredType默认为number。

如果不传preferredType进来,默认的是'number'。

如果preferredType的值是"string",那就先执行toString方法,执行后如果是原始值,那么返回这个原始值,如果不返回原始值, 再执行valueOf方法,执行后如果是原始值,那么返回这个原始值,如果不返回原始值,就抛出异常。否则,先执行"valueOf", 后执行"toString"。

由此可见,"toString","valueOf"的执行顺序,取决于preferred的值,且[Symbol.toPrimitive]的调用优先级最高

代码说明:

let a = {
  valueOf() {
    return 0
  },
  toString() {
    return '1'
  },
  [Symbol.toPrimitive]() {
    return 2
  }
}
1 + a // => 3

通过上面的栗子,可以做一道 经(e)典(xin)题

如何让 (a == 1 && a == 2 && a == 3) 的值为 true???

1.方法一

// 部署 [Symbol.toPrimitive] / valueOf/ toString 皆可
// 一次返回 1,2,3 即可。
let a = {
  [Symbol.toPrimitive]: (function(hint) {
    let i = 1;
    // 闭包的特性之一:i 不会被回收
    return function() {
        return i++;
    }
  })()
}

2.方法二

let a = new Proxy({}, {
    i: 1,
    get: function () {
        return () => this.i++;
    }
});

3.数组的 toString 方法默认调用数组的 join 方法,重写 join 方法(也可以重写toString方法)

let a = [1, 2, 3];
a.join = a.shift;

是不是很恶心??

== 类型转换

x == y

转换规则:

  • 如果 Type(x) 是数字,Type(y) 是字符串,则返回 x == ToNumber(y) 的结果。

  • 如果 Type(x) 是字符串,Type(y) 是数字,则返回 ToNumber(x) == y 的结果。

  • 如果 Type(x) 是布尔类型,则返回 ToNumber(x) == y 的结果;

  • 如果 Type(y) 是布尔类型,则返回 x == ToNumber(y) 的结果。

  • 如果 x 为 null,y 为 undefined,则结果为 true。

  • 如果 x 为 undefined,y 为 null,则结果为 true。

  • 如果 Type(x) 是字符串或数字,Type(y) 是对象,则返回 x == ToPrimitive(y) 的结果;

  • 如果 Type(x) 是对象,Type(y) 是字符串或数字,则返回 ToPromitive(x) == y 的结果。

注意:

null == undefined // true
null == false // false
null == 0 // false

没有任何类型转换,对象,string,number类型的变量是通过对应的构造函数(Object,String,Number)原型上的方法去做的转换,而undefined,null没有封装对象。

再来看一个栗子

true == "42"; // false
"42" == false; // false

Type(true) 是布尔值,所以 ToNumber(true) 将 true 强制类型转换为 1,变成 1 == "42",二者的
类型仍然不同,"42" 根据规则被强制类型转换为 42,最后变成 1 == 42,结果为 false。

肯定会有人有疑问,'42'怎么会既不是真值,又不是假值呢??

"42" 是一个真值没错,但 "42" == true 中并没有发生布尔值的比较和强制类型转换。这里
不是 "42" 转换为布尔值(true),而是 true 转换为 1,"42" 转换为 42,再进行相等判断。

重点是我们要搞清楚 == 对不同的类型组合怎样处理。== 两边的布尔值会被强制类型转换
为数字。

所以上面的栗子就给了我们提示

var a="42";

// 不要这样用,涉及隐式类型转换,条件判断不成立:
if (a == true) {
 // ..
}

不要因此而抱怨类型转换。对一种机制的滥用并不能成
为诟病它的借口。我们应该正确合理地运用强制类型转换,避免这些极端的情况。

下面给一些练习题,如果所有的都能答对,说明转换规则记的不错

"0" == null; // false
"0" == undefined; // false
"0" == false; // true 
"0" == NaN; // false
"0" == 0; // true
"0" == ""; // false 相同类型,没有转换了
false == null; // false
false == undefined; // false
false == NaN; // false
false == 0; // true 
false == ""; // true 
false == []; // true 
false == {}; // false
"" == null; // false
"" == undefined; // false
"" == NaN; // false
"" == []; // true 
"" == {}; // false
0 == null; // false
0 == undefined; // false
0 == NaN; // false
0 == []; // true 
0 == {}; // false


2 == [2]; // true  2== '2'
"" == [null]; // true   ''==''
{} == {} // false  对象和对象比较,是相同类型的比较,不涉及类型转换,不管是 == 还是===,都是引用的判断,即是否是相同对象的引用

运算符 '+' 的类型转换

转换规则的总结

  • +运算中其中一方为字符串,那么就会把另一方也转换为字符串。也就是任何类型的变量和字符串相加,结果都是字符串。

  • NaN和任何类型的变量相加都为NaN

  • 其余情况都会把运算符的两边的基本类型变量转换为number类型进行+运算,对象会转换为原始值,注意,对象会转换为原始值。

1 + '1' // '11'
true + true // 2
4 + [1,2,3] // "41,2,3",这里是把[1,2,3]转换为原始类型的值,调用了toString方法转换为了"1,2,3"

'a' + + 'b' // -> "aNaN"   相当于'a'+(+'b'),(+'b')转换为数字类型为NaN

再举个栗子分析转换过程

var a = [1,2];
var b = [3,4];
a + b; // "1,23,4"

因为数组的
valueOf( ) 操作无法得到简单基本类型值,于是它转而调用 toString( )。因此上例中的两
个数组变成了 "1,2" 和 "3,4"。+ 将它们拼接后返回 "1,23,4"。

a+"" 和 String(a) 有什么区别?? (a={ })

根据
ToPrimitive 抽象操作规则
a + "" 会对 a 调用 valueOf( ) 方法,然后通过 toString( ) 抽象
操作将返回值转换为字符串。而 String(a) 则是直接调用 toString( )。它们最后返回的都是字符串,但如果 a 是对象而非数字结果可能会不一样!

var a = {
 valueOf: function() { return 42; },
 toString: function() { return 4; }
};
a + ""; // "42"
String( a ); // "4"

下面通过一个比较奇葩的例子,重新分析

[] + {} // [object Object]

+操作符两边都是引用数据类型,要将 [ ] 和 { } 都转换为原始值,再去套用原始值相加的转换规则。

先看[ ],toPrimitive的preferredType值没变,仍为原始值number,先调用valueOf,没有返回原始值,返回了[ ]本身,再调用toString方法,返回空字符串'',此为原始值。

再看{ },一样先调用valueOf,返回{ }本身,再调用toString方法,返回'[Object Object]',所以[]+{}最后变为了 ''+'[Object Object]' ,得出结果。

{}+[] // 0

这个例子很特殊,表达式第一个就是{ },这时候编译器只会把这个{ } 当作一个空代码块,{ } + [ ] 就可以当作是+ [ ], 而 + [ ]是强制将[ ]转换为number ,转换的过程是 + [ ] --> +"" -->0 最终的结果就是0。

具体请看这里

通过上面这么多的分析,发现了一个问题,preferredType一直都是默认值,总是先调用valueOf,再调用toString。

那么,问题来了。

toPrimitive的preferredType值怎么就会不是默认值了呢?怎么修改它呢???

preferredType的值只有3种情况,default(也就是number),number,string

const object1 = {
  [Symbol.toPrimitive](hint) {
    if (hint == 'number') {
      return 42;
    }
    return null;
  }
};

console.log(+object1); // preferredType为number  42
console.log(object1+1); // preferredType为默认值  null+1  ==》 0+1
console.log(String(object1)); // preferredType为string null
console.log(Number(object1)); // preferredType为number  42

只有在将对象 强制类型转换 为基本数据类型的值时,preferredType才会改变。

字符串<>比较的是什么

字符串在进行大于(小于)比较时,会根据第一个不同的字符的ascii值码进行比较,当数字(number)与字符串(string)进行比较大小时,会强制的将数字(number)转换成字符串(string)然后再进行比较。

console.log("4">8) // false  "4"的ASCII码为52   "8"的ASCII码为56   str.charCodeAt()查看ASCII码
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

1 participant