通过使用带有可选 属性列表 的花括号 {…}
来创建对象。一个属性就是一个键值对(“key: value”)
,其中键(key)
是一个字符串(也叫做属性名),值(value)
可以是任何值
let user = new Object(); // “构造函数” 的语法
let user = {}; // “字面量” 的语法
通常,我们用花括号。这种方式我们叫做字面量。
属性的值可以是任意类型,也可以用多字词语来作为属性名,但必须给它们加上引号:
let user = {
name: "John",
age: 30,
"likes birds": true // 多词属性名必须加引号
};
使用 const 声明的对象是可以被修改的
const user = {
name: "John"
};
user.name = "Pete"; // (*)
alert(user.name); // Pete
(*) 行似乎会触发一个错误,但实际并没有。const 声明仅固定了 user 的值,而不是值(该对象)里面的内容。仅当我们尝试将 user=… 作为一个整体进行赋值时,const 会抛出错误。
对于多词属性,点操作就不能用了
// 这将提示有语法错误
user.likes birds = true
点符号要求 key 是有效的变量标识符。这意味着:不包含空格,不以数字开头,也不包含特殊字符(允许使用 $ 和 _)。
有另一种方法,就是使用方括号,可用于任何字符串:
方括号中的字符串要放在引号中,单引号或双引号都可以
let user = {};
// 设置
user["likes birds"] = true;
// 读取
alert(user["likes birds"]); // true
// 删除
delete user["likes birds"];
方括号同样提供了一种可以通过任意表达式来获取属性名的方法 —— 跟语义上的字符串不同 —— 比如像类似于下面的变量,给了我们很大的灵活性。
let key = "likes birds";
// 跟 user["likes birds"] = true; 一样
user[key] = true;
当创建一个对象时,我们可以在对象字面量中使用方括号。这叫做 计算属性。
let fruit = prompt("Which fruit to buy?", "apple");
let bag = {
[fruit]: 5, // 属性名是从 fruit 变量中得到的
};
alert( bag.apple ); // 5 如果 fruit="apple"
计算属性的含义很简单:[fruit]
含义是属性名应该从 fruit 变量中获取。所以,如果一个用户输入 “apple”,bag 将变为 {apple: 5}
。
相当于
let fruit = prompt("Which fruit to buy?", "apple");
let bag = {};
// 从 fruit 变量中获取值
bag[fruit] = 5;
以在方括号中使用更复杂的表达式
let fruit = 'apple';
let bag = {
[fruit + 'Computers']: 5 // bag.appleComputers = 5
};
实际开发中,我们通常用已存在的变量当做属性名。
function makeUser(name, age) {
return {
name: name,
age: age,
// ……其他的属性
};
}
let user = makeUser("John", 30);
alert(user.name); // John
属性名跟变量名一样。这种通过变量生成属性的应用场景很常见,在这有一种特殊的属性值缩写方法,使属性名变得更短。
可以用 name
来代替 name:name
像下面那样:
function makeUser(name, age) {
return {
name, // 与 name: name 相同
age, // 与 age: age 相同
// ...
};
}
可以把属性名简写方式和正常方式混用:
let user = {
name, // 与 name:name 相同
age: 30
};
属性命名没有限制。属性名可以是任何字符串或者 symbol且关键字也可以,其他类型会被自动地转换为字符串。
例如,当数字 0 被用作对象的属性的键时,会被转换为字符串 “0”:
let obj = {
0: "test" // 等同于 "0": "test"
};
// 都会输出相同的属性(数字 0 被转为字符串 "0")
alert( obj["0"] ); // test
alert( obj[0] ); // test (相同的属性)
这里有个小陷阱:一个名为 __proto__
的属性。我们不能将它设置为一个非对象的值:
let obj = {};
obj.__proto__ = 5; // 分配一个数字
alert(obj.__proto__); // [object Object] — 值为对象,与预期结果不同
JavaScript 的对象有一个需要注意的特性:能够被访问任何属性。即使属性不存在也不会报错!
读取不存在的属性只会得到 undefined
。所以我们可以很容易地判断一个属性是否存在:
let user = {};
alert( user.noSuchProperty === undefined ); // true 意思是没有这个属性
检查属性是否存在的操作符 “in
”
语法是:
"key" in object
let user = { name: "John", age: 30 };
alert( "age" in user ); // true,user.age 存在
alert( "blabla" in user ); // false,user.blabla 不存在。
in
的左边必须是 属性名。通常是一个带引号的字符串。
如果我们省略引号,就意味着左边是一个变量,它应该包含要判断的实际属性名。例如
let user = { age: 30 };
let key = "age";
alert( key in user ); // true,属性 "age" 存在
为何会有 in 运算符呢?与 undefined 进行比较来判断还不够吗?
确实,大部分情况下与 undefined 进行比较来判断就可以了。但有一个例外情况,这种比对方式会有问题,但 in 运算符的判断结果仍是对的。那就是属性存在,但存储的值是 undefined
的时候
let obj = {
test: undefined
};
alert( obj.test ); // 显示 undefined,所以属性不存在?
alert( "test" in obj ); // true,属性存在!
为了遍历一个对象的所有键(key),可以使用一个特殊形式的循环:for..in
。这跟我们在前面学到的 for(;;)
循环是完全不一样的东西。
语法:
for (key in object) {
// 对此对象属性中的每个键执行的代码
}
例如,让我们列出 user 所有的属性:
let user = {
name: "John",
age: 30,
isAdmin: true
};
for (let key in user) {
// 属性键
alert( key ); // name, age, isAdmin
// 属性键的值
alert( user[key] ); // John, 30, true
}
所有的 “for” 结构体都允许我们在循环中定义变量,像这里的 let key
。同样,我们可以用其他属性名来替代 key。例如 "for(let prop in obj)"
也很常用
遍历一个对象,我们获取属性有特别的顺序,整数属性会被进行排序,其他属性则按照创建的顺序显示
let codes = {
"49": "Germany",
"41": "Switzerland",
"44": "Great Britain",
// ..,
"1": "USA"
};
for(let code in codes) {
alert(code); // 1, 41, 44, 49
}
这里的“整数属性”指的是一个可以在不做任何更改的情况下与一个整数进行相互转换的字符串。所以,“49” 是一个整数属性名,因为我们把它转换成整数,再转换回来,它还是一样的。但是 “+49” 和 “1.2” 就不行了:
// Math.trunc 是内置的去除小数部分的方法。
alert( String(Math.trunc(Number("49"))) ); // "49",相同,整数属性
alert( String(Math.trunc(Number("+49"))) ); // "49",不同于 "+49" ⇒ 不是整数属性
alert( String(Math.trunc(Number("1.2"))) ); // "1",不同于 "1.2" ⇒ 不是整数属性
如果属性名不是整数,那它们就按照创建时的顺序来排序,例如:
let user = {
name: "John",
surname: "Smith"
};
user.age = 25; // 增加一个
// 非整数属性是按照创建的顺序来排列的
for (let prop in user) {
alert( prop ); // name, surname, age
}
与原始类型相比,对象的根本区别之一是对象是“通过引用”被存储和复制的,与原始类型值相反:字符串,数字,布尔值等 —— 始终是以“整体值”的形式被复制的。
将 message 复制到 phrase
let message = "Hello!";
let phrase = message;
赋值了对象的变量存储的不是对象本身,而是该对象“在内存中的地址”,换句话说就是对该对象的“引用”。
let user = {
name: "John"
};
当一个对象变量被复制 —— 引用则被复制,而该对象并没有被复制。
let user = { name: "John" };
let admin = user; // 复制引用
现在我们有了两个变量,它们保存的都是对同一个对象的引用:
我们可以通过其中任意一个变量来访问该对象并修改它的内容:
let user = { name: 'John' };
let admin = user;
admin.name = 'Pete'; // 通过 "admin" 引用来修改
alert(user.name); // 'Pete',修改能通过 "user" 引用看到
仅当两个对象为同一对象时,两者才相等。
例如,这里 a 和 b 两个变量都引用同一个对象,所以它们相等:
let a = {};
let b = a; // 复制引用
alert( a == b ); // true,都引用同一对象
alert( a === b ); // true
而这里两个独立的对象则并不相等,即使它们看起来很像(都为空):
let a = {};
let b = {}; // 两个独立的对象
alert( a == b ); // false
如果我们想要复制一个对象,那么就需要创建一个新对象,并通过遍历现有属性的结构,在原始类型值的层面,将其复制到新对象,以复制已有对象的结构。
let user = {
name: "John",
age: 30
};
let clone = {}; // 新的空对象
// 将 user 中所有的属性拷贝到其中
for (let key in user) {
clone[key] = user[key];
}
// 现在 clone 是带有相同内容的完全独立的对象
clone.name = "Pete"; // 改变了其中的数据
alert( user.name ); // 原来的对象中的 name 属性依然是 John
也可以使用 Object.assign 方法来达成同样的效果
语法是:
Object.assign(dest, [src1, src2, src3...])
let user = { name: "John" };
let permissions1 = { canView: true };
let permissions2 = { canEdit: true };
// 将 permissions1 和 permissions2 中的所有属性都拷贝到 user 中
Object.assign(user, permissions1, permissions2);
// 现在 user = { name: "John", canView: true, canEdit: true }
如果被拷贝的属性的属性名已经存在,那么它会被覆盖:
let user = { name: "John" };
Object.assign(user, { name: "Pete" });
alert(user.name); // 现在 user = { name: "Pete" }
我们也可以用 Object.assign 代替 for…in 循环来进行简单克隆:
let user = {
name: "John",
age: 30
};
let clone = Object.assign({}, user);
//它将 user 中的所有属性拷贝到了一个空对象中,并返回这个新的对象。
但属性可以是对其他对象的引用。那应该怎样处理它们
let user = {
name: "John",
sizes: {
height: 182,
width: 50
}
};
alert( user.sizes.height ); // 182
现在这样拷贝 clone.sizes = user.sizes
已经不足够了,因为 user.sizes 是个对象,它会以引用形式被拷贝。因此 clone 和 user 会共用一个 sizes:
let user = {
name: "John",
sizes: {
height: 182,
width: 50
}
};
let clone = Object.assign({}, user);
alert( user.sizes === clone.sizes ); // true,同一个对象
// user 和 clone 分享同一个 sizes
user.sizes.width++; // 通过其中一个改变属性值
alert(clone.sizes.width); // 51,能从另外一个看到变更的结果
为了解决此问题,我们应该使用会检查每个 user[key] 的值的克隆循环,如果值是一个对象,那么也要复制它的结构。这就叫“深拷贝”。
我们可以用递归来实现。或者不自己造轮子,使用现成的实现,例如 JavaScript 库 lodash 中的 _.cloneDeep(obj)
。
作为对象属性的函数被称为方法。
let user = {
// ...
};
// 首先,声明函数
function sayHi() {
alert("Hello!");
};
// 然后将其作为一个方法添加
user.sayHi = sayHi;
user.sayHi(); // Hello!
// 这些对象作用一样
user = {
sayHi: function() {
alert("Hello");
}
};
// 方法简写看起来更好,对吧?
let user = {
sayHi() { // 与 "sayHi: function()" 一样
alert("Hello");
}
};
通常,对象方法需要访问对象中存储的信息才能完成其工作。
为了访问该对象,方法中可以使用 this 关键字。this 的值即调用该方法的对象。
let user = {
name: "John",
age: 30,
sayHi() {
// "this" 指的是“当前的对象”
alert(this.name);
}
};
user.sayHi(); // John
在这里 user.sayHi()
执行过程中,this 的值是 user。
在 JavaScript 中,this 关键字与其他大多数编程语言中的不同。JavaScript 中的 this 可以用于任何函数,即使它不是对象的方法。this 的值是在代码运行时计算出来的,它取决于代码上下文。在 JavaScript 中,this 是“自由”的,它的值是在调用时计算出来的,它的值并不取决于方法声明的位置,而是取决于在“点符号前”的是什么对象。
let user = { name: "John" };
let admin = { name: "Admin" };
function sayHi() {
alert( this.name );
}
// 在两个对象中使用相同的函数
user.f = sayHi;
admin.f = sayHi;
// 这两个调用有不同的 this 值
// 函数内部的 "this" 是“点符号前面”的那个对象
user.f(); // John(this == user)
admin.f(); // Admin(this == admin)
admin['f'](); // Admin(使用点符号或方括号语法来访问这个方法,都没有关系。)
这个规则很简单:如果 obj.f() 被调用了,则 this 在 f 函数调用期间是 obj
function sayHi() {
alert(this);
}
sayHi(); // undefined
在这种情况下,严格模式下的 this
值为 undefined
。如果我们尝试访问 this.name
,将会报错。
在非严格模式的情况下,this 将会是 全局对象
j箭头函数有些特别:它们没有自己的 this。如果我们在这样的函数中引用 this,this 值取决于外部“正常的”函数。
let user = {
firstName: "Ilya",
sayHi() {
let arrow = () => alert(this.firstName);
arrow();
}
};
user.sayHi(); // Ilya
构造函数在技术上是常规函数。不过有两个约定:
function User(name) {
this.name = name;
this.isAdmin = false;
}
let user = new User("Jack");
alert(user.name); // Jack
alert(user.isAdmin); // false
当一个函数被使用 new 操作符执行时,它按照以下步骤:
function User(name) {
// this = {};(隐式创建)
// 添加属性到 this
this.name = name;
this.isAdmin = false;
// return this;(隐式返回)
}
通常,构造器没有 return 语句。它们的任务是将所有必要的东西写入 this,并自动转换为结果。
但是,如果这有一个 return 语句,那么规则就简单了:
换句话说,带有对象的 return 返回该对象,在所有其他情况下返回 this。
例如,这里 return 通过返回一个对象覆盖 this:
unction BigUser() {
this.name = "John";
return { name: "Godzilla" }; // <-- 返回这个对象
}
alert( new BigUser().name ); // Godzilla,得到了那个对象
这里有一个 return 为空的例子(或者我们可以在它之后放置一个原始类型,没有什么影响):
function SmallUser() {
this.name = "John";
return; // <-- 返回 this
}
alert( new SmallUser().name ); // John
通常构造器没有 return 语句
如果没有参数,我们可以省略 new 后的括号,这里省略括号不是一种“好风格”,但是规范允许使用该语法。
let user = new User; // <-- 没有参数
// 等同于
let user = new User();
我们不仅可以将属性添加到 this 中,还可以添加方法。
function User(name) {
this.name = name;
this.sayHi = function() {
alert( "My name is: " + this.name );
};
}
let john = new User("John");
john.sayHi(); // My name is: John
let user = {}; // 一个没有 "address" 属性的 user 对象
alert(user.address.street); // Error!
因为 user.address
为 undefined
,尝试读取 user.address.street
会失败,并收到一个错误。
我们希望避免出现这种错误
可能最先想到的方案是在访问该值的属性之前,使用 if 或条件运算符 ? 对该值进行检查
let user = {};
alert(user.address ? user.address.street : undefined);
这样可以,这里就不会出现错误了……但是不够优雅。对于嵌套层次更深的属性就会出现更多次这样的重复,这就是问题了。
例如,尝试获取 user.address.street.name。
我们既需要检查 user.address,又需要检查 user.address.street
let user = {}; // user 没有 address 属性
alert(user.address ? user.address.street ? user.address.street.name : null : null);
我们有一种更好的实现方式,就是使用 && 运算符
依次对整条路径上的属性使用与运算进行判断,以确保所有节点是存在的(如果不存在,则停止计算)
let user = {}; // user 没有 address 属性
alert( user.address && user.address.street && user.address.street.name ); // undefined(不报错)
如果可选链 ?.
前面的部分是 undefined
或者 null
,它会停止运算并返回该部分。
例如 value?.prop
value.prop
相同,undefined
/null
时)则返回 undefined
。let user = {}; // user 没有 address 属性
alert( user?.address?.street ); // undefined(不报错)
即使 对象 user 不存在,使用 user?.address 来读取地址也没问题:
let user = null;
alert( user?.address ); // undefined
alert( user?.address.street ); // undefined
如果未声明变量 user,那么 user?.anything 会触发一个错误:
// ReferenceError: user is not defined
user?.address;
?. 前的变量必须已声明(例如 let/const/var user 或作为一个函数参数)。可选链仅适用于已声明的变量。
我们应该只将 ?. 使用在一些东西可以不存在的地方。
例如,如果根据我们的代码逻辑,user 对象必须存在,但 address 是可选的,那么我们应该这样写 user.address?.street
,而不是这样 user?.address?.street
。
所以,如果 user 恰巧因为失误变为 undefined,我们会看到一个编程错误并修复它。否则,代码中的错误在不恰当的地方被消除了,这会导致调试更加困难。
正如前面所说的,如果 ?. 左边部分不存在,就会立即停止运算(“短路效应”)。
所以,如果后面有任何函数调用或者副作用,它们均不会执行。
let user = null;
let x = 0;
user?.sayHi(x++); // 没有 "sayHi",因此代码执行没有触达 x++
alert(x); // 0,值没有增加
可选链 ?.
不是一个运算符,而是一个特殊的语法结构。它还可以与函数和方括号一起使用。
例如,将 ?.()
用于调用一个可能不存在的函数。
在下面这段代码中,有些用户具有 admin 方法,而有些没有:
let userAdmin = {
admin() {
alert("I am admin");
}
};
let userGuest = {};
userAdmin.admin?.(); // I am admin
userGuest.admin?.(); // 啥都没有(没有这样的方法)
在这两行代码中,我们首先使用点符号userAdmin.admin
来获取 admin 属性,因为用户对象一定存在,因此可以安全地读取它。
然后 ?.()
会检查它左边的部分:如果 admin 函数存在,那么就调用运行它(对于 userAdmin)。否则(对于 userGuest)运算停止,没有错误。
如果我们想使用方括号 [] 而不是点符号 . 来访问属性,语法 ?.[]
也可以使用。跟前面的例子类似,它允许从一个可能不存在的对象上安全地读取属性。
let user1 = {
firstName: "John"
};
let user2 = null; // 假设,我们不能授权此用户
let key = "firstName";
alert( user1?.[key] ); // John
alert( user2?.[key] ); // undefined
alert( user1?.[key]?.something?.not?.existing); // undefined
此外,我们还可以将 ?.
跟 delete 一起使用:
delete user?.name; // 如果 user 存在,则删除 user.name
?.
链使我们能够安全地访问嵌套属性。
但是,我们应该谨慎地使用 ?.
,仅在当左边部分不存在也没问题的情况下使用为宜。以保证在代码中有编程上的错误出现时,也不会对我们隐藏。
根据规范,对象的属性键只能是字符串类型或者 Symbol 类型。不是 Number,也不是 Boolean,只有字符串或 Symbol 这两种类型。
“Symbol” 值表示唯一的标识符。
可以使用 Symbol() 来创建这种类型的值:
// id 是 symbol 的一个实例化对象
let id = Symbol();
创建时,我们可以给 Symbol 一个描述(也称为 Symbol 名),这在代码调试时非常有用:
// id 是描述为 "id" 的 Symbol
let id = Symbol("id");
Symbol 保证是唯一的。即使我们创建了许多具有相同描述的 Symbol,它们的值也是不同。描述只是一个标签,不影响任何东西。
例如,这里有两个描述相同的 Symbol —— 它们不相等:
let id1 = Symbol("id");
let id2 = Symbol("id");
alert(id1 == id2); // false
JavaScript 中的大多数值都支持字符串的隐式转换。例如,我们可以 alert 任何值,都可以生效。Symbol 比较特殊,它不会被自动转换。
例如,这个 alert 将会提示出错:
let id = Symbol("id");
alert(id); // 类型错误:无法将 Symbol 值转换为字符串。
如果我们真的想显示一个 Symbol,我们需要在它上面调用 .toString()
let id = Symbol("id");
alert(id.toString()); // Symbol(id),现在它有效了
或者获取 symbol.description
属性,只显示描述(description):
let id = Symbol("id");
alert(id.description); // id
Symbol 允许我们创建对象的“隐藏”属性,代码的任何其他部分都不能意外访问或重写这些属性。
例如,如果我们使用的是属于第三方代码的 user 对象,我们想要给它们添加一些标识符。
我们可以给它们使用 Symbol 键:
let user = { // 属于另一个代码
name: "John"
};
let id = Symbol("id");
user[id] = 1;
alert( user[id] ); // 我们可以使用 Symbol 作为键来访问数据
使用 Symbol(“id”) 作为键,比起用字符串 “id” 来有什么好处?
因为 user 对象属于其他的代码,那些代码也会使用这个对象,所以我们不应该在它上面直接添加任何字段,这样很不安全。但是你添加的 Symbol 属性不会被意外访问到,第三方代码根本不会看到它,所以使用 Symbol 基本上不会有问题。
然后该脚本可以创建自己的 Symbol(“id”),像这样:
// ...
let id = Symbol("id");
user[id] = "Their id value";
如果我们要在对象字面量 {…} 中使用 Symbol,则需要使用方括号把它括起来。
let id = Symbol("id");
let user = {
name: "John",
[id]: 123 // 而不是 "id":123
};
let id = Symbol("id");
console.log(id);//Symbol(id)
这是因为我们需要变量 id 的值作为键,而不是字符串 “id”
Symbol 属性不参与 for…in 循环。
let id = Symbol("id");
let user = {
name: "John",
age: 30,
[id]: 123
};
for (let key in user) alert(key); // name, age (no symbols)
// 使用 Symbol 任务直接访问
alert( "Direct: " + user[id] );
Object.keys(user)
也会忽略它们。这是一般“隐藏符号属性”原则的一部分。如果另一个脚本或库遍历我们的对象,它不会访问到符号属性。
相反,Object.assign
会同时复制字符串和 symbol 属性
Object.assign()
方法用于将所有可枚举属性的值从一个或多个源对象分配到目标对象。它将返回目标对象。
let id = Symbol("id");
let user = {
[id]: 123
};
let clone = Object.assign({}, user);
alert( clone[id] ); // 123
这里有一个全局 Symbol 注册表。我们可以在其中创建 Symbol 并在稍后访问它们,它可以确保每次访问相同名字的 Symbol 时,返回的都是相同的 Symbol。
要从注册表中读取(不存在则创建)Symbol,请使用 Symbol.for(key)
。
该调用会检查全局注册表,如果有一个描述为 key 的 Symbol,则返回该 Symbol,否则将创建一个新 Symbol(Symbol(key)),并通过给定的 key 将其存储在注册表中。
注册表内的 Symbol 被称为 全局 Symbol
// 从全局注册表中读取
let id = Symbol.for("id"); // 如果该 Symbol 不存在,则创建它
// 再次读取(可能是在代码中的另一个位置)
let idAgain = Symbol.for("id");
// 相同的 Symbol
alert( id === idAgain ); // true
对于全局 Symbol,不仅有 Symbol.for(key) 按名字返回一个 Symbol,还有一个反向调用:Symbol.keyFor(sym),它的作用完全反过来:通过全局 Symbol 返回一个描述。
// 通过 name 获取 Symbol
let sym = Symbol.for("name");
let sym2 = Symbol.for("id");
// 通过 Symbol 获取 name
alert( Symbol.keyFor(sym) ); // name
alert( Symbol.keyFor(sym2) ); // id
Symbol.keyFor 内部使用全局 Symbol 注册表来查找 Symbol 的键。所以它不适用于非全局 Symbol。如果 Symbol 不是全局的,它将无法找到它并返回 undefined。
但任何任何 Symbol 都具有 description
属性。
let globalSymbol = Symbol.for("name");
let localSymbol = Symbol("name");
alert( Symbol.keyFor(globalSymbol) ); // name,全局 Symbol
alert( Symbol.keyFor(localSymbol) ); // undefined,非全局
alert( localSymbol.description ); // name
从技术上说,Symbol 不是 100% 隐藏的。有一个内置方法 Object.getOwnPropertySymbols(obj)
允许我们获取所有的 Symbol。还有一个名为 Reflect.ownKeys(obj)
的方法可以返回一个对象的 所有 键,包括 Symbol。所以它们并不是真正的隐藏。
date1 - date2
的结果是两个日期之间的差值。对象到原始值的转换,是由许多期望以原始值作为值的内建函数和运算符自动调用的。
// 输出
alert(obj);
// 将对象作为属性键
anotherObj[obj] = 123;
// 显式转换
let num = Number(obj);
// 数学运算(除了二元加法)
let n = +obj; // 一元加法
let delta = date1 - date2;
// 小于/大于的比较
let greater = user1 > user2;
规范明确描述了哪个运算符使用哪个 hint。很少有运算符“不知道期望什么”并使用 “default” hint。通常对于内建对象,default hint
的处理方式与 “number
” 相同,因此在实践中,最后两个 hint 常常合并在一起。没有 “boolean” hint,在布尔上下文中所有对象都是 true.
为了进行转换,JavaScript 尝试查找并调用三个对象方法:
obj[Symbol.toPrimitive](hint)
—— 带有 symbol 键 Symbol.toPrimitive
(系统symbol)的方法,如果这个方法存在的话,obj.toString()
和 obj.valueOf()
,无论哪个存在。obj.valueOf()
和obj.toString()
,无论哪个存在。