引子
JavaScrpt中的对象是动态的,可在代码执行的任意时刻发生改变。基于类的语言会根据类的定义锁定对象,JavaScript对象没有这种限制。JavaScript变成一大重点就是管理那些自己创建的对象,这就是为什么理解对象如何运作是理解整个JavaScript的关键。
定义属性
有两种创建对象的方式:使用 Object() 构造函数和使用对象的字面量形式。
var person1 = {
name : "ming" //调用了 [[Put]] 方法:[[Put]]name,在对象上创建新节点保存新属性 name
};
var person2 = new Object();
person2.name = "hua";
person1.age = 20; //调用了 [[Put]] 方法:[[Put]]age,在对象上创建新节点保存新属性 name
person2.age = 20;
person1.name = "gang"; //调用了 [[Set]] 方法:[[Set]]name,用新的属性值覆盖旧属性值
person2.name = "hong";
上例中定义了 person1 和 person2 两个对象,起初它们都具有 name 属性;然后两个对象又被添加了新的 age 属性,再然后,为二者的 name 属性分别赋了新值。我们自己创建的对象总是可以任我们随意修改,频繁地定义属性或者为属性赋新值也不会出现错误。实际上,这得益于JavaScript的内部方法,内部方法在后台帮我们完成了一系列操作。
当一个属性第一次被添加给对象时,JavaScript 在对象上调用一个名为 [[Put]] 的内部方法。[[Put]] 方法会在对象上创建一个新节点来保存属性,就像第一次在哈希表上添加一个键一样。这个操作不仅指定了初始的值,也定义了属性的一些特征。所以在前例中,当属性 name 和 age 在每个对象上第一次被定义时,[[Put]] 方法都在该对象上被调用了。调用 [[Put]] 的结果是在对象上创建了一个自有属(有别于原型属性),一个自有属性表明仅该指定的对象实例拥有该属性。
当一个已有的属性被赋予一个新值时,调用的是一个名为 [[Set]] 的方法。该方法将属性的当前值替换为新值。上例为 name 属性设置第二个值,调用了 [[Set]] 方法。
属性探测
由于属性可以在任何时候添加,所以有时候有必要检查对象是否已有一个属性。JavaScript开发新手常错误地使用以下方式探测属性是否存在。
//不可靠的探测方法
if (person1.age) {
//使用 age 属性做一些操作
}
问题在于JavaScript的类型强制会影响该方式的输出结果。当 if 判断中的值是一个对象、非空字符串、非零数字或 true 时,判断结果为真;而当值是一个 null、undefined、0、false、NaN 或空字符串时判断结果为假。由于一个对象属性可以包含这些假值,上例代码有可能导致错误的属性探测。例如,当 person1.age 为 0 时,虽然 age 属性存在,if 条件仍然无法满足。更加可靠的判断属性是否存在的方式是使用 in 操作符。
in 操作符在给定对象中查找一个给定名称的属性,如果找到返回 true。
console.log("name" in person1); // true
console.log("age" in person1); // true
console.log("title" in person1); // false
从上例可以看出,使用 in 操作符时:属性名须以字符串形式置于 in 操作符左侧,对象名置于 in 操作符右侧。in 操作符还可以用于检查对象是否存在某个方法,毕竟,方法就是值为函数的属性嘛。
var person1 = {
name : "ming",
sayName : function() {
console.log(this.name);
}
};
console.log("sayName" in person1); // true
同检查对象是否存在某个属性一样,只不过这里置于 in 操作符左侧的是函数名(须以字符串形式给出)。
in 操作符有一个不容忽略的特性,它不仅会检测自有属性还会检测原型属性。然而在某些情况下,我们只希望仅仅检测某个属性是否是给定对象的自有属性。这时,可以使用 hasOwnProperty() 方法,该方法在给定的属性存在且为给定对象的自有属性时返回 true。
var person1 = {
name : "ming",
sayName : function() {
console.log(this.name);
}
};
console.log("name" in person1); // true
console.log(person1.hasOwnProperty("name")); // true
console.log("toString" in person1); // true
console.log(person1.hasOwnProperty("toString"));// false
上例中,name 是 person1 的一个自有属性,所以 in 操作符和 hasOwnProperty() 方法都返回 true。而 toString() 方法是一个所有对象都有的原型属性,所以 in 操作符对 toString 返回 true,而 hasOwnProperty() 方法对 toString 返回 false。
删除属性
属性可以在任何时候被添加到自定义的对象上,同理地,我们也可以在任何时候删除自定义对象的属性。将一个属性的值设置为 null 并不能从对象中彻底移除那个属性,只是调用内部方法 [[Set]] 用 null 值替换了该属性原来的值而已。 若要彻底移除对象的某个属性,需要使用 delete 操作符。
delete 操作符针对单个属性调用名为 [[Delete]] 的内部方法,从而在对象上移除此属性值对。当 delete 操作符操作成功时返回 true,事实上,不是所有属性都可以 delete 操作符移除。
var person1 = {
name : "ming",
};
console.log(person1.hasOwnProperty("name")); // true
delete person1.name; // 操作成功,返回 true,但不显示
console.log(person1.hasOwnProperty("name")); // false
console.log(person1.name); // undefined
上例中,name 属性被从 person1 中删除。操作完成后,使用 hasOwnProperty() 方法探测返回 false,说明 person1 对象的自由属性中已不存在 name 这个属性。console.log(person1.name)
返回 undefined 也证实了这一点,因为 person1 对象不存在 name 属性嘛。
属性枚举
所有我们自己添加的属性默认都是可枚举的,可枚举属性的内部特征 [[Enumerable]]
被设置为 true,可以用 for...in
循环遍历一个对象的所有可枚举属性。
var person = {
name: 'ming',
age: 20,
weight: 62
}
for (var property in ming) {
console.log(property); // outputs "name" then "age" then "weight"
}
for...in
循环会遍历一个对象的所有可枚举属性并在每次循环时将属性名赋值给一个变量。在上面这个例子中 for...in
循环每次迭代时对象 person 的下一个可枚举属性的名字就被赋值给变量 property,直到遍历完对象 person 的所有可枚举属性。
如果要获取一个对象的可枚举属性列表,可以使用 Object.keys()
方法。使用时将对象作为参数传递给它,即可获取当前对象可枚举属性的名字组成的数组。
var person = {
name: 'ming',
age: 20,
weight: 62
}
var propertyArr = Object.keys(person);
console.log(propertyArr); // outputs "[ 'name', 'age', 'weight' ]"
总结一下:通常需要直接操作可枚举属性名数组时,我们会选用 Object.keys()
方法;否则,选用 for...in
循环遍历当前对象。
!!!注意:for...in
循环返回的和 Object.keys()
返回的可枚举属性有一个区别,for...in
循环同时也会遍历原型属性而 Object.keys()
只返回自有(实例)属性。
另外,并不是所有的属性都是可枚举的,对象的大部分原生属性的 [[Enumerable]]
特征都被设置为 false。我们可以用 propertyIsEnumerable()
方法检查一个属性是否为可枚举的,使用时需向其传递要检查的属性名。
var person = {
name: 'ming'
}
console.log(person.hasOwnProperty('name')); // outputs "true"
console.log(person.propertyIsEnumerable('name')); // outputs "true"
var propertyArr = Object.keys(person);
console.log(propertyArr.hasOwnProperty('length')); // outputs "true"
console.log(propertyArr.propertyIsEnumerable('length')); // ouuputs "false"
上例中,属性 name 是我们自己为对象 person 手动添加的一个属性,它是可枚举的。而 propertyArr 数组的属性 length 是 Array 类型的原生属性,所以它是不可枚举的。正是由于 JavaScript 中很多原生属性默认都是不可枚举的,所以通常情况下,对一个对象使用 for...in
循环和 Object.keys()
方法得到的结果没有什么差异。
属性类型
属性有两种类型:数据属性和访问器属性。数据属性比较好理解,其仅仅用于存储一个值,如前例中的 name 属性。而理解访问器属性可能要费些功夫,首先要明确一件事儿,访问器属性与传统的数据属性的性质一样,也是作为某个具体对象的属性而存在。与数据属性的区别在于,访问器属性本身不包含值而是包含了两个函数:一个称为 getter
,当访问器属性被读取时调用;另一个被称为 setter
,当访问器属性被写入时调用。一般情况下,只需定义这二者之一即可定义一个访问器属性,当然了,getter
和 setter
都定义也是可以的,在后面将给出同时定义两个函数和只定义一个函数的区别。
var person = {
_name: 'ming',
get name() {
console.log('Getting name');
return this._name;
},
set name(value) {
console.log('Setting name to %s', value)
this._name = value;
}
};
console.log(person.name); // outputs "Getting name" then "ming"
person.name = 'xiaoMing'; // outputs "Setting name to xiaoMing"
console.log(person.name); // outputs "Getting name" then "xiaoMing"
上例中定义了一个访问器属性,这个访问器属性的名字叫做“name”。用于定义访问器属性 name 的 getter
和 setter
的语法看上去很像函数但没有 function
关键字:特殊关键字 get 和 set 被用在访问器属性名字(在这里是 name)的前面,后面跟着小括号和函数体。getter
被期望返回一个值,而 setter
则接受一个需要被赋值给访问器属性的值作为参数。
上例中还定义了一个数据属性 _name,它是用来干嘛的呢?前面我们说,访问器属性本身不包含值而是包含了两个函数,这可能会引起一些误解,因为访问器属性虽然本身不包含值但事实上访问器属性也是有确切的值的,毕竟上例中的代码 console.log(person.name) 正确地输出了一个值呢。正是自定义的数据属性 _name 保存了访问器属性的实际值,这里,前置下划线是一个约定俗成的命名规范,表示该属性被认为是私有的,实际上它还是公开的。上例使用 _name 来保存访问器属性的数据,但我们也可以将访问器属性的数据保存在变量甚至是另一个对象中。
总结一下:访问器属性有自己的值,但是需要额外的数据属性来存储,访问器属性本身仅包含了两个函数,分别在读取和写入访问器属性时被调用,我们可以在这两个函数体内部任意创建自己的代码。简单来说,与数据属性相比,访问器属性可以自定义读取和写入属性的行为。如果只是需要保存数据,通常没有什么理由使用访问器属性,直接使用数据属性即可。但某些时候,我们可能希望读取和写入属性时会触发一些其他操作,这个时候使用访问器属性定义的 getter()
和 setter()
就显得有必要了。例如,上例中是给读取和写入属性的行为添加了日志记录。
!!!注意:并不一定要同时定义 getter
和 setter
,可以选择定义其中之一。但若仅定义了 getter
,该属性就变成只读了,在非严格模式下试图写入将失败,而在严格模式下将抛出错误;若仅定义了 setter
,该属性就变成只写,在两种模式下试图读取都将失败。下面是一个用来说明的例子:
var person1 = {
_name: 'ming',
get name() {
console.log('Getting name');
return this._name;
}
};
console.log(person1.name); // outputs "Getting name" then "ming"
person1.name = 'xiaoMing';
console.log(person1.name); // outputs "Getting name" then "ming"
var person2 = {
_name: 'gang',
set name(value) {
console.log('Setting name to %s', value);
this._name = value;
}
};
console.log(person2.name); // undefined
person2.name = 'xiaoGang'; // "Setting name to xiaoGang"
console.log(person2.name); // undefined
建议:若无特殊要求,定义访问器属性时最好将 getter
和 setter
两者都定义;若想保护对象的某个属性,可以只定义 getter
以将其设置为只读。
属性特征
通用特征
有两个属性特征是数据属性和访问器属性都具备的。一个是 [[Enumerable]]
,决定了属性是否可枚举,即能否遍历该属性。另一个是 [[Configurable]]
,决定了属性是否可以配置,可配置的属性我们能够随时改变它(这意味着可配置属性的类型可以从数据属性变成访问器属性或相反)。delete
操作符可以删除一个可配置的属性而不能删除一个不可配置的属性。我们自定义的所有属性默认都是可枚举的、可配置的。
如果想改变属性特征,可以使用 Object.defineProperty()
方法。该方法接受3个参数:拥有该属性的对象、属性名和一个对象(称呼其为“属性描述对象”吧)。属性描述对象包含一些属性以用于设置新的属性特征,属性的名字与内部特征同名但不包括中括号(像“Enumerable”、“Configurable”这样),值为要设置的新属性特征值。
var person = {
name: 'ming',
};
Object.defineProperty(person, 'name', {
enumerable: false
});
console.log(person.propertyIsEnumerable('name')); // outputs "false"
Object.defineProperty(person, 'name', {
configurable: false
});
// try to delete the property "person.name"
delete person.name; // throw an error in strict mode
console.log('name' in person); // outputs "true"
console.log(person.name); // outputs "ming"
Object.defineProperty(person, 'name', { // error!
configurable: true
});
上例先是定义了 name 属性,然后使用 Object.defineProperty()
方法将其 [[Enumerable]]
特征设为 false,之后通过 propertyIsEnumerable()
方法验证可知 name 属性已经被成功地设置为不可枚举。同样地,我们使用 Object.defineProperty()
方法将 name 属性的 [[Configurable]]
特征设置为 false,之后试图使用 delete
操作符删除 name 属性,然而 name 属性已经变为不可配置的了,所以会操作失效(严格模式下,试图删除一个不可配置的属性将会抛出一个错误),我们依然能访问 name 属性的值证明了这一点。
最后几行代码试图调用 Object.defineProperty()
方法重新将 name 属性的 [[Configurable]]
特征设置为 true,然而这将抛出一个错误,因为我们无法将一个不可配置的属性变成可配置(仔细想想,就能理解其中的逻辑)。
数据属性特征
数据属性特征额外拥有两个访问器属性不具备的特征。第1个是 [[Value]]
,包含属性的值。当我们在对象上创建属性时该特征被自动赋值。所有的属性的值都被保存在 [[Value]]
中,哪怕该值是一个函数。第2个特征是 [[Writable]]
,该特征值是一个布尔值,指示该属性是否可写(即能否被重新赋值),所有的属性默认都是可写的。
有了这两个额外特征,我们可以使用 Objecat.defineProperty()
方法完整地定义一个数据属性,即使该属性还不存在。
var person = {
name: 'ming'
};
--------------------------------------------------------------------------
// the effect of the above is the same with the follow code's
--------------------------------------------------------------------------
var person = {};
Object.defineProperty(person, 'name', {
value: 'ming',
writable: true,
enumerable: true,
configurable: true
});
当 Object.defineProperty()
被调用时,它首先检查同名属性是否存在。若不存在,将根据属性描述对象指定的特征创建一个新的属性,上例中,name 不是 person 已有的属性,因此它被成功创建。若存在,将使用属性描述对象中给出的属性特征去为此同名属性的属性特征重新赋值,如果属性描述对象没有给出完整的的特征值列表,那么将会使用属性原有的特征值。
当用 Object.defineProperty()
定义新的属性时一定要为所有特征指定一个值,否则布尔型的特征会被默认设置为 false。下面是一个例子。
var person = {};
Object.defineProperty(person, 'name', {
value: 'ming'
});
console.log('name' in person); // outputs "true"
console.log(person.name); // outputs "ming"
console.log(person.propertyIsEnumerable('name')); // outputs "false"
delete person.name; // throw en error in strict mode
console.log('name' in person); // outputs "true"
person.name = 'xiaoMing';
console.log(person.name); // outputs "ming"
上面这段代码创建的 name 属性是不可枚举、不可配置、不可写的,这是因为在调用 Object.defineProperty()
时没有显示地指定这些特征值为 true。在这个例子中,除了读取 name 属性的值,无法对它做任何事情,因为其他操作都被锁定了。
!!!注意:在严格模式下试图改变不可写属性会抛出错误,而在非严格模式下则会失效。
访问器属性特征
除通用特征之外,访问器属性也有两个额外特征。它们是 [[Get]]
和 [[Set]]
,内含 getter
和 setter
函数。和对象字面形式的 getter
/ setter
一样,仅需要定义其中一个特征就可以创建一个访问器属性。
!!!注意:如果试图创建一个同时具有数据特征和访问器属性特则的属性,将会得到一个错误。
使用访问器属性特征比使用对象字面形式定义访问器属性的优势在于,我们可以为已有的对象定义这些属性。要知道,使用对象字面量形式定义访问器属性,只能在创建对象时进行。
var person = {
_name: 'ming',
get name() {
console.log('Getting name');
return this._name;
},
set name(value) {
console.log('Setting name to %s', value);
this._name = vaue;
}
};
----------------------------------------------------------------------------------------
// the effect of the above is the same with the follow code's
----------------------------------------------------------------------------------------
var person = {
_name: 'ming'
};
Object.defineProperty(person, 'name', {
get: function() {
console.log('Getting name');
return this._name;
},
set: function(value) {
console.log('Setting name to %s', value);
this._name = value;
},
enumerable: true,
configurable: true
});
注意那个传给 Object.defineProperty()
的属性描述对象中的 get
和 set
关键字,它们是包含函数的数据属性。在这里不能使用对象字面形式。
设置 [[Enumerable]]
和 [[Configurable]]
可以改变访问器属性的工作方式。下面是一个例子:
var person = {
_name: 'ming'
};
Object.defineProperty(person, 'name', {
get: function() {
console.log('Getting name');
return this._name;
}
});
console.log('name' in person); // outputs "true"
console.log(person.propertyIsEnumerable('name')); // outputs "false"
delete person.name; // throw an error in strict mode
console.log('name' in person); // outputs "true"
person.name = 'xiaoMing';
console.log(person.name); // outputs "ming"
在上面这段代码中,name 属性是一个只有 getter
的访问器属性。没有 setter
,也没有任何特征被显示地指定为 true,所以它的值只能被读取,不能被改变。
!!!注意:和对象字面形式定义的访问器属性一样,在严格模式下试图写入没有 setter
的访问器属性会抛出错误,而在非严格模式下失败。试图读取一个没有 getter
的访问器属性则总是返回 undefined。
定义多个属性
要想同时为一个对象定义多个属性,可以使用 Object.defineProperties()
方法,看起来像 Object.defineProperties
的复数形式,容易理解吧。这个方法接收两个参数:需要改变的对象和一个包含所有属性信息的对象。后者可以被看成一个哈希表,键是属性名,值是为该属性定义特征的属性描述对象。
var person = {};
Object.defineProperties(person, {
// data property to store date
_name: {
value: 'ming',
writable: true,
enumerable: true,
configurable: true
},
// accessor property
name: {
get: function() {
console.log('Getting name');
return this._name;
},
set: function(value) {
console.log('Setting name to %s', value);
this._name = value;
},
enumerable: true;
configurable: true;
}
});
上例中定义了 _name 数据属性和 name 访问器属性。我们可以用 Object.defineProperties()
定义任意数量的属性,甚至可以同时改变已有的属性并创建新的属性。
获取属性特征
如果需要获取属性的特征,可以使用 Object.getOwnPropertyDescriptor()
方法。从名字可以看出来,这个方法只可用于自有属性。它接收两个参数:对象和属性名。如果属性存在,返回一个属性描述对象,内含4个属性:enumerable 和 configurable,另外两个属性则根据属性类型决定。
var person = {
_name: 'ming'
};
console.log(Object.getOwnPropertyDescriptor(person, '_name'));
// { value: 'ming', writable: true, enumerable: true, configurable: true }
Object.defineProperties(person, {
name: {
get: function() {
console.log('Getting name');
return this._name;
}
}
});
console.log(Object.getOwnPropertyDescriptor(person, 'name'));
// { get: [Function], set: undefined, enumerable: false, configurable: false }
上例中,属性 _name 作为对象字面形式的一部分被定义。调用 Object.getOwnPropertyDescriptor()
方法返回的属性描述对象具有4个属性:value、writable、enumerable 和 configurable,即使它们从没有被 Obejct.defineProperty()
显式定义。访问器属性 name 是使用 Object.defineProperty()
显式定义的,但仅仅是指定了访问器属性的 getter
函数,然而我们使用 Object.getOwnPropertyDescriptor()
查看属性特征时,其返回的属性描述对象依然具有4个属性特征,由于我们是定义一个全新的属性,所以 name 属性的特征 enumerable 和 configurable 值均为 false。
禁止修改对象
对象和属性一样具有指导其行为的内部特征。其中,[[Extensible]]
是一个布尔值,它指明该对象本身是否可以被修改。我们自己创建的所有对象默认都是可扩展的,这意味着新的属性可以随时被添加。设置[[Extensible]]
为 fasle,就能禁止新属性的添加。要想检查一个对象内部特征 [[Extensible]]
的值,可以使用 Object.isExtensible()
方法,它接收一个对象作为其参数然后返回一个布尔值。
有3种方法可以帮助我们锁定对象属性,禁止修改对象:
禁止扩展
第1种方法是使用 Object.preventExtensions()
创建一个不可扩展的对象。该方法接收一个对象作为参数,一旦在一个对象上使用该方法,它的 [[Extensible]]
特征就被设置为 false,我们就永远不能再给它添加新的属性了。
var person = {
name: 'ming'
};
console.log(Object.isExtensible(person)); // outputs "true"
Object.preventExtensions(person);
console.log(Object.isExtensible(person)); // outputs "false"
person.age = 20; // throw an error in strict mode
console.log('age' in person); // outputs "false"
上例中,创建 person 后,这段代码先检查其 [[Extensible]]
特征,然后将其变得不可扩展。由于不可扩展,sayName() 方法永远无法被加到 person 上。
!!!注意:严格模式下试图给一个不可扩展对象添加属性会抛出错误,而在非严格模式下则会失效。应该对不可扩展对象使用严格模式,这样,当一个不可扩展对象被错误使用时我们就会知道。
对象封印
对象封印是创建不可扩展对象的第二种方法。一个被封印的对象是不可扩展的且所有属性都不可配置。这意味着不仅不能给对象添加属性,也不能删除属性或改变其类型(从数据属性变成访问器属性或相反)。如果一个对象被封印,只能读写它的属性。
可以用 Object.seal()
方法来封印一个对象。该方法被调用时,对象的 [[Extensible]]
特征被设置为 false,其所有属性的 [[Configurable]]
特征被设置为 false。我们可以使用 Object.isSealed()
判断一个对象是否被封印。
var person = {
name: 'ming'
};
console.log(Object.isExtensible(person)); // true
console.log(Object.isSealed(person)); // false
console.log(Object.getOwnPropertyDescriptor(person, 'name'));
// { value: 'ming', writable: true, enumerable: true, configurable: true }
Object.seal(person);
console.log(Object.isExtensible(person)); // false
console.log(Object.isSealed(person)); // true
console.log(Object.getOwnPropertyDescriptor(person, 'name'));
// { value: 'ming', writable: true, enumerable: true, configurable: false }
person.age = 20;
console.log('age' in person); // false
person.name = 'xiaoMing';
console.log(person.name); // xiaoMing
delete person.name;
console.log('name' in person); // true
console.log(person.name); // xiaoMing
上面这段代码封印了 person,从封印前后使用 Object.getOwnProperty()
方法返回的属性描述对象来看,封印对象的同时也将其所有属性的 configurable 特征设为了 false,因此不能在被封印的对象上添加或删除属性,之后的代码也验证这一点。所有的被封印对象都是不可扩展对象,这一点通过观察封印 person 前后使用 Object.isExtensible()
方法和 Object.isSealed()
方法返回的值可知。
实际上,JavaScript 中的封印对象有点儿像 Java 和 C++ 语言中的用类创建对象时无法给对象添加新的属性,但可修改属性的原有值。
!!!注意中:确保被封印对象使用严格模式,这样当有人误用该对象时,会得到一个错误。
对象冻结
创建不可扩展对象的最后一种方法是冻结它。如果一个对象被冻结,则不能在其上添加删除属性,不能改变属性类型,也不能写入任何数据类型。简而言之,被冻结对象是一个数据属性都为只读的被封印对象。被冻结对象无法解冻。可以用 Object.freeze()
来冻结一个对象,用 Object.isFrozen()
来判断一个对象是否被冻结。
var person = {
name: 'ming'
};
console.log(Object.isExtensible(person)); // true
console.log(Object.isSealed(person)); // false
console.log(Object.isFrozen(person)); // false
console.log(Object.getOwnPropertyDescriptor(person, 'name'));
//{ value: 'ming', writable: true, enumerable: true, configurable: true }
Object.freeze(person);
console.log(Object.isExtensible(person)); // false
console.log(Object.isSealed(person)); // true
console.log(Object.isFrozen(person)); // true
console.log(Object.getOwnPropertyDescriptor(person, 'name'));
// { value: 'ming', writable: false, enumerable: true, configurable: false }
person.age = 20;
console.log('age' in person); // false
person.name = 'xiaoMing';
console.log(person.name); // "ming"
delete person.name;
console.log('name' in person); // true
console.log(person.name); // "ming"
上例中,person 被冻结。被冻结对象也被认为是不可扩展对象和被封印对象,所以 Object.isExtensible()
返回 false,而 Object.isSealed()
返回 true。属性 name 无法改变,所以试图对其赋值为“xiaoMing”的操作失败,后续的检查依旧返回“ming”。
!!!注意:被冻结对象仅仅只是对象在某个时间点上的快照。其用途有限且极少使用。和所有不可扩展对象一样,应该对被冻结象使用严格模式。