JavaScript(四)面向对象

本文最后更新于:8 个月前

对象Object、类Class

JavaScript(四)面向对象

一、Object属性

对象就是一组属性的无序集合(ES定义),对象的内容可以看作一组名/值对,其中值可以是数据或函数(方法)。

对象内部属性用两个中括号括起来以示区别,开发者并不能直接访问这些属性

对象内部属性分为两种类型:数据属性、访问器属性。对于每个对象的内部属性,这两种类型只能为其中一种。

1.1、数据属性

数据描述符 说明 默认值
[[Configurable]] 表示属性是否可以通过delete删除并重新定义,是否可以修改它的特性 false
[[Enumerable]] 表示属性是否可以通过for-in循环返回 false
[[Writable]] 表示属性的值是否可以被修改 false
[[Value]] 读取和写入属性值的位置 undefined

1.2、访问器属性

不包含数据值,包含getter和setter函数,用来读取和设置属性值

存取描述符 说明 默认值
[[Configurable]] 表示属性是否可以通过delete删除并重新定义,是否可以修改它的特性,以及是否可以把它改为数据属性 false
[[Enumerable]] 表示属性是否可以通过for-in循环返回 false
[[Get]] 获取函数,在读取属性时调用。默认值为undefined。 undefined
[[Set]] 设置函数,在写入属性时调用。默认值为undefined undefined

修改、获取属性的特性的方法:

  • Object.defineProperty()方法
  • Object.defineProperties()方法
  • Object.getOwnPropertyDescriptor()方法
  • Object.getOwnPropertyDescriptors()方法

1.3、defineProperty()方法

ES5 提供了 Object.defineProperty 方法,该方法可以在一个对象上定义一个新属性,或者修改一个对象的现有属性,并返回这个对象。

defineProperty方法只能设置set和get方法

1
2
3
4
5
6
Object.defineProperty(obj, prop, descriptor)
//obj: 要在其上定义属性的对象。
//prop: 要定义或修改的属性的名称。
//descriptor: 将被定义或修改的属性的描述符。如果不进行配置,需要用空集合表示,该字段不能为空,如下

var obj = Object.defineProperty({}, "num", {});
1
2
3
4
5
6
7
8
var obj = {};
Object.defineProperty(obj, "num", {
value : 1,
writable : true,
enumerable : true,
configurable : true
});
// 对象 obj 拥有属性 num,值为 1
1
2
3
4
5
6
7
8
obj.x = 1;
// 如果用.号的方式定义属性,相当于
Object.defineProperty(obj, "x", {
value : 1,
writable : true,
enumerable : true,
configurable : true
});
1
2
3
4
5
6
7
8
9
10
11
12
13
14
const object1 = {property1:40};

Object.defineProperty(object1, 'property2', {
get(){return 40;},
set(newVal){console.log("set property with ${newVal}")}
});

object1.property2 = 77;
console.log(object1);
// property1: 40
// property2: (…)
// get property2: ƒ get()
// set property2: ƒ set(newVal)
// [[Prototype]]: Object

1.4、assign()方法

Object.assign() 方法用于将所有可枚举属性的值从一个或多个源对象复制到目标对象。它将返回目标对象。

1
2
3
4
//语法格式
Object.assign(target, ...sources)
//target:目标对象
//sources:源对象(可多个)

使用场景:合并对象。返回合并后的对象,并且将合并结果同步修改到作为第一个参数的对象,但不影响后面参数的对象

1
2
3
4
5
6
7
8
const o1 = { a: 1 };
const o2 = { b: 2 };
const o3 = { c: 3 };

const obj = Object.assign(o1, o2, o3);
console.log(obj); // { a: 1, b: 2, c: 3 }
console.log(o1); // { a: 1, b: 2, c: 3 }, 注意目标对象自身也会改变。
//其实就是对象的拷贝,o1就是目标对象,后面的是源对象,后面的属性等会拷贝到目标对象,o2、o3不会变

合并具有相同属性的对象,遇到同名属性,后面参数的对象中的同名属性覆盖前面的

1
2
3
4
5
6
const o1 = { a: 1, b: 1, c: 1 };
const o2 = { b: 2, c: 2 };
const o3 = { c: 3 };

const obj = Object.assign({}, o1, o2, o3);
console.log(obj); // { a: 1, b: 2, c: 3 }

拷贝对象(浅拷贝)

1
2
3
4
5
6
7
8
const object1 = {
a: 1,
b: 2,
c: 3
};
const object2 = Object.assign({c: 4, d: 5}, object1);
console.log(object1) // { a: 1, b: 2, c: 3 }
console.log(object2) // { c: 3, d: 5, a: 1, b: 2 }

