D16 - 那个圆圆的东西 - 物件原型 & 原型链

前言

Object.prototype.like = '舌尖上的JS'

请在任意的 JavaScript 环境输入以上程序码,然後随意宣告一个变数、阵列或函式,甚至一个空物件都可以 let name = {} ,然後在变数後加上 .like 看看印出什麽

神奇的杰克,每个变数都突然有个 like 属性是 舌尖上的JS,这不是魔法,是 JavaScript 透过原型做到继承的效果!

这篇文带你认识 JavaScript 中这个圆圆的东西,包括:

  • new + constructor 无法共享属性?
  • prototype 原型继承 && 共享
  • 原型链上找属性
  • prototype 相关语法
  • 仙女下凡来解答
  • prototype 注意事项

new + constructor 无法共享属性?

还记得之前的鸡蛋糕比喻,透过 new + constructor 创造物件吗? 再看一次程序码

生成的两个鸡蛋糕 rabbit1rabbit2 确实都依照 RabbitCake constructor 的设定各自拥有三个属性,修改 rabbit1 中的 shape 属性并不会影响 rabbit2,因为他们是各自独立的。

function RabbitCake(flavor) {
    this.producer = 'Hooo';
    this.shape = 'rabbit';
    this.flavor = flavor;
}

let rabbit1 = new RabbitCake('cream');
let rabbit2 = new RabbitCake('chocolate');


rabbit1.shape = 'bunny'
console.log(rabbit1.shape)  // bunny
console.log(rabbit2.shape)  // rabbit

若是希望将所有的兔子鸡蛋糕都加上一个制造日期的属性,可以怎麽做呢?
难道再去修改 RabbitCake 内加上 this.productionDate 吗,不不不,一来已经创出来的物件并不会因此多出这个属性,二来属性还是独立存在在新物件内,并没有达到共用的目的。

解决办法就是 - 将共享的属性和方法放入 原型 prototype 中。

prototype 实现继承 & 共享属性

来看看 ECMA 针对 prototype 的说明, 原型 prototype 是个存在於所有 constructor 内的一个物件属性,用於实现基於原型的继承属性共享

4.3.1 Objects
Each constructor is a function that has a property named "prototype" that is used to implement prototype-based inheritance and shared properties.

4.4.8 prototype
object that provides shared properties for other objects
NOTE
When a constructor creates an object, that object implicitly references the constructor's "prototype" property for the purpose of resolving property references. The constructor's "prototype" property can be referenced by the program expression constructor.prototype, and properties added to an object's prototype are shared, through inheritance, by all objects sharing the prototype. Alternatively, a new object may be created with an explicitly specified prototype by using the Object.create built-in function.

有了 prototype 这个物件,将所有共享的属性放入 RabbitCake 的 prototype 中,之後创出来的所有兔子鸡蛋糕都可以共享属性了!

function RabbitCake(flavor) {
    this.flavor = flavor;
}

// 将共享的属性放入 prototype 物件中
RabbitCake.prototype = {
    producer: 'Hooo',
    shape: 'rabbit',
    productionDate: 'Oct.1, 2021'
}

let rabbit1 = new RabbitCake('cream');
let rabbit2 = new RabbitCake('chocolate');

console.log(rabbit1.productionDate)  // 'Oct.1, 2021'
console.log(rabbit2.productionDate)  // 'Oct.1, 2021'

上一篇物件导向中提到,与 class-based 语言不同,prototype-based 的语言透过原型的操作可以动态的添加或删除属性,可在任何时间不受限在定义时,这什麽意思呢?

刚刚创造出的 rabbit1rabbit2 两个实例成功继承了 RabbitCake.prototype 内的属性,那若我们修改了 RabbitCake.prototype 的内容会受影响吗?
看看以下示范:

// 删除 shape 属性
delete RabbitCake.prototype.productionDate

// 修改 producer 属性
RabbitCake.prototype.producer: 'HooGoodGood',

// 共享的属性修改也影响实例 instance
console.log( rabbit1.productionDate)   // undefined
console.log( rabbit1.producer )       // HooGoodGood

答案是:会的!
由於这些属性并非存在於 实例 instance 中,而是透过查访属性所在的 prototype ,所以当然会跟着变动,那又是怎麽查访的呢? 透过一条名为原型链的线索!

找找原型链上的属性

ECMA 的原型链定义:

