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

面向某某编程

面向对象编程

面向对象编程:Object-Oriented Programming,程序的主要组织单位是对象。
在 JS 中的对象定义为:无序属性的集合,其属性可以为基本值、对象或函数
行话:单个物体的抽象

面向过程编程

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

函数式编程

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

面向对象编程

对象

基础概念

对象由一组属性组成,每个属性都包括一个键(字符串或 Symbol)和一个值(任意数据类型)。
属性的键是唯一的,不同属性之间用逗号分隔。属性的值可以是任何数据类型,包括基本数据类型和其他对象。

对象创建

Object

1
2
3
4
5
6
const obj = new Object()
obj.name = 'xx'
obj.age = '23'
obj.sayAge = function() {
console.log(this.age)
}

字面量

1
2
3
4
5
6
7
const obj = {
name: 'xx',
age: '23',
sayAge(){
console.log(this.age)
}
}

Object.create()

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
const obj = Object.create({})

// create 实现原理
Object.create = (_obj) => {
if(typeof _obj !== 'object'){
return {}
}

// 创建一个空函数
function F() {}
// 将空函数的原型设置为传入的proto对象
F.prototype = _obj
// 返回一个新对象,该对象的原型指向传入的proto对象
return new F()
}

对象存储

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

构造函数

用来创建对象的特殊函数,通常以大写字母开头。
使用 new 关键字调用构造函数可以创建新对象,并将构造函数内部的属性和方法添加到新对象上。

1
2
3
4
5
6
7
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. 如果构造函数没有显式返回一个对象,那么会返回这个新对象的引用地址

工厂模式

在工厂模式中,不直接调用构造函数来创建对象,而是使用一个工厂函数(或者方法)来创建对象。
这种模式封装了对象的创建过程(不让外部感知),使得代码更具灵活性和可维护性

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
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 关键字调用),也可以被当作工厂函数使用(直接调用)

单例模式

是一种设计模式,它确保一个类只有一个实例,并提供一个全局访问点以访问该实例。
常用于:路由、全局状态等

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
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

1
2
3
4
5
6
7
8
9
10
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 张三

继承

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

普通继承

将父类实例构赋值给子造函数的原型对象

1
2
3
4
5
6
7
8
9
function Parent(...args){
this.address = '成都'
this.name = '张三'
this.like = ['钓鱼', '洗碗']
this.args0 = args[0]
}
Parent.prototype.sayName = function(){
console.log(`my name is ${this.name}`)
}
1
2
3
4
5
6
7
8
9
10
11
12
13
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 也会被改

优点

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

缺点

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

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

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

1
2
3
4
5
6
7
8
9
function Parent(...args){
this.address = '成都'
this.name = '张三'
this.like = ['钓鱼', '洗碗']
this.args0 = args[0]
}
Parent.prototype.sayName = function(){
console.log(`my name is ${this.name}`)
}
1
2
3
4
5
6
7
8
9
10
11
12
13
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属性

组合继承

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

1
2
3
4
5
6
7
8
9
function Parent(...args){
this.address = '成都'
this.name = '张三'
this.like = ['钓鱼', '洗碗']
this.args0 = args[0]
}
Parent.prototype.sayName = function(){
console.log(`my name is ${this.name}`)
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
function Child(...args){
// 调用父构造函数,将其属性继承到子类实例上
Parent.call(this, ...args)
}

// 重写原型对象---start
Child.prototype = new Parent()
Child.prototype.constuctor = Child // constuctor 修正
// 重写原型对象---end


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

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

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

优点

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

缺点

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

寄生组合继承

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

1
2
3
4
5
6
7
8
9
function Parent(...args){
this.address = '成都'
this.name = '张三'
this.like = ['钓鱼', '洗碗']
this.args0 = args[0]
}
Parent.prototype.sayName = function(){
console.log(`my name is ${this.name}`)
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
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 张三

寄生组合继承其实就是ES6class Child extends ParentES5代码,对标的是super()

多重继承

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

1
2
3
4
5
6
7
8
9
function Parent1(...args){
this.address1 = '成都'
this.name1 = '张三'
this.like1 = ['钓鱼', '洗碗']
this.args10 = args[0]
}
Parent1.prototype.sayName = function(){
console.log(`my name is ${this.name}`)
}
1
2
3
4
5
6
7
8
9
function Parent2(...args){
this.address2 = '上海'
this.name2 = '李四'
this.like2 = ['游泳']
this.args20 = args[0]
}
Parent2.prototype.sayLike = function(){
console.log(`my like is ${this.like}`)
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
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
  2. hasOwnProperty:检查属性是否在对象上(仅自身不涉及原型链上)
    1. my.hasOwnProperty(‘name’) // true || false
  3. instanceof:检查对象是否属于某个构造函数的实例
    1. my instanceof Object // true || false
    2. 实现原理:检查对象的原型链上是否包含构造函数的 prototype 属性,即判断实例.__proto__ === 构造函数.prototype

对象分类

对象分为 2 类:实例对象、函数对象
实例对象:通过 [new 构造函数()] 生成的
函数对象:通过 [new Function()] 生成的

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

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

1
2
3
4
5
6
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)

1
2
3
4
5
Object.__proto__ === Function.__proto__ === Function.prototype // true
Object instanceof Function // true,表明 Object 是 Function 的实例
Function instanceof Function // true,表明 Function 是 Function 的实例
Function instanceof Object // true,表明 Function 是 Object 的实例
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;

面试题

手写 new

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
// 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;
}

基础判断题

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
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

1-2、面向对象编程_原型与原型链
https://mrhzq.github.io/职业上一二事/前端面试/前端八股文/1-2、面向对象编程_原型与原型链/
作者
黄智强
发布于
2024年1月13日
许可协议