本系列内容由ZouStrong整理收录
整理自《JavaScript权威指南(第六版)》,《JavaScript高级程序设计(第三版)》
引用类型是一种数据结构,用于组织数据和功能,描述的是一类对象所具有的属性和方法
引用类型常被称为类,但他们并不相同,因为JavaScript不具备传统的面向对象语言所支持的类和接口等概念
引用类型的值(需要实例化)就是对象,对象是特定引用类型的一个实例
Object类型是最基础的类型,其他所有类型都继承自Object(都从Object类型继承了基本的数据和方法)
大多数对象都是Object类型的实例,虽然Object的实例不具备多少功能,但多用于存储和传输数据
每个对象实例都具有继承自Object类型的以下属性或者方法
除了属性名和属性值之外,每个属性还有一些属性特性
每个对象还拥有三个相关的对象特性
最后,简单地说,可以将对象分为三类
属性可以分为两类
创建对象有很多种方式
创建对象最简单的方式就是使用对象直接量,它是由若干名/值对(属性名和属性值)组成的映射表
看几个例子
var empty={}; //创建一个空对象,但包含默认属性和方法
var people={name:"strong",age:25}; //拥有两个属性的对象
var people2 ={name:people.name,age:people.gae}; //更复杂的值
var book={
"main title" : "JavaScript", //属性名包含空格,必须用字符串表示
"subi-title" : "Java", //属性名包含连字符,必须用字符串表示
"for":"all people", //属性名是保留字,必须用字符串表示
auther:{ //属性值是对象
name:"david",
age:56
}
}
对象字面量简化了创建包含大量属性的对象的过程,使得代码量减少,并且有封装数据的感觉,而且对象字面量也是向函数传递大量可选参数的首选方式
function test(obj){
if(typeof obj.name=="string"){
}
if(typeof obj.age=="number"){
}
}
这种传递参数的模式最适合需要向函数传入大量可选参数的情形。一般来讲,命名参数虽然容易处理,但在有多个可选参数的情况下就会显示不够灵活(受顺序影响);最好的做法是对那些必需值使用命名参数,而使用对象字面量来封装多个可选参数
new运算符创建并初始化一个新对象
关键字new后跟一个构造函数(通常都是首字母大写),构造函数用以初始化一个新创建的对象
JavaScript中的内置对象都包含内置构造函数
var obj= new Object(); //创建Object引用类型的一个实例
var obj = Object; //new和括号()都可以同时省略,作用同上
除了内置构造函数,还可以使用自定义的构造函数来初始化一个对象
function Name(){
this.name = "strong";
}
var obj = new Name(); //{name:"strong"}
注:构造函数通常都是首字母大写,以与普通函数区别,除此之外,与普通函数没有什么区别
先了解一下原型(后续详解)
几乎每一个JavaScript对象都有原型,它们会从其原型继承属性
所有通过对象直接量创建的对象都具有同一个原型对象——Object.prototype
通过关键字new和构造函数创建的对象的原型就是——构造函数.prototype
prototype属性是函数的属性
没有原型的对象不多——null 和 Object.prototype就是其中的两个,他们不继承任何属性(使用Object.create()可以自定义创建没有原型的对象)
所有的内置构造函数以及大部分自定义构造函数都有一个继承自Object.prototype的原型,如Date.prototype的属性继承自Object.prototype,所以Date对象的属性同时继承自Date.prototype 和 Object.prototype,这一系列的原型对象就是所谓的——原型链
ECMAScript5 新增了创建对象的新方法Object.create(),可接受两个参数
继承指定对象
var o = Object.create({name:"hello"});
//创建一个对象,继承了指定对象的name属性
可以传入null来创建一个没有原型的对象,他们不继承任何属性,甚至没有基础方法,比如toString(),这意味着它不能和"+"运算符一起正常工作
var o = Object.create(null); //创建一个没有原型的对象,不继承任何属性和方法
如果想创建一个普通的空对象(比如{}或者new创建的),则需要传入Object.prototype
var o = Object.create(Object.prototype); //创建一个对象,跟{}一样
可以通过任意原型创建新对象(也就是可以使任意对象可继承)
function inherit(prototype){
if(prototype==null){
throw TypeError();
}
if(Object.create){
return Object.create(prototype);
}
var type = typeof prototype;
if(type!=="object" && type!=="function"){
throw TypeError();
}
function F(){};
F.prototype=prototype;
return new F();
}
inherit()函数的其中一个用途就是防止对象被无意中修改,我们不是直接处理该对象,而是处理它的继承对象,当需要读取值时,读取的是继承来的值,而赋值或者修改值时,修改的继承对象,而不是原始对象
属性的查询和设置统称为属性访问
可以通过点(.)或者方括号([])运算符来获取属性的值
运算符左侧应当是一个表达式,返回一个对象,运算符右侧是属性名
当属性名右侧是保留字或者包含非正常字符时,只能使用方括号语法(ECMAScript5中,当属姓名是保留字时,也可以使用点号来访问属性)
var name = obj.name;
var title = obj["main-title"];
同样通过点(.)或者方括号([])语法也可以创建属性或者给属性赋值
book.name="zsz";
book["main title"]="js";
方括号语法的主要优点是可以通过变量来访问属性
var propertyName = "name";
person[propertyName]
当我们使用方括号语法时,看起来很像在操作数组,只不过这个数组的索引不是数字而是字符串,就像关联数组(也称散列、映射或字典)
JavaScript中的对象都是关联数组
在强类型语言中,对象必须拥有固定数目的属性,且这些属性名称必须提前定义好,JavaScript是弱类型语言,对象是动态的,可以拥有任意数量的属性,并且可以随时更改
当我们使用点(.)运算符访问对象属性时,属性名是一个标识符,标识符必须直接出现在JavaScript程序中,因为它不是数据类型,因此程序无法修改他们(也就是无法动态指定一个标识符)(标识符是静态的,必须写死在程序中)
但当使用方括号([])语法时,属性名是通过字符串表示的,字符串是数据类型,在程序运行过程中可以修改和创建它们(字符串是动态的,可以再运行时更改)
for(var i=0;i<5;i++){
alert(obj["name"+i]);
} //否则,就只能使用for in循环了
JavaScript对象的属性可能是“自有属性”也可能是从原型对象继承来的
当查询对象A的属性时,如果对象本身不存在该属性,那么将会继续在A的原型对象中查询属性,如果原型对象中也没有,但这个原型对象还有原型,那么继续在这个原型对象的原型中查询...直到找到属性或者返回undefined
对象的原型构成了原型链,通过这个“链”可以实现属性的继承
当给对象A的属性赋值时,如果A已有这个属性(自有属性),那么这个属性会被新值代替,如果这个属性是存在于原型中的,会给A创建并添加一个新属性(原型中的不受影响)
属性查询操作,首先检查当前对象,不存在时,才去查找原型链
属性赋值操作,首先检查原型链,如果这个属性继承自原型对象中的只读属性,那么赋值操作是不允许的,因此属性赋值要么失败,要么覆盖当前对象的属性或者为当前对象创建一个新属性
在JavaScript中,只有查询属性时才会体会到继承的存在,而设置属性则和继承无关,因为它总是在原始对象上创建或者设置属性,而不会去修改原型链
属性访问并不总是返回或者设置一个值
查询一个不存在(自身和原型中都不存在)的属性不会报错——而是返回undefined
但是查询一个不存在的对象的属性就会报错。null和undefined都没有属性,因此查询它们的属性会报错
var a;
a.name; //TypeError
给null和undefined设置属性时,也会报类型错误
给其它值设置属性时,也不总是成功,因为有些属性是只读的,不能重新赋值,而有一些对象不允许新增属性,但是这些设置属性的失败操作不会报错
//内置构造函数的原型是只读的
Object.prototype="zsz"; //赋值失败,不报错,仅仅是什么都没有发生
这其实是一个BUG,在ECMAScript5的严格模式下得以修复,任何失败的属性设置操作都会抛出一个类型异常错误
总结起来,属性设置时,会失败的几种情形
delete运算符可以删除对象的属性
delete book.name;
delete只是断开对象和属性的联系,而不会去操作属性中的属性
a={b:{c:1}};
d=a.b;
delete a.b;
d.c //仍然返回1
可能因为这种不严谨的代码而造成内存泄露,所以在销毁对象的时候,要遍历属性中的属性,依次删除
delete运算符只能删除自有属性,不能删除继承属性,要删除继承属性必须从定义这个属性的原型对象上删除它,而这会影响到所有继承自这个原型的对象
当delete表达式删除成功或者没有任何副作用(比如删除不存在的属性)时,它返回true,如果delete后不是一个属性访问表达式,delete同样返回true
o={x:1};
delete o.x; //删除属性x,返回true
delete o.x; //属性x不存在了,因此什么都没做,返回true
delete o.toString; //不能删除继承属性,因此什么都没做,返回true
delete 1; //无意义,返回true
delete不能删除那些可配置性为false的属性,某些内置对象的属性就是不可配置的,比如通过变量声明和函数声明创建的全局对象的属性(也就是使用var声明的变量不可删除,省略var创建的变量可以删除)
注:在严格模式下,删除一个不可配置属性会报类型错误,在非严格模式下,会返回false
当在非严格模式中删除全局对象的可配置属性时,可以省略对全局对象的引用,直接在delete操作符后跟随要删除的属性即可,在严格模式下,必须显示指定对象及属性名
delete Object.prototype; //不可删除
x=1;
delete window.x;(或delete x) //删除成功
var x=1;
delete window.x;(或delete x) //不能删除使用var声明的全局变量
function a(){}
delete window.a;(或delete a) //不能删除全局函数
检测某个属性是否存在于某个对象中,可以使用in运算符、hasOwnProperty()方法和propertyIsEnumerable()方法,甚至通过属性查询也可以做到
in运算符的左侧是属性名(必须是字符串形式),右侧是对象
如果对象的自有属性或者继承属性中包含这个属性则返回true
var o = {x:1};
"x" in o; //true
"y" in o; //false
"toString" in o; //true
该方法用于检测指定的属性是不是对象的自有属性,只有属性是自有属性时才返回true
var o = {x:1};
o.hasOwnProperty("x"); //true
o.hasOwnProperty("y"); //false
o.hasOwnProperty("toString"); //false
该方法是hasOwnProperty()方法的增强版,只有属性是自有属性且属性是可枚举的时才返回true
var o = Object.create({y:1});
o.x=1;
o.propertyIsEnumerable("x"); //true
o.propertyIsEnumerable("y"); //false
Object.prototype.propertyIsEnumerable("toString"); //false
var o = {x:1};
if(o.x !==undefined){
//x属性存在
}
这种方式不能代替in运算符,且不太可靠,因为这种方式不能区分不存在的属性和存在但是值为undefined的属性
遍历对象属性通常使用for/in循环,ECMAScript5提供了两个更好用的方法
for/in循环用来遍历对象的自有属性和继承的属性,但必须是可枚举的(例如继承的内置方法都是不可枚举的,自己添加的属性是可枚举的,除非使用ECMAScript5的方法自己设置为不可枚举)
var o={x:1,y:2,z:3};
for(var p in o){
console.log(p); //x,y,z
}
很多JavaScript库都给Object.prototype添加了新的属性或者方法,以便被所有对象继承,但是在ECMAScript5之前,这些新添加的方法是不能定义为不可枚举的,因此总会被for/in循环枚举出来,需要过滤
//方式1
for(var p in o){
if(!o.hasOwnProperty(p)){
continue; //跳过继承的属性
}
//正常操作在这
}
//方式2
for(var p in o){
if(typeof o[p]==="function"){
continue; //仅跳过方法
}
}
使用for/in循环实现的几个功能
/*将p的可枚举属性复制到o中,并返回o
*如果o中有同名属性,则覆盖
*这个函数不处理getter和setter以及复制属性
*/
function extend(o,p){
for(var pro in p){
o[pro]=p[pro];
}
return o;
}
.....
/*将p的可枚举属性复制到o中,并返回o
*如果o中有同名属性,o中的属性将不受影响
*这个函数不处理getter和setter以及复制属性
*/
function merge(o,p){
for(var pro in p){
if(o.hasOwnProperty(pro)){
continue;
}
o[pro]=p[pro];
}
return o;
}
.....
/*如果o中的属性在p中没有同名属性,则从o中删除该属性
*返回o
*/
function restrict(o,p){
for(var pro in o){
if(!(pro in p)){
delete o[pro];
}
}
return o;
}
.....
/*如果o中的属性在p中存在同名属性,则从o中删除该属性
*返回o
*/
function subtract(o,p){
for(var pro in p){
delete o[pro]; //这里有个点,直接删除就是,删除不存在的属性不会报错
}
return o;
}
.....
/*返回一个新对象,这个对象同时拥有o的属性和p的属性
*重名的,则使用p中的属性值
*/
function union(o,p){
return extend(extend({},o),p);
}
.....
/*返回一个新对象,这个对象拥有o和p的共有属性
*p中属性的值被忽略
*/
function intersection(o,p){
return restrict(extend({},o),p);
}
.....
/*返回一个数组,这个数组包含的是o中可枚举的自有属性*/
function keys(o){
if(typeof o !=="object"){
throw TypeError();
}
var result=[];
for(var pro in o){
if(o.hasOwnProperty(pro)){
result.push(pro);
}
}
return result;
}
它接受一个对象,返回一个数组,数组由对象中的可枚举的自有属性的名称组成(与上面的keys()方法类似),如果没有,则返回空数组
var a={x:1,y:2};
Object.keys(a); //["x","y"]
它接受一个对象,返回一个数组,数组由对象中自有属性的名称组成(不管是不是可枚举的)
Object.getOwnPropertyNames(Object.prototype)
返回
["constructor", "toString", "toLocaleString", "valueOf",
"hasOwnProperty", "isPrototypeOf", "propertyIsEnumerable",
"__defineGetter__", "__lookupGetter__", "__defineSetter__",
"__lookupSetter__", "__proto__"]
这个方法就不能模拟了,因为ECMAScript3中没有办法获取不可枚举的属性
对象的属性由属性名、属性值和一组特性构成
一般的属性都称为数据属性,数据属性就是一个值而已(包括了方法,函数也是一个值)
ECMAScript5中,属性值可以用一个或者两个方法代替,这两个方法是getter和setter,由getter和setter定义的属性称作——存取器属性(访问器属性),存取器属性不包含数据值
当查询存取器属性时,JavaScript调用getter方法(无参数),这个方法必须有返回值,返回的值就是属性存取表达式的值
当设置存取器属性时,JavaScript调用setter方法,将赋值表达式右侧的值当做参数传入setter,这个方法负责“设置”属性值,可以忽略setter方法的返回值
和数据属性不同,存取器属性不具有可写性
定义存取器属性最简单的方法就是使用对象直接量的一种扩展写法
var o ={
x:1, //普通的数据属性
//存取器属性,一般都是成对定义的函数
get a(){
return Math.random();
},
set a(value){
this.x=value; //this仍然表示当前对象
}
}
o.a; //返回一个随机数
o.a = 2 ; //将对象的x属性重写为2
存取器属性定义为一个或者两个和属性同名的函数,这个函数定义没有使用function关键字,而是使用get或者set,并且这里没有使用冒号将属性名和函数体分开,但是函数体的结束和下一个属性之间仍用逗号分隔
注意,this指代当前函数,这说明JavaScript把这些函数当做对象方法来调用的
存取器属性本质上是方法,但是在对象的外部作为属性来访问
和数据属性一样,存取器属性也是可以继承的
有很多场景可以用到存取器属性,比如智能检测属性的写入值以及在每次读取属性时返回不同的值
也就是当属性是动态的时候,最好使用存取器属性
//这个对象产生严格自增的序列号
var o = {
n:0,
get next(){
return this.n++;
},
set next(value){
if(value>this.n){
this.n=value;
}else{
throw "必须大于当前值";
}
}
};
还有一个例子
var o = {
get random(){
return Math.random();
}
};
o.random; //返回随机数
o.random; //返回不同的随机数
var o = {
random:Math.random()
};
o.random; //返回随机数
o.random; //返回相同的随机数
现在,我们讨论了给对象直接量定义存储器属性,下面会讲解给已经存在的对象添加存取器属性
除了属性的名字和值以外,属性还包含一些标识它们可写、可枚举和可配置的特性
在ECMAScript3中,这些特性就已经存在,由ECMAScript3程序创建的对象的属性,始终都是可写、可枚举、可配置的
ECMAScript5新增了一些API,可以对属性的特性进行相关的修改或者设置
我们将存取器属性的getter和setter方法看成是属性的特性,按照这个逻辑,数据属性的值同样也可以看成是属性的特性,因此,一个属性包含一个属性名和四个特性
数据属性的四个特性分别是
存取器属性不具有值特性和可写性,它的可写性是由setter方法存在与否决定的,存取器属性的四个特性分别是
该方法返回一个属性描述符对象,该对象包含了四个属性特性
第一个参数,Object,必须,表示要检测的属性所在的对象
第一个参数,String,必须,表示要检测对象的属性
var a={x:1}
Object.getOwnPropertyDescriptor(a,"x");
返回{value: 1, writable: true, enumerable: true, configurable: true}
var a={get b(){return 1}};
Object.getOwnPropertyDescriptor(a,"b");
//返回{get: function, set: undefined, enumerable: true, configurable: true}
从名字就可以看出,该方法只能得到自有属性的描述符,对于继承属性和不存在的属性,该方法返回undefined
Object.getOwnPropertyDescriptor({},"x"); //undefined
Object.getOwnPropertyDescriptor({},"toString"); //undefined
该方法用于设置属性的特性(对于不存在的属性,则创建该属性并设置特性)
第一个参数,Object,必须,表示要设置属性所在的对象
第一个参数,String,必须,表示要设置的属性
第三个参数,Object,必须,表示属性描述符对象(不必全部包含四个特性)
var o ={};
Object.defineProperty(o,"x",{
value:1,
writable:true,
enumerable:false,
configurable:true
});
o.x; //1
Object.keys[o]; //[] 不可枚举
Object.defineProperty(o,"x",{writable:false});
o.x=2; //赋值失败
o.x; //1
//但是属性仍然是可配置的,所以.....
Object.defineProperty(o,"x",{value:2});
o.x; //2
也就是不可写属性的值也是可以修改的,只要属性特性还是可配置的,就可以通过Object.defineProperty()方法修改属性值
需要注意的是,当没有包含全部四个特性时,对于新创建的属性来说,不包含的特性默认取得false或者undefined(对于存取器属性),对于修改已有的属性来说,默认的特性值没有任何修改
并且该方法要么修改已有属性要么自建属性,但不能修改继承属性
此方法还可用于创建存取器属性
Object.defineProperty(o,"x",{get:function(){return 1;}});
该方法用于同时修改或创建多个属性
第一个参数,Object,必须,表示要设置属性所在的对象
第二个参数,Object,必须,这个对象的属性名表示要新建或修改的属性名,属性值表示它们的属性描述符对象
var o ={};
Object.defineProperties(o,{
x:{value:2},
y:{writable:false}
});
对于那些不允许创建或者修改的属性来说,如果用上面两个方法操作就会抛出异常,基本规则如下
每一个对象都有与之相关的三个特性
对象的原型是用来继承属性的
原型是在对象创建之初就设置好的,通过对象直接量创建的对象使用Object.prototype作为它们的原型,通过new创建的对象使用构造函数.prototype作为它们的原型,通过Object.create()创建的对象使用第一个参数作为它们的原型(也可以传入null,则对象没有原型)
使用该方法可以获得对象的原型
接收一个参数,要查询的对象,返回该对象的原型,或者在该对象没有原型时,返回null
Object.getPrototypeOf({})
//返回Object.prototype
在ECMAScript3中,使用o.constructor.prototype来检测一个对象的原型,通过new创建的对象,通常会继承一个constructor属性,这个属性指代创建这个对象的构造函数(后续详述)
通过对象直接量创建的对象也包含一个constructor属性,指代的是Object()构造函数,因此对象直接量的原型是Object.prototype
通过Object.create()创建的对象也包含一个constructor属性,指代的是Object()构造函数,但是当传入null时,则不包含constructor属性
对象有一个非标准的__proto__属性(双下划线),用以直接获取或者设置对象的原型
**IE10及以下不支持__proto__**
该方法用于检测一个对象是否是传入对象的原型(或者位于原型链中)
var a = {};
Object.prototype.isPrototypeOf(a); //true
对象的类特性是一个字符串,表示的是对象的类型信息
但ECMAScript3和ECMAScript5都没有提供方法来获取或者设置对象的这个特性
只有一种间接的方法,toString()方法(继承自Object.prototype),返回如下字符串"[object class]"
var a = {}
a.toString(); // "[object object]"
字符串的第8个到倒数第二个位置之间的字符就是对象的类特性字符串
但是很多对象的toString()方法被重写了,因此必须调用call()方法
function classOf(o){
if(o==null){
return "Null";
}
if(o==undefined){
return "Undefined";
}
return Object.prototype.toString.call(o).slice(8,-1);
}
classOf(null) //"Null"
classOf(1) //"Number"
classOf("") //"String"
classOf(false) //"Boolean"
classOf({}) //"Object"
classOf([]) //"Array"
classOf(/./) //"Regexp"
classOf(new Date()) //"Date"
classOf(window) //"global"
function f() {};
classOf(f); //"Function"
classOf(new f()); //"Object"
判断一个对象是不是数组除了使用ECMAScript5中的Array.isArray()之外,就可以使用类特性来判断了
可扩展性表示是否可以给对象添加新属性
所有的内置对象和自定义对象默认都是可扩展的,宿主对象是否可扩展由JavaScript引擎决定
该方法将对象转换为不可扩展,一旦转换,就无法再恢复成可扩展的了
一旦将对象转换成不可扩展,就不能添加新属性了,但是可以给对象的原型添加新属性,不可扩展的对象任然可以继承这些属性
可扩展性是对象的特性,将对象标记为不可扩展,仅表示不能给该对象添加新属性,但是可以修改、删除已有属性,所以要和属性的特性(可写行,可配置性)配合使用
该方法查询对象是否可可扩展的,返回布尔值
该方法将封闭对象,除了将对象转换为不可扩展,还将对象的自有属性都设置为不可配置(但是可写的仍然是可写的),同样不能解封
该方法检测对象是否是封闭的,返回布尔值
该方法将冻结对象,除了将对象转换为不可扩展,还将对象的自有属性都设置为不可配置,并将自有的数据属性设置为只读的
存取器属性如果具有setter方法,存取器属性将不受影响,仍然可以通过给属性赋值调用它们
该方法检测对象是否是冻结的,返回布尔值
对象序列化,是指将对象的状态转换为字符串
对象还原是将字符串还原为对象
该方法用于序列化对象,返回对象的...
var a = {x:1};
JSON.stringify(a); //"{"x":1}"
JSON.stringify()只能序列化对象可枚举的自有属性,在序列化后输出的字符串中将会忽略不可枚举属性和继承属性
该方法用于还原对象,返回...
var a = {x:1};
var b = JSON.stringify(a); //"{"x":1}"
var c = JSON.parse(b); //{x:1}是a的深度拷贝
两个方法都是用JSON作为数据交换格式(后述)
函数,RegExp,Error对象和undefined不能序列化和还原
所有JavaScript对象都从Object.prototype继承属性(除了null和不是通过原型创建的对象,例如Object.create(null))
继承的主要是方法,前面讲过hasOwnProperty()、propertyIsEnumerable()、isPrototypeOf()三个方法,以及在Object构造函数里定义的静态函数Object.create()、Object.getPrototypeOf()等
下面再讲解几个定义在Object.prototype里的方法,不同的对象会将它们重写
该方法不接受参数,返回对象的字符串形式
在需要将对象转换为字符串的时候,都会隐式调用该方法,比如当使用"+"运算符连接一个字符串和对象时,或者在希望使用字符串的地方使用了对象时都会调用toString()方法
默认的toString()方法返回值包含的信息量很少(因为包含了对象的类型特性字符串,所以检测对象类型时很有用)
var a = {};
a.toString(); //"[object Object]"
很多类型都重写了toString()
对于数组来说,在有必要的情况下会对每个元素调用toString()方法,返回拼接而成的字符串
var a =[1,2,3];
a.toString(); //"1,2,3"
var a =[1,2,[3,4]];
a.toString(); //"1,2,3,4"
var a =[1,2,{x:1}];
a.toString(); //"1,2,[object Object]"
对于函数来说,会返回函数的源码,对于正则表达式来说,会返回正则表达式对象(后面各个对象,详述)
该方法不接受参数,返回对象的本地化字符串形式
Object中默认的toLocaleString()方法并不做任何本地化操作,仅仅调用toString()方法
Date类型重写了toLocaleString()方法,对日期做本地化转换
Array类型的toLocaleString()方法和toString()方法类似,只不过对于每一项都是调用的toLocaleString()方法
valueOf()方法和toString()方法类似,但是只在需要将对象转换为某种原始数据而不是字符串的时候使用,尤其是转换为数字的时候
如果在希望使用原始值得地方使用了对象,就会隐式调用valueOf()方法
很多类型都重写了valueOf()方法(后面详述)
Object.prototype定没有定义toJSON()方法,但对于需要序列化的对象,JSON.stringify()方法会调用toJSON()方法