《JavaScript: The Definitive Guide》【知识体系】- 01. 面向对象

177 阅读9分钟

【知识体系】是个人学习JS基础总结的前端知识体系,目的是由点及面的关联知识点,更好更深入地理解,灵活运用。文章尽可能写的简单、清晰,把某一块的知识体系讲清楚。

🐧 前情提要

阅读本章节你会了解到以下内容:

  1. 理解面向对象
  2. 构建对象
  3. 理解原型对象、构造函数、实例对象、原型链
  4. 继承的演变

1. 理解面向对象

要想更好的理解面向对象,知识体系学习和构建的过程,可以参考前情提要中的顺序。首先,理解提到的知识点的概念和关系,从构建对象开始,工厂模式、构造函数模式、原型模式。这些方式都是在解决一些问题,包括功能代码的复用、明确对象继承自哪里、共享对象的属性和方法等;其次,理解原型对象、构造函数、实例对象三者的关系,以及它们所形成的原型链;最后,理解面向对象编程中继承、类的实现方式。

以上,就能对面向对象有更深入的理解,主要去了解编程思想,在实际运用时能够更加灵活的应对不同的编程场景,使代码写的更巧妙、高效。

2. 构建对象

创建对象的方式有,对象字面量、构造函数、Object.create()

若想构建较复杂的对象,有属性和方法,且希望能够复用多次,这时为了避免重复创建多次,引出了面向对象编程思想。即以对象为基础单元,封装可复用的属性和方法,实现创建每个实例对象都可继承部分原型对象的功能。以下,介绍几种在 ES6 类还未出现时,利用构造函数、原型模拟面向对象编程方式...

(1)工厂模式

  • 实现:声明一个普通函数,其功能是构建对象,包含一些属性和方法。函数调用时就是在创建具体对象的过程。
  • 优点:解决对象复用问题,属性和方法都实现了继承
  • 缺点:无法确定新创建的对象具体继承自谁,没有实现类的概念
function createPerson(name, age, job) { 
    let obj = new Object() 

    obj.name = name 
    obj.age = age 
    obj.job = job

    obj.getName = function() { 
       console.log(this.name) 
    } 
    return obj 
} 
    
let nick = createPerson('Nick', 28, 'painter') 
let phoebe = createPerson('Phoebe', 27, 'singer')

(2)构造函数模式

构造函数模式,为了解决工厂模式的问题,即有类的概念。

  • 实现:构造函数和普通函数最大的区别在于,new 调用构造函数,构造函数名需要大写。new 调用构造函数的过程就是创建实例对象的过程
  • 优点:解决对象复用问题,同时明确了实例对象是继承自谁的问题
  • 缺点:实例对象未共享构造函数的方法(每创建一个实例对象,构造函数的方法都会重新创建一遍)
function Person(name, age, job) {
    this.name = name 
    this.age = age
    this.job = job
    
    this.sayName = function() {
        console.log(this.name)
    }
}

let person1 = new Person('Nick', 28, 'painter')
let person2 = new Person('Phoebe', 27, 'singer')

(3)原型模式

原型模式,是在工厂模式和构造函数模式基础上提出来的。为了解决构造函数方法共享的问题。

  • 实现:利用构造函数的 prototype 属性,来共享属性和方法
  • 注意:用对象字面量方式添加属性和方法,需要手动设置 constructor
function Person() {}

Person.prototype = {
    constructor: Person, 
    name: 'nick',
    age: 28,
    sayName: function() {
        console.log(this.name)
    }
}

let person1 = new Person('Nick', 28)
let person2 = new Person('Phoebe', 27)

(4)小结 - 最佳实践

了解了以上三种构建对象的方式,最佳实践利用构造函数模式 + 原型模式,即希望共享的属性和方法,定义在构造函数的 prototype 属性上,私有的属性和方法定义在构造函数上

// 写法一 
function Person(name) {
    this.name = name
}

Person.prototype.sayName = function() {
    console.log(this.name)
}

