0


js 深入理解 对象和对象属性

目录

1. Object 类型

在JS ,所有的引用类型的归根溯源 都是继承于 Object 类型。所以Object 类型 是最干净的 引用类型,它的实例 拥有最少的功能,很适合存储 和在应用程序间交换数据(因为功能少,占用的内存空间小)。创建方式有两种:

1.1 new 操作符

let person =newObject()// 创建Object 类型的对象 Person
    person.name ="Pitt";// 添加一个属性 name,并赋值为 Pitt
    person.age =29;//添加一个属性 age,并赋值为 29
    person.sayName=function(){//这个属性 还可以是方法
        console.log(this.name);};
    person.sayName();

1.2 对象字面量 ( object literal )

对象字面量是对象定义的简写形式。看看下面代码:

let person ={name:"Pitt",age:68,sayName(){
        console.log(this.name);}};

这里 提一下两个上下文的概念:
表达式上下文:大括号 { } 中的内容,一般用于返回值,跟在赋值操作符 “=” 后面。
语句上下文 :小括号 () 中的内容,比如 if 语句的条件后面。

上面的例子中定义了一个person 对象,它有两个属性 name 和 age, 用逗号隔开。

在对象字面量表示法中,属性名可以是字符串或数值,比如:

let person ={"name":"Pitt",//这里的 name 用了双引号,使用的字符串"age":68,5:true//数字5 作为属性名};

当然也可以用对象字面量表示法来定义一个只有默认属性和方法的对象,只要使用一对大括号,中间留空就行了:

let person ={};// 与 与 new Object() 相同
person.name ="Pitt";
person.age =68;

注意 在使用对象字面量表示法定义对象时,并不会实际调用 Object 构造函数。

开发者更倾向于使用对象字面量表示法。因为对象字面量代码更少,看起来也更有封装所有相关数据的感觉。事实上,对象字面量已经成为给函数传递大量可选参数的主要方式,比如:

functiondisplayInfo(args){let output ="";if(typeof args.name =="string"){
    output +="Name: "+ args.name +"\n";}if(typeof args.age =="number"){
    output +="Age: "+ args.age +"\n";}alert(output);}displayInfo({//使用两个参数的字面量对象name:"Bourne",age:29});displayInfo({//使用一个参数 的字面量对象       name:"Pitt"});

函数 displayInfo() 接收一个名为 args 的参数。它 有两个可选属性 name 或 age ,。函数内部会使用 typeof 操作符测试每个属性是否存在,然后根据属性有无构造并显示一条消息。然后,这个函数被调用了两次,每次都通过一个对象字面量传入了不同的数据。

注意 这种模式非常适合函数有大量可选参数的情况。一般来说,命名参数更直观,但在可选参数过多的时候就显得笨拙了。最好的方式是对必选参数使用命名参数,再通过一个对象字面量来封装多个可选参。

1.3 属性访问方式

  • 点语法 (dot notation),如:
    console.log(person.name);
  • 中括号 ,如:
    console.log(person["name"]);// "Pitt"

使用中括号,打字稍微繁琐,但也有它的优势,比如中括号内的字符串可以使用 变量代替,这样就非常灵活:

let propertyName ="name";
    console.log(person[propertyName]);// "Pitt"

通常我们还是首选 点语法的方式,简洁明了。


2. 属性的类型

属性可以分为两类:数据属性访问器属性
简而言之,这一节主要就是学习 如何将对象的属性设置为 只读、只写、私有化(private),可配置(包括删除、修改)等等,而通过设置 数据属性 和 访问器属性 就是两种手段。
使用一些内部特性来描述属性的特征。这些特性是由为 JavaScript 实现引擎的规范定义的。因此,开发者不能在 JavaScript 中直接访问这些特性。为了将某个特性标识为内部特性,规范会用两个中括号把特性的名称括起来,比如 [[Enumerable]]。

2.1 数据属性

数据属性包含一个保存数据值的位置。值会从这个位置读取,也会写入到这个位置。

2.1.1 四种特性

  • [[Configurable]] : 表示属性是否可以通过 delete 删除并重新定义,是否可以修改它的特性,已经是否可以把它改为访问器属性。默认值:true。
  • [[Enumberable]] : 表示属性是否可以通过 for -in 循环返回。默认值:true。
  • [[Writeable]] : 表示属性的值是否可以被修改。默认值:true。
  • [[Value]] : 包含属性实际的值。这就是前面提到的那个读取和写入属性值的位置。默认值 为 undefined (未赋值)。

前面例子中将属性显式添加到对象之后, [[Configurable]] 、 [[Enumerable]] 和 [[Writable]] 都会被设置为 true ,而 [[Value]] 特性会被设置为指定的值。比如:

let person ={name:"Pitt"// [[Value]] 特性值为 "Pitt"};

2.1.2 修改特性

方法 :Object.defineProperty()。
参数 : 对象 、属性名称、描述符对象(configurable 、 enumerable 、 writable 和 value)。

[[writeable]] 、[[Value]]举例

let person ={};
    Object.defineProperty(person,"name",{writable:false,//不可写入(只读)value:"Pitt"});
    console.log(person.name);// "Pitt"
    person.name ="Bourne";
    console.log(person.name);// 还是 "Pitt",不可修改

[[configurable]] 举例 :

let person ={};
    Object.defineProperty(person,"name",{configurable:false,//不可配置value:"Pitt"});
    console.log(person.name);// "Pitt"delete person.name;//删除属性name
    console.log(person.name);// 还是能输出 "Pitt",//抛出错误,因为上面configurable 已经被设置为 false。在进行配置,就会出错
    Object.defineProperty(person,"name",{configurable:true,value:"Pitt"});

在调用 Object.defineProperty() 时, configurable 、 enumerable 和writable的值如果不指定,则都默认为 false。

2.2 访问器属性

访问器属性不包含数据值。相反,它们包含一个获取(getter)函数和一个设置(setter)函数,不过这两个函数不是必需的。

在读取访问器属性时,会调用获取函数 get() ,这个函数的责任就是返回一个有效的值。

在写入访问器属性时,会调用设置函数 set(newValue) 并传入新值 ,这个函数必须决定对数据做出什么修改。

2.2.1 四个特性

  • [[Configurable]] :表示属性是否可以通过 delete 删除并重新定义,是否可以修改它的特性,以及是否可以把它改为数据属性。默认情况下,所有直接定义在对象上的属性的这个特性都是 true 。
  • [[Enumerable]] :表示属性是否可以通过 for-in 循环返回。默认情况下,所有直接定义在对象上的属性的这个特性都是 true 。
  • [[Get]] :获取函数,在读取属性时调用。默认值为 undefined 。
  • [[Set]] :设置函数,在写入属性时调用。默认值为 undefined 。

2.2.2 修改特性

访问器属性也一样不能直接定义,需要使用 Object.defineProperty()。看看具体例子:

// 定义一个对象,包含伪私有成员 year_和公共成员 editionlet book ={year_:2020,edition:1};
Object.defineProperty(book,"year",{get(){returnthis.year_;},set(newValue){if(newValue >2020){this.year_ = newValue;this.edition += newValue -2020;}}});
book.year =2018;
console.log(book.edition);// 2

  在这个例子中,对象 book 有两个默认属性: year_ 和 edition 。 year_ 中的下划线常用来表示该属性并不希望在对象方法的外部被访问。第三个属性 year 被定义为一个访问器属性,其中获取函数返回 year_ 的值,而设置函数会做一些计算以决定正确的版本(edition)。
  获取函数和设置函数不一定都要定义。只定义获取函数 get() 意味着属性是只读的,尝试修改属性会被忽略。在严格模式下,尝试写入只定义了获取函数 get() 的属性会抛出错误。类似地,**只有一个设置函数 set(newValue) 的属性是不能读取的( 相当于被定义了 private )**,非严格模式下读取会返回 undefined ,严格模式下会抛出错误。


3. 定义多个属性

   看这个标题,是 定义 多个属性,不是 新添 多个属性。在一个对象上同时定义多个属性的可能性是非常大的。为此,ECMAScript 提供了 Object.defineProperties() 方法。这个方法可以通过多个描述符一次性定义多个属性。它接收两个参数:要为之添加或修改属性的对象和另一个描述符对象,其属性与要添加或修改的属性一一对应。比如:

let book ={};// 定义一个book对象
Object.defineProperties(book,{year_:{value:2017},edition:{value:1},year:{get(){returnthis.year_;},set(newValue){if(newValue >2017){this.year_ = newValue;this.edition += newValue -2017;}}}});

  这段代码在 book 对象上定义了两个数据属性 year_ 和 edition ,还有一个访问器属性 year 。最终的对象跟上一节示例中的一样。唯一的区别是所有属性都是同时定义的,并且数据属性的 configurable 、 enumerable 和 writable 特性值都是 false 。


4. 读取属性的特性

第三节中 学习了 如何设置各个特性,这一节就看看如果读取这些特性。

  • 方法 :Object.getOwnPropertyDescriptor()
  • 参数 :对象、属性名
  • 返回值: 一个描述对象
let book ={};
Object.defineProperties(book,{year_:{value:2020},edition:{value:1},year:{get:function(){returnthis.year_;},set:function(newValue){if(newValue >2020){this.year_ = newValue;this.edition += newValue -2020;}}}});let descriptor = Object.getOwnPropertyDescriptor(book,"year_");
console.log(descriptor.value);// 2017
console.log(descriptor.configurable);// false 
console.log(typeof descriptor.get);// "undefined",因为 year_ 是数据属性let descriptor = Object.getOwnPropertyDescriptor(book,"year");
console.log(descriptor.value);// undefined ,因为 year 是访问器属性
console.log(descriptor.enumerable);// false
console.log(typeof descriptor.get);// "function",get 是一个函数

ECMAScript 2017 新增了 Object.getOwnPropertyDescriptors() 静态方法,。这个方法实际上会在对象的每个自有属性上调用 Object.getOwnPropertyDescriptor() 并在一个新对象中返回它们。这个真是好东西啊,不用一个个去调用了。来看看下面的 例子:

let book ={};
Object.defineProperties(book,{year_:{value:2017},edition:{value:1},year:{get:function(){returnthis.year_;},set:function(newValue){if(newValue >2017){this.year_ = newValue;this.edition += newValue -2017;}}}});
console.log(Object.getOwnPropertyDescriptors(book));//book的输出结果如下:// {// edition: {// configurable: false,// enumerable: false,// value: 1,// writable: false// },// year: {// configurable: false,// enumerable: false,// get: f(),// set: f(newValue),// },// year_: {// configurable: false,// enumerable: false,// value: 2017,// writable: false// }// }

5. 合并对象(merge objects)

方法:Object.assign()
参数
   a. 一个目标对象
   b. 一个或多个源对象
功能:每个源对象中可枚举( Object.propertyIsEnumerable() 返回 true )和自有( Object.hasOwnProperty() 返回 true )属性复制到目标对象。
  以字符串和符号为键的属性会被复制。方法内部就是 对每个符合条件的属性,这个方法会使用源对象上的 [[Get]] 取得属性的值,然后使用目标对象上的 [[Set]] 设置属性的值。看看下面的例子:

let dest, src, result;/**
* 赋值单个源对象
*/
dest ={};
src ={id:'src'};
result = Object.assign(dest, src);// Object.assign 修改目标对象// 也会返回修改后的目标对象
console.log(dest === result);// true //类型和值都相同
console.log(dest !== src);// true     //类型不同,值相同
console.log(result);// { id: src }  
console.log(dest);// { id: src }/**
* 多个源对象
*/
dest ={};
result = Object.assign(dest,{a:'foo'},{b:'bar'});
console.log(result);// { a: foo, b: bar }/**
* 获取函数与设置函数 看看属性 a 的复制
*/
dest ={seta(val){
        console.log(`调用 dest 参数为 ${val}的设置函数setter `);}};
src ={geta(){//获取a属性时候
        console.log('调用 src 的获取函数 getter');return'foo';}};
Object.assign(dest, src);// 调用 src 的获取方法// 调用 dest 的设置方法并传入参数"foo"// 因为这里的设置函数不执行赋值操作// 所以实际上并没有把值转移过来
console.log(dest);// { set a(val) {...} }

Object.assign() 实际上对每个源对象执行的是浅复制。如果多个源对象都有相同的属性,则使用最后一个复制的值。此外,从源对象访问器属性取得的值,比如获取函数,会作为一个静态值赋给目标对象。换句话说,不能在两个对象间转移获取函数和设置函数

let dest, src, result;/**
* 覆盖属性
*/
dest ={id:'dest'};
result = Object.assign(dest,{id:'src1',a:'foo'},{id:'src2',b:'bar'});// Object.assign 会覆盖重复的属性
console.log(result);// { id: src2, a: foo, b: bar }// 可以通过目标对象上的设置函数观察到覆盖的过程:
dest ={setid(x){
        console.log(x);}};
Object.assign(dest,{id:'first'},{id:'second'},{id:'third'});//输出结果(最后的值为third):// first// second// third /**
* 对象引用
*/
dest ={};
src ={a:{}};
Object.assign(dest, src);// 浅复制意味着只会复制对象的引用(即内存地址) 即dest.a 和src.a 指向内存的同一个对象
console.log(dest);// { a :{} }
console.log(dest.a === src.a);// true,类型和值 都一致。

如果赋值期间出错,则操作会中止并退出,同时抛出错误。 Object.assign() 没有“回滚”之前赋值的概念,所以可能只会完成部分复制。

let dest, src, result;/**
* 错误处理
*/
dest ={};
src ={a:'foo',getb(){// Object.assign()在调用这个获取函数时会抛出错误thrownewError();},c:'bar'};try{//这里使用try{} catch(){} 是为了出错也能继续执行
    Object.assign(dest, src);}catch(e){}// Object.assign()没办法回滚已经完成的修改// 因此在抛出错误之前,目标对象上已经完成的修改会继续存在:
console.log(dest);// { a: foo } ,这里发现值合并了 a 属性,没有合并 c 属性。

6. 对象标识及相等判定

6.1 === 操作符

=== 操作符大部分 判定相等都没有问题,但也会有一些特殊的判定效果不尽人意,看看下面的代码:

// 这些是===符合预期的情况
console.log(true===1);// false
console.log({}==={});// false  对于引用类型,  必须是同一个引用(地址),才会相等。两个空对象,地址是不相等的,所以是false
console.log("2"===2);// false// 这些情况在不同 JavaScript 引擎中表现不同,但仍被认为相等
console.log(+0===-0);// true
console.log(+0===0);// true
console.log(-0===0);// true// 要确定 NaN 的相等性,必须使用极为讨厌的 isNaN()
console.log(NaN===NaN);// false
console.log(isNaN(NaN));// true

6.2 Object.is() 函数

原型 : Object.is( obj1,obj2)
参数 : 两个参数必填的(既然要比较,肯定需要两个对象)
看看 下面的例子:

// 和 === 操作符一样
console.log(Object.is(true,1));// false
console.log(Object.is({},{}));// false
console.log(Object.is("2",2));// false// 正确的 0、-0、+0 相等/不等判定
console.log(Object.is(+0,-0));// false
console.log(Object.is(+0,0));// true  这个就达到了预期了
console.log(Object.is(-0,0));// false// 正确的 NaN 相等判定
console.log(Object.is(NaN,NaN));// true

要检查超过两个值,递归地利用相等性传递:

functionrecursivelyCheckEqual(x,...rest){return Object.is(x, rest[0])&&(rest.length <2||recursivelyCheckEqual(...rest));}

Object.is 是 ECMAScript 6 新增方法,解决了一些特殊对象比较的问题。


7. ECMAScript 6 优化的语法

ECMAScript 6 为定义和操作对象新增了很多极其有用的语法糖特性。这些特性都没有改变现有引擎
的行为,但极大地提升了处理对象的方便程度。

7.1 属性值简写

在给对象添加变量的时候,开发者经常会发现属性名和变量名是一样的。例如:

let name ='Pitt';let person ={name: name
};
console.log(person);// { name: 'Pitt' }

为此,简写属性名语法出现了。简写属性名只要使用变量名(不用再写冒号)就会自动被解释为同
名的属性键。如果没有找到同名变量,则会抛出 ReferenceError 。
以下代码和之前的代码是等价的:

let name ='Pitt';let person ={
    name
};
console.log(person);// { name: 'Pitt' }

代码压缩程序会在不同作用域间保留属性名,以防止找不到引用。以下面的代码为例:

functionmakePerson(name){return{//返回一个对象,对象的属性名就是name
        name  
    };}let person =makePerson('Pitt');//这里的 person 是一个对象 { name: 'pitt' }
console.log(person.name);// Pitt

在这里,即使参数标识符只限定于函数作用域,编译器也会保留初始的 name 标识符。如果使用 Google Closure 编译器压缩,那么函数参数会被缩短,而属性名不变:

functionmakePerson(a){return{name: a
    };}var person =makePerson('Pitt');
console.log(person.name);// Pitt

7.2 可计算属性

在EcmaScript6之前,使用变量作为属性,需要先声明对象,对象使用方括号方式添加属性。不能在对象字面量中直接动态命名属性。如下面代码所示:

const nameKey ='name';const ageKey ='age';const jobKey ='job';let person ={};//先声明了一个对象。
person[nameKey]='Matt';
person[ageKey]=27;
person[jobKey]='Software engineer';
console.log(person);// { name: 'Matt', age: 27, job: 'Software engineer' }

可计算属性就可以在对象字面量中完成动态属性赋值,中括号包围的对象属性键告诉运行时将其作为 JavaScript 表达式而不是字符串来求值:

const nameKey ='name';const ageKey ='age';const jobKey ='job';let person ={[nameKey]:'Pitt',[ageKey]:27,[jobKey]:'Software engineer'};
console.log(person);// { name: 'Pitt', age: 27, job: 'Software engineer' }

因为被当作 JavaScript 表达式求值,所以可计算属性本身可以是复杂的表达式,在实例化时再求值:

const nameKey ='name';const ageKey ='age';const jobKey ='job';let uniqueToken =0;functiongetUniqueKey(key){return`${key}_${uniqueToken++}`;//正则表达式,在key字符串后加下换线和数字}let person ={[getUniqueKey(nameKey)]:'Pitt',//这里中括号内使用了函数调用的结果。[getUniqueKey(ageKey)]:27,[getUniqueKey(jobKey)]:'Software engineer'};
console.log(person);// { name_0: 'Pitt', age_1: 27, job_2: 'Software engineer' }

注意 可计算属性表达式中抛出任何错误都会中断对象创建。如果计算属性的表达式有副作用,那就要小心了,因为如果表达式抛出错误,那么之前完成的计算是不能回滚的。

7.3 简写方法名

在给对象定义方法时,通常都要写一个方法名、冒号,然后再引用一个匿名函数表达式,如下所示:

let person ={sayName:function(name){
        console.log(`My name is ${name}`);}};
person.sayName('Matt');// My name is Matt

新的简写方法的语法遵循同样的模式,但开发者要放弃给函数表达式命名(给作为方法的函数命名通常没什么用)。相应地,这样也可以明显缩短方法声明。以下代码和之前的代码在行为上是等价的:

let person ={sayName(name){
        console.log(`My name is ${name}`);}};

这是不是原来越像其他的高级语言类中的方法了?
简写方法名对获取函数和设置函数也是适用的:

let person ={name_:'',getname(){returnthis.name_;},setname(name){this.name_ = name;},sayName(){
        console.log(`My name is ${this.name_}`);}};
person.name ='Matt';
person.sayName();// My name is Matt

来一个更牛的使用,简写方法名与可计算属性键相互兼容

const methodKey ='sayName';let person ={[methodKey](name){//此处使用可计算属性
        console.log(`My name is ${name}`);}}
person.sayName('Matt');// My name is Matt

8. 对象的解构

可以在一条语句中获取对象的多个属性的值,赋值给多个变量

看看下面两段等价的代码:

// 不使用对象解构let person ={name:'Pitt',age:27};let personName = person.name,// 给 personName 赋值
        personAge = person.age;// 给 personAge 赋值
console.log(personName);// Pitt
console.log(personAge);// 27};// 使用对象解构let person ={name:'Matt',age:27};let{name: personName,age: personAge }= person;
console.log(personName);// Matt
console.log(personAge);// 27

如果想让变量直接使用属性的名称,那么可以使用简写语法,比如:

let person ={name:'Matt',age:27};let{ name, age }= person;
console.log(name);// Matt
console.log(age);// 27

解构赋值不一定与对象的属性匹配。赋值的时候可以忽略某些属性,而如果引用的属性不存在,则该变量的值就是 undefined :

let person ={name:'Matt',age:27};let{ name, job }= person;
console.log(name);// Matt
console.log(job);// undefined

也可以在解构赋值的同时定义默认值,这适用于刚刚例子中引用的属性不存在于源对象中的情况:

let person ={name:'Matt',age:27};let{ name, job='Software engineer'}= person;
console.log(name);// Matt
console.log(job);// Software engineer

但是我没明白 job 这个变量 何必非要在结构里面赋值呢,在外面不一样吗?是为了显得和 name 是一起的吗?

解构在内部使用函数 ToObject() (不能在运行时环境中直接访问)把源数据结构转换为对象。这意味着在对象解构的上下文中,原始值会被当成对象。这也意味着(根据 ToObject() 的定义), null 和 undefined 不能被解构,否则会抛出错误。

let{ length }='foobar';
console.log(length);// 6let{constructor: c }=4;
console.log(c === Number);// truelet{ _ }=null;// TypeErrorlet{ _ }=undefined;// TypeError

解构并不要求变量必须在解构表达式中声明。不过,如果是给事先声明的变量赋值,则赋值表达式必须包含在一对括号中:

let personName, personAge;let person ={name:'Matt',age:27};({name: personName,age: personAge}= person);//必须放在小括号中
console.log(personName, personAge);// Matt, 27

8.1 嵌套解构

解构对于引用嵌套的属性或赋值目标没有限制。为此,可以通过解构来复制对象属性:

let person ={name:'Matt',age:27,job:{title:'Software engineer'}};let personCopy ={};({name: personCopy.name,age: personCopy.age,job: personCopy.job
}= person);// 因为一个对象的引用被赋值给 personCopy,所以修改// person.job 对象的属性也会影响 personCopy
person.job.title ='Hacker'
console.log(person);// { name: 'Matt', age: 27, job: { title: 'Hacker' } }
console.log(personCopy);//发现 personCopy的属性也改变了,说明personCopy里的属性的地址就是person属性的地址// { name: 'Matt', age: 27, job: { title: 'Hacker' } }

解构赋值可以使用嵌套结构,以匹配嵌套的属性:

let person ={name:'Matt',age:27,job:{title:'Software engineer'}};// 声明 title 变量并将 person.job.title 的值赋给它let{job:{ title }}= person;// 本来 job: 后面是一个变量名的,现在换成 { title } ,实现了对 title的赋值 ,真的是打入敌人内部了// 等价语句let title = person.job.title;

console.log(title);// Software engineer

在外层属性没有定义的情况下不能使用嵌套解构。无论源对象还是目标对象都一样:

let person ={job:{title:'Software engineer'}};let personCopy ={};// foo 在源对象上是 undefined({foo:{bar: personCopy.bar  // 因为bar 是嵌套在foo中,而 person本身没有bar属性,所以会报错}}= person);// TypeError: Cannot destructure property 'bar' of 'undefined' or 'null'.// job 在目标对象上是 undefined({job:{title: personCopy.job.title // 因为title 是嵌套在foo中,而 personcopy本身没有job.bar属性,所以会报错}}= person);// TypeError: Cannot set property 'title' of undefined

8.2 部分解构

需要注意的是,涉及多个属性的解构赋值是一个输出无关的顺序化操作。如果一个解构表达式涉及
多个赋值,开始的赋值成功而后面的赋值出错,则整个解构赋值只会完成一部分:

let person ={name:'Matt',age:27};let personName, personBar, personAge;try{// person.foo 是 undefined,因此会抛出错误({name: personName,foo:{bar: personBar },age: personAge }= person);}catch(e){}
console.log(personName, personBar, personAge);// Matt, undefined, undefined

8.3 参数上下文匹配

在函数参数列表中也可以进行解构赋值。对参数的解构赋值不会影响 arguments(函数的参数列表) 对象,但可以在函数签名中声明在函数体内使用局部变量:

let person ={name:'Matt',age:27};functionprintPerson(foo,{name, age}, bar){
    console.log(arguments);
    console.log(name, age);}functionprintPerson2(foo,{name: personName,age: personAge}, bar){
    console.log(arguments);
    console.log(personName, personAge);}printPerson('1st', person,'2nd');// ['1st', { name: 'Matt', age: 27 }, '2nd']// 'Matt', 27printPerson2('1st', person,'2nd');// ['1st', { name: 'Matt', age: 27 }, '2nd']// 'Matt', 27

本文转载自: https://blog.csdn.net/weiweiliude2/article/details/140710662
版权归原作者 伯恩bourne 所有, 如有侵权,请联系我们删除。

“js 深入理解 对象和对象属性”的评论:

还没有评论