1-2 面向对象编程/原型与原型链

42 阅读7分钟

面向对象编程

面向对象编程:Object-Oriented Programming,程序的主要组织单位是对象

在 JS 中的对象定义为:无序属性的集合,其属性可以为基本值、对象或函数

行话:单个物体的抽象

面向过程编程

在面向过程编程中,程序的主要组织单位是函数。函数接收输入(参数),经过一系列处理,产生输出(返回值)。面向过程编程通常将问题分解为一系列的步骤,每个步骤由一个函数实现

函数式编程

鼓励使用纯函数(Pure Functions),即对于相同的输入,始终产生相同的输出,并且没有副作用(没有改变外部状态的行为)

对象

基础概念

对象由一组属性组成,每个属性都包括一个键(字符串或 Symbol)和一个值(任意数据类型)。

属性的键是唯一的,不同属性之间用逗号分隔。属性的值可以是任何数据类型,包括基本数据类型和其他对象。

对象创建

Object

const obj = new Object()
obj.name = 'xx'
obj.age = '23'
obj.sayAge = function() { 
  console.log(this.age) 
}

字面量

const obj = {
  name: 'xx',
  age: '23',
  sayAge(){
    console.log(this.age)
  }
}

Object.create()

创建一个新对象,该对象的原型(即__proto__属性)指向传入的参数对象

const obj = Object.create({})

// create 实现原理
Object.create = (_obj) => {
  if(typeof _obj !== 'object'){
    return {}
  }
  
  // 创建一个空函数
  function F() {}
  // 将空函数的原型设置为传入的proto对象
  F.prototype = _obj
  // 返回一个新对象,该对象的原型指向传入的proto对象
  return new F()
}

对象存储

对象的存储为:对象的内容是存储在堆中,变量在栈中存储对象的引用

构造函数

用来创建对象的特殊函数,通常以大写字母开头。

使用 new 关键字调用构造函数可以创建新对象,并将构造函数内部的属性和方法添加到新对象上。

function Person (age){
  this.age = age
}

const my = new Person(29)

console.log(my.age) // 29

new 做的事情

  1. 在堆里面建一个新的空对象
  2. 将这个新对象的__proto__指向构造函数的prototype,以便实例可以继承构造函数原型上的属性和方法。--可用Object.setPrototypeOf(obj, prototype)
  3. 执行构造函数,其中 this 关键字指向新创建的空对象上,这样构造函数内部的代码可以操作这个新对象。--可用call()
  4. 如果构造函数没有显式返回一个对象,那么会返回这个新对象的引用地址

工厂模式

在工厂模式中,不直接调用构造函数来创建对象,而是使用一个工厂函数(或者方法)来创建对象。

这种模式封装了对象的创建过程(不让外部感知),使得代码更具灵活性和可维护性

function Person (...args) {
  // 判断 this 是否为实例
  // 是:表明不是函数了,已经 new,所以执行下面的 this.xx = args[x]
  // 否:表明当前还是函数,未 new
  const _isClass = this instanceof Person

  if(!_isClass) return new Person(...args)
  
  this.name = args[0]
  this.age = args[1]
}
// Person 使用
const myself1 = Person('zhangsan', 58)
console.log(myself1) // { name: 'zhangsan', age: 58 }

const myself2 = new Person('lisi', 69)
console.log(myself2) // { name: 'lisi', age: 69 }

Person 函数既可以被当作构造函数使用(通过 new 关键字调用),也可以被当作工厂函数使用(直接调用)

单例模式

是一种设计模式,它确保一个类只有一个实例,并提供一个全局访问点以访问该实例。

常用于:路由、全局状态等

function Person (...args) {
  // 判断是否已经存在实例
	if(typeof Person.instance === 'object'){
    return Person.instance
  }

  // 正常的构造逻辑
  this.name = args[0]
  this.age = args[1]

  // 将实例保存在静态属性中
  Person.instance = this
}
// Person 使用
const myself1 = new Person('xxx', 12)
const myself2 = new Person('zzz', 21)

console.log(myself1 === myself2) // true,因为它们是同一个实例

原型与原型链

原型

对象的原型指的就是__proto__属性,但它不是标准的 JavaScript API,不建议直接使用

原型链

当在对象上面找不到属性时,就会通过__proto__属性一层层网上找,这就是原型链

最终找不到就返回 undefined

function Parent(name){
  this.name = name
}
Parent.prototype.sayName = function(){
  console.log(`my name is ${this.name}`)
}

const obj = new Parent('张三')

obj.getName() // my name is 张三

继承

通过构造函数的原型对象来实现实例的继承

