03月09, 2019

JS基础(6)——对象(1)——对象基本介绍

在前面,我们有介绍过 JavaScript 中的数据类型。整体上可以分为两大类:简单数据类型复杂数据类型

简单数据类型一共有 6 种,分别为:

stringnumberbooleannullundefined、symbol

而复杂数据类型就只有 1 种:

object

也就是我们本章将会向大家所介绍的对象。

事实上,我们前面已经遇到过一些对象了。例如之前所介绍过的数组就是一种对象,不过它是 JavaScript 语言中内置的对象。

在本章中,我们将学习如何自定义对象,以及一些其他的内置对象。

本章将学习如下内容:

  • 创建对象的方式
  • 访问对象属性的方式
  • 对象常用的属性和方法
  • 对象的嵌套与解构
  • this 关键字
  • JSON 对象
  • Math 对象
  • Date 对象
  • 正则表达式

6-1 对象基础介绍

6-1-1 JavaScript 对象概述

JavaScript 里面的对象就是一组键值对的集合。这些键一般由字符串构成,而值可以是任意数据类型。比如字符串,数字,布尔,数组或者函数。

一般来讲,如果一个键映射的是一个非函数的值,我们将其称之为该对象的属性,而如果一个键映射的是一个函数的值,我们将其称之为方法

6-1-2 创建对象

要创建一个对象,我们只需要输入一对大括号即可。这样我们就可以创建一个空的对象,如下:

let objName = {};

创建好对象以后,我们就可以给该对象添加相应的属性,例如这里我们给 yan 这个对象添加相应的属性:

let yan = {};
yan.name = "Mr.Yan";
yan.age = 18;
yan.gender = "male";
yan.score = 100;

当然,和数组一样,我们可以在创建对象时就给该对象初始化好相应的信息,如下:

let yan = {
    name : "Mr.Yan",
    age : 18,
    gender : "male",
    score : 100
};

可以看到,当我们创建包含属性的对象的时候,属性与属性之间是以逗号隔开的。这里我们可以将属性名称之为键,而属性对应的数据称之为值。

所以,正如开头我们所介绍的那样,对象是由一个一个键值对组成的。

6-1-3 访问对象属性

访问对象的属性的方法有 3 种:点访问法,中括号访问法,symbol 访问法。

1. 点访问法

我们可以通过一个点.来访问到对象的属性,如下:

let yan = {
    name : "Mr.Yan",
    age : 18,
    gender : "male",
    score : 100
};
console.log(yan.name); // Mr.Yan
console.log(yan.age); // 18
console.log(yan.gender); // male
console.log(yan.score); // 100

2. 中括号访问法

第二种方法,是使用中括号法来访问对象的属性,如下:

let yan = {
    name : "Mr.Yan",
    age : 18,
    gender : "male",
    score : 100
};
console.log(yan["name"]); // Mr.Yan
console.log(yan["age"]); // 18
console.log(yan["gender"]); // male
console.log(yan["score"]); // 100

一般来讲,访问对象属性的时候使用点访问法的情况要多一些,那什么时候使用中括号访问方法呢?

当我们的属性名来自于变量的时候,这个时候中括号就要比点要灵活许多。来看下面的例子:

let yan = {
    name : "Mr.Yan",
    age : 18,
    gender : "male",
    score : 100
};
let str = "name";
console.log(yan[str]); // Mr.Yan

既然讲到了对象属性的中括号访问法,那我们就顺便为大家介绍一下伪数组对象的原理。

前面给大家介绍过的arguments就是一个伪数组对象。伪数组对象的原理就在于对象的键都是数字。如果属性名是数字的话,通过中括号法来访问时可以不用添加引号,如下:

let obj = {
    1 : "Bill",
    2 : "Lucy",
    3 : "David"
}
console.log(obj[1]); // Bill
console.log(obj[2]); // Lucy
console.log(obj[3]); // David

这样,就形成了给人感觉像是数组,但是并不是数组的伪数组对象。

3. symbol 访问法

在 ECMAScript 6 之前,对象的属性名都只能是字符串。但是这样很容易造成属性名的冲突。

比如我们使用了一个别人提供的对象,然后我们想在这个对象的基础上进行一定的扩展,添加新的属性,这个时候可能会因为不知道原来的对象里面包含哪些属性名,所以就把别人的对象所具有的属性给覆盖掉了。

示例如下:

// 假设 person 对象是从外部库引入的一个对象
let person = {
    name : "yan"
}
console.log(person.name); // yan
// 接下来我手动为 person 这个对象添加了一个 name 属性
// 这里便产生的属性冲突
person.name = "xiaojing";
console.log(person.name); // xiaojing

可以看到,这里两个 name 属性就产生了冲突,下面的 name 属性值把上面的 name 属性值给覆盖掉了。

