第八章 对象、类与面向对象编程

对象是一组属性的无序集合

理解对象

可以通过Object的实例创建,或者直接通过字面量创建

// 通过Object构造函数创建
let person = new Object();
person.name = 'zou';
person.age = 18;
person.sayName = function() {
    console.log(this.name);
};

// 通过字面量创建对象
let person = {
    name: "zou";
    age: 18;
    sayName () {
        console.log(this.name);
    }
}

属性的类型

数据属性

保存值的位置

有4个默认特性

configurable:是否可以铜鼓odelete删除并重新定义

enumerable:是否可以通过for-in循环返回

writable:是否可以被修改

value:包含属性实际的值

想要修改数据属性的默认特性,必须使用Object.defineproperty()方法

let person = {}; 
Object.defineProperty(person, "name", { 
 writable: false, 
 value: "Nicholas" 
}); 
console.log(person.name); // "Nicholas" 
person.name = "Greg"; 
console.log(person.name); // "Nicholas"

访问器属性

一个函数,用来描述对象的属性读取和设置行为

有4个默认特性:

configurable:是否可以通过delete删除并重新定义

enumerable:是否可以铜鼓for-in循环返回

get:获取函数,在读取属性时调用,默认值为undefined

set:设置函数,在写入属性时调用,默认值为undefined

同样,访问器属性不能直接定义,必须使用Object.defineProperty()

合并对象

ES6提供了Object.assign()方法进行合并对象,这个方法接受一个目标对象和一个或者多个源对象作为参数,然后将每个源对象中

// 目标对象
dest = { name: 'dest'};
// 源对象
src = { id: 'src'};

result = Object.assign(dest,src);
console.log(dest); //{name: 'dest', id: 'src'}
console.log(src); //{id: 'src'}
console.log(result); ////{name: 'dest', id: 'src'}

对象标识及相等判定

Object.is():功能与===相似,但是能够准确判断+0、-0、0、NaN的情况(使用isNaN()也可以判断)

增强的对象语法

属性简写

属性名与变量名一致时,可以只写变量名

let name = 'Matt'; 

let person = { 
 name: name 
}; 
// 上述内容可以简写 
let person = {
    name
};

console.log(person); // { name: 'Matt' }

可计算属性

简写方法名

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

// 上述方法可以简写成
let person = {
    sayName(name) {
        console.log(`My name is ${name}`);
    }
};

对象解构

使用与对象匹配的结构来实现对象属性的赋值

let person = {
    name: "matt",
    age: 24
}
let (name,age,job='Software engineer') = person;
console.log(name); // "matt"
console.log(age); // 24
console.log(job); // 'Software engineer' 如果没有定义默认值则会输出undefined

1、嵌套解构

2、部分解构

3、参数上下文匹配

创建对象

工厂模式

是一种设计模式,广泛应用于软件工程领域,用于抽象创建特定对象的过程。

function createPerson(name, age, job) { 
 let o = new Object(); 
 o.name = name; 
 o.age = age; 
 o.job = job; 
 o.sayName = function() { 
 console.log(this.name); 
 }; 
 return o; 
} 
let person1 = createPerson("Nicholas", 29, "Software Engineer"); 
let person2 = createPerson("Greg", 27, "Doctor"); 

通过上述方法,可以用不同的参数多次调用这个函数,每次都会返回包含不同属性值的对象,这种工厂模式可以解决创建多个类似对象的问题,但是没有解决对象标识问题。

构造函数模式

// 上述的例子可以使用构造函数
function Person(name,age,job) {
    this.name = name;
    this.age = age;
    this.job = job;
    this.sayName = function() {
        console.log(this.name);
    };
}

// 构造函数不一定要写成函数声明的形式,赋值给变量的函数表达式也可以表示构造函数
let Person = function(name,age,job) {
    this.name = name;
    this.age = age;
    this.job = job;
    this.sayName = function() {
        console.log(this.name);
    };
}

let person1 = new Person("Nicholas", 29, "Software Engineer"); 
let person2 = new Person("Greg", 27, "Doctor"); 
person1.sayName(); // Nicholas 
person2.sayName(); // Greg 

创建Person实例,使用了new操作符,以这种方式调用构造函数会执行如下操作:

1、在内存中创建一个新对象

2、新对象的_proto_属性指向构造函数的prototype属性

3、构造函数内部this指向新对象。

4、执行构造函数内部代码

5、构造函数返回新对象。如果手动return,则返回return的内容。

原型模式

每个函数都有一个prototype属性,这个属性是一个对象,包含一些属性和方法。它就是通过调用构造函数创建的对象的==原型==,使用原型的好处就是在它上面定义的属性和方法可以被对象实例共享。