普通继承

将父类实例作为子构造函数的原型对象

function Parent(...args){
  this.address = '成都'
  this.name = '张三'
  this.like = ['钓鱼', '洗碗']
  this.args0 = args[0]
}
Parent.prototype.sayName = function(){
  console.log(`my name is ${this.name}`)
}
function Child(){}

// 重写原型对象---start
Child.prototype = new Parent()
Child.prototype.constructor = Child // 重写构造函数
// 重写原型对象---end

const child1 = new Child()
const child2 = new Child()

child1.sayName() // my name is 张三
child2.like.push('喝酒') // 通过 child2 去改 like
child1.like  // ['钓鱼', '洗碗', '喝酒'],child1 的 like 也会被改

优点

  • 完成基础的继承功能:子类实例将会完全继承父类实例的属性、原型对象

缺点

  • 父构造函数将不支持传参
  • 子类实例的原型对象是共享的,那如果直接改原型对象的值后,影响所有的子类实例

构造函数继承(经典继承)

在子构造函数中调用父构造函数来实现继承

function Parent(...args){
  this.address = '成都'
  this.name = '张三'
  this.like = ['钓鱼', '洗碗']
  this.args0 = args[0]
}
Parent.prototype.sayName = function(){
  console.log(`my name is ${this.name}`)
}
function Child(...args){
  // 调用父构造函数,将其属性继承到子类实例上
  Parent.call(this, ...args)
}

const child1 = new Child()
const child2 = new Child()

child2.like.push('喝酒')
child2.like  // ['钓鱼', '洗碗', 喝酒']
child1.like  // ['钓鱼', '洗碗']

child1.sayName() // 报错:child1.sayName is not a function

优点

  • 父构造函数将支持传递参数
  • 子类实例的原型不会共享,避免了原型继承中的共享问题。

缺点

  • 子类实例将无法继承父构造函数的prototype属性

组合继承

结合普通继承与经典继承,完全弥补这两个继承的缺点

function Parent(...args){
  this.address = '成都'
  this.name = '张三'
  this.like = ['钓鱼', '洗碗']
  this.args0 = args[0]
}
Parent.prototype.sayName = function(){
  console.log(`my name is ${this.name}`)
}
function Child(...args){
  // 调用父构造函数,将其属性继承到子类实例上
  Parent.call(this, ...args)
}

Child.prototype = new Parent()
Child.prototype.constuctor = Child // constuctor 修正


const child1 = new Child()
const child2 = new Child()

child2.like.push('喝酒')
child2.like  // ['钓鱼', '洗碗', 喝酒']
child1.like  // ['钓鱼', '洗碗']

child1.sayName() // my name is 张三

优点

  • 子类实例将能继承父构造函数的prototype属性

缺点

  • 会调用两次父构造函数
    • Child.prototype = new Parent()
    • new Child()
  • 原型对象上多了不必要的属性
    • 因为Child.prototype = new Parent(); 这行代码会创建一个父类的实例,所以子类的原型对象上会多出一些不必要的属性,尽管它们在子类的构造函数中被覆盖了。

寄生组合继承

基于组合继承,解决两次调用父类构造函数问题。

function Parent(...args){
  this.address = '成都'
  this.name = '张三'
  this.like = ['钓鱼', '洗碗']
  this.args0 = args[0]
}
Parent.prototype.sayName = function(){
  console.log(`my name is ${this.name}`)
}
function Child(...args){
  // 调用父构造函数,将其属性继承到子类实例上
  Parent.call(this, ...args)
}

// 手动将子构造函数的 prototype 指向 父构造函数的 prototype
Child.prototype = Object.create(Parent.prototype)
Child.prototype.constuctor = Child // constuctor 修正


const child1 = new Child()
const child2 = new Child()

child2.like.push('喝酒')
child2.like  // ['钓鱼', '洗碗', 喝酒']
child1.like  // ['钓鱼', '洗碗']

child1.sayName() // my name is 张三

寄生组合继承其实就是ES6的 class Child extends ParentES5代码

多重继承

指的是一个类(或对象)同时继承了多个父类(或对象),从而可以拥有多个父类的属性和方法

function Parent1(...args){
  this.address1 = '成都'
  this.name1 = '张三'
  this.like1 = ['钓鱼', '洗碗']
  this.args10 = args[0]
}
Parent1.prototype.sayName = function(){
  console.log(`my name is ${this.name}`)
}
function Parent2(...args){
  this.address2 = '上海'
  this.name2 = '李四'
  this.like2 = ['游泳']
  this.args20 = args[0]
}
Parent2.prototype.sayLike = function(){
  console.log(`my like is ${this.like}`)
}
function Child(...args){
  // 调用父1构造函数,将其属性继承到子类实例上
  Parent1.call(this, ...args)

  
  // 调用父2构造函数,将其属性继承到子类实例上
  Parent2.call(this, ...args)
}

