简单聊一聊 JS 面向对象之原型

108 阅读7分钟

持续创作,加速成长!这是我参与「掘金日新计划 · 10 月更文挑战」的第 5 天

前言

为了更好的学习 TS,这里将会简单聊一聊 JS 面向对象之原型

原型:造一个小兵

const 近战兵 = {
  兵种: '近战',
  血量: 1488,
  物理攻击力: 60,  
  护甲: 180,
  金钱: 42,  
  补刀奖励: 16,  
  出生: function (){/*出生动画*/},
  死亡: function (){/*死亡动画*/},
  攻击: function (){/*攻击特效*/},
  行走: function (){/*行走动画*/},
}
兵营(近战兵)

原型:造100个小兵

// 第一种写法
const 近战兵1 = {/* 略 */}
const 近战兵2 = {/* 略 */}
/* ... */
const 近战兵100 = {/* 略 */}
兵营(近战兵1, 近战兵2, ..., 近战兵100)
// 太重复了
// 第二种写法
// 使用循环
const list = []
for (let i = 0; i < 100; i++) {
  list.push({
    id: i,
    兵种: '近战', 血量: 1488, 物理攻击力: 60,
    护甲: 180, 金钱: 42, 补刀奖励: 16,
    出生: function (){/*出生动画*/},
    死亡: function (){/*死亡动画*/},
    攻击: function (){/*攻击特效*/},
    行走: function (){/*行走动画*/},  
  }) 
}
兵营(...list)
// 这里太浪费内存了

我们希望把独有的属性分别放到对象里,把共有的属性放到一个对象里。

{
  id: 1, 血量: 1488, 
  物理攻击力: 60, 护甲: 180,
  more: 近战兵共有属性
}
{
  id: 2, 血量: 1488, 
  物理攻击力: 60, 护甲: 180,
  more: 近战兵共有属性
}
...
近战兵共有属性 = {
  兵种: '近战',金钱: 42, 补刀奖励: 16,  
  出生: function (){/*出生动画*/},
  死亡: function (){/*死亡动画*/},
  攻击: function (){/*攻击特效*/},
  行走: function (){/*行走动画*/},
}

这样一来:

  • 优化前:(7 个属性 + 4 个函数) * 100 = 1100
  • 优化后:(4 个属性 + 1 个 more) * 100 + 3 个共有属性 + 4 个共有方法 = 507

节省了内存

那么怎么造出 100 个,同时内存又尽量的少呢?

const 近战兵共有属性 = {
  兵种: '近战',金钱: 42, 补刀奖励: 16,  
  出生: function (){/*出生动画*/},
  死亡: function (){/*死亡动画*/},
  攻击: function (){/*攻击特效*/},
  行走: function (){/*行走动画*/},
}

const list = []
for (let i = 0; i < 100; i++) {
  const 近战兵 = {
    id: i, 血量: 1488, 
    物理攻击力: 60, 护甲: 180
  }
  近战兵.__proto__ = 近战兵共有属性  // 这句代码不规范
  list.push(近战兵)
}

兵营(...list)
// 以上代码的缺点:代码太分散了

原型:高内聚

对以上代码进行封装

// soldier.js
export const 创建近战兵 = function(id){
  const 近战兵 = {
    id: id, 血量: 1488, 
    物理攻击力: 60, 护甲: 180
  }
  近战兵.__proto__ = 近战兵共有属性  // 这句代码不规范
  return 近战兵
}

const 近战兵共有属性 = {
  兵种: '近战',金钱: 42, 补刀奖励: 16,  
  出生: function (){/*出生动画*/},
  死亡: function (){/*死亡动画*/},
  攻击: function (){/*攻击特效*/},
  行走: function (){/*行走动画*/},
}

// 缺点:不够内聚
// main.js
import {创建近战兵} form './soldier'
const list = []
for (let i = 0; i < 100; i++) {
  list.push(创建近战兵(i))
}

兵营(...list)

高内聚:该放在一起的代码,就尽量让它分不开。

低耦合:能分开的尽量不要放在一起。

近战兵.__proto__ = 近战兵共有属性 // 这句代码不规范 这句代码不能用的时候,如何知道 创建近战兵近战兵共有属性 它是有关联的?显然不能通过变量名来确定两个变量的逻辑关系。

// soldier.js
export const 创建近战兵 = function (id) {
  const 近战兵 = { id: id, 血量: 1488, 物理攻击力: 60, 护甲: 180 }
  近战兵.__proto__ = 创建近战兵.prototype  //这句代码不规范
  return 近战兵
}

// 高内聚,达到你中有我,我中有你
创建近战兵.prototype = {
  constructor: 创建近战兵,
  兵种: '近战',金钱: 42, 补刀奖励: 16,  
  出生: function (){/*出生动画*/},
  死亡: function (){/*死亡动画*/},
  攻击: function (){/*攻击特效*/},
  行走: function (){/*行走动画*/},
}

// 则代码非常的经典,应该把它推广,于是就有了 new 操作符

new 做了什么事情?

Screen Shot 2022-10-14 at 1.51.01 PM.png

