那些年,被原型折磨的前端小白


在刚开始学 JavaScript 时,最深恶痛绝的三个 JS怪👹:this妖原型精AJAX魔

最近在写总结回顾时竟意外发现这三大怪早已在不知不觉中被我消化吸收了,趁着热乎记下来✍~~~

有请今天的出场嘉宾🎤:原型精

内存图学习法

在了解原型前,先来学一个内存图的简单画法。

内存图画法示例.png

注:图示对象属性不完全,#数字代表内存地址,其数值为虚拟数值,仅作示例,下同

window 对象为例,属性名保存属性值地址,例如,属性 Object 保存 Object 对象的地址,即 window 对象的属性 Object 指向内存中的Object 对象

【这里要明白 Object Object 对象是两个东西,Object 是存放 Object 对象的地址,而 Object 对象是 Heap 中的一坨内存数据,同理,consoleconsole 对象ArrayArray 对象不是同一个东西 🔑 】

你有我有全都有

假设现在要声明一个士兵,士兵要包含的属性:[编号,生命值,攻击(动作),防御(动作)]

var 士兵 = {
    编号: 1,
    生命力: 59,
    攻击: function(){/* 呼一巴掌 */},
    防御: funcrion(){/* 闪 */}
}

画个内存图看看

一个士兵内存图.png

好啦,那现在如果要制造一百个士兵呢?

var 士兵们 = []
var 士兵
for(var i = 0; i < 100; i++) {
    var 士兵 = {
        编号: i,
        生命力: Math.floor(Math.random() * 100) + 1,
        攻击: function(){/* 呼一巴掌 */},
        防御: funcrion(){/* 闪 */}
    }
    士兵们.push(士兵)
}
兵营.批量制造(士兵们)

一百个士兵内存图.png

从内存图不难看出,这样的代码十分浪费内存。

对于每个士兵而言,编号和生命值是各不一样的,所以需要创建一百次,但攻击、防御是一样的,只需要各自引用同一个函数就可以了。

既然如此,那为什么不把这些相同的代码整合到一个地方,让这些士兵都能从这个地方拿到这些属性,另外又对每个士兵自身的属性进行单独赋值

这里引入两个知识点:

  • 每个实例对象(object)都有一个私有属性(称之为__proto__)指向它的构造函数的原型对象「MDN 传送门」

  • Object.create() 方法创建一个新对象,使用现有的对象来提供新创建的对象的 __proto__「MDN 传送门」

这个原型对象不就可以用来充当那个整合相同代码的地方,然后让「士兵」的 __proto__ 指向「士兵原型」

var 士兵原型 = {
    攻击: function(){/* 呼一巴掌 */},
    防御: funcrion(){/* 闪 */}
}
var 士兵们 = []
var 士兵
for (var i = 0; i < 100; i++>) {
    士兵[i] = Object.create(士兵原型)
    士兵[i].编号 = i
    士兵[i].生命力 = Math.floor(Math.random() * 100) + 1
}
兵营.批量制造(士兵们)

士兵原型图第1版.png

现在假若兵营要再制造50名士兵,那么制造士兵(for循环内)的代码就要再重新写一遍。既然如此,那就把制造士兵抽离成一个函数,后面有需要就调用这个函数好了

var 士兵原型 = {
    攻击: function(){/* 呼一巴掌 */},
    防御: funcrion(){/* 闪 */}
}
var 士兵们 = []
var 士兵数 = 士兵们.length
function 制造士兵(编号i) {
    var 士兵 = Object.create(士兵原型)
    士兵.编号 = 编号i
    士兵.生命力 = Math.floor(Math.random() * 100) + 1
    return 士兵
}
for (var i = 0; i < 100; i++>) {
    士兵们[i] = 制造士兵(i)
}
士兵数 = 士兵们.length
兵营.批量制造(士兵们)
for (var i = 士兵数; i < 士兵数 + 50; i++>) {
    士兵们[i] = 制造士兵(i)
}
兵营.批量制造(士兵们)

士兵原型图第2版.png

注:内存图中蓝色区域是 JS 内置的,只是前面的图没有补全

从内存图不难看出,虽然制造士兵函数制造出来的士兵的 __proto__ 是指向士兵原型的,但制造士兵函数本身和士兵原型却没有联系,这样显得代码分散。那么试着把制造士兵函数和士兵原型结合一起