// 写法二 对象字面量(需要手动设置构造函数)
function Person(name) {
    this.name = name
}

Person.prototype = {
    constructor: Person, 
    name = ''
    sayName: function() {
            console.log(this.name)
    }
}

let person1 = new Person('Nick')
let person2 = new Person('Phoebe')

3. 理解原型对象、构造函数、实例对象、原型链

(1)原型对象、构造函数、实例对象的关系

学习之后的内容时,可以根据这张图所描绘的三者的关系理解,所以放在最前面了。

Untitled.png

(2)构造函数

构造函数是封装属性和方法的函数,通过 new 来调用创建实例对象。

function Person(name, age) {
    this.name = name
    this.age = age
    
    this.sayName = function() {
        console.log(this.name)
    }
}

var person1 = new Person('Phoebe', 27)

(3)原型对象

原型对象,即构造函数 prototype 属性的引用。原型对象解决了构造函数中定义属性和方法无法共享的问题。

Person.prototype.sayName = function() {
    console.log(this.name)
}

// Person 原型对象中有 constructor 属性,指向构造函数本身,即 Person
Person.prototype.constructor = Person

(4)实例对象

实例对象,是通过调用构造函数创建出来的具体的对象。继承自构造函数的原型对象,同样也继承自 Object 对象。实例对象的 __ proto__ 属性等于构造函数的原型对象,但目前已废弃。

let p = new Person('Phoebe', 27)

p.__proto__ = Person.prototype

(5)原型链 —— 三者间的关系链

原型链是描述原型对象、构造函数、实例对象三者之间关系的关系链。查找对象的属性和方法,先从实例对象中找,没有再去原型对象中找,逐层往原型对象的原型对象查找。若实例对象和原型对象中存在同名的属性或方法时,优先返回实例对象的。

同时,串起原型链的是任何一个对象都有的 __ proto__ 属性。

以下列举下原型链中涉及到的等式:

  • 构造函数的原型对象
    • Person.prototype
  • 构造函数的原型对象的 constrcutor 属性,指向构造函数本身
    • Person.prototype.constructor = Person
  • 对象的 __ proto__ 属性,指向构造函数的原型对象
    • person.__ proto__ = Person.prototype
// 构造函数
fucntion Person() {}   

// 实例对象 p
var p = new Person()

// 构造函数的原型对象
Person.prototype 

// 构造函数的原型对象的 constructor 属性,指向构造函数本身
Person.prototype.constructor = Person

------------------------------------------------------

* 对象的 __proto__ 属性,指向构造函数的原型对象

// 实例对象的 __proto__ 属性,指向构造函数 Person 的原型对象
person.__proto__ = Person.prototype 

// 构造函数本身是函数,函数也是对象,构造函数也有 __proto__ 属性,指向 Function 的原型对象
// 所有函数的 __proto__ 属性,都指向 Function 的原型对象,构造函数是 Function 实例
Person.__proto__ = Function.prototype

// Person 原型对象的 __proto__ 属性,指向 Object 的原型对象
Person.prototype.__proto__ = Object.prototpye  

// Object 原型对象的 __proto__ 属性,指向 null
Object.prototype.__proto__ = null

4. 继承的演变

继承跟上面提到的构建对象有很多相似的地方,在 ES6 类的概念被提出之前,是在不断的模拟和优化实现类的过程。提出一种继承方式,解决某个问题,分析这种方式的优缺点,再提出另一种优化后方式...

下面介绍继承的实现方式:

  • 原型链继承
  • 构造函数继承
  • 组合继承(原型链 + 构造函数)
  • 寄生组合继承
  • 类继承