Person.prototype.name = "Nicholas";
Person.prototype.age = 29;
Person.prototype.job = "software Engineer";
let person = new Person();
console.log(person.name); // "Nicholas"

prototype会自动获得一个constructor属性,指向创建函数的构造函数。

也就是(构造函数有一个prototype属性,也是原型对象,这个原型对象也有一个constructor属性,指向构造函数)

Person.prototype.constructor === Person; // true

console.log(Person.prototype.__proto__ === Object.prototype); // true 
console.log(Person.prototype.__proto__.constructor === Object); // true 
console.log(Person.prototype.__proto__.__proto__ === null); // true 

对象:_proto_和constructor

函数:prototype(原型属性)、_proto_、constructor(因为函数也是对象,所以也有上面两个属性)

_proto_作用:访问一个对象的属性时,如果该对象内部不存在这个属性时,那么就会去它的_proto_属性所指向的那个对象查找。因此是从对象——对象。

__proto__

prototype:从函数——对象,作用:让该函数所实例化的对象都可以找到公用的属性和方法。

prototype属性

constructor:对象——函数,含义:指向该对象的构造函数。

constructor属性

hasOwnProperty(“name”); // 判断实例上是否有name这个属性 (如果只在原型上存在但是实例没有则显示false)

console.log(“name” in person1); // true (不管name在实例还是原型上,只要存在,使用in返回的结果都是true)

for in 可以循环实例对象所有可枚举属性,包括原型对象的属性。Object.keys()只会返回实例对象上的可枚举属性,Object.getOwnPropertyNames()可以返回所有实例属性,包括不可枚举属性。

对象迭代

Object.values():接收一个对象,返回对象值的数组

Object.entries():接收一个对象,返回键/值对的数组

const o = {
    foo: 'bar',
    baz: 1,
    qux: {}
};
console.log(Object.values(o));
// ["bar",1,{}]
console.log(Object.entries(o));
// [["foo","bar"],["baz",1],["qux",{}]]

继承

每个构造函数都有一个原型对象,原型有个属性指回构造函数。个人理解就是因为js不是面向对象的语言,没有什么public、privacy等属性,那么如果是函数中的公用部分就放入原型对象中,使得通过构造函数得到的实例可以共用原型对象上的属性和方法,不能共用的就单独放到实例的属性和方法中。

原型链

ES中把原型链作为ES的主要继承方式,基本思想就是通过原型链继承多个引用类型的属性和方法。

function SuperType() {
    this.property = true;
}
SuperType.prototype.getSuperValue = function() {
    return this.property;
};

function SubType() {
    this.subproperty = false;
}
SubType.prototype.getSubValue = function() {
    return this.subproperty;
}
// 继承SuperType
SubType.prototype = new SuperType();

let instance = new SubType();
console.log(instace.getSuperValue()); // true

1、默认原型

默认情况下,所有引用类型都继承自Object,这也是通过原型链实现的。

2、原型与继承关系

instanceof操作符:如果一个实例的原型链中出现过相应的构造函数,则返回true

console.log(instance instanceof Object); // true 
console.log(instance instanceof SuperType); // true 
console.log(instance instanceof SubType); // true 

isPrototypeOf方法:原型链中每个原型都可以调用这个方法,只要原型链中包含这个原型,就会返回true。

console.log(Object.prototype.isPrototypeOf(instance)); // true 
console.log(SuperType.prototype.isPrototypeOf(instance)); // true 
console.log(SubType.prototype.isPrototypeOf(instance)); // true

原型链的问题:

原型中包含引用值的时候,会在所有的实例间共享,这也就是为什么属性通常会在构造函数中定义而不会在原型上的原因。

function SuperType() {
    this.colors = ['red','blue','green'];
}
function SubType() {}
// 继承SuperType
SubType.prototype =new SuperType();

let instance1 = new SubType();
instance1.colors.push("black");
console.log(instance1.colors); // "red,blue,green,black"

let instance2 = new SubType();
console.log(instance2.colors); // "red,blue,green,black"

在实例instance1中修改了colors导致所有的实例的colors都发生了改变,就是原型上的属性是所有实例所共享的,也都是可以进行修改的。

盗用构造函数

为了解决原型包含引用值导致的继承问题。

基本思路:在子类构造函数中调用父类构造函数,使用apply和call方法以新创建的对象为上下文执行构造函数。

function SuperType() {
    this.colors = ["red","blue","green"];
}
function SubType() {
    // 继承SuperType
    SuperType.call(this);
}
let instance1 = new SubType();
instance1.colors.push("black");
console.log(instance1.colors); // "red,blue,green,black"

let instance2 = new SubType();
console.log(instance2.colors); // "red,blue,green"

优点:可以在子类构造函数中向父类构造函数传参。