从 ECMAScript 6 开始,新增了symbol这种数据类型,专门用来解决这样的问题。

创建一个 symbol,需要使用Symbol()函数,其语法如下:

let sym = Symbol(描述信息);

示例:

let name = Symbol("这是一个名字");
console.log(name); // Symbol(这是一个名字)
console.log(typeof name); // symbol

这里的描述信息是可选的,是对我们自己创建的 symbol 的一个描述。接下来我们来用 symbol 作为对象的属性,示例如下:

let person = {
    name : "yan"
}
let name = Symbol("这是一个名字");
person[name] = "xiaojing";
console.log(person.name); // yan
console.log(person[name]); // xiaojing

可以看到,使用 symbol 来作为对象的属性,避免了同名的属性名发生冲突。

有些时候我们希望在不同的代码中共享一个 symbol,那么这个时候可以使用Symbol.for()方法来创建一个共享的 symbol。

ECMAScript 6 提供了一个可以随时访问的全局 symbol 注册表。当我们使用Symbol.for()方法注册一个 symbol 的时候,系统会首先在全局表里面查找对应键名的 symbol 是否存在。如果存在,直接返回已经有的 symbol,如果不存在,则在全局表里面创建一个新的 symbol,如下:

let obj = {};
let name = Symbol.for("test"); // 此时的 test 不在是描述信息,而是全局表里的键
obj[name] = "yan";
let name2 = Symbol.for("test");
console.log(obj[name2]); // yan

如果使用Symbol.for()方法创建 symbol 的时候没有传递任何参数,那么也会将 undefined 作为全局表里面的键来进行注册,证明如下:

let obj = {};
let name = Symbol.for();
obj[name] = "yan";
let name2 = Symbol.for(undefined);
console.log(obj[name2]); // yan

ECMAScript 6 里面还提供了Symbol.keyFor()方法来查找一个 symbol 的键是什么。

但是需要注意的是,该方法只能找到注册到全局表里面的 symbol 的键。如果是通过Symbol()方法创建的 symbol,是无法找到的。

其实也很好理解,因为通过Symbol()方法创建的 symbol 不存在有键。

let obj = {};
let name1 = Symbol("test1");
let name2 = Symbol.for("test2");
let i = Symbol.keyFor(name1);
let j = Symbol.keyFor(name2);
console.log(i); // udnefined
console.log(j); // test2

前面有提到,如果一个对象的属性对应的是一个函数,那么这个函数被称之为对象的方法。调用对象方法的方式和前面介绍的访问对象属性的方式是一样的,可以通过点访问法,中括号访问法以及 symbol 访问法来进行对象方法的调用。

其实方法也可以看作是对象的一个属性,只是对应的值为一个函数而已。

let walk = Symbol("this is a test");
let person = {
    name : "yan",
    walk : function(){
        console.log("I'm walking");
    },
    [walk] : function(){
        console.log("I'm walking,too");
    }
}
person.walk(); // I'm walking
person["walk"](); // I'm walking
person[walk](); // I'm walking,too

6-1-4 删除对象属性

对象的任何属性都可以通过delete运算符来从对象中删除。示例如下:

let person = {
    name : "yan",
    age : 18,
    walk : function(){
        console.log("I'm walking");
    }
}
console.log(person.age); // 18
delete person.age; // 删除 age 这个属性
console.log(person.age); // undefined
person.walk(); // I'm walking
delete person.walk; // 删除 walk 方法
person.walk(); // 报错
// TypeError: person.walk is not a function

如果是删除的是属性,那么再次访问值为变为 undefined,而如果删除的是方法,那么调用时会直接报错。

6-1-5 对象常用属性和方法

1. in 操作符

该操作符用于判断一个对象是否含有某一个属性,如果有返回 true,没有返回 false。

需要注意的是,如果对象对应的某个属性为 symbol 属性,那么写法上有一定的区别,如下:

let person = {
    name : "yan",
    age : 18,
    walk : function(){
        console.log("I'm walking");
    }
}
let gender = Symbol("person's gender");
person[gender] = "male";
console.log("name" in person); // true
console.log("age" in person); // true
console.log([gender] in person); // 报错
// TypeError: Cannot convert a Symbol value to a string

书写为[gender],系统给出了无法将 symbol 值转换为字符串的错误。

正确的写法,应该是直接书写 symbol 名,而不需要包含在中括号里面,如下:

let person = {
    name : "yan",
    age : 18,
    walk : function(){
        console.log("I'm walking");
    }
}
let gender = Symbol("person's gender");
person[gender] = "male";
console.log("name" in person); // true
console.log("age" in person); // true
console.log(gender in person); // true

2. for..in

