持续创作,加速成长!这是我参与「掘金日新计划 · 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 做了什么事情?
// 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 如何创建对象
- 以 new 为语法糖
- 用构造函数给对象添加独有属性
- 用构造函数的 prototype 容纳 共有属性
- 使用属性查找规则
属性查找规则与隐藏属性
// 这个是缩写
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' 属性时
- 先看 obj 的独有属性有没有 x
- 再看 obj 的共有属性有没有 x
- 再看 obj 的共有属性的共有属性有没有 x
- 直到共有属性为
null,则认为 obj.x 不存在
// 随便声明一个对象,我这个对象怎么声明的我不告诉你
var obj = ???
// 请问这个对象的共有属性在哪里?
// 是不是得要回头看代码才知道,这不可能啊
// 那能不能把共有属性的所在地存到 obj 上面呢?可以也不可以
// 因为一旦在 obj 上存了属性,就会影响它的独有属性
// 所以浏览器必须用一个开发者看不见的属性来存共有属性的所在地
这个属性叫什么不重要,因为每一代浏览器可能会变,不需要管这个属性名它叫什么,我只需要浏览器每次需要查找属性的时候,浏览器知道就行了,我不需要知道它叫什么。
obj.__proto__ // ❌
obj.[[Prototype]] // ❌
// 我不需要知道 JS 如何确定共有属性
原型就是共有属性所在对象
什么是原型?
const obj = new 近战兵(i) 请问 obj 的原型是?
近战兵.prototype这个不可以隐藏 👌obj.__proto__或obj.[[Prototype]]这个名字不重要 obj.隐藏属性 👌
上面的存的都是一样的地址,所以 1 和 2 都是一样的。
const fn = function(){} 请问 fn 的原型是什么?
fn.prototypefn.__proto__Function.prototype
Function.constructor === Function // true
// 1. 浏览器构造 Function
// 2. 浏览器写了代码 Function.constructor = Function
原型就是指一个对象的共有属性的所在地。