缺点:必须在构造函数中定义方法,因此函数不能重用,此外,子类也不能访问父类原型上定义的方法。因此所有类型只能使用构造函数模式。

组合继承

使用原型链继承原型上的属性和方法,通过盗用构造函数继承实例属性。

function SuperType(name) {
    this.name = name;
    this.colors = ["red","blue","green"];
}
SuperType.prototype.sayName = function() {
    console.log(this.name);
};
function SubType(name,age){
    // 继承属性
    SuperType.call(this,name);
    this.age = age;
}

// 继承方法
SubType.prototype = new SuperType();
SubType.prototype.sayAge = function(){
    console.log(this.age);
};
let instance1 = new SubType("Nicholas",29);
instance1.colors.push("black");
console.log(instace1.colors); // "red,blue,green,black"
instance1.sayName(); // "Nicholas"
instance1.sayAge(); // 29

let instance2 = new SubType("Greg",27);
console.log(instance2.colors); // "red,blue,green"
instance2.sayName(); // Greg
instance2.sayAge(); // 27

原型式继承

let person = { 
 name: "Nicholas", 
 friends: ["Shelby", "Court", "Van"] 
}; 
let anotherPerson = object(person); 
anotherPerson.name = "Greg"; 
anotherPerson.friends.push("Rob"); 
let yetAnotherPerson = object(person); 
yetAnotherPerson.name = "Linda"; 
yetAnotherPerson.friends.push("Barbie"); 
console.log(person.friends); // "Shelby,Court,Van,Rob,Barbie"

寄生式继承

ES6新引入了class关键字具有正式定义类的能力

类定义

// 类声明
class Person{}
// 类表达式
const Animal = class{};

类没有提升,受块作用域限制,类可以包含构造函数方法、实例方法、获取函数、设置函数和静态类方法。

    // 空类定义,有效
    class Foo {}
    // 有构造函数的类,有效
    class Bar {
      constructor() {}
    }
    // 有获取函数的类,有效
    class Baz {
      get myBaz() {}
    }
    // 有静态方法的类,有效
    class Qux {
      static myQux() {}
    }

类构造函数

1、实例化

使用new调用类的构造函数会执行如下操作:

1)内存中创建一个新对象

2)这个新对象内部的_proto_ 指针被赋值为构造函数的prototype属性

3)构造函数内部的this被赋值为这个新对象(即this指向新对象)

4)执行构造函数内部的代码(给新对象添加属性)

5)如果构造函数返回非空对象,则返回该对象;否则,返回刚创建的新对象

类构造函数与构造函数的主要区别是,调用类构造函数必须使用 new 操作符。而普通构造函数如果 不使用 new 调用,那么就会以全局的 this(通常是 window)作为内部对象。调用类构造函数时如果 忘了使用 new 则会抛出错误:

function Person() {} 
class Animal {} 
// 把 window 作为 this 来构建实例
let p = Person(); 
let a = Animal(); 
// TypeError: class constructor Animal cannot be invoked without 'new'

实例、原型和类成员

可以在类上定义静态方法,每个类只能定义一个静态方法,在静态方法中,this指向类自身。静态方法将直接加在类身上,而普通方法则加在类的原型上,静态方法适合作为实例工厂。

class Person {
    constructor(age) {
        this.age_ = age;
    }	
    sayAge() {
        console.log(this.age_);
    }
    static create() {
        // 使用随机年龄创建一个Person实例
        return new Person(Math.floor(Math.random() * 100));
    }
}
console.log(Person.create()); // Person {age_: ... }

类内部不支持添加原型或者成员数据,但是外部可以

Person.prototype.name = "Linda";
Person.name = 'Jack';

类的继承

类可以继承类,也可以继承构造函数,使用extends

// 继承类
class Vehicle {}
class Bus extends Vehicle {}
let b = new Bus();
console.log(b instanceof Bus) // true
console.log(b instanceof Vehicle) // true
// 继承普通构造函数
function Person() {}
class Engineer extends Person {}
let e = new Engineer();
console.log(e instanceof Engineer) // true
console.log(e instanceof Person) // true

派生类可以通过super调用父类的构造函数

class Vehicle {
    constructor() {
        this.hasEngine = true;
    }
    static identify() {
        console.log('vehicle')
    }
}
class Bus extends Vehicle {
    constructor() {
        // 不要在调用super之前引用this,否则会抛出ReferenceError
        super(); // 相当于super.constructor()
        console.log(this instanceof Vehicle) // true
        console.log(this) // Bus {hasEngine: true}
    }
    // 静态方法中可以通过super调用继承父类的静态方法
    static identify() {
        super.identify()
    }
}
new Bus()
Bus.identify() // vehicle
  • 没有定义构造函数会自动调用super(),定义了则必须手动调用super()或返回一个对象。

抽象基类:只能被继承,不能被实例化