var 士兵们 = []
var 士兵数 = 士兵们.length
function 制造士兵(编号i) {
    var 士兵 = Object.create(制造士兵.士兵原型)
    士兵.编号 = 编号i
    士兵.生命力 = Math.floor(Math.random() * 100) + 1
    return 士兵
}
制造士兵.士兵原型 = {       // 这步把制造士兵和士兵原型绑定
    攻击: function(){/* 呼一巴掌 */},
    防御: funcrion(){/* 闪 */}
    constructor: 制造士兵   // 这步把士兵原型和制造士兵绑定
}
for (var i = 0; i < 100; i++>) {
    士兵们[i] = 制造士兵(i)
}
士兵数 = 士兵们.length
兵营.批量制造(士兵们)
for (var i = 士兵数; i < 士兵数 + 50; i++>) {
    士兵们[i] = 制造士兵(i)
}
兵营.批量制造(士兵们)

士兵原型图第3版.png

好啦,现在就通过这个图来了解有关原型的概念

  • 原型

    在javascript中,函数可以有属性。 每个函数都有一个特殊的属性叫作原型(prototype),默认指向一个自带属性constructor(这个属性值指向函数本身)的对象,这个对象可以重写(联系上面代码 制造士兵.士兵原型 = {/* 一些代码 */}

  • 原型对象

    每个实例对象都有一个私有属性(__proto__)指向它的构造函数的原型对象(如内存图中的 #789士兵原型对象)。该原型对象也有一个自己的原型对象(__proto__),层层向上直到一个对象的原型对象为 null。注意函数也是对象

  • 原型链

    内存图中通过__proto__指向原型的链接

    图中 士兵->士兵原型->Object.prototype->null 这条线就是一条原型链

  • 构造函数

    制造士兵函数就是构造函数。

    构造函数本身负责给对象本身添加属性(如 士兵.编号 = 编号i)

    构造函数.prototype负责保存对象的共有属性(如 士兵原型)

    所有构造函数(专门由于创建对象的函数)首字母大写

    所有被构造出来的对象首字母小写

原型精 CP — prototype 和 __proto__

看完有关原型的概念,最迷惑的就是 prototype 和 __proto__ 到底有什么区别 🤔

prototype__proto__ 都存着原型的地址

prototype :挂在函数上,被构造函数创建的实例对象的 __proto__ 指向构造函数的 prototype 属性,用来实现基于原型的继承与属性的共享

__proto__ :挂在对象上,它的值指向构造函数的原型对象,构成原型链,同样用于实现基于原型的继承

三个重要知识 🧨:

  1. 对象.__proto__ === 构造函数.prototype

  2. Object.prototype 是所有对象的(直接或间接)原型对象,Object.prototype 的原型对象为 null

  3. 任何函数.__proto__ === Function.prototype

下面来一个无奖问答 🎮

Object.prototype.__proto__ === null

true,根据知识2

Function.prototype.__proto__ === Object.prototype

true,根据知识2

var f = () => {}
f.__proto__ === Function.prototype

true,根据知识3

Function.__proto__ === Function.prototype

true,根据知识1

new 运算符

new 关键字会进行如下的操作:

  1. 创建一个空的简单JavaScript对象(即{})
  2. 为步骤1新创建的对象添加属性__proto__,将该属性链接至构造函数的原型对象
  3. 将步骤1新创建的对象作为this的上下文
  4. 如果该函数没有返回对象,则返回this

回到制造士兵最后那段代码

// 省略部分代码
function 制造士兵(编号i) {
    var 士兵 = Object.create(制造士兵.士兵原型)
    士兵.编号 = 编号i
    士兵.生命力 = Math.floor(Math.random() * 100) + 1
    return 士兵
}
制造士兵.士兵原型 = {       // 这步把制造士兵和士兵原型绑定
    攻击: function(){/* 呼一巴掌 */},
    防御: funcrion(){/* 闪 */}
    constructor: 制造士兵   // 这步把士兵原型和制造士兵绑定
}
for (var i = 0; i < 100; i++>) {
    士兵们[i] = 制造士兵(i)
}
// 省略部分代码

换成 new 写法

// 省略部分代码
function 制造士兵(编号i) {
    this.编号 = 编号i
    this.生命力 = Math.floor(Math.random() * 100) + 1
}
制造士兵.prototype.攻击 = function(){/* 呼一巴掌 */}
制造士兵.prototype.攻击 = funcrion(){/* 闪 */}
for (var i = 0; i < 100; i++>) {
    士兵们[i] = new 制造士兵(i)
}
// 省略部分代码

参考文章:

JS 的 new 到底是干什么的?

js中__proto__和prototype的区别和关系?


写在后面:因为时间关系,这篇文章写得有些分散杂乱,有空再好好梳理一遍。


文章作者: April-cl
版权声明: 本博客所有文章除特別声明外,均采用 CC BY 4.0 许可协议。转载请注明来源 April-cl !
  目录