Abo

不为人知的数据类型

温故知新,判断数据类型的四种方法


startup-3267505_1280

共勉

数据类型

JavaScript原始类型:Undefined、Null、Boolean、Number、String、Symbol

JavaScript引用类型:Object

关于原始类型与引用类型

在 ECMAScript 规范中,共定义了 7 种数据类型,分为 基本类型 和 引用类型 两大类,如下所示:

基本类型:String、Number、Boolean、Symbol、Undefined、Null

引用类型:Object

基本类型也称为简单类型,由于其占据空间固定,是简单的数据段,为了便于提升变量查询速度,将其存储在栈中,即按值访问。

引用类型也称为复杂类型,由于其值的大小会改变,所以不能将其存放在栈中,否则会降低变量查询速度,因此,其值存储在堆(heap)中,而存储在变量处的值,是一个指针,指向存储对象的内存处,即按址访问。引用类型除 Object 外,还包括 Function 、Array、RegExp、Date 等等。

原始类型

原始类型又被称为基本类型,原始类型保存的变量和值直接保存在栈内存(Stack)中,且空间相互独立,通过值来访问,说到这里肯定一同懵逼,不过我们可以通过一个例子来解释.

1
2
var person = 'Messi';
var person1 = person;

上述代码在栈内存的示意图是这样的,可以看到,虽然person赋值给了person1.但是两个变量并没有指向同一个值,而是person1自己单独建立一个内存空间,虽然两个变量的值相等,但却是相互独立的.

值得一提的是,虽然原始类型的值是储存在相对独立空间,但是它们之间的比较是按值比较的.

1
2
3
var person = 'Messi';
var person1 = 'Messi';
console.log(person === person1); //true

引用类型

剩下的就是引用类型了,即Object 类型,再往下细分,还可以分为:Object 类型、Array 类型、Date 类型、Function 类型 等。

与原始类型不同的是,引用类型的内容是保存在堆内存中,而栈内存(Heap)中会有一个堆内存地址,通过这个地址变量被指向堆内存中Object真正的值,因此引用类型是按照引用访问的.

1
2
3
4
5
6
7
8
9
10
11
12
var a = {name:"percy"};
var b;
b = a;
a.name = "zyj";
console.log(b.name); // zyj
b.age = 22;
console.log(a.age); // 22
var c = {
name: "zyj",
age: 22
};
console.log(a === c); //false

我们可以逐行分析: 1. b = a,如果是原始类型的话,b会在栈内自己独自创建一个内存空间保存值,但是引用类型只是b的产生一个对内存地址,指向堆内存中的Object. 2.a.name = "zyj",这个操作属于改变了变量的值,在原始类型中会重新建立新的内存空间,而引用类型只需要自己在堆内存中更新自己的属性即可. 3.最后创建了一个新的对象c,看似跟b a一样,但是在堆内存中确实两个相互独立的Object,引用类型是按照引用比较,由于a c引用的是不同的Object所以得到的结果是false.

判断Js数据类型的四种方法

typeof

typeof 是一个操作符,其右侧跟一个一元表达式,并返回这个表达式的数据类型。返回的结果用该类型的字符串(全小写字母)形式表示,包括以下 7 种:number、boolean、symbol、string、object、undefined、function 等。

1
2
3
4
5
6
7
8
9
10
typeof ''; // string 有效
typeof 1; // number 有效
typeof Symbol(); // symbol 有效
typeof true; //boolean 有效
typeof undefined; //undefined 有效
typeof null; //object 无效
typeof [] ; //object 无效
typeof new Function(); // function 有效
typeof new Date(); //object 无效
typeof new RegExp(); //object 无效

有些时候,typeof 操作符会返回一些令人迷惑但技术上却正确的值:

  • 对于基本类型,除 null 以外,均可以返回正确的结果。
  • 对于引用类型,除 function 以外,一律返回 object 类型。
  • 对于 null ,返回 object 类型。
  • 对于 function 返回 function 类型。