Every object created by a constructor has an implicit reference (called the object's prototype) to the value of its constructor's "prototype" property.
Furthermore, a prototype may have a non-null implicit reference to its prototype, and so on; this is called the prototype chain.

每个由 constructor 创出来的物件都有个隐性的指标,指向了 constructor 的 prototype,并且,constructor 也会有一个隐性指标再指向创造他的 constructor prototype内,就这样一直指下去,最终连结到 Object.prototype,就称为原型链。

所以虽然 producershape 不存在 rabbit1rabbi2 内,但可以透过原型链往上查找到 RabbitCake 的 prototype,这些隐性的指标指向了 constructor 的 prototype,每个创出来的实例 instance (那些鸡蛋糕),都可以成功取得原型链上的所有属性。

与 prototype 有关的语法

prototype && Object.getPrototypeOf

使用 __proto__ 可以在原型链上一层一层往上查访 prototype

rabbit1.__proto__ === RabbitCake.prototype
Rabbit.prototype.__proto__ === Object.prototype
Object.prototype.__proto__ === null

// 直接从 rabbit1 连到原型链末端
rabbit1.__proto__.__proto__.__proto__ === null

// RabbitCake 本身是 function,也可以往上查找到 Function constructor
RabbitCake.__proto__ === Function.prototype
Function.prototype.__proto__ === Object.prototype

所有原型链最起始的原型都是 Object.prototype,而 Object.prototype.__proto__null,这就是原型链的终点。

也可以使用 Object.getPrototypeOf() 回传括号内物件的原型

Object.getPrototypeOf(rabbit1) === RabbitCake.prototype
Object.getPrototypeOf(RabbitCake.prototype) === Object.prototype
Object.getPrototypeOf(Object.prototype) === null

hasOwnProperty

如果想知道一个属性是存在该 实例instance 本身,还是继承自原型链上,可以用 hasOwnProperty 确认 true or false

rabbit1.hasOwnProperty('flavor')    // true
rabbit1.hasOwnProperty('producer')  // false,继承自 CakeRabbit prototype
rabbit1.hasOwnProperty('shape')     // false,继承自 CakeRabbit prototype

instanceOf

透过 X instanceof Y 来判断 X 是否为 Y 的 实例 instance

rabbit1 instanceof RabbitCake   // true
rabbit1 instanceof Function     // false,rabbi1 的 constructor 是 RabbitCake 不是 Function
RabbitCake instanceof Function  // true
Function instanceof Object      // true
Object instanceof Function      // true,有趣的是 Object 与 Function 互为 constructor

仙女下凡来解答 🧚

来解答昨天和今天的答案吧

Q1: 在查询 MDN 语法关於 string、array、object 的内建方法时,发现开头都有 prototype 这个字样,可是明明在使用上没有呀!

// MDN 语法
Array.prototype.map()

// 实际使用
['Ritz', 'Lotus', 'Oreo'].filter(chocolate => chocolate.length > 4)

答:有没有发现这些内建方法都是写在 Array.prototype 下呢,大写的 Array 就是一个内建的 constructor,任何的阵列都是这个 Array constructor 的 实例 instance,所以写在 Array prototype 下的方法都能透过原型链继承给任何阵列,如同文中的 rabbit1 要取得 producer 一样,直接使用 [].map 就可以使用罗!

(想当初菜鸟时百思不得其解,现在看完原型链的解释 4 不 4 小菜一碟呢 )

Q2: 为什麽任何的变数都能印出 舌尖上的JS

Object.prototype.like = '舌尖上的JS'

答:由於我将 like 这个属性写在 Object.prototype 内,而JavaScript 中任何的实例 instance 都能在原型链中查访到 Object.prototype,所以不论是哪种型别的变数一定都能找到这个属性!

若是写在 Array.prototype.like = '舌尖上的JS' 就只有阵列可以印出这个 like 属性

prototype 使用注意事项

好的,我要来自首,开头的 Object.prototype.like = '舌尖上的JS' 是个不良示范,在程序中尽可能 「不要去修改预设的物件原型」,不去修改不属於你建立的物件 Don’t modify objects you don’t own
Object.prototype 的示范仅作为 prototype 与继承概念,实务上若变动此 prototype 容易造成非预期的变动。

有个名词称为 prototype pollution 原型污染,就是恶意的使用者利用了原型链的设计,在其中的原型中埋入有害的程序码,造成整个原型链的污染,刚好这几天 Huli 大大发表一篇相关文章 基於 JS 原型链的攻击手法:Prototype Pollution ,资安也是在学习程序码中必备的观念,推荐可以往这部分深究!


图片来源

Reference

ECMA
MDN instanceof
该来理解 JavaScript 的原型链了
从ES6 开始的JavaScript 学习生活
从设计初衷解释 JavaScript 原型链
Javascripter 必须知道的继承 prototype, prototype, proto
JavaScript Prototype
你懂 JavaScript 吗?#19 原型(Prototype)
基於 JS 原型链的攻击手法:Prototype Pollution
The Complete Guide to Prototype Pollution Vulnerabilities
Don’t modify objects you don’t own

结语

圆圆的原型篇结束!一知半解的部分在整理文章的过程得到解答,希望这篇文章对於初探原型的你有所帮助,Reference 附上在研究时帮助我许多的文章,分享给你们~


<<:  Day19 简易资料库RealmSwift小实作6

>>:  [Day16]汇总函数实作

Python Time套件

今天我要来教大家是个套件,套件名称叫做time,顾名思义就是有关时间的套件。有时候我们会绍定在某个时...

资讯治理(Data Governance)

数据管理员(Data Steward) 数据管理员是组织中的一个角色,负责利用组织的数据治理流程来...

【Day11】前端环境重设之工作流水帐

由於之前测试的前端中台模板 Antd 都是跟别人借大陆那边的工厂 IP 做测试 , 今天在办公室以及...

虹语岚访仲夏夜-30(打杂的Allen终)

我拿出手机...安装了自己做的那个『心洞年代』...小七说,它会透过API,自动更新内容,就跟现在的...

【Day 1】前言

大家好,我是第一次参加铁人赛,主要是想透过这个活动纪录自己的学习笔记。 因为是以学习纪录为主,所以就...