二、创建对象

2.1、对象字面量表示法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
let person1 ={
name:"Liu",
age:21
}
let person2 = {
"first name":"Qiu",
age:24
}
person2 // {first name: 'Qiu', age: 24}

// 错误实例,必须加双引号
let person3 = {
first name: 'Q'
}

显而易见,这种创建对象的方式,会出现大量重复的代码,于是有了下面的构造函数模式

2.2、构造函数模式

1
2
3
4
5
6
7
function Person(name,age){
this.name=name;
this.age=age;
this.sayName = function(){console.log(this.name);}
}
let person1 = new Person('Liu',21);
let person2 = new Person('Qiu',24);
1
2
3
4
5
6
7
8
//使用函数表达式也可以
let Person = function(name,age){
this.name=name;
this.age=age;
this.sayName = function(){console.log(this.name);}
}
let person1 = new Person('Liu',21);
let person2 = new Person; //在实例化时,如果不想传参数,那么构造函数后面的括号可加可不加

按照惯例,构造函数名称的首字母都是要大写的,非构造函数则以小写字母开头

new+构造函数 底层解析

new操作符 + 构造函数 方式的创建过程,后台会执行以下步骤:(Prototype:原型)

(1)在内存中创建一个新对象。(2)这个新对象内部的[[Prototype]]特性被赋值为构造函数的prototype属性。(3)构造函数内部的this被赋值为这个新对象(即this指向新对象)。(4)执行构造函数内部的代码(给新对象添加属性)。

这种方式没有完全解决重复的问题,例如sayName函数其实代码是一样的,但是它会在每个对象中新建一个Function对象,为了解决这个问题,就引出了下面的原型模式

三、读写对象

3.1、读写对象属性

读写对象属性有两种方式:

  • 点语法
  • 中括号,可以使用一些不符合变量命名要求的键
1
2
person.name = "Liu";
person["first name"]="Liu";

3.2、判断对象属性存在与否

  • in判断。in操作符可以检查对象是否具有某个属性(实例上以及原型上)
  • hasOwnProperty()方法。可以检查对象实例上是否具有某个属性
1
2
'name' in person
person.hasOwnProperty('name')

2.3、对象语法(糖)

ES6为定义和操作对象新增了很多语法糖

2.3.1 属性值简写

1
2
3
4
5
let name = "Mike";
let person = {
name //原本应该是name:name的形式,这里简写成name
}
console.log(person); //{name: 'Mike'}

2.3.2 可计算属性

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
const nameKey = 'name';
let person = {
[nameKey]:"Liu" //用中括号包裹变量,作为属性名
}
console.log(person); //{name: 'Liu'}

const nameKey = 'name';
const ageKey = 'age';
let uniqueToken = 0;
function getUniqueKey(key){
return `${key}_${uniqueToken++}`;
}
let person = {
[getUniqueKey(nameKey)]:'Qiu', //用中括号包裹表达式,表达式的运算结果作为属性名
[getUniqueKey(ageKey)]:25
}
console.log(person); //{name_0: 'Qiu', age_1: 25}

2.3.3 简写方法名

1
2
3
let person = {
sayHello(name){console.log(`hello ${name}`);}
}

2.3.4 解构赋值

1
2
3
4
5
6
7
8
9
let person = {
name:'Matt',
age:21
}
let {name:personName,age=11,job="student",grade} = person;
console.log(personName); //Matt
console.log(age); //21
console.log(job); //student
console.log(grade); //undefined

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

1
2
3
4
5
6
let personName,personAge;
let person = {
name:'Matt',
age:21
}
({name:personName,age=personAge} = person);

2.4、遍历对象的内容

2.4.1 for-in循环

需要注意:在for-in循环遍历对象属性时,需要用括号标记法来访问属性值,而不能使用点语法

1
2
3
4
5
myObj =  { "name":"Bill Gates", "age":62, "car":null };
for (x in myObj) {
console.log(myObj[x]);
//console.log(myObj.x); //错误的写法,报错,或者无法获取属性值
}

直接for-in循环会将对象原型链上的属性也遍历出来,如果要避免,可以在循环内部使用hasOwnProperty()方法进行判断

1
2
3
4
5
for(x in obj){
if(obj.hasOwnProperty(x)){
console.log(x)
}
}

2.4.2 keys()

Object.keys()方法可以罗列出对象所有可枚举的属性,