其中,null 有属于自己的数据类型 Null , 引用类型中的 数组、日期、正则 也都有属于自己的具体类型,而 typeof 对于这些类型的处理,只返回了处于其原型链最顶端的 Object 类型,没有错,但不是我们想要的结果。

instanceof

instanceof 是用来判断 A 是否为 B 的实例,表达式为:A instanceof B,如果 A 是 B 的实例,则返回 true,否则返回 false。 在这里需要特别注意的是:instanceof 检测的是原型,我们用一段伪代码来模拟其内部执行过程:

1
2
3
4
5
6
7
8
9
instanceof (A,B) = {
var L = A.__proto__;
var R = B.prototype;
if(L === R) {
// A的内部属性 __proto__ 指向 B 的原型对象
return true;
}
return false;
}
关于__proto__和prototype的区别

__proto__属性(前后各两个下划线),用来读取或设置当前对象的prototype对象,他本质是一个内部属性,不是一个正式对外的api,由于浏览器的广泛支持,才加入了es6,__proto__调用的是Object.prototype.proto

当我们new Person()的时候到底发生了什么?
new一个构造函数,相当于实例化一个对象,这期间其实进行了这三个步骤:

创建对象,设为o,即: var o = {};
上文提到了,每个对象都有__proto__属性,该属性指向一个对象,这里,将o对象的__Proto__指向构造函数Person的原型对象(Person.prototype;
将o作为this去调用构造函数Person,从而设置o的属性和方法并初始化。
当这3步完成,这个o对象就与构造函数Person再无联系,这个时候即使构造函数Person再加任何成员,都不再影响已经实例化的o对象了。
此时,o对象具有了name和age属性,同时具有了构造函数Person的原型对象的所有成员,当然,此时该原型对象是没有成员的。

现在大家都明白了吧,简单的总结下就是:
js在创建对象的时候,都有一个叫做__proto__的内置属性,用于指向创建它的函数对象的原型对象prototype
那么一个对象的__proto__属性究竟怎么决定呢?答案显而易见了:是由构造该对象的方法决定的。

下面讲解三种常见的创建对象方法。

对象字面量

比如:

1
2
3
4
var Person = {
name: 'jessica',
age: 27
}

这种形式就是对象字面量,通过对象字面量构造出的对象,其__proto__指向Object.prototype。
所以,其实Object是一个函数也不难理解了。Object、Function都是是js自带的函数对象。
可以跑下面的代码看看:

1
2
console.log(typeof Object);  function
console.log(typeof Function); function
构造函数

就如我前面讲的,形如:

1
2
function Person(){}
var person1 = new Person();

这种形式创建对象的方式就是通过构造函数创建对象,这里的构造函数是Person函数。上面也讲过了,通过构造函数创建的对象,其__proto__指向的是构造函数的prototype属性指向的对象。

Object.create
1
2
3
4
5
var person1 = {
name: 'jessica',
age: 27
}
var person2 = Object.create(person1);

这种情况下,person2的__proto__指向person1。在没有Object.create函数的时候,人们大多是这样做的:

1
2
3
4
5
Object.create = function(p) {
function f(){};
f.prototype = p;
return new f();
}

首先来说说prototype属性,不像每个对象都有__proto__属性来标识自己所继承的原型,只有函数才有prototype属性。当你创建函数时,JS会为这个函数自动添加prototype属性,值是空对象 值是一个有 constructor 属性的对象,不是空对象。而一旦你把这个函数当作构造函数(constructor)调用(即通过new关键字调用),那么JS就会帮你创建该构造函数的实例,实例继承构造函数prototype的所有属性和方法(实例通过设置自己的__proto__指向承构造函数的prototype来实现这种继承)。

__proto__是es6加入的内部属性,不是正式对外的api

1598522001565

JS正是通过__proto__prototype的合作实现了原型链,以及对象的继承。

构造函数,通过prototype来存储要共享的属性和方法,也可以设置prototype指向现存的对象来继承该对象。

对象的__proto__指向自己构造函数的prototypeobj.__proto__.__proto__...的原型链由此产生,包括我们的操作符instanceof正是通过探测obj.__proto__.__proto__... === Constructor.prototype来验证obj是否是Constructor的实例。

two = new Object()Object是构造函数,所以two.__proto__就是Object.prototype。至于one,ES规范定义对象字面量的原型就是Object.prototype

从上述过程可以看出,当 A 的 __proto__ 指向 B 的 prototype 时,就认为 A 就是 B 的实例,我们再来看几个例子:

1
2
3
4
5
6
7
8
9
10
[] instanceof Array; // true
{} instanceof Object;// true
new Date() instanceof Date;// true

function Person(){};
new Person() instanceof Person;

[] instanceof Object; // true
new Date() instanceof Object;// true
new Person instanceof Object;// true

我们发现,虽然 instanceof 能够判断出 [ ] 是Array的实例,但它认为 [ ] 也是Object的实例,为什么呢?

我们来分析一下 [ ]、Array、Object 三者之间的关系:

从 instanceof 能够判断出 [ ].__proto__ 指向 Array.prototype,而 Array.prototype.__proto__ 又指向了Object.prototype,最终 Object.prototype.__proto__ 指向了null,标志着原型链的结束。因此,[]、Array、Object 就在内部形成了一条原型链

从原型链可以看出,[] 的 __proto__ 直接指向Array.prototype,间接指向 Object.prototype,所以按照 instanceof 的判断规则,[] 就是Object的实例。依次类推,类似的 new Date()、new Person() 也会形成一条对应的原型链 。因此,instanceof 只能用来判断两个对象是否属于实例关系, 而不能判断一个对象实例具体属于哪种类型。

constructor

当一个函数 F被定义时,JS引擎会为F添加 prototype 原型,然后再在 prototype上添加一个 constructor 属性,并让其指向 F 的引用。

1
2
3
4
5
6
7
8
''.constructor == String
new Number(1).constructor == Number
true.constructor == Boolean
New Function().constructor == Date
new Error().constructor == Error
[].constructor == Array
document.constructor == HTMLDocument
window.constructor == Window

以上全员均为true

  1. null 和 undefined 是无效的对象,因此是不会有 constructor 存在的,这两种类型的数据需要通过其他方式来判断。

  2. 函数的 constructor 是不稳定的,这个主要体现在自定义对象上,当开发者重写 prototype 后,原有的 constructor 引用会丢失,constructor 会默认为 Object

1
2
3
4
5
6
function F() {}
F.prototype = {a:'xxxx'}
var f = new F()
f.constructor == F //false
> f.constructor
< function Object() {}

因为 prototype 被重新赋值的是一个 { }, { } 是 new Object() 的字面量,因此 new Object() 会将 Object 原型上的 constructor 传递给 { },也就是 Object 本身。

因此,为了规范开发,在重写对象原型时一般都需要重新给 constructor 赋值,以保证对象实例的类型不被篡改。

toString

toString() 是 Object 的原型方法,调用该方法,默认返回当前对象的 [[Class]] 。这是一个内部属性,其格式为 [object Xxx] ,其中 Xxx 就是对象的类型。

对于 Object 对象,直接调用 toString() 就能返回 [object Object] 。而对于其他对象,则需要通过 call / apply 来调用才能返回正确的类型信息。

1
2
3
4
5
6
7
8
9
10
11
12
13
Object.prototype.toString.call('') ;   // [object String]
Object.prototype.toString.call(1) ; // [object Number]
Object.prototype.toString.call(true) ; // [object Boolean]
Object.prototype.toString.call(Symbol()); //[object Symbol]
Object.prototype.toString.call(undefined) ; // [object Undefined]
Object.prototype.toString.call(null) ; // [object Null]
Object.prototype.toString.call(new Function()) ; // [object Function]
Object.prototype.toString.call(new Date()) ; // [object Date]
Object.prototype.toString.call([]) ; // [object Array]
Object.prototype.toString.call(new RegExp()) ; // [object RegExp]
Object.prototype.toString.call(new Error()) ; // [object Error]
Object.prototype.toString.call(document) ; // [object HTMLDocument]
Object.prototype.toString.call(window) ; //[object global] window 是全局对象 global 的引用

数组中的坑

稀疏数组:指的是含有空白或空缺单元的数组

1
2
3
4
5
6
7
8
9
10
11
12
13
var a = [];

console.log(a.length); //0

a[4] = a[5];

console.log(a.length); //5

a.forEach(elem => {
console.log(elem); //undefined
});

console.log(a); //[,,,,undefined]

这里有几个坑需要注意:

  1. 一开始建立的空数组a的长度为0,这可以理解,但是在a[4] = a[5]之后出现了问题,a的长度居然变成了5,此时a数组是[,,,,undefined]这种形态.
  2. 我们通过遍历,只得到了undefined这一个值,这个undefind是由于a[4] = a[5]赋值,由于a[5]没有定义值为undefined被赋给了a[4],可以等价为a[4] = undefined

字符串索引

1
2
3
4
5
6
var a = [];
a[0] = 'Bale';
a['age'] = 28;
console.log(a.length); //1
console.log(a['age']); //28
console.log(a); //[ 'Bale', age: 28 ]

数组不仅可以通过数字索引,也可以通过字符串索引,但值得注意的是,字符串索引的键值对并不算在数组的长度里.

数字中的坑 :二进制浮点数

JavaScript 中的数字类型是基于“二进制浮点数”实现的,使用的是“双精度”格式,这就带来了一些反常的问题,我们那一道经典面试提来讲解下.

1
2
3
var a = 0.1 + 0.2;
var b = 0.3;
console.log(a === b); //false

这是个出人意料的结果,实际上a的值约为0.30000000000000004这并不是一个整数值,这就是二进制浮点数带来的副作用.

1
2
3
4
5
6
var a = 0.1 + 0.2;
var b = 0.3;
console.log(a === b); //false
console.log(Number.isInteger(a*10)); //false
console.log(Number.isInteger(b*10)); //true
console.log(a); //0.30000000000000004

NaN

1
2
3
4
var a = 1/new Object();
console.log(typeof a); //Number
console.log(a); //NaN
console.log(isNaN(a)); //true

NaN属于特殊的Number类型,我们可以把它理解为坏数值,因为它属于数值计算中的错误,更加特殊的是它自己都不等价于自己NaN === NaN //false,我们只能用isNaN()来检测一个数字是否为NaN.

类型转换原理

类型转换指的是将一种类型转换为另一种类型,例如:

1
2
3
var b = 2;
var a = String(b);
console.log(typeof a); //string

奇葩题:

1
{}+[] //0

是什么原因造成了上述结果呢?那么我们得从ECMA-262中提到的转换规则和抽象操作说起,有兴趣的童鞋可以仔细阅读下这浩如烟海的语言规范,如果没这个耐心还是往下看.

这是JavaScript种类型转换可以从原始类型转为引用类型,同样可以将引用类型转为原始类型,转为原始类型的抽象操作为ToPrimitive,而后续更加细分的操作为:ToNumber ToString ToBoolean

如果想应付面试,我觉得这张表就差不多了,但是为了更深入的探究JavaScript引擎是如何处理代码中类型转换问题的,就需要看 ECMA-262详细的规范,从而探究其内部原理,我们从这段内部原理示意代码开始

类型转换主要分为两大类:ToPrimitiveToObject。其中ToPrimitive又分为了:ToNumberToStringToBoolean

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
// ECMA-262, section 9.1, page 30. Use null/undefined for no hint,
// (1) for number hint, and (2) for string hint.
function ToPrimitive(x, hint) {
// Fast case check.
if (IS_STRING(x)) return x;
// Normal behavior.
if (!IS_SPEC_OBJECT(x)) return x;
if (IS_SYMBOL_WRAPPER(x)) throw MakeTypeError(kSymbolToPrimitive);
if (hint == NO_HINT) hint = (IS_DATE(x)) ? STRING_HINT : NUMBER_HINT;
return (hint == NUMBER_HINT) ? DefaultNumber(x) : DefaultString(x);
}

// ECMA-262, section 8.6.2.6, page 28.
function DefaultNumber(x) {
if (!IS_SYMBOL_WRAPPER(x)) {
var valueOf = x.valueOf; //注意这里是valueOf,而不是valueOf(),所以这里是传递函数内容,并不是执行函数
//// 如果传入参数是一个函数的话
if (IS_SPEC_FUNCTION(valueOf)) {
var v = %_CallFunction(x, valueOf);
if (IsPrimitive(v)) return v; //不是ToPrimitive方法噢
}

var toString = x.toString;
if (IS_SPEC_FUNCTION(toString)) {
var s = %_CallFunction(x, toString);
if (IsPrimitive(s)) return s;
}
}
throw MakeTypeError(kCannotConvertToPrimitive);
}

// ECMA-262, section 8.6.2.6, page 28.
function DefaultString(x) {
if (!IS_SYMBOL_WRAPPER(x)) {
var toString = x.toString;
if (IS_SPEC_FUNCTION(toString)) {
var s = %_CallFunction(x, toString);
if (IsPrimitive(s)) return s;
}

var valueOf = x.valueOf;
if (IS_SPEC_FUNCTION(valueOf)) {
var v = %_CallFunction(x, valueOf);
if (IsPrimitive(v)) return v;
}
}
throw MakeTypeError(kCannotConvertToPrimitive);
}

上面有一个hint的参数,当没有传入hint参数时,且x不是Date对象时会通过%DefaultNumber(x)来转换,否则通过%DefaultString(x)。这里也可以看到日期类型的对象转换为原始类型时的不同。

上面代码的逻辑是这样的:

  1. 如果变量为字符串,直接返回.
  2. 如果!IS_SPEC_OBJECT(x),直接返回.
  3. 如果IS_SYMBOL_WRAPPER(x),则抛出异常.
  4. 否则会根据传入的hint来调用DefaultNumberDefaultString,比如如果为Date对象,会调用DefaultString.
  5. DefaultNumber:首先x.valueOf,如果为primitive,则返回valueOf后的值,否则继续调用x.toString,如果为primitive,则返回toString后的值,否则抛出异常
  6. DefaultString:和DefaultNumber正好相反,先调用toString,如果不是primitive再调用valueOf.

那讲了实现原理,这个ToPrimitive有什么用呢?实际很多操作会调用ToPrimitive,比如加、相等或比较操。在进行加操作时会将左右操作数转换为primitive,然后进行相加。

下面来个实例,({}) + 1(将{}放在括号中是为了内核将其认为一个代码块)会输出啥?可能日常写代码并不会这样写,不过网上出过类似的面试题。

加操作只有左右运算符同时为String或Number时会执行对应的%_StringAdd或%NumberAdd

下面看下({}) + 1内部会经过哪些步骤:

{}1首先会调用ToPrimitive {}会走到DefaultNumber,首先会调用valueOf,返回的是Object {},不是primitive类型,从而继续走到toString,返回[object Object],是String类型 最后加操作,结果为[object Object]1 再比如有人问你[] + 1输出啥时,你可能知道应该怎么去计算了,先对[]调用ToPrimitive,返回空字符串,最后结果为”1”。

({}).valueOf() // {}

({}).toString() // “[object Object]”

[].toString() // “”

附上ADD源码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// ECMA-262, section 11.6.1, page 50.
function ADD(x) {
// Fast case: Check for number operands and do the addition.
// 如果都为number或string,则直接调用NumberAdd或_StringAdd
if (IS_NUMBER(this) && IS_NUMBER(x)) return %NumberAdd(this, x);
if (IS_STRING(this) && IS_STRING(x)) return %_StringAdd(this, x);

// Default implementation.
// 否则将两边操作数分别转换为原始类型
var a = %ToPrimitive(this, NO_HINT);
var b = %ToPrimitive(x, NO_HINT);

if (IS_STRING(a)) {
return %_StringAdd(a, %ToString(b));
} else if (IS_STRING(b)) {
return %_StringAdd(%NonStringToString(a), b);
} else {
return %NumberAdd(%ToNumber(a), %ToNumber(b));
}
}

可以看到在操作数有一个不为number或string时,ADD操作就会将相应的操作数转换为原始类型,然后再进行相应的加法操作,可以看到上面的{} + 1{}不为原始类型,所以就会调用ToPrimitive({})ToPrimitive(1)ToPrimitive({})调用的结果为[object Object],所以最后会进行_StringAdd操作,最后的结果是:[object Object]1

ToNumber:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// ECMA-262, section 9.3, page 31.
function ToNumber(x) {
// 如果为number直接返回
if (IS_NUMBER(x)) return x;
// 如果为字符串,则调用StringToNumber转换
if (IS_STRING(x)) {
return %_HasCachedArrayIndex(x) ? %_GetCachedArrayIndex(x)
: %StringToNumber(x);
}
if (IS_BOOLEAN(x)) return x ? 1 : 0;
// 如果为undefined,则返回NAN
if (IS_UNDEFINED(x)) return NAN;
// 如果为symbol,则抛出异常
if (IS_SYMBOL(x)) throw MakeTypeError('symbol_to_number', []);
// 如果为null或
return (IS_NULL(x)) ? 0 : ToNumber(%DefaultNumber(x));

ToString:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// ECMA-262, section 9.8, page 35.
function ToString(x) {
// 如果为string,则直接返回
if (IS_STRING(x)) return x;
// 如果为number,则调用_NumberToString
if (IS_NUMBER(x)) return %_NumberToString(x);
// 如果为boolean
if (IS_BOOLEAN(x)) return x ? 'true' : 'false';
// 如果为undefined,则返回undefined字符串
if (IS_UNDEFINED(x)) return 'undefined';
// 如果为symbol,则抛出异常
if (IS_SYMBOL(x)) throw %MakeTypeError('symbol_to_string', []);
// 如果为null,或者对象
return (IS_NULL(x)) ? 'null' : %ToString(%DefaultString(x));
}

toString()与valueOf()

8.31日补充:Number()以及String()方法不同于ValueOf、ToString,

前者为数据转换,当调用Add方法(+a)的时候,此时涉及到的转换数据格式,而不是ToPrimitive,所以在+a的时候,当a为数字字符串,则会转成数字,当 a为空对象的时候,则为 NaN, 当 a 为空数组的时候,则为 0

​ toString( ):返回对象的字符串表示。

​ valueOf( ):返回对象的字符串、数值或布尔值表示。

好了,写几个例子就明白返回结果了(undefined 和 null 的值就不举例了,因为它们都没有这两个方法,所以肯定会报错的):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
//先看看toString()方法的结果
var a = 3;
var b = '3';
var c = true;
var d = {test:'123',example:123}
var e = function(){console.log('example');}
var f = ['test','example'];

a.toString();// "3"
b.toString();// "3"
c.toString();// "true"
d.toString();// "[object Object]"
e.toString();// "function (){console.log('example');}"
f.toString();// "test,example"
1
2
3
4
5
6
7
8
9
10
11
12
13
14
//再看看valueOf()方法的结果
var a = 3;
var b = '3';
var c = true;
var d = {test:'123',example:123}
var e = function(){console.log('example');}
var f = ['test','example'];

a.valueOf();// 3
b.valueOf();// "3"
c.valueOf();// true
d.valueOf();// {test:'123',example:123}
e.valueOf();// function(){console.log('example');}
f.valueOf();// ['test','example']

​ 很清楚了,toString( )就是将其他东西用字符串表示,比较特殊的地方就是,表示对象的时候,变成”[object Object]”,表示数组的时候,就变成数组内容以逗号连接的字符串,相当于Array.join(‘,’)。 而valueOf( )就返回它自身了。

​ 至于迷惑的地方,就在于它们在什么时候被调用,举个例子:

1
2
3
4
5
6
7
8
9
10
11
12
var a = '3';
console.log(+a);// 3 //如果是+某值的形式则理解成ToNumber
console.log(+b); // b is not defined
console.log(+[]) //0
console.log(+{}) //NaN
Number([]) // 0
Number({}) //NaN

console.log(2+a) //23
console.log(a+2) //32
console.log(-a) //-3
console.log(*a) // Uncaught SyntaxError: Unexpected token '*'

当然了,+a打印结果是数字3(不是字符串‘3’),因为一元加操作符接在字符串前面就将其转换为数字了(字符串转化为数字的一种方式,相当于Number( )方法),但是如果它应用在对象上,过程是怎样的呢,再举例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
//例子一
var example = {test:'123'};
console.log(+example);// NaN //理解成toNumber
console.log(1+example)// "1[object Object]"
console.log(''+exmple)// [object Object]
//例子二 同时改写 toString 和 valueOf 方法
var example = {
toString:function(){
return '23';
},
valueOf:function(){
return '32';
}
};
console.log(+example);// 32

//例子三 只改写 toString 方法
var example = {
toString:function(){
return '23';
}
};
console.log(+example);// 23

​ 通过例子一和例子二的比较,我们可以知道,一元加操作符在操作对象的时候,会先调用对象的valueOf方法来转换,最后再用Number( )方法转换,而通过例子二和例子三的比较,我们可以知道,如果只改写了toString方法,对象则会调用toString方法,证明valueOf的优先级比toString高。

​ 好了,如果是alert呢?

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
//例子一
var example = {test:'123'};
alert(example);// "[object Object]"

//例子二 同时改写 toString 和 valueOf 方法
var example = {
toString:function(){
return '23';
},
valueOf:function(){
return '32';
}
};
alert(example);// "23"

//例子三 只改写 valueOf 方法
var example = {
valueOf:function(){
return '32';
}
};
alert(example);// "[object Object]"

虽然上面结果我用双引号了,但是你知道弹窗不会将字符串的双引号表示出来的。通过上面几个例子,我们就知道了,alert它对待对象,就和字符串和对象相加一样,就是调用它的toString( )方法,和 valueOf方法无关

好了,总结一下,一般用操作符单独对对象进行转换的时候,如果对象存在valueOf或toString改写的话,就先调用改写的方法,valueOf更高级,如果没有被改写,则直接调用对象原型的valueOf方法。如果是弹窗的话,直接调用toString方法。至于其他情况,待续……

我们尝试把第一步返回的值改成一个对象 {}

1
2
3
4
5
6
7
8
9
10
11
12
13
14
var a={
toString:function(){
console.log('调用了 a.toString');
return {};
},
valueOf:function(){
console.log('调用了 a.valueOf');
return '111';
}
}
alert(a);

// 调用了 a.toString
// 调用了 a.valueOf

从结果可以看到,当toString不可用的时候,系统会再尝试valueOf方法,我们继续修改valueOf方法,把valueOf方法也改成返回对象 {}

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
var a={
toString:function(){
console.log('调用了 a.toString');
return {};
},
valueOf:function(){
console.log('调用了 a.valueOf');
return {};
}
}
alert(a);

// 调用了 a.toString
// 调用了 a.valueOf
// Uncaught TypeError: Cannot convert object to primitive value

可以发现,如果toString和valueOf方法均不可用的情况下,系统会直接返回一个错误。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
var a={
toString:function(){
console.log('调用了 a.toString');
return 12;
},
valueOf:function(){
console.log('调用了 a.valueOf');
return {};
}
}
a+1
//调用了 a.valueOf
//调用了 a.toString
//13

可以看到,这里我们改写了valueOf和toString方法,系统在调用valueOf方法之后发现返回的不是“原始类型”数据,于是又尝试调用了toString方法,并返回了该方法返回的值12,最后+1变成了13。

1
2
3
{} 的 valueOf 结果为 {} ,toString 的结果为 "[object Object]"

[]valueOf 结果为 []toString 的结果为 ""
1
2
3
4
5
Object.prototype.toString.call(null); // "[object Null]"
Object.prototype.toString.call(undefined); // "[object Undefined]"
Object.prototype.toString.call(“abc”);// "[object String]"
Object.prototype.toString.call(123);// "[object Number]"
Object.prototype.toString.call(true);// "[object Boolean]"
1
2
3
var obj={name:'Mofei'}
var str = ' ' + obj
console.log(str); // [object Object]

大吃一惊

1598833484342

“0” 转换成 Boolean为 true

NaN转换成 Boolean为 false

{} 转换成 Boolean 为 true

[] 转换成 Boolean 为 true, 数组本身就是Object

,

1
2
3
4
parseInt(0.0000004)  // 4
![]==[] //true
['x','y'] == 'x,y' //true
alert({name:'mofei'}) //"[object Object]"

关于Boolean

1
2
3
4
5
6
7
8
9
10
11
12
13
// ECMA-262, section 9.2, page 30
function ToBoolean(x) {
// 如果为boolean,则直接返回
if (IS_BOOLEAN(x)) return x;
// 如果为string,则当字符串长度不为0时返回true
if (IS_STRING(x)) return x.length != 0;
// 如果为null,返回false
if (x == null) return false;
// 如果为number,当number不为0,且不为NAN时返回true
if (IS_NUMBER(x)) return !((x == 0) || NUMBER_IS_NAN(x));
// 否则返回true
return true;
}

Boolean转换

在进行布尔比较的时候,比如 if(obj) , while(obj)等等,会进行布尔转换,布尔转换遵循如下规则:

布尔值
true/false true/false
undefined,null false
Number 0,NaN 对应 false, 其他的对应 true
String “”对应false,其他对应true(’0’对应的是true)
Object true

注意:我们知道,初始化后,即使数组arr中没有元素,也是一个object。既然是object,用于判断条件时就会被转化为true

1
2
if(arr)console.log("it's true");
// it's true

但是,如果将arr与布尔值比较:

1
2
3
4
arr == false;
// true
arr == true;
// false

可是,如果把arr转化为Boolean,的确是true:

1
2
Boolean(arr);
// true

那arr与布尔值比较时,到底发生了什么?

这是因为,任意值与布尔值比较,都会将两边的值转化为Number。

如arr与false比较,false转化为0,而arr为空数组,也转化为0:

1
2
3
4
Number(false)
// 0
Number(arr)
// 0

所以,当空数组作为判断条件时,相当于true。当空数组与布尔值直接比较时,相当于false。

举个比较典型的例子

1
2
3
4
5
6
7
8
9
[] == ![]  //true

// 首先第一步右边的是逻辑判断![],所以先转成boolean
// [] == !true
// [] == false
// 左边不是原始类型,尝试把左边转成原始类型,变成
// '' == false
// 转成Number
// 0 == 0

图片描述

发现对象确实不可valueOf

1
2
var num = 123;  //通过对象字面量声明console.log(typeof num);  //输出:'number'
var num = new Number(123); //通过构造方法声明console.log(typeof num); //输出:'object'

注意:比如parseInt(“0057”) 结果为57 ,而parseInt(”0058”)结果为5,parseInt(“007”)结果为7,而parseInt(“008”)结果为0

parseInt()函数的完整形式是:parseInt(string, radix)的作用是将string转换为整数,第二个参数是设置string的格式,常用的有2、8、10、16,表示string是多少进制的数。 不要把这里的Int理解成十进制数

radix 可取值的范围是2~36,如果不在这个范围内,将返回NaN
如果设置radix的值是0或者不设置时,会自动识别string的格式:

以 “0x” 开头,parseInt() 会把 string 除0x外的其余部分当作十六进制数,
以 “0” 开头,parseInt() 会把 string 除0外的字符当作八进制或十六进制数,
以 1 ~ 9 的数字开头,parseInt() 将把它当作十进制数。

所以产生上述原因是因为我们没有设置第二个类型参数,以致以”0”开头的字符串数据被当做了八进制。

所以以后在使用parseInt(String,radix)函数时,把第二个进制参数加上,以防出错。

parseInt(”0058”,10)结果为58,就是我们想要的了

这表示把”0058”以十进制表示

parseInt():不认识小数点,是从左到右一个一个解析,看到数字就通过,非数字就停止解析。

1
2
var a = '100px123456';
alert(parseInt(a));//100

parseInt认识加号、减号、空格等,遇到加号减号等会认为是数字的一部分,越过这些符号继续解析

1
2
var a = '+100px';
alert(parseInt(a));//100