JS基礎:Prototype Chain 原型鏈
JS 基礎:Prototype Chain 原型鏈
文章目錄
- JS 基礎:Prototype Chain 原型鏈
- 簡介
- 參考
- 正文
- Object 對象創建
- 直接量 `{}`
- 內置構造函數 `new Object()`
- 使用 `Object.create(proto, propertiesObject)`
- Class 類型
- Prototype 原型
- Function without prototype
- Function with prototype
- Concept 概念
- Sample
- Prototypte is an Object 原型也是對象
- Prototype Chain 原型鏈(也就是所謂的`繼承Inherit`啦!)
- Sample
- Built-in Classes 內置類(內置的構造函數)
- 補充:ES6 Class
- 結語
簡介
說到面對對象編程(OOP),當然不能沒有繼承(Inherit)。作為代碼複用最基礎的方式便是透過繼承,將子類透過繼承父類的方法或屬性來擴展對象的能力。對象(Object)類型肯定是 JS 代碼中最常用的類型,那麼 JS 是如何實現繼承的呢?
然而與 Java、C++ 的基於類型(class-based)
的繼承機制不同,JavaScript 並沒有實作類型(class)
定義(即便是 ES6 的 class
也僅僅是語法糖)。JS 是基於原型(prototype-based)
的繼承機制,而 JS 中的類型定義其本質就是一個函數。接下來就讓我娓娓道來 JavaSciprt 基於原型的繼承機制吧。
參考
JavaScript深入之从原型到原型链 | https://github.com/mqyqingfeng/blog/issues/2 |
繼承與原型鏈 | https://developer.mozilla.org/zh-TW/docs/Web/JavaScript/Inheritance_and_the_prototype_chain |
正文
Object 對象創建
首先我們先來看看最基本的對象(Object)
是如何聲明和創建的:
直接量 {}
const o1 = { name: 'John', age: 18 }
內置構造函數 new Object()
const o1 = new Object()
const o2 = new Object(null)
const o3 = new Object({})
const o4 = new Object(o1) // 這邊傳遞的是引用,o1 和 o4 將指向同一個對象
使用 Object.create(proto, propertiesObject)
// 使用 Object.create
const o6 = Object.create(Object.prototype)
const o7 = Object.create(Object.prototype, {name: {value: 'John'},age: {value: 18,writable: false}
})
注意! Object.create
方法非常重要,將作為下面繼承方式的基礎
Class 類型
有了對象之後,我們創建出來的依舊是一個個獨立的對象,沒有統一的規範和樣式。在面對對象的編程思維(OOP)中,需要定義類型(Class)
,而所有對象實例(Instance)
都應該基於類型來創建,創建出來的物件簡稱為對象或是實例(反正就是一個 Object
或是稱為 Instance
)。
但是 JavaScript 裡面根本沒有 class
關鍵字(這邊不考慮 ES6 提供的 class
語法糖),沒有辦法像 Java、C++ 那樣聲明定義類型然後創建對象。當然 JS 的對象機制也不是基於類型(class-based)的,而是基於原型(prototype-based)
的
在 JS 中,類型(class)
本質上就是一個函數(Function)
,有趣的是同樣使用 new
關鍵字來創建對象實例:
// 聲明 Person 類型,使用 this 來指向對象實例的屬性
function Person() {this.name = 'John'this.age = 18
}const person = new Person()
可以從上面的形式看出來,作為"類型"的函數就好像這個類型的構造函數一樣,下面貼上 babel 編譯後的代碼讓大家也能了解到 ES6 的 class
語法糖是如何轉換回 ES5 以前的代碼的(有興趣的可以自己到Babel 官方嘗試):
- ES6 class
class Person {constructor() {this.name = 'John'this.age = 18}
}
- babel 編譯後
'use strict'var Person = function Person() {this.name = 'John'this.age = 18
}
我們能夠看到 Person
類就被編譯為一個函數,而構造函數(constructor)
便是這個方法的主函數體
Prototype 原型
現在我們能夠基於類型創建對象了,接下來我們來了解什麼叫原型(prototype)
。
首先我們先來看沒有使用原型的時候,為對象添加方法會發生什麼事:
Function without prototype
function Person(name) {this.name = namethis.hi = function () {console.log(`Hi! I am ${this.name}`)}
}
const p1 = new Person('John')
const p2 = new Person('Andy')
p2.hi = function () {console.log('new hi function')
}
p1.hi()
p2.hi()// output:
// Hi! I am John
// new hi function
這邊我們發現 p1、p2 的 hi
方法是互相獨立的,也就導致我們修改了 p2 的方法之後,p1 的方法還是原來的形式。
然而對於面對對象編程(OOP)來說,同樣類型的方法相同,並且應該只能存在一個函數體,而不是為每個對象創建一個新的方法。這時候我們就必須使用到原型。
Function with prototype
我們先看範例,再來說明原型的概念:
function Person(name) {this.name = name
}
Person.prototype.hi = function () {console.log(`Hi! I am ${this.name}`)
}
const p1 = new Person('John')
const p2 = new Person('Andy')
p1.hi()
p2.hi()// output:
// Hi! I am John
// Hi! I am Andy
太神奇了傑克!如此一來,我們透過將 hi 方法變成 Person
類型的 prototype
對象上,就使得所有基於 Person
類型所創建的對象都能調用同樣的方法,也只需要定義一次。那麼話說回來,這個 prototype
到底是個什麼玩意兒?
Concept 概念
在 JS 中,所謂類型(class)
的本質就是一個函數(Function)
(上面也提過了),它在創建對象(new)
的時候將被作為構造函數(constructor)
來調用。而一個對象(object)
的創建必定離不開三大要素:
構造函數(constructor)
:即定義類型的原函數原型對象(prototype)
:每個構造函數都會存在一個原型對象(注意!原型本身也是一個對象)對象實例(object)
:真正運行時存活在堆(Heap)中的對象實例
三個要素之間的關係如下圖:
- 橢圓形表示一個存活在
堆(Heap)
中的對象(object)
- 六邊表示
構造方法(constructor)
,也就是最原始的類型函數聲明
並且三者之間有如下訪問規則:
__proto__
:對象訪問其原型prototype
:構造函數訪問其原型constructor
:對象訪問其構造函數時
通常一個對象最基本只會擁有 __proto__
屬性,而其 constructor
屬性則是綁定在其原型對象(prototype)
之上。
接下來我們舉個例子:
Sample
- 類型聲明 & 創建對象
function Person(name) {this.name = name
}
const person = new Person()
- 對應類圖
Prototypte is an Object 原型也是對象
注意!這邊必須記得,所謂的原型(prototype)
本身也是某一個對象,這對接下來了解原型鏈
以及繼承機制
非常重要
Prototype Chain 原型鏈(也就是所謂的繼承Inherit
啦!)
接下來我們就可以透過將某個對象實例(object)
變成新的類型的原型對象(prototype)
,如此便能夠創造出 JS 語言中的繼承形式,也就是所謂的原型鏈(prototype chain)
:
如上圖所示,B 繼承 A,C 又繼承 B。
而 JS 在運行時會先查找對象自身的屬性,若訪問的屬性不存在則會在原型鏈上不斷向上查找直到最頂部。接下來我們看一個範例:
Sample
首先我們先借助 babel 編譯後的一個函數:
function _inheritsLoose(subClass, superClass) {subClass.prototype = Object.create(superClass.prototype)subClass.prototype.constructor = subClasssubClass.__proto__ = superClass
}
- 說明:為了要實現類型間的繼承,我們需要對子類進行三個步驟:
subClass.prototype = Object.create(superClass.prototype)
:創建一個父類的對象實例並作為子類的原型對象subClass.prototype.constructor = subClass
:將原型對象的構造函數重新指向子類subClass.__proto__ = superClass
:將子類的構造函數原型指向父類的構造函數
接下來寫出我們的類(class 本質上是 funciton)
定義(參考 babel 編譯後代碼):
const SuperClass = (function () {function SuperClass() {this.name = 'super class'}const _proto = SuperClass.prototype_proto.hi = function hi() {console.log(this.name)}return SuperClass
})()const SubClass = (function (_SuperClass) {_inheritsLoose(SubClass, _SuperClass)function SubClass() {let _this_this = _SuperClass.call(this) || this_this.name = 'sub class'return _this}return SubClass
})(SuperClass)const sub = new SubClass()
sub.hi()// output:
// sub class
如果上面的寫法太難的話這邊給出一個白話文版本(不過上面的代碼還是建議把它看懂,也是提升自己的代碼素養):
function SuperClass() {this.name = 'super class'
}SuperClass.prototype.hi = function () {console.log(this.name)
}function SubClass() {this.name = 'sub class'
}SubClass.prototype = Object.create(SuperClass.prototype)
SubClass.prototype.constructor = SubClass
SubClass.__proto__ = SuperClassconst sub = new SubClass()
sub.hi()// output:
// sub class
下面用圖來表示繼承的步驟:
示例代碼中調用的 hi
方法步驟:
sub
對象中查找,沒找到對應函數- 再到
SubClass.prototype
,也就是sub
的原型對象中查找,還是沒有 - 最後到
SuperClass.prototype
中找到對應函數並調用,同時SuperClass.prototype
就是SubClass.prototype
的原型
Built-in Classes 內置類(內置的構造函數)
最後來介紹一下 JS 的內置函數。平常寫代碼的時候可能會用過如 Boolean
、Number
、Object
、Array
、RegExp
等內置函數,而這些內置函數的關係又是如何呢?話不多說先上圖:
我們發現內置類
與我們自己定義的類型是擁有同樣層級的原型鏈的,謎底揭曉(好吧其實也不是什麼大秘密)
這也是為什麼所有對象必定都有一個 toString
方法,因為他定義在 Object.prototype
上,而 Object.prototype
就是所有對象的原型鏈的根源,他本身是沒有原型的(Object.prototype.__proto__ === null
),並且所有構造函數的原型都是 Function.prototype
(Function.__proto__ === Function.prototype
的定義能夠保證一致性)
補充:ES6 Class
最後貼上一段 ES6 的 Class 寫法,以及 babel 編譯過後的結果供大家參考:
- ES6 class
class A {constructor() {this.a = 'a'}showDetail() {console.log(`this.a = ${this.a}`)}
}class B extends A {constructor() {super()this.b = 'b'}showDetail() {super.showDetail()console.log(`this.b = ${this.b}`)}
}class C extends B {constructor() {super()this.a = 'ca'this.b = 'cb'}showDetail() {super.showDetail()}
}const a = new A()
const b = new B()
const c = new C()
console.log('invoke a.show')
a.showDetail()
console.log('invoke b.show')
b.showDetail()
console.log('invoke c.show')
c.showDetail()// output:
// invoke a.show
// this.a = a
// invoke b.show
// this.a = a
// this.b = b
// invoke c.show
// this.a = ca
// this.b = cb
- babel 編譯後
'use strict'function _inheritsLoose(subClass, superClass) {subClass.prototype = Object.create(superClass.prototype)subClass.prototype.constructor = subClasssubClass.__proto__ = superClass
}var A = /*#__PURE__*/ (function () {function A() {this.a = 'a'}var _proto = A.prototype_proto.showDetail = function showDetail() {console.log('this.a = ' + this.a)}return A
})()var B = /*#__PURE__*/ (function (_A) {_inheritsLoose(B, _A)function B() {var _this_this = _A.call(this) || this_this.b = 'b'return _this}var _proto2 = B.prototype_proto2.showDetail = function showDetail() {_A.prototype.showDetail.call(this)console.log('this.b = ' + this.b)}return B
})(A)var C = /*#__PURE__*/ (function (_B) {_inheritsLoose(C, _B)function C() {var _this2_this2 = _B.call(this) || this_this2.a = 'ca'_this2.b = 'cb'return _this2}var _proto3 = C.prototype_proto3.showDetail = function showDetail() {_B.prototype.showDetail.call(this)}return C
})(B)var a = new A()
var b = new B()
var c = new C()
console.log('invoke a.show')
a.showDetail()
console.log('invoke b.show')
b.showDetail()
console.log('invoke c.show')
c.showDetail()
結語
本篇介紹了原型鏈,也是 JS 面對對象編程的基礎。雖然與 Java、C++ 等基於類型的繼承機制不同,但是 JavaScript 的原型鏈也有其自己的好處。完全搞清楚原型鏈是身為一個 JavaScript 工程師必備的基礎技能之一,唯有暸解語言最基礎的能力,才能寫出更穩定而健全的代碼。
JS基礎:Prototype Chain 原型鏈相关推荐
- JS基礎:void冷知識
JS 基礎:void 冷知識 文章目錄 JS 基礎:void 冷知識 簡介 參考 正文 語法 Usage 作用 Application `` IIFE(Immediately Invoked Func ...
- JS基礎:Hoisting 變量提升、TDZ 暫時性死區(Temporal Dead Zone)
JS 基礎:Hoisting 變量提升.TDZ 暫時性死區(Temporal Dead Zone) 文章目錄 JS 基礎:Hoisting 變量提升.TDZ 暫時性死區(Temporal Dead Z ...
- JS基礎:Closure 閉包
JS 基礎:Closure 閉包 文章目錄 JS 基礎:Closure 閉包 簡介 參考 正文 IIFE 立即執行函數(Immediately Invoked Functions Expression ...
- JS中关于构造函数、原型链、prototype、constructor、instanceof、__proto__属性
在Javascript不存在类(Class)的概念,javascript中不是基于类的,而是通过构造函数(constructor)和原型链(prototype chains)实现的.但是在ES6中引入 ...
- [js高手之路]原型对象(prototype)与原型链相关属性与方法详解
一,instanceof: instanceof检测左侧的__proto__原型链上,是否存在右侧的prototype原型. 我在之前的两篇文章 [js高手之路]构造函数的基本特性与优缺点 [js高手 ...
- 作用域链(scope chain)和原型链(prototype chain)
locate the object in the scope chain, then proceed up the object's prototype chain until the propert ...
- JavaScript的prototype(原型)是什么?
JS在设计之初 根本没有类的概念(现在也没有)为了实现实例对象的属性和方法的共享,就给function对象设计流一个prototype的概念,prototype是函数对象上面预设的对象属性. 简单的说 ...
- JS中的prototype、__proto__与constructor
作为一名前端工程师,必须搞懂JS中的prototype.__proto__与constructor属性,相信很多初学者对这些属性存在许多困惑,容易把它们混淆,本文旨在帮助大家理清它们之间的关系并彻底搞 ...
- JS中的prototype
JS中的prototype 2011-06-03 14:40 by 轩脉刃, 17040 阅读, 16 评论, 收藏, 编辑 JS中的phototype是JS中比较难理解的一个部分 本文基于下面几个知 ...
最新文章
- js如何判断当前文本的输入状态——中文输入法的那些坑
- [转]在WPF中自定义控件 UserControl
- 连线IBM大数据案例 让大数据接地气
- java ArrayList的实现
- Spark性能调优-RDD算子调优篇
- pyqt5在mac os 安装
- 阿里巴巴《rocketmq开发指南》_如何看待阿里巴巴孤尽觉得Java是世界上最好的语言?...
- 在Linux中,用.swp文件恢复未保存的文件
- adb指令禁用软件_免Root使用ADB命令_停用手机系统应用
- 4款简单实用的的服务器文件管理工具推荐
- 针式打印机打印显示传真服务器,针式打印机三联纸怎么设置 点击上面的工具栏上的打印服务...
- html 中thead标签,HTML thead 标签
- 融资1个亿,5000万给魔窗,凭啥?
- BDrate、BDBR、BDPSNR的计算原理和程序
- 2020-10-05 Python编程从入门到实践 第16章 下载数据 动手试一试 16-2 比较锡特卡和死亡谷的气温 习题练习
- android adb 模拟长按,adb 模拟长按电源键
- 数学图形(2.2)N叶结
- C. The Intriguing Obsession(组合数学)
- 台积电:3纳米芯片工厂地址首选台湾 美国次之
- 忆享聚焦|全球云计算市场份额、数字虚拟人、“元宇宙”实体店……近期行业热点速览