Object.getOwnPropertyNames()方法可以罗列出对象所有属性,不管是否可以枚举

2.4.3 values()

Object.values()返回对象值的数组

2.4.4 entries()

Object.entries()返回键/值对的数组

2.5、定义对象方法

在一个对象中绑定函数,称为这个对象的方法

2.5.1 对象内定义

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
var xiaoming = {
name:"小明",
birth:1998,
age:function(){
var y = new Date().getFullYear();
return y-this.birth; //对象方法的this,为该方法所属的对象,所以this指向xiaoming
}
}
xiaoming.age();

//ES6之后,支持简写方法名
var xiaoming = {
birth:1998,
age(){
var y = new Date().getFullYear()
return y-this.birth
}
}
xiaoming.age()

2.5.2 对象外定义

1
2
3
4
5
6
7
8
9
10
11
12
13
14
//依据:函数的this,指向调用函数的对象
function getAge(){
var y = new Date().getFullYear();
return y-this.birth;
}
var xiaoming = {
name:"小明",
birth:1998,
age:getAge
}
xiaoming.age(); //返回正确结果
getAge(); //NaN,此时this指向window
var fn = xiaoming.age;
fn(); //NaN,此时this指向window

也可以使用apply或call或bind改变函数的this指向

1
2
3
4
5
6
7
8
9
function getAge() {
var y = new Date().getFullYear();
return y - this.birth;
}
var xiaoming = {
name: '小明',
birth: 1998
};
getAge.apply(xiaoming);

2.5.3 对象方法内再定义函数

这是一种特殊情况,内部函数的this并不指向对象,而是指向全局对象window

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
var xiaoming = {
name:"小明",
birth:1998,
age:function(){
function getAge(){
var y = new Date().getFullYear();
console.log(this) //Window
return y-this.birth;
}
return getAge();
}
}
xiaoming.age() //返回NaN,age方法内部定义的函数,this没有指向对象本身。只有在age方法的函数内才指向对象本身

正确的写法:
var xiaoming = {
name:"小明",
birth:1998,
age:function(){
var that = this; //关键一步,获取到对象的this
function getAge(){
var y = new Date().getFullYear();
return y-that.birth;
}
return getAge();
}
}
xiaoming.age()

四、对象原型

转到:《JS进阶(一)原型链》

五、类

在ES6中,class (类)作为对象的模板被引入,可以通过 class 关键字定义类。class 的本质是 function。它可以看作一个语法糖,让对象原型的写法更加清晰、更像面向对象编程的语法。

类的所有方法都定义在类的prototype属性上面,在类的实例上面调用方法,其实就是调用原型上的方法 原型方法可以通过实例对象调用,但不能通过类名调用,会报错

5.1、创建类

5.1.1 类定义

把类表达式赋予一个变量,存在类定义的作用之一,是可以写出立即执行的Class

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
// 匿名类
let Example = class {
constructor(a){
this.a=a
}
}
let e = new Example(1)
console.log(e) //Example {a: 1}

// 命名类
let Example = class E{
constructor(a){
this.a=a
}
}
let e1 = new Example(2) //E {a: 2}
let e2 = new E(3) //报错

// 立即执行的Class
let person = new class {
constructor(name) {
this.name = this.name;
}
sayname(){
console.log(this.name);
}
}("常东东") //这段代码中class是立即执行的实例

5.1.2 类的声明

1
2
3
4
5
class Person{}			//类声明
const Animal = class{} //类表达式

var person1 = new Person()
var person2 = new Person //new的时候,可以不加()
  • 函数声明可以提升,但类定义不能
  • 函数受函数作用域限制,而类受块作用域限制

5.1.3 添加类实例成员

在构造函数内,为实例添加“自有”属性,在构造函数执行后,还可以在外部为实例添加成员

注意,构造函数中的内部变量需要加 this. 前缀。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
class Person{
constructor(){
this.name='Mike';
this.sayName = ()=>console.log(this.name);
}
}
let p1 = new Person();

//构造函数中设置缺省默认值
class Person{
constructor(a=1){
this.a = a
}
}

5.1.4 添加类方法

类中方法不用加 function 关键字

定义在constructor 内的属性和方法 即调用在this上 属于实例属性和方法 否则属于原型属性和方法

1
2
3
4
5
6
7
8
9
10
11
class Person{
constructor(){
//添加到this的内容会保存到每个不同的实例上
this.name='Mike';
this.sayName = ()=>console.log(this.name);
}
//在类块中定义的内容会保存在类的原型上
locate(){
console.log('prototype');
}
}

5.1.5 静态方法