// 手动将子构造函数的 prototype 指向 父构造函数的 prototype
Child.prototype = Object.create(Object.assign(Parent1.prototype, Parent2.prototype))

Child.prototype.constuctor = Child // constuctor 修正

const child1 = new Child()
const child2 = new Child()

child2.like.push('喝酒')
child2.like  // ['钓鱼', '洗碗', 喝酒']
child1.like  // ['钓鱼', '洗碗']

child1.sayName() // my name is 张三

其他补充知识

in、hasOwnProperty、instanceof

  1. in:检查属性是否在对象上(自身以及原型链上)
    1. "name" in my // true || false
  1. hasOwnProperty:检查属性是否在对象上(仅自身不涉及原型链上)
    1. my.hasOwnProperty('name') // true || false
  1. instanceof:检查对象是否属于某个构造函数的实例
    1. my instanceof Object // true || false
    2. 实现原理:检查对象的原型链上是否包含构造函数的 prototype 属性,即判断 实例.proto===构造函数.prototype

对象分类

对象分为 2 类:实例对象、函数对象

实例对象:通过 [new 构造函数()] 生成的

函数对象:通过 [new Function()] 生成的

  1. 每个对象(包含函数)都有__proto__属性,其指向等于其构造函数的prototype指向
  2. 每个函数都有 prototype 属性,指向一个普通对象,该对象具有__proto__constructor属性
    1. __proto__ 指向等于其构造函数(Object)的prototype指向
    2. constructor指向函数本身

图解:实例、构造函数、Function、Object、null 的关系

person.__proto__===Person.prototype // true === person instanceof Person(true)
Person.__proto__===Function.prototype // true === Person instanceof Function(true)
Function.__proto__===Function.prototype // true === Function instanceof Function(true)
Object.__proto__===Function.prototype // true === Object instanceof Function(true)
Object.__proto__.__proto__===Object.prototype // true === Object.__proto__ instanceof Object(true)
Function.__proto__===Object.prototype // false !== Function instanceof Object(true)

Object 与 Function 的关系

Function 与 Object 的__proto__都指向同一个原型对象(Function.prototype)

Object.__proto__ === Function.__proto__ === Function.prototype // true
Object instanceof Function // true,表明 Object 是 Function 的实例
Function instanceof Object // true,表明 Function 是 Object 的实例
Function instanceof Function // true,表明 Function 是 Function 的实例
Object instanceof Object // true,表明 Object 是 Object 的实例

更改对象的原型

  1. 粗暴(不推荐):obj.proto===newObj;
  2. 优雅(推荐):Object.setPrototypeOf(obj, newObj) 等价于 1
  3. 到位:const obj = Object.create(newObj) 等价于两步 const obj ={}; obj.proto=newObj;

面试题

1. 手写 new

// new 做的事情
function Person(age){
  this.age = age
}
const my = myNew(Person, 29)

function myNew = function (context, ...args) {
  // 补充相关代码
}

// 答案如下:

function myNew = function (context, ...args) {
  // 补充相关代码
  
  // 1. 创建一个空对象
  const obj = {};
  
  // 2. 更改 obj 的原型
  Object.setPrototypeOf(obj, context.prototype); // 等价于 obj.__proto__ = context.prototype;
  // 1和2等价于 const obj = Object.create(context.prototype);

  // 3. 将函数里面的 this 指向该对象 并 执行函数代码
  const res = context.apply(obj, args);
  
  // 4. 返回结果:函数自身结果或新对象
  return res instanceof Object ? res : obj;
}

2. 基础判断题

function Person(name, age) {
  this.name = name;
  this.age = age;
}

const person = new Person("xh", 29);

console.log(person.__proto__ === Person.prototype)
console.log(person.constructor === Person.constuctor)
console.log(Person.constructor === Function)
console.log(person.constructor === Person)
console.log(Person === Person.prototype.constructor)

console.log(person.__proto__.constructor === Object)
console.log(Person.prototype.__proto__ === Object.prototype)
console.log(person.__proto__.__proto__.constructor === Object)
console.log(person.__proto__.__proto__.__proto__ === null)
console.log(Function.constructor === Object)
console.log(Object.constructor === Function)

// 问题:以上打印结果

// 答案:
// true
// false
// true
// true
// true

// false
// true
// true
// true
// false
// true