这个for..in我们在前面讲解遍历数组的时候已经见到过了。可以使用for..in来取出数组的键。因为数组也是一种对象,所以我们可以使用for..in来循环遍历一个对象的所有属性,示例如下:

let person = {
    name : "yan",
    age : 18,
    walk : function(){
        console.log("I'm walking");
    }
}
for(let i in person){
    console.log(i);
}
// name
// age
// walk

需要注意的是,使用for..in虽然说可以遍历出一个对象的所有的属性和方法(包括继承的,关于继承后面会介绍),但是无法遍历出用 symbol 来定义的属性,证明如下:

let person = {
    name : "yan",
    age : 18,
    walk : function(){
        console.log("I'm walking");
    }
}
let gender = Symbol("person's gender");
person[gender] = "male";
for(let i in person){
    console.log(i);
}
// name
// age
// walk

那么,这个时候可能有人会问了,那我如何才能遍历出一个对象的 symbol 属性呢?这里介绍两种方式。

第一种是使用Object.getOwnPropertySymbols()来返回一个对象所有的 symbol 属性,如下:

let person = {
    name: "yan",
    age: 18,
    walk: function () {
        console.log("I'm walking");
    }
}
let gender = Symbol("person's gender");
let score = Symbol("person's score");
let hobby = Symbol.for("hobby");
let food = Symbol.for("food");
person[gender] = "male";
person[score] = 100;
person[hobby] = "JavaScript";
person[food] = "meat";
console.log(Object.getOwnPropertySymbols(person));
// [ Symbol(person's gender),
//   Symbol(person's score),
//   Symbol(hobby),
//   Symbol(food) ]

除了上面的方法以外,ECMAScript 6 中还提供了一个叫做Reflect.ownkeys()方法。该方法可以遍历出一个对象的所有类型的键名,包括字符串的键名以及 symbol 类型的键名。

let person = {
    name : "yan",
    age : 18,
    walk : function(){
        console.log("I'm walking");
    }
}
let gender = Symbol("person's gender");
person[gender] = "male";
console.log(Reflect.ownKeys(person));
// [ 'name', 'age', 'walk', Symbol(person's gender) ]

3. keys(),values(),entries()

前面在介绍遍历数组,集合以及映射的时候,有介绍过这 3 个方法,分别用于找出可迭代对象的键,值,以及键和值。这里,我们也可以使用这 3 个方法来找出非可迭代对象的键和值,如下:

Object.key()

let person = {
    name : "yan",
    age : 18,
    walk : function(){
        console.log("I'm walking");
    }
}
let gender = Symbol("person's gender");
person[gender] = "male";
for(let i of Object.keys(person)){
    console.log(i);
}
// name
// age
// walk

Object.values()

let person = {
    name : "yan",
    age : 18,
    walk : function(){
        console.log("I'm walking");
    }
}
let gender = Symbol("person's gender");
person[gender] = "male";
for(let i of Object.values(person)){
    console.log(i);
}
// yan
// 18
// [Function: walk]

Object.entries()

let person = {
    name : "yan",
    age : 18,
    walk : function(){
        console.log("I'm walking");
    }
}
let gender = Symbol("person's gender");
person[gender] = "male";
for(let i of Object.entries(person)){
    console.log(i);
}
// [ 'name', 'yan' ]
// [ 'age', 18 ]
// [ 'walk', [Function: walk] ]

6-1-6 嵌套对象

一个对象里面包含其他的对象,这个我们称之为对象的嵌套。示例如下:

let family = {
    yan : {
        age : 18,
        gender : "male"
    },
    lin : {
        age : 20,
        gender : "female"
    }
};

当我们访问嵌套对象里面的值的时候,和访问单个对象的方式是一样的。

let family = {
    yan : {
        age : 18,
        gender : "male"
    },
    lin : {
        age : 20,
        gender : "female"
    }
};
console.log(family.yan.gender); // male 点访问法
console.log(family["lin"]["age"]); // 20 中括号访问法

对象的解构(扩展)

在前面介绍解构的时候,我们有介绍过数组的解构。这里来看一下对象的解构,基本上和数组的解构类似:

let a = {name:"yan",age:18};
let b = {name:"lin",age:20};
let {name:aName,age:aAge} = a;
let {name:bName,age:bAge} = b;
console.log(aName); // yan
console.log(aAge); // 18
console.log(bName); // lin
console.log(bAge); // 20

当属性名和变量名一致的时候,可以进行简写,如下:

let a = {name:"yan",age:18};
let {name,age} = a;
console.log(name); // yan
console.log(age); // 18

和数组一样,同样可以解构嵌套的对象,如下:

let family = {
    yan : {
        age : 18,
        gender : "male"
    },
    lin : {
        age : 20,
        gender : "female"
    }
};
let {yan,lin} = family;
console.log(yan); // { age: 18, gender: 'male' }
console.log(lin); // { age: 20, gender: 'female' }

顺便一提的是,解构我们也是可以像函数一样设置一个默认值的,如下:

let { name = "yan", age } = {};
console.log(name); // yan
console.log(age); // undefined

let [a = 3, b] = [];
console.log(a); // 3
console.log(b); // undefined

在上面的对象的解构中,我们为 name 变量设置了一个默认值为 yan,当我们解构一个空对象的时候,name 变量的值就使用了默认的 yan 这个值,而 age 这个变量由于没有设置默认值,所以值为 undefined。下面数组的解构也是同理。

当然,既然叫做默认值,和函数一样,如果有解构的值传过来的话,肯定就是使用解构传过来的值,如下:

let { name = "yan", age } = { name: "lin", age: 10 };
console.log(name); // lin
console.log(age); // 10

let [a = 3, b = 5] = [1, 2];
console.log(a); // 1
console.log(b); // 2

6-1-7 对象作为函数参数(扩展)

对象也可以作为函数的形式参数。这在有很多形参的时候显得非常有用,因为它允许我们在调用函数时不用记住参数的顺序。

我们先来看一下我们一般函数调用的例子,如下:

let test = function(name,age){
    console.log(`我叫${name},我今年${age}岁`);
}
test("yan",18); // 我叫yan,我今年18岁
test(18,"yan"); // 我叫18,我今年yan岁

可以看到,以前我们传递参数时,实参的顺序必须要和形参的顺序一致。否则就会出现上面第 2 次调用函数时的情况,输出不符合预期的结果。

当我们使用对象来作为实参时,形参可以书写解构表达式。这样就不用必须按照定义时形参的顺序来传值,只需要传递一个对象过去即可,对象属性的顺序可以随意。示例如下:

let test = function({name,age}){
    console.log(`我叫${name},我今年${age}岁`);
}
test({name:"yan",age:18}); // 我叫 yan,我今年 18 岁
test({age:18,name:"yan"}); // 我叫 yan,我今年 18 岁

我们也可以为解构的变量设置一个默认值,甚至可以给整个解构表达式设置一个默认值,示例如下:

// 给整个解构表达式设置了一个默认值 {name:"Bill",age:20}
let test = function({name = "yan",age = 18} = {name:"Bill",age:20}){
    console.log(`我叫${name},我今年${age}岁`);
}
test(); // 我叫 Bill,我今年 20 岁
test({}); // 我叫 yan,我今年 18 岁
test({name:"xiaojing"}); // 我叫 xiaojing,我今年 18 岁
test({age:1,name:"xizhi"}); // 我叫 xizhi,我今年 1 岁

这种技术被称之为命名参数,经常被用在函数有很多可选参数的时候。

6-1-8 this 关键字

既然学习到了对象,那么有必要介绍一下this关键字。

this,翻译成中文是这个的意思。当我们在一个对象中使用 this 关键字时,代表的就是当前对象。

来看下面的例子:

let person = {
    name : 'yan',
    age : 18,
    intro : function(){
        console.log(this); 
        // { name: 'yan', age: 18, intro: [Function: intro] }
        console.log(`My name is ${this.name},I'm ${this.age} years old`);
        // My name is yan,I'm 18 years old
    }
}
person.intro();

这里我们调用了 person 对象的intro()方法,里面涉及到了 this 关键字。由于是在对象里面,所以 this 指向当前对象,也就是 person 这个对象。

所以this.name等价于person.namethis.age等价于person.age

6-1-9 命名空间(扩展)

当相同的变量和函数名被共享在同一作用域的时候,就会发生命名冲突。这看起来不太可能,但是我们可以想象一下,随着时间的推移,我们已经写了很多的代码,可能不知不觉就重用了一个变量名。如果是使用的其他开发者的代码库,这种问题就变得更加有可能。

解决命名冲突的方式,就是使用对象字面量来为一组相关函数创建一个命名空间。这样在调用这些函数的时候需要先写上对象名,这里的对象名就充当了命名空间的角色。

示例如下:

let myMaths = {
    // 求平方函数
    square : function(x){
        return x * x;
    },
    // 传入数组求平均值函数
    avg : function(arr){
        let total = arr.reduce((a,b) => a + b);
        return total / arr.length;
    }
}
let arr = [1,2,3,4,5];
console.log(myMaths.avg(arr)); // 3
console.log(myMaths.square(5)); // 25

这里我们的 myMaths 就是我们的命名空间,这样就不用担心和其他人的变量或者函数名发生命名冲突。

本文链接:http://www.yanhongzhi.com/post/js-basis-17.html

-- EOF --

Comments