不需要通过实例对象,可以直接通过类来调用的方法,其中的 this 指向类本身。实例中不会出现这个静态方法,也无法调用。静态方法可以被子类继承

1
2
3
4
5
6
7
8
9
class Person{
static say(){console.log('hello')}
}
Person.say() //hello
let p = new Person
console.log(p) //Person {}
p.say() //报错
class Child extends Person{}
Child.say() //hello

5.1.6 类访问器

语法与行为跟普通对象一样

1
2
3
4
5
6
7
class Person{
set name(newName){this.name_ = newName;}
get name(){return this.name_;}
}
let p = new Person();
p.name="Jake";
console.log(p.name);

5.2、类的继承

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
class Student{
constructor(name){
this.name = name;
}
hello(){
return "Hello " + this.name;
}
}
var Liu = new Student('Liu');

//实现继承
class PrimaryStudent extends Student{
constructor(name,grade){
super(name);
this.grade = grade;
}
getGrade(){
return this.grade;
}
}

ECMAScript 6新增特性中最出色的一个就是原生支持了类继承机制。虽然类继承使用的是新语法,但背后依旧使用的是原型链

通过 extends 实现类的继承。如果子类定义了 constructor,必须在它里面调用 super 方法

5.2.1 extends关键字

extends关键字用来实现类的继承,如下

1
2
3
4
class Vehicle{}
class Bus extends Vehicle{}
let b = new Bus();
b instanceof Vehicle //true

派生类都会通过原型链访问到类和原型上定义的方法。this的值会反映调用相应方法的实例或者类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
class Vehicle{
identifyPrototype(id){
console.log(id,this);
}
static identifyClass(id){
console.log(id,this);
}
}
class Bus extends Vehicle{}
let v = new Vehicle();
let b = new Bus;
v.identifyPrototype('vehicle'); //vehicle Vehicle {}
b.identifyPrototype('bus'); //bus Bus {}
Bus.identifyClass('bus');
Vehicle.identifyClass('vehicle');

5.2.2 super()方法

super关键字用于访问和调用 父类上的函数,可以调用父类的构造函数 也可以调用父类的普通函数

如果在子类中定义了constructor方法,必须在里面调用super()

1
2
3
4
5
6
7
8
9
10
class Vehicle{
constructor(){
this.hasEngine=true;
}
}
class Bus{
constructor(){
super(); //调用父类构造函数
}
}
1
2
3
4
5
6
7
8
9
10
11
12
//示例一:在静态方法中可以通过super调用继承的类上定义的静态方法
class Vehicle{
static identify(){
console.log('vehicle');
}
}
class Bus extends Vehicle{
static test(){
super.identify(); //调用父类上定义的静态方法
}
}
Bus.test(); //vehicle
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
//示例二:调用父类的普通函数:
class Father{
constructor(surname){this.surname = surname}
say(){console.log('父级:'+this.surname)}
}
class Child extends Father{
constructor(surname,name){
super(surname)
this.name = name
}
say(){
super.say()
console.log('子级:'+this.name)
}
}
let p = new Child('华师','计算机')
p.say() //父级:华师 子级:计算机

//现在让我们来做一个小改动,将Father的this.surname 改成 this.name,看看会发生什么不同
class Father{
constructor(surname){this.name = surname}
say(){console.log('父级:'+this.name)}
}
class Child extends Father{
constructor(surname,name){
super(surname)
this.name = name
}
say(){
super.say()
console.log('子级:'+this.name)
}
}
let p = new Child('华师','计算机')
p.say() //父级:计算机 子级:计算机
// 分析:super.say()调用,访问this.name时,在当前的执行上下文的变量对象中,首先寻找name,结果找到了Child实例的name,于是直接获取了 ”计算机“

5.3 判断对象属于哪个类

1
2
3
4
5
6
7
class Example{}
let e = new Example

e instanceof Example //true
e.constructor === Example //true
Example.prototype.isPrototypeOf(e) //true
Object.getPrototypeOf(e) === Example.prototype //true

参考文章

es6之(class)类的用法 - 掘金 (juejin.cn)

[4.3 ES6 Class 类 | 菜鸟教程 (runoob.com)](https://www.runoob.com/w3cnote/es6-class.html#:~:text=在ES6中,class (类)作为对象的模板被引入,可以通过,class 关键字定义类。)


JavaScript(四)面向对象
http://timegogo.top/2022/05/13/JavaScript/JavaScript(四)面向对象/
作者
丘智聪
发布于
2022年5月13日
更新于
2023年7月16日
许可协议