(1)原型链继承

  • 原理:每个构造函数都有原型对象 Person.prototype;原型对象的属性还指回构造函数自身 Person.prototype.constructor = Person;实例对象有一个属性指向原型对象 p.__ proto__ = Person.prototype;原型对象的属性还指向另一个原型对象 Person.__ proto__ = Function.prototype 且所有对象都继承自 Object.prototype
  • 实现:子类的原型对象,指向父类的实例,形成原型链间关系实现继承(不太理解的话,可以参看之前关系图)
    • Child.prototype.__ proto__ === Parent.prototype
  • 优点:利用原型对象共享属性和方法
  • 缺点:某个实例对象修改了原型对象共享的属性,其他实例对象继承的属性,也会随之改变;此外,子类实例化时无法向父类传参
function Father() {
    // 私有属性
    this.name = 'Nick'
    this.hobbies = ['reading', 'swimming']
}
// 共享方法
Father.prototype.getName = function() {
    console.log(this.name)
}

Father.prototype.getHobbies = function() {
    console.log(this.hobbies)
}

function Child(name, age) {
    this.name = name
    this.age = age
}

// 实现继承
Child.prototype = new Father()

let cc1 = new Child('Phoebe', 27)
let cc2 = new Child('Nick', 28)

cc1.hobbies.push('singing')
console.log(cc1.getHobbies())  // ['reading', 'swimming', 'singing']
console.log(cc2.getHobbies())  // ['reading', 'swimming', 'singing']

(2)构造函数继承

  • 实现:定义子类构造函数时,将子类构造函数的 this 值绑定到弗雷构造函数上。子类在被调用创建实例对象时,调用上下文可以访问到父类的属性和方法了,进行初始化
  • 优点:解决了原型链继承方式中所有实例对象共享同一个原型对象属性和方法,及子类实例化时可以给父类传参
  • 缺点:每次实例化都会重新创建构造函数的方法,未达到复用的目的;子类无法访问父类原型上的属性和方法,因为共享特性被切断了
function Parent(name) {
    this.name = name
    this.getName = function() {
        console.log(this.name)
    }
}

function Child(name, age) {
    Parent.call(this, name)  // 实现继承
    this.age = age
}

let c = new Child('Phoebe', 27)

(3)组合继承(原型链 + 构造函数)

  • 实现:结合俩者的优势,利用原型链继承原型上的属性和方法,目的是共享复用;利用构造函数继承让实例都有自己的属性,且可以传参给父类
  • 优点:解决以上两种方式的问题
  • 缺点:创建子类实例时会调用两次父类构造函数,一次是创建子类原型时,一次是子类构造函数被调用时
function Parent(name) {
    this.name = name
}

Parent.prototype.getName = function() {
    console.log(this.name)
}

function Child(name, age) {
    Parent.call(this, name) // 实现继承
    this.age = age
}

Child.prototype = new Parent() // 实现继承
Child.prototype.constructor = Child

let cc = new Child('Phoebe', 27)

(4)寄生组合继承

  • 实现:先创建一个父类原型的副本,将副本赋值给子类的原型,最后手动修改子类的 constructor 属性,指回自身,因为重写原型时导致的 constructor 丢失
  • 优点:寄生组合继承解决了调用两次父类构造函数的问题
function Parent(name) {
    this.name = name
}

Parent.prototype.getName = function() {
    console.log(this.name)
}

function Child(name, age) {
    Parent.call(this, name) // 实现继承
    this.age = age
}

// 实现继承
function inheritPrototype(child, parent) {
    child.prototype = Object.create(parent.prototype)
    child.prototype.constructor = child
}

inheritPrototype(Child, Parent)

let c = new Child('Phoebe', 27)

(5)类继承

终于讲到目前实现继承最常用的方式了,类。但本质上还是在原型链和构造函数继承的基础上封装而来的,做了些优化,实现思路还是一样的。所以,了解 JS 继承演变的过程,能更深入的理解其原理,而知识原理才是最值得关注的内核东西。

class Parent {
    constructor(name) {
         this.name = name
    }
    getName() {
         console.log(this.name)
    }
}

class Child extends Parent {
    constructor(name, age) {
        super(name)
        this.name = name
        this.age = age
    }
    getAge() {
        console.log(this.age)
    }
}

let cc = new Child('Phoebe', 27)
cc.getName()