// soldier.js
export const 近战兵 = function (id) {
  this.id = id, 
  this.血量 = 1488, 
  this.物理攻击力 = 60, 
  this.护甲 = 180 
}
近战兵.prototype.兵种 = '近战',
近战兵.prototype.金钱 = 42, 
近战兵.prototype.补刀奖励 = 16,  
近战兵.prototype.出生 = function (){/*出生动画*/},
近战兵.prototype.死亡 = function (){/*死亡动画*/},
近战兵.prototype.攻击 = function (){/*攻击特效*/},
近战兵.prototype.行走 = function (){/*行走动画*/},
// main.js
import {创建近战兵} form './soldier'
const list = []
for (let i = 0; i < 100; i++) {
  list.push(new 近战兵(i))
}

兵营(...list)

以上代码还能简化吗?

// soldier.js
export const 近战兵 = function (id) {
  copy(this, { id: id, 血量: 1488, 物理攻击力: 60, 护甲: 180 }) // copy 为自己实现的浅拷贝 for..in 循环
}
近战兵.prototype = {
  constructor: 近战兵,  // 把弄丢的 constructor 加回来
  兵种: '近战',金钱: 42, 补刀奖励: 16,  
  出生: function (){/*出生动画*/},
  死亡: function (){/*死亡动画*/},
  攻击: function (){/*攻击特效*/},
  行走: function (){/*行走动画*/},  
}

new 做了几件事

new 近战兵(i) 的时候

export const 近战兵 = function (id) {
 // 1. this = {}
 // 2. this.__proto__ = 近战兵.prototype
  copy(this, { id: id, 血量: 1488, 物理攻击力: 60, 护甲: 180 })
 // 3. return this
}

// ? 近战兵.prototype = {   // 这里是 JS 本来就帮你做好了 
 // ? constructor: 近战兵,  // 这里是 JS 已经帮你做好了
  兵种: '近战',金钱: 42, 补刀奖励: 16,  
  出生: function (){/*出生动画*/},
  死亡: function (){/*死亡动画*/},
  攻击: function (){/*攻击特效*/},
  行走: function (){/*行走动画*/}, 
}

constructor

所有的 JS 里面的函数,从它出生的时候就自带一个叫做 prototype 的属性,这个属性里面从它出生的时候就有一个叫做 constructor, 这个 constructor 从它出生的时候它的值就是这个函数自身。

总结

new 近战兵(i)自动做了四件事情

  • 自动创建空对象
  • 自动为空对象关联原型,原型地址指定为 近战兵.prototype
  • 自动将空对象作为 this 关键字运行构造函数
  • 自动 return this

JS 如何创建对象

  1. 以 new 为语法糖
  2. 用构造函数给对象添加独有属性
  3. 用构造函数的 prototype 容纳 共有属性
  4. 使用属性查找规则

属性查找规则与隐藏属性

// 这个是缩写
var a = {}
var b = []
// 实际真正的代码
var a = new Object()
var b = new Array()

也就是说当我们写 {},还是用到了 new, 只是我们平时都简写了,它内部始终会调用 new 的逻辑。

var obj = { x: 1 }
// 这个时候我们要去读 obj 的一个属性
obj.x // 它会怎么读? 它会首先去看 obj 的独有属性,有 就返回 1 

// 那如果访问的是它非独有属性呢?
obj.y // 它就会去看它的共有属性

独有属性没有就看共有属性,共有属性没有就看共有的共有。

属性查找规则:

读取 obj 的 'x' 属性时

  1. 先看 obj 的独有属性有没有 x
  2. 再看 obj 的共有属性有没有 x
  3. 再看 obj 的共有属性的共有属性有没有 x
  4. 直到共有属性为 null,则认为 obj.x 不存在
// 随便声明一个对象,我这个对象怎么声明的我不告诉你
var obj =  ???
// 请问这个对象的共有属性在哪里?
// 是不是得要回头看代码才知道,这不可能啊
// 那能不能把共有属性的所在地存到 obj 上面呢?可以也不可以
// 因为一旦在 obj 上存了属性,就会影响它的独有属性
// 所以浏览器必须用一个开发者看不见的属性来存共有属性的所在地

Screen Shot 2022-10-14 at 7.35.40 PM.png 这个属性叫什么不重要,因为每一代浏览器可能会变,不需要管这个属性名它叫什么,我只需要浏览器每次需要查找属性的时候,浏览器知道就行了,我不需要知道它叫什么。

obj.__proto__     // ❌
obj.[[Prototype]]  // ❌
// 我不需要知道 JS 如何确定共有属性

原型就是共有属性所在对象

什么是原型?

const obj = new 近战兵(i) 请问 obj 的原型是?

  1. 近战兵.prototype 这个不可以隐藏 👌
  2. obj.__proto__obj.[[Prototype]] 这个名字不重要 obj.隐藏属性 👌

上面的存的都是一样的地址,所以 1 和 2 都是一样的。

const fn = function(){} 请问 fn 的原型是什么?

  1. fn.prototype
  2. fn.__proto__
  3. Function.prototype
Function.constructor === Function // true
// 1. 浏览器构造 Function 
// 2. 浏览器写了代码 Function.constructor = Function

原型就是指一个对象的共